Qt 绘图系统

参考:Paint System

Paint System 是使用相同 API 在屏幕或打印设备上绘画的系统。这里的 API 主要基于 QPainterQPaintDevice,和 QPaintEngine 类。

QPainter用于执行绘制操作,QPaintDevice是可以使用QPainter绘制的二维空间的抽象,并且QPaintEngine提供了绘制器用来在不同类型的设备上绘制的接口。QPaintEngine类由QPainterQPaintDevice内部使用,并且除非它们创建自己的设备类型,否则对应用程序程序员而言是隐藏的。

更多细节参考 Topics

1 绘图设备

QPaintDevice类是可以用QPainter进行绘图的设备对象(也可以视为画板)的基类。paint device(QPaintDevice 及其子类的实例对象)是可以使用QPainter绘图的二维空间的一种抽象。其默认坐标系的原点位于左上角。X 向右增加,Y 向下方增加。单位是一个像素。

QPaintDeviceQBitmap, QImage, QPagedPaintDevice, QPicture, QPixmap, QGLFramebufferObject, QGLPixelBuffer, QPrinter, QSvgGenerator 继承。

QPaintDevice 的绘图功能由 QWidget[1], QImage[2], QPixmap[3], QPicture[4], 和 QOpenGLPaintDevice[5]实现。QPaintDevice构造一个绘画设备且只能从QPaintDevice的子类调用此构造函数。

对新后端的支持可以通过从QPaintDevice类派生并重新实现 virtual 函数 paintEngine() 来实现,以告诉QPainter应该使用哪个绘画引擎在此特定设备上进行绘制。为了真正能够在设备上绘画,此绘画引擎必须是通过从QPaintEngine类派生而创建的自定义绘画引擎。

警告:Qt 要求在创建任何绘画设备之前必须存在 QGuiApplication对象。绘画设备访问窗口系统资源,并且在创建应用程序对象之前不会初始化这些资源。

QPaintDevice类提供了几个返回各种设备指标(metrics)的函数:depth()函数返回其位深度(位平面的数量)。height()函数以默认坐标系单位(default coordinate system units,例如QPixmapQWidget的像素)返回其高度,而heightMM()返回以毫米为单位的设备高度。类似地,width()widthMM()函数分别以默认坐标系单位和毫米为单位返回设备的宽度。或者,通过将所需的PaintDeviceMetric指定为参数,可以使用 protected 函数metric()PySide2.QtGui.QPaintDevice.PaintDeviceMetric)来检索度量信息。

logicalDpiX()logicalDpiY() 函数以每英寸点数返回设备的水平和垂直分辨率(resolution)。physicalDpiX()physicalDpiY() 函数也返回以英寸为单位的设备分辨率,但请注意,如果逻辑分辨率和物理分辨率不同,则相应的QPaintEngine必须处理映射。最后,colorCount()函数返回可用于绘制设备的不同颜色的数量。

1.1 OffScreen Rendering

GPU 屏幕渲染有两种方式:

  • On-Screen Rendering 当前屏幕渲染,是指渲染操作是在当前用于显示屏幕缓冲区中进行的。
  • Off-Screen Rendering 离屏渲染,指的是GPU在当前屏幕缓冲区以外新开辟一个缓冲区进行渲染操作。

另一种特殊的离屏渲染:CPU 渲染

如果我们重写了 drawRect 方法,并且使用任何 Core Graphics 的技术进行了绘制操作,就涉及到了 CPU 渲染。整个渲染过程由 CPU 在 App 内同步地完成,渲染得到的 bitmap 最后再交由 GPU 用于显示。

相比于当前屏幕渲染,离屏渲染的代价比较高,主要体现在两个方面:

  • 创建新缓冲区
  • 想要离屏渲染,就必须创建一个新的缓冲区;
  • 上下文切换
  • 离屏渲染的整个过程,需要多次切换上下文环境:先是从当前屏幕(On-Screen)切换到离屏(Off-Screen);等到离屏渲染结束以后,将离屏缓冲区的渲染结果显示到屏幕上。而上下文环境的切换是要付出很大代价的。

1.2 图像处理类

Qt 提供了四个用于处理图像数据的类:QImageQPixmapQBitmapQPicture

QImage是为 I/O 设计的,并针对直接像素访问和操作进行了优化,而QPixmap是为在屏幕上显示图像而设计和优化的。QBitmap只是继承QPixmap并确保深度为 1 的便捷类。如果QPixmap对象确实是位图,则isQBitmap()函数将返回True,否则返回False。最后,QPicture类是一种绘画设备,可以记录并重放QPainter命令。

2 Drawing

QPainter提供了高度优化的功能,可以完成大多数 GUI 程序所需的功能。它可以绘制所有内容,从简单的图形基元(由QPointQLineQRectQRegionQPolygon 类表示)到复杂的形状(如矢量路径)。 在 Qt 中,矢量路径由QPainterPath类表示。QPainterPath提供了一个用于绘画操作的容器,从而可以构造和重用图形形状。

painter path 是由直线和曲线组成的对象。例如,矩形由直线组成,椭圆由曲线组成。与常规绘图操作相比,painter path 的主要优势在于,复杂的形状仅需要创建一次;那么仅使用 drawPath() 函数即可将它们绘制多次。

QPainterPath 对象可用于填充,概述和裁剪。要为给定的 painter path 生成可填充的轮廓,请使用 QPainterPathStroker 类。

使用 QPen 类绘制线条和轮廓(Lines and outlines)。笔由其样式(style,i.e. its line-type),宽度,画刷(brush),如何绘制端点(endpoints,cap-style)以及如何绘制两条相连线之间的连接(join-style)定义。笔刷是一个 QBrush 对象,用于填充用笔生成的笔划,即QBrush类定义填充图案。

QPainter还可以绘制对齐的文本和像素图。

绘制文本时,使用 QFont 类指定字体。Qt 将使用具有指定属性的字体,或者如果不存在匹配的字体,则 Qt 将使用最接近的匹配安装字体。可以使用 QFontInfo 类来检索实际使用的字体的属性。此外,QFontMetrics 类提供字体度量,而 QFontDatabase 类提供有关基础窗口系统中可用字体的信息。

通常,QPainter会绘制“自然”坐标系,但是它能够使用 QTransform 类执行视图和世界变换。

绘制时,像素渲染由“抗锯齿”渲染提示控制。RenderHint枚举用于指定QPainter的标志,任何给定的引擎可能会或可能不会考虑这些标志。Antialiasing 值指示引擎应尽可能对图元的边缘进行抗锯齿,即通过使用不同的颜色强度对边缘进行平滑处理。

3 Filling

使用QBrush类填充形状。画刷由其颜色和样式(即填充样式)定义。

Qt 中的任何颜色都由支持 RGB,HSV 和 CMYK 颜色模型的 QColor 类表示。QColor还支持 alpha 混合的轮廓和填充(指定透明效果),并且该类与平台和设备无关(使用 QColormap 类将颜色映射到硬件)。

可用的填充模式由BrushStyle枚举描述。这些包括从均匀颜色到非常稀疏的图案的基本图案,各种线条组合,渐变填充和纹理。Qt 提供了 QGradient 类来定义自定义渐变填充,而纹理图案是使用QPixmap类指定的。

QGradient类与QBrush结合使用以指定渐变填充。

Qt 当前支持三种类型的渐变填充:线性渐变(Linear gradients)在起点和终点之间插值颜色;径向渐变(radial gradients)在焦点与终点之间的圆上的终点之间插值颜色;圆锥形渐变(conical gradients)在中心点周围插值颜色。

4 Coordinate System

有关绘制系统使用的坐标系的信息。坐标系由QPainter类控制。QPainterQPaintDeviceQPaintEngine类一起构成了 Qt 绘画系统 Arthur 的基础。

paint device 的默认坐标系的原点位于左上角。X 值向右增加,Y 值向下增加。在基于像素的设备上,默认单位是一个像素,在打印机上,默认单位是一个点(1/72 英寸)。

logical QPainter 坐标到 physical QPaintDevice 坐标的映射由 QPainter 的转换矩阵,视口(viewport)和 “window”处理。默认情况下,逻辑坐标系和物理坐标系重合。QPainter还支持坐标转换(例如旋转和缩放)。

4.1 Rendering

4.2 Logical Representation

图形基元的大小(宽度和高度)始终与其数学模型相对应,而忽略用以下方式呈现的笔的宽度:

