Qt5中引进了画布元素(canvas element) ,允许脚本绘制。画布元素(canvaselement) 提供了一个依赖于分辨率的位图画布,你可以使用JavaScript脚本来绘制图形,制作游戏或者其它的动态图像。画布元素(canvas element) 是基于HTML5的画布元素来完成的。
画布元素(canvas element) 的基本思想是使用一个2D对象来渲染路径。这个2D对象包括了必要的绘图函数,画布元素(canvas element) 充当绘制画布。2D对象
支持画笔,填充,渐变,文本和绘制路径创建命令。
让我们看看一个简单的路径绘制的例子:
import QtQuick 2.12
import QtQuick.Window 2.12
Canvas{
id: root
// canvas size
width: 200; height: 200
// handler to override for drawing
onPaint: {
var ctx = getContext("2d") //绘制对象
// setup the stroke
ctx.lineWidth = 4
ctx.strokeStyle = "blue"
//setup the fill
ctx.fillStyle = "steelblue"
// begin a new path to draw
ctx.beginPath()
ctx.moveTo(50,50)
ctx.lineTo(150,50)
ctx.lineTo(150,150)
ctx.lineTo(50,150)
ctx.closePath()
ctx.fill()
ctx.stroke()
}
}
这个例子产生了一个在坐标(50,50) ,高宽为100的填充矩形框,并且使用了画笔来修饰边界。画笔的宽度被设置为4个像素,并且定义strokeStyle(画笔样式) 为蓝色。最后的形状由设置填充样式(fillStyle) 为steelblue颜色,然后填充完成的。只有调用stroke或者fill函数,创建的路径才会绘制,它们与其它的函数使用是相互独立的。调用stroke或者fill将会绘制当前的路径,创建的路径是不可重用的,只有绘制状态能够被存储和恢复。
在QML中,画布元素(canvas element) 充当了绘制的容器。2D绘制对象提供了实际绘制的方法。绘制需要在onPaint事件中完成。
在绘制矩形时,我们提供了一个便捷的接口,而不需要调用stroke或者fill来完成。
import QtQuick 2.0
Canvas {
id: root
// canvas size
width: 200; height: 200
// handler to override for drawing
onPaint: {
var ctx = getContext("2d")
// setup the stroke
ctx.lineWidth = 4
ctx.strokeStyle = "green"
//setup the fill
ctx.fillStyle = "steelblue"
ctx.fillRect(20,20,80,80)
ctx.clearRect(30,30, 60, 60)
ctx.strokeRect(20,20, 40, 40)
}
}
画笔的绘制区域由中间向两边延展。一个宽度为4像素的画笔将会在绘制路径的里面绘制2个像素,外面绘制2个像素。
画布中可以使用颜色填充也可以使用渐变或者图像来填充。
import QtQuick 2.0
Canvas {
id: root
width: 200; height: 200
onPaint: {
var ctx = getContext("2d")
//(100,0)开始点 (100,200)结束点
var gradient = ctx.createLinearGradient(100,0,100,200)
gradient.addColorStop(0,"blue")
gradient.addColorStop(0.5,"lightsteelblue")
ctx.fillStyle = gradient
ctx.fillRect(50,50,100,100)
}
}
在这个例子中,渐变色定义在开始点(100,0) 到结束点(100,200) 。在我们画布中是一个中间垂直的线。渐变色在停止点定义一个颜色,范围从0.0到1.0。这里我们使用一个蓝色作为0.0(100,0) ,一个高亮刚蓝色作为0.5(100,200) 。渐变色的定义比我们想要绘制的矩形更大,所以矩形在它定义的范围内对渐变进行了裁剪。
在Qt5的alpha版本中,我们使用阴影遇到了一些问题。
2D对象的路径可以使用阴影增强显示效果。阴影是一个区域的轮廓线使用偏移量,颜色和模糊来实现的。所以你需要指定一个阴影颜色(shadowColor) ,阴影X轴
偏移值(shadowOffsetX) ,阴影Y轴偏移值(shadowOffsetY) 和阴影模糊(shadowBlur) 。这些参数的定义都使用2D context来定义。2D context是唯一的
绘制操作接口。
阴影也可以用来创建发光的效果。在下面的例子中我们使用白色的光创建了一个“Earth”的文本。在一个黑色的背景上可以有更加好的显示效果。
import QtQuick 2.0
Canvas {
id: canvas
width: 400; height: 200
smooth: true
onPaint: {
var ctx = getContext("2d")
ctx.strokeStyle = "#333333"
ctx.fillRect(0,0,canvas.width,canvas.height);
ctx.shadowColor = "#2ed5fa";
ctx.shadowOffsetX = 2;
ctx.shadowOffsetY = 2;
ctx.shadowBlur = 10;
ctx.font = 'bold 80px Windows';
ctx.fillStyle = "#24d12e";
ctx.fillText("Canvas!",30,180);
}
}
QML画布支持多种资源的图片绘制。在画布中使用一个图片需要先加载图片资源。在我们的例子中我们使用Component.onCompleted操作来加载图片。
在项目中放了一个图片资源,运行:
import QtQuick 2.0
Canvas {
id: root
width: 400; height: 120
onPaint: {
var ctx = getContext("2d")
ctx.drawImage('assets/ball.png', 10, 10)
ctx.save() //保存
ctx.strokeStyle = '#ff2a68'
//开始画切割图
ctx.beginPath()
ctx.moveTo(110,10)
ctx.lineTo(155,10)
ctx.lineTo(135,55)
ctx.closePath()
ctx.clip()// create clip from the path
ctx.drawImage('assets/ball.png', 100, 10)
ctx.stroke()
// restore previous context
ctx.restore()
}
Component.onCompleted: {
loadImage('assets/ball.png')
}
}
在左边,足球图片使用10×10的大小绘制在左上方的位置。在右边我们对足球图片进行了裁剪。图片或者轮廓路径都可以使用一个路径来裁剪。裁剪需要定义一个裁剪路径,然后调用clip()函数来实现裁剪。在clip()之前所有的绘制操作都会用来进行裁剪。如果还原了之前的状态或者定义裁剪区域为整个画布时,裁剪是无效的。
画布有多种方式来转换坐标系。这些操作非常类似于QML元素的转换。你可以通过缩放(scale) ,旋转(rotate) ,translate(移动) 来转换坐标系。与QML元素的转换不同的是,转换原点通常就是画布原点。例如,从中心点放大一个封闭的路径,你需要先将画布原点移动到整个封闭的路径的中心点上。使用这些转换的方法你可以创建一些更加复杂的转换。
import QtQuick 2.0
Canvas {
id: root
width: 240; height: 120
onPaint: {
var ctx = getContext("2d");
ctx.lineWidth = 4;
ctx.strokeStyle = "blue";
ctx.translate(root.width/2, root.height/2);
ctx.beginPath(); //开始
ctx.rect(-20, -20, 40, 40);
ctx.stroke();
// rotate coordinate system
ctx.rotate(Math.PI/4); //旋转
ctx.strokeStyle = "green";
// draw path
ctx.beginPath();
ctx.rect(-20, -20, 40, 40);
ctx.stroke();
}
}
除了移动画布外,也可以使用scale(x,y)来缩放x,y坐标轴。旋转使用rotate(angle),angle是角度(360度=2*Math.PI) 。使用setTransform(m11,m12,m21,m22,dx,dy) 来完成矩阵转换。
组合允许你绘制一个形状然后与已有的像素点集合混合。画布提供了多种组合模式,使用globalCompositeOperation(mode)来设置。
import QtQuick 2.0
Canvas {
id: root
width: 600; height: 400
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
当你使用画布时,你可以检索读取画布上的像素数据,或者操作画布上的像素。读取图像数据使用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元素来读取。
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()
}
}
在我们这个例子中,我们每秒在左边的画布中绘制一个的圆形。当使用鼠标点击画布内容时,会将内容存储为一个图片链接。在右边将会展示这个存储的图片。
在这个例子中我们将使用画布(Canvas) 创建一个简单的绘制程序。
在我们场景的顶部我们使用行定位器排列四个方形的颜色块。一个颜色块是一个简单的矩形,使用鼠标区域来检测点击。
import QtQuick 2.0
Rectangle {
width: 400; height: 300
color: "#333333"
Row{
id: colorTools
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: 8
}
property variant activeSquare: red
property color paintColor: "#33B5E5"
spacing: 4
Repeater{
model:["#33B5E5", "#99CC00", "#FFBB33", "#FF4444"]
ColorSquare
{
id:red
color: modelData
active: parent.paintColor ==color
onClicked: {
parent.paintColor = color
}
}
}
}
Rectangle {
anchors.fill: canvas
border.color: "#666666"
border.width: 4
}
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()
}
}
}
}
颜色存储在一个数组中,作为绘制颜色使用。当用户点击一个矩形时,矩形内的颜色被设置为colorTools的paintColor属性。
为了在画布上跟踪鼠标事件,我们使用鼠标区域(MouseArea) 覆盖画布元素,并连接点击和移动操作。
鼠标点击存储在laxstX与lastY属性中。每次鼠标位置的改变会触发画布的重绘,这将会调用onPaint操作。
最后绘制用户的笔划,在onPaint操作中,我们绘制从最近改变的点上开始绘制一条新的路径,然后我们从鼠标区域采集新的点,使用选择的颜色绘制线段到新的点
上。鼠标位置被存储为新改变的位置。
螺旋图形(Spiro Graph)
我们使用一个来自Mozila项目的螺旋图形例子来作为我们的基础示例。原始的HTML5代码被作为画布教程发布。
import QtQuick 2.0
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(20+j*50,20+i*50);
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();
}
}
import QtQuick 2.0
Canvas {
id: canvas
width: 800; height: 450
property real hue: 0
property real lastX: width * Math.random()
property real lastY: height * Math.random()
// M1>>
property bool requestLine: false
property bool requestBlank: false
// < 1.0) {
hue -= 1
}
context.strokeStyle = Qt.hsla(hue, 0.5, 0.5, 1.0)
context.shadowColor = 'white';
context.shadowBlur = 10;
context.stroke()
context.restore()
}
function blank(context) {
context.fillStyle = Qt.rgba(0,0,0,0.1)
context.fillRect(0, 0, canvas.width, canvas.height)
}
Component.onCompleted: {
lineTimer.start()
blankTimer.start()
}
}
在QML中,只能在onPaint操作中绘制。在HTML5中,通常调用setInterval使用计时器触发线段的绘制或者清屏。由于QML中不同的操作方法,仅仅只是调用这些函数不能实现我们想要的结
果,因为我们需要通过onPaint操作来实现。我们也需要修改颜色的格式。让我们看看需要改变哪些东西。
我们使用画布元素(Canvas) 作为我们QML文件的根元素。
代替直接调用的setInterval函数,我们使用两个计时器来请求重新绘制。一个计时器触发间隔较短,允许我们可以执行一些代码。我们无法告诉绘制函数哪个操作是我想触发的,我们为每个操作定义一个布尔标识,当重新绘制请求时,我们请求一个操作并且触发它。
property bool requestLine: false
property bool requestBlank: false
// <
现在我们的绘制函数应该像这样:
onPaint: {
var context = getContext('2d')
if(requestLine)
{
line(context)
requestLine = false
}
if(requestBlank)
{
blank(context)
requestBlank = false
}
}
最大的变化是使用QML的Qt.rgba()和Qt.hsla()。在QML中需要把变量值适配在0.0到1.0之间。同样应用在清屏函数中。
function blank(context) {
context.fillStyle = Qt.rgba(0,0,0,0.1)
context.fillRect(0, 0, canvas.width, canvas.height)
}
本章内容就是这莫多了,喜欢的小伙伴可以关注,一起学习qml book知识点。