(89)Canvas(续)
最后更新于:2022-04-01 06:31:36
#(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()
}
}
~~~
运行结果如下:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328ea20ee.png)](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()
}
}
}
~~~
代码运行结果如下:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328eb4a02.png)](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`元素创建一个画板程序。我们程序的运行结果如下所示:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328ec953a.png)](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();
}
}
~~~
运行一下这段代码:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328ed7bb0.png)](http://files.devbean.net/images/2015/09/canvas-from-html5.png)
(88)Canvas
最后更新于:2022-04-01 06:31:34
#(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()函数即可。调用该函数之后,所有的绘制都将限制在这个路径中,也就是所谓“裁剪”。裁剪会在恢复上次状态时被取消。
(87)模型-视图高级技术
最后更新于:2022-04-01 06:31:32
#(87):模型-视图高级技术
## PathView
`PathView`是 QtQuick 中最强大的视图,同时也是最复杂的。`PathView`允许创建一种更灵活的视图。在这种视图中,数据项并不是方方正正,而是可以沿着任意路径布局。沿着同一布局路径,数据项的属性可以被更详细的设置,例如缩放、透明度等。
使用`PathView`首先需要定义一个代理和一个路径。除此之外,`PathView`还可以设置很多其它属性,其中最普遍的是`pathItemCount`,用于设置可视数据项的数目;`preferredHighlightBegin`、`preferredHighlightEnd`和`highlightRangeMode`可以设置高亮的范围,也就是沿着路径上面的当前可以被显示的数据项。
在深入了解高亮范围之前,我们必须首先了解`path`属性。`path`接受一个`Path`元素,用于定义`PathView`中的代理所需要的路径。该路径使用`startX`和`startY`属性,结合`PathLine`、`PathQuad`、`PathCubic`等路径元素进行定义。这些元素可以结合起来形成一个二维路径。
一旦路径定义完成,我们可以使用`PathPercent`和`PathAttribute`元素进行调整。这些元素用于两个路径元素之间,更好的控制路径和路径上面的代理。`PathPercent`控制两个元素之间的路径部分有多大。它控制了路径上面代理的分布,这些代理按照其定义的百分比进行分布。
`PathAttribute`元素同`PathPercent`同样放置在元素之间。该元素允许沿路径插入一些属性值。这些属性值附加到代理上面,可用于任何能够使用的属性。
下面的例子演示了如何利用`PathView`实现卡片的弹入。这里使用了一些技巧来达到这一目的。它的路径包含三个`PathLine`元素。通过`PathPercent`元素,中间的元素可以正好位于中央位置,并且能够留有充足的空间,以避免被别的元素遮挡。元素的旋转、大小缩放和 Z 轴都是由`PathAttribute`进行控制。除了定义路径,我们还设置了`PathView`的`pathItemCount`属性。该属性用于指定路径所期望的元素个数。最后,代理中的`PathView.onPath`使用`preferredHighlightBegin`和`preferredHighlightEnd`属性控制代理的可见性。
~~~
PathView {
anchors.fill: parent
delegate: flipCardDelegate
model: 100
path: Path {
startX: root.width/2
startY: 0
PathAttribute { name: "itemZ"; value: 0 }
PathAttribute { name: "itemAngle"; value: -90.0; }
PathAttribute { name: "itemScale"; value: 0.5; }
PathLine { x: root.width/2; y: root.height*0.4; }
PathPercent { value: 0.48; }
PathLine { x: root.width/2; y: root.height*0.5; }
PathAttribute { name: "itemAngle"; value: 0.0; }
PathAttribute { name: "itemScale"; value: 1.0; }
PathAttribute { name: "itemZ"; value: 100 }
PathLine { x: root.width/2; y: root.height*0.6; }
PathPercent { value: 0.52; }
PathLine { x: root.width/2; y: root.height; }
PathAttribute { name: "itemAngle"; value: 90.0; }
PathAttribute { name: "itemScale"; value: 0.5; }
PathAttribute { name: "itemZ"; value: 0 }
}
pathItemCount: 16
preferredHighlightBegin: 0.5
preferredHighlightEnd: 0.5
}
~~~
代理直接使用了通过`PathAttribute`元素附加的`itemZ`、`itemAngle`和`itemScale`属性。需要注意的是,被附加到代理的属性只能在`wrapper`中使用。因此,我们又定义了一个`rotX`属性,以便在内部的`Rotation`元素中使用。另一点需要注意的是附件属性`PathView.onPath`的使用。通常我们会将这个属性绑定到可视化属性,这样允许`PathView`保留非可见元素,以便进行缓存。如果不这样设置,不可见元素可能会由于界面裁剪等原因被销毁,因为`PathView`比`ListView`和`GridView`要灵活得多,所以为提高性能,一般会使用这种绑定实现缓存。
~~~
Component {
id: flipCardDelegate
BlueBox {
id: wrapper
width: 64
height: 64
antialiasing: true
gradient: Gradient {
GradientStop { position: 0.0; color: "#2ed5fa" }
GradientStop { position: 1.0; color: "#2467ec" }
}
visible: PathView.onPath
scale: PathView.itemScale
z: PathView.itemZ
property variant rotX: PathView.itemAngle
transform: Rotation {
axis { x: 1; y: 0; z: 0 }
angle: wrapper.rotX;
origin { x: 32; y: 32; }
}
text: index
}
}
~~~
示例运行结果如下:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328c2d4d3.png)](http://files.devbean.net/images/2015/09/pathview.png)
完成`PathView`中图片和一些复杂元素的变换之后,通常会进行一定的性能优化,比如,将`Image`元素的`smooth`属性绑定到`PathView.view.moving`附加属性。这意味着在移动时,图片质量会稍有下降,静止时恢复正常。在视图移动时,很少有用户会在意图片的清晰度,因此,这样的妥协一般是可以接受的。
## 从 XML 加载模型
XML 是一种非常常见的数据格式,QML 提供了`XmlListModel`元素支持将 XML 数据转换为模型。`XmlListModel`可以加载本地或远程的 XML 文档,使用 XPath 表达式处理数据。
下面的例子给出了如何从 RSS 获取图片。`source`属性指向了一个远程地址,其数据会被自动下载下来。
~~~
import QtQuick 2.0
import QtQuick.XmlListModel 2.0
Background {
width: 300
height: 480
Component {
id: imageDelegate
Box {
width: listView.width
height: 220
color: '#333'
Column {
Text {
text: title
color: '#e0e0e0'
}
Image {
width: listView.width
height: 200
fillMode: Image.PreserveAspectCrop
source: imageSource
}
}
}
}
XmlListModel {
id: imageModel
source: "http://www.padmag.cn/feed"
query: "/rss/channel/item"
XmlRole { name: "title"; query: "title/string()" }
XmlRole { name: "imageSource"; query: "substring-before(substring-after(description/string(), 'img src=\"'), '\"')" }
}
ListView {
id: listView
anchors.fill: parent
model: imageModel
delegate: imageDelegate
}
}
~~~
当数据被下载下来,这个 XML 就被处理成模型的数据项和角色。`query`属性是 XPath 表达式语言,用于创建模型数据项。在这个例子中,该属性值为`/rss/channel/item`,因此,rss 标签下的每一个 channel 标签中的每一个 item 标签,都会被生成一个数据项。每一个数据项都可以定义一系列角色,这些角色使用`XmlRole`表示。每一个角色都有一个名字,代理可以使用附件属性访问到其值。角色的值是使用 XPath 表达式获取的。例如,`title`属性的值由`title/string()`表达式决定,返回的是`<title>`和`</title>`标签之间的文本。`imageSource`属性值则更有趣。它并不是直接由 XML 获取的字符串,而是一系列函数的运算结果。在返回的 XML 中,有些 item 中包含图片,使用`<img src=`标签表示。使用`substring-after`和`substring-before`XPath 函数,可以找到每张图片的地址并返回。因此,`imageSource`属性可以直接作为`Image`元素的`source`属性值。
## 分组列表
有时,列表中的数据可以分成几个部分,例如,按照列表数据的首字母分组。利用`ListView`可以将一个扁平的列表分为几个组,如下图所示:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328c3bbc8.png)](http://files.devbean.net/images/2015/09/section.png)
为了使用分组,需要设置`section.property`和`section.criteria`两个属性。`section.property`定义了使用哪个属性进行分组。这里,需要确保模型已经排好序了,以便每一部分能够包含连续的元素,否则,同一属性的名字可能出现在多个位置。`section.criteria`的可选值为`ViewSection.FullString`或`ViewSection.FirstCharacter`。前者为默认值,适用于具有明显分组的模型,例如,音乐集等;后者按照属性首字母分组,并且意味着所有属性都适用于此,常见例子是电话本的通讯录名单。
一旦分组定义完毕,在每一个数据项就可以使用附加属性`ListView.section`、`ListView.previousSection`和`ListView.nextSection`访问到这个分组。使用这个属性,我们就可以找到一个分组的第一个和最后一个元素,从而实现某些特殊功能。
我们也可以给`ListView`的`section.delegate`属性赋值,以便自定义分组显示的代理。这会在一个组的数据项之前插入一个用于显示分组的代理。这个代理可以使用附加属性访问当前分组的名字。
下面的例子按照国别对一组人进行分组。国别被设置为`section.property`属性的值。`section.delegate`组件,也就是`sectionDelegate`,用于显示每组的名字,也就是国家名。每组中的人名则使用`spaceManDelegate`显示。
~~~
import QtQuick 2.0
Background {
width: 300
height: 290
ListView {
anchors.fill: parent
anchors.margins: 20
clip: true
model: spaceMen
delegate: spaceManDelegate
section.property: "nation"
section.delegate: sectionDelegate
}
Component {
id: spaceManDelegate
Item {
width: ListView.view.width
height: 20
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 8
font.pixelSize: 12
text: name
color: '#1f1f1f'
}
}
}
Component {
id: sectionDelegate
BlueBox {
width: ListView.view.width
height: 20
text: section
fontColor: '#e0e0e0'
}
}
ListModel {
id: spaceMen
ListElement { name: "Abdul Ahad Mohmand"; nation: "Afganistan"; }
ListElement { name: "Marcos Pontes"; nation: "Brazil"; }
ListElement { name: "Alexandar Panayotov Alexandrov"; nation: "Bulgaria"; }
ListElement { name: "Georgi Ivanov"; nation: "Bulgaria"; }
ListElement { name: "Roberta Bondar"; nation: "Canada"; }
ListElement { name: "Marc Garneau"; nation: "Canada"; }
ListElement { name: "Chris Hadfield"; nation: "Canada"; }
ListElement { name: "Guy Laliberte"; nation: "Canada"; }
ListElement { name: "Steven MacLean"; nation: "Canada"; }
ListElement { name: "Julie Payette"; nation: "Canada"; }
ListElement { name: "Robert Thirsk"; nation: "Canada"; }
ListElement { name: "Bjarni Tryggvason"; nation: "Canada"; }
ListElement { name: "Dafydd Williams"; nation: "Canada"; }
}
}
~~~
## 关于性能
模型视图的性能很大程度上取决于创建新的代理所造成的消耗。例如,如果`clip`属性设置为`false`,当向下滚动`ListView`时,系统会在列表末尾创建新的代理,并且将列表上部已经不可显示的代理移除。显然,当初始化代理需要消耗大量时间时,用户在快速拖动滚动条时,这种现象就会造成一定程度的影响。
为了避免这种情况,你可以调整被滚动视图的外边框的值。通过修改`cacheBuffer`属性即可达到这一目的。在上面所述的有关竖直滚动的例子中,这个属性会影响到列表上方和下方会有多少像素。这些像素则影响到是否能够容纳这些代理。例如,将异步加载图片与此结合,就可以实现在图片真正加载完毕之后才显示出来。
更多的代理意味着更多的内存消耗,从而影响到用户的操作流畅度,同时也有关代理初始化的时间。对于复杂的代理,上面的方法并不能从根本上解决问题。代理初始化一次,其内容就会被重新计算。这会消耗时间,如果这个时间很长,很显然,这会降低用户体验。代理中子元素的个数同样也有影响。原因很简单,移动更多的元素当然要更多的时间。为了解决前面所说的两个问题,我们推荐使用`Loader`元素。`Loader`元素允许延时加载额外的元素。例如,一个可展开的代理,只有当用户点击时,才会显示这一项的详细信息,包含一个很大的图片。那么,利用`Loader`元素,我们可以做到,只有其被显示时才进行加载,否则不加载。基于同样的原因,应该使每个代理中包含的 JavaScript 代码尽可能少。最好能做到在代理之外调用复杂的 JavaScript 代码。这将减少代理创建时编译 JavaScript 所消耗的时间。
**附件**
1. [pathview.zip](http://files.devbean.net/code/pathview.zip)
2. [xmllistmodel.zip](http://files.devbean.net/code/xmllistmodel.zip)
3. [section.zip](http://files.devbean.net/code/section.zip)
(86)视图代理
最后更新于:2022-04-01 06:31:29
#(86):视图代理
与 [Qt model/view 架构](http://www.devbean.net/2013/01/qt-study-road-2-model-view/)类似,在自定义用户界面中,代理扮演着重要的角色。模型中的每一个数据项都要通过一个代理向用户展示,事实上,用户看到的可视部分就是代理。
每一个代理都可以访问一系列属性和附加属性。这些属性及附加属性中,有些来自于数据模型,有些则来自于视图。前者为代理提供了每一个数据项的数据信息;后者则是有关视图的状态信息。
代理中最常用到的是来自于视图的附加属性`ListView.isCurrentItem`和`ListView.view`。前者是一个布尔值,用于表示代理所代表的数据项是不是视图所展示的当前数据项;后者则是一个只读属性,表示该代理所属于的视图。通过访问视图的相关数据,我们就可以创建通用的可复用的代理,用于适配视图的大小和展示特性。下面的例子展示了每一个代理的宽度都绑定到视图的宽度,而代理的背景色则根据附加属性`ListView.isCurrentItem`的不同而有所不同。
~~~
import QtQuick 2.0
Rectangle {
width: 120
height: 300
gradient: Gradient {
GradientStop { position: 0.0; color: "#f6f6f6" }
GradientStop { position: 1.0; color: "#d7d7d7" }
}
ListView {
anchors.fill: parent
anchors.margins: 20
clip: true
model: 100
delegate: numberDelegate
spacing: 5
focus: true
}
Component {
id: numberDelegate
Rectangle {
width: ListView.view.width
height: 40
color: ListView.isCurrentItem?"#157efb":"#53d769"
border.color: Qt.lighter(color, 1.1)
Text {
anchors.centerIn: parent
font.pixelSize: 10
text: index
}
}
}
}
~~~
代码运行结果如下图所示:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328a72a82.png)](http://files.devbean.net/images/2015/08/delegate.png)
如果该模型的每一个数据项都关联一个动作,例如,响应对该数据项的点击操作,那么,这种功能就应该是每一个代理的一部分。这将事件管理从视图分离出来。视图主要处理的是各个子视图之间的导航、切换,而代理则是对一个特定的数据项的事件进行处理。完成这一功能最常用的方法是,为每一个视图创建一个`MouseArea`,然后响应其`onClicked`信号。我们会在后面看到这种实现的示例。
## 为增加、移除项添加动画
很多情况下,一个视图中的数据项并不是固定不变的,而是需要动态地增加、移除。数据项的增加、移除,其实是底层模型的修改的相关反应。此时,添加动画效果往往是个不错的选择,可以让用户清晰地明白究竟是哪些数据发生了改变。
为了达到这一目的,QML 为每个代理提供了两个信号,`onAdd`和`onRemove`,只要将这些信号与动画效果关联起来即可。
下面的例子演示了为动态修改`ListModel`增加动画效果。在屏幕下方有一个用于新增数据项的按钮。点击该按钮,会通过调用`append`函数向模型增加一个数据项。这将触发视图创建一个新的代理,并且发出`GridView.onAdd`信号。该信号关联了一个`SequentialAnimation`类型的动画,利用`scale`属性的变化,将代理缩放到视图。当视图中的一个数据项被点击时,该项会通过调用视图的`remove`函数被移除。这会发出`GridView.onRemove`信号,触发另一个`SequentialAnimation`类型的动画。不过,这一次代理需要在动画结束之后才能被销毁(相比之下,在添加代理时,代理必须在动画开始之前就被创建)。为了达到这一目的,我们使用`PropertyAction`元素,在动画开始之前将`GridView.delayRemove`属性设置为`true`,动画完成之后,再将其设置为`false`。这保证了在代理被销毁之前,动画能够顺利完成。
~~~
import QtQuick 2.0
Rectangle {
width: 480
height: 300
gradient: Gradient {
GradientStop { position: 0.0; color: "#dbddde" }
GradientStop { position: 1.0; color: "#5fc9f8" }
}
ListModel {
id: theModel
ListElement { number: 0 }
ListElement { number: 1 }
ListElement { number: 2 }
ListElement { number: 3 }
ListElement { number: 4 }
ListElement { number: 5 }
ListElement { number: 6 }
ListElement { number: 7 }
ListElement { number: 8 }
ListElement { number: 9 }
}
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
anchors.margins: 20
height: 40
color: "#53d769"
border.color: Qt.lighter(color, 1.1)
Text {
anchors.centerIn: parent
text: "Add item!"
}
MouseArea {
anchors.fill: parent
onClicked: {
theModel.append({"number": ++parent.count});
}
}
property int count: 9
}
GridView {
anchors.fill: parent
anchors.margins: 20
anchors.bottomMargin: 80
clip: true
model: theModel
cellWidth: 45
cellHeight: 45
delegate: numberDelegate
}
Component {
id: numberDelegate
Rectangle {
id: wrapper
width: 40
height: 40
gradient: Gradient {
GradientStop { position: 0.0; color: "#f8306a" }
GradientStop { position: 1.0; color: "#fb5b40" }
}
Text {
anchors.centerIn: parent
font.pixelSize: 10
text: number
}
MouseArea {
anchors.fill: parent
onClicked: {
if (!wrapper.GridView.delayRemove)
theModel.remove(index);
}
}
GridView.onRemove: SequentialAnimation {
PropertyAction { target: wrapper; property: "GridView.delayRemove"; value: true }
NumberAnimation { target: wrapper; property: "scale"; to: 0; duration: 250; easing.type: Easing.InOutQuad }
PropertyAction { target: wrapper; property: "GridView.delayRemove"; value: false }
}
GridView.onAdd: SequentialAnimation {
NumberAnimation { target: wrapper; property: "scale"; from: 0; to: 1; duration: 250; easing.type: Easing.InOutQuad }
}
}
}
}
~~~
下图是运行初始效果。
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328a7fc93.png)](http://files.devbean.net/images/2015/08/delegate-animation.png)
## 改变代理的形状
在表现列表时,常常会有这么一种机制:当数据项被选中时,该项会变大以充满屏幕。这种行为可以将被激活的数据项放置在屏幕中央,或者为用户显示更详细的信息。在下面的例子中,`ListView`的每一个数据项在点击时都会充满整个列表视图,多出来的额外空间用于显示更多信息。我们使用状态实现这种机制。在这个过程中,列表的很多属性都会发生改变。
首先,`wrapper`的高度会被设置为`ListView`的高度;缩略图会变大,从先前的位置移动到一个更大的位置。除此以外,两个隐藏的组件,`factsView`和`closeButton`会显示在恰当的位置。最后,`ListView`的`contentsY`属性会被重新设置为代理的`y`值。`contentsY`属性其实就是视图的可见部分的顶部距离。视图的`interactive`属性会被设置为`false`,这可以避免用户通过拖动滚动条使视图移动。当数据项第一次被点击时,它会进入`expanded`状态,是其代理充满整个`ListView`,并且重新布局内容。当点击关闭按钮时,`expanded`状态被清除,代理重新回到原始的状态,`ListView`的交互也重新被允许。
~~~
import QtQuick 2.0
Item {
width: 300
height: 480
Rectangle {
anchors.fill: parent
gradient: Gradient {
GradientStop { position: 0.0; color: "#4a4a4a" }
GradientStop { position: 1.0; color: "#2b2b2b" }
}
}
ListView {
id: listView
anchors.fill: parent
delegate: detailsDelegate
model: planets
}
ListModel {
id: planets
ListElement { name: "Mercury"; imageSource: "images/mercury.jpeg"; facts: "Mercury is the smallest planet in the Solar System. It is the closest planet to the sun. It makes one trip around the Sun once every 87.969 days." }
ListElement { name: "Venus"; imageSource: "images/venus.jpeg"; facts: "Venus is the second planet from the Sun. It is a terrestrial planet because it has a solid, rocky surface. The other terrestrial planets are Mercury, Earth and Mars. Astronomers have known Venus for thousands of years." }
ListElement { name: "Earth"; imageSource: "images/earth.jpeg"; facts: "The Earth is the third planet from the Sun. It is one of the four terrestrial planets in our Solar System. This means most of its mass is solid. The other three are Mercury, Venus and Mars. The Earth is also called the Blue Planet, 'Planet Earth', and 'Terra'." }
ListElement { name: "Mars"; imageSource: "images/mars.jpeg"; facts: "Mars is the fourth planet from the Sun in the Solar System. Mars is dry, rocky and cold. It is home to the largest volcano in the Solar System. Mars is named after the mythological Roman god of war because it is a red planet, which signifies the colour of blood." }
}
Component {
id: detailsDelegate
Item {
id: wrapper
width: listView.width
height: 30
Rectangle {
anchors.left: parent.left
anchors.right: parent.right
anchors.top: parent.top
height: 30
color: "#333"
border.color: Qt.lighter(color, 1.2)
Text {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 4
font.pixelSize: parent.height-4
color: '#fff'
text: name
}
}
Rectangle {
id: image
width: 26
height: 26
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 2
anchors.topMargin: 2
color: "black"
Image {
anchors.fill: parent
fillMode: Image.PreserveAspectFit
source: imageSource
}
}
MouseArea {
anchors.fill: parent
onClicked: parent.state = "expanded"
}
Item {
id: factsView
anchors.top: image.bottom
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
opacity: 0
Rectangle {
anchors.fill: parent
gradient: Gradient {
GradientStop { position: 0.0; color: "#fed958" }
GradientStop { position: 1.0; color: "#fecc2f" }
}
border.color: '#000000'
border.width: 2
Text {
anchors.fill: parent
anchors.margins: 5
clip: true
wrapMode: Text.WordWrap
color: '#1f1f21'
font.pixelSize: 12
text: facts
}
}
}
Rectangle {
id: closeButton
anchors.right: parent.right
anchors.top: parent.top
anchors.rightMargin: 2
anchors.topMargin: 2
width: 26
height: 26
color: "#157efb"
border.color: Qt.lighter(color, 1.1)
opacity: 0
MouseArea {
anchors.fill: parent
onClicked: wrapper.state = ""
}
}
states: [
State {
name: "expanded"
PropertyChanges { target: wrapper; height: listView.height }
PropertyChanges { target: image; width: listView.width; height: listView.width; anchors.rightMargin: 0; anchors.topMargin: 30 }
PropertyChanges { target: factsView; opacity: 1 }
PropertyChanges { target: closeButton; opacity: 1 }
PropertyChanges { target: wrapper.ListView.view; contentY: wrapper.y; interactive: false }
}
]
transitions: [
Transition {
NumberAnimation {
duration: 200;
properties: "height,width,anchors.rightMargin,anchors.topMargin,opacity,contentY"
}
}
]
}
}
}
~~~
运行结果如下所示:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328a91c8e.png)](http://files.devbean.net/images/2015/09/delegate-sharp.png)
点击每一项可以开始一个动画:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328aa3e05.png)](http://files.devbean.net/images/2015/09/delegate-sharp-2.png)
这里展示的技术在某些方面非常实用,比如一些歌曲播放器允许用户在点击某首歌曲后,会将该歌曲的信息放大显示等。
(85)动态视图
最后更新于:2022-04-01 06:31:27
#(85):动态视图
`Repeater`适用于少量的静态数据集。但是在实际应用中,数据模型往往是非常复杂的,并且数量巨大。这种情况下,`Repeater`并不十分适合。于是,QtQuick 提供了两个专门的视图元素:`ListView`和`GridView`。这两个元素都继承自`Flickable`,因此允许用户在一个很大的数据集中进行移动。同时,`ListView`和`GridView`能够复用创建的代理,这意味着,`ListView`和`GridView`不需要为每一个数据创建一个单独的代理。这种技术减少了大量代理的创建造成的内存问题。
由于`ListView`和`GridView`在使用上非常相似,因此我们以`ListView`为例进行介绍。
`ListView`类似[前面章节](http://www.devbean.net/2014/06/qt-study-road-2-qml-repeater/)提到的`Repeater`元素。`ListView`使用模型提供数据,创建代理渲染数据。下面是`ListView`的简单使用:
~~~
import QtQuick 2.2
Rectangle {
width: 80
height: 300
color: "white"
ListView {
anchors.fill: parent
anchors.margins: 20
clip: true
model: 100
delegate: numberDelegate
spacing: 5
}
Component {
id: numberDelegate
Rectangle {
width: 40
height: 40
color: "lightGreen"
Text {
anchors.centerIn: parent
font.pixelSize: 10
text: index
}
}
}
}
~~~
代码运行结果如下图所示:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_56823287ed958.png)](http://files.devbean.net/images/2014/07/listview-demo.png)
如果数据模型包含的数据不能在一屏显示完全,`ListView`只会显示整个列表的一部分。但是,作为 QtQuick 的一种默认行为,`ListView`并不能限制显示范围就在代理显示的区域内。这意味着,代理可能会在`ListView`的外部显示出来。为避免这一点,我们需要设置`clip`属性为`true`,使得超出`ListView`边界的代理能够被裁减掉。注意下图所示的行为(左面是设置了`clip`的`ListView`而右图则没有):
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328809f39.png)](http://files.devbean.net/images/2014/07/listview-clip.png)
对于用户而言,`ListView`是一个可滚动的区域。`ListView`支持平滑滚动,这意味着它能够快速流畅地进行滚动。默认情况下,这种滚动具有在向下到达底部时会有一个反弹的特效。这一行为由`boundsBehavior`属性控制。`boundsBehavior`属性有三个可选值:`Flickable.StopAtBounds`完全消除反弹效果;`Flickable.DragOverBounds`在自由滑动时没有反弹效果,但是允许用户拖动越界;`Flickable.DragAndOvershootBounds`则是默认值,意味着不仅用户可以拖动越界,还可以通过自由滑动越界。
当列表滑动结束时,列表可能停在任意位置:一个代理可能只显示一部分,另外部分被裁减掉。这一行为是由`snapMode`属性控制的。`snapMode`属性的默认值是`ListView.NoSnap`,也就是可以停在任意位置;`ListView.SnapToItem`会在某一代理的顶部停止滑动;`ListView.SnapOneItem`则规定每次滑动时不得超过一个代理,也就是每次只滑动一个代理,这种行为在分页滚动时尤其有效。
默认情况下,列表视图是纵向的。通过`orientation`属性可以将其改为横向。属性可接受值为`ListView.Vertical`或`ListView.Horizontal`。例如下面的代码:
~~~
import QtQuick 2.2
Rectangle {
width: 480
height: 80
color: "white"
ListView {
anchors.fill: parent
anchors.margins: 20
clip: true
model: 100
orientation: ListView.Horizontal
delegate: numberDelegate
spacing: 5
}
Component {
id: numberDelegate
Rectangle {
width: 40
height: 40
color: "lightGreen"
Text {
anchors.centerIn: parent
font.pixelSize: 10
text: index
}
}
}
}
~~~
当列表视图横向排列时,其中的元素按照从左向右的顺序布局。使用`layoutDirection`属性可以修改这一设置。该属性的可选值为`Qt.LeftToRight`或`Qt.RightToLeft`。
在触摸屏环境下使用`ListView`,默认的设置已经足够。但是,如果在带有键盘的环境下,使用方向键一般应该突出显示当前项。这一特性在 QML 中称为“高亮”。与普通的代理类似,视图也支持使用一个专门用于高亮的代理。这可以认为是一个额外的代理,只会被实例化一次,并且只会移动到当前项目的位置。
下面的例子设置了两个属性。第一,`focus`属性应当被设置为`true`,这允许`ListView`接收键盘焦点。第二,`highlight`属性被设置为一个被使用的高亮代理。这个高亮代理可以使用当前项目的`x`、`y`和`height`属性;另外,如果没有指定`width`属性,也可以使用当前项目的`width`属性。在这个例子中,宽度是由`ListView.view.width`附加属性提供的。我们会在后面的内容详细介绍这个附加属性。
~~~
import QtQuick 2.2
Rectangle {
width: 240
height: 300
color: "white"
ListView {
anchors.fill: parent
anchors.margins: 20
clip: true
model: 100
delegate: numberDelegate
spacing: 5
highlight: highlightComponent
focus: true
}
Component {
id: highlightComponent
Rectangle {
width: ListView.view.width
color: "lightGreen"
}
}
Component {
id: numberDelegate
Item {
width: 40
height: 40
Text {
anchors.centerIn: parent
font.pixelSize: 10
text: index
}
}
}
}
~~~
运行结果如下图所示:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328818134.png)](http://files.devbean.net/images/2014/10/highlight-delegate.png)
在使用高亮时,QML 提供了很多属性,用于控制高亮的行为。例如,`highlightRangeMode`设置高亮如何在视图进行显示。默认值`ListView.NoHighlightRange`意味着高亮区域和项目的可视范围没有关联;`ListView.StrictlyEnforceRange`则使高亮始终可见,如果用户试图将高亮区域从视图的可视区域移开,当前项目也会随之改变,以便保证高亮区域始终可见;介于二者之间的是`ListView.ApplyRange`,它会保持高亮区域可视,但是并不强制,也就是说,如果必要的话,高亮区域也会被移出视图的可视区。
默认情况下,高亮的移动是由视图负责的。这个移动速度和大小的改变都是可控的,相关属性有`highlightMoveSpeed`,`highlightMoveDuration`,`highlightResizeSpeed`以及`highlightResizeDuration`。其中,速度默认为每秒 400 像素;持续时间被设置为 -1,意味着持续时间由速度和距离控制。同时设置速度和持续时间则由系统选择二者中较快的那个值。有关高亮更详细的设置则可以通过将`highlightFollowCurrentItem`属性设置为`false`达到。这表示视图将不再负责高亮的移动,完全交给开发者处理。下面的例子中,高亮代理的`y`属性被绑定到`ListView.view.currentItem.y`附加属性。这保证了高亮能够跟随当前项目。但是,我们不希望视图移动高亮,而是由自己完全控制,因此在`y`属性上面应用了一个`Behavior`。下面的代码将这个移动的过程分成三步:淡出、移动、淡入。注意,`SequentialAnimation`和`PropertyAnimation`可以结合`NumberAnimation`实现更复杂的移动。有关动画部分,将在后面的章节详细介绍,这里只是先演示这一效果。
~~~
Component {
id: highlightComponent
Item {
width: ListView.view.width
height: ListView.view.currentItem.height
y: ListView.view.currentItem.y
Behavior on y {
SequentialAnimation {
PropertyAnimation { target: highlightRectangle; property: "opacity"; to: 0; duration: 200 }
NumberAnimation { duration: 1 }
PropertyAnimation { target: highlightRectangle; property: "opacity"; to: 1; duration: 200 }
}
}
Rectangle {
id: highlightRectangle
anchors.fill: parent
color: "lightGreen"
}
}
}
~~~
最后需要介绍的是`ListView`的 header 和 footer。header 和 footer 可以认为是两个特殊的代理。虽然取名为 header 和 footer,但是这两个部分实际会添加在第一个元素之前和最后一个元素之后。也就是说,对于一个从左到右的横向列表,header 会出现在最左侧而不是上方。下面的例子演示了 header 和 footer 的位置。header 和 footer 通常用于显示额外的元素,例如在最底部显示“加载更多”的按钮。
~~~
import QtQuick 2.2
Rectangle {
width: 80
height: 300
color: "white"
ListView {
anchors.fill: parent
anchors.margins: 20
clip: true
model: 4
delegate: numberDelegate
spacing: 5
header: headerComponent
footer: footerComponent
}
Component {
id: headerComponent
Rectangle {
width: 40
height: 20
color: "yellow"
}
}
Component {
id: footerComponent
Rectangle {
width: 40
height: 20
color: "red"
}
}
Component {
id: numberDelegate
Rectangle {
width: 40
height: 40
color: "lightGreen"
Text {
anchors.centerIn: parent
font.pixelSize: 10
text: index
}
}
}
}
~~~
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328825311.png)](http://files.devbean.net/images/2014/10/listview-header-footer.png)
需要注意的是,header 和 footer 与`ListView`之间没有预留间距。这意味着,header 和 footer 将紧贴着列表的第一个和最后一个元素。如果需要在二者之间留有一定的间距,则这个间距应该成为 header 和 footer 的一部分。
`GridView`与`ListView`非常相似,唯一的区别在于,`ListView`用于显示一维列表,`GridView`则用于显示二维表格。相比列表,表格的元素并不依赖于代理的大小和代理之间的间隔,而是由`cellWidth`和`cellHeight`属性控制一个单元格。每一个代理都会被放置在这个单元格的左上角。
~~~
import QtQuick 2.2
Rectangle {
width: 240
height: 300
color: "white"
GridView {
anchors.fill: parent
anchors.margins: 20
clip: true
model: 100
cellWidth: 45
cellHeight: 45
delegate: numberDelegate
}
Component {
id: numberDelegate
Rectangle {
width: 40
height: 40
color: "lightGreen"
Text {
anchors.centerIn: parent
font.pixelSize: 10
text: index
}
}
}
}
~~~
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328930b8a.png)](http://files.devbean.net/images/2014/10/gridview-demo.png)
与`ListView`类似,`GridView`也可以设置 header 和 footer,也能够使用高亮代理和类似列表的边界行为。`GridView`支持不同的显示方向,这需要使用`flow`属性控制,可选值为`GridView.LeftToRight`和`GridView.TopToBottom`。前者按照先从左向右、再从上到下的顺序填充,滚动条出现在竖直方向;后者按照先从上到下、在从左到右的顺序填充,滚动条出现在水平方向。
(84)Repeater
最后更新于:2022-04-01 06:31:25
#(84):Repeater
[前面的章节](http://www.devbean.net/2013/01/qt-study-road-2-model-view/)我们介绍过模型视图。这是一种数据和显示相分离的技术,在 Qt 中有着非常重要的地位。在 QtQuick 中,数据和显示的分离同样也是利用这种“模型-视图”技术实现的。对于每一个视图,数据元素的可视化显示交给代理完成。与 Qt/C++ 类似,QtQuick 提供了一系列预定义的模型和视图。本章开始,我们着重介绍这部分内容。这部分内容主要来自[http://qmlbook.org/ch06/index.html](http://qmlbook.org/ch06/index.html),在此表示感谢。
由于 QtQuick 中的模型视图的基本概念同[前面的章节](http://www.devbean.net/2013/01/qt-study-road-2-model-view/)没有本质的区别,所以这里不再赘述这部分内容。
将数据从表现层分离的最基本方法是使用`Repeater`元素。`Repeater`元素可以用于显示一个数组的数据,并且可以很方便地在用户界面进行定位。`Repeater`的模型范围很广:从一个整型到网络数据,均可作为其数据模型。
`Repeater`最简单的用法是将一个整数作为其`model`属性的值。这个整型代表`Repeater`所使用的模型中的数据个数。例如下面的代码中,`model: 10`代表`Repeater`的模型有 10 个数据项。
~~~
import QtQuick 2.2
Column {
spacing: 2
Repeater {
model: 10
Rectangle {
width: 100
height: 20
radius: 3
color: "lightBlue"
Text {
anchors.centerIn: parent
text: index
}
}
}
}
~~~
现在我们设置了 10 个数据项,然后定义一个`Rectangle`进行显示。每一个`Rectangle`的宽度和高度分别为 100px 和 20px,并且有圆角和浅蓝色背景。`Rectangle`中有一个`Text`元素为其子元素,`Text`文本值为当前项的索引。代码运行结果如下:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_56823286b6ca2.png)](http://files.devbean.net/images/2014/06/repeater-demo.png)
虽然指定模型项的个数很简单,但实际用处不大。`Repeater`还支持更复杂的方式,例如,把一个 JavaScript 数组作为模型。JavaScript 数组元素可以是任意类型:字符串、数字或对象。在下面的例子中,我们将一个字符串数组作为`Repeater`的模型。我们当然可以使用`index`获得当前索引,同时,我们也可以使用`modelData`访问到数组中的每一个元素的值:
~~~
import QtQuick 2.2
Column {
spacing: 2
Repeater {
model: ["Enterprise", "Colombia", "Challenger", "Discovery", "Endeavour", "Atlantis"]
Rectangle {
width: 100
height: 20
radius: 3
color: "lightBlue"
Text {
anchors.centerIn: parent
text: index +": "+modelData
}
}
}
}
~~~
代码运行结果如下:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_56823286c3af0.png)
由于能够使用 JavaScript 数组作为`Repeater`的模型,而 JavaScript 数组能够以对象作为其元素类型,因而`Repeater`就可以处理复杂的数据项,比如带有属性的对象。这种情况其实更为常见。相比普通的 JavaScript 对象,更常用的是`ListElement`类型。类似普通 JavaScript 对象,每一个`ListElement`可以有任意属性。例如下面的代码示例中,每一个数据项都有一个名字和外观颜色。
~~~
import QtQuick 2.2
Column {
spacing: 2
Repeater {
model: ListModel {
ListElement { name: "Mercury"; surfaceColor: "gray" }
ListElement { name: "Venus"; surfaceColor: "yellow" }
ListElement { name: "Earth"; surfaceColor: "blue" }
ListElement { name: "Mars"; surfaceColor: "orange" }
ListElement { name: "Jupiter"; surfaceColor: "orange" }
ListElement { name: "Saturn"; surfaceColor: "yellow" }
ListElement { name: "Uranus"; surfaceColor: "lightBlue" }
ListElement { name: "Neptune"; surfaceColor: "lightBlue" }
}
Rectangle {
width: 100
height: 20
radius: 3
color: "lightBlue"
Text {
anchors.centerIn: parent
text: name
}
Rectangle {
anchors.left: parent.left
anchors.verticalCenter: parent.verticalCenter
anchors.leftMargin: 2
width: 16
height: 16
radius: 8
border.color: "black"
border.width: 1
color: surfaceColor
}
}
}
}
~~~
运行结果如下图所示:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_56823286d2d22.png)](http://files.devbean.net/images/2014/06/repeater-listelement.png)
`ListElement`的每个属性都被`Repeater`绑定到实例化的显示项。正如上面代码中显示的那样,这意味着每一个用于显示数据的`Rectangle`作用域内都可以访问到`ListElement`的`name`和`surfaceColor`属性。
像上面几段代码中,`Repeater`的每一个数据项都使用一个`Rectangle`渲染。事实上,这是由于`Repeater`具有一个`delegate`的默认属性,由于`Rectangle`没有显式赋值给任何一个属性,因此它直接成为默认属性`delegate`的值,所以才会使用`Rectangle`渲染。理解了这一点,我们就可以写出具有显式赋值的代码:
~~~
import QtQuick 2.2
Column {
spacing: 2
Repeater {
model: 10
delegate: Rectangle {
width: 100
height: 20
radius: 3
color: "lightBlue"
Text {
anchors.centerIn: parent
text: index
}
}
}
}
~~~
实际上,这段代码与前面提到的是等价的。
(83)Qt Quick Controls
最后更新于:2022-04-01 06:31:22
#(83):Qt Quick Controls
自 QML 第一次发布已经过去一年多的时间,但在企业应用领域,QML 一直没有能够占据一定地位。很大一部分原因是,QML 缺少一些在企业应用中亟需的组件,比如按钮、菜单等。虽然移动领域,这些组件已经变得可有可无,但在桌面系统中依然不可或缺。为了解决这一问题,Qt 5.1 发布了 Qt Quick 的一个全新模块:Qt Quick Controls。顾名思义,这个模块提供了大量类似 Qt Widgets 模块那样可重用的组件。本章我们将介绍 Qt Quick Controls,你会发现这个模块与 Qt 组件非常类似。
为了开发基于 Qt Quick Controls 的程序,我们需要创建一个 Qt Quick Application 类型的应用程序,选择组件集的时候注意选择 Qt Quick Controls 即可:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_568232860e1e9.png)](http://files.devbean.net/images/2014/05/qqc-project-type.png)
注意,Qt Creator 给出的是 Qt Quick Controls 1.0,而最新版本的 Qt 5.2 搭载的 Qt Quick Controls 是 1.1。1.1 比 1.0 新增加了一些组件,比如`BusyIndicator`等。所以,如果你发现某个组件找不到,记得更新下 Qt Quick Controls 的版本。
Qt Quick Controls 1.1 提供了多种组件:
| 应用程序窗口 ||--|
| -- || -- |
| 用于描述应用程序的基本窗口属性的组件 |
| ApplicationWindow | 对应`QMainWindow`,提供顶层应用程序窗口 |
| MenuBar | 对应`QMenuBar`,提供窗口顶部横向的菜单栏 |
| StatusBar | 对应`QStatusBar`,提供状态栏 |
| ToolBar | 对应`QToolBar`,提供工具栏,可以添加`ToolButton`和其它组件 |
| Action | 对应`QAction`,提供能够绑定到导航和视图的抽象的用户界面动作 |
| 导航和视图 |
| 方便用户在一个布局中管理和显示其它组件 |
| ScrollView | 对应`QScrollView`,提供滚动视图 |
| SplitView | 对应`QSplitter`,提供可拖动的分割视图布局 |
| StackView | 对应`QStackedWidget`,提供基于栈的层叠布局 |
| TabView | 对应`QTabWidget`,提供带有标签的基于栈的层叠布局 |
| TableView | 对应`QTableWidget`,提供带有滚动条、样式和表头的表格 |
| 控件 |
| 控件用于表现或接受用户输入 |
| BusyIndicator | 提供忙等示意组件 |
| Button | 对应`QPushButton`,提供按钮组件 |
| CheckBox | 对应`QCheckBox`,提供复选框 |
| ComboBox | 对应`QComboBox`,提供下拉框 |
| GroupBox | 对应`QGroupBox`,提供带有标题、边框的容器 |
| Label | 对应`QLabel`,提供标签组件 |
| ProgressBar | 对应`QProgressBar`,提供进度条组件 |
| RadioButton | 对应`QRadioButton`,提供单选按钮 |
| Slider | 对应`QSlider`,提供滑动组件 |
| SpinBox | 对应`QSpinBox`,提供微调组件 |
| Switch | 提供类似单选按钮的开关组件 |
| TextArea | 对应`QTextEdit`,提供能够显示多行文本的富文本编辑框 |
| TextField | 对应`QTextLine`,提供显示单行文本的纯文本编辑框 |
| ToolButton | 对应`QToolButton`,提供在工具栏上显示的工具按钮 |
| ExclusiveGroup | 提供互斥 |
| 菜单 |
| 用于构建菜单的组件 |
| Menu | 对应`QMenu`,提供菜单、子菜单、弹出菜单等 |
| MenuSeparator | 提供菜单分隔符 |
| MenuItem | 提供添加到菜单栏或菜单的菜单项 |
| StatusBar | 对应`QStatusBar`,提供状态栏 |
| ToolBar | 对应`QToolBar`,提供工具栏,可以添加`ToolButton`和其它组件 |
我们尝试实现一个编辑器。这是一个简单的文本编辑器,具有新建、剪切、复制和粘贴等操作。程序运行出来效果如下:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328621fed.png)](http://files.devbean.net/images/2014/05/simpleeditor.png)
整个程序都是在 IDE 帮我们生成的 main.qml 中实现的。首先我们需要添加`import`语句:
~~~
import QtQuick 2.1
import QtQuick.Controls 1.1
~~~
注意我们修改了 IDE 生成的默认语句。整个 QML 文档的根元素是`ApplicationWindow`:
~~~
ApplicationWindow {
title: qsTr("Simple Editor")
width: 640
height: 480
...
}
~~~
`ApplicationWindow`是应用程序的主窗口,类似`QMainWindow`,提供了很多预定义的功能,比如菜单、工具栏等。代码里面的`qsTr()`函数类似`tr()`函数,用于以后的国际化。所有面向用户的文本都应该使用这个函数。
下面向`ApplicationWindow`中添加控件:
~~~
menuBar: MenuBar {
Menu {
title: qsTr("&File")
MenuItem { action: newAction }
MenuItem { action: exitAction }
}
Menu {
title: qsTr("&Edit")
MenuItem { action: cutAction }
MenuItem { action: copyAction }
MenuItem { action: pasteAction }
MenuSeparator {}
MenuItem { action: selectAllAction }
}
}
toolBar: ToolBar {
Row {
anchors.fill: parent
ToolButton { action: newAction }
ToolButton { action: cutAction }
ToolButton { action: copyAction }
ToolButton { action: pasteAction }
}
}
TextArea {
id: textArea
anchors.fill: parent
}
~~~
首先看最后面的`TextArea`,这是整个窗口的主要控件,类似于`setCentralWidget()`函数调用。
`menuBar`和`toolBar`两个属性都是`ApplicationWindow`提供的属性。
`menuBar`是`MenuBar`类型的,所以我们创建一个新的`MenuBar`控件。`MenuBar`具有层次结构,这是通过`Menu`的嵌套实现的。每一个菜单项都是用`MenuItem`实现的;菜单项之间的分隔符则使用`MenuSeparator`控件。这点与 QtWidgets 有所不同。
`toolBar`是`Item`类型的,不过通常都会使用`ToolBar`控件。`ToolBar`默认没有提供布局,因此我们必须给它设置一个布局。这里我们直接添加了一个`Row`,作为横向工具栏的布局。这个工具栏要横向充满父窗口,因此设置锚点为`anchors.fill: parent`。虽然我们设置的是充满整个父窗口,但是工具栏的行为是,如果其中只有一个子元素(比如这里的`Row`),那么工具栏的高度将被设置为这个子元素的`implicitHeight`属性。这对结合布局使用非常有用。事实上,这也是工具栏最常用的方法。工具栏中添加了四个按钮,都是`ToolButton`类型。
每一个`MenuItem`和`ToolButton`都添加了一个`action`属性。下面是这部分代码:
~~~
Action {
id: exitAction
text: qsTr("E&xit")
onTriggered: Qt.quit()
}
Action {
id: newAction
text: qsTr("New")
iconSource: "images/new.png"
onTriggered: {
textArea.text = "";
}
}
Action {
id: cutAction
text: qsTr("Cut")
iconSource: "images/cut.png"
onTriggered: textArea.cut()
}
Action {
id: copyAction
text: qsTr("Copy")
iconSource: "images/copy.png"
onTriggered: textArea.copy()
}
Action {
id: pasteAction
text: qsTr("Paste")
iconSource: "images/paste.png"
onTriggered: textArea.paste()
}
Action {
id: selectAllAction
text: qsTr("Select All")
onTriggered: textArea.selectAll()
}
~~~
`Action`类似`QAction`。这里我们还是使用`qsTr()`函数设置其显示的文本。
使用`iconSource`属性可以指定图标。注意这里的图标只能是位于文件系统中的,不能加载资源文件中的图标(当然,这并不是绝对的。如果我们将整个 QML 文档放在资源文件中,那么就可以直接加载资源文件中的图标。我们会在后面的章节详细介绍这种技术。)。当我们直接类似“images/new.png”这种路径时,注意 QML 是运行时解释的,因此这个路径是相对与 QML 文件的路径。所以这里的图标需要放在与 main.qml 文件同目录下的 images 目录中。
`onTriggered`属性是一种信号处理函数,后面可以添加 JavaScript 语句。如果是多条语句,可以使用大括号,例如`newAction`的`onTriggered`。QML 组件可以发出信号。与 C++ 不同的是,QML 组件的信号并不需要特别的连接语句,而是使用”on信号名字”的形式。比如,`Action`有一个名为`triggered`的信号,则其信号处理函数即为`onTriggered`。事实上,这是最简单的一种信号槽的实现。不过,这种实现的困难在于,同一个信号只能有一个固定名字的信号处理函数。不过,我们也可以使用 connect 连接语句。后面的章节中将详细介绍这一点。
至此,我们的编辑器便实现了。由于全部使用了`TextArea`提供的功能,所以代码很简单。不过,复杂的程序都是这些简单的元素堆积而成,所以,我们现在只是简单介绍,具体的控件使用还要根据文档仔细研究。
附件:[SimpleEditor.zip](http://files.devbean.net/code/SimpleEditor.zip)
(82)输入元素
最后更新于:2022-04-01 06:31:20
#(82):输入元素
前面的章节中,我们看到了作为输入元素的`MouseArea`,用于接收鼠标的输入。下面,我们再来介绍关于键盘输入的两个元素:`TextInput`和`TextEdit`。
`TextInput`是单行的文本输入框,支持验证器、输入掩码和显示模式等。
~~~
import QtQuick 2.0
Rectangle {
width: 200
height: 80
color: "linen"
TextInput {
id: input1
x: 8; y: 8
width: 96; height: 20
focus: true
text: "Text Input 1"
}
TextInput {
id: input2
x: 8; y: 36
width: 96; height: 20
text: "Text Input 2"
}
}
~~~
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328503cb1.png)](http://files.devbean.net/images/2014/02/qml-input-element.png)
注意,我们这里放置了两个`TextInput`,用户可以通过点击输入框改变焦点。如果我们想支持键盘导航,可以添加`KeyNavigation`附加属性。
~~~
import QtQuick 2.0
Rectangle {
width: 200
height: 80
color: "linen"
TextInput {
id: input1
x: 8; y: 8
width: 96; height: 20
focus: true
text: "Text Input 1"
KeyNavigation.tab: input2
}
TextInput {
id: input2
x: 8; y: 36
width: 96; height: 20
text: "Text Input 2"
KeyNavigation.tab: input1
}
}
~~~
`KeyNavigation`是一个附加属性。当用户点击了指定的按键时,属性指定的组件就会获得焦点。附加属性类似于普通属性,但是又有所区别。普通的属性隶属于这个类型;附加属性一般用于修饰或补充目标类型。比如这里的`KeyNavigation.tab`并不是`TextInput`的普通属性,仅仅是用来说明`TextInput`的一种特征。附加属性的一般语法是`类型.属性名`,以此为例,类型就是`KeyNavigation`,属性名就是`tab`。
与`QLineEdit`不同,QML 的文本出入组件只有一个闪动的光标和用户输入的文本,没有边框等可视元素。因此,为了能够让用户意识到这是一个可输入元素,通常需要一些可视化修饰,比如绘制一个矩形框。当我们这么做的时候,创建一个完整的组件可能是更好的选择,只是要记得导出所需要的属性,以便外部使用。按照这种思路,我们创建一个组件:
~~~
// LineEdit.qml
import QtQuick 2.0
Rectangle {
width: 96;
height: input.height + 8
color: "lightsteelblue"
border.color: "gray"
property alias text: input.text
property alias input: input
TextInput {
id: input
anchors.fill: parent
anchors.margins: 4
focus: true
}
}
~~~
为了让外界可以直接设置`TextInput`的`text`属性,我们给这个属性声明了一个别名。同时,为了让外界可以访问到内部的`textInput`,我们将这个子组件也暴露出来。不过,从封装的角度而言,将实现细节暴露出去并不是一个好的设计,这要看暴露出来这个子组件的影响究竟有多大。然而这些都是关于设计的问题,需要具体问题具体分析,这里不再赘述。
下面我们可以将前面的例子修改成我们新创建的`LineEdit`组件:
~~~
import QtQuick 2.0
Rectangle {
width: 200
height: 80
color: "linen"
LineEdit {
id: input1
x: 8; y: 8
width: 96; height: 20
focus: true
text: "Text Input 1"
KeyNavigation.tab: input2
}
LineEdit {
id: input2
x: 8; y: 36
width: 96; height: 20
text: "Text Input 2"
KeyNavigation.tab: input1
}
}
~~~
只要将 LineEdit.qml 与 main.qml 放在同一目录下,我们就不需要额外的操作,即可在 main.qml 中直接使用`LineEdit`。运行结果如下:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_568232851012e.png)](http://files.devbean.net/images/2014/03/custom-lineedit.png)
现在再来试试键盘导航。这次无论怎么按键盘,焦点始终不会到`input2`。虽然我们在组件中添加了`focus: true`,可是不起作用。原因是,焦点被`inputText`的父组件`Rectangle`获得,然而,`Rectangle`不会将焦点转发给`inputText`。为了解决这一问题,QML提供了另外一个组件`FocusScope`。
当`FocusScope`接收到焦点时,会将焦点转发给最后一个设置了`focus:true`的子对象。所以,我们可以使用`FocusScope`重写`LineEdit`组件:
~~~
// LineEdit.qml
import QtQuick 2.0
FocusScope {
width: 96;
height: input.height + 8
color: "lightsteelblue"
border.color: "gray"
property alias text: input.text
property alias input: input
TextInput {
id: input
anchors.fill: parent
anchors.margins: 4
focus: true
}
}
~~~
这样修改过之后,我们就可以像之前的`TextInput`一样正常使用了。
`TextEdit`与`TextInput`非常类似,唯一区别是`TextEdit`是多行的文本编辑组件。与`TextInput`类似,`TextEdit`也没有一个可视化的显示,所以我们也需要自己绘制其显示区域。这些内容与前面代码几乎一样,这里不再赘述。
附加属性`Keys`类似于键盘事件,允许我们相应特定的按键按下事件。例如,我们可以利用方向键控制举行的位置,如下代码所示:
~~~
import QtQuick 2.0
DarkSquare {
width: 400; height: 200
GreenSquare {
id: square
x: 8; y: 8
}
focus: true
Keys.onLeftPressed: square.x -= 8
Keys.onRightPressed: square.x += 8
Keys.onUpPressed: square.y -= 8
Keys.onDownPressed: square.y += 8
Keys.onPressed: {
switch(event.key) {
case Qt.Key_Plus:
square.scale += 0.2
break;
case Qt.Key_Minus:
square.scale -= 0.2
break;
}
}
}
~~~
(81)元素布局
最后更新于:2022-04-01 06:31:18
#(81):元素布局
上一章我们介绍了 QML 中用于定位的几种元素,被称为定位器。除了定位器,QML 还提供了另外一种用于布局的机制。我们将这种机制成为锚点(anchor)。锚点允许我们灵活地设置两个元素的相对位置。它使两个元素之间形成一种类似于锚的关系,也就是两个元素之间形成一个固定点。锚点的行为类似于一种链接,它要比单纯地计算坐标改变更强。由于锚点描述的是相对位置,所以在使用锚点时,我们必须指定两个元素,声明其中一个元素相对于另外一个元素。锚点是`Item`元素的基本属性之一,因而适用于所有 QML 可视元素。
一个元素有 6 个主要的锚点的定位线,如下图所示:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328407b64.png)](http://files.devbean.net/images/2014/02/qml-anchors.png)
这 6 个定位线分别是:`top`、`bottom`、`left`、`right`、`horizontalCenter`和`verticalCenter`。对于`Text`元素,还有一个`baseline`锚点。每一个锚点定位线都可以结合一个偏移的数值。其中,`top`、`bottom`、`left`和`right`称为外边框;`horizontalCenter`、`verticalCenter`和`baseline`称为偏移量。
下面,我们使用例子来说明这些锚点的使用。首先,我们需要重新定义一下上一章使用过的`BlueRectangle`组件:
~~~
import QtQuick 2.0
Rectangle {
width: 48
height: 48
color: "blue"
border.color: Qt.lighter(color)
MouseArea {
anchors.fill: parent
drag.target: parent
}
}
~~~
简单来说,我们在`BlueRectangle`最后增加了一个`MouseArea`组件。前面的章节中,我们简单使用了这个组件。顾名思义,这是一个用于处理鼠标事件的组件。之前我们使用了它处理鼠标点击事件。这里,我们使用了其拖动事件。`anchors.fill: parent`一行的含义马上就会解释;`drag.target: parent`则说明拖动目标是`parent`。我们的拖动对象是`MouseArea`的父组件,也就是`BlueRectangle`组件。
接下来看第一个例子:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328415ee6.png)](http://files.devbean.net/images/2014/02/qml-anchors-fill.png)
代码如下:
~~~
import QtQuick 2.0
Rectangle {
id: root
width: 220
height: 220
color: "black"
GreenRectangle {
x: 10
y: 10
width: 100
height: 100
BlueRectangle {
width: 12
anchors.fill: parent
anchors.margins: 8
}
}
}
~~~
在这个例子中,我们使用`anchors.fill`设置内部蓝色矩形的锚点为填充(fill),填充的目的对象是`parent`;填充边距是 8px。注意,尽管我们设置了蓝色矩形宽度为 12px,但是因为锚点的优先级要高于宽度属性设置,所以蓝色矩形的实际宽度是 100px – 8px – 8px = 84px。
第二个例子:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328425c1d.png)](http://files.devbean.net/images/2014/02/qml-anchors-left.png)
代码如下:
~~~
import QtQuick 2.0
Rectangle {
id: root
width: 220
height: 220
color: "black"
GreenRectangle {
x: 10
y: 10
width: 100
height: 100
BlueRectangle {
width: 48
y: 8
anchors.left: parent.left
anchors.leftMargin: 8
}
}
}
~~~
这次,我们使用`anchors.left`设置内部蓝色矩形的锚点为父组件的左边线(parent.left);左边距是 8px。另外,我们可以试着拖动蓝色矩形,看它的移动方式。在我们拖动时,蓝色矩形只能沿着距离父组件左边 8px 的位置上下移动,这是由于我们设置了锚点的缘故。正如我们前面提到过的,锚点要比单纯地计算坐标改变的效果更强,更优先。
第三个例子:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328433e3f.png)](http://files.devbean.net/images/2014/02/qml-anchors-left-right.png)
代码如下:
~~~
import QtQuick 2.0
Rectangle {
id: root
width: 220
height: 220
color: "black"
GreenRectangle {
x: 10
y: 10
width: 100
height: 100
BlueRectangle {
width: 48
anchors.left: parent.right
}
}
}
~~~
这里,我们修改代码为`anchors.left: parent.right`,也就是将组件锚点的左边线设置为父组件的右边线。效果即如上图所示。当我们拖动组件时,依然只能上下移动。
下一个例子:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328444705.png)](http://files.devbean.net/images/2014/02/qml-anchors-horizontalcenter.png)
代码如下:
~~~
import QtQuick 2.0
Rectangle {
id: root
width: 220
height: 220
color: "black"
GreenRectangle {
x: 10
y: 10
width: 100
height: 100
BlueRectangle {
id: blue1
width: 48; height: 24
y: 8
anchors.horizontalCenter: parent.horizontalCenter
}
BlueRectangle {
id: blue2
width: 72; height: 24
anchors.top: blue1.bottom
anchors.topMargin: 4
anchors.horizontalCenter: blue1.horizontalCenter
}
}
}
~~~
这算是一个稍微复杂的例子。这里有两个蓝色矩形:`blue1`和`blue2`。`blue1`的锚点水平中心线设置为父组件的水平中心;`blue2`的锚点上边线相对于`blue1`的底部,其中边距为 4px,另外,我们还增加了一个水平中线为`blue1`的水平中线。这样,`blue1`相对于父组件,`blue2`相对于`blue1`,这样便决定了三者之间的相对关系。当我们拖动蓝色矩形时可以发现,`blue1`和`blue2`的相对位置始终不变,因为我们已经明确指定了这种相对位置,而二者可以像一个整体似的同时上下移动(因为我们没有指定其中任何一个的上下边距与父组件的关系)。
另外一个例子:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328453a87.png)](http://files.devbean.net/images/2014/02/qml-anchors-centerin.png)
代码如下所示:
~~~
import QtQuick 2.0
Rectangle {
id: root
width: 220
height: 220
color: "black"
GreenRectangle {
x: 10
y: 10
width: 100
height: 100
BlueRectangle {
width: 48
anchors.centerIn: parent
}
}
}
~~~
与第一个例子类似,我们使用的是`anchors.centerIn: parent`将蓝色矩形的中心固定在父组件的中心。由于我们已经指明是中心,所以也不能拖动这个蓝色矩形。
最后一个例子:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328462d50.png)](http://files.devbean.net/images/2014/02/qml-anchors-hc-vc.png)
代码如下:
~~~
import QtQuick 2.0
Rectangle {
id: root
width: 220
height: 220
color: "black"
GreenRectangle {
x: 10
y: 10
width: 100
height: 100
BlueRectangle {
width: 48
anchors.horizontalCenter: parent.horizontalCenter
anchors.horizontalCenterOffset: -12
anchors.verticalCenter: parent.verticalCenter
}
}
}
~~~
上一个例子中,`anchors.centerIn: parent`可以看作等价于`anchors.horizontalCenter: parent.horizontalCenter`和`anchors.verticalCenter: parent.verticalCenter`。而这里,我们设置了`anchors.horizontalCenterOffset`为 -12,也就是向左偏移 12px。当然,我们也可以在`anchors.centerIn: parent`的基础上增加`anchors.horizontalCenterOffset`的值,二者是等价的。由于我们在这里指定的相对位置已经很明确,拖动也是无效的。
至此,我们简单介绍了 QML 中定位器和锚点的概念。看起来这些元素和机制都很简单,但是,通过有机地结合,足以灵活应对更复杂的场景。我们所要做的就是不断熟悉、深化对这些定位布局技术的理解。
(80)定位器
最后更新于:2022-04-01 06:31:16
#(80):定位器
QML 提供了很多用于定位的元素。这些元素叫做定位器,都包含在 QtQuick 模块。这些定位器主要有 `Row`、`Column`、`Grid`和`Flow`等。
为了介绍定位器,我们先添加三个简单的组件用于演示:
首先是`RedRectangle`,
~~~
import QtQuick 2.0
Rectangle {
width: 48
height: 48
color: "red"
border.color: Qt.lighter(color)
}
~~~
然后是`BlueRectangle`,
~~~
import QtQuick 2.0
Rectangle {
width: 48
height: 48
color: "blue"
border.color: Qt.lighter(color)
}
~~~
最后是`GreenRectangle`,
~~~
import QtQuick 2.0
Rectangle {
width: 48
height: 48
color: "green"
border.color: Qt.lighter(color)
}
~~~
这三个组件都很简单,仅有的区别是颜色不同。这是一个 48×48 的矩形,分别是红、黄、蓝三种颜色。注意,我们把边框颜色设置为`Qt.lighter(color)`,也就是比填充色亮一些的颜色,默认是填充色的 50%。
`Column`将子元素按照加入的顺序从上到下,在同一列排列出来。`spacing`属性用于定义子元素之间的间隔:
~~~
import QtQuick 2.0
Rectangle {
id: root
width: 120
height: 240
color: "black"
Column {
id: row
anchors.centerIn: parent
spacing: 8
RedRectangle { }
GreenRectangle { width: 96 }
BlueRectangle { }
}
}
~~~
运行结果如下:
[![QML Column](http://files.devbean.net/images/2014/01/qml-column.png)](http://files.devbean.net/images/2014/01/qml-column.png)
注意,我们按照红、绿、蓝的顺序加入了子组件,`Column`按照同样的顺序把它们添加进来。其中,我们独立设置了绿色矩形的宽度,这体现了我们后来设置的属性覆盖了组件定义时设置的默认值。`anchors`是另外一种布局方式,指定该组件与父组件的相对关系。我们会在后面的章节详细介绍这种布局。
与`Column`类似,`Row`将其子组件放置在一行的位置,既可以设置从左向右,也可以设置从右向左,这取决于`layoutDirection`属性。同样,它也有`spacing`属性,用于指定子组件之间的间隔:
~~~
import QtQuick 2.0
Rectangle {
id: root
width: 240
height: 120
color: "black"
Row {
id: row
anchors.centerIn: parent
spacing: 8
RedRectangle { }
GreenRectangle { width: 96 }
BlueRectangle { }
}
}
~~~
这段代码与前面的非常类似。我们可以运行下看看结果:
[![QML Row](http://files.devbean.net/images/2014/01/qml-row.png)](http://files.devbean.net/images/2014/01/qml-row.png)
运行结果同前面的也非常类似。这里不再赘述。
`Grid`元素将其子元素排列为一个网格。它需要制定`rows`和`columns`属性,也就是行和列的数值。如果二者有一个不显式设置,则另外一个会根据子元素的数目计算出来。例如,如果我们设置为 3 行,一共放入 6 个元素,那么列数会自动计算为 2。`flow`和`layoutDirection`属性则用来控制添加到网格的元素的顺序。同样,`Grid`元素也有`spacing`属性。我们还是看一个简单的例子:
~~~
import QtQuick 2.0
Rectangle {
id: root
width: 200
height: 200
color: "black"
Grid {
id: grid
rows: 2
anchors.centerIn: parent
spacing: 8
RedRectangle { }
RedRectangle { }
RedRectangle { }
RedRectangle { }
RedRectangle { }
}
}
~~~
同前面的代码类似。需要注意的是,我们仅设定了`Grid`的`rows`属性为 2,添加了 5 个子元素,那么,它的`columns`属性会自动计算为 3。运行结果也是类似的:
[![QML Grid](http://files.devbean.net/images/2014/01/qml-grid.png)](http://files.devbean.net/images/2014/01/qml-grid.png)
最后一个定位器是`Flow`。顾名思义,它会将其子元素以流的形式显示出来。我们使用`flow`和`layoutDirection`两个属性来控制显示方式。它可以从左向右横向布局,也可以从上向下纵向布局,或者反之。初看起来,这种布局方式与`Column`和`Row`极其类似。不同之处在于,添加到`Flow`里面的元素,当`Flow`的宽度或高度不足时,这些元素会自动换行。因此,为了令`Flow`正确工作,我们需要指定其宽度或者高度。这种指定既可以是显式的,也可以依据父元素计算而得。来看下面的例子:
~~~
import QtQuick 2.0
Rectangle {
id: root
width: 160
height: 160
color: "black"
Flow {
anchors.fill: parent
anchors.margins: 20
spacing: 20
RedRectangle { }
BlueRectangle { }
GreenRectangle { }
}
}
~~~
运行结果是这样的:
[![QML Flow](http://files.devbean.net/images/2014/01/qml-flow.png)](http://files.devbean.net/images/2014/01/qml-flow.png)
注意,我们每个色块的边长都是 48px,整个主窗口的宽是 160px,`Flow`元素外边距 20px,因此`Flow`的宽度其实是 160px – 20px – 20px = 120px。`Flow`子元素间距为 20px,两个子元素色块所占据的宽度就已经是 48px + 20px + 48px = 116px,3 个则是 116px + 20px + 48px = 184px > 160px,因此,默认窗口大小下一行只能显示两个色块,第三个色块自动换行。当我们拖动改变窗口大小时,可以观察`Flow`元素是如何工作的。
最后,我们再来介绍一个经常结合定位器一起使用的元素:`Repeater`。`Repeater`非常像一个`for`循环,它能够遍历数据模型中的元素。下面来看代码:
~~~
import QtQuick 2.0
Rectangle {
id: root
width: 252
height: 252
color: "black"
property variant colorArray: ["#00bde3", "#67c111", "#ea7025"]
Grid {
anchors.fill: parent
anchors.margins: 8
spacing: 4
Repeater {
model: 16
Rectangle {
width: 56; height: 56
property int colorIndex: Math.floor(Math.random()*3)
color: root.colorArray[colorIndex]
border.color: Qt.lighter(color)
Text {
anchors.centerIn: parent
color: "black"
text: "Cell " + index
}
}
}
}
}
~~~
结合运行结果来看代码:
[![QML Repeater](http://files.devbean.net/images/2014/01/qml-repeater.png)](http://files.devbean.net/images/2014/01/qml-repeater.png)
这里,我们将`Repeater`同`Grid`一起使用,可以理解成,`Repeater`作为`Grid`的数据提供者。`Repeater`的`model`可以是任何能够接受的数据模型,并且只能重复基于`Item`的组件。我们可以将上面的代码理解为:重复生成 16 个如下定义的`Rectangle`元素。首先,我们定义了一个颜色数组`colorArray`。`Repeater`会按照`model`属性定义的个数循环生成其子元素。每一次循环,`Repeater`都会创建一个矩形作为自己的子元素。这个新生成的矩形的颜色按照`Math.floor(Math.random()*3)`的算法计算而得(因此,你在本地运行代码时很可能与这里的图片不一致)。这个算法会得到 0,1,2 三者之一,用于选择数组`colorArray`中预定义的颜色。由于 JavaScript 是 QtQuick 的核心部分,所以 JavaScript 标准函数都是可用的。
`Repeater`会为每一个子元素注入一个`index`属性,也就是当前的循环索引(例子中即 0、1 直到 15)。我们可以在子元素定义中直接使用这个属性,就像例子中给`Text`赋值那样。
注意,在`Repeater`时,我们可能需要注意性能问题。处理很大的数据模型,或者需要动态获取数据时,`Repeater`这种代码就非常吃力了,我们需要另外的实现。后面的章节中,我们会再来讨论这个问题。这里只需要了解,`Repeater`不适用于处理大量数据或者动态数据,仅适用于少量的静态数据的呈现。
(79)QML 组件
最后更新于:2022-04-01 06:31:13
#(79):QML 组件
前面我们简单介绍了几种 QML 的基本元素。QML 可以由这些基本元素组合成一个复杂的元素,方便以后我们的重用。这种组合元素就被称为组件。组件就是一种可重用的元素。QML 提供了很多方法来创建组件。不过,本章我们只介绍一种方式:基于文件的组件。基于文件的组件将 QML 元素放置在一个单独的文件中,然后给这个文件一个名字。以后我们就可以通过这个名字来使用这个组件。例如,如果有一个文件名为 Button.qml,那么,我们就可以在 QML 中使用`Button { … }`这种形式。
下面我们通过一个例子来演示这种方法。我们要创建一个带有文本说明的`Rectangle`,这个矩形将成为一个按钮。用户可以点击矩形来响应事件。
~~~
import QtQuick 2.0
Rectangle {
id: root
property alias text: label.text
signal clicked
width: 116; height: 26
color: "lightsteelblue"
border.color: "slategrey"
Text {
id: label
anchors.centerIn: parent
text: "Start"
}
MouseArea {
anchors.fill: parent
onClicked: {
root.clicked()
}
}
}
~~~
我们将这个文件命名为 Button.qml,放在 main.qml 同一目录下。这里的 main.qml 就是 IDE 帮我们生成的 QML 文件。此时,我们已经创建了一个 QML 组件。这个组件其实就是一个预定义好的`Rectangle`。这是一个按钮,有一个`Text`用于显示按钮的文本;有一个`MouseArea`用于接收鼠标事件。用户可以定义按钮的文本,这是用过设置`Text`的`text`属性实现的。为了不对外暴露`Text`元素,我们给了它的`text`属性一个别名。`signal clicked`给这个`Button`一个信号。由于这个信号是无参数的,我们也可以写成`signal clicked()`,二者是等价的。注意,这个信号会在`MouseArea`的`clicked`信号被发出,具体就是在`MouseArea`的`onClicked`属性中调用个这个信号。
下面我们需要修改 main.qml 来使用这个组件:
~~~
import QtQuick 2.0
Rectangle {
width: 360
height: 360
Button {
id: button
x: 12; y: 12
text: "Start"
onClicked: {
status.text = "Button clicked!"
}
}
Text {
id: status
x: 12; y: 76
width: 116; height: 26
text: "waiting ..."
horizontalAlignment: Text.AlignHCenter
}
}
~~~
在 main.qml 中,我们直接使用了`Button`这个组件,就像 QML 其它元素一样。由于 Button.qml 与 main.qml 位于同一目录下,所以不需要额外的操作。但是,如果我们将 Button.qml 放在不同目录,比如构成如下的目录结果:
~~~
app
|- QML
| |- main.qml
|- components
|- Button.qml
~~~
那么,我们就需要在 main.qml 的`import`部分增加一行`import ../components`才能够找到`Button`组件。
有时候,选择一个组件的根元素很重要。比如我们的`Button`组件。我们使用`Rectangle`作为其根元素。`Rectangle`元素可以设置背景色等。但是,有时候我们并不允许用户设置背景色。所以,我们可以选择使用`Item`元素作为根。事实上,`Item`元素作为根元素会更常见一些。
(78)QML 基本元素
最后更新于:2022-04-01 06:31:11
#(78):QML 基本元素
QML 基本元素可以分为可视元素和不可视元素两类。可视元素(例如前面提到过的`Rectangle`)具有几何坐标,会在屏幕上占据一块显示区域。不可视元素(例如`Timer`)通常提供一种功能,这些功能可以作用于可视元素。
本章我们将会集中介绍集中最基本的可视元素:`Item`、`Rectangle`、`Text`、`Image`和`MouseArea`。
`Item`是所有可视元素中最基本的一个。它是所有其它可视元素的父元素,可以说是所有其它可视元素都继承`Item`。`Item`本身没有任何绘制,它的作用是定义所有可视元素的通用属性:
| 分组 | 属性 |
| --- | --- |
| 几何 | `x`和`y`用于定义元素左上角的坐标,`width`和`height`则定义了元素的范围。`z`定义了元素上下的层叠关系。 |
| 布局 | `anchors`(具有 left、right、top、bottom、vertical 和 horizontal center 等属性)用于定位元素相对于其它元素的`margins`的位置。 |
| 键盘处理 | `Key`和`KeyNavigation`属性用于控制键盘;`focus`属性则用于启用键盘处理,也就是获取焦点。 |
| 变形 | 提供`scale`和`rotate`变形以及更一般的针对 x、y、z 坐标值变换以及`transformOrigin`点的`transform`属性列表。 |
| 可视化 | `opacity`属性用于控制透明度;`visible`属性用于控制显示/隐藏元素;`clip`属性用于剪切元素;`smooth`属性用于增强渲染质量。 |
| 状态定义 | 提供一个由状态组成的列表`states`和当前状态`state`属性;同时还有一个`transitions`列表,用于设置状态切换时的动画效果。 |
前面我们说过,`Item`定义了所有可视元素都具有的属性。所以在下面的内容中,我们会再次详细介绍这些属性。
除了定义通用属性,`Item`另外一个重要作用是作为其它可视元素的容器。从这一点来说,`Item`非常类似于 HTML 中 div 标签的作用。
`Rectangle`继承了`Item`,并在`Item`的基础之上增加了填充色属性、边框相关的属性。为了定义圆角矩形,`Rectangle`还有一个`radius`属性。下面的代码定义了一个宽 100 像素、高 150 像素,浅金属蓝填充,红色 4 像素的边框的矩形:
~~~
Rectangle {
id: rect
width: 100
height: 150
color: "lightsteelblue"
border {
color: "#FF0000"
width: 4
}
radius: 8
}
~~~
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682327fe6cdd.png)](http://files.devbean.net/images/2014/01/qml-rect.png)
QML 中的颜色值可以使用颜色名字,也可以使用 # 十六进制的形式。这里的颜色名字同 SVG 颜色定义一致,具体可以参见[这个网页](http://www.w3.org/TR/css3-color/#svg-color)。
`Rectangle`除了`color`属性之外,还有一个`gradient`属性,用于定义使用渐变色填充。例如:
~~~
Rectangle {
width: 100
height: 150
gradient: Gradient {
GradientStop { position: 0.0; color: "red" }
GradientStop { position: 0.33; color: "yellow" }
GradientStop { position: 1.0; color: "green" }
}
border.color: "slategray"
}
~~~
`gradient`要求一个`Gradient`对象。该对象需要一个`GradientStop`的列表。我们可以这样理解渐变:所谓渐变,就是我们指定在某个位置必须是某种颜色,这期间的过渡色则由计算而得。`GradientStop`对象就是用于这种指定,它需要两个属性:`position`和`color`。前者是一个 0.0 到 1.0 的浮点数,说明 y 轴方向的位置,例如元素的最顶部是 0.0,最底部是 1.0,介于最顶和最底之间的位置可以用这么一个浮点数表示,也就是一个比例;后者是这个位置的颜色值。例如上面的`GradientStop { position: 0.33; color: "yellow" }`说明在从上往下三分之一处是黄色。当前最新版本的 QML(Qt 5.2)只支持 y 轴方向的渐变,如果需要 x 轴方向的渐变,则需要执行旋转操作,我们会在后文说明。另外,当前版本 QML 也不支持角度渐变。如果你需要角度渐变,那么最好选择一张事先制作的图片。这段代码的执行结果如下:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328002f10.png)](http://files.devbean.net/images/2014/01/qml-rect-gradient.png)
需要注意的是,`Rectangle`必须同时指定(显式地或隐式地)宽和高,否则的话是不能在屏幕上面显示出来的。这通常是一个常见的错误。
如果需要显示文本,你需要使用`Text`元素。`Text`元素最重要的属性当然就是`text`属性。这个属性类型是`string`。`Text`元素会根据文本和字体计算自己的初始宽度和高度。字体则可以通过字体属性组设置(例如`font.family`、`font.pixelSize`等)。如果要设置文本颜色,只需要设置`color`属性。`Text`最简单的使用如下:
~~~
Text {
text: "The quick brown fox"
color: "#303030"
font.family: "Century"
font.pixelSize: 28
}
~~~
运行结果:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_568232800fe7b.png)](http://files.devbean.net/images/2014/01/qml-text.png)
`Text`元素中的文本可以使用`horizontalAlignment`和`verticalAlignment`属性指定对齐方式。为了进一步增强文本渲染,我们还可以使用`style`和`styleColor`两个属性。这两个属性允许我们指定文本的显示样式和这些样式的颜色。对于很长的文本,通常我们会选择在文本末尾使用 … ,此时我们需要使用`elide`属性。`elide`属性还允许你指定 … 的显示位置。如果不希望使用这种显示方式,我们还可以选择通过`wrapMode`属性指定换行模式。例如下面的代码:
~~~
Text {
width: 160
height: 120
text: "A very very long text"
elide: Text.ElideMiddle
style: Text.Sunken
styleColor: '#FF4444'
verticalAlignment: Text.AlignTop
font {
pixelSize: 24
}
}
~~~
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_568232821e829.png)](http://files.devbean.net/images/2014/01/qml-text-styled.png)
这里的`Text`元素的文本省略号位置这一行文本的中部;具有一个 #FF4444 颜色的样式 Sunken。
`Text`元素的作用是显示文本。它不会显示文本的任何背景,这是另外的元素需要完成的事情。
`Image`元素则用于显示图像。目前 QML 支持的图像格式有 PNG、JPG、GIF 和 BMP 等。除此之外,我们也可以直接给`source`属性一个 URL 来自动从网络加载图片,也可以通过`fillMode`属性设置改变大小的行为。例如下面代码片段:
~~~
Image {
x: 12;
y: 12
// width: 48
// height: 118
source: "assets/rocket.png"
}
Image {
x: 112;
y: 12
width: 48
height: 118/2
source: "assets/rocket.png"
fillMode: Image.PreserveAspectCrop
clip: true
}
~~~
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682328230382.png)](http://files.devbean.net/images/2014/01/qml-image.png)
注意这里我们说的 URL,可以是本地路径(./images/home.png),也可以使网络路径(http://example.org/home.png)。这也是 QML 的一大特色:网络透明。如果还记得先前我们尝试做的那个天气预报程序,那时候为了从网络加载图片,我们费了很大的精力。但是在 QML 中,这都不是问题。如果一个 URL 是网络的,QML 会自动从这个地址加载对应的资源。
上面的代码中,我们使用了`Image.PreserveAspectCrop`,意思是等比例切割。此时,我们需要同时设置`clip`属性,避免所要渲染的对象超出元素范围。
最后一个我们要介绍的基本元素是`MouseArea`。顾名思义,这个元素用于用户交互。这是一个不可见的矩形区域,用于捕获鼠标事件。我们在前面的例子中已经见过这个元素。通常,我们会将这个元素与一个可视元素结合起来使用,以便这个可视元素能够与用户交互。例如:
~~~
Rectangle {
id: rect1
x: 12;
y: 12
width: 76;
height: 96
color: "lightsteelblue"
MouseArea {
/* ~~ */
}
}
~~~
`MouseArea`是 QtQuick 的重要组成部分,它将可视化展示与用户输入控制解耦。通过这种技术,你可以显示一个较小的元素,但是它有一个很大的可交互区域,以便在界面显示与用户交互之间找到一个平衡(如果在移动设备上,较小的区域非常不容易被用户成功点击。苹果公司要求界面的交互部分最少要有 40 像素以上,才能够很容易被手指点中)。
(77)QML 语法
最后更新于:2022-04-01 06:31:09
#(77):QML 语法
前面我们已经见识过 QML 文档。一个 QML 文档分为 import 和对象声明两部分。如果你要使用 Qt Quick,就需要 import QtQuick 2。QML 是一种声明语言,用于描述程序界面。QML 将用户界面分解成一块块小的元素,每一元素都由很多组件构成。QML 定义了用户界面元素的外观和行为;更复杂的逻辑则可以结合 JavaScript 脚本实现。这有点类似于 HTML 和 JavaScript 的关系,前者用来显示界面,后者用来定义行为。我们这部分文章有些来自于 [QmlBook](http://qmlbook.org/),在此表示感谢!
QML 在最简单的元素关系是层次关系。子元素处于相对于父元素的坐标系统中。也就是说,子元素的 x 和 y 的坐标值始终相对于父元素。这一点比起 Graphics View Framework 要简单得多。
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682327eeb267.png)
下面我们使用一个简单的示例文档来了解 QML 的语法:
~~~
// rectangle.qml
import QtQuick 2.0
// 根元素:Rectangle
Rectangle {
// 命名根元素
id: root // 声明属性:<name>: <value>
width: 120; height: 240
color: "#D8D8D8" // 颜色属性
// 声明一个嵌套元素(根元素的子元素)
Image {
id: rocket
x: (parent.width - width)/2; y: 40 // 使用 parent 引用父元素
source: 'assets/rocket.png'
}
// 根元素的另一个子元素
Text {
// 该元素未命名
y: rocket.y + rocket.height + 20 // 使用 id 引用元素
width: root.width // 使用 id 引用元素
horizontalAlignment: Text.AlignHCenter
text: 'Rocket'
}
}
~~~
第一个需要注意的是 import 语句。前面我们简单介绍过,QML 文档总要有 import 部分,用于指定该文档所需要引入的模块。通常这是一个模块名和版本号,比如这里的`QtQuick 2.0`。当然,我们也可以引入自己的模块或者其他文件,具体细节会在后面的章节中详细介绍。
QML文档的第二个部分是 QML 元素。一个 QML 文档有且只有一个根元素,类似 XML 文档的规定。QML 文档中的元素同样类似 XML 文档,构成一棵树。在我们的例子中,这个根元素就是`Rectangle`元素。QML 元素使用 {} 包围起来。{} 之中是该元素的属性;属性以键值对`name : value`的形式给出。这十分类似与 JSON 语法。QML 元素可以有一个`id`属性,作为该元素的名字。以后我们可以直接用这个名字指代该元素,相当于该元素的指针。需要注意的是,`id`属性在整个 QML 文档中必须是唯一的。QML 元素允许嵌套,一个 QML 元素可以没有、可以有一个或多个子元素。子元素可以使用`parent`关键字访问其父元素。正如上面的例子中显示的那样,我们可以用 id,也可以用`parent`关键字访问其他元素。一个最佳实践是,将根元素的 id 命名为 root。这样我们就可以很方便地访问到根元素。
QML 文档的注释使用`//`或者`/* */`。这同 C/C++ 或者 JavaScript 是一致的。
QML 元素的属性就是键值对,这同 JSON 是一致的。属性是一些预定义的类型,也可以有自己的初始值。比如下面的代码:
~~~
Text {
// (1) 标识符
id: thisLabel
// (2) x、y 坐标
x: 24; y: 16
// (3) 绑定
height: 2 * width
// (4) 自定义属性
property int times: 24
// (5) 属性别名
property alias anotherTimes: times
// (6) 文本和值
text: "Greetings " + times
// (7) 字体属性组
font.family: "Ubuntu"
font.pixelSize: 24
// (8) 附加属性 KeyNavigation
KeyNavigation.tab: otherLabel
// (9) 属性值改变的信号处理回调
onHeightChanged: console.log('height:', height)
// 接收键盘事件需要设置 focus
focus: true
// 根据 focus 值改变颜色
color: focus?"red":"black"
}
~~~
标识符 id 用于在 QML 文档中引用这个元素。id 并不是一个字符串,而是一个特殊的标识符类型,这是 QML 语法的一部分。如前文所述,id 在文档中必须是唯一的,并且一旦指定,不允许重新设置为另外的元素。因此,id 很像 C++ 的指针。和指针类似,id 也不能以数字开头,具体规则同 C++ 指针的命名一致。id 看起来同其它属性没有什么区别,但是,我们不能使用`id`反查出具体的值。例如,`aElement.id`是不允许的。
元素 id 应该在 QML 文档中是唯一的。实际上,QML 提供了一种动态作用域(dynamic-scoping)的机制,后加载的文档会覆盖掉前面加载的文档的相同 id。这看起来能够“更改” id 的指向,其意义是构成一个 id 的查询链。如果当前文档没有找到这个 id,那么可以在之前加载的文档中找到。这很像全局变量。不过,这种代码很难维护,因为这种机制意味着你的代码依赖于文档的加载顺序。不幸的是,我们没有办法关闭这种机制。因此,在选用 id 时,我们一定要注意唯一性这个要求,否则很有可能出现一些很难调试的问题。
属性的值由其类型决定。如果一个属性没有给值,则会使用属性的默认值。我们可以通过查看文档找到属性默认值究竟是什么。
属性可以依赖于其它属性,这种行为叫作绑定。绑定类似信号槽机制。当所依赖的属性发生变化时,绑定到这个属性的属性会得到通知,并且自动更新自己的值。例如上面的`height: 2 * width`。`height`依赖于`width`属性。当`width`改变时,`height`会自动发生变化,将自身的值更新为`width`新值的两倍。`text`属性也是一个绑定的例子。注意,`int`类型的属性会自动转换成字符串;并且在值变化时,绑定依然成立。
系统提供的属性肯定是不够的。所以 QML 允许我们自定义属性。我们可以使用`property`关键字声明一个自定义属性,后面是属性类型和属性名,最后是属性值。声明自定义属性的语法是`property <type> <name> : <value>`。如果没有默认值,那么将给出系统类型的默认值。
我们也可以声明一个默认属性,例如:
~~~
// MyLabel.qml
import QtQuick 2.0
Text {
default property var defaultText
text: "Hello, " + defaultText.text
}
~~~
在 MyLabel 中,我们声明了一个默认属性`defaultText`。注意这个属性的类型是`var`。这是一种通用类型,可以保存任何类型的属性值。
默认属性的含义在于,如果一个子元素在父元素中,但是没有赋值给父元素的任何属性,那么它就成为这个默认属性。利用上面的`MyLabel`,我们可以有如下的代码:
~~~
MyLabel {
Text { text: "world" }
}
~~~
MyLabel.qml 实际可以直接引入到另外的 QML 文档,当做一个独立的元素使用。所以,我们可以把 MyLabel 作为根元素。注意 MyLabel 的子元素 Text 没有赋值给 MyLabel 的任何属性,所以,它将自动成为默认属性 defaultText 的值。因此,上面的代码其实等价于:
~~~
MyLabel {
defaultText:Text { text: "world" }
}
~~~
如果仔细查看代码你会发现,这种默认属性的写法很像嵌套元素。其实嵌套元素正是利用这种默认属性实现的。所有可以嵌套元素的元素都有一个名为`data`的默认属性。所以这些嵌套的子元素都是添加到了`data`属性中。
属性也可以有别名。我们使用`alias`关键字声明属性的别名:`property alias <name> : <reference>`。别名和引用类似,只是给一个属性另外一个名字。C++ 教程里面经常说,“引用即别名”,这里就是“别名即引用”。这种技术对于导出属性非常有用。例如,我们希望让一个子元素的属性外部可用,那么就可以给这个属性一个别名,让外部文档通过这个别名访问这个属性。别名不需要特别声明属性类型,它使用被引用属性的类型或者 id。需要注意的是,属性别名在组件完全初始化之后才可用。因此,下面的代码是非法的:
~~~
property alias myLabel: label
myLabel.text: "error" // 错误!此时组件还没有初始化
property alias myLabelText: myLabel.text // 错误!不能为属性别名的属性创建别名
~~~
属性也可以分组。分组可以让属性更具结构化。上面示例中的`font`属性另外一种写法是:
~~~
font { family: "Ubuntu"; pixelSize: 24 }
~~~
有些属性可以附加到元素本身,其语法是`<Element>.<property>: <value>`。
每一个属性都可以发出信号,因而都可以关联信号处理函数。这个处理函数将在属性值变化时调用。这种值变化的信号槽命名为 on + 属性名 + Changed,其中属性名要首字母大写。例如上面的例子中,`height`属性变化时对应的槽函数名字就是`onHeightChanged`。
QML 和 JavaScript 关系密切。我们将在后面的文章中详细解释,不过现在可以先看个简单的例子:
~~~
Text {
id: label
x: 24; y: 24
// 自定义属性,表示空格按下的次数
property int spacePresses: 0
text: "Space pressed: " + spacePresses + " times"
// (1) 文本变化的响应函数
onTextChanged: console.log("text changed to:", text)
// 接收键盘事件,需要设置 focus 属性
focus: true
// (2) 调用 JavaScript 函数
Keys.onSpacePressed: {
increment()
}
// 按下 Esc 键清空文本
Keys.onEscapePressed: {
label.text = ''
}
// (3) 一个 JavaScript 函数
function increment() {
spacePresses = spacePresses + 1
}
}
~~~
Text 元素会发出`textChanged`信号。我们使用 on + 信号名,信号名首字母大写的属性表示一个槽函数。也就是说,当 Text 元素发出`textChanged`信号时,`onTextChanged`就会被调用。类似的,`onSpacePressed`属性会在空格键按下时被调用。此时,我们调用了一个 JavaScript 函数。
QML 文档中可以定义 JavaScript 函数,语法同普通 JavaScript 函数一样。
QML 的绑定机制同 JavaScript 的赋值运算符有一定类似。它们都可以将右面的值赋值给前面。不同之处在于,绑定会在后面的值发生改变时,重新计算前面的值;但是赋值只是一次性的。
(76)QML 和 QtQuick 2
最后更新于:2022-04-01 06:31:06
#(76):QML 和 QtQuick 2
前面我们已经了解了 Qt 的一部分内容。这部分内容全部集中在 C++ 方面。也就是说,至今为止我们的程序都是使用 C++ 语言完成的。这在 Qt 5 之前的版本中是唯一的途径。不过,自从 Qt 5 开始,情况有了变化。事实上,从 Qt 4.7 开始,Qt 引入了一种声明式脚本语言,称为 QML(Qt Meta Language 或者 Qt Modeling Language),作为 C++ 语言的一种替代。而 Qt Quick 就是使用 QML 构建的一套类库。
QML 是一种基于 JavaScript 的声明式语言。在 Qt 5 中,QML 有了长足进步,并且同 C++ 并列成为 Qt 的首选编程语言。也就是说,使用 Qt 5,我们不仅可以使用 C++ 开发 Qt 程序,而且可以使用 QML。虽然 QML 是解释型语言,性能要比 C++ 低一些,但是新版 QML 使用 V8,Qt 5.2 又引入了专为 QML 优化的 V4 引擎,使得其性能不再有明显降低。在 Nokia 发布 Qt 4.7 的时候,QML 被用于开发手机应用程序,全面支持触摸操作、流畅的动画效果等。但是在 Qt 5 中,QML 已经不仅限于开发手机应用,也可以用户开发传统的桌面程序。
QML 文档描述了一个对象树。QML 元素包含了其构造块、图形元素(矩形、图片等)和行为(例如动画、切换等)。这些 QML 元素按照一定的嵌套关系构成复杂的组件,供用户交互。
本章我们先来编写一个简单的 QML 程序,了解 QML 的基本概念。需要注意的是,这里的 Qt Quick 使用的是 Qt Quick 2 版本,与 Qt 4.x 提供的 Qt Quick 1 不兼容。
首先,使用 Qt Creator 创建一个 Qt Quick Application。在之后的 Qt Quick Component 选项中,我们选择 Qt Quick 2.0:
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682327da6bcb.png)](http://files.devbean.net/images/2013/12/qq-pro-setting.png)
完成创建之后,我们可以看到一个 QML 项目所需要的基本文件。这里的项目名字为 qqdemo,而且有一个自己添加的资源文件,所以这个示意图可能与你的不同。
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682327dbff9e.png)](http://files.devbean.net/images/2013/12/qq-pro-files.png)
qtquick2applicationviewer 里面的内容是 Qt Creator 帮助我们生成的,这里面的文件一般不会修改。我们可以认为这个文件夹下的文件就是运行 QML 的一个加载器。Application Viewer 用于加载 QML 文件并将其解释显示,类似于浏览器解释运行网页。
我们真正关心的是 main.qml 里面的内容:
~~~
import QtQuick 2.0
Rectangle {
width: 360
height: 360
Text {
text: qsTr("Hello World")
anchors.centerIn: parent
}
MouseArea {
anchors.fill: parent
onClicked: {
Qt.quit();
}
}
}
~~~
这段代码看起来很简单,事实也的确如此。一个 QML 文档分为 import 和 declaration 两部分。前者用于引入文档中所需要的组件(有可能是类库,也可以是一个 JavaScript 文件或者另外的 QML 文件);后者用于声明本文档中的 QML 元素。
第一行,我们使用`import`语句引入 QtQuick 2.0。由于这只是一个示例程序,所以我们没有引入更多的内容。
每一个 QML 有且只有一个根元素,类似于 XML 文档。这个根元素就是这个 QML 文档中定义的 QML 元素,在这个例子中就是一个`Rectangle`对象。注意一下这个 QML 文档的具体语法,非常类似于 JSON 的定义,使用键值对的形式区分元素属性。所以我们能够很清楚看到,我们定义了一个矩形,宽度为 360 像素,高度为 360 像素。记得我们说过,QML 文档定义了一个对象树,所以 QML 文档中元素是可以嵌套的。在这个矩形中,我们又增加了一个`Text`元素,顾名思义,就是一个文本。`Text`显示的是 Hello World 字符串,而这个字符串是由`qsTr()`函数返回的。`qsTr()`函数就是`QObject::tr()`函数的 QML 版本,用于返回可翻译的字符串。`Text`的位置则是由锚点(anchor)定义。示例中的`Text`位置定义为 parent 中心,其中`parent`属性就是这个元素所在的外部的元素。同理,我们可以看到`MouseArea`是充满父元素的。`MouseArea`还有一个`onClicked`属性。这是一个回调,也就是鼠标点击事件。`MouseArea`可以看作是可以相应鼠标事件的区域。当点击事件发出时,就会执行`onClicked`中的代码。这段代码其实是让整个程序退出。注意我们的`MouseArea`充满整个矩形,所以整个区域都可以接受鼠标事件。
当我们运行这个项目时,我们就可以看到一个宽和高都是 360 像素的矩形,中央有一行文本,鼠标点击矩形任意区域就会使其退出:
![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682327dd658a.png)
接下来我们可以改变 main.qml 文件中的“Hello World”字符串,不重新编译直接运行,就会看到运行结果也会相应的变化。这说明 QML 文档是运行时解释的,不需要经过编译。所以,利用 QML 的解释执行的特性,QML 尤其适合于快速开发和原型建模。另外,由于 QML 比 C++ 简单很多,所以 QML 也适用于提供插件等机制。
(75)线程总结
最后更新于:2022-04-01 06:31:04
#(75):线程总结
前面我们已经详细介绍过有关线程的一些值得注意的事项。现在我们开始对线程做一些总结。
有关线程,你可以做的是:
* 在`QThread`子类添加信号。这是绝对安全的,并且也是正确的(前面我们已经详细介绍过,发送者的线程依附性没有关系)
不应该做的是:
* 调用`moveToThread(this)`函数
* 指定连接类型:这通常意味着你正在做错误的事情,比如将`QThread`控制接口与业务逻辑混杂在了一起(而这应该放在该线程的一个独立对象中)
* 在`QThread`子类添加槽函数:这意味着它们将在错误的线程被调用,也就是`QThread`对象所在线程,而不是`QThread`对象管理的线程。这又需要你指定连接类型或者调用`moveToThread(this)`函数
* 使用`QThread::terminate()`函数
不能做的是:
* 在线程还在运行时退出程序。使用`QThread::wait()`函数等待线程结束
* 在`QThread`对象所管理的线程仍在运行时就销毁该对象。如果你需要某种“自行销毁”的操作,你可以把`finished()`信号同`deleteLater()`槽连接起来
那么,下面一个问题是:我什么时候应该使用线程?
**首先,当你不得不使用同步 API 的时候。**
如果你需要使用一个没有非阻塞 API 的库或代码(所谓非阻塞 API,很大程度上就是指信号槽、事件、回调等),那么,避免事件循环被阻塞的解决方案就是使用进程或者线程。不过,由于开启一个新的工作进程,让这个进程去完成任务,然后再与当前进程进行通信,这一系列操作的代价都要比开启线程要昂贵得多,所以,线程通常是最好的选择。
一个很好的例子是地址解析服务。注意我们这里并不讨论任何第三方 API,仅仅假设一个有这样功能的库。这个库的工作是将一个主机名转换成地址。这个过程需要去到一个系统(也就是域名系统,Domain Name System, DNS)执行查询,这个系统通常是一个远程系统。一般这种响应应该瞬间完成,但是并不排除远程服务器失败、某些包可能会丢失、网络可能失去链接等等。简单来说,我们的查询可能会等几十秒钟。
UNIX 系统上的标准 API 是阻塞的(不仅是旧的`gethostbyname(3)`,就连新的`getservbyname(3)`和`getaddrinfo(3)`也是一样)。Qt 提供的`QHostInfo`类同样用于地址解析,默认情况下,内部使用一个`QThreadPool`提供后台运行方式的查询(如果关闭了 Qt 的线程支持,则提供阻塞式 API)。
另外一个例子是图像加载和缩放。`QImageReader`和`QImage`只提供了阻塞式 API,允许我们从设备读取图片,或者是缩放到不同的分辨率。如果你需要处理很大的图像,这种任务会花费几十秒钟。
**其次,当你希望扩展到多核应用的时候。**
线程允许你的程序利用多核系统的优势。每一个线程都可以被操作系统独立调度,如果你的程序运行在多核机器上,调度器很可能会将每一个线程分配到各自的处理器上面运行。
举个例子,一个程序需要为很多图像生成缩略图。一个具有固定 n 个线程的线程池,每一个线程交给系统中的一个可用的 CPU 进行处理(我们可以使用`QThread::idealThreadCount()`获取可用的 CPU 数)。这样的调度将会把图像缩放工作交给所有线程执行,从而有效地提升效率,几乎达到与 CPU 数的线性提升(实际情况不会这么简单,因为有时候 CPU 并不是瓶颈所在)。
**第三,当你不想被别人阻塞的时候。**
这是一个相当高级的话题,所以你现在可以暂时不看这段。这个问题的一个很好的例子是在 WebKit 中使用`QNetworkAccessManager`。WebKit 是一个现代的浏览器引擎。它帮助我们展示网页。Qt 中的`QWebView`就是使用的 WebKit。
`QNetworkAccessManager`则是 Qt 处理 HTTP 请求和响应的通用类。我们可以将它看做浏览器的网络引擎。在 Qt 4.8 之前,这个类没有使用任何协助工作线程,所有的网络处理都是在`QNetworkAccessManager`及其`QNetworkReply`所在线程完成。
虽然在网络处理中不使用线程是一个好主意,但它也有一个很大的缺点:如果你不能及时从 socket 读取数据,内核缓冲区将会被填满,于是开始丢包,传输速度将会直线下降。
socket 活动(也就是从一个 socket 读取一些可用的数据)是由 Qt 的事件循环管理的。因此,阻塞事件循环将会导致传输性能的损失,因为没有人会获得有数据可读的通知,因此也就没有人能够读取这些数据。
但是什么会阻塞事件循环?最坏的答案是:WebKit 自己!只要收到数据,WebKit 就开始生成网页布局。不幸的是,这个布局的过程非常复杂和耗时,因此它会阻塞事件循环。尽管阻塞时间很短,但是足以影响到正常的数据传输(宽带连接在这里发挥了作用,在很短时间内就可以塞满内核缓冲区)。
总结一下上面所说的内容:
* WebKit 发起一次请求
* 从服务器响应获取一些数据
* WebKit 利用到达的数据开始进行网页布局,阻塞事件循环
* 由于事件循环被阻塞,也就没有了可用的事件循环,于是操作系统接收了到达的数据,但是却不能从`QNetworkAccessManager`的 socket 读取
* 内核缓冲区被填满,传输速度变慢
网页的整体加载时间被自身的传输速度的降低而变得越来越坏。
注意,由于`QNetworkAccessManager`和`QNetworkReply`都是`QObject`,所以它们都不是线程安全的,因此你不能将它们移动到另外的线程继续使用。因为它们可能同时有两个线程访问:你自己的和它们所在的线程,这是因为派发给它们的事件会由后面一个线程的事件循环发出,但你不能确定哪一线程是“后面一个”。
Qt 4.8 之后,`QNetworkAccessManager`默认会在一个独立的线程处理 HTTP 请求,所以导致 GUI 失去响应以及操作系统缓冲区过快填满的问题应该已经被解决了。
那么,什么情况下不应该使用线程呢?
**定时器**
这可能是最容易误用线程的情况了。如果我们需要每隔一段时间调用一个函数,很多人可能会这么写代码:
~~~
// 最错误的代码
while (condition) {
doWork();
sleep(1); // C 库里面的 sleep(3) 函数
}
~~~
当读过我们前面的文章之后,可能又会引入线程,改成这样的代码:
~~~
// 错误的代码
class Thread : public QThread {
protected:
void run() {
while (condition) {
// 注意,如果我们要在别的线程修改 condition,那么它也需要加锁
doWork();
sleep(1); // 这次是 QThread::sleep()
}
}
};
~~~
最好最简单的实现是使用定时器,比如`QTimer`,设置 1s 超时,然后将`doWork()`作为槽:
~~~
class Worker : public QObject
{
Q_OBJECT
public:
Worker()
{
connect(&timer, SIGNAL(timeout()), this, SLOT(doWork()));
timer.start(1000);
}
private slots:
void doWork()
{
/* ... */
}
private:
QTimer timer;
};
~~~
**网络/状态机**
下面是一个很常见的处理网络操作的设计模式:
~~~
socket->connect(host);
socket->waitForConnected();
data = getData();
socket->write(data);
socket->waitForBytesWritten();
socket->waitForReadyRead();
socket->read(response);
reply = process(response);
socket->write(reply);
socket->waitForBytesWritten();
/* ... */
~~~
在经过前面几章的介绍之后,不用多说,我们就会发现这里的问题:大量的`waitFor*()`函数会阻塞事件循环,冻结 UI 界面等等。注意,上面的代码还没有加入异常处理,否则的话肯定会更复杂。这段代码的错误在于,我们的网络实际是异步的,如果我们非得按照同步方式处理,就像拿起枪打自己的脚。为了解决这个问题,很多人会简单地将这段代码移动到一个新的线程。
一个更抽象的例子是:
~~~
result = process_one_thing();
if (result->something()) {
process_this();
} else {
process_that();
}
wait_for_user_input();
input = read_user_input();
process_user_input(input);
/* ... */
~~~
这段抽象的代码与前面网络的例子有“异曲同工之妙”。
让我们回过头来看看这段代码究竟是做了什么:我们实际是想创建一个状态机,这个状态机要根据用户的输入作出合理的响应。例如我们网络的例子,我们实际是想要构建这样的东西:
~~~
空闲 → 正在连接(调用<code>connectToHost()</code>)
正在连接 → 成功连接(发出<code>connected()</code>信号)
成功连接 → 发送登录数据(将登录数据发送到服务器)
发送登录数据 → 登录成功(服务器返回 ACK)
发送登录数据 → 登录失败(服务器返回 NACK)
~~~
以此类推。
既然知道我们的实际目的,我们就可以修改代码来创建一个真正的状态机(Qt 甚至提供了一个状态机类:`QStateMachine`)。创建状态机最简单的方法是使用一个枚举来记住当前状态。我们可以编写如下代码:
~~~
class Object : public QObject
{
Q_OBJECT
enum State {
State1, State2, State3 /* ... */
};
State state;
public:
Object() : state(State1)
{
connect(source, SIGNAL(ready()), this, SLOT(doWork()));
}
private slots:
void doWork() {
switch (state) {
case State1:
/* ... */
state = State2;
break;
case State2:
/* ... */
state = State3;
break;
/* ... */
}
}
};
~~~
`source`对象是哪来的?这个对象其实就是我们关心的对象:例如,在网络的例子中,我们可能希望把 socket 的`QAbstractSocket::connected()`或者`QIODevice::readyRead()`信号与我们的槽函数连接起来。当然,我们很容易添加更多更合适的代码(比如错误处理,使用`QAbstractSocket::error()`信号就可以了)。这种代码是真正异步、信号驱动的设计。
**将任务分割成若干部分**
假设我们有一个很耗时的计算,我们不能简单地将它移动到另外的线程(或者是我们根本无法移动它,比如这个任务必须在 GUI 线程完成)。如果我们将这个计算任务分割成小块,那么我们就可以及时返回事件循环,从而让事件循环继续派发事件,调用处理下一个小块的函数。回一下如何实现队列连接,我们就可以轻松完成这个任务:将事件提交到接收对象所在线程的事件循环;当事件发出时,响应函数就会被调用。
我们可以使用`QMetaObject::invokeMethod()`函数,通过指定`Qt::QueuedConnection`作为调用类型来达到相同的效果。不过这要求函数必须是内省的,也就是说这个函数要么是一个槽函数,要么标记有`Q_INVOKABLE`宏。如果我们还需要传递参数,我们需要使用`qRegisterMetaType()`函数将参数注册到 Qt 元类型系统。下面是代码示例:
~~~
class Worker : public QObject
{
Q_OBJECT
public slots:
void startProcessing()
{
processItem(0);
}
void processItem(int index)
{
/* 处理 items[index] ... */
if (index < numberOfItems) {
QMetaObject::invokeMethod(this,
"processItem",
Qt::QueuedConnection,
Q_ARG(int, index + 1));
}
}
};
~~~
由于没有任何线程调用,所以我们可以轻易对这种计算任务执行暂停/恢复/取消,以及获取结果。
至此,我们利用五个章节将有关线程的问题简单介绍了下。线程应该说是全部设计里面最复杂的部分之一,所以这部分内容也会比较困难。在实际运用中肯定会更多的问题,这就只能让我们具体分析了。
(74)线程和 QObject
最后更新于:2022-04-01 06:31:02
#(74):线程和 QObject
前面两个章节我们从事件循环和线程类库两个角度阐述有关线程的问题。本章我们将深入线程间得交互,探讨线程和`QObject`之间的关系。在某种程度上,这才是多线程编程真正需要注意的问题。
现在我们已经讨论过事件循环。我们说,每一个 Qt 应用程序至少有一个事件循环,就是调用了`QCoreApplication::exec()`的那个事件循环。不过,`QThread`也可以开启事件循环。只不过这是一个受限于线程内部的事件循环。因此我们将处于调用`main()`函数的那个线程,并且由`QCoreApplication::exec()`创建开启的那个事件循环成为主事件循环,或者直接叫主循环。注意,`QCoreApplication::exec()`只能在调用`main()`函数的线程调用。主循环所在的线程就是主线程,也被成为 GUI 线程,因为所有有关 GUI 的操作都必须在这个线程进行。`QThread`的局部事件循环则可以通过在`QThread::run()`中调用`QThread::exec()`开启:
~~~
class Thread : public QThread
{
protected:
void run() {
/* ... 初始化 ... */
exec();
}
};
~~~
记得我们前面介绍过,Qt 4.4 版本以后,`QThread::run()`不再是纯虚函数,它会调用`QThread::exec()`函数。与`QCoreApplication`一样,`QThread`也有`QThread::quit()`和`QThread::exit()`函数来终止事件循环。
线程的事件循环用于为线程中的所有`QObjects`对象分发事件;默认情况下,这些对象包括线程中创建的所有对象,或者是在别处创建完成后被移动到该线程的对象(我们会在后面详细介绍“移动”这个问题)。我们说,一个`QObject`的所依附的线程(thread affinity)是指它所在的那个线程。它同样适用于在`QThread`的构造函数中构建的对象:
~~~
class MyThread : public QThread
{
public:
MyThread()
{
otherObj = new QObject;
}
private:
QObject obj;
QObject *otherObj;
QScopedPointer yetAnotherObj;
};
~~~
在我们创建了`MyThread`对象之后,`obj`、`otherObj`和`yetAnotherObj`的线程依附性是怎样的?是不是就是`MyThread`所表示的那个线程?要回答这个问题,我们必须看看究竟是哪个线程创建了它们:实际上,是调用了`MyThread`构造函数的线程创建了它们。因此,这些对象不在`MyThread`所表示的线程,而是在创建了`MyThread`的那个线程中。
我们可以通过调用`QObject::thread()`可以查询一个`QObject`的线程依附性。注意,在`QCoreApplication`对象之前创建的`QObject`没有所谓线程依附性,因此也就没有对象为其派发事件。也就是说,实际是`QCoreApplication`创建了代表主线程的`QThread`对象。
[![](https://docs.gechiui.com/gc-content/uploads/sites/kancloud/2015-12-29_5682327b75bd9.png)](http://files.devbean.net/images/2013/12/threadsandobjects.png)
我们可以使用线程安全的`QCoreApplication::postEvent()`函数向一个对象发送事件。它将把事件加入到对象所在的线程的事件队列中,因此,如果这个线程没有运行事件循环,这个事件也不会被派发。
值得注意的一点是,`QObject`及其所有子类都不是线程安全的(但都是可重入的)。因此,你不能有两个线程同时访问一个`QObject`对象,除非这个对象的内部数据都已经很好地序列化(例如为每个数据访问加锁)。记住,在你从另外的线程访问一个对象时,它可能正在处理所在线程的事件循环派发的事件!基于同样的原因,你也不能在另外的线程直接`delete`一个`QObject`对象,相反,你需要调用`QObject::deleteLater()`函数,这个函数会给对象所在线程发送一个删除的事件。
此外,`QWidget`及其子类,以及所有其它 GUI 相关类(即便不是`QObject`的子类,例如`QPixmap`),甚至不是可重入的:它们只能在 GUI 线程访问。
`QObject`的线程依附性是可以改变的,方法是调用`QObject::moveToThread()`函数。该函数会改变一个对象及其所有子对象的线程依附性。由于`QObject`不是线程安全的,所以我们只能在该对象所在线程上调用这个函数。也就是说,我们只能在对象所在线程将这个对象移动到另外的线程,不能在另外的线程改变对象的线程依附性。还有一点是,Qt 要求`QObject`的所有子对象都必须和其父对象在同一线程。这意味着:
* 不能对有父对象(parent 属性)的对象使用`QObject::moveToThread()`函数
* 不能在`QThread`中以这个`QThread`本身作为父对象创建对象,例如:
~~~
class Thread : public QThread {
void run() {
QObject *obj = new QObject(this); // 错误!
}
};
~~~
这是因为`QThread`对象所依附的线程是创建它的那个线程,而不是它所代表的线程。
Qt 还要求,在代表一个线程的`QThread`对象销毁之前,所有在这个线程中的对象都必须先`delete`。要达到这一点并不困难:我们只需在`QThread::run()`的栈上创建对象即可。
现在的问题是,既然线程创建的对象都只能在函数栈上,怎么能让这些对象与其它线程的对象通信呢?Qt 提供了一个优雅清晰的解决方案:我们在线程的事件队列中加入一个事件,然后在事件处理函数中调用我们所关心的函数。显然这需要线程有一个事件循环。这种机制依赖于 moc 提供的反射:因此,只有信号、槽和使用`Q_INVOKABLE`宏标记的函数可以在另外的线程中调用。
`QMetaObject::invokeMethod()`静态函数会这样调用:
~~~
QMetaObject::invokeMethod(object, "methodName",
Qt::QueuedConnection,
Q_ARG(type1, arg1),
Q_ARG(type2, arg2));
~~~
主意,上面函数调用中出现的参数类型都必须提供一个公有构造函数,一个公有的析构函数和一个公有的复制构造函数,并且要使用`qRegisterMetaType()`函数向 Qt 类型系统注册。
跨线程的信号槽也是类似的。当我们将信号与槽连接起来时,`QObject::connect()`的最后一个参数将指定连接类型:
* `Qt::DirectConnection`:直接连接意味着槽函数将在信号发出的线程直接调用
* `Qt::QueuedConnection`:队列连接意味着向接受者所在线程发送一个事件,该线程的事件循环将获得这个事件,然后之后的某个时刻调用槽函数
* `Qt::BlockingQueuedConnection`:阻塞的队列连接就像队列连接,但是发送者线程将会阻塞,直到接受者所在线程的事件循环获得这个事件,槽函数被调用之后,函数才会返回
* `Qt::AutoConnection`:自动连接(默认)意味着如果接受者所在线程就是当前线程,则使用直接连接;否则将使用队列连接
注意在上面每种情况中,发送者所在线程都是无关紧要的!在自动连接情况下,Qt 需要查看**信号发出的线程**是不是与**接受者所在线程**一致,来决定连接类型。注意,Qt 检查的是**信号发出的线程**,而不是信号发出的对象所在的线程!我们可以看看下面的代码:
~~~
class Thread : public QThread
{
Q_OBJECT
signals:
void aSignal();
protected:
void run() {
emit aSignal();
}
};
/* ... */
Thread thread;
Object obj;
QObject::connect(&thread, SIGNAL(aSignal()), &obj, SLOT(aSlot()));
thread.start();
~~~
这里的`obj`发出`aSignal()`信号时,使用哪种连接方式?答案是:直接连接。因为`Thread`对象所在线程发出了信号,也就是信号发出的线程与接受者是同一个。在`aSlot()`槽函数中,我们可以直接访问`Thread`的某些成员变量,但是注意,在我们访问这些成员变量时,`Thread::run()`函数可能也在访问!这意味着二者并发进行:这是一个完美的导致崩溃的隐藏bug。
另外一个例子可能更为重要:
class Thread : public QThread
{
Q_OBJECT
slots:
void aSlot() {
/* ... */
}
protected:
void run() {
QObject *obj = new Object;
connect(obj, SIGNAL(aSignal()), this, SLOT(aSlot()));
/* ... */
}
};
这个例子也会使用队列连接。然而,这个例子比上面的例子更具隐蔽性:在这个例子中,你可能会觉得,`Object`所在`Thread`所代表的线程中被创建,又是访问的`Thread`自己的成员数据。稍有不慎便会写出这种代码。
为了解决这个问题,我们可以这么做:`Thread`构造函数中增加一个函数调用:`moveToThread(this)`:
~~~
class Thread : public QThread {
Q_OBJECT
public:
Thread() {
moveToThread(this); // 错误!
}
/* ... */
};
~~~
实际上,这的确可行(因为`Thread`的线程依附性被改变了:它所在的线程成了自己),但是这并不是一个好主意。这种代码意味着我们其实误解了线程对象(`QThread`子类)的设计意图:`QThread`对象不是线程本身,它们其实是用于管理它所代表的线程的对象。因此,它们应该在另外的线程被使用(通常就是它自己所在的线程),而不是在自己所代表的线程中。
上面问题的最好的解决方案是,将处理任务的部分与管理线程的部分分离。简单来说,我们可以利用一个`QObject`的子类,使用`QObject::moveToThread()`改变其线程依附性:
~~~
class Worker : public QObject
{
Q_OBJECT
public slots:
void doWork() {
/* ... */
}
};
/* ... */
QThread *thread = new QThread;
Worker *worker = new Worker;
connect(obj, SIGNAL(workReady()), worker, SLOT(doWork()));
worker->moveToThread(thread);
thread->start();
~~~
(73)Qt 线程相关类
最后更新于:2022-04-01 06:30:59
#(73):Qt 线程相关类
希望上一章有关事件循环的内容还没有把你绕晕。本章将重新回到有关线程的相关内容上面来。在前面的章节我们了解了有关`QThread`类的简单使用。不过,Qt 提供的有关线程的类可不那么简单,否则的话我们也没必要再三强调使用线程一定要万分小心,一不留神就会陷入陷阱。
事实上,Qt 对线程的支持可以追溯到2000年9月22日发布的 Qt 2.2。在这个版本中,Qt 引入了`QThread`。不过,当时对线程的支持并不是默认开启的。Qt 4.0 开始,线程成为所有平台的默认开启选项(这意味着如果不需要线程,你可以通过编译选项关闭它,不过这不是我们现在的重点)。现在版本的 Qt 引入了很多类来支持线程,下面我们将开始逐一了解它们。
`QThread`是我们将要详细介绍的第一个类。它也是 Qt 线程类中最核心的底层类。由于 Qt 的跨平台特性,`QThread`要隐藏掉所有平台相关的代码。
正如前面所说,要使用`QThread`开始一个线程,我们可以创建它的一个子类,然后覆盖其`QThread::run()`函数:
~~~
class Thread : public QThread
{
protected:
void run()
{
/* 线程的相关代码 */
}
};
~~~
然后我们这样使用新建的类来开始一个新的线程:
~~~
Thread *thread = new Thread;
thread->start(); // 使用 start() 开始新的线程
~~~
注意,从 Qt 4.4 开始,`QThread`就已经不是抽象类了。`QThread::run()`不再是纯虚函数,而是有了一个默认的实现。这个默认实现其实是简单地调用了`QThread::exec()`函数,而这个函数,按照我们前面所说的,其实是开始了一个事件循环(有关这种实现的进一步阐述,我们将在后面的章节详细介绍)。
`QRunnable`是我们要介绍的第二个类。这是一个轻量级的抽象类,用于开始一个另外线程的任务。这种任务是运行过后就丢弃的。由于这个类是抽象类,我们需要继承`QRunnable`,然后重写其纯虚函数`QRunnable::run()`:
~~~
class Task : public QRunnable
{
public:
void run()
{
/* 线程的相关代码 */
}
};
~~~
要真正执行一个`QRunnable`对象,我们需要使用`QThreadPool`类。顾名思义,这个类用于管理一个线程池。通过调用`QThreadPool::start(runnable)`函数,我们将一个`QRunnable`对象放入`QThreadPool`的执行队列。一旦有线程可用,线程池将会选择一个`QRunnable`对象,然后在那个线程开始执行。所有 Qt 应用程序都有一个全局线程池,我们可以使用`QThreadPool::globalInstance()`获得这个全局线程池;与此同时,我们也可以自己创建私有的线程池,并进行手动管理。
需要注意的是,`QRunnable`不是一个`QObject`,因此也就没有内建的与其它组件交互的机制。为了与其它组件进行交互,你必须自己编写低级线程原语,例如使用 mutex 守护来获取结果等。
`QtConcurrent`是我们要介绍的最后一个对象。这是一个高级 API,构建于`QThreadPool`之上,用于处理大多数通用的并行计算模式:map、reduce 以及 filter。它还提供了`QtConcurrent::run()`函数,用于在另外的线程运行一个函数。注意,`QtConcurrent`是一个命名空间而不是一个类,因此其中的所有函数都是命名空间内的全局函数。
不同于`QThread`和`QRunnable`,`QtConcurrent`不要求我们使用低级同步原语:所有的`QtConcurrent`都返回一个`QFuture`对象。这个对象可以用来查询当前的运算状态(也就是任务的进度),可以用来暂停/回复/取消任务,当然也可以用来获得运算结果。注意,并不是所有的`QFuture`对象都支持暂停或取消的操作。比如,由`QtConcurrent::run()`返回的`QFuture`对象不能取消,但是由`QtConcurrent::mappedReduced()`返回的是可以的。`QFutureWatcher`类则用来监视`QFuture`的进度,我们可以用信号槽与`QFutureWatcher`进行交互(注意,`QFuture`也没有继承`QObject`)。
下面我们可以对比一下上面介绍过的三种类:
| 特性 | `QThread` | `QRunnable` | `QtConcurrent` |
| -- || -- || -- || -- |
| 高级 API | ✘ | ✘ | ![✔](http://s.w.org/images/core/emoji/72x72/2714.png) |
| 面向任务 | ✘ | ![✔](http://s.w.org/images/core/emoji/72x72/2714.png) | ![✔](http://s.w.org/images/core/emoji/72x72/2714.png) |
| 内建对暂停/恢复/取消的支持 | ✘ | ✘ | ![✔](http://s.w.org/images/core/emoji/72x72/2714.png) |
| 具有优先级 | ![✔](http://s.w.org/images/core/emoji/72x72/2714.png) | ✘ | ✘ |
| 可运行事件循环 | ![✔](http://s.w.org/images/core/emoji/72x72/2714.png) | ✘ | ✘ |
(72)线程和事件循环
最后更新于:2022-04-01 06:30:57
#(72):线程和事件循环
前面一章我们简单介绍了如何使用`QThread`实现线程。现在我们开始详细介绍如何“正确”编写多线程程序。我们这里的大部分内容来自于[Qt的一篇Wiki文档](http://qt-project.org/wiki/Threads_Events_QObjects),有兴趣的童鞋可以去看原文。
在介绍在以前,我们要认识两个术语:
* **可重入的(Reentrant)**:如果多个线程可以在同一时刻调用一个类的所有函数,并且保证每一次函数调用都引用一个唯一的数据,就称这个类是可重入的(Reentrant means that all the functions in the referenced class can be called simultaneously by multiple threads, provided that each invocation of the functions reference unique data.)。大多数 C++ 类都是可重入的。类似的,一个函数被称为可重入的,如果该函数允许多个线程在同一时刻调用,而每一次的调用都只能使用其独有的数据。全局变量就不是函数独有的数据,而是共享的。换句话说,这意味着类或者函数的使用者必须使用某种额外的机制(比如锁)来控制对对象的实例或共享数据的序列化访问。
* **线程安全(Thread-safe)**:如果多个线程可以在同一时刻调用一个类的所有函数,即使每一次函数调用都引用一个共享的数据,就说这个类是线程安全的(Threadsafe means that all the functions in the referenced class can be called simultaneously by multiple threads even when each invocation references shared data.)。如果多个线程可以在同一时刻访问函数的共享数据,就称这个函数是线程安全的。
进一步说,对于一个类,如果不同的实例可以被不同线程同时使用而不受影响,就说这个类是可重入的;如果这个类的所有成员函数都可以被不同线程同时调用而不受影响,即使这些调用针对同一个对象,那么我们就说这个类是线程安全的。由此可以看出,线程安全的语义要强于可重入。接下来,我们从事件开始讨论。之前我们说过,Qt 是事件驱动的。在 Qt 中,事件由一个普通对象表示(`QEvent`或其子类)。这是事件与信号的一个很大区别:事件总是由某一种类型的对象表示,针对某一个特殊的对象,而信号则没有这种目标对象。所有`QObject`的子类都可以通过覆盖`QObject::event()`函数来控制事件的对象。
事件可以由程序生成,也可以在程序外部生成。例如:
* `QKeyEvent`和`QMouseEvent`对象表示键盘或鼠标的交互,通常由系统的窗口管理器产生;
* `QTimerEvent`事件在定时器超时时发送给一个`QObject`,定时器事件通常由操作系统发出;
* `QChildEvent`在增加或删除子对象时发送给一个`QObject`,这是由 Qt 应用程序自己发出的。
需要注意的是,与信号不同,事件并不是一产生就被分发。事件产生之后被加入到一个队列中(这里的队列含义同数据结构中的概念,先进先出),该队列即被称为事件队列。事件分发器遍历事件队列,如果发现事件队列中有事件,那么就把这个事件发送给它的目标对象。这个循环被称作事件循环。事件循环的伪代码描述大致如下所示:
~~~
while (is_active)
{
while (!event_queue_is_empty) {
dispatch_next_event();
}
wait_for_more_events();
}
~~~
正如前面所说的,调用`QCoreApplication::exec()` 函数意味着进入了主循环。我们把事件循环理解为一个无限循环,直到`QCoreApplication::exit()`或者`QCoreApplication::quit()`被调用,事件循环才真正退出。
伪代码里面的`while`会遍历整个事件队列,发送从队列中找到的事件;`wait_for_more_events()`函数则会阻塞事件循环,直到又有新的事件产生。我们仔细考虑这段代码,在`wait_for_more_events()`函数所得到的新的事件都应该是由程序外部产生的。因为所有内部事件都应该在事件队列中处理完毕了。因此,我们说事件循环在`wait_for_more_events()`函数进入休眠,并且可以被下面几种情况唤醒:
* 窗口管理器的动作(键盘、鼠标按键按下、与窗口交互等);
* 套接字动作(网络传来可读的数据,或者是套接字非阻塞写等);
* 定时器;
* 由其它线程发出的事件(我们会在后文详细解释这种情况)。
在类 UNIX 系统中,窗口管理器(比如 X11)会通过套接字(Unix Domain 或 TCP/IP)向应用程序发出窗口活动的通知,因为客户端就是通过这种机制与 X 服务器交互的。如果我们决定要实现基于内部的`socketpair(2)`函数的跨线程事件的派发,那么窗口的管理活动需要唤醒的是:
* 套接字 socket
* 定时器 timer
这也正是`select(2)`系统调用所做的:它监视窗口活动的一组描述符,如果在一定时间内没有活动,它会发出超时消息(这种超时是可配置的)。Qt 所要做的,就是把`select()`的返回值转换成一个合适的`QEvent`子类的对象,然后将其放入事件队列。好了,现在你已经知道事件循环的内部机制了。
至于为什么需要事件循环,我们可以简单列出一个清单:
* **组件的绘制与交互**:`QWidget::paintEvent()`会在发出`QPaintEvent`事件时被调用。该事件可以通过内部`QWidget::update()`调用或者窗口管理器(例如显示一个隐藏的窗口)发出。所有交互事件(键盘、鼠标)也是类似的:这些事件都要求有一个事件循环才能发出。
* **定时器**:长话短说,它们会在`select(2)`或其他类似的调用超时时被发出,因此你需要允许 Qt 通过返回事件循环来实现这些调用。
* **网络**:所有低级网络类(`QTcpSocket`、`QUdpSocket`以及`QTcpServer`等)都是异步的。当你调用`read()`函数时,它们仅仅返回已可用的数据;当你调用`write()`函数时,它们仅仅将写入列入计划列表稍后执行。只有返回事件循环的时候,真正的读写才会执行。注意,这些类也有同步函数(以`waitFor`开头的函数),但是它们并不推荐使用,就是因为它们会阻塞事件循环。高级的类,例如`QNetworkAccessManager`则根本不提供同步 API,因此必须要求事件循环。
有了事件循环,你就会想怎样阻塞它。阻塞它的理由可能有很多,例如我就想让`QNetworkAccessManager`同步执行。在解释为什么**永远不要阻塞事件循环**之前,我们要了解究竟什么是“阻塞”。假设我们有一个按钮`Button`,这个按钮在点击时会发出一个信号。这个信号会与一个`Worker`对象连接,这个`Worker`对象会执行很耗时的操作。当点击了按钮之后,我们观察从上到下的函数调用堆栈:
~~~
main(int, char **)
QApplication::exec()
[…]
QWidget::event(QEvent *)
Button::mousePressEvent(QMouseEvent *)
Button::clicked()
[…]
Worker::doWork()
~~~
我们在`main()`函数开始事件循环,也就是常见的`QApplication::exec()`函数。窗口管理器侦测到鼠标点击后,Qt 会发现并将其转换成`QMouseEvent`事件,发送给组件的`event()`函数。这一过程是通过`QApplication::notify()`函数实现的。注意我们的按钮并没有覆盖`event()`函数,因此其父类的实现将被执行,也就是`QWidget::event()`函数。这个函数发现这个事件是一个鼠标点击事件,于是调用了对应的事件处理函数,就是`Button::mousePressEvent()`函数。我们重写了这个函数,发出`Button::clicked()`信号,而正是这个信号会调用`Worker::doWork()`槽函数。有关这一机制我们在前面的事件部分曾有阐述,如果不明白这部分机制,请参考[前面的章节](http://www.devbean.net/2012/10/qt-study-road-2-event-func/)。
在`worker`努力工作的时候,事件循环在干什么?或许你已经猜到了答案:什么都没做!事件循环发出了鼠标按下的事件,然后等着事件处理函数返回。此时,它一直是阻塞的,直到`Worker::doWork()`函数结束。注意,我们使用了“阻塞”一词,也就是说,所谓**阻塞事件循环**,意思是没有事件被派发处理。
在事件就此卡住时,**组件也不会更新自身**(因为`QPaintEvent`对象还在队列中),**也不会有其它什么交互发生**(还是同样的原因),**定时器也不会超时**并且**网络交互会越来越慢直到停止**。也就是说,前面我们大费周折分析的各种依赖事件循环的活动都会停止。这时候,需要窗口管理器会检测到你的应用程序不再处理任何事件,于是**告诉用户你的程序失去响应**。这就是为什么我们需要快速地处理事件,并且尽可能快地返回事件循环。
现在,重点来了:我们不可能避免业务逻辑中的耗时操作,那么怎样做才能既可以执行那些耗时的操作,又不会阻塞事件循环呢?一般会有三种解决方案:第一,我们将任务移到另外的线程(正如我们[上一章](http://www.devbean.net/2013/11/qt-study-road-2-thread-intro/)看到的那样,不过现在我们暂时略过这部分内容);第二,我们手动强制运行事件循环。想要强制运行事件循环,我们需要在耗时的任务中一遍遍地调用`QCoreApplication::processEvents()`函数。`QCoreApplication::processEvents()`函数会发出事件队列中的所有事件,并且立即返回到调用者。仔细想一下,我们在这里所做的,就是模拟了一个事件循环。
另外一种解决方案我们在[前面的章节](http://www.devbean.net/2013/11/qt-study-road-2-access-network-4/)提到过:使用`QEventLoop`类重新进入新的事件循环。通过调用`QEventLoop::exec()`函数,我们重新进入新的事件循环,给`QEventLoop::quit()`槽函数发送信号则退出这个事件循环。拿前面的例子来说:
~~~
QEventLoop eventLoop;
connect(netWorker, &NetWorker::finished,
&eventLoop, &QEventLoop::quit);
QNetworkReply *reply = netWorker->get(url);
replyMap.insert(reply, FetchWeatherInfo);
eventLoop.exec();
~~~
`QNetworkReply`没有提供阻塞式 API,并且要求有一个事件循环。我们通过一个局部的`QEventLoop`来达到这一目的:当网络响应完成时,这个局部的事件循环也会退出。
前面我们也强调过:通过“其它的入口”进入事件循环要特别小心:因为它会导致递归调用!现在我们可以看看为什么会导致递归调用了。回过头来看看按钮的例子。当我们在`Worker::doWork()`槽函数中调用了`QCoreApplication::processEvents()`函数时,用户再次点击按钮,槽函数`Worker::doWork()又`**一次**被调用:
~~~
main(int, char **)
QApplication::exec()
[…]
QWidget::event(QEvent *)
Button::mousePressEvent(QMouseEvent *)
Button::clicked()
[…]
Worker::doWork() // <strong>第一次调用</strong>
QCoreApplication::processEvents() // <strong>手动发出所有事件</strong>
[…]
QWidget::event(QEvent * ) // <strong>用户又点击了一下按钮…</strong>
Button::mousePressEvent(QMouseEvent *)
Button::clicked() // <strong>又发出了信号…</strong>
[…]
Worker::doWork() // <strong>递归进入了槽函数!</strong>
~~~
当然,这种情况也有解决的办法:我们可以在调用`QCoreApplication::processEvents()`函数时传入`QEventLoop::ExcludeUserInputEvents`参数,意思是不要再次派发用户输入事件(这些事件仍旧会保留在事件队列中)。
幸运的是,在**删除事件**(也就是由`QObject::deleteLater()`函数加入到事件队列中的事件)中,**没有**这个问题。这是因为删除事件是由另外的机制处理的。删除事件只有在事件循环有比较小的“嵌套”的情况下才会被处理,而不是调用了`deleteLater()`函数的那个循环。例如:
~~~
QObject *object = new QObject;
object->deleteLater();
QDialog dialog;
dialog.exec();
~~~
这段代码**并不会**造成野指针(注意,`QDialog::exec()`的调用是嵌套在`deleteLater()`调用所在的事件循环之内的)。通过`QEventLoop`进入局部事件循环也是类似的。在 Qt 4.7.3 中,唯一的例外是,在没有事件循环的情况下直接调用`deleteLater()`函数,那么,之后第一个进入的事件循环会获取这个事件,然后直接将这个对象删除。不过这也是合理的,因为 Qt 本来不知道会执行删除操作的那个“外部的”事件循环,所以第一个事件循环就会直接删除对象。
(71)线程简介
最后更新于:2022-04-01 06:30:55
#():线程简介
前面我们讨论了有关进程以及进程间通讯的相关问题,现在我们开始讨论线程。事实上,现代的程序中,使用线程的概率应该大于进程。特别是在多核时代,随着 CPU 主频的提升,受制于发热量的限制,CPU 散热问题已经进入瓶颈,另辟蹊径地提高程序运行效率就是使用线程,充分利用多核的优势。有关线程和进程的区别已经超出了本章的范畴,我们简单提一句,一个进程可以有一个或更多线程同时运行。线程可以看做是“轻量级进程”,进程完全由操作系统管理,线程即可以由操作系统管理,也可以由应用程序管理。
Qt 使用`QThread` 来**管理**线程。下面来看一个简单的例子:
~~~
///!!! Qt5
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
QWidget *widget = new QWidget(this);
QVBoxLayout *layout = new QVBoxLayout;
widget->setLayout(layout);
QLCDNumber *lcdNumber = new QLCDNumber(this);
layout->addWidget(lcdNumber);
QPushButton *button = new QPushButton(tr("Start"), this);
layout->addWidget(button);
setCentralWidget(widget);
QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, [=]() {
static int sec = 0;
lcdNumber->display(QString::number(sec++));
});
WorkerThread *thread = new WorkerThread(this);
connect(button, &QPushButton::clicked, [=]() {
timer->start(1);
for (int i = 0; i < 2000000000; i++);
timer->stop();
});
}
~~~
我们的主界面有一个用于显示时间的 LCD 数字面板还有一个用于启动任务的按钮。程序的目的是用户点击按钮,开始一个非常耗时的运算(程序中我们以一个 2000000000 次的循环来替代这个非常耗时的工作,在真实的程序中,这可能是一个网络访问,可能是需要复制一个很大的文件或者其它任务),同时 LCD 开始显示逝去的毫秒数。毫秒数通过一个计时器`QTimer`进行更新。计算完成后,计时器停止。这是一个很简单的应用,也看不出有任何问题。但是当我们开始运行程序时,问题就来了:点击按钮之后,程序界面直接停止响应,直到循环结束才开始重新更新。
有经验的开发者立即指出,这里需要使用线程。这是因为 Qt 中所有界面都是在 UI 线程中(也被称为主线程,就是执行了`QApplication::exec()`的线程),在这个线程中执行耗时的操作(比如那个循环),就会阻塞 UI 线程,从而让界面停止响应。界面停止响应,用户体验自然不好,不过更严重的是,有些窗口管理程序会检测到你的程序已经失去响应,可能会建议用户强制停止程序,这样一来你的程序可能就此终止,任务再也无法完成。所以,为了避免这一问题,我们要使用 QThread 开启一个新的线程:
~~~
///!!! Qt5
class WorkerThread : public QThread
{
Q_OBJECT
public:
WorkerThread(QObject *parent = 0)
: QThread(parent)
{
}
protected:
void run()
{
for (int i = 0; i < 1000000000; i++);
emit done();
}
signals:
void done();
};
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent)
{
QWidget *widget = new QWidget(this);
QVBoxLayout *layout = new QVBoxLayout;
widget->setLayout(layout);
lcdNumber = new QLCDNumber(this);
layout->addWidget(lcdNumber);
QPushButton *button = new QPushButton(tr("Start"), this);
layout->addWidget(button);
setCentralWidget(widget);
QTimer *timer = new QTimer(this);
connect(timer, &QTimer::timeout, [=]() {
static int sec = 0;
lcdNumber->display(QString::number(sec++));
});
WorkerThread *thread = new WorkerThread(this);
connect(thread, &WorkerThread::done, timer, &QTimer::stop);
connect(thread, &WorkerThread::finished, thread, &WorkerThread::deleteLater);
connect(button, &QPushButton::clicked, [=]() {
timer->start(1);
thread->start();
});
}
~~~
注意,我们增加了一个`WorkerThread`类。`WorkerThread`继承自`QThread`类,重写了其`run()`函数。我们可以认为,`run()`函数就是新的线程需要执行的代码。在这里就是要执行这个循环,然后发出计算完成的信号。而在按钮点击的槽函数中,使用`QThread::start()`函数启动一个线程(注意,这里不是`run()`函数)。再次运行程序,你会发现现在界面已经不会被阻塞了。另外,我们将`WorkerThread::deleteLater()`函数与`WorkerThread::finished()`信号连接起来,当线程完成时,系统可以帮我们清除线程实例。这里的`finished()`信号是系统发出的,与我们自定义的`done()`信号无关。
这是 Qt 线程的最基本的使用方式之一(确切的说,这种使用已经不大推荐使用,不过因为看起来很清晰,而且简单使用起来也没有什么问题,所以还是有必要介绍)。代码看起来很简单,不过,如果你认为 Qt 的多线程编程也很简单,那就大错特错了。Qt 多线程的优势设计使得它使用起来变得容易,但是坑很多,稍不留神就会被绊住,尤其是涉及到与 QObject 交互的情况。稍懂多线程开发的童鞋都会知道,调试多线程开发简直就是煎熬。下面几章,我们会更详细介绍有关多线程编程的相关内容。
(70)进程间通信
最后更新于:2022-04-01 06:30:53
#(70):进程间通信
上一章我们了解了有关进程的基本知识。我们将进程理解为相互独立的正在运行的程序。由于二者是相互独立的,就存在交互的可能性,也就是我们所说的进程间通信(Inter-Process Communication,IPC)。不过也正因此,我们的一些简单的交互方式,比如普通的信号槽机制等,并不适用于进程间的相互通信。我们说过,进程是操作系统的基本调度单元,因此,进程间交互不可避免与操作系统的实现息息相关。
Qt 提供了四种进程间通信的方式:
1. 使用共享内存(shared memory)交互:这是 Qt 提供的一种各个平台均有支持的进程间交互的方式。
2. TCP/IP:其基本思想就是将同一机器上面的两个进程一个当做服务器,一个当做客户端,二者通过网络协议进行交互。除了两个进程是在同一台机器上,这种交互方式与普通的 C/S 程序没有本质区别。Qt 提供了 QNetworkAccessManager 对此进行支持。
3. D-Bus:freedesktop 组织开发的一种低开销、低延迟的 IPC 实现。Qt 提供了 QtDBus 模块,把信号槽机制扩展到进程级别(因此我们前面强调是“普通的”信号槽机制无法实现 IPC),使得开发者可以在一个进程中发出信号,由其它进程的槽函数响应信号。
4. QCOP(Qt COmmunication Protocol):QCOP 是 Qt 内部的一种通信协议,用于不同的客户端之间在同一地址空间内部或者不同的进程之间的通信。目前,这种机制只用于 Qt for Embedded Linux 版本。
从上面的介绍中可以看到,通用的 IPC 实现大致只有共享内存和 TCP/IP 两种。后者我们前面已经大致介绍过(应用程序级别的 QNetworkAccessManager 或者更底层的 QTcpSocket 等);本章我们主要介绍前者。
Qt 使用`QSharedMemory`类操作共享内存段。我们可以把`QSharedMemory`看做一种指针,这种指针指向分配出来的一个共享内存段。而这个共享内存段是由底层的操作系统提供,可以供多个线程或进程使用。因此,`QSharedMemory`可以看做是专供 Qt 程序访问这个共享内存段的指针。同时,`QSharedMemory`还提供了单一线程或进程互斥访问某一内存区域的能力。当我们创建了`QSharedMemory`实例后,可以使用其`create()`函数请求操作系统分配一个共享内存段。如果创建成功(函数返回`true`),Qt 会自动将系统分配的共享内存段连接(attach)到本进程。
前面我们说过,IPC 离不开平台特性。作为 IPC 的实现之一的共享内存也遵循这一原则。有关共享内存段,各个平台的实现也有所不同:
* Windows:`QSharedMemory`不“拥有”共享内存段。当使用了共享内存段的所有线程或进程中的某一个销毁了`QSharedMemory`实例,或者所有的都退出,Windows 内核会自动释放共享内存段。
* Unix:`QSharedMemory`“拥有”共享内存段。当最后一个线程或进程同共享内存分离,并且调用了`QSharedMemory`的析构函数之后,Unix 内核会将共享内存段释放。注意,这里与 Windows 不同之处在于,如果使用了共享内存段的线程或进程没有调用`QSharedMemory`的析构函数,程序将会崩溃。
* HP-UX:每个进程只允许连接到一个共享内存段。这意味着在 HP-UX 平台,`QSharedMemory`不应被多个线程使用。
下面我们通过一段经典的代码来演示共享内存的使用。这段代码修改自 Qt 自带示例程序(注意这里直接使用了 Qt5,Qt4 与此类似,这里不再赘述)。程序有两个按钮,一个按钮用于加载一张图片,然后将该图片放在共享内存段;第二个按钮用于从共享内存段读取该图片并显示出来。
~~~
//!!! Qt5
class QSharedMemory;
class MainWindow : public QMainWindow
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0);
~MainWindow();
private:
QSharedMemory *sharedMemory;
};
~~~
头文件中,我们将`MainWindow`添加一个`sharedMemory`属性。这就是我们的共享内存段。接下来得实现文件中:
~~~
const char *KEY_SHARED_MEMORY = "Shared";
MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent),
sharedMemory(new QSharedMemory(KEY_SHARED_MEMORY, this))
{
QWidget *mainWidget = new QWidget(this);
QVBoxLayout *mainLayout = new QVBoxLayout(mainWidget);
setCentralWidget(mainWidget);
QPushButton *saveButton = new QPushButton(tr("Save"), this);
mainLayout->addWidget(saveButton);
QLabel *picLabel = new QLabel(this);
mainLayout->addWidget(picLabel);
QPushButton *loadButton = new QPushButton(tr("Load"), this);
mainLayout->addWidget(loadButton);
~~~
构造函数初始化列表中我们将`sharedMemory`成员变量进行初始化。注意我们给出一个键(Key),前面说过,我们可以把`QSharedMemory`看做是指向系统共享内存段的指针,而这个键就可以看做指针的名字。多个线程或进程使用同一个共享内存段时,该键值必须相同。接下来是两个按钮和一个标签用于界面显示,这里不再赘述。
下面来看加载图片按钮的实现:
~~~
connect(saveButton, &QPushButton::clicked, [=]() {
if (sharedMemory->isAttached()) {
sharedMemory->detach();
}
QString filename = QFileDialog::getOpenFileName(this);
QPixmap pixmap(filename);
picLabel->setPixmap(pixmap);
QBuffer buffer;
QDataStream out(&buffer);
buffer.open(QBuffer::ReadWrite);
out << pixmap;
int size = buffer.size();
if (!sharedMemory->create(size)) {
qDebug() << tr("Create Error: ") << sharedMemory->errorString();
} else {
sharedMemory->lock();
char *to = static_cast<char *>(sharedMemory->data());
const char *from = buffer.data().constData();
memcpy(to, from, qMin(size, sharedMemory->size()));
sharedMemory->unlock();
}
});
~~~
点击加载按钮之后,如果`sharedMemory`已经与某个线程或进程连接,则将其断开(因为我们就要向共享内存段写入内容了)。然后使用`QFileDialog`选择一张图片,利用`QBuffer`将图片数据作为`char *`格式。在即将写入共享内存之前,我们需要请求系统创建一个共享内存段(`QSharedMemory::create()`函数),创建成功则开始写入共享内存段。需要注意的是,在读取或写入共享内存时,都需要使用`QSharedMemory::lock()`函数对共享内存段加锁。共享内存段就是一段普通内存,所以我们使用 C 语言标准函数`memcpy()`复制内存段。不要忘记之前我们对共享内存段加锁,在最后需要将其解锁。
接下来是加载按钮的代码:
~~~
connect(loadButton, &QPushButton::clicked, [=]() {
if (!sharedMemory->attach()) {
qDebug() << tr("Attach Error: ") << sharedMemory->errorString();
} else {
QBuffer buffer;
QDataStream in(&buffer);
QPixmap pixmap;
sharedMemory->lock();
buffer.setData(static_cast<const char *>(sharedMemory->constData()), sharedMemory->size());
buffer.open(QBuffer::ReadWrite);
in >> pixmap;
sharedMemory->unlock();
sharedMemory->detach();
picLabel->setPixmap(pixmap);
}
});
~~~
如果共享内存段已经连接,还是用`QBuffer`读取二进制数据,然后生成图片。注意我们在操作共享内存段时还是要先加锁再解锁。最后在读取完毕后,将共享内存段断开连接。
注意,如果某个共享内存段不是由 Qt 创建的,我们也是可以在 Qt 应用程序中使用。不过这种情况下我们必须使用`QSharedMemory::setNativeKey()`来设置共享内存段。使用原始键(native key)时,`QSharedMemory::lock()`函数就会失效,我们必须自己保护共享内存段不会在多线程或进程访问时出现问题。
IPC 使用共享内存通信是一个很常用的开发方法。多个进程间得通信要比多线程间得通信少一些,不过在某一族的应用情形下,比如 QQ 与 QQ 音乐、QQ 影音等共享用户头像,还是非常有用的。