赞
踩
(本文续上一篇《Qt底层原理:深入解析QWidget的绘制技术细节(1)》)
在传统的C++图形界面框架中,例如DUILib等,控件的绘制逻辑往往直接在控件的类的内部,例如PushButton的draw/paint的函数内部,Qt的QWidget费了老大劲,定义了一堆枚举和基类,把大部分的绘制逻辑都抽离了具体的类,转到了QStyle上。这种做法说实话,是有弊有利的。
下面是对利弊的详细讨论:
提高绘制逻辑的复用性:
提高绘制逻辑的风格化能力:
实现绘制逻辑和具体的控件类的解耦:
增加了绘制逻辑的复杂度:
给绘制体系新增控件增加了难度:
总结来说,Qt选择这种设计,核心是2个考虑,第一个是性能,就如前面提到的,当我们需要绘制一个按钮的时候是不需要实例化按钮类的,这给QListView的性能天花板打到比其他任何图形界面框架都要高。另一个方面是实现非常接近原生的界面风格元素,这也是Qt界面框架和其他界面框架独特之处。Qt界面默认情况下是可以达到以假乱真的原生效果,要实现如此高度的还原,还要保障绘制的性能,那么把所有绘制逻辑针对不同平台提供高度的定制化是必然的做法,因此QStyle这套体系就形成了。
在Qt中,为了在绘制时不在屏幕出现绘制过程导致画面闪烁,会采用双缓冲机制。与此相关的一些类和组件包括:
QPixmap:
QPixmap
是一个用于处理图像的类,通常用于离屏绘制(off-screen drawing)。它可以作为双缓冲的后台缓冲区使用,在这个缓冲区上进行绘制操作,然后将其内容一次性绘制到屏幕上。
QWidget:
QWidget
类有一个属性,决定是否使用双缓冲。默认情决定了Qt是否为QWidget启用双缓冲。大多数情况下,Qt会自动为所有的QWidget及其子类使用双缓冲策略,但是开发者可以通过setAttribute(Qt::WA_PaintOnScreen)
来修改这个行为。
QBackingStore:
QBackingStore
是Qt中负责管理窗口内容的后台存储的类。它是Qt双缓冲机制的核心组件之一,在窗口系统层面处理缓冲区。当窗口或部件的内容需要更新时,QBackingStore
负责将缓冲区的内容复制到屏幕上。
QPaintEngine:
QPaintEngine
是一个抽象基类,它定义了Qt绘图操作的底层接口。具体的实现类,如QRasterPaintEngine
,会使用双缓冲技术来提高绘制效果和性能。
QWindow:
在Qt中,QWindow
代表了一个系统窗口。它可以使用QBackingStore
来管理其内容的双缓冲,尤其是在Qt Quick中,QWindow
是与平台窗口系统交互的主要接口。
QScreen:
QScreen
类代表了应用程序可以使用的显示器。虽然它不直接参与双缓冲,但是它提供了与屏幕相关的功能,包括分辨率、颜色深度等信息,这些信息可能会影响双缓冲策略的选择和优化。
在Qt的绘制过程中,当你在QWidget的paintEvent()
方法中使用QPainter
进行绘图时,你实际上是在绘制到一个离屏缓冲区。然后,该缓冲区的内容会被复制到屏幕上。这个过程对于开发者来说是透明的,因为Qt框架在底层处理了所有的细节。
如果需要控制双缓冲的行为,或者需要更深入地理解其实现,可以查看以上提到的类的文档和源代码。
需要注意的是,Qt Quick(基于QML的高级UI框架)与传统的QWidget系统在渲染上有所不同。Qt Quick使用场景图(scene graph)和通常基于OpenGL的渲染器进行绘制,而不是使用传统的QWidget绘制流程。尽管如此,QWindow
和QBackingStore
仍然在Qt Quick的窗口管理和屏幕渲染中发挥作用。
提高绘制性能通常涉及减少不必要的绘制工作和优化绘制路径。以下是一些策略来提高Qt控件的绘制性能:
避免半透明和透明度:
Qt::WA_OpaquePaintEvent
。减少重绘区域:
QWidget::update(const QRect&)
来指定只重绘控件的一个子区域。update()
调用。优化绘制代码:
paintEvent
中避免复杂计算。paintEvent
中创建临时对象。延迟更新:
QWidget::update()
而不是QWidget::repaint()
,因为update()
会合并多个重绘请求,延迟到下一个事件循环中。使用双缓冲:
缓存绘制结果:
QPixmap
或QImage
中,然后在paintEvent
中直接绘制这些缓存。减少布局调整:
使用QStaticText
或QPixmap
:
QStaticText
可以提高绘制性能。QPixmap
进行缓存。避免使用图形效果:
合理使用更新策略:
QWidget::setUpdateRect()
来定义更高效的更新策略。使用硬件加速:
多线程:
调整渲染选项:
QPainter
的渲染提示来平衡质量和性能。避免无效的层级结构:
需要注意的是,性能调整往往需要根据具体的应用场景和需求来定制,因此推荐在做出调整后进行充分的测试,以确保既达到了性能目标,又保持了用户界面的质量和响应性。
在Qt中,UI更新(包括绘制)必须在主线程(也就是UI线程)中完成。但是,我们可以在另一个线程中生成图像数据,然后将这些数据发送回主线程进行显示。下面是这种方法的主要流程:
在工作线程中生成图像:
创建一个工作线程,在这个线程中进行图像的生成或处理,比如绘制到一个QImage
或者QPixmap
对象上。这可以通过直接在工作线程中创建图像对象并使用QPainter
来绘制。
使用信号和槽传输图像:
当图像生成完毕,使用信号和槽机制将图像从工作线程发送回主线程。这通常涉及到在工作线程中发射一个信号,携带生成的图像作为参数。在主线程中,一个槽函数将会接收这个图像。
在主线程中显示图像:
在主线程的槽函数中接收图像,并将其设置到一个控件上显示。这可以是通过调用QLabel::setPixmap()
设置QPixmap
,或者在自定义控件的paintEvent()
中使用QPainter::drawImage()
来绘制QImage
。
以下是一个简化的代码示例,展示了如何在工作线程中生成图像,并在主线程中显示:
// MyWorkerThread.h
#include <QThread>
#include <QImage>
class MyWorkerThread : public QThread {
Q_OBJECT
public:
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);
// ... 在这里进行绘制操作 ...
emit imageReady(image);
}
};
// MyWidget.h
#include <QWidget>
#include <QImage>
class MyWidget : public QWidget {
Q_OBJECT
public:
MyWidget(QWidget *parent = nullptr) : QWidget(parent) {
// Start the worker thread
connect(&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
类在一个工作线程中生成了一个QImage
。一旦图像生成完毕,它通过信号imageReady
将图像发送回主线程。MyWidget
类有一个槽函数updateImage
来接收图像,并使用update()
方法请求重绘。在paintEvent()
中,接收到的图像被绘制在控件上。
当在工作线程中使用QImage
时,应该使用线程安全的图像格式,如QImage::Format_ARGB32
。QPixmap
是专门为显示优化的,并且通常不应在非UI线程中使用。
通过这两篇文章,相信大家对Qt的绘制体系有了总体上的印象,并且对Qt绘制体系的设计缘由也更加清晰。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。