讓你的Qt桌面程式看上去更加native(三):自定義style

科技小能手發表於2017-11-29
前面我們一再強調,Qt 使用自己的方式繪製元件。然而我們也看到,在不同的平臺上,Qt 的元件表現也不相同。這和 Swing 有些類似:Swing 使用 look and feel 表現元件的外觀,Qt 也是類似的。用來繪製元件外觀的類就是 QStyle。
 
需要說明一點,元件的 style 是一個非常複雜的內容,僅在這裡不可能全部講解清楚。如果需要,還是要自己仔細閱讀相關文件。另外,這部分牽扯的類很多,函式也很複雜,步步為營才是最好的對待方法。除非非常必要,還是建議不要輕易去碰 style 這部分。
 
好了,說明也說明過了,嚇唬也嚇唬過了,下面進入正題。
 
自定義 style,顧名思義,也就是自己實現外觀。這裡通常有兩種實現方式:第一,重寫 widget 的 paintEvent() 函式;第二,使用 QStyle 類。兩種方式的側重點不同:重寫元件的 paintEvent() 函式,可以簡單地實現某一類元件的樣式,而繼承 QStyle 類,則可以實現對全部元件一致性處理,例如,將程式中所有的 text 變成紅色等。
 
首先我們來看看重寫 paintEvent() 函式。paintEvent() 是 QWidget 的一個函式,用於實現自身的繪製。一個元件顯示到螢幕上,就是通過呼叫 paintEvent() 函式。看看一個元件有多複雜,全部要使用 QPainter 提供的畫點、畫線的函式實現,就知道這裡的工作量了。當然也有偷懶的辦法,就是重寫 paintEvent() 的時候使用一張圖片代替。我們這裡就不討論這種思路了,完全從程式碼開始。
 
我們以 QPushButton 為例。這裡,我們建立一個 button,這個 button 在點選時可以凹下顯示。為了重寫 paintEvent() 函式,我們必須繼承 QPushButton 類。標頭檔案很簡單,暫且略去,下面只看 paintEvent() 這個函式:

  1. void MyPushButton::paintEvent(QPaintEvent *) 
  2.     QStyleOptionButton option; 
  3.     option.initFrom(this); 
  4.     qDebug() << option.state; 
  5.     option.state |= isDown() ? QStyle::State_Sunken : QStyle::State_Raised; 
  6.     qDebug() << option.state; 
  7.     if (isDefault()) 
  8.         option.features |= QStyleOptionButton::DefaultButton; 
  9.     option.text = text(); 
  10.     option.icon = icon(); 
  11.  
  12.     QPainter painter(this); 
  13.     style()->drawControl(QStyle::CE_PushButton, &option, &painter, this); 
儘管前面說過,我們需要重頭繪製整個元件,但實際上,Qt 為我們提供了一系列方便的函式,用於繪製出各個元件。這種在將組建組合的時候非常有用。例如,一個 combo box 實際上是一個 button 加上一個向下的三角形構成。那麼,我不需要將整個 combo box 用畫素畫出來,而是借用 Qt 已有的組建繪製,畫出一個 button 和一個三角形就可以了。所以,這裡我們也使用類似的思路,讓 Qt 繪製出元件,我們要做的就是修改引數,讓它按照我們的引數繪製。
 
如果呼叫 Qt 的元件繪製函式呢?這個繪製函式是 QStyle 類的成員。QWidget 提供了 style() 函式,返回當前的 QStyle 物件。那麼,我們就可以通過這個物件繪製。注意上面程式碼中最後一行,我們從這裡看起。下面給出這個函式的簽名:

  1. 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,覆蓋我們感興趣的函式即可。看下面一個簡單的例項:

  1. class MyProxyStyle : public QProxyStyle 
  2.   public
  3.     void drawControl(ControlElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const
  4. }; 
  5.  
  6. void MyProxyStyle::drawControl(ControlElement element, const QStyleOption *option, QPainter *painter, const QWidget *widget) const 
  7.     if(element == QStyle::CE_PushButtonLabel) { 
  8.         painter->drawText(option->rect, “fixed”); 
  9.     } else { 
  10.         QProxyStyle::drawControl(element, option, painter, widget); 
  11.     } 
MyProxyStyle 覆蓋了 drawControl() 函式,然後判斷,如果是 button label 的話,繪製文字 “fixed”。可想而知,我們的 QPushButton::setText() 函式已經沒有作用了,因為我們在繪製時沒有使用這個屬性,也就不會顯示出來了。不管你設不設定,所有 button 的 text 都會是 fixed。如果要使用這個 style ,需要在執行前設定,例如:

  1. int main(int argc, char **argv) 
  2.     QApplication app(argc, argv); 
  3.     app.setStyle(new MyProxyStyle); 
  4.     MainWindow w; 
  5.     w.show(); 
  6.     return app.exec(); 
這樣,我們就可以用我們自己的 style 顯示元件了。
 
就像前面所說,自定義 style 是一個相當複雜的話題,我們不可能在這裡完全說明。不過,也正因為 Qt 提供了這種機制,也能夠讓我們可以比較輕鬆地實現自定義 style。
本文轉自 FinderCheng 51CTO部落格,原文連結: 
http://blog.51cto.com/devbean/471941


相關文章