1. 程式人生 > >Qt 無邊框無非客戶區視窗拖動、縮放改變大小、滑鼠變形,windows同款,完美實現

Qt 無邊框無非客戶區視窗拖動、縮放改變大小、滑鼠變形,windows同款,完美實現

簡單點說,是在windows下實現視窗拖動和改變大小,一種方法是過載mousePressEvent系列全家福函式,然後在mouseMoveEvent中實現邊移滑鼠視窗就一邊重繪。這種方法網上資料很多,見下:

這種方法缺點是移動的時候視窗閃爍厲害。摒棄。但該方法給了一個很好的思路。

另一種方法:

看到MFC中有呼叫NCHitTest方法的,這個方法是當滑鼠觸碰到非客戶區時觸發,然後呼叫windows api,實現視窗移動,參照下:

在這篇帖子的最下方,有MFC改變視窗的方法。

於是我也想到了用Qt調windows api,反正我也不想做跨平臺的軟體,咳咳。然後發現了Qt有nativeEvent函式,捕獲本地所有事件,過載之,專門處理滑鼠移動訊號。ok,實現了,但絕不完美!為何,繼續看。

過載nativeEvent的方法網上查查,有第三個引數Long* result,result=HTLEFT時,滑鼠就會變成橫向雙箭頭圖案,其他就參照MFC實現吧,後面會給出程式碼。

然後我們來看哪裡不完美。Qt沒有非客戶區的概念,但類之間的訊號傳遞很明顯,當頂層視窗的最外層佈局這樣設定:

m_layoutMain->setMargin(0);

那麼滑鼠移到邊緣時,滑鼠訊號的捕獲是被最上層的子控制元件捕獲了,實際上就觸發不了頂層視窗的nativeEvent了。退而求其次,以為把邊界設成5就可以了麼?是的,不美觀點,設成5確實就可以了,但如果想觸發拖動事件的範圍也是5,就會發現,滑鼠在邊界的5範圍內,是橫向雙箭頭圖案,繼續往裡移,OK,進入子控制元件了,滑鼠移動事件不觸發了,滑鼠一直是橫向雙箭頭圖案,何況,5也是接受不了的,我要0,不能忍,繼續想辦法。

然後就想到了過濾器,接著發現了Qt5.7的一個bug。過濾器,在頂層視窗實現nativeEventFilter方法,接著在qApp安裝過濾器。

過濾器其實一點都不復雜,選個類A,在類A中實現nativeEventFilter方法後,建個物件a,只要在其他類用installNativeEventFilter(&a)方法安裝過濾器,那個這個被安裝過濾器的類,接收到了事件後,就會優先發給a來處理,而在qApp中安裝過濾器,qApp是第一個接收到訊息的,會全部先丟給a處理,a吃剩的殘羹冷炙還給qApp,qApp再拿去分給其他改領盒飯的類。怎麼算吃剩呢,在過濾器中return false就是吃剩不要了。然後上程式碼了

這是Class A中的方法,判斷滑鼠是否在規定區域中,PADDING自己定一個,可以在四個角加大範圍,方便觸發,我這裡+2。

#include <windows.h>
#include <QAbstractNativeEventFilter>
#define PADDING 5

enum Direction{
	UP = 0,
	DOWN = 1,
	LEFT,
	RIGHT,
	LEFTTOP,
	LEFTBOTTOM,
	RIGHTBOTTOM,
	RIGHTTOP,
	NONE
};

class A: public QDialog,public QAbstractNativeEventFilter
{
	Q_OBJECT

public:
	A(QWidget *parent=0);
	~A();

private:
	void mousePressEvent(QMouseEvent* event);
	void mouseDoubleClickEvent(QMouseEvent *event);
	//bool nativeEvent(const QByteArray &eventType, void *message, long *result);
	void region(const QPoint &cursorPoint,bool &activeFlag);
	bool nativeEventFilter(const QByteArray &eventType, void *message, long *result);
}


