QT無邊框窗體——攔截windows消息實現


無邊框其實就是去掉windows自帶的標題欄,去掉標題欄之后手動實現標題欄的功能:
1、左鍵按住標題欄移動窗體
2、雙擊標題欄切換最大化和normal狀態
3、貼靠窗口功能
4、四個邊和四個角resize窗體大小
5、窗體陰影

窗體的客戶區和非客戶區

用一個超級老的圖(來自msdn)來介紹客戶區和非客戶區,簡單來說就是,客戶區是下圖白色區域,除此之外都是非客戶區。

客戶區好說,qml寫的東西都是堆在客戶區的,非客戶區比較麻煩,但是win32提供了一些辦法來自定義非客戶區
https://docs.microsoft.com/en-us/windows/win32/dwm/customframe#related-topics
msdn上面的這個鏈接提供了非常詳細的介紹,簡單總結一下
1、客戶區和非客戶區的關系就類似於ps中的圖層,客戶區是在非客戶的上邊
2、我們可以通過win32的api設置非客戶區四個邊框的大小

這個圖左右兩個只有細微的差別,那就是看起來左邊的客戶區帶邊框,而右邊的沒有,實際上是有邊的非客戶區邊框被重新設置過,做了加粗處理,客戶區覆蓋了邊框(邊框是非客戶區的,客戶區沒有),如果把客戶區設置為透明就能看出其中玄機:

窗體的樣式

這個屬於非客戶的功能,窗體帶不帶標題欄、有沒有最大化那幾個按鈕等都是Windows系統的窗體樣式。
每個窗口都有一個或多個窗口樣式。 窗口樣式是一個命名常量(named constant),它定義了窗口類(並不是那個結構體的window class,而是指create函數創建的一個窗體)未指定的窗口外觀和行為。 應用程序通常在創建窗口時設置窗口樣式(create函數),也可以在創建window后使用SetWindowLong函數設置樣式。
窗體樣式的參考可以看這個鏈接: https://docs.microsoft.com/zh-cn/windows/win32/winmsg/window-styles
除此之外還有擴展樣式: https://docs.microsoft.com/zh-cn/windows/win32/winmsg/extended-window-styles
setwindowlong就是設置樣式的函數,比如去掉標題欄:

效果如下圖

雖然從代碼上我們是去掉了標題欄,但是很明顯,標題欄還有一個高度,測量了一下是3個像素,所以直接設置窗體樣式並不能實現我們的需求(需要別的操作,我看有人可以實現,我沒深究這個方案)

與窗體相關的幾個Windows的消息

WM_NCHITTEST
鼠標在窗體上的時候Windows發送該消息,通過“窗口過程”的返回值能讓Windows了解鼠標是在哪,比如返回HTCAPTION就是在標題欄中。關於窗口過程這里不細講。
假設我們攔截了這個消息,然后不管什么情況都返回HTCAPTION,那么Windows系統就認為你一直在標題欄上。用鼠標左鍵點按就可以移動窗體。
這個消息是resize和按住標題欄移動窗體的關鍵。
這個消息的詳細定義可以看這里 https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-nchittest
WM_NCCALCSIZE
當客戶區大小和坐標需要被重新計算的時候Windows會發送這個消息,默認os會自動處理該消息,但是如果我們攔截該消息並返回一些特定的值比如數字0x00,那么就能更改客戶區的大小和坐標。
實際上返回0x00就是讓客戶區完全覆蓋窗體,效果可以看下圖

紅框就是客戶區(客戶區做了透明處理),右邊的圖完全覆蓋了窗體(包括非客戶區)。其實這就是創造無邊框窗體的核心操作
這個消息的詳細定義可以看這里 https://docs.microsoft.com/en-us/windows/win32/winmsg/wm-nccalcsize
WM_ACTIVATE
窗口被激活的時候os會發送這條消息
https://docs.microsoft.com/en-us/windows/win32/inputdev/wm-activate

qt中的消息過濾器

win32程序中可以在WndProc函數中處理消息,這非常方便,但是qt封裝了系統相關的東西做到了跨平台,要在qt中截獲windows的系統消息就沒有win32程序那么方便,但是qt仍開放了接口
使用 QAbstractNativeEventFilter類 , 參考 https://doc.qt.io/qt-5/qabstractnativeeventfilter.html
除了構造函數,就一個成員函數nativeEventFilter

所有的native消息都會經過這個函數,如果重寫該函數,抓取到某個消息后函數返回值為true,那么該消息就永遠不會進入qt系統(被阻斷了),如果返回false,那么這條消息就會正常進入qt系統被處理。
該函數的第三個參數就相當於win32中WndProc函數的返回值。比如在win32中處理WM_NCCALCSIZE消息的代碼

在qt的消息過濾器中上圖就等價於:

所以:我們繼承QAbstractNativeEventFilter類就可以對Windows系統消息做自定義處理,當然,我們寫的消息過濾器要在qt的main函數中注冊到qt系統,否則qt不會識別的:

去掉Windows系統的非客戶區

