当前位置: 首页 > news >正文

Qt源码分析:窗体绘制与响应

作为一套开源跨平台的UI代码库,窗体绘制与响应自然是最为基本的功能。在前面的博文中,已就Qt中的元对象系统(反射机制)、事件循环等基础内容进行了分析,并捎带阐述了窗体响应相关的内容。因此,本文着重分析Qt中窗体绘制相关的内容。

在本文最后,通过FreeCAD SheetTableView单元格缩放功能的实现,来对研究分析予以检验与测试。

注1:限于研究水平,分析难免不当,欢迎批评指正。

注2:文章内容会不定期更新。

一、坐标系统

在Qt中,每个窗口(准确说是派生于QPaintDevice的C++类)均有一个以像素为单位的二维窗体坐标系,默认情况下,坐标原点位于窗体左上角,x轴水平向右,y轴竖直向下。

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.

 整体上,世界坐标(也称作逻辑坐标)需要首先转换成窗体坐标,然后再转换为设备坐标(也称作物理坐标)。

Ref. from Qt Coordinate System

将上述坐标变换写成矩阵变换的形式,如下

\left ( x_{device}^{'},y_{device}^{'}, w\right )=\left ( x_{world},y_{world}, 1\right )\cdot \mathbf{W}\cdot \mathbf{V}

