一、QtQuick Canvas简介
QT5引入了Canvas元素。Canvas元素提供了一种与分辨率无关的位图绘制机制。通过Canvas,可以使用 JavaScript 代码进行绘制。Canvas元素的基本思想是,使用一个2D上下文对象渲染路径。2D上下文对象包含所必须的绘制函数,从而使Canvas元素看起来就像一个画板。2D上下文对象支持画笔、填充、渐变、文本以及其它一系列路径创建函数。
import QtQuick 2.0
Canvas
{
id: root
// 画板大小
width: 200; height: 200
// 重写绘制函数
onPaint:
{
// 获得 2D 上下文对象
var ctx = getContext("2d")
// 设置画笔
ctx.lineWidth = 4
ctx.strokeStyle = "blue"
// 设置填充
ctx.fillStyle = "steelblue"
// 开始绘制路径
ctx.beginPath()
// 移动到左上点作为起始点
ctx.moveTo(50,50)
// 上边线
ctx.lineTo(150,50)
// 右边线
ctx.lineTo(150,150)
// 底边线
ctx.lineTo(50,150)
// 左边线,并结束路径
ctx.closePath()
// 使用填充填充路径
ctx.fill()
// 使用画笔绘制边线
ctx.stroke()
}
}
本例中,画笔的宽度设置为4像素;使用strokeStyle属性,将画笔的颜色设置为蓝色。fillStyle属性则是设置填充色为steelblue。只有调用stroke()或fill()函数时,真实的绘制才会执行。调用stroke()或fill()函数会将当前路径绘制出来。路径是不能够被复用的,只有当前绘制状态才能够被复用。所谓“当前绘制状态”,指的是当前的画笔颜色、宽度、填充色等属性。
Canvas元素就是一种绘制的容器。2D上下文对象作为实际绘制的执行者。绘制过程必须在onPaint事件处理函数中完成。
Canvas本身提供一个典型的二维坐标系,原点在左上角,X轴正方向向右,Y轴正方向向下。使用Canvas进行绘制的典型过程是:
A、设置画笔和填充样式
B、创建路径
C、应用画笔和填充
二、Canvas操作
1、绘制API
除了自己进行路径的创建外,Canvas还提供了一系列方便使用的函数,用于一次添加一个矩形等
import QtQuick 2.0
Canvas
{
id: root
width: 120; height: 120
onPaint:
{
var ctx = getContext("2d")
ctx.fillStyle = 'green'
ctx.strokeStyle = "blue"
ctx.lineWidth = 4
// 填充矩形
ctx.fillRect(20, 20, 80, 80)
// 裁减掉内部矩形
ctx.clearRect(30,30, 60, 60)
// 从左上角起,到外层矩形中心绘制一个边框
ctx.strokeRect(20,20, 40, 40)
}
}
2、渐变
Canvas元素可以使用颜色进行填充,也可以使用渐变。
import QtQuick 2.0
Canvas
{
id: root
width: 120; height: 120
onPaint:
{
var ctx = getContext("2d")
var gradient = ctx.createLinearGradient(100,0,100,200)
gradient.addColorStop(0, "blue")
gradient.addColorStop(0.5, "lightsteelblue")
ctx.fillStyle = gradient
ctx.fillRect(10,10,100,100)
}
}
本例中,渐变的起始点位于(100, 0),终止点位于(100, 200)。两个点创建了一条位于画布中央位置的竖直线。渐变类似于插值,可以在 [0.0, 1.0] 区间内插入一个定义好的确定的颜色;其中,0.0 意味着渐变的起始点,1.0 意味着渐变的终止点。在0.0的位置(渐变起始点(100,0)的位置)设置颜色为“blue”;在1.0的位置(渐变终止点 (100, 200)位置)设置颜色为“lightsteelblue”。渐变的范围可以大于实际绘制的矩形,但绘制出来的矩形实际上裁减了渐变的一部分。渐变的定义其实是依据画布的坐标,不是定义的绘制路径的坐标。
3、阴影
路径可以使用阴影增强视觉表现力,可以把阴影定义为一个围绕在路径周围的区域,区域会有一定的偏移、有一定的颜色和特殊的模糊效果。可以使用shadowColor属性定义阴影的颜色;使用shadowOffsetX属性定义阴影在X轴方向的偏移量;使用shadowOffsetY属性定义阴影在Y轴方向的偏移量;使用shadowBlur属性定义阴影模糊的程度。可以实现一种围绕在路径周边的发光特效。
Canvas
{
id: root
width: 280; height: 120
onPaint:
{
var ctx = getContext("2d")
// 背景矩形
ctx.strokeStyle = "#333"
ctx.fillRect(0, 0, root.width, root.height);
// 设置阴影属性
ctx.shadowColor = "blue";
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.shadowBlur = 10;
// 设置字体并绘制
ctx.font = 'bold 80px sans-serif';
ctx.fillStyle = "#33a9ff";
ctx.fillText("Hello", 30, 80);
}
}
本例创建一个带有发光效果的文本。为了更明显的显示发光效果,其背景界面将会是深色的。利用#333填充了一个背景矩形。矩形的起始点位于原点,长度和宽度分别绑定到画布的长度和宽度。定义阴影的属性,设置文本字体为80像素加粗的sans-serif,绘制了“Hello”。
4、图像
Canvas元素支持从多种源绘制图像。为了绘制图像,需要首先加载图像;使用Component.onCompleted事件处理函数可以达到目的。
import QtQuick 2.0
Canvas
{
id: root
width: 300; height: 300
onPaint:
{
var ctx = getContext("2d")
// 绘制图像
ctx.drawImage('images/saturn.png', 10, 10)
// 保存当前状态
ctx.save()
// 平移坐标系
ctx.translate(100,0)
ctx.strokeStyle = 'red'
// 创建裁剪范围
ctx.beginPath()
ctx.moveTo(10,10)
ctx.lineTo(55,10)
ctx.lineTo(35,55)
ctx.closePath()
ctx.clip() // 根据路径裁剪
// 绘制图像并应用裁剪
ctx.drawImage('images/saturn.png', 10, 10)
// 绘制路径
ctx.stroke()
// 恢复状态
ctx.restore()
}
Component.onCompleted:
{
loadImage("images/saturn.png")
}
}
5、变换
Canvas中的“变形”,主要指的是坐标系的变换,而不是路径的变换,变换的原点是画布原点。例如,如果以一个路径的中心点为定点进行缩放,需要现将画布原点移动到路径中心点。
import QtQuick 2.0
Canvas
{
id: root
width: 240; height: 120
onPaint:
{
var ctx = getContext("2d")
ctx.strokeStyle = "blue"
ctx.lineWidth = 4
ctx.translate(120, 60)
ctx.strokeRect(-20, -20, 40, 40)
// draw path now rotated
ctx.strokeStyle = "green"
ctx.rotate(Math.PI / 4)
ctx.strokeRect(-20, -20, 40, 40)
ctx.restore()
}
}
6、组合
组合是将绘制的图形与已存在的像素做一些融合操作。Canvas支持几种组合方式,使用globalCompositeOperation属性可以设置组合的模式。
import QtQuick 2.0
Canvas
{
id: root
width: 600; height: 450
property var operation : [
'source-over', 'source-in', 'source-over',
'source-atop', 'destination-over', 'destination-in',
'destination-out', 'destination-atop', 'lighter',
'copy', 'xor', 'qt-clear', 'qt-destination',
'qt-multiply', 'qt-screen', 'qt-overlay', 'qt-darken',
'qt-lighten', 'qt-color-dodge', 'qt-color-burn',
'qt-hard-light', 'qt-soft-light', 'qt-difference',
'qt-exclusion'
]
onPaint:
{
var ctx = getContext('2d')
for(var i=0; i<operation.length; i++)
{
var dx = Math.floor(i%6)*100
var dy = Math.floor(i/6)*100
ctx.save()
ctx.fillStyle = '#33a9ff'
ctx.fillRect(10+dx,10+dy,60,60)
// TODO: does not work yet
ctx.globalCompositeOperation = root.operation[i]
ctx.fillStyle = '#ff33a9'
ctx.globalAlpha = 0.75
ctx.beginPath()
ctx.arc(60+dx, 60+dy, 30, 0, 2*Math.PI)
ctx.closePath()
ctx.fill()
ctx.restore()
}
}
}
7、像素缓存
使用canvas,可以将canvas内容的像素数据读取出来,并且能够针对像素数据做一些操作。
使用createImageData(sw, sh)或getImageData(sx, sy, sw, sh)函数可以读取图像数据。两个函数都会返回一个ImageData对象,ImageData对象具有width、height和data等变量。data包含一个以RGBA格式存储的像素一维数组,其每一个分量值的范围都是[0,255]。如果要设置画布上面的像素,可以使用putImageData(imagedata, dx, dy)函数。
另外一个获取画布内容的方法是,将数据保存到一个图片。可以通过Canvas的函数save(path)或toDataURL(mimeType)实现,后者会返回一个图像的 URL,可以供Image元素加载图像。
import QtQuick 2.0
Rectangle
{
width: 240; height: 120
Canvas
{
id: canvas
x: 10; y: 10
width: 100; height: 100
property real hue: 0.0
onPaint:
{
var ctx = getContext("2d")
var x = 10 + Math.random(80)*80
var y = 10 + Math.random(80)*80
hue += Math.random()*0.1
if(hue > 1.0) { hue -= 1 }
ctx.globalAlpha = 0.7
ctx.fillStyle = Qt.hsla(hue, 0.5, 0.5, 1.0)
ctx.beginPath()
ctx.moveTo(x+5,y)
ctx.arc(x,y, x/10, 0, 360)
ctx.closePath()
ctx.fill()
}
MouseArea
{
anchors.fill: parent
onClicked:
{
var url = canvas.toDataURL('image/png')
print('image url=', url)
image.source = url
}
}
}
Image
{
id: image
x: 130; y: 10
width: 100; height: 100
}
Timer
{
interval: 1000
running: true
triggeredOnStart: true
repeat: true
onTriggered: canvas.requestPaint()
}
}
本例创建了两个画布,左侧的画布每一秒产生一个圆点;鼠标点击会将画布内容保存,并且生成一个图像的 URL,右侧则会显示这个图像。
8、HTML Canvas移植
QML的Canvas对象由HTML 5的Canvas标签借鉴而来,将HTML 5的Canvas 应用移植到QML Canvas很容易。
HTML 5 canvas:
function draw()
{
var ctx = document.getElementById('canvas').getContext('2d');
ctx.fillRect(0,0,300,300);
for (var i=0;i<3;i++)
{
for (var j=0;j<3;j++)
{
ctx.save();
ctx.strokeStyle = "#9CFF00";
ctx.translate(50+j*100,50+i*100);
drawSpirograph(ctx,20*(j+2)/(j+1),-8*(i+3)/(i+1),10);
ctx.restore();
}
}
}
function drawSpirograph(ctx,R,r,O)
{
var x1 = R-O;
var y1 = 0;
var i = 1;
ctx.beginPath();
ctx.moveTo(x1,y1);
do {
if (i>20000) break;
var x2 = (R+r)*Math.cos(i*Math.PI/72) - (r+O)*Math.cos(((R+r)/r)*(i*Math.PI/72))
var y2 = (R+r)*Math.sin(i*Math.PI/72) - (r+O)*Math.sin(((R+r)/r)*(i*Math.PI/72))
ctx.lineTo(x2,y2);
x1 = x2;
y1 = y2;
i++;
} while (x2 != R-O && y2 != 0 );
ctx.stroke();
}
draw();
HTML按照顺序执行,draw()会成为脚本的入口函数。但在QML中,绘制必须在onPaint中完成,需要将draw()函数的调用移至onPaint。通常会在onPaint中获取绘制上下文,给draw()函数添加一个参数,用于接受Context2D对象。
QML Canvas:
import QtQuick 2.2
Canvas {
id: root
width: 300; height: 300
onPaint:
{
var ctx = getContext("2d");
draw(ctx);
}
function draw (ctx)
{
ctx.fillRect(0, 0, 300, 300);
for (var i = 0; i < 3; i++)
{
for (var j = 0; j < 3; j++)
{
ctx.save();
ctx.strokeStyle = "#9CFF00";
ctx.translate(50 + j * 100, 50 + i * 100);
drawSpirograph(ctx, 20 * (j + 2) / (j + 1), -8 * (i + 3) / (i + 1), 10);
ctx.restore();
}
}
}
function drawSpirograph (ctx, R, r, O)
{
var x1 = R - O;
var y1 = 0;
var i = 1;
ctx.beginPath();
ctx.moveTo(x1, y1);
do {
if (i > 20000) break;
var x2 = (R + r) * Math.cos(i * Math.PI / 72) - (r + O) * Math.cos(((R + r) / r) * (i * Math.PI / 72))
var y2 = (R + r) * Math.sin(i * Math.PI / 72) - (r + O) * Math.sin(((R + r) / r) * (i * Math.PI / 72))
ctx.lineTo(x2, y2);
x1 = x2;
y1 = y2;
i++;
} while (x2 != R-O && y2 != 0 );
ctx.stroke();
}
}