十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

原理图编辑器从零实现:数据建模与交互渲染的核心设计

原理图编辑器从零实现:数据建模与交互渲染的核心设计 EDA 软件一直是工业软件里“难啃的骨头”之一。很多人听到 EDA第一反应是 Cadence、Altium、KiCad 这些庞然大物觉得原理图编辑器这种功能一定是某个大团队花了好几年才做出来的东西。但实际上如果你把“原理图编辑器”拆开看会发现它最核心的部分并没有那么玄乎。这篇文章要聊的是“手搓 EDA 软件汇报演出”的第二篇原理图编辑器。上一篇我们可能聊了整体框架这一篇专门把原理图编辑器拎出来看看一个能用的、能画元件、能连线、能导网表的原理图编辑器到底是怎么从零搭起来的。先给一个明确判断原理图编辑器的第一版门槛不在图形算法而在数据建模和交互设计。换句话说你能不能用好它取决于你用什么结构去存“元件、引脚、连线”这三样东西而不是你画线的算法有多快。本文会从原理图编辑器的核心概念讲起拆解它的三大核心设计——模型、交互、渲染再给出一套可运行的最小实现思路最后聊聊实际项目中会遇到哪些坑。如果你是正在做 EDA 工具、原理图工具、低代码画布、流程图编辑器或者任何“拖拖拽拽连连线”类应用的开发者这篇文章值得你收藏慢慢看。1. 这篇文章真正要解决的问题1.1 为什么“原理图编辑器”看起来难做原理图编辑器难做不是因为画布难写也不是因为鼠标事件难处理而是因为它要同时兼顾“画图”和“工程语义”。普通绘图工具比如 Visio、draw.io画一个矩形就是矩形连一根线就是一根线。但原理图编辑器里一个矩形代表一个电阻两根线连在一起代表电气导通引脚和引脚之间必须严格匹配。也就是说用户看到的是一张图程序底层跑的却是一张带规则的拓扑图。这个“图”字决定了原理图编辑器和普通画板工具的分野。很多团队第一次做原理图编辑器最容易犯的错误就是把重心放在“画得好看”结果元件一多、连线一复杂数据全乱了。1.2 它真正降低的是哪一类开发成本从成本角度看自研一个原理图编辑器真正能降低的是两类成本集成成本商业 EDA 的编辑器往往很难深度定制如果你们公司做的是垂直领域 EDA比如电缆布线、芯片内部原理图、教学仿真自研小工具比在通用 EDA 上做插件开发要顺手得多。学习成本商业 EDA 工具功能多、菜单深、快捷键复杂。自研编辑器可以只保留你们业务需要的功能把用户的学习曲线压得非常低。当然它不适合的场景也很明确如果你要做的是通用 EDA 全流程工具想靠几个人从零挑战 Altium那不现实。原理图编辑器只是 EDA 里的一环之后还有仿真、PCB 布局布线、规则检查等一大堆事。1.3 什么样的读者最该读这篇文章正在做 EDA 工具、原理图工具方向的技术选型或原型开发。负责低代码平台、流程图编辑器、拓扑编辑器想把交互做得更专业。对“数据模型驱动图形界面”这个思想感兴趣想知道一套应用怎么从数据结构反推界面设计。读完这篇文章你能得到三样东西原理图编辑器的整体架构认知。一套最小可运行的代码实现思路。一份常见问题排查清单和工程化建议。2. 原理图编辑器的基础概念与核心原理2.1 它和 PCB 编辑器、仿真器的区别先划清边界。一个完整的 EDA 工具链通常包含模块作用核心输出原理图编辑器以图形化方式表达电路连接关系原理图文件、网表仿真器对电路进行行为/瞬态/频域分析波形、仿真报告PCB 编辑器把器件摆到板子上并布线Gerber 文件、BOM规则检查器检查电气规则、制造规则DRC/ERC 报告原理图编辑器在整个链条里的定位是“前端交互层 数据模型层”。它不负责算电路也不负责画 PCB它只负责两件事让用户能直观地放置元件、连线。让这些“图形”变成结构化的电路数据网表。理解了这个定位你就知道为什么建模比渲染更重要如果底层数据不能完整描述“哪些引脚被连在一起”你画得再漂亮也没用。2.2 核心概念引脚、网络、元件实例在看任何原理图编辑器代码之前建议先把这三个概念刻在脑子里。引脚Pin元件上的接线点。每个引脚有名字如 1、2、3 或 A、B、C、方向输入、输出、双向、电气类型电源、地、信号等属性。原理图里那些“小短线”就是引脚的可视化表现。元件实例Component Instance被放置到原理图中的元件是某个元件库元件的具体“拷贝”。同一个电阻你放三次图上是三个实例但库里的定义只有一个。实例需要有自己的编号如 R1、R2、C1因为同一个元件出现多次编号必须唯一。网络Net一组电气上连通的引脚集合。你把电阻的 1 脚和 LED 的阳极连起来它们就属于同一个网络。网表的本质就是描述这些网络的连接关系。2.3 核心设计模型—交互—渲染三层分离原理图编辑器想做好最好从一开始就采用“模型—交互—渲染”三层分离的结构模型层Model存元件实例、引脚、连线的拓扑关系。这是数据的唯一真相。交互层Controller / Command处理鼠标点击、拖拽、框选、按键。交互层不应该直接改图形而是改模型或者发送命令。渲染层View / Renderer把模型画到屏幕上。每次模型变化渲染层刷新。这三层不分离的后果是什么最典型的情况是你拖一个元件图上位置变了但内部连接的引脚的“电路关系”没更新或者你拉一根线把两个引脚连起来图形看着有根线但导出网表时发现网络是空的。从第一个版本开始就坚持“模型驱动渲染”能避免掉后面 80% 的麻烦。2.4 门级、符号级、物理级的粒度差异再补一个稍微进阶的概念。不同 EDA 领域“原理图”的含义有不同的粒度门级Gate Level面向数字电路每个门是一个基本单元。符号级Schematic Symbol Level通用电路设计每个元件是一个带引脚的符号。物理级Physical Level面向 PCB 或芯片版图关注封装、焊盘、层叠。本文讲的是符号级的原理图编辑器也就是最通用的一种。如果你要做芯片内部的原理图查看器可能还要考虑层次化设计Hierarchical Design、总线Bus、分页图纸Sheet这些更复杂的东西。3. 原理图编辑器的环境准备与前置条件用“手搓”的方式实现一个原理图编辑器语言和框架选型很自由。网页端可以选 Vue/React Canvas/SVG桌面端可以选 Qt/C、Python PySide6、C# WPF。为了演示方便并且让没有 C 经验的读者也能读懂本文以一个 Python PySide6 的示例为主线。3.1 环境依赖Python 3.9 及以上。PySide6Qt for Python6.x。操作系统Windows / Linux / macOS 均可。安装命令pip install PySide6版本说明PySide6 的大版本 6.x 都可以跑通本文代码。如果网络环境受限可以换成 PyQt6API 基本一致只需改动 import 部分。3.2 为什么选 Qt 而不选纯 Web并不是说 Web 不行而是 Qt 的QGraphicsView/QGraphicsScene框架对“原理图编辑器”这类应用非常友好它自带图元Item、场景Scene、视图View三层结构天然适合做“模型—交互—渲染”分离。尤其是它的QGraphicsScene直接支持选中、拖拽、分组、碰撞检测能省掉大量造轮子的时间。相比之下Web 端 Canvas 方案需要自己实现命中测试和选中逻辑工作量会大不少。当然Web 端的跨平台分发优势也很明显最终选型要看你产品的分发场景。本文用 Qt 演示重点是讲清原理语言层面迁移到 Web 并不难。3.3 最小工程结构在开始写代码前先建议这样一个目录结构schematic_editor/ ├── main.py # 程序入口 ├── model/ │ ├── __init__.py │ ├── component.py # 元件实例模型 │ ├── pin.py # 引脚模型 │ └── net.py # 网络模型 ├── view/ │ ├── __init__.py │ ├── scene.py # QGraphicsScene 子类 │ └── items.py # 元件图元、连线图元 └── controller/ ├── __init__.py ├── commands.py # 命令 / 操作封装 └── editor.py # 编辑器主窗口这个结构不是强制的但它能帮你保持“数据模型在 model 目录画布相关在 view 目录命令操作用 controller 目录”的边界感。对一个会持续演进的项目来说分层是性价比最高的决定。4. 原理图编辑器核心流程拆解下面我们把原理图编辑器最常走的几条操作路径拆开来看。不管你的编辑器最终功能多复杂背后都是这几条流程的组合。4.1 从元件库摆放元件到画布流程大致是用户在左侧元件库中点选一个元件条拖到画布上或者双击后出现在画布中央。这一步要完成从元件库定义中克隆出一个“实例”生成唯一 ID。初始化实例坐标、旋转角度。把实例加入模型层。通知渲染层刷新。这里最容易被忽略的是“克隆”这个动作。元件库里的定义是模板不能直接把模板当实例用。否则你旋转 A 电阻B 电阻也跟着转了。4.2 拖动元件与连线跟随用户拖动一个元件连在它引脚上的线要跟着动。这个听起来很简单但如果连线只是“画在图上的一段线段”那你必须手动维护“这个线段的端点绑定在哪个引脚上”。正确做法是连线图元在构造时记录两端的引脚引用。当引脚坐标变化时连线端点自动 update 到引脚位置。这里的关键是“引用”而不是“坐标值”。如果存的是坐标值引脚一移动线就脱离。4.3 引脚连线与网络合并连线是原理图编辑器里最核心的操作。用户从引脚 A 拉线到引脚 B背后发生的其实是创建一根连线Wire记录起点引脚和终点引脚。把引脚 A 所在网络和引脚 B 所在网络合并成一个网络。如果两个引脚本就在同一个网络连线就是“冗余”的但不应该报错。合并网络这件事是原理图编辑器和普通画板的最大区别。你可以用一个简单的并查集Union-Find来维护网络关系也可以直接用一个 dict 保存“网络 ID - 引脚 ID 列表”。并查集写起来更快在重连、删除线时更省心。4.4 框选、移动、复制、删除这些常规编辑操作核心要注意的就是必须保持模型的一致性。比如你删除一个元件和它相连的线要一起删掉网络关系要更新你复制一组元件新元件要生成新的实例 ID引脚之间的内部连接要保留但对外连接“断掉”。在代码实现上建议把所有操作都实现为 Command 模式命令模式。每个操作都是“可执行 可撤销”的。虽然第一版可能用不上撤销但命令模式的引入会让代码结构好很多。4.5 导出网表网表是原理图编辑器对外最重要的产品输出。一个最简单的网表格式可以长这样Net: VCC R1.1 C1.1 Net: GND R1.2 C1.2 Net: NODE_3 R1.2 LED1.A实现时遍历所有网络每个网络列出所有引脚的“元件编号.引脚名”。这里也能顺带做一个 DRC设计规则检查如果一个网络里只有一个引脚说明有引脚悬空或漏连可以给出警告。5. 完整示例与代码实现这部分我们做一个最小但能跑的示例实现下列功能在画布上摆放两个电阻元件和一个 LED。元件可以拖动。给元件添加引脚并支持连线。在控制台输出当前连线的网络连接关系。为了控制文章篇幅代码只保留核心内容但所有代码可以合并成一个脚本直接运行。5.1 主程序入口# 文件路径schematic_editor/main.py import sys from PySide6.QtWidgets import QApplication, QMainWindow from view.scene import SchematicScene from PySide6.QtWidgets import QGraphicsView from PySide6.QtCore import Qt from model.component import Component from model.net import NetManager class MainWindow(QMainWindow): def __init__(self): super().__init__() self.setWindowTitle(手搓 EDA - 原理图编辑器) # 场景与视图 self.scene SchematicScene() self.view QGraphicsView(self.scene) self.setCentralWidget(self.view) self.resize(1200, 800) # 初始化数据 self.net_manager NetManager() self._build_demo() def _build_demo(self): r1 Component(R1, pos_x150, pos_y150) r1.add_pin(1, 0, 0) r1.add_pin(2, 60, 0) self.scene.add_component(r1) led Component(LED1, pos_x350, pos_y150) led.add_pin(A, 0, 0) led.add_pin(K, 60, 0) self.scene.add_component(led) if __name__ __main__: app QApplication(sys.argv) win MainWindow() win.show() sys.exit(app.exec())这里要注意SchematicScene是场景子类需要提供add_component方法。为了保持可读性演示中并未严格拆分 model/view 目录但概念上仍然分层。5.2 元件与引脚模型# 文件路径schematic_editor/model/component.py from dataclasses import dataclass, field from typing import List dataclass class Pin: name: str x: float y: float component_id: str def scene_pos(self, comp_x: float, comp_y: float): return comp_x self.x, comp_y self.y dataclass class Component: id: str pos_x: float 0.0 pos_y: float 0.0 pins: List[Pin] field(default_factorylist) def add_pin(self, name: str, x: float, y: float): self.pins.append(Pin(namename, xx, yy, component_idself.id))这是模型层。Component持有元件坐标和引脚列表引脚坐标是相对元件的局部坐标这样元件整体拖动时只要把局部坐标加上元件坐标就能得到引脚在画布上的绝对坐标。NetManager负责网络关系这里用一个简化版本# 文件路径schematic_editor/model/net.py from typing import Dict, Set class NetManager: def __init__(self): self.net_pins: Dict[str, Set[str]] {} self.pin_net: Dict[str, str] {} def create_net(self, pin_key: str) - str: net_id fnet_{len(self.net_pins) 1} self.net_pins[net_id] {pin_key} self.pin_net[pin_key] net_id return net_id def merge_nets(self, pin_a: str, pin_b: str): net_a self.pin_net.get(pin_a) net_b self.pin_net.get(pin_b) if net_a is None and net_b is None: net_id self.create_net(pin_a) self.net_pins[net_id].add(pin_b) self.pin_net[pin_b] net_id elif net_a is None: self.net_pins[net_b].add(pin_a) self.pin_net[pin_a] net_b elif net_b is None: self.net_pins[net_a].add(pin_b) self.pin_net[pin_b] net_a elif net_a net_b: pass # 已经在一个网络里这里不做合并 else: # 合并网络把小网合并到大网 merged self.net_pins[net_a] | self.net_pins[net_b] self.net_pins[net_a] merged for pk in self.net_pins[net_b]: self.pin_net[pk] net_a del self.net_pins[net_b] def dump(self): print( Net List ) for net_id, pins in self.net_pins.items(): print(f{net_id}: {sorted(pins)})这段代码只是演示网络合并的思路。真正项目里网络 ID 和引脚 ID 要更严谨合并时还要考虑历史记录、撤销恢复、电气规则检查等。但核心逻辑就是“两个引脚一连接它们所在的节点集合就合并”。5.3 场景与图元视图层使用QGraphicsScene并自定义两个图元元件图元和连线图元。# 文件路径schematic_editor/view/scene.py from PySide6.QtWidgets import QGraphicsScene, QGraphicsRectItem, QGraphicsLineItem, QGraphicsTextItem, QGraphicsItem from PySide6.QtCore import Qt, QRectF, QPointF from PySide6.QtGui import QPen, QBrush, QColor from model.component import Component class ComponentItem(QGraphicsRectItem): def __init__(self, component: Component): w, h 120, 60 super().__init__(0, 0, w, h) self.component component self.setFlags( QGraphicsItem.ItemIsMovable | QGraphicsItem.ItemIsSelectable | QGraphicsItem.ItemSendsGeometryChanges ) self.setBrush(QBrush(QColor(#d9ead3))) self.setPen(QPen(QColor(#333333), 1.5)) self._init_pins() def _init_pins(self): for pin in self.component.pins: px pin.x py pin.y 28 line QGraphicsLineItem(0, py, 12, py, self) line.setPen(QPen(QColor(blue), 2)) txt QGraphicsTextItem(pin.name, self) txt.setPos(0, py - 10) def itemChange(self, change, value): if change QGraphicsItem.ItemPositionChange: self.component.pos_x value.x() self.component.pos_y value.y() return super().itemChange(change, value) class WireItem(QGraphicsLineItem): def __init__(self, pin_a_key: str, pin_b_key: str, scene: QGraphicsScene): super().__init__() self.pin_a_key pin_a_key self.pin_b_key pin_b_key self.scene scene self.setPen(QPen(QColor(black), 1.8)) self.update_position() def update_position(self): pt_a self._pin_scene_pos(self.pin_a_key) pt_b self._pin_scene_pos(self.pin_b_key) if pt_a and pt_b: self.setLine(pt_a.x(), pt_a.y(), pt_b.x(), pt_b.y()) def _pin_scene_pos(self, pin_key): for item in self.scene.items(): if isinstance(item, ComponentItem): for pin in item.component.pins: if pin_key f{item.component.id}.{pin.name}: return QPointF(pin.x item.x(), pin.y item.y() 28) return None class SchematicScene(QGraphicsScene): def __init__(self, parentNone): super().__init__(parent) self.conn_items [] self.selected_pin_key None def add_component(self, component: Component): item ComponentItem(component) item.setPos(component.pos_x, component.pos_y) self.addItem(item) def connect_pins(self, pin_a_key: str, pin_b_key: str): wire WireItem(pin_a_key, pin_b_key, self) self.addItem(wire) self.conn_items.append(wire)这段代码有个简化元件图元里的引脚线实际上是画在元件矩形内部的端子而不是独立可点击的图元。真实项目里引脚应该是独立的可交互子图元。这里为了演示可以把连线逻辑放在场景的鼠标事件里。5.4 鼠标事件点击引脚连线给SchematicScene加鼠标事件实现“点到引脚时记录起点再点到另一个引脚时完成连线”# 继续在 SchematicScene 中添加 def mousePressEvent(self, event): pos event.scenePos() # 找最近引脚 pin_hit self._hit_pin(pos) if pin_hit is not None: if self.selected_pin_key is None: self.selected_pin_key pin_hit print(f选中起始引脚: {pin_hit}) else: print(f连线: {self.selected_pin_key} - {pin_hit}) # 网络合并 from model.net import NetManager NetManager().merge_nets(self.selected_pin_key, pin_hit) self.connect_pins(self.selected_pin_key, pin_hit) self.selected_pin_key None else: super().mousePressEvent(event) def _hit_pin(self, pos, threshold15.0): for item in self.items(): if isinstance(item, ComponentItem): for pin in item.component.pins: px pin.x item.x() py pin.y item.y() 28 dx pos.x() - px dy pos.y() - py if dx * dx dy * dy threshold * threshold: return f{item.component.id}.{pin.name} return None这里为了演示每次合并都 new 了一个NetManager这显然不是正确用法。正确做法是让场景持有同一个NetManager实例。把以下逻辑理顺class SchematicScene(QGraphicsScene): def __init__(self, parentNone): super().__init__(parent) self.conn_items [] self.selected_pin_key None self.net_manager NetManager()鼠标事件里也改成调用self.net_manager.merge_nets(...)。5.5 导出网表在NetManager里增加dump方法就能输出网表。在实际使用中可以把这个输出重定向到文件也可以做成菜单操作。到这里一个最小原理图编辑器已经能跑起来了。你可以摆元件、拖元件、点引脚连线、看网表。是不是比想象中简单别急它只是“能跑”距离“能用”还有不少距离下一节就是排查和工程化的内容。6. 运行结果与效果验证6.1 如何运行接着上面的代码运行python main.py预期会弹出一个窗口画布上有三个元件R1、R2实际上演示代码里只有一个 R1 和一个 LED如果想加 R2 就再调用一次 add_component 并换个坐标。6.2 如何验证成功手动执行三步验证拖动一个元件引脚端子的短线会跟随元件整体移动。点击一个引脚控制台输出“选中起始引脚: R1.1”再点击另一个引脚控制台输出“连线: R1.1 - LED1.A”。点击工具栏或菜单触发net_manager.dump()控制台会打印所有网络及引脚。如果第二步点击引脚没有反应先排查是不是_hit_pin的坐标算错了。这里最容易错的是引脚局部坐标到场景坐标的换算。元件图元本身有item.x()/item.y()再加上 pin 的局部偏移才是场景坐标。6.3 失败时第一步看哪里窗口起不来查看 PySide6 是否安装成功Python 版本是否满足。元件不显示检查add_component时是否调用了self.addItem(item)。连线后线不刷新检查WireItem.update_position是否被调用或者坐标换算是否用了旧坐标。网络合并结果不对在merge_nets里加 print 调试看每次连接两个引脚的 net_id 是否正确。7. 原理图编辑器常见问题与排查方法问题现象可能原因排查方式解决方案拖动元件后连线断开了连线端点存的是坐标值而非引脚引用检查 WireItem 是否持有 pin_key 并每次更新坐标改为在 itemChange 里触发所有关联连线 update_position引脚点击不灵敏引脚图元太小或命中检测范围太小调大 threshold 或在引脚周围添加透明热区为引脚添加更大的透明点击区域删除元件后残留连线删除操作只删了元件图元没删连线图元检查删除逻辑是否同时清理了该元件引脚上的连线写一个统一的 remove_component 方法遍历引脚并删除关联连线网络合并结果错乱多个连线重复创建网络打印每个引脚 pin_net 映射合并前先判断两个引脚是否已经属于同一网络撤销/重做无法恢复连线没有使用命令模式操作直接改模型重构所有模型变更操作为 Command引入 undo/redo 栈复制元件后编号冲突复制时没有生成新的实例 ID检查复制逻辑是否复用旧 ID复制时生成新 UUID 并重新编号画布缩放后引脚难以点击命中检测用的是缩放前坐标检查是否把 viewport 坐标映射为 scene 坐标后再做命中测试使用 event.scenePos() 而不是 viewport 坐标如果你遇到过“明明元件画得很标准但一放大就点不准”的问题大概率就是坐标映射这一环出了问题。Qt 的event.scenePos()已经帮我们处理了视图缩放但如果自己写的是鼠标原始像素坐标那就要万分小心。7.1 引脚热区设计上面表格里提到热区这是原理图编辑器里非常实用的一个细节。引脚的可视化通常只是一根 10 到 20 像素的短线鼠标很难精准点中。成熟工具的做法是在引脚周围放置一个透明矩形或圆形把它们当作事件的真正接收者。这样既不影响视觉又能提升点击体验。在 Qt 里可以给引脚图元设置半透明的boundingRect但不绘制具体内容class PinItem(QGraphicsItem): def boundingRect(self): return QRectF(-8, -8, 16, 16) def paint(self, painter, option, widgetNone): pass # 不画保持透明这样命中区域是 16x16 像素比单根 2 像素的线好点多了。7.2 缩放和鼠标悬停提示在实际产品里缩放功能几乎是刚需。QGraphicsView自带scale方法可以直接支持滚轮缩放。要注意的是缩放后线宽和字体是否也要跟着缩放。通常原理图编辑器里线宽和引脚名不应随缩放剧烈变化否则当缩小到 10% 时图纸会变成一团黑。处理方式是在图元 paint 时反转 view 的缩放系数或者在 view 层重写 drawItems 相关逻辑。这个话题展开很深第一版可以先用“整体缩放”顶着后面再优化。8. 最佳实践与工程建议8.1 坚持模型驱动渲染这是最重要的一条也是全文反复强调的点。不要在做第二版时才后悔第一版把坐标写死。模型层是唯一真相源视图层只是模型的可视化投影。不管用户做什么操作最后落到底层都应该是模型变化再触发对应的刷新。这样做的好处是撤销重做好实现。多人协同好同步。导出网表、仿真对接时不用关心界面状态。自动化测试可以直接对模型做断言不用模拟鼠标。8.2 用命令模式封装所有编辑操作在模型驱动的基础上建议所有修改性操作都走命令模式。一个命令至少包含execute()和undo()两个方法。例如“放置元件”是一个命令“删除元件”是一个命令“连线”是一个命令。这样当用户不小心删错了线时可以直接撤销不用手工重连。命令模式第一版可能稍显繁琐但一旦数据结构变复杂它是最省心的方案。8.3 并查集或合适的数据结构维护网络关系前面 NetManager 例子用的是 dict 合并实际上网络关系更适合用并查集class UnionFind: def __init__(self): self.parent {} def find(self, x): if x not in self.parent: self.parent[x] x return x if self.parent[x] ! x: self.parent[x] self.find(self.parent[x]) return self.parent[x] def union(self, a, b): ra, rb self.find(a), self.find(b) if ra ! rb: self.parent[rb] ra并查集在“两个引脚是否连通”的查询上更快而且在大量连线、删除线、重新布局时维护成本更低。原理图规模一大这种数据结构层面的选择会直接影响编辑器流畅度。8.4 网络 ID 的持久化策略每次导出网表你是希望网络名稳定还是允许重新编号如果希望稳定那在网络合并、拆分、删除时要保留最小的网络 ID并维护好“网络名 - 网络成员”的映射。另外用户通常希望给网络起名比如 VCC、GND这个功能背后就是网络表里的 Name 属性。建议在模型层给 Net 加一个name字段默认自动生成双击网络时允许重命名。8.5 引脚命名与编号规范引脚名是原理图编辑器连接规则的核心。建议从第一版就统一pin_name的命名规则用R1.1、R1.2这种“实例 ID.引脚名”作为全图唯一引脚标识。不要在多个地方维护“元件引用 引脚索引”的字符串拼接方式容易出错。如果支持位号数量变化引脚 ID 的规则要提前设计好。8.6 对齐、网格吸附与坐标精度网格吸附是原理图编辑器用户的习惯性预期拖动元件时元件和引脚应该自动吸附到网格点。这样做出来的原理图整齐美观也方便后续的自动布线。实现方式很简单在itemChange里对坐标做取整snap 10 value.setX(round(value.x() / snap) * snap) value.setY(round(value.y() / snap) * snap)但这里要小心如果元件吸附了引脚上的连线端点要同步更新否则线会歪。所以连线更新必须监听元件位置变化而不是等鼠标释放后再重绘。8.7 性能优化方向当原理图有几千个元件、上万根连线时Qt 的QGraphicsScene默认性能可能会下降。可以考虑关闭不必要的图元反锯齿。使用 LOD细节层次缩小到一定程度时简化图元绘制。只重绘可见区域内的图元而不是全场景刷新。对连线采用分段缓存避免每帧重算坐标。不过如果只是做工具类产品几千个元件通常够用不必过早优化。8.8 面向团队协作的版本管理原理图文件本质是文本或二进制数据。如果你们的产品需要多人协作建议用文本格式如 JSON 或自定义 DSL保存原理图方便 diff。引入乐观锁或指令级同步。为每个元件、连线、网络设置全局唯一 ID避免合并冲突。如果你只做单机版这个可以往后放但文件格式建议一开始就用 JSON 而不是自定义二进制否则后续做协同、做导入导出都会很痛苦。9. 总结与后续学习方向这篇文章的核心其实可以用一句话概括原理图编辑器不是一个绘图软件而是一个带图形界面的电路数据建模工具。我们从零拆解了原理图编辑器的三大核心设计——模型、交互、渲染讲清楚了引脚、元件实例、网络这三个基础概念然后给了一个最小可运行的 Python PySide6 示例覆盖了摆放元件、拖动元件、引脚连线、网络合并、导出网表这几条关键路径。最后整理了常见问题排查表和工程化建议。如果你是把这篇文章当作技术选型和可行性验证的参考那么下一步可以把这个最小原型扩展成三件事加入撤销/重做能力让操作可以被回退这是工具类软件的底线功能。加入元件库管理让元件不再硬编码在代码里而是从库中加载。加入网表导出对接把模型层的网络数据实际输出成后续仿真或 PCB 导入所需的格式。如果你之前没接触过 EDA只是做低代码画布或拓扑编辑器这篇文章里的模型抽象、命令模式、网络合并逻辑也同样可以直接借鉴。画布只是壳数据模型才是灵魂这个判断在任何一个“拖拽连线”类工具里都成立。最后提醒一句不要急着在一个版本里实现所有功能。原理图编辑器真正难的不是第一版而是后续不断叠加的电气规则、层次化设计、多人协同、庞杂的元件库。把模型层和交互层从第一天就分开你会感谢自己这个决定。
返回列表