13. Canvas画布
一、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.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 sys
from PySide6.QtWidgets import QApplication
from PySide6.QtQml import QQmlApplicationEngine
if __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 {
width: 800 // 窗口的宽度
height: 600 // 窗口的高度
visible: true // 显示窗口
color: "lightgray" // 窗口的背景颜色
Canvas {
id: canvasId
anchors.fill: parent
contextType: "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 {
width: 800 // 窗口的宽度
height: 600 // 窗口的高度
visible: true // 显示窗口
color: "lightgray" // 窗口的背景颜色
Canvas {
id: canvasId
anchors.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 = gradient
ctx.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 = gradient
ctx.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 = gradient
ctx.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 {
width: 800 // 窗口的宽度
height: 600 // 窗口的高度
visible: true // 显示窗口
color: "lightgray" // 窗口的背景颜色
Canvas {
id: canvasId
anchors.fill: parent
contextType: "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 {
width: 800 // 窗口的宽度
height: 600 // 窗口的高度
visible: true // 显示窗口
color: "lightgray" // 窗口的背景颜色
Canvas {
id: canvasId
anchors.fill: parent
contextType: "2d"
// 定义一个属性保存图片的URL
property 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 {
width: 800 // 窗口的宽度
height: 600 // 窗口的高度
visible: true // 显示窗口
color: "lightgray" // 窗口的背景颜色
Canvas {
id: canvasId
anchors.fill: parent
contextType: "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 {
width: 800 // 窗口的宽度
height: 600 // 窗口的高度
visible: true // 显示窗口
color: "lightgray" // 窗口的背景颜色
Canvas {
id: canvasId
anchors.fill: parent
contextType: "2d"
// 定义一个属性保存图片的URL
property 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() // 请求重新绘制
}
}
}


浙公网安备 33010602011771号