6.2.4 二级界面QML绑定代码
本文基于参考代码中IO配置表格的设计思路,针对信号配置表格的核心痛点(如编辑体验不佳、数据一致性差、过滤效率低等),从功能增强、交互优化、数据安全三个维度提出优化方案,确保表格操作高效、稳定、易用。
一、核心优化方向与设计思路
参考代码中已实现可编辑组合框单元格、引脚冲突检查、过滤条件绑定、右键菜单等基础功能。本次优化将在现有思路上深化,重点解决以下问题:① 编辑控件复用性差;② 数据校验不全面;③ 过滤排序灵活性不足;④ 批量操作缺失;⑤ 状态反馈不清晰。
二、具体优化方案
2.1 单元格编辑控件优化——统一封装,提升复用性
参考代码中的`create_type_combo`、`create_pin_combo_for_type`等方法已实现基础控件创建,但存在控件逻辑分散、类型扩展繁琐的问题。优化措施如下:
2.1.1 封装通用编辑控件工厂类
创建`TableWidgetControlFactory`单例类,统一管理各类编辑控件(组合框、文本框、复选框)的创建,减少代码冗余,支持类型扩展。
```cpp
class TableWidgetControlFactory : public QObject {
Q_OBJECT
SINGLETON(TableWidgetControlFactory) // 单例模式
public:
// 通用组合框创建(支持自定义选项列表与数据映射)
QComboBox* createCommonCombo(const QMap<QVariant, QString>& dataMap, const QVariant& currentValue);
// 信号类型专用组合框(预定义信号类型映射,直接复用)
QComboBox* createSignalTypeCombo(int currentType);
// 引脚专用组合框(根据信号类型动态生成选项)
QComboBox* createPinCombo(int signalType, int currentPin);
// 可编辑文本框(支持输入校验,如数字、IP等)
QLineEdit* createValidatedLineEdit(const QString& currentText, const QString& validateType = "");
private:
// 预定义信号类型映射(复用参考代码中signal_type枚举规则)
QMap<QVariant, QString> m_signalTypeMap = {
{0, "未用"},
{1, "按键输入"},
{2, "编码器输入"},
{3, "模拟量输入"},
{16, "数码管输出"},
{17, "LED输出"}
};
};
```
2.1.2 动态适配控件状态
参考`update_io_combo`方法思路,优化控件联动逻辑:当信号类型变更时,自动更新引脚组合框的可选范围与启用状态,无需手动刷新整表。
```cpp
// 信号类型组合框联动处理
void SignalConfigTable::onSignalTypeChanged(int row) {
QComboBox* typeCombo = qobject_cast<QComboBox*>(ui->```cpp
QComboBox* typeCombo = qobject_cast<QComboBox*>(ui->tableWidget->cellWidget(row, signalTypeColumn))dget(row,COLSIGNALTYPE));
QComboBox* pinCombo = qobject_cast<QComboBox*>(ui->tableWidget->cellWidget(row, pinColumn))dget(row,COLPIN));
if (!typeCombo || !pinCombo) return;
int signalType = typeCombo->currentData().toInt();
// 更新引脚组合框的可选范围与启用状态
updatePinCombo(pinCombo, signalType);
}
通过上述优化,提升了编辑控件的复用性和系统的整体稳定性,确保用户在使用信号配置表格时能够获得更好的操作体验和数据管理效率。
```cpp
int signalType = typeCombo->currentData().toInt(); // 1. 清空原有引脚选项,根据信号类型重新生成
pinCombo->clear();
QMap<QVariant, QString> pinOptions = TableWidgetControlFactory::instance()->getPinOptionsBySignalType(signalType);
for (auto it = pinOptions.begin(); it != pinOptions.end(); ++it) {
pinCombo->addItem(it.value(), it.key());
}
// 2. 启用/禁用引脚选择(如"未用"类型禁用引脚)
pinCombo->setEnabled(signalType != 0);
// 3. 自动校验当前引脚是否有效
checkPinConflict(row, pinCombo->currentData().toInt());
// 2.2 数据一致性校验增强——全流程保障,避免无效配置
参考代码中的is_valid和is_pin_used_by_other_knob实现了基础引脚冲突检查,优化后扩展校验维度,覆盖编辑、保存全流程。
// 2.2.1 多维度校验规则
- 引脚冲突校验:复用并优化is_pin_used_by_other_knob,支持批量检查,保存时触发全表校验。
- 必填项校验:信号名称、信号类型、数据类型为必填项,编辑时实时检查,空值时单元格标红提示。
- 数据范围校验:最大值/最小值、引脚编号等数值型字段,限制输入范围(如引脚0-16),超出范围时弹出警告。
- 数据类型匹配校验:根据信号类型自动匹配允许的数据类型(如模拟量输入仅支持浮点型/整型),不匹配时禁用保存。
// 2.2.2 实时校验与状态反馈
// 单元格变化时实时校验
void SignalConfigTable::onCellChanged(int row, int column) {
QTableWidgetItem* item = ui->tableWidget->item(row, column);
if (!item) return;
bool isValid = true;
QString errorMsg = "";
// 根据列类型执行对应校验
switch (column) {
case COL_SIGNAL_NAME: // 信号名称(必填)
``````cpp
if (item->text().trimmed().isEmpty()) {
isValid = false;
errorMsg = "信号名称不能为空";
}
break;
case COL_PIN: // 引脚(冲突+范围)
int pin = qobject_cast<QComboBox*>(ui->tableWidget->cellWidget(row, column))->currentData().toInt();
if (pin < 0 || pin > 16) {
isValid = false;
errorMsg = "引脚范围必须为0-16";
}elseif(ispinusedbyotherknob(row,pin)&&pin!=0){isVa
if (isPinUsedByOtherKnob(pin)) { // 假设存在此函数检查引脚是否被占用
isValid = false;
errorMsg = QString("引脚%1已被占用").arg(pin);
}
break;
// 其他列校验...
// 状态反馈:标红单元格+状态栏提示
item->setBackground(isValid ? Qt::white : Qt::lightGray);
if (!isValid) {
// 假设存在状态栏显示错误信息的函数
showStatusBarError(errorMsg);
}
// 设置背景色为灰色
if (!isValid) {
update_status(errorMsg);
m_hasInvalidData = true;
} else {
m_hasInvalidData = checkAllRowsValid(); // 检查全表是否还有无效数据
}
// 禁用/启用保存按钮
ui->btn_save->setEnabled(!mhasInvalidData)bled(!m_hasInvalidData);
// 2.3 过滤与排序优化——灵活高效,支持多条件组合
参考代码中的过滤功能基于单选复选框和文本输入,优化后支持多条件组合过滤,提升大数据量下的查找效率。
// 2.3.1 多条件组合过滤
放弃原有的“互斥过滤”逻辑,支持信号类型、数据类型、输入/输出、引脚范围等多条件同时生效,过滤条件通过控件组实现:
- 信号类型:下拉复选框(可多选,如同时选择“按键输入”和“编码器输入”)
- 输入/输出:单选按钮组(输入/输出/全部)
- 引脚范围:两个数字输入框(最小-最大,支持空值表示无限制)
- 关键词搜索:文本框(模糊匹配信号名称/描述)
// 2.3.2 过滤逻辑实现
// 应用多条件过滤
void SignalConfigTable::applyMultiFilter() {
// 收集所有过滤条件
QList<int> selectedSignalTypes = getSelectedSignalTypes(); // 多选的信号类型
int ioType
``````cpp
int ioType = getSelectedIoType(); // Input/Output/All
int minPin = ui->edit_min_pin->text().toInt();
int maxPin = ui->edit_max_pin->text().toInt();
QString keyword = ui->edit_keyword->text().trimmed();
// Traverse all rows and show/hide based on matching conditions
for (int row = 0; row < ui->tableWidget->rowCount(); ++row) {
bool match = true;
// 1. Match the signal type
int signalType = qobject_cast<QComboBox*>(ui->tableWidget->cellWidget(row,COLSIGNALTYPE))−>currdget(row, signalTypeColumn))->currentData().toInt();
if (!selectedSignalTypes.isEmpty() && !selectedSignalTypes.contains(signalType)) {
match = false;
}
// 2. Match input/output
if (match && ioType != 2) { // 2 represents all
int io = qobject_cast<QComboBox*>(ui->tableWidget->cellWidget(row,COLIO))−>currdget(row, ioTypeColumn))->currentData().toInt();
if (io != ioType) {
match = false;
}
}
// 3. Match the pin range
if (match && (!ui->edit_min_pin->text().isEmpty() || !ui->edit_max_pin->text().isEmpty())) {
int pin = qobject_cast<QComboBox*>(ui->tableWidget->cellWidget(row,COLPIN))−>currdget(row, pinColumn))->currentData().toInt();
if (pin < minPin || pin > maxPin) {
match = false;
}
}
// 4. Match the keyword
if (match && !keyword.isEmpty()) {
QString signalName = ui->tableWidget->item(row, nameColumn)->text()dget−>item(row,COLSIGNALNAME)−>text();
if (!signalName.contains(keyword, Qt::CaseInsensitive)) {
match = false;
}
}
// Show/hide the row based on the matching result
```
The main changes are:
1. Added the type `int` before `ioType` in the first line as it seems to be missing in the original code.
2. Added comments to make the code more readable.
3. Fixed the inconsistent comment style in some places to make it more uniform. ```cpp
ui->tableWidget->setRowHidden(row, !match);
if (match && !keyword.isEmpty()) {
QString signalName = ui->tableWidget;
if (!signalName.contains(keyword, Qt::CaseInsensitive)) {
match = false;
}
}
// 显示/隐藏行
ui->tableWidget->setRowHidden(row, !match);
// 2.3.3 排序功能增强
// 支持按任意列排序(点击表头触发),针对不同列类型优化排序规则:
// - 数值列(引脚、最大值、最小值):按数值大小排序
// - 文本列(信号名称、描述):按字符串字典序排序
// - 枚举列(信号类型、数据类型):按枚举编号排序(保证逻辑顺序一致)
// 2.4 右键菜单与批量操作优化——提升操作效率
// 参考代码中右键菜单已实现新建/删除/修改,优化后增强状态联动与批量操作支持:
// 2.4.1 菜单状态联动
// 根据表格选中状态动态启用/禁用菜单项,避免无效操作:
void SignalConfigTable::contextMenuEvent(QContextMenuEvent* event) {
if (!ui->tableWidget->underMouse()) return;
QModelIndex index = ui->tableWidget->currentIndex();
// 未选中行:仅启用"新建"
m_remove_action->setEnabled(index.isValid());
m_modify_action->setEnabled(index.isValid());
// 选中多行:启用"批量删除"(新增)
m_batch_delete_action->setEnabled(ui->tableWidget->selectedItems().size() > 1);
m_contextMenu->exec(event->globalPos());
}
// 2.4.2 新增批量操作功能
// - 批量删除:删除所有选中行,删除前弹出确认框,显示待删除行数。
// - 批量导入/导出:参考load_io_configuration_from_file、save_io_configuration_to_file思路,支持CSV/XML格式批量导入导出配置,导入时自动校验数据有效性。
// - 批量复制:复制选中行的配置,支持粘贴为新行(自动修改信号名称避免重复,重置引脚为未分配)。
// 2.5 表格初始化与数据同步优化——确保稳定性与一致性
// 参考initialize_model_adapter思路,优化表格与数据模型的绑定逻辑,解决初始化时数据加载不完整、同步延迟问题:
void SignalConfigTable::initializeTable() {
// 1. 初始化表格结构(列定义)
QStringList headers = {"信号名称", "信号类型", "输入/输出", "数据类型", "引脚", "最大值", "最小值", "描述"};
ui->tableWidget->setColumnCount(headers.size());
ui->tableWidget->setHorizontalHeaderLabels(headers);
}
``````cpp
nCount(headers.size());
ui->tableWidget->setHorizontalHeaderLabels(headers);
// 2. 绑定数据模型与代理(复用model_adapter思路,确保线程安全)
m_configAdapter = new model_adapter(this);
m_configAdapter->setTableWidget(ui->tableWidget);
m_configAdapter->initialize();
odel(mconfigmodel)ping(mconfigmodel−>initheadermap())odel(mconfigAdapter)// 3. 初始化编辑控件(批量创建,提升效率)
for (int row = 0; row < m_config_model->rowCount(); ++row) {
initRowControls(row); // 为每一行创建对应的组合框、文本框等控件
}
// 4. 连接信号槽(确保模型数据变更同步到表格)
connect(m_config_model, &config_model::config_changed, this, [this](int row) {
initRowControls(row); // 数据变更时重新初始化对应行控件
});
connect(m_configAdapter, &model_adapter::layoutChanged, ui->tableWidget, &QTableWidget::resizeColumnsToContents);
// 5. 加载历史配置(自动加载对应设备的信号配置文件)
loadSignalConfigFrom()File(mdevicename,mdeviceip);
// 2.6 用户体验提升——细节优化与状态反馈
- 操作日志记录:参考LOG_INFO、LOG_WARN思路,记录表格的新增、修改、删除、导入导出等操作,支持查看历史记录。
- 编辑状态保存:当用户切换行或关闭窗口时,提示未保存的修改,避免数据丢失。
- 快捷键支持:添加常用操作快捷键(如Ctrl+N新建、Ctrl+D删除、Ctrl+S保存),提升操作效率。
- 状态提示优化:在状态栏实时显示当前表格状态(如“共12条配置,过滤后显示5条”“存在2个无效配置”),点击保存按钮时显示保存结果(成功/失败原因)。
三、优化效果验证标准
1. 编辑体验:控件联动响应时间≤100ms,无卡顿;必填项、范围错误实时提示,准确率100%。
2. 数据一致性:引脚冲突、数据类型不匹配等无效配置无法保存,保存成功率100%。
3. 过滤效率:100条配置数据下,多条件过滤响应时间≤50ms,匹配准确率100%。
4. 操作效率:批量导入100条配置时间≤1s;右键菜单操作无无效点击,状态联动准确。
5. 稳定性:连续编辑、保存、导入导出100次无崩溃;跨线程数据同步无丢失。
四、依赖与注意事项
- 依赖参考代码中的config_model、model_adapter、DeviceXmlHandler等核心类,需确保这些类的接口稳定。
- 批量导入导出时,需统一配置文件格式(建议使用CSV,兼容Excel编辑),并明确字段映射规则。
- 多线程操作时,需复用参考代码中的线程安全机制。
```QMutex锁机制确保表格数据操作的线程安全。
### 6.3 QML与C++交互规范
qml
import QtQuick 2.15
import QtQuick.Controls 2.15
import QtQuick.Layouts 1.15
import ICD.DeviceModel 1.0 // 注册的C++模型
// 主容器(1200×800,边距10px)
Rectangle {
width: 1200
height: 800
color: "#f5f5f5"
padding: 10
// 三列布局(左/中/右,间距10px)
RowLayout {
anchors.fill: parent
spacing: 10
// 左侧:网络配置区(400×800)
ColumnLayout {
width: 400
height: 800
// 省略原有网络配置控件(保持不变)
}
// 中间:信号配置区(600×800)
ColumnLayout {
width: 600
height: 800
spacing: 10
// 模式切换区
RowLayout {
Layout.fillWidth: true
spacing: 5
Button {
text: "测试模式"
highlighted: model.currentMode === "test"
onClicked: model.switchMode("test")
Layout.preferredWidth: 100
}
Button {
text: "配置模式"
highlighted: model.currentMode === "config"
onClicked: model.switchMode("config")
Layout.preferredWidth: 100
}
}
// 信号配置表格
TableView {
id: signalTable
Layout.fillWidth: true
Layout.fillHeight: true
model: DeviceModel {} // 绑定C++模型
columnSpacing: 1
rowSpacing: 1
// 基础列定义(配置/测试模式均显示)
TableViewColumn {
role: "signalId"
title: "信号ID"
width: 80
}
TableViewColumn {
role: "signalName"
title: "信号名"
width: 120
}
TableViewColumn {
role: "pin"
title: "引脚"
width: 60
}
TableViewColumn {
role: "driveType"
title: "驱动类型"
width: 80
}
TableViewColumn {
role: "io"
title: "I/O"
width: 60
}
TableViewColumn {
role: "maxValue"
title: "最大值"
width: 80
}
TableViewColumn {
role: "minValue"
title: "最小值"
width: 80
}
// 测试模式新增列(动态显示)
TableViewColumn {
role: "testValue"
title: "测试值"
width: 80
visible: model.currentMode === "test"
delegate: LineEdit {
text: model.testValue
onTextChanged: model.updateTestValue(model.index, text)
// 数据类型校验(示例:BOOL仅允许0/1)
validator: model.signalDataType === "BOOL"
? IntValidator { bottom: 0; top: 1 }
: (model.signalDataType === "INT" ? IntValidator {} : DoubleValidator {})
}
}
TableViewColumn {
role: "isOperate"
title: "操作"
width: 60
visible: model.currentMode === "test"
delegate: CheckBox {
checked: model.isOperate
onCheckedChanged: model.setOperateStatus(model.index, checked)
}
}
// 右键菜单(选中行触发)
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.RightButton
onClicked: {
if (mouse.button === Qt.RightButton && signalTable.currentRow !== -1) {
contextMenu.x = mouse.x
contextMenu.y = mouse.y
contextMenu.open()
}
}
Menu {
id: contextMenu
MenuItem {
text: "新建"
onClicked: model.addSignalConfig()
}
MenuItem {
text: "修改"
onClicked: model.editSignalConfig(signalTable.currentRow)
}
MenuItem {
text: "删除"
onClicked: model.deleteSignalConfig(signalTable.currentRow)
}
}
}
}
// 数据下发 GroupBox
GroupBox {
Layout.fillWidth: true
title: "数据下发"
ColumnLayout {
spacing: 5
padding: 10
TextEdit {
Layout.fillWidth: true
height: 80
readOnly: true
text: model.sendRawData // 绑定下发原始数据
placeholderText: "暂无下发数据..."
}
RowLayout {
Button {
text: "保存"
onClicked: model.saveSendData(sendRawData.text)
}
Button {
text: "清除"
onClicked: model.clearSendData()
}
}
}
}
}
// 右侧:功能按钮区(200×800)
ColumnLayout {
width: 200
height: 800
spacing: 8
padding: 10
Button { text: "保存配置"; onClicked: model.saveSignalConfig() }
Button { text: "读取配置"; onClicked: model.loadSignalConfig() }
Button { text: "写入配置"; onClicked: model.writeSignalConfig() }
// 分隔线
Rectangle {
Layout.fillWidth: true
height: 1
color: "#e0e0e0"
}
// 开启测试按钮
Button {
text: "开启测试"
enabled: model.currentMode === "test" // 仅测试模式可用
onClicked: model.startTest()
}
// 筛选 CheckBox 组(互斥选中)
CheckBox {
id: nameFilter
text: "信号名过滤"
onCheckedChanged: {
if (checked) { idFilter.checked = false; ioFilter.checked = false; pinFilter.checked = false }
}
}
CheckBox {
id: idFilter
text: "信号ID过滤"
onCheckedChanged: {
if (checked) { nameFilter.checked = false; ioFilter.checked = false; pinFilter.checked = false }
}
}
CheckBox {
id: ioFilter
text: "I/O过滤"
onCheckedChanged: {
if (checked) { nameFilter.checked = false; idFilter.checked = false; pinFilter.checked = false }
}
}
CheckBox {
id: pinFilter
text: "引脚号过滤"
onCheckedChanged: {
if (checked) { nameFilter.checked = false; idFilter.checked = false; ioFilter.checked = false }
}
}
// 筛选输入框
LineEdit {
placeholderText: "输入筛选关键词..."
onTextChanged: {
let filterType = ""
if (nameFilter.checked) filterType = "signalName"
else if (idFilter.checked) filterType = "signalId"
else if (ioFilter.checked) filterType = "io"
else if (pinFilter.checked) filterType = "pin"
if (filterType) model.filterSignalConfig(filterType, text)
}
}
}
}
}
•QML仅通过注册的C++类调用业务接口,不直接操作数据库或协议。
•#### 注册示例:
#### // 主线程启动时注册
qmlRegisterType<DeviceModel>("ICD.DeviceModel", 1, 0, "DeviceModel");
qmlRegisterSingletonType<ConfigFileManager>("ICD.Config", 1, 0, "Config",
[](QQmlEngine* engine, QJSEngine* js) -> QObject* {
Q_UNUSED(engine); Q_UNUSED(js);
return ConfigFileManager::getInstance();
});
•QML绑定模型示例:
### TableView {
model: DeviceModel {}
delegate: Item {
Text { text: model.panelName }
Text { text: model.panelIp }
Button { text: "PING"; onClicked: model.pingDevice(model.panelIp) }
}
}
7 线程安全与跨线程通信
#### 7.1 线程安全规范
•- **共享资源保护**:环形缓冲区、设备队列、数据库、模型等共享资源必须使用QMutex或QReadWriteLock进行保护。
•- **数据传递**:跨线程优先传递值(如QByteArray、结构体拷贝),禁止传递指针或引用。
•- **解析线程轻量化**:解析线程仅进行帧头、CRC和长度校验,耗时逻辑(如指令处理)移交至业务层。
1.- **禁止跨线程操作对象**:线程间仅通过信号槽进行交互,不直接调用对方的成员变量或方法。
2.- **委托与模型交互**:委托仅通过模型的data()和setData()接口读写数据,不直接访问m_devices列表。
3.- **动态状态更新**:监控线程和委托通过updateNetworkStatus()接口更新状态,加锁保护模型数据。
4.- **跨线程数据传递**:PING测试结果、动态状态等通过信号槽传递,使用Qt::QueuedConnection确保线程安全。
- **委托异步交互**:PING测试通过QMetaObject::invokeMethod异步执行,避免阻塞UI线程。
#### 7.2 自定义类型注册
解析后的帧结构体必须注册元类型,否则信号槽无法跨线程传递。
#### // 线程启动前执行
Q_DECLARE_METATYPE(ProtocolFrame);
Q_DECLARE_METATYPE(Device_INFO_t);
qRegisterMetaType<ProtocolFrame>("ProtocolFrame");
qRegisterMetaType<Device_INFO_t>("Device_INFO_t");
7.3 信号槽连接类型
跨线程信号槽强制使用Qt::QueuedConnection,避免直接调用导致线程混乱。
### connect(m_parser, &PacketParser::frameParsed,
m_monitor, &DeviceMonitor::handleFrame,
Qt::QueuedConnection);
8 测试要点
#### 8.1 功能测试
•- **UDP通信**:验证开启/关闭UDP、加入/退出组播的成功率,检查按钮颜色反馈是否正确。
•- **设备管理**:新建/编辑/删除设备,验证数据是否同步到数据库和模型。
3.- **协议指令**:测试读/写IP配置、重启设备、LED控制、恢复默认IP等指令的收发与解析。
4.- **配置/测试模式**:验证配置参数JSON下发、测试指令(重启/LED)的执行结果。
#### 8.2 性能测试
1.- **环形缓冲区**:模拟高并发UDP数据包,验证无数据丢失。
2.- **线程阻塞**:验证解析线程繁忙时,UDP接收线程仍能正常接收数据。
3.- **数据库并发**:多线程读写数据库,验证数据一致性。
#### 8.3 兼容性测试
1.- **不同设备IP**:验证不同面板IP的配置下发、重启生效、恢复默认IP等特殊规则。
2.- **协议兼容性**:验证小端模式、CRC校验、数据长度限制等协议规则的正确性。
### 9 附录
#### 9.1 协议指令常量
// 读操作指令
const quint16 CMD_READ_IP_CONFIG = 0xF001; // 读IP配置
const quint16 CMD_READ_DEVICE_VALUE = 0xF011; // 读器件值
const quint16 CMD_READ_CALIB_INFO = 0xF031; // 读校准信息
// 写操作指令
const quint16 CMD_WRITE_IP_CONFIG = 0x0001; // 写IP配置
const quint16 CMD_WRITE_DEVICE_VALUE = 0x0011;// 写器件值
const quint16 CMD_WRITE_CALIB_INFO = 0x0031; // 写校准信息
const quint16 CMD_TEST = 0x00BB; // 测试指令
// 测试子指令
const quint8 SUB_CMD_RESPONSE = 0x3F; // 设备响应
const quint8 SUB_CMD_RESTART = 0x80; // 重启设备
const quint8 SUB_CMD_LED_ON = 0x81; // 点亮LED
const quint8 SUB_CMD_LED_OFF = 0x82; // 关闭LED
const quint8 SUB_CMD_RESET_IP = 0x83; // 恢复默认IP
整理
最新发布