qt的无边框窗口支持拖拽、Aero Snap、窗口阴影等特性

更新(2025-11-26)

更新代码使用QWidget时,简化了代码,相比于原文,删减掉了事件过滤器。

同时标题栏不再需要单独实现,直接绘制在QWidget中即可。

QMainWindow理论也可以,笔者未测试。

以下为普通QWidget的修改内容。

补充知识 Windows Hit-Test 常量(WM_NCHITTEST)

这些宏用于 Windows 消息 WM_NCHITTEST,表示鼠标在窗口上的不同区域。系统和程序可根据这些值处理拖动、调整大小、按钮点击等行为。

含义
HTERROR -2 错误或无效区域
HTTRANSPARENT -1 鼠标事件穿透,底层窗口处理
HTNOWHERE 0 鼠标不在窗口可用区域(窗口外或空白)
HTCLIENT 1 客户区(窗口内容区)
HTCAPTION 2 标题栏(可拖动窗口)
HTSYSMENU 3 系统菜单按钮(左上角小图标,可右键调菜单)
HTGROWBOX 4 窗口右下角/边缘可拖动调整大小的区域(Resize Box)
HTSIZE 4 HTGROWBOX,兼容历史代码
HTSIZEFIRST 10 左边界开始的调整大小序号
HTSIZELAST 17 右下角结束的调整大小序号
HTMENU 5 菜单栏
HTHSCROLL 6 水平滚动条
HTVSCROLL 7 垂直滚动条
HTMINBUTTON 8 最小化按钮
HTMAXBUTTON 9 最大化按钮
HTREDUCE 8 HTMINBUTTON(兼容)
HTZOOM 9 HTMAXBUTTON(兼容)
HTLEFT 10 左边框
HTRIGHT 11 右边框
HTTOP 12 上边框
HTTOPLEFT 13 左上角
HTTOPRIGHT 14 右上角
HTBOTTOM 15 下边框
HTBOTTOMLEFT 16 左下角
HTBOTTOMRIGHT 17 右下角
HTBORDER 18 普通边框(历史保留)
HTOBJECT 19 客户区内的 OLE 对象或控件(少用)
HTCLOSE 20 关闭按钮
HTHELP 21 帮助按钮

修改头文件

  • 删除 mousePressEvent、mouseReleaseEvent、mouseMoveEvent 的拖动内容
  • 重载 bool nativeEvent(const QByteArray& eventType, void* message, long* result) override;函数

qt6 为 nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result)

  • 增加 void initWindow()const int BORDER_WIDTH = 8;

代码总结:

class MyWidget: public QWidget
{
	Q_OBJECT
protected:
	bool nativeEvent(const QByteArray& eventType, void* message, long* result) override;

private:
	void initWindow();
	const int BORDER_WIDTH = 8;      // 用于调整大小的边缘宽度(像素)
};

修改源文件

  • 构造函数调用initWindow()
  • hitTestWindows函数用来判断当前鼠标位置下,需要做什么操作。以下是函数内容分析,也是读者可能会需要修改的内容:
    • GetWindowRect获取窗口在显示屏的区域,传参x,y是鼠标在显示屏的位置,计算得到localX\localY是鼠标相对于窗口左上角的位置
    • 获取你自己定义的TitleBargeometry()区域范围,也就是TitleBar相对于窗口的位置
    • 判断TitleBar左上角的相对窗口位置pos1是否在(0,0)是为了判断当前是否是全屏状态
    • WINDOWS窗口在全屏状态下,四周会有8个像素的拓展宽度,非全屏下,pos1是(0,0);全屏下,一般是(8,8)
    • 因为全屏下,不需要进行顶部边缘的拖动拉伸窗口,所以就不需要把TitleBar的左上右三个边缘区域排除掉;
      否则非全屏下,进行缩减rect范围,便于标题栏四周进行窗口的整体大小拖动修改
    • 满足titleRect.contains(localX, localY)时,意味着鼠标在TitleBar区域内
    • 需要排除的区域,返回HTCLIENT客户区, 其他返回HTCAPTION标题区(这里笔者排除了按钮部分)
    • 剩余部分是判断窗口边缘的拖动行为

实现部分:

#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <dwmapi.h>
#include <windows.h>
#include <uxtheme.h>
#pragma comment(lib, "Dwmapi.lib")
#endif
void MyWidget::initWindow()
{
	setWindowFlags(Qt::Window | Qt::FramelessWindowHint);

#ifdef _WIN32
	HWND hwnd = reinterpret_cast<HWND>(this->winId());

	const LONG style = (WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_THICKFRAME | WS_CLIPCHILDREN);
	SetWindowLongPtr(hwnd, GWL_STYLE, style);

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

	SetWindowPos(hwnd, 0, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE);
#endif
}
#ifdef _WIN32
static LRESULT hitTestWindows(QWidget* widget, HWND hwnd, int x, int y, int borderWidth)
{
	// x,y are screen coords
	RECT winRect;
	if (!GetWindowRect(hwnd, &winRect)) return HTNOWHERE;

	int localX = x - winRect.left;
	int localY = y - winRect.top;

	QWidget* window = widget;
	QWidget* title = window->findChild<QWidget*>("wgt_TitleBar");

	if (title) {
		QRect titleRect = title->geometry();
		if (titleRect.left() == 0 && titleRect.top() == 0) {
			titleRect.adjust(borderWidth, borderWidth, -borderWidth, 0);
		}
		if (titleRect.contains(localX, localY)) {
			QList<QAbstractButton*> btns = title->findChildren<QAbstractButton*>();
			for (QAbstractButton* b : btns)
			{
				if (b->geometry().contains(localX, localY)) {
					return HTCLIENT;
				}
			}
			QLabel* icon = window->findChild<QLabel*>("icon_label");
			if (icon && icon->geometry().contains(localX, localY)){
				return HTSYSMENU;
			}
			return HTCAPTION;  // 只有标题栏区域返回标题栏
		}
	}

	int left = winRect.left;
	int top = winRect.top;
	int right = winRect.right;
	int bottom = winRect.bottom;

	// Determine if point is in border areas
	bool topArea = y >= top && y < top + borderWidth;
	bool bottomArea = y < bottom && y >= bottom - borderWidth;
	bool leftArea = x >= left && x < left + borderWidth;
	bool rightArea = x < right && x >= right - borderWidth;

	// Corners
	if (topArea && leftArea) return HTTOPLEFT;
	if (topArea && rightArea) return HTTOPRIGHT;
	if (bottomArea && leftArea) return HTBOTTOMLEFT;
	if (bottomArea && rightArea) return HTBOTTOMRIGHT;
	// Edges
	if (leftArea) return HTLEFT;
	if (rightArea) return HTRIGHT;
	if (topArea) return HTTOP;
	if (bottomArea) return HTBOTTOM;

	return HTCLIENT;
}
#endif

