Windows平台Qt无边款窗口技术细节

这篇具有很好参考价值的文章主要介绍了Windows平台Qt无边款窗口技术细节。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

Windows平台Qt无边款窗口技术细节

(本文只讨论带有窗口特效的无边框实现,通过自绘阴影、自定义鼠标事件的方式不具备参考价值)

无论是哪个GUI框架,在Windows平台实现无边框窗口,都是一个绕不过去的话题,毕竟标题栏总是与设计师的图格格不入。自Win7以来,Windows的桌面窗口管理器(DWM)给应用程序的窗口交互增加了一些功能,如:最小化动态效果、拖拽标题栏停靠顶部最大化等。通过 Qt::FramelessWindowHint 可以直接将默认边框去掉,但这些效果也随之消失,为了保留效果,只能通过Windows消息实现,即重写QWidget::nativeEvent。

本文的目的是为了整理这些年使用Qt实现无边框窗口技术细节,因为个人比较懒,所以暂时不提供一个兼容所有系统、所有Qt版本的实现方案,只讨论需要注意的细节。

1. 先去掉边框

首先,MSDN提供了一个简单的示例,可以用来参考。

当窗口尺寸改变时,系统使用 WM_NCCALCSIZE 消息来计算窗口客户区在桌面的区域。窗口区域是包含标题栏、边框在内的整个区域,客户区通常是程序显示内容的区域。默认情况下,系统会自动计算合适的客户区,客户区比窗口区要小。所以原则上只要将客户区与窗口区域保持一致即可。

重写QWidget::nativeEvent,WM_NCCALCSIZE 的 lParam 参数在 wParam 为 true 时是NCCALCSIZE_PARAMS ,其 rgrc 是一个RECT数组,第一个就是目标窗口区域,比如移动、缩放窗口。当该消息返回时,第一个应该是客户区的区域:

  • 当窗口处于正常显示状态(非最大化)

    此时窗口区域应该与客户区一致,所以 *result = 0,直接返回即可。

  • 当窗口处于最大化时

    默认的窗口边框一部分实际是处于当前屏幕之外的(可以通过spy++查看或者 GetWindowRect 获取),如果直接返回,就会导致客户区内容一部分显示在屏幕之外。所以需要手动修改,如何计算还是比较麻烦的

    个人在参考 Windows Terminal 的源码时看到一种方案,这里简单抄一了一下逻辑:

    case WM_NCCALCSIZE:
    {
        *result = 0;
        if(!msg->wParam)
            return true;
        // 这部分代码参考windows terminal源码
        NCCALCSIZE_PARAMS &params = *reinterpret_cast<NCCALCSIZE_PARAMS *>(msg->lParam);
        const int originalTop = params.rgrc[0].top;
        const RECT originalRect = params.rgrc[0];
        const auto ret = ::DefWindowProc(msg->hwnd, WM_NCCALCSIZE, msg->wParam, msg->lParam);
        if(ret != 0)
        {
            *result = ret;
            return true;
        }
        params.rgrc[0].top = originalTop;
    
        bool isMaximized = GetWindowStyle(msg->hwnd) & WS_MAXIMIZE;
        if(isMaximized)
        {
            // 这里计算一个默认边框尺寸border,原则上应该根据dpi计算,参考windows terminal源码
            int border = GetSystemMetrics(SM_CXSIZEFRAME) + GetSystemMetrics(SM_CXPADDEDBORDER);
            RECT &rect = params.rgrc[0];
            rect.top += border;
    
            QMargins margins = calculateTaskBarMargins(msg->hwnd);
            rect.top += margins.top();
            rect.bottom -= margins.bottom();
            rect.left += margins.left();
            rect.right -= margins.right();
        }
        else
        {
            params.rgrc[0] = originalRect;
        }
        return true;
    }
    

    代码逻辑是,先保存初始的窗口区域,然后调用 DefWindowProc 执行默认计算,计算出默认的客户区区域。如果是最大化窗口,默认的计算已经将左、右、下的区域计算准确了, 只修改top。非最大化窗口,则修改为初始值。Windows Terminal 的无边框窗口方案实际很复杂,而且考虑了全屏状态(Qt::WindowFullScreen)。

窗口最大化时,还有一个比较麻烦的问题是,如果任务栏处于隐藏状态,由于窗口区域是整个屏幕,而客户区也刚好也覆盖了屏幕,导致鼠标移动至显示器边界,不会弹出任务栏。所以需要判断任务栏在屏幕的哪一个边,预留出2像素的边距(上面代码中calculateTaskBarMargins函数实现)。然而win8.1以前没有直接接口判断,这个可以自行查找方案。

