https://www.devbean.net/2011/08/native-style-qt-3/
https://blog.csdn.net/hyongilfmmm/article/details/83238938
需要說明一點的是,組件的 style 是一個非常復雜的內容,僅在這里不可能全部講解清楚。如果需要自定義組件 style,還是自己仔細閱讀相關文檔。另外,這部分牽扯的類很多,函數也很復雜,步步為營才是最好的對待方法。除非非常必要,還是建議不要輕易去碰 style 這部分。
自定義 style,顧名思義,也就是自己實現樣式。這里通常有兩種實現方式:第一,重寫 widget 的paintEvent()
函數;第二,使用QStyle
類。兩種方式的側重點有所不同:重寫組件的paintEvent()
函數,可以簡單地實現某一類組件的樣式;繼承QStyle
類,則可以實現對全部組件的一致性處理,例如,將程序中所有的 text 變成紅色等。
首先我們來看看重寫paintEvent()
函數。paintEvent()
是QWidget
的一個函數,用於實現自身的繪制。一個組件顯示到屏幕上,就是通過調用paintEvent()
函數把它畫出來。看看一個組件有多復雜,全部要使用QPainter
提供的畫點、畫線的函數繪制出來,就知道這里的工作量了。當然也有偷懶的辦法,就是重寫paintEvent()
的時候使用一張圖片代替。我們這里就不討論這種思路了,而是完全從代碼開始。
我們以QPushButton
為例。這里,我們創建一個 button,這個 button 在點擊時可以凹下顯示。為了重寫paintEvent()
函數,我們必須繼承QPushButton
類。頭文件很簡單,暫且略去,下面只看paintEvent()
這個函數:
盡管前面說過,我們需要重頭繪制整個組件,但實際上,Qt 為我們提供了一系列方便的函數,用於繪制出各個組件的組成部分。這種繪制方式在將組合進行組合的情況下非常有用。例如,一個 combo box 實際上是一個 button 加上一個向下的三角形構成。那么,我不需要將整個 combo box 用像素畫出來,而是借用 Qt 已有的組件進行繪制:畫出一個 button 和一個三角形。所以,這里我們也使用類似的思路,讓 Qt 繪制出組件的每個部分,而我們要做的就是修改參數,讓它按照我們的參數繪制。
如何調用 Qt 的組件繪制函數呢?這個繪制函數是QStyle
類的成員。QWidget
提供了style()
函數,返回當前的QStyle
對象。那么,我們就可以通過這個對象繪制。注意上面代碼中最后一行,我們從這里看起。下面給出這個函數的簽名:
virtual void QStyle::drawControl ( ControlElement element, const QStyleOption * option, QPainter * painter, const QWidget * widget = 0 ) const = 0;
盡管這是一個純虛函數,但是類似於 Java 的 interface,我們可以直接使用style()
返回的對象調用。這是一個很典型的 style 式的函數調用。翻看一下QStyle
的聲明,QStyle
類提供了很多以 draw 打頭的函數,用於繪制整個系統組件的繪制。這類 draw 函數一般會有四個參數:
- 一個 enum,用於指定要繪制哪個元素。這個 enum 在不同的 draw 函數中可能是不一樣的。例如,在
drawControl()
中是QStyle::ControlElement
,指的是組件;在drawPrimitive()
中則是QStyle::PrimitiveElement
,指的是組件的原始組成元素,例如焦點框,check box 的小勾等; QStyleOption
對象指針。這個對象保存了 painter 繪制時所需要的所有數據信息,比如繪制大小、坐標、繪制文本等。不同的 element 可能對應着不同的QStyleOption
的子類,這個在文檔中可以找到;QPainter
對象指針。系統即用這個 painter 進行繪制;QWidget
對象指針,用於輔助繪制。
回到代碼,我們可以看到,在drawControl()
函數的四個參數中,只有最后一個有默認值。也就是說,如果要調用這個函數,我們必須准備好參數數據。這就是在paintEvent()
中,前面幾行代碼做在的工作。
通過文檔我們查到,QPushButton
需要的是QStyleOptionButton
作為第二個參數。於是,我們新建一個QStyleOptionButton
對象。初始化調用initFrom()
,也就是使用本對象設置一個初始值。QStyleOption
有很多屬性。比如QStyleOption::state
指的是當前狀態。例如,如果 button 被按下,也就是isDown()
返回true
的時候,我們將 state 設置為QStyle::State_Sunken
,也就是凹下,否則則是QStyle::State_Raised
。這樣,我們就完成了設置。另外,還要根據需要設置別的屬性,例如,如果isDefault()
返回true
時,我們需要設置option.features
,這樣才能繪制出默認的效果。text 和 icon 屬性則是通過 button 自身函數獲得。這樣,我們完成對繪制數據的設置,就可以調用QStyle::drawControl()
函數,將這個 button 繪制出來。
這里注意一點是,對於QFlags
對象,使用 = 賦值很可能不是你所期望的結果。QFlags
實現的是 bitmap 位圖,如果簡單的使用 = 賦值,在賦值的同時會清除原有位的值。你可以將上面的option.state |= isDown() ? QStyle::State_Sunken : QStyle::State_Raised;
修改為option.state = isDown() ? QStyle::State_Sunken : QStyle::State_Raised;
,注意比較下前后兩個 debug 輸出的不同。
調用QStyle::drawControl()
函數時,第一個參數可以通過文檔查到。這里的 CE_ 前綴實際就是 ControlElement 的意思。
這樣,我們就完成了一個簡單的自定義 button。代碼雖然簡單,但是大體流程已經表現出來,剩下的就是去翻閱大量文檔,仔細了解各個 draw 函數的使用,才能夠做出滿意的自定義組件效果。
前面說的第一種自定義組件實現就簡單說到這里。然后看看第二種,QStyle
的實現。其實在上面,我們已經使用了QStyle
。想必也能夠想到,這里我們依舊要用到QStyle
的各個 draw 函數,只不過這里我們不是簡單的去調用它們,而是通過繼承,將這些 draw 函數替換成我們自己的版本,達到自定義樣式的目的。
雖然我們可以直接繼承QStyle
來實現,但是這並不是一個好主意。因為QStyle
這個類很復雜,幾乎所有的函數都是純虛函數,這要求我們必須一個個實現它們。有時候,我們並不需要自己實現所有功能,僅僅是做簡單的修改。於是,從 4.6 版本開始,Qt 提供了一個專門的類,QProxyStyle
。我們要做的就是繼承QProxyStyle
,覆蓋我們感興趣的函數即可。看下面一個簡單的實例:
class MyProxyStyle : public QProxyStyle { public: void drawControl(ControlElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const; }; void MyProxyStyle::drawControl(ControlElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const { if(element == QStyle::CE_PushButtonLabel) { painter->drawText(option->rect, "fixed"); } else { QProxyStyle::drawControl(element, option, painter, widget); } }
MyProxyStyle
覆蓋了drawControl()
函數,然后判斷,如果是 button label 的話,繪制文本 “fixed”。可想而知,我們的QPushButton::setText()
函數已經沒有作用了,因為我們在繪制時沒有使用這個屬性,也就不會顯示出來了。不管你設不設置,所有 button 的 text 都會是 fixed。如果要使用這個 style ,需要在運行前設置,例如:
這樣,我們就可以用我們自己的 style 顯示組件了。
就像前面所說,自定義 style 是一個相當復雜的話題,我們不可能在這里完全說明。不過,也正因為 Qt 提供了這種機制,也能夠讓我們可以比較輕松地實現自定義 style。