运行效果
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