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小白。