作為一套開源跨平臺的UI代碼庫,窗體繪制與響應自然是最為基本的功能。在前面的博文中,已就Qt中的元對象系統(反射機制)、事件循環等基礎內容進行了分析,并捎帶闡述了窗體響應相關的內容。因此,本文著重分析Qt中窗體繪制相關的內容。
在本文最后,通過FreeCAD SheetTableView單元格縮放功能的實現,來對研究分析予以檢驗與測試。
注1:限于研究水平,分析難免不當,歡迎批評指正。
注2:文章內容會不定期更新。
一、坐標系統
在Qt中,每個窗口(準確說是派生于QPaintDevice的C++類)均有一個以像素為單位的二維窗體坐標系,默認情況下,坐標原點位于窗體左上角,軸水平向右,
軸豎直向下。
Ref. from QPaintDevice?
A paint device is an abstraction of a two-dimensional space that can be drawn on using a?QPainter. Its default coordinate system has its origin located at the top-left position. X increases to the right and Y increases downwards. The unit is one pixel.
The drawing capabilities of QPaintDevice are currently implemented by the?QWidget,?QImage,?QPixmap, QGLPixelBuffer,?QPicture, and?QPrinter?subclasses.
?整體上,世界坐標(也稱作邏輯坐標)需要首先轉換成窗體坐標,然后再轉換為設備坐標(也稱作物理坐標)。

將上述坐標變換寫成矩陣變換的形式,如下
其中,
默認值 | ||
|
從中看可以看出,默認情況下,世界坐標與窗體坐標是重合的;但設備坐標則由窗體尺寸、設備尺寸等決定。上述分析,也可以通過QPainter的代碼分析印證。
// src/gui/painting/qpainter.cppvoid QPainter::setViewport(const QRect &r)
{
#ifdef QT_DEBUG_DRAWif (qt_show_painter_debug_output)printf("QPainter::setViewport(), [%d,%d,%d,%d]\n", r.x(), r.y(), r.width(), r.height());
#endifQ_D(QPainter);if (!d->engine) {qWarning("QPainter::setViewport: Painter not active");return;}d->state->vx = r.x();d->state->vy = r.y();d->state->vw = r.width();d->state->vh = r.height();d->state->VxF = true;d->updateMatrix();
}QTransform QPainterPrivate::viewTransform() const
{if (state->VxF) {qreal scaleW = qreal(state->vw)/qreal(state->ww);qreal scaleH = qreal(state->vh)/qreal(state->wh);return QTransform(scaleW, 0, 0, scaleH,state->vx - state->wx*scaleW, state->vy - state->wy*scaleH);}return QTransform();
}
二、窗體繪制
2.1 整體流程


