You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Qt 6 Windows标题栏定制:扩展内容并保留原生窗口按钮

问题: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)。


解决方案

当前方法的问题分析

你的方案方向正确,但存在几个关键问题导致未达预期:

  1. 全局事件过滤器误捕获消息:全局过滤器会处理所有窗口的消息(包括Qt内部辅助窗口),这就是No Qt Window found警告的来源,应该只针对主窗口的HWND处理。
  2. 未禁用Qt原生标题栏绘制:即使调用DwmExtendFrameIntoClientArea,Qt默认仍会绘制自己的标题栏,需要调整窗口标志让Qt放弃标题栏绘制,但保留系统原生按钮。
  3. 消息处理时机错误:WM_ACTIVATE不是调用DWM扩展的最佳时机,应在窗口创建完成后执行,且需确保只操作主窗口。
  4. 未调整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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.11 15:55:33