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:

下面是我一個實際工程的工程截圖,以及其中編寫的主界面代碼:


import QtQuick 2.7nimport QtQuick.Controls 2.0nnimport com.newflypig.Finger 1.0 //來自於C++類finger.h和finger.cppnnApplicationWindow {n id:rootn visible: truen width: 800n height: 600n title: qsTr("賽洋面板指紋模塊輔助工具")nn property int intPower: 1n property bool connected: falsenn Row {n spacing: 0n width: 800n height: parent.heightnn //左部列表 ListView的MVC模型n Rectangle {n width: 300; height: parent.height; color: "#4A5459"n ListView {n anchors.fill: parentn model: FingerListModel{id:fingerListModel} //MVC中的Model層,來自於FingerListModel.qmln delegate: Rectangle{ //Rectangle是MVC中的View層描述。其中的MouseArea以及Menu等描述,可理解為Control層n id: fingerListDelegaten width: parent.width; height: 60n color: "#4A5459"n Row{n leftPadding: 5n topPadding: 5n spacing: 20n Image {n width: 40n height: 40n source: "images/finger2.png"n }n Text{n width: 90n anchors.verticalCenter: parent.verticalCentern text: namen color: "white"n font.pixelSize: 20n font.family: "微軟雅黑"n }n Text{color: "white";anchors.verticalCenter: parent.verticalCenter;font.family: "微軟雅黑";n text: powern width:50n font.pixelSize: 15n }n Text{color: "white";anchors.verticalCenter: parent.verticalCenter;font.family: "微軟雅黑";n text: fidn width:20n horizontalAlignment: Text.AlignRightn font.pixelSize: 15n }n }n //滑鼠進入,hover樣式改變,右擊菜單n MouseArea{n id:mouseMA;n acceptedButtons: Qt.RightButtonn anchors.fill: parentn hoverEnabled: truen propagateComposedEvents: truen enabled:truen onEntered:{n fingerListDelegate.color = "#404244"n }n onExited:{n fingerListDelegate.color = "#4A5459"n }n onClicked: { //右擊菜單n contextMenu.x = mouseMA.mouseX;n contextMenu.y = mouseMA.mouseY;n contextMenu.fid = fid;n contextMenu.open();n }n }n Menu { id: contextMenun width: 100n property string fid: "0"n MenuItem {n width: 100n text: "刪除"n font.family: "微軟雅黑"n font.pixelSize: 15n enabled: connectedn onTriggered: {n if(!fingerModual.deleteFingerAddress(parseInt(fid))){n taMessage.append("成功刪除 " + fid + " 號位置指紋")n fingerListModel.removeFid(fid)n }n }n }n }n }n }n }nn Column {n width: 500; height: 600;n leftPadding: 40n topPadding: 20n spacing: 10nn TextField {n id: tfNamen placeholderText: "姓名"n width: 220n selectByMouse: truen }nn //許可權按鈕n Row {n id:powerButtonsn spacing: 5n PowerButton{id:pButton1; powStr: "1"} //PowerButton來自於PowerButton.qml是一個個小小的許可權按鈕,點擊後變色,並且全局許可權改變n PowerButton{id:pButton2; powStr: "2"}n PowerButton{id:pButton3; powStr: "3"}n PowerButton{id:pButton4; powStr: "4"}n PowerButton{id:pButton5; powStr: "5"}n PowerButton{id:pButton6; powStr: "6"}n PowerButton{id:pButton7; powStr: "7"}n PowerButton{id:pButton8; powStr: "8"}n PowerButton{id:pButton9; powStr: "9"}n }nn //錄入按鈕n Row{n spacing: 30n Button {n id: buttonInputn text: "錄入指紋"n width: 95n enabled: connectedn onClicked: fingerInput();n }n Rectangle{n id: input1Circlen width: 30n height: 30n color: "white"n radius: 15n border.color: "#66A334"; border.width: 2n }nn Rectangle{n id: input2Circlen width: 30n height: 30n color: "white"n radius: 15n border.color: "#66A334"; border.width: 2n }n }nn Button{n text: "驗證測試"n width: 220n enabled: connectedn onClicked: {n taMessage.append("準備驗證指紋")n taMessage.append("請將手指置於指紋採集器上,否則系統將於20秒後停止採集")nn searchFingerTimer.start()n }n Timer{n id: searchFingerTimern interval: 500;n running: false;n repeat: false;n onTriggered: {n searchFinger()n }n }n }nn Row {n spacing: 5n Button{n text: "備份指紋庫"n width: 220n enabled: connectedn onClicked: {n fingerModual.backupFingerAddress()n taMessage.append("成功備份 " + fingerModual.objFingerList.length + "個指紋數據到資料庫")n }n }nn Button{n text: "還原指紋庫"n width: 220n enabled: connectedn onClicked: {n fingerModual.restoreFingerAddress()n updateModel();n taMessage.append("成功還原 " + fingerModual.objFingerList.length +" 個指紋數據")n }n }n }nn Flickable {n id: flickablen width: parent.width - 55n height:parent.height - 230nn TextArea.flickable: TextArea {n id: taMessagen wrapMode: TextArea.Wrapn background: Rectangle{n color: "#2E2F30"n }n readOnly: truen color: "#38FF28"n selectByMouse: truen }n ScrollBar.vertical: ScrollBar { }n }n }n }nn Image {n id: imagen x: 605n y: 16n width: 146n height: 146n source: "images/finger3.png"n }nn //使用children訪問子元素,使用for i in list的方式遍歷n function changeAllPowerButtonColor(){n var list = powerButtons.children;n for (var i in list){n list[i].color = "#4A5459";n }n }nn //第二次錄入指紋的定時器n Timer{n id:input2Timern interval: 1000n running: falsen repeat: falsen onTriggered: {n var result = fingerModual.input2(intPower, tfName.text);n input2Circle.color = "white"n if(result >= 0){n taMessage.append("指紋錄入成功,保存於指紋庫 " + result + " 號位置") n fingerListModel.append({n fid: fingerModual.returnFid,n name: tfName.text===""?"無名":tfName.text,n power: (parseInt(result/100) + 1) + ""n })n }else if(result === -2){n taMessage.append("兩次採集的指紋特徵差異太大,錄入失敗")n }else{n taMessage.append("採集失敗,error code:" + result)n }n }n }nn //第一次錄入指紋的定時器。防止UI阻塞可用Timern Timer{n id:input1Timern interval: 500n running: falsen repeat: falsen onTriggered: {n var result = fingerModual.input1();n input1Circle.color = "white"n if(result === 0){n taMessage.append("第一次採集成功,準備採集第二次,請將手指放在採集器上")n input2Circle.color = "#66A334"n input2Timer.start()n }else{n taMessage.append("採集失敗,error code:" + result)n }n }n }nn //錄入指紋n function fingerInput(){n taMessage.append("請將手指放在採集器上錄入第一次指紋")n input1Circle.color = "#66A334"n input1Timer.start()n }nn //搜索指紋n function searchFinger(){n var result = fingerModual.search();n if(result === -1)n taMessage.append("採集超時,請重試")n else if(result === -2)n taMessage.append("沒有匹配到任何指紋,請重試")n elsen taMessage.append("成功匹配到" + result + "號指紋,姓名:" + fingerModual.searchName)n }nn Component.onCompleted: {n var resultn taMessage.append("正在連接指紋模塊...")n if(fingerModual.connect()){n taMessage.append("連接指紋模塊「失敗」,目前處於離線狀態,相關操作無法使用,請檢查連接。")n connected = false;n }else{n taMessage.append("連接指紋模塊「成功」")n result = fingerModual.buildFingerList()n if(result !== -1){n taMessage.append("構造指紋庫成功,從指紋模塊讀取 " + result + " 個指紋數據")n updateModel();n connected = true;n } else {n taMessage.append("構造指紋庫失敗")n }n }n }nn function updateModel(){n fingerListModel.clear();n for(var i = 0; i < fingerModual.objFingerList.length; i++){n fingerListModel.append({n fid: fingerModual.objFingerList[i].fid,n power: fingerModual.objFingerList[i].power,n name: fingerModual.objFingerList[i].name===""?"無名":fingerModual.objFingerList[i].namen });n }n }n}n
主界面代碼的書寫我基本上都寫好注釋了,可以看到其中有控制項的定義,也有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.qmlimport QtQuick 2.0nnListModel {n// ListElement {n// fid: "1"n// power: "1"n// name: "test"n// }n function removeFid(fid){n for(var i = 0; i < count; i++){n if(get(i).fid === fid){n remove(i);n break;n }n }n }n}n
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{n text: "驗證測試"n width: 220n enabled: connectedn onClicked: {n taMessage.append("準備驗證指紋")n taMessage.append("請將手指置於指紋採集器上,否則系統將於20秒後停止採集")nn searchFingerTimer.start()n }n Timer{n id: searchFingerTimern interval: 500;n running: false;n repeat: false;n onTriggered: {n searchFinger()n }n }n}n
這裡定義了一個按鈕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的思想,封裝成一個類,比如我這裡需要將於指紋交互的所有方法設計到一個類中,下面是這個類的實現:fingermodual.h:#ifndef FINGERMODUAL_Hn#define FINGERMODUAL_Hnn#include <QObject>n#include <QList>n#include <ShlObj.h>nn#include "ARITH_LIB.h"n#include "Protocol.h"n#include "finger.h"n#include "daowrap.h"nn#ifndef __UCHAR__n#define uchar unsigned charn#endifnn#pragma comment(lib,"user32.lib")n#pragma comment(lib,"D:workspace-npmfingurecpp-backupARITH_LIB.lib")n#pragma comment(lib,"D:workspace-npmfingurecpp-backupSynoAPIEx.lib")nn//QML中使用C++對象和方法nclass FingerModual : public QObjectn{n Q_OBJECTn Q_PROPERTY(QList<QObject*> objFingerList READ objFingerList)n Q_PROPERTY(QString searchName READ searchName)n Q_PROPERTY(QString returnFid READ returnFid)npublic:n FingerModual();n Q_INVOKABLE uchar connect();n Q_INVOKABLE int input1();n Q_INVOKABLE int input2(int power, QString name);n Q_INVOKABLE int buildFingerList();n Q_INVOKABLE int search();n Q_INVOKABLE int deleteFingerAddress(int);n Q_INVOKABLE int backupFingerAddress();n Q_INVOKABLE int restoreFingerAddress();nn QString searchName(){return m_searchName;}n QString returnFid(){return m_returnFid;}nn QList<QObject*> objFingerList(){return this->fingerList;}nsignals:nnpublic slots:nnprivate:n HANDLE pHandle = NULL;n //QML中使用QList的方法,將原型降級為QObject即可n QList<QObject*> fingerList;n QString m_searchName;n QString m_returnFid;n DaoWrap* dao;n int backupFingerAddress(int fid);n int clear(); //清空指紋模塊數據n};nn#endif // FINGERMODUAL_Hn
上面這個頭文件,知識點實在太多,如果對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>n#include <QQmlApplicationEngine>n#include <QIcon>n#include <QQmlContext>nn#include "fingermodual.h"n#include "finger.h"nnint main(int argc, char *argv[])n{n QCoreApplication::setAttribute(Qt::AA_EnableHighDpiScaling);n QGuiApplication app(argc, argv);n app.setWindowIcon(QIcon(":/images/finger.png"));nnn qmlRegisterType<Finger>("com.newflypig.Finger", 1, 0, "Finger");n qmlRegisterType<FingerModual>("com.newflypig.FingerModual", 1, 0, "FingerModual");nn QQmlApplicationEngine engine;n FingerModual fingerModual;n engine.rootContext()->setContextProperty("fingerModual", &fingerModual);nn engine.load(QUrl(QLatin1String("qrc:/main.qml")));nn return app.exec();n}n
觀察第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上了,如果有需要的朋友可以去下載參考:newflydd/nodejs-fingure有什麼問題歡迎聯繫我,郵箱:[email protected],微信:168138332本文原創博客載於:Qt5.8中QML教程並結合實際工程項目詳解-丁丁的博客
推薦閱讀:
※《C++ Primer》讀書筆記-第十二章 03 使用標準庫 part1
※【源碼眾讀】之每日問答,Day1
※基於現代C++的四則運算語法分析器(上)
※【源碼眾讀】之問題解答,Part_8
