| 
                             
                            | 
								
										
                                    | 编辑推荐: |  
										
                                    | 本文来自于丁丁的博客,本文主要讲解整个工程的四个知识点展开,期间会不断贴出涉及的相关模块的代码,最终理解整个工程及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: 1property 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阻塞可用TimerTimer{
 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前端设计中,有两个定时器神器:setInterval和setTimeout,在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。在Button的click事件中,我们启动了定时器,定时器500毫秒后会执行searchFinger()事件,这样就避免了searchFinger()事件一下子将GUI卡死的悲剧发生,并且taMessage可以及时做出消息提示,如果不使用定时器,直接在Button的onClicked事件中调用searchFinger()方法,这两行消息提示都会一直等到searchFinger()方法结束后才打印到界面上。 QML定义C++类对象及调用其方法 
                            最后着重讲解一下QML与C++交互的方式,我们的指纹界面会调用大量的后台C++代码,这些C++代码负责与指纹模块硬件设备进行通信,显然这种复杂的通信和函数调用时QML这种界面描述语言无法胜任的,虽然QML可以有一些简单的GUI交互逻辑函数,但大多的网络通信、数据库交互、复杂的算法我们只能使用C++来完成。 
                            首先我们将需要在C++中完成的一系列任务使用OOP的思想,封装成一个类,比如我这里需要将于指纹交互的所有方法设计到一个类中,下面是这个类的实现: 
                             
                              | #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_EnableHigh
 DpiScaling);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注入。 
                           |