• 8005阅读
  • 15回复

Qt编写自定义控件属性设计器 [复制链接]

上一主题 下一主题
离线liudianwu
 

图酷模式  只看楼主 倒序阅读 楼主  发表于: 2018-09-23
以前做.NET开发中,.NET直接就集成了属性设计器,VS不愧是宇宙第一IDE,你能够想到的都给你封装好了,用起来不要太爽!因为项目需要自从全面转Qt开发已经6年有余,在工业控制领域,有一些应用场景需要自定义绘制一些控件满足特定的需求,比如仪器仪表、组态等,而且需要直接用户通过属性设计的形式生成导出控件及界面数据,下次导入使用,要想从内置控件或者自定义控件拿到对应的属性方法等,首先联想到的就是反射,Qt反射对应的类叫QMetaObject,着实强大,其实整个Qt开发框架也是超级强大的,本人自从转为Qt开发为主后,就深深的爱上了她,在其他跨平台的GUI开发框架平台面前,都会被Qt秒成渣,Qt的跨平台性是毋庸置疑的,几十兆的内存存储空间即可运行,尤其是嵌入式linux这种资源相当紧张的情况下,Qt的性能发挥到极致。
接下来我们就一步步利用QMetaObject类和QtPropertyBrower(第三方开源属性设计器)来实现自己的控件属性设计器,其中包含了所见即所得的控件属性控制,以及xml数据的导入导出。

第一步:获取控件的属性名称集合。
所有继承自QObject类的类,都有元对象,都可以通过这个QObject类的元对象metaObject()获取属性+事件+方法等。

代码如下:
  1. QPushButton *btn = new QPushButton;
  2. const QMetaObject *metaobject = btn->metaObject();
  3. int count = metaobject->propertyCount();
  4. for (int i = 0; i < count; ++i) {
  5.     QMetaProperty metaproperty = metaobject->property(i);
  6.     const char *name = metaproperty.name();
  7.     QVariant value = btn->property(name);
  8.     qDebug() << name << value;
  9. }


打印输出如下:
  1. objectName QVariant(QString, "")
  2. modal QVariant(bool, false)
  3. windowModality QVariant(int, 0)
  4. enabled QVariant(bool, true)
  5. geometry QVariant(QRect, QRect(0,0 640x480))
  6. frameGeometry QVariant(QRect, QRect(0,0 639x479))
  7. normalGeometry QVariant(QRect, QRect(0,0 0x0))
  8. 省略后面很多…

可以看到打印了很多父类的属性,这些基本上我们不需要的,那怎么办呢,放心,Qt肯定帮我们考虑好了,该propertyOffset上场了。metaObject->propertyOffset()表示出了父类外,自己类本身属性的偏移位置即索引开始的位置,这下就好办了。

代码改为:
  1. QPushButton *btn = new QPushButton;
  2. const QMetaObject *metaobject = btn->metaObject();
  3. int count = metaobject->propertyCount();
  4. int index = metaobject->propertyOffset();
  5. for (int i = index; i < count; ++i) {
  6.     QMetaProperty metaproperty = metaobject->property(i);
  7.     const char *name = metaproperty.name();
  8.     QVariant value = btn->property(name);
  9.     qDebug() << name << value;
  10. }

就是将i的起始位置改为偏移位置即可。
打印输出如下:
  1. autoDefault QVariant(bool, false)
  2. default QVariant(bool, false)
  3. flat QVariant(bool, false)


这个过滤非常有用,因为真实用到的大部分应用场景都是控件类本身的属性,而不是父类的。

第二步:将控件类绑定到属性设计器。
拿到了控件的属性是第一步,接下来就是需要拿到属性所关联的方法等,这里省略,因为QtPropertyBrower这个屌爆了的第三方开源的属性设计器,全部给我们写好了,可以查看Qt帮助文档或者QMetaObject的头文件看到,QMetaObject提供了哪些接口去获取或使用这些元信息。比如classInfo获取类的信息、enumerator获取枚举值信息、method获取方法,property获取属性、superClass获取父类的名称等。
QtPropertyBrower中提供了ObjectController类,该类继承自QWidget,这样的话我们在界面上拖一个QWidget控件,鼠标右键提升为ObjectController即可。
这个轮子造的不要太好,我们只需要一行代码就可以让所有属性自动罗列到属性设计器中,代码是ui->objectController->setObject(btn);

看下效果如图:


到这里是不是很兴奋呢,任意控件都可以这样来展示自己的属性。在右侧动态更改属性会立即应用生效。

