QML开发:画布元素

QML Canvas元素绘图技巧详解

一、画布绘制(Canvas Paint)

  在 QML 中,画布绘制(Canvas Paint) 是指使用 Canvas 元素提供的 2D 绘图上下文(类似 HTML5 Canvas)对画布进行像素级绘制和渲染的过程。它允许你以程序化方式绘制图形、处理像素数据或制作动画,而不仅仅依赖 QML 内置的可视元素(如 Rectangle、Image 等)。
基于 Canvas 2D Context,可以:

  • 绘制直线、曲线、矩形、圆形
  • 渲染文字
  • 创建渐变和图案
  • 实现动态图形(配合定时器或动画)

适用场景:

  • 绘制自定义控件
  • 绘制图表、示意图
  • 动态特效

基本语法:

import QtQuick 2.12
import QtQuick.Controls 2.12

ApplicationWindow {
    visible: true
    width: 400
    height: 300
    title: "QML Canvas 示例"

    Canvas {
        id: myCanvas
        anchors.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: canvas
    anchors.fill: parent

    property int posX: 50

    Timer {
        interval: 16 // 约 60 FPS
        running: true
        repeat: true
        onTriggered: {
            canvas.posX += 2
            if (canvas.posX > canvas.width) canvas.posX = 0
            canvas.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.12

Canvas {
    width: 300; height: 200
    onPaint: {
        var ctx = getContext("2d")
        var grad = ctx.createLinearGradient(0, 0, width, 0)
        grad.addColorStop(0, "red")
        grad.addColorStop(1, "blue")
        ctx.fillStyle = grad
        ctx.fillRect(0, 0, width, height)
    }
}

径向渐变:

Canvas {
    width: 300; height: 200
    onPaint: {
        var ctx = getContext("2d")
        var grad = ctx.createRadialGradient(150, 100, 20, 150, 100, 100)
        grad.addColorStop(0, "yellow")
        grad.addColorStop(1, "green")
        ctx.fillStyle = grad
        ctx.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: 300
    height: 200
    onPaint: {
        var ctx = getContext("2d")

        // 设置阴影
        ctx.shadowColor = "rgba(0,0,0,0.5)"
        ctx.shadowOffsetX = 10
        ctx.shadowOffsetY = 10
        ctx.shadowBlur = 15

        // 绘制矩形
        ctx.fillStyle = "orange"
        ctx.fillRect(50, 50, 100, 80)
    }
}

文字阴影:

Canvas {
    width: 300
    height: 200
    onPaint: {
        var ctx = getContext("2d")

        ctx.font = "30px sans-serif"
        ctx.fillStyle = "blue"

        ctx.shadowColor = "rgba(0,0,0,0.7)"
        ctx.shadowOffsetX = 4
        ctx.shadowOffsetY = 4
        ctx.shadowBlur = 5

        ctx.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: myImage
    source: "images/picture.png"   // 图片路径
    width: 200
    height: 150
    fillMode: 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: 300
    height: 200
    fillMode: Image.PreserveAspectCrop
}

4.4 动态加载图片

Image {
    id: dynamicImage
    width: 200
    height: 200
}

Button {
    text: "加载图片"
    onClicked: dynamicImage.source = "images/new_image.png"
}

4.5 使用 Image 与 Canvas 配合
如果你在 Canvas 上绘图,但需要显示图片,可以用 context.drawImage:

Canvas {
    id: canvas
    width: 400
    height: 300

    onPaint: {
        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: canvas
    width: 400
    height: 300

    onPaint: {
        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: canvas
    width: 400
    height: 300

    onPaint: {
        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: parent
    transform: Rotation {
        origin.x: width/2
        origin.y: height/2
        angle: 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.12

Window {
    width: 400
    height: 300
    visible: true
    title: "QML Canvas 组合模式"

    Canvas {
        id: canvas
        anchors.fill: parent
        onPaint: {
            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; // RGBA
        line[index + 0] = 255; // R
        line[index + 1] = 0;   // G
        line[index + 2] = 0;   // B
        line[index + 3] = 255; // A
    }
}

// 绘制到QWidget
QPainter painter(this);
painter.drawImage(0, 0, image);

示例:QML Canvas 修改像素颜色:

import QtQuick 2.12
import QtQuick.Window 2.12

Window {
    visible: true
    width: 200
    height: 200
    title: "Canvas Pixel Buffer 示例"

    Canvas {
        id: canvas
        anchors.fill: parent
        width: 100
        height: 100

        onPaint: {
            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;     // R
                    pixels[index + 1] = 0;   // G
                    pixels[index + 2] = 0;   // B
                    pixels[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 的位置。

像素缓冲数组示意图:
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值