其實就是用客戶區完全覆蓋整個窗體,根據msdn消息說明
截取WM_NCCALCSIZE消息,並設置返回值為0
就這么簡單,就可以實現了。不過首先還是要寫一個類,繼承QAbstractNativeEventFilter類 ,代碼如下:

此時的窗口,用鼠標根本無法操作,也沒有陰影,因為非客戶區的所有可控因素都被客戶區蓋住了。

添加陰影

設置一下非客戶區邊框就可以了,利用WM_ACTIVATE消息

resize和移動窗體

利用WM_NCHITTEST消息
下邊代碼絕大多數是參考了 https://docs.microsoft.com/en-us/windows/win32/dwm/customframe#appendix-c-hittestnca-function msdn這個提供的代碼

case WM_NCHITTEST:
		{
			//處理resize
			//標記只處理resize
			bool isResize = false;

			//鼠標點擊的坐標
			POINT ptMouse = { GET_X_LPARAM(pMsg->lParam), GET_Y_LPARAM(pMsg->lParam) };
			//窗口矩形
			RECT rcWindow;
			GetWindowRect(pMsg->hwnd, &rcWindow);
			RECT rcFrame = { 0,0,0,0 };
			AdjustWindowRectEx(&rcFrame, WS_OVERLAPPEDWINDOW & ~WS_CAPTION, FALSE, NULL);
			USHORT uRow = 1;
			USHORT uCol = 1;
			bool fOnResizeBorder = false;

			//確認鼠標指針是否在top或者bottom
			if (ptMouse.y >= rcWindow.top && ptMouse.y < rcWindow.top + 1)
			{
				fOnResizeBorder = (ptMouse.y < (rcWindow.top - rcFrame.top));
				uRow = 0;
				isResize = true;
			}
			else if (ptMouse.y < rcWindow.bottom && ptMouse.y >= rcWindow.bottom - 5)
			{
				uRow = 2;
				isResize = true;
			}
			//確認鼠標指針是否在left或者right
			if (ptMouse.x >= rcWindow.left && ptMouse.x < rcWindow.left + 5)
			{
				uCol = 0; // left side
				isResize = true;
			}
			else if (ptMouse.x < rcWindow.right && ptMouse.x >= rcWindow.right - 5)
			{
				uCol = 2; // right side
				isResize = true;
			}
            if (ptMouse.x >= rcWindow.left && ptMouse.x <= rcWindow.right - 135 && ptMouse.y > rcWindow.top + 3 && ptMouse.y <= rcWindow.top + 30)
			{
                *result = HTCAPTION;
				return true;
			}

			LRESULT hitTests[3][3] =
			{
				{ HTTOPLEFT,    fOnResizeBorder ? HTTOP : HTCAPTION,    HTTOPRIGHT },
				{ HTLEFT,       HTNOWHERE,     HTRIGHT },
				{ HTBOTTOMLEFT, HTBOTTOM, HTBOTTOMRIGHT },
			};

			if (isResize == true)
			{
				*result = hitTests[uRow][uCol];
				return true;
			}
			else
			{
				return false;
			}

		}

關於移動窗體,這個可選的方案有很多,qt中可以使用mousearea實現,但是使用Windows消息更好用,因為mousearea實現的辦法沒有“貼邊停靠”,而且雙擊標題欄的操作也要手動實現。
關於貼邊停靠功能,我並沒有找到什么可查詢的資料准確定義怎么實現,我做了很多次嘗試,總結一下基本上是有兩個條件可以實現,首先是設置窗體樣式,具體是哪幾個樣式我沒細究,總之必須有某些樣式,才能實現貼邊停靠,第二個條件是鼠標按住標題欄貼近屏幕某個邊緣。這兩個條件缺一不可,這也是為什么移動窗體我是攔截系統消息而不是在qml中使用mousearea的原因。關於第一個條件“窗體樣式”在我的代碼中我猜測是在WM_ACTIVATE處理陰影的時候添加了合適的窗口樣式,這里沒有細究。

完整代碼

#ifndef WINMSGFILTER_H
#define WINMSGFILTER_H
#include <QAbstractNativeEventFilter>
#include <QDebug>
#include <Windows.h>
#pragma comment(lib, "dwmapi")
#pragma comment(lib,"user32.lib")
#include <dwmapi.h>
#include <windowsx.h>