第三步:获取自定义控件的插件的所有控件。
接下来这一步才是最关键的一步,以上举例是Qt自带控件的,如果是自定义控件插件比如就一个DLL文件呢,怎么办?放心,办法肯定是有的。
该插件类QPluginLoader上场了。通过QPluginLoader载入后的实例,通过QDesignerCustomWidgetCollectionInterface类获取插件容器,然后逐个遍历容器找出单个插件,包括获得类名+图标。

代码如下:
  1. void frmMain::openPlugin(const QString &fileName)
  2. {
  3.     qDeleteAll(listWidgets);
  4.     listWidgets.clear();
  5.     listNames.clear();
  6.     ui->listWidget->clear();
  7.     //加载自定义控件插件集合信息,包括获得类名+图标
  8.     QPluginLoader loader(fileName);
  9.     if (loader.load()) {
  10.         QObject *plugin = loader.instance();
  11.         //获取插件容器,然后逐个遍历容器找出单个插件
  12.         QDesignerCustomWidgetCollectionInterface *interfaces = qobject_cast<QDesignerCustomWidgetCollectionInterface *>(plugin);
  13.         if (interfaces)  {
  14.             listWidgets = interfaces->customWidgets();
  15.             int count = listWidgets.count();
  16.             for (int i = 0; i < count; i++) {
  17.                 QIcon icon = listWidgets.at(i)->icon();
  18.                 QString className = listWidgets.at(i)->name();
  19.                 QListWidgetItem *item = new QListWidgetItem(ui->listWidget);
  20.                 item->setText(className);
  21.                 item->setIcon(icon);
  22.                 listNames << className;
  23.             }
  24.         }
  25.         //获取所有插件的类名
  26.         const QObjectList objList = plugin->children();
  27.         foreach (QObject *obj, objList) {
  28.             QString className = obj->metaObject()->className();
  29.             //qDebug() << className;
  30.         }
  31.     }
  32. }

效果图如下:



第四步:实例化new出控件并放到窗体。
拿到了所有的控件,前面还有个对应控件的小图标,是不是又有点小激动呢,接下来就是怎么双击或者拖动该控件到界面上立马实例化一个控件出来。上一步我们将所有控件放到了一个链表变量listWidgets中,该变量在头文件中定义如下:
QList<QDesignerCustomWidgetInterface *> listWidgets;

这里写了个函数,传入列表中控件的索引,即该类的索引位置,和控件默认要放置的坐标,即可在主界面生成该控件。

代码如下:
  1. void frmMain::newWidget(int row, const QPoint &point)
  2. {
  3.     //列表按照同样的索引生成的,所以这里直接对该行的索引就行
  4.     QWidget *widget = listWidgets.at(row)->createWidget(ui->centralwidget);
  5.     widget->move(point);
  6.     widget->resize(widget->sizeHint());
  7.     //实例化选中窗体跟随控件一起
  8.     newSelect(widget);
  9.     //立即执行获取焦点以及设置属性
  10.     widgetPressed(widget);
  11. }


第五步:动态绑定控件到设计器。
这一步就比较轻松了,上面提到过,直接获取当前界面上选中的是哪个控件,遍历可以得到,然后设置object到属性设计器控件即可。

代码如下:
  1. void frmMain::clearFocus()
  2. {
  3.     //将原有焦点窗体全部设置成无焦点
  4.     foreach (SelectWidget *widget, selectWidgets) {
  5.         widget->setDrawPoint(false);
  6.     }
  7. }
  8. void frmMain::widgetPressed(QWidget *widget)
  9. {
  10.     //清空所有控件的焦点
  11.     clearFocus();
  12.     //设置当前按下的控件有焦点
  13.     foreach (SelectWidget *w, selectWidgets) {
  14.         if (w->getWidget() == widget) {
  15.             w->setDrawPoint(true);
  16.             break;
  17.         }
  18.     }
  19.     //设置自动加载该控件的所有属性
  20.     ui->objectController->setObject(widget);
  21. }


第六步:导入导出控件属性到xml文件。
这一步比较难,本人也是花了好几个小时才搞定,前后折腾了好多次,因为遇到好几个棘手的问题,比如有些自定义控件中其实里边封装了Qt自带的控件例如QPushButton等,如果遍历控件设计窗体的所有控件,也会把该控件也遍历进去,所以要做过滤处理。

导入xml数据自动生成控件代码如下:
  1. void frmMain::openFile(const QString &fileName)
  2. {
  3.     //打开文件
  4.     QFile file(fileName);
  5.     if (!file.open(QFile::ReadOnly | QFile::Text)) {
  6.         return;
  7.     }
  8.     //将文件填充到dom容器
  9.     QDomDocument doc;
  10.     if (!doc.setContent(&file)) {
  11.         file.close();
  12.         return;
  13.     }
  14.     file.close();
  15.     //先清空原有控件
  16.     QList<QWidget *> widgets = ui->centralwidget->findChildren<QWidget *>();
  17.     qDeleteAll(widgets);
  18.     widgets.clear();
  19.     //先判断根元素是否正确
  20.     QDomElement docElem = doc.documentElement();
  21.     if (docElem.tagName() == "canvas") {
  22.         QDomNode node = docElem.firstChild();
  23.         QDomElement element = node.toElement();
  24.         while(!node.isNull()) {
  25.             QString name = element.tagName();
  26.             //存储坐标+宽高
  27.             int x, y, width, height;
  28.             //存储其他自定义控件属性
  29.             QList<QPair<QString, QVariant> > propertys;
  30.             //节点名称不为空才继续
  31.             if (!name.isEmpty()) {
  32.                 //遍历节点的属性名称和属性值
  33.                 QDomNamedNodeMap attrs = element.attributes();
  34.                 for (int i = 0; i < attrs.count(); i++) {
  35.                     QDomNode n = attrs.item(i);
  36.                     QString nodeName = n.nodeName();
  37.                     QString nodeValue = n.nodeValue();
  38.                     //qDebug() << nodeName << nodeValue;
  39.                     //优先取出坐标+宽高属性,这几个属性不能通过setProperty实现
  40.                     if (nodeName == "x") {
  41.                         x = nodeValue.toInt();
  42.                     } else if (nodeName == "y") {
  43.                         y = nodeValue.toInt();
  44.                     } else if (nodeName == "width") {
  45.                         width = nodeValue.toInt();
  46.                     } else if (nodeName == "height") {
  47.                         height = nodeValue.toInt();
  48.                     } else {
  49.                         propertys.append(qMakePair(nodeName, QVariant(nodeValue)));
  50.                     }
  51.                 }
  52.             }
  53.             //qDebug() << name << x << y << width << height;
  54.             //根据不同的控件类型实例化控件
  55.             int count = listWidgets.count();
  56.             for (int i = 0; i < count; i++) {
  57.                 QString className = listWidgets.at(i)->name();
  58.                 if (name == className) {
  59.                     QWidget *widget = listWidgets.at(i)->createWidget(ui->centralwidget);
  60.                     //逐个设置自定义控件的属性
  61.                     int count = propertys.count();
  62.                     for (int i = 0; i < count; i++) {
  63.                         QPair<QString, QVariant> property = propertys.at(i);
  64.                         widget->setProperty(property.first.toLatin1().constData(), property.second);
  65.                     }
  66.                     //设置坐标+宽高
  67.                     widget->setGeometry(x, y, width, height);
  68.                     //实例化选中窗体跟随控件一起
  69.                     newSelect(widget);
  70.                     break;
  71.                 }
  72.             }
  73.             //移动到下一个节点
  74.             node = node.nextSibling();
  75.             element = node.toElement();
  76.         }
  77.     }
  78. }


导出所有控件到xml文件代码如下:
  1. void frmMain::saveFile(const QString &fileName)
  2. {
  3.     QFile file(fileName);
  4.     if (!file.open(QFile::WriteOnly | QFile::Text | QFile::Truncate)) {
  5.         return;
  6.     }
  7.     //以流的形式输出文件
  8.     QTextStream stream(&file);
  9.     //构建xml数据
  10.     QStringList list;
  11.     //添加固定头部数据
  12.     list << "<?xml version=\"1.0\" encoding=\"UTF-8\"?>";
  13.     list << QString("<canvas width=\"%1\" height=\"%2\">")
  14.          .arg(ui->centralwidget->width()).arg(ui->centralwidget->height());
  15.     //从容器中找到所有控件,根据控件的类名保存该类的所有属性
  16.     QList<QWidget *> widgets = ui->centralwidget->findChildren<QWidget *>();
  17.     foreach (QWidget *w, widgets) {
  18.         const QMetaObject *metaObject = w->metaObject();
  19.         QString className = metaObject->className();
  20.         QStringList values;
  21.         //如果当前控件的父类不是主窗体则无需导出,有些控件有子控件无需导出
  22.         if (w->parent() != ui->centralwidget || className == "SelectWidget") {
  23.             continue;
  24.         }
  25.         //metaObject->propertyOffset()表示当前控件的属性开始索引,0开始的是父类的属性
  26.         int index = metaObject->propertyOffset();
  27.         for (int i = index; i < metaObject->propertyCount(); i++) {
  28.             QMetaProperty p = metaObject->property(i);
  29.             QString nodeName = p.name();
  30.             QVariant nodeValue = p.read(w);
  31.             //枚举值要特殊处理,需要以字符串形式写入,不然存储到配置文件数据为int
  32.             if (p.isEnumType()) {
  33.                 QMetaEnum enumValue = p.enumerator();
  34.                 nodeValue = enumValue.valueToKey(nodeValue.toInt());
  35.             }
  36.             QString temp = nodeValue.toString().toLocal8Bit().constData();
  37.             values << QString("%1=\"%2\"").arg(nodeName).arg(temp);
  38.             //qDebug() << nodeName << nodeValue;
  39.         }
  40.         //逐个添加界面上的控件的属性
  41.         QString str = QString("\t<%1 x=\"%2\" y=\"%3\" width=\"%4\" height=\"%5\" %6/>")
  42.                       .arg(className).arg(w->x()).arg(w->y()).arg(w->width()).arg(w->height()).arg(values.join(" "));
  43.         list << str;
  44. }
  45. //添加固定尾部数据
  46.     list << "</canvas>";
  47.     //写入文件
  48.     QString data = list.join("\n");
  49.     stream << data;
  50.     file.close();
  51. }

xml数据格式效果图:

完整效果图:

自定义控件属性设计器:https://pan.baidu.com/s/1iZvQe7L0Dfif_p50qodZ8Q

最后分享一些自己整理好的Qt开发过程中的小技巧,Qt武林秘籍。
1:当编译发现大量错误的时候,从第一个看起,一个一个的解决,不要急着去看下一个错误,往往后面的错误都是由于前面的错误引起的,第一个解决后很可能都解决了。
2:定时器是个好东西,学会好使用它,有时候用QTimer::singleShot可以解决意想不到的问题。
3:打开creator,在构建套件的环境中增加MAKEFLAGS=-j8,可以不用每次设置多线程编译。珍爱时间和生命。
4:如果你想顺利用QtCreator部署安卓程序,首先你要在AndroidStudio 里面配置成功,把坑全部趟平。
5:很多时候找到Qt对应封装的方法后,记得多看看该函数的重载,多个参数的,你会发现不一样的世界,有时候会恍然大悟,原来Qt已经帮我们封装好了。
6:可以在pro文件中写上标记版本号+ico图标
VERSION       = 2018.7.25
RC_ICONS    = main0.ico
7:管理员运行程序,限定在MSVC编译器。
QMAKE_LFLAGS += /MANIFESTUAC:\"level=\'requireAdministrator\' uiAccess=\'false\'\" #以管理员运行
QMAKE_LFLAGS += /SUBSYSTEM:WINDOWS,\"5.01\" #VS2013 在XP运行
8:运行文件附带调试输出窗口
CONFIG += console pro
9:绘制平铺背景QPainter::drawTiledPixmap
绘制圆角矩形QPainter::drawRoundedRect(),而不是QPainter::drawRoundRect();
10:移除旧的样式
style()->unpolish(ui->btn);
重新设置新的该控件的样式。
style()->polish(ui->btn);
11:获取类的属性
const QMetaObject *metaobject = object->metaObject();
int count = metaobject->propertyCount();
for (int i = 0; i < count; ++i) {
    QMetaProperty metaproperty = metaobject->property(i);
    const char *name = metaproperty.name();
    QVariant value = object->property(name);
    qDebug() << name << value;
}
12:Qt内置图标封装在QStyle中,大概七十多个图标,可以直接拿来用。
QStyle::SP_TitleBarMenuButton
13:根据操作系统位数判断加载
win32 {
    contains(DEFINES, WIN64) {
        DESTDIR = $${PWD}/../../bin64
    } else {
        DESTDIR = $${PWD}/../../bin32
    }
}
14:Qt5增强了很多安全性验证,如果出现setGeometry: Unable to set geometry,请将该控件的可见移到加入布局之后。
15:可以将控件A添加到布局,然后控件B设置该布局,这种灵活性大大提高了控件的组合度,比如可以在文本框左侧右侧增加一个搜索按钮,按钮设置图标即可。
QPushButton *btn = new QPushButton;
btn->resize(30, ui->lineEdit->height());
QHBoxLayout *layout = new QHBoxLayout(ui->lineEdit);
layout->setMargin(0);
layout->addStretch();
layout->addWidget(btn);
16:对QLCDNumber控件设置样式,需要将QLCDNumber的segmentstyle设置为flat。
17:巧妙的使用findChildren可以查找该控件下的所有子控件。findChild为查找单个。
//查找指定类名objectName的控件
QList<QWidget *> widgets = parentWidget.findChildren<QWidget *>("widgetname");
//查找所有QPushButton
QList<QPushButton *> allPButtons = parentWidget.findChildren<QPushButton *>();
//查找一级子控件,不然会一直遍历所有子控件
QList<QPushButton *> childButtons = parentWidget.findChildren<QPushButton *>(QString(), Qt::FindDirectChildrenOnly);
18:巧妙的使用inherits判断是否属于某种类。
QTimer *timer = new QTimer;         // QTimer inherits QObject
timer->inherits("QTimer");          // returns true
timer->inherits("QObject");         // returns true
timer->inherits("QAbstractButton"); // returns false
19:使用弱属性机制,可以存储临时的值用于传递判断。
20:在开发时, 无论是出于维护的便捷性, 还是节省内存资源的考虑, 都应该有一个 qss 文件来存放所有的样式表, 而不应该将 setStyleSheet 写的到处都是。
21:如果出现Z-order assignment: " is not a valid widget.错误提示,用记事本打开对应的ui文件,找到<zorder></zorder>为空的地方,删除即可。
22:善于利用QComboBox的addItem的第二个参数设置用户数据,可以实现很多效果,使用itemData取出来。
23:如果用了webengine模块,发布程序的时候带上QtWebEngineProcess.exe+translations文件夹+resources文件夹。
24:a.setAttribute(Qt::AA_NativeWindows);可以让每个控件都拥有独立的句柄。
100:终极秘籍:如果遇到问题搜索Qt方面找不到答案,试着将关键字用JAVA C# android打头,你会发现别有一番天地,其他人很可能做过!
最后一条:珍爱生命,远离编程。


欢迎关注微信公众号:Qt实战 (各种开源作品、经验整理、项目实战技巧,专注Qt/C++软件开发,视频监控、物联网、工业控制、嵌入式软件、国产化系统应用软件开发)QQ:517216493  WX:feiyangqingyun  QQ群:751439350

只看该作者 1楼 发表于: 2018-09-23
这种文章很不错,比直接发源码好多了,刘大佬这篇文章我给100分
在线clickto

只看该作者 2楼 发表于: 2018-09-24
我只能说,大师,让我膜拜两分钟先!
离线305750665

只看该作者 3楼 发表于: 2018-09-24
     刘大师流量又要噌噌噌的暴涨了~
雨田哥: 群号:853086607
QQ: 3246214072

刘典武-feiyangqingyun:专业各种自定义控件编写+UI定制+输入法定制+视频监控+工业控制+仪器仪表+嵌入式linux+各种串口网络通信,童叟无欺,量大从优,欢迎咨询购买定制!QQ:517216493
离线nigoole

只看该作者 4楼 发表于: 2018-09-25
良心作品!~
有句话说得好:好好学习,天天向上。加油~~!有上船的朋友联系企鹅393320854
离线liuchangyin

只看该作者 5楼 发表于: 2018-09-25
离线ccazqyy

只看该作者 6楼 发表于: 2018-09-25
        
离线wbcg00

只看该作者 7楼 发表于: 2018-09-25
   收藏,学习!
离线玖零儛

只看该作者 8楼 发表于: 2018-09-27
下面的秘籍太赞了
离线九重水

只看该作者 9楼 发表于: 2018-09-28
喲,又跑去工控玩HMI了。
HMI市场份额虽然是有,但很多有开发实力的厂家好多都是用C或C++直接做。
离线hanheyfon

只看该作者 10楼 发表于: 2018-10-15
    

只看该作者 11楼 发表于: 2018-11-30
离线futureq

只看该作者 12楼 发表于: 2019-01-20
因吹斯听
离线misgn

只看该作者 13楼 发表于: 2019-01-25
使用Qt5写的吗?
离线mangogo

只看该作者 14楼 发表于: 2019-04-01
大神学习了 ,另想请教一下,拖动控件到界面这一步有什么实现思路吗?
离线hanheyfon

只看该作者 15楼 发表于: 2019-04-13
快速回复
限100 字节
 
上一个 下一个