4.3 Aliased Painting

绘制时,像素渲染由 Antialiasing 渲染提示控制。RenderHint 枚举用于指定QPainter的标志,任何给定的引擎可能会或可能不会考虑这些标志。 Antialiasing 值指示引擎应尽可能对图元的边缘进行抗锯齿,即通过使用不同的颜色强度对边缘进行平滑处理。但是默认情况下,画家是别名的,并且适用其他规则:当使用一个像素宽的笔进行渲染时,像素将在数学上定义的点的右侧和下方渲染。 例如:

当使用像素数为偶数的笔进行渲染时,将在数学定义的点周围对称地渲染像素,而使用像素数为奇数的笔进行渲染时,备用像素将在数学点的右边和下方渲染 与一像素的情况一样。更多信息参考:Qt:QRect 和 QRectF

请注意,由于历史原因,right()bottom()函数的返回值偏离矩形的真实右下角。QRectright()函数返回left()+ width() -1bottom()函数返回top()+ height() -1。我们建议您改用QRectFQRectF类使用浮点坐标定义平面中的矩形以提高准确性(QRect使用整数坐标),而right()bottom()函数确实返回真正的右下角。或者,使用QRect,应用x()+ width()y()+ height()来找到右下角,并避免使用right()bottom()函数。

4.4 Anti-aliased Painting

如果您设置QPainter的抗锯齿渲染提示(anti-aliasing render hint),则像素将在数学定义的点的两侧对称地渲染:

4.5 Transformations

默认情况下,QPainter在关联设备自己的坐标系上运行,但是它也完全支持仿射坐标变换。您可以使用 scale() 函数按给定的偏移量缩放坐标系,可以使用 rotate() 函数顺时针旋转坐标系,并可以使用 translate() 函数平移坐标系(即将给定偏移量添加到点)。

您还可以使用shear()函数围绕原点扭曲坐标系。 所有转换操作都在QPainter的转换矩阵上进行,您可以使用worldTransform()函数进行检索。矩阵变换将平面中的一个点转换为另一个点。如果需要反复进行相同的转换,则还可以使用QTransform对象以及worldTransform()setWorldTransform()函数。您随时可以调用save()函数保存QPainter的转换矩阵,该函数将矩阵保存在内部堆栈中。restore()函数将其弹出。经常需要转换矩阵,这是在各种绘图设备上重复使用相同的绘图代码时。如果不进行转换,则结果将紧密绑定到绘画设备的分辨率。打印机具有高分辨率,例如 每英寸600点,而屏幕通常每英寸72至100点。

4.6 Window-Viewport Conversion

使用QPainter进行绘制时,我们使用逻辑坐标指定点,然后将其转换为绘画设备的物理坐标。

逻辑坐标到物理坐标的映射由QPainter的世界转换worldTransform()以及viewport()window()处理。视口表示指定任意矩形的物理坐标。“window” 在逻辑坐标中描述了相同的矩形。 默认情况下,逻辑坐标系和物理坐标系重合,并且等效于绘画设备的矩形。使用窗口视口转换,可以使逻辑坐标系适合您的首选项。该机制还可以用于使绘图代码独立于 paint device。例如,您可以通过调用setWindow()函数使逻辑坐标从(-50, -50)扩展到(50, 50),并在中心具有(0,0)

painter = QPainter(self)
painter.setWindow(QRect(-50, -50, 100, 100))

现在,逻辑坐标(-50, -50)对应于绘图设备的物理坐标(0,0)。独立于绘制设备,绘制代码将始终在指定的逻辑坐标上运行。

通过设置“window”或视口矩形,可以对坐标进行线性变换。请注意,“window”的每个角都映射到视口的相应角,反之亦然。因此,通常最好使视口和“window”保持相同的宽高比以防止变形:

int side = qMin(width(), height())
int x = (width() - side / 2);
int y = (height() - side / 2);

painter.setViewport(x, y, side, side);

如果将逻辑坐标系设为正方形,则还应该使用setViewport()函数将视口设为正方形。在上面的示例中,我们将其等效为适合绘画设备矩形的最大正方形。在设置窗口或视口时,通过考虑绘画设备的尺寸,可以使绘图代码独立于绘画设备。请注意,窗口-视口转换仅是线性转换,即不执行裁剪。这意味着,如果在当前设置的“window”之外进行绘制,则您的绘制仍将使用相同的线性代数方法转换为视口。