bool MyWidget::nativeEvent(const QByteArray& eventType, void* message, long* result)
{
#ifdef _WIN32
#define GET_X_LPARAM(lp) ((int)(short)LOWORD(lp))
#define GET_Y_LPARAM(lp) ((int)(short)HIWORD(lp))
	// Windows-specific native event handling
	if (eventType == "windows_generic_MSG" || eventType == "windows_dispatcher_MSG") {
		MSG* msg = static_cast<MSG*>(message);
		switch (msg->message) {
		case WM_NCCALCSIZE:
		{
			// this kills the window frame and title bar we added with WS_THICKFRAME and WS_CAPTION
			*result = 0;
			return true;
		}
		case WM_NCHITTEST: {
			// convert to screen coords
			POINT pt;
			pt.x = GET_X_LPARAM(msg->lParam);
			pt.y = GET_Y_LPARAM(msg->lParam);
			HWND hwnd = (HWND)winId();
			// get hit test result based on border width
			LRESULT h = hitTestWindows(this, hwnd, pt.x, pt.y, BORDER_WIDTH);
			*result = h;
			return true; // handled
		}
		// 若full时边框不合适,可以去除此case
		case WM_GETMINMAXINFO:
		{
			if (::IsZoomed(msg->hwnd)) {
				// 最大化时会超出屏幕,所以填充边框间距
				RECT frame = { 0, 0, 0, 0 };
				AdjustWindowRectEx(&frame, WS_OVERLAPPEDWINDOW, FALSE, 0);
				frame.left = abs(frame.left);
				frame.top = abs(frame.bottom);
				this->setContentsMargins(frame.left, frame.top, frame.right, frame.bottom);
			}
			else {
				this->setContentsMargins(0, 0, 0, 0);
			}

			*result = ::DefWindowProc(msg->hwnd, msg->message, msg->wParam, msg->lParam);
			return true;
		}
		default:
			break;
		}
	}
#endif
	return QWidget::nativeEvent(eventType, message, result);
}

原文

环境:Desktop Qt 6.7.2 MSVC2019 64bit

需要的库:dwmapi.libuser32.lib

需要头文件:<dwmapi.h><windowsx.h>

只显示重要代码

1、去除原边框、加上阴影、Aero Snap以及其他动画特效

(1)头文件

#include "Windows.h"
#include "uxtheme.h"
#include "dwmapi.h"
#include "titlebar.h"//自定义类

(2)去除标题、原边框

初始化,去除边框

void MainWindow::init()
{
    setWindowFlags(Qt::Window | Qt::FramelessWindowHint);

#ifdef Q_OS_WIN
    HWND hwnd = reinterpret_cast<HWND>(this->winId());

    const LONG style = ( WS_POPUP | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX | WS_MAXIMIZEBOX | WS_THICKFRAME | WS_CLIPCHILDREN );
    SetWindowLongPtr(hwnd, GWL_STYLE, style);

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

    SetWindowPos(hwnd, 0, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOMOVE | SWP_NOSIZE);
#endif
    // 标题拖动、双击事件
    MyTitleBar *title = new MyTitleBar(this);
    qobject_cast<QBoxLayout *>(ui->centralwidget->layout())->insertWidget(0, title);
}

同时在最大化时,增加了边界,可以自行删除
若使用qt5,重载 bool nativeEvent(const QByteArray &eventType, void *message, long *result);

bool MainWindow::nativeEvent(const QByteArray& eventType, void* message, qintptr* result)
{
    MSG* msg = (MSG*)message;
    switch (msg->message)
    {
    case WM_NCCALCSIZE:
    {
        // this kills the window frame and title bar we added with WS_THICKFRAME and WS_CAPTION
        *result = 0;
        return true;
    }
    // 若full时边框不合适,可以去除此case
    case WM_GETMINMAXINFO:
    {
        if (::IsZoomed(msg->hwnd)) {
            // 最大化时会超出屏幕,所以填充边框间距
            RECT frame = { 0, 0, 0, 0 };
            AdjustWindowRectEx(&frame, WS_OVERLAPPEDWINDOW, FALSE, 0);
            frame.left = abs(frame.left);
            frame.top = abs(frame.bottom);
            this->setContentsMargins(frame.left, frame.top, frame.right, frame.bottom);
        }
        else {
            this->setContentsMargins(0, 0, 0, 0);
        }

        *result = ::DefWindowProc(msg->hwnd, msg->message, msg->wParam, msg->lParam);
        return true;
    }
    break;
    default:
        return QMainWindow::nativeEvent(eventType, message, result);
    }
}

(3)支持手动修改窗口

需要实现一个QAbstractNativeEventFilter 类,内容如下:

头文件
#include <QAbstractNativeEventFilter>
#include <QWidget>
#include "Windows.h"
#define GET_X_LPARAM(lp) ((int)(short)LOWORD(lp))
#define GET_Y_LPARAM(lp) ((int)(short)HIWORD(lp))

