C++/Qt Window系统下无边框窗体

运行效果

Window系统下Qt无边框窗体

NativeWindowTemplate.hpp

#ifndef NATIVEWINDOWTEMPLATE_H
#define NATIVEWINDOWTEMPLATE_H

#include <QWidget>
#include <QTimer>
#include <QPointer>
#include <QSet>
#include <QEvent>
#include <QApplication>
#include <QScreen>
#include <QLabel>
#include <QtDebug>
#include <windows.h>
#include <WinUser.h>
#include <windowsx.h>
#include <dwmapi.h>
#include <objidl.h> // 修复错误 C2504: 'IUnknown' : base class undefined

template<typename T>
class NativeWindowTemplate : public T
{
public:
    // 构造函数,接受可变参数并设置窗口可调整大小
    template<typename... Ts>
    explicit NativeWindowTemplate(Ts... args): T(args...)
    {
        setResizeable(m_bResizeable);

        // 设置强制更新定时器
        m_forceUpdateTimer.setSingleShot(true);
        m_forceUpdateTimer.setInterval(0);
        QObject::connect(&m_forceUpdateTimer, &QTimer::timeout, this, [this]() {
            // 强制刷新窗口mask,解决最大化时的显示问题
            const auto oldMask = T::mask();
            T::setMask(QRegion(this->rect()));
            T::setMask(oldMask);
        });
    }

    // 设置窗口是否可调整大小
    void setResizeable(bool resizeable = true)
    {
        m_bResizeable = resizeable;
        HWND hwnd = reinterpret_cast<HWND>(this->effectiveWinId());
        if(hwnd == nullptr) {
            return;
        }

        // 根据是否可调整大小设置窗口样式
        if (m_bResizeable) {
            const DWORD style = ::GetWindowLong(hwnd, GWL_STYLE);
            // 添加 WS_MAXIMIZEBOX 和 WS_THICKFRAME 以支持最大化和拖拽调整大小
            // WS_CAPTION: 没有这项属性会导致在 VS 下出现窗口边框闪动(窗口激活状态切换时), 有这项属性会导致最大化时内容超出屏幕, 因此保留这项属性, 最大化时尺寸另作处理
            // WS_MAXIMIZEBOX: 添加这项属性以支持窗口拖动到屏幕边缘放大效果
            // WS_SYSMENU: 禁用这项属性, 避免win7下出现系统的最大最小化和关闭按钮
            // WS_THICKFRAME: 添加这项属性以使窗口附带系统阴影效果
            ::SetWindowLong(hwnd, GWL_STYLE, (style & ~WS_SYSMENU) | WS_MAXIMIZEBOX | WS_CAPTION | WS_THICKFRAME);
        } else {
            const DWORD style = ::GetWindowLong(hwnd, GWL_STYLE);
            // WS_MAXIMIZEBOX: 不允许缩放窗口时, 禁用此属性
            ::SetWindowLong(hwnd, GWL_STYLE, (style & ~WS_SYSMENU & ~WS_MAXIMIZEBOX) | WS_CAPTION | WS_THICKFRAME);
        }

        // 为了绘制窗口周围的阴影,保留1像素的边框
        const MARGINS shadow = {1, 1, 1, 1};
        DwmExtendFrameIntoClientArea(hwnd, &shadow);
    }

    // 获取窗口是否可调整大小
    bool isResizeable() const
    {
        return m_bResizeable;
    }

    // 设置可调整大小的边框宽度
    void setResizeableAreaWidth(int width = 5)
    {
        if (1 > width)
            width = 1;
        m_borderWidth = width;
    }

protected:
    // 设置一个小部件作为系统标题栏
    void setTitleBar(QWidget* titlebar, bool autoIgnore = true)
    {
        m_titlebar = titlebar;
        // 自动忽略标题栏中的某些小部件
        if(titlebar != nullptr && autoIgnore) {
            auto children = titlebar->findChildren<QLabel*>();
            for(const auto& child : children) {
                addIgnoreWidget(child);
            }
        }
    }

    // 添加要忽略的小部件,避免影响窗口拖动
    void addIgnoreWidget(QWidget* widget)
    {
        m_whiteList.insert(widget);
    }

#if (QT_VERSION >= QT_VERSION_CHECK(6, 0, 0))
    bool nativeEvent(const QByteArray &eventType, void *message, qintptr *result) override
#else
    bool nativeEvent(const QByteArray &eventType, void *message, long *result) override // 处理窗口原生事件
#endif
    {
        if (!T::isWindow()) {
            return T::nativeEvent(eventType, message, result);
        }

        // Workaround for known bug -> check Qt forum : https://forum.qt.io/topic/93141/qtablewidget-itemselectionchanged/13
#if (QT_VERSION == QT_VERSION_CHECK(5, 11, 1))
        MSG* msg = *static_cast<MSG**>(message);
#else
        MSG* msg = static_cast<MSG*>(message);  // 处理 Windows 消息
#endif

        switch (msg->message) {
            case WM_NCCALCSIZE: {   // 窗口尺寸计算事件
                // 在非最大化时,减小底部边框1像素,防止绘制抖动
                NCCALCSIZE_PARAMS* sz = reinterpret_cast< NCCALCSIZE_PARAMS* >( msg->lParam );
                if(!::IsZoomed(msg->hwnd)) {
                    // sz->rgrc[0] 的值必须跟原来的不同, 否则拉伸左/上边框缩放窗口时, 会导致右/下侧出现空白区域 (绘制抖动)
                    // 窗口下边框失去1像素对视觉影响最小, 因此底部减少1像素
                    sz->rgrc[ 0 ].bottom += 1;
                } else {
                    // 在最大化时,修正内容超出屏幕的问题
                    // 获取工作区域信息,确保最大化时内容不超出屏幕
                    if(sz->lppos->flags & 0x8000 && IsWindowVisible(msg->hwnd)) {
                        m_forceUpdateTimer.start(); // 启动强制刷新定时器
                    } else {
                        m_forceUpdateTimer.stop(); // 窗口已正常显示, 没有必要再执行强制刷新
                        // 修正最大化时内容超出屏幕问题
                        auto monitor = MonitorFromWindow(msg->hwnd, MONITOR_DEFAULTTONEAREST);
                        MONITORINFO info;
                        info.cbSize = sizeof(MONITORINFO);
                        if(GetMonitorInfo(monitor, &info)) {
                            const auto workRect = info.rcWork;
                            sz->rgrc[0].left = qMax(sz->rgrc[0].left, long(workRect.left));
                            sz->rgrc[0].top = qMax(sz->rgrc[0].top, long(workRect.top));
                            sz->rgrc[0].right = qMin(sz->rgrc[0].right, long(workRect.right));
                            sz->rgrc[0].bottom = qMin(sz->rgrc[0].bottom, long(workRect.bottom));
                        }
                    }
                }

                *result = 0;
                return true;
            }
            // 鼠标事件处理
            case WM_NCHITTEST: {
                *result = 0;

                const LONG border_width = m_borderWidth;
                RECT winrect{};
                GetWindowRect(reinterpret_cast<HWND>(this->winId()), &winrect);

                const long x = GET_X_LPARAM(msg->lParam);
                const long y = GET_Y_LPARAM(msg->lParam);

                if (m_bResizeable && !IsZoomed(msg->hwnd)) {
                    const bool resizeWidth = T::minimumWidth() != T::maximumWidth();
                    const bool resizeHeight = T::minimumHeight() != T::maximumHeight();

                    if (resizeWidth) {
                        // 左边框
                        if (x >= winrect.left && x < winrect.left + border_width) {
                            *result = HTLEFT;
                        }
                        // 右边框
                        if (x < winrect.right && x >= winrect.right - border_width) {
                            *result = HTRIGHT;
                        }
                    }
                    if (resizeHeight) {
                        // 底边框
                        if (y < winrect.bottom && y >= winrect.bottom - border_width) {
                            *result = HTBOTTOM;
                        }
                        // 顶边框
                        if (y >= winrect.top && y < winrect.top + border_width) {
                            *result = HTTOP;
                        }
                    }
                    if (resizeWidth && resizeHeight) {
                        // 左下角
                        if (x >= winrect.left && x < winrect.left + border_width && y < winrect.bottom && y >= winrect.bottom - border_width) {
                            *result = HTBOTTOMLEFT;
                        }
                        // 右下角
                        if (x < winrect.right && x >= winrect.right - border_width && y < winrect.bottom && y >= winrect.bottom - border_width) {
                            *result = HTBOTTOMRIGHT;
                        }
                        // 左上角
                        if (x >= winrect.left && x < winrect.left + border_width && y >= winrect.top && y < winrect.top + border_width) {
                            *result = HTTOPLEFT;
                        }
                        // 右上角
                        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 = DefWindowProc(msg->hwnd, msg->message, msg->wParam, msg->lParam);
                const static QSet<long> sizeBorders = {HTLEFT, HTRIGHT, HTTOP, HTBOTTOM, HTBOTTOMLEFT, HTBOTTOMRIGHT, HTTOPLEFT, HTTOPRIGHT};
                if(IsZoomed(msg->hwnd) && sizeBorders.contains(*result)) {
                    *result = HTNOWHERE;
                }

                // *result 仍然等于 0,意味着光标位于窗口外部
                // 但可能位于标题栏区域
                if (!m_titlebar)
                    return true;

                // 支持高DPI
                const double dpr = this->devicePixelRatioF();
                const QPoint pos = m_titlebar->mapFromGlobal(QPoint(x / dpr, y / dpr));

                if (!m_titlebar->rect().contains(pos))
                    return true;
                QWidget* child = m_titlebar->childAt(pos);
                if (!child) {
                    *result = HTCAPTION;
                    return true;
                } else {
                    if (m_whiteList.contains(child)) {
                        *result = HTCAPTION;
                        return true;
                    }
                }

                return true;
            } // WM_NCHITTEST
            case WM_WINDOWPOSCHANGING: {
                // 告诉Windows丢弃客户端区域的全部内容,作为重用
                // 部分客户端区域在调整大小时会导致抖动
                auto* windowPos = reinterpret_cast<WINDOWPOS*>(msg->lParam);
                windowPos->flags |= SWP_NOCOPYBITS;
                break;
            }
            default:
                break;
        }
        return T::nativeEvent(eventType, message, result);
    }

    bool event(QEvent *event) override
    {
        if (!T::isWindow()) {
            return T::event(event);
        }

        switch (event->type()) {
            case QEvent::WindowStateChange: {
                // 在窗口最大化时, 用鼠标向下拖拽标题栏还原窗口, 不松手然后重新贴边最大化, 此时再进行窗口还原时(包括双击标题栏, showNormal()等方式), 标题栏会有一部分在屏幕之外
                // 这种现象无论有没有使用无边框属性都会发生, 应该是 Qt 的又一个 Bug (测试场景 Qt 5.15/Qt 6.3 + Win10)
                // 此处进行行为修正
                if(T::windowState() == Qt::WindowNoState) {
                    const auto workRect = qApp->primaryScreen()->availableVirtualGeometry();
                    // 这个地方不能直接用 pos() 方法, 因为 Qt 的尺寸和位置相关接口总是延迟几帧, 此时拿到的 pos() 仍然是最大化时的位置
                    RECT rect;
                    GetWindowRect(reinterpret_cast<HWND>(this->winId()), &rect);

                    if(rect.top < workRect.top()) {
                        T::move(rect.left, workRect.top());
                    }
                }
                break;
            }
            case QEvent::WinIdChange:
                setResizeable(m_bResizeable);
                break;
#if (QT_VERSION < QT_VERSION_CHECK(6, 0, 0))
            case QEvent::ScreenChangeInternal: {
                // 通过设置Mask强制触发更新, 修正跨屏拖拽时的错位问题, 同时会导致失去窗口阴影
                const auto oldMask = T::mask();
                T::setMask(QRegion(this->rect()));
                T::setMask(oldMask);

                // 重新设置窗口属性, 把窗口阴影带回来
                setResizeable(m_bResizeable);
                break;
            }
#endif
            default:
                break;
        }
        return T::event(event);
    }

private:
    QPointer<QWidget> m_titlebar;
    QSet<QWidget*> m_whiteList;
    int m_borderWidth{5};
    bool m_bResizeable{true};
    QTimer m_forceUpdateTimer;
};

#endif // NATIVEWINDOWTEMPLATE_H

mainwindow.h

#ifndef MAINWINDOW_H
#define MAINWINDOW_H

#include "NativeWindowTemplate.hpp"
#include <QString>
#include <QMainWindow>

namespace Ui
{
    class MainWindow;
}

class MainWindow : public NativeWindowTemplate<QMainWindow>
{
    Q_OBJECT

public:
    explicit MainWindow(QWidget *parent = 0);
    ~MainWindow();

private slots:
    void on_btnMin_clicked();
    void on_btnMax_clicked();
    void on_btnClose_clicked();
    void on_btnResizeable_clicked();

private:
    Ui::MainWindow *ui;
};

#endif // MAINWINDOW_H

main.cpp

#include "mainwindow.h"
#include <QApplication>

int main(int argc, char *argv[])
{
    QApplication::setAttribute(Qt::AA_EnableHighDpiScaling);    // 设置Qt应用程序属性,启用高DPI缩放
    QApplication a(argc, argv);
    MainWindow w;
    w.show();

    return a.exec();
}

mainwindow.cpp

#include "mainwindow.h"
#include "ui_mainwindow.h"
#include <QRect>
#include <QtDebug>

MainWindow::MainWindow(QWidget *parent) :
    NativeWindowTemplate<QMainWindow>(parent),
    ui(new Ui::MainWindow)
{
    ui->setupUi(this);
#ifdef Q_OS_WIN
    // 设置可调整大小边框的宽度为8像素
    setResizeableAreaWidth(8);

    // 设置标题栏小部件,以便通过它拖动主窗口
    setTitleBar(ui->widgetTitlebar);

    // labelTitleText 是 widgetTitlebar 的子小部件
    // 将 labelTitleText 添加到忽略列表,以便通过它拖动主窗口
    // addIgnoreWidget(ui->labelTitleText);

    // 此外,btnMin/btnMax... 也是 widgetTitlebar 的子小部件
    // 但我们不希望通过它们拖动主窗口
#endif

}

MainWindow::~MainWindow()
{
    delete ui;
}

void MainWindow::on_btnMin_clicked()
{
    showMinimized();
}

void MainWindow::on_btnMax_clicked()
{
    if (isMaximized()) showNormal();
    else showMaximized();
}

void MainWindow::on_btnClose_clicked()
{
    close();
}

void MainWindow::on_btnResizeable_clicked()
{
    setResizeable(!isResizeable());
}

mainwindows.ui

<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
 <class>MainWindow</class>
 <widget class="QMainWindow" name="MainWindow">
  <property name="geometry">
   <rect>
    <x>0</x>
    <y>0</y>
    <width>554</width>
    <height>300</height>
   </rect>
  </property>
  <property name="windowTitle">
   <string>MainWindow</string>
  </property>
  <widget class="QWidget" name="centralWidget">
   <layout class="QVBoxLayout" name="verticalLayout">
    <property name="leftMargin">
     <number>0</number>
    </property>
    <property name="topMargin">
     <number>0</number>
    </property>
    <property name="rightMargin">
     <number>0</number>
    </property>
    <property name="bottomMargin">
     <number>0</number>
    </property>
    <item>
     <widget class="QWidget" name="widgetTitlebar" native="true">
      <property name="styleSheet">
       <string notr="true">#widgetTitlebar{
background-color: rgb(200, 200,200);
}</string>
      </property>
      <layout class="QHBoxLayout" name="horizontalLayout_3">
       <property name="spacing">
        <number>0</number>
       </property>
       <property name="leftMargin">
        <number>0</number>
       </property>
       <property name="topMargin">
        <number>0</number>
       </property>
       <property name="rightMargin">
        <number>0</number>
       </property>
       <property name="bottomMargin">
        <number>0</number>
       </property>
       <item>
        <layout class="QHBoxLayout" name="horizontalLayout">
         <item>
          <widget class="QLabel" name="labelTitleText">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Preferred" vsizetype="Preferred">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="styleSheet">
            <string notr="true">background-color: rgb(164, 220, 129);</string>
           </property>
           <property name="text">
            <string>Title Text</string>
           </property>
          </widget>
         </item>
         <item>
          <spacer name="horizontalSpacer">
           <property name="orientation">
            <enum>Qt::Horizontal</enum>
           </property>
           <property name="sizeType">
            <enum>QSizePolicy::MinimumExpanding</enum>
           </property>
           <property name="sizeHint" stdset="0">
            <size>
             <width>100</width>
             <height>20</height>
            </size>
           </property>
          </spacer>
         </item>
         <item>
          <widget class="QPushButton" name="btnMin">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="minimumSize">
            <size>
             <width>0</width>
             <height>0</height>
            </size>
           </property>
           <property name="maximumSize">
            <size>
             <width>16777215</width>
             <height>16777215</height>
            </size>
           </property>
           <property name="text">
            <string>Min</string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QPushButton" name="btnMax">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="maximumSize">
            <size>
             <width>16777215</width>
             <height>16777215</height>
            </size>
           </property>
           <property name="text">
            <string>Max</string>
           </property>
          </widget>
         </item>
         <item>
          <widget class="QPushButton" name="btnClose">
           <property name="sizePolicy">
            <sizepolicy hsizetype="Preferred" vsizetype="Fixed">
             <horstretch>0</horstretch>
             <verstretch>0</verstretch>
            </sizepolicy>
           </property>
           <property name="maximumSize">
            <size>
             <width>16777215</width>
             <height>16777215</height>
            </size>
           </property>
           <property name="text">
            <string>Close</string>
           </property>
          </widget>
         </item>
        </layout>
       </item>
      </layout>
     </widget>
    </item>
    <item>
     <spacer name="verticalSpacer_2">
      <property name="orientation">
       <enum>Qt::Vertical</enum>
      </property>
      <property name="sizeHint" stdset="0">
       <size>
        <width>20</width>
        <height>40</height>
       </size>
      </property>
     </spacer>
    </item>
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout_5">
      <item alignment="Qt::AlignHCenter">
       <widget class="QPushButton" name="btnResizeable">
        <property name="text">
         <string>Toggle Resizeable</string>
        </property>
       </widget>
      </item>
     </layout>
    </item>
    <item>
     <spacer name="verticalSpacer">
      <property name="orientation">
       <enum>Qt::Vertical</enum>
      </property>
      <property name="sizeType">
       <enum>QSizePolicy::Expanding</enum>
      </property>
      <property name="sizeHint" stdset="0">
       <size>
        <width>20</width>
        <height>40</height>
       </size>
      </property>
     </spacer>
    </item>
    <item>
     <layout class="QHBoxLayout" name="horizontalLayout_2">
      <item>
       <widget class="QPushButton" name="pushButton_2">
        <property name="text">
         <string>I Do Nothing</string>
        </property>
       </widget>
      </item>
      <item>
       <spacer name="horizontalSpacer_2">
        <property name="orientation">
         <enum>Qt::Horizontal</enum>
        </property>
        <property name="sizeHint" stdset="0">
         <size>
          <width>40</width>
          <height>20</height>
         </size>
        </property>
       </spacer>
      </item>
      <item>
       <widget class="QPushButton" name="pushButton">
        <property name="text">
         <string>I Do Nothing Too</string>
        </property>
       </widget>
      </item>
     </layout>
    </item>
   </layout>
  </widget>
  <action name="action_MenuItem1">
   <property name="text">
    <string>MenuItem1</string>
   </property>
  </action>
  <action name="actionMenuItem2">
   <property name="text">
    <string>MenuItem2</string>
   </property>
  </action>
 </widget>
 <layoutdefault spacing="6" margin="11"/>
 <resources/>
 <connections/>
</ui>

.pro

QT       += core gui

greaterThan(QT_MAJOR_VERSION, 4): QT += widgets

TARGET = FramelessWindow
TEMPLATE = app


SOURCES += main.cpp\
        mainwindow.cpp
HEADERS  += mainwindow.h\
        NativeWindowTemplate.hpp
FORMS    += mainwindow.ui

win32{
        SOURCES +=
}

LIBS += -ldwmapi
LIBS += -lUser32

CONFIG(debug, debug|release) {
message("debug mode")
}else {
message("release mode")
}

示例分享

链接:https://pan.baidu.com/s/15CKF1WcGXgkWSUO6QAJ5nQ?pwd=20ut
提取码:20ut

  • 10
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值