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号
浙公网安备 33010602011771号