视口,"window"和转换矩阵确定逻辑QPainter坐标如何映射到绘图设备的物理坐标。默认情况下,世界变换矩阵为单位矩阵,并且"window"和视口设置等同于绘画设备的设置,即世界,"window"和设备坐标系是等效的,但是正如我们所看到的,系统可以 使用转换操作和窗口视口转换进行操纵。上图说明了该过程。示例可参考:Analog Clock Example

5 Reading and Writing Image Files

读取图像的最常见方式是通过QImageQPixmap的构造函数,或者通过调用load()函数。另外,Qt 提供了QImageReader类,该类提供了对该过程的更多控制。根据图像格式的基本支持,该类提供的功能可以节省内存并加快图像加载速度。

同样,Qt 提供了QImageWriter类,该类支持在存储图像之前设置格式特定的选项,例如 gamma 级别,压缩级别和质量。如果不需要此类选项,则可以改用save()函数。

QMovie 是内部使用QImageReader类显示动画的便捷类。创建后,QMovie类将提供用于运行和控制给定动画的各种功能。

QImageReaderQImageWriter类依赖于QImageIOHandler类,该类是 Qt 中所有图像格式的通用图像 I/O 接口。QImageReaderQImageWriter在内部使用QImageIOHandler对象向 Qt 添加对不同图像格式的支持。

可通过supportedImageFormats()supportedImageFormats()函数获得受支持文件格式的列表。Qt 默认情况下支持多种文件格式,此外,可以将新格式添加为插件。QImageReaderQImageWriter类文档中列出了当前支持的格式。

Qt 的插件机制还可以用于编写自定义图像格式处理程序。这是通过派生QImageIOHandler类并创建一个QImageIOPlugin对象来完成的,该对象是用于创建QImageIOHandler对象的工厂。安装插件后,QImageReaderQImageWriter将自动加载插件并开始使用。


  1. QWidget 类是 Qt Widgets 模块中用户界面元素的基类。它从窗口系统接收鼠标,键盘和其他事件,并在屏幕上绘制其自身的表示。

  2. QImage类提供了独立于硬件的图像表示,该图像表示针对 I/O 以及直接像素访问和操作进行了设计和优化。QImage支持多种图像格式,包括单色,8 位,32 位和 alpha 混合图像。将QImage用作绘制设备的一个优点是可以以与平台无关的方式保证任何绘制操作的像素准确性。另一个好处是可以在当前 GUI 线程之外的其他线程中执行绘制。

  3. QPixmap类是一种屏幕外图像表示形式,其设计和优化用于在屏幕上显示图像。与QImage不同,像素图中的像素数据是内部的,并由基础窗口系统管理,即像素只能通过QPainter函数或将QPixmap转换为QImage来访问。为了使用QPixmap优化绘图,Qt 提供了 QPixmapCache 类,该类可用于存储临时像素图,这些像素图的生成成本很高,并且不使用超出缓存限制的存储空间。Qt 还提供了 QBitmap 便利类,继承了QPixmapQBitmap保证单色(1位深度)像素图,主要用于创建自定义 QCursorQBrush 对象,构造 QRegion 对象。你可以使用QPixmapisQBitmap()函数来确定这个QPixmap是不是一个QBitmap

  4. QPicture类是一种绘画设备,可以记录和重放QPainter命令。图片以平台无关的格式将Painter命令序列化到 IO 设备。QPicture 也是与分辨率无关的,即QPicture可以在看起来相同的不同设备(例如 svg,pdf,ps,打印机和屏幕)上显示。Qt 提供 load()save() 函数,以及用于加载和保存图片的流运算符(streaming operators)。

  5. OpenGL Paint Device,Qt 提供的类使在 Qt 应用程序中使用 OpenGL 变得容易。例如, QOpenGLPaintDevice 启用 OpenGL API 以使用 QPainter 进行渲染。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,839评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,543评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,116评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,371评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,384评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,111评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,416评论 3 400
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,053评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,558评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,007评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,117评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,756评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,324评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,315评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,539评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,578评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,877评论 2 345