Qt 基于win11无边框界面的实现(最大化按钮悬浮弹出snap layout)

  • Post author:
  • Post category:其他


提示:文章写完后,目录可以自动生成,如何生成可参考右边的帮助文档




前言


提示:这里可以添加本文要记录的大概内容:

贴靠布局是 Windows 11 中的一项新功能,用户可通过该布局了解强大的窗口贴靠功能。 通过将鼠标悬停在窗口的最大化按钮上或按 Win + Z,可以轻松访问对齐布局。

win11贴靠布局

在Qt界面开发中,为了使UI界面更加协调,或者说标题栏需要添加自定义的一些功能或者控件时,常常使用无边框窗口进行设计符合自己需求的标题栏,然而当去掉系统边框之后,在普通按钮上悬浮又不会弹出snap layout布局,强烈的好奇心下,就产生一个可以支持win11 snap layout 的无边框窗口。

本文主要目的在于如何使Qt的无边框窗口支持win11 snap layout ,先来放一下最终的实现效果图。

在这里插入图片描述

在这里插入图片描述



提示:以下是本篇文章正文内容,下面案例可供参考



一、说明



1、QT版本以及编译器

Qt版本: Qt5.9.9

编译器:MSVC2015 64bit



2、主要参考文章以及代码参考


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


这里还要感谢该篇文章大佬提供的思路以及帮助,很有耐心的解决我的疑问。


windows系统实现无边框,同时支持Aero效果


代码是在这个开源项目的基础之上进行修改的,是一个很完美的Qt无边框解决方案,具体无边框的细节可以参考这个,下面是一个翻译的文章

基于QMainWindow 实现的效果很好的 Qt 无边框窗口



3、声明

由于本人知识有限,如有什么错误或者不足的地方,还请各位大佬帮忙指出。



二、基本实现思路

由于window目前并没有提供一个API接口来调用snap layout,所以就想通过消息欺骗的方式使系统自己调用snap layout,通俗说,我提供给系统一个假消息,某某按钮就是最大化按钮,然后交由系统处理。

其中

WM_NCHITTEST消息

就是用来发送到窗口以确定窗口的哪个部分对应于特定的屏幕坐标。 例如,当光标移动、按下或释放鼠标按钮或响应对 WindowFromPoint 等函数的调用时,可能会发生这种情况。

如果未捕获鼠标,则会将消息发送到光标下方的窗口。 否则,消息将发送到已捕获鼠标的窗口。

那我们需要做的是WM_NCHITTEST消息的返回值置为最大化按钮,即命中测试返回HTMAXBUTTON。

在这里插入图片描述

这里MSDN提供的一个方案。

支持为 Windows 11 上的桌面应用使用贴靠布局

LRESULT CALLBACK TestWndProc(HWND window, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg)
    {
          case WM_NCHITTEST:
        {
            // Get the point in screen coordinates.
            // GET_X_LPARAM and GET_Y_LPARAM are defined in windowsx.h
            POINT point = { GET_X_LPARAM(lParam), GET_Y_LPARAM(lParam) };
            // Map the point to client coordinates.
            ::MapWindowPoints(nullptr, window, &point, 1);
            // If the point is in your maximize button then return HTMAXBUTTON
            if (::PtInRect(&m_maximizeButtonRect, point))
            {
                return HTMAXBUTTON;
            }
        }
        break;
    }
    return ::DefWindowProcW(window, msg, wParam, lParam);
}

