MFC 界面自适应不同分辨率
随着高分辨率显示器(HiDPI)的普及,Windows 桌面应用的 DPI 适配成为必须解决的问题。本文从历史背景讲起,详细介绍在 MFC 中实现 DPI 自适应的完整方案。
DPI 缩放历史
早期(Vista 之前)
Windows 使用固定的 96 DPI 设计基准。所有 UI 元素按像素硬编码,分辨率高了但物理尺寸小的屏幕上界面会变得很小。
Vista/7 的系统 DPI 缩放
Windows 引入了系统级 DPI 设置,系统会对整个程序进行位图拉伸(DPI 虚拟化),让老程序在高 DPI 下仍可用,但图像会模糊。
Windows 8.1:Per-Monitor DPI
引入 Per-Monitor DPI Awareness,允许应用程序在不同 DPI 的显示器上以不同比例渲染,但 API 支持有限。
Windows 10 1607:Per-Monitor V2
最完善的 DPI 支持:
- 窗口移到不同 DPI 显示器时自动通知
- 非客户区(标题栏、边框)自动缩放
- 对话框支持 DPI 变更
现状(Windows 11)
系统默认推荐使用 PerMonitorV2,老程序通过兼容性层处理。
DPI 感知模式
Windows 定义了以下 DPI 感知模式:
| 模式 | 说明 | 清晰度 |
|---|---|---|
| 不感知(默认) | 系统拉伸,模糊 | 差 |
| System Aware | 以主显示器 DPI 渲染 | 中 |
| Per-Monitor | 监听 DPI 变化,手动处理 | 好 |
| Per-Monitor V2 | Windows 处理更多,手动干预更少 | 最好 |
Manifest 配置
在 app.manifest(Visual Studio 项目属性 → 清单工具)中配置:
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
<assemblyIdentity
version="1.0.0.0"
processorArchitecture="*"
name="MyMFCApp"
type="win32"/>
<application xmlns="urn:schemas-microsoft-com:asm.v3">
<windowsSettings>
<!-- Windows 10 1607+ 推荐 -->
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
PerMonitorV2, PerMonitor
</dpiAwareness>
<!-- 兼容旧版 Windows 8.1 的设置 -->
<dpiAware xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
True/PM
</dpiAware>
</windowsSettings>
</application>
</assembly>
注意:PerMonitorV2, PerMonitor 中间的逗号表示回退,Windows 10 1607+ 使用 V2,旧版回退到 V1。
GetDpiForWindow API
核心 API(Windows 10 1607+):
// 获取当前窗口的 DPI
UINT dpi = GetDpiForWindow(m_hWnd);
// 基准 DPI(96 = 100% 缩放)
const UINT BASE_DPI = 96;
// 计算缩放比例
float scale = (float)dpi / BASE_DPI;
// scale = 1.0 → 100%(96 DPI)
// scale = 1.25 → 125%(120 DPI)
// scale = 1.5 → 150%(144 DPI)
// scale = 2.0 → 200%(192 DPI,常见 HiDPI)
旧版 Windows 的兼容方案:
UINT GetWindowDPI(HWND hwnd) {
// 尝试 Windows 10 1607+ API
typedef UINT(WINAPI* PFN_GetDpiForWindow)(HWND);
static PFN_GetDpiForWindow s_pfnGetDpiForWindow = nullptr;
static bool s_checked = false;
if (!s_checked) {
HMODULE hUser32 = GetModuleHandle(L"user32.dll");
if (hUser32) {
s_pfnGetDpiForWindow = (PFN_GetDpiForWindow)
GetProcAddress(hUser32, "GetDpiForWindow");
}
s_checked = true;
}
if (s_pfnGetDpiForWindow) {
return s_pfnGetDpiForWindow(hwnd);
}
// 回退:获取系统 DPI
HDC hdc = GetDC(hwnd);
UINT dpi = GetDeviceCaps(hdc, LOGPIXELSX);
ReleaseDC(hwnd, hdc);
return dpi;
}
字体缩放
字体大小需要根据 DPI 动态计算:
// 辅助函数:将逻辑尺寸转换为当前 DPI 下的像素
int ScaleForDpi(int value, UINT dpi) {
return MulDiv(value, dpi, 96);
}
// 创建 DPI 感知的字体
CFont* CreateDpiAwareFont(HWND hwnd, int ptSize = 9,
const wchar_t* faceName = L"Microsoft YaHei UI") {
UINT dpi = GetDpiForWindow(hwnd);
LOGFONT lf = {};
lf.lfHeight = -MulDiv(ptSize, dpi, 72); // pt 转逻辑单位
lf.lfWeight = FW_NORMAL;
lf.lfCharSet = DEFAULT_CHARSET;
wcscpy_s(lf.lfFaceName, faceName);
CFont* pFont = new CFont();
pFont->CreateFontIndirect(&lf);
return pFont;
}
// 在 WM_DPICHANGED 时更新字体
void CMyDialog::OnDpiChanged(UINT nDpiX, UINT nDpiY, LPRECT pNewRect) {
// 重新创建字体
if (m_pFont) {
m_pFont->DeleteObject();
delete m_pFont;
}
m_pFont = CreateDpiAwareFont(m_hWnd);
// 应用到所有控件
EnumChildWindows(m_hWnd, [](HWND hwndChild, LPARAM lParam) -> BOOL {
CFont* pFont = (CFont*)lParam;
::SendMessage(hwndChild, WM_SETFONT, (WPARAM)pFont->m_hObject, TRUE);
return TRUE;
}, (LPARAM)m_pFont);
// 移动窗口到建议位置
if (pNewRect) {
SetWindowPos(nullptr,
pNewRect->left, pNewRect->top,
pNewRect->right - pNewRect->left,
pNewRect->bottom - pNewRect->top,
SWP_NOZORDER | SWP_NOACTIVATE
);
}
}
控件和布局缩放
MFC 对话框的控件位置使用对话框单位(DLU),通常会随系统字体自动缩放。但如果用像素硬编码,就需要手动换算:
// 按 DPI 缩放控件位置和大小
void CMyDialog::ScaleControl(CWnd* pCtrl, UINT dpiOld, UINT dpiNew) {
CRect rect;
pCtrl->GetWindowRect(&rect);
ScreenToClient(&rect);
// 缩放
rect.left = MulDiv(rect.left, dpiNew, dpiOld);
rect.top = MulDiv(rect.top, dpiNew, dpiOld);
rect.right = MulDiv(rect.right, dpiNew, dpiOld);
rect.bottom = MulDiv(rect.bottom, dpiNew, dpiOld);
pCtrl->SetWindowPos(nullptr,
rect.left, rect.top,
rect.Width(), rect.Height(),
SWP_NOZORDER | SWP_NOACTIVATE
);
}
// 在 WM_DPICHANGED 中缩放所有控件
void CMyDialog::ScaleAllControls(UINT dpiOld, UINT dpiNew) {
CWnd* pChild = GetWindow(GW_CHILD);
while (pChild) {
ScaleControl(pChild, dpiOld, dpiNew);
pChild = pChild->GetNextWindow();
}
// 也需要调整对话框自身大小(由 pNewRect 给出)
}
位图缩放
图标和位图需要提供多个分辨率版本,或者动态缩放:
// 方法一:提供多分辨率图标(推荐)
// 在 .ico 文件中包含 16x16, 32x32, 48x48, 256x256 等多个尺寸
// Windows 会自动选择最合适的
// 方法二:动态缩放位图
CBitmap* CreateScaledBitmap(HBITMAP hBitmapSrc, int srcW, int srcH,
int dstW, int dstH) {
CBitmap* pBmp = new CBitmap();
CDC dcMem, dcSrc;
dcMem.CreateCompatibleDC(nullptr);
dcSrc.CreateCompatibleDC(nullptr);
pBmp->CreateCompatibleBitmap(CDC::FromHandle(GetDC(nullptr)), dstW, dstH);
CBitmap* pOldDst = dcMem.SelectObject(pBmp);
CBitmap srcBmp;
srcBmp.Attach(hBitmapSrc);
CBitmap* pOldSrc = dcSrc.SelectObject(&srcBmp);
// 高质量缩放
dcMem.SetStretchBltMode(HALFTONE);
dcMem.StretchBlt(0, 0, dstW, dstH, &dcSrc, 0, 0, srcW, srcH, SRCCOPY);
dcMem.SelectObject(pOldDst);
dcSrc.SelectObject(pOldSrc);
srcBmp.Detach();
return pBmp;
}
处理 WM_DPICHANGED
当窗口移动到不同 DPI 的显示器时,系统发送 WM_DPICHANGED:
// 消息映射
BEGIN_MESSAGE_MAP(CMyDialog, CDialog)
ON_MESSAGE(WM_DPICHANGED, &CMyDialog::OnDpiChanged)
END_MESSAGE_MAP()
LRESULT CMyDialog::OnDpiChanged(WPARAM wParam, LPARAM lParam) {
UINT dpiX = LOWORD(wParam);
UINT dpiY = HIWORD(wParam);
RECT* pRect = reinterpret_cast<RECT*>(lParam); // 建议的新窗口位置/大小
UINT dpiOld = m_currentDpi;
m_currentDpi = dpiX;
// 1. 更新字体
UpdateFonts();
// 2. 缩放控件
ScaleAllControls(dpiOld, dpiX);
// 3. 移到建议位置(保持物理大小不变)
SetWindowPos(nullptr,
pRect->left, pRect->top,
pRect->right - pRect->left,
pRect->bottom - pRect->top,
SWP_NOZORDER | SWP_NOACTIVATE
);
// 4. 重绘
Invalidate();
return 0;
}
测试方法
-
Windows 设置:设置 → 显示 → 缩放,改为 125%、150%、200%,观察应用外观。
-
多显示器测试:连接两台不同 DPI 的显示器,把窗口在两台显示器间拖动,观察是否正确调整。
-
工具辅助:使用
Inspect(Accessibility Insights)或DpiScaling工具测试。 -
手动修改注册表(测试用):
HKCU\Control Panel\Desktop\LogPixels改为 120(125%)或 144(150%)后注销重登录。
遵循以上方案,你的 MFC 应用就能在各种 DPI 设置和显示器组合下显示清晰、布局正确。