【Qt源码笔记】深谈 Qt 绘制

2021-07-27 10:11:33 浏览数 (1)

之前写了一篇 浅谈Qt控件绘制 。之所以叫浅谈是因为调用都是比较表层的调用。其实 Qt 的绘制,可以说用 Qt 的人都有用到,但是对于绘制底层,了解的人并不见得很多。我其实之前也是云山雾罩,从来没有深究过。所以想着知其然还是要知其所以然。

结论

在 Windows 平台 默认的 Qt 绘制,最终到底层,是直接调用指令集指令的,这有别于我最初的猜测,我以为是用 Windows API 。这着实让我吃了一惊。这让我对 Qt 的性能又放心了一些。

探究过程

其实研究这个,比其他的更好溯源。附上三段堆栈信息。

代码语言:javascript复制
Qt5Guid.dll!BLEND_SOURCE_OVER_ARGB32_AVX2(unsigned int * dst, const unsigned int * src, const int length) 行 184	C  
	Qt5Guid.dll!qt_blend_argb32_on_argb32_avx2(unsigned char * destPixels, int dbpl, const unsigned char * srcPixels, int sbpl, int w, int h, int const_alpha) 行 253	C  
	Qt5Guid.dll!QRasterPaintEnginePrivate::drawImage(const QPointF & pt, const QImage & img, void(*)(unsigned char *, int, const unsigned char *, int, int, int, int) func, const QRect & clip, int alpha, const QRect & sr) 行 1057	C  
	Qt5Guid.dll!QRasterPaintEngine::drawImage(const QPointF & p, const QImage & img) 行 2250	C  
	Qt5Guid.dll!QRasterPaintEngine::drawPixmap(const QPointF & pos, const QPixmap & pixmap) 行 2128	C  
	Qt5Guid.dll!QPainter::drawPixmap(const QPointF & p, const QPixmap & pm) 行 5079	C  
	Qt5Guid.dll!QPainter::drawPixmap(const QPoint & p, const QPixmap & pm) 行 796	C  
------------------
Qt5Guid.dll!alphargbblend_argb32(unsigned int * dst, unsigned int coverage, const QRgba64 & srcLinear, unsigned int src, const QColorProfile * colorProfile) 行 5771	C  
	Qt5Guid.dll!qt_alphargbblit_argb32(QRasterBuffer * rasterBuffer, int x, int y, const QRgba64 & color, const unsigned int * src, int mapWidth, int mapHeight, int srcStride, const QClipData * clip, bool useGammaCorrection) 行 5878	C  
	Qt5Guid.dll!QRasterPaintEngine::alphaPenBlt(const void * src, int bpl, int depth, int rx, int ry, int w, int h, bool useGammaCorrection) 行 2723	C  
	Qt5Guid.dll!QRasterPaintEngine::drawCachedGlyphs(int numGlyphs, const unsigned int * glyphs, const QFixedPoint * positions, QFontEngine * fontEngine) 行 2976	C  
	Qt5Guid.dll!QRasterPaintEngine::drawTextItem(const QPointF & p, const QTextItem & textItem) 行 3183	C  
	Qt5Guid.dll!QPainterPrivate::drawTextItem(const QPointF & p, const QTextItem & _ti, QTextEngine * textEngine) 行 6531	C  
	Qt5Guid.dll!QTextLine::draw(QPainter * p, const QPointF & pos, const QTextLayout::FormatRange * selection) 行 2615	C  
	Qt5Guid.dll!qt_format_text(const QFont & fnt, const QRectF & _r, int tf, const QTextOption * option, const QString & str, QRectF * brect, int tabstops, int * ta, int tabarraylen, QPainter * painter) 行 7702	C  
	Qt5Guid.dll!QPainter::drawText(const QRect & r, int flags, const QString & str, QRect * br) 行 5955	C  
   -------------------
Qt5Guid.dll!qt_memfill32(unsigned int * dest, unsigned int value, int count) 行 262	C  
	Qt5Guid.dll!qt_memfill<unsigned int>(unsigned int * dest, unsigned int color, int count) 行 901	C  
	Qt5Guid.dll!blend_color_argb(int count, const QT_FT_Span_ * spans, void * userData) 行 4347	C  
	Qt5Guid.dll!qt_span_fill_clipRect(int count, const QT_FT_Span_ * spans, void * userData) 行 4229	C  
	Qt5Guid.dll!QSpanBuffer::flushSpans() 行 112	C  
	Qt5Guid.dll!QSpanBuffer::~QSpanBuffer() 行 87	C  
	Qt5Guid.dll!QRasterizer::rasterizeLine(const QPointF & a, const QPointF & b, double width, bool squareCap) 行 1191	C  
	Qt5Guid.dll!QRasterPaintEngine::fillRect(const QRectF & r, QSpanData * data) 行 1858	C  
	Qt5Guid.dll!QRasterPaintEngine::fillRect(const QRectF & r, const QBrush & brush) 行 1882	C  
	Qt5Guid.dll!QPainter::fillRect(const QRect & r, const QBrush & brush) 行 6971	C  

只要从绘制代码,单步调试即可找到指定地点。不过最终使用的指令集却并不一样。有的用 avx2 、有的则是用 sse2 。如果想探究指令集部分的使用,需要到源码目录 qtbasesrcguipainting ,根据目录下代码文件名即可知道是哪种指令集,一目了然。

回过头来再看上边的那些函数调用。其实不难发现,所有的绘制在中间都必然要经过QPaintEngineQRasterPaintEngine只不过是它的一个派生,这个后边再说。而 QPaintEngine 根据所要绘制的内容,来区分绘制逻辑,比方说涂色采用填充 buffer 、统一刷新的方式;字体绘制要调用字体图元相关绘制逻辑等等。

所有的表层绘制都要经过绘制引擎来向下传递绘制信息。这是 Qt 作为一个高级框架的闪光点,在其他的 Qt 模块也有类似发现,比如控件的绘制上。这样看来 Qt 这个框架能给我们的,除了代码逻辑本身,还有设计。

意外收获

在整个代码探究的过程,我发现了这样一段代码,可以说是非常惊喜了。

代码语言:javascript复制
if (pd->devType() == QInternal::Pixmap)
    static_cast<QPixmap *>(pd)->detach();
else if (pd->devType() == QInternal::Image)
    static_cast<QImage *>(pd)->detach();
d->engine = pd->paintEngine();

这段代码是QPainter::begin()中的代码。当时是在研究QWidget的绘制过程中,走到了这里。只看代码很难体验它的神奇之处。

pd 在前边是 QWidget 的一个指针,当经过这个 if 语句之后,pd 就变成了一个 QImage 指针。这不可谓之不神奇。对于稍微对 Qt 源码有一些理解的同学对 detach() 并不陌生,它本是 Qt 中最常用的 Copy-on-Write 的实现。不过经常用于在类的成员方法中调用,今天看到它这种用法着实惊艳到了。至于为什么这种用法可行,这也是一个可研究的点,有时间,将其整理出来。这段代码算是研究绘制过程中的一个小礼物,这也解开了QWidget绘制的本质。至于QWidget的绘制,也是一个很有意思的东西了,以后有机会详细整理一下。

附注

之前我说QRasterPaintEngine只是QPaintEngine的派生类。我也说 Windows 平台下默认的 Qt 绘制是使用指令集的。原因就在于默认条件下,绝大部分的QPaintDevice是选择用QRasterPaintEngine的,这里我说绝大部分是因为,我没有完整的看过所有派生自QPaintDevice的类的代码。而选择用何种QPaintEngine具体逻辑可以以QImage为例:

代码语言:javascript复制
QPaintEngine *QImage::paintEngine() const
{
    if (!d)
        return 0;
    if (!d->paintEngine) {
        QPaintDevice *paintDevice = const_cast<QImage *>(this);
        QPaintEngine *paintEngine = 0;
        QPlatformIntegration *platformIntegration = QGuiApplicationPrivate::platformIntegration();
        if (platformIntegration)
            paintEngine = platformIntegration->createImagePaintEngine(paintDevice);
        d->paintEngine = paintEngine ? paintEngine : new QRasterPaintEngine(paintDevice);
    }
    return d->paintEngine;
}

简而言之就是取决于QGuiApplicationPrivate::platformIntegration()的返回值。至于这个调用相关的,那是有关 QPA 的范畴了,就不再这篇赘述了。有时间再整理 QPA 相关的内容出来。

后记

对于 Qt 绘制的深入探究,可以说是受益匪浅,这篇文章只是描述了冰山一角,其实整个流程比这个简要概括要高级的多。从研究 Qt 源码至今,对整个 Qt 项目的感受与评价,已和往日截然不同。而网上大部分人对 Qt 的评价,其实在我看来,无异于盲人摸象。只有对源码稍有了解的人,才知道 Qt 这个项目,对于客户端开发人员的价值。

0 人点赞