简介面向 Qt/C 桌面开发者的无边框窗口实战工程聚焦 Windows 平台下将 Qt 框架与 WINAPI、DWM 接口结合解决直接使用 FramelessWindowHint 后丢失系统拖拽、缩放和阴影等问题。工程覆盖标题栏图标、边缘缩放、窗口拖拽、Aero 阴影、最大最小化及模糊背景等效果涉及 WM_NCHITTEST、WM_NCCALCSIZE、WM_GETMINMAXINFO 等消息处理也包含 DwmExtendFrameIntoClientArea、DwmSetWindowAttribute 等调用并需关注系统合成开关检测与非客户区绘制适合桌面客户端界面美化、系统集成方向的中高级开发者参考复用。压缩包共 48 个文件、约 73KB以 13 个 h 与 12 个 cpp 承载窗口逻辑、浏览器界面及 Win 迁移封装另有 2 个 ui、9 个 png 及 ico、qrc、pri、pro、css、md 等兼顾源码、图标资源、工程配置与说明文档。已有 1267 人学习可作为无边框窗口框架、Aero 风格实现和 WinAPI 混合编程的完整样例便于对照梳理消息流程、排错思路与可复用组件。1. 从 FramelessWindowHint 说起做出 Aero 风格的无边框窗口要补哪些 Win32 消息产品经理给的需求往往只有一句话窗口要像系统自带的那样左上角有图标能拖能拉贴到屏幕边自动分屏还得有阴影和最大最小化动画。可真把Qt::FramelessWindowHint打开标题栏一没了剩下的东西也全都没了——Snap 吸附没了最大化动画没了DWM 投影没了连拖到屏幕顶端触发 Aero 分屏预览都没了。这份代码走的是另一条路不去覆盖 Qt 的绘制而是保留 Win32 的窗口样式用nativeEvent接管WM_NCCALCSIZE、WM_NCHITTEST、WM_GETMINMAXINFO这几条关键消息把系统原生的 Snap 吸附、投影、动画原封不动借过来用。它适合已经能用 Qt Widgets 写界面、又想让桌面端观感接近原生的开发者纯新手照着跑也能通但最好先知道HWND是什么。2. 技术选型与工程骨架为什么用 nativeEvent 接管而不是纯 Qt 自绘2.1 三条常见路线的取舍做无边框窗口业内基本就三条路选错一条后面全是补丁。第一条是纯 Qt 路线setWindowFlags(Qt::FramelessWindowHint)标题栏用QWidget自己拼阴影用QGraphicsDropShadowEffect拖拽在mouseMoveEvent里算偏移右下角塞一个QSizeGrip。写起来最快一天能出原型。代价是鼠标移到窗口边缘时光标不会变成缩放箭头因为系统根本不知道那里有条边拖动时窗口跟手性差快速甩动会掉帧最要命的是 Snap 吸附彻底失效用户把窗口拖到屏幕顶部什么都不会发生他的肌肉记忆直接被破坏。第二条是 QML / Quick 全自绘观感可以做到极致但 Aero 分屏预览、最大化动画这类东西属于窗口管理器DWM的活儿你只能模仿个七八成而且要多写一套窗口拖拽状态机。第三条就是这份代码的路子保留WS_CAPTION | WS_THICKFRAME这些原生样式让系统以为这是个标准窗口然后用WM_NCCALCSIZE把非客户区“吃掉”。系统仍然负责投影、吸附、动画、缩放边框的命中判定你只负责在客户区里画自己的标题栏。观感最接近原生坑也集中——集中在几条消息的返回值上。我一般会优先选第三条除非项目要跨 macOS 和 Linux 同时保持一致观感那种情况才退回第一条。2.2 消息流QWidget、QWindow、HWND 三层怎么对上很多人卡在第一步不知道nativeEvent是从哪冒出来的。简单捋一遍。在 Qt Widgets 里每个顶层QWidget在第一次需要原生句柄时创建时、或者调用winId()时会生成一个QWidgetWindow它继承自QWindowQWindow在 Windows 平台上再对应一个真实的HWND。系统的消息先进到 Qt 的平台插件插件把MSG打包后派发给QWindowQWindow再回调到QWidget::nativeEvent。所以这个虚函数是窗口消息的总入口签名长这样// Qt 5long *resultQt 6qintptr *result移植时这里最容易编译不过 bool nativeEvent(const QByteArray eventType, void *message, qintptr *result) override;eventType常见两个值windows_generic_MSG表示发给窗口本身的消息windows_dispatcher_MSG表示线程级消息比如某些 DPI 变化通知。绝大多数窗口外观相关的消息走的是前者所以入口第一件事就是过滤。返回值语义要记牢返回true表示这条消息被你处理掉了Qt 和DefWindowProc都不会再管返回false表示继续走默认流程。很多人在这里翻车是因为只return true却没给*result赋值系统拿到一个未初始化的值行为就变得很玄学。2.3 工程骨架与编译项目结构我建议一开始就拆清楚别全塞进main.cppAeroFrame/ ├─ CMakeLists.txt ├─ src/ │ ├─ main.cpp │ ├─ aerowindow.h # 无边框主窗口nativeEvent 总入口 │ ├─ aerowindow.cpp │ ├─ titlebar.h # 自绘标题栏图标、标题、最小化/最大化/关闭 │ ├─ titlebar.cpp │ └─ win32helper.h # DWM 属性、DPI、显示器信息的小封装 └─ res/ └─ app.ico构建脚本只需要多链两个系统库dwmapi用来设置投影和圆角user32里的MonitorFromWindow、GetMonitorInfo用来处理最大化区域cmake_minimum_required(VERSION 3.16) project(AeroFrame LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_AUTOMOC ON) find_package(QT NAMES Qt6 Qt5 REQUIRED COMPONENTS Widgets) find_package(Qt${QT_VERSION_MAJOR} REQUIRED COMPONENTS Widgets) add_executable(AeroFrame src/main.cpp src/aerowindow.cpp src/aerowindow.h src/titlebar.cpp src/titlebar.h ) target_link_libraries(AeroFrame PRIVATE Qt${QT_VERSION_MAJOR}::Widgets dwmapi # DwmExtendFrameIntoClientArea / DwmSetWindowAttribute user32 # 显示器信息、系统度量 )CMAKE_AUTOMOC一定要开Q_OBJECT宏没有 moc 处理会在链接期报一堆undefined reference to vtable这是新手最常见的第一个失败点。编译用 MSVC 或 MinGW 都行但要在 Windows 上跑因为整篇代码依赖 Win32 消息Linux 上这些消息编号根本不会出现。3. 无边框的骨架WM_NCCALCSIZE 与 WM_GETMINMAXINFO 的一次性改造3.1 构造函数里必须补回来的窗口样式Qt::FramelessWindowHint在 Windows 平台上的实际动作是把WS_CAPTION和WS_THICKFRAME从窗口样式里摘掉。这两个样式恰恰是 DWM 投影、最大化动画、以及 Snap 吸附的前置条件——没有WS_THICKFRAME系统压根不认为这个窗口可缩放拖动到屏幕边缘自然不会有分屏反应。所以构造函数里要做的是“先把样式加回去再自己把非客户区吃掉”AeroWindow::AeroWindow(QWidget *parent) : QWidget(parent) { setWindowFlags(Qt::Window | Qt::FramelessWindowHint | Qt::WindowMinMaxButtonsHint | Qt::WindowSystemMenuHint); setAttribute(Qt::WA_NativeWindow); // 提前拿到 HWND避免后面反复触发重建 HWND hwnd reinterpret_castHWND(winId()); LONG_PTR style GetWindowLongPtr(hwnd, GWL_STYLE); // 关键把被 FramelessWindowHint 摘掉的样式补回来 // WS_THICKFRAME 负责缩放边框、DWM 投影和 Snap 吸附 // WS_CAPTION 负责最大化/最小化动画、任务栏缩略图行为 style | WS_CAPTION | WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX; SetWindowLongPtr(hwnd, GWL_STYLE, style); // 改完样式必须让窗口重新计算一次非客户区否则新样式要等下一次尺寸变化才生效 SetWindowPos(hwnd, nullptr, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE | SWP_NOZORDER | SWP_NOACTIVATE | SWP_FRAMECHANGED); }SWP_FRAMECHANGED这个标志容易被漏掉。漏了之后你会看到窗口顶部莫名其妙多出一条白边或者投影时有时无因为它还在用旧的窗口框架缓存。SWP_NOMOVE | SWP_NOSIZE保证这次调用只刷新框架、不动位置和大小。提示winId()会强制创建原生窗口如果你的窗口初始化阶段有大量子控件布局把它放到构造末尾或者showEvent里会更稳。3.2 WM_NCCALCSIZE把标题栏吃掉的正确姿势WM_NCCALCSIZE是系统在问“这个窗口的客户区应该占多大”。默认实现会把标题栏、边框的高度算进去然后返回一个缩小的客户区矩形。我们想要客户区等于整个窗口那就得接管这条消息。它有两个分支靠wParam区分wParam TRUE时lParam是NCCALCSIZE_PARAMS*三个矩形分别代表建议的新窗口矩形、旧窗口矩形、旧客户区矩形wParam FALSE时lParam只是一个RECT*这是窗口创建或样式变化的简化路径。case WM_NCCALCSIZE: { if (msg-wParam TRUE) { // 什么都不改客户区 窗口矩形标题栏被吃掉 *result 0; return true; // 吞掉消息DefWindowProc 不会再补标准标题栏 } break; // wParam FALSE 的场景交给 Qt 默认处理 }逻辑就三行但含义要讲透*result 0表示客户区计算成功return true表示不要再让DefWindowProc去调整。如果只赋值不返回true系统还会在你这版基础上再减一次边框结果是内容被裁掉一圈。这里有个必须提前知道的前提WS_THICKFRAME还在的前提下窗口最大化时系统会把窗口矩形按边框外扩因为默认逻辑认为客户区比窗口小一圈。现在客户区等于窗口外扩量就变成了溢出——内容会顶到屏幕外面并盖住任务栏。这个问题在 3.3 用WM_GETMINMAXINFO解决两处只改一处就够。3.3 WM_GETMINMAXINFO最大化不能盖住任务栏WM_GETMINMAXINFO是系统在问“这个窗口最大能多大、最小能多小”。默认答案是基于主显示器的多屏环境下经常出错。我们在这里做两件事把最大化目标矩形锁定到当前所在显示器的工作区并把WS_THICKFRAME造成的外扩量补偿回去。case WM_GETMINMAXINFO: { auto *mmi reinterpret_castMINMAXINFO *(msg-lParam); // 关键用窗口当前所在显示器而不是默认主显示器 HMONITOR hmon MonitorFromWindow(msg-hwnd, MONITOR_DEFAULTTONEAREST); MONITORINFO mi{}; mi.cbSize sizeof(MONITORINFO); if (GetMonitorInfo(hmon, mi)) { // 最大化时窗口矩形会被按边框外扩这里先算出外扩量 const int fx GetSystemMetrics(SM_CXSIZEFRAME) GetSystemMetrics(SM_CXPADDEDBORDER); const int fy GetSystemMetrics(SM_CYSIZEFRAME) GetSystemMetrics(SM_CXPADDEDBORDER); // rcWork 是排除任务栏后的工作区rcMonitor 是整个屏幕 mmi-ptMaxPosition.x mi.rcWork.left - mi.rcMonitor.left - fx; mmi-ptMaxPosition.y mi.rcWork.top - mi.rcMonitor.top - fy; mmi-ptMaxSize.x (mi.rcWork.right - mi.rcWork.left) fx * 2; mmi-ptMaxSize.y (mi.rcWork.bottom - mi.rcWork.top) fy * 2; } // 最小尺寸跟着 Qt 的 minimumWidth/minimumHeight 走避免拖到 0 宽 mmi-ptMinTrackSize.x qMaxLONG(mmi-ptMinTrackSize.x, minimumWidth()); mmi-ptMinTrackSize.y qMaxLONG(mmi-ptMinTrackSize.y, minimumHeight()); *result 0; return true; }参数含义拆开看ptMaxPosition是最大化后窗口左上角相对于屏幕左上角的偏移用rcWork.left - rcMonitor.left就得到任务栏高度形成的偏移ptMaxSize是最大化后的窗口尺寸。SM_CXPADDEDBORDER是 Win8 之后给边框额外加的一圈不可见内边距不算进去的话最大化会差 1~2 像素表现为边缘露出桌面。3.4 WM_NCACTIVATE 与 DWM 投影让阴影和圆角回来窗口失去焦点时系统默认会重绘一遍非客户区把标题栏刷成灰色。我们既然已经把非客户区吃掉了这一次重绘就变成了纯粹的闪烁。所以直接告诉系统“激活状态没变化”case WM_NCACTIVATE: *result TRUE; // 不去重绘非客户区避免切换焦点时闪一下 return true;投影和圆角交给 DWM。DwmExtendFrameIntoClientArea的作用是把系统边框“延伸”进客户区哪怕只延伸 1 像素DWM 也会给这个窗口配上标准的窗口阴影void AeroWindow::applyDwmEffect() { HWND hwnd reinterpret_castHWND(winId()); // 四周各延伸 1pxDWM 会顺带画出标准投影 MARGINS margins{1, 1, 1, 1}; DwmExtendFrameIntoClientArea(hwnd, margins); // 告诉 DWM 非客户区仍由系统渲染拖动时不容易出现白底残影 DWMNCRENDERINGPOLICY policy DWMNCRP_ENABLED; DwmSetWindowAttribute(hwnd, DWMWA_NCRENDERING_POLICY, policy, sizeof(policy)); // Win11 的圆角。常量值 33 在旧 SDK 里没有定义手写数值更省事 // 1 不圆角2 系统默认圆角3 小圆角 const DWORD cornerPref 2; DwmSetWindowAttribute(hwnd, 33 /* DWMWA_WINDOW_CORNER_PREFERENCE */, cornerPref, sizeof(cornerPref)); }MARGINS填{1,1,1,1}而不是全 0是有原因的全 0 表示关闭延伸投影也就没了。填 1 像素的代价是窗口四周会出现一条极淡的系统边框如果你要绝对纯净的圆角可以只在底部填 1{0,0,0,1}投影照样会有但高度会短一点。注意一旦打开了Qt::WA_TranslucentBackground窗口会变成分层窗口DWM 投影会直接消失。这两个方案二选一不能叠加。4. 命中测试与交互用 WM_NCHITTEST 手写八向缩放、拖拽和 Snap 吸附4.1 WM_NCHITTEST 的坐标系与调用频率WM_NCHITTEST是系统在问“这个坐标点上鼠标落在了窗口的哪个部位”。返回值决定了后续行为返回HTLEFT系统就进入左边框拖拽缩放模式返回HTCAPTION系统就进入标题栏拖动模式Snap 吸附自动生效返回HTCLIENT鼠标事件才会正常派发给你的控件。这条消息有个必须记住的特性lParam里的坐标是屏幕坐标物理像素不是客户区坐标也不是 Qt 的逻辑坐标。很多人的缩放判定永远不生效就是拿逻辑坐标去和物理像素的窗口矩形做比较。// lParam 高 16 位是 y低 16 位是 x必须用 GET_X_LPARAM 拆 // 直接强转 short 在负坐标副屏在主屏左侧时会拿到错误值 const QPoint globalPos(GET_X_LPARAM(msg-lParam), GET_Y_LPARAM(msg-lParam)); const QPoint localPos mapFromGlobal(globalPos); // 转成 Qt 逻辑坐标方便和控件几何比较还有一点是性能这条消息在鼠标每次移动时都会触发如果你的判定逻辑里塞了GetWindowRect、遍历所有子控件、甚至查数据库拖动窗口时就会明显掉帧。我一般把窗口矩形和边框宽度缓存在成员变量里只在WM_SIZE和WM_DPICHANGED时更新。4.2 八向缩放边框宽度怎么算才跟手八个方向四条边加四个角的判定顺序很重要先判角再判边。反过来的话鼠标落在左上角时会先命中左边缩放方向就只有一个轴用户会觉得角上“拉不动”。case WM_NCHITTEST: { // 最大化状态下不允许拖拽缩放直接交回默认处理 if (isMaximized() || IsZoomed(msg-hwnd)) break; const QPoint globalPos(GET_X_LPARAM(msg-lParam), GET_Y_LPARAM(msg-lParam)); // 边框宽度用逻辑像素定义乘上 DPI 缩放才是物理像素 // 否则 200% 缩放的屏幕上8px 的判定区只剩 4px鼠标根本点不中 const int b qRound(m_resizeBorder * devicePixelRatioF()); POINT pt{globalPos.x(), globalPos.y()}; ScreenToClient(msg-hwnd, pt); // 转成物理像素的客户区坐标 RECT rc; GetClientRect(msg-hwnd, rc); const bool onLeft pt.x b; const bool onRight pt.x rc.right - b; const bool onTop pt.y b; const bool onBottom pt.y rc.bottom - b; // 先判四个角 if (onTop onLeft) { *result HTTOPLEFT; return true; } if (onTop onRight) { *result HTTOPRIGHT; return true; } if (onBottom onLeft) { *result HTBOTTOMLEFT; return true; } if (onBottom onRight) { *result HTBOTTOMRIGHT; return true; } // 再判四条边 if (onLeft) { *result HTLEFT; return true; } if (onRight) { *result HTRIGHT; return true; } if (onTop) { *result HTTOP; return true; } if (onBottom) { *result HTBOTTOM; return true; } break; // 其他区域交回 Qt让子控件正常收鼠标事件 }HTLEFT、HTTOPRIGHT这些常量就是系统的缩放方向标识返回之后光标形状、拖拽逻辑、松开鼠标后的窗口重排全部由DefWindowProc完成你一行都不用写。这也是这套方案最大的红利——缩放行为天然和系统一致。4.3 标题栏拖拽HTCAPTION 与 startSystemMove 两条路拖拽有两条路各有取舍。第一条是命中测试路线在WM_NCHITTEST里判断鼠标是否落在标题栏区域是就返回HTCAPTION。系统接管后拖动、Snap 吸附、双击最大化、甚至 Win11 的贴靠布局全都免费获得。但有个硬伤一旦返回HTCAPTION这个坐标点上的鼠标事件就不会再派发给 Qt 控件。如果你的标题栏上放了最小化、关闭按钮按钮就永远点不动。解决办法是让标题栏自己做命中判定把按钮区域排除出去// TitleBar 里维护一份可拖动区域按钮所在的矩形不参与 bool TitleBar::isCaptionArea(const QPoint localPos) const { // 遍历标题栏上的交互控件图标、三个按钮 for (QWidget *w : m_interactiveWidgets) { if (w-geometry().contains(localPos)) return false; // 落在按钮上交给控件自己处理 } return rect().contains(localPos); }然后WM_NCHITTEST里把全局坐标转成标题栏的局部坐标再问一次if (m_titleBar !m_titleBar-isCaptionArea(m_titleBar-mapFrom(this, localPos))) { // 落在按钮上什么都不返回让 Qt 正常派发点击 } else if (m_titleBar m_titleBar-geometry().contains(localPos)) { *result HTCAPTION; return true; }第二条是事件路线适合不想维护命中测试的场景在标题栏的mousePressEvent里手动触发系统的移动循环。void TitleBar::mousePressEvent(QMouseEvent *e) { if (e-button() ! Qt::LeftButton) { QWidget::mousePressEvent(e); return; } // 先释放 Qt 自己的鼠标捕获否则系统的移动循环收不到鼠标松开 ReleaseCapture(); SendMessage(reinterpret_castHWND(window()-winId()), WM_NCLBUTTONDOWN, HTCAPTION, 0); }这个写法看起来更短Snap 吸附同样可用但SendMessage会阻塞到用户松开鼠标为止期间 Qt 的事件循环是被占住的。如果你有定时器在跑动画这段时间会冻住。Qt 5.15 之后还有个更干净的替代window()-windowHandle()-startSystemMove()跨平台可用Windows 上同样触发系统的移动循环但不会阻塞事件循环。我现在的项目基本都用这个。4.4 Snap 吸附、双击最大化与 Win11 布局预览Snap 吸附不需要额外代码它就是HTCAPTION的副产品。用户把窗口拖到屏幕顶部系统检测到标题栏拖动自动弹出贴靠预览松手后窗口占半屏——这就是标题里 Aero 风格的核心体验用纯 Qt 自绘是模拟不出来的。双击标题栏最大化同理属于HTCAPTION的默认行为。如果你想自定义比如双击后只放大不铺满可以在标题栏的mouseDoubleClickEvent里拦截但要注意拦截后系统行为就失效了。Win11 还多了一个东西鼠标悬停在最大化按钮上会弹出贴靠布局选择器。这个默认不生效需要在WM_NCHITTEST里把按钮区域返回成HTMAXBUTTON// Win11 才有的常量旧 SDK 里没有手写值即可 #ifndef HTMAXBUTTON #define HTMAXBUTTON 9 #endif if (m_titleBar m_titleBar-maxButtonRect().contains(localPos)) { *result HTMAXBUTTON; return true; }返回之后最大化按钮就不再走 Qt 的点击逻辑了你需要额外处理WM_NCLBUTTONDOWN里wParam HTMAXBUTTON的按下以及WM_NCMOUSEMOVE的离开才能在用户点击时正确最大化。如果项目对 Win11 布局预览没强需求我建议先不做——它带来的状态机复杂度不低容易把点击最大化搞坏。4.5 完整的 nativeEvent 拼装把前面几块拼起来入口长这样bool AeroWindow::nativeEvent(const QByteArray eventType, void *message, qintptr *result) { // 只处理发往窗口本身的消息线程消息交回 Qt if (eventType ! windows_generic_MSG) return QWidget::nativeEvent(eventType, message, result); auto *msg static_castMSG *(message); switch (msg-message) { case WM_NCCALCSIZE: return handleNcCalcSize(msg, result); case WM_NCACTIVATE: *result TRUE; return true; case WM_GETMINMAXINFO: return handleGetMinMaxInfo(msg, result); case WM_NCHITTEST: return handleNcHitTest(msg, result); case WM_DPICHANGED: return handleDpiChanged(msg, result); default: break; } // 没处理的消息一定交回父类否则 Qt 的内部状态会和自己打架 return QWidget::nativeEvent(eventType, message, result); }这里的分发结构建议一开始就按消息类型拆成独立成员函数。我见过把几百行switch全塞在nativeEvent里的版本加一个 Win11 特性就要在中间翻半天改错一个return整个窗口的拖拽就废了。WM_DPICHANGED单独处理的原因窗口被拖到不同缩放比例的显示器时系统会发这条消息并附带一个建议矩形Qt 自己也会处理但我们的边框判定宽度依赖 DPI必须在收到消息后刷新缓存的m_resizeBorder。5. 常见问题排查无边框窗口最容易被忽略的五个坑5.1 布局与最大化相关的坑坑一最大化之后内容溢出屏幕或者整体缩进了 8 像素。现象是窗口最大化后底部内容被任务栏压住看不到或者四周出现一圈奇怪的空白。原因只有一个WS_THICKFRAME的外扩补偿做了两次或者一次都没做。WM_NCCALCSIZE里在IsZoomed时收缩客户区、WM_GETMINMAXINFO里调整ptMaxSize这是两种并行的解法只能选一种。两个都写就是双重补偿内容缩进两圈两个都不写内容就溢出去。排查方法很简单在WM_GETMINMAXINFO里打个日志看ptMaxSize是不是正好等于工作区尺寸如果等于工作区还溢出说明补偿量没加。坑二多显示器环境下最大化跑到主屏去了。现象是窗口放在副屏上点最大化窗口跳回主屏并铺满。原因是用了MonitorFromWindow(hwnd, MONITOR_DEFAULTTOPRIMARY)或者干脆用了GetSystemMetrics(SM_CXSCREEN)取屏幕尺寸。这两个 API 拿到的永远是主显示器。改成MonitorFromWindow(hwnd, MONITOR_DEFAULTTONEAREST)配合GetMonitorInfo并注意ptMaxPosition是相对当前显示器的偏移不是相对虚拟桌面原点。5.2 交互与视觉相关的坑坑三标题栏上的按钮点不动或者点一下窗口就跟着拖走了。现象是点击关闭按钮没反应但拖拽窗口是正常的。原因是WM_NCHITTEST对整个标题栏区域无脑返回了HTCAPTION按钮的点击事件在系统层就被吃掉了根本传不到 Qt。解决思路就是 4.3 里的isCaptionArea先把按钮矩形排除落在按钮上的坐标不返回HTCAPTION让它自然落到HTCLIENT。判断顺序不能反必须先问控件再问标题栏。坑四高 DPI 屏幕上缩放边框“点不中”或者光标变了但拖不动。现象是在 150% 缩放的笔记本上鼠标明明贴着窗口边缘光标却不变成缩放箭头。原因是边框判定用了逻辑像素比如 8而WM_NCHITTEST拿到的是物理像素坐标150% 缩放下 8 逻辑像素只有 12 物理像素的判定区扣掉系统边框本身占的部分就所剩无几。解决是判定时乘devicePixelRatioF()并且用GetSystemMetricsForDpi(SM_CXSIZEFRAME, GetDpiForWindow(hwnd))取当前 DPI 下的边框值而不是全局的GetSystemMetrics——后者在每显示器 DPI 感知的进程里返回的是系统 DPI不等于窗口所在显示器的 DPI。坑五拖动窗口时出现白底残影或者拖动过程中窗口闪黑。现象是拖拽时窗口内容短暂变白松手才恢复。原因通常是打开了Qt::WA_TranslucentBackground又同时调了DwmExtendFrameIntoClientArea。分层窗口会关闭 DWM 的非客户区渲染扩展边框的那 1 像素变成未绘制区域拖动时合成器拿不到内容就只能给白色。解法是二选一要么关掉透明背景用 DWM 原生投影要么保留透明背景自己用QGraphicsDropShadowEffect在窗口内部留出 1 像素透明边距画阴影同时把DwmExtendFrameIntoClientArea的MARGINS全部置 0。注意改动MARGINS之后必须重新调用一次SetWindowPos带上SWP_FRAMECHANGED否则新的框架设置不会立即应用到已存在的窗口上。6. 二次开发把散落的消息处理收进一个可复用基类当第二个、第三个窗口都需要无边框时把nativeEvent里的东西复制粘贴三份是最省事也最痛苦的决定。我的做法是抽一个AeroWidget基类只暴露三个可配置点其余全部封死在基类里配置点类型默认值用途resizeBorderint逻辑像素8缩放热区宽度小窗口可以调到 5captionProvider回调 / 虚函数返回整条标题栏告诉基类哪块区域算标题栏dwmShadowEnabledbooltrue关掉后自动切换成内部阴影方案本文还有配套的精品资源点击获取
阅读完成 · 觉得有帮助?