Qt Desktop Widgets 控件绘图原理逐步分析拆解

news2024/9/20 1:35:31

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/1304321.html

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

相关文章

QX320F28346,TI的TMS320F28346定制的DSP吗?为什么没有模拟外设ADC、DAC等?

QX320F28346&#xff0c;TI的TMS320F28346定制的DSP吗&#xff1f;为什么没有模拟外设ADC、DAC等&#xff1f;

基于Java+vue的音乐网站设计与实现(源码+文档+数据库)

摘 要 在此基础上&#xff0c;提出了一种基于javavue的在线音乐排行榜系统的设计与实现方法。本系统分为两个大的功能&#xff0c;即&#xff1a;前端显示、后端管理。而在前台&#xff0c;则是播放不同的歌曲&#xff0c;让人可以在上面观看不同的歌曲&#xff0c;也可以观看…

Vue--第八天

Vue3 1.优点&#xff1a; 2.创建&#xff1a; 3.文件&#xff1a; 换运行插件&#xff1a; 4.运行&#xff1a; setup函数&#xff1a; setup函数中获取不到this&#xff08;this 在定义的时候是Undefined) reactive()和ref(): 代码&#xff1a; <script setup> // …

springboot listener、filter登录实战

转载自&#xff1a; www.javaman.cn 博客系统访问&#xff1a; http://175.24.198.63:9090/front/index 登录功能 1、前端页面 采用的是layui-admin框架&#xff0c;文中的验证码内容&#xff0c;请参考作者之前的验证码功能 <!DOCTYPE html> <html lang"zh…

如何通过VNC实现公网远程控制macOS设备

&#x1f337;&#x1f341; 博主猫头虎&#xff08;&#x1f405;&#x1f43e;&#xff09;带您 Go to New World✨&#x1f341; &#x1f984; 博客首页——&#x1f405;&#x1f43e;猫头虎的博客&#x1f390; &#x1f433; 《面试题大全专栏》 &#x1f995; 文章图文…

Pinia无废话,快速上手

Pinia无废话&#xff0c;快速上手 Vue3 状态管理 - Pinia 1. 什么是Pinia Pinia 是 Vue 的专属的最新状态管理库 &#xff0c;是 Vuex 状态管理工具的替代品 2. 手动添加Pinia到Vue项目 后面在实际开发项目的时候&#xff0c;Pinia可以在项目创建时自动添加&#xff0c;现…

PDI/Kettle-9.4.0.0-343源码下载及编译

目录 &#x1f351;一、概要&#x1f34a;最新版本10.x&#xff08;2023-11-30&#xff09; &#x1f351;二、下载&#x1f351;三、编译&#x1f34a;3.1、导入开发工具&#x1f34a;3.2、开始编译&#x1f34a;3.3、编译报错&#x1f34a;3.4、报错原因&#xff1a;jdk版本低…

if - else 实现点击展开 / 折叠

在前端开发过程中&#xff0c;我们经常需要使用到点击展开/折叠的按钮。 此案例是一个数组嵌套数组的效果展示&#xff0c;使用的是v-if else 来实现的展开效果。 一、实现方法 if...else&#xff1a;当指定条件为真&#xff0c;if 语句会执行一段语句。如果条件为假&#x…

2023/12/11 作业

1.思维导图 2.作业 成果&#xff1a; 第一个头文件 #ifndef TEST3GET_H #define TEST3GET_H #include <QWidget> #include<QMessageBox> QT_BEGIN_NAMESPACE namespace Ui { class test3get; } QT_END_NAMESPACE class test3get : public QWidget { Q_OBJE…

用docker创建jmeter容器,如何实现性能测试?

用 docker 创建 jmeter 容器, 实现性能测试 我们都知道&#xff0c;jmeter可以做接口测试&#xff0c;也可以用于性能测试&#xff0c;现在企业中性能测试也大多使用jmeter。docker是最近这些年流行起来的容器部署工具&#xff0c;可以创建一个容器&#xff0c;然后把项目放到…

chrome浏览器使用flash player

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

多维时序 | MATLAB实现SAO-CNN-BiGRU-Multihead-Attention多头注意力机制多变量时间序列预测

多维时序 | MATLAB实现SAO-CNN-BiGRU-Multihead-Attention多头注意力机制多变量时间序列预测 目录 多维时序 | MATLAB实现SAO-CNN-BiGRU-Multihead-Attention多头注意力机制多变量时间序列预测预测效果基本介绍模型描述程序设计参考资料 预测效果 基本介绍 MATLAB实现SAO-CNN-B…

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

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

Science Robotics 挖掘机升级智能机器人,充分使用当地材料自主搭建石墙和土墙

建筑业对人类生产力至关重要&#xff0c;但需要实质性创新来满足不断增长的需求并减少其对环境的严重影响。建筑业是世界上最大的经济部门之一&#xff0c;占全球国内生产总值的13%。推而广之&#xff0c;它几乎是所有其他行业的重要组成部分&#xff1a;建筑业负责运输和农业基…

定时补偿方案

1&#xff1a;需求描述 支持NVR升级后通道数变更&#xff0c;完成升级后&#xff0c;设备SDK上报通道数量给A平台&#xff0c;A平台将NVR通道数量同步给B平台&#xff0c;B平台自动调用C平台接口&#xff0c;同步通道数量给C平台&#xff0c;C平台重新生成通道序列号&#xff…

PHP 之道(PHP The Right Way 中文版)

PHP 之道&#xff08;PHP The Right Way 中文版&#xff09;

HeartBeat监控Mysql状态

目录 一、概述 二、 安装部署 三、配置 四、启动服务 五、查看数据 一、概述 使用heartbeat可以实现在kibana界面对 Mysql 服务存活状态进行观察&#xff0c;如有必要&#xff0c;也可在服务宕机后立即向相关人员发送邮件通知 二、 安装部署 参照章节&#xff1a;监控组件…

液态二氧化碳储存罐远程无线监测系统

二氧化碳强化石油开采技术&#xff0c;须先深入了解石油储层的地质特征和二氧化碳的作用机制。现场有8辆二氧化碳罐装车&#xff0c;每辆罐车上有4台液态二氧化碳储罐&#xff0c;每台罐的尾部都装有一台西门子S7-200 smart PLC。在注入二氧化碳的过程中&#xff0c;中控室S7-1…

租一台服务器多少钱决定服务器的价格因素有哪些

租一台服务器多少钱决定服务器的价格因素有哪些 大家好我是艾西&#xff0c;服务器这个名词对于不从业网络行业的人们看说肯定还是比较陌生的。在21世纪这个时代发展迅速的年代服务器在现实生活中是不可缺少的一环&#xff0c;平时大家上网浏览自己想要查询的信息等都是需要服…

terser

环境&#xff1a; 一、使用vueCli创建的项目的vue.config.js 添加terser配置 验证了在打包后生成的.js文件中确实没有了console.log() 这里的.js.map可以省略&#xff0c;公司里代码打包后就没有.js.map文件了 要配置下除去.js.map文件或者统一分到.map文件夹里 二、vite 安…