近日写一软件,遇到了高DPI下界面错乱的问题,在网上搜索了好几天,都没有满意的解决方法。也下载了一些坛友的解决方案示例,其基本思路是按比例将高DPI下控件的位置及大小恢复为默认DPI下的位置及大小,经实验,这种方法对简单界面是有效的,当界面比较复杂,控件比较多时,仍会错乱。
反复对比计算不同DPI下的控件大小及位置,发现实在是摸不透WINDOWS 对高DPI下的控件是如何调整其位置及大小的,完全没有固定的比例,所以坛友的解决方案只能将部件控件的位置予以恢复,大小比原来小了许多,另一些则位置也不正确。
在其它论坛及博客等也查看了些类似的文章,始终是无法解决。感觉有个网友说的很正确:说能完全解决高DPI界面错乱问题的都是牛鬼蛇神!
问题总得解决,思路还是让高DPI下控件恢复到默认DPI时位置及大小,即然无法按比例调整,何不我就记录下控件原有位置!
说干就干。第一次,在窗口初始化完成后,我调用一个函数,枚举所有子窗口,记录下其位置,生成一个表格,然后保存到文件中,之后再把这个文件加入软件。以后每次启动软件,就按这个表格调整窗口所有控件的大小及位置。界面终于不再错乱了。
至于字体,按比例调整是没有问题的,所以字体信息无需记录。
当然这个解决方案不是完美的,因为控件大小及字体一直是默认DPI下的大小,所以在高DPI下显得与整个桌面不协调,就是比桌面上其它软件的字体要小。这个要解决也行,就是把所有控件按自定的比例缩放。不过我嫌麻烦,没去做。
其次就是,由系统设置的一些窗口没有调整,比如标题框、弹出的对话框、右键菜单等,所以在你的软件上会有些字大,有些字小。
但至少不错乱了!
附枚举窗口及调整字体的代码:
#ifdef DEBUG_WINDOW_LIST
//控件列表
typedef struct structWindowItem
{
UINT CtrlId;
CHAR szClassName[STRING_SPACE];
RECT Location;
}WNDITEM;
typedef struct structWindowList
{
HWND ParentHwnd;
UINT Count;
WNDITEM Item[WINDOWS_MAX];
}WNDLIST;
WNDLIST strWindowList;
#define STRING_SPACE 256
/*****************************************************************************/// 枚举子窗口
/*****************************************************************************/
int ChildWindowList(HWND hwnd)
{
strWindowList.ParentHwnd = hwnd;
strWindowList.Count = 0;
memset(strWindowList.Item, 0, sizeof(strWindowList.Item));::EnumChildWindows(hwnd, ChildWindowProcess, NULL);
//将子窗口参数保存到文件
#if 0CStdioFile cFileList;
if(cFileList.Open(_T("window.list"), CFile::modeCreate|CFile::modeWrite|CFile::shareDenyNone|CFile::typeText))
{
int nLength;
char szBuffer[STRING_SPACE];
char szClassName[STRING_SPACE];
cFileList.WriteString(_T("const WNDITEM WindowsList[] = \n{\n"));
for(UINT nIndex = 0; nIndex < strWindowList.Count; nIndex++)
{
memset(szBuffer, 0, sizeof(szBuffer));
memset(szClassName, 0x20, sizeof(szBuffer));
nLength = strlen(strWindowList.Item[nIndex].szClassName);
szClassName[0] = _T('\"');
memcpy(szClassName + 1, strWindowList.Item[nIndex].szClassName, nLength);
szClassName[nLength + 1] = _T('\"');
szClassName[nLength + 2] = _T(',');
szClassName[15] = 0;
sprintf_s(szBuffer, _T(" {%4d, %s {%4d, %4d, %4d, %4d}},\n"),
strWindowList.Item[nIndex].CtrlId,
szClassName,
strWindowList.Item[nIndex].Location.left,
strWindowList.Item[nIndex].Location.top,
strWindowList.Item[nIndex].Location.right,
strWindowList.Item[nIndex].Location.bottom);
cFileList.WriteString(szBuffer);
}
cFileList.WriteString(_T("};\n"));
cFileList.Close();
}
#endif
return strWindowList.Count;
}
/*****************************************************************************/
// 枚举子窗口调用
/*****************************************************************************/
BOOL CALLBACK ChildWindowProcess(HWND hwnd, LPARAM lParam)
{
int nIndex;
if((hwnd != NULL) && (strWindowList.Count < WINDOWS_MAX))
{
nIndex = strWindowList.Count;
//ID
strWindowList.Item[nIndex].CtrlId = ::GetDlgCtrlID(hwnd);
//类名
::GetClassName(hwnd, strWindowList.Item[nIndex].szClassName, STRING_SPACE);
//位置
::GetWindowRect(hwnd, &strWindowList.Item[nIndex].Location);
//转换为窗口内坐标
CWnd *pWnd = CWnd::FromHandle(strWindowList.ParentHwnd);
pWnd->ScreenToClient(&strWindowList.Item[nIndex].Location);
strWindowList.Count++;
}
return TRUE;
}
#endif
//调整字体
#define DEFAULT_DPI 96.0
BOOL ChildWindowFontRestore(HWND hChildWnd )
{
LOGFONT LgFont;
int DpiY;
HDC hDC = ::GetDC(NULL);
if(hDC != NULL)
{
DpiY= GetDeviceCaps(hDC, LOGPIXELSY);
::ReleaseDC(NULL, hDC);
}
double dbScale = (double)DEFAULT_DPI / DpiY;
//获取当前字体
HFONT hFont = (HFONT)::SendMessage(hChildWnd, WM_GETFONT, NULL, NULL);
if(hFont != NULL){
//获取字体信息
::GetObject(hFont, sizeof(LOGFONT), &LgFont);//按比例修改大小
LgFont.lfHeight = LONG( (double)LgFont.lfHeight * dbScale + 0.5f);
//生成物新的字体
hFont = ::CreateFontIndirect(&LgFont);
if(hFont != NULL)
{
//重设字体
SendMessage(hChildWnd, WM_SETFONT, (LPARAM)hFont, TRUE);
}
}
return TRUE;
}
HFONT的保存及释放过程我没有写出来,实际应用中建立的字体句柄要保存,在程序结束时释放。
还有就是,若程序是在WIN8以上系统下运行,这样调的结果可能反倒导致界面变乱,貌似WIN8以上系统已解决了这个问题,所以在软件里应加上系统检测,在WIN8以上就交由系统去处理。