使用QML进行界面开发

 2018-1-18     作者:廖光泽     [nemail]    
[lablebox]

1、概述


  ESM6802是英创公司推出的基于Freescale i.MX6DL双核处理器(ARM Cortex-A9,主频1GHz)的高性能工控主板,imx6dl内部带有硬件3D、2D图像处理模块,能够对图像界面绘制进行硬件加速,使得应用程序显示更加快速流畅。ESM6802支持Qt 5.8版本,支持使用QML进行界面开发,而且能够利用硬件图像加速处理模块优化QML构建的图像界面的渲染。本文将会简单介绍一下QML的基础知识,给客户提供一种新的界面的开发方案。


  QML是Qt提供的一种描述性的脚本语言,类似于CSS(Cascading Style Sheets),可以在脚本里创建图形对象,并且支持各种图形特效,以及状态机等,同时又能跟Qt写的C++代码进行方便的交互,使用起来非常方便。采用QML加插件的方式主要是为了将界面设计与程序逻辑解耦,一般的系统开发中界面设计的变动往往多于后台逻辑,因此采用QML加插件的方式将界面设计与逻辑分离有利于开发人员的分工,加速产品迭代速度,降低后期维护成本。而且QML解释性语言的特性使得其语法更加简单,可以将界面设计部分交给专业的设计人员开发,而不要求设计人员会c++等编程语言。Qt底层对QML做了优化,将会优先使用硬件图形加速器进行界面的渲染,也针对触摸屏应用做了优化,使用QML能够更简单快捷的搭建流畅、优美的界面。QML也支持嵌入Javascript处理逻辑,但是底层逻辑处理使用Qt C++编写插件,能够更好的控制数据结构,数据处理也更加高效,Qt提供了多种方式将C++数据类型导入QML脚本中,更多详细资料可以查看Qt官方的文档,本文将简单介绍一下QML语言的语法以展示QML相对于以往的Qt C++界面编程的区别,文中所有代码均在ESM6802 Linux上运行测试过了,有需要的客户可以向我们索取,QML文件是脚本文件,不需要编译,可以使用/usr/bin/qt5/qmlscene程序直接打开QML脚本文件查看效果。


2、QML基础介绍


  QML将界面分解为一个一个小的元素,通过使用QML描述各元素的排列以及对特定事件的响应来搭建一个动态的界面。QML中的元素是以层级的形式进行描述的,子元素继承父元素的坐标系统,子元素的坐标以父元素作为参考,父元素的左上角为子元素的坐标原点,子元素中可以以parent关键字引用父元素。


  在一个QML文件中,每个元素都可以设定唯一的id,在其他元素中可以引用id来更改此元素的属性等。QML提供一系列内置的元素类型供开发中快速搭建界面,包括最常用的Rectangle、Image、Text、MouseArea、Item等。元素都有自己内置的属性,比如之前介绍的id,以及用于指定坐标的x、y,和width、height等,同时也支持使用property关键字自定义属性。一个简单的QML文件如下:



import QtQuick 2.0


Rectangle {

    width: 100

    height: 100

    color: "red"

}



  这个QML文件将会展示一个红色的正方形,如下图:


使用QML进行界面开发.gif


  在上面的例子中为Rectangle增加radius属性的设置就可以得到圆角长方形:



Rectangle {

    width: 100

    height: 100

    color: "red"

    radius: 10

}



使用QML进行界面开发.gif


  使用Rectangle就可以构建出消息展示框和按钮等大部分的界面元素了,而Text类型可以用于在Rectangle中增加文字信息,Image可以加载图片,MouseArea提供鼠标/触摸屏事件,组合使用这几个元素就能够快速的搭建基本的交互界面了。


  当然现实开发中由于需要展示的数据往往会以数组等更复杂形式进行管理,他们具有相同的属性,需要展示的外形效果(背景、对事件的相应)是一样的,而每个元素的需要展示的内容不一样,这时就可以使用Row、Column、ListView、GridView等更复杂的元素。这类元素的设计理念是将数据与展现效果分开,数据用model来存放,而展示效果用view来描述,model和view通过delegate联系起来,一个简单的ListView的用法示例如下:



Background {

  width: 480

  height: 80

  ListView {

  anchors.fill: parent

  anchors.margins: 20

  spacing: 4

  clip: true

  model: 100

  orientation: ListView.Horizontal

  delegate: numberDelegate

}

Component {

  id: numberDelegate

  GreenBox {

    width: 40

    height: 40

    text: index

  }

}



  上面中的GreenBox是使用Rectangle元素和Text元素构建的长方形,上面的代码显示效果如下:


使用QML进行界面开发.gif


  QML也内置了一些类型来描述显示元素的转变、动画效果,例如PropertyAnimation、NumberAnimation、ColorAnimation、RotationAnimation以及State、Transition等,使用这些类型能够快速实现界面的动画效果,比如下面展示一个绿灯闪烁的界面的QML代码:



import QtQuick 2.0


Rectangle {

    id: backgroud

    width: 100

    height: 100

    color: "grey"


  Rectangle {

    id: greenlight

    width: 60

    height: 60

    x: 20

    y: 20

    color: "green"

    radius: 30

    Component.onCompleted: flick.start()


    SequentialAnimation{

      id: flick

      ColorAnimation { target: greenlight; properties: "color"; to: "black"; duration: 1000 }

      ColorAnimation { target: greenlight; properties: "color"; to: "green"; duration: 1000 }

      ColorAnimation { target: greenlight; properties: "color"; to: "black"; duration: 1000 }

      ColorAnimation { target: greenlight; properties: "color"; to: "green"; duration: 1000 }

      ColorAnimation { target: greenlight; properties: "color"; to: "black"; duration: 1000 }

      }

  }

}



  上面的代码会使id为greenlight的图形从绿色变为黑色,在由黑色变为绿色,重复三次,模拟绿灯闪烁的效果。由于动画效果不方便以图片展示,客户可以自行复制上面的代码运行查看效果。


使用QML进行界面开发.gif


  从上面的介绍可以看到,QML的语法非常简单,整个文件的结构很清楚,比起使用Qt C++开发界面的更加简单快速,不需要管理各种不同的类(class),降低了编程难度,而且整个界面元素以长方形为基础来搭建,非常适合触摸屏设备。另外QML文件不需要编译,可以直接运行,搭建的界面能够快速的预览然后进行优化,也一定程度上加快了开发速度。当然由于QML的设计理念,QML只是对界面显示进行描述,虽然可以内嵌JavaScript处理数据,但是处理数据的效率不高,所以显示所需数据的生成、管理还是建议使用Qt C++进行处理。这样底层数据能够高效的进行处理,而上层界面又能够快速搭建,数据处理和界面构建分离,程序开发分工也更加明确,也更加方便程序的维护和更新换代。


3、使用C++扩展QML


  Qt提供了C++的函数接口来方便的将C++程序中的数据提供给QML文件进行显示,这些接口可以分为两种,一种是直接在C++中注册QML类型,然后在C++中执行QML文件,这样QML文件中就可以直接访问注册的类型;另一种是创建QML插件,之后可以在QML文件中引入插件,作为独立的类型进行实例化。限于篇幅,我们这里只简单介绍一下创建QML插件,QML文件代码基于我们之前一片文章《Linux双进程应用示例》中介绍的,插件的功能我们不再具体介绍,只在此使用代码进行展示。


  使用qtcreator的指导界面创建QML插件,设置插件名称为com.emtronix.qmlcomponents,在QML中通过语句import com.emtronix.qmlcomponents加载插件,插件编译过后以共享库的形式存在,需要放到/usr/lib/qt5/qml/com/emtronix/目录下:


使用QML进行界面开发.gif


使用QML进行界面开发.gif


  插件使用Qt C++进行开发,实现插件的C++类需要继承QQmlExtensionPlugin类,并且实现registerTypes()函数,如下:



#include <QQmlExtensionPlugin>

class MsgClientPlugin : public QQmlExtensionPlugin

{

    Q_OBJECT

    Q_PLUGIN_METADATA(IID QQmlExtensionInterface_iid)

public:

    void registerTypes(const char *uri);

};

void MsgClientPlugin::registerTypes(const char *uri)

{

    // @uri com.emtronix.qmlcomponents

    qmlRegisterType<MsgClient>(uri, 1, 0, "MsgClient");

}




  上面的代码qtcreator会自动生成,MsgClient类即为这个插件注册的新的QML类型,之后在QML文件中使用import语句加载插件,就可以在QML文件中实例化MsgClient类型了。



import com.emtronix.qmlcomponents 1.0

MsgClient{

  id: msgclient

}



  实现MsgClient类与Qt中其他的类类似,只要注意需要在QML中能访问的变量需要用Q_PROPERTY宏定义为MsgClient的属性,而需要在QML文件中访问的函数需要用Q_INVOKABLE进行修饰,如下:



class MsgClient : public QQuickItem

{

    Q_OBJECT

    Q_DISABLE_COPY(MsgClient)

    Q_PROPERTY(int m_interface READ interface WRITE setif NOTIFY ifChanged)

    Q_PROPERTY(QString rmsg READ rmsg WRITE setRmsg NOTIFY rmsgChanged)

    Q_PROPERTY(QString smsg READ smsg WRITE setSmsg NOTIFY smsgChanged)

    Q_PROPERTY(int serverConnection READ serverConnection NOTIFY serverConnected())

    Q_PROPERTY(DynamicEntryModel* pinstate READ pinstate WRITE setPinState NOTIFY pinStateChanged)

    Q_PROPERTY(DynamicEntryModel* outSwitch READ outSwitch WRITE setOutSwitch NOTIFY outSwitchChanged)

public:

    MsgClient(QQuickItem *parent = nullptr);

    ~MsgClient();

    Q_INVOKABLE void getAll();

    Q_INVOKABLE void getNew();



  使用slots定义的函数也可以在QML中直接访问,而使用signals定义的消息也可以在QML文件中访问:



public slots:

    void sendMsg();

    void setPinState(DynamicEntryModel* obj) { Q_UNUSED(obj) };

    void setOutSwitch(DynamicEntryModel* obj) { Q_UNUSED(obj) };

    ……….

signals:

    void ifChanged(int id);

    void newMsgRcved(void);

    void rmsgChanged(void);

    void smsgChanged(void);

    void sendMsgSignal(void);

    void serverConnected(void);



  这些函数以及变量都是正常的C++函数和变量,使用C++来实现函数需要实现的功能以及操作变量,例如sendMsg实现如下:



void MsgClient::sendMsg()

{

    struct msg_head buf_h;

    sprintf((char *)&buf_h.to[0], "S1");

    int buf_lenth = m_smsg.size();

    buf_h.lenth = buf_lenth;

    printf("Client sending msg: %d bytes\n", buf_lenth);

    write(sockedfd, (void *)&buf_h.to[0], 6);

    write(sockedfd, (void *)m_smsg.data(), buf_lenth);

    return;

}



  signals信号的访问需要做一定的转换,例如上面的ifChanged消息,在QML中需要使用onIfChanged来访问,即将函数首字母大写,然后在前面加上‘on’。


  MsgClient类型在QML中的实际调用如下:



MsgClient{

  id: msgclient

  m_interface: interfaceId.currentButton

  onNewMsgRcved:{

    getNew()

    console.log("got new msg" + rmsg)

  }

  onIfChanged: {

    console.log("interface changed to " + msgclient.m_interface)

    getAll()

    console.log("all history msg:" + rmsg)

  }

  onServerConnected: {

    notReady.destroy()

  }

}



  可以见到MsgClient类型的使用和QML内置的类型使用没有什么区别,而插件中定义的signals消息以及其他可调用的函数方法也可以在QML中直接访问,MsgClient本身并不会产生显示输出,只是处理数据然后提供给QML其他的类型进行显示。我们使用QML搭建了整个显示界面,显示效果如下:


使用QML进行界面开发.gif


  有兴趣的客户可以向我们索要例程的源码文件。


4、总结


  ESM6802带有GPU硬件加速模块,能够加速QML的渲染,而且QML语法简单,使用QML进行程序开发能够快速的构建流畅的交互式界面,同时使用QML开发使得界面开发与程序数据处理、硬件管理分开,明确程序开发的分工,降低程序维护成本。对于工业控制系统来说,将界面与底层硬件管理分开,一定程度上增加了底层硬件管理部分的程序的稳定性,而界面又可以实现快速的迭代,适应市场的需求,用户在使用过程中可以考虑使用这种方式进行程序开发。有兴趣的客户可以参考更多的网上资料以及Qt的官方文档。在ESM6802的使用过程中如有问题可以和我们联系。

[lablebox]