class NativeEventFilter : public QAbstractNativeEventFilter
{
public:
    virtual bool nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result)Q_DECL_OVERRIDE;
};
cpp文件
bool NativeEventFilter::nativeEventFilter(const QByteArray &eventType, void *message, qintptr *result)

{
#ifdef Q_OS_WIN
    if (eventType != "windows_generic_MSG")
        return false;

    MSG* msg = static_cast<MSG*>(message);
    QWidget* widget = QWidget::find(reinterpret_cast<WId>(msg->hwnd));
    if (!widget)
        return false;

    switch (msg->message) {
    case WM_NCHITTEST: {
        const LONG borderWidth = 9;
        RECT winrect;
        GetWindowRect(msg->hwnd, &winrect);
        long x = GET_X_LPARAM(msg->lParam);
        long y = GET_Y_LPARAM(msg->lParam);

        // bottom left
        if (x >= winrect.left && x < winrect.left + borderWidth &&
            y < winrect.bottom && y >= winrect.bottom - borderWidth)
        {
            *result = HTBOTTOMLEFT;
            return true;
        }

        // bottom right
        if (x < winrect.right && x >= winrect.right - borderWidth &&
            y < winrect.bottom && y >= winrect.bottom - borderWidth)
        {
            *result = HTBOTTOMRIGHT;
            return true;
        }

        // top left
        if (x >= winrect.left && x < winrect.left + borderWidth &&
            y >= winrect.top && y < winrect.top + borderWidth)
        {
            *result = HTTOPLEFT;
            return true;
        }

        // top right
        if (x < winrect.right && x >= winrect.right - borderWidth &&
            y >= winrect.top && y < winrect.top + borderWidth)
        {
            *result = HTTOPRIGHT;
            return true;
        }

        // left
        if (x >= winrect.left && x < winrect.left + borderWidth)
        {
            *result = HTLEFT;
            return true;
        }

        // right
        if (x < winrect.right && x >= winrect.right - borderWidth)
        {
            *result = HTRIGHT;
            return true;
        }

        // bottom
        if (y < winrect.bottom && y >= winrect.bottom - borderWidth)
        {
            *result = HTBOTTOM;
            return true;
        }

        // top
        if (y >= winrect.top && y < winrect.top + borderWidth)
        {
            *result = HTTOP;
            return true;
        }

        return false;
    }
    default:
        break;
    }

    return false;
#else
    return false;
#endif
};

应用

然后在窗口创建之前,使用QApplication::installNativeEventFilter 方法把监听器注册给主程序。

int main(int argc, char *argv[])
{
    NativeEventFilter f;
    QApplication a(argc, argv);
    a.installNativeEventFilter(&f);//支持手动修改窗口大小
    MainWindow w;
    w.show();
    return a.exec();
}

2、自定义标题栏

  • 需要重载QWidget::mousePressEvent 方法
  • 保存 Window 句柄操作原窗口
class MyTitleBar : public QFrame  {
    Q_OBJECT
public:
    explicit MyTitleBar(QWidget *parent = nullptr);

protected:
    void mousePressEvent(QMouseEvent* ev);


private:
    QWidget *Window = nullptr; // 保存主窗口的指针

    QVBoxLayout *verticalLayout;
    QHBoxLayout *horizontalLayout;
    QSpacerItem *horizontalSpacer;
    QPushButton *pushButton_min;
    QPushButton *pushButton_normal;
    QPushButton *pushButton_max;
    QPushButton *pushButton_full;
    QSpacerItem *horizontalSpacer_6;
    QPushButton *pushButton_close;
};
创建ui