x_{device}=\frac{x_{device}^{'}}{w}

y_{device}=\frac{y_{device}^{'}}{w}

其中,

默认值
\mathbf{W}=\begin{bmatrix} w_{11} & w_{12} & w_{13}\\ w_{21} & w_{22} & w_{23}\\ w_{31}& w_{32} & w_{33} \end{bmatrix}\begin{bmatrix} 1 & 0 & 0\\ 0& 1& 0\\ 0& 0 & 1 \end{bmatrix}
\mathbf{V}=\begin{bmatrix} v_{11} & v_{12} & v_{13}\\ v_{21} & v_{22} & v_{23}\\ v_{31}& v_{32} & v_{33} \end{bmatrix}\begin{bmatrix} \frac{vw}{ww} & 0 &0 \\ 0& \frac{vh}{wh} & 0\\ vx-wx\frac{vw}{ww} & vy-wy\frac{vh}{wh} &1 \end{bmatrix}

wx:窗体左边界坐标

wy:窗体上边界坐标

ww: 窗体宽度

wh: 窗体高度

vx:设备左边界坐标

vy:设备上边界坐标

vw: 设备宽度

vh: 设备高度

从中看可以看出,默认情况下,世界坐标与窗体坐标是重合的;但设备坐标则由窗体尺寸、设备尺寸等决定。上述分析,也可以通过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()
QWidget::repaint()

从上述流程分析,可以得到以下结论,

  • 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 portrait

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在x方向缩放与水平QHeaderView保持一致,而在y方向伸缩与竖直QHeaderView保持一致。

以水平QHeaderView为例,设坐标变换表示为\boldsymbol{h}_{d}=\boldsymbol{h}\cdot \boldsymbol{ W}_{1}\cdot \boldsymbol{V}_{1};对于QTableView,设坐标变换为\boldsymbol{x}_{d}=\boldsymbol{x}\cdot \boldsymbol{ W}_{2}\cdot \boldsymbol{V}_{2}。则有,\boldsymbol{x}_{d}=\boldsymbol{x}\cdot \left (\boldsymbol{ W}_{2}\cdot \boldsymbol{ W}_{1}^{-1} \right )\cdot \boldsymbol{ W}_{1}\cdot \boldsymbol{V}_{2}

另一方面,从代码实现可以看出,QTableView::paintEvent(QPaintEvent *event)绘制函数采用了默认的变换矩阵\boldsymbol{W}=\boldsymbol{I}

/*!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),指定合适的变换矩阵\boldsymbol{W}来实现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指定了变换矩阵\boldsymbol{W},所以单元格坐标要施加矩阵变化\boldsymbol{W}^{-1}

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实现原理icon-default.png?t=N7T8https://blog.csdn.net/qq_26221775/article/details/137023709?spm=1001.2014.3001.5502

Qt源码分析: QEventLoop实现原理icon-default.png?t=N7T8https://blog.csdn.net/qq_26221775/article/details/136776793?spm=1001.2014.3001.5502

QWidgeticon-default.png?t=N7T8https://doc.qt.io/qt-5/qwidget.html
QPaintericon-default.png?t=N7T8https://doc.qt.io/qt-5/qpainter.html
QPaintDeviceicon-default.png?t=N7T8https://doc.qt.io/qt-5/qpaintdevice.html
QPaintEngineicon-default.png?t=N7T8https://doc.qt.io/qt-5/qpaintengine.html
Coordinate Systemicon-default.png?t=N7T8https://doc.qt.io/qt-5/coordsys.html

相关文章:

  • js文件的执行和变量初始化缓存
  • Java面向对象练习(1.手机类)(2024.7.4)
  • Liunx网络配置
  • Sqlmap中文使用手册 - 各个参数介绍(持续更新)
  • 每日两题 / 20. 有效的括号 155. 最小栈(LeetCode热题100)
  • 实验五 数据库完整性约束的实现与验证
  • linux守护进程生命周期管理-supervisord
  • ABAP开发:动态Open SQL编程案例介绍
  • elasticsearch 8.14.1 和 Spring Data elasticsearch 实例演示
  • React 中 useState 和 useReducer 的联系和区别
  • spring security + vue,登录功能
  • 2024鲲鹏昇腾创新大赛集训营Ascend C算子学习笔记
  • docker k8s
  • CentOS 7 内存占用过大导致 OOM Killer 杀掉了 Java 进程
  • AI是在帮助开发者还是取代他们?
  • 【162天】黑马程序员27天视频学习笔记【Day02-上】
  • canvas 高仿 Apple Watch 表盘
  • exif信息对照
  • Java多线程(4):使用线程池执行定时任务
  • mongodb--安装和初步使用教程
  • ⭐ Unity 开发bug —— 打包后shader失效或者bug (我这里用Shader做两张图片的合并发现了问题)
  • 当SetTimeout遇到了字符串
  • 汉诺塔算法
  • 线性表及其算法(java实现)
  • 一个普通的 5 年iOS开发者的自我总结,以及5年开发经历和感想!
  • 怎么把视频里的音乐提取出来
  • 怎样选择前端框架
  • 阿里云API、SDK和CLI应用实践方案
  • ​人工智能书单(数学基础篇)
  • ​油烟净化器电源安全,保障健康餐饮生活
  • #HarmonyOS:基础语法
  • #知识分享#笔记#学习方法
  • %check_box% in rails :coditions={:has_many , :through}
  • (2015)JS ES6 必知的十个 特性
  • (Oracle)SQL优化技巧(一):分页查询
  • (react踩过的坑)Antd Select(设置了labelInValue)在FormItem中initialValue的问题
  • (非本人原创)我们工作到底是为了什么?​——HP大中华区总裁孙振耀退休感言(r4笔记第60天)...
  • (附源码)springboot“微印象”在线打印预约系统 毕业设计 061642
  • (五)MySQL的备份及恢复
  • (五)大数据实战——使用模板虚拟机实现hadoop集群虚拟机克隆及网络相关配置
  • (一)为什么要选择C++
  • (杂交版)植物大战僵尸
  • (转)VC++中ondraw在什么时候调用的
  • .Net 4.0并行库实用性演练
  • .NET 6 在已知拓扑路径的情况下使用 Dijkstra,A*算法搜索最短路径
  • .NET C# 使用 SetWindowsHookEx 监听鼠标或键盘消息以及此方法的坑
  • .Net CoreRabbitMQ消息存储可靠机制
  • .Net Web项目创建比较不错的参考文章
  • .NET 药厂业务系统 CPU爆高分析
  • .NET国产化改造探索(三)、银河麒麟安装.NET 8环境
  • .net经典笔试题
  • .net通用权限框架B/S (三)--MODEL层(2)
  • .NET业务框架的构建
  • .NET中GET与SET的用法
  • /usr/local/nginx/logs/nginx.pid failed (2: No such file or directory)