Qt跨平臺窗口選擇功能的實現過程
1、概述
- Qt版本:V5.12.5
- 兼容系統:
- Windows:這裡測試瞭Windows10,其它的版本沒有測試;
- Linux:這裡測試瞭ubuntu18.04、20.04,其它的沒有測試(ubuntu自帶的截圖軟件沒有這個功能);
- Mac:等啥時候我有瞭Mac電腦再說。
- 我們在使用截圖軟件、錄屏軟件時常常有一個選項,就是窗口截圖,當我們鼠標移動到窗口上時,程序會自動識別到鼠標位置的窗口,獲取窗口的大小、位置,這是怎麼實現的呢;
- 這裡就研究瞭一下,如果使用Qt的鼠標事件、事件過濾器,一般鼠標出瞭窗口范圍就不會觸發鼠標事件瞭,想要獲取全局鼠標事件隻能使用系統API,Windows下就是使用user32、ubuntu下就是X11;
- 在這個示例中實現瞭自動獲取鼠標所在坐標窗口的位置、大小信息,並使用一個矩形窗口框選、覆蓋住所在窗口;
- 在windows實現的窗口選擇功能可精確識別系統任務欄、程序圖標、文件資源管理器的各個窗口部件等(很多截屏軟件都沒這個功能)。
2、實現效果
Windows下實現效果
Linux下實現效果
3、實現原理
Windows
- 使用定時器每隔200ms獲取一次當前鼠標位置;
- 調用系統user32 API通過鼠標位置查詢當前位置的窗口句柄;
- 通過獲取的窗口句柄獲取窗口的位置、大小;
- 將當前窗口覆蓋到鼠標所在窗口上方(註意:當前窗口需要設置鼠標穿透,否則第2部獲取到的就是當前窗口的句柄);
Linux
- 使用定時器每隔200ms獲取一次當前鼠標位置;
- 調用系統x11 API獲取當前屏幕的所有窗口;
- 通過獲取的窗口句柄獲取窗口的位置、大小;
- 通過當前窗口的句柄過濾掉獲取的所有窗口句柄中的當前窗口(否則當前窗口因為是在最上層,每次獲取的都是當前窗口的大小);
- 遍歷所有窗口的位置、大小,判斷包含鼠標位置的窗口,並記錄位置、大小信息,由於遍歷是從最底層窗口到最頂層窗口,所以需要保存最後一個窗口的位置、大小信息;
- 將當前窗口覆蓋到鼠標所在窗口上方(註意:linux下不能設置鼠標穿透,否則窗口出現顯示不全的問題);
4、關鍵代碼
由於使用到瞭系統API,所以pro文件中需要鏈接系統庫
win32 { LIBS+= -luser32 # 使用WindowsAPI需要鏈接庫 } unix:!macx{ LIBS += -lX11 # linux獲取窗口信息需要用到xlib }
widget.h
#ifndef WIDGET_H #define WIDGET_H #include <QWidget> #include <QTimer> class Widget : public QWidget { Q_OBJECT public: Widget(QWidget *parent = nullptr); ~Widget(); protected: void on_timeout(); private: QTimer m_timer; }; #endif // WIDGET_H
NtpClient.cpp
#include "widget.h" #include <QDebug> #include <qgridlayout.h> #if defined(Q_OS_WIN) #include <Windows.h> #include <windef.h> #elif defined(Q_OS_LINUX) #include <X11/Xlib.h> #include <X11/Xatom.h> #elif defined(Q_OS_MAC) #endif #if defined(Q_OS_WIN) static HHOOK g_hook = nullptr; /** * @brief 處理鼠標事件的回調函數 * @param nCode * @param wParam * @param lParam * @return */ LRESULT CALLBACK CallBackProc(int nCode, WPARAM wParam, LPARAM lParam) { switch (wParam) { case WM_LBUTTONDOWN: // 鼠標左鍵按下 { POINT pos; bool ret = GetCursorPos(&pos); if(ret) { qDebug() << pos.x <<" " << pos.y; } qDebug() << "鼠標左鍵按下"; break; } default: break; } return CallNextHookEx(nullptr, nCode, wParam, lParam); // 註意這一行一定不能少,否則會出大問題 } #endif Widget::Widget(QWidget *parent) : QWidget(parent) { this->setWindowTitle(QString("Qt-框選鼠標當前位置窗口范圍 - V%1").arg(APP_VERSION)); #if defined(Q_OS_WIN) // linux下鼠標穿透要放在後面兩行代碼的全前面,否則無效(但是鼠標穿透瞭會導致一些奇怪的問題,如窗口顯示不全,所以這裡不使用) // windows下如果不設置鼠標穿透則隻能捕獲到當前窗口 this->setAttribute(Qt::WA_TransparentForMouseEvents, true); #endif this->setWindowFlags(Qt::FramelessWindowHint); // 去掉邊框、標題欄 this->setAttribute(Qt::WA_TranslucentBackground); // 背景透明 this->setWindowFlags(this->windowFlags() | Qt::WindowStaysOnTopHint); // 設置頂級窗口,防止遮擋 #if defined(Q_OS_WIN) // 由於windows不透明的窗體如果不設置設置鼠標穿透WindowFromPoint隻能捕捉到當前窗體,而設置鼠標穿透後想要獲取鼠標事件隻能通過鼠標鉤子 g_hook = SetWindowsHookExW(WH_MOUSE_LL, CallBackProc, GetModuleHandleW(nullptr), 0); // 掛載全局鼠標鉤子 if (g_hook) { qDebug() << "鼠標鉤子掛接成功,線程ID:" << GetCurrentThreadId(); } else { qDebug() << "鼠標鉤子掛接失敗:" << GetLastError(); } #endif // 在當前窗口上增加一層QWidget,否則不會顯示邊框 QGridLayout* gridLayout = new QGridLayout(this); gridLayout->setSpacing(0); gridLayout->setContentsMargins(0, 0, 0, 0); gridLayout->addWidget(new QWidget(), 0, 0, 1, 1); this->setStyleSheet(" background-color: rgba(58, 196, 255, 40); border: 2px solid rgba(58, 196, 255, 200);"); // 設置窗口邊框樣式 dashed虛線,solid 實線 // 使用定時器定時獲取當前鼠標位置的窗口位置信息 connect(&m_timer, &QTimer::timeout, this, &Widget::on_timeout); m_timer.start(200); } Widget::~Widget() { #if defined(Q_OS_WIN) if(g_hook) { bool ret = UnhookWindowsHookEx(g_hook); if(ret) { qDebug() << "卸載鼠標鉤子。"; } } #endif } void Widget::on_timeout() { QPoint point = QCursor::pos(); // 獲取鼠標當前位置 #if defined(Q_OS_WIN) POINT pos; pos.x = point.x(); pos.y = point.y(); HWND hwnd = nullptr; hwnd = WindowFromPoint(pos); // 通過鼠標位置獲取窗口句柄 if(!hwnd) return; RECT lrect; bool ret = GetWindowRect(hwnd, &lrect); //獲取窗口位置 if(!ret) return; QRect rect; rect.setX(lrect.left); rect.setY(lrect.top); rect.setWidth(lrect.right - lrect.left); rect.setHeight(lrect.bottom - lrect.top); this->setGeometry(rect); // 設置窗口邊框 #elif defined(Q_OS_LINUX) // linux下使用x11獲取的窗口大小有可能不太準確,例如瀏覽器的大小會偏小 // 獲取根窗口 Display* display = XOpenDisplay(nullptr); Window rootWindow = DefaultRootWindow(display); Window root_return, parent_return; Window * children = nullptr; unsigned int nchildren = 0; // 函數詳細說明見xlib文檔:https://tronche.com/gui/x/xlib/window-information/XQueryTree.html // 該函數會返回父窗口的子窗口列表children,因為這裡用的是當前桌面的根窗口作為父窗口,所以會返回所有子窗口 // 註意:窗口順序(z-order)為自底向上 XQueryTree(display, rootWindow, &root_return, &parent_return, &children, &nchildren); QRect recte; // 保存鼠標當前所在窗口的范圍 for(unsigned int i = 0; i < nchildren; ++i) { if(children[i] == this->winId()) continue; // 由於當前窗口一直在最頂層,所以這裡要過濾掉當前窗口,否則一直獲取到的就是當前窗口大小 XWindowAttributes attrs; XGetWindowAttributes(display, children[i], &attrs); // 獲取窗口參數 if (attrs.map_state == IsViewable) // 隻處理可見的窗口, 三個狀態:IsUnmapped, IsUnviewable, IsViewable { #if 0 QRect rect(attrs.x + 1, attrs.y, attrs.width, attrs.height); // 這裡x+1防止全屏顯示,如果不+1,設置的大小等於屏幕大小是會自動切換成全屏顯示狀態,後面就無法縮小瞭 #else QRect rect(attrs.x, attrs.y, attrs.width, attrs.height); #endif if(rect.contains(point)) // 判斷鼠標坐標是否在窗口范圍內 { recte = rect; // 記錄最後一個窗口的范圍 } } } #if 0 // 在linux下使用setGeometry設置窗口會有一些問題 this->showNormal(); // 第一次顯示是如果是屏幕大小,則後面無法縮小,這裡需要設置還原 this->setGeometry(recte); // 設置窗口邊框 #else // 使用setFixedSize+move可以避免這些問題 this->move(recte.x(), recte.y()); this->setFixedSize(recte.width(), recte.height()); #endif // qDebug() << this->rect() <<recte<< this->windowState(); // 註意釋放資源 XFree(children); XCloseDisplay(display); #elif defined(Q_OS_MAC) #endif }
5、源代碼
- gitee
- github
- 當前示例所在模塊
總結
到此這篇關於Qt跨平臺窗口選擇功能實現的文章就介紹到這瞭,更多相關Qt跨平臺窗口選擇功能內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!