一、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() // 请求重新绘制}}
}

