一、前言
组态设计是应一个客户要求新增加进去的,设计理念就是在提供一个组态设计的初级功能,比如读取自定义控件动态库,加载所有的控件放到控件栏(有点类似qtcreator的控件栏)用户拖曳对应的控件到画布上,自动生成控件,控件可以直接在画布上拉伸大小和拖动调整位置,设置一些控件本身的属性,比如各种颜色,同时还提供用户属性设置功能,用户可以自定义很多属性绑定在这个控件上,一般来说一个控件会定义一些Q_PROPERTY的东西来讲属性暴露出来进行设置,但是毕竟是有限的,大部分的时候用户希望的是除了这些自带的本身的属性以外,还能自定义一些用户的属性绑定到这个控件上,比如设备编号、设备名称等,而且这些信息要求可以保存的,下次自动加载并且应用进去。
弱属性机制在Qt中异常的强大,这种用户属性的处理必须要用到弱属性,弱属性要读取直接用widget->property(name)即可,要赋值直接widget->setProperty(name, value)即可,使用非常方便,支持多个,如果名字相当值取最后那个,会自动覆盖,所以想设备编号、设备名称这类的用户属性绑定到该控件以后,在程序中其他地方就可以直接用property函数取出来进行处理了。为此我专门做了个自定义控件属性设计器,支持中英文属性自动映射,有专门的一个系列的文章讲解那个属性设计器。
皮肤开源:https://gitee.com/feiyangqingyun/QWidgetDemo https://github.com/feiyangqingyun/QWidgetDemo
文件名称:styledemo
体验地址:https://gitee.com/feiyangqingyun/QWidgetExe https://github.com/feiyangqingyun/QWidgetExe
文件名称:bin_sams.zip
二、功能特点
- 采集数据端口,支持串口端口+网络端口,串口支持自由设置串口号+波特率,网络支持自由设置IP地址+通讯端口,每个端口支持采集周期,默认1秒钟一个地址,支持设置通讯超时次数,默认3次,支持最大重连时间,用于重新读取离线的设备。
- 控制器信息,能够添加控制器名称,选择控制器地址+控制器型号,设置该控制器下面的探测器数量。
- 探测器信息,能够添加位号,可自由选择探测器型号,气体种类,气体符号,高报值,低报值,缓冲值,清零值,是否启用,报警声音,背景地图,存储周期,数值换算小数点位数,报警延时时间,报警的类型(HH,LL,HL)等。
- 控制器型号+探测器型号+气体种类+气体符号,均可自由配置。
- 地图支持导入和删除,所有的探测器对应地图位置可自由拖动保存。
- 端口信息+控制器信息+探测器信息,支持导入导出+导出到excel+打印。
- 运行记录+报警记录+用户记录,支持多条件组合查询,比如时间段+控制器+探测器等,所有记录支持导出到excel+打印。
- 导出到excel的记录支持所有excel+wps等表格文件版本,不依赖excel等软件。
- 可删除指定时间范围内的数据,支持自动清理早期数据,设置最大保存记录数。
- 支持报警短信转发,支持多个接收手机号码,可设定发送间隔,比如即时发送或者6个小时发送一次所有的报警信息,短信内容过长,自动拆分多条短信。
- 支持报警邮件转发,支持多个接收邮箱,可设定发送间隔,比如即时发送或者6个小时发送一次所有的报警信息,支持附件发送。
- 高报颜色+低报颜色+正常颜色+0值颜色+曲线背景+曲线颜色等,都可以自由选择。
- 软件的中文标题+英文标题+logo路径+版权所有都可以自由设置。
- 提供开关设置开机运行+报警声音+自动登录+记住密码等。
- 报警声音可设置播放次数,界面提供17种皮肤文件选择。
- 支持云端数据同步,可设置云端数据库的信息,比如数据库名称,用户名+密码等。
- 支持网络转发和网络接收,网络接收开启后,软件从udp接收数据进行解析。网络转发支持多个目标IP,这样就实现了本地采集的软件,自由将数据转到客户端,随时查看探测器数据。
- 自动记住用户最后停留的界面+其他信息,重启后自动应用。
- 报警自动切换到对应的地图,探测器按钮闪烁。
- 双击探测器图标,可以进行回控。
- 支持用户权限管理,管理员+操作员两大类,用户登录+用户退出,可以记住密码和自动登录,超过三次报错提示并关闭程序。
- 支持四种监控模式,设备面板监控+地图监控+表格数据监控+曲线数据监控,可自由切换,四种同步应用。
- 支持报警继电器联动,一个位号可以跨串口联动多个模块和继电器号,支持多对多。
- 本地数据存储支持sqlite+mysql,支持远程数据同步到云端数据库。自动重连。
- 本地设备采集到的数据实时上传到云端,以便手机APP或者web等其他方式提取。
- 支持两种数据源,一种是串口和网络通过协议采集设备数据,一种是数据库采集。数据库采集模式可以作为通用的系统使用。
- 自带设备模拟工具,支持16个设备数据模拟,同时还带数据库数据模拟,以便在没有设备的时候测试数据。
- 默认通信协议采用modbus协议,后期增加mqtt等物联网协议的支持,做成通用系统。
- 支持所有windows操作系统+linux操作系统和其他操作系统。
三、效果图
四、核心代码
void frmConfigXml::initPlugin()
{
//载入默认的插件,debug则加载带d结尾的文件
QString appPath = qApp->applicationDirPath();
#ifdef QT_NO_DEBUG
QString str = "";
#else
QString str = "d";
#endif
#if defined(Q_OS_WIN)
QString fileName = QString("%1/quc%2.dll").arg(appPath).arg(str);
#elif defined(Q_OS_UNIX)
QString fileName = QString("%1/libquc%2.so").arg(appPath).arg(str);
#elif defined(Q_OS_MAC)
QString fileName = QString("%1/libquc%2.dylib").arg(appPath).arg(str);
#endif
//载入默认的自定义控件插件
loadPlugin(fileName);
//载入默认的控件xml数据
openFile(appPath + "/quc.xml");
//自动滚动条滚到指定位置
qApp->processEvents();
ui->listWidget->verticalScrollBar()->setValue(0);
}
void frmConfigXml::loadPlugin(const QString &fileName)
{
openPlugin(fileName);
}
void frmConfigXml::openPlugin(const QString &fileName)
{
//先清空原有的控件
qDeleteAll(listWidgets);
listWidgets.clear();
listNames.clear();
ui->listWidget->clear();
//加载自定义控件插件集合信息,包括获得类名+图标
QPluginLoader loader(fileName);
if (loader.load()) {
QObject *plugin = loader.instance();
//获取插件容器,然后逐个遍历容器找出单个插件
QDesignerCustomWidgetCollectionInterface *interfaces = qobject_cast<QDesignerCustomWidgetCollectionInterface *>(plugin);
if (interfaces) {
listWidgets = interfaces->customWidgets();
int count = listWidgets.count();
for (int i = 0; i < count; i++) {
//取出图标和类名
QIcon icon = listWidgets.at(i)->icon();
QString className = listWidgets.at(i)->name();
QListWidgetItem *item = new QListWidgetItem(ui->listWidget);
item->setText(className);
item->setIcon(icon);
listNames << className;
}
}
//获取所有插件的类名
const QObjectList objList = plugin->children();
foreach (QObject *obj, objList) {
QString className = obj->metaObject()->className();
//qDebug() << className;
}
}
}
void frmConfigXml::openFile(const QString &fileName)
{
//如果控件列表没有则不用继续
if (ui->listWidget->count() == 0) {
return;
}
//打开文件
QFile file(fileName);
if (!file.open(QFile::ReadOnly | QFile::Text)) {
return;
}
//将文件填充到dom容器
QDomDocument doc;
if (!doc.setContent(&file)) {
file.close();
return;
}
file.close();
listSelect.clear();
listUserProperty.clear();
xmlName = fileName;
//先清空原有控件
QList<QWidget *> widgets = ui->centralwidget->findChildren<QWidget *>();
qDeleteAll(widgets);
widgets.clear();
//先判断根元素是否正确
QDomElement docElem = doc.documentElement();
if (docElem.tagName() == "canvas") {
QDomNode node = docElem.firstChild();
QDomElement element = node.toElement();
while(!node.isNull()) {
//控件名称
QString name = element.tagName();
//取出当前控件在控件列表中的索引,如果不存在则意味着配置文件中的该控件不存在了
int index = listNames.indexOf(name);
if (index < 0) {
continue;
}
//存储控件的坐标位置和宽度高度
int x, y, width, height;
//存储自定义控件属性
QList<QPair<QString, QVariant> > propertys;
//存储控件自定义属性
QStringList userProperty;
//节点名称不为空才继续
if (!name.isEmpty()) {
//遍历节点的属性名称和属性值
QDomNamedNodeMap attrs = element.attributes();
for (int i = 0; i < attrs.count(); i++) {
QDomNode node = attrs.item(i);
QString nodeName = node.nodeName();
QString nodeValue = node.nodeValue();
//qDebug() << name << nodeName << nodeValue;
//优先取出坐标+宽高属性,这几个属性不能通过设置弱属性实现
if (nodeName == "x") {
x = nodeValue.toInt();
} else if (nodeName == "y") {
y = nodeValue.toInt();
} else if (nodeName == "width") {
width = nodeValue.toInt();
} else if (nodeName == "height") {
height = nodeValue.toInt();
} else if (nodeName.startsWith("user-")) {
//取出user-开头的自定义属性
nodeName = nodeName.split("-").last();
userProperty << QString("%1|%2").arg(nodeName).arg(nodeValue);
} else {
QVariant value = QVariant(nodeValue);
//为了兼容Qt4,需要将颜色值的rgba分别取出来,因为Qt4不支持16进制字符串带透明度
//#6422a3a9 这种格式依次为 argb 带了透明度的才需要特殊处理
if (nodeValue.startsWith("#") && nodeValue.length() == 9) {
bool ok;
int alpha = nodeValue.mid(1, 2).toInt(&ok, 16);
int red = nodeValue.mid(3, 2).toInt(&ok, 16);
int green = nodeValue.mid(5, 2).toInt(&ok, 16);
int blue = nodeValue.mid(7, 2).toInt(&ok, 16);
value = QColor(red, green, blue, alpha);
}
propertys.append(qMakePair(nodeName, value));
}
}
}
//qDebug() << name << x << y << width << height;
//根据不同的控件类型实例化控件
int countWidget = listWidgets.count();
int countProperty = propertys.count();
for (int i = 0; i < countWidget; i++) {
QString className = listWidgets.at(i)->name();
if (name == className) {
//生成对应的控件
QWidget *widget = createWidget(i);
//逐个设置自定义控件的属性
for (int j = 0; j < countProperty; j++) {
QPair<QString, QVariant> property = propertys.at(j);
QString name = property.first;
QVariant value = property.second;
widget->setProperty(name.toStdString().c_str(), value);
}
//设置控件坐标及宽高
widget->setGeometry(x, y, width, height);
//实例化选中窗体跟随控件一起
newSelect(widget, userProperty);
break;
}
}
//移动到下一个节点
node = node.nextSibling();
element = node.toElement();
}
}
}
void frmConfigXml::saveFile(const QString &fileName)
{
//如果控件列表没有则不用继续
if (ui->listWidget->count() == 0) {
return;
}
QFile file(fileName);
if (!file.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) {
return;
}
//以流的形式输出文件
QTextStream stream(&file);
//构建xml数据
QStringList list;
//添加固定头部数据
list << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
//添加canvas主标签,保存宽高和背景图片,还可以自行添加其他属性
list << QString("<canvas width=\"%1\" height=\"%2\" image=\"%3\">")
.arg(ui->centralwidget->width()).arg(ui->centralwidget->height()).arg("bg.jpg");
//从容器中找到所有控件,根据控件的类名保存该类的所有属性
QList<QWidget *> widgets = ui->centralwidget->findChildren<QWidget *>();
foreach (QWidget *widget, widgets) {
const QMetaObject *metaObject = widget->metaObject();
QString className = metaObject->className();
//如果当前控件的父类不是主窗体则无需导出,有些控件有子控件无需导出
if (widget->parent() != ui->centralwidget || className == "SelectWidget") {
continue;
}
//逐个存储自定义控件属性
//metaObject->propertyOffset()表示当前控件的属性开始索引,0开始的是父类的属性
QStringList values;
int index = metaObject->propertyOffset();
int count = metaObject->propertyCount();
for (int i = index; i < count; i++) {
QMetaProperty property = metaObject->property(i);
QString nodeName = property.name();
QVariant variant = property.read(widget);
QString typeName = variant.typeName();
QString nodeValue = variant.toString();
//如果是颜色值则取出透明度一起,颜色值toString在Qt4中默认不转透明度
if (typeName == "QColor") {
QColor color = variant.value<QColor>();
if (color.alpha() < 255) {
//Qt4不支持HexArgb格式的字符串,需要挨个取出来拼接
//nodeValue = color.name(QColor::HexArgb);
QString alpha = QString("%1").arg(color.alpha(), 2, 16, QChar('0'));
QString red = QString("%1").arg(color.red(), 2, 16, QChar('0'));
QString green = QString("%1").arg(color.green(), 2, 16, QChar('0'));
QString blue = QString("%1").arg(color.blue(), 2, 16, QChar('0'));
nodeValue = QString("#%1%2%3%4").arg(alpha).arg(red).arg(green).arg(blue);
}
}
//枚举值要特殊处理,需要以字符串形式写入,不然存储到配置文件数据为int
if (property.isEnumType()) {
QMetaEnum enumValue = property.enumerator();
nodeValue = enumValue.valueToKey(nodeValue.toInt());
}
values << QString("%1=\"%2\"").arg(nodeName).arg(nodeValue);
//qDebug() << nodeName << nodeValue << variant;
}
//找到当前控件对应的索引
index = -1;
count = listSelect.count();
for (int i = 0; i < count; i++) {
if (listSelect.at(i)->getWidget() == widget) {
index = i;
break;
}
}
//可以用下面方法列出所有的用户属性,然后取值,本程序已经用 listUserProperty 存储了
//qDebug() << widget->dynamicPropertyNames();
//逐个存储控件的用户属性
QStringList userProperty = listUserProperty.at(index);
count = userProperty.count();
for (int i = 0; i < count; i++) {
QStringList list = userProperty.at(i).split("|");
values << QString("user-%1=\"%2\"").arg(list.at(0)).arg(list.at(1));
}
//逐个添加界面上的控件的属性
QString geometry = QString("x=\"%1\" y=\"%2\" width=\"%3\" height=\"%4\"").arg(widget->x()).arg(widget->y()).arg(widget->width()).arg(widget->height());
QString str = QString("\t<%1 %2 %3/>").arg(className).arg(geometry).arg(values.join(" "));
list << str;
}
//添加固定尾部数据
list << "</canvas>";
//写入文件
QString data = list.join("\n");
stream << data;
file.close();
}