從上述流程分析,可以得到以下結論,
- QWidget update()、repaint()繪制流程幾乎相似,最終均會路由到QWidget::paintEvent函數。不同點在于update通過QCoreApplication::postEvent觸發了QEvent::UpdateLater事件;而repaint()通過QCoreApplication::sendEvent觸發了QEvent::UpdateRequest事件。
Ref. from QWidget::update()?
Updates the widget unless updates are disabled or the widget is hidden.
This function does not cause an immediate repaint; instead it schedules a paint event for processing when Qt returns to the main event loop. This permits Qt to optimize for more speed and less flicker than a call to?repaint() does.
Calling update() several times normally results in just one?paintEvent() call.
Ref. from QWidget::repaint()?
Repaints the widget directly by calling?paintEvent() immediately, unless updates are disabled or the widget is hidden.
We suggest only using repaint() if you need an immediate repaint, for example during animation. In almost all circumstances?update() is better, as it permits Qt to optimize for speed and minimize flicker.
- 對于QPaintEvent事件,相關的繪制區域實際上是在窗口坐標系下描述的,這可通過??QWidgetRepaintManager::paintAndFlush()看出。
void QWidgetRepaintManager::paintAndFlush()
{qCInfo(lcWidgetPainting) << "Painting and flushing dirty"<< "top level" << dirty << "and dirty widgets" << dirtyWidgets;const bool updatesDisabled = !tlw->updatesEnabled();bool repaintAllWidgets = false;const bool inTopLevelResize = tlw->d_func()->maybeTopData()->inTopLevelResize;const QRect tlwRect = tlw->data->crect;const QRect surfaceGeometry(tlwRect.topLeft(), store->size());if ((inTopLevelResize || surfaceGeometry.size() != tlwRect.size()) && !updatesDisabled) {if (hasStaticContents() && !store->size().isEmpty() ) {// Repaint existing dirty area and newly visible area.const QRect clipRect(0, 0, surfaceGeometry.width(), surfaceGeometry.height());const QRegion staticRegion(staticContents(0, clipRect));QRegion newVisible(0, 0, tlwRect.width(), tlwRect.height());newVisible -= staticRegion;dirty += newVisible;store->setStaticContents(staticRegion);} else {// Repaint everything.dirty = QRegion(0, 0, tlwRect.width(), tlwRect.height());for (int i = 0; i < dirtyWidgets.size(); ++i)resetWidget(dirtyWidgets.at(i));dirtyWidgets.clear();repaintAllWidgets = true;}}// ... ...
}
三、分析演練:FreeCAD?SheetTableView鼠標滾動縮放
“學以致用”,作為前面分析研究的驗證與測試,本文拋出下面一個小功能的實現,以期將上述原理串聯起來。
在FreeCAD中,通過引用Sheet內的單元格數據,可以方便的實現幾何參數化建模,同時FreeCAD SpreadsheetGui模塊也提供了SheetTableView來顯示/編輯電子表格。
當參數數據較多時,希望滾動縮放電子表格從而可以在屏幕內完整的顯示整個電子表格,也就是說,鼠標滾動縮放時,要求單元格尺寸、單元格內容同等比例縮放。但SheetTableView目前并不支持此功能。
SheetTableView繼承自QTableView,由horizontal header、vertical header、view port、horizontal scrollbar、vertical scrollbar、corner widget等組成。而且horizontal header、vertical header、QTableView共享了相同的model。

QTableView實際上使用水平/豎直QHeaderView來定位(表格)單元格,也就是說,QTableView利用水平/豎直QHeaderView將窗體坐標轉換成單元格索引,這一點可由QTableView::paintEvent(QPaintEvent *event)看出。
void QTableView::paintEvent(QPaintEvent *event)
{// ... ...for (QRect dirtyArea : region) {dirtyArea.setBottom(qMin(dirtyArea.bottom(), int(y)));if (rightToLeft) {dirtyArea.setLeft(qMax(dirtyArea.left(), d->viewport->width() - int(x)));} else {dirtyArea.setRight(qMin(dirtyArea.right(), int(x)));}// dirtyArea may be invalid when the horizontal header is not stretchedif (!dirtyArea.isValid())continue;// get the horizontal start and end visual sectionsint left = horizontalHeader->visualIndexAt(dirtyArea.left());int right = horizontalHeader->visualIndexAt(dirtyArea.right());if (rightToLeft)qSwap(left, right);if (left == -1) left = 0;if (right == -1) right = horizontalHeader->count() - 1;// get the vertical start and end visual sections and if alternate colorint bottom = verticalHeader->visualIndexAt(dirtyArea.bottom());if (bottom == -1) bottom = verticalHeader->count() - 1;int top = 0;bool alternateBase = false;if (alternate && verticalHeader->sectionsHidden()) {const int verticalOffset = verticalHeader->offset();int row = verticalHeader->logicalIndex(top);for (int y = 0;((y += verticalHeader->sectionSize(top)) <= verticalOffset) && (top < bottom);++top) {row = verticalHeader->logicalIndex(top);if (alternate && !verticalHeader->isSectionHidden(row))alternateBase = !alternateBase;}} else {top = verticalHeader->visualIndexAt(dirtyArea.top());alternateBase = (top & 1) && alternate;}if (top == -1 || top > bottom)continue;// Paint each row itemfor (int visualRowIndex = top; visualRowIndex <= bottom; ++visualRowIndex) {int row = verticalHeader->logicalIndex(visualRowIndex);if (verticalHeader->isSectionHidden(row))continue;int rowY = rowViewportPosition(row);rowY += offset.y();int rowh = rowHeight(row) - gridSize;// Paint each column itemfor (int visualColumnIndex = left; visualColumnIndex <= right; ++visualColumnIndex) {int currentBit = (visualRowIndex - firstVisualRow) * (lastVisualColumn - firstVisualColumn + 1)+ visualColumnIndex - firstVisualColumn;if (currentBit < 0 || currentBit >= drawn.size() || drawn.testBit(currentBit))continue;drawn.setBit(currentBit);int col = horizontalHeader->logicalIndex(visualColumnIndex);if (horizontalHeader->isSectionHidden(col))continue;int colp = columnViewportPosition(col);colp += offset.x();int colw = columnWidth(col) - gridSize;const QModelIndex index = d->model->index(row, col, d->root);if (index.isValid()) {option.rect = QRect(colp + (showGrid && rightToLeft ? 1 : 0), rowY, colw, rowh);if (alternate) {if (alternateBase)option.features |= QStyleOptionViewItem::Alternate;elseoption.features &= ~QStyleOptionViewItem::Alternate;}d->drawCell(&painter, option, index);}}alternateBase = !alternateBase && alternate;}if (showGrid) {// Find the bottom right (the last rows/columns might be hidden)while (verticalHeader->isSectionHidden(verticalHeader->logicalIndex(bottom))) --bottom;QPen old = painter.pen();painter.setPen(gridPen);// Paint each rowfor (int visualIndex = top; visualIndex <= bottom; ++visualIndex) {int row = verticalHeader->logicalIndex(visualIndex);if (verticalHeader->isSectionHidden(row))continue;int rowY = rowViewportPosition(row);rowY += offset.y();int rowh = rowHeight(row) - gridSize;painter.drawLine(dirtyArea.left(), rowY + rowh, dirtyArea.right(), rowY + rowh);}// Paint each columnfor (int h = left; h <= right; ++h) {int col = horizontalHeader->logicalIndex(h);if (horizontalHeader->isSectionHidden(col))continue;int colp = columnViewportPosition(col);colp += offset.x();if (!rightToLeft)colp += columnWidth(col) - gridSize;painter.drawLine(colp, dirtyArea.top(), colp, dirtyArea.bottom());}painter.setPen(old);}}// ... ...
}
因此,要實現QTableView支持鼠標滾動縮放,就需要使QTableView在方向縮放與水平QHeaderView保持一致,而在
方向伸縮與豎直QHeaderView保持一致。
以水平QHeaderView為例,設坐標變換表示為;對于QTableView,設坐標變換為
。則有,
。
另一方面,從代碼實現可以看出,QTableView::paintEvent(QPaintEvent *event)繪制函數采用了默認的變換矩陣。
/*!Paints the table on receipt of the given paint event \a event.
*/
void QTableView::paintEvent(QPaintEvent *event)
{// ... ...QPainter painter(d->viewport);// if there's nothing to do, clear the area and returnif (horizontalHeader->count() == 0 || verticalHeader->count() == 0 || !d->itemDelegate)return;const int x = horizontalHeader->length() - horizontalHeader->offset() - (rightToLeft ? 0 : 1);const int y = verticalHeader->length() - verticalHeader->offset() - 1;// ... ...
}
因此,一種實現方法,就是通過重寫QTableView::paintEvent(QPaintEvent *event),指定合適的變換矩陣來實現QTableView滾動縮放。
具體來說,首先重寫wheelEvent(QWheelEvent* event)以將鼠標滾動輸入轉化成縮放比例,
// following the zoom strategy in SALOME GraphicsView_Viewer
// see SALOME gui/src/GraphicsView/GraphicsView_Viewer.cpp
void SheetTableView::wheelEvent(QWheelEvent* event)
{if (QApplication::keyboardModifiers() & Qt::ControlModifier) {const double d = 1.05;double q = pow(d, -event->delta() / 120.0);this->scale(q, q);event->accept();return;}return QTableView::wheelEvent(event);
}
void SheetTableView::scale(qreal sx, qreal sy)
{//Q_D(QGraphicsView);QTransform matrix = myMatrix;matrix.scale(sx, sy);setTransform(matrix);for (int i = 0; i < horizontalHeader()->count(); ++i) {int s = horizontalHeader()->sectionSize(i);horizontalHeader()->resizeSection(i, s * sx);}for (int i = 0; i < verticalHeader()->count(); ++i) {int s = verticalHeader()->sectionSize(i);verticalHeader()->resizeSection(i, s * sy);}this->update();
}
?然后重寫paintEvent(QPaintEvent* event),依據縮放比例將單元格內容進行縮放。需要注意的是,由于水平/豎直 QHeaderView已經進行了縮放,而QTableView是依據水平/豎直QHeaderView計算單元格坐標,因此,在繪制窗體時,需要使用傳入的窗體坐標;但為了縮放單元格內容,為QPainter指定了變換矩陣,所以單元格坐標要施加矩陣變化
。
void SheetTableView::paintEvent(QPaintEvent* event)
{// ... ...auto matrix = viewportTransform();auto inv_matrix = matrix.inverted();QPainter painter(viewport());painter.setWorldTransform(matrix);// ...for (QRect dirtyArea : region) {// ... ...// Paint each row itemfor (int visualRowIndex = top; visualRowIndex <= bottom; ++visualRowIndex) {int row = verticalHeader->logicalIndex(visualRowIndex);if (verticalHeader->isSectionHidden(row))continue;int rowY = rowViewportPosition(row);rowY += offset.y();int rowh = rowHeight(row) - gridSize;// Paint each column itemfor (int visualColumnIndex = left; visualColumnIndex <= right; ++visualColumnIndex) {int currentBit =(visualRowIndex - firstVisualRow) * (lastVisualColumn - firstVisualColumn + 1)+ visualColumnIndex - firstVisualColumn;if (currentBit < 0 || currentBit >= drawn.size() || drawn.testBit(currentBit))continue;drawn.setBit(currentBit);int col = horizontalHeader->logicalIndex(visualColumnIndex);if (horizontalHeader->isSectionHidden(col))continue;int colp = columnViewportPosition(col);colp += offset.x();int colw = columnWidth(col) - gridSize;const QModelIndex index = model()->index(row, col, rootIndex());if (index.isValid()) {//option.rect = QRect(colp + (showGrid && rightToLeft ? 1 : 0), rowY, colw, rowh);option.rect = inv_matrix.mapRect(QRect(colp + (showGrid && rightToLeft ? 1 : 0), rowY, colw, rowh));if (alternate) {if (alternateBase)option.features |= QStyleOptionViewItem::Alternate;elseoption.features &= ~QStyleOptionViewItem::Alternate;}this->drawCell(&painter, option, index);}}alternateBase = !alternateBase && alternate;}if (showGrid) {// Find the bottom right (the last rows/columns might be hidden)while (verticalHeader->isSectionHidden(verticalHeader->logicalIndex(bottom)))--bottom;QPen old = painter.pen();painter.setPen(gridPen);// Paint each rowfor (int visualIndex = top; visualIndex <= bottom; ++visualIndex) {int row = verticalHeader->logicalIndex(visualIndex);if (verticalHeader->isSectionHidden(row))continue;int rowY = rowViewportPosition(row);rowY += offset.y();int rowh = rowHeight(row) - gridSize;//painter.drawLine(dirtyArea.left(), rowY + rowh, dirtyArea.right(), rowY + rowh);QPoint p1(dirtyArea.left(), rowY + rowh), p2(dirtyArea.right(), rowY + rowh);painter.drawLine(inv_matrix.map(p1), inv_matrix.map(p2));}// Paint each columnfor (int h = left; h <= right; ++h) {int col = horizontalHeader->logicalIndex(h);if (horizontalHeader->isSectionHidden(col))continue;int colp = columnViewportPosition(col);colp += offset.x();if (!rightToLeft)colp += columnWidth(col) - gridSize;//painter.drawLine(colp, dirtyArea.top(), colp, dirtyArea.bottom());QPoint p1(colp, dirtyArea.top()), p2(colp, dirtyArea.bottom());painter.drawLine(inv_matrix.map(p1), inv_matrix.map(p2));}painter.setPen(old);}}//#if QT_CONFIG(draganddrop)
// // Paint the dropIndicator
// d->paintDropIndicator(&painter);
//#endif
}
依據上述方案,的確可以實現QTableView單元格尺寸、單元格內容的滾動縮放,但是存在以下問題:
- 性能問題
QHeaderView由一串連續的section組成,每個section對應一個列/行字段,在section移動過程中,visualIndex會發生變化,但logicalIndex不變。
Ref. from? QHeaderView?
Each header has an?orientation() and a number of sections, given by the?count() function. A section refers to a part of the header - either a row or a column, depending on the orientation.
Sections can be moved and resized using?moveSection() and?resizeSection(); they can also be hidden and shown with?hideSection() and?showSection().
Each section of a header is described by a section ID, specified by its section(), and can be located at a particular?visualIndex() in the header.?
You can identify a section using the?logicalIndex() and?logicalIndexAt() functions, or by its index position, using the?visualIndex() and?visualIndexAt() functions. The visual index will change if a section is moved, but the logical index will not change.
雖然QHeaderView::resizeSection(int logical, int size)可以調整單元格大小,但如果section數較多,逐個調整section尺寸比較卡。
- 縮放QHeaderView
在QHeaderView::paintEvent(QPaintEvent *e)中,所使用QPainter的沒有施加縮放變換矩陣。因此,無法對QHeaderView section內容進行縮放。
網絡資料
Qt源碼分析:QMetaObject實現原理https://blog.csdn.net/qq_26221775/article/details/137023709?spm=1001.2014.3001.5502
Qt源碼分析: QEventLoop實現原理https://blog.csdn.net/qq_26221775/article/details/136776793?spm=1001.2014.3001.5502
QWidgethttps://doc.qt.io/qt-5/qwidget.html
QPainterhttps://doc.qt.io/qt-5/qpainter.html
QPaintDevicehttps://doc.qt.io/qt-5/qpaintdevice.html
QPaintEnginehttps://doc.qt.io/qt-5/qpaintengine.html
Coordinate Systemhttps://doc.qt.io/qt-5/coordsys.html