劉琪 找誰做網(wǎng)站靠譜東莞網(wǎng)絡(luò)營(yíng)銷全網(wǎng)推廣
(本文續(xù)上一篇《Qt底層原理:深入解析QWidget的繪制技術(shù)細(xì)節(jié)(1)》)
QWidget繪制體系為什么這么設(shè)計(jì)【重點(diǎn)】
在傳統(tǒng)的C++圖形界面框架中,例如DUILib等,控件的繪制邏輯往往直接在控件的類的內(nèi)部,例如PushButton的draw/paint的函數(shù)內(nèi)部,Qt的QWidget費(fèi)了老大勁,定義了一堆枚舉和基類,把大部分的繪制邏輯都抽離了具體的類,轉(zhuǎn)到了QStyle上。這種做法說實(shí)話,是有弊有利的。
下面是對(duì)利弊的詳細(xì)討論:
有利之處:
-
提高繪制邏輯的復(fù)用性:
- 在Qt中,繪制邏輯不是硬編碼在每個(gè)控件中的,而是通過QStyle這個(gè)中心化的類來處理的。這意味著,像在QListView中繪制按鈕這樣的任務(wù),不需要?jiǎng)?chuàng)建QPushButton實(shí)例,而是通過QStyle來繪制具有按鈕視覺效果的元素。這樣,任何需要具有按鈕風(fēng)格的控件都可以復(fù)用這一段繪制代碼,大大提高了代碼的復(fù)用性。
-
提高繪制邏輯的風(fēng)格化能力:
- 由于QStyle負(fù)責(zé)所有控件的繪制細(xì)節(jié),這使得統(tǒng)一應(yīng)用程序的風(fēng)格變得容易。開發(fā)者可以通過改變QStyle或者使用QStyleSheets來快速地修改應(yīng)用程序的風(fēng)格,而無需修改每個(gè)控件。這種能力使得開發(fā)者能夠更快地響應(yīng)設(shè)計(jì)的改變,并且為用戶提供一致的視覺體驗(yàn)。
-
實(shí)現(xiàn)繪制邏輯和具體的控件類的解耦:
- 在傳統(tǒng)的GUI框架中,繪制代碼通常與控件邏輯緊密耦合。Qt通過將繪制邏輯抽象到QStyle中,實(shí)現(xiàn)了繪制邏輯與控件類的分離。這樣的解耦使得開發(fā)者可以在不改變控件邏輯的情況下,通過修改QStyle或QStyleSheets來定制控件的外觀。這也為實(shí)現(xiàn)像QSS這樣的高級(jí)樣式特性奠定了基礎(chǔ)。
弊處:
-
增加了繪制邏輯的復(fù)雜度:
- Qt的這種抽象方式確實(shí)增加了學(xué)習(xí)和實(shí)現(xiàn)自定義繪制邏輯的復(fù)雜度。新加入的Qt開發(fā)者需要理解QStyle的工作原理以及如何與QStyleOption等類配合使用,這對(duì)于初學(xué)者而言可能是一個(gè)挑戰(zhàn)。
-
給繪制體系新增控件增加了難度:
- 在Qt中,控件的繪制細(xì)節(jié)往往被封裝成枚舉類型,這些枚舉在整個(gè)QStyle體系中都有明確的定義。當(dāng)需要增加新的控件或者擴(kuò)展控件的功能時(shí),可能需要在QStyle中添加新的枚舉值,并要求所有的QStyle實(shí)現(xiàn)都支持這個(gè)新的枚舉。這不僅增加了開發(fā)的難度,也可能導(dǎo)致現(xiàn)有的風(fēng)格類需要進(jìn)行大量的更新來適應(yīng)新的枚舉。
總結(jié)來說,Qt選擇這種設(shè)計(jì),核心是2個(gè)考慮,第一個(gè)是性能,就如前面提到的,當(dāng)我們需要繪制一個(gè)按鈕的時(shí)候是不需要實(shí)例化按鈕類的,這給QListView的性能天花板打到比其他任何圖形界面框架都要高。另一個(gè)方面是實(shí)現(xiàn)非常接近原生的界面風(fēng)格元素,這也是Qt界面框架和其他界面框架獨(dú)特之處。Qt界面默認(rèn)情況下是可以達(dá)到以假亂真的原生效果,要實(shí)現(xiàn)如此高度的還原,還要保障繪制的性能,那么把所有繪制邏輯針對(duì)不同平臺(tái)提供高度的定制化是必然的做法,因此QStyle這套體系就形成了。
繪制雙緩沖細(xì)節(jié)
在Qt中,為了在繪制時(shí)不在屏幕出現(xiàn)繪制過程導(dǎo)致畫面閃爍,會(huì)采用雙緩沖機(jī)制。與此相關(guān)的一些類和組件包括:
-
QPixmap:
QPixmap
是一個(gè)用于處理圖像的類,通常用于離屏繪制(off-screen drawing)。它可以作為雙緩沖的后臺(tái)緩沖區(qū)使用,在這個(gè)緩沖區(qū)上進(jìn)行繪制操作,然后將其內(nèi)容一次性繪制到屏幕上。 -
QWidget:
QWidget
類有一個(gè)屬性,決定是否使用雙緩沖。默認(rèn)情決定了Qt是否為QWidget啟用雙緩沖。大多數(shù)情況下,Qt會(huì)自動(dòng)為所有的QWidget及其子類使用雙緩沖策略,但是開發(fā)者可以通過setAttribute(Qt::WA_PaintOnScreen)
來修改這個(gè)行為。 -
QBackingStore:
QBackingStore
是Qt中負(fù)責(zé)管理窗口內(nèi)容的后臺(tái)存儲(chǔ)的類。它是Qt雙緩沖機(jī)制的核心組件之一,在窗口系統(tǒng)層面處理緩沖區(qū)。當(dāng)窗口或部件的內(nèi)容需要更新時(shí),QBackingStore
負(fù)責(zé)將緩沖區(qū)的內(nèi)容復(fù)制到屏幕上。 -
QPaintEngine:
QPaintEngine
是一個(gè)抽象基類,它定義了Qt繪圖操作的底層接口。具體的實(shí)現(xiàn)類,如QRasterPaintEngine
,會(huì)使用雙緩沖技術(shù)來提高繪制效果和性能。 -
QWindow:
在Qt中,QWindow
代表了一個(gè)系統(tǒng)窗口。它可以使用QBackingStore
來管理其內(nèi)容的雙緩沖,尤其是在Qt Quick中,QWindow
是與平臺(tái)窗口系統(tǒng)交互的主要接口。 -
QScreen:
QScreen
類代表了應(yīng)用程序可以使用的顯示器。雖然它不直接參與雙緩沖,但是它提供了與屏幕相關(guān)的功能,包括分辨率、顏色深度等信息,這些信息可能會(huì)影響雙緩沖策略的選擇和優(yōu)化。
在Qt的繪制過程中,當(dāng)你在QWidget的paintEvent()
方法中使用QPainter
進(jìn)行繪圖時(shí),你實(shí)際上是在繪制到一個(gè)離屏緩沖區(qū)。然后,該緩沖區(qū)的內(nèi)容會(huì)被復(fù)制到屏幕上。這個(gè)過程對(duì)于開發(fā)者來說是透明的,因?yàn)镼t框架在底層處理了所有的細(xì)節(jié)。
如果需要控制雙緩沖的行為,或者需要更深入地理解其實(shí)現(xiàn),可以查看以上提到的類的文檔和源代碼。
需要注意的是,Qt Quick(基于QML的高級(jí)UI框架)與傳統(tǒng)的QWidget系統(tǒng)在渲染上有所不同。Qt Quick使用場(chǎng)景圖(scene graph)和通常基于OpenGL的渲染器進(jìn)行繪制,而不是使用傳統(tǒng)的QWidget繪制流程。盡管如此,QWindow
和QBackingStore
仍然在Qt Quick的窗口管理和屏幕渲染中發(fā)揮作用。
如何提高應(yīng)用程序的繪制性能
提高繪制性能通常涉及減少不必要的繪制工作和優(yōu)化繪制路徑。以下是一些策略來提高Qt控件的繪制性能:
-
避免半透明和透明度:
- 避免半透明的控件,因?yàn)樗枰~外的合成步驟。
- 使用不透明的控件,設(shè)置屬性
Qt::WA_OpaquePaintEvent
。
-
減少重繪區(qū)域:
- 只重繪變化的部分,而不是整個(gè)控件。
- 使用
QWidget::update(const QRect&)
來指定只重繪控件的一個(gè)子區(qū)域。 - 避免不必要的
update()
調(diào)用。
-
優(yōu)化繪制代碼:
- 在
paintEvent
中避免復(fù)雜計(jì)算。 - 使用簡(jiǎn)單的幾何圖形和操作,避免繪制復(fù)雜的圖形。
- 避免在
paintEvent
中創(chuàng)建臨時(shí)對(duì)象。
- 在
-
延遲更新:
- 使用
QWidget::update()
而不是QWidget::repaint()
,因?yàn)?code>update()會(huì)合并多個(gè)重繪請(qǐng)求,延遲到下一個(gè)事件循環(huán)中。
- 使用
-
使用雙緩沖:
- Qt默認(rèn)使用雙緩沖來避免閃爍,確保此功能未被禁用。
-
緩存繪制結(jié)果:
- 對(duì)于不經(jīng)常變化的內(nèi)容,可以將其緩存到
QPixmap
或QImage
中,然后在paintEvent
中直接繪制這些緩存。
- 對(duì)于不經(jīng)常變化的內(nèi)容,可以將其緩存到
-
減少布局調(diào)整:
- 避免頻繁的布局改變,特別是包含大量控件的布局。
-
使用
QStaticText
或QPixmap
:- 對(duì)于不更改的文本,使用
QStaticText
可以提高繪制性能。 - 對(duì)于重復(fù)使用的圖像,使用
QPixmap
進(jìn)行緩存。
- 對(duì)于不更改的文本,使用
-
避免使用圖形效果:
- 圖形效果如陰影、模糊等會(huì)增加繪制負(fù)擔(dān),應(yīng)謹(jǐn)慎使用。
-
合理使用更新策略:
- 對(duì)于自定義控件,使用
QWidget::setUpdateRect()
來定義更高效的更新策略。
- 對(duì)于自定義控件,使用
-
使用硬件加速:
- 如果可能,利用OpenGL或Vulkan等進(jìn)行硬件加速繪制。
-
多線程:
- 對(duì)于復(fù)雜的圖像處理或準(zhǔn)備工作,可以在后臺(tái)線程中進(jìn)行,以免阻塞UI線程。
-
調(diào)整渲染選項(xiàng):
- 使用
QPainter
的渲染提示來平衡質(zhì)量和性能。
- 使用
-
避免無效的層級(jí)結(jié)構(gòu):
- 減少嵌套層次和不必要的父子控件關(guān)系。
需要注意的是,性能調(diào)整往往需要根據(jù)具體的應(yīng)用場(chǎng)景和需求來定制,因此推薦在做出調(diào)整后進(jìn)行充分的測(cè)試,以確保既達(dá)到了性能目標(biāo),又保持了用戶界面的質(zhì)量和響應(yīng)性。
使用多線程繪制提高性能的例子
在Qt中,UI更新(包括繪制)必須在主線程(也就是UI線程)中完成。但是,我們可以在另一個(gè)線程中生成圖像數(shù)據(jù),然后將這些數(shù)據(jù)發(fā)送回主線程進(jìn)行顯示。下面是這種方法的主要流程:
-
在工作線程中生成圖像:
創(chuàng)建一個(gè)工作線程,在這個(gè)線程中進(jìn)行圖像的生成或處理,比如繪制到一個(gè)QImage
或者QPixmap
對(duì)象上。這可以通過直接在工作線程中創(chuàng)建圖像對(duì)象并使用QPainter
來繪制。 -
使用信號(hào)和槽傳輸圖像:
當(dāng)圖像生成完畢,使用信號(hào)和槽機(jī)制將圖像從工作線程發(fā)送回主線程。這通常涉及到在工作線程中發(fā)射一個(gè)信號(hào),攜帶生成的圖像作為參數(shù)。在主線程中,一個(gè)槽函數(shù)將會(huì)接收這個(gè)圖像。 -
在主線程中顯示圖像:
在主線程的槽函數(shù)中接收?qǐng)D像,并將其設(shè)置到一個(gè)控件上顯示。這可以是通過調(diào)用QLabel::setPixmap()
設(shè)置QPixmap
,或者在自定義控件的paintEvent()
中使用QPainter::drawImage()
來繪制QImage
。
以下是一個(gè)簡(jiǎn)化的代碼示例,展示了如何在工作線程中生成圖像,并在主線程中顯示:
// MyWorkerThread.h
#include <QThread>
#include <QImage>class MyWorkerThread : public QThread {Q_OBJECTpublic:MyWorkerThread(QObject *parent = nullptr) : QThread(parent) {}signals:void imageReady(const QImage &image);protected:void run() override {QImage image(100, 100, QImage::Format_ARGB32);QPainter painter(&image);// ... 在這里進(jìn)行繪制操作 ...emit imageReady(image);}
};// MyWidget.h
#include <QWidget>
#include <QImage>class MyWidget : public QWidget {Q_OBJECTpublic:MyWidget(QWidget *parent = nullptr) : QWidget(parent) {// Start the worker threadconnect(&workerThread, &MyWorkerThread::imageReady, this, &MyWidget::updateImage);workerThread.start();}~MyWidget() {workerThread.quit();workerThread.wait();}public slots:void updateImage(const QImage &image) {this->image = image;update(); // Schedule a repaint}protected:void paintEvent(QPaintEvent *event) override {QPainter painter(this);if (!image.isNull()) {painter.drawImage(0, 0, image);}}private:MyWorkerThread workerThread;QImage image;
};
在上面的例子中,MyWorkerThread
類在一個(gè)工作線程中生成了一個(gè)QImage
。一旦圖像生成完畢,它通過信號(hào)imageReady
將圖像發(fā)送回主線程。MyWidget
類有一個(gè)槽函數(shù)updateImage
來接收?qǐng)D像,并使用update()
方法請(qǐng)求重繪。在paintEvent()
中,接收到的圖像被繪制在控件上。
當(dāng)在工作線程中使用QImage
時(shí),應(yīng)該使用線程安全的圖像格式,如QImage::Format_ARGB32
。QPixmap
是專門為顯示優(yōu)化的,并且通常不應(yīng)在非UI線程中使用。
通過這兩篇文章,相信大家對(duì)Qt的繪制體系有了總體上的印象,并且對(duì)Qt繪制體系的設(shè)計(jì)緣由也更加清晰。