#(88):Canvas
在 QML 刚刚被引入到 Qt 4 的那段时间,人们往往在讨论 Qt Quick 是不是需要一个椭圆组件。由此,人们又联想到,是不是还需要其它的形状?这种没玩没了的联想导致了一个最直接的结果:除了圆角矩形,Qt Quick 什么都没有提供,包括椭圆。如果你需要一个椭圆,那就找个图片,或者干脆自己用 C++ 写一个吧(反正 Qt Quick 是可以扩展的,不是么)!
为了使用脚本化的绘图机制,Qt 5 引入的Canvas元素。Canvas元素提供了一种与分辨率无关的位图绘制机制。通过Canvas,你可以使用 JavaScript 代码进行绘制。如果熟悉 HTML5 的话,Qt Quick 的Canvas元素与 HTML5 中的Canvas元素如出一辙。
Canvas元素的基本思想是,使用一个 2D 上下文对象渲染路径。这个 2D 上下文对象包含所必须的绘制函数,从而使Canvas元素看起来就像一个画板。这个对象支持画笔、填充、渐变、文本以及其它一系列路径创建函数。
下面我们看一个简单的路径绘制的例子:
~~~
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()
}
}
~~~
上面的代码将在左上角为 (50, 50) 处,绘制一个长和宽均为 100 像素的矩形。这个矩形使用钢铁蓝填充,并且具有蓝色边框。程序运行结果如下所示:
Canvas
让我们来仔细分析下这段代码。首先,画笔的宽度设置为 4 像素;使用strokeStyle属性,将画笔的颜色设置为蓝色。fillStyle属性则是设置填充色为 steelblue。只有当调用了stroke()或fill()函数时,真实的绘制才会执行。当然,我们也完全可以独立使用这两个函数,而不是一起。调用stroke()或fill()函数意味着将当前路径绘制出来。需要注意的是,路径是不能够被复用的,只有当前绘制状态才能够被复用。所谓“当前绘制状态”,指的是当前的画笔颜色、宽度、填充色等属性。
在 QML 中,Canvas元素就是一种绘制的容器。2D 上下文对象作为实际绘制的执行者。绘制过程必须在onPaint事件处理函数中完成。下面即一个代码框架:
~~~
Canvas {
width: 200; height: 200
onPaint: {
var ctx = getContext("2d")
// 设置绘制属性
// 开始绘制
}
}
~~~
Canvas本身提供一个典型的二维坐标系,原点在左上角,X 轴正方向向右,Y 轴正方向向下。使用Canvas进行绘制的典型过程是:
设置画笔和填充样式
创建路径
应用画笔和填充
例如:
~~~
onPaint: {
var ctx = getContext("2d")
// 设置画笔
ctx.strokeStyle = "red"
// 创建路径
ctx.beginPath()
ctx.moveTo(50,50)
ctx.lineTo(150,50)
// 绘制
ctx.stroke()
}
~~~
上面这段代码运行结果应该是一个从 (50, 50) 开始,到 (150, 50) 结束的一条红色线段。
由于我们在创建路径之前会将画笔放在起始点的位置,因此,在调用beginPath()函数之后的第一个函数往往是moveTo()。
##形状 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)
}
}
~~~
代码运行结果如下:
canvas rectangle
注意蓝色边框的位置。在绘制边框时,画笔会沿着路径进行绘制。上面给出的 4 像素边框,其中心点为路径,因此会有 2 像素在路径外侧,2 像素在路径内侧。
##渐变
Canvas元素可以使用颜色进行填充,同样也可以使用渐变。例如下面的代码:
~~~
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(50,50,100,100)
}
~~~
运行结果如下所示:
canvas 渐变
在这个例子中,渐变的起始点位于 (100, 0),终止点位于 (100, 200)。注意这两个点的位置,这两个点实际创建了一条位于画布中央位置的竖直线。渐变类似于插值,可以在 [0.0, 1.0] 区间内插入一个定义好的确定的颜色;其中,0.0 意味着渐变的起始点,1.0 意味着渐变的终止点。上面的例子中,我们在 0.0 的位置(也就是渐变起始点 (100, 0) 的位置)设置颜色为“blue”;在 1.0 的位置(也就是渐变终止点 (100, 200) 的位置)设置颜色为“lightsteelblue”。注意,渐变的范围可以大于实际绘制的矩形,此时,绘制出来的矩形实际上裁减了渐变的一部分。因此,渐变的定义其实是依据画布的坐标,也不是定义的绘制路径的坐标。
##阴影
路径可以使用阴影增强视觉表现力。我们可以把阴影定义为一个围绕在路径周围的区域,这个区域会有一定的偏移、有一定的颜色和特殊的模糊效果。我们可以使用shadowColor属性定义阴影的颜色;使用shadowOffsetX属性定义阴影在 X 轴方向的偏移量;使用shadowOffsetY属性定义阴影在 Y 轴方向的偏移量;使用shadowBlur属性定义阴影模糊的程度。不仅是阴影,利用这种效果,我们也可以实现一种围绕在路径周边的发光特效。下面的例子中,我们将创建一个带有发光效果的文本。为了更明显的显示发光效果,其背景界面将会是深色的。下面是相应的代码:
~~~
import QtQuick 2.0
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("Earth", 30, 80);
}
}
~~~
首先,我们利用 #333 填充了一个背景矩形。矩形的起始点位于原点,长度和宽度分别绑定到画布的长度和宽度。接下来定义阴影的属性。最后,我们设置文本字体为 80 像素加粗的 sans-serif,会绘制了“Earth”单词。代码运行结果如下所示:
canvas 阴影
注意观察字母旁边的发光效果,这其实是使用阴影制作的。
##图像
Canvas元素支持从多种源绘制图像。为了绘制图像,需要首先加载图像;使用Component.onCompleted事件处理函数可以达到这一目的:
~~~
onPaint: {
var ctx = getContext("2d")
// 绘制图像
ctx.drawImage('assets/earth.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('assets/earth.png', 10, 10)
// 绘制路径
ctx.stroke()
// 恢复状态
ctx.restore()
}
Component.onCompleted: {
loadImage("assets/earth.png")
}
~~~
代码运行结果如下:
canvas image
左侧的地球图像绘制在左上角坐标为 (10, 10) 的位置;右侧的图像应用了路径裁剪。图像和路径都可以被另外的路径裁剪,只需使用clip()函数即可。调用该函数之后,所有的绘制都将限制在这个路径中,也就是所谓“裁剪”。裁剪会在恢复上次状态时被取消。
- (1)序
- (2)Qt 简介
- (3)Hello, world!
- (4)信号槽
- (5)自定义信号槽
- (6)Qt 模块简介
- (7)MainWindow 简介
- (8)添加动作
- (9)资源文件
- (10)对象模型
- (11)布局管理器
- (12)菜单栏、工具栏和状态栏
- (13)对话框简介
- (14)对话框数据传递
- (15)标准对话框 QMessageBox
- (16)深入 Qt5 信号槽新语法
- (17)文件对话框
- (18)事件
- (19)事件的接受与忽略
- (21)事件过滤器
- (22)事件总结
- (23)自定义事件
- (24)Qt 绘制系统简介
- (25)画刷和画笔
- (26)反走样
- (27)渐变
- (28)坐标系统
- (29)绘制设备
- (30)Graphics View Framework
- (31)贪吃蛇游戏(1)
- (32)贪吃蛇游戏(2)
- (33)贪吃蛇游戏(3)
- (34)贪吃蛇游戏(4)
- (35)文件
- (36)二进制文件读写
- (37)文本文件读写
- (38)存储容器
- (39)遍历容器
- (40)隐式数据共享
- (41)model/view 架构
- (42)QListWidget、QTreeWidget 和 QTableWidget
- (43)QStringListModel
- (44)QFileSystemModel
- (45)模型
- (46)视图和委托
- (47)视图选择
- (48)QSortFilterProxyModel
- (49)自定义只读模型
- (50)自定义可编辑模型
- (51)布尔表达式树模型
- (52)使用拖放
- (53)自定义拖放数据
- (54)剪贴板
- (55)数据库操作
- (56)使用模型操作数据库
- (57)可视化显示数据库数据
- (58)编辑数据库外键
- (59)使用流处理 XML
- (60)使用 DOM 处理 XML
- (61)使用 SAX 处理 XML
- (62)保存 XML
- (63)使用 QJson 处理 JSON
- (64)使用 QJsonDocument 处理 JSON
- (65)访问网络(1)
- (66)访问网络(2)
- (67)访问网络(3)
- (68)访问网络(4)
- (69)进程
- (70)进程间通信
- (71)线程简介
- (72)线程和事件循环
- (73)Qt 线程相关类
- (74)线程和 QObject
- (75)线程总结
- (76)QML 和 QtQuick 2
- (77)QML 语法
- (78)QML 基本元素
- (79)QML 组件
- (80)定位器
- (81)元素布局
- (82)输入元素
- (83)Qt Quick Controls
- (84)Repeater
- (85)动态视图
- (86)视图代理
- (87)模型-视图高级技术
- (88)Canvas
- (89)Canvas(续)