From 4222200ae202f71ab0d5ee4e9beb750c34bc6d2d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=A8=AA?= <2938139566@qq.com> Date: Thu, 11 Sep 2025 09:24:07 +0800 Subject: [PATCH] =?UTF-8?q?3d=E4=BF=A1=E6=81=AF=E9=9D=A2=E6=9D=BF=E6=96=87?= =?UTF-8?q?=E5=AD=97=E4=BF=A1=E6=81=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- core/InfoPanelManager.py | 368 ++++++++++++++++- core/selection.py | 4 +- gui/gui_manager.py | 6 +- main.py | 1 + ui/main_window.py | 207 +++++++--- ui/property_panel.py | 843 ++++++++++++++++++++++++++++----------- ui/widgets.py | 50 ++- 7 files changed, 1174 insertions(+), 305 deletions(-) diff --git a/core/InfoPanelManager.py b/core/InfoPanelManager.py index dbe6dede..3083408d 100644 --- a/core/InfoPanelManager.py +++ b/core/InfoPanelManager.py @@ -317,11 +317,10 @@ class InfoPanelManager(DirectObject): return True + # 更新 registerDataSource 方法以更好地处理不同类型面板 def registerDataSource(self, panel_id, data_callback, update_interval=1.0): """ - 注册数据源,定期更新面板内容 - data_callback: 返回数据的回调函数 - update_interval: 更新间隔(秒) + 注册数据源,定期更新面板内容 - 改进版 """ if panel_id not in self.panels: print(f"面板 {panel_id} 不存在") @@ -335,7 +334,8 @@ class InfoPanelManager(DirectObject): data_source = { 'callback': data_callback, 'interval': update_interval, - 'stop': False + 'stop': False, + 'panel_type': '3d' if self._is3DPanel(panel_id) else '2d' if self._is2DPanel(panel_id) else 'unknown' } self.data_sources[panel_id] = data_source @@ -346,9 +346,10 @@ class InfoPanelManager(DirectObject): return True + # 在 InfoPanelManager 类中修复 _updateDataThread 方法 def _updateDataThread(self, panel_id): """ - 数据更新线程 + 数据更新线程 - 最终修复版 """ while panel_id in self.data_sources and not self.data_sources[panel_id]['stop']: try: @@ -357,7 +358,18 @@ class InfoPanelManager(DirectObject): # 更新面板内容 if data and panel_id in self.panels: - self.updatePanelContent(panel_id, content=data) + panel_type = self.data_sources[panel_id].get('panel_type', 'unknown') + + if panel_type == '2d': + self.updatePanelContent(panel_id, content=data) + elif panel_type == '3d': + self.update3DPanelContent(panel_id, content=data) + else: + # 尝试自动检测 + if self._is2DPanel(panel_id): + self.updatePanelContent(panel_id, content=data) + elif self._is3DPanel(panel_id): + self.update3DPanelContent(panel_id, content=data) # 等待下次更新 interval = self.data_sources[panel_id]['interval'] @@ -365,8 +377,29 @@ class InfoPanelManager(DirectObject): except Exception as e: print(f"更新面板 {panel_id} 数据时出错: {e}") + import traceback + traceback.print_exc() time.sleep(1.0) # 出错时等待1秒再重试 + # 在 InfoPanelManager 类中添加以下方法 + def _is3DPanel(self, panel_id): + """ + 判断面板是否为3D面板 + """ + if panel_id not in self.panels: + return False + panel_data = self.panels[panel_id] + return 'content_node' in panel_data and 'content_label' not in panel_data + + def _is2DPanel(self, panel_id): + """ + 判断面板是否为2D面板 + """ + if panel_id not in self.panels: + return False + panel_data = self.panels[panel_id] + return 'content_label' in panel_data and 'content_node' not in panel_data + def unregisterDataSource(self, panel_id): """ 注销数据源 @@ -657,10 +690,237 @@ class InfoPanelManager(DirectObject): return True + def create3DInfoPanel(self, panel_id, position=(0, 0, 0), size=(1.0, 0.6), + bg_color=(0.15, 0.15, 0.15, 0.9), border_color=(0.3, 0.3, 0.3, 1.0), + title_color=(1.0, 1.0, 1.0, 1.0), content_color=(0.9, 0.9, 0.9, 1.0), + visible=True, font=None, bg_image=None): + """ + 创建简化版3D信息面板 - 只显示文字,无面板背景,避免闪烁 + """ + # 如果面板已存在,先移除它 + if panel_id in self.panels: + self.removePanel(panel_id) + + # 确保父节点存在 + parent_node = self.parent if self.parent else self.world.render + + # 根据面板ID确定标题和内容 + title, content = self._getPanelContent(panel_id) + + # 创建主节点,便于统一管理 + panel_node = parent_node.attachNewNode(f"info_panel_3d_{panel_id}") + panel_node.setPos(position[0], position[1], position[2]) + + # 直接创建文字节点,不创建面板背景和边框 + from panda3d.core import TextNode + + # 创建标题文本 + title_text_node = TextNode(f'title_{panel_id}') + title_text_node.setText(title) + title_text_node.setTextColor(*title_color) + title_text_node.setAlign(TextNode.ACenter) + if font: + title_text_node.setFont(font) + + title_text = panel_node.attachNewNode(title_text_node) + title_text.setScale(0.06) + title_text.setPos(0, 0, size[1] / 4) # 将标题放在上方 + + # 创建内容文本 + content_text_node = TextNode(f'content_{panel_id}') + content_text_node.setText(content) + content_text_node.setTextColor(*content_color) + content_text_node.setAlign(TextNode.ALeft) + content_text_node.setWordwrap(size[0] * 2) # 根据面板宽度设置换行 + if font: + content_text_node.setFont(font) + + content_text = panel_node.attachNewNode(content_text_node) + content_text.setScale(0.045) + content_text.setPos(-size[0] / 2, 0, size[1] / 4 - 0.1) # 将内容放在标题下方 + + # 保存引用 + self.panels[panel_id] = { + 'node': panel_node, + 'title_text': title_text, + 'content_text': content_text, + 'title_node': title_text_node, + 'content_node': content_text_node, + 'properties': { + 'size': size, + 'position': position, + 'title_color': title_color, + 'content_color': content_color, + 'font': font + } + } + + # 设置GUI类型标记和支持3D编辑的标记 + panel_node.setTag("gui_type", "info_panel_3d") + panel_node.setTag("panel_id", panel_id) + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + + if not visible: + panel_node.hide() + + return panel_node + + def update3DPanelContent(self, panel_id, title=None, content=None): + """ + 更新3D面板内容 + """ + if panel_id not in self.panels: + print(f"面板 {panel_id} 不存在") + return False + + panel_data = self.panels[panel_id] + + if title is not None and 'title_node' in panel_data: + panel_data['title_node'].setText(title) + + if content is not None and 'content_node' in panel_data: + panel_data['content_node'].setText(content) + + return True + + def update3DPanelProperties(self, panel_id, **properties): + """ + 更新3D面板属性 + """ + if panel_id not in self.panels: + print(f"面板 {panel_id} 不存在") + return False + + panel_data = self.panels[panel_id] + props = panel_data['properties'] + + # 更新位置 + if 'position' in properties: + pos = properties['position'] + panel_data['node'].setPos(pos[0], pos[1], pos[2]) + props['position'] = pos + + # 更新大小 + if 'size' in properties: + size = properties['size'] + props['size'] = size + + # 由于3D面板使用CardMaker创建,需要重新创建几何体 + print("注意:3D面板大小调整需要重新创建面板几何体") + + # 更新背景颜色 + if 'bg_color' in properties: + bg_color = properties['bg_color'] + if 'panel_bg' in panel_data: + from panda3d.core import Material + material = Material() + material.setDiffuse(Vec4(*bg_color)) + material.setAmbient(Vec4(*bg_color[:3], 1.0)) + panel_data['panel_bg'].setMaterial(material, 1) + props['bg_color'] = bg_color + + # 更新边框颜色 + if 'border_color' in properties: + border_color = properties['border_color'] + from panda3d.core import Material + border_mat = Material() + border_mat.setDiffuse(Vec4(*border_color)) + for border in panel_data['borders'].values(): + border.setMaterial(border_mat, 1) + props['border_color'] = border_color + + # 更新标题颜色 + if 'title_color' in properties: + title_color = properties['title_color'] + if 'title_node' in panel_data: + panel_data['title_node'].setTextColor(*title_color) + props['title_color'] = title_color + + # 更新内容颜色 + if 'content_color' in properties: + content_color = properties['content_color'] + if 'content_node' in panel_data: + panel_data['content_node'].setTextColor(*content_color) + props['content_color'] = content_color + + # 更新标题 + if 'title' in properties: + if 'title_node' in panel_data: + panel_data['title_node'].setText(properties['title']) + + # 更新内容 + if 'content' in properties: + if 'content_node' in panel_data: + panel_data['content_node'].setText(properties['content']) + + # 更新字体大小 + if 'title_size' in properties: + if 'title_text' in panel_data: + panel_data['title_text'].setScale(properties['title_size']) + props['title_size'] = properties['title_size'] + + if 'content_size' in properties: + if 'content_text' in panel_data: + panel_data['content_text'].setScale(properties['content_size']) + props['content_size'] = properties['content_size'] + + return True + + def create3DHTTPInfoPanel(self, panel_id, url, method="GET", headers=None, data=None, + position=(0, 0, 0), size=(1.0, 0.6), + bg_color=(0.15, 0.15, 0.15, 0.9), + border_color=(0.3, 0.3, 0.3, 1.0), + title_color=(1.0, 1.0, 1.0, 1.0), + content_color=(0.9, 0.9, 0.9, 1.0), + update_interval=30.0, font=None): + """ + 创建3D HTTP信息面板 + """ + # 创建面板 + domain = urlparse(url).netloc or url[:30] + title = f"HTTP数据: {domain}" + + panel_node = self.create3DInfoPanel( + panel_id=panel_id, + position=position, + size=size, + bg_color=bg_color, + border_color=border_color, + title_color=title_color, + content_color=content_color, + font=font + ) + + # 更新标题 + self.update3DPanelContent(panel_id, title=title) + + # 立即获取并显示数据 + content = fetchHTTPData(url, method, headers, data) + self.update3DPanelContent(panel_id, content=content) + + # 注册数据源,定期更新 + def http_data_callback(): + return fetchHTTPData(url, method, headers, data) + + self.registerDataSource(panel_id, http_data_callback, update_interval) + + # 保存HTTP请求信息,便于后续更新 + if panel_id not in self.data_sources: + self.data_sources[panel_id] = {} + self.data_sources[panel_id]['http_info'] = { + 'url': url, + 'method': method, + 'headers': headers, + 'data': data + } + + return panel_node + # 在 add_methods_to_property_panel 函数中添加以下方法 def add_methods_to_property_panel(property_panel_instance): # ... (原有代码保持不变) + import types def createHTTPInfoPanel(self, url, panel_id="http_info", method="GET", headers=None, data=None, position=(0.8, 0.0), size=(0.4, 0.3), update_interval=30.0): @@ -721,8 +981,100 @@ class InfoPanelManager(DirectObject): print(f"✗ 更新HTTP信息面板失败: {e}") return False + def create3DRealtimeDataPanel(self, data_callback=None, update_interval=1.0): + """创建3D实时数据面板""" + try: + # 确保父节点已设置 + if self.info_panel_manager.parent is None and hasattr(self, 'world'): + self.info_panel_manager.setParent(self.world.render) + + # 创建3D实时数据面板 + panel_node = self.info_panel_manager.create3DInfoPanel( + panel_id="realtime_data_3d", + position=(0, 0, 0), + size=(0.35, 0.3), + bg_color=(0.15, 0.25, 0.35, 0.95), # 蓝色背景 + border_color=(0.3, 0.5, 0.7, 1.0), # 蓝色边框 + title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题 + content_color=(0.95, 0.95, 0.95, 1.0) + ) + + # 设置标签 + panel_node.setTag("element_type", "info_panel_3d") + panel_node.setTag("is_scene_element", "1") + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + + # 如果提供了数据回调函数,则注册数据源 + if data_callback: + # 立即显示初始数据 + initial_data = data_callback() + self.info_panel_manager.update3DPanelContent("realtime_data_3d", content=initial_data) + + # 注册数据源 + self.info_panel_manager.registerDataSource("realtime_data_3d", data_callback, update_interval) + else: + # 使用默认数据 + default_data = "等待数据..." + self.info_panel_manager.update3DPanelContent("realtime_data_3d", content=default_data) + + print("✓ 已创建3D实时数据面板") + + return panel_node + + except Exception as e: + print(f"✗ 创建3D实时数据面板失败: {e}") + return None + + def create3DModelInfoPanel(self, model): + """为模型创建3D信息面板""" + try: + # 确保父节点已设置 + if self.info_panel_manager.parent is None and hasattr(self, 'world'): + self.info_panel_manager.setParent(self.world.render) + + # 获取模型信息 + model_name = model.getName() if hasattr(model, 'getName') else 'Unknown' + num_children = model.getNumChildren() if hasattr(model, 'getNumChildren') else 0 + + # 创建面板内容 + content = f"模型名称: {model_name}\n子节点数: {num_children}\n类型: {type(model).__name__}" + + # 创建或更新面板 + panel_node = self.info_panel_manager.create3DInfoPanel( + panel_id="model_info_3d", + position=(2, 0, 0), # 默认放在模型旁边 + size=(0.35, 0.25), + bg_color=(0.15, 0.15, 0.25, 0.95), # 蓝紫色背景 + border_color=(0.3, 0.3, 0.7, 1.0), # 蓝色边框 + title_color=(0.5, 0.8, 1.0, 1.0), # 浅蓝色标题 + content_color=(0.95, 0.95, 0.95, 1.0) + ) + + # 更新面板内容为模型特定信息 + self.info_panel_manager.update3DPanelContent("model_info_3d", + title="模型信息", + content=content) + + # 设置标签 + panel_node.setTag("element_type", "info_panel_3d") + panel_node.setTag("is_scene_element", "1") + panel_node.setTag("supports_3d_position_editing", "1") # 支持3D位置编辑 + + print(f"✓ 已创建3D模型信息面板: {model_name}") + + return panel_node + + except Exception as e: + print(f"✗ 创建3D模型信息面板失败: {e}") + return None + + # 在绑定方法的部分添加3D面板方法 + property_panel_instance.create3DRealtimeDataPanel = types.MethodType(create3DRealtimeDataPanel, + property_panel_instance) + property_panel_instance.create3DModelInfoPanel = types.MethodType(create3DModelInfoPanel, + property_panel_instance) + # 将新方法绑定到实例 - import types # ... (原有绑定保持不变) property_panel_instance.createHTTPInfoPanel = types.MethodType(createHTTPInfoPanel, property_panel_instance) property_panel_instance.updateHTTPInfoPanel = types.MethodType(updateHTTPInfoPanel, property_panel_instance) @@ -777,6 +1129,7 @@ def add_methods_to_property_panel(property_panel_instance): """ 为 property_panel 实例添加 DirectGUI 信息面板支持 """ + import types # 添加信息面板管理器作为类属性 if not hasattr(property_panel_instance, 'info_panel_manager'): @@ -982,7 +1335,6 @@ def add_methods_to_property_panel(property_panel_instance): property_panel_instance.removeInfoPanel = types.MethodType(removeInfoPanel, property_panel_instance) property_panel_instance.setupInfoPanelManager = types.MethodType(setupInfoPanelManager, property_panel_instance) - def fetchHTTPData(url, method="GET", headers=None, data=None, timeout=5): """ 获取HTTP数据的通用函数 diff --git a/core/selection.py b/core/selection.py index 7cef719c..f3d87b37 100644 --- a/core/selection.py +++ b/core/selection.py @@ -877,7 +877,7 @@ class SelectionSystem: handle_node = children[0] if not handle_node: - print(f"未找到{axis}轴的处理模型") + #print(f"未找到{axis}轴的处理模型") return # 创建或获取材质 @@ -985,7 +985,7 @@ class SelectionSystem: handle_node = children[0] if not handle_node: - print(f"未找到{axis}轴的处理模型") + # print(f"未找到{axis}轴的处理模型") return # 创建或获取材质 diff --git a/gui/gui_manager.py b/gui/gui_manager.py index bf122ef1..cf695161 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -3217,7 +3217,7 @@ class GUIManager: try: gui_type = gui_element.getTag("gui_type") - if gui_type in ["3d_text", "3d_image", "video_screen","info_panel"]: + if gui_type in ["3d_text", "3d_image", "video_screen","info_panel","info_panel_3d"]: current_pos = gui_element.getPos() if axis == "x": @@ -3230,7 +3230,7 @@ class GUIManager: return False gui_element.setPos(*new_pos) - print(f"✓ 更新3D GUI元素位置: {axis}={value}") + #print(f"✓ 更新3D GUI元素位置: {axis}={value}") return True else: print(f"✗ 不支持的GUI类型进行3D位置编辑: {gui_type}") @@ -3251,7 +3251,7 @@ class GUIManager: if value == 0: value = 0.01 - if gui_type in ["3d_text", "3d_image","video_screen","virtual_screen","info_panel"]: + if gui_type in ["3d_text", "3d_image","video_screen","virtual_screen","info_panel","info_panel_3d"]: # 3D元素处理 if axis == "x": new_scale = (value, current_scale.getY(), current_scale.getZ()) diff --git a/main.py b/main.py index edf872b9..507b90c2 100644 --- a/main.py +++ b/main.py @@ -830,6 +830,7 @@ def createNewProject(parent_window): def saveProject(appw): """保存项目 - 代理到project_manager""" + world = appw.centralWidget().world return world.project_manager.saveProject(appw) diff --git a/ui/main_window.py b/ui/main_window.py index 3149265d..28c28413 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -384,19 +384,26 @@ class MainWindow(QMainWindow): self.createSamplePanelAction = self.infoPanelMenu.addAction('创建示例面板') self.createSamplePanelAction.triggered.connect(self.onCreateSampleInfoPanel) # 添加更多面板创建选项 - self.createSystemStatusPanelAction = self.infoPanelMenu.addAction('创建系统状态面板') - self.createSystemStatusPanelAction.triggered.connect(self.onCreateSystemStatusPanel) + # self.createSystemStatusPanelAction = self.infoPanelMenu.addAction('创建系统状态面板') + # self.createSystemStatusPanelAction.triggered.connect(self.onCreateSystemStatusPanel) + # + # self.createSensorDataPanelAction = self.infoPanelMenu.addAction('创建传感器数据面板') + # self.createSensorDataPanelAction.triggered.connect(self.onCreateSensorDataPanel) + # + # self.createSceneInfoPanelAction = self.infoPanelMenu.addAction('创建场景信息面板') + # self.createSceneInfoPanelAction.triggered.connect(self.onCreateSceneInfoPanel) - self.createSensorDataPanelAction = self.infoPanelMenu.addAction('创建传感器数据面板') - self.createSensorDataPanelAction.triggered.connect(self.onCreateSensorDataPanel) - - self.createSceneInfoPanelAction = self.infoPanelMenu.addAction('创建场景信息面板') - self.createSceneInfoPanelAction.triggered.connect(self.onCreateSceneInfoPanel) + self.infoPanelMenu.addSeparator() + self.create3DSamplePanelAction = self.infoPanelMenu.addAction('创建3D实例面板') + self.create3DSamplePanelAction.triggered.connect(self.onCreate3DSampleInfoPanel) + # + # self.create3DSystemStatusPanelAction = self.infoPanelMenu.addAction('创建3D系统状态面板') + # self.create3DSystemStatusPanelAction.triggered.connect(self.onCreate3DSystemStatusPanel) # 添加分隔符和批量创建选项 - self.infoPanelMenu.addSeparator() - self.createAllPanelsAction = self.infoPanelMenu.addAction('创建所有面板') - self.createAllPanelsAction.triggered.connect(self.onCreateAllInfoPanels) + # self.infoPanelMenu.addSeparator() + # self.createAllPanelsAction = self.infoPanelMenu.addAction('创建所有面板') + # self.createAllPanelsAction.triggered.connect(self.onCreateAllInfoPanels) @@ -606,10 +613,10 @@ class MainWindow(QMainWindow): self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock) # 创建底部停靠控制台 - # self.consoleDock = QDockWidget("控制台", self) - # self.consoleView = CustomConsoleDockWidget(self.world) - # self.consoleDock.setWidget(self.consoleView) - # self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock) + self.consoleDock = QDockWidget("控制台", self) + self.consoleView = CustomConsoleDockWidget(self.world) + self.consoleDock.setWidget(self.consoleView) + self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock) def setupToolbar(self): """创建工具栏""" @@ -1131,7 +1138,7 @@ class MainWindow(QMainWindow): # 立即显示加载中信息 info_manager.updatePanelContent(unique_id, content="正在获取天气数据...") - info_manager.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.0) # 每10分钟更新一次 + info_manager.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.0) # # 立即显示示例数据 # sample_data = self.getSampleWeatherData() @@ -1225,6 +1232,142 @@ class MainWindow(QMainWindow): except Exception as e: return f"获取示例数据失败: {str(e)}" + # 在 main_window.py 中修改 onCreate3DSampleInfoPanel 方法 + def onCreate3DSampleInfoPanel(self): + """创建3D示例天气信息面板(修复透明度问题)""" + try: + # 获取中文字体 + from panda3d.core import TextNode + font = self.world.getChineseFont() if self.world.getChineseFont() else None + + # 创建面板 + info_manager = self.world.info_panel_manager + info_manager.setParent(self.world.render) + + # 使用唯一的面板ID + import time + unique_id = f"weather_info_3d_{int(time.time())}" + + # 创建3D示例面板 - 修复透明度问题 + weather_panel = info_manager.create3DInfoPanel( + panel_id=unique_id, + position=(2, 0, 2), # 调整Z坐标避免与其他对象重叠 + size=(1, 1), + bg_color=(0.15, 0.25, 0.35, 0.85), # 设置合适的透明度值 + border_color=(0.3, 0.5, 0.7, 1.0), + title_color=(0.7, 0.9, 1.0, 1.0), + content_color=(0.95, 0.95, 0.95, 1.0), + font=font + ) + + # 重要:手动设置面板的透明度渲染模式 + # if weather_panel: + # # 确保面板支持透明度 + # weather_panel.setTransparency(True) + # # 设置合适的渲染顺序,确保透明对象正确渲染 + # weather_panel.setBin("transparent", 0) + # # 启用深度写入,但保持透明度 + # weather_panel.setDepthWrite(False) + + # 更新面板标题 + info_manager.update3DPanelContent(unique_id, title="3D北京天气") + + # 添加到场景树 + self.addInfoPanelToTree(weather_panel, "3D天气信息面板") + + # 显示加载中信息 + info_manager.update3DPanelContent(unique_id, content="正在获取天气数据...") + + info_manager.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.0) + + print("✓ 3D示例天气信息面板已创建") + + except Exception as e: + print(f"✗ 创建3D示例天气信息面板失败: {e}") + import traceback + traceback.print_exc() + QMessageBox.critical(self, "错误", f"创建3D示例天气信息面板时出错: {str(e)}") + + def onCreate3DSystemStatusPanel(self): + """创建3D系统状态信息面板""" + try: + # 获取中文字体 + from panda3d.core import TextNode + font = self.world.getChineseFont() if self.world.getChineseFont() else None + + # 创建面板 + info_manager = self.world.info_panel_manager + info_manager.setParent(self.world.render) + + # 使用唯一的面板ID + import time + unique_id = f"system_status_3d_{int(time.time())}" + + panel = info_manager.create3DInfoPanel( + panel_id=unique_id, + position=(2, 0, 0), + size=(0.8, 1.2), + bg_color=(0.25, 0.15, 0.15, 0.95), # 红色背景 + border_color=(0.7, 0.3, 0.3, 1.0), # 红色边框 + title_color=(1.0, 0.5, 0.5, 1.0), # 浅红色标题 + content_color=(0.95, 0.95, 0.95, 1.0), + font=font + ) + + # 添加到场景树 + self.addInfoPanelToTree(panel, "3D系统状态信息面板") + + # 立即显示初始数据 + initial_data = self.getSystemStatusData() + info_manager.update3DPanelContent(unique_id, content=initial_data) + + # 注册数据源,每5秒更新一次 + info_manager.registerDataSource(unique_id, self.getSystemStatusData, update_interval=5.0) + + except Exception as e: + print(f"✗ 创建3D系统状态信息面板失败: {e}") + import traceback + traceback.print_exc() + QMessageBox.critical(self, "错误", f"创建3D系统状态信息面板时出错: {str(e)}") + + # 更新 addInfoPanelToTree 方法以支持3D面板 + def addInfoPanelToTree(self, panel, panel_name): + """ + 将信息面板添加到场景树控件中 + """ + if panel and self.treeWidget: + # 找到场景根节点 + scene_root = None + for i in range(self.treeWidget.topLevelItemCount()): + item = self.treeWidget.topLevelItem(i) + if item.text(0) == "render": + scene_root = item + break + + # 如果找不到场景根节点,使用第一个顶级节点 + if not scene_root and self.treeWidget.topLevelItemCount() > 0: + scene_root = self.treeWidget.topLevelItem(0) + + if scene_root: + # 根据面板类型确定节点类型 + node_type = "INFO_PANEL_3D" if "3d" in panel_name.lower() else "INFO_PANEL" + + tree_item = self.treeWidget.add_node_to_tree_widget( + node=panel, + parent_item=scene_root, + node_type=node_type + ) + if tree_item: + self.treeWidget.setCurrentItem(tree_item) + self.treeWidget.update_selection_and_properties(panel, tree_item) + print(f"✓ {panel_name}节点已添加到场景树") + return True + else: + print(f"⚠️ {panel_name}节点添加到场景树失败") + else: + print("❌ 未找到场景根节点") + return False + def onCreateSystemStatusPanel(self): """创建系统状态信息面板""" try: @@ -1449,40 +1592,6 @@ class MainWindow(QMainWindow): except Exception as e: QMessageBox.critical(self, "错误", f"创建信息面板时出错: {str(e)}") - def addInfoPanelToTree(self, panel, panel_name): - """ - 将信息面板添加到场景树控件中 - """ - if panel and self.treeWidget: - # 找到场景根节点 - scene_root = None - for i in range(self.treeWidget.topLevelItemCount()): - item = self.treeWidget.topLevelItem(i) - if item.text(0) == "render": - scene_root = item - break - - # 如果找不到场景根节点,使用第一个顶级节点 - if not scene_root and self.treeWidget.topLevelItemCount() > 0: - scene_root = self.treeWidget.topLevelItem(0) - - if scene_root: - tree_item = self.treeWidget.add_node_to_tree_widget( - node=panel, - parent_item=scene_root, - node_type="INFO_PANEL" - ) - if tree_item: - self.treeWidget.setCurrentItem(tree_item) - self.treeWidget.update_selection_and_properties(panel, tree_item) - print(f"✓ {panel_name}节点已添加到场景树") - return True - else: - print(f"⚠️ {panel_name}节点添加到场景树失败") - else: - print("❌ 未找到场景根节点") - return False - # ==================== 脚本管理事件处理 ==================== def refreshScriptsList(self): diff --git a/ui/property_panel.py b/ui/property_panel.py index 37d21acf..21a173e2 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -14,7 +14,7 @@ from direct.actor.Actor import Actor from direct.gui import DirectGui from idna import check_label from jinja2.compiler import has_safe_repr -from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup +from panda3d.core import Vec3, Vec4, transpose, TransparencyAttrib, PartGroup, ColorAttrib from scene import util from direct.gui.DirectGui import DirectLabel, DirectFrame from panda3d.core import TextNode @@ -1286,6 +1286,48 @@ class PropertyPanelManager: """更新GUI元素属性面板""" self.clearPropertyPanel() + itemText = gui_element.getTag("gui_type") or "未命名GUI元素" + + user_visible = True + user_visible = gui_element.getPythonTag("user_visible") + if user_visible is None: + user_visible = True + gui_element.setPythonTag("user_visible",True) + + self.name_group = QGroupBox("物体名称") + name_layout = QHBoxLayout() + self.active_check = QCheckBox() + # 根据元素的实际可见性状态设置复选框 + self.active_check.setChecked(user_visible) + self.name_input = QLineEdit(itemText) + + # 注意:对于GUI元素,我们需要特殊处理名称更新 + def updateGUIName(text): + # 更新GUI元素的标签 + gui_element.setTag("name", text) + # 如果有场景管理器,也需要更新场景树 + if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): + self.world.scene_manager.updateSceneTree() + + self.name_input.returnPressed.connect(lambda: updateGUIName(self.name_input.text())) + + # 如果失去焦点也更新名称 + self.name_input.editingFinished.connect(lambda: updateGUIName(self.name_input.text())) + + name_layout.addWidget(self.active_check) + name_layout.addWidget(self.name_input) + self.name_group.setLayout(name_layout) + self._propertyLayout.addWidget(self.name_group) + + if gui_element: + try: + self.active_check.stateChanged.disconnect() + except TypeError: + pass + self.active_check.stateChanged.connect( + lambda state, elem=gui_element: self._setUserVisible(elem, state == Qt.Checked) + ) + gui_type = gui_element.getTag("gui_type") gui_text = gui_element.getTag("gui_text") @@ -1305,15 +1347,19 @@ class PropertyPanelManager: gui_info_layout.addWidget(QLabel("文本:"), 1, 0) textEdit = QLineEdit(gui_text or "") - # 创建一个更新函数来处理文本变化 - def updateText(text): + # 使用编辑完成信号而不是文本变化信号 + def updateText(): + text = textEdit.text() success = self.world.gui_manager.editGUIElement(gui_element, "text", text) if success: # 更新场景树显示的名称 if hasattr(self.world, 'scene_manager') and hasattr(self.world.scene_manager, 'updateSceneTree'): self.world.scene_manager.updateSceneTree() - textEdit.textChanged.connect(updateText) + # 只在按下回车键或失去焦点时更新 + textEdit.returnPressed.connect(updateText) + textEdit.editingFinished.connect(updateText) + gui_info_layout.addWidget(textEdit, 1, 1) gui_info_group.setLayout(gui_info_layout) @@ -1649,7 +1695,7 @@ class PropertyPanelManager: self._add2DVideoScreenProperties(gui_element) elif gui_type == "spherical_video": self._addSphericalVideoProperties(gui_element) - elif gui_type == 'info_panel': + elif gui_type in ['info_panel','info_panel_3d']: self._addInfoPanelProperties(gui_element) self._propertyLayout.addStretch() @@ -1662,7 +1708,7 @@ class PropertyPanelManager: propertyWidget.update() def _addInfoPanelProperties(self, info_panel): - """为信息面板添加属性控制面板""" + """为信息面板添加属性控制面板 - 同时适配2D和3D信息面板""" try: from PyQt5.QtWidgets import (QGroupBox, QGridLayout, QPushButton, QLabel, QDoubleSpinBox, QColorDialog, QSpinBox, QTextEdit) @@ -1675,9 +1721,13 @@ class PropertyPanelManager: print("无法找到信息面板ID") return + # 检测面板类型 (2D或3D) + gui_type = info_panel.getTag("gui_type") + is_3d_panel = gui_type == "info_panel_3d" + # 获取面板当前属性 current_size = (1.0, 0.6) - current_position = (0, 0) + current_position = (0, 0) if not is_3d_panel else (0, 0, 0) current_bg_color = (0.15, 0.15, 0.15, 0.9) current_border_color = (0.3, 0.3, 0.3, 1.0) current_title_color = (1.0, 1.0, 0.0, 1.0) @@ -1708,44 +1758,55 @@ class PropertyPanelManager: current_content_color = props.get('content_color', current_content_color) current_bg_image = props.get('bg_image', current_bg_image) - # 获取文本内容 - if 'title_label' in panel_data and panel_data['title_label']: - current_title_text = panel_data['title_label']['text'] - # 获取标题位置 - title_pos = panel_data['title_label'].getPos() - current_title_pos = (title_pos.getX(), title_pos.getZ()) + # 获取文本内容和位置(根据面板类型区分处理) + if not is_3d_panel: # 2D面板 + if 'title_label' in panel_data and panel_data['title_label']: + current_title_text = panel_data['title_label']['text'] + # 获取标题位置 + title_pos = panel_data['title_label'].getPos() + current_title_pos = (title_pos.getX(), title_pos.getZ()) - if 'content_label' in panel_data and panel_data['content_label']: - current_content_text = panel_data['content_label']['text'] - # 获取内容位置 - content_pos = panel_data['content_label'].getPos() - current_content_pos = (content_pos.getX(), content_pos.getZ()) + if 'content_label' in panel_data and panel_data['content_label']: + current_content_text = panel_data['content_label']['text'] + # 获取内容位置 + content_pos = panel_data['content_label'].getPos() + current_content_pos = (content_pos.getX(), content_pos.getZ()) - # 获取字体大小(确保获取的是数值而不是元组) - if 'title_label' in panel_data and panel_data['title_label']: - # 正确获取DirectLabel的text_scale属性 - try: - title_scale = panel_data['title_label']['text_scale'] - # 如果是元组,取第一个值 - if isinstance(title_scale, (tuple, list)): - current_title_size = title_scale[0] if len(title_scale) > 0 else 0.06 - else: - current_title_size = title_scale if isinstance(title_scale, (int, float)) else 0.06 - except: - current_title_size = 0.06 + # 获取字体大小 + if 'title_label' in panel_data and panel_data['title_label']: + try: + title_scale = panel_data['title_label']['text_scale'] + # 如果是元组,取第一个值 + if isinstance(title_scale, (tuple, list)): + current_title_size = title_scale[0] if len(title_scale) > 0 else 0.06 + else: + current_title_size = title_scale if isinstance(title_scale, (int, float)) else 0.06 + except: + current_title_size = 0.06 - if 'content_label' in panel_data and panel_data['content_label']: - # 正确获取DirectLabel的text_scale属性 - try: - content_scale = panel_data['content_label']['text_scale'] - # 如果是元组,取第一个值 - if isinstance(content_scale, (tuple, list)): - current_content_size = content_scale[0] if len(content_scale) > 0 else 0.045 - else: - current_content_size = content_scale if isinstance(content_scale, - (int, float)) else 0.045 - except: - current_content_size = 0.045 + if 'content_label' in panel_data and panel_data['content_label']: + try: + content_scale = panel_data['content_label']['text_scale'] + # 如果是元组,取第一个值 + if isinstance(content_scale, (tuple, list)): + current_content_size = content_scale[0] if len(content_scale) > 0 else 0.045 + else: + current_content_size = content_scale if isinstance(content_scale, + (int, float)) else 0.045 + except: + current_content_size = 0.045 + else: # 3D面板 + if 'title_node' in panel_data and panel_data['title_node']: + current_title_text = panel_data['title_node'].getText() + + if 'content_node' in panel_data and panel_data['content_node']: + current_content_text = panel_data['content_node'].getText() + + if 'title_text' in panel_data and panel_data['title_text']: + current_title_size = panel_data['title_text'].getScale().getX() + + if 'content_text' in panel_data and panel_data['content_text']: + current_content_size = panel_data['content_text'].getScale().getX() # 获取sort值 if info_panel.hasTag("sort"): @@ -1866,12 +1927,16 @@ class PropertyPanelManager: panel_width = width_spin.value() panel_height = height_spin.value() - # 更新面板大小 - self.world.info_panel_manager.updatePanelProperties( - panel_id, size=(panel_width, panel_height)) + # 根据面板类型选择更新方法 + if is_3d_panel: + self.world.info_panel_manager.update3DPanelProperties( + panel_id, size=(panel_width, panel_height)) + else: + self.world.info_panel_manager.updatePanelProperties( + panel_id, size=(panel_width, panel_height)) - # 同步更新内容的换行设置 - self._adjustContentWordwrap(panel_id, panel_width, content_size_spin.value()) + # 同步更新内容的换行设置 + self._adjustContentWordwrap(panel_id, panel_width, content_size_spin.value()) width_spin.valueChanged.connect(onSizeChanged) height_spin.valueChanged.connect(onSizeChanged) @@ -1879,6 +1944,70 @@ class PropertyPanelManager: size_group.setLayout(size_layout) self._propertyLayout.addWidget(size_group) + # 位置设置组 (根据面板类型显示不同的位置控件) + position_group = QGroupBox("位置设置") + position_layout = QGridLayout() + + if is_3d_panel: + # 3D面板位置设置 (X, Y, Z) + position_layout.addWidget(QLabel("X:"), 0, 0) + pos_x_spin = QDoubleSpinBox() + pos_x_spin.setRange(-1000, 1000) + pos_x_spin.setSingleStep(0.1) + pos_x_spin.setValue(current_position[0] if len(current_position) > 0 else 0) + position_layout.addWidget(pos_x_spin, 0, 1) + + position_layout.addWidget(QLabel("Y:"), 0, 2) + pos_y_spin = QDoubleSpinBox() + pos_y_spin.setRange(-1000, 1000) + pos_y_spin.setSingleStep(0.1) + pos_y_spin.setValue(current_position[1] if len(current_position) > 1 else 0) + position_layout.addWidget(pos_y_spin, 0, 3) + + position_layout.addWidget(QLabel("Z:"), 1, 0) + pos_z_spin = QDoubleSpinBox() + pos_z_spin.setRange(-1000, 1000) + pos_z_spin.setSingleStep(0.1) + pos_z_spin.setValue(current_position[2] if len(current_position) > 2 else 0) + position_layout.addWidget(pos_z_spin, 1, 1) + + # 连接位置变化信号 + def on3DPositionChanged(): + if hasattr(self.world, 'info_panel_manager'): + self.world.info_panel_manager.update3DPanelProperties( + panel_id, position=(pos_x_spin.value(), pos_y_spin.value(), pos_z_spin.value())) + + pos_x_spin.valueChanged.connect(on3DPositionChanged) + pos_y_spin.valueChanged.connect(on3DPositionChanged) + pos_z_spin.valueChanged.connect(on3DPositionChanged) + else: + # 2D面板位置设置 (X, Z) + position_layout.addWidget(QLabel("X:"), 0, 0) + pos_x_spin = QDoubleSpinBox() + pos_x_spin.setRange(-1000, 1000) + pos_x_spin.setSingleStep(0.1) + pos_x_spin.setValue(current_position[0] if len(current_position) > 0 else 0) + position_layout.addWidget(pos_x_spin, 0, 1) + + position_layout.addWidget(QLabel("Z:"), 0, 2) + pos_z_spin = QDoubleSpinBox() + pos_z_spin.setRange(-1000, 1000) + pos_z_spin.setSingleStep(0.1) + pos_z_spin.setValue(current_position[1] if len(current_position) > 1 else 0) + position_layout.addWidget(pos_z_spin, 0, 3) + + # 连接位置变化信号 + def on2DPositionChanged(): + if hasattr(self.world, 'info_panel_manager'): + self.world.info_panel_manager.updatePanelProperties( + panel_id, position=(pos_x_spin.value(), pos_z_spin.value())) + + pos_x_spin.valueChanged.connect(on2DPositionChanged) + pos_z_spin.valueChanged.connect(on2DPositionChanged) + + position_group.setLayout(position_layout) + self._propertyLayout.addWidget(position_group) + # 边框属性组 border_group = QGroupBox("边框属性") border_layout = QGridLayout() @@ -1926,15 +2055,19 @@ class PropertyPanelManager: # 连接边框颜色变化信号 def onBorderColorChanged(): if hasattr(self.world, 'info_panel_manager'): - self.world.info_panel_manager.updatePanelProperties( - panel_id, - border_color=( - border_r_spin.value(), - border_g_spin.value(), - border_b_spin.value(), - border_a_spin.value() - ) + border_color = ( + border_r_spin.value(), + border_g_spin.value(), + border_b_spin.value(), + border_a_spin.value() ) + # 根据面板类型选择更新方法 + if is_3d_panel: + self.world.info_panel_manager.update3DPanelProperties( + panel_id, border_color=border_color) + else: + self.world.info_panel_manager.updatePanelProperties( + panel_id, border_color=border_color) border_r_spin.valueChanged.connect(onBorderColorChanged) border_g_spin.valueChanged.connect(onBorderColorChanged) @@ -2056,39 +2189,51 @@ class PropertyPanelManager: title_size_spin = QDoubleSpinBox() title_size_spin.setRange(0.01, 1.0) title_size_spin.setSingleStep(0.01) - title_size_spin.setValue(current_title_size) # 现在确保是浮点数 + title_size_spin.setValue(current_title_size) title_layout.addWidget(title_size_spin, 5, 1, 1, 3) - # 标题位置控制 - title_layout.addWidget(QLabel("标题位置:"), 6, 0) - title_layout.addWidget(QLabel("X:"), 7, 0) - title_x_spin = QDoubleSpinBox() - title_x_spin.setRange(-1.0, 1.0) - title_x_spin.setSingleStep(0.01) - title_x_spin.setValue(current_title_pos[0]) # 从面板获取的实际位置 - title_layout.addWidget(title_x_spin, 7, 1) + # 标题位置控制 (仅对2D面板) + if not is_3d_panel: + title_layout.addWidget(QLabel("标题位置:"), 6, 0) + title_layout.addWidget(QLabel("X:"), 7, 0) + title_x_spin = QDoubleSpinBox() + title_x_spin.setRange(-1.0, 1.0) + title_x_spin.setSingleStep(0.01) + title_x_spin.setValue(current_title_pos[0]) + title_layout.addWidget(title_x_spin, 7, 1) - title_layout.addWidget(QLabel("Y:"), 7, 2) - title_y_spin = QDoubleSpinBox() - title_y_spin.setRange(-1.0, 1.0) - title_y_spin.setSingleStep(0.01) - title_y_spin.setValue(current_title_pos[1]) # 从面板获取的实际位置 - title_layout.addWidget(title_y_spin, 7, 3) + title_layout.addWidget(QLabel("Y:"), 7, 2) + title_y_spin = QDoubleSpinBox() + title_y_spin.setRange(-1.0, 1.0) + title_y_spin.setSingleStep(0.01) + title_y_spin.setValue(current_title_pos[1]) + title_layout.addWidget(title_y_spin, 7, 3) - # 连接标题属性变化信号,自动更新标题属性 + # 连接标题属性变化信号 def onTitleTextChanged(): if hasattr(self.world, 'info_panel_manager'): - self.world.info_panel_manager.updatePanelContent( - panel_id, title=title_text_edit.toPlainText()) + # 根据面板类型选择更新方法 + if is_3d_panel: + self.world.info_panel_manager.update3DPanelContent( + panel_id, title=title_text_edit.toPlainText()) + else: + self.world.info_panel_manager.updatePanelContent( + panel_id, title=title_text_edit.toPlainText()) def onTitlePropertyChanged(): - self._applyInfoPanelTitleProperties( - info_panel, title_r_spin.value(), title_g_spin.value(), title_b_spin.value(), - title_a_spin.value(), title_size_spin.value()) + # 根据面板类型选择更新方法 + if is_3d_panel: + self._applyInfoPanel3DTitleProperties( + info_panel, title_r_spin.value(), title_g_spin.value(), title_b_spin.value(), + title_a_spin.value(), title_size_spin.value()) + else: + self._applyInfoPanelTitleProperties( + info_panel, title_r_spin.value(), title_g_spin.value(), title_b_spin.value(), + title_a_spin.value(), title_size_spin.value()) def onTitlePositionChanged(): - # 更新标题位置 - if hasattr(self.world, 'info_panel_manager'): + # 仅对2D面板更新标题位置 + if not is_3d_panel and hasattr(self.world, 'info_panel_manager'): panel_data = self.world.info_panel_manager.panels.get(panel_id) if panel_data and 'title_label' in panel_data: # 设置标题位置 @@ -2100,8 +2245,10 @@ class PropertyPanelManager: title_b_spin.valueChanged.connect(onTitlePropertyChanged) title_a_spin.valueChanged.connect(onTitlePropertyChanged) title_size_spin.valueChanged.connect(onTitlePropertyChanged) - title_x_spin.valueChanged.connect(onTitlePositionChanged) - title_y_spin.valueChanged.connect(onTitlePositionChanged) + + if not is_3d_panel: # 仅对2D面板连接位置变化信号 + title_x_spin.valueChanged.connect(onTitlePositionChanged) + title_y_spin.valueChanged.connect(onTitlePositionChanged) title_group.setLayout(title_layout) self._propertyLayout.addWidget(title_group) @@ -2163,61 +2310,89 @@ class PropertyPanelManager: content_size_spin = QDoubleSpinBox() content_size_spin.setRange(0.01, 1.0) content_size_spin.setSingleStep(0.01) - content_size_spin.setValue(current_content_size) # 现在确保是浮点数 + content_size_spin.setValue(current_content_size) content_layout.addWidget(content_size_spin, 5, 1, 1, 3) - # 内容位置控制 - content_layout.addWidget(QLabel("内容位置:"), 6, 0) - content_layout.addWidget(QLabel("X:"), 7, 0) - content_x_spin = QDoubleSpinBox() - content_x_spin.setRange(-1.0, 1.0) - content_x_spin.setSingleStep(0.01) - content_x_spin.setValue(current_content_pos[0]) # 从面板获取的实际位置 - content_layout.addWidget(content_x_spin, 7, 1) + # 内容位置控制 (仅对2D面板) + if not is_3d_panel: + content_layout.addWidget(QLabel("内容位置:"), 6, 0) + content_layout.addWidget(QLabel("X:"), 7, 0) + content_x_spin = QDoubleSpinBox() + content_x_spin.setRange(-1.0, 1.0) + content_x_spin.setSingleStep(0.01) + content_x_spin.setValue(current_content_pos[0]) + content_layout.addWidget(content_x_spin, 7, 1) - content_layout.addWidget(QLabel("Y:"), 7, 2) - content_y_spin = QDoubleSpinBox() - content_y_spin.setRange(-1.0, 1.0) - content_y_spin.setSingleStep(0.01) - content_y_spin.setValue(current_content_pos[1]) # 从面板获取的实际位置 - content_layout.addWidget(content_y_spin, 7, 3) + content_layout.addWidget(QLabel("Y:"), 7, 2) + content_y_spin = QDoubleSpinBox() + content_y_spin.setRange(-1.0, 1.0) + content_y_spin.setSingleStep(0.01) + content_y_spin.setValue(current_content_pos[1]) + content_layout.addWidget(content_y_spin, 7, 3) - # 连接内容属性变化信号,自动更新内容属性 + # 连接内容属性变化信号 def onContentTextChanged(): if hasattr(self.world, 'info_panel_manager'): - self.world.info_panel_manager.updatePanelContent( - panel_id, content=content_text_edit.toPlainText()) + # 根据面板类型选择更新方法 + if is_3d_panel: + self.world.info_panel_manager.update3DPanelContent( + panel_id, content=content_text_edit.toPlainText()) + else: + self.world.info_panel_manager.updatePanelContent( + panel_id, content=content_text_edit.toPlainText()) def onContentPropertyChanged(): - self._applyInfoPanelContentProperties( - info_panel, content_r_spin.value(), content_g_spin.value(), content_b_spin.value(), - content_a_spin.value(), content_size_spin.value()) + # 根据面板类型选择更新方法 + if is_3d_panel: + self._applyInfoPanel3DContentProperties( + info_panel, content_r_spin.value(), content_g_spin.value(), content_b_spin.value(), + content_a_spin.value(), content_size_spin.value()) + else: + self._applyInfoPanelContentProperties( + info_panel, content_r_spin.value(), content_g_spin.value(), content_b_spin.value(), + content_a_spin.value(), content_size_spin.value()) def onContentPositionChanged(): - # 更新内容位置 - if hasattr(self.world, 'info_panel_manager'): + # 仅对2D面板更新内容位置 + if not is_3d_panel and hasattr(self.world, 'info_panel_manager'): panel_data = self.world.info_panel_manager.panels.get(panel_id) if panel_data and 'content_label' in panel_data: # 设置内容位置 panel_data['content_label'].setPos(content_x_spin.value(), 0, content_y_spin.value()) - # 添加内容换行调整函数 def adjustContentWordwrap(): """根据面板宽度和字体大小调整内容换行""" - panel_width = width_spin.value() - font_size = content_size_spin.value() - self._adjustContentWordwrap(panel_id, panel_width, font_size) + # 仅对2D面板调整换行 + if not is_3d_panel: + panel_width = width_spin.value() + font_size = content_size_spin.value() + self._adjustContentWordwrap(panel_id, panel_width, font_size) # 连接相关信号 - content_text_edit.textChanged.connect(onContentTextChanged) content_r_spin.valueChanged.connect(onContentPropertyChanged) content_g_spin.valueChanged.connect(onContentPropertyChanged) content_b_spin.valueChanged.connect(onContentPropertyChanged) content_a_spin.valueChanged.connect(onContentPropertyChanged) - content_size_spin.valueChanged.connect(lambda: [onContentPropertyChanged(), adjustContentWordwrap()]) - content_x_spin.valueChanged.connect(onContentPositionChanged) - content_y_spin.valueChanged.connect(onContentPositionChanged) - width_spin.valueChanged.connect(adjustContentWordwrap) + + # 内容字体大小变化信号连接 + content_size_spin.valueChanged.connect(onContentPropertyChanged) + + # 内容文本变化信号连接 + content_text_edit.textChanged.connect(onContentTextChanged) + + # 标题相关信号连接 + title_text_edit.textChanged.connect(onTitleTextChanged) + title_r_spin.valueChanged.connect(onTitlePropertyChanged) + title_g_spin.valueChanged.connect(onTitlePropertyChanged) + title_b_spin.valueChanged.connect(onTitlePropertyChanged) + title_a_spin.valueChanged.connect(onTitlePropertyChanged) + title_size_spin.valueChanged.connect(onTitlePropertyChanged) + + if not is_3d_panel: # 仅对2D面板连接位置变化信号 + title_x_spin.valueChanged.connect(onTitlePositionChanged) + title_y_spin.valueChanged.connect(onTitlePositionChanged) + content_x_spin.valueChanged.connect(onContentPositionChanged) + content_y_spin.valueChanged.connect(onContentPositionChanged) content_group.setLayout(content_layout) self._propertyLayout.addWidget(content_group) @@ -2237,27 +2412,65 @@ class PropertyPanelManager: 'content_a_spin': content_a_spin, 'title_size_spin': title_size_spin, 'content_size_spin': content_size_spin, - 'title_x_spin': title_x_spin, - 'title_y_spin': title_y_spin, - 'content_x_spin': content_x_spin, - 'content_y_spin': content_y_spin, 'width_spin': width_spin, 'height_spin': height_spin } + # 对于2D面板,添加位置控件引用 + if not is_3d_panel: + self._info_panel_controls.update({ + 'title_x_spin': title_x_spin, + 'title_y_spin': title_y_spin, + 'content_x_spin': content_x_spin, + 'content_y_spin': content_y_spin + }) + # 启动定时器定期检查信息面板内容变化 self._startInfoPanelMonitoring(info_panel, panel_id) - # 初始调整内容换行 - adjustContentWordwrap() + # 初始调整内容换行 (仅对2D面板) + if not is_3d_panel: + adjustContentWordwrap() - print("✅ 信息面板属性面板已添加") + print(f"✅ 信息面板属性面板已添加 (类型: {'3D' if is_3d_panel else '2D'})") except Exception as e: print(f"❌ 添加信息面板属性面板失败: {e}") import traceback traceback.print_exc() + def _applyInfoPanel3DTitleProperties(self, info_panel, r, g, b, a, size): + """应用3D信息面板标题属性""" + try: + panel_id = info_panel.getTag("panel_id") + if not panel_id or not hasattr(self.world, 'info_panel_manager'): + return + + # 更新3D面板标题属性 + self.world.info_panel_manager.update3DPanelProperties( + panel_id, + title_color=(r, g, b, a), + title_size=size + ) + except Exception as e: + print(f"应用3D信息面板标题属性失败: {e}") + + def _applyInfoPanel3DContentProperties(self, info_panel, r, g, b, a, size): + """应用3D信息面板内容属性""" + try: + panel_id = info_panel.getTag("panel_id") + if not panel_id or not hasattr(self.world, 'info_panel_manager'): + return + + # 更新3D面板内容属性 + self.world.info_panel_manager.update3DPanelProperties( + panel_id, + content_color=(r, g, b, a), + content_size=size + ) + except Exception as e: + print(f"应用3D信息面板内容属性失败: {e}") + def _adjustContentWordwrap(self, panel_id, panel_width, font_size): """调整信息面板内容的换行设置""" try: @@ -2386,7 +2599,7 @@ class PropertyPanelManager: color = QColorDialog.getColor(current_color, None, "选择边框颜色") if color.isValid(): # 更新数值框 - r_spin.setValue(color.redF()) # redF() 返回 0.0-1.0 范围的值 + r_spin.setValue(color.redF()) g_spin.setValue(color.greenF()) b_spin.setValue(color.blueF()) a_spin.setValue(color.alphaF()) @@ -2395,7 +2608,7 @@ class PropertyPanelManager: print(f"❌ 选择边框颜色失败: {e}") def updateInfoPanelBackgroundImage(self, info_panel, image_path): - """更新信息面板背景图片""" + """更新信息面板背景图片 - 修复版""" try: panel_id = info_panel.getTag("panel_id") if not panel_id: @@ -2406,25 +2619,33 @@ class PropertyPanelManager: if hasattr(self.world, 'info_panel_manager'): info_panel_manager = self.world.info_panel_manager - # 直接调用 InfoPanelManager 的方法设置背景图片 - if image_path: - # 设置新的背景图片 - success = info_panel_manager.setPanelBackgroundImage(panel_id, image_path) - if success: - print(f"✅ 成功设置信息面板背景图片: {image_path}") - return True - else: - print(f"❌ 设置信息面板背景图片失败: {image_path}") - return False + # 检查是否是3D信息面板 + gui_type = info_panel.getTag("gui_type") + is_3d_panel = gui_type == "info_panel_3d" + + if is_3d_panel: + # 对于3D信息面板,使用专门的方法 + return self._update3DInfoPanelBackgroundImage(panel_id, info_panel, image_path) else: - # 清除背景图片 - success = info_panel_manager.setPanelBackgroundImage(panel_id, None) - if success: - print("✅ 成功清除信息面板背景图片") - return True + # 对于2D信息面板,使用原有方法 + if image_path: + # 设置新的背景图片 + success = info_panel_manager.setPanelBackgroundImage(panel_id, image_path) + if success: + print(f"✅ 成功设置信息面板背景图片: {image_path}") + return True + else: + print(f"❌ 设置信息面板背景图片失败: {image_path}") + return False else: - print("❌ 清除信息面板背景图片失败") - return False + # 清除背景图片 + success = info_panel_manager.setPanelBackgroundImage(panel_id, None) + if success: + print("✅ 成功清除信息面板背景图片") + return True + else: + print("❌ 清除信息面板背景图片失败") + return False else: print("❌ 未找到 info_panel_manager") return False @@ -2435,13 +2656,165 @@ class PropertyPanelManager: traceback.print_exc() return False + def _applyInfoPanelBackgroundImage(self, panel_node, image_path, panel_data): + """应用信息面板背景图片 - 改进版""" + try: + from panda3d.core import CardMaker, TransparencyAttrib, Vec4 + + # 如果已有背景图片,先移除 + if 'bg_image' in panel_data and panel_data['bg_image']: + try: + panel_data['bg_image'].removeNode() + except: + pass + panel_data['bg_image'] = None + + # 加载纹理 + texture = self.world.loader.loadTexture(image_path) + if not texture: + print(f"❌ 无法加载背景图片: {image_path}") + return False + + # 设置纹理属性 + texture.setMagfilter(texture.FTLinear) + texture.setMinfilter(texture.FTLinearMipmapLinear) + + # 获取面板尺寸 + size = panel_data.get('size', (1.0, 0.6)) + + # 创建背景卡片 + cm = CardMaker('info_panel_background') + cm.setFrame(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2) + + # 生成几何体并创建节点 + bg_node = panel_node.attachNewNode(cm.generate()) + + # 应用纹理 + bg_node.setTexture(texture, 1) + + # 设置渲染属性 + bg_node.setTransparency(TransparencyAttrib.MAlpha) + bg_node.setBin("background", -10) + bg_node.setDepthWrite(False) + bg_node.setDepthTest(False) + bg_node.setLightOff() + bg_node.setTwoSided(True) + + # 确保在最底层 + bg_node.setZ(-0.1) + + # 保存引用 + panel_data['bg_image'] = bg_node + + print(f"✅ 成功应用信息面板背景图片: {image_path}") + return True + + except Exception as e: + print(f"❌ 应用信息面板背景图片失败: {e}") + import traceback + traceback.print_exc() + return False + + def _update3DInfoPanelBackgroundImage(self, panel_id, info_panel, image_path): + """更新3D信息面板背景图片 - 完整修复版""" + try: + # 从info_panel_manager获取面板数据 + if not hasattr(self.world, 'info_panel_manager'): + print("❌ 未找到 info_panel_manager") + return False + + panel_data = self.world.info_panel_manager.panels.get(panel_id) + if not panel_data: + print(f"❌ 无法找到面板数据: {panel_id}") + return False + + # 获取面板节点 + panel_node = panel_data.get('node') + if not panel_node: + print("❌ 无法找到面板节点") + return False + + # 如果已有背景图片,先销毁它 + if 'bg_image' in panel_data and panel_data['bg_image']: + try: + # 如果是NodePath对象,使用removeNode方法 + if hasattr(panel_data['bg_image'], 'removeNode'): + panel_data['bg_image'].removeNode() + # 如果是DirectGUI对象,使用destroy方法 + elif hasattr(panel_data['bg_image'], 'destroy'): + panel_data['bg_image'].destroy() + except Exception as e: + print(f"⚠️ 清理旧背景图片时出错: {e}") + panel_data['bg_image'] = None + + # 如果image_path为None,只清除背景图片 + if not image_path: + # 清除节点标签 + if info_panel.hasTag("bg_image_path"): + info_panel.clearTag("bg_image_path") + print("✅ 成功清除3D信息面板背景图片") + return True + + # 使用 world.loader 加载新图片 + texture = self.world.loader.loadTexture(image_path) + if not texture: + print(f"❌ 无法加载图片: {image_path}") + return False + + # 设置纹理过滤 + texture.setMagfilter(texture.FTLinear) + texture.setMinfilter(texture.FTLinearMipmapLinear) + + # 获取面板大小 + size = panel_data['properties'].get('size', (1.0, 0.6)) + + # 使用CardMaker创建卡片几何体 + from panda3d.core import CardMaker, TransparencyAttrib + + cm = CardMaker('info_panel_bg') + # 设置卡片大小,居中放置 + cm.setFrame(-size[0] / 2, size[0] / 2, -size[1] / 2, size[1] / 2) + bg_geom = cm.generate() + + # 创建节点并附加几何体,直接附加到面板节点 + bg_node = panel_node.attachNewNode(bg_geom) + + # 应用纹理 + bg_node.setTexture(texture, 1) + + # 设置正确的渲染属性 + bg_node.setTransparency(TransparencyAttrib.MAlpha) + bg_node.setBin("background", -10) # 确保在最底层 + bg_node.setDepthWrite(False) + bg_node.setDepthTest(False) + bg_node.setLightOff() + bg_node.setTwoSided(True) # 双面渲染 + + # 调整位置确保在面板内容后面 + bg_node.setPos(0, 0.1, 0) # 稍微向后一点避免z-fighting + + # 保存引用 + panel_data['bg_image'] = bg_node + panel_data['properties']['bg_image'] = image_path + + # 更新节点标签 + info_panel.setTag("bg_image_path", image_path) + + print(f"✅ 成功设置3D信息面板背景图片: {image_path}") + return True + + except Exception as e: + print(f"❌ 更新3D信息面板背景图片失败: {e}") + import traceback + traceback.print_exc() + return False def _selectInfoPanelBackgroundColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): """选择信息面板背景颜色""" try: from PyQt5.QtWidgets import QColorDialog from PyQt5.QtGui import QColor - # 获取当前颜色值 + # 获取当前背景颜色 current_color = QColor( int(r_spin.value() * 255), int(g_spin.value() * 255), @@ -2449,40 +2822,41 @@ class PropertyPanelManager: int(a_spin.value() * 255) ) - # 打开颜色选择对话框 + # 显示颜色选择对话框 color = QColorDialog.getColor(current_color, None, "选择背景颜色") if color.isValid(): # 更新数值框 - r_spin.setValue(color.redF()) # redF() 返回 0.0-1.0 范围的值 - g_spin.setValue(color.greenF()) - b_spin.setValue(color.blueF()) - a_spin.setValue(color.alphaF()) + r_spin.setValue(color.red() / 255.0) + g_spin.setValue(color.green() / 255.0) + b_spin.setValue(color.blue() / 255.0) + a_spin.setValue(color.alpha() / 255.0) except Exception as e: - print(f"❌ 选择背景颜色失败: {e}") + print(f"选择背景颜色失败: {e}") - def _applyInfoPanelBackgroundColor(self,info_panel,r,g,b,a): - """应用信息面部背景颜色""" + def _applyInfoPanelBackgroundColor(self, info_panel, r, g, b, a): + """应用信息面板背景颜色""" try: panel_id = info_panel.getTag("panel_id") if not panel_id: - print("❌ 找不到信息面板ID") return - if hasattr(self.world,'info_panel_manager'): - info_panel_manager = self.world.info_panel_manager - success = info_panel_manager.updatePanelProperties(panel_id,bg_color=(r,g,b,a)) - if success: - print(f"✅ 成功设置信息面板背景颜色: R={r:.2f}, G={g:.2f}, B={b:.2f}, A={a:.2f}") - return True + # 更新InfoPanelManager中的背景颜色 + if hasattr(self.world, 'info_panel_manager'): + bg_color = (r, g, b, a) + + # 根据面板类型选择更新方法 + gui_type = info_panel.getTag("gui_type") + if gui_type == "info_panel_3d": + self.world.info_panel_manager.update3DPanelProperties( + panel_id, bg_color=bg_color) else: - print("❌ 设置信息面板背景颜色失败") - else: - print("❌ 未找到 info_panel_manager") - return False + self.world.info_panel_manager.updatePanelProperties( + panel_id, bg_color=bg_color) + + print(f"已更新信息面板 {panel_id} 的背景颜色为 ({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f})") except Exception as e: - print(f"应用背景颜色失败{e}") - return False + print(f"应用背景颜色失败: {e}") def _selectInfoPanelTitleColor(self, info_panel, r_spin, g_spin, b_spin, a_spin): """选择信息面板标题颜色""" @@ -2502,7 +2876,7 @@ class PropertyPanelManager: color = QColorDialog.getColor(current_color, None, "选择标题颜色") if color.isValid(): # 更新数值框 - r_spin.setValue(color.redF()) # redF() 返回 0.0-1.0 范围的值 + r_spin.setValue(color.redF()) g_spin.setValue(color.greenF()) b_spin.setValue(color.blueF()) a_spin.setValue(color.alphaF()) @@ -2528,7 +2902,7 @@ class PropertyPanelManager: color = QColorDialog.getColor(current_color, None, "选择内容颜色") if color.isValid(): # 更新数值框 - r_spin.setValue(color.redF()) # redF() 返回 0.0-1.0 范围的值 + r_spin.setValue(color.redF()) g_spin.setValue(color.greenF()) b_spin.setValue(color.blueF()) a_spin.setValue(color.alphaF()) @@ -2548,38 +2922,31 @@ class PropertyPanelManager: if hasattr(self.world, 'info_panel_manager'): info_panel_manager = self.world.info_panel_manager - # 更新标题颜色和字体大小 - # 注意:DirectGUI中字体大小通过text_scale设置,需要直接操作title_label对象 - panel_data = info_panel_manager.panels.get(panel_id) - if panel_data and 'title_label' in panel_data: - # 更新颜色 - panel_data['title_label']['text_fg'] = (r, g, b, a) - # 更新字体大小 - panel_data['title_label']['text_scale'] = size - # 同时更新属性存储 - if 'properties' in panel_data: - panel_data['properties']['title_color'] = (r, g, b, a) - - print(f"✅ 成功设置信息面板标题属性: 颜色=({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}), 大小={size}") - return True - else: - # 回退到使用updatePanelProperties方法 - success = info_panel_manager.updatePanelProperties( + # 根据面板类型选择不同的更新方法 + gui_type = info_panel.getTag("gui_type") + if gui_type == "info_panel_3d": + # 3D面板使用update3DPanelProperties + info_panel_manager.update3DPanelProperties( panel_id, - title_color=(r, g, b, a) + title_color=(r, g, b, a), + title_size=size + ) + else: + # 2D面板使用updatePanelProperties + info_panel_manager.updatePanelProperties( + panel_id, + title_color=(r, g, b, a), + title_size=size ) - if success: - print(f"✅ 成功设置信息面板标题颜色: ({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f})") - return True - print("❌ 设置信息面板标题属性失败") - return False + print(f"✓ 信息面板标题属性已更新: 颜色RGBA({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}), 大小{size:.3f}") + return True else: - print("❌ 未找到 info_panel_manager") + print("❌ 无法找到 info_panel_manager") return False except Exception as e: - print(f"❌ 应用标题属性失败: {e}") + print(f"❌ 应用信息面板标题属性失败: {e}") return False def _applyInfoPanelContentProperties(self, info_panel, r, g, b, a, size): @@ -2594,38 +2961,31 @@ class PropertyPanelManager: if hasattr(self.world, 'info_panel_manager'): info_panel_manager = self.world.info_panel_manager - # 更新内容颜色和字体大小 - # 注意:DirectGUI中字体大小通过text_scale设置,需要直接操作content_label对象 - panel_data = info_panel_manager.panels.get(panel_id) - if panel_data and 'content_label' in panel_data: - # 更新颜色 - panel_data['content_label']['text_fg'] = (r, g, b, a) - # 更新字体大小 - panel_data['content_label']['text_scale'] = size - # 同时更新属性存储 - if 'properties' in panel_data: - panel_data['properties']['content_color'] = (r, g, b, a) - - print(f"✅ 成功设置信息面板内容属性: 颜色=({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}), 大小={size}") - return True - else: - # 回退到使用updatePanelProperties方法 - success = info_panel_manager.updatePanelProperties( + # 根据面板类型选择不同的更新方法 + gui_type = info_panel.getTag("gui_type") + if gui_type == "info_panel_3d": + # 3D面板使用update3DPanelProperties + info_panel_manager.update3DPanelProperties( panel_id, - content_color=(r, g, b, a) + content_color=(r, g, b, a), + content_size=size + ) + else: + # 2D面板使用updatePanelProperties + info_panel_manager.updatePanelProperties( + panel_id, + content_color=(r, g, b, a), + content_size=size ) - if success: - print(f"✅ 成功设置信息面板内容颜色: ({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f})") - return True - print("❌ 设置信息面板内容属性失败") - return False + #print(f"✓ 信息面板内容属性已更新: 颜色RGBA({r:.2f}, {g:.2f}, {b:.2f}, {a:.2f}), 大小{size:.3f}") + return True else: - print("❌ 未找到 info_panel_manager") + print("❌ 无法找到 info_panel_manager") return False except Exception as e: - print(f"❌ 应用内容属性失败: {e}") + print(f"❌ 应用信息面板内容属性失败: {e}") return False def _addSphericalVideoProperties(self, spherical_video): @@ -3877,7 +4237,6 @@ class PropertyPanelManager: except Exception as e: print(f"✗ 更新3D元素轴缩放失败: {e}") - def _selectGUIColor(self, gui_element): """选择GUI元素的字体颜色""" from PyQt5.QtWidgets import QColorDialog @@ -4007,39 +4366,39 @@ class PropertyPanelManager: except Exception as e: print(f"✗ 更新GUI元素Z轴缩放失败: {e}") - def update2DImageTexture(self, gui_element, image_path): + def update2DImageTexture(self, image_element, texture_path): + """更新2D图片纹理 - 修复版""" try: - new_texture = self.world.loader.loadTexture(image_path) - if new_texture: - if hasattr(gui_element,'setTexture'): - gui_element.setTexture(new_texture,1) - else: - from direct.gui.DirectGui import DirectFrame - if isinstance(gui_element,DirectFrame): - gui_element['frameTexture']=None - gui_element['frameTexture']=new_texture - else: - print("❌ 不支持的GUI元素类型,无法更新纹理") - return False - gui_element.setTag("image_path",image_path) + from panda3d.core import TextureStage, TransparencyAttrib - if not gui_element.hasMaterial(): - from panda3d.core import Material,LColor - mat = Material() - mat.setName(f"image-material-{id(gui_element)}") - mat.setBaseColor(LColor(1,1,1,1)) - mat.setDiffuse(LColor(1,1,1,1)) - mat.setAmbient(LColor(0.5,0.5,0.5,1)) - mat.setSpecular(LColor(0.1,0.1,0.1,1.0)) - mat.setShininess(10.0) - gui_element.setMaterial(mat,1) - print(f"✅ 2D图像纹理已更新为: {image_path}") - return True - else: - print(f"❌ 无法加载2D图片纹理: {image_path}") + # 加载纹理 + texture = self.world.loader.loadTexture(texture_path) + if not texture: + print(f"❌ 无法加载纹理: {texture_path}") return False + + # 设置纹理过滤 + texture.setMagfilter(texture.FTLinear) + texture.setMinfilter(texture.FTLinearMipmapLinear) + + # 应用纹理到元素 + image_element.setTexture(texture, 1) + + # 设置正确的渲染属性以避免黑色显示 + image_element.setTransparency(TransparencyAttrib.MAlpha) + image_element.setBin("fixed", 20) # 确保在正确层级渲染 + image_element.setDepthWrite(False) + image_element.setDepthTest(False) + image_element.setLightOff() + image_element.setTwoSided(True) + + print(f"✅ 2D图片纹理已更新: {texture_path}") + return True + except Exception as e: - print(f"❌ 更新2D图片纹理时出错: {e}") + print(f"❌ 更新2D图片纹理失败: {e}") + import traceback + traceback.print_exc() return False diff --git a/ui/widgets.py b/ui/widgets.py index f794ee07..3303bc58 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -655,7 +655,7 @@ class CustomAssetsTreeWidget(QTreeWidget): if os.path.exists(directory) and directory not in self.watched_directories: if self.file_watcher.addPath(directory): self.watched_directories.add(directory) - print(f"开始监控目录:{directory}") + #print(f"开始监控目录:{directory}") try: for item in os.listdir(directory): item_path = os.path.join(directory,item) @@ -1833,6 +1833,8 @@ class CustomTreeWidget(QTreeWidget): selected_items = self.selectedItems() if selected_items: # 执行删除操作 + # if selected_items.data(0, Qt.UserRole + 1) == "LIGHT_NODE": + # self._preprocess_light_items_for_deletion(selected_items) self.delete_items(selected_items) else: # 没有选中任何项目,执行默认操作 @@ -1840,6 +1842,52 @@ class CustomTreeWidget(QTreeWidget): else: super().keyPressEvent(event) + def _preprocess_light_items_for_deletion(self, selected_items): + """预处理灯光节点删除,特别处理最后一个灯光节点的问题""" + if not selected_items: + return selected_items + + # 检查选中的项目中是否包含灯光节点 + light_items = [] + for item in selected_items: + node_type = item.data(0, Qt.UserRole + 1) + if node_type == "LIGHT_NODE": + light_items.append(item) + + # 如果没有灯光节点,直接返回 + if not light_items: + return selected_items + + # 检查是否只有最后一个灯光节点被选中 + processed_items = list(selected_items) # 创建副本 + + for item in light_items: + panda_node = item.data(0, Qt.UserRole) + if not panda_node: + continue + + # 获取灯光类型 + if hasattr(panda_node, 'getTag'): + light_type = panda_node.getTag("light_type") + + # 检查是否是最后一个Spotlight + if (light_type == "spot_light" and hasattr(self.world, 'Spotlight') and + self.world.Spotlight and self.world.Spotlight[-1] == panda_node and + len(self.world.Spotlight) > 1): + + print(f"⚠️ 检测到选中最后一个Spotlight节点: {item.text(0)}") + # 这里可以添加特殊处理逻辑,比如提示用户或阻止删除 + + # 检查是否是最后一个Pointlight + elif (light_type == "point_light" and hasattr(self.world, 'Pointlight') and + self.world.Pointlight and self.world.Pointlight[-1] == panda_node and + len(self.world.Pointlight) > 1): + + print(f"⚠️ 检测到选中最后一个Pointlight节点: {item.text(0)}") + # 这里可以添加特殊处理逻辑,比如提示用户或阻止删除 + + return processed_items + def delete_items(self, selected_items): """删除选中的item - 简化版本""" if not selected_items: