控件能够被拖曳和拉伸是基本前提,接下来的核心任务便是如何动态加载被选中控件的属性。Qt的属性机制异常强大,堪称“强大到爆”。如果你在编写自定义控件时,使用 Q_PROPERTY 宏仔细描述过属性,那么这些属性将能在整个Qt生态的各种应用场景中被轻松识别和加载,无论是Widget的属性设计还是QML的属性绑定,都极为方便。
观察Qt Designer的属性编辑器,其外观与表格控件颇为相似。若要自己从头实现这样一个组件,代价巨大。事实上,Qt官方在 qt-solutions 项目中提供了一个开源组件——qtpropertybrowser。网络上有很多关于它的使用教程和演示,非常详尽。笔者也对其部分代码进行了修改,以实现父类属性过滤和中文属性名称映射等功能。
功能概览
- 自动加载插件文件中的所有控件并生成列表,默认自带控件超过120个。
- 拖曳控件到画布即可自动生成对应实例,所见即所得。
- 右侧提供中文属性栏,修改属性可立即应用到选中的控件上,直观简洁,易于上手。
- 独创高效的属性栏文字翻译映射机制,方便扩展支持其他语言。
- 自动提取并显示控件的所有属性,包括枚举值下拉框等。
- 支持手动选择并导入外部插件文件。
- 可将当前画布的所有控件配置导出为XML文件。
- 可通过选择XML文件打开并自动加载已保存的控件布局。
- 支持通过滑动条、勾选模拟数据复选框、文本框输入三种方式生成测试数据并应用到所有控件。
- 控件支持八个方向拉伸调整大小,自适应任意分辨率,支持键盘方向键微调位置。
- 支持通过串口采集、网络采集、数据库采集三种方式设置数据。
- 代码精简,注释详尽,可作为组态软件的雏形,便于拓展更多功能。
- 纯Qt编写,支持任意Qt版本、任意编译器及任意操作系统。
界面效果

图:属性设计器主界面,展示多种仪表盘控件及其属性设置面板。
核心代码解析
以下代码展示了如何通过Qt的元对象系统(Meta-Object System)动态加载和显示对象的属性,这是实现属性编辑器的关键。如果你对 C++ 的反射和元编程机制感兴趣,可以在云栈社区的C/C++板块找到更多深度讨论。
void QtObjectController::setObject(QObject *object)
{
// 如果设置的控件已是当前控件,则直接返回
if (d_ptr->m_object == object) {
return;
}
if (d_ptr->m_object) {
d_ptr->saveExpandedState();
QListIterator<QtProperty *> it(d_ptr->m_topLevelProperties);
while (it.hasNext()) {
d_ptr->m_browser->removeProperty(it.next());
}
d_ptr->m_topLevelProperties.clear();
}
d_ptr->m_object = object;
if (!d_ptr->m_object) {
return;
}
// 加载父类的属性
d_ptr->addClassPropertiesParent(d_ptr->m_object->metaObject()->superClass());
// 加载当前控件的属性
d_ptr->addClassProperties(d_ptr->m_object->metaObject());
// 恢复节点展开状态
d_ptr->restoreExpandedState();
// 折叠所有节点
d_ptr->collapseAll();
}
void QtObjectControllerPrivate::addClassProperties(const QMetaObject *metaObject)
{
if (!metaObject) {
return;
}
QtProperty *classProperty = m_classToProperty.value(metaObject);
if (!classProperty) {
QString className = QLatin1String(metaObject->className());
classProperty = m_manager->addProperty(QtVariantPropertyManager::groupTypeId(), className);
m_classToProperty[metaObject] = classProperty;
m_propertyToClass[classProperty] = metaObject;
for (int idx = metaObject->propertyOffset(); idx < metaObject->propertyCount(); idx++) {
QMetaProperty metaProperty = metaObject->property(idx);
int type = metaProperty.userType();
QtVariantProperty *subProperty = 0;
// 将英文属性名映射为中文
QString propertyName = metaProperty.name();
propertyName = QtPropertyName::maps.value(propertyName, propertyName);
if (!metaProperty.isReadable()) {
subProperty = m_readOnlyManager->addProperty(QVariant::String, propertyName);
subProperty->setValue(QLatin1String("< Non Readable >"));
} else if (metaProperty.isEnumType()) {
if (metaProperty.isFlagType()) {
subProperty = m_manager->addProperty(QtVariantPropertyManager::flagTypeId(), propertyName);
QMetaEnum metaEnum = metaProperty.enumerator();
QMap<int, bool> valueMap;
QStringList flagNames;
for (int i = 0; i < metaEnum.keyCount(); i++) {
int value = metaEnum.value(i);
if (!valueMap.contains(value) && isPowerOf2(value)) {
valueMap[value] = true;
flagNames.append(QLatin1String(metaEnum.key(i)));
}
subProperty->setAttribute(QLatin1String("flagNames"), flagNames);
subProperty->setValue(flagToInt(metaEnum, metaProperty.read(m_object).toInt()));
}
} else {
subProperty = m_manager->addProperty(QtVariantPropertyManager::enumTypeId(), propertyName);
QMetaEnum metaEnum = metaProperty.enumerator();
QMap<int, bool> valueMap; // 避免显示具有相同值的多个枚举项
QStringList enumNames;
for (int i = 0; i < metaEnum.keyCount(); i++) {
int value = metaEnum.value(i);
if (!valueMap.contains(value)) {
valueMap[value] = true;
// 将枚举键名也强制转为中文
QString enumName = metaEnum.key(i);
enumName = QtPropertyName::maps.value(enumName, enumName);
enumNames.append(enumName);
}
}
subProperty->setAttribute(QLatin1String("enumNames"), enumNames);
subProperty->setValue(enumToInt(metaEnum, metaProperty.read(m_object).toInt()));
}
} else if (m_manager->isPropertyTypeSupported(type)) {
if (!metaProperty.isWritable()) {
subProperty = m_readOnlyManager->addProperty(type, propertyName + QLatin1String(" (Non Writable)"));
}
if (!metaProperty.isDesignable()) {
subProperty = m_readOnlyManager->addProperty(type, propertyName + QLatin1String(" (Non Designable)"));
} else {
subProperty = m_manager->addProperty(type, propertyName);
}
subProperty->setValue(metaProperty.read(m_object));
} else {
subProperty = m_readOnlyManager->addProperty(QVariant::String, propertyName);
subProperty->setValue(QLatin1String("< Unknown Type >"));
subProperty->setEnabled(false);
}
classProperty->addSubProperty(subProperty);
m_propertyToIndex[subProperty] = idx;
m_classToIndexToProperty[metaObject][idx] = subProperty;
}
} else {
updateClassProperties(metaObject, false);
}
m_topLevelProperties.append(classProperty);
m_browser->addProperty(classProperty);
}
void QtObjectControllerPrivate::addClassPropertiesParent(const QMetaObject *metaObject)
{
if (!metaObject) {
return;
}
// 存储需要过滤的父类属性名(例如某些通用属性可能不需要显示)
QStringList keyName;
keyName << "geometry";
QtProperty *classProperty = m_classToProperty.value(metaObject);
if (!classProperty) {
QString className = QLatin1String(metaObject->className());
classProperty = m_manager->addProperty(QtVariantPropertyManager::groupTypeId(), className);
m_classToProperty[metaObject] = classProperty;
m_propertyToClass[classProperty] = metaObject;
for (int idx = metaObject->propertyOffset(); idx < metaObject->propertyCount(); idx++) {
QMetaProperty metaProperty = metaObject->property(idx);
int type = metaProperty.userType();
QtVariantProperty *subProperty = 0;
// 如果当前属性不在允许显示的列表中,则跳过
QString propertyName = metaProperty.name();
if (!keyName.contains(propertyName)) {
continue;
}
propertyName = QtPropertyName::maps.value(propertyName, propertyName);
// ... (属性创建逻辑与addClassProperties类似,此处省略以保持简洁)
classProperty->addSubProperty(subProperty);
m_propertyToIndex[subProperty] = idx;
m_classToIndexToProperty[metaObject][idx] = subProperty;
}
} else {
updateClassProperties(metaObject, false);
}
m_topLevelProperties.append(classProperty);
m_browser->addProperty(classProperty);
}
控件库介绍
本设计器背后是一个丰富的自定义控件库,为可视化开发提供了强大支持。
- 种类丰富:包含超过150个精美控件,涵盖各种仪表盘、进度条、曲线图、标尺、温度计、导航栏等,数量远超Qwt等传统库。
- 独立低耦合:每个控件类独立封装,仅包含一个头文件和一个实现文件,不依赖其他控件代码。你可以轻松地将单个控件以源码形式集成到项目中,极大减少了代码量。
- 跨平台兼容:全部采用纯Qt(QWidget+QPainter)编写,支持Qt 4.6至Qt 5.12的所有版本,兼容MSVC、GCC、MinGW等编译器,可在Windows、Linux、macOS及嵌入式Linux上稳定运行,无乱码问题。
- 易于集成与学习:控件可直接集成到Qt Creator中,像使用内置控件一样拖拽使用。每个控件都有独立的演示程序(DEMO)和一个综合所有控件的集成DEMO。源代码包含详细中文注释,遵循统一规范,是学习 Qt自定义控件开发的优秀资料,欢迎到云栈社区交流相关经验。
- 设计器支持:集成了本文所述的自定义控件属性设计器,支持拖拽设计和XML格式的导入导出,实现所见即所得。
- 灵活部署:所有控件最终可编译生成一个动态库文件(DLL或SO),方便在Qt Creator中直接使用。同时提供ActiveX控件版本,可直接在IE浏览器中运行。
- 持续更新:控件库会不定期增加新控件并完善现有功能,SDK持续更新。
|