#(89):Canvas(續)
## 變換
`Canvas`中的“變形”,主要指的是坐標系的變換,而不是路徑的變換。這與 QML 元素變換非常相似,都可以實現坐標系統的`scale`(縮放)、`rotate`(旋轉)和`translate`(平移);不同的是,變換的原點是畫布原點。例如,如果以一個路徑的中心點為定點進行縮放,那么,你需要現將畫布原點移動到路徑中心點。我們也可以使用變換函數實現復雜的變換。理解“變換是針對坐標系的”這一點非常重要,有時候可以避免很多意外的結果。
~~~
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()
}
}
~~~
運行結果如下:
[](http://files.devbean.net/images/2015/09/qq-canvas-transform.png)
通過調用`resetTransform()`函數,可以將變換矩陣重置為單位矩陣:
~~~
ctx.resetTransform()
~~~
## 組合
組合意思是,將你繪制的圖形與已存在的像素做一些融合操作。`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()
}
}
}
~~~
代碼運行結果如下:
[](http://files.devbean.net/images/2015/09/canvas-composition.png)
## 像素緩存
使用`canvas`,你可以將`canvas`內容的像素數據讀取出來,并且能夠針對這些數據做一些操作。
使用`createImageData(sw, sh)`或`getImageData(sx, sy, sw, sh)`函數可以讀取圖像數據。這兩個函數都會返回一個`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,右側則會顯示這個圖像。
## Canvas 繪制
下面我們利用`Canvas`元素創建一個畫板程序。我們程序的運行結果如下所示:
[](http://files.devbean.net/images/2015/09/canvas-painter.png)
窗口上方是調色板,用于設置畫筆顏色。色板是一個填充了顏色的矩形,其中覆蓋了一個鼠標區域,用于檢測鼠標點擊事件。
~~~
Row {
id: colorTools
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: 8
}
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
}
}
}
}
~~~
調色板所支持的顏色保存在一個數組中,畫筆的當前顏色則保存在`paintColor`屬性。當用戶點擊調色板的一個色塊,該色塊的顏色就會被賦值給`paintColor`屬性。
為了監聽鼠標事件,我們在畫布上面覆蓋了一個鼠標區域,利用鼠標按下和位置改變的信號處理函數完成繪制:
~~~
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()
}
}
}
~~~
鼠標左鍵按下時,其初始位置保存在`lastX`和`lastY`兩個屬性。鼠標位置的改變會請求畫布進行重繪,該請求則會調用`onPaint()`處理函數。
最后,為了繪制用戶筆記,在`onPaint()`處理函數中,我們首先創建了一個新的路徑,將其移動到最后的位置,然后我們從鼠標區域獲得新的位置,在最后的位置與新的位置之間繪制直線,同時,將當前鼠標位置(也就是新的位置)設置為新的最后的位置。
## 從 HTML5 移植
由于 QML 的`Canvas`對象由 HTML 5 的 canvas 標簽借鑒而來,將 HTML 5 的 canvas 應用移植到 QML?`Canvas`也是相當容易。我們以 Mozilla 提供的繁華曲線頁面為例,演示移植的過程。可以在[這里](http://files.devbean.net/code/spirograph.html)看到該頁面的運行結果。下面是 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 如下所示:
~~~
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();
}
}
~~~
運行一下這段代碼:
[](http://files.devbean.net/images/2015/09/canvas-from-html5.png)
- (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(續)