编辑推荐: |
本文来自于丁丁的博客,本文主要讲解整个工程的四个知识点展开,期间会不断贴出涉及的相关模块的代码,最终理解整个工程及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前端设计中,有两个定时器神器: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注入。
|