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

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


前言

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

贴靠布局是 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源码代码仅供参考学习。

Qt实现点击按钮弹出一个新界面,你需要以下步骤: 1. 创建一个新的 QWidget 类,作为你要弹出的新界面。 2. 在 QWidget 类中添加你需要的控件,比如按钮、文本框等。 3. 在主窗口中创建一个 QPushButton 控件,并将其添加到布局中。 4. 为 QPushButton 控件添加一个槽函数,在槽函数中创建新的 QWidget 对象,并显示它。 下面是一个简单的示例代码: ```cpp // 新界面类 class NewWidget : public QWidget { public: NewWidget(QWidget *parent = nullptr) : QWidget(parent) { // 添加控件 QLabel *label = new QLabel("Hello, World!", this); label->setAlignment(Qt::AlignCenter); label->setStyleSheet("font-size: 20px;"); } }; // 主窗口类 class MainWindow : public QMainWindow { public: MainWindow(QWidget *parent = nullptr) : QMainWindow(parent) { // 添加按钮 QPushButton *button = new QPushButton("Click me", this); connect(button, &QPushButton::clicked, this, &MainWindow::onButtonClick); setCentralWidget(button); } private slots: void onButtonClick() { // 弹出界面 NewWidget *newWidget = new NewWidget(this); newWidget->setWindowTitle("New Widget"); newWidget->show(); } }; // 应用程序入口 int main(int argc, char *argv[]) { QApplication app(argc, argv); MainWindow window; window.show(); return app.exec(); } ``` 在这个示例中,我们创建了一个 NewWidget 类作为新界面,添加了一个 QLabel 控件,并设置了字体样式。在主窗口中,我们创建了一个 QPushButton 控件,并将其添加到中央部件中。然后,我们为按钮添加了一个槽函数 onButtonClick(),在这个函数中创建了一个 NewWidget 对象,并显示它。 当用户点击按钮时,程序会执行 onButtonClick() 函数,弹出一个新的 NewWidget 界面
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值