需要注意的是,早期的Qt(具体没做测试,应该5.9以前)还有一些问题:

  • 早期Qt是通过 Qt::FramelessWindowHint 标志判断是否是无边框,win32的坐标又是全局的,导致Qt内部计算错误。所以会常见类似这样的写法:

    setWindowFlags(Qt::FramelessWindowHint | Qt::WindowSystemMenuHint | Qt::WindowMinMaxButtonsHint | Qt::WindowCloseButtonHint);
    HWND hwnd = (HWND)this->winId();
    DWORD style = ::GetWindowLong(hwnd, GWL_STYLE);
    ::SetWindowLong(hwnd, GWL_STYLE, style | WS_MAXIMIZEBOX | WS_THICKFRAME | WS_CAPTION);
    

    通过Qt::FramelessWindowHint关闭无边框,保证Qt内部计算准确,同时调用SetWindowLong使原生窗口样式保留样式基本样式。

  • 而且窗口最大化时,不会关心WM_NCCALCSIZE计算的客户区区域信息,所以需要手动调整整个窗口的显示区域,例如调用 setContentsMargins 设置边距。多屏幕场景,甚至会在相邻屏幕出现白边。有个私有接口似乎可以同时解决边距和白变问题,没有严格测试。

现在的版本应该已经不需要这样做了,Qt内部拦截了 WM_NCCALCSIZE ,根据返回值计算了实际的边框信息。

2. 恢复默认阴影

恢复默认阴影的方案比较统一,通过DWM API,将默认边框扩展到客户区。常见做法是:

const MARGINS shadow = {1, 1, 1, 1};
DwmExtendFrameIntoClientArea(HWND(winId()), &shadow);

具体在什么时机调用比较合适没有准确的说法,上面MSDN的示例里是在 WM_ACTIVATE 消息里执行,Windows Terminal 的源码又在多种情况下都有调用,只在构造函数里执行似乎也行。

3. 移动和缩放

通过 WM_NCHITTEST 消息实现缩放,WM_NCHITTEST 的 lParam 参数包含了鼠标坐标,配合窗口区域,来返回命中区域。

case WM_NCHITTEST:
{
    long x = GET_X_LPARAM(msg->lParam);
    long y = GET_Y_LPARAM(msg->lParam);
    QPoint pos = mapFromNative(QPoint(x, y));
    if(pos.y() < captionHeight)
    	*result = HTCAPTION;
    else
        *result = HTCLIENT;
    return true;
}

基本逻辑比较简单,根据坐标返回对应的区域即可,主要有以下几个:

描述
HTCAPTION 标题栏,实现拖拽移动窗口,自动最大化,自动布局等
HTCLIENT 客户区,返回该值的区域,会收到鼠标事件
HTSYSMENU 在该区域点击会弹出系统菜单,与在默认窗口左上角图标处点击一致
HTLEFT 左边框,向左缩放窗口
HTRIGHT 有边框,向右缩放窗口
HTTOP 上边框
HTTOPLEFT 左上缩放
HTTOPRIGHT 右上缩放
HTBOTTOM 下边框
HTBOTTOMLEFT 左下缩放
HTBOTTOMRIGHT 右下缩放

需要注意的是,从Win32坐标转换到Qt坐标,多屏场景下不能直接mapFromGlobal,参考 Win32屏幕坐标转换Qt坐标 进行转换。

4. 其他问题

  • Win11 系统,当鼠标在最大化按钮悬停时,会弹出snap layout选项,实现更丰富的布局。

    如果自定义了一个最大化按钮,该功能的实现需要在 WM_NCHITTEST,判断是否悬停在自定义的最大化按钮,并返回 HTMAXBUTTON。

    这样带来的问题是,QWidget 不会收到鼠标事件,不能点击,相对对应的悬停、按下等效果都失效。所以需要在 WM_NCLBUTTONDOWN、WM_NCLBUTTONUP、WM_NCMOUSEHOVER、WM_NCMOUSEMOVE等消息中,转换成WM_MOUSEMOVE等相关鼠标消息发送给按钮,或者直接修改样式达到状态改变的效果,(建议后者)。

    WM_NCLBUTTONDOWN等消息的 wParam 参数,就是 WM_NCHITTEST 的返回值,直接判断即可,具体可自行实现:

    case WM_NCLBUTTONDOWN:
    case WM_NCLBUTTONUP:
    case WM_NCLBUTTONDBLCLK:
        if(msg->wParam == HTMAXBUTTON)
        {
            *result = 0;
            // 处理鼠标事件
            return true;
        }
        break;
    case WM_NCMOUSEHOVER:
    case WM_NCMOUSELEAVE:
    case WM_NCMOUSEMOVE:
        if(msg->wParam == HTMAXBUTTON)
        {
            *result = 0;
            // 处理鼠标事件
            return true;
        }
    
  • win11系统 snap layout 特性仅在最大化时生效

    如果出现仅在最大化时才生效 snap layout,可能是因为 DwmExtendFrameIntoClientArea 设置阴影的时候,MARGINS 参数设置了四周的非客户区扩展都是1像素,此时系统认为顶部区域存在非客户区,优先认为鼠标位置没有在最大化按钮上。

    可以考虑 将 MARGINS 参数的top改为0,似乎只要四个边有一个不是0,就可以显示阴影

    (兴许也可以通过拦截某些win32消息解决,但这块确实没有什么资料。)

  • 缩放窗口时的画面闪烁问题:

    (再补充)文章来源地址https://www.toymoban.com/news/detail-474450.html

