【VS Code 與 Qt6】運用事件過濾器批量操作子級組件

来源:https://www.cnblogs.com/tcjiaan/archive/2023/06/11/17471085.html
-Advertisement-
Play Games

如果某個派生自 QObject 的類重寫 eventFilter 方法,那它就成了事件過濾器(Event Filter)。該方法的聲明如下: virtual bool eventFilter(QObject *watched, QEvent *event); watched 參數是監聽事件的對象,即 ...


如果某個派生自 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* 等。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 某日二師兄參加XXX科技公司的C++工程師開發崗位第11面: > 面試官:在C++中,你都知道都哪些運算符? > > 二師兄:啥?運算符?`+-*/=`這些算嗎? > > 面試官:嗯,還有其他的嗎? > > 二師兄:當然還有,`+=,-=,*=,/=,==`,還有邏輯運算,位運算等。 > > 面試官 ...
  • ## 什麼是GTH GTH 是Xilinx UltraScale系列FPGA上高速收發器的一種類型,本質上和其它名稱如GTP, GTX等只是器件類型不同、速率有差異;GTH 最低速率在500Mbps,最高在16Gbps ![](https://img2023.cnblogs.com/blog/274 ...
  • 博客推行版本更新,成果積累制度,已經寫過的博客還會再次更新,不斷地琢磨,高質量高數量都是要追求的,工匠精神是學習必不可少的精神。因此,大家有何建議歡迎在評論區踴躍發言,你們的支持是我最大的動力,你們敢投,我就敢肝 ...
  • List 介面是 Collection 介面的子介面。List 中元素有序,是按照元素的插入順序進行排序的。每個元素都有一個與之關聯的整數型索引(索引從 0 開始),可以根據索引來訪問和操作元素,可以使用普通 for 迴圈遍歷。List 中可以包含重覆的元素。 ...
  • # Go 實現 MySQL 資料庫事務 ## 一、MySQL事務 MySQL事務是指一組資料庫操作,它們被視為一個邏輯單元,並且要麼全部成功執行,要麼全部回滾(撤銷)。事務是資料庫管理系統提供的一種機制,用於確保數據的一致性和完整性。 事務具有以下特性(通常由ACID原則定義): 1. 原子性(At ...
  • 前言 本文主要介紹使用spring boot 配置多個資料庫,即動態資料庫 開始搭建 首先創建一個SpringWeb項目——dynamicdb(spring-boot2.5.7) 然後引入相關依賴lombok、swagger2、mybatis-plus,如下: <?xml version="1.0" ...
  • # todo 列表 - [ ] clang-format - [ ] c++ 整合 # 軟體安裝 略 # 基本的環境搭建 ## 最基本的 vscode 插件 只需要安裝如下兩個插件即可 c/c++ 擴展是為了最基本的代碼提示和調試支持 cmake language support 是為了提示 CMa ...
  • [系列文章目錄和關於我](https://www.cnblogs.com/cuzzz/p/16609728.html) ## 零丶背景 最近有很多想學的,像netty的使用、原理源碼,但是苦於自己對於操作系統和nio瞭解不多,有點無從下手,遂學習之。 ## 一丶網路io的過程 ![image-202 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...