MyTitleBar::MyTitleBar(QWidget *parent): QFrame (parent), Window(parent)
{
    // 边缘贴合
    parent->setContentsMargins(0, 0, 0, 0);

    setObjectName("title");
    setMaximumHeight(50);
    this->setStyleSheet("#title{background-color: rgb(255, 255, 255);}");
    verticalLayout = new QVBoxLayout(this);
    verticalLayout->setObjectName("verticalLayout");
    verticalLayout->setContentsMargins(0, 0, 0, 0);
    horizontalLayout = new QHBoxLayout();
    horizontalLayout->setObjectName("horizontalLayout");
    horizontalSpacer = new QSpacerItem(40, 20, QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Minimum);

    horizontalLayout->addItem(horizontalSpacer);

    pushButton_min = new QPushButton(this);
    pushButton_min->setObjectName("pushButton_min");

    horizontalLayout->addWidget(pushButton_min);

    pushButton_normal = new QPushButton(this);
    pushButton_normal->setObjectName("pushButton_normal");

    horizontalLayout->addWidget(pushButton_normal);

    pushButton_max = new QPushButton(this);
    pushButton_max->setObjectName("pushButton_max");

    horizontalLayout->addWidget(pushButton_max);

    pushButton_full = new QPushButton(this);
    pushButton_full->setObjectName("pushButton_full");

    horizontalLayout->addWidget(pushButton_full);

    horizontalSpacer_6 = new QSpacerItem(40, 20, QSizePolicy::Policy::Expanding, QSizePolicy::Policy::Minimum);

    horizontalLayout->addItem(horizontalSpacer_6);

    pushButton_close = new QPushButton(this);
    pushButton_close->setObjectName("pushButton_close");

    horizontalLayout->addWidget(pushButton_close);

    horizontalLayout->setStretch(0, 1);

    verticalLayout->addLayout(horizontalLayout);

    pushButton_min->setText(QCoreApplication::translate("MainWindow", "MIN", nullptr));
    pushButton_normal->setText(QCoreApplication::translate("MainWindow", "NORMAL", nullptr));
    pushButton_max->setText(QCoreApplication::translate("MainWindow", "MAX", nullptr));
    pushButton_full->setText(QCoreApplication::translate("MainWindow", "FULL", nullptr));
    pushButton_close->setText(QCoreApplication::translate("MainWindow", "CLOSE", nullptr));
}
连接主窗口的变化

最大化和关闭按扭,正常调用QWidget::showMaximized()QWidget::close() 等Qt自带方法即可。

    connect(pushButton_min, &QPushButton::clicked, Window, &QWidget::showMinimized);
    connect(pushButton_close, &QPushButton::clicked, Window, &QWidget::close);
    connect(pushButton_normal, &QPushButton::clicked, Window, &QWidget::showNormal);
    connect(pushButton_full, &QPushButton::clicked, Window, &QWidget::showFullScreen);
    connect(pushButton_max, &QPushButton::clicked, Window, &QWidget::showMaximized);
重截QWidget::mousePressEvent 方法
#include "Windows.h"
void MyTitleBar::mousePressEvent(QMouseEvent* ev)
{
    QWidget::mousePressEvent(ev);

    if (Window == nullptr) return;
    if (!ev->isAccepted()) {
        if (ev->type() == QEvent::MouseButtonDblClick) {
            // Toggle maximize/restore on double-click
            if (Window->isMaximized()) {
                Window->showNormal(); // Restore
            }
            else {
                Window->showMaximized(); // Maximize
            }
            return; // Prevent further processing
        }
#ifdef Q_OS_WIN
        ReleaseCapture();
        SendMessage(reinterpret_cast<HWND>(Window->winId()), WM_SYSCOMMAND, SC_MOVE + HTCAPTION, 0);
#endif
    }
}
添加
    // 标题拖动、双击事件
    MyTitleBar *title = new MyTitleBar(this);
    qobject_cast<QBoxLayout *>(ui->centralwidget->layout())->insertWidget(0, title);

3、最终实现效果


参考链接:https://github.com/deimos1877/BorderlessWindow

posted @ 2024-10-29 02:28  Yzi321  阅读(981)  评论(0)    收藏  举报