到了这里,关于Windows平台Qt无边款窗口技术细节的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【QT】Qt Charts的实际使用中的一些小细节完善如:resetZoom、fitInView

    在Qt中, 使用 Qt Charts来创建和操作图表,重置图表缩放状态的功能可以通过调整图表视图的缩放比例来实现。Qt Charts中的 QChartView 提供了相关的方法来控制图表的缩放和平移。 示例代码,以及如何对此功能进行扩展: chartView-resetTransform(); 是重置图表视图的缩放到默认状态。

    2024年04月17日
    浏览(41)
  • eth ens 合约技术代码细节

    https://medium.com/crypto-wisdom/what-is-ethereum-name-service-how-it-changes-the-world-of-dns-8829756a8b30 https://blog.cloudflare.com/cloudflare-distributed-web-resolver/ ENS最新合约源码分析二_DoubleCherish的博客-CSDN博客  以太坊域名服务ENS剖析 - 知乎  ENS源码分析 | 登链社区 | 区块链技术社区 

    2024年02月11日
    浏览(29)
  • 主流大语言模型的技术原理细节

    1.0 transformer 与 LLM 1.1 模型结构 1.2 训练目标 1.3 tokenizer 1.4 位置编码 1.5 层归一化 1.6 激活函数 1.7 Multi-query Attention 与 Grouped-query Attention 1.8 并行 transformer block 1.9 总结-训练稳定性 2.0 点对点通信与集体通信 2.1 数据并行 2.2 张量并行 2.4 3D 并行 2.5 混合精度训练 2.6 激活重计算 2

    2024年02月08日
    浏览(36)
  • 1262(构造)(bfs无边权最短路)(E - Nearest Black Vertex

    E - Nearest Black Vertex (atcoder.jp) 题面 如果无法将每个顶点绘制为黑色或白色以满足条件,请打印  No .否则,打印  Yes 如果顶点 i is painted black and 被漆成黑色 00 if white. 如果是白色。 第一行是否有解,YES NO 第二行字符串1是黑,0是白。 连通无向图 不包含自环和多边 第i条边

    2024年02月02日
    浏览(36)
  • 外卖系统的运转:背后的技术和管理细节

    外卖系统的运作涉及许多技术和管理方面,其中包括前端应用程序、后端服务器、数据库管理、订单处理和配送等环节。 前端应用程序 : 外卖平台的用户界面,包括顾客点餐界面和餐厅端的接单界面。通常使用HTML、CSS和JavaScript来构建,也可能会涉及移动应用程序开发(A

    2024年02月06日
    浏览(33)
  • QT---将第三方软件窗口嵌入窗口中

    通过Windows API获取窗口句柄。 代码如下:

    2024年02月12日
    浏览(80)
  • Qt Creator 创建 Qt 默认窗口程序

    Qt 入门实战教程(目录) Windows Qt 5.12.10下载与安装 本文介绍用Qt自带的集成开发工具Qt Creator创建Qt默认的窗口程序。 本文不需要你另外安装Visual Studio 2022这样的集成开发环境,也不需要你再在Visual Studio 2022中安装Qt VS Tools这样的插件。 目的就是为了能够让你可以更快的把Q

    2024年02月09日
    浏览(50)
  • Qt 中如何在主窗口中添加子窗口

    方法 原理其实简单,和在窗口上动态 (代码的形式) 添加控件的方法一样,但需要设置一下子窗口的属性: 在子窗口构造函数中添加代码: setWindowFlags(Qt::FramelessWindowHint); 作用:隐藏子窗口的标题栏和边框,如果不隐藏的话,子窗口无法嵌套到其它控件上面! 2. 实例: 指针法 new实例

    2024年02月13日
    浏览(38)
  • QT中如何在主窗口中添加子窗口

    1.方法         原理其实很简单,和在窗口上动态(代码的形式)添加控件的方法一样,但需要设置一下子窗口的属性: 在子窗口构造函数中添加代码: 作用:隐藏子窗口的标题栏和边框,如果不隐藏的话,子窗口无法嵌套到其它控件上面! 2.举例 在项目中添加一个子窗口(sub1)     子

    2024年02月11日
    浏览(44)
  • QT中将 子窗口添加到父窗口中,并实现子窗口随父窗口大小伸缩。

    在主窗口的 ui中 在MW ui中,整一个layout部件; 以及主窗口的其他部件一起都在MW(MainWindow)中,首先进行局部布局; 然后, 点击 MW ui再次进行 水平/垂直/..布局:这步重要,否则,子窗口就无法跟随主窗口一起进行缩放。 在 mainwindow.cpp嵌入子窗口 ui-xxxLayout-addWidget(子窗口

    2024年02月11日
    浏览(39)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包