MFC 界面自适应不同分辨率

MFC 对话框程序实现控件和字体随分辨率自动缩放的完整方案,附 DPI Awareness 配置说明

背景

开发 MFC 对话框程序时,在开发机上布局好的界面,换一台分辨率不同的电脑就会出现控件错位、字体过大或过小的问题。原因是对话框控件的位置和尺寸在设计时是固定的像素值,不会自动跟随屏幕分辨率缩放。

DPI Awareness

在解决自适应前,先理解 DPI 感知(DPI Awareness)。Windows 默认会对未声明 DPI 感知的程序做 bitmap 拉伸,导致界面模糊。建议在程序入口显式声明:

// 在 WinMain 或 InitInstance 的最开始调用
SetProcessDPIAware(); // 需要 #include <windows.h>

或者在 manifest 中声明(更推荐的方式):

<application xmlns="urn:schemas-microsoft-com:asm.v3">
  <windowsSettings>
    <dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">true</dpiAware>
  </windowsSettings>
</application>

声明后 Windows 不再拉伸,但你需要自己处理缩放逻辑。

实现方案

核心思路:记录初始尺寸,在窗口大小变化时按比例缩放所有控件

1. 在头文件中添加成员变量

class CMyDlg : public CDialogEx {
    // ...
private:
    POINT m_oldSize;   // 记录对话框初始客户区尺寸
    CFont m_MainUIFont; // 字体自适应用
    void ReSize();     // 缩放函数
};

2. OnInitDialog 中记录初始尺寸并居中

BOOL CMyDlg::OnInitDialog() {
    CDialogEx::OnInitDialog();

    // 记录初始客户区大小
    CRect rect;
    GetClientRect(&rect);
    m_oldSize.x = rect.Width();
    m_oldSize.y = rect.Height();

    // 获取屏幕可用区域(不含任务栏)
    int screenW = GetSystemMetrics(SM_CXFULLSCREEN);
    int screenH = GetSystemMetrics(SM_CYFULLSCREEN);

    // 居中显示
    CRect dlgRect;
    GetWindowRect(&dlgRect);
    int x = (screenW - dlgRect.Width()) / 2;
    int y = (screenH - dlgRect.Height()) / 2;
    MoveWindow(x, y, dlgRect.Width(), dlgRect.Height());

    return TRUE;
}

3. 实现 ReSize() 函数

void CMyDlg::ReSize() {
    CRect clientRect;
    GetClientRect(&clientRect);

    // 计算新旧尺寸比例
    float scaleX = (float)clientRect.Width()  / m_oldSize.x;
    float scaleY = (float)clientRect.Height() / m_oldSize.y;

    // 遍历所有子控件
    HWND hChild = ::GetWindow(m_hWnd, GW_CHILD);
    while (hChild) {
        int ctrlId = ::GetDlgCtrlID(hChild);
        CRect ctrlRect;
        GetDlgItem(ctrlId)->GetWindowRect(&ctrlRect);
        ScreenToClient(&ctrlRect); // 转为客户区坐标

        // 按比例计算新位置和尺寸
        CRect newRect(
            (long)(ctrlRect.left   * scaleX),
            (long)(ctrlRect.top    * scaleY),
            (long)(ctrlRect.right  * scaleX),
            (long)(ctrlRect.bottom * scaleY)
        );
        GetDlgItem(ctrlId)->MoveWindow(newRect, TRUE);

        // 字体自适应(Static 和 Button 控件)
        char className[MAX_PATH] = {0};
        GetClassNameA(hChild, className, MAX_PATH);
        if (strcmp(className, "Static") == 0 || strcmp(className, "Button") == 0) {
            CFont* pFont = GetDlgItem(ctrlId)->GetFont();
            LOGFONT lf;
            pFont->GetLogFont(&lf);

            int newHeight = newRect.Height() * 4 / 5;
            lf.lfHeight = newHeight;

            CString text;
            GetDlgItem(ctrlId)->GetWindowText(text);
            int textLen = text.GetLength();
            // 防止字体过宽导致文字超出控件
            lf.lfWidth = (textLen > 0 && (textLen * 9) > newRect.Width())
                ? newRect.Width() / textLen
                : 9;

            m_MainUIFont.DeleteObject();
            m_MainUIFont.CreateFontIndirect(&lf);
            GetDlgItem(ctrlId)->SetFont(&m_MainUIFont);
            m_MainUIFont.Detach(); // 解除关联,防止 DeleteObject 影响控件
        }

        hChild = ::GetWindow(hChild, GW_HWNDNEXT);
    }

    // 更新记录的尺寸
    m_oldSize.x = clientRect.Width();
    m_oldSize.y = clientRect.Height();
}

4. 在 OnSize 中调用

void CMyDlg::OnSize(UINT nType, int cx, int cy) {
    CDialogEx::OnSize(nType, cx, cy);

    // 避免初始化阶段(m_oldSize 未赋值)触发
    if (m_oldSize.x > 0 && m_oldSize.y > 0) {
        ReSize();
    }
}

常见坑

问题原因解决
首次触发 OnSize 时崩溃m_oldSize 未初始化,除以 0在 OnInitDialog 之后才允许 ReSize
字体越来越大每次 ReSize 基于上一次缩放后的字体再缩放重新从控件原始 LOGFONT 取,或单独记录初始字体大小
Edit 控件字体不更新GetClassNameA 返回 “Edit”,未在判断里按需加入 Edit 的字体处理
控件位置漂移ScreenToClient 在窗口移动后坐标不准保证每次都重新 GetWindowRect + ScreenToClient

延伸

如果项目对 DPI 支持要求更高,可以考虑用 Windows 10 引入的 Per-Monitor DPI Awareness v2(DPI_AWARENESS_CONTEXT_PER_MONITOR_AWARE_V2),配合 WM_DPICHANGED 消息重新布局。不过 MFC 对此支持有限,现代项目更建议直接换 Qt 或 WinUI 3。