class WinMsgFilter :public QAbstractNativeEventFilter
{
public:
	WinMsgFilter();
	//過濾掉消息返回true,否則返回false
	bool nativeEventFilter(const QByteArray& eventType, void* message, long* result) override
	{
		MSG* pMsg = reinterpret_cast<MSG*>(message);

		switch (pMsg->message)
		{
			//去掉邊框
		case WM_NCCALCSIZE:
		{
			*result = 0;
			return true;
			break;
		}
		//陰影
        case WM_ACTIVATE:
        {
            MARGINS margins = { 1,1,1,1 };
            HRESULT hr = S_OK;
            hr = DwmExtendFrameIntoClientArea(pMsg->hwnd, &margins);
            *result = hr;
            return true;
        }
		case WM_NCHITTEST:
		{
			//處理resize
			//標記只處理resize
			bool isResize = false;

			//鼠標點擊的坐標
			POINT ptMouse = { GET_X_LPARAM(pMsg->lParam), GET_Y_LPARAM(pMsg->lParam) };
			//窗口矩形
			RECT rcWindow;
			GetWindowRect(pMsg->hwnd, &rcWindow);
			RECT rcFrame = { 0,0,0,0 };
			AdjustWindowRectEx(&rcFrame, WS_OVERLAPPEDWINDOW & ~WS_CAPTION, FALSE, NULL);
			USHORT uRow = 1;
			USHORT uCol = 1;
			bool fOnResizeBorder = false;

			//確認鼠標指針是否在top或者bottom,順帶說一下屏幕坐標原點是左上角,窗體坐標原點也是左上角
			if (ptMouse.y >= rcWindow.top && ptMouse.y < rcWindow.top + 1)
			{
				fOnResizeBorder = (ptMouse.y < (rcWindow.top - rcFrame.top));
				uRow = 0;
				isResize = true;
			}
			else if (ptMouse.y < rcWindow.bottom && ptMouse.y >= rcWindow.bottom - 5)
			{
				uRow = 2;
				isResize = true;
			}
			//確認鼠標指針是否在left或者right
			if (ptMouse.x >= rcWindow.left && ptMouse.x < rcWindow.left + 5)
			{
				uCol = 0; // left side
				isResize = true;
			}
			else if (ptMouse.x < rcWindow.right && ptMouse.x >= rcWindow.right - 5)
			{
				uCol = 2; // right side
				isResize = true;
			}
            //檢測是不是在標題欄上,右邊預留出了45*3 = 135的寬度,是留給關閉按鈕、最大化、最小化的。
            if (ptMouse.x >= rcWindow.left && ptMouse.x <= rcWindow.right - 135 && ptMouse.y > rcWindow.top + 3 && ptMouse.y <= rcWindow.top + 30)
			{
                *result = HTCAPTION;
				return true;
			}

			LRESULT hitTests[3][3] =
			{
				{ HTTOPLEFT,    fOnResizeBorder ? HTTOP : HTCAPTION,    HTTOPRIGHT },
				{ HTLEFT,       HTNOWHERE,     HTRIGHT },
				{ HTBOTTOMLEFT, HTBOTTOM, HTBOTTOMRIGHT },
			};

			if (isResize == true)
			{
				*result = hitTests[uRow][uCol];
				return true;
			}
			else
			{
				return false;
			}

		}
		}

		//這里一定要返回false,否則是屏蔽所有消息了
		return false;
	}

};

#endif // WINMSGFILTER_H

不要忘了在main函數中注冊消息攔截的類

demo演示

demo代碼
https://gitee.com/feipeng8848/frameless-window-qml/tree/master

瑕疵

這並不是完美的解決方案,切換多顯示器的時候會有下圖這種邊框出現(為了方便觀察,我把window的顏色做成了紅色)

在別人實現的無邊框方案中,也出現了這種問題,目前還沒解決 https://github.com/qtdevs/FramelessHelper/issues/10
測試過程中發現Window的height和width發生了改變(top和right也有變化),而qt中的window的height和width實際是“客戶區”的size
當設置qml中window的寬和高分別是300*400,啟動軟件,實際軟件寬高變成了316*439,被拉長了。
切換到另一顯示器的時候,這個數字會被重置為300*400.也就是說在從一個屏幕移動到另一個屏幕的時候客戶區發生了變化。

但是,重點來了,我們設置window的顏色是紅色,卻是全部覆蓋。window的height、width、top、right等屬性發生了改變。猜測這是qt的bug。

fix

收到了上邊提到的項目(https://github.com/qtdevs/FramelessHelper/issues/10)作者回復,內容如下:

我嘗試在nativeEventFilter函數中處理WM_MOVE消息,執行如下代碼
SetWindowPos(msg->hwnd, nullptr, 0, 0, 0, 0, SWP_FRAMECHANGED | SWP_NOACTIVATE | SWP_NOSIZE | SWP_NOMOVE | SWP_NOZORDER | SWP_NOOWNERZORDER); RedrawWindow(msg->hwnd, nullptr, nullptr, RDW_INVALIDATE | RDW_UPDATENOW | RDW_NOCHILDREN);
即可解決多個顯示器之間切換問題,但是程序還是會有其他問題。
這個方法我在VS調試診斷中看到,使用這個方法CPU性能前后都是在25左右。
感覺是歪門邪道,但是目前確實解決了我的問題,大家可以參考一下。
確實解決了,還可以精簡下只設置SetWindowPos就行:
RECT rcClient;
GetWindowRect(msg->hwnd, &rcClient);
SetWindowPos(msg->hwnd, NULL, rcClient.left, rcClient.top, rcClient.right - rcClient.left, rcClient.bottom - rcClient.top, SWP_FRAMECHANGED);


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM