一、Canvas画布
QML 中的 Canvas
,俗称画布,它用来定义一个绘图区域,画布的 原点 在左上角 (0, 0)
处,x
轴 水平向右为正,y
轴 垂直向下为正。我们可以使用 ECMAScript 代码来绘制直线、矩形、贝塞尔曲线、弧线、图片、文字等图元,还可以为这些图元应用填充颜色和边框颜色,甚至还可以进行低阶的像素级的操作。
Canvas
是 Item
的派生类,通过设置 width
和 height
属性,就可以定一个绘图区域,然后在 onPaint()
信号处理器内使用 Context2D
对象来绘图。当需要绘图(更新)时会触发 paint()
信号
Context2D
是 QML 中负责 2D 绘图的对象,与 Canvas
结合使用。有两种使用 Context2D
对象的方式,一种是在 onPaint()
信号处理器中调用 getContext("2d")
获取 Context2D
对象。另外一种是,当我们设置了 Canvas
对象的 contextType
属性(2D 绘图时取值为 "2d"
)后,context
属性就会保存一个可用的 Context2D
对象。
我们可以在终端中使用 pip
安装 PySide6 模块。默认是从国外的主站上下载,因此,我们可能会遇到网络不好的情况导致下载失败。我们可以在 pip
指令后通过 -i
指定国内镜像源下载。
pip install pyside6 -i https://mirrors.aliyun.com/pypi/simple
国内常用的 pip
下载源列表:
- 阿里云 https://mirrors.aliyun.com/pypi/simple
- 中国科技大学 https://pypi.mirrors.ustc.edu.cn/simple
- 清华大学 https://pypi.tuna.tsinghua.edu.cn/simple
- 中国科学技术大学 http://pypi.mirrors.ustc.edu.cn/simple
二、绘制路径
在 Context2D
中,我们可以使用 lineWidth
属性 设置画笔的宽度;我们可以使用 strokeStyle
属性 设置画笔的颜色;我们可以使用 fillStyle
属性 保存用于填充图元的画刷,它可以是一个颜色值,也可以是 CanvasGradient
或 CanvasPattern
对象。
我们可以通过 Context2D
中的如下方法绘制路径:
// 将当前路径重置为新的路径
object beginPath()// 通过绘制一条线连接到子路径的起始点来闭合当前子路径,并自动开始一个新的路径。新路径的当前点即为上一个子路径的第一个点
object closePath() // 创建一个位于点(x,y)处的新子路径
object moveTo(real x, real y)// 从当前位置画一条线至坐标为(x,y)的点处
object lineTo(real x, real y)// 在当前子路径上添加一个由给定控制点和半径构成的弧线,并通过一条直线与前一点相连
object arcTo(real x1, real y1, real x2, real y2, real radius)// 在当前点与终点(x,y)之间添加一条二次贝塞尔曲线,其控制点由(cpx,cpy)指定
object quadraticCurveTo(real cpx, real cpy, real x, real y)// 在当前位置与给定的终点之间添加一条由指定的控制点(cplx,cply)和(cp2x,cp2y)控制的三次贝塞尔曲线。添加曲线后,当前位置将更新为该曲线的终点(x,y)
object bezierCurveTo(real cp1x, real cp1y, real cp2x, real cp2y, real x, real y)// 在当前子路径上添加一条位于以点(x,y)为圆心、半径为radius的圆的圆周上的弧。
// anticlockwise为true时顺时针绘制,为false时逆时针绘制
// 起始角度和结束角度均是以弧度为单位,从×轴测量得出的。
object arc(real x, real y, real radius, real startAngle, real endAngle, bool anticlockwise)// 在位置(x,y)处添加一个矩形,其宽度为w,高度为h,并将其作为闭合的子路径进行绘制
object rect(real x, real y, real w, real h)// 在由其左上角坐标(x,y)、宽度w和高度h定义的边界矩形内创建一个椭圆,并将其作为闭合子路径添加到路径中
object ellipse(real x, real y, real w, real h)
在绘制完路径之后,我们可以调用 fill()
方法 填充路径,或者调用 stroke()
方法 进行描边。
object fill() // 用当前的填充样式填充子路径
object stroke() // 使用当前的描边样式对子路径进行描边处理
我们新建一个 template.py 文件。
import sysfrom PySide6.QtWidgets import QApplication
from PySide6.QtQml import QQmlApplicationEngineif __name__ == "__main__":app = QApplication(sys.argv) # 1.创建一个QApplication类的实例engine = QQmlApplicationEngine() # 2.创建QML引擎对象engine.load("template.qml") # 3.加载QML文件sys.exit(app.exec()) # 4.进入程序的主循环并通过exit()函数确保主循环安全结束
我们新建一个 template.qml 文件。
import QtQuick.Window
import QtQuick.Controls// Window控件表示一个顶级窗口
// 在QML中,元素是通过大括号{}内的属性来配置的。
Window {id: windowIdwidth: 800 // 窗口的宽度height: 600 // 窗口的高度visible: true // 显示窗口color: "lightgray" // 窗口的背景颜色Canvas {id: canvasIdanchors.fill: parentcontextType: "2d"// 当需要绘图(更新)时会触发paint()信号onPaint: {context.lineWidth = 10 // 设置画笔宽度context.strokeStyle = "#FF6666" // 设置画笔颜色context.fillStyle = "#99CCFF" // 设置填充颜色 context.rect(10, 10, 120, 80) // 绘制矩形context.fill() // 填充路径context.stroke() // 绘制路径context.ellipse(150, 100, 100, 100) // 绘制椭圆context.fill() // 填充路径context.moveTo(260, 260) // 创建一个位于点(x,y)处的新子路径context.lineTo(360, 360) // 从当前位置画一条线至坐标为(x,y)的点处context.lineTo(400, 300) // 从当前位置画一条线至坐标为(x,y)的点处context.closePath() // 通过绘制一条线连接到子路径的起始点来闭合当前子路径context.stroke() // 绘制路径}}
}
三、渐变填充
我们可以使用 Context2D
的如下方法创建一个渐变:
// 返回一个CanvasGradient对象,该对象代表一条线性渐变,它沿着从起始点(x0,y0)到结束点(x1,y1)的一条线来改变颜色
object createLinearGradient(real x0, real y0, real x1, real y1)// 返回一个CanvasGradient对象,该对象代表一个径向渐变,用于沿着由起始圆(圆心为(x0,y0)且半径为r0,终点圆的圆心为(x1,y1)且半径为r1)所确定的圆锥进行绘制。
object createRadialGradient(real x0, real y0, real r0, real x1, real y1, real r1)// 通过绘制一条线连接到子路径的起始点来闭合当前子路径,并自动开始一个新的路径。新路径的当前点即为上一个子路径的第一个点
object createConicalGradient(real x, real y, real angle)
我们可以使用 CanvasGradient
对象的 addColorStop()
方法 添加渐变路径上的关键点的颜色。
// 在给定的偏移量处为渐变添加带有给定颜色的色点。0.0是渐变一端的偏移量,1.0是另一端的偏移量
CanvasGradient addColorStop(real offset, string color)
我们新建一个 template.qml 文件。
import QtQuick.Window
import QtQuick.Controls// Window控件表示一个顶级窗口
// 在QML中,元素是通过大括号{}内的属性来配置的。
Window {id: windowIdwidth: 800 // 窗口的宽度height: 600 // 窗口的高度visible: true // 显示窗口color: "lightgray" // 窗口的背景颜色Canvas {id: canvasIdanchors.fill: parent// 当需要绘图(更新)时会触发paint()信号onPaint: {var ctx = getContext("2d")ctx.beginPath() // 将当前路径重置为新的路径ctx.rect(10, 10, 385, 285) // 绘制矩形var gradient = ctx.createLinearGradient(10, 10, 385, 285) // 创建线性渐变gradient.addColorStop(0, "#99CCFF") // 添加渐变路径上的关键点的颜色gradient.addColorStop(0.3, "#9933CC") // 添加渐变路径上的关键点的颜色gradient.addColorStop(0.7, "#FF33CC") // 添加渐变路径上的关键点的颜色gradient.addColorStop(1, "#FF6666") // 添加渐变路径上的关键点的颜色ctx.fillStyle = gradient // 设置填充样式ctx.fill() // 填充路径ctx.beginPath()ctx.rect(405, 10, 385, 285)gradient = ctx.createRadialGradient(600, 150, 10, 600, 150, 200) // 创建径向渐变gradient.addColorStop(0, "#99CCFF")gradient.addColorStop(0.3, "#9933CC")gradient.addColorStop(0.7, "#FF33CC")gradient.addColorStop(1, "#FF6666")ctx.fillStyle = gradientctx.fill()ctx.beginPath()ctx.rect(10, 305, 385, 285)gradient = ctx.createConicalGradient(200, 450, 30) // 创建圆锥渐变gradient.addColorStop(0, "#99CCFF")gradient.addColorStop(0.3, "#9933CC")gradient.addColorStop(0.7, "#FF33CC")gradient.addColorStop(1, "#FF6666")ctx.fillStyle = gradientctx.fill()ctx.beginPath()ctx.rect(405, 305, 385, 285)gradient = ctx.createLinearGradient(405, 305, 790, 305)gradient.addColorStop(0, "#99CCFF")gradient.addColorStop(0.3, "#9933CC")gradient.addColorStop(0.7, "#FF33CC")gradient.addColorStop(1, "#FF6666")ctx.fillStyle = gradientctx.fill()}}
}
四、绘制文本
我们可以使用 Context2D
的如下方法绘制文本:
object fillText(text, x, y) // 在指定位置(x,y)处填充指定的文本
object strokeText(text, x, y) // 在由(x,y)指定的位置对给定文本进行描边处理
object text(string text, real x, real y) // 将给定的文本添加到路径中,作为由当前上下文字体生成的一组封闭子路径组成
我们可以通过 Context2D
的 font
属性 设置当前画布上文本内容的当前字体,我们可以按下方式设置字体:
font-style
(字体样式,可选),可以取normal
(正常)、italic
(斜体)、oblique
(斜体)三值之一。font-variant
(字体变体,可选),可以取normal
(正常)、small-caps
(小型大写字母)二值之一。font-weight
(字重,可选),可以取normal
(正常)、bold
(粗体)二值之一,或0 ~ 99
的数字。font-size
(字体大小),取Npx
或Npt
,其中N
为数字,px
代表像素,pt
代表点,对于移动设备,使用pt
为单位更合适一些。font-family
(字体族),常见的有serif
(衬线字体族)、sans-serif
(无衬线字体族)、cursive
(手写字体族)、fantasy
(梦幻字体族)、monospace
(等宽字体族)。
字体大小和字体系列属性是必填项,且需要按照上述所示的顺序进行设置(前面三个可选项的顺序不固定,但不能放在后面,否则样式不生效,并且字体大小必须在字体族前面)。
如果字体系列名称中包含空格,则必须使用引号将其括起来,属性值之间只能用一个空格分隔,否则会报
Invalid or misplaced token "" found in font string
。默认的字体值为
"10px 等线字体"
。
我们可以通过 Context2D
的 textAlign
属性 设置字体的对齐方式,它是一个字符串,我们可以取值如下:
// 默认设置,与文本的起始边缘对齐(对于从左向右排列的文本,在左侧对齐;对于从右向左排列的文本,在右侧对齐)
"start"
// 与文本的末尾边缘对齐(对于从右向左书写的文字,应位于右侧;对于从左向右书写的文字,则应位于左侧)
"end""left" // 左对齐
"right" // 右对齐
"center" // 水平居中对齐
我们可以通过 Context2D
的 textBaseline
属性 设置字体的基线对齐,它是一个字符串,我们可以取值如下:
"top" // 矩形的顶部
"middle" // 矩形的中间
"bottom" // 矩形的底部
"hanging" // 悬垂基准线
"alphabetic" // 默认值,字母基线
"ideographic" // 表意字下基线
修改 template.qml 文件的内容。
import QtQuick.Window
import QtQuick.Controls// Window控件表示一个顶级窗口
// 在QML中,元素是通过大括号{}内的属性来配置的。
Window {id: windowIdwidth: 800 // 窗口的宽度height: 600 // 窗口的高度visible: true // 显示窗口color: "lightgray" // 窗口的背景颜色Canvas {id: canvasIdanchors.fill: parentcontextType: "2d"// 当需要绘图(更新)时会触发paint()信号onPaint: {context.lineWidth = 2 // 设置画笔宽度context.strokeStyle = "#FF6666" // 设置画笔颜色context.fillStyle = "#99CCFF" // 设置填充颜色 context.font = "48px fantasy" // 设置字体context.beginPath() // 将当前路径重置为新的路径context.text("Hello World", 10, 50) // 将给定的文本添加到路径中,作为由当前上下文字体生成的一组封闭子路径组成 context.fill() // 填充路径context.stroke() // 绘制路径context.font = "oblique bold small-caps 48px sans-serif"context.beginPath()context.fillText("Hello World", 10, 100) // 填充文本context.font = "italic small-caps bold 48px sans-serif"context.beginPath()context.strokeText("Hello World", 10, 150) // 绘制文本的轮廓}}
}
五、绘制图片
我们可以使用 Context2D
的如下方法绘制图片:
// 将给定的图像绘制到画布上,位置为(dx,dy)
// 图像类型可以是Image对象、图像URL或CanvaslmageData对象
drawImage(variant image, real dx, real dy)// 将给定的项目以图像形式绘制到画布上,绘制位置为(dx,dy),宽度为dw,高度为dh
drawImage(variant image, real dx, real dy, real dw, real dh)// 将给定的项目从源点(sx,sy)以及源宽度sW、源高度sh处绘制到画布上(位置为(dx,dy)),并且绘制的宽度为dw、高度为dh
drawImage(variant image, real sx, real sy, real sw, real sh, real dx, real dy, real dw, real dh)
修改 template.qml 文件的内容。
import QtQuick.Window
import QtQuick.Controls// Window控件表示一个顶级窗口
// 在QML中,元素是通过大括号{}内的属性来配置的。
Window {id: windowIdwidth: 800 // 窗口的宽度height: 600 // 窗口的高度visible: true // 显示窗口color: "lightgray" // 窗口的背景颜色Canvas {id: canvasIdanchors.fill: parentcontextType: "2d"// 定义一个属性保存图片的URLproperty var imageUrl: "https://upload-bbs.miyoushe.com/upload/2025/09/27/75276539/7d3a0a9126d65cca62eab88e62804166_8060074586752109227.jpg"// 当需要绘图(更新)时会触发paint()信号onPaint: {context.drawImage(imageUrl, 240, 260, width, height, 0, 0, width, height)}// 当组件完成加载时触发Component.onCompleted: {loadImage(imageUrl) // 加载图片}// 当图片加载完成时触发onImageLoaded: {requestPaint() // 请求重新绘制}}
}
上述代码,我们首先在 Canvas
对象内定义了一个属性来保存图片 URL,然后在 Component.onCompleted
附加信号处理器内调用 Canvas
的 loadImage()
方法来 加载图片,该方法会异步加载图片,当图片加载完成时,会发射 imageLoaded()
信号,然后我们在对应的信号处理器 onImageLoaded()
内调用了 requestPaint()
方法来重绘 Canvas
。只有成功加载的图片,我们才可以使用 Context2D
来绘制图像。一个 Canvas
可以加载多张图片,既可以加载本地图片,也可以加载网络图片。
六、图像变换
就像 QPainter
一样,Context2D
也支持 平移、旋转、缩放、错切 等简单的图像变换,它还支持简单的 矩阵变换。
// 在坐标空间单位中,将画布的原点沿水平方向移动×个单位,沿垂直方向移动y个单位
object translate(real x, real y)// 将画布围绕当前的原点按弧度值和顺时针方向旋转
object rotate(real angle)// 通过将缩放因子乘以当前的变换矩阵,来增大或缩小画布网格中每个单元的尺寸。其中×是水平方向的缩放因子,y是垂直方向的缩放因子。
object scale(real x, real y)// 通过在水平方向上乘以sh并在垂直方向上乘以sv来对变换矩阵进行处理
object shear(real sh, real sv)// 通过相乘的方式将给定的变换矩阵应用到当前矩阵上
object transform(real a, real b, real c, real d, real e, real f)
在绘图操作完成后,应当调用 restore()
来 恢复之前保存的画布状态,否则后面绘画的图形也会应用此变换。而使用 restore()
方法之前,一定要先用 save()
方法 保存画布状态。
修改 template.qml 文件的内容。
import QtQuick.Window
import QtQuick.Controls// Window控件表示一个顶级窗口
// 在QML中,元素是通过大括号{}内的属性来配置的。
Window {id: windowIdwidth: 800 // 窗口的宽度height: 600 // 窗口的高度visible: true // 显示窗口color: "lightgray" // 窗口的背景颜色Canvas {id: canvasIdanchors.fill: parentcontextType: "2d"// 当需要绘图(更新)时会触发paint()信号onPaint: {context.lineWidth = 2 // 设置画笔宽度context.strokeStyle = "#FF6666" // 设置画笔颜色context.fillStyle = "#99CCFF" // 设置填充颜色 context.font = "48px fantasy" // 设置字体context.save() // 保存当前绘图状态context.beginPath() // 将当前路径重置为新的路径context.translate(width / 2, height / 2) // 移动坐标原点context.rotate(Math.PI / 4) // 旋转坐标系统context.scale(1.2, 1.2) // 缩放坐标系统context.text("Hello, Sakura!", 10, 50) // 将给定的文本添加到路径中,作为由当前上下文字体生成的一组封闭子路径组成 context.fill() // 填充路径context.stroke() // 绘制路径context.restore() // 恢复之前保存的绘图状态context.save() // 保存当前绘图状态context.beginPath() // 将当前路径重置为新的路径context.shear(0.2, 0.2) // 倾斜坐标系统context.text("Hello, Sakura!", 10, 100) // 将给定的文本添加到路径中,作为由当前上下文字体生成的一组封闭子路径组成 context.fill() // 填充路径context.stroke() // 绘制路径context.restore() // 恢复之前保存的绘图状态context.save() // 保存当前绘图状态context.beginPath() // 将当前路径重置为新的路径context.text("Hello, Sakura!", 10, 300) // 将给定的文本添加到路径中,作为由当前上下文字体生成的一组封闭子路径组成 context.fill() // 填充路径context.stroke() // 绘制路径}}
}
七、图像裁切
Context2D
的 clip()
方法,让我们能够根据当前路径包围的区域来裁切后续的绘图操作,在此区域之外的图像都会被毫不留情地丢弃掉。
修改 template.qml 文件的内容。
import QtQuick.Window
import QtQuick.Controls// Window控件表示一个顶级窗口
// 在QML中,元素是通过大括号{}内的属性来配置的。
Window {id: windowIdwidth: 800 // 窗口的宽度height: 600 // 窗口的高度visible: true // 显示窗口color: "lightgray" // 窗口的背景颜色Canvas {id: canvasIdanchors.fill: parentcontextType: "2d"// 定义一个属性保存图片的URLproperty var imageUrl: "https://upload-bbs.miyoushe.com/upload/2025/09/27/75276539/7d3a0a9126d65cca62eab88e62804166_8060074586752109227.jpg"// 当需要绘图(更新)时会触发paint()信号onPaint: {context.lineWidth = 2 // 设置画笔宽度context.strokeStyle = "#FF6666" // 设置画笔颜色context.save() // 保存当前绘图状态context.beginPath() // 将当前路径重置为新的路径context.rect(10, 10, 300, 200) // 绘制矩形context.stroke() // 绘制路径context.ellipse(500, 200, 200, 200) // 绘制椭圆context.stroke() // 绘制路径context.moveTo(300, 300) // 创建一个位于点(x,y)处的新子路径context.lineTo(500, 460) // 从当前位置画一条线至坐标为(x,y)的点处context.lineTo(420, 300) // 从当前位置画一条线至坐标为(x,y)的点处context.closePath() // 通过绘制一条线连接到子路径的起始点来闭合当前子路径context.stroke() // 绘制路径context.clip()context.drawImage(imageUrl, 0, 0) // 绘制图片}// 当组件完成加载时触发Component.onCompleted: {loadImage(imageUrl) // 加载图片}// 当图片加载完成时触发onImageLoaded: {requestPaint() // 请求重新绘制}}
}