Android手机有个挺好的功能,它允许你往桌面上放窗口小部件(widget),有一个叫相框的小部件,可以让你选择一张相片,截取一部分,放在相框里。我桌面上就放了几个相框,里面是我女儿的照片,隔阵子换一换,挺喜欢。这次的实例受相框小部件启发而成,我称之为挖头像,先看看运行效果。
运行效果
电脑上的运行效果如图1:
图1 电脑挖头像效果图
Android手机上运行效果如图2:
项目创建
项目创建过程参考《Qt Quick 之 Hello World 图文详解》,安卓配置参考《Windows下Qt 5.2 for Android开发入门》和《Qt on Android:图文详解Hello World全过程》。
项目名称是PickThumb,Android包名是an.qt.PickThumb,其它的木啥咧。
源码分析
C++代码
为了能够让PickThumb正常退出,我给QGuiApplication安装了事件过滤器,过滤BACK按键。下面是main.cpp文件:
#include <QGuiApplication> #include <QQmlApplicationEngine> #include <QKeyEvent> class KeyBackQuit: public QObject { public: KeyBackQuit(QObject *parent = 0) : QObject(parent) {} bool eventFilter(QObject *watched, QEvent * e) { switch(e->type()) { case QEvent::KeyPress: if( ((QKeyEvent*)e)->key() == Qt::Key_Back ) { e->accept(); return true; } break; case QEvent::KeyRelease: if( ((QKeyEvent*)e)->key() == Qt::Key_Back ) { e->accept(); qApp->quit(); return true; } break; default: break; } return QObject::eventFilter(watched, e); } }; int main(int argc, char *argv[]) { QGuiApplication app(argc, argv); app.installEventFilter(new KeyBackQuit); QQmlApplicationEngine engine; engine.load(QUrl(QStringLiteral("qrc:///main.qml"))); return app.exec(); }
KeyBackQuit类重写eventFilter()方法来过滤Key_Back按键,调用QCoreApplication的quit()方法退出应用。过滤器在main()函数中被安装到QGuiApplication实例上。
QML代码分析
该主角登场了,main.qml文件有200多行代码,内容如下:
import QtQuick 2.2 import QtQuick.Window 2.1 import QtQuick.Controls 1.2 import QtQuick.Controls.Styles 1.2 import QtQuick.Dialogs 1.1 Window { visible: true width: 480; height: 320; minimumHeight: 320; minimumWidth: 480; color: "black"; onWidthChanged: mask.recalc(); onHeightChanged: mask.recalc(); Image { id: source; anchors.fill: parent; fillMode: Image.PreserveAspectFit; visible: false; asynchronous: true; onStatusChanged: { if(status == Image.Ready){ console.log("image loaded"); mask.recalc(); } } } FileDialog { id: fileDialog; title: "Please choose an Image File"; nameFilters: ["Image Files (*.jpg *.png *.gif)"]; onAccepted: { source.source = fileDialog.fileUrl; } } Canvas { id: forSaveCanvas; width: 128; height: 128; contextType: "2d"; visible: false; z: 2; anchors.top: parent.top; anchors.right: parent.right; anchors.margins: 4; property var imageData: null; onPaint: { if(imageData != null){ context.drawImage(imageData, 0, 0); } } function setImageData(data){ imageData = data; requestPaint(); } } Canvas { id: mask; anchors.fill: parent; z: 1; property real w: width; property real h: height; property real dx: 0; property real dy: 0; property real dw: 0; property real dh: 0; property real frameX: 66; property real frameY: 66; function calc(){ var sw = source.sourceSize.width; var sh = source.sourceSize.height; if(sw > 0 && sh > 0){ if(sw <= w && sh <=h){ dw = sw; dh = sh; }else{ var sRatio = sw / sh; dw = sRatio * h; if(dw > w){ dh = w / sRatio; dw = w; }else{ dh = h; } } dx = (w - dw)/2; dy = (h - dh)/2; } } function recalc(){ calc(); requestPaint(); } function getImageData(){ return context.getImageData(frameX - 64, frameY - 64, 128, 128); } onPaint: { var ctx = getContext("2d"); if(dw < 1 || dh < 1) { ctx.fillStyle = "#0000a0"; ctx.font = "20pt sans-serif"; ctx.textAlign = "center"; ctx.fillText("Please Choose An Image File", width/2, height/2); return; } ctx.clearRect(0, 0, width, height); ctx.drawImage(source, dx, dy, dw, dh); var xStart = frameX - 66; var yStart = frameY - 66; ctx.save(); ctx.fillStyle = "#a0000000"; ctx.fillRect(0, 0, w, yStart); var yOffset = yStart + 132; ctx.fillRect(0, yOffset, w, h - yOffset); ctx.fillRect(0, yStart, xStart, 132); var xOffset = xStart + 132; ctx.fillRect(xOffset, yStart, w - xOffset, 132); //see through area ctx.strokeStyle = "red"; ctx.fillStyle = "#00000000"; ctx.lineWidth = 2; ctx.beginPath(); ctx.rect(xStart, yStart, 132, 132); ctx.fill(); ctx.stroke(); ctx.closePath (); ctx.restore(); } } MultiPointTouchArea { anchors.fill: parent; minimumTouchPoints: 1; maximumTouchPoints: 1; touchPoints:[ TouchPoint{ id: point1; } ] onUpdated: { mask.frameX = point1.x; mask.frameY = point1.y; mask.requestPaint(); } onReleased: { forSaveCanvas.setImageData(mask.getImageData()); actionPanel.visible = true; } onPressed: { actionPanel.visible = false; } } Component { id: flatButton; ButtonStyle { background: Rectangle{ implicitWidth: 70; implicitHeight: 30; border.width: control.hovered ? 2: 1; border.color: control.hovered ? "#c0c0c0" : "#909090"; color: control.pressed ? "#a0a0a0" : "#707070"; } label: Text { anchors.fill: parent; font.pointSize: 12; horizontalAlignment: Text.AlignHCenter; verticalAlignment: Text.AlignVCenter; text: control.text; color: (control.hovered && !control.pressed) ? "blue": "white"; } } } Row { anchors.horizontalCenter: parent.horizontalCenter; anchors.bottom: parent.bottom; anchors.bottomMargin: 20; id: actionPanel; z: 5; spacing: 8; Button { style: flatButton; text: "Open"; onClicked: fileDialog.open(); } Button { style: flatButton; text: "Save"; onClicked: { forSaveCanvas.save("selected.png"); actionPanel.visible = false; } } Button { style: flatButton; text: "Cancel"; onClicked: actionPanel.visible = false; } } }
代码的逻辑是这样的:点击“Open”按钮打开一个对话框,用户选择一张图片,使用隐藏的Image对象加载,加载成功后触发Canvas对象绘制图片;当用户用手指(或按下鼠标左键)拖动时,选中框中心跟随手指移动,框内图像是正常亮度;当用户抬起手指后,弹出操作菜单,如选择“Save”,则通过一个隐藏的Canvas把选中区域的图像保存到文件中。
QML中用到的Row、Button、ButtonStyle、Component、Image、FileDialog等我都有文章讲过,参考我的专栏《Qt Quick简明教程》;MultiPointTouchArea和Canvas没讲过,参考Qt帮助吧。这里咱单说“整个照片变暗而唯有选中框内正常显示”这种效果的实现。
我定义了一个id为mask的Canvas,它使用id为source的Image对象绘制图片。图片在最底层绘制,然后在它上面绘制使用透明色填充的矩形,于是图片就变暗了。整个Canvas被分成一个“回”字形,中间是完全透明的矩形,周围是半透明的。半透明部门由顶部、底部、左面、右面四个矩形组成,分别填充即可。
图片是按比例显示的,等图片加载成功后,先计算了绘制时需要的目标矩形,绘制时直接引用,避免重复计算。而桌面版本为了适应窗口大小变化,实现了onWidthChanged和onHeightChanged两个信号处理器来更新绘制参数。
当用户选择保存时,把mask的透明区域内的像素挖出来(getImageData),生成一个CanvasImageData对象,交给另一个Canvas对象去显示,调用它的save()方法把内容写入到文件。
这就是全部了。
回顾我Qt Quick系列的文章:
- Qt Quick 简介
- QML 语言基础
- Qt Quick 之 Hello World 图文详解
- Qt Quick 简单教程
- Qt Quick 事件处理之信号与槽
- Qt Quick事件处理之鼠标、键盘、定时器
- Qt Quick 事件处理之捏拉缩放与旋转
- Qt Quick 组件与对象动态创建详解
- Qt Quick 布局介绍
- Qt Quick 之 QML 与 C++ 混合编程详解
- Qt Quick 图像处理实例之美图秀秀(附源码下载)
- Qt Quick 之 PathView 详解