引言
本文是我在学习 Qt 开发和 CAN 总线应用过程中的实践总结,基于 Qt 5.12.2 开发 CAN 总线数据分析软件。本文将聚焦软件核心功能 ——CAN 总线数据与 QGraphicsScene 线条光点的精准联动,详细说明如何通过唯一标识匹配数据流向,并展示当前功能进度与后续优化方向。代码和思路还有很多可以优化的地方,欢迎各位大佬在评论区多多指点,一起交流进步。
效果展示


一、软件整体架构与核心功能规划
1. 整体界面布局设计
软件采用 **“左 – 中 – 下 – 右” 四段式布局 **,结合 QGraphicsScene 图形化能力与规则配置逻辑,实现 “协议规则查看 – 可视化画布 – 数据日志 – 参数配置” 的完整工作流:
| 左侧:协议规则区 | QTreeWidget | 展示 CAN 协议规则(帧 ID、协议标识、数据范围),为右侧配置提供唯一标识依据 |
| 中间:可视化画布 | QGraphicsScene + QGraphicsView | 核心展示区,承载 CAN 通道、电气设备、数据控件节点,通过蓝色线条连接节点,在指定线条上显示数据流向光点 |
| 底部:设备数据区 | QTableWidget | 实时记录 CAN 数据接收日志(时间、端口、帧 ID、原始数据),便于调试与追溯 |
| 右侧:信息配置区 | QListWidget + QTableWidget | 上半区展示画布中选中节点;下半区配置节点唯一标识、协议属性、收发关系,确保数据精准流向对应线条 |
2. 核心功能实现进度
✅ 已实现功能:
⏳ 待完善功能:
二、核心技术实现
环境准备
- Qt 版本:5.12.2(MinGW 7.3.0 32/64 位,兼容工业级开发环境);
- 核心模块:QtWidgets(界面)、QtCore(线程、容器)、QtSerialPort/SocketCAN(CAN 通信);
- 关键约束:避免使用 C++11 auto 关键字,确保与 Qt 5.12.2 兼容。
模块 1:线条唯一标识与公共存储
核心逻辑:节点拖拽连线时,自动生成 起点设备名_终点设备名 格式的唯一标识,存入全局 QList,为后续数据匹配提供依据。
1. 生成线缆唯一ID(LinkItem.h)
//生成线缆唯一ID(基于两端节点设备名)
void LinkItem::generateLinkId()
{
if (!m_startNode || !m_endNode) {
qWarning() << "[LinkItem] 起点/终点节点为空,无法生成唯一ID!";
return;
}
// 格式:起点设备名_终点设备名(保证全局唯一)
QString startName = m_startNode->getDeviceChart();
QString endName = m_endNode->getDeviceChart();
QString autoLinkId = QString("%1_%2").arg(startName).arg(endName);
setLinkId(autoLinkId);
}
2. 匹配CAN接口线缆的光点(GraphicData.h)
bool GraphicData::upData_GraphicPublicData_LinkItem(const QString& port, const QString& id, const QString &canData)
{
Q_UNUSED(canData);
//判断MClassCanInfo公共数据中port:并进行匹配
if(MClassCanInfo.contains(port)){
//取出关于CAN接口下存储的所有ClassCanInfo
QList<GraphicCanClass *> LGraphicCanClass = MClassCanInfo[port];
for (int i = 0; i < LGraphicCanClass.size(); i++){
//取出单个ClassCanInfo
GraphicCanClass * graphicCanClass = LGraphicCanClass.at(i);
//判断ID是匹配
if(graphicCanClass->Name == id){
//循环遍历LLinkItem中的存储线条
for (int i = 0; i < LLinkItem.size(); i++) {
//ID判断:获取线条ID,并匹配->graphicCanClass中存储的信息
if(LLinkItem.at(i)->getLinkId() == graphicCanClass->sender+"_"+graphicCanClass->recipient
|| LLinkItem.at(i)->getLinkId() == graphicCanClass->recipient+"_"+graphicCanClass->sender){
//获取线条中的起始节点
GraphicNode_Bus* graphicNode = LLinkItem.at(i)->getStartNode();
//获取该节点的widget类型
if(getWidgetObjectName(graphicNode->getDeviceChart()) == "Can_Port_Chart"){
//判断:如果是Device_Chart则正向,反之
if(LLinkItem.at(i)->addAndStartLightPoint(id, graphicCanClass->timeN.toInt(), QColor(0,255,0), Direction_Forward,1,id)){
return true;
}
}
else{
if(LLinkItem.at(i)->addAndStartLightPoint(id,graphicCanClass->timeN.toInt(),QColor(0,255,0),Direction_Backward,1,id)) {
return true;
}
}
}
}
return true;
}
}
}
return false;
}
3. 匹配数据控件线缆的光点(GraphicData.h)
void GraphicData::upData_GraphicPublicData_LightPoint()
{
QMap<QString, QList<Result_info> > MResult_info;
MResult_info = PublicRulesCollector::getInstance().getTempRulesInFo();
QMap<QString,QList<Result_info>>::iterator itor1;
for (itor1 = MResult_info.begin(); itor1 != MResult_info.end(); ++itor1){
QList<Result_info> LResult_info = itor1.value();
for(int i = 0; i < LResult_info.size(); i++){
Result_info result_info = LResult_info.at(i);
//4. 遍历视图中的widget中的code名称并进行匹配
QMap<QString,QWidget*>::iterator it3;
for (it3 = MWidget.begin(); it3 != MWidget.end(); ++it3){
QWidget* targetWidget = it3.value();
if (Can_Port_Chart *can_port_chart = qobject_cast<Can_Port_Chart*>(targetWidget)){
//匹配code进行数据更新
if(result_info.code == can_port_chart->getCodeName()){
//更新控件数据
}
}
if (Device_Chart *deviceChart = qobject_cast<Device_Chart*>(targetWidget)){
if(result_info.code == deviceChart->getCodeName()){
//更新控件数据
}
}
else if(Dashboard_Chart *DashboardChart = qobject_cast<Dashboard_Chart*>(targetWidget)){
if(result_info.code == DashboardChart->getCodeName())
{
//更新控件数据
}
}else if(State_Chart *StateChart = qobject_cast<State_Chart*>(targetWidget)){
if(result_info.code == StateChart->getCodeName())
{
//更新控件数据
}
}
else if(Value_Chart *ValueChart = qobject_cast<Value_Chart*>(targetWidget)){
if(result_info.code == ValueChart->getCodeName()){
for (int i = 0; i < LLinkItem.size(); i++) {
if(LLinkItem.at(i)->getLinkId().contains(ValueChart->getTiteText())){
//获取线条中的起始节点
GraphicNode_Bus* graphicNode = LLinkItem.at(i)->getStartNode();
//获取该节点的widget类型
if(getWidgetObjectName(graphicNode->getDeviceChart()) == "Device_Chart") {
//判断:如果是Device_Chart则正向,反之
if(LLinkItem.at(i)->addAndStartLightPoint(result_info.code,900,QColor(0,255,0),Direction_Forward, 1,result_info.code)){
break;
}
}
else{
if(LLinkItem.at(i)->addAndStartLightPoint(result_info.code,900,QColor(0,255,0),Direction_Backward, 1,
result_info.code)) {
break;
}
}
}
}
}
}
}
}
}
}
三、功能演示与效果说明
结合代码实现,软件运行效果与截图完全匹配:
四、后续功能优化方向
1. 大数据量光点显示优化
- 数据聚合:相同协议数据,仅触发一次光点动画,避免界面卡顿;
- 超时清理:光点动画结束后自动删除,避免堆积;
- 高频高亮:高频数据对应的光点使用红色高亮,突出数据频率。
2. CAN 数据发送功能完善
- 定时发送:为电气设备配置发送周期,自动发送指定协议数据;
- 手动触发:在数据控件上添加 “发送” 按钮,手动触发单条 CAN 数据发送;
- 发送可视化:发送数据的光点使用橙色,与接收数据的绿色光点区分,直观展示收发流向。
3. 性能与体验优化
- 线程分离:将 CAN 数据接收、解析放入独立线程,避免阻塞主线程;
- 配置持久化:优化节点配置、线条关系保存为 JSON 文件,下次启动自动加载;
- 异常提示:对未配置唯一标识的节点,在界面上给出红色警告提示。
五、总结
本文基于 Qt 5.12.2 实现了 CAN 总线数据与 QGraphicsScene 线条光点的精准联动,通过唯一标识匹配和公共线条列表遍历,解决了数据流向可视化的核心问题。当前软件已完成基础接收与展示功能,后续将聚焦大数据量优化与主动发送功能,进一步提升工业级场景下的实用性。
本文旨在为同领域开发者提供可复用的实现思路,也欢迎各位在评论区交流 Qt 图形化开发、CAN 总线通信的技术细节,共同完善工业级数据可视化工具。

![【题解】[COCI 2025/2026 #6] 滑雪 / Skijanje(李超树 0 基础友好喵)-171主机测评](https://www.171host.com/wp-content/uploads/2026/08/20260826083930-6a8ea642697bc-220x25.png)