void A::region(const QPoint &cursorGlobalPoint,bool &activeFlag)
{
	QRect rect = this->rect();
	QPoint tl = mapToGlobal(rect.topLeft());
	QPoint rb = mapToGlobal(rect.bottomRight());
	int x = cursorGlobalPoint.x();
	int y = cursorGlobalPoint.y();
	activeFlag = true;
	if (tl.x() + PADDING+2 >= x && tl.x() <= x && tl.y() + PADDING+2 >= y && tl.y() <= y) {
		// 左上角
		dir = LEFTTOP;
		this->setCursor(QCursor(Qt::SizeFDiagCursor));
	}
	else if (x >= rb.x() - PADDING-2 && x <= rb.x() && y >= rb.y() - PADDING-2 && y <= rb.y()) {
		// 右下角
		dir = RIGHTBOTTOM;
		this->setCursor(QCursor(Qt::SizeFDiagCursor));
	}
	else if (x <= tl.x() + PADDING+2 && x >= tl.x() && y >= rb.y() - PADDING-2 && y <= rb.y()) {
		//左下角
		dir = LEFTBOTTOM;
		this->setCursor(QCursor(Qt::SizeBDiagCursor));
	}
	else if (x <= rb.x() && x >= rb.x() - PADDING-2 && y >= tl.y() && y <= tl.y() + PADDING+2) {
		// 右上角
		dir = RIGHTTOP;
		this->setCursor(QCursor(Qt::SizeBDiagCursor));
	}
	else if (x <= tl.x() + PADDING && x >= tl.x()) {
		// 左邊
		dir = LEFT;
		this->setCursor(QCursor(Qt::SizeHorCursor));
	}
	else if (x <= rb.x() && x >= rb.x() - PADDING) {
		// 右邊
		dir = RIGHT;
		this->setCursor(QCursor(Qt::SizeHorCursor));
	}
	else if (y >= tl.y() && y <= tl.y() + PADDING){
		// 上邊
		dir = UP;
		this->setCursor(QCursor(Qt::SizeVerCursor));
	}
	else if (y <= rb.y() && y >= rb.y() - PADDING) {
		// 下邊
		dir = DOWN;
		this->setCursor(QCursor(Qt::SizeVerCursor));
	}
	else {
		// 預設
		dir = NONE;
		this->setCursor(QCursor(Qt::ArrowCursor));
		activeFlag = false;
	}
}

bool A::nativeEventFilter(const QByteArray &eventType, void *message, long *result)
{
	if (eventType == "windows_generic_MSG" || eventType == "windows_dispatcher_MSG")
	{
		MSG* msg = (MSG*)message;

		if (msg->message == WM_MOUSEMOVE)
		{
			QPoint pt = cursor().pos();
			bool activeFlag;
			region(pt, activeFlag);
			if (activeFlag)
			{
				switch (dir)
				{
				case UP:
				case DOWN:
					SetCursor(LoadCursor(NULL, MAKEINTRESOURCE(IDC_SIZENS)));
					break;
				case LEFT:
				case RIGHT:
					SetCursor(LoadCursor(NULL, MAKEINTRESOURCE(IDC_SIZEWE)));
					break;
				case LEFTTOP:
				case RIGHTBOTTOM:
					SetCursor(LoadCursor(NULL, MAKEINTRESOURCE(IDC_SIZENWSE)));
					break;
				case RIGHTTOP:
				case LEFTBOTTOM:
					SetCursor(LoadCursor(NULL, MAKEINTRESOURCE(IDC_SIZENESW)));
					break;
				case NONE:
				default:
					SetCursor(LoadCursor(NULL, MAKEINTRESOURCE(IDC_ARROW)));
					return false;
					break;
				}
				return true;
			}
		}

		if (msg->message == WM_LBUTTONDOWN)
		{
			QPoint pt = cursor().pos();
			bool activeFlag;
			region(pt, activeFlag);
			if (activeFlag)
			{
				if (ReleaseCapture())
				{
					switch (dir)
					{
					case UP:
						SendMessage((HWND)this->winId(), WM_SYSCOMMAND, SC_SIZE | WMSZ_TOP, 0);
						break;
					case DOWN:
						SendMessage((HWND)this->winId(), WM_SYSCOMMAND, SC_SIZE | WMSZ_BOTTOM, 0);
						break;
					case LEFT:
						SendMessage((HWND)this->winId(), WM_SYSCOMMAND, SC_SIZE | WMSZ_LEFT, 0);
						break;
					case RIGHT:
						SendMessage((HWND)this->winId(), WM_SYSCOMMAND, SC_SIZE | WMSZ_RIGHT, 0);
						break;
					case LEFTTOP:
						SendMessage((HWND)this->winId(), WM_SYSCOMMAND, SC_SIZE | WMSZ_TOPLEFT, 0);
						break;
					case RIGHTTOP:
						SendMessage((HWND)this->winId(), WM_SYSCOMMAND, SC_SIZE | WMSZ_TOPRIGHT, 0);
						break;
					case LEFTBOTTOM:
						SendMessage((HWND)this->winId(), WM_SYSCOMMAND, SC_SIZE | WMSZ_BOTTOMLEFT, 0);
						break;
					case RIGHTBOTTOM:
						SendMessage((HWND)this->winId(), WM_SYSCOMMAND, SC_SIZE | WMSZ_BOTTOMRIGHT, 0);
						break;
					case NONE:return false;
						break;
					default:
						break;
					}
					return true;
				}

			}
		}
	}

	return false;
}

