Qt5.8中QML教程并结合实际工程项目详解

Qt是一个非常不错的C++平台,如果想创造出多平台的客户端程序,并且在GUI编程中引入时髦、高效、语法简洁清晰的XML,JS等特性,可以尝试一下Qt。此外Qt的库封装也有点类似JAVA,如果对JAVA语言熟悉,并且希望创造出漂亮的GUI界面,也可以来尝试一下Qt。这篇博客会介绍一下Qt中使用QML来设计GUI界面,以及QML与C++交互的方式。

QML是Qt自定义的一种GUI描述文件,其文档结构有点类似NodeJS或者TypeScript,跟Android编程中的Activity的设计也很类似,在客户端GUI编程的模型中引入了大量的WEB前端设计思想,着实让人惊艳。

在QT5.8中新建一个Qt Quick2 Application即可使用QML来定义GUI:

下面是我一个实际工程的工程截图,以及其中编写的主界面代码:

main.qml:

import QtQuick 2.7
import QtQuick.Controls 2.0

import com.newflypig.Finger 1.0 //来自于C++类finger.h和finger.cpp

ApplicationWindow {
    id:root
    visible: true
    width: 800
    height: 600
    title: qsTr("赛洋面板指纹模块辅助工具")

    property int intPower: 1
    property bool connected: false

    Row {
        spacing: 0
        width: 800
        height: parent.height

        //左部列表 ListView的MVC模型
        Rectangle {
            width: 300; height: parent.height; color: "#4A5459"
            ListView {
                anchors.fill: parent
                model: FingerListModel{id:fingerListModel}  //MVC中的Model层,来自于FingerListModel.qml
                delegate: Rectangle{                        //Rectangle是MVC中的View层描述。其中的MouseArea以及Menu等描述,可理解为Control层
                    id: fingerListDelegate
                    width: parent.width; height: 60
                    color: "#4A5459"
                    Row{
                        leftPadding: 5
                        topPadding: 5
                        spacing: 20
                        Image {
                            width: 40
                            height: 40
                            source: "images/finger2.png"
                        }
                        Text{
                            width: 90
                            anchors.verticalCenter: parent.verticalCenter
                            text: name
                            color: "white"
                            font.pixelSize: 20
                            font.family: "微软雅黑"
                        }
                        Text{color: "white";anchors.verticalCenter: parent.verticalCenter;font.family: "微软雅黑";
                            text: power
                            width:50
                            font.pixelSize: 15
                        }
                        Text{color: "white";anchors.verticalCenter: parent.verticalCenter;font.family: "微软雅黑";
                            text: fid
                            width:20
                            horizontalAlignment: Text.AlignRight
                            font.pixelSize: 15
                        }
                    }
                    //鼠标进入,hover样式改变,右击菜单
                    MouseArea{
                        id:mouseMA;
                        acceptedButtons: Qt.RightButton
                        anchors.fill: parent
                        hoverEnabled: true
                        propagateComposedEvents: true
                        enabled:true
                        onEntered:{
                            fingerListDelegate.color = "#404244"
                        }
                        onExited:{
                            fingerListDelegate.color = "#4A5459"
                        }
                        onClicked: {    //右击菜单
                            contextMenu.x = mouseMA.mouseX;
                            contextMenu.y = mouseMA.mouseY;
                            contextMenu.fid = fid;
                            contextMenu.open();
                        }
                    }
                    Menu { id: contextMenu
                        width: 100
                        property string fid: "0"
                        MenuItem {
                            width: 100
                            text: "删除"
                            font.family: "微软雅黑"
                            font.pixelSize: 15
                            enabled: connected
                            onTriggered: {
                                if(!fingerModual.deleteFingerAddress(parseInt(fid))){
                                    taMessage.append("成功删除 " + fid + " 号位置指纹")
                                    fingerListModel.removeFid(fid)
                                }
                            }
                        }
                    }
                }
            }
        }

        Column {
            width: 500; height: 600;
            leftPadding: 40
            topPadding: 20
            spacing: 10

            TextField {
                id: tfName
                placeholderText: "姓名"
                width: 220
                selectByMouse: true
            }

            //权限按钮
            Row {
                id:powerButtons
                spacing: 5
                PowerButton{id:pButton1; powStr: "1"}   //PowerButton来自于PowerButton.qml是一个个小小的权限按钮,点击后变色,并且全局权限改变
                PowerButton{id:pButton2; powStr: "2"}
                PowerButton{id:pButton3; powStr: "3"}
                PowerButton{id:pButton4; powStr: "4"}
                PowerButton{id:pButton5; powStr: "5"}
                PowerButton{id:pButton6; powStr: "6"}
                PowerButton{id:pButton7; powStr: "7"}
                PowerButton{id:pButton8; powStr: "8"}
                PowerButton{id:pButton9; powStr: "9"}
            }

            //录入按钮
            Row{
                spacing: 30
                Button {
                    id: buttonInput
                    text: "录入指纹"
                    width: 95
                    enabled: connected
                    onClicked: fingerInput();
                }
                Rectangle{
                    id: input1Circle
                    width: 30
                    height: 30
                    color: "white"
                    radius: 15
                    border.color: "#66A334"; border.width: 2
                }

                Rectangle{
                    id: input2Circle
                    width: 30
                    height: 30
                    color: "white"
                    radius: 15
                    border.color: "#66A334"; border.width: 2
                }
            }

            Button{
                text: "验证测试"
                width: 220
                enabled: connected
                onClicked: {
                    taMessage.append("准备验证指纹")
                    taMessage.append("请将手指置于指纹采集器上,否则系统将于20秒后停止采集")

                    searchFingerTimer.start()
                }
                Timer{
                    id: searchFingerTimer
                    interval: 500;
                    running: false;
                    repeat: false;
                    onTriggered: {
                        searchFinger()
                    }
                }
            }

            Row {
                spacing: 5
                Button{
                    text: "备份指纹库"
                    width: 220
                    enabled: connected
                    onClicked: {
                        fingerModual.backupFingerAddress()
                        taMessage.append("成功备份 " + fingerModual.objFingerList.length + "个指纹数据到数据库")
                    }
                }

                Button{
                    text: "还原指纹库"
                    width: 220
                    enabled: connected
                    onClicked: {
                        fingerModual.restoreFingerAddress()
                        updateModel();
                        taMessage.append("成功还原 " + fingerModual.objFingerList.length +" 个指纹数据")
                    }
                }
            }

            Flickable {
                id: flickable
                width: parent.width - 55
                height:parent.height - 230

                TextArea.flickable: TextArea {
                    id: taMessage
                    wrapMode: TextArea.Wrap
                    background: Rectangle{
                        color: "#2E2F30"
                    }
                    readOnly: true
                    color: "#38FF28"
                    selectByMouse: true
                }
                ScrollBar.vertical: ScrollBar { }
            }
        }
    }

    Image {
        id: image
        x: 605
        y: 16
        width: 146
        height: 146
        source: "images/finger3.png"
    }

    //使用children访问子元素,使用for i in list的方式遍历
    function changeAllPowerButtonColor(){
        var list = powerButtons.children;
        for (var i in list){
            list[i].color = "#4A5459";
        }
    }

    //第二次录入指纹的定时器
    Timer{
        id:input2Timer
        interval: 1000
        running: false
        repeat: false
        onTriggered: {
            var result = fingerModual.input2(intPower, tfName.text);
            input2Circle.color = "white"
            if(result >= 0){
                taMessage.append("指纹录入成功,保存于指纹库 " + result + " 号位置")                
                fingerListModel.append({
                    fid: fingerModual.returnFid,
                    name: tfName.text===""?"无名":tfName.text,
                    power: (parseInt(result/100) + 1) + ""
                })
            }else if(result === -2){
                taMessage.append("两次采集的指纹特征差异太大,录入失败")
            }else{
                taMessage.append("采集失败,error code:" + result)
            }
        }
    }

    //第一次录入指纹的定时器。防止UI阻塞可用Timer
    Timer{
        id:input1Timer
        interval: 500
        running: false
        repeat: false
        onTriggered: {
            var result = fingerModual.input1();
            input1Circle.color = "white"
            if(result === 0){
                taMessage.append("第一次采集成功,准备采集第二次,请将手指放在采集器上")
                input2Circle.color = "#66A334"
                input2Timer.start()
            }else{
                taMessage.append("采集失败,error code:" + result)
            }
        }
    }

    //录入指纹
    function fingerInput(){
        taMessage.append("请将手指放在采集器上录入第一次指纹")
        input1Circle.color = "#66A334"
        input1Timer.start()
    }

    //搜索指纹
    function searchFinger(){
        var result = fingerModual.search();
        if(result === -1)
            taMessage.append("采集超时,请重试")
        else if(result === -2)
            taMessage.append("没有匹配到任何指纹,请重试")
        else
            taMessage.append("成功匹配到" + result + "号指纹,姓名:" + fingerModual.searchName)
    }

    Component.onCompleted: {
        var result
        taMessage.append("正在连接指纹模块...")
        if(fingerModual.connect()){
            taMessage.append("连接指纹模块「失败」,目前处于离线状态,相关操作无法使用,请检查连接。")
            connected = false;
        }else{
            taMessage.append("连接指纹模块「成功」")
            result = fingerModual.buildFingerList()
            if(result !== -1){
                taMessage.append("构造指纹库成功,从指纹模块读取 " + result + " 个指纹数据")
                updateModel();
                connected = true;
            } else {
                taMessage.append("构造指纹库失败")
            }
        }
    }

    function updateModel(){
        fingerListModel.clear();
        for(var i = 0; i < fingerModual.objFingerList.length; i++){
            fingerListModel.append({
                fid: fingerModual.objFingerList[i].fid,
                power: fingerModual.objFingerList[i].power,
                name: fingerModual.objFingerList[i].name===""?"无名":fingerModual.objFingerList[i].name
            });
        }
    }
}

整个程序跑起来是这样的:

主界面代码的书写我基本上都写好注释了,可以看到其中有控件的定义,也有function函数的编写,及其类似WEB前端编程。
整个工程涉及了不少知识点:

  • MVC模型的使用
  • 鼠标进入事件及右击菜单
  • 定时器使用
  • QML定义C++类对象及调用其方法

下面就上述四个知识点展开说一说,期间会不断贴出涉及的相关模块的代码,最终理解整个工程及QML编程大致思路。

MVC模型的使用

在界面的左侧区域是一个ListView列表,类似Android编程的ListView或者GridView,就是一个容器空间,里面让你堆放一组数据,以自定义的样式显示出来。ListView通常情况下都会使用MVC结构来设计,渲染方式View和数据模型Model相互隔离,有时候控制逻辑Control比较简单的情况下可以跟V层混合,主界面main.qml中的22-100行代码就是在描述ListView。
可以看到第26行描述了数据模型:model: FingerListModel{id:fingerListModel}
第27-100行是描述的显示渲染和控制逻辑:delegate
下面是FingerListModel的定义:
FingerListModel.qml

import QtQuick 2.0

ListModel {
//    ListElement {
//        fid: "1"
//        power: "1"
//        name: "test"
//    }
    function removeFid(fid){
        for(var i = 0; i < count; i++){
            if(get(i).fid === fid){
                remove(i);
                break;
            }
        }
    }
}

model

可以看到这个数据模型中什么都没有,只有一个测试数据被注释掉了,和一个删除元素的函数。
我们来分析一下,这个FingerListModel.qml是我们自定义的控件,其中只有一个元素就是QML自带的ListModel,这个ListModel中可以放若干ListElement元素,并且自带了若干个函数,比如在我们自定义removeFid函数中需要调用的remove()函数。
程序初始时,左侧列表中元素是动态从指纹设备和数据库加载进来的,因此这个ListModel的初始时是没有元素的。我们在主界面main.qml的302-330行的Component.onCompleted,updateModel()函数就是在做动态加载的操作。

delegate

delegate的英文解释是委托代表,这里理解为每一个子元素的显示方式,可以看到其中定义了一个Row,然后左侧一个图标元素Image,接着是三个文本元素Text,分别显示指纹的姓名、权限和ID,这样的描述是非常简洁易懂的。此外delegate中还使用了MouseArea来设计鼠标hover事件和右键菜单Menu,这些看我的代码和注释应该很容易理解。

鼠标进入事件及右击菜单

上面解释delegate时已经介绍过了,鼠标的事件通过MouseArea来描述,在main.qml的61-97行就是整个鼠标hover样式和右键菜单的设计。这里就不再展开介绍,如果有什么不理解的可以给我留言或者加我微信。

定时器使用

我们知道在WEB前端设计中,有两个定时器神器:setIntervalsetTimeout,在QML中,定时器被封装成了一个元素Timer
main.qml中用到了很多定时器,是用来防止GUI阻塞的,因为后台处理指纹的速度非常慢,用户在界面上一个操作下去会直接导致整个GUI阻塞,并且鼠标呈沙漏状,同时GUI上的消息提示文本框也会阻塞,导致信息提示无法及时更新到界面上,于是我用了Timer来变相的实现了一种多线程的假象。
main.qml的159行:

Button{
    text: "验证测试"
    width: 220
    enabled: connected
    onClicked: {
        taMessage.append("准备验证指纹")
        taMessage.append("请将手指置于指纹采集器上,否则系统将于20秒后停止采集")

        searchFingerTimer.start()
    }
    Timer{
        id: searchFingerTimer
        interval: 500;
        running: false;
        repeat: false;
        onTriggered: {
            searchFinger()
        }
    }
}

这里定义了一个按钮Button和一个定时器Timer,定时器取名id为searchFingerTimer,并且设置了初始运行和重复执行参数均为false。在Buttonclick事件中,我们启动了定时器,定时器500毫秒后会执行searchFinger()事件,这样就避免了searchFinger()事件一下子将GUI卡死的悲剧发生,并且taMessage可以及时做出消息提示,如果不使用定时器,直接在ButtononClicked事件中调用searchFinger()方法,这两行消息提示都会一直等到searchFinger()方法结束后才打印到界面上。

QML定义C++类对象及调用其方法

最后着重讲解一下QML与C++交互的方式,我们的指纹界面会调用大量的后台C++代码,这些C++代码负责与指纹模块硬件设备进行通信,显然这种复杂的通信和函数调用时QML这种界面描述语言无法胜任的,虽然QML可以有一些简单的GUI交互逻辑函数,但大多的网络通信、数据库交互、复杂的算法我们只能使用C++来完成。
首先我们将需要在C++中完成的一系列任务使用OOP的思想,封装成一个类,比如我这里需要将于指纹交互的所有方法设计到一个类中,下面是这个类的实现:
fingermodual.h

#ifndef FINGERMODUAL_H
#define FINGERMODUAL_H

#include <QObject>
#include <QList>
#include <ShlObj.h>

#include "ARITH_LIB.h"
#include "Protocol.h"
#include "finger.h"
#include "daowrap.h"

#ifndef __UCHAR__
#define uchar unsigned char
#endif

#pragma comment(lib,"user32.lib")
#pragma comment(lib,"D:\\workspace-npm\\fingure\\cpp-backup\\ARITH_LIB.lib")
#pragma comment(lib,"D:\\workspace-npm\\fingure\\cpp-backup\\SynoAPIEx.lib")

//QML中使用C++对象和方法
class FingerModual : public QObject
{
    Q_OBJECT
    Q_PROPERTY(QList<QObject*> objFingerList READ objFingerList)
    Q_PROPERTY(QString searchName READ searchName)
    Q_PROPERTY(QString returnFid READ returnFid)
public:
    FingerModual();
    Q_INVOKABLE uchar connect();
    Q_INVOKABLE int   input1();
    Q_INVOKABLE int   input2(int power, QString name);
    Q_INVOKABLE int   buildFingerList();
    Q_INVOKABLE int   search();
    Q_INVOKABLE int   deleteFingerAddress(int);
    Q_INVOKABLE int   backupFingerAddress();
    Q_INVOKABLE int   restoreFingerAddress();

    QString searchName(){return m_searchName;}
    QString returnFid(){return m_returnFid;}

    QList<QObject*> objFingerList(){return this->fingerList;}
signals:

public slots:

private:
    HANDLE pHandle = NULL;
    //QML中使用QList的方法,将原型降级为QObject即可
    QList<QObject*> fingerList;
    QString m_searchName;
    QString m_returnFid;
    DaoWrap* dao;
    int backupFingerAddress(int fid);
    int clear();    //清空指纹模块数据
};

#endif // FINGERMODUAL_H

上面这个头文件,知识点实在太多,如果对C++和Qt一点基础知识都没有的话,理解起来会有一些困难。我这里言简意赅解释一下:
这里定义了一个FingerModual类,继承了Qt中的QObject类,要想在QML使用我们的类,必须将我们的类继承QObject
#pragma comment这个是使用第三方库的语法,我们这里使用了第三方的指纹设备函数库,这个函数库提供了两个lib文件:ARITH_LIB.lib,SynoAPIEx.lib,和两个头文件:ARITH_LIB.h,Protocol.h",这里就不展开讲了。
在这个类中,如果我们定义一些变量,需要让QML访问,则使用这种方式定义:Q_PROPERTY(QString searchName READ searchName),这就定义了一个searchName对象供QML访问,在这里只定义了READ方法,表示对于QML来说,这个变量是只读的,并没有写操作,如果你需要让QML也能设置searchName值,则需要添加上WRITE标志。
同时,如果我们需要定义一些方法,供前端QML调用,并且再来点参数和返回值,那我们可以这么定义:Q_INVOKABLE int input2(int power, QString name)这个很简单,只需要给方法打上Q_INVOKABLE标志就行了,基本类型包括QString,可以直接在QML传入。
public域,我们还需要实现上面Q_PROPERTY(QString searchName READ searchName)所描述的searchName()方法,基本上就是返回真实的private私有变量,这些规范有点类似于JAVA的setter&getter函数的规范,不展开讲。
至此,这么一个类就定义好了,那么如何在QML使用呢:
QML工程的入口函数main.cpp

#include <QGuiApplication>
#include <QQmlApplicationEngine>
#include <QIcon>
#include <QQmlContext>

#include "fingermodual.h"
#include "finger.h"

int main(int argc, char *argv[])
{
    QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);
    QGuiApplication app(argc, argv);
    app.setWindowIcon(QIcon(":/images/finger.png"));


    qmlRegisterType<Finger>("com.newflypig.Finger", 1, 0, "Finger");
    qmlRegisterType<FingerModual>("com.newflypig.FingerModual", 1, 0, "FingerModual");

    QQmlApplicationEngine engine;
    FingerModual fingerModual;
    engine.rootContext()->setContextProperty("fingerModual", &fingerModual);

    engine.load(QUrl(QLatin1String("qrc:/main.qml")));

    return app.exec();
}

观察第17和21行
17行首先通过注册函数qmlRegisterType()将我们写好的FingerModual类注册给qml,注册的名称,大版本,小版本这些都可以自由设置
21行通过engine.rootContext()->setContextProperty()将一个已经构造好的fingerModual对象注入给了QML engine的上下文,这样,我们在main.qml中就可以爽快的直接使用fingerModual变量了,参考main.qml第一次出现fingerModual的91行,是不是直接就用,不需要定义和构造了,这一点倒是突然让我想到是不是有点类似Java中的Spring注入。

至此整个工程就差不多梳理好了,四个知识点也讲得差不多了。如果你正在学习Qt和QML,希望这篇文章能帮到你。
这个工程密级不高,我开源到GitHub上了,如果有需要的朋友可以去下载参考:https://github.com/newflydd/nodejs-fingure/tree/master/QtquickCustomerApp
有什么问题欢迎联系我,邮箱:newflydd@gmail.com,微信:168138332

丁丁生于 1987.07.01 ,30岁,英文ID:newflydd

  • 现居住地 江苏 ● 泰州 ● 姜堰
  • 创建了 Jblog 开源博客系统
  • 坚持十余年的 独立博客 作者
  • 大学本科毕业后就职于 中国电信江苏泰州分公司,前两年从事Oracle数据库DBA工作,两年后公司精简技术人员,被安排到农村担任支局长(其本质是搞销售),于2016年因志向不合从国企辞职,在小城镇找了一份程序员的工作。
  • Git OSChina 上积极参与开源社区