简述
坐标系统由 QPainter 类控制,再加上 QPaintDevice 和 QPaintEngine,就形成了 Qt 的绘图体系。
- QPainter:用于执行绘图操作
- QPaintDevice:二维空间的抽象层,可以使用 QPainter 在它上面进行绘制。
- QPaintEngine:提供了统一的接口,用于 QPainter 在不同的设备上进行绘制。
QPaintDevice 类是可以被绘制的对象的基类,它的绘图功能由 QWidget、QImage、QPixmap、QPicture 和 QOpenGLPaintDevice 继承。默认坐标系统位于设备的左上角(即:坐标原点 (0, 0))。X 轴由左向右递增,Y 轴由上向下递增。在基于像素的设备上(例如:显示器),坐标的默认单位是 1 像素,在打印机上则是 1 点(1/72 英寸)。
QPainter 逻辑坐标与 QPaintDevice 物理坐标的映射,由 QPainter 的变换矩阵(transformation matrix)、视口(viewport)和窗口(window)完成。默认情况下,物理坐标与逻辑坐标系统是重合的,QPainter 也支持坐标转换,例如:旋转、缩放。
渲染
逻辑表示
一个图形元素(图元)的大小(宽度和高度)总与其数学模型相对应,忽略渲染时画笔的宽度:
锯齿绘制
在绘图时,像素渲染由 QPainter::Antialiasing 来控制。
枚举 RenderHint 用于指定 QPainter 的渲染标志,绘图引擎会用到。QPainter::Antialiasing 表明引擎应该尽可能的让图元边缘抗锯齿,即:使用不同的颜色亮度让边缘平滑。
默认情况下,QPainter 绘制时有锯齿,并且有其它规则:当用 1 像素宽的画笔绘制时,像素会被绘制在右下角。例如:
绘制时,如果画笔的宽度像素是偶数,则实际绘制会包裹住逻辑坐标值;如果是奇数,则是包裹住逻辑坐标值,再加上右下角 1 个像素的偏移。具体请看下面 QRectF 图示:
注意:由于历史原因,QRect::right() 和 QRect::bottom() 的返回值并不是矩形右下角的真实坐标值。
QRect::right() 返回的是 left() + width() – 1,QRect::bottom() 返回的是 top() + height() – 1,上图中右下角的绿色点指出了这两个函数返回的坐标值。
为避免这个问题,建议直接使用 QRectF 来代替:QRectF 使用浮点坐标来定义一个平面矩形(QRect 则使用整形坐标)以确保更加精确,并且函数 QRectF::right() 和 QRectF::bottom() 会返回真正的右下角坐标值。
如果要使用 QRect,可以利用 x() + width() 和 y() + height() 来替代 right() 和 bottom()。
抗锯齿绘制
如果设置了 QPainter 的抗锯齿标志,像素就会被均匀地绘制在两侧。
坐标转换
默认情况下,QPainter 在相关设备自身的坐标系统上操作,但它也完全支持仿射坐标转换。
可以使用 QPainter::scale() 来缩放坐标系统,使用 QPainter::rotate() 来顺时针旋转它,使用 QPainter::translate() 来平移它。
还可以使用 QPainter::shear() 函数绕着原点扭曲坐标系统,所有的转换操作都作用在 QPainter 的转换矩阵上,可以使用 QPainter::worldTransform() 进行检索,一个矩阵转换平面上的一个点到另一个点。
如果要反复地进行相同的转换,可以使用 QTransform 对象和 QPainter::worldTransform()、QPainter::setWorldTransform() 函数。通过调用 QPainter::save() 函数(将矩阵保存在内部堆栈上),可以随时保存 QPainter 的转换矩阵,QPainter::restore() 函数用于恢复上次的结果。
矩阵转换的一个常见的需求是:在不同的绘图设备上使用相同的绘图代码。如果没有转换,结果将与绘图设备的分辨率紧密地联系在一起。打印机有很高的分辨率,例如:600 DPI(每英寸点数),而屏幕的通常为 72 - 100 DPI。
窗口-视口转换
当使用 QPainter 绘图时,我们使用逻辑坐标来指定点,然后将逻辑坐标转换成绘图设备的物理坐标。
逻辑坐标到物理坐标的映射,由 QPainter 世界变换 worldTransform()(“坐标转换”中所描述的部分)、及 QPainter 的 viewport()、window() 处理。视口表示由任意矩形指定的物理坐标,窗口则用逻辑坐标描述了相同的矩形。默认情况下,物理坐标和逻辑坐标一致,与绘图设备的矩形是等价的。
使用窗口-视口(viewport-window)转换,可以使逻辑坐标系统符合我们的喜好,这也可以让绘图代码独立于绘图设备。例如:调用 QPainter::setWindow() 函数,以 (0, 0) 为中心将逻辑坐标从 (-50, -50) 转换到 (50, 50)。
QPainter painter(this);
painter.setWindow(QRect(-50, -50, 100, 100));
现在,逻辑坐标的 (-50,-50) 对应绘图设备物理坐标的 (0, 0) 点。独立于绘图设备,你的绘图代码在指定的逻辑坐标上总能运行。
通过设置“窗口”或视口矩形,可以对坐标进行线性变换。注意: 窗口的每个角落映射到视口的相应角落,反之亦然。因此,让视口和“窗口”保持相同的长宽比通常是一个好主意,可以防止变形:
int side = qMin(width(), height())
int x = (width() - side / 2);
int y = (height() - side / 2);
painter.setViewport(x, y, side, side);
如果让逻辑坐标系统成为一个正方形,那么,也应该使用 QPainter::setViewport() 函数让视口坐标成为一个正方形。上面的示例中,我们让其和最大的正方形相同来适应绘图设备的矩形。在设置窗口或视口时,考虑到绘图设备的大小,也能够保持绘图代码独立于绘制设备。
注意: 窗口-视口转换只是一个线性转换,即:它不执行剪切。这意味着,如果在当前设置的“窗口”之外绘制,绘制依然会被转换到视口,使用相同的线性代数方法。
视口、“窗口”和矩阵转换,决定了如何将逻辑 QPainter 坐标映射到绘图设备的物理坐标。默认情况下,世界变换矩阵是单位矩阵,“窗口”和视口设置等同于绘图设备的设置,即:世界、“窗口”和设备坐标系统是等价的。正如我们所看到的,系统可以使用转换运算和窗口-视口转换进行操作,上图说明了该过程。