写这篇文章的想法是使用自定义QML控件的方式访问EPICS过程变量,现有的Qt EPICS框架之前也介绍过了,还有配套的QEGui工具,框架的基本功能已经比较完善,但实际使用过程中还是遇到了一些问题。我总结有以下2点:
QEGui工具只能加载基于Widget的.ui界面文件,文件只能包含布局、控件和控件属性,不能嵌入代码(C++代码必须编译)。用户的操作(如:输入、点击按钮等)的相应槽函数封装在框架自定义的控件里实现,这样极大方便了用户的开发,用户只需要设计界面,填写变量名就可以实现变量访问,但相应的也失去了很大的灵活性,程序的功能变得很单一。
QEGui工具不一定符合实际开发过程中的需求,仅使用QEGui加载.ui界面,脱离了C++代码,程序很难实现用户想要的效果。而更好的选择是调用Qt EPICS框架动态库提供的控件,结合C++代码开发应用程序,但却不得不进行编译操作。
那么,有没有更好的基于Qt的EPICS框架方案呢?
刚好最近我也学习实践了QML相关的内容,这种前后端分离开发的方式给了我一些灵感。我们完全可以在Qt EPICS框架的基础上,实现自定义QML控件访问EPICS过程变量,用户使用QML编写界面,调用自定义QML控件即可,可以实现和Qt EPICS框架加载.ui界面文件类似的功能。但QML在动画、3D显示等方面明显具有优势,还有一点是基于Widget的.ui界面文件不具备的:QML本身可以嵌入javascript
函数,动态控制界面的显示、切换等,甚至可以实现和底层C++接口的交互。而QML本身也不需要进行编译,完全可以只使用QML语言实现程序开发,且具有很高灵活性。
实现思路#

Qt EPICS 框架提供了QCaObject
类访问EPICS过程变量,但该类几乎是完全为QEWidget
服务的,并没有声明属性(property),无法直接在QML中使用。
所以第1步需要对QCaObject
做一层封装,将EPICS过程变量的字段声明为QmlPvObject
类的属性,然后就可以在QML中访问EPICS过程变量了。
第2步是将QmlPvObject
封装进自定义的QML控件QmlPvControl
,实现数据的显示、输入等操作。
第3步将QmlPvControl
导入(import)到QML文件,在外部QML文件中使用自定义控件。
代码示例#
由于完整的代码较多,这里只放部分代码。
QmlPvObject类的定义
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
| /* qmlpvobject.h */
class QmlPvObject : public QObject
{
Q_OBJECT
public:
enum epicsAlarmSeverity {
NO_ALARM, /**< No alarm */
MINOR_ALARM, /**< Minor alarm severity */
MAJOR_ALARM, /**< Major alarm severity */
INVALID_ALARM,/**< Invalid alarm severity */
ALARM_NSEV /**< Number of alarm severities */
};
Q_ENUM(epicsAlarmSeverity)
private:
Q_PROPERTY(QString pvName READ pvName WRITE setPvName NOTIFY pvNameChanged FINAL)
Q_PROPERTY(QVariant value READ value WRITE setValue NOTIFY valueChanged FINAL)
Q_PROPERTY(QString hostName READ hostName NOTIFY hostNameChanged FINAL)
Q_PROPERTY(QString fieldType READ fieldType NOTIFY fieldTypeChanged FINAL)
Q_PROPERTY(QString descriptor READ descriptor NOTIFY descriptorChanged FINAL)
Q_PROPERTY(QString egu READ egu NOTIFY eguChanged FINAL)
Q_PROPERTY(QCaDateTime dateTime READ dateTime NOTIFY dateTimeChanged FINAL)
Q_PROPERTY(quint16 status READ status NOTIFY statusChanged FINAL)
Q_PROPERTY(QmlPvObject::epicsAlarmSeverity severity READ severity NOTIFY severityChanged FINAL)
Q_PROPERTY(QString statusName READ statusName NOTIFY statusNameChanged FINAL)
Q_PROPERTY(QString severityName READ severityName NOTIFY severityNameChanged FINAL)
Q_PROPERTY(bool readAccess READ readAccess NOTIFY readAccessChanged FINAL)
Q_PROPERTY(bool writeAccess READ writeAccess NOTIFY writeAccessChanged FINAL)
public:
explicit QmlPvObject(QObject *parent = Q_NULLPTR);
QString pvName() const;
QVariant value() const;
QString hostName() const;
QString fieldType() const;
QString descriptor() const;
QString egu() const;
QCaDateTime dateTime() const;
quint16 status() const;
QmlPvObject::epicsAlarmSeverity severity() const;
QString statusName() const;
QString severityName() const;
bool readAccess() const;
bool writeAccess() const;
public Q_SLOTS:
virtual void setPvName(const QString &pvname) = 0;
virtual void setValue(const QVariant &value) = 0;
virtual void onConnectionChanged(QCaConnectionInfo& connectionInfo, const uint& variableIndex);
virtual void onDataChanged(const QVariant& value, QCaAlarmInfo& alarmInfo, QCaDateTime& timeStamp, const uint& variableIndex);
Q_SIGNALS:
void pvNameChanged(const QString&);
void valueChanged(const QVariant&);
void dateTimeChanged(const QCaDateTime&);
void fieldTypeChanged(const QString&);
void descriptorChanged(const QString&);
void eguChanged(const QString&);
void hostNameChanged(const QString&);
void statusChanged(const quint16);
void severityChanged(const epicsAlarmSeverity);
void statusNameChanged(const QString);
void severityNameChanged(const QString);
void readAccessChanged(const bool);
void writeAccessChanged(const bool);
protected:
QPointer<qcaobject::QCaObject> m_caobject;
// ...
};
|
注意到QmlPvObject
类有两个虚函数setPvName
和setValue
,这两个函数需要子类实现。
在setPvName
中实现QCaObject
变量的实例化和EPICS过程变量的连接等操作,在setValue
中将值写入到EPICS过程变量。
例如:整数类型的过程变量
1
2
3
4
5
6
7
8
9
10
11
12
| /* qmlpvint.h */
class QmlPvInt : public QmlPvObject
{
Q_OBJECT
Q_DISABLE_COPY_MOVE(QmlPvInt)
public:
explicit QmlPvInt(QObject *parent = Q_NULLPTR);
public Q_SLOTS:
virtual void setPvName(const QString &pvname) Q_DECL_OVERRIDE;
virtual void setValue(const QVariant &value) Q_DECL_OVERRIDE;
};
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
| /* qmlpvint.cpp */
QmlPvInt::QmlPvInt(QObject *parent)
: QmlPvObject{parent}
{}
void QmlPvInt::setPvName(const QString &pvname)
{
if (!pvname.isEmpty() && pvname != m_pv_name) {
m_pv_name = pvname;
Q_EMIT pvNameChanged(pvname);
if (!m_caobject.isNull()) {
m_caobject->closeChannel();
m_caobject->deleteLater();
}
m_caobject = new QEInteger(pvname, this, Q_NULLPTR, 0);
connect(m_caobject, &QCaObject::connectionChanged, this, &QmlPvInt::onConnectionChanged);
connect(m_caobject,
QOverload<const QVariant&, QCaAlarmInfo&, QCaDateTime&, const unsigned int&>::of(&QCaObject::dataChanged),
this, &QmlPvInt::onDataChanged);
m_caobject->subscribe();
}
}
void QmlPvInt::setValue(const QVariant &value)
{
if (value != m_value) {
bool ok = false;
int val = value.toInt(&ok);
if (ok && !m_caobject.isNull() && m_caobject->getChannelIsConnected()) {
m_caobject->writeIntegerValue(val);
}
}
}
|
根据Qt EPICS框架提供的数据类型,定义QML中可使用的过程变量类,如:QmlPvInt
、QmlPvDouble
、QmlPvString
。
然后需要注册自定义的数据类型,才能在QML中使用。
1
2
3
| qmlRegisterType<QmlPvInt>("com.example.epics", 0, 1, "QmlPvInt");
qmlRegisterType<QmlPvDouble>("com.example.epics", 0, 1, "QmlPvDouble");
qmlRegisterType<QmlPvString>("com.example.epics", 0, 1, "QmlPvString");
|
QmlPvControl 控件的定义
这里自定义了Label控件,可以自动更新EPICS过程变量的值,根据严重等级自动改变控件背景色。控件声明了pvName
属性,用户在使用时需要填写此项才可以连接到EPICS过程变量。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
| /* QmlPvLabel.qml */
import QtQuick 2.15
import QtQuick.Controls 2.15
import com.example.epics 0.1
Label {
id: root
/* 声明pvName属性 */
property alias pvName: pv.pvName
/* 提示信息 */
ToolTip.delay: 1000
ToolTip.visible: mouseArea.containsMouse
ToolTip.text: pvName
/* 背景色 */
background: Rectangle {
id: backgroundRect
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
}
/* EPICS过程变量 */
QmlPvString {
id: pv
onValueChanged: {
console.log(pvName + " value changed to: " + value)
root.text = value.toString()
}
onSeverityChanged: (severity) => {
switch (severity) {
case QmlPvString.NO_ALARM:
backgroundRect.color = 'transparent'
break
case QmlPvString.MINOR_ALARM:
backgroundRect.color = 'yellow'
break
case QmlPvString.MAJOR_ALARM:
backgroundRect.color = 'red'
break
case QmlPvString.INVALID_ALARM:
backgroundRect.color = '#FF00FF'
root.text = '---'
break
default:
backgroundRect.color = 'lightgray'
break
}
}
}
}
|
使用自定义控件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
| /* main.qml */
import QtQuick 2.15
import QtQuick.Window 2.15
// 导入自定义控件
import 'qrc:/'
Window {
id: window
width: 640
height: 480
visible: true
title: qsTr("Hello World") // @disable-check M16
QmlPvLabel {
id: testLabel
pvName: "ca://user:circle:angle"
}
QmlPvLabel {
id: baseLabel
pvName: "user:iocExample:version"
}
QmlPvEdit {
id: testEdit
pvName: "pva://user:circle:period"
}
}
|
pvName
支持CA
/PVA
协议的变量,例如:
ca://user:circle:angle
,使用CA
协议
pva://user:circle:angle
,使用PVA
协议
user:circle:angle
,默认使用CA
协议
最后,定义程序启动时加载的qml文件路径即可。
1
2
3
4
5
| /* main.cpp */
QQmlApplicationEngine engine;
// 定义qml文件的路径或通过 argv 参数传入
const QUrl url("./main.qml");
engine.load(url);
|
使用qputenv
设置EPICS相关的环境变量。- 可使用传入程序的参数动态加载界面文件。例如:myapp myui.qml
运行结果

本文给出了自定义QML控件实现EPICS过程变量访问的思路,后续可能需要添加更多的自定义控件和更多的功能实现。