ReleaseCapture是幹嘛的?windows 的api,官方說是可以捕捉到滑鼠的一舉一動,呵呵,我也覺得怎麼就實現了呢?但真的實現了啊。

然後接下來安裝過濾器:

int main(int argc, char *argv[])
{
	QApplication a(argc, argv);
	A w;
	w.show();
	a.installNativeEventFilter(&w);
	return a.exec();
}

好了,這樣,不管頂層視窗w裡面有什麼控制元件,都先給w處理了,不在判定區域內,就在nativeEventFilter中丟出false,給子視窗處理,拖動效果和windows是一樣的,先拖出一個虛線框,滑鼠鬆開,重繪視窗,到此完美實現!沒有連續拖動時候的閃爍問題。

接下來說說bug。

參照本帖中第二個參考貼中的例子,最開始設定滑鼠圖案的時候,並沒有用

SetCursor(LoadCursor(NULL, MAKEINTRESOURCE(IDC_SIZEWE)));

而是採用了 *result=HTLEFT這樣,用HTLEFT後,就不需要ReleaseCapture,而可以直接調SendMessage,改一下最後一個引數,改成這樣,就是最後一個引數直接傳入座標,和第二貼中一樣。

SendMessage( WM_SYSCOMMAND, SC_SIZE | WMSZ_TOP, MAKELPARAM(point.x, point.y));

然後發現,qApp中傳過來的result,居然為空,氣炸,於是換成了SetCursor,測試通過。

順帶,還有按住標題移動,見程式碼

void A::mousePressEvent(QMouseEvent* event)
{
	QPoint ptMouse = event->globalPos();
	m_pointRelative = ptMouse - mapToGlobal(QPoint(0, 0));
	//點住標題則拖拽
	if (m_pointRelative.ry() < 150 )
	{
		if (ReleaseCapture())
			SendMessage((HWND)this->winId(), WM_SYSCOMMAND, SC_MOVE + HTCAPTION, 0);
	}

	QDialog::mousePressEvent(event);
}
void A::mouseDoubleClickEvent(QMouseEvent *event)
{
        QPoint ptMouse = event->globalPos();
	m_pointRelative = ptMouse - mapToGlobal(QPoint(0, 0));
	//雙擊最大化
	if (m_pointRelative.ry() < 150)
	{
		if (this->windowState() == Qt::MaximumSize)
		{
			showNormal();
		}
		else
		{
			showMaximized();
		}
	}

	QDialog::mouseDoubleClickEvent(event);

}
這裡也用了windows 的api,先拖出一個虛線框。心滿意足,繼續幹活。

就是虛線框有點細,如果有人知道怎麼改變虛線框粗細,請一定要告訴我!我是windows api小白。