Qt Quick实例之挖头像

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系列的文章:

时间: 2024-08-15 10:57:33

Qt Quick实例之挖头像的相关文章

Qt Quick综合实例之文件查看器

如果你基于Qt SDK 5.3.1来创建一个Qt Quick App项目,项目模板为你准备的main.qml文档的根元素是ApplicationWindow或Window.这次我们就以ApplicationWindow为例,围绕着它实现一个综合实例:文件查看器.通过文件查看器的实现,我们来再次领略一下Qt Quick的犀利. 版权所有foruok,转载请注明出处:http://blog.csdn.net/foruok. 本实例将会用到下列特性: ApplicationWindow MenuBar

Qt Quick核心编程从入门到精通

本文是个推荐文章,推荐foruok博主的Qt quick 核心编程的系列经典编程! foruok 博主 的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里的图形效果——渐变(Gradient)

Qt Quick提供了三种渐变图形效果: ConicalGradient,锥形渐变 LinearGradient,线性渐变 RadialGradient,径向渐变 效果 下图是我设计的示例效果: 图 1 渐变图形效果 如图所示,第一行为线性渐变,第二行为锥形渐变,第三行为径向渐变. 渐变元素与其他图形效果元素不同之处在于:渐变元素既可以用来改变一个已有的元素(如Image),也有可以独立使用.如你在示例效果图中看到的那样,每一行前两个是独立使用渐变元素的效果,后两个是讲渐变效果应用到其它元素上的

Qt Quick实现的疯狂算数游戏

使用 Qt Quick 写了个小游戏:疯狂算数.支持 Windows 和 Android 两个平台. 就差您这一票了亲:博客之星评选,点击投我一票,谢谢.投过了也可以点哦,每天都可以投投一票. 游戏简单,但牵涉到下面你的 Qt Quick 主题: 自己实现一个按钮 自适应分辨率 国际化 QML与C++混合编程 APK图标设置 APK名称汉化 动画 其实所有这些内容,在我的书<Qt Quick核心编程>里都讲到了,感兴趣的朋友可以看我的书. 大概来看一下吧,先看效果. Android 手机运行效

Qt Quick里的粒子系统

就差您这一票了亲:博客之星评选,点击投我一票,谢谢.投过了也可以点哦,每天都可以投投一票. Qt Quick提供了一个粒子系统,提供了四种主要的 QML 类型: ParticleSystem ,粒子系统,它维护一个粒子系统相关的 Emitters . Painters . Affectors ,Emitters . Painters . Affectors 要想一起玩儿,就得指定同一个 ParticleSystem. ParticleSystem Painters , 它负责渲染一个粒子.Par

Qt Quick里的图形效果——颜色(Color)

Qt Quick提供了通过改变一个 Item 的颜色来产生各种各样效果的元素.有下面几种: BrightnessContrast,调整亮度和对比度 ColorOverlay,在源 Item 上覆盖一层颜色 Colorize,设置源 Item 的 HSL 颜色空间 Desaturate,降低颜色的饱和度 GammaAdjust,使用 gamma 曲线来改变源 Item 的照度 HueSaturation,在 HSL 颜色空间改变源 Item 的颜色 LevelAdjust,在 RGBA 颜色空间调

Qt Quick里的AnimatedSprite的用法

之前用 AnimatedImage 时一直对 AnimatedSprite 很奇怪,想试一下怎么用,一下子没试出来,放下了,后来一直没时间. OK ,今天想起来,又搞了一下. AnimatedSprite 说明 AnimatedSprite 元素用来播放精灵动画. 一些常见的属性解释: source 属性是 url 类型的,接受一个包含多帧的图片. frameWidth 和 frameHeight 指定帧大小. frameX 和 frameY 指定第一帧的左上角. frameCount 指定这个

Qt Quick之ListView下拉刷新数据

Qt Quick里的ListView,本身是Flickable的派生类,当你用鼠标拖曳或者手指触摸(触摸屏)时,会产生flickStarted和flickEnded两个信号,利用这两个信号,就可以实现下拉刷新数据,当然上拉刷新也是可以的. 创建一个Qt Quick App项目,添加dynamicModel.h和dynamicModel.cpp两个文件,用于实现DynamicListModel.项目创建过程参考<Qt Quick 之 Hello World 图文详解>. 我们实现的下拉刷新效果有

Qt Quick 中的 drag and drop(拖放)

研究了一下 Qt Quick 里的 drag and drop 功能,大概讲一下. 类库 Qt Quick与 drag and drop 相关的,有这么几个类库: DropArea DragEvent Drag DropArea DropArea 其实是个方便类,它不可见,但是定义了一个可以接收拖放的区域.它的 entered 信号在有物体被拖入区域时发射,exited 信号在物体被拖出区域时发射,当物体在区域内被拖着来回移动时会不断发射 positionChanged 信号,当用户释放了物体,