Canvas Element(画布元素)
在早些时候的
Qt4
中加入
QML
时,一些开发者讨论如何在
QtQuick
中绘制一个圆形。 类似圆形的问题,一些开发者也对于其它的形状的支持进行了讨论。在QtQuick
中 没有圆形,只有矩形。在Qt4
中,如果你需要一个除了矩形外的形状,你需要使用 图片或者使用你自己写的C++
圆形元素。
Qt5
中引进了画布元素(canvas element),允许脚本绘制。画布元素(canvas element)提供了一个依赖于分辨率的位图画布,你可以使用
JavaScript
脚本来绘制 图形,制作游戏或者其它的动态图像。画布元素(canvas element)是基于
HTML5 的画布元素来完成的。
画布元素(canvas element)的基本思想是使用一个
2D
对象来渲染路径。这个
2D 对象包括了必要的绘图函数,画布元素(canvas element)充当绘制画布。
2D
对象 支持画笔,填充,渐变,文本和绘制路径创建命令。
让我们看看一个简单的路径绘制的例子:
Window {//定义一个窗口
visible: true //设置窗口的可见性为true,这意味着当您运行此代码时,窗口将立即显示出来。
width: 600
height: 480
title: "Image Window"
//这一行定义了一个Canvas元素(画布),
//Canvas是Qt Quick中用于绘图的一个组件。
Canvas {
id: root
width: 200; height: 200
//这一行开始定义一个onPaint事件处理程序,
//当Canvas需要重新绘制时,这个处理程序将被调用。
onPaint: {
//获取一个2D渲染上下文,并存储在变量ctx中。
//这个上下文是用来绘制图形、文本和图像的
var ctx = getContext("2d")
//这一行设置了线条的宽度为4像素。
ctx.lineWidth = 4
//设置了笔触颜色为蓝色。
ctx.strokeStyle = "blue"
//设置了填充颜色为钢蓝色。
ctx.fillStyle = "steelblue"
//开始一个新的路径。所有后续的绘图命令都会添加到这个路径上
ctx.beginPath()
//将画笔移动到坐标(50,50)。
ctx.moveTo(50,50)
//从当前位置画一条线到坐标(150,50)。
ctx.lineTo(150,50)
ctx.lineTo(150,150)
ctx.lineTo(50,150)
//关闭路径,形成一个封闭的多边形。
ctx.closePath()
//通过ctx.fill()和ctx.stroke()命令,填充矩形并绘制其轮廓。
ctx.fill()
ctx.stroke()
}
}
}
画笔的宽度被设置为
4
个像素,并且定义
strokeStyle
(画笔样式)为蓝色。最后的 形状由设置填充样式(fillStyle)为
steelblue
颜色,然后填充完成的。只有调用 stroke或者
fill
函数,创建的路径才会绘制,它们与其它的函数使用是相互独立的。
调用stroke或者fill
将会绘制当前的路径,创建的路径是不可重用的,只有绘制状态能够被存储和恢复。
在
QML
中,画布元素(canvas element)充当了绘制的容器。
2D绘制对象提供了实际绘制的方法。绘制需要在onPaint
事件中完成。
画布自身提供了典型的二维笛卡尔坐标系统,左上角是(0,0)坐标。
Y轴坐标轴向下,X
轴坐标轴向右。
典型绘制命令调用如下:
1. 装载画笔或者填充模式
2. 创建绘制路径
3. 使用画笔或者填充绘制路径
注意
通常在你重置了路径后你将会设置一个开始点,所以,在
beginPath()
这个操作后,你需要使用moveTo
来设置开始点。如果是第一次设置,默认就是(0,0)
便捷的接口(Convenient API)
在绘制矩形时,我们提供了一个便捷的接口,而不需要调用
stroke或者fill
来完成。
![](https://img-blog.csdnimg.cn/direct/1dfd5f552c084dc18492f11a8f09f102.png)
注意
画笔的绘制区域由中间向两边延展。一个宽度为
4
像素的画笔将会在绘制路径的里 面绘制2
个像素,外面绘制
2
个像素。
渐变(Gradients)
画布中可以使用颜色填充也可以使用渐变或者图像来填充。
Canvas {
id: root
width: 150; height: 150
onPaint: {
var ctx = getContext("2d")
//创建一个线性渐变。渐变的起始点是 (100, 0),结束点是 (100, 200)。
//这意味着渐变的方向是从垂直向上到垂直向下
//起始点和结束点都是针对画布而言的,渐变点的范围可以比较大,
//垂直渐变实际上跟x关系不大,y的值范围包含下面的矩形就可以凸显渐变
//渐变色节点比例实际上只跟这里的0-200有关系,跟下面矩形没关系
var gradient = ctx.createLinearGradient(1000,0,1000,200)
//这行代码在渐变的起始位置添加一个颜色停止点
gradient.addColorStop(0, "blue")
gradient.addColorStop(0.5, "lightsteelblue")
//这行代码将填充样式设置为之前创建的渐变。
ctx.fillStyle = gradient
//矩形的左上角坐标是 (50, 50),宽度是 100,高度是 100。由于设置了
//填充样式为渐变,这个矩形将显示为从蓝色到浅钢蓝色的渐变效果。
ctx.fillRect(50,50,100,100)
}
}
在这个例子中,渐变色定义在开始点(
100,0)到结束点(100,200
)。在我们画布 中是一个中间垂直的线。渐变色在停止点定义一个颜色,范围从0.0
到
1.0
。这里我 们使用一个蓝色作为0.0
(
100,0
),一个高亮刚蓝色作为
0.5
(
100,200
)。渐变色 的定义比我们想要绘制的矩形更大,所以矩形在它定义的范围内对渐变进行了裁剪。
Canvas {
id: root
width: 150; height: 150
onPaint: {
var ctx = getContext("2d")
//斜线(0,0)-(200,200)左上角往右下角的一条线渐变
var gradient = ctx.createLinearGradient(0,0,200,200)
//这行代码在渐变的起始位置添加一个颜色停止点
gradient.addColorStop(0, "blue")
gradient.addColorStop(0.5, "lightsteelblue")
//这行代码将填充样式设置为之前创建的渐变。
ctx.fillStyle = gradient
//矩形的左上角坐标是 (50, 50),宽度是 100,高度是 100。由于设置了
//填充样式为渐变,这个矩形将显示为从蓝色到浅钢蓝色的渐变效果。
ctx.fillRect(50,50,100,100)
}
}
阴影(Shadows)
注意
在
Qt5
的
alpha
版本中,我们使用阴影遇到了一些问题。
2D
对象的路径可以使用阴影增强显示效果。阴影是一个区域的轮廓线使用偏移量, 颜色和模糊来实现的。所以你需要指定一个阴影颜色(shadowColor),阴影
X
轴 偏移值(shadowOffsetX),阴影
Y
轴偏移值(shadowOffsetY)和阴影模糊 (shadowBlur)。这些参数的定义都使用
2D context
来定义。
2D context
是唯一的 绘制操作接口。
阴影也可以用来创建发光的效果。在下面的例子中我们使用白色的光创建了一 个“Earth”
的文本。在一个黑色的背景上可以有更加好的显示效果。
首先我们绘制黑色背景:
![](https://img-blog.csdnimg.cn/direct/7ca9be4eeca6476f819898af8b6bd33e.png)
图片(Images)
QML
画布支持多种资源的图片绘制。在画布中使用一个图片需要先加载图片资源。
在我们的例子中我们使用
Component.onCompleted
操作来加载图片。
Canvas {
id: root
width: 300; height: 300
// 当Canvas需要重新绘制时,会触发onPaint事件。
onPaint: {
var ctx = getContext("2d")
// 在坐标(10,10)处绘制一个名为'football.png'的图像。
ctx.drawImage('images/football.png', 10, 10)
//存储当前上下文设置
ctx.save()
ctx.strokeStyle = 'red'
//创建一个三角形作为剪辑区域
//将坐标系原点移动到(100,0)。
ctx.translate(200,0)
ctx.beginPath()
ctx.moveTo(10,10)
ctx.lineTo(55,10)
ctx.lineTo(35,55)
ctx.closePath()
ctx.clip() //根据当前的路径创建一个剪切区域。
//只显示剪切区域内的部分。
// draw image with clip applied
ctx.drawImage('images/football.png', 10, 10)
// draw stroke around path
ctx.stroke()
// restore previous setup恢复之前保存的绘图上下文状态。
ctx.restore()
}
//当组件完成加载时执行以下操作
Component.onCompleted: {
loadImage("images/football.png")
}
}
在左边,足球图片使用
10×10
的大小绘制在左上方的位置。在右边我们对足球图片 进行了裁剪。图片或者轮廓路径都可以使用一个路径来裁剪。裁剪需要定义一个裁 剪路径,然后调用clip()
函数来实现裁剪。在
clip()
之前所有的绘制操作都会用来进行 裁剪。如果还原了之前的状态或者定义裁剪区域为整个画布时,裁剪是无效的。
转换(Transformation)
画布有多种方式来转换坐标系。这些操作非常类似于
QML
元素的转换。你可以通过 缩放(scale),旋转(rotate),translate(移动)来转换坐标系。与
QML
元素的 转换不同的是,转换原点通常就是画布原点。例如,从中心点放大一个封闭的路径,你需要先将画布原点移动到整个封闭的路径的中心点上。使用这些转换的方法 你可以创建一些更加复杂的转换。
Canvas {
id: root
width: 240; height: 120
onPaint: {
var ctx = getContext("2d")
ctx.translate(120,60)
ctx.strokeStyle = "blue"
ctx.lineWidth = 4
ctx.beginPath()
ctx.rect(-20, -20, 40, 40)
ctx.stroke()
// draw path now rotated
ctx.rotate(Math.PI/4) //旋转整个画布45度
//没有下面这两行根本显示不出来两个矩形框
//因为旋转操作是在绘制第二个矩形之前完成的,
//所以第一个矩形(蓝色)在旋转之后不会被重新绘制。
ctx.beginPath()
ctx.rect(-20, -20, 40, 40)
ctx.strokeStyle = "green"
ctx.stroke()
}
}
除了移动画布外,也可以使用
scale(x,y)
来缩放
x,y
坐标轴。旋转使用
rotate(angle)
, angle是角度(
360
度
=2*Math.PI
)。使用
setTransform(m11,m12,m21,m22,dx,dy) 来完成矩阵转换。
警告
QML
画布中的转换与
HTML5
画布中的机制有些不同。不确定这是不是一个
Bug
。
注意
重置矩阵你可以调用
resetTransform()
函数来完成,这个函数会将转换矩阵还原为 单位矩阵。
组合模式(Composition Mode)
组合允许你绘制一个形状然后与已有的像素点集合混合。画布提供了多种组合模 式,使用globalCompositeOperation(mode)
来设置。
![](https://img-blog.csdnimg.cn/direct/e5bf6601e7c54d8db9282a41dc85d406.png)
Canvas {
id: root
width: 240; height: 120
onPaint: {
var ctx = getContext("2d")
//设置全局的合成操作为“xor”。
//这意味着绘制的新图形和已有的图形将进行XOR操作,而不是简单的覆盖。
ctx.globalCompositeOperation = "xor"
ctx.fillStyle = "#33a9ff"
for(var i=0; i<40; i++) {
ctx.beginPath()
//在Canvas上绘制一个随机位置和大小的圆形。
//这个圆形的位置由Math.random()*400和Math.random()*200决定,
//半径为20像素,起始角度为0,结束角度为2*Math.PI(一个完整的圆)。
ctx.arc(Math.random()*400, Math.random()*200, 20, 0, 2*Math.PI)
ctx.closePath()
ctx.fill()
}
}
}
下面这个例子遍历了列表中的组合模式,使用对应的组合模式生成了一个矩形与圆形的组合。
Canvas {
id: root
width: 640; height: 120
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.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()
}
}
}
像素缓冲(Pixels Buffer)
当你使用画布时,你可以检索读取画布上的像素数据,或者操作画布上的像素。读取图像数据使用createImageData(sw,sh)
或者getImageData(sx,sy,sw,sh)
。这两个函数都会返回一个包含宽度(width
),高度(height)和数据(data)的图像数据 (ImageData)对象。图像数据包含了一维数组像素数据,使用
RGBA
格式进行检索。每个数据的数据范围在0
到
255
之间。设置画布的像素数据你可以使用 putImageData(imagedata,dx,dy)函数来完成。
另一种检索画布内容的方法是将画布的数据存储进一张图片中。可以使用画布的函数save(path)
或者toDataURL(mimeType)
来完成,toDataURL(mimeType)会返回一个图片的地址,这个链接可以直接用Image
元素来读取。
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
//生成0-0.1随机,当累计超过1时重置为0
hue += Math.random()*0.1
if(hue > 1.0) { hue -= 1 }
//设置绘图的全局透明度为0.7。
ctx.globalAlpha = 0.7
//使用HSLA颜色模式设置填充颜色,其中hue是色相,
//0.5是饱和度,0.5是亮度,1.0是alpha(透明度)
ctx.fillStyle = Qt.hsla(hue, 0.5, 0.5, 1.0)
ctx.beginPath()
ctx.moveTo(x+5,y)
//绘制一个从随机点开始的圆形路径,并填充该路径。x/10是半径
ctx.arc(x,y, x/10, 0, 360)
ctx.closePath()
ctx.fill()
}
MouseArea {
anchors.fill: parent
onClicked: {
//将Canvas的内容转换为PNG格式的图像数据URL
var url = canvas.toDataURL('image/png')
print('image url=', url)
//将Image组件的源设置为这个URL,从而显示Canvas上的内容。
//因此随机绘画什么,点击绘画鼠标区域的时候,就会在右侧同步绘画
image.source = url
}
}
}
Image {
id: image
x: 130; y: 10
width: 100; height: 100
}
Timer {
//当Timer触发时触发的事件处理器,请求重新绘制Canvas。
//不启动定时器也只能画一次
interval: 1000
//使Timer立即开始运行。
running: true
//在Timer开始时触发一次。
triggeredOnStart: true
//定时器重复触发事件。
repeat: true
//当Timer触发时触发的事件处理器,请求重新绘制Canvas。
onTriggered: canvas.requestPaint()
}
}
画布绘制(Canvas Paint)
在这个例子中我们将使用画布(
Canvas
)创建一个简单的绘制程序。
在我们场景的顶部我们使用行定位器排列四个方形的颜色块。一个颜色块是一个简 单的矩形,使用鼠标区域来检测点击。
Window {//定义一个窗口
visible: true //设置窗口的可见性为true,这意味着当您运行此代码时,窗口将立即显示出来。
width: 680
height: 480
title: "Image Window"
//这一行定义了一个Canvas元素(画布),
//Canvas是Qt Quick中用于绘图的一个组件。
Row {
id: colorTools
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: 8
}
property color paintColor: "#33B5E5"
spacing: 4
Repeater {
model: ["#33B5E5", "#99CC00", "#FFBB33", "#FF4444"]
Rectangle {
id: red
width: 48
height: 48
color: modelData
MouseArea{
anchors.fill: parent
onClicked: {
//经点击的颜色赋值给自定义颜色
colorTools.paintColor = red.color
}
}
}
}
}
Canvas {
id: canvas
anchors {
left: parent.left
right: parent.right
top: colorTools.bottom
bottom: parent.bottom
margins: 8
}
property real lastX
property real lastY
property color color: colorTools.paintColor
onPaint: {
var ctx = getContext('2d')
ctx.lineWidth = 1.5
ctx.strokeStyle = canvas.color
ctx.beginPath()
ctx.moveTo(lastX, lastY)
lastX = area.mouseX
lastY = area.mouseY
ctx.lineTo(lastX, lastY)
ctx.stroke()
}
MouseArea {
id: area
anchors.fill: parent
onPressed: {
//这个是很有必要的,第一次画点的位置
canvas.lastX = mouseX
canvas.lastY = mouseY
}
onPositionChanged: {
//重绘
canvas.requestPaint()
}
}
}
}
![](https://img-blog.csdnimg.cn/direct/047eb8e0947b488aa15f045031db3629.png)
HTML5画布移植(Porting from HTML5 Canvas)
这个后面再研究