上述方案在Qt中的代码实现

    case WM_NCHITTEST:
    {
        *result = 0;
        const LONG border_width = m_borderWidth;
        RECT winrect;
        GetWindowRect(HWND(winId()), &winrect);

        //x,y 为鼠标在屏幕的坐标
        long x = GET_X_LPARAM(msg->lParam);
        long y = GET_Y_LPARAM(msg->lParam);
        //...
        //...
        if (0!=*result) return true;

        //*result still equals 0, that means the cursor locate OUTSIDE the frame area
        //but it may locate in titlebar area
        if (!m_titlebar) return false;

        //support highdpi
        double dpr = this->devicePixelRatioF();
        QPoint pos = m_titlebar->mapFromGlobal(QPoint(x/dpr,y/dpr));

        if (!m_titlebar->rect().contains(pos)) return false;
        QWidget* child = m_titlebar->childAt(pos);
        if (!child)
        {
            *result = HTCAPTION;
            return true;
        }else{
            if (m_whiteList.contains(child))
            {
                *result = HTCAPTION;
                return true;
            }
            if(mMaxBtnHelper->isValid()){       //鼠标位于标题栏中最大化
                if(mMaxBtnHelper->AbstractButton() == child){
                    mMaxBtnHelper->setInRectBtnFlag(true);
                    *result = HTMAXBUTTON;      //最大化
                    return true;	//返回为true,截获信息,并提供一个虚假的信息,告诉系统这个区域为最大化区域,即可实现下消息欺骗。
                }
            }
        }
        return false;
    } //end case WM_NCHITTEST



三、方案优化

当然上面这个方法是有副作用的,即当我们截获鼠标信息以后

最大化区域内无法响应鼠标事件,不能点击,相对对应的悬停、按下等效果都失效。所以需要在

WM_NCLBUTTONDOWN、WM_NCLBUTTONUP、WM_NCMOUSEHOVER、WM_NCMOUSEMOVE等消息中,转换成WM_MOUSEMOVE等相关鼠标消息发送给按钮。


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

,接下来详细实现win对应鼠标消息发送相应的事件交由QT的事件处理机制,来恢复该区域的相对应的事件,以及悬浮、按下样式。



1、样式表为状态与Qt事件

QPushButton:{ background-color:rgb(180 , 200, 255); } 
QPushButton:hover{ background-color:rgb(220 , 200 , 255); } 
QPushButton:pressed{ background-color:rgb(200 , 250 , 0); }";

上面 :hover、pressed 为样式表中常见

伪状态

用户在操作时,可以根据不同的交互状态展示不同的用户样式,界面能够识别用户操作,不需要代码控制即可响应不同状态下的样式。

其中我们所用到的伪状态hover和press与Qt对应的事件如下。

normal   ->  :hover     ----  QEvent::Enter
:hover   ->  normal     ----  QEvent::Leave
normal   ->  :pressed   ----  QEvent::MouseButtonPress
:pressed ->  normal     ----  QEvent::MouseButtonRelease

清楚了这些,样式的恢复思路就清晰了。如果我们想达到hover样式,只需要发送QEvent::Enter就可以了

(注意:发送QEvent::Ente与QEvent::Leave需要发送update事件重绘一下按钮,不然-样式不会生效)。



2、确定需求

以最大化按钮为例,可具体为功能需求和样式需求。



1.功能需求

在这里插入图片描述

如图,正常情况下,鼠标释放,窗口响应最大化,非正常情况下,窗口维持原状。

对于正常2和非正常2情况,

当鼠标从按钮左键按下不松开移动到窗口客户区以后,鼠标将不在进入WM_NCHITTEST命中测试,此时发送鼠标按钮外释放事件,在此之后所有的鼠标释放均为WM_LBUTTONUP,此时WM_MOUSEMOVE中判断鼠标释放时刻的位置。

如果鼠标再次回到按钮区域内,发送QEvent::MouseButtonPress与QEvent::MouseButtonRelease模拟鼠标点击事件,完成最大化功能。



2.样式需求

在这里插入图片描述

这里需要特殊说明的是,当鼠标 进入->按下->按钮内释放(释放后不移动鼠标),按钮将会处于hover状态,所以在鼠标在按钮内释放时,发送QEvent::MouseButtonRelease的同时,发送QEvent::Leave。



3、具体实现代码

    case WM_LBUTTONUP:
//        qDebug() << " ==========   WM_LBUTTONUP   ========== "   <<  countAll++;
        mMinBtnHelper->mouseRealseDeal(result, false);
        mMaxBtnHelper->mouseRealseDeal(result, false);
        mCloseBtnHelper->mouseRealseDeal(result, false);
        return false;
        //鼠标在客户区移动
    case WM_MOUSEMOVE:
    {
        //qDebug() << " ==========   WM_MOUSEMOVE   ========== "   <<  countAll++;
        *result = 0;
        // 鼠标按下的情况下,第一次从按钮移入客户区,发送鼠标释放的事件
        if(mMinBtnHelper->isFirstMove()){
            mMinBtnHelper->setMoveInClientFirst(false);
            mMinBtnHelper->sendMouseRelease(false);
            //qDebug() << " ==========   MOVE  mMinBtnHelper  MOVE   ========== "   <<  countAll++;
        }
        if(mMaxBtnHelper->isFirstMove()){
            mMaxBtnHelper->setMoveInClientFirst(false);
            mMaxBtnHelper->sendMouseRelease(false);
            //qDebug() << " ==========   MOVE    mMaxBtnHelper   MOVE   ========== "   <<  countAll++;
        }
        if(mCloseBtnHelper->isFirstMove()){
            mCloseBtnHelper->setMoveInClientFirst(false);
            mCloseBtnHelper->sendMouseRelease(false);
            //qDebug() << " ==========   MOVE  mCloseBtnHelper  MOVE   ========== "   <<  countAll++;
        }

        mMinBtnHelper->mouseEnterLeaveDeal();
        mMaxBtnHelper->mouseEnterLeaveDeal();
        mCloseBtnHelper->mouseEnterLeaveDeal();

        //x,y 为鼠标在窗口客户区的坐标
        long x = GET_X_LPARAM(msg->lParam);
        long y = GET_Y_LPARAM(msg->lParam);
        //support highdpi
        double dpr = this->devicePixelRatioF();
        QPoint pos = QPoint(x/dpr,y/dpr);
        if (!m_titlebar->rect().contains(pos)) return false;
        QWidget* child = m_titlebar->childAt(pos);
        if (child){
            if (m_whiteList.contains(child))
            {
                *result = HTCAPTION;
                return true;
            }
            if(mMinBtnHelper->isValid()){       //鼠标位于标题栏中最小化按钮
                if(mMinBtnHelper->AbstractButton() == child){
                    mMinBtnHelper->setInRectBtnFlag(true);
                }
            }
            if(mMaxBtnHelper->isValid()){       //鼠标位于标题栏中最大化
                if(mMaxBtnHelper->AbstractButton() == child){
                    mMaxBtnHelper->setInRectBtnFlag(true);
                }
            }
            if(mCloseBtnHelper->isValid()){       //鼠标位于标题栏中关闭按钮
                if(mCloseBtnHelper->AbstractButton() == child){
                    mCloseBtnHelper->setInRectBtnFlag(true);
                }
            }
        }
        return false;
    }

    //非客户端区域鼠标左键按下
    case WM_NCLBUTTONDOWN:
    {
//        qDebug() << "**********   WM_NCLBUTTONDOWN   ****************"   <<  countAll++;
        // 处理鼠标事件
        mMinBtnHelper->mouseEnterLeaveDeal();
        mMaxBtnHelper->mouseEnterLeaveDeal();
        mCloseBtnHelper->mouseEnterLeaveDeal();
        if(msg->wParam == HTMINBUTTON){     //最小化按钮
            if(mMinBtnHelper->mousePressDeal(result))
                return true;
        }
        else if(msg->wParam == HTMAXBUTTON){    //最大化按钮
            if(mMaxBtnHelper->mousePressDeal(result))
                return true;
        }
        else if(msg->wParam == HTCLOSE){    //关闭按钮
            if(mCloseBtnHelper->mousePressDeal(result))
                return true;
        }
        return false;   //

    }
    //非客户端区域鼠标左键释放
    case WM_NCLBUTTONUP:
    {
//        qDebug() << "=========   WM_NCLBUTTONUP   ****************"   <<  countAll++;
        if(msg->wParam == HTMINBUTTON){     //最小化按钮
            if(mMinBtnHelper->mouseRealseDeal(result))
                return true;
        }
        else if(msg->wParam == HTMAXBUTTON){    //最大化按钮
                    if(mMaxBtnHelper->mouseRealseDeal(result))
                        return true;
        }
        else if(msg->wParam == HTCLOSE){    //关闭按钮
                    if(mCloseBtnHelper->mouseRealseDeal(result))
                        return true;
        }

        mMinBtnHelper->releaseFlag();
        mMaxBtnHelper->releaseFlag();
        mCloseBtnHelper->releaseFlag();
        return false;
    }
    case WM_NCHITTEST:
    {
        //qDebug() << "**********   WM_NCHITTEST   ****************"   <<  countAll++;
        mMinBtnHelper->mouseEnterLeaveDeal();
        mMaxBtnHelper->mouseEnterLeaveDeal();
        mCloseBtnHelper->mouseEnterLeaveDeal();

        *result = 0;
        const LONG border_width = m_borderWidth;
        RECT winrect;
        GetWindowRect(HWND(winId()), &winrect);

        //x,y 为鼠标在屏幕的坐标
        long x = GET_X_LPARAM(msg->lParam);
        long y = GET_Y_LPARAM(msg->lParam);

        if(m_bResizeable)
        {

            bool resizeWidth = minimumWidth() != maximumWidth();
            bool resizeHeight = minimumHeight() != maximumHeight();

            if(resizeWidth)
            {
                //left border
                if (x >= winrect.left && x < winrect.left + border_width)
                {
                    *result = HTLEFT;
                }
                //right border
                if (x < winrect.right && x >= winrect.right - border_width)
                {
                    *result = HTRIGHT;
                }
            }
            if(resizeHeight)
            {
                //bottom border
                if (y < winrect.bottom && y >= winrect.bottom - border_width)
                {
                    *result = HTBOTTOM;
                }
                //top border
                if (y >= winrect.top && y < winrect.top + border_width)
                {
                    *result = HTTOP;
                }
            }
            if(resizeWidth && resizeHeight)
            {
                //bottom left corner
                if (x >= winrect.left && x < winrect.left + border_width &&
                        y < winrect.bottom && y >= winrect.bottom - border_width)
                {
                    *result = HTBOTTOMLEFT;
                }
                //bottom right corner
                if (x < winrect.right && x >= winrect.right - border_width &&
                        y < winrect.bottom && y >= winrect.bottom - border_width)
                {
                    *result = HTBOTTOMRIGHT;
                }
                //top left corner
                if (x >= winrect.left && x < winrect.left + border_width &&
                        y >= winrect.top && y < winrect.top + border_width)
                {
                    *result = HTTOPLEFT;
                }
                //top right corner
                if (x < winrect.right && x >= winrect.right - border_width &&
                        y >= winrect.top && y < winrect.top + border_width)
                {
                    *result = HTTOPRIGHT;
                }
            }
        }
        if (0!=*result) return true;

        //*result still equals 0, that means the cursor locate OUTSIDE the frame area
        //but it may locate in titlebar area
        if (!m_titlebar) return false;

        //support highdpi
        double dpr = this->devicePixelRatioF();
        QPoint pos = m_titlebar->mapFromGlobal(QPoint(x/dpr,y/dpr));

        if (!m_titlebar->rect().contains(pos)) return false;
        QWidget* child = m_titlebar->childAt(pos);
        if (!child)
        {
            *result = HTCAPTION;
            return true;
        }else{
            if (m_whiteList.contains(child))
            {
                *result = HTCAPTION;
                return true;
            }
            if(mMinBtnHelper->isValid()){       //鼠标位于标题栏中最小化按钮
                if(mMinBtnHelper->AbstractButton() == child){
                    mMinBtnHelper->setInRectBtnFlag(true);
                    *result = HTMINBUTTON;      //最小化
                    return true;
                }
            }
            if(mMaxBtnHelper->isValid()){       //鼠标位于标题栏中最大化
                if(mMaxBtnHelper->AbstractButton() == child){
                    mMaxBtnHelper->setInRectBtnFlag(true);
                    *result = HTMAXBUTTON;      //最大化
                    return true;
                }
            }
            if(mCloseBtnHelper->isValid()){       //鼠标位于标题栏中关闭按钮
                if(mCloseBtnHelper->AbstractButton() == child){
                    mCloseBtnHelper->setInRectBtnFlag(true);
                    *result = HTCLOSE;      //关闭
                    return true;
                }
            }
        }
        return false;
    } //end case WM_NCHITTEST



总结

以上就是本文要讲的内容,有些地方可能描述的不太清楚,具体可以看源码实现。


CSDN源码

积分多的大佬,可以采用此方式下载


GitCode源码

代码仅供参考学习。



版权声明:本文为aaada123原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。