以前做.NET 开发中,.NET 直接就集成了属性设计器,VS 不愧是宇宙第一 IDE,你能够想到的都给你封装好了,用起来不要太爽!因为
项目需要自从全面转 Qt 开发已经 6 年有余,在工业控制领域,有一些应用场景需要自定义绘制一些控件满足特定的需求,比如仪器仪表、
组态等,而且需要直接用户通过属性设计的形式生成导出控件及界面数据,下次导入使用,要想从内置控件或者自定义控件拿到对应的
属性方法等,首先联想到的就是反射,Qt 反射对应的类叫 QMetaObject,着实强大,其实整个 Qt 开发框架也是超级强大的,本人自从
转为 Qt 开发为主后,就深深的爱上了她,在其他跨平台的 GUI 开发框架平台面前,都会被 Qt 秒成渣,Qt 的跨平台性是毋庸置疑的,几
十兆的内存存储空间即可运行,尤其是嵌入式 linux 这种资源相当紧张的情况下,Qt 的性能发挥到极致。
接下来我们就一步步利用 QMetaObject 类和 QtPropertyBrower(第三方开源属性设计器)来实现自己的控件属性设计器,其中包含了所
见即所得的控件属性控制,以及 xml 数据的导入导出。
第一步:获取控件的属性名称集合。
所有继承自 QObject 类的类,都有元对象,都可以通过这个 QObject 类的元对象 metaObject()获取属性+事件+方法等。
代码如下:
?
1
2
3
4
5
6
7
8
9
QPushButton *btn = new QPushButton;
const QMetaObject *metaobject = btn->metaObject();
int count = metaobject->propertyCount();
for (int i = 0; i < count; ++i) {
QMetaProperty metaproperty = metaobject->property(i);
const char *name = metaproperty.name();
QVariant value = btn->property(name);
qDebug() << name << value;
}
打印输出如下:
?
1
2
3
4
5
6
7
8
objectName QVariant(QString, "")
modal QVariant(bool, false)
windowModality QVariant(int, 0)
enabled QVariant(bool, true)
geometry QVariant(QRect, QRect(0,0 640x480))
frameGeometry QVariant(QRect, QRect(0,0 639x479))
normalGeometry QVariant(QRect, QRect(0,0 0x0))
省略后面很多…
可以看到打印了很多父类的属性,这些基本上我们不需要的,那怎么办呢,放心,Qt 肯定帮我们考虑好了,该 propertyOffset 上场了。
metaObject->propertyOffset()表示出了父类外,自己类本身属性的偏移位置即索引开始的位置,这下就好办了。
代码改为:
?
1
2
3
4
5
6
7
8
9
QPushButton *btn = new QPushButton;
const QMetaObject *metaobject = btn->metaObject();
int count = metaobject->propertyCount();
int index = metaobject->propertyOffset();
for (int i = index; i < count; ++i) {
QMetaProperty metaproperty = metaobject->property(i);
const char *name = metaproperty.name();
QVariant value = btn->property(name);
qDebug() << name << value;
10
}
就是将 i 的起始位置改为偏移位置即可。
打印输出如下:
?
1
2
3
autoDefault QVariant(bool, false)
default QVariant(bool, false)
flat QVariant(bool, false)
这个过滤非常有用,因为真实用到的大部分应用场景都是控件类本身的属性,而不是父类的。
第二步:将控件类绑定到属性设计器。
拿到了控件的属性是第一步,接下来就是需要拿到属性所关联的方法等,这里省略,因为 QtPropertyBrower 这个屌爆了的第三方开源的
属性设计器,全部给我们写好了,可以查看 Qt 帮助文档或者 QMetaObject 的头文件看到,QMetaObject 提供了哪些接口去获取或使用
这些元信息。比如 classInfo 获取类的信息、enumerator 获取枚举值信息、method 获取方法,property 获取属性、superClass 获取父类
的名称等。
QtPropertyBrower 中提供了 ObjectController 类,该类继承自 QWidget,这样的话我们在界面上拖一个 QWidget 控件,鼠标右键提升为
ObjectController 即可。
这个轮子造的不要太好,我们只需要一行代码就可以让所有属性自动罗列到属性设计器中,代码是 ui->objectController->setObject(bt
n);
看下效果如图:
到这里是不是很兴奋呢,任意控件都可以这样来展示自己的属性。在右侧动态更改属性会立即应用生效。
第三步:获取自定义控件的插件的所有控件。
接下来这一步才是最关键的一步,以上举例是 Qt 自带控件的,如果是自定义控件插件比如就一个 DLL 文件呢,怎么办?放心,办法肯
定是有的。
该插件类 QPluginLoader 上场了。通过 QPluginLoader 载入后的实例,通过 QDesignerCustomWidgetCollectionInterface 类获取插件容
器,然后逐个遍历容器找出单个插件,包括获得类名+图标。
代码如下:
?
1
2
3
4
5
6
7
8
9
1
0
1
1
1
2
1
3
1
4
1
5
void frmMain::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(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;
}
}
}
1
6
1
7
1
8
1
9
2
0
2
1
2
2
2
3
2
4
2
5
2
6
2
7
2
8
2
9
3
0
3
1
3
2
效果图如下:
第四步:实例化 new 出控件并放到窗体。
拿到了所有的控件,前面还有个对应控件的小图标,是不是又有点小激动呢,接下来就是怎么双击或者拖动该控件到界面上立马实例化
一个控件出来。上一步我们将所有控件放到了一个链表变量 listWidgets 中,该变量在头文件中定义如下:
QList listWidgets;
这里写了个函数,传入列表中控件的索引,即该类的索引位置,和控件默认要放置的坐标,即可在主界面生成该控件。
代码如下:
?
1
2
3
4
5
6
7
8
9
10
11
void frmMain::newWidget(int row, const QPoint &point)
{
}
//列表按照同样的索引生成的,所以这里直接对该行的索引就行
QWidget *widget = listWidgets.at(row)->createWidget(ui->centralwidget);
widget->move(point);
widget->resize(widget->sizeHint());
//实例化选中窗体跟随控件一起
newSelect(widget);
//立即执行获取焦点以及设置属性
widgetPressed(widget);
第五步:动态绑定控件到设计器。
这一步就比较轻松了,上面提到过,直接获取当前界面上选中的是哪个控件,遍历可以得到,然后设置 object 到属性设计器控件即可。
代码如下:
?
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
void frmMain::clearFocus()
{
}
//将原有焦点窗体全部设置成无焦点
foreach (SelectWidget *widget, selectWidgets) {
widget->setDrawPoint(false);
}
void frmMain::widgetPressed(QWidget *widget)
{
}
//清空所有控件的焦点
clearFocus();
//设置当前按下的控件有焦点
foreach (SelectWidget *w, selectWidgets) {
if (w->getWidget() == widget) {
w->setDrawPoint(true);
break;
}
}
//设置自动加载该控件的所有属性
ui->objectController->setObject(widget);
第六步:导入导出控件属性到 xml 文件。
这一步比较难,本人也是花了好几个小时才搞定,前后折腾了好多次,因为遇到好几个棘手的问题,比如有些自定义控件中其实里边封
装了 Qt 自带的控件例如 QPushButton 等,如果遍历控件设计窗体的所有控件,也会把该控件也遍历进去,所以要做过滤处理。
导入 xml 数据自动生成控件代码如下:
?
1
2
3
4
5
6
7
8
9
10
11
12
void frmMain::openFile(const QString &fileName)
{
//打开文件
QFile file(fileName);
if (!file.open(QFile::ReadOnly | QFile::Text)) {
return;
}
//将文件填充到 dom 容器
QDomDocument doc;
if (!doc.setContent(&file)) {
file.close();
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
return;
}
file.close();
//先清空原有控件
QList widgets = ui->centralwidget->findChildren();
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 x, y, width, height;
//存储其他自定义控件属性
QList > propertys;
//节点名称不为空才继续
if (!name.isEmpty()) {
//遍历节点的属性名称和属性值
QDomNamedNodeMap attrs = element.attributes();
for (int i = 0; i < attrs.count(); i++) {
QDomNode n = attrs.item(i);
QString nodeName = n.nodeName();
QString nodeValue = n.nodeValue();
//qDebug() << nodeName << nodeValue;
//优先取出坐标+宽高属性,这几个属性不能通过 setProperty 实现
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 {
propertys.append(qMakePair(nodeName, QVariant(nodeValue)));
}
}
}
//qDebug() << name << x << y << width << height;
//根据不同的控件类型实例化控件
int count = listWidgets.count();
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
for (int i = 0; i < count; i++) {
QString className = listWidgets.at(i)->name();
if (name == className) {
QWidget *widget = listWidgets.at(i)->createWidget(ui->centralwidget);
//逐个设置自定义控件的属性
int count = propertys.count();
for (int i = 0; i < count; i++) {
QPair property = propertys.at(i);
widget->setProperty(property.first.toLatin1().constData(), property.second);
}
//设置坐标+宽高
widget->setGeometry(x, y, width, height);
//实例化选中窗体跟随控件一起
newSelect(widget);
break;
}
}
//移动到下一个节点
node = node.nextSibling();
element = node.toElement();
}
}
}
导出所有控件到 xml 文件代码如下:
?