From 63801cfb0af5f16e93ec2b24bc379914859b5f03 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E6=A8=AA?= <2938139566@qq.com> Date: Mon, 15 Sep 2025 16:32:23 +0800 Subject: [PATCH] =?UTF-8?q?1.=E4=BF=AE=E6=94=B9=E4=BF=9D=E5=AD=98=E5=B1=82?= =?UTF-8?q?=E7=BA=A7=E6=95=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- RenderPipelineFile/config/daytime.yaml | 4 +- Start_Run.py | 1 - core/terrain_manager.py | 6 + gui/gui_manager.py | 12 + project/project_manager.py | 4 +- scene/scene_manager.py | 56 +- ui/main_window.py | 1036 ++++++++++++++++++++---- ui/property_panel.py | 72 +- ui/widgets.py | 243 ++++-- 9 files changed, 1171 insertions(+), 263 deletions(-) diff --git a/RenderPipelineFile/config/daytime.yaml b/RenderPipelineFile/config/daytime.yaml index 731e5beb..af9861ed 100644 --- a/RenderPipelineFile/config/daytime.yaml +++ b/RenderPipelineFile/config/daytime.yaml @@ -17,8 +17,8 @@ control_points: scattering: sun_intensity: [[[0.0000000000,0.0000000000],[0.0041666667,0.0000000000],[0.0083333333,0.0000000000],[0.0125000000,0.0000000000],[0.0166666667,0.0000000000],[0.0208333333,0.0000000000],[0.0250000000,0.0000000000],[0.0291666667,0.0000000000],[0.0333333333,0.0000000000],[0.0375000000,0.0000000000],[0.0416666667,0.0000000000],[0.0458333333,0.0000000000],[0.0500000000,0.0000000000],[0.0541666667,0.0000000000],[0.0583333333,0.0000000000],[0.0625000000,0.0000000000],[0.0666666667,0.0000000000],[0.0708333333,0.0000000000],[0.0750000000,0.0000000000],[0.0791666667,0.0000000000],[0.0833333333,0.0000000000],[0.0875000000,0.0000000000],[0.0916666667,0.0000000000],[0.0958333333,0.0000000000],[0.1000000000,0.0000000000],[0.1041666667,0.0000000000],[0.1083333333,0.0000000000],[0.1125000000,0.0000000000],[0.1166666667,0.0000000000],[0.1208333333,0.0000000000],[0.1250000000,0.0000000000],[0.1291666667,0.0000000000],[0.1333333333,0.0000000000],[0.1375000000,0.0000000000],[0.1416666667,0.0000000000],[0.1458333333,0.0000000000],[0.1500000000,0.0000000000],[0.1541666667,0.0000000000],[0.1583333333,0.0000028805],[0.1625000000,0.0003577724],[0.1666666667,0.0013331400],[0.1708333333,0.0029671803],[0.1750000000,0.0052963381],[0.1791666667,0.0083550556],[0.1833333333,0.0121755589],[0.1875000000,0.0167876159],[0.1916666667,0.0222183530],[0.1958333333,0.0284919947],[0.2000000000,0.0356297193],[0.2041666667,0.0436494349],[0.2083333333,0.0525656099],[0.2125000000,0.0623891610],[0.2166666667,0.0731272461],[0.2208333333,0.0847831708],[0.2250000000,0.0973563167],[0.2291666667,0.1108419698],[0.2333333333,0.1252313631],[0.2375000000,0.1405115250],[0.2416666667,0.1566653434],[0.2458333333,0.1736715009],[0.2500000000,0.1915046014],[0.2541666667,0.2101350464],[0.2583333333,0.2295292930],[0.2625000000,0.2496498145],[0.2666666667,0.2704552670],[0.2708333333,0.2919006662],[0.2750000000,0.3139375192],[0.2791666667,0.3365139497],[0.2833333333,0.3595750662],[0.2875000000,0.3830630359],[0.2916666667,0.4069173972],[0.2958333333,0.4310753462],[0.3000000000,0.4554720417],[0.3041666667,0.4800408236],[0.3083333333,0.5047136020],[0.3125000000,0.5294212108],[0.3166666667,0.5540936424],[0.3208333333,0.5786605298],[0.3250000000,0.6030514553],[0.3291666667,0.6271963182],[0.3333333333,0.6510256858],[0.3375000000,0.6744711982],[0.3416666667,0.6974659988],[0.3458333333,0.7199450163],[0.3500000000,0.7418453485],[0.3541666667,0.7631067095],[0.3583333333,0.7836717291],[0.3625000000,0.8034862953],[0.3666666667,0.8224999302],[0.3708333333,0.8406661079],[0.3750000000,0.8579425235],[0.3791666667,0.8742914270],[0.3833333333,0.8896799131],[0.3875000000,0.9040801386],[0.3916666667,0.9174695289],[0.3958333333,0.9298310650],[0.4000000000,0.9411533765],[0.4041666667,0.9514309312],[0.4083333333,0.9606641691],[0.4125000000,0.9688595571],[0.4166666667,0.9760296330],[0.4208333333,0.9821930708],[0.4250000000,0.9873746114],[0.4291666667,0.9916050060],[0.4333333333,0.9949209310],[0.4375000000,0.9973647924],[0.4416666667,0.9989845508],[0.4458333333,0.9998334497],[0.4500000000,0.9999696949],[0.4541666667,0.9994560801],[0.4583333333,0.9983595429],[0.4625000000,0.9967506613],[0.4666666667,0.9947030614],[0.4708333333,0.9922927758],[0.4750000000,0.9895975125],[0.4791666667,0.9866958610],[0.4833333333,0.9836664262],[0.4875000000,0.9805868867],[0.4916666667,0.9775330316],[0.4958333333,0.9745777179],[0.5000000000,0.9717898417],[0.5041666667,0.9692332877],[0.5083333333,0.9669658924],[0.5125000000,0.9650384806],[0.5089595376,0.9690650222],[0.5208333333,0.9623666659],[0.5250000000,0.9616814371],[0.5291666667,0.9614534423],[0.5333333333,0.9616877089],[0.5375000000,0.9623790807],[0.5416666667,0.9635123329],[0.5458333333,0.9650624244],[0.5500000000,0.9669949804],[0.5541666667,0.9692669864],[0.5583333333,0.9718275065],[0.5625000000,0.9746185969],[0.5666666667,0.9775762863],[0.5708333333,0.9806315864],[0.5750000000,0.9837115661],[0.5791666667,0.9867403433],[0.5833333333,0.9896401655],[0.5875000000,0.9923323562],[0.5916666667,0.9947382579],[0.5958333333,0.9967800977],[0.6000000000,0.9983817820],[0.6041666667,0.9994696263],[0.6083333333,0.9999730028],[0.6125000000,0.9998249266],[0.6166666667,0.9989625601],[0.6208333333,0.9973276624],[0.6250000000,0.9948669567],[0.6291666667,0.9915324664],[0.6333333333,0.9872817545],[0.6375000000,0.9820781426],[0.6416666667,0.9758908775],[0.6458333333,0.9686952146],[0.6500000000,0.9604725211],[0.6541666667,0.9512102537],[0.6583333333,0.9409019858],[0.6625000000,0.9295473441],[0.6666666667,0.9171518878],[0.6708333333,0.9037270619],[0.6750000000,0.8892899902],[0.6791666667,0.8738633008],[0.6833333333,0.8574749656],[0.6875000000,0.8401579787],[0.6916666667,0.8219502453],[0.6958333333,0.8028941798],[0.7000000000,0.7830364456],[0.7041666667,0.7624277344],[0.7083333333,0.7411222520],[0.7125000000,0.7191776044],[0.7166666667,0.6966542563],[0.7208333333,0.6736152714],[0.7250000000,0.6501259629],[0.7291666667,0.6262533880],[0.7333333333,0.6020661121],[0.7375000000,0.5776338043],[0.7416666667,0.5530267796],[0.7458333333,0.5283156992],[0.7500000000,0.5035711751],[0.7541666667,0.4788634341],[0.7583333333,0.4542618347],[0.7625000000,0.4298347613],[0.7666666667,0.4056490351],[0.7708333333,0.3817697830],[0.7750000000,0.3582600107],[0.7791666667,0.3351803495],[0.7833333333,0.3125888445],[0.7875000000,0.2905406366],[0.7916666667,0.2690876955],[0.7958333333,0.2482787388],[0.8000000000,0.2281588906],[0.8041666667,0.2087696425],[0.8083333333,0.1901486315],[0.8125000000,0.1723295359],[0.8166666667,0.1553419918],[0.8208333333,0.1392115328],[0.8250000000,0.1239595144],[0.8291666667,0.1096030703],[0.8333333333,0.0961551918],[0.8375000000,0.0836246599],[0.8416666667,0.0720161369],[0.8458333333,0.0613302273],[0.8500000000,0.0515635598],[0.8541666667,0.0427088803],[0.8583333333,0.0347551990],[0.8625000000,0.0276878920],[0.8666666667,0.0214889271],[0.8708333333,0.0161369711],[0.8750000000,0.0116076130],[0.8791666667,0.0078735477],[0.8833333333,0.0049047927],[0.8875000000,0.0026688977],[0.8916666667,0.0011311782],[0.8958333333,0.0002549473],[0.9000000000,0.0000000000],[0.9041666667,0.0000000000],[0.9083333333,0.0000000000],[0.9125000000,0.0000000000],[0.9166666667,0.0000000000],[0.9208333333,0.0000000000],[0.9250000000,0.0000000000],[0.9291666667,0.0000000000],[0.9333333333,0.0000000000],[0.9375000000,0.0000000000],[0.9416666667,0.0000000000],[0.9458333333,0.0000000000],[0.9500000000,0.0000000000],[0.9541666667,0.0000000000],[0.9583333333,0.0000000000],[0.9625000000,0.0000000000],[0.9666666667,0.0000000000],[0.9708333333,0.0000000000],[0.9750000000,0.0000000000],[0.9791666667,0.0000000000],[0.9833333333,0.0000000000],[0.9875000000,0.0000000000],[0.9916666667,0.0000000000],[0.9958333333,0.0000000000]]] sun_color: [[[0.5010435645,0.5818710306],[0.0433100000,0.8999700000],[0.8635787716,0.9130000000],[0.1785000000,0.8973600000],[0.8099800000,0.8651100000],[0.2360800000,0.7712700000],[0.6583432177,0.8485126184],[0.1266806142,0.9648102053],[0.9558541267,0.9090909091],[0.5568400771,0.7353760446]],[[0.5001318426,0.5160300000],[0.0572700000,0.6541600000],[0.2395000000,0.5976800000],[0.8104600000,0.6009000000],[0.6967400000,0.5483900000]],[[0.0862400000,0.4257800000],[0.4955600000,0.4033000000],[0.8234200000,0.4340200000]]] - sun_azimuth: [[[0.5000000000,0.0000000000]]] - sun_altitude: [[[0.5000000000,0.9333333333]]] + sun_azimuth: [[[0.5000000000,0.5000000000]]] + sun_altitude: [[[0.5000000000,1.0000000000]]] extinction: [[[0.4913294798,0.6378830084]]] volumetrics: fog_ramp_size: [[[0.5510597303,0.7409470752]]] diff --git a/Start_Run.py b/Start_Run.py index 225cd6a8..fa515efa 100644 --- a/Start_Run.py +++ b/Start_Run.py @@ -17,7 +17,6 @@ sys.path.insert(0, render_pipeline_file_path) icons_path = os.path.join(project_root, "icons") sys.path.insert(0, icons_path) - # 现在可以导入并运行主程序 if __name__ == "__main__": args = sys.argv[1:] diff --git a/core/terrain_manager.py b/core/terrain_manager.py index bb7e0e47..6807c968 100644 --- a/core/terrain_manager.py +++ b/core/terrain_manager.py @@ -104,6 +104,9 @@ class TerrainManager: print("错误:无法生成有效的地形节点") return None + terrain_node.setTag("is_scene_element", "1") + terrain_node.setTag("tree_item_type", "TERRAIN_NODE") + node_name = f"Terrain_{os.path.basename(heightmap_path)}_{len(self.terrains)}" terrain_node.setName(node_name) @@ -235,6 +238,9 @@ class TerrainManager: print("错误:无法生成有效的平面地形节点") return None + terrain_node.setTag("is_scene_element", "1") + terrain_node.setTag("tree_item_type", "TERRAIN_NODE") + node_name = f"FlatTerrain_{len(self.terrains)}_{int(time.time() * 1000000) % 10000}" terrain_node.setName(node_name) diff --git a/gui/gui_manager.py b/gui/gui_manager.py index 7f7fdfb8..4fcdf027 100644 --- a/gui/gui_manager.py +++ b/gui/gui_manager.py @@ -84,6 +84,8 @@ except ImportError: # 为GUI元素添加标识(效仿3D文本方法) image_node.setTag("gui_type", "3d_image") image_node.setTag("gui_id", f"3d_image_{len(self.gui_elements)}") + image_node.setTag("is_scene_element", "1") + image_node.setTag("tree_item_type", "GUI_3DIMAGE") if image_path: image_node.setTag("gui_image_path", image_path) image_node.setTag("is_gui_element", "1") @@ -189,6 +191,7 @@ class GUIManager: button.setTag("gui_text", text) button.setTag("is_gui_element", "1") button.setTag("is_scene_element", "1") # 确保这个标签被设置 + button.setTag("tree_item_type", "GUI_BUTTON") button.setTag("saved_gui_type", "button") # 添加这个标签以确保兼容性 button.setTag("gui_element_type","button") button.setTag("created_by_user", "1") @@ -305,6 +308,7 @@ class GUIManager: label.setTag("gui_id", f"label_{len(self.gui_elements)}") label.setTag("gui_text", text) label.setTag("is_gui_element", "1") + label.setTag("tree_item_type", "GUI_LABEL") label.setTag("is_scene_element", "1") label.setTag("created_by_user", "1") label.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") @@ -416,6 +420,7 @@ class GUIManager: entry.setTag("gui_id", f"entry_{len(self.gui_elements)}") entry.setTag("gui_placeholder", placeholder) entry.setTag("is_gui_element", "1") + entry.setTag("tree_item_type", "GUI_ENTRY") entry.setTag("is_scene_element", "1") entry.setTag("created_by_user", "1") entry.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") @@ -549,6 +554,7 @@ class GUIManager: image_node.setTag("gui_text", f"2D图片_{len(self.gui_elements)}") image_node.setTag("is_gui_element", "1") image_node.setTag("is_scene_element", "1") + image_node.setTag("tree_item_type", "GUI_IMAGE") image_node.setTag("created_by_user", "1") image_node.setTag("gui_parent_type", "gui" if parent_gui_node else "3d") image_node.setName(image_name) @@ -700,6 +706,7 @@ class GUIManager: textNodePath.setTag("gui_text", text) textNodePath.setTag("is_gui_element", "1") textNodePath.setTag("is_scene_element", "1") + textNodePath.setTag("tree_item_type", "GUI_3DTEXT") textNodePath.setTag("created_by_user", "1") # 添加到GUI元素列表 @@ -836,6 +843,7 @@ class GUIManager: image_node.setTag("gui_image_path", image_path) image_node.setTag("is_gui_element", "1") image_node.setTag("is_scene_element", "1") + image_node.setTag("tree_item_type", "GUI_3DIMAGE") image_node.setTag("created_by_user", "1") # 添加到GUI元素列表 @@ -953,6 +961,7 @@ class GUIManager: video_screen.setTag("gui_text", f"视频屏幕_{len(self.gui_elements)}") video_screen.setTag("is_gui_element", "1") video_screen.setTag("is_scene_element", "1") + video_screen.setTag("tree_item_type", "GUI_VIDEO_SCREEN") video_screen.setTag("created_by_user", "1") # 设置视频路径标签 @@ -1473,6 +1482,7 @@ class GUIManager: video_screen.setTag("gui_text", f"2D视频屏幕_{len(self.gui_elements)}") video_screen.setTag("is_gui_element", "1") video_screen.setTag("is_scene_element", "1") + video_screen.setTag("tree_item_type", "GUI_2D_VIDEO_SCREEN") video_screen.setTag("created_by_user", "1") # 设置视频路径标签 @@ -1785,6 +1795,7 @@ class GUIManager: # 设置标签以便识别和管理 sphere_np.setTag("gui_type", "spherical_video") sphere_np.setTag("is_gui_element", "1") + sphere_np.setTag("tree_item_type", "GUI_SPHERICAL_VIDEO") sphere_np.setTag("video_path", video_path or "") sphere_np.setTag("original_radius", str(radius)) @@ -1997,6 +2008,7 @@ class GUIManager: virtual_screen.setTag("gui_text", text) virtual_screen.setTag("is_gui_element", "1") virtual_screen.setTag("is_scene_element", "1") + virtual_screen.setTag("tree_item_type", "GUI_VirtualScreen") virtual_screen.setTag("created_by_user", "1") # 添加到GUI元素列表 diff --git a/project/project_manager.py b/project/project_manager.py index 5ed451ce..c3370792 100644 --- a/project/project_manager.py +++ b/project/project_manager.py @@ -79,7 +79,7 @@ class ProjectManager: # 自动保存初始场景 scene_file = os.path.join(scenes_path, "scene.bam") - if self.world.scene_manager.saveScene(scene_file): + if self.world.scene_manager.saveScene(scene_file, project_path): # 更新配置文件中的场景路径 project_config["scene_file"] = os.path.relpath(scene_file, full_project_path) with open(config_file, "w", encoding="utf-8") as f: @@ -302,7 +302,7 @@ class ProjectManager: return False # 保存场景 - if self.world.scene_manager.saveScene(scene_file): + if self.world.scene_manager.saveScene(scene_file, project_path): # 更新项目配置文件 config_file = os.path.join(project_path, "project.json") if os.path.exists(config_file): diff --git a/scene/scene_manager.py b/scene/scene_manager.py index 261b8e73..6e21687b 100644 --- a/scene/scene_manager.py +++ b/scene/scene_manager.py @@ -169,6 +169,7 @@ class SceneManager: model.setTag("file", model_name) model.setTag("is_model_root", "1") model.setTag("is_scene_element", "1") + model.setTag("tree_item_type", "IMPORTED_MODEL_NODE") # 记录应用的处理选项 if apply_unit_conversion: @@ -724,7 +725,7 @@ class SceneManager: # ==================== 场景保存和加载 ==================== - def saveScene(self, filename): + def saveScene(self, filename, project_path): """保存场景到BAM文件 - 完整版,支持GUI元素""" try: print(f"\n=== 开始保存场景到: {filename} ===") @@ -874,6 +875,7 @@ class SceneManager: self.world.render.ls() print("---------------------------------") + self.take_screenshot(project_path) # 保存场景 success = self.world.render.writeBamFile(Filename.fromOsSpecific(filename)) #success_2d = self.world.render2d.writeBamFile(Filename.fromOsSpecific(filename)) @@ -902,6 +904,51 @@ class SceneManager: traceback.print_exc() return False + def take_screenshot(self, projectpath): + """ + 截图并保存到指定的完整路径 + + Args: + full_path (str): 完整的文件保存路径,包括文件名和扩展名 + + Returns: + bool: 截图是否成功 + """ + try: + from panda3d.core import Filename + import os + + print(f"\n=== 截图保存: {projectpath} ===") + + # 确保目录存在 + directory = os.path.dirname(projectpath) + if directory and not os.path.exists(directory): + os.makedirs(directory) + print(f"创建目录: {directory}") + + # 规范化路径 + filename = os.path.basename(os.path.normpath(projectpath)) + filename = f'{filename}.png' + print(f'project_path: {projectpath}') + print(f'project_name: {filename}') + full_path = os.path.normpath(os.path.join(projectpath, filename)) + p3d_filename = Filename.from_os_specific(full_path) + # 使用 Panda3D 的截图功能 + success = self.world.win.saveScreenshot(p3d_filename) + + if success: + print(f"✅ 成功截图并保存到: {full_path}") + return True + else: + print(f"❌ 截图保存失败: {full_path}") + return False + + except Exception as e: + print(f"保存截图时发生错误: {str(e)}") + import traceback + traceback.print_exc() + return False + def loadScene(self, filename): """从BAM文件加载场景 - 修复版""" try: @@ -972,7 +1019,7 @@ class SceneManager: print("场景加载失败") return False - # tree_widget.create_model_items(scene) + tree_widget.create_model_items(scene) # 遍历场景中的所有模型节点 # 用于存储处理后的灯光节点,避免重复处理 processed_lights = [] @@ -1226,7 +1273,7 @@ class SceneManager: scene.removeNode() # 更新场景树 - self.updateSceneTree() + # self.updateSceneTree() # self._get_tree_widget().create_model_items(scene) print(f"加载完成,GUI元素数量: {len(self.world.gui_elements)}") @@ -1835,6 +1882,7 @@ class SceneManager: # 设置节点属性和标签 light_np.setTag("light_type", "spot_light") light_np.setTag("is_scene_element", "1") + light_np.setTag("tree_item_type", "LIGHT_NODE") light_np.setTag("light_energy", str(light.energy)) light_np.setTag("created_by_user", "1") @@ -1941,6 +1989,7 @@ class SceneManager: # 设置节点属性和标签 light_np.setTag("light_type", "point_light") light_np.setTag("is_scene_element", "1") + light_np.setTag("tree_item_type", "LIGHT_NODE") light_np.setTag("light_energy", str(light.energy)) light_np.setTag("created_by_user", "1") @@ -2555,6 +2604,7 @@ except Exception as e: # 添加标签以便场景识别和保存 tileset_node.setTag("is_scene_element", "1") + tileset_node.setTag("tree_item_type", "CESIUM_TILESET_NODE") tileset_node.setTag("element_type", "cesium_tileset") tileset_node.setTag("tileset_url", tileset_url) # 使用唯一名称作为文件标识,代替索引 diff --git a/ui/main_window.py b/ui/main_window.py index 9f935eaf..66f5b9ff 100644 --- a/ui/main_window.py +++ b/ui/main_window.py @@ -9,7 +9,7 @@ import os import sys -from PyQt5.QtGui import QKeySequence, QIcon +from PyQt5.QtGui import QKeySequence, QIcon, QPalette, QColor from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction, QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem, QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, @@ -23,12 +23,119 @@ from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget,Cus class MainWindow(QMainWindow): """主窗口类""" - + def __init__(self, world): super().__init__() self.world = world self.world.main_window = self # 关键:让world对象能访问主窗口 + self.setStyleSheet(""" + QMainWindow { + background-color: #1e1e2e; + } + QMenuBar { + background-color: #252538; + color: #e0e0ff; + border-bottom: 1px solid #3a3a4a; + } + QMenuBar::item { + background-color: transparent; + padding: 4px 8px; + } + QMenuBar::item:selected { + background-color: rgba(139, 92, 246, 100); + } + QMenuBar::item:pressed { + background-color: rgba(139, 92, 246, 150); + } + QMenu { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + } + QMenu::item { + padding: 4px 20px; + } + QMenu::item:selected { + background-color: rgba(139, 92, 246, 100); + } + QPushButton { + background-color: #8b5cf6; + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + } + QPushButton:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + QPushButton:disabled { + background-color: #4c4c6e; + color: #8888aa; + } + QComboBox { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + padding: 4px 8px; + } + QComboBox::drop-down { + border: none; + } + QComboBox QAbstractItemView { + background-color: #2d2d44; + color: #e0e0ff; + selection-background-color: rgba(139, 92, 246, 100); + } + QLineEdit { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + padding: 4px; + } + QSpinBox, QDoubleSpinBox { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + padding: 4px; + } + QScrollBar:vertical { + background-color: #252538; + width: 15px; + border: none; + } + QScrollBar::handle:vertical { + background-color: #3a3a4a; + border-radius: 4px; + min-height: 20px; + } + QScrollBar::handle:vertical:hover { + background-color: #8b5cf6; + } + QScrollBar:horizontal { + background-color: #252538; + width: 15px; + border: none; + } + QScrollBar::handle:horizontal { + background-color: #3a3a4a; + border-radius: 4px; + min-height: 20px; + } + QScrollBar::handle:horizontal:hover { + background-color: #8b5cf6; + } + """) + # 设置 QMessageBox 样式表 + self.setupMessageBoxStyles() + self.setupCenterWidget() # 创建中间部分Panda3D self.setupMenus() # 创建菜单栏 self.setupDockWindows() @@ -47,6 +154,173 @@ class MainWindow(QMainWindow): self.dragStartPos = QPoint(0, 0) self.toolbarStartPos = QPoint(0, 0) + def setupMessageBoxStyles(self): + """设置 QMessageBox 的全局样式""" + # 设置 QMessageBox 的样式表 + msg_box_style = """ + QMessageBox { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + } + QMessageBox QLabel { + color: #e0e0ff; + } + QMessageBox QPushButton { + background-color: #8b5cf6; + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + min-width: 80px; + } + QMessageBox QPushButton:hover { + background-color: #7c3aed; + } + QMessageBox QPushButton:pressed { + background-color: #6d28d9; + } + QMessageBox QPushButton:disabled { + background-color: #4c4c6e; + color: #8888aa; + } + """ + + # 应用全局样式 + self.setStyleSheet(self.styleSheet() + msg_box_style) + + def createStyledInputDialog(self, parent, title, label, mode=QLineEdit.Normal, text=""): + """创建带有统一主题样式的 QInputDialog""" + dialog = QInputDialog(parent) + dialog.setWindowTitle(title) + dialog.setLabelText(label) + dialog.setTextEchoMode(mode) + dialog.setTextValue(text) + + # 设置样式表 + dialog.setStyleSheet(""" + QInputDialog { + background-color: #252538; + color: #e0e0ff; + } + QLabel { + color: #e0e0ff; + font-weight: 500; + } + QLineEdit { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + padding: 6px; + } + QPushButton { + background-color: #8b5cf6; + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + min-width: 80px; + } + QPushButton:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + QPushButton:disabled { + background-color: #4c4c6e; + color: #8888aa; + } + """) + + return dialog + + def createStyledFileDialog(self, parent, caption, directory="", filter=""): + """创建带有统一主题样式的 QFileDialog""" + dialog = QFileDialog(parent) + dialog.setWindowTitle(caption) + + # 设置样式表 + dialog.setStyleSheet(""" + QFileDialog { + background-color: #252538; + color: #e0e0ff; + } + QLabel { + color: #e0e0ff; + } + QListView { + background-color: #1e1e2e; + color: #e0e0ff; + border: 1px solid #3a3a4a; + alternate-background-color: #252538; + } + QListView::item:hover { + background-color: #3a3a4a; + } + QListView::item:selected { + background-color: rgba(139, 92, 246, 100); + color: white; + } + QTreeView { + background-color: #1e1e2e; + color: #e0e0ff; + border: 1px solid #3a3a4a; + alternate-background-color: #252538; + } + QTreeView::item:hover { + background-color: #3a3a4a; + } + QTreeView::item:selected { + background-color: rgba(139, 92, 246, 100); + color: white; + } + QComboBox { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + padding: 4px 8px; + } + QComboBox::drop-down { + border: none; + } + QComboBox QAbstractItemView { + background-color: #2d2d44; + color: #e0e0ff; + selection-background-color: rgba(139, 92, 246, 100); + } + QPushButton { + background-color: #8b5cf6; + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + } + QPushButton:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + QPushButton:disabled { + background-color: #4c4c6e; + color: #8888aa; + } + QLineEdit { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + padding: 4px; + } + """) + + return dialog def setupCenterWidget(self): """设置窗口基本属性""" @@ -88,28 +362,56 @@ class MainWindow(QMainWindow): # 创建工具栏容器 self.embeddedToolbar = QFrame(self.pandaWidget) self.embeddedToolbar.setObjectName("UnityToolbar") + # self.embeddedToolbar.setStyleSheet(""" + # QFrame#UnityToolbar { + # background-color: rgba(240, 240, 240, 180); + # border: 1px solid rgba(200, 200, 200, 200); + # border-radius: 4px; + # } + # QToolButton { + # background-color: rgba(250, 250, 250, 150); + # border: none; + # color: #333333; + # padding: 5px; + # border-radius: 3px; + # } + # QToolButton:hover { + # background-color: rgba(220, 220, 220, 200); + # } + # QToolButton:checked { + # background-color: rgba(100, 150, 220, 180); + # color: white; + # } + # QToolButton:pressed { + # background-color: rgba(80, 130, 200, 200); + # } + # """) self.embeddedToolbar.setStyleSheet(""" QFrame#UnityToolbar { - background-color: rgba(240, 240, 240, 180); - border: 1px solid rgba(200, 200, 200, 200); - border-radius: 4px; + background-color: rgba(40, 40, 60, 200); /* 深蓝灰色背景 */ + border: 1px solid rgba(80, 80, 120, 200); /* 深蓝灰边框 */ + border-radius: 6px; + padding: 4px; } QToolButton { - background-color: rgba(250, 250, 250, 150); - border: none; - color: #333333; - padding: 5px; - border-radius: 3px; + background-color: rgba(60, 60, 90, 180); /* 稍亮的深蓝灰 */ + border: 1px solid rgba(100, 100, 150, 150); + color: #e0e0ff; /* 浅蓝白色文字 */ + padding: 6px 8px; + border-radius: 4px; + font-weight: 500; } QToolButton:hover { - background-color: rgba(220, 220, 220, 200); + background-color: rgba(80, 80, 130, 200); /* 悬停时更亮 */ + border: 1px solid rgba(139, 92, 246, 150); /* 紫色边框 */ } QToolButton:checked { - background-color: rgba(100, 150, 220, 180); + background-color: rgba(139, 92, 246, 180); /* 紫色背景 */ color: white; + border: 1px solid rgba(180, 140, 255, 200); } QToolButton:pressed { - background-color: rgba(80, 130, 200, 200); + background-color: rgba(120, 75, 220, 200); /* 按下时稍暗的紫色 */ } """) @@ -530,6 +832,43 @@ class MainWindow(QMainWindow): """创建停靠窗口""" # 创建左侧停靠窗口(层级窗口) self.leftDock = QDockWidget("层级", self) + self.leftDock.setStyleSheet(""" + QDockWidget { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + } + QDockWidget::title { + background-color: #2d2d44; + padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ + border-bottom: 0px solid #3a3a4a; + } + QDockWidget::close-button, QDockWidget::float-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + } + QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background-color: #7c3aed; /* 悬停时显示较亮的背景 */ + } + QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + background-color: #8b5cf6; /* 按下时显示紫色高亮 */ + } + QTreeView { + background-color: #1e1e2e; + color: #e0e0ff; + border: 1px solid #3a3a4a; + alternate-background-color: #252538; + } + QTreeView::item:hover { + background-color: #3a3a4a; + } + QTreeView::item:selected { + background-color: rgba(139, 92, 246, 100); + color: white; + } + """) self.treeWidget = CustomTreeWidget(self.world) self.world.setTreeWidget(self.treeWidget) # 设置树形控件引用 self.leftDock.setWidget(self.treeWidget) @@ -537,6 +876,55 @@ class MainWindow(QMainWindow): # 创建右侧停靠窗口(属性窗口) self.rightDock = QDockWidget("属性", self) + self.rightDock.setStyleSheet(""" + QDockWidget { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + } + QDockWidget::title { + background-color: #2d2d44; + padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ + border-bottom: 0px solid #3a3a4a; + } + QDockWidget::close-button, QDockWidget::float-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + } + QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background-color: #7c3aed; /* 悬停时显示较亮的背景 */ + } + QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + background-color: #8b5cf6; /* 按下时显示紫色高亮 */ + } + QScrollArea { + background-color: #1e1e2e; + border: none; + } + QWidget#PropertyContainer { + background-color: #1e1e2e; + } + QLabel { + color: #e0e0ff; + } + QGroupBox { + background-color: #252538; + border: 1px solid #3a3a4a; + border-radius: 6px; + margin-top: 1ex; + color: #e0e0ff; + font-weight: 500; + padding-top: 10px; /* 增加顶部内边距,使标题和内容分离 */ + } + QGroupBox::title { + subline-offset: -2px; + padding: 0 8px; + color: #c0c0e0; + font-weight: 500; + } + """) # 创建属性面板的主容器和布局 self.propertyContainer = QWidget() @@ -585,9 +973,64 @@ class MainWindow(QMainWindow): # 创建脚本管理停靠窗口 self.scriptDock = QDockWidget("脚本管理", self) + self.scriptDock.setStyleSheet(""" + QDockWidget { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + } + QDockWidget::title { + background-color: #2d2d44; + padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ + border-bottom: 0px solid #3a3a4a; + } + QDockWidget::close-button, QDockWidget::float-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + } + QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background-color: #7c3aed; /* 悬停时显示较亮的背景 */ + } + QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + background-color: #8b5cf6; /* 按下时显示紫色高亮 */ + } + QScrollArea { + background-color: #1e1e2e; + border: none; + } + QWidget#PropertyContainer { + background-color: #1e1e2e; + } + QLabel { + color: #e0e0ff; + } + QGroupBox { + background-color: #252538; + border: 1px solid #3a3a4a; + border-radius: 6px; + margin-top: 1ex; + color: #e0e0ff; + font-weight: 500; + padding-top: 10px; /* 增加顶部内边距,使标题和内容分离 */ + } + QGroupBox::title { + subline-offset: -2px; + padding: 0 8px; + color: #c0c0e0; + font-weight: 500; + } + """) # 创建脚本面板的主容器和布局(与属性面板相同结构) self.scriptContainer = QWidget() + # 设置脚本容器的背景色 + self.scriptContainer.setStyleSheet(""" + QWidget#ScriptContainer { + background-color: #1e1e2e; + } + """) self.scriptContainer.setObjectName("ScriptContainer") self.scriptLayout = QVBoxLayout(self.scriptContainer) @@ -604,9 +1047,6 @@ class MainWindow(QMainWindow): self.setupScriptPanel(self.scriptLayout) self.addDockWidget(Qt.RightDockWidgetArea, self.scriptDock) - # 将右侧停靠窗口设为标签形式 - self.tabifyDockWidget(self.rightDock, self.scriptDock) - # # 创建底部停靠窗口(资源窗口) # self.bottomDock = QDockWidget("资源", self) # self.bottomDock.setAllowedAreas(Qt.BottomDockWidgetArea) @@ -634,16 +1074,159 @@ class MainWindow(QMainWindow): # 创建底部停靠窗口(资源窗口) self.bottomDock = QDockWidget("资源", self) + self.bottomDock.setStyleSheet(""" + QDockWidget { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + } + QDockWidget::title { + background-color: #2d2d44; + padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ + border-bottom: 0px solid #3a3a4a; + } + QDockWidget::close-button, QDockWidget::float-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + } + QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background-color: #7c3aed; /* 悬停时显示较亮的背景 */ + } + QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + background-color: #8b5cf6; /* 按下时显示紫色高亮 */ + } + """) + self.fileView = CustomAssetsTreeWidget(self.world) + # 为资源树添加样式 + self.fileView.setStyleSheet(""" + QTreeWidget { + background-color: #1e1e2e; + color: #e0e0ff; + border: 1px solid #3a3a4a; + alternate-background-color: #252538; + } + QTreeWidget::item:hover { + background-color: #3a3a4a; + } + QTreeWidget::item:selected { + background-color: rgba(139, 92, 246, 100); + color: white; + } + QHeaderView::section { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + padding: 4px; + } + """) self.bottomDock.setWidget(self.fileView) self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock) # 创建底部停靠控制台 self.consoleDock = QDockWidget("控制台", self) + self.consoleDock.setStyleSheet(""" + QDockWidget { + background-color: #252538; + color: #e0e0ff; + border: 1px solid #3a3a4a; + } + QDockWidget::title { + background-color: #2d2d44; + padding: 0px 0px; /* 增加内边距,提供更多的垂直空间 */ + border-bottom: 0px solid #3a3a4a; + } + QDockWidget::close-button, QDockWidget::float-button { + background-color: #8b5cf6; + border: none; + icon-size: 8px; /* 调整图标大小 */ + border-radius: 4px; /* 增加圆角 */ + } + QDockWidget::close-button:hover, QDockWidget::float-button:hover { + background-color: #7c3aed; /* 悬停时显示较亮的背景 */ + } + QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { + background-color: #8b5cf6; /* 按下时显示紫色高亮 */ + } + """) self.consoleView = CustomConsoleDockWidget(self.world) + # 为控制台添加样式 + self.consoleView.setStyleSheet(""" + QTextEdit { + background-color: #1e1e2e; + color: #e0e0ff; + border: 1px solid #3a3a4a; + font-family: 'Consolas', 'Monaco', monospace; + } + """) self.consoleDock.setWidget(self.consoleView) self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock) + # 将右侧停靠窗口设为标签形式 + # self.tabifyDockWidget(self.rightDock, self.scriptDock) + # # 将底部的两个窗口也标签化 + # self.tabifyDockWidget(self.bottomDock, self.consoleDock) + # 设定默认显示的标签 + self.bottomDock.raise_() + self.rightDock.raise_() + self.scriptDock.raise_() + self.consoleDock.raise_() + self.leftDock.raise_() + # ========================================================================= + # ↓↓↓ 新增代码:为停靠窗口的标签栏(QTabBar)设置统一样式 ↓↓↓ + # ========================================================================= + # 这段样式会应用到主窗口内的所有 QTabBar,特别是停靠区域的标签栏。 + tab_bar_style = """ + /* QTabBar 的整体样式 */ + QTabBar { + qproperty-drawBase: 0; /* 移除标签栏底部的线条 */ + } + + QTabBar::tab { + background-color: #2d2d44; /* 未选中标签的背景色,与标题栏一致 */ + color: #c0c0e0; /* 未选中标签的文字颜色 */ + border: 1px solid #3a3a4a; /* 边框颜色 */ + border-bottom: none; /* 移除底部边框 */ + border-top-left-radius: 6px; + border-top-right-radius: 6px; + padding: 8px 16px; /* 内边距,让文字不拥挤 */ + font-weight: 500; + margin-right: 2px; /* 标签之间的间距 */ + } + + /* 鼠标悬停在标签上时的样式 */ + QTabBar::tab:hover { + background-color: #3a3a4a; /* 悬停时使用稍亮的背景色 */ + } + + /* 当前选中的标签页的样式 */ + QTabBar::tab:selected { + background-color: #252538; /* 选中时使用 Dock 背景色 */ + color: #ffffff; /* 选中时使用更亮的文字颜色 */ + font-weight: bold; /* 字体加粗 */ + border-color: #3a3a4a; + border-bottom: 2px solid #8b5cf6; /* 选中标签的底部高亮线 */ + } + + /* 未选中的标签 */ + QTabBar::tab:!selected { + margin-top: 2px; /* 未选中标签稍微下移,创建层次感 */ + } + + /* 标签栏底部的线条 */ + QTabBar::tab-bar { + alignment: left; + border: none; /* 移除标签栏边框 */ + } + """ + + # 获取主窗口现有的样式表,并附加我们新的样式规则 + # 这样可以避免覆盖掉其他可能存在的全局样式 + existing_style = self.styleSheet() + self.setStyleSheet(existing_style + tab_bar_style) + def setupToolbar(self): """创建工具栏""" self.toolbar = self.addToolBar('工具栏') @@ -785,6 +1368,31 @@ class MainWindow(QMainWindow): # 脚本列表 self.scriptsList = QListWidget() + self.scriptsList.setStyleSheet(""" + QListWidget { + background-color: #1e1e2e; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + alternate-background-color: #252538; + selection-background-color: rgba(139, 92, 246, 100); + selection-color: white; + } + QListWidget::item { + padding: 6px 8px; + border-bottom: 1px solid #2d2d44; + } + QListWidget::item:last-child { + border-bottom: none; + } + QListWidget::item:hover { + background-color: #3a3a4a; + } + QListWidget::item:selected { + background-color: rgba(139, 92, 246, 120); + color: white; + } + """) self.scriptsList.itemDoubleClicked.connect(self.onScriptDoubleClick) scriptsLayout.addWidget(self.scriptsList) @@ -828,6 +1436,32 @@ class MainWindow(QMainWindow): # 已挂载脚本列表 self.mountedScriptsList = QListWidget() + self.mountedScriptsList.setStyleSheet(""" + QListWidget { + background-color: #1e1e2e; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + alternate-background-color: #252538; + selection-background-color: rgba(139, 92, 246, 100); + selection-color: white; + max-height: 120px; + } + QListWidget::item { + padding: 4px 8px; + border-bottom: 1px solid #2d2d44; + } + QListWidget::item:last-child { + border-bottom: none; + } + QListWidget::item:hover { + background-color: #3a3a4a; + } + QListWidget::item:selected { + background-color: rgba(139, 92, 246, 120); + color: white; + } + """) self.mountedScriptsList.setMaximumHeight(100) mountLayout.addWidget(QLabel("已挂载脚本:")) mountLayout.addWidget(self.mountedScriptsList) @@ -920,13 +1554,21 @@ class MainWindow(QMainWindow): def onUpdateCesiumURL(self): """更新 Cesium URL""" - url, ok = QInputDialog.getText(self, "更新 Cesium URL", "输入新的 URL:", - QLineEdit.Normal, "http://localhost:8080/Apps/HelloWorld.html") - if ok and url: - if hasattr(self.world, 'gui_manager') and self.world.gui_manager: - self.world.gui_manager.updateCesiumURL(url) - else: - QMessageBox.warning(self, "错误", "GUI 管理器不可用") + dialog = self.createStyledInputDialog( + self, + "更新 Cesium URL", + "输入新的 URL:", + QLineEdit.Normal, + "http://localhost:8080/Apps/HelloWorld.html" + ) + if dialog.exec_() == QDialog.Accepted: + url = dialog.textValue() + if url: + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + self.world.gui_manager.updateCesiumURL(url) + else: + QMessageBox.warning(self, "错误", "GUI 管理器不可用") + def onAddModelClicked(self): """处理加入模型按钮点击事件""" @@ -960,53 +1602,56 @@ class MainWindow(QMainWindow): def showAddModelDialog(self): """显示添加模型对话框""" # 打开文件选择对话框 - file_path, _ = QFileDialog.getOpenFileName( + dialog = self.createStyledFileDialog( self, "选择 3D 模型文件", "", "3D 模型文件 (*.glb *.gltf *.obj);;所有文件 (*)" ) - if file_path: - # 获取模型位置信息 - coords, ok = self.getModelCoordinates() - if ok: - longitude, latitude, height, scale = coords + if dialog.exec_() == QDialog.Accepted: + file_path = dialog.selectedFiles()[0] + if file_path: + # 获取模型位置信息 + coords, ok = self.getModelCoordinates() + if ok: + longitude, latitude, height, scale = coords - # 生成唯一的模型 ID - import uuid - model_id = f"model_{uuid.uuid4().hex[:8]}" + # 生成唯一的模型 ID + import uuid + model_id = f"model_{uuid.uuid4().hex[:8]}" - try: - # 添加模型到 Cesium - if hasattr(self.world, 'gui_manager') and self.world.gui_manager: - success = self.world.gui_manager.addModelToCesium( - model_id, - file_path, - longitude, - latitude, - height, - scale + try: + # 添加模型到 Cesium + if hasattr(self.world, 'gui_manager') and self.world.gui_manager: + success = self.world.gui_manager.addModelToCesium( + model_id, + file_path, + longitude, + latitude, + height, + scale + ) + + if success: + QMessageBox.information( + self, + "成功", + f"模型已成功添加到地图!\n模型ID: {model_id}" + ) + else: + QMessageBox.warning( + self, + "失败", + "添加模型失败,请检查控制台输出" + ) + except Exception as e: + QMessageBox.critical( + self, + "错误", + f"添加模型时发生错误:\n{str(e)}" ) - if success: - QMessageBox.information( - self, - "成功", - f"模型已成功添加到地图!\n模型ID: {model_id}" - ) - else: - QMessageBox.warning( - self, - "失败", - "添加模型失败,请检查控制台输出" - ) - except Exception as e: - QMessageBox.critical( - self, - "错误", - f"添加模型时发生错误:\n{str(e)}" - ) def getModelCoordinates(self): """获取模型坐标信息的对话框""" @@ -1079,7 +1724,7 @@ class MainWindow(QMainWindow): return None, False def onLoadCesiumTileset(self): - url,ok = QInputDialog.getText( + dialog = self.createStyledInputDialog( self, "加载 Cesium 3D Tiles", "输入 tileset.json URL:", @@ -1087,34 +1732,35 @@ class MainWindow(QMainWindow): "https://assets.ion.cesium.com/96128/tileset.json" ) - if ok and url: - try: - # 生成唯一的 tileset 名称 - import uuid - tileset_name = f"tileset_{uuid.uuid4().hex[:8]}" - - # 加载 tileset - if hasattr(self.world, 'addCesiumTileset'): - success = self.world.addCesiumTileset(tileset_name, url, (0, 0, 0)) - if success: - QMessageBox.information( - self, - "成功", - f"Cesium 3D Tiles 已加载到场景中!\n名称: {tileset_name}" - ) - else: - QMessageBox.warning( - self, - "失败", - "加载 Cesium 3D Tiles 失败" - ) - except Exception as e: - QMessageBox.critical( - self, - "错误", - f"加载 Cesium 3D Tiles 时发生错误:\n{str(e)}" - ) + if dialog.exec_() == QDialog.Accepted: + url = dialog.textValue() + if url: + try: + # 生成唯一的 tileset 名称 + import uuid + tileset_name = f"tileset_{uuid.uuid4().hex[:8]}" + # 加载 tileset + if hasattr(self.world, 'addCesiumTileset'): + success = self.world.addCesiumTileset(tileset_name, url, (0, 0, 0)) + if success: + QMessageBox.information( + self, + "成功", + f"Cesium 3D Tiles 已加载到场景中!\n名称: {tileset_name}" + ) + else: + QMessageBox.warning( + self, + "失败", + "加载 Cesium 3D Tiles 失败" + ) + except Exception as e: + QMessageBox.critical( + self, + "错误", + f"加载 Cesium 3D Tiles 时发生错误:\n{str(e)}" + ) def onToolChanged(self, button): """工具切换事件处理""" @@ -1707,17 +2353,21 @@ class MainWindow(QMainWindow): def onCreateScriptDialog(self): """菜单创建脚本事件""" - script_name, ok = QInputDialog.getText(self, "创建脚本", "输入脚本名称:") - if ok and script_name.strip(): - try: - success = self.world.createScript(script_name.strip(), "basic") - if success: - QMessageBox.information(self, "成功", f"脚本 '{script_name}' 创建成功!") - self.refreshScriptsList() - else: - QMessageBox.warning(self, "错误", f"脚本 '{script_name}' 创建失败!") - except Exception as e: - QMessageBox.critical(self, "错误", f"创建脚本时出错: {str(e)}") + dialog = self.createStyledInputDialog(self, "创建脚本", "输入脚本名称:") + + if dialog.exec_() == QDialog.Accepted: + script_name = dialog.textValue() + if script_name.strip(): + try: + success = self.world.createScript(script_name.strip(), "basic") + if success: + QMessageBox.information(self, "成功", f"脚本 '{script_name}' 创建成功!") + self.refreshScriptsList() + else: + QMessageBox.warning(self, "错误", f"脚本 '{script_name}' 创建失败!") + except Exception as e: + QMessageBox.critical(self, "错误", f"创建脚本时出错: {str(e)}") + def onLoadScript(self): """加载脚本按钮事件""" @@ -1738,19 +2388,26 @@ class MainWindow(QMainWindow): def onLoadScriptFile(self): """加载脚本文件菜单事件""" - file_path, _ = QFileDialog.getOpenFileName( - self, "选择脚本文件", "", "Python文件 (*.py)" + dialog = self.createStyledFileDialog( + self, + "选择脚本文件", + "", + "Python文件 (*.py)" ) - if file_path: - try: - success = self.world.loadScript(file_path) - if success: - QMessageBox.information(self, "成功", "脚本文件加载成功!") - self.refreshScriptsList() - else: - QMessageBox.warning(self, "错误", "脚本文件加载失败!") - except Exception as e: - QMessageBox.critical(self, "错误", f"加载脚本文件时出错: {str(e)}") + + if dialog.exec_() == QDialog.Accepted: + file_path = dialog.selectedFiles()[0] + if file_path: + try: + success = self.world.loadScript(file_path) + if success: + QMessageBox.information(self, "成功", "脚本文件加载成功!") + self.refreshScriptsList() + else: + QMessageBox.warning(self, "错误", "脚本文件加载失败!") + except Exception as e: + QMessageBox.critical(self, "错误", f"加载脚本文件时出错: {str(e)}") + def onReloadAllScripts(self): """重载所有脚本事件""" @@ -1869,6 +2526,43 @@ class MainWindow(QMainWindow): dialog.setWindowTitle("创建平面地形") dialog.setModal(True) dialog.resize(300,200) + # 设置对话框样式 + dialog.setStyleSheet(""" + QDialog { + background-color: #252538; + color: #e0e0ff; + } + QLabel { + color: #e0e0ff; + font-weight: 500; + } + QPushButton { + background-color: #8b5cf6; + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + min-width: 80px; + } + QPushButton:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + QPushButton:disabled { + background-color: #4c4c6e; + color: #8888aa; + } + QDoubleSpinBox, QSpinBox { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + padding: 4px; + } + """) layout = QVBoxLayout(dialog) @@ -1926,85 +2620,87 @@ class MainWindow(QMainWindow): def onCreateHeightmapTerrain(self): """从高度图创建地形""" - file_path,_=QFileDialog.getOpenFileName( + dialog = self.createStyledFileDialog( self, "选择高度图文件", "", "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga);;所有文件 (*)" ) - if file_path: - #创建对话框获取地形参数 - dialog = QDialog(self) - dialog.setWindowTitle("设置地形参数") - dialog.setModal(True) - dialog.resize(300,250) + if dialog.exec_() == QDialog.Accepted: + file_path = dialog.selectedFiles()[0] + if file_path: + #创建对话框获取地形参数 + dialog = QDialog(self) + dialog.setWindowTitle("设置地形参数") + dialog.setModal(True) + dialog.resize(300,250) - layout = QVBoxLayout(dialog) + layout = QVBoxLayout(dialog) - x_scale_layout = QHBoxLayout() - x_scale_layout.addWidget(QLabel("X缩放:")) - x_scale_spin = QDoubleSpinBox() - x_scale_spin.setRange(0.1,1000) - x_scale_spin.setValue(0.3) - x_scale_spin.setSingleStep(10) - x_scale_layout.addWidget(x_scale_spin) - layout.addLayout(x_scale_layout) + x_scale_layout = QHBoxLayout() + x_scale_layout.addWidget(QLabel("X缩放:")) + x_scale_spin = QDoubleSpinBox() + x_scale_spin.setRange(0.1,1000) + x_scale_spin.setValue(0.3) + x_scale_spin.setSingleStep(10) + x_scale_layout.addWidget(x_scale_spin) + layout.addLayout(x_scale_layout) - # Y缩放 - y_scale_layout = QHBoxLayout() - y_scale_layout.addWidget(QLabel("Y缩放:")) - y_scale_spin = QDoubleSpinBox() - y_scale_spin.setRange(0.1, 1000) - y_scale_spin.setValue(0.3) - y_scale_spin.setSingleStep(10) - y_scale_layout.addWidget(y_scale_spin) - layout.addLayout(y_scale_layout) + # Y缩放 + y_scale_layout = QHBoxLayout() + y_scale_layout.addWidget(QLabel("Y缩放:")) + y_scale_spin = QDoubleSpinBox() + y_scale_spin.setRange(0.1, 1000) + y_scale_spin.setValue(0.3) + y_scale_spin.setSingleStep(10) + y_scale_layout.addWidget(y_scale_spin) + layout.addLayout(y_scale_layout) - # Z缩放 - z_scale_layout = QHBoxLayout() - z_scale_layout.addWidget(QLabel("Z缩放:")) - z_scale_spin = QDoubleSpinBox() - z_scale_spin.setRange(0.1, 1000) - z_scale_spin.setValue(50) - z_scale_spin.setSingleStep(5) - z_scale_layout.addWidget(z_scale_spin) - layout.addLayout(z_scale_layout) + # Z缩放 + z_scale_layout = QHBoxLayout() + z_scale_layout.addWidget(QLabel("Z缩放:")) + z_scale_spin = QDoubleSpinBox() + z_scale_spin.setRange(0.1, 1000) + z_scale_spin.setValue(50) + z_scale_spin.setSingleStep(5) + z_scale_layout.addWidget(z_scale_spin) + layout.addLayout(z_scale_layout) - # 按钮 - button_layout = QHBoxLayout() - ok_button = QPushButton("创建") - cancel_button = QPushButton("取消") - button_layout.addWidget(ok_button) - button_layout.addWidget(cancel_button) - layout.addLayout(button_layout) + # 按钮 + button_layout = QHBoxLayout() + ok_button = QPushButton("创建") + cancel_button = QPushButton("取消") + button_layout.addWidget(ok_button) + button_layout.addWidget(cancel_button) + layout.addLayout(button_layout) - # 连接信号 - ok_button.clicked.connect(dialog.accept) - cancel_button.clicked.connect(dialog.reject) + # 连接信号 + ok_button.clicked.connect(dialog.accept) + cancel_button.clicked.connect(dialog.reject) - # 显示对话框 - if dialog.exec_() == QDialog.Accepted: - x_scale = x_scale_spin.value() - y_scale = y_scale_spin.value() - z_scale = z_scale_spin.value() + # 显示对话框 + if dialog.exec_() == QDialog.Accepted: + x_scale = x_scale_spin.value() + y_scale = y_scale_spin.value() + z_scale = z_scale_spin.value() - # 调用世界对象创建地形 - terrain_info = self.world.createTerrainFromHeightMap( - file_path, - (x_scale, y_scale, z_scale) - ) - if terrain_info: - QMessageBox.information(self, "成功", "高度图地形创建成功!") - else: - QMessageBox.warning(self, "错误", "高度图地形创建失败!") + # 调用世界对象创建地形 + terrain_info = self.world.createTerrainFromHeightMap( + file_path, + (x_scale, y_scale, z_scale) + ) + if terrain_info: + QMessageBox.information(self, "成功", "高度图地形创建成功!") + else: + QMessageBox.warning(self, "错误", "高度图地形创建失败!") def setup_main_window(world,path = None): """设置主窗口的便利函数""" app = QApplication.instance() if app is None: app = QApplication(sys.argv) - + main_window = MainWindow(world) main_window.show() from main import openProjectForPath diff --git a/ui/property_panel.py b/ui/property_panel.py index 0678dd6b..a943f54a 100644 --- a/ui/property_panel.py +++ b/ui/property_panel.py @@ -7,7 +7,7 @@ from typing import Hashable from PyQt5.QtGui import QColor from PyQt5.QtWidgets import (QLabel, QLineEdit, QDoubleSpinBox, QPushButton, QTreeWidget, QTreeWidgetItem, QMenu, QCheckBox, QComboBox, QHBoxLayout, QWidget, - QVBoxLayout, QGroupBox, QGridLayout, QSpinBox, QFileDialog) + QVBoxLayout, QGroupBox, QGridLayout, QSpinBox, QFileDialog, QMessageBox) from PyQt5.QtCore import Qt from deploy_libs.unicodedata import normalize from direct.actor.Actor import Actor @@ -235,23 +235,23 @@ class PropertyPanelManager: #材质属性 self._updateTerrainMaterialPanel(terrain_node,terrain_info) - #删除按钮 - delete_btn = QPushButton("删除地形") - delete_btn.setStyleSheet(""" - QPushButton{ - background-color:#ff4444; - color:white; - border:none; - padding:8px; - border-radius:4px; - margin-top:10px - } - QPushButton:hover{ - background-color:#ff6666; - } - """) - delete_btn.clicked.connect(lambda:self._deleteTerrain(terrain_info,item)) - self._propertyLayout.addWidget(delete_btn) + # #删除按钮 + # delete_btn = QPushButton("删除地形") + # delete_btn.setStyleSheet(""" + # QPushButton{ + # background-color:#ff4444; + # color:white; + # border:none; + # padding:8px; + # border-radius:4px; + # margin-top:10px + # } + # QPushButton:hover{ + # background-color:#ff6666; + # } + # """) + # delete_btn.clicked.connect(lambda:self._deleteTerrain(terrain_info,item)) + # self._propertyLayout.addWidget(delete_btn) except Exception as e: print(f"显示地形属性时出错: {e}") import traceback @@ -909,23 +909,23 @@ class PropertyPanelManager: scale_group.setLayout(scale_layout) self._propertyLayout.addWidget(scale_group) - # 删除按钮 - delete_btn = QPushButton("删除 Tileset") - delete_btn.setStyleSheet(""" - QPushButton { - background-color: #ff4444; - color: white; - border: none; - padding: 8px; - border-radius: 4px; - margin-top: 10px; - } - QPushButton:hover { - background-color: #ff6666; - } - """) - delete_btn.clicked.connect(lambda: self._deleteCesiumTileset(nodePath, item)) - self._propertyLayout.addWidget(delete_btn) + # # 删除按钮 + # delete_btn = QPushButton("删除 Tileset") + # delete_btn.setStyleSheet(""" + # QPushButton { + # background-color: #ff4444; + # color: white; + # border: none; + # padding: 8px; + # border-radius: 4px; + # margin-top: 10px; + # } + # QPushButton:hover { + # background-color: #ff6666; + # } + # """) + # delete_btn.clicked.connect(lambda: self._deleteCesiumTileset(nodePath, item)) + # self._propertyLayout.addWidget(delete_btn) # 添加弹性空间 self._propertyLayout.addStretch() @@ -1280,8 +1280,6 @@ class PropertyPanelManager: model.setScale(current_scale.getX(), current_scale.getY(), value) self.refreshModelValues(model) - - def updateGUIPropertyPanel(self, gui_element,item): """更新GUI元素属性面板""" self.clearPropertyPanel() diff --git a/ui/widgets.py b/ui/widgets.py index c1507f18..a2347cfb 100644 --- a/ui/widgets.py +++ b/ui/widgets.py @@ -18,7 +18,7 @@ from PyQt5.QtCore import Qt, QUrl, QMimeData from PyQt5.QtGui import QDrag, QPainter, QPixmap, QPen, QBrush from PyQt5.sip import wrapinstance from direct.showbase.ShowBaseGlobal import aspect2d -from panda3d.core import ModelRoot, NodePath +from panda3d.core import ModelRoot, NodePath, CollisionNode from QPanda3D.QPanda3DWidget import QPanda3DWidget from scene import util @@ -30,7 +30,68 @@ class NewProjectDialog(QDialog): super().__init__(parent) self.setWindowTitle("新建项目") self.setMinimumWidth(500) - + + # 设置对话框样式与主窗口保持一致 + self.setStyleSheet(""" + QDialog { + background-color: #252538; + color: #e0e0ff; + } + QGroupBox { + background-color: #2d2d44; + border: 1px solid #3a3a4a; + border-radius: 6px; + margin-top: 1ex; /* 保持这个设置 */ + color: #e0e0ff; + font-weight: 500; + padding-top: 10px; /* 增加顶部内边距,为标题留出空间 */ + } + QGroupBox::title { + subline-offset: -2px; + padding: 0 8px; + color: #c0c0e0; + font-weight: 500; + } + QLineEdit { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + border-radius: 4px; + padding: 6px; + } + QLineEdit:disabled { + background-color: #1e1e2e; + color: #8888aa; + } + QPushButton { + background-color: #8b5cf6; + color: white; + border: none; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + } + QPushButton:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + QPushButton:disabled { + background-color: #4c4c6e; + color: #8888aa; + } + QLabel { + color: #e0e0ff; + } + QLabel:disabled { + color: #8888aa; + } + QDialogButtonBox QPushButton { + min-width: 80px; + } + """) + # 创建布局 layout = QVBoxLayout(self) @@ -1251,12 +1312,60 @@ class CustomConsoleDockWidget(QWidget): self.autoScrollBtn = QPushButton("自动滚动") self.autoScrollBtn.setCheckable(True) self.autoScrollBtn.setChecked(True) + self.autoScrollBtn.setStyleSheet(""" + QPushButton { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + } + QPushButton:checked { + background-color: #8b5cf6; + color: white; + border: 1px solid #7c3aed; + } + QPushButton:hover { + background-color: #3a3a4a; + } + QPushButton:checked:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + """) toolbar.addWidget(self.autoScrollBtn) # 时间戳开关 self.timestampBtn = QPushButton("显示时间") self.timestampBtn.setCheckable(True) self.timestampBtn.setChecked(True) + self.timestampBtn.setStyleSheet(""" + QPushButton { + background-color: #2d2d44; + color: #e0e0ff; + border: 1px solid #3a3a4a; + padding: 6px 12px; + border-radius: 4px; + font-weight: 500; + } + QPushButton:checked { + background-color: #8b5cf6; + color: white; + border: 1px solid #7c3aed; + } + QPushButton:hover { + background-color: #3a3a4a; + } + QPushButton:checked:hover { + background-color: #7c3aed; + } + QPushButton:pressed { + background-color: #6d28d9; + } + """) toolbar.addWidget(self.timestampBtn) toolbar.addStretch() @@ -1419,34 +1528,54 @@ class CustomTreeWidget(QTreeWidget): self.original_scales={} + self.setStyleSheet(""" + /* 设置折叠状态下,带子节点的箭头颜色 */ + QTreeWidget::branch:has-children:!open { + color: #8b5cf6; /* 紫色 */ + } + + /* 设置展开状态下,带子节点的箭头颜色 */ + QTreeWidget::branch:has-children:open { + color: #9ca3af; /* 灰色,提供状态变化反馈 */ + } + + /* 鼠标悬停在任意箭头上时,颜色变亮 */ + QTreeWidget::branch:hover { + color: #a78bfa; /* 亮紫色 */ + } + """) + def initData(self): """初始化变量""" # 定义2D GUI元素类型 self.gui_2d_types = { - "GUI_BUTTON", # DirectButton - "GUI_LABEL", # DirectLabel - "GUI_ENTRY", # DirectEntry - "GUI_IMAGE", + "GUI_BUTTON", # GUI 按钮 + "GUI_LABEL", # GUI 标签 + "GUI_ENTRY", # GUI 输入框 + "GUI_IMAGE", # GUI 图片 + "GUI_2D_VIDEO_SCREEN", # GUI 2D视频 + "GUI_SPHERICAL_VIDEO", # GUI 3D球形视频 "GUI_NODE" # 其他2D GUI容器 } # 定义3D GUI元素类型 self.gui_3d_types = { - "GUI_3DTEXT", # 3D TextNode - "GUI_3DIMAGE", - "GUI_VIRTUAL_SCREEN" # Virtual Screen + "GUI_3DTEXT", # 3D 文本节点 + "GUI_3DIMAGE", # 3D 图片节点 + "GUI_VIRTUAL_SCREEN", # 3D视频 + "GUI_VirtualScreen" # 3D虚拟视频 } # 定义3D场景节点类型(可以接受3D GUI元素和其他3D场景元素) self.scene_3d_types = { "SCENE_ROOT", "SCENE_NODE", - "LIGHT_NODE", + "LIGHT_NODE", # 灯节点 "CAMERA_NODE", - "IMPORTED_MODEL_NODE", + "IMPORTED_MODEL_NODE", # 导入模型节点 "MODEL_NODE", - "TERRAIN_NODE", - "CESIUM_TILESET_NODE" + "TERRAIN_NODE", # 地形节点 + "CESIUM_TILESET_NODE" # 3D Tileset } # 这是一个最佳实践,它让代码的意图变得非常清晰。 @@ -2192,53 +2321,60 @@ class CustomTreeWidget(QTreeWidget): return top_item return None - def create_model_items(self, model): + def create_model_items(self, model: NodePath): + """ + 【此函数保持不变】 + 创建模型项。 + 只寻找模型下一层带有 'is_scene_element' 标签的子节点作为分支的根, + 然后完整地展示这些分支。 + """ if not model: print("传入的参数model为空") return - # 创建根节点项 - # root_item = QTreeWidgetItem(self) - # root_item.setText(0, model.getName() or "Unnamed Node") - # root_item.setText(1, model.node().getTypeName()) - # root_item.setIcon(0, self.item_icons.get('model', self.item_icons['default'])) - # 存储NodePath引用以便后续操作 - # root_item.setData(0, Qt.UserRole, model) + # 找到场景树的根节点,我们将把模型节点添加到这里 root_item = self._findSceneRoot() + if not root_item: + print("错误:未能找到场景根节点项") + return - # 递归添加子节点 - self._add_children_recursive(root_item, model) + # 1. 在模型的第一层子节点中进行筛选 + for child_node in model.getChildren(): + if child_node.hasTag("is_scene_element"): + print(f"找到带标签的根节点:{child_node.getName()}") - return root_item + # 为这个带标签的节点创建一个树项 + child_item = QTreeWidgetItem(root_item) + child_item.setText(0, child_node.getName() or "Unnamed Tagged Node") + child_item.setData(0, Qt.UserRole, child_node) + child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type")) + # self._add_node_info(child_item, child_node) # 可选信息 - def _add_children_recursive(self, parent_item, node_path: NodePath): - """递归添加子节点到树项""" - print(f'开始递归添加子节点') - # 获取所有子节点 - children = node_path.getChildren() + # 2. 对这个节点的所有后代进行“无条件”递归添加 (但会跳过碰撞体) + self._add_all_children_unconditionally(child_item, child_node) - for i in range(children.getNumPaths()): - child_node: NodePath = children.getPath(i) + def _add_all_children_unconditionally(self, parent_item: QTreeWidgetItem, node_path: NodePath): + """ + 【此函数已更新】 + 无条件地、递归地添加一个节点下的所有子节点,但会跳过碰撞节点。 + """ + for child_node in node_path.getChildren(): - # 过滤条件 - if not child_node.hasTag("is_scene_element"): - print(f"不存在------------------------{child_node.hasTag('is_scene_element')}") - continue + # 新增:检查节点是否为碰撞节点 + if isinstance(child_node.node(), CollisionNode): + # print(f"跳过碰撞节点: {child_node.getName()}") # 用于调试 + continue # 如果是,则跳过此节点及其所有子节点 - print(f"存在------------------------{child_node.getName()}") # 创建子项 child_item = QTreeWidgetItem(parent_item) - child_item.setText(0, child_node.getName() or f"Child_{i}") - - # 存储NodePath引用 + child_item.setText(0, child_node.getName() or "Unnamed Child") child_item.setData(0, Qt.UserRole, child_node) + child_item.setData(0, Qt.UserRole + 1, child_node.getTag("tree_item_type")) + # self._add_node_info(child_item, child_node) # 可选信息 - # 添加额外信息(可选) - # self._add_node_info(child_item, child_node) - - # 递归处理子节点的子节点 - if child_node.getNumChildren() > 0: - self._add_children_recursive(child_item, child_node) + # 继续无条件地递归 + if not child_node.is_empty(): + self._add_all_children_unconditionally(child_item, child_node) # ==================== 辅助方法 ==================== def _findSceneRoot(self): @@ -2269,6 +2405,17 @@ class CustomTreeWidget(QTreeWidget): def add_node_to_tree_widget(self, node, parent_item, node_type): """将node元素添加到树形控件""" + if hasattr(node, 'getTag'): + if node.hasTag('tree_item_type'): + print(f"node0: {node.getName()},{node.getTag('tree_item_type')}") + tree_type = node.getTag('tree_item_type') + else: + node.setTag('tree_item_type', node_type) + else: + print(f"node2: {node.getName()},{node_type}") + tree_type = node_type + + # BLACK_LIST 和依赖项导入保持不变 BLACK_LIST = {'', '**', 'temp', 'collision'} from panda3d.core import CollisionNode, ModelRoot @@ -2283,7 +2430,7 @@ class CustomTreeWidget(QTreeWidget): nodeItem = QTreeWidgetItem(parentItem, [node.getName()]) nodeItem.setData(0, Qt.UserRole, node) - nodeItem.setData(0, Qt.UserRole + 1, node_type) + nodeItem.setData(0, Qt.UserRole + 1, tree_type) for child in node.getChildren(): # 递归调用,但我们只关心顶级的nodeItem @@ -2301,7 +2448,7 @@ class CustomTreeWidget(QTreeWidget): node_name = "" try: - if node_type == "IMPORTED_MODEL_NODE": + if tree_type == "IMPORTED_MODEL_NODE": # getTag('file') 可能是你自己设置的tag,这里假设它存在 node_name = node.getTag("file") if hasattr(node, 'getTag') and node.hasTag("file") else node.getName() @@ -2312,7 +2459,7 @@ class CustomTreeWidget(QTreeWidget): node_name = node.getName() if hasattr(node, 'getName') else "node" new_qt_item = QTreeWidgetItem(parent_item, [node_name]) new_qt_item.setData(0, Qt.UserRole, node) - new_qt_item.setData(0, Qt.UserRole + 1, node_type) + new_qt_item.setData(0, Qt.UserRole + 1, tree_type) # 确保 new_qt_item 成功创建后再继续操作 if new_qt_item: