Qt 6 Windows标题栏定制:扩展内容并保留原生窗口按钮
我正在开发基于Qt 6的应用,希望将窗口内容扩展到标题栏区域,但保留最小化、最大化、关闭等原生窗口按钮(效果参考Windows 10/11)。该需求已在macOS平台实现,但在Windows平台遇到困难,我对Windows原生API并不熟悉。
我尝试在测试应用中使用DwmExtendFrameIntoClientArea方法,按照微软官方指南实现了QAbstractNativeEventFilter子类并安装到应用中,事件能被接收,但窗口仍显示普通标题栏,菜单栏位于其下方,同时运行时产生警告信息:
exiting ctor WM_CREATE, hwnd: 0xca04cc QWindowsContext::windowsProc: No Qt Window found for event 0x1c (WM_ACTIVATEAPP), hwnd=0x0xca04cc. WM_ACTIVATE succeeded, will return true. QWindowsContext::windowsProc: No Qt Window found for event 0x7 (WM_SETFOCUS), hwnd=0x0xca04cc.
相关代码如下:
main.cpp
#include <QApplication> #include "mainWindow.h" #include "winEventFilter.h" int main( int argc, char* argv[] ) { QApplication app( argc, argv ); WinEventFilter eventFilter; app.installNativeEventFilter( &eventFilter ); MainWindow mainWin; mainWin.show(); return app.exec(); }
winEventFilter.h
#pragma once #include <QAbstractNativeEventFilter> class WinEventFilter : public QAbstractNativeEventFilter { public: bool nativeEventFilter( const QByteArray& eventType, void* message, qintptr* result ) override; };
winEventFilter.cpp
#include <dwmapi.h> #include <QDebug> #include "winEventFilter.h" #define LEFTEXTENDWIDTH 8 #define RIGHTEXTENDWIDTH 8 #define BOTTOMEXTENDWIDTH 20 #define TOPEXTENDWIDTH 27 #define RECTWIDTH( R ) ( R.right - R.left ) #define RECTHEIGHT( R ) R.bottom - R.top bool WinEventFilter::nativeEventFilter( const QByteArray& eventType, void* message, qintptr* result ) { auto msg = reinterpret_cast<MSG*>( message ); if( msg == nullptr ) { return false; } BOOL fDwmEnabled = FALSE; HRESULT hr = S_OK; hr = DwmIsCompositionEnabled(&fDwmEnabled); if( !SUCCEEDED( hr ) ) { qDebug() << "composition is not enabled."; return false; } auto hWnd = msg->hwnd; switch( msg->message ) { case WM_CREATE: qDebug() << "WM_CREATE, hwnd:" << msg->hwnd; RECT rcClient; GetWindowRect( hWnd, &rcClient ); // Inform application of the frame change. SetWindowPos( hWnd, NULL, rcClient.left, rcClient.top, RECTWIDTH( rcClient ), RECTHEIGHT( rcClient ), SWP_FRAMECHANGED ); if( result != nullptr ) { *result = 0; } return true; case WM_ACTIVATE: qDebug() << "WM_ACTIVATE"; MARGINS margins; margins.cxLeftWidth = LEFTEXTENDWIDTH; // 8 margins.cxRightWidth = RIGHTEXTENDWIDTH; // 8 margins.cyBottomHeight = BOTTOMEXTENDWIDTH; // 20 margins.cyTopHeight = TOPEXTENDWIDTH; // 27 // only if content window is hWnd? hr = DwmExtendFrameIntoClientArea( hWnd, &margins ); if( !SUCCEEDED( hr ) ) { qDebug() << "extending frame into client area failed"; return false; } qDebug() << "succeeded, will return true."; if( result != nullptr ) { *result = 0; } return true; default: return false; } }
mainWindow.h
#pragma once #include <QMainWindow> class MainWindow : public QMainWindow { public: MainWindow(); private: void createMenus(); void createCentralWidget(); };
mainWindow.cpp
#include <memory> #include <QLabel> #include <QMenuBar> #include <QVBoxLayout> #include "mainWindow.h" MainWindow::MainWindow() { createMenus(); createCentralWidget(); qDebug() << "exiting ctor"; } void MainWindow::createMenus( ) { auto fileMenu = menuBar()->addMenu( tr( "&File" ) ); fileMenu->addAction( tr( "Placeholder" ) ); } void MainWindow::createCentralWidget() { auto widget = std::make_unique<QWidget>(); auto pal = palette(); pal.setColor( QPalette::Window, Qt::darkRed ); pal.setColor( QPalette::WindowText, Qt::white ); widget->setPalette( pal ); widget->setAutoFillBackground( true ); auto layout = new QVBoxLayout( widget.get() ); layout->addWidget( new QLabel( "Hello" ) ); setCentralWidget( widget.release() ); }
原生事件过滤器的文档并不完善,我不确定当前方案是否正确,也不确定这是否是将内容移至标题栏的有效方法。我还看到基于WinUI 2/WinUI 3/Windows App SDK的最新解决方案,但不知道如何在Qt中实现。
请问当前使用的方法能否实现预期效果?如果不能,是否可以使用最新的WinUI API来实现?我不介意仅支持Windows 11或较新版本的Windows 10,理想情况下不想使用Qt::FramelessWindowHint标志,以保留原生按钮行为(包括Windows 11的snap layouts)。
当前方法的问题分析
你的方案方向正确,但存在几个关键问题导致未达预期:
- 全局事件过滤器误捕获消息:全局过滤器会处理所有窗口的消息(包括Qt内部辅助窗口),这就是
No Qt Window found警告的来源,应该只针对主窗口的HWND处理。 - 未禁用Qt原生标题栏绘制:即使调用
DwmExtendFrameIntoClientArea,Qt默认仍会绘制自己的标题栏,需要调整窗口标志让Qt放弃标题栏绘制,但保留系统原生按钮。 - 消息处理时机错误:
WM_ACTIVATE不是调用DWM扩展的最佳时机,应在窗口创建完成后执行,且需确保只操作主窗口。 - 未调整Qt布局区域:扩展帧后,需要让Qt的布局系统将内容延伸到标题栏区域,否则菜单栏和内容会被挤到原生标题栏下方。
修正后的实现步骤
1. 调整窗口标志,保留系统按钮但关闭Qt标题栏绘制
在MainWindow构造函数中添加:
MainWindow::MainWindow() { // 关闭Qt的标题栏绘制,但保留系统框架和按钮 setWindowFlags(windowFlags() | Qt::CustomizeWindowHint); setWindowFlags(windowFlags() & ~Qt::WindowTitleHint); createMenus(); createCentralWidget(); qDebug() << "exiting ctor"; }
2. 优化原生事件过滤器,仅处理主窗口消息
修改事件过滤器,让它只针对主窗口的HWND操作:
// winEventFilter.h #pragma once #include <QAbstractNativeEventFilter> #include <QWindow> class WinEventFilter : public QAbstractNativeEventFilter { public: explicit WinEventFilter(QWindow* mainWindow); bool nativeEventFilter(const QByteArray& eventType, void* message, qintptr* result) override; private: WId m_mainHwnd; }; // winEventFilter.cpp #include <dwmapi.h> #include <QDebug> #include <QWindow> #include "winEventFilter.h" #define TOPEXTENDWIDTH 32 // 适配Windows 11标题栏高度 WinEventFilter::WinEventFilter(QWindow* mainWindow) : m_mainHwnd(mainWindow->winId()) { } bool WinEventFilter::nativeEventFilter(const QByteArray& eventType, void* message, qintptr* result) { auto msg = reinterpret_cast<MSG*>(message); if (!msg || msg->hwnd != reinterpret_cast<HWND>(m_mainHwnd)) { return false; // 只处理主窗口消息 } BOOL fDwmEnabled = FALSE; HRESULT hr = DwmIsCompositionEnabled(&fDwmEnabled); if (!SUCCEEDED(hr)) { qDebug() << "composition is not enabled."; return false; } switch (msg->message) { case WM_NCCALCSIZE: // 调整非客户区域,让客户端区域延伸到标题栏 if (result) { *result = 0; } return true; case WM_DWMCOMPOSITIONCHANGED: case WM_SHOWWINDOW: { MARGINS margins = {0, 0, 0, TOPEXTENDWIDTH}; hr = DwmExtendFrameIntoClientArea(reinterpret_cast<HWND>(m_mainHwnd), &margins); if (SUCCEEDED(hr)) { // 通知窗口重新计算布局 SetWindowPos(reinterpret_cast<HWND>(m_mainHwnd), nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE); } return false; // 不拦截消息,让Qt继续处理 } default: return false; } }
3. 调整Qt布局,让内容延伸到标题栏
在MainWindow中重写resizeEvent,调整菜单栏和中央部件的位置:
#include <QResizeEvent> void MainWindow::resizeEvent(QResizeEvent* event) { QMainWindow::resizeEvent(event); // 将菜单栏移动到标题栏顶部区域 menuBar()->setGeometry(0, 0, width(), menuBar()->height()); // 让中央部件从窗口顶部开始,覆盖标题栏区域 centralWidget()->setGeometry(0, 0, width(), height()); }
4. 正确安装事件过滤器
修改main.cpp,仅给主窗口的原生窗口安装过滤器:
#include <QApplication> #include "mainWindow.h" #include "winEventFilter.h" int main(int argc, char* argv[]) { QApplication app(argc, argv); MainWindow mainWin; // 获取主窗口的原生QWindow对象 QWindow* nativeWindow = mainWin.windowHandle(); WinEventFilter eventFilter(nativeWindow); app.installNativeEventFilter(&eventFilter); mainWin.show(); return app.exec(); }
关于WinUI API的使用
WinUI 3/Windows App SDK的标题栏自定义API(如AppWindow.TitleBar)可实现更现代化的效果,但直接在Qt中集成难度极高:
- 需要通过Windows App SDK创建
AppWindow,再将Qt窗口嵌入其中,涉及窗口句柄嵌套和消息转发,复杂度远高于DWM方案。 - 这种集成会导致Qt窗口管理与Windows App SDK窗口管理冲突,比如模态对话框、窗口状态同步等问题。
因此,基于DWM的方案是更可行的选择,它能在保留原生系统按钮、Snap布局等行为的同时,实现内容延伸到标题栏的效果,且仅需少量原生API调用,与Qt兼容性更好。
内容的提问来源于stack exchange,提问作者rainbowgoblin

