深入理解Qt的屬性系統
? 筆者最近正在大規模的開發Qt的項目和工程,這里筆者需要指出的是,這個玩意在最常規的Qt開發中是相對比較少用的,筆者也只是在Qt的QPropertyAnimation需要動畫感知筆者設置的一個屬性的時候方才知道這個東西的。因此,這篇文章的屬性更加象是整合和消化,請各位Qt大佬批評指正。
先說說Qt的屬性系統可以做啥
? 我不想一上來就扔出來一大堆概念和寫法,這沒意思,看著啥也看不懂就來整一處,只會把人嚇跑。
更加高級的和封閉的屬性讀寫機制(C++的反射和動態屬性)
? 在純 C++ 環境下,通過 QObject::setProperty 和 QObject::property,開發者可以在不知道具體類定義的情況下,以字符串形式讀寫對象屬性,從而實現對第三方或未知類型對象的動態操作,這在插件系統或運行時配置中尤為有用。如果需要在不同線程之間安全地調用方法,也可以借助 QMetaObject::invokeMethod 配合 Qt::QueuedConnection,將對屬性的修改或方法調用排入目標線程的事件隊列,從而避免手動管理互斥鎖和死鎖風險。這個就是把對對象屬性的修改扔到巨大的事件循環隊列中,事件循環隊列是保證我們的數據讀寫不會發生爭奪,必須按照順序一個個執行的。
UI 自動化與 Qt Designer 集成
? Qt Designer 以及 Qt Quick Designer 均依賴元對象系統和屬性系統來生成可視化屬性面板。無論是 QWidget 還是 QML 項目,當在設計器中選中控件時,右側屬性編輯器都會列出該控件所有可設計屬性,包括可腳本化(SCRIPTABLE)和可存儲(STORED)的屬性,通過動態讀取 QMetaObject 數據實現即時更新和回顯,這為界面原型設計和主題定制提供了極大便利。這個事情在筆者之后體驗如何自己制作自己的插件的時候會進行嘗試。
QML 中的數據綁定與模型驅動(這個沒做過,gpt這樣說的)
在 QML 層面,屬性系統是響應式界面的基石。將 C++ 對象通過 QQmlContext 或 qmlRegisterType 暴露給 QML 時,凡是使用 Q_PROPERTY 宏聲明并帶有 NOTIFY 信號的屬性,都可以在 QML 中像普通屬性一樣使用綁定表達式。當模型層的屬性在 C++ 側被修改時,對應的 QML 界面會自動更新,無需手動編寫額外的同步代碼,這使得使用 Qt Quick 構建數據驅動型界面變得直觀而高效。即便是在復雜的邏輯場景下,QML 引擎也會跟蹤屬性的依賴關系,在底層的 BindingEvaluator 中智能地避免多余的計算與重繪,從而兼顧性能和開發效率
動畫、狀態機與屬性驅動
Qt 的動畫框架廣泛使用屬性系統來驅動動畫效果。QPropertyAnimation 類通過屬性名稱來獲取和設置目標對象的屬性值,并在不同時間點上插值更新,例如對 QWidget 的 geometry、opacity 或自定義屬性進行平滑過渡;在狀態機(QStateMachine)中,也可以使用屬性動作(QPropertyAction)根據狀態切換自動修改屬性,從而將動畫與狀態邏輯解耦,簡化狀態驅動的界面行為實現。這個在筆者自己手搓網易云的動畫效果的時候又遇到過,筆者會在那里詳細的說明這個用途
非常心動,所以咋玩
? 很正常的想法!這個屬性非常的有趣,所以我們需要怎么做呢?答案是下面的:
Q_PROPERTY(type name(READ getFunction [WRITE setFunction] |MEMBER memberName [(READ getFunction | WRITE setFunction)])[RESET resetFunction][NOTIFY notifySignal][REVISION int | REVISION(int[, int])][DESIGNABLE bool][SCRIPTABLE bool][STORED bool][USER bool][BINDABLE bindableProperty][CONSTANT][FINAL][REQUIRED])
? 注意,這些代碼實際上啥也不會留下,他是給我們的MOC提供信息用的。告訴我們這個屬性的性質,名稱,如何讀寫等等方法。舉個例子:
Q_PROPERTY(bool focus READ hasFocus)
Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled)
Q_PROPERTY(QCursor cursor READ cursor WRITE setCursor RESET unsetCursor)
? 依次說明了如下的基本事實:
1. Q_PROPERTY(bool focus READ hasFocus)
聲明了一個名為 focus
的布爾類型屬性:
- READ:通過
hasFocus()
方法讀取屬性值(無寫入功能)。 - 用途:通常表示控件是否獲得鍵盤焦點(如輸入框被選中時)。
- 特點:這是一個只讀屬性,無法直接修改(沒有
WRITE
或MEMBER
標記)。
2. Q_PROPERTY(bool enabled READ isEnabled WRITE setEnabled)
聲明了一個名為 enabled
的布爾類型屬性:
- READ:通過
isEnabled()
方法讀取屬性值。 - WRITE:通過
setEnabled(bool)
方法修改屬性值。 - 用途:控制控件是否啟用(禁用時通常變灰且不響應用戶輸入)。
- 特點:支持讀寫,修改時會自動觸發 Qt 的屬性系統通知(如信號或樣式更新)。
3. Q_PROPERTY(QCursor cursor READ cursor WRITE setCursor RESET unsetCursor)
聲明了一個名為 cursor
的 QCursor
類型屬性:
- READ:通過
cursor()
方法獲取當前光標樣式。 - WRITE:通過
setCursor(QCursor)
方法設置光標樣式(如鼠標懸停時變為手型)。 - RESET:通過
unsetCursor()
方法恢復默認光標(移除自定義設置)。 - 用途:動態改變控件的光標外觀。
- 特點:支持讀、寫和重置操作,靈活性更高。
#include <QObject>
class Person : public QObject
{Q_OBJECTQ_PROPERTY(QString name READ name WRITE setName NOTIFY nameChanged)
public:explicit Person(QObject *parent = nullptr) : QObject(parent) {}QString name() const { return m_name; }void setName(const QString &name) {if (m_name != name) {m_name = name;emit nameChanged();}}
signals:void nameChanged();
private:QString m_name;
};// 在某處使用
Person p;
p.setProperty("name", "Alice");
QString current = p.property("name").toString();
? 當然,這樣我們就完成了一次對屬性的讀寫。這個的好處其實跟正常的get set接口類似,但是多了Qt上的很多元對象支持,就是我剛剛提到的那些。
附錄:屬性文檔
- 如果未指定 MEMBER 變量,則需要一個 READ 訪問器函數。該函數用于讀取屬性值。理想情況下,使用 const 函數來實現此目的,并且它必須返回屬性的類型或該類型的 const 引用。例如,QWidget::focus 是一個只讀屬性,其 READ 函數為 QWidget::hasFocus()。如果指定了 BINDABLE 變量,則可以編寫 READ default 來從 BINDABLE 變量生成 READ 訪問器。
- WRITE 訪問器函數是可選的。它用于設置屬性值。它必須返回 void,并且必須只接受一個參數,該參數可以是屬性的類型、指向該類型的指針或引用。例如,QWidget::enabled 具有 WRITE 函數 QWidget::setEnabled()。只讀屬性不需要 WRITE 函數。例如,QWidget::focus 沒有 WRITE 函數。如果您同時指定了 BINDABLE 和 WRITE 的默認值,則系統會從 BINDABLE 生成一個 WRITE 訪問器。生成的 WRITE 訪問器不會顯式發出任何使用 NOTIFY 聲明的信號。您應該將該信號注冊為 BINDABLE 的變更處理程序,例如使用 Q_OBJECT_BINDABLE_PROPERTY。
- 如果沒有指定 READ 訪問器函數,則需要 MEMBER 變量關聯。這使得給定的成員變量無需創建 READ 和 WRITE 訪問器函數即可讀寫。如果您需要控制變量訪問,除了 MEMBER 變量關聯之外,仍然可以使用 READ 或 WRITE 訪問器函數(但不能同時使用)。
- RESET 函數是可選的。它用于將屬性設置回其上下文相關的默認值。例如,QWidget::cursor 具有典型的 READ 和 WRITE 函數,即 QWidget::cursor() 和 QWidget::setCursor(),此外它還具有 RESET 函數,即 QWidget::unsetCursor(),因為任何對 QWidget::setCursor() 的調用都不能重置為上下文特定的游標。RESET 函數必須返回 void 且不接受任何參數。
- NOTIFY 信號是可選的。如果定義,則應指定該類中一個現有的信號,該信號在屬性值發生變化時發出。MEMBER 變量的 NOTIFY 信號必須接受零個或一個參數,并且該參數必須與屬性的類型相同。該參數將采用屬性的新值。NOTIFY 信號應僅在屬性確實發生更改時發出,以避免在 QML 中不必要地重新評估綁定。當通過 Qt API(QObject::setProperty、QMetaProperty 等)更改屬性時,會自動發出信號,但直接更改 MEMBER 時則不會發出信號。
- REVISION 編號或 REVISION() 宏是可選的。如果包含,則它定義了在特定 API 版本(通常用于暴露給 QML)中使用的屬性及其通知信號。如果不包含,則默認為 0。
- DESIGNABLE 屬性指示該屬性是否應在 GUI 設計工具(例如 Qt Widgets Designer)的屬性編輯器中可見。大多數屬性都是 DESIGNABLE(默認為 true)。有效值為 true 和 false。
- SCRIPTABLE 屬性指示此屬性是否應由腳本引擎訪問(默認為 true)。有效值為 true 和 false。
- STORED 屬性指示應將屬性視為獨立存在還是依賴于其他值。它還指示在存儲對象狀態時是否必須保存屬性值。大多數屬性都是 STORED 屬性(默認為 true),但例如 QWidget::minimumWidth() 的 STORED 屬性為 false,因為它的值取自 QWidget::minimumSize() 屬性的寬度部分,而該屬性的類型為 QSize。
- USER 屬性指示該屬性是被指定為面向用戶的屬性還是用戶可編輯的屬性。通常,每個類只有一個 USER 屬性(默認為 false)。例如,QAbstractButton::checked 是(可勾選的)按鈕的用戶可編輯屬性。請注意,QItemDelegate 可以獲取和設置小部件的 USER 屬性。
- BINDABLE bindableProperty 屬性指示該屬性支持綁定,并且可以通過元對象系統 (QMetaProperty) 設置和檢查與該屬性的綁定。bindableProperty 命名一個 QBindable 類型的類成員,其中 T 是屬性類型。此屬性是在 Qt 6.0 中引入的。
- CONSTANT 屬性的存在表示該屬性值為常量。對于給定的對象實例,常量屬性的 READ 方法每次調用時必須返回相同的值。對于不同的對象實例,此常量值可能不同。常量屬性不能具有 WRITE 方法或 NOTIFY 信號。
- FINAL 屬性的存在表示該屬性不會被派生類覆蓋。在某些情況下,這可以用于性能優化,但 moc 并不強制執行。必須注意切勿覆蓋 FINAL 屬性。
- REQUIRED 屬性的存在表示該屬性應由類的用戶設置。moc 并不強制執行此操作,并且主要用于適用于暴露給 QML 的類。在 QML 中,除非設置了所有必需屬性,否則無法實例化具有必需屬性的類。