Qt Widgets 绘图行为逐步分析拆解

news2024/12/23 6:15:02

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特性的一大特征。这不是对各种轮子的反复封装,而是硬核的底层实现!

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【EMNLP 2023】面向垂直领域的知识预训练语言模型

近日&#xff0c;阿里云人工智能平台PAI与华东师范大学数据科学与工程学院合作在自然语言处理顶级会议EMNLP2023上发表基于双曲空间和对比学习的垂直领域预训练语言模型。通过比较垂直领域和开放领域知识图谱数据结构的不同特性&#xff0c;发现在垂直领域的图谱结构具有全局稀…

Flink之迟到的数据

迟到数据的处理 推迟水位线推进: WatermarkStrategy.<Event>forBoundedOutOfOrderness(Duration.ofSeconds(2))设置窗口延迟关闭&#xff1a;.allowedLateness(Time.seconds(3))使用侧流接收迟到的数据: .sideOutputLateData(lateData) public class Flink12_LateDataC…

【华为数据之道学习笔记】3-9元数据治理面临的挑战

华为在进行元数据治理以前&#xff0c;遇到的元数据问题主要表现为数据找不到、读不懂、不可信&#xff0c;数据分析师们往往会陷入数据沼泽中&#xff0c;例如以下常见的场景。 某子公司需要从发货数据里对设备保修和维保进行区分&#xff0c;用来不对过保设备进行服务场景分析…

uniapp - 简单版本自定义tab栏切换

tab切换是APP开发最常见的功能之一&#xff0c;uniapp中提供了多种形式的tab组件供我们使用。对于简单的页面而言&#xff0c;使用tabbar组件非常方便快捷&#xff0c;可以快速实现底部导航栏的效果。对于比较复杂的页面&#xff0c;我们可以使用tab组件自由定义样式和内容 目录…

Spring 的缓存机制【记录】

一、背景 在最近的业务需求开发过程中遇到了“传说中”的循环依赖问题&#xff0c;在之前学习Spring的时候经常会看到Spring是如何解决循环依赖问题的&#xff0c;所谓循环依赖即形成了一个环状的依赖关系&#xff0c;这个环中的某一个点产生不稳定变化都会导致整个链路产生不…

WPF仿网易云搭建笔记(5):信息流控制之IOC容器

文章目录 专栏和Gitee仓库前言IOC容器Prism IOC使用声明两个测试的服务类MainWindow IOC 注入[单例]MainWindow里面获取UserController无法使用官方解决方案 使用自定义IOC容器&#xff0c;完美解决既然Prism不好用&#xff0c;直接上微软的IOC解决方案App.xaml.csViewModel里面…

axios 基础的 一次封装 二次封装

