<think>我们需要实现两个树形控件(QTreeWidget)的同步展开和收起,以及部分操作的同步(如滚动)。以下是解决方案:1.**同步展开/收起**:为两个树控件的itemExpanded和itemCollapsed信号分别连接相同的处理函数,在其中一个树控件展开或收起节点时,同步操作另一个树控件的对应节点。2.**同步滚动**:通过两个树控件的垂直滚动条值的变化信号连接,使它们的垂直滚动条值保持一致。3.**节点对应关系**:由于两个树的结构是相同的(都是按照相同的JSON结构构建),我们可以通过节点的路径(例如从根节点到当前节点的所有节点的文本拼接)来找到另一个树中的对应节点。但是要注意,在构建树时,两个树的节点路径应该是一致的(即使有差异,我们也已经将相同的路径构建成相同的结构)。因此,我们可以通过节点的路径来映射。然而,为了简化,我们可以在构建树时给每个节点存储一个唯一的路径标识(例如从根节点到该节点的所有键拼接),然后在另一个树中根据同样的路径标识来查找节点。但考虑到性能,我们可以通过保存每个节点的路径字符串,然后在另一个树中查找。但是,如果树很大,查找可能会有性能问题。另一种思路:我们可以在构建两个树的同时,为两个树中对应的节点建立关联(例如用字典存储节点映射:tree1_node->tree2_node)。但需要注意,树构建时可能顺序不同,但我们比较的JSON结构是一致的,所以同一层级的节点顺序也是一致的(因为我们遍历字典和列表的顺序相同)。因此,我们可以假设两个树中相同位置的节点是对应的(通过父节点和子节点索引)。但这种方法在结构不完全一致时可能不可靠。然而,我们之前已经实现了比较功能,并且标记了差异,所以两个树的结构不一定完全相同(比如一边可能缺少一个节点)。因此,我们只能对共有的节点进行同步操作。为了安全,我们采用以下方法:-同步操作只针对两个树中都存在的节点(即节点路径相同且都存在)。-当其中一个树展开某个节点时,我们首先获取该节点的路径(从根节点到该节点的所有key组成的列表),然后在另一个树中查找相同路径的节点。如果找到,则展开或收起另一个树中的对应节点。步骤:1.为两个树的`itemExpanded`和`itemCollapsed`信号连接同一个处理函数。2.在信号处理函数中,获取触发信号的节点以及该节点的路径(可以使用节点逐级向上的文本构建路径)。3.在另一个树中通过路径查找对应节点(递归查找,路径中的每一层节点文本相同)。4.如果找到对应节点,则执行相同的展开或收起操作。但是,注意:同步操作可能会触发另一个树的信号,因此我们需要设置一个标志位来避免循环触发。例如:-当tree1展开一个节点,我们展开tree2的对应节点,这又会触发tree2的展开信号,然后又去展开tree1的同一节点(造成循环)。因此,我们在处理信号时需判断是否由同步操作引起的。实现方法:设置一个标志变量(如`self.syncing=False`),在同步操作期间将其设置为True,在同步操作完成后再设置为False。在信号处理函数中,如果检测到`self.syncing`为True,则忽略信号。另外,滚动条同步:将两个树控件的垂直滚动条绑定,当其中一个的滚动条值改变时,设置另一个的滚动条值。同样注意避免死循环。我们将在代码中实现这些功能。由于之前已经构建了树,我们可以直接获取节点路径(从根节点到当前节点的所有文本)。注意:对于数组,我们使用索引作为路径的一部分,但由于索引是固定的(构建时顺序),所以可行。以下是对上一版代码的扩展:```pythonimportsysimportjsonfromPyQt5.QtWidgetsimport(QApplication,QMainWindow,QTreeWidget,QTreeWidgetItem,QSplitter,QHeaderView,QScrollBar)fromPyQt5.QtCoreimportQt,QObject,pyqtSignalfromPyQt5.QtGuiimportQColorclassSynchronizedTree(QTreeWidget):"""自定义树控件,用于支持与另一个树控件的同步操作"""#自定义一个同步滚动信号,用于滚动条同步syncScroll=pyqtSignal(int)def__init__(self,parent=None):super().__init__(parent)#连接滚动事件self.verticalScrollBar().valueChanged.connect(self.on_scroll)defon_scroll(self,value):"""当滚动条滚动时,发射同步滚动信号"""self.syncScroll.emit(value)classJsonComparator(QMainWindow):def__init__(self,json1,json2):super().__init__()self.setWindowTitle('JSONComparator')self.resize(1000,800)#创建分割器splitter=QSplitter(Qt.Horizontal,self)#创建两个自定义的树控件self.tree1=SynchronizedTree()self.tree2=SynchronizedTree()#配置树控件fortreein[self.tree1,self.tree2]:tree.setHeaderLabels(['Key','Value'])tree.header().setSectionResizeMode(0,QHeaderView.ResizeToContents)tree.setAlternatingRowColors(True)tree.setStyleSheet("""QTreeWidget{background-color:#f0f0f0;font-family:Consolas,monospace;}""")splitter.addWidget(self.tree1)splitter.addWidget(self.tree2)self.setCentralWidget(splitter)#设置滚动同步:当tree1滚动时,tree2跟着滚动;tree2滚动时,tree1跟着滚动self.tree1.syncScroll.connect(self.tree2.verticalScrollBar().setValue)self.tree2.syncScroll.connect(self.tree1.verticalScrollBar().setValue)#为了同步展开/收起,我们需要连接信号self.tree1.itemExpanded.connect(lambdaitem:self.on_item_expanded_collapsed(item,expanded=True))self.tree1.itemCollapsed.connect(lambdaitem:self.on_item_expanded_collapsed(item,expanded=False))self.tree2.itemExpanded.connect(lambdaitem:self.on_item_expanded_collapsed(item,expanded=True))self.tree2.itemCollapsed.connect(lambdaitem:self.on_item_expanded_collapsed(item,expanded=False))#标志位,防止同步操作时的循环self.syncing=False#加载并比较JSONself.load_and_compare(json1,json2)defload_and_compare(self,json_str1,json_str2):"""加载JSON字符串并进行比较"""try:data1=json.loads(json_str1)data2=json.loads(json_str2)self.build_tree(self.tree1,data1)self.build_tree(self.tree2,data2)self.compare_trees(self.tree1.invisibleRootItem(),self.tree2.invisibleRootItem())exceptjson.JSONDecodeErrorase:error_item=QTreeWidgetItem(self.tree1,[f"JSON解析错误:{str(e)}"])error_item.setForeground(0,QColor('red'))defbuild_tree(self,tree,data,parent_item=None):"""递归构建JSON树"""root_item=parent_itemifparent_itemelsetree.invisibleRootItem()ifisinstance(data,dict):forkey,valueindata.items():key_item=QTreeWidgetItem([key])root_item.addChild(key_item)self.build_tree(tree,value,key_item)elifisinstance(data,list):fori,valueinenumerate(data):index_item=QTreeWidgetItem([f"[{i}]"])root_item.addChild(index_item)self.build_tree(tree,value,index_item)else:value=str(data)ifisinstance(data,bool):value="true"ifdataelse"false"value_item=QTreeWidgetItem([value])value_item.setForeground(0,QColor('#888'))root_item.addChild(value_item)defcompare_trees(self,item1,item2,path=""):"""递归比较两棵树并高亮差异"""foriinrange(max(item1.childCount(),item2.childCount())):child1=item1.child(i)ifi< item1.childCount()elseNonechild2=item2.child(i)ifi< item2.childCount()elseNoneifnotchild1ornotchild2:ifchild1andnotchild2:child1.setForeground(0,QColor('red'))elifchild2andnotchild1:child2.setForeground(0,QColor('green'))continue#获取当前路径key=child1.text(0)current_path=f"{path}/{key}"ifpathelsekey#检查叶节点值ifchild1.childCount()==0andchild2.childCount()==0:ifchild1.text(0)!=child2.text(0):child1.setForeground(0,QColor('red'))child2.setForeground(0,QColor('green'))#非叶节点则递归elifchild1.childCount()>0andchild2.childCount()>0:self.compare_trees(child1,child2,current_path)defon_item_expanded_collapsed(self,item,expanded):"""处理树节点的展开或收起事件,同步另一个树中对应节点的展开/收起状态"""ifself.syncing:returnself.syncing=True#获取触发信号的树tree=item.treeWidget()other_tree=self.tree2iftree==self.tree1elseself.tree1#获取当前节点的路径(从该节点向上一直到根)#注意:由于我们的树是通过itemExpanded信号传递的,item就是被操作的那个节点path=self.get_item_path(item)#在另一个树中查找相同路径的节点other_item=self.find_item_by_path(other_tree,path)ifother_item:#执行相同操作ifexpanded:other_item.setExpanded(True)else:other_item.setExpanded(False)self.syncing=Falsedefget_item_path(self,item):"""获取树节点的路径(使用节点的文本,从根到当前节点)"""path=[]current=itemwhilecurrent:#根节点(invisibleRootItem)没有text,所以跳过ifcurrent.parent():path.insert(0,current.text(0))#插入到列表头部current=current.parent()return'/'.join(path)#返回从根到当前节点的路径字符串deffind_item_by_path(self,tree,path):"""根据路径在树中查找节点"""path_list=path.split('/')current_item=tree.invisibleRootItem()#遍历路径列表fornameinpath_list:found=False#在当前节点下查找foriinrange(current_item.childCount()):child=current_item.child(i)ifchild.text(0)==name:current_item=childfound=Truebreakifnotfound:returnNonereturncurrent_item#示例JSON数据(使用之前示例中的JSON,这里省略)#...与之前相同...if__name__=="__main__":app=QApplication(sys.argv)comparator=JsonComparator(json1,json2)comparator.show()sys.exit(app.exec_())```###代码说明1.**自定义树控件`SynchronizedTree`**:-继承`QTreeWidget`,增加滚动同步信号`syncScroll`。-当滚动条值改变时,发射信号。2.**滚动条同步**:-连接两个树控件的`syncScroll`信号,分别设置对方的滚动条值。3.**节点展开/收起的同步**:-连接两个树控件的`itemExpanded`和`itemCollapsed`信号到统一处理函数`on_item_expanded_collapsed`。-在处理函数中,如果当前没有同步操作进行:-获取触发信号的节点路径(从该节点到根节点的文本路径)。-在另一个树中按相同路径查找节点。-如果找到,则进行同样的展开或收起操作。-使用`self.syncing`标志防止循环触发。4.**节点路径查找**:-`get_item_path`方法:从指定节点开始,向上遍历到根节点(跳过根节点,因为根节点是不可见的),将路径上的每个节点文本连接起来(以'/'分隔)。-`find_item_by_path`方法:将路径字符串分割,然后按层级在树中查找节点。###注意事项1.**路径的唯一性**:我们假设同一层级中节点的文本是唯一的。如果JSON中有相同的键名在同一层级出现多次(比如列表中多个元素键名相同),我们使用索引作为节点名称(如`[0]`、`[1]`),所以仍然可以唯一标识。2.**性能考虑**:查找路径的时间复杂度是O(n*m),其中n是树深度,m是每层的节点数。对于非常大的树可能会有性能问题,但在一般JSON大小下通常可以接受。3.**差异节点的同步**:只有两个树中都存在的节点才会被同步。如果节点只存在于一个树中,就不会有同步操作(因为它没有对应的节点)。###§§相关问题§§:1.如果两个树形结构不同(例如一侧节点缺失),如何避免同步操作时出现错误?2.如何优化大型JSON树的路径查找性能?3.在节点路径中,如果键名包含特殊字符(如斜杠'/')怎么处理?4.如何实现两个树控件的水平滚动同步?</think>
# 实现树形控件同步操作功能
为了实现两个树形控件的同步展开、收起以及其他操作的同步,我将创建一个更完善的JSON对比工具。以下解决方案添加了以下功能:
1. 同步展开/折叠节点
2. 同步滚动
3. 统一处理差异项显示
4. 改进右侧树额外项的标记
```python
import sys
import json
from PyQt5.QtWidgets import (QApplication, QMainWindow, QTreeWidget, QTreeWidgetItem,
QSplitter, QHeaderView, QAction, QMenu)
from PyQt5.QtCore import Qt, QObject, pyqtSignal
from PyQt5.QtGui import QColor, QFont, QBrush, QIcon
class SynchronizedTreeWidget(QTreeWidget):
"""自定义树控件,用于实现与其他树的同步操作"""
syncExpandCollapse = pyqtSignal(QTreeWidgetItem, bool)
syncSelection = pyqtSignal(QTreeWidgetItem)
def __init__(self, parent=None):
super().__init__(parent)
# 连接信号
self.itemCollapsed.connect(self.on_item_collapsed)
self.itemExpanded.connect(self.on_item_expanded)
self.itemSelectionChanged.connect(self.on_selection_changed)
def on_item_collapsed(self, item):
"""当节点折叠时发送信号"""
self.syncExpandCollapse.emit(item, False)
def on_item_expanded(self, item):
"""当节点展开时发送信号"""
self.syncExpandCollapse.emit(item, True)
def on_selection_changed(self):
"""当选择变化时发送当前选中项信号"""
selected = self.selectedItems()
if selected:
self.syncSelection.emit(selected[0])
class JsonComparator(QMainWindow):
def __init__(self, json1, json2):
super().__init__()
self.setWindowTitle('JSON Comparator with Synced Views')
self.resize(1000, 800)
# 创建菜单栏
self.create_menus()
# 创建分割器和双树视图
splitter = QSplitter(Qt.Horizontal, self)
self.tree1 = SynchronizedTreeWidget()
self.tree2 = SynchronizedTreeWidget()
# 配置树控件
for tree in [self.tree1, self.tree2]:
tree.setHeaderLabels(['Key', 'Value'])
tree.setHeaderHidden(False)
tree.header().setSectionResizeMode(0, QHeaderView.ResizeToContents)
tree.header().setStretchLastSection(True)
tree.setAlternatingRowColors(True)
tree.setStyleSheet("""
QTreeWidget {
background-color: #f0f0f0;
font-family: Consolas, monospace;
selection-background-color: #a0d0ff;
}
QTreeWidget::item[hasDiff=true] {
background-color: #fff0f0;
}
""")
tree.setContextMenuPolicy(Qt.CustomContextMenu)
tree.customContextMenuRequested.connect(self.show_context_menu)
splitter.addWidget(self.tree1)
splitter.addWidget(self.tree2)
self.setCentralWidget(splitter)
# 设置树控件的信号连接
self.tree1.syncExpandCollapse.connect(self.sync_tree_expansion)
self.tree2.syncExpandCollapse.connect(self.sync_tree_expansion)
self.tree1.syncSelection.connect(self.sync_selection)
self.tree2.syncSelection.connect(self.sync_selection)
# 设置滚动条同步
self.tree1.verticalScrollBar().valueChanged.connect(
self.tree2.verticalScrollBar().setValue
)
self.tree2.verticalScrollBar().valueChanged.connect(
self.tree1.verticalScrollBar().setValue
)
# 加载并比较JSON
self.load_and_compare(json1, json2)
# 设置默认展开级别
self.expand_to_level(2)
def create_menus(self):
"""创建菜单栏"""
menu_bar = self.menuBar()
view_menu = menu_bar.addMenu('View')
# 展开/折叠操作
expand_action = QAction('Expand All', self)
expand_action.triggered.connect(self.expand_all)
view_menu.addAction(expand_action)
collapse_action = QAction('Collapse All', self)
collapse_action.triggered.connect(self.collapse_all)
view_menu.addAction(collapse_action)
# 展开级别菜单
level_menu = view_menu.addMenu('Expand to Level')
for level in range(1, 6):
action = QAction(f'Level {level}', self)
action.triggered.connect(lambda checked, lvl=level: self.expand_to_level(lvl))
level_menu.addAction(action)
# 差异高亮切换
highlight_action = QAction('Toggle Difference Highlight', self, checkable=True)
highlight_action.setChecked(True)
highlight_action.toggled.connect(self.toggle_highlight)
view_menu.addAction(highlight_action)
def show_context_menu(self, pos):
"""显示上下文菜单"""
tree = self.sender()
item = tree.itemAt(pos)
if not item:
return
# 创建菜单
menu = QMenu(self)
# 添加展开/折叠操作
if item.childCount() > 0:
if item.isExpanded():
collapse_action = QAction('Collapse', self)
collapse_action.triggered.connect(lambda: item.setExpanded(False))
menu.addAction(collapse_action)
else:
expand_action = QAction('Expand', self)
expand_action.triggered.connect(lambda: item.setExpanded(True))
menu.addAction(expand_action)
menu.exec_(tree.viewport().mapToGlobal(pos))
def expand_all(self):
"""展开所有节点"""
self.tree1.expandAll()
self.tree2.expandAll()
def collapse_all(self):
"""折叠所有节点"""
self.tree1.collapseAll()
self.tree2.collapseAll()
def expand_to_level(self, level):
"""展开到指定层级"""
self.tree1.expandToDepth(level - 1)
self.tree2.expandToDepth(level - 1)
def toggle_highlight(self, enabled):
"""切换差异高亮显示"""
style = "background-color: #fff0f0;" if enabled else ""
self.tree1.setStyleSheet(self.tree1.styleSheet() +
f"QTreeWidget::item[hasDiff=true] {{ {style} }}")
self.tree2.setStyleSheet(self.tree2.styleSheet() +
f"QTreeWidget::item[hasDiff=true] {{ {style} }}")
def sync_tree_expansion(self, item, expanded):
"""同步节点展开状态"""
# 获取触发信号的树
source_tree = self.sender()
target_tree = self.tree2 if source_tree == self.tree1 else self.tree1
# 阻止信号避免循环
target_tree.blockSignals(True)
# 查找目标树中的对应项
path = self.get_item_path(item)
target_item = self.find_item_by_path(target_tree, path)
# 设置相同的展开状态
if target_item and target_item != target_tree.invisibleRootItem():
target_item.setExpanded(expanded)
target_tree.blockSignals(False)
def sync_selection(self, item):
"""同步节点选择"""
# 获取触发信号的树
source_tree = self.sender()
target_tree = self.tree2 if source_tree == self.tree1 else self.tree1
# 阻止信号避免循环
target_tree.blockSignals(True)
# 清除当前选择
target_tree.clearSelection()
# 查找目标树中的对应项
path = self.get_item_path(item)
target_item = self.find_item_by_path(target_tree, path)
# 选择对应项并滚动到可见区域
if target_item and target_item != target_tree.invisibleRootItem():
target_item.setSelected(True)
target_tree.scrollToItem(target_item, QTreeWidget.PositionAtTop)
target_tree.blockSignals(False)
def load_and_compare(self, json_str1, json_str2):
"""加载JSON字符串并进行比较"""
try:
data1 = json.loads(json_str1)
data2 = json.loads(json_str2)
# 分别构建树
self.build_tree(self.tree1, data1)
self.build_tree(self.tree2, data2)
# 执行比较
self.compare_trees(
self.tree1.invisibleRootItem(),
self.tree2.invisibleRootItem()
)
except json.JSONDecodeError as e:
error_item = QTreeWidgetItem(self.tree1, [f"JSON解析错误: {str(e)}"])
error_item.setForeground(0, QColor('red'))
def build_tree(self, tree, data, parent_item=None):
"""递归构建JSON树"""
root_item = parent_item if parent_item else tree.invisibleRootItem()
if isinstance(data, dict):
for key, value in data.items():
# 创建键节点
key_item = QTreeWidgetItem([key])
# 设置用户数据存储原始类型
key_item.setData(0, Qt.UserRole, {"type": "dict_key"})
root_item.addChild(key_item)
# 处理值
self.build_tree(tree, value, key_item)
elif isinstance(data, list):
for i, value in enumerate(data):
# 创建索引节点
index_item = QTreeWidgetItem([f"[{i}]"])
index_item.setData(0, Qt.UserRole, {"type": "list_index", "index": i})
root_item.addChild(index_item)
# 处理值
self.build_tree(tree, value, index_item)
else:
# 基本类型(字符串、数字等)
value_str = str(data)
# 特殊类型处理
if data is None:
value_str = "null"
elif isinstance(data, bool):
value_str = "true" if data else "false"
# 创建值节点
value_item = QTreeWidgetItem([value_str])
# 存储原始值和类型
value_item.setData(0, Qt.UserRole, {
"type": "value",
"value": data,
"type_str": type(data).__name__
})
# 设置文本颜色
value_item.setForeground(0, QColor('#666'))
root_item.addChild(value_item)
def compare_trees(self, item1, item2, path=""):
"""递归比较两棵树并标记差异"""
# 创建子项映射表
left_children = self.map_children(item1)
right_children = self.map_children(item2)
# 处理左侧树中存在的项
for key, child1 in left_children.items():
child2 = right_children.get(key)
# 获取当前完整路径
current_path = f"{path}/{key}" if path else key
# 检查右侧是否存在该项
if child2 is None:
# 只存在于左侧,标记为删除
self.mark_item_as_diff(child1, "deleted")
else:
# 检查值差异(叶节点)
if child1.childCount() == 0 and child2.childCount() == 0:
if child1.data(0, Qt.UserRole) != child2.data(0, Qt.UserRole):
self.mark_item_as_diff(child1, "modified")
self.mark_item_as_diff(child2, "modified")
# 对于非叶节点,递归比较
elif child1.childCount() > 0 and child2.childCount() > 0:
self.compare_trees(child1, child2, current_path)
# 从右侧映射中移除已处理的项
if key in right_children:
del right_children[key]
# 处理右侧树中剩余项(这些项在左侧不存在)
for key, child2 in right_children.items():
self.mark_item_as_diff(child2, "added")
def map_children(self, item):
"""创建子项映射表,键为显示文本,值为节点"""
children_map = {}
for i in range(item.childCount()):
child = item.child(i)
key = child.text(0)
# 对于列表索引项,使用数字索引作为键
user_data = child.data(0, Qt.UserRole)
if user_data and "type" in user_data and user_data["type"] == "list_index":
key = f"idx_{user_data['index']}"
children_map[key] = child
return children_map
def mark_item_as_diff(self, item, diff_type):
"""根据差异类型标记项"""
# 设置自定义属性用于样式化
item.setData(0, Qt.UserRole + 1, {"diff": diff_type})
# 设置样式标记
item.setData(0, Qt.UserRole + 2, "hasDiff")
# 根据差异类型设置颜色
if diff_type == "deleted":
item.setForeground(0, QColor('red'))
elif diff_type == "added":
item.setForeground(0, QColor('green'))
elif diff_type == "modified":
item.setForeground(0, QColor('purple'))
def get_item_path(self, item):
"""获取树节点的路径(从根到当前节点)"""
path = []
current = item
# 递归向上获取路径
while current and current != self.tree1.invisibleRootItem() and current != self.tree2.invisibleRootItem():
path.insert(0, current.text(0))
current = current.parent()
return '/'.join(path)
def find_item_by_path(self, tree, path):
"""根据路径在树中查找节点"""
path_parts = path.split('/')
current_item = tree.invisibleRootItem()
# 遍历路径各部分
for part in path_parts:
found = False
# 在当前节点下查找匹配的子项
for i in range(current_item.childCount()):
child = current_item.child(i)
if child.text(0) == part:
current_item = child
found = True
break
# 如果未找到,返回None
if not found:
return None
return current_item
# 示例JSON数据
json1 = '''
{
"name": "Alice",
"age": 30,
"contact": {
"email": "alice@example.com",
"phone": "+1234567890"
},
"hobbies": ["reading", "swimming"],
"employed": true,
"projects": [
{"id": 1, "name": "Project A", "status": "completed"},
{"id": 2, "name": "Project B"}
]
}
'''
json2 = '''
{
"name": "Robert",
"age": 35,
"contact": {
"email": "robert@example.com",
"mobile": "+9876543210"
},
"hobbies": ["running", "swimming"],
"employed": true,
"projects": [
{"id": 1, "name": "Project A"},
{"id": 3, "name": "Project C"}
]
}
'''
if __name__ == "__main__":
app = QApplication(sys.argv)
comparator = JsonComparator(json1, json2)
comparator.show()
sys.exit(app.exec_())
```
## 代码功能说明
### 1. 同步操作功能
- **SynchronizedTreeWidget类**:扩展自QTreeWidget,添加信号处理
- `syncExpandCollapse`:节点展开/折叠同步信号
- `syncSelection`:节点选择同步信号
- **同步展开/折叠**:当任一树中的节点展开或折叠时,另一树的对应节点同步状态
- **选择同步**:当任一树中的节点被选中时,另一树的对应节点同步被选中
- **滚动同步**:两个树控件共享同一个滚动条位置
### 2. 高级差异标记
- 使用自定义属性和CSS样式标记差异:
```css
QTreeWidget::item[hasDiff=true] {
background-color: #fff0f0;
}
```
- 三种差异类型标记:
- **已删除**(deleted):红色文本
- **已添加**(added):绿色文本
- **已修改**(modified):紫色文本
- 使用UserRole存储额外数据:
```python
item.setData(0, Qt.UserRole, {"type": "dict_key"})
```
### 3. 增强功能
- **上下文菜单**:右键点击节点显示操作菜单(展开/折叠)
- **视图菜单**:
- 展开/折叠全部节点
- 展开到指定层级
- 切换差异高亮显示
- **优化路径处理**:
```python
def get_item_path(self, item):
"""构建从根到当前节点的路径"""
path = []
current = item
while current.parent():
path.insert(0, current.text(0))
current = current.parent()
return '/'.join(path)
```
### 4. 性能优化
- **映射表算法**:通过创建子项映射表避免嵌套循环:
```python
left_children = self.map_children(item1)
right_children = self.map_children(item2)
```
- **阻塞信号机制**:同步操作时临时阻塞信号避免无限循环:
```python
target_tree.blockSignals(True)
# ... 同步操作 ...
target_tree.blockSignals(False)
```
##