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 &region,
                                 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();
    }

1
可以看到, 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() and
    endPaint(). 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”,

2
会发现在类 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 of
  the functionality offered by QWindow, QRasterWindow adds a virtual
  paintEvent() function and the possibility to open a QPainter on itself. The
  underlying paint engine will be the raster one, meaning that all drawing will
  happen on the CPU. For performing accelerated, OpenGL-based drawing, use
  QOpenGLWindow instead.

  Internally the class is thin wrapper for QWindow and QBackingStore
  and is very similar to the \l{Raster Window Example}{Raster Window
  Example} that uses these classes directly.

  \sa QPaintDeviceWindow::paintEvent(), QPaintDeviceWindow::update()
*/

class QRasterWindowPrivate : public QPaintDeviceWindowPrivate
{
public:
    void beginPaint(const QRegion &region) 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 &region) 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 process
  d->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 event
                sendPaintEvent(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 border
                if (selected) {
                    p->fillRect(QRect(x1,y2-1,x2-x1,1), tab->palette.window());
                    p->fillRect(QRect(x1,y2,x2-x1,1), tab->palette.window());
                }
                // Left
                if (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);
                }
                // Right
                if (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 绘制椭圆:

*!
    \internal
    Draws 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 1
    while (a*a*(2*y - 1) > 2*b*b*(x + 1)) {
        if (d < 0) { // select E
            d += b*b*(2*x + 3);
            ++x;
        } else {     // select SE
            d += 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 2
    d = 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 SE
            d += b*b*(2*x + 2) + a*a*(-2*y + 3);
            ++x;
        } else {     // select S
            d += 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;

    // topleft
    outline[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;

    // topright
    outline[1].x = x;
    outline[1].len = length;
    outline[1].y = y;
    outline[1].coverage = 255;

    // bottomleft
    outline[2].x = outline[0].x;
    outline[2].len = outline[0].len;
    outline[2].y = midy + (midy - y) - (rect.height() & 0x1);
    outline[2].coverage = 255;

    // bottomright
    outline[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 fill
        fill[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 fill
        fill[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 performance
        while (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是遵循这样的逻辑来进行绘图的:

  1. 控件的绘制逻辑,是在“Style” 系列风格类里具体实现。不同的style显然绘制按钮的样子也不同。
  2. 绘制的操作类是 QPainter,提供各种 draw的具体实现。
  3. 绘制的目的设备是 QPainterDevice,大部分情况下是一个临时 QImage对象,并最终合并到 QWindowsBackingStore::m_image里。
  4. 绘制行为的触发点是Event,比如鼠标或者别的窗口划过了按钮;
  5. 最终绘制生效被显示的位置就是 QWindowsBackingStore::flush。

Qt没有使用windows自带的控件库,而是靠 QWindowsVistaStyle 或者其他的风格类,来硬画图,一个点一条线地画出来。同时,因为上层的绘制全部是在设备无关位图 QImage里绘制的,所以无论是显示、打印或者 render 到一个文件里,上层的代码都是不需要改动的。

这也是为什么Qt可以在嵌入式系统不完全的绘图支持下(比如仅有一个FrameBuf),依旧可以绘制出复杂图形的原因。Qt完全可作为OS一部分而存在,Linux KDE桌面就是基于Qt的实现。同时由于 backing store的可替换性,借助openGL等加速的绘制系统也很方便的集成进来了。

当然,上述跟踪只是Desktop Widgets的一个很小的一撇。Qt在融合各类显示策略上的规划非常宏大。

QtG
p能够直接看到计算机图形学中点线面的生成算法,是Qt作为GUI工具链具备OS特性的一大特征。这不是对各种轮子的反复封装,而是硬核的底层实现!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/238409.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Linux常用命令---- test 命令

文章目录 基本语法文件测试检查文件是否存在检查文件是否是目录检查文件是否为空检查文件是否可读、可写或可执行 字符串测试检查字符串是否为空检查字符串是否相等检查字符串是否不相等 数字测试检查数字是否相等检查数字是否大于或小于 在Linux操作系统中&#xff0c;test命令…

Oracle 透明网关安装

Oracle 11g透明网关连接Sqlserver oracle 透明网关是oracle连接异构数据库提供的一种技术。通过Gateway&#xff0c;可以在Oracle里透明的访问其他不同的数据库&#xff0c;如SQL Server, DB2, Sybase等等&#xff0c;就像远程Oracle数据库一样。配置后的sql查询的处理流程&…

数据库中常用的锁

目录 1、数据库中常用的锁类型 2、常见的数据库 3、以MySQL为例 3.1 MySQL的事务 3.2 MySQL事务的四大特性 1. 原子性&#xff08;Atomicity&#xff09; 2. 一致性&#xff08;Consistency&#xff09; 3. 隔离性&#xff08;Isolation&#xff09; ⭐mysql中的事务隔…

容器化升级对服务有哪些影响?

容器技术是近几年计算机领域的热门技术&#xff0c;特别是随着各种云服务的发展&#xff0c;越来越多的服务运行在以 Docker 为代表的容器之内。 本文我们就来分享一下容器化技术相关的知识。 容器化技术简介 相比传统虚拟化技术&#xff0c;容器技术是一种更加轻量级的操作…

程序员考公笔记之逻辑判断(图形推理)

文章目录 写在前面1、逻辑判断1.1、图形推理1.1.1、位置类1.1.2、样式类1.1.3、数量类1.1.4、属性类1.1.5、六面体 写在前面 1、逻辑判断 1.1、图形推理 观察&#xff1a;先宏观&#xff0c;再微观 图形推理的命题形式&#xff1a; 一组式 观察路径&#xff1a;顺序看(考最…

数据结构之优先级队列(堆)及top-k问题讲解

&#x1f495;"哪里会有人喜欢孤独&#xff0c;不过是不喜欢失望。"&#x1f495; 作者&#xff1a;Mylvzi 文章主要内容&#xff1a;数据结构之优先级队列(堆) 一.优先级队列 1.概念 我们已经学习过队列&#xff0c;队列是一种先进先出(FIFO)的数据结构&#xff…

单线圈无刷直流电机驱动芯片选型分析,可应用于笔记本,显卡风散热风扇,变频冷却风扇,打印机风扇等产品上

单线圈无刷直流电机的电机驱动器。 GC1298R/S&#xff0c;GC1262E/S&#xff0c;GC1298R/S&#xff0c;GC1262R/S具有高效的直接PWM控制方式&#xff0c;它可以控制无刷直流电机转速。它集成了最低速度限制模式、可调速度斜率控制模式、软启动模式、风扇转速计、锁保护、自动重…

PGSQL 设置autovacuum

VACUUM和ANALYZE是PostgreSQL 数据库维护最重要的两个操作。 vacuum用于恢复表中“死元组”占用的空间。删除或更新&#xff08;删除后插入&#xff09;记录时&#xff0c;将产生死元组。PostgreSQL不会从表中物理删除旧行&#xff0c;而是在其上放置一个“标记”&#xff0c;以…

java定位系统源码,UWB技术的无线定位系统源码

UWB技术是一种传输速率高&#xff0c;发射功率较低&#xff0c;穿透能力较强并且是基于极窄脉冲的无线技术。UWB最优的应用环境是室内或者相对密闭的空间&#xff0c;有着厘米级的定位精度&#xff0c;不仅可以非常精准地进行位置跟踪&#xff0c;还可以快速地进行数据传输。 智…

DNF 单机联网 搭建教程(附视频)

更多游戏搭建&pvf修改教程请见: DNF教程 注意&#xff1a;请不要将游戏进行商业化&#xff0c;一切后果概不负责。仅供单机&#xff0c;好友之间进行娱乐&#xff01;&#xff01; 注意&#xff1a;请不要将游戏进行商业化&#xff0c;一切后果概不负责。仅供单机&#…

Vue笔记-在axios中的than函数中使用this需要注意的地方

在Vue中&#xff0c;可以使用this关键字来访问到组件中定义的变量。然而&#xff0c;在axios的then函数中&#xff0c;this关键字的作用域会改变&#xff0c;会指向axios对象本身而不是Vue组件实例。因此&#xff0c;不能直接访问到Vue组件中定义的变量。 解决这个问题的一种方…

git入门教程+常用命令

Git入门教程 本文章主要参照视频教程&#xff1a;https://www.bilibili.com/video/BV1FE411P7B3/?spm_id_from333.337.search-card.all.click&vd_source06caf161b187fb3f4c039bc15e238fea 为什么要使用GIT 版本控制是项目、文档迭代的必然要求&#xff0c;所以需要使用…

如何在 IDEA 中设置远程连接服务器开发环境并实现固定地址远程 Linux 环境

文章目录 1. 检查Linux SSH服务2. 本地连接测试3. Linux 安装Cpolar4. 创建远程连接公网地址5. 公网远程连接测试6. 固定连接公网地址7. 固定地址连接测试 本文主要介绍如何在IDEA中设置远程连接服务器开发环境&#xff0c;并结合Cpolar内网穿透工具实现无公网远程连接&#xf…

肥猫游戏报价器|计价器|王者荣耀代练陪练等游戏报价器软件介绍说明

目录 1. 前言2. 软件著作权3. 软件使用说明3.1 进入软件3.2 用户登录3.3 首页3.4 报价器3.4.1 总体介绍3.4.2 王者报价器3.4.3 LOL手游报价器3.4.4 英雄联盟报价器3.4.5 云顶之弈报价器3.4.7 王者水晶报价器3.4.8 和平精英报价器3.4.9 蛋仔派对报价器3.4.10 穿越火线报价器3.4.…

西工大网络空间安全学院计算机系统基础实验二(phase_2下——漫漫深夜过后的黎明!!!)

内存地址内存地址中的数注释指向这块内存的寄存器0xffffd0e8函数phase_2的栈帧0xffffd0e40xffffd0f4函数phase_2的栈帧0xffffd0e00x5655b7b0函数phase_2的栈帧0xffffd0dc0x565566ca函数read_six_numbers的返回地址&#xff0c;函数phase_2的栈帧0xffffd0d80x5655af64旧%ebx的值…

使用加密软件Enigma Protector,可以轻松获得软件许可!

软件许可是在互联网上销售商业软件以及将程序的使用锁定到特定用户或计算机的最常用方式。在这篇文章中&#xff0c;我将解释什么是软件许可证以及如何使用Enigma Protector将其应用到现有软件。 The Enigma Protector 是一款专门设计用来为应用程序添加高强度保护的强大工具。…

【VTK】VTK中的宏定义

很高兴在雪易的CSDN遇见你 【vtkWidgetRepresentation】第九期 vtk中的仿射变换 前言 本文分享VTK中的宏定义&#xff0c;希望对各位小伙伴有所帮助&#xff01; 感谢各位小伙伴的点赞关注&#xff0c;小易会继续努力分享&#xff0c;一起进步&#xff01; 你的点赞就是我的…

chrome浏览器使用flash player

今天用chrome打开学校校园网&#xff0c;显示不出来成绩单提示如下&#xff1a; 结果下了也没用。 Chrome浏览器在2020年12月已经停止支持Flash Player插件&#xff0c;所以无法在Chrome浏览器上使用Flash Player。 使用其他浏览器 如果之前安装了Flash Player插件的小伙伴&…

关于引用unpkg.com的mars3d相关依赖文件报错无法请求的说明

问题来源&#xff1a; 1.关于引用unpkg.com的mars3d相关依赖文件报错无法请求的说明 说明&#xff1a; 1.最近npm、unpkeg都访问异常&#xff0c;可能是unpkg.com等国外的服务器不稳定导致的请求未响应。 解决方案&#xff1a; 1.请切换静态文件引入的方式请求相关资源。参…

QEMU源码全解析 —— virtio(1)

接前一篇文章&#xff1a; 本文内容参考&#xff1a; 《趣谈Linux操作系统》 —— 刘超&#xff0c;极客时间 《QEMU/KVM》源码解析与应用 —— 李强&#xff0c;机械工业出版社 特此致谢&#xff01; virtio简介 对于一台虚拟机而言&#xff0c;除了要虚拟化CPU和内存&…