一、平常axios的请求发送方式 修改起来麻烦的一批 代码一大串 二、axios的一次封装 我们会在src/utils创建一个request.js的文件来存放我们的基地址与拦截器 /* 封装axios用于发送请求 */ import axios from axios/* (1)request 相当于 Axios 的实例对象 (2)为什么要有reque…

python自动化测试实战 —— WebDriver API的使用

软件测试专栏 感兴趣可看&#xff1a;软件测试专栏 自动化测试学习部分源码 python自动化测试相关知识&#xff1a; 【如何学习Python自动化测试】—— 自动化测试环境搭建 【如何学习python自动化测试】—— 浏览器驱动的安装 以及 如何更…

Web安全-SQL注入【sqli靶场第11-14关】(三)

★★实战前置声明★★ 文章中涉及的程序(方法)可能带有攻击性&#xff0c;仅供安全研究与学习之用&#xff0c;读者将其信息做其他用途&#xff0c;由用户承担全部法律及连带责任&#xff0c;文章作者不承担任何法律及连带责任。 0、总体思路 先确认是否可以SQL注入&#xff0…

深拷贝、浅拷贝 react的“不可变值”

知识获取源–晨哥&#xff08;现实中的人 嘿嘿&#xff09; react中如果你想让一个值始终不变 或者说其他操作不影响该值 它只是作用初始化的时候 使用了浅拷贝–改变了初始值 会改变初始值(selectList1) 都指向同一个地址 const selectList1 { title: 大大, value: 1 };con…

ES-分析器

分析器 两种常用的英语分析器 1 测试工具 #可以通过这个来测试分析器 实际生产环境中我们肯定是配置在索引中来工作 GET _analyze {"text": "My Moms Son is an excellent teacher","analyzer": "english" }2 实际效果 比如我们有下…

前端框架(Front-end Framework)和库(Library)的区别

聚沙成塔每天进步一点点 ⭐ 专栏简介 前端入门之旅&#xff1a;探索Web开发的奇妙世界 欢迎来到前端入门之旅&#xff01;感兴趣的可以订阅本专栏哦&#xff01;这个专栏是为那些对Web开发感兴趣、刚刚踏入前端领域的朋友们量身打造的。无论你是完全的新手还是有一些基础的开发…

基于OpenCV+CNN+IOT+微信小程序智能果实采摘指导系统——深度学习算法应用(含python、JS工程源码)+数据集+模型(三)

目录 前言总体设计系统整体结构图系统流程图 运行环境Python环境TensorFlow 环境Jupyter Notebook环境Pycharm 环境微信开发者工具OneNET云平台 模块实现1. 数据预处理1&#xff09;爬取功能2&#xff09;下载功能 2. 创建模型并编译1&#xff09;定义模型结构2&#xff09;优化…

appium安卓app自动化,遇到搜索框无搜索按钮元素时无法搜索的解决方案

如XX头条&#xff0c;搜索框后面有“搜索”按钮&#xff0c;这样实现搜索操作较为方便。 但有些app没有设置该搜索按钮&#xff0c;初学者就要花点时间去学习怎么实现该功能了&#xff0c;如下图。 这时候如果定位搜索框&#xff0c;再点击操作&#xff0c;再输入文本后&#x…

java工程(ajax/axios/postman)向请求头中添加消息

1、问题概述 在项目中我们经常会遇到需要向请求头中添加消息的场景&#xff0c;然后后端通过request.getRequest()或者RequestHeader获取请求头中的消息。 下面提供几种前端向请求头添加消息的方式 2、创建一个springmvc工程用于测试 2.1、创建工程并引入相关包信息 sprin…

Maven项目引入本地jar

Maven项目引入本地jar 1.对应maven模块项目中建lib目录&#xff0c;将jar放入进去 2.在对应的模块pom.xml中引入此依赖jar 3.在对应的maven-plugin插件打包的pom.xml中指定需要includeSystemScope为true的jar

做数据分析为何要学统计学(10)——如何进行时间序列分析

时间序列是由随时间变化的值构成&#xff0c;如产品销量、气温数据等等。通过对时间序列展开分析&#xff0c;能够回答如下问题&#xff1a; &#xff08;1&#xff09;被研究对象的活动特征是否有周期性&#xff08;也称季节性&#xff09;&#xff08;2&#xff09;被研究对…

strict-origin-when-cross-origin

严格限制同源策略 &#xff08;1&#xff09;允许服务器的同源IP地址访问 &#xff08;2&#xff09;允许Referer --- 后端服务器要配置

2023年阿里云云栖大会-核心PPT资料下载

一、峰会简介 历经14届的云栖大会&#xff0c;是云计算产业的建设者、推动者、见证者。2023云栖大会以“科技、国际、年轻”为基调&#xff0c;以“计算&#xff0c;为了无法计算的价值”为主题&#xff0c;发挥科技平台汇聚作用&#xff0c;与云计算全产业链上下游的先锋代表…

树莓派,opencv,Picamera2利用舵机云台追踪人脸

一、需要准备的硬件 Raspiberry 4b两个SG90 180度舵机&#xff08;注意舵机的角度&#xff0c;最好是180度且带限位的&#xff0c;切勿选360度舵机&#xff09;二自由度舵机云台&#xff08;如下图&#xff09;Raspiberry CSI 摄像头 组装后的效果&#xff1a; 二、项目目标…