如果某個派生自 QObject 的類重寫 eventFilter 方法,那它就成了事件過濾器(Event Filter),該方法的宣告如下:
virtual bool eventFilter(QObject *watched, QEvent *event);
watched 引數是監聽事件的物件,即事件的接收者;event 引數當然就是待處理的事件了,事件過濾器(也可以翻譯為“篩選器”)可在接收者之前攔截事件,處理完畢后還可以決定是否把事件轉發給接收者,如果不想轉發給事件接收者,就回傳 true;若還想讓事件繼續傳播就回傳 false,
這玩意兒最有益的用途就是:你的頂層視窗上有 K 個子級組件(正常情形是 QWidget 的子類),如果組件沒有定義你想用的信號,只能通過處理事件的途徑解決,可你又不想只為了處理一個事件就派生一個類(比如,QLabel組件在滑鼠懸浮時做點事情),就可以用上事件過濾器了,頂層視窗類重寫 eventFilter 方法,攔截發往子組件的事件(如mouseMove)直接處理,這樣能節省 N 百行代碼,
重寫了 eventFilter 方法的類就成了事件的過濾者,而呼叫 installEventFilter 方法安裝過濾器的類才是事件的原始接收者,就拿上文咱們舉的 QLabel 組件的例,假設頂層視窗的類名是 DuckWindow,那么,DuckWindow 重寫 eventFilter 方法,它就是事件的攔截者;而 QLabel 組件就是事件的原始接收者,所以,呼叫 installEventFilter 方法的是它,即 QLabel::installEventFilter( DuckWindow ),
不知道老周這樣說大伙伴們能否理解,就是負責過濾事件的物件重寫 eventFilter 方法;被別人過濾的物件才呼叫 installEventFilter 方法,
我們用示例說事,下面咱們要做的練習是這樣的:
我定義了一個類叫 MyWindow,繼承 QWidget 類,作為頂層視窗,然后在視窗里,我用一個 QHBoxLayout 布局,讓視窗內的子級組件水平排列,但每個子組件的顏色不同,常規做法是寫個自定義組件類,從建構式或通過成員函式傳一個 QColor 物件過去,然后重寫 paintEvent 方法繪圖,這種做法肯定沒問題的,但是!我要是不想寫自定義類呢,那就得考慮事件過濾器了,把 paintEvent 事件過濾,直接用某顏色給子組件畫個背景就行了,
頭檔案宣告 MyWindow 類,
#ifndef MYWIN #define MYWIN #include <QWidget> #include <QHBoxLayout> #include <QPainter> #include <QEvent> #include <QColor> #include <QRect> class MyWindow : public QWidget { Q_OBJECT public: MyWindow(QWidget* parent=nullptr); bool eventFilter(QObject *obj, QEvent *event) override; private: // 私有成員,畫痘痘用的 void paintSomething(QPainter *p, const QColor &color, const QRect &paintRect); // 布局 QHBoxLayout *layout; // 三個子級組件 QWidget *w1, *w2, *w3; }; #endif
這里提一下這個 eventFilter 方法,這廝宣告為 public 和 protected 都是可行的,老周這里就宣告為 public,與基類的宣告一致,
paintSomething 是私有方法,自定義用來畫東西的,有伙伴們會問:QPainter 的 paintDevice 不是可以獲取到繪圖設定(這里指視窗或組件)的大小的矩形區域嗎,為啥要從引數傳個 QRect?因為這個 rect 來自 QPaintEvent 物件的事件引數,它指的可不一定視窗/組件的整個矩形區域,如果是區域重繪,這個矩形可能就是其中一小部磁區域,所以,咱們用事件傳遞過來的矩形區域繪圖,
視窗布局用的是 QHBoxLayout,非常簡單的布局方式,子級組件在視窗上水平排列,
下面代碼實作建構式,初始化各個物件,
MyWindow::MyWindow(QWidget *parent) : QWidget(parent) { // 初始化 layout = new QHBoxLayout; this->setLayout(layout); w1 = new QWidget(this); w2 = new QWidget(this); w3 = new QWidget(this); layout->addWidget(w1); layout->addWidget(w2); layout->addWidget(w3); // 安裝事件過濾器 w1->installEventFilter(this); w2->installEventFilter(this); w3->installEventFilter(this); }
只有在被攔截的物件上呼叫 installEventFilter 方法系結過濾器后,事件過濾器才會生效,此處,由于 MyWindow 類重寫了 eventFilter 方法,所以過濾器就是 this,
下面是 eventFilter 方法的實作代碼,只過濾 paint 事件即可,其他傳給基類自己去玩,
bool MyWindow::eventFilter(QObject *obj, QEvent *event) { // 如果是paint事件 // 這里“與”判斷事件接收者是不是在那三個子組件中 // 防止有其他意外物件出現 // 不過這里不會發生,因為只有install了過濾器的物件才會被攔截事件 if(event->type() == QEvent::Paint && (obj==w1 || obj==w2 || obj==w3)) { QPaintEvent* pe = static_cast<QPaintEvent*>(event); QWidget* uiobj = static_cast<QWidget*>(obj); QPainter painter; // 注意這里,繪圖設備不是this了,而是接識訓圖事件的物件 // 由于它要求的型別是QPaintDevcie*,所以要進行型別轉換 // 轉換后的uiobj變數的型別是QWidget*,傳參沒問題 painter.begin(uiobj); if(w1 == uiobj) { // 紅色 paintSomething(&painter, QColor("red"), pe->rect()); } if(w2 == uiobj) { // 橙色 paintSomething(&painter, QColor("orange"), pe->rect()); } if(w3 == uiobj) { // 紫色 paintSomething(&painter, QColor("purple"), pe->rect()); } painter.end(); return true; } return QWidget::eventFilter(obj, event); }
攔截并處理了 paint 事件后,記得回傳 true,這樣事件就不會傳給目標物件了(咱們幫它處理了,不必再重復處理,畢竟 QWidget 類默認的 paint 事件是啥也不做),
下面代碼是 paintSomething 方法,只是畫了顆巨型青春痘……哦不,是一個橢圓,
void MyWindow::paintSomething(QPainter *p, const QColor &color, const QRect &paintRect) { // 設定畫刷 p->setBrush(QBrush(color)); // 無輪廓 p->setPen(Qt::NoPen); // 畫橢圓 p->drawEllipse(paintRect); }
setPen中設定 NoPen 是為了在繪制圓時去掉輪廓,默認會畫上輪廓線的,
最后,該到 main 函式了,
int main(int argc, char **argv) { QApplication app(argc,argv); MyWindow wind; // 視窗標題 wind.setWindowTitle("干點雜活"); // 調整視窗大小 wind.resize(321, 266); wind.show(); return QApplication::exec(); }
運行一下,看,橫躺著三顆痘痘,多好看,

再來一例,這次咱們攔截的是視窗的 close 事件,當視窗要關閉的時候,咱們輸出一條除錯資訊,
#ifndef 奶牛 #define 奶牛 #include <QObject> #include <QEvent> class MyFilter : public QObject { protected: bool eventFilter(QObject *obj, QEvent *e) override; }; #endif
這次我們不從任何可視化型別派生,而是直接派生自 QObject 類,這里只是重寫 eventFilter 方法,沒有用到信號和 cao,所以,可以不加 Q_OBJECT 宏,也就是說咱們這個過濾器是獨立用的,不打算加入到 Qt 的物件樹中,
下面是實作代碼:
bool MyFilter::eventFilter(QObject *obj, QEvent *e) { if(e->type() == QEvent::Close) { // 此處要型別轉換 QWidget* window = qobject_cast<QWidget*>(obj); // 看看這貨是不是視窗(有可能是控制元件) if(window->windowFlags() & Qt::Window) { // 獲取這個視窗的標題 QString title = window->windowTitle(); // 輸出除錯資訊 qDebug() << "正在關閉的視窗:" << title; } } // 事件繼續傳遞 return false; }
最好回傳 false,把事件繼續傳遞給視窗,畢竟視窗可能在關閉時要做一些重要的事,比如保存打開的檔案,QWidget 的 WindowFlags 如果包含 Window 值,表明它是一個視窗,
下面直接寫main函式,
int main(int argc, char **argv) { QApplication app(argc, argv); MyFilter *filter = new MyFilter; // 弄三個視窗試試 QWidget *win1 = new QWidget; win1->setWindowTitle("狗頭"); win1->installEventFilter(filter); win1->show(); QWidget *win2 = new QWidget; win2->setWindowTitle("雞頭"); win2->installEventFilter(filter); win2->show(); QWidget *win3 = new QWidget; win3->setWindowTitle("鼠頭"); win3->installEventFilter(filter); win3->show(); return QApplication::exec(); // 可選 delete filter; filter = nullptr; }
filter 是指標型別,它沒有添加到 Qt 物件樹中,不會自動清理,在exec回傳后用 delete 解決它,在清理時有個好習慣,就是 del 之后把指標變數重設為 null,這樣下次再參考變數時不容易產生錯誤,只要 if(! filter) 就能測出它是空的,
反正程式都退出了,所以此處你也可以讓它泄漏一下也無妨,程式掛了后行程空間會被系統識訓,
當然,用Qt專供的“作用域”指標也不錯,超出作用域自動XX掉,
int main(int argc, char **argv) { QApplication app(argc, argv); QScopedPointer<MyFilter> filter(new MyFilter); // 弄三個視窗試試 QWidget *win1 = new QWidget; win1->setWindowTitle("狗頭"); win1->installEventFilter(filter.data()); win1->show(); QWidget *win2 = new QWidget; win2->setWindowTitle("雞頭"); win2->installEventFilter(filter.data()); win2->show(); QWidget *win3 = new QWidget; win3->setWindowTitle("鼠頭"); win3->installEventFilter(filter.data()); win3->show(); return QApplication::exec(); }
QScopedPointer 通過建構式參考要封裝的物件,要訪問被封裝的指標物件,可以使用 data 成員,
運行之后,會出現三個視窗,逐個關閉,會輸出以下除錯資訊:

---------------------------------------------------------------------------------------
最后老周扯點別的,
咱們知道,Qt官方推出 Python for Qt,名曰 PySide,五月份的時候,老周遇到一個問題:PySide6 無法加載 QML 檔案,報的錯誤是加載 dll 失敗,找不到指定的模塊,
網上的方法都是不行的,首先,Qt 在版本號相同(均為 6.5.1)的情況下,C++是可以正常加載 QML 檔案的,不管是生成資源檔案還是直接訪問檔案均可,但 Python 是報錯的,這至少說明我的機器上不缺某些 .dll,不然C++代碼應該也報錯,
接著,老周想是不是Qt官方編譯的有問題,于是,我把自己編譯的Qt動態庫替換 PySide6 里面的元件,報錯依舊,那就排除編譯的差異性,
那么,老周就想到,就是 Python 的問題了,3.7 到 3.10 幾個版本測驗也報錯;用不同路徑建的虛擬環境也報錯;更換 Qt 版本(6.0 到 6.5)同樣報錯,
這時可以直接肯定就是 Python 的問題了,不是版本號的問題,是 Windows 商店安裝的 Python 就會報錯,非 Windows 商店安裝的就正常,
不過,還得再加一句話:想把 Qt 用得 666 還是用 C++ 吧,用 Python 僅適合初學和娛樂,由于 Rust 可以呼叫 C/C++ 代碼,所以你是可以嘗試用 Rust 的,Rust 也不是什么鬼自動記憶體管理,要 GC 用 .NET 就完事了,Rust 的重點是記憶體安全,看似挺誘人,官方也把牛吹得入木四分,可用了之后(和用 Go 一樣的感覺),是真的沒 C++ 好用,C++ 能背負上這么多的歷史包袱也不是靠吹的,當然 C++ 記憶體泄漏也沒你想的那么恐怖,養成好習慣,作用域短,存放資料不多的物件就直接堆疊分配就行了;要在不同代碼背景關系傳遞物件,或分配的資料較大的,用指標,指標型別的變數,在不要的時候堅決干掉,然后記得設定變數為 nullptr,養成這些好習慣基本沒多大問題,
一般代碼你寫慣了是不會忘記 delete 的,容易遺漏的是龐大復雜的代碼之間會共享某些物件,在很多地方會參考到某物件,于是,碼著碼著就頭暈了,就不記得銷毀了,
會被多處參考的物件,可以寫上注釋提醒自己或別人要清理它,或者加個書簽,寫完代碼后去看看書簽串列,就會想起有哪些物件還沒銷毀,代碼寫復雜了會容易混,經常會訪問已清理的物件,于是,不妨在訪問指標變數前 if 陳述句一下,if (ptr),在 bool 運算式中,若指標型別的變數是慷訓得到 false,非空為 true,這樣就可以避免許多低級錯誤,
哪怕是不常用指標的語言也不見得不出事,C# 里面你要是訪問 null 的變數(VB 是 Nothing)也會報那個很經典的錯誤:“未將物件參考設定到物件的實體”,就是 NullReferenceException,在.NET 代碼中你只要看到這貨就得明白肯定有某個為 null 的物件被訪問了,
C++ 里面,這樣寫就能實體化 MyClass 類,只是分配在堆疊上,
MyClass x;
但在 C# 中,初始值是 null,即未初始化的,初始化你還得 new,哦,順便想起個事,C# 中資料的隱式基類是 Array,所以它是參考型別,初始值也是 null 的,就算你資料組里面的元素是值型別,但陣列自身是參考型別,委托也是參考型別,有的剛入門的同學會以為委托是值型別,
C++函式按“參考”傳值的話,一般會用到指標、參考引數,如 int *p、const int &a、const char *w(不能改)等,C# 中如果是參考型別,直接宣告就行了,如 MyClass x,值型別可以用 ref 關鍵字,ref int v,
C# 中 int?、double? 等可以讓其成為參考型別,你可以類比 C 中的 int* 等,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/554898.html
標籤:其他
下一篇:返回列表
