Qt Desktop Widgets 控件绘图原理逐步分析拆解
Qt 是目前C++语言首选的框架库。之所以称为框架库而不单单是GUI库,是因为Qt提供了远远超过GUI的功能封装,即使不使用GUI的后台服务,也可以用Qt大大提高跨平台的能力。
仅就界面来说,Qt 保持各个平台绘图等效果的统一,并不是对windows标准控件或者GTK等控件库的简单封装,而是完全从像素级别实现了GUI渲染。这一点和MFC等库有本质的区别。今天,就借着Qt 6.6的源码,看看widgets是如何绘制一个按钮的。
1. 从windows API找起
由于不想重新编译一个 debug 版本的 Qt 来跟踪其 Callstack,我们直接用 QtCreator打开QtBase的CMakeList.txt,静态的进行分析。
静态分析代码,可以从最顶层开始,也可以从最底层。还可以利用 doxygen等工具,分析调用关系。我们这次从最底层开始,即看看Qt为了绘图,到底调用了哪些windows API。
首先搜索GDI的一些典型绘制API,比如划线等等,但没有找到切入点。直到搜索 BitBlt这个API,即贴图,终于发现了调用的位置:
void QWindowsBackingStore::flush(QWindow *window, const QRegion ®ion,const QPoint &offset)
{//省略大量上下文QWindowsWindow *rw = QWindowsWindow::windowsWindowOf(window);const bool hasAlpha = rw->format().hasAlpha();if (rw) {const HDC dc = rw->getDC();if (!dc) {qErrnoWarning("%s: GetDC failed", __FUNCTION__);return;}if (!BitBlt(dc, br.x(), br.y(), br.width(), br.height(),m_image->hdc(), br.x() + offset.x(), br.y() + offset.y(), SRCCOPY)) {const DWORD lastError = GetLastError(); // QTBUG-35926, QTBUG-29716: may fail after lock screen.if (lastError != ERROR_SUCCESS && lastError != ERROR_INVALID_HANDLE)qErrnoWarning(int(lastError), "%s: BitBlt failed", __FUNCTION__);}rw->releaseDC();}
可以看到, QWindowsBackingStore::flush 实际上是把一个 QImage 设备无关位图缓存里的像素粘贴到窗口的设备上下文(DC)上。这说明,在整体贴图之前,各种绘制已经完成了。那么,是谁绘制了 m_Image呢?看看 m_image的定义:
class QWindowsBackingStore : public QPlatformBackingStore
{Q_DISABLE_COPY_MOVE(QWindowsBackingStore)
public:QImage toImage() const override;
private:QScopedPointer<QWindowsNativeImage> m_image;
};
这是一个windows平台的本地Image,使用智能指针管理的对象实例。继续跟踪这个类的定义:
class Q_GUI_EXPORT QWindowsNativeImage
{Q_DISABLE_COPY_MOVE(QWindowsNativeImage)
public:QWindowsNativeImage(int width, int height,QImage::Format format);~QWindowsNativeImage();inline int width() const { return m_image.width(); }inline int height() const { return m_image.height(); }QImage &image() { return m_image; }const QImage &image() const { return m_image; }HDC hdc() const { return m_hdc; }static QImage::Format systemFormat();private:const HDC m_hdc;QImage m_image;HBITMAP m_bitmap = 0;HBITMAP m_null_bitmap = 0;
};
可以看到,这个类是对设备无关位图 QImage进行了封装,同时封装了windows平台上的设备上下文 const HDC m_hdc。从这一步,基本就可以确定我们要跟踪的就是这个QWindowsNativeImage::m_image。
2. 顺藤摸瓜
熟悉Qt类继承关系的话,就会知道QImage本身就是一个可被 QPainter 绘图的 QPaintDevice 派生类。这个东西是可以返回绘图设备指针的。
继续查看 QWindowsBackingStore 的方法,果然有一个成员函数:
// QPaintDevice *paintDevice() override;QPaintDevice *QWindowsBackingStore::paintDevice()
{Q_ASSERT(!m_image.isNull());return &m_image->image();
}以及基类的定义:
/*!Returns the paint device for this surface.\warning The device is only valid between calls to beginPaint() andendPaint(). You should not cache the returned value.
*/
QPaintDevice *QBackingStore::paintDevice()
{QPaintDevice *device = handle()->paintDevice();if (QHighDpiScaling::isActive() && device->devType() == QInternal::Image)return d_ptr->highDpiBackingstore.data();return device;
}
那么,谁又使用了这个 paintDevice呢?直接右键单击 QtCreator里“QPaintDevice *QBackingStore::paintDevice()” 这一行,在弹出的菜单里选择“Find reference to Symbol Under Cursor”,
会发现在类 QRasterWindow 及其私有数据成员类 QRasterWindowPrivate里有引用。
/*!\class QRasterWindow\inmodule QtGui\since 5.4\brief QRasterWindow is a convenience class for using QPainter on a QWindow.QRasterWindow is a QWindow with a raster-based, non-OpenGL surface. On top ofthe functionality offered by QWindow, QRasterWindow adds a virtualpaintEvent() function and the possibility to open a QPainter on itself. Theunderlying paint engine will be the raster one, meaning that all drawing willhappen on the CPU. For performing accelerated, OpenGL-based drawing, useQOpenGLWindow instead.Internally the class is thin wrapper for QWindow and QBackingStoreand is very similar to the \l{Raster Window Example}{Raster WindowExample} that uses these classes directly.\sa QPaintDeviceWindow::paintEvent(), QPaintDeviceWindow::update()
*/class QRasterWindowPrivate : public QPaintDeviceWindowPrivate
{
public:void beginPaint(const QRegion ®ion) override{Q_Q(QRasterWindow);const QSize size = q->size();if (backingstore->size() != size) {backingstore->resize(size);markWindowAsDirty();}backingstore->beginPaint(region);}void endPaint() override{backingstore->endPaint();}void flush(const QRegion ®ion) override{Q_Q(QRasterWindow);backingstore->flush(region, q);}QScopedPointer<QBackingStore> backingstore;
};/*!Constructs a new QRasterWindow with \a parent.
*/
QRasterWindow::QRasterWindow(QWindow *parent): QPaintDeviceWindow(*(new QRasterWindowPrivate), parent)
{setSurfaceType(QSurface::RasterSurface);d_func()->backingstore.reset(new QBackingStore(this));
}QRasterWindow::~QRasterWindow()
{Q_D(QRasterWindow);// Delete backingstore while window is still alive, as it// might need to reference the window in the processd->backingstore.reset(nullptr);
}/*!\internal
*/
int QRasterWindow::metric(PaintDeviceMetric metric) const
{Q_D(const QRasterWindow);switch (metric) {case PdmDepth:return d->backingstore->paintDevice()->depth();default:break;}return QPaintDeviceWindow::metric(metric);
}/*!\internal
*/
QPaintDevice *QRasterWindow::redirected(QPoint *) const
{Q_D(const QRasterWindow);return d->backingstore->paintDevice();
}QT_END_NAMESPACE#include "moc_qrasterwindow.cpp"
这个 QRasterWindow 就是代表了CPU渲染的传统GUI窗口(窗体)。需要注意在gui模块外部,尤其是 widget 模块下,是什么类首先调用了上文中的方法,如 beginPaint(), paintDevice等。
3. 从GUI到Widgets
上文的顺藤摸瓜,还是在Qt GUI模块里摸索。当我们全文搜索对 GUI 主要入口点 paintDevice\beginPaint的引用时,很快就能注意到QWidgetRepaintManager这个类。
void QWidgetRepaintManager::paintAndFlush()
{
//...store->setStaticContents(staticRegion);store->beginPaint(toClean);wd->drawWidget(store->paintDevice(), toBePainted, offset, flags, nullptr, this);tlw->d_func()->drawWidget(store->paintDevice(), dirtyCopy, QPoint(), flags, nullptr, this);store->endPaint();flush();
}
在上述经过大幅度缩减的代码里,我们大致能够看到对一次绘制,要经过 beginPaint、drawWidget、endPaint、flush四步骤。
这些步骤在 Qt GUI模块的跟踪里都反复遇到过。其中只有 flush 是发生了 windows Native DC的 bitblt,其余都在 Image 内存对象里进行。
调用 paintAndFlush有且仅在QWidgetRepaintManager::sync()中。这个函数的作用就是把改变的窗口图案同步到后台存储中(Synchronizes the backing store)。
/*!Synchronizes the backing store, i.e. dirty areas are repainted and flushed.
*/
void QWidgetRepaintManager::sync()
{qCInfo(lcWidgetPainting) << "Syncing dirty widgets";updateRequestSent = false;if (qt_widget_private(tlw)->shouldDiscardSyncRequest()) {// If the top-level is minimized, it's not visible on the screen so we can delay the// update until it's shown again. In order to do that we must keep the dirty states.// These will be cleared when we receive the first expose after showNormal().// However, if the widget is not visible (isVisible() returns false), everything will// be invalidated once the widget is shown again, so clear all dirty states.if (!tlw->isVisible()) {dirty = QRegion();for (int i = 0; i < dirtyWidgets.size(); ++i)resetWidget(dirtyWidgets.at(i));dirtyWidgets.clear();}return;}if (syncAllowed())paintAndFlush();
}
看起来,QWidgetRepaintManager 包揽了所有widgets的重绘工作,而不是各个Widget直接绘制到backing store。
那么,什么时候调用sync呢? 可以很方便的跟踪到 QWidgetPrivate::syncBackingStore
void QWidgetPrivate::syncBackingStore()
{if (shouldPaintOnScreen()) {paintOnScreen(dirty);dirty = QRegion();} else if (QWidgetRepaintManager *repaintManager = maybeRepaintManager()) {repaintManager->sync();}
}
以及 QWidget::event(QEvent *event)
bool QWidget::event(QEvent *event)
{case QEvent::UpdateRequest:d->syncBackingStore();break;}
总的出发地点,就是这个 event,具体就是 UpdateRequest的响应了。
这里要额外插一句,这个 Q_D(QWidget);其实展开为:
QWidgetPrivate * const d = d_func();
为啥子Qt大多数类都有一个对应的私有封装类呢?主要是要提纯接口,把不需要用户看到的辅助东西放在 Priavte类里,接口只保留公开的属性、方法。这个技术网上有专门介绍。
4. 顺流而上
搞清楚了事件的起点,我们再回到第三章的一开始,void QWidgetRepaintManager::paintAndFlush()里有一个关键的drawWidget,这个方法应该就是执行widget的具体绘制了,真正的实现是在 QWidgetPrivate::drawWidget 里。
void QWidgetPrivate::drawWidget(QPaintDevice *pdev, const QRegion &rgn, const QPoint &offset, DrawWidgetFlags flags,QPainter *sharedPainter, QWidgetRepaintManager *repaintManager)
{if (!skipPaintEvent) {//actually send the paint eventsendPaintEvent(toBePainted);}
}
这个函数非常长,最重要的就是上面这句, sendPaintEvent(toBePainted);
由此,各个 Widget 的paintEvent会在事件响应中被调用了。我们随便看看一个按钮的paintEvent
/*!\reimp
*/
void QPushButton::paintEvent(QPaintEvent *)
{QStylePainter p(this);QStyleOptionButton option;initStyleOption(&option);p.drawControl(QStyle::CE_PushButton, option);
}
它会具体驱动一个风格对象来绘图,比如fusion或者 vista。
有兴趣可以看看
void QWindowsStyle::drawControl(ControlElement ce, const QStyleOption *opt, QPainter *p,const QWidget *widget) const
里面有非常可怕和冗长的绘制代码。以简单的一个小拐角风格来说:
case QTabBar::RoundedNorth: {if (!selected) {y1 += 2;x1 += onlyOne || firstTab ? borderThinkness : 0;x2 -= onlyOne || lastTab ? borderThinkness : 0;}p->fillRect(QRect(x1 + 1, y1 + 1, (x2 - x1) - 1, (y2 - y1) - 2), tab->palette.window());// Delete borderif (selected) {p->fillRect(QRect(x1,y2-1,x2-x1,1), tab->palette.window());p->fillRect(QRect(x1,y2,x2-x1,1), tab->palette.window());}// Leftif (firstTab || selected || onlyOne || !previousSelected) {p->setPen(light);p->drawLine(x1, y1 + 2, x1, y2 - ((onlyOne || firstTab) && selected && leftAligned ? 0 : borderThinkness));p->drawPoint(x1 + 1, y1 + 1);}// Top{int beg = x1 + (previousSelected ? 0 : 2);int end = x2 - (nextSelected ? 0 : 2);p->setPen(light);p->drawLine(beg, y1, end, y1);}// Rightif (lastTab || selected || onlyOne || !nextSelected) {p->setPen(shadow);p->drawLine(x2, y1 + 2, x2, y2 - ((onlyOne || lastTab) && selected && rightAligned ? 0 : borderThinkness));p->drawPoint(x2 - 1, y1 + 1);p->setPen(dark);p->drawLine(x2 - 1, y1 + 2, x2 - 1, y2 - ((onlyOne || lastTab) && selected && rightAligned ? 0 : borderThinkness));}break; }
可见,这是在用最底层的点线面函数在绘制界面。
5. 堆栈跟踪验证
尽管靠着阅读代码,基本跟踪到了各个关键环节,还是想真正验证一下。直接CMake一个带有调试信息的Qt6.6 base,花了十几分钟,还是很快的。我们从一次重绘事件开始,直到真正意义上的按照风格绘制按钮的色彩、文本。
5.1 堆栈跟踪
从事件开始,到按钮执行绘图,经历的堆栈如下:
> qwindowsvistastyled.dll!QWindowsVistaStyle::drawControl(QStyle::ControlElement element, const QStyleOption * option, QPainter * painter, const QWidget * widget) 行 2544 C++Qt6Widgetsd.dll!QCommonStyle::drawControl(QStyle::ControlElement element, const QStyleOption * opt, QPainter * p, const QWidget * widget) 行 1322 C++Qt6Widgetsd.dll!QWindowsStyle::drawControl(QStyle::ControlElement ce, const QStyleOption * opt, QPainter * p, const QWidget * widget) 行 1817 C++qwindowsvistastyled.dll!QWindowsVistaStyle::drawControl(QStyle::ControlElement element, const QStyleOption * option, QPainter * painter, const QWidget * widget) 行 3284 C++Qt6Widgetsd.dll!QStylePainter::drawControl(QStyle::ControlElement ce, const QStyleOption & opt) 行 52 C++Qt6Widgetsd.dll!QPushButton::paintEvent(QPaintEvent * __formal) 行 415 C++Qt6Widgetsd.dll!QWidget::event(QEvent * event) 行 9150 C++Qt6Widgetsd.dll!QAbstractButton::event(QEvent * e) 行 932 C++Qt6Widgetsd.dll!QPushButton::event(QEvent * e) 行 684 C++Qt6Widgetsd.dll!QApplicationPrivate::notify_helper(QObject * receiver, QEvent * e) 行 3290 C++Qt6Widgetsd.dll!QApplication::notify(QObject * receiver, QEvent * e) 行 3237 C++Qt6Cored.dll!QCoreApplication::notifyInternal2(QObject * receiver, QEvent * event) 行 1118 C++Qt6Cored.dll!QCoreApplication::sendSpontaneousEvent(QObject * receiver, QEvent * event) 行 1551 C++Qt6Widgetsd.dll!QWidgetPrivate::sendPaintEvent(const QRegion & toBePainted) 行 5651 C++Qt6Widgetsd.dll!QWidgetPrivate::drawWidget(QPaintDevice * pdev, const QRegion & rgn, const QPoint & offset, QFlags<enum QWidgetPrivate::DrawWidgetFlag> flags, QPainter * sharedPainter, QWidgetRepaintManager * repaintManager) 行 5602 C++Qt6Widgetsd.dll!QWidgetPrivate::paintSiblingsRecursive(QPaintDevice * pdev, const QList<QObject *> & siblings, int index, const QRegion & rgn, const QPoint & offset, QFlags<enum QWidgetPrivate::DrawWidgetFlag> flags, QPainter * sharedPainter, QWidgetRepaintManager * repaintManager) 行 5779 C++Qt6Widgetsd.dll!QWidgetPrivate::drawWidget(QPaintDevice * pdev, const QRegion & rgn, const QPoint & offset, QFlags<enum QWidgetPrivate::DrawWidgetFlag> flags, QPainter * sharedPainter, QWidgetRepaintManager * repaintManager) 行 5643 C++Qt6Widgetsd.dll!QWidgetRepaintManager::paintAndFlush() 行 906 C++Qt6Widgetsd.dll!QWidgetRepaintManager::sync(QWidget * exposedWidget, const QRegion & exposedRegion) 行 630 C++Qt6Widgetsd.dll!QWidgetPrivate::syncBackingStore(const QRegion & region) 行 1775 C++Qt6Widgetsd.dll!QWidgetWindow::handleExposeEvent(QExposeEvent * event) 行 1023 C++Qt6Widgetsd.dll!QWidgetWindow::event(QEvent * event) 行 289 C++//...省去NNNN多行^^^^^^^^^Qt6Cored.dll!QEventDispatcherWin32::processEvents(QFlags<enum QEventLoop::ProcessEventsFlag> flags) 行 540 C++Qt6Guid.dll!QWindowsGuiEventDispatcher::processEvents(QFlags<enum QEventLoop::ProcessEventsFlag> flags) 行 36 C++Qt6Cored.dll!QEventLoop::processEvents(QFlags<enum QEventLoop::ProcessEventsFlag> flags) 行 101 C++Qt6Cored.dll!QEventLoop::exec(QFlags<enum QEventLoop::ProcessEventsFlag> flags) 行 182 C++Qt6Cored.dll!QCoreApplication::exec() 行 1439 C++Qt6Guid.dll!QGuiApplication::exec() 行 1922 C++Qt6Widgetsd.dll!QApplication::exec() 行 2570 C++testBtn.exe!main(int argc, char * * argv) 行 9 C++testBtn.exe!qtEntryPoint() 行 50 C++testBtn.exe!WinMain(HINSTANCE__ * __formal, HINSTANCE__ * __formal, char * __formal, int __formal) 行 60 C++
在抵达绘图代码 QWindowsVistaStyle::drawControl 后,多次绘制各个widget,合成的image会最终被flush到一个空白的windows窗体上。
> qwindowsd.dll!QWindowsBackingStore::flush(QWindow * window, const QRegion & region, const QPoint & offset) 行 82 C++Qt6Guid.dll!QBackingStore::flush(const QRegion & region, QWindow * window, const QPoint & offset) 行 223 C++Qt6Widgetsd.dll!QWidgetRepaintManager::flush(QWidget * widget, const QRegion & region, QPlatformTextureList * widgetTextures) 行 1101 C++Qt6Widgetsd.dll!QWidgetRepaintManager::flush() 行 977 C++Qt6Widgetsd.dll!QWidgetRepaintManager::paintAndFlush() 行 909 C++Qt6Widgetsd.dll!QWidgetRepaintManager::sync() 行 657 C++Qt6Widgetsd.dll!QWidgetPrivate::syncBackingStore() 行 1766 C++Qt6Widgetsd.dll!QWidget::event(QEvent * event) 行 9314 C++//...省去NNNN多行Qt6Cored.dll!QEventDispatcherWin32::processEvents(QFlags<enum QEventLoop::ProcessEventsFlag> flags) 行 540 C++Qt6Guid.dll!QWindowsGuiEventDispatcher::processEvents(QFlags<enum QEventLoop::ProcessEventsFlag> flags) 行 36 C++Qt6Cored.dll!QEventLoop::processEvents(QFlags<enum QEventLoop::ProcessEventsFlag> flags) 行 101 C++Qt6Cored.dll!QEventLoop::exec(QFlags<enum QEventLoop::ProcessEventsFlag> flags) 行 182 C++Qt6Cored.dll!QCoreApplication::exec() 行 1439 C++Qt6Guid.dll!QGuiApplication::exec() 行 1922 C++Qt6Widgetsd.dll!QApplication::exec() 行 2570 C++testBtn.exe!main(int argc, char * * argv) 行 9 C++testBtn.exe!qtEntryPoint() 行 50 C++testBtn.exe!WinMain(HINSTANCE__ * __formal, HINSTANCE__ * __formal, char * __formal, int __formal) 行 60 C++
注意上述两次触发的事件不同,事件在Call Stack上还会嵌套多层。实际的 Callstack很长。
5.2 抵达最原始的像素操作
如果继续跟踪,可以看到典型的底层计算及图形学椭圆生成算法,如何根据画笔对原始的RGB像素进行逐一操作;
> Qt6Guid.dll!blend_color_argb(int count, const QT_FT_Span_ * spans, void * userData) 行 3858 C++Qt6Guid.dll!drawEllipsePoints(int x, int y, int length, const QRect & rect, const QRect & clip, void(*)(int, const QT_FT_Span_ *, void *) pen_func, void(*)(int, const QT_FT_Span_ *, void *) brush_func, QSpanData * pen_data, QSpanData * brush_data) 行 4794 C++Qt6Guid.dll!drawEllipse_midpoint_i(const QRect & rect, const QRect & clip, void(*)(int, const QT_FT_Span_ *, void *) pen_func, void(*)(int, const QT_FT_Span_ *, void *) brush_func, QSpanData * pen_data, QSpanData * brush_data) 行 4821 C++Qt6Guid.dll!QRasterPaintEngine::drawEllipse(const QRectF & rect) 行 3301 C++Qt6Guid.dll!QPaintEngineEx::drawEllipse(const QRect & r) 行 830 C++Qt6Guid.dll!QPainter::drawEllipse(const QRect & r) 行 4014 C++Qt6Guid.dll!QPainter::drawEllipse(int x, int y, int w, int h) 行 586 C++testBtn.exe!testBtn::paintEvent(QPaintEvent * evt) 行 24 C++Qt6Widgetsd.dll!QWidget::event(QEvent * event) 行 9150 C++
可以看到 integer point midpoint algorithm 绘制椭圆:
*!\internalDraws an ellipse using the integer point midpoint algorithm.
*/
static void drawEllipse_midpoint_i(const QRect &rect, const QRect &clip,ProcessSpans pen_func, ProcessSpans brush_func,QSpanData *pen_data, QSpanData *brush_data)
{const qreal a = qreal(rect.width()) / 2;const qreal b = qreal(rect.height()) / 2;qreal d = b*b - (a*a*b) + 0.25*a*a;int x = 0;int y = (rect.height() + 1) / 2;int startx = x;// region 1while (a*a*(2*y - 1) > 2*b*b*(x + 1)) {if (d < 0) { // select Ed += b*b*(2*x + 3);++x;} else { // select SEd += b*b*(2*x + 3) + a*a*(-2*y + 2);drawEllipsePoints(startx, y, x - startx + 1, rect, clip,pen_func, brush_func, pen_data, brush_data);startx = ++x;--y;}}drawEllipsePoints(startx, y, x - startx + 1, rect, clip,pen_func, brush_func, pen_data, brush_data);// region 2d = b*b*(x + 0.5)*(x + 0.5) + a*a*((y - 1)*(y - 1) - b*b);const int miny = rect.height() & 0x1;while (y > miny) {if (d < 0) { // select SEd += b*b*(2*x + 2) + a*a*(-2*y + 3);++x;} else { // select Sd += a*a*(-2*y + 3);}--y;drawEllipsePoints(x, y, 1, rect, clip,pen_func, brush_func, pen_data, brush_data);}
}
在需要绘制的各个像素位置,需要调用画笔和画刷的像素画逻辑
/*!\internal\a x and \a y is relative to the midpoint of \a rect.
*/
static inline void drawEllipsePoints(int x, int y, int length,const QRect &rect,const QRect &clip,ProcessSpans pen_func, ProcessSpans brush_func,QSpanData *pen_data, QSpanData *brush_data)
{if (length == 0)return;QT_FT_Span _outline[4];QT_FT_Span *outline = _outline;const int midx = rect.x() + (rect.width() + 1) / 2;const int midy = rect.y() + (rect.height() + 1) / 2;x = x + midx;y = midy - y;// topleftoutline[0].x = midx + (midx - x) - (length - 1) - (rect.width() & 0x1);outline[0].len = qMin(length, x - outline[0].x);outline[0].y = y;outline[0].coverage = 255;// toprightoutline[1].x = x;outline[1].len = length;outline[1].y = y;outline[1].coverage = 255;// bottomleftoutline[2].x = outline[0].x;outline[2].len = outline[0].len;outline[2].y = midy + (midy - y) - (rect.height() & 0x1);outline[2].coverage = 255;// bottomrightoutline[3].x = x;outline[3].len = length;outline[3].y = outline[2].y;outline[3].coverage = 255;if (brush_func && outline[0].x + outline[0].len < outline[1].x) {QT_FT_Span _fill[2];QT_FT_Span *fill = _fill;// top fillfill[0].x = outline[0].x + outline[0].len - 1;fill[0].len = qMax(0, outline[1].x - fill[0].x);fill[0].y = outline[1].y;fill[0].coverage = 255;// bottom fillfill[1].x = outline[2].x + outline[2].len - 1;fill[1].len = qMax(0, outline[3].x - fill[1].x);fill[1].y = outline[3].y;fill[1].coverage = 255;int n = (fill[0].y >= fill[1].y ? 1 : 2);n = qt_intersect_spans(fill, n, clip);if (n > 0)brush_func(n, fill, brush_data);}if (pen_func) {int n = (outline[1].y >= outline[2].y ? 2 : 4);n = qt_intersect_spans(outline, n, clip);if (n > 0)pen_func(n, outline, pen_data);}
}
上文的pen_func(n, outline, pen_data); 是一个functional对象,调用 blend_color_argb 方法对每个像素进行内存级别的比特操作:
static void blend_color_argb(int count, const QT_FT_Span *spans, void *userData)
{QSpanData *data = reinterpret_cast<QSpanData *>(userData);const Operator op = getOperator(data, nullptr, 0);const uint color = data->solidColor.rgba();if (op.mode == QPainter::CompositionMode_Source) {// inline for performancewhile (count--) {uint *target = ((uint *)data->rasterBuffer->scanLine(spans->y)) + spans->x;if (spans->coverage == 255) {qt_memfill(target, color, spans->len);
#ifdef __SSE2__} else if (spans->len > 16) {op.funcSolid(target, spans->len, color, spans->coverage);
#endif} else {uint c = BYTE_MUL(color, spans->coverage);int ialpha = 255 - spans->coverage;for (int i = 0; i < spans->len; ++i)target[i] = c + BYTE_MUL(target[i], ialpha);}++spans;}return;}const auto funcSolid = op.funcSolid;auto function = [=] (int cStart, int cEnd) {for (int c = cStart; c < cEnd; ++c) {uint *target = ((uint *)data->rasterBuffer->scanLine(spans[c].y)) + spans[c].x;funcSolid(target, spans[c].len, color, spans[c].coverage);}};QT_THREAD_PARALLEL_FILLS(function);
}
6 总结
我们可以看到,Qt是遵循这样的逻辑来进行绘图的:
- 控件的绘制逻辑,是在“Style” 系列风格类里具体实现。不同的style显然绘制按钮的样子也不同。
- 绘制的操作类是 QPainter,提供各种 draw的具体实现。
- 绘制的目的设备是 QPainterDevice,大部分情况下是一个临时 QImage对象,并最终合并到 QWindowsBackingStore::m_image里。
- 绘制行为的触发点是Event,比如鼠标或者别的窗口划过了按钮;
- 最终绘制生效被显示的位置就是 QWindowsBackingStore::flush。
Qt没有使用windows自带的控件库,而是靠 QWindowsVistaStyle 或者其他的风格类,来硬画图,一个点一条线地画出来。同时,因为上层的绘制全部是在设备无关位图 QImage里绘制的,所以无论是显示、打印或者 render 到一个文件里,上层的代码都是不需要改动的。
这也是为什么Qt可以在嵌入式系统不完全的绘图支持下(比如仅有一个FrameBuf),依旧可以绘制出复杂图形的原因。Qt完全可作为OS一部分而存在,Linux KDE桌面就是基于Qt的实现。同时由于 backing store的可替换性,借助openGL等加速的绘制系统也很方便的集成进来了。
当然,上述跟踪只是Desktop Widgets的一个很小的一撇。Qt在融合各类显示策略上的规划非常宏大。
能够直接看到计算机图形学中点线面的生成算法,是Qt作为GUI工具链具备OS特性的一大特征。这不是对各种轮子的反复封装,而是硬核的底层实现!
相关文章:

Qt Desktop Widgets 控件绘图原理逐步分析拆解
Qt 是目前C语言首选的框架库。之所以称为框架库而不单单是GUI库,是因为Qt提供了远远超过GUI的功能封装,即使不使用GUI的后台服务,也可以用Qt大大提高跨平台的能力。 仅就界面来说,Qt 保持各个平台绘图等效果的统一,并…...

什么是rocketmq❓
在大规模分布式系统中,各个服务之间的通信是至关重要的,而RocketMQ作为一款分布式消息中间件,为解决这一问题提供了强大的解决方案。本文将深入探讨RocketMQ的基本概念、用途,以及在实际分布式系统中的作用,并对Produc…...

【网络安全】HTTP Slowloris攻击原理解析
文章目录 Slowloris攻击的概念Slowloris攻击原理Slowloris攻击的步骤其他的DDoS攻击类型UDP FloodICMP (Ping) FloodSYN FloodPing of DeathNTP AmplificationHTTP FloodZero-day DDoS 攻击 推荐阅读 Slowloris攻击的概念 Slowloris是在2009年由著名Web安全专家RSnake提出的一…...

从最近爆火的ChatGPT,我看到了电商的下一个形态
爆火的ChatGPT似乎让每个行业有了改造的可能性,电商行业也不例外。 在讨论了很多流量红利消失的话题后,我们看到互联网电商行业不再性感,从淘宝天猫,京东,到拼多多,再到抖音,快手,电…...

云原生向量计算引擎 PieCloudVector:为大模型提供独特记忆
拓数派大模型数据计算系统(PieDataComputingSystem,缩写:πDataCS)在10月24日程序员节「大模型数据计算系统」2023拓数派年度技术论坛正式发布。πDataCS 以云原生技术重构数据存储和计算,「一份存储,多引擎…...

大创项目推荐 深度学习 opencv python 实现中国交通标志识别
文章目录 0 前言1 yolov5实现中国交通标志检测2.算法原理2.1 算法简介2.2网络架构2.3 关键代码 3 数据集处理3.1 VOC格式介绍3.2 将中国交通标志检测数据集CCTSDB数据转换成VOC数据格式3.3 手动标注数据集 4 模型训练5 实现效果5.1 视频效果 6 最后 0 前言 🔥 优质…...

深度学习实战67-基于Stable-diffusion的图像生成应用模型的搭建,在Kaggle平台的搭建部署,解决本地没有算力资源问题
大家好,我是微学AI,今天给大家介绍一下深度学习实战67-基于Stable-diffusion的图像生成应用模型的搭建,在Kaggle平台的搭建部署,解决本地没有算力资源问题。稳定扩散模型(Stable Diffusion Model)是一种用于图像增强和去噪的计算机视觉算法。它通过对输入图像进行扩散过程…...

云原生之深入解析Kubernetes本地持久化存储方案OpenEBS LocalPV的最佳实践
一、K8s 本地存储 K8s 支持多达 20 种类型的持久化存储,如常见的 CephFS 、Glusterfs 等,不过这些大都是分布式存储,随着社区的发展,越来越多的用户期望将 K8s 集群中工作节点上挂载的数据盘利用起来,于是就有了 loca…...

设计模式-策略(Strategy)模式
又被称为政策(方针)模式策略模式(Strategy Design Pattern):封装可以互换的行为,并使用委托来决定要使用哪一个策略模式是一种行为设计模式,它能让你定义一系列算法,并将每种算法分别放入独立的类中&#x…...

Star 4.1k!Gitee GVP开源项目!新一代桌面应用开发框架 ElectronEgg!
前言 随着现代技术的快速升级迭代及发展,桌面应用开发已经变得越来越普及。然而对于非专业桌面应用开发工程师在面对这项任务时,可能会感到无从下手,甚至觉得这是一项困难的挑战。 本篇文章将分享一种新型桌面应用开发框架 ElectronEgg&…...

node.js学习(简单聊天室)
在掘金查看该文章 1. TCP服务搭建 1.1 socket 先来粗略了解下socket 套接字(socket)是一个抽象层,应用程序可以通过它发送或接收数据,可对其进行像对文件一样的打开、读写和关闭等操作。套接字允许应用程序将I/O插入到网络中&am…...
cfa一级考生复习经验分享系列(四)
备考CFA一级满打满算用了一个多月,每天八个小时以上。可能如果仅以通过为目标的话完全不用这样,看过太多类似于只看了一周就通过了考试又或是放弃了好几门飘过了考试的情况,我觉得这是不正确的考试状态,完全不必惊叹,踏…...

PPT插件-好用的插件-放映笔、绘图板-大珩助手
放映笔 幻灯片放映时,工具在幻灯片的左下方,本工具在幻灯片的右侧,可以移动,可以方便在右侧讲课时候使用 绘图板 可在绘图板上写签名、绘制图画、写字等等,点画笔切换橡皮擦,点插入绘图,将背景…...

弧形导轨的安装注意事项
随着弧形导轨的应用日渐普遍,在日常使用中总会遇到很多各种各样的问题,原因很多是安装不正确或者使用不恰当。不合理的使用不但不能充分发挥其价值还会导致使用寿命大打折扣,使企业造成不必要的损失,因此大伙有必要了解一些安装的…...

Elasticsearch优化-04
Elasticsearch优化 1、优化-硬件选择 Elasticsearch 的基础是 Lucene,所有的索引和文档数据是存储在本地的磁盘中,具体的路径可在 ES 的配置文件…/config/elasticsearch.yml中配置,如下: # #Path to directory where to store …...

Springboot+vue的公寓报修管理系统(有报告)。Javaee项目,springboot vue前后端分离项目
演示视频: Springbootvue的公寓报修管理系统(有报告)。Javaee项目,springboot vue前后端分离项目 项目介绍: 本文设计了一个基于Springbootvue的前后端分离的公寓报修管理系统,采用M(model&…...
uniapp腾讯地图路线规划
在uniapp中使用腾讯地图进行路线规划需要通过腾讯地图API进行操作。以下是基本的步骤: 在腾讯地图开放平台上注册账号,并创建应用获取API key。 在uniapp的项目中引入腾讯地图API的JS文件,例如在index.html中添加以下代码: <…...
Python 全栈体系【四阶】(五)
第四章 机器学习 三、数据预处理 1. 数据预处理的目的 去除无效数据、不规范数据、错误数据 补齐缺失值 对数据范围、量纲、格式、类型进行统一化处理,更容易进行后续计算 2. 预处理方法 2.1 标准化(均值移除) 让样本矩阵中的每一列的…...
原点处可微问题
文章目录 原点可微问题例例 原点可微问题 lim x → 0 , y → 0 f ( x , y ) − f ( 0 , 0 ) x 2 y 2 \lim\limits_{x\to{0},y\to{0}} \frac{f(x,y)-f(0,0)}{\sqrt{x^2y^2}} x→0,y→0limx2y2 f(x,y)−f(0,0) 0 0 0(1)是函数 f ( x , y ) f(x,y) f(x,y)在 ( 0 , 0 ) (…...

Flink+Kafka消费
引入jar <dependency><groupId>org.apache.flink</groupId><artifactId>flink-java</artifactId><version>1.8.0</version> </dependency> <dependency><groupId>org.apache.flink</groupId><artifactI…...
React Native 开发环境搭建(全平台详解)
React Native 开发环境搭建(全平台详解) 在开始使用 React Native 开发移动应用之前,正确设置开发环境是至关重要的一步。本文将为你提供一份全面的指南,涵盖 macOS 和 Windows 平台的配置步骤,如何在 Android 和 iOS…...
Linux简单的操作
ls ls 查看当前目录 ll 查看详细内容 ls -a 查看所有的内容 ls --help 查看方法文档 pwd pwd 查看当前路径 cd cd 转路径 cd .. 转上一级路径 cd 名 转换路径 …...
使用van-uploader 的UI组件,结合vue2如何实现图片上传组件的封装
以下是基于 vant-ui(适配 Vue2 版本 )实现截图中照片上传预览、删除功能,并封装成可复用组件的完整代码,包含样式和逻辑实现,可直接在 Vue2 项目中使用: 1. 封装的图片上传组件 ImageUploader.vue <te…...

基于Docker Compose部署Java微服务项目
一. 创建根项目 根项目(父项目)主要用于依赖管理 一些需要注意的点: 打包方式需要为 pom<modules>里需要注册子模块不要引入maven的打包插件,否则打包时会出问题 <?xml version"1.0" encoding"UTF-8…...
【决胜公务员考试】求职OMG——见面课测验1
2025最新版!!!6.8截至答题,大家注意呀! 博主码字不易点个关注吧,祝期末顺利~~ 1.单选题(2分) 下列说法错误的是:( B ) A.选调生属于公务员系统 B.公务员属于事业编 C.选调生有基层锻炼的要求 D…...

NFT模式:数字资产确权与链游经济系统构建
NFT模式:数字资产确权与链游经济系统构建 ——从技术架构到可持续生态的范式革命 一、确权技术革新:构建可信数字资产基石 1. 区块链底层架构的进化 跨链互操作协议:基于LayerZero协议实现以太坊、Solana等公链资产互通,通过零知…...

10-Oracle 23 ai Vector Search 概述和参数
一、Oracle AI Vector Search 概述 企业和个人都在尝试各种AI,使用客户端或是内部自己搭建集成大模型的终端,加速与大型语言模型(LLM)的结合,同时使用检索增强生成(Retrieval Augmented Generation &#…...
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数 在软件开发中,单例模式(Singleton Pattern)是一种常见的设计模式,确保一个类仅有一个实例,并提供一个全局访问点。在多线程环境下,实现单例模式时需要注意线程安全问题,以防止多个线程同时创建实例,导致…...

【笔记】WSL 中 Rust 安装与测试完整记录
#工作记录 WSL 中 Rust 安装与测试完整记录 1. 运行环境 系统:Ubuntu 24.04 LTS (WSL2)架构:x86_64 (GNU/Linux)Rust 版本:rustc 1.87.0 (2025-05-09)Cargo 版本:cargo 1.87.0 (2025-05-06) 2. 安装 Rust 2.1 使用 Rust 官方安…...

逻辑回归暴力训练预测金融欺诈
简述 「使用逻辑回归暴力预测金融欺诈,并不断增加特征维度持续测试」的做法,体现了一种逐步建模与迭代验证的实验思路,在金融欺诈检测中非常有价值,本文作为一篇回顾性记录了早年间公司给某行做反欺诈预测用到的技术和思路。百度…...