QML开发:画布元素
文章目录
- 一、画布绘制(Canvas Paint)
- 二、渐变(Gradients)
- 三、阴影(Shadows)
- 四、图片(Images)
- 五、变换(Transform)
- 六、组合模式(Composition Mode)
- 七、像素缓冲(Pixels Buffer)
一、画布绘制(Canvas Paint)
在 QML 中,画布绘制(Canvas Paint) 是指使用 Canvas 元素提供的 2D 绘图上下文(类似 HTML5 Canvas)对画布进行像素级绘制和渲染的过程。它允许你以程序化方式绘制图形、处理像素数据或制作动画,而不仅仅依赖 QML 内置的可视元素(如 Rectangle、Image 等)。
基于 Canvas 2D Context,可以:
- 绘制直线、曲线、矩形、圆形
- 渲染文字
- 创建渐变和图案
- 实现动态图形(配合定时器或动画)
适用场景:
- 绘制自定义控件
- 绘制图表、示意图
- 动态特效
基本语法:
import QtQuick 2.12
import QtQuick.Controls 2.12ApplicationWindow {visible: truewidth: 400height: 300title: "QML Canvas 示例"Canvas {id: myCanvasanchors.fill: parent// 当画布准备好绘制时触发onPaint: {var ctx = getContext("2d"); // 获取 2D 绘图上下文// 设置绘制颜色ctx.fillStyle = "lightblue";ctx.fillRect(0, 0, width, height); // 填充背景// 绘制红色矩形ctx.fillStyle = "red";ctx.fillRect(50, 50, 100, 80);// 绘制黑色边框矩形ctx.strokeStyle = "black";ctx.lineWidth = 2;ctx.strokeRect(200, 50, 100, 80);// 绘制圆ctx.beginPath();ctx.arc(150, 200, 50, 0, Math.PI * 2);ctx.fillStyle = "green";ctx.fill();// 绘制文字ctx.fillStyle = "black";ctx.font = "20px sans-serif";ctx.fillText("Hello Canvas", 100, 280);}Component.onCompleted: requestPaint() // 触发绘制}
}
输出结果:
常用 API:
- fillRect(x, y, w, h):绘制填充矩形
- strokeRect(x, y, w, h):绘制矩形边框
- clearRect(x, y, w, h):清除区域
- beginPath():开始新路径
- moveTo(x, y):移动画笔位置
- lineTo(x, y):绘制直线
- arc(x, y, r, start, end):绘制圆/弧
- fill():填充路径
- stroke():描边路径
- fillText(text, x, y):绘制文字
- strokeText(text, x, y):描边文字
- drawImage(img, x, y):绘制图片
样式属性:
- fillStyle:填充颜色或渐变
- strokeStyle:边框颜色
- lineWidth:线宽
- font:字体样式
动态绘制(动画)
Canvas 本身不会自动重绘,需要手动调用:
requestPaint()
来触发 onPaint。
示例:绘制一个移动的圆
Canvas {id: canvasanchors.fill: parentproperty int posX: 50Timer {interval: 16 // 约 60 FPSrunning: truerepeat: trueonTriggered: {canvas.posX += 2if (canvas.posX > canvas.width) canvas.posX = 0canvas.requestPaint()}}onPaint: {var ctx = getContext("2d");ctx.clearRect(0, 0, width, height);ctx.beginPath();ctx.arc(posX, height / 2, 30, 0, Math.PI * 2);ctx.fillStyle = "orange";ctx.fill();}
}
效果:一个橙色的圆不断从左向右移动。
注意事项:
- 性能:Canvas 在 QML 中性能比 OpenGL 或 ShaderEffect 弱,不适合非常高频复杂绘制。
- 重绘机制:必须手动 requestPaint() 才会触发绘制。
- 分辨率:在高 DPI 屏幕下,可根据 Canvas.devicePixelRatio 调整绘制尺寸。
- 图像缓存:对于频繁绘制的背景图,可以先绘制到离屏 Canvas,再重复绘制,提高性能。
二、渐变(Gradients)
在 QML 的 Canvas 元素中,渐变(Gradients) 是通过 CanvasRenderingContext2D 提供的 createLinearGradient() 和 createRadialGradient() 方法来创建的,它们和 HTML5 Canvas API 是一致的。
2.1 概念
渐变是一种平滑的颜色过渡效果,可以用作 填充(fillStyle) 或 描边(strokeStyle)。QML Canvas 支持两种主要渐变:
- 线性渐变(Linear Gradient):颜色沿直线方向变化
- 径向渐变(Radial Gradient):颜色沿圆心向外变化
2.2 线性渐变
var gradient = ctx.createLinearGradient(x0, y0, x1, y1)
- (x0, y0) → 渐变起点
- (x1, y1) → 渐变终点
2.3 径向渐变
var gradient = ctx.createRadialGradient(x0, y0, r0, x1, y1, r1)
- (x0, y0, r0) → 内圆(中心和半径)
- (x1, y1, r1) → 外圆(中心和半径)
添加颜色断点:
gradient.addColorStop(offset, color)
- offset:0.0 ~ 1.0(表示渐变的相对位置)
- color:CSS 样式颜色(如 “red”, “#FF0000”, “rgba(255,0,0,0.5)”)
2.4 示例
线性渐变:
import QtQuick 2.12Canvas {width: 300; height: 200onPaint: {var ctx = getContext("2d")var grad = ctx.createLinearGradient(0, 0, width, 0)grad.addColorStop(0, "red")grad.addColorStop(1, "blue")ctx.fillStyle = gradctx.fillRect(0, 0, width, height)}
}
径向渐变:
Canvas {width: 300; height: 200onPaint: {var ctx = getContext("2d")var grad = ctx.createRadialGradient(150, 100, 20, 150, 100, 100)grad.addColorStop(0, "yellow")grad.addColorStop(1, "green")ctx.fillStyle = gradctx.fillRect(0, 0, width, height)}
}
注意事项:
- 渐变是绘图样式,要先创建渐变对象,再赋值给 fillStyle 或 strokeStyle 才有效。
- addColorStop() 的顺序影响最终效果(会按 offset 排序)。
- 在 QML Canvas 中,性能比 QML 原生 Rectangle { gradient: Gradient {…} } 稍低,因为它是脚本绘制。
三、阴影(Shadows)
2.1 概念
阴影可以给 Canvas 上绘制的形状或文字添加投影效果,增强立体感。
主要作用于:
- fillRect, strokeRect 等矩形
- arc 绘制的圆形
- fillText / strokeText 绘制的文字
阴影不会改变形状本身,只是增加一层投影。
2.2 属性说明
Canvas 中的阴影由以下 3 个属性控制:
- shadowColor:阴影颜色,支持 CSS 颜色,例如 “rgba(0,0,0,0.5)”
- shadowOffsetX:阴影水平偏移量(像素),正值向右,负值向左
- shadowOffsetY:阴影垂直偏移量(像素),正值向下,负值向上
- shadowBlur:阴影模糊半径(像素),越大越柔和
2.3 示例
矩形阴影:
Canvas {width: 300height: 200onPaint: {var ctx = getContext("2d")// 设置阴影ctx.shadowColor = "rgba(0,0,0,0.5)"ctx.shadowOffsetX = 10ctx.shadowOffsetY = 10ctx.shadowBlur = 15// 绘制矩形ctx.fillStyle = "orange"ctx.fillRect(50, 50, 100, 80)}
}
文字阴影:
Canvas {width: 300height: 200onPaint: {var ctx = getContext("2d")ctx.font = "30px sans-serif"ctx.fillStyle = "blue"ctx.shadowColor = "rgba(0,0,0,0.7)"ctx.shadowOffsetX = 4ctx.shadowOffsetY = 4ctx.shadowBlur = 5ctx.fillText("Hello Shadows", 50, 100)}
}
QML Canvas 阴影效果彩色示意图:
注意事项:
阴影作用域:
- 阴影是上下文属性,设置后会作用于后续绘制的所有图形,直到被覆盖或重置。
- 如果想只给特定图形加阴影,可以在绘制前后分别保存/恢复上下文:
ctx.save()
ctx.shadowColor = "rgba(0,0,0,0.5)"
ctx.fillRect(50,50,100,80)
ctx.restore()
性能:阴影和模糊会增加绘制开销,尤其在动画场景下。
透明度:阴影颜色透明度越高,投影越柔和。
四、图片(Images)
在 QML 中,Image 是最常用的显示图片的元素,它非常灵活,可以在画布(Canvas)或者普通界面中使用。下面我给你详细整理 Image 的使用方法、属性和技巧。
4.1 基本用法
Image {id: myImagesource: "images/picture.png" // 图片路径width: 200height: 150fillMode: Image.PreserveAspectFit // 保持宽高比缩放
}
4.2 常用属性
- source:图片文件路径,可以是相对路径或 URL
- width / height:图片显示的尺寸
- fillMode:缩放方式:Stretch / PreserveAspectFit / PreserveAspectCrop
- smooth:是否启用平滑缩放(默认 true)
- opacity:透明度(0~1)
- visible:是否显示
- layer.enabled:可启用图层渲染,实现旋转、阴影等效果
4.3 缩放方式
- Image.Stretch:拉伸填充,不保持比例
- Image.PreserveAspectFit:保持比例,整个图片显示在区域内
- Image.PreserveAspectCrop:保持比例,可能裁剪超出区域的部分
Image {source: "images/picture.png"width: 300height: 200fillMode: Image.PreserveAspectCrop
}
4.4 动态加载图片
Image {id: dynamicImagewidth: 200height: 200
}Button {text: "加载图片"onClicked: dynamicImage.source = "images/new_image.png"
}
4.5 使用 Image 与 Canvas 配合
如果你在 Canvas 上绘图,但需要显示图片,可以用 context.drawImage:
Canvas {id: canvaswidth: 400height: 300onPaint: {var ctx = getContext("2d")var img = new Image()img.src = "images/picture.png"img.onload = function() {ctx.drawImage(img, 0, 0, canvas.width, canvas.height)}}
}
注意:Canvas 绘制时,Image 是 JS 对象,需要等待 onload 事件
4.6 图片替换与缓存
- QML Image 会自动缓存图片,提高性能
- 可通过 cache: false 禁用缓存,适合动态图片频繁变化的情况
Image {source: "images/live.png"cache: false
}
五、变换(Transform)
5.1 Canvas 上的转换(2D context)
Canvas 提供类似 HTML5 Canvas 的 2D API,通过 context 可以进行转换:
- translate(x, y):平移坐标系,将原点移动到 (x, y)
- rotate(angle):旋转坐标系,角度为弧度
- scale(sx, sy):缩放坐标系,sx / sy 为水平/垂直缩放比例
- transform(a, b, c, d, e, f):复合变换,直接设置变换矩阵
- setTransform(a, b, c, d, e, f):重置变换矩阵为指定矩阵
- save() / restore():保存/恢复当前坐标系状态
5.2 示例:平移、旋转、缩放
Canvas {id: canvaswidth: 400height: 300onPaint: {var ctx = getContext("2d")ctx.clearRect(0, 0, width, height)// 保存状态ctx.save()// 平移到画布中心ctx.translate(width/2, height/2)// 旋转 45 度ctx.rotate(Math.PI / 4)// 缩放ctx.scale(1.5, 1.5)// 绘制矩形ctx.fillStyle = "skyblue"ctx.fillRect(-50, -25, 100, 50)// 恢复状态ctx.restore()}
}
注意:Canvas 的变换是累积的,每次操作都会影响后续绘制,所以常用 save() 和 restore() 控制局部变换。
5.3 对 Image 的变换
Canvas {id: canvaswidth: 400height: 300onPaint: {var ctx = getContext("2d")var img = new Image()img.src = "images/picture.png"img.onload = function() {ctx.save()ctx.translate(200, 150)ctx.rotate(Math.PI / 6)ctx.scale(0.8, 0.8)ctx.drawImage(img, -img.width/2, -img.height/2)ctx.restore()}}
}
这里先平移到中心,再旋转、缩放,然后绘制图片,最后 restore() 复原坐标系。
5.4 QML 元素层级的变换
如果不使用 Canvas,而直接操作 Image、Rectangle 等 QML 元素,也可以用 Transform:
Image {source: "images/picture.png"anchors.centerIn: parenttransform: Rotation {origin.x: width/2origin.y: height/2angle: 45}
}
使用技巧::
- 中心旋转:旋转前先 translate 到中心,绘制后 translate 回去
- 组合变换顺序:变换顺序影响结果(先平移再旋转 vs 先旋转再平移)
- 局部变换:save() / restore()(Canvas)或单独元素 transform(QML)
- 动画变换:可用 NumberAnimation 或 RotationAnimation 对属性如 angle、scale 动态变化
六、组合模式(Composition Mode)
6.1 什么是组合模式
在 QML Canvas(即 HTML5 Canvas 的 API 接口)中,globalCompositeOperation 决定了新绘制的形状(source)如何与已有画布内容(destination)进行像素级混合。它的本质是一个 像素合成公式,类似 Photoshop、Illustrator 里的“混合模式”。
6.2 常用组合模式分类
常规叠加模式:
- source-over:默认值,新图形画在旧图形上面(常规绘制)
- destination-over:新图形画在旧图形下面
覆盖与替换:
- copy:只保留新图形,清空旧内容
- destination:保留旧图形,忽略新图形
相交/相减模式:
- source-in:只保留新图形与旧图形重叠部分
- destination-in:只保留旧图形与新图形重叠部分
- source-out:只保留新图形中与旧图形不重叠部分
- destination-out:只保留旧图形中与新图形不重叠部分
- xor:只保留新旧图形不重叠部分
混色模式:
- lighter:颜色相加,变亮
- multiply:颜色相乘,变暗
- screen:反相相乘,变亮
- overlay:叠加:暗的更暗,亮的更亮
- darken:取新旧颜色的较暗值
- lighten:取新旧颜色的较亮值
6.3 使用示例
import QtQuick 2.12
import QtQuick.Window 2.12Window {width: 400height: 300visible: truetitle: "QML Canvas 组合模式"Canvas {id: canvasanchors.fill: parentonPaint: {var ctx = getContext("2d");// 清空画布ctx.clearRect(0, 0, width, height);// 画第一个矩形ctx.fillStyle = "blue";ctx.fillRect(50, 50, 150, 150);// 设置组合模式ctx.globalCompositeOperation = "lighter"; // 可替换为 multiply, source-over 等// 画第二个矩形ctx.fillStyle = "red";ctx.fillRect(100, 100, 150, 150);}}
}
输出结果:
使用技巧与场景:
- 遮罩效果:source-in、destination-in
- 擦除(橡皮擦):destination-out
- 高光/发光效果:lighter
- 变暗阴影:multiply
- 渐变融合:screen、overlay
- 只显示交集:source-in
注意事项:
- 默认模式是 source-over,其他模式需要显式设置。
- 组合模式会影响后续绘制,建议在需要的地方设置,用完恢复:
ctx.globalCompositeOperation = "source-over"; // 恢复默认
QML Canvas 组合模式效果对照图:
七、像素缓冲(Pixels Buffer)
7.1 什么是像素缓冲
定义:像素缓冲就是一块连续的内存,用来存储图像中每个像素的颜色信息。每个像素通常包含 红、绿、蓝(RGB),有时还包含 透明度(Alpha)。
访问方式:通过 Canvas.getContext(“2d”).getImageData() 获取像素数据。
存储方式:
- 按行排列:像素数据通常按行从左到右,从上到下排列。
- 按通道排列:RGB:每个像素 3 字节(Red, Green, Blue);RGBA:每个像素 4 字节(Red, Green, Blue, Alpha)
- 灰度图像:每个像素 1 字节
常见类型:
- uint8_t* 或 uchar*(每通道 8 位)
- uint16_t*(高精度图像,如医学图像或深度图)
- float*(HDR 或科学计算图像)
7.2 像素缓冲的用途
- 自定义绘制:可以按像素修改画布,例如绘制渐变、生成噪声、绘制动态效果。
- 图像处理:滤镜、卷积、灰度化、反转颜色等都可以直接修改像素缓冲实现。
- 动画渲染:在 onFrameSwapped 或定时器中不断修改像素缓冲实现像素级动画。
- 数据可视化:将外部数组(如热力图、深度图)映射到画布像素缓冲显示。
7.3 基本使用示例
以 C++/Qt 为例,使用 QImage 直接操作像素缓冲:
int width = 100;
int height = 100;// 创建一个RGBA格式的图像
QImage image(width, height, QImage::Format_RGBA8888);// 获取像素缓冲
uchar* buffer = image.bits();
int bytesPerLine = image.bytesPerLine(); // 每行的字节数// 填充像素缓冲
for (int y = 0; y < height; ++y) {uchar* line = buffer + y * bytesPerLine;for (int x = 0; x < width; ++x) {int index = x * 4; // RGBAline[index + 0] = 255; // Rline[index + 1] = 0; // Gline[index + 2] = 0; // Bline[index + 3] = 255; // A}
}// 绘制到QWidget
QPainter painter(this);
painter.drawImage(0, 0, image);
示例:QML Canvas 修改像素颜色:
import QtQuick 2.12
import QtQuick.Window 2.12Window {visible: truewidth: 200height: 200title: "Canvas Pixel Buffer 示例"Canvas {id: canvasanchors.fill: parentwidth: 100height: 100onPaint: {var ctx = getContext("2d");// 填充黑色背景ctx.fillStyle = "black";ctx.fillRect(0, 0, width, height);// 获取像素缓冲var imageData = ctx.getImageData(0, 0, width, height);var pixels = imageData.data;// 左上角 50x50 红色for (var y = 0; y < 50; y++) {for (var x = 0; x < 50; x++) {var index = (y * width + x) * 4;pixels[index] = 255; // Rpixels[index + 1] = 0; // Gpixels[index + 2] = 0; // Bpixels[index + 3] = 255; // A}}// 写回画布ctx.putImageData(imageData, 0, 0);}// 确保 Canvas 完成初始化后触发绘制Component.onCompleted: requestPaint()}
}
说明:
- getImageData(x, y, width, height) 获取指定区域的像素缓冲。
- Uint8ClampedArray 确保像素值在 0~255 范围内。
- 修改后需要 putImageData 才能刷新画布。
注意事项:
- 性能问题:getImageData 和 putImageData 在大画布上频繁调用会比较慢。优化方法:尽量只修改需要的区域。另外也可以使用 WebGL Canvas 或 Shader 进行 GPU 加速。
- 透明度处理:Alpha 值 0 表示完全透明,255 表示完全不透明。
- 数组索引:index = (y * width + x) * 4,千万不要漏乘 4,否则颜色会错乱。
- 浮点计算:像素值必须是整数,使用 Uint8ClampedArray 自动截断。
7.4 像素缓冲的内存布局
假设画布宽度是 width,像素缓冲是 Uint8ClampedArray,每个像素有 4 个通道:
pixels = [R0, G0, B0, A0, R1, G1, B1, A1, R2, G2, B2, A2, ...R(width), G(width), B(width), A(width), ......
]
- 每行有 width 个像素,每个像素 4 个字节
- 所以第 y 行第 x 列的像素在数组中的起始索引为:index=y×width×4+x×4
也可以写成:
index = (y * width + x) * 4
- y * width → 第 y 行有多少个像素
-
- x → 第 x 列
-
- 4 → 每个像素有 4 个字节(R,G,B,A)
示例:
假设画布 3x2(宽3,高2):
行0: pix0, pix1, pix2
行1: pix3, pix4, pix5
每个像素 4 字节,那么缓冲数组就是:
[ R0,G0,B0,A0, R1,G1,B1,A1, R2,G2,B2,A2, R3,G3,B3,A3, R4,G4,B4,A4, R5,G5,B5,A5 ]
- 第 1 行第 2 列(y=0,x=1)的索引:index=(0∗3+1)∗4=4;对应 R1 的位置。
- 第 2 行第 3 列(y=1,x=2)的索引:index=(1∗3+2)∗4=20;对应 R5 的位置。
像素缓冲数组示意图: