""" 主窗口设置模块 负责主窗口的界面构建和事件绑定: - 菜单栏、工具栏创建 - 停靠窗口设置 - 事件连接和信号处理 """ import os import sys from PyQt5.QtGui import QKeySequence, QIcon, QPalette, QColor from PyQt5.QtWebEngineWidgets import QWebEngineView from PyQt5.QtWidgets import (QApplication, QMainWindow, QMenuBar, QMenu, QAction, QDockWidget, QTreeWidget, QListWidget, QWidget, QVBoxLayout, QTreeWidgetItem, QLabel, QLineEdit, QFormLayout, QDoubleSpinBox, QScrollArea, QFileSystemModel, QButtonGroup, QToolButton, QPushButton, QHBoxLayout, QComboBox, QGroupBox, QInputDialog, QFileDialog, QMessageBox, QDesktopWidget, QDialog, QSpinBox, QFrame) from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint, QUrl, QRect from direct.showbase.ShowBaseGlobal import aspect2d from ui.widgets import CustomPanda3DWidget, CustomFileView, CustomTreeWidget,CustomAssetsTreeWidget, CustomConsoleDockWidget try: from PyQt5.QtWebEngineWidgets import QWebEngineView WEB_ENGINE_AVAILABLE = True except ImportError: QWebEngineView = None WEB_ENGINE_AVAILABLE = False 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() # self.setupToolbar() self.connectEvents() # 移动窗口到屏幕中央 self.move_center() # 创建定时器来更新脚本管理面板状态 self.updateTimer = QTimer() self.updateTimer.timeout.connect(self.updateScriptPanel) self.updateTimer.start(500) # 每500毫秒更新一次 self.toolbarDragging = False 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): """设置窗口基本属性""" self.setWindowTitle("引擎编辑器") # 使用自定义的 Panda3D 部件作为中央部件 self.pandaWidget = CustomPanda3DWidget(self.world) self.setCentralWidget(self.pandaWidget) # 创建内嵌工具栏 self.setupEmbeddedToolbar() def move_center(self): """设置窗口居中显示""" self.setGeometry(50, 50, 1920, 1080) screen = QDesktopWidget().screenGeometry() self.move( int(screen.width() / 2 - self.width() / 2), int(screen.height() / 2 - self.height() / 2), ) @staticmethod def get_icon_path(icon_name): """获取图标文件的完整路径""" # 假设 icons 文件夹在项目根目录下 project_root = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) icon_path = os.path.join(project_root, "icons", icon_name) # 检查文件是否存在,如果不存在则返回默认值或None if not os.path.exists(icon_path): print(f"警告: 图标文件不存在: {icon_path}") return "" # 返回空字符串,QIcon会处理空路径 return icon_path def setupEmbeddedToolbar(self): """创建Unity风格的内嵌工具栏""" # 创建工具栏容器 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(40, 40, 60, 200); /* 深蓝灰色背景 */ border: 1px solid rgba(80, 80, 120, 200); /* 深蓝灰边框 */ border-radius: 6px; padding: 4px; } QToolButton { 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(80, 80, 130, 200); /* 悬停时更亮 */ border: 1px solid rgba(139, 92, 246, 150); /* 紫色边框 */ } QToolButton:checked { background-color: rgba(139, 92, 246, 180); /* 紫色背景 */ color: white; border: 1px solid rgba(180, 140, 255, 200); } QToolButton:pressed { background-color: rgba(120, 75, 220, 200); /* 按下时稍暗的紫色 */ } """) # 水平布局 self.toolbarLayout = QHBoxLayout(self.embeddedToolbar) self.toolbarLayout.setContentsMargins(5, 5, 5, 5) self.toolbarLayout.setSpacing(2) # 创建工具按钮组 self.toolGroup = QButtonGroup() # 选择工具 self.selectTool = QToolButton() icon_path = self.get_icon_path("select_tool.png") if icon_path and os.path.exists(icon_path): self.selectTool.setIcon(QIcon(icon_path)) else: self.selectTool.setText('选择') # 如果没有图标则显示文字 self.selectTool.setIconSize(QSize(16, 16)) self.selectTool.setCheckable(True) self.selectTool.setToolTip("选择工具 (Q)") # self.selectTool.setShortcut(QKeySequence("Q")) self.toolGroup.addButton(self.selectTool) self.toolbarLayout.addWidget(self.selectTool) # 移动工具 self.moveTool = QToolButton() self.moveTool.setText("移动") icon_path = self.get_icon_path("move_tool.png") # if icon_path and os.path.exists(icon_path): # self.moveTool.setIcon(QIcon(icon_path)) # else: # self.moveTool.setText('移动') self.moveTool.setIconSize(QSize(16, 16)) self.moveTool.setCheckable(True) self.moveTool.setToolTip("移动工具 (W)") # self.moveTool.setShortcut(QKeySequence("W")) self.toolGroup.addButton(self.moveTool) self.toolbarLayout.addWidget(self.moveTool) # 旋转工具 self.rotateTool = QToolButton() self.rotateTool.setText("旋转") icon_path = self.get_icon_path("rotate_tool.png") # if icon_path and os.path.exists(icon_path): # self.rotateTool.setIcon(QIcon(icon_path)) # else: # self.rotateTool.setText('旋转') self.rotateTool.setIconSize(QSize(16, 16)) self.rotateTool.setCheckable(True) self.rotateTool.setToolTip("旋转工具 (E)") # self.rotateTool.setShortcut(QKeySequence("E")) self.toolGroup.addButton(self.rotateTool) self.toolbarLayout.addWidget(self.rotateTool) # 缩放工具 self.scaleTool = QToolButton() self.scaleTool.setText("缩放") icon_path = self.get_icon_path("scale_tool.png") # if icon_path and os.path.exists(icon_path): # self.scaleTool.setIcon(QIcon(icon_path)) # else: # self.scaleTool.setText('缩放') self.scaleTool.setIconSize(QSize(16, 16)) self.scaleTool.setCheckable(True) self.scaleTool.setToolTip("缩放工具 (R)") # self.scaleTool.setShortcut(QKeySequence("R")) self.toolGroup.addButton(self.scaleTool) self.toolbarLayout.addWidget(self.scaleTool) # 添加分隔符 separator = QFrame() separator.setFrameShape(QFrame.VLine) separator.setFrameShadow(QFrame.Sunken) separator.setStyleSheet("background-color: rgba(240, 240, 240, 255);") separator.setFixedWidth(1) self.toolbarLayout.addWidget(separator) # 设置位置到左上角 self.embeddedToolbar.move(10, 10) self.embeddedToolbar.adjustSize() self.embeddedToolbar.show() # 连接事件 self.toolGroup.buttonClicked.connect(self.onToolChanged) # 设置工具栏拖拽事件 self.embeddedToolbar.mousePressEvent = self.toolbarMousePressEvent self.embeddedToolbar.mouseMoveEvent = self.toolbarMouseMoveEvent self.embeddedToolbar.mouseReleaseEvent = self.toolbarMouseReleaseEvent # 默认选择"选择"工具 self.selectTool.setChecked(True) self.world.setCurrentTool("选择") def toolbarMousePressEvent(self, event): """工具栏鼠标按下事件""" if event.button() == Qt.LeftButton: self.toolbarDragging = True self.dragStartPos = event.globalPos() self.toolbarStartPos = self.embeddedToolbar.pos() event.accept() def toolbarMouseMoveEvent(self, event): """工具栏鼠标移动事件""" try: if self.toolbarDragging and event.buttons() == Qt.LeftButton: # 计算新位置 delta = event.globalPos() - self.dragStartPos new_pos = self.toolbarStartPos + delta # 边界检测 panda_rect = self.pandaWidget.geometry() toolbar_size = self.embeddedToolbar.size() # 限制在Panda3D区域内 new_pos.setX(max(0, min(new_pos.x(), panda_rect.width() - toolbar_size.width()))) new_pos.setY(max(0, min(new_pos.y(), panda_rect.height() - toolbar_size.height()))) self.embeddedToolbar.move(new_pos) event.accept() except Exception as e: print(f"工具栏鼠标移动事件出错") import traceback traceback.print_exc() def toolbarMouseReleaseEvent(self, event): """工具栏鼠标释放事件""" if event.button() == Qt.LeftButton and self.toolbarDragging: self.toolbarDragging = False # 自动吸附到最近的预设位置 self.snapToNearestPosition() event.accept() def snapToNearestPosition(self): """自动吸附到最近的预设位置""" current_pos = self.embeddedToolbar.pos() panda_rect = self.pandaWidget.geometry() toolbar_size = self.embeddedToolbar.size() margin = 10 # 定义所有预设位置 positions = { "top_center": QPoint( (panda_rect.width() - toolbar_size.width()) // 2, margin ), "top_left": QPoint(margin, margin), "top_right": QPoint( panda_rect.width() - toolbar_size.width() - margin, margin ), "bottom_center": QPoint( (panda_rect.width() - toolbar_size.width()) // 2, panda_rect.height() - toolbar_size.height() - margin ), "bottom_left": QPoint( margin, panda_rect.height() - toolbar_size.height() - margin ), "bottom_right": QPoint( panda_rect.width() - toolbar_size.width() - margin, panda_rect.height() - toolbar_size.height() - margin ) } # 找到最近的位置 min_distance = float('inf') nearest_position = "top_center" for pos_name, pos_point in positions.items(): distance = ((current_pos.x() - pos_point.x()) ** 2 + (current_pos.y() - pos_point.y()) ** 2) ** 0.5 if distance < min_distance: min_distance = distance nearest_position = pos_name # 更新位置并平滑移动 self.toolbarPosition = nearest_position target_pos = positions[nearest_position] # 简单的平滑移动动画 from PyQt5.QtCore import QPropertyAnimation, QEasingCurve self.toolbarAnimation = QPropertyAnimation(self.embeddedToolbar, b"pos") self.toolbarAnimation.setDuration(200) self.toolbarAnimation.setStartValue(current_pos) self.toolbarAnimation.setEndValue(target_pos) self.toolbarAnimation.setEasingCurve(QEasingCurve.OutCubic) self.toolbarAnimation.start() def setupMenus(self): """创建菜单栏""" menubar = self.menuBar() # 文件菜单 self.fileMenu = menubar.addMenu('文件') self.newAction = self.fileMenu.addAction('新建') self.openAction = self.fileMenu.addAction('打开') self.saveAction = self.fileMenu.addAction('保存') self.buildAction = self.fileMenu.addAction('打包') self.fileMenu.addSeparator() self.exitAction = self.fileMenu.addAction('退出') # 编辑菜单 self.editMenu = menubar.addMenu('编辑') self.undoAction = self.editMenu.addAction('撤销') self.redoAction = self.editMenu.addAction('重做') self.editMenu.addSeparator() self.cutAction = self.editMenu.addAction('剪切') self.copyAction = self.editMenu.addAction('复制') self.pasteAction = self.editMenu.addAction('粘贴') # 视图菜单 self.viewMenu = menubar.addMenu('视图') self.viewPerspectiveAction = self.viewMenu.addAction('透视图') self.viewTopAction = self.viewMenu.addAction('俯视图') self.viewFrontAction = self.viewMenu.addAction('前视图') self.viewMenu.addSeparator() self.viewGridAction = self.viewMenu.addAction('显示网格') # 工具菜单 self.toolsMenu = menubar.addMenu('工具') self.selectAction = self.toolsMenu.addAction('选择工具') self.moveAction = self.toolsMenu.addAction('移动工具') self.rotateAction = self.toolsMenu.addAction('旋转工具') self.scaleAction = self.toolsMenu.addAction('缩放工具') self.sunsetAction = self.toolsMenu.addAction('光照编辑') self.pluginAction = self.toolsMenu.addAction('图形编辑') # 统一创建菜单 - 关键修改 self.createMenu = menubar.addMenu('创建') self.setupCreateMenuActions() # 统一创建菜单动作 # self.createGUIaddMenu = self.createMenu.addMenu('GUI') # self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮') # self.createLabelAction = self.createGUIaddMenu.addAction('创建标签') # self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框') # self.createGUIaddMenu.addSeparator() # self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕') # self.createCesiumViewAction = self.createGUIaddMenu.addAction('创建Cesium地图') # self.toggleCesiumViewAction = self.createGUIaddMenu.addAction('开关地图') # self.refreshCesiumViewAction = self.createGUIaddMenu.addAction('刷新地图') # # self.createLightaddMenu = self.createMenu.addMenu('光源') # self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯') # self.createPointLightAction = self.createLightaddMenu.addAction('点光源') #添加地形菜单 self.createTerrainMenu = self.createMenu.addMenu('地形') self.createFlatTerrainAction = self.createTerrainMenu.addAction('创建平面地形') self.createHeightmapTerrainAction = self.createTerrainMenu.addAction('从高度图创建地形') self.createTerrainMenu.addSeparator() # self.terrainEditModeAction = self.createTerrainMenu.addAction('地形编辑模式') # GUI菜单 # GUI菜单 - 复用创建菜单的动作 self.guiMenu = menubar.addMenu('GUI') self.guiEditModeAction = self.guiMenu.addAction('进入GUI编辑模式') self.guiMenu.addSeparator() self.guiMenu.addAction(self.createButtonAction) self.guiMenu.addAction(self.createLabelAction) self.guiMenu.addAction(self.createEntryAction) self.guiMenu.addSeparator() self.guiMenu.addAction(self.create3DTextAction) self.guiMenu.addAction(self.createVirtualScreenAction) # 脚本菜单 self.scriptMenu = menubar.addMenu('脚本') self.createScriptAction = self.scriptMenu.addAction('创建脚本...') self.loadScriptAction = self.scriptMenu.addAction('加载脚本文件...') self.loadAllScriptsAction = self.scriptMenu.addAction('重载所有脚本') self.scriptMenu.addSeparator() self.toggleHotReloadAction = self.scriptMenu.addAction('启用热重载') self.toggleHotReloadAction.setCheckable(True) self.toggleHotReloadAction.setChecked(True) # 默认启用 self.scriptMenu.addSeparator() self.openScriptsManagerAction = self.scriptMenu.addAction('脚本管理器') self.cesiumMenu = menubar.addMenu('Cesium') self.loadCesiumTilesetAction = self.cesiumMenu.addAction('加载3Dtiles') self.loadCesiumTilesetAction.triggered.connect(self.onLoadCesiumTileset) # 添加地球视图相关菜单项 self.createCesiumViewAction = self.cesiumMenu.addAction('创建地球视图') self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView) self.toggleCesiumViewAction = self.cesiumMenu.addAction('开关地球视图') self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView) self.refreshCesiumViewAction = self.cesiumMenu.addAction('刷新地球视图') self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView) # 添加模型相关菜单项 self.addModelToCesiumAction = self.cesiumMenu.addAction('添加模型到地球') self.addModelToCesiumAction.triggered.connect(self.onAddModelClicked) self.infoPanelMenu = menubar.addMenu('信息面板') # 创建示例面板动作 self.createSamplePanelAction = self.infoPanelMenu.addAction('创建示例面板') self.createSamplePanelAction.triggered.connect(self.onCreateSampleInfoPanel) # 添加更多面板创建选项 # 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.infoPanelMenu.addSeparator() self.create3DSamplePanelAction = self.infoPanelMenu.addAction('创建3D实例面板') self.create3DSamplePanelAction.triggered.connect(self.onCreate3DSampleInfoPanel) # 添加网页浏览器菜单项 self.webBrowserAction = self.infoPanelMenu.addAction("信息面板") self.webBrowserAction.triggered.connect(self.openWebBrowser) # # 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.assetsMenu = menubar.addMenu('资源') self.refreshAssetsAction = self.assetsMenu.addAction('刷新资源') self.refreshAssetsAction.triggered.connect(self.refreshAssetsView) # 帮助菜单 self.helpMenu = menubar.addMenu('帮助') self.aboutAction = self.helpMenu.addAction('关于') def refreshAssetsView(self): """"刷新资源视图""" if hasattr(self,'fileView') and self.fileView: self.fileView.refreshView() print("资源视图已刷新") def setupCreateMenuActions(self): """统一设置创建菜单的所有动作 - 避免重复代码""" # 基础对象 self.createEnptyaddAction = self.createMenu.addAction('空对象') # 3D对象子菜单 self.create3dObjectaddMenu = self.createMenu.addMenu('3D对象') # 可以在这里添加更多3D对象类型 # 3D GUI子菜单 self.create3dGUIaddMenu = self.createMenu.addMenu('3D GUI') self.create3DTextAction = self.create3dGUIaddMenu.addAction('3D文本') self.create3DImageAction = self.create3dGUIaddMenu.addAction('3D图片') # GUI子菜单 self.createGUIaddMenu = self.createMenu.addMenu('GUI') self.createButtonAction = self.createGUIaddMenu.addAction('创建按钮') self.createLabelAction = self.createGUIaddMenu.addAction('创建标签') self.createEntryAction = self.createGUIaddMenu.addAction('创建输入框') self.createImageAction = self.createGUIaddMenu.addAction('创建图片') self.createGUIaddMenu.addSeparator() self.createVideoScreen = self.createGUIaddMenu.addAction('创建视频屏幕') self.create2DVideoScreen = self.createGUIaddMenu.addAction('创建2D视频屏幕') self.createSphericalVideo = self.createGUIaddMenu.addAction('创建球形视频') self.createGUIaddMenu.addSeparator() self.createVirtualScreenAction = self.createGUIaddMenu.addAction('创建虚拟屏幕') # 光源子菜单 self.createLightaddMenu = self.createMenu.addMenu('光源') self.createSpotLightAction = self.createLightaddMenu.addAction('聚光灯') self.createPointLightAction = self.createLightaddMenu.addAction('点光源') # 统一连接信号到处理方法 self.connectCreateMenuActions() def connectCreateMenuActions(self): """统一连接创建菜单的信号到处理方法""" # 连接到world对象的创建方法 # self.createEnptyaddAction.triggered.connect(self.world.createEmptyObject) self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText()) self.create3DImageAction.triggered.connect(lambda: self.world.createGUI3DImage()) self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton()) self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel()) self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry()) self.createImageAction.triggered.connect(lambda: self.world.createGUI2DImage()) self.createVideoScreen.triggered.connect(self.world.createVideoScreen) self.create2DVideoScreen.triggered.connect(self.world.create2DVideoScreen) self.createSphericalVideo.triggered.connect(self.world.createSphericalVideo) self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen()) self.createSpotLightAction.triggered.connect(lambda :self.world.createSpotLight()) self.createPointLightAction.triggered.connect(lambda :self.world.createPointLight()) # # self.createVideoScreen.triggered.connect(lambda: self.world.video_manager.create_video_screen( # # pos=(0, 0, 2), # # size=(4, 3), # # name=f"video_screen_{len(self.world.video_manager.video_objects) if hasattr(self.world, 'video_manager') else 0}" # # )) # # self.createSphericalVideo.triggered.connect(lambda: self.world.createGUISphericalVideo()) # self.createVirtualScreenAction.triggered.connect(lambda: self.world.create_spherical_video()) def getCreateMenuActions(self): """获取所有创建菜单动作的字典 - 供右键菜单使用""" return { 'createEmpty': self.createEnptyaddAction, 'create3DText': self.create3DTextAction, 'create3DImage': self.create3DImageAction, 'createButton': self.createButtonAction, 'createLabel': self.createLabelAction, 'createEntry': self.createEntryAction, 'createImage': self.createImageAction, 'createVideoScreen': self.createVideoScreen, 'create2DVideoScreen':self.create2DVideoScreen, 'createSphericalVideo': self.createSphericalVideo, 'createVirtualScreen': self.createVirtualScreenAction, 'createSpotLight': self.createSpotLightAction, 'createPointLight': self.createPointLightAction, } def setupDockWindows(self): """创建停靠窗口""" # 创建左侧停靠窗口(层级窗口) 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) self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.leftDock) # 创建右侧停靠窗口(属性窗口) 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 { padding: 0 8px; color: #c0c0e0; font-weight: 500; } """) # 创建属性面板的主容器和布局 self.propertyContainer = QWidget() self.propertyContainer.setObjectName("PropertyContainer") try: self.propertyLayout = QVBoxLayout(self.propertyContainer) print(f"✓ 属性布局创建完成: {self.propertyLayout}") # 添加初始提示信息 tipLabel = QLabel("选择对象以查看属性") tipLabel.setStyleSheet("color: gray; padding: 10px;") # 使用灰色字体 self.propertyLayout.addWidget(tipLabel) print("✓ 提示标签添加完成") # 创建滚动区域并设置属性 self.scrollArea = QScrollArea() self.scrollArea.setWidgetResizable(True) self.scrollArea.setWidget(self.propertyContainer) print("✓ 滚动区域设置完成") # 设置滚动区域为停靠窗口的主部件 self.rightDock.setWidget(self.scrollArea) self.rightDock.setMinimumWidth(300) self.addDockWidget(Qt.RightDockWidgetArea, self.rightDock) print("✓ 右侧停靠窗口添加完成") # 设置属性面板到世界对象 if hasattr(self.world, 'setPropertyLayout'): print("开始设置属性布局到世界对象...") self.world.setPropertyLayout(self.propertyLayout) print("✓ 属性布局设置完成") else: print("⚠ 世界对象没有 setPropertyLayout 方法") except Exception as e: print(f"✗ 设置属性面板时出错: {e}") import traceback traceback.print_exc() # 创建基本的属性面板作为后备方案 fallback_widget = QLabel("属性面板初始化失败\n请查看控制台日志") fallback_widget.setStyleSheet("color: red; background-color: white; padding: 10px;") self.rightDock.setWidget(fallback_widget) self.addDockWidget(Qt.RightDockWidgetArea, self.rightDock) # 创建脚本管理停靠窗口 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 { 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) # 创建滚动区域并设置属性 self.scriptScrollArea = QScrollArea() self.scriptScrollArea.setWidgetResizable(True) self.scriptScrollArea.setWidget(self.scriptContainer) # 设置滚动区域为停靠窗口的主部件 self.scriptDock.setWidget(self.scriptScrollArea) self.scriptDock.setMinimumWidth(300) # 设置脚本面板内容 - 这里添加调用 self.setupScriptPanel(self.scriptLayout) self.addDockWidget(Qt.RightDockWidgetArea, self.scriptDock) # # 创建底部停靠窗口(资源窗口) # self.bottomDock = QDockWidget("资源", self) # self.bottomDock.setAllowedAreas(Qt.BottomDockWidgetArea) # # # 创建文件系统模型 # self.fileModel = QFileSystemModel() # self.fileModel.setRootPath(QDir.homePath()) # 设置为用户主目录 # # # 创建树形视图显示文件系统 # self.fileView = CustomFileView(self.world) # self.fileView.setModel(self.fileModel) # self.fileView.setRootIndex(self.fileModel.index(QDir.homePath())) # 设置为用户主目录索引 # # # 设置列宽 # self.fileView.setColumnWidth(0, 250) # 名称列 # self.fileView.setColumnWidth(1, 100) # 大小列 # self.fileView.setColumnWidth(2, 100) # 类型列 # self.fileView.setColumnWidth(3, 150) # 修改日期列 # # # 设置视图属性 # self.fileView.setMinimumHeight(200) # 设置最小高度 # # self.bottomDock.setWidget(self.fileView) # self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock) # 创建底部停靠窗口(资源窗口) 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('工具栏') # 创建工具按钮组 self.toolGroup = QButtonGroup() # 选择工具 self.selectTool = QToolButton() self.selectTool.setText("选择") self.selectTool.setCheckable(True) self.toolGroup.addButton(self.selectTool) self.toolbar.addWidget(self.selectTool) # 旋转工具 self.rotateTool = QToolButton() self.rotateTool.setText("旋转") self.rotateTool.setCheckable(True) self.toolGroup.addButton(self.rotateTool) self.toolbar.addWidget(self.rotateTool) # 缩放工具 self.scaleTool = QToolButton() self.scaleTool.setText("缩放") self.scaleTool.setCheckable(True) self.toolGroup.addButton(self.scaleTool) self.toolbar.addWidget(self.scaleTool) # 添加分隔符 self.toolbar.addSeparator() # GUI创建工具 self.createButtonTool = QToolButton() self.createButtonTool.setText("创建按钮") self.toolbar.addWidget(self.createButtonTool) self.createLabelTool = QToolButton() self.createLabelTool.setText("创建标签") self.toolbar.addWidget(self.createLabelTool) self.create3DTextTool = QToolButton() self.create3DTextTool.setText("3D文本") self.toolbar.addWidget(self.create3DTextTool) self.create3DImageTool = QToolButton() self.create3DImageTool.setText("3D图片") self.toolbar.addWidget(self.create3DImageTool) self.create2DImageTool = QToolButton() self.create2DImageTool.setText("2D图片") self.toolbar.addWidget(self.create2DImageTool) self.createSpotLight = QToolButton() self.createSpotLight.setText("聚光灯") self.toolbar.addWidget(self.createSpotLight) self.createPointLight = QToolButton() self.createPointLight.setText("点光灯") self.toolbar.addWidget(self.createPointLight) # Cesium 工具按钮 self.cesiumViewTool = QToolButton() self.cesiumViewTool.setText("地图视图") self.cesiumViewTool.clicked.connect(self.onCreateCesiumView) self.toolbar.addWidget(self.cesiumViewTool) self.refreshCesiumTool = QToolButton() self.refreshCesiumTool.setText("刷新地图") self.refreshCesiumTool.clicked.connect(self.onRefreshCesiumView) self.toolbar.addWidget(self.refreshCesiumTool) self.addModelTool = QToolButton() self.addModelTool.setText("添加模型") self.addModelTool.clicked.connect(self.onAddModelClicked) self.toolbar.addWidget(self.addModelTool) #地形 # 地形工具 self.createFlatTerrainTool = QToolButton() self.createFlatTerrainTool.setText("平面地形") self.toolbar.addWidget(self.createFlatTerrainTool) self.createHeightmapTerrainTool = QToolButton() self.createHeightmapTerrainTool.setText("高度图地形") self.toolbar.addWidget(self.createHeightmapTerrainTool) # 默认选择"选择"工具 self.selectTool.setChecked(True) self.world.setCurrentTool("选择") def setupScriptPanel(self, layout): """创建脚本管理面板""" # 脚本状态组 statusGroup = QGroupBox("脚本系统状态") statusLayout = QVBoxLayout() self.scriptStatusLabel = QLabel("脚本系统: 已启动") self.scriptStatusLabel.setStyleSheet("color: green; font-weight: bold;") statusLayout.addWidget(self.scriptStatusLabel) self.hotReloadLabel = QLabel("热重载: 已启用") self.hotReloadLabel.setStyleSheet("color: blue;") statusLayout.addWidget(self.hotReloadLabel) statusGroup.setLayout(statusLayout) layout.addWidget(statusGroup) # 脚本创建组 createGroup = QGroupBox("创建脚本") createLayout = QVBoxLayout() # 脚本名称输入 nameLayout = QHBoxLayout() nameLayout.addWidget(QLabel("脚本名称:")) self.scriptNameEdit = QLineEdit() self.scriptNameEdit.setPlaceholderText("输入脚本名称...") nameLayout.addWidget(self.scriptNameEdit) createLayout.addLayout(nameLayout) # 模板选择 templateLayout = QHBoxLayout() templateLayout.addWidget(QLabel("模板:")) self.templateCombo = QComboBox() self.templateCombo.addItems(["basic", "movement", "animation"]) templateLayout.addWidget(self.templateCombo) createLayout.addLayout(templateLayout) # 创建按钮 self.createScriptBtn = QPushButton("创建脚本") self.createScriptBtn.clicked.connect(self.onCreateScript) createLayout.addWidget(self.createScriptBtn) createGroup.setLayout(createLayout) layout.addWidget(createGroup) # 可用脚本组 scriptsGroup = QGroupBox("可用脚本") scriptsLayout = QVBoxLayout() # 脚本列表 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) # 脚本操作按钮 scriptButtonsLayout = QHBoxLayout() self.loadScriptBtn = QPushButton("加载脚本") self.loadScriptBtn.clicked.connect(self.onLoadScript) scriptButtonsLayout.addWidget(self.loadScriptBtn) self.reloadAllBtn = QPushButton("重载全部") self.reloadAllBtn.clicked.connect(self.onReloadAllScripts) scriptButtonsLayout.addWidget(self.reloadAllBtn) scriptsLayout.addLayout(scriptButtonsLayout) scriptsGroup.setLayout(scriptsLayout) layout.addWidget(scriptsGroup) # 脚本挂载组 mountGroup = QGroupBox("脚本挂载") mountLayout = QVBoxLayout() # 当前选中对象显示 self.selectedObjectLabel = QLabel("未选择对象") self.selectedObjectLabel.setStyleSheet("color: gray; font-style: italic;") mountLayout.addWidget(self.selectedObjectLabel) # 脚本选择和挂载 mountControlLayout = QHBoxLayout() self.mountScriptCombo = QComboBox() self.mountScriptCombo.setEnabled(False) mountControlLayout.addWidget(self.mountScriptCombo) self.mountBtn = QPushButton("挂载") self.mountBtn.setEnabled(False) self.mountBtn.clicked.connect(self.onMountScript) mountControlLayout.addWidget(self.mountBtn) mountLayout.addLayout(mountControlLayout) # 已挂载脚本列表 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) # 卸载按钮 self.unmountBtn = QPushButton("卸载选中脚本") self.unmountBtn.clicked.connect(self.onUnmountScript) mountLayout.addWidget(self.unmountBtn) mountGroup.setLayout(mountLayout) layout.addWidget(mountGroup) # 添加拉伸以填充剩余空间 layout.addStretch() # 初始化脚本列表 self.refreshScriptsList() def connectEvents(self): """连接事件信号""" # 导入项目管理功能函数 from main import createNewProject, saveProject, openProject, buildPackage # 连接文件菜单事件 self.newAction.triggered.connect(lambda: createNewProject(self)) self.openAction.triggered.connect(lambda: openProject(self)) self.saveAction.triggered.connect(lambda: saveProject(self)) self.buildAction.triggered.connect(lambda: buildPackage(self)) self.exitAction.triggered.connect(QApplication.instance().quit) # 连接工具事件 self.sunsetAction.triggered.connect(lambda: self.world.setCurrentTool("光照编辑")) self.pluginAction.triggered.connect(lambda: self.world.setCurrentTool("图形编辑")) # 连接GUI编辑模式事件 self.guiEditModeAction.triggered.connect(lambda: self.world.toggleGUIEditMode()) # 连接创建事件 - 使用菜单动作而不是不存在的工具栏按钮 # self.createSpotLightAction.triggered.connect(lambda: self.world.createSpotLight()) # self.createPointLightAction.triggered.connect(lambda: self.world.createPointLight()) # self.createButtonAction.triggered.connect(lambda: self.world.createGUIButton()) # self.createLabelAction.triggered.connect(lambda: self.world.createGUILabel()) # self.createEntryAction.triggered.connect(lambda: self.world.createGUIEntry()) # self.create3DTextAction.triggered.connect(lambda: self.world.createGUI3DText()) # self.createVirtualScreenAction.triggered.connect(lambda: self.world.createGUIVirtualScreen()) self.createCesiumViewAction.triggered.connect(self.onCreateCesiumView) self.toggleCesiumViewAction.triggered.connect(self.onToggleCesiumView) self.refreshCesiumViewAction.triggered.connect(self.onRefreshCesiumView) # 连接地形创建事件 self.createFlatTerrainAction.triggered.connect(self.onCreateFlatTerrain) self.createHeightmapTerrainAction.triggered.connect(self.onCreateHeightmapTerrain) # self.terrainEditModeAction.triggered.connect(self.onTerrainEditMode) # 连接树节点点击信号 self.treeWidget.itemSelectionChanged.connect( lambda: self.world.onTreeItemClicked(self.treeWidget.currentItem(), 0)) print("已连接点击信号") # 连接工具切换信号 #self.toolGroup.buttonClicked.connect(self.onToolChanged) # 连接脚本菜单事件 # self.createScriptAction.triggered.connect(self.onCreateScriptDialog) # self.loadScriptAction.triggered.connect(self.onLoadScriptFile) # self.loadAllScriptsAction.triggered.connect(self.onReloadAllScripts) # self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload) # self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager) def onCreateCesiumView(self): if hasattr(self.world,'gui_manager') and self.world.gui_manager: self.world.gui_manager.createCesiumView() else: QMessageBox.warning(self,"错误","GUI管理其不可用") def onToggleCesiumView(self): """切换 Cesium 视图显示状态""" if hasattr(self.world, 'gui_manager') and self.world.gui_manager: self.world.gui_manager.toggleCesiumView() else: QMessageBox.warning(self, "错误", "GUI 管理器不可用") def onRefreshCesiumView(self): """刷新 Cesium 视图""" if hasattr(self.world, 'gui_manager') and self.world.gui_manager: self.world.gui_manager.refreshCesiumView() else: QMessageBox.warning(self, "错误", "GUI 管理器不可用") def onUpdateCesiumURL(self): """更新 Cesium URL""" 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): """处理加入模型按钮点击事件""" # 检查 Cesium 视图是否存在 cesium_view_exists = False if hasattr(self.world, 'gui_manager') and self.world.gui_manager: for element in self.world.gui_manager.gui_elements: if hasattr(element, 'objectName') and element.objectName() == "CesiumView": cesium_view_exists = True break if not cesium_view_exists: reply = QMessageBox.question( self, '提示', 'Cesium 地图视图尚未打开,是否先打开地图视图?', QMessageBox.Yes | QMessageBox.No, QMessageBox.Yes ) if reply == QMessageBox.Yes: self.onCreateCesiumView() # 给一点时间让 Cesium 视图加载 QTimer.singleShot(1000, self.showAddModelDialog) return else: return self.showAddModelDialog() def showAddModelDialog(self): """显示添加模型对话框""" # 打开文件选择对话框 dialog = self.createStyledFileDialog( self, "选择 3D 模型文件", "", "3D 模型文件 (*.glb *.gltf *.obj);;所有文件 (*)" ) 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]}" 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)}" ) def getModelCoordinates(self): """获取模型坐标信息的对话框""" # 创建对话框 dialog = QDialog(self) dialog.setWindowTitle("设置模型位置") dialog.setModal(True) dialog.resize(300, 200) layout = QVBoxLayout(dialog) # 经度 lon_layout = QHBoxLayout() lon_layout.addWidget(QLabel("经度:")) lon_spin = QDoubleSpinBox() lon_spin.setRange(-180, 180) lon_spin.setValue(116.3975) # 默认北京位置 lon_layout.addWidget(lon_spin) layout.addLayout(lon_layout) # 纬度 lat_layout = QHBoxLayout() lat_layout.addWidget(QLabel("纬度:")) lat_spin = QDoubleSpinBox() lat_spin.setRange(-90, 90) lat_spin.setValue(39.9085) # 默认北京位置 lat_layout.addWidget(lat_spin) layout.addLayout(lat_layout) # 高度 height_layout = QHBoxLayout() height_layout.addWidget(QLabel("高度(米):")) height_spin = QDoubleSpinBox() height_spin.setRange(-10000, 100000) height_spin.setValue(0) height_layout.addWidget(height_spin) layout.addLayout(height_layout) # 缩放 scale_layout = QHBoxLayout() scale_layout.addWidget(QLabel("缩放:")) scale_spin = QDoubleSpinBox() scale_spin.setRange(0.001, 100000) scale_spin.setValue(1.0) scale_spin.setSingleStep(0.1) scale_layout.addWidget(scale_spin) layout.addLayout(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) # 连接信号 ok_button.clicked.connect(dialog.accept) cancel_button.clicked.connect(dialog.reject) # 显示对话框 if dialog.exec_() == QDialog.Accepted: return ( lon_spin.value(), lat_spin.value(), height_spin.value(), scale_spin.value() ), True else: return None, False def onLoadCesiumTileset(self): dialog = self.createStyledInputDialog( self, "加载 Cesium 3D Tiles", "输入 tileset.json URL:", QLineEdit.Normal, "https://assets.ion.cesium.com/96128/tileset.json" ) 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): """工具切换事件处理""" if button.isChecked(): tool_name = button.text().strip() # 添加strip()去除空格 if tool_name: # 确保工具名称不为空 self.world.setCurrentTool(tool_name) print(f"工具栏: 选择了 {tool_name} 工具") else: print("工具栏: 选择了空工具名称") else: self.world.setCurrentTool(None) print("工具栏: 取消选择工具") def openWebBrowser(self): if not WEB_ENGINE_AVAILABLE: return None try: from PyQt5.QtWebEngineWidgets import QWebEngineView from PyQt5.QtWidgets import QDockWidget from PyQt5.QtCore import QUrl import os main_window = self.world.main_window # 尝试获取主窗口引用 if main_window is None: print("🔍 尝试获取主窗口引用...") # 检查各种可能的主窗口引用 if hasattr(self.world, 'interface_manager'): print(f" - interface_manager 存在: {self.world.interface_manager}") if hasattr(self.world.interface_manager, 'main_window'): main_window = self.world.interface_manager.main_window print(f" - interface_manager.main_window: {main_window}") if main_window is None and hasattr(self.world, 'main_window'): main_window = self.world.main_window print(f" - world.main_window: {main_window}") # 如果仍然没有主窗口,尝试从树形控件获取 if main_window is None and self.world.treeWidget: try: main_window = self.world.treeWidget.window() print(f" - 从 treeWidget 获取窗口: {main_window}") except: pass if main_window is None: print("✗ 无法获取主窗口引用") return None else: print(f"✅ 使用传入的主窗口引用: {main_window}") # 检查主窗口是否有效 if not hasattr(main_window, 'addDockWidget'): print(f"✗ 主窗口引用无效,缺少 addDockWidget 方法") return None # 检查是否已经存在浏览器视图 for element in self.world.gui_elements: if hasattr(element, 'objectName') and element.objectName() == "WebBrowserView": print("⚠ 浏览器视图已经存在") # 将其前置显示 element.show() element.raise_() return element # 创建停靠窗口 print(f"🔧 创建浏览器停靠窗口,父窗口: {main_window}") browser_dock = QDockWidget("信息面板", main_window) browser_dock.setObjectName("WebBrowserView") # 创建 Web 视图 self.web_view = QWebEngineView() # 加载百度网页 #print("🌐 加载百度网页: https://www.baidu.com") self.web_view.load(QUrl("https://www.bootstrapmb.com/item/15762/preview")) # 设置内容 browser_dock.setWidget(self.web_view) # 添加到主窗口 print("📍 将浏览器视图添加到主窗口") main_window.addDockWidget(Qt.RightDockWidgetArea, browser_dock) # 添加到GUI元素列表以便管理 self.gui_elements.append(browser_dock) print("✓ 网页浏览器视图已创建并集成到项目中") return browser_dock except Exception as e: print(f"✗ 创建浏览器视图失败: {str(e)}") import traceback traceback.print_exc() return None def onCreateSampleInfoPanel(self): """创建示例天气信息面板(模拟数据)""" 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(aspect2d) # 使用唯一的面板ID import time unique_id = f"weather_info_{int(time.time())}" # 创建示例面板 weather_panel = info_manager.createInfoPanel( panel_id=unique_id, # 使用唯一ID position=(1.32, 0.68), size=(1, 0.6), bg_color=(0.15, 0.25, 0.35, 0), # 蓝色背景 border_color=(0.3, 0.5, 0.7, 0), # 蓝色边框 title_color=(0.7, 0.9, 1.0, 1.0), # 浅蓝色标题 content_color=(0.95, 0.95, 0.95, 1.0), font=font, bg_image="/home/tiger/图片/内部信息框2@2x.png" ) # 更新面板标题 info_manager.updatePanelContent(unique_id, title="北京天气") # 添加到场景树 self.addInfoPanelToTree(weather_panel, "天气信息面板") # 立即显示加载中信息 info_manager.updatePanelContent(unique_id, content="正在获取天气数据...") info_manager.registerDataSource(unique_id, self.getRealWeatherData, update_interval=5.0) # # 立即显示示例数据 # sample_data = self.getSampleWeatherData() # info_manager.updatePanelContent(unique_id, content=sample_data) # # # 注册数据源,定期更新示例数据 # info_manager.registerDataSource(unique_id, self.getSampleWeatherData, update_interval=2.0) print("✓ 示例天气信息面板已创建") except Exception as e: print(f"✗ 创建示例天气信息面板失败: {e}") import traceback traceback.print_exc() QMessageBox.critical(self, "错误", f"创建示例天气信息面板时出错: {str(e)}") def getRealWeatherData(self): """获取真实天气数据""" try: import requests import json from datetime import datetime # 请求天气数据 url = "https://wttr.in/Beijing?format=j1" response = requests.get(url, timeout=10) response.raise_for_status() # 解析JSON数据 weather_data = response.json() # 提取当前天气信息 current_condition = weather_data['current_condition'][0] weather_desc = current_condition['weatherDesc'][0]['value'] temp_c = current_condition['temp_C'] feels_like = current_condition['FeelsLikeC'] humidity = current_condition['humidity'] pressure = current_condition['pressure'] visibility = current_condition['visibility'] wind_speed = current_condition['windspeedKmph'] wind_dir = current_condition['winddir16Point'] # 提取空气质量(如果可用) air_quality = "N/A" if 'air_quality' in weather_data and weather_data['air_quality']: if 'us-epa-index' in current_condition: air_quality_index = current_condition['air_quality_index'] air_quality = f"指数: {air_quality_index}" # 获取更新时间 update_time = datetime.now().strftime("%Y-%m-%d %H:%M") # 格式化显示内容 content = f"天气状况: {weather_desc}\n温度: {temp_c}°C (体感 {feels_like}°C)\n湿度: {humidity}%\n气压: {pressure} hPa\n能见度: {visibility} km\n风速: {wind_speed} km/h ({wind_dir})\n空气质量: {air_quality}\n更新时间: {update_time}" return content except requests.exceptions.Timeout: return "错误: 获取天气数据超时" except requests.exceptions.ConnectionError: return "错误: 网络连接失败" except requests.exceptions.HTTPError as e: return f"HTTP错误: {e}" except json.JSONDecodeError: return "错误: 无法解析天气数据" except KeyError as e: return f"错误: 天气数据格式不正确 (缺少字段: {e})" except Exception as e: return f"获取天气数据失败: {str(e)}" def getSampleWeatherData(self): """获取示例天气数据""" try: from datetime import datetime import random # 模拟天气数据 cities = ["北京", "上海", "广州", "深圳", "杭州", "成都", "武汉", "西安"] conditions = ["晴天", "多云", "阴天", "小雨", "雷阵雨", "雪", "雾"] city = random.choice(cities) condition = random.choice(conditions) temp = random.randint(-5, 35) humidity = random.randint(30, 90) wind_speed = round(random.uniform(0, 20), 1) pressure = random.randint(980, 1030) current_time = datetime.now().strftime("%Y-%m-%d %H:%M") return f"城市: {city}\n天气状况: {condition}\n温度: {temp}°C\n湿度: {humidity}%\n风速: {wind_speed} m/s\n气压: {pressure} hPa\n更新时间: {current_time}" 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=(5, 5), 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: # 获取中文字体 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(aspect2d) panel = info_manager.createInfoPanel( panel_id="system_status", position=(1.4, 0.2), 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, "系统状态信息面板") # 立即显示初始数据 initial_data = self.getSystemStatusData() info_manager.updatePanelContent("system_status", content=initial_data) # 注册数据源,每5秒更新一次 info_manager.registerDataSource("system_status", self.getSystemStatusData, update_interval=5.0) except Exception as e: print(f"✗ 创建系统状态信息面板失败: {e}") import traceback traceback.print_exc() QMessageBox.critical(self, "错误", f"创建系统状态信息面板时出错: {str(e)}") def getSystemStatusData(self): """ 获取系统状态数据的回调函数 """ try: import psutil import time from datetime import datetime # 获取系统信息 cpu_percent = psutil.cpu_percent(interval=0.1) memory = psutil.virtual_memory() memory_mb = round(memory.used / (1024 * 1024), 1) memory_total_mb = round(memory.total / (1024 * 1024), 1) memory_percent = memory.percent # 网络状态 net_io = psutil.net_io_counters() bytes_sent = round(net_io.bytes_sent / (1024 * 1024), 2) # MB bytes_recv = round(net_io.bytes_recv / (1024 * 1024), 2) # MB # 磁盘使用情况 disk = psutil.disk_usage('/') disk_used_gb = round(disk.used / (1024 ** 3), 2) disk_total_gb = round(disk.total / (1024 ** 3), 2) disk_percent = round((disk.used / disk.total) * 100, 1) # 时间戳 timestamp = datetime.now().strftime("%H:%M:%S") return f"CPU使用率: {cpu_percent}%\n内存使用: {memory_mb}MB / {memory_total_mb}MB ({memory_percent}%)\n磁盘使用: {disk_used_gb}GB / {disk_total_gb}GB ({disk_percent}%)\n网络发送: {bytes_sent}MB\n网络接收: {bytes_recv}MB\n更新时间: {timestamp}" except Exception as e: return f"获取系统状态失败: {str(e)}" def onCreateSensorDataPanel(self): """创建传感器数据信息面板""" 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(aspect2d) panel = info_manager.createInfoPanel( panel_id="sensor_data", position=(0.8, -0.2), size=(0.8, 0.6), bg_color=(0.15, 0.25, 0.15, 0.95), # 绿色背景 border_color=(0.3, 0.7, 0.3, 1.0), # 绿色边框 title_color=(0.5, 1.0, 0.5, 1.0), # 浅绿色标题 content_color=(0.95, 0.95, 0.95, 1.0), font=font ) # 添加到场景树 self.addInfoPanelToTree(panel, "传感器数据信息面板") # 立即显示初始数据 initial_data = self.getSensorData() info_manager.updatePanelContent("sensor_data", content=initial_data) # 注册数据源,每2秒更新一次 info_manager.registerDataSource("sensor_data", self.getSensorData, update_interval=2.0) # 绑定键盘事件 info_manager.accept("F3", info_manager.togglePanel, ["sensor_data"]) print("✓ 传感器数据信息面板已创建(按 F3 切换显示)") except Exception as e: print(f"✗ 创建传感器数据信息面板失败: {e}") import traceback traceback.print_exc() QMessageBox.critical(self, "错误", f"创建传感器数据信息面板时出错: {str(e)}") def getSensorData(self): """ 获取传感器数据的回调函数(模拟数据) """ try: import random from datetime import datetime # 模拟传感器数据 temperature = round(random.uniform(20, 35), 1) humidity = round(random.uniform(30, 70), 1) pressure = round(random.uniform(990, 1030), 1) light_level = round(random.uniform(0, 1000), 1) # 时间戳 timestamp = datetime.now().strftime("%H:%M:%S") return f"温度: {temperature}°C\n湿度: {humidity}%\n气压: {pressure} hPa\n光照: {light_level} lux\n更新时间: {timestamp}" except Exception as e: return f"获取传感器数据失败: {str(e)}" def onCreateSceneInfoPanel(self): """创建场景信息面板""" 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(aspect2d) panel = info_manager.createInfoPanel( panel_id="scene_info", position=(-0.8, 0.5), size=(0.8, 0.6), bg_color=(0.12, 0.12, 0.12, 0.95), # 深灰色背景 border_color=(0.4, 0.4, 0.4, 1.0), # 灰色边框 title_color=(0.2, 0.8, 1.0, 1.0), # 蓝色标题 content_color=(0.9, 0.9, 0.9, 1.0), font=font ) # 添加到场景树 self.addInfoPanelToTree(panel, "场景信息面板") # 立即显示初始数据 initial_data = self.getSceneInfoData() info_manager.updatePanelContent("scene_info", content=initial_data) # 注册数据源,每3秒更新一次 info_manager.registerDataSource("scene_info", self.getSceneInfoData, update_interval=3.0) # 绑定键盘事件 info_manager.accept("F4", info_manager.togglePanel, ["scene_info"]) print("✓ 场景信息面板已创建(按 F4 切换显示)") except Exception as e: print(f"✗ 创建场景信息面板失败: {e}") import traceback traceback.print_exc() QMessageBox.critical(self, "错误", f"创建场景信息面板时出错: {str(e)}") def getSceneInfoData(self): """ 获取场景信息数据的回调函数 """ try: # 获取场景信息 node_count = 0 texture_count = 0 light_count = 0 # 如果有场景管理器,获取实际数据 if hasattr(self.world, 'scene_graph'): # 这里可以根据实际的场景结构来统计节点数 node_count = len([node for node in self.world.scene_graph.nodes]) if hasattr(self.world.scene_graph, 'nodes') else 0 # 统计光源数量 if hasattr(self.world, 'lights'): light_count = len(self.world.lights) # 统计纹理数量 if hasattr(self.world, 'textures'): texture_count = len(self.world.textures) # 当前时间 from datetime import datetime current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S") return f"场景节点数: {node_count}\n纹理数量: {texture_count}\n光源数量: {light_count}\nFPS: {self.world.clock.getAverageFrameRate():.1f}\n更新时间: {current_time}" except Exception as e: return f"获取场景信息失败: {str(e)}" def onCreateAllInfoPanels(self): """创建所有信息面板""" try: self.onCreateSampleInfoPanel() self.onCreateSystemStatusPanel() self.onCreateSensorDataPanel() self.onCreateSceneInfoPanel() QMessageBox.information(self, "成功", "所有信息面板已创建完成!\n快捷键:\nF1 - 示例面板\nF2 - 系统状态面板\nF3 - 传感器数据面板\nF4 - 场景信息面板") except Exception as e: QMessageBox.critical(self, "错误", f"创建信息面板时出错: {str(e)}") # ==================== 脚本管理事件处理 ==================== def refreshScriptsList(self): """刷新脚本列表""" self.scriptsList.clear() self.mountScriptCombo.clear() available_scripts = self.world.getAvailableScripts() for script_name in available_scripts: self.scriptsList.addItem(script_name) self.mountScriptCombo.addItem(script_name) def updateScriptPanel(self): """更新脚本面板状态""" # 更新热重载状态 hot_reload_enabled = self.world.script_manager.hot_reload_enabled self.hotReloadLabel.setText(f"热重载: {'已启用' if hot_reload_enabled else '已禁用'}") self.hotReloadLabel.setStyleSheet(f"color: {'blue' if hot_reload_enabled else 'gray'};") # 更新热重载菜单状态 self.toggleHotReloadAction.setChecked(hot_reload_enabled) # 更新选中对象信息 selected_object = getattr(self.world.selection, 'selectedObject', None) if selected_object: self.selectedObjectLabel.setText(f"选中对象: {selected_object.getName()}") self.selectedObjectLabel.setStyleSheet("color: green; font-weight: bold;") self.mountScriptCombo.setEnabled(True) self.mountBtn.setEnabled(True) # 更新已挂载脚本列表 self.updateMountedScriptsList(selected_object) else: self.selectedObjectLabel.setText("未选择对象") self.selectedObjectLabel.setStyleSheet("color: gray; font-style: italic;") self.mountScriptCombo.setEnabled(False) self.mountBtn.setEnabled(False) self.mountedScriptsList.clear() def updateMountedScriptsList(self, game_object): """更新已挂载脚本列表""" # 保存当前选中项的脚本名(去除状态前缀) current_item = self.mountedScriptsList.currentItem() selected_script_name = None if current_item: # 提取脚本名(移除 "✓ " 或 "✗ " 前缀) selected_script_name = current_item.text()[2:] # 清空并重新填充列表 self.mountedScriptsList.clear() scripts = self.world.getScripts(game_object) for script_component in scripts: script_name = script_component.script_name enabled = "✓" if script_component.enabled else "✗" item_text = f"{enabled} {script_name}" self.mountedScriptsList.addItem(item_text) # 恢复选中状态(根据脚本名匹配) if selected_script_name: for i in range(self.mountedScriptsList.count()): item = self.mountedScriptsList.item(i) # 提取当前项的脚本名进行比较 current_script_name = item.text()[2:] if current_script_name == selected_script_name: self.mountedScriptsList.setCurrentItem(item) break def onCreateScript(self): """创建脚本按钮事件""" script_name = self.scriptNameEdit.text().strip() if not script_name: QMessageBox.warning(self, "错误", "请输入脚本名称!") return template = self.templateCombo.currentText() try: success = self.world.createScript(script_name, template) if success: QMessageBox.information(self, "成功", f"脚本 '{script_name}' 创建成功!") self.scriptNameEdit.clear() self.refreshScriptsList() else: QMessageBox.warning(self, "错误", f"脚本 '{script_name}' 创建失败!") except Exception as e: QMessageBox.critical(self, "错误", f"创建脚本时出错: {str(e)}") def onCreateScriptDialog(self): """菜单创建脚本事件""" 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): """加载脚本按钮事件""" current_item = self.scriptsList.currentItem() if not current_item: QMessageBox.warning(self, "错误", "请选择要加载的脚本!") return script_name = current_item.text() try: success = self.world.reloadScript(script_name) if success: QMessageBox.information(self, "成功", f"脚本 '{script_name}' 重载成功!") else: QMessageBox.warning(self, "错误", f"脚本 '{script_name}' 重载失败!") except Exception as e: QMessageBox.critical(self, "错误", f"重载脚本时出错: {str(e)}") def onLoadScriptFile(self): """加载脚本文件菜单事件""" dialog = self.createStyledFileDialog( self, "选择脚本文件", "", "Python文件 (*.py)" ) 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): """重载所有脚本事件""" try: scripts_loaded = self.world.loadAllScripts() QMessageBox.information(self, "成功", f"重载完成,共加载 {len(scripts_loaded)} 个脚本!") self.refreshScriptsList() except Exception as e: QMessageBox.critical(self, "错误", f"重载脚本时出错: {str(e)}") def onToggleHotReload(self): """切换热重载状态""" enabled = self.toggleHotReloadAction.isChecked() self.world.enableHotReload(enabled) status = "启用" if enabled else "禁用" QMessageBox.information(self, "热重载", f"热重载已{status}") def onOpenScriptsManager(self): """打开脚本管理器""" # 显示脚本管理停靠窗口 self.scriptDock.show() self.scriptDock.raise_() def onScriptDoubleClick(self, item): """脚本列表双击事件""" # 可以在这里添加打开外部编辑器的功能 script_name = item.text() QMessageBox.information(self, "提示", f"双击了脚本: {script_name}\n\n可以使用外部编辑器编辑脚本文件。") def onMountScript(self): """挂载脚本事件""" selected_object = getattr(self.world.selection, 'selectedObject', None) if not selected_object: QMessageBox.warning(self, "错误", "请先选择一个对象!") return script_name = self.mountScriptCombo.currentText() if not script_name: QMessageBox.warning(self, "错误", "请选择要挂载的脚本!") return try: success = self.world.addScript(selected_object, script_name) if success: QMessageBox.information(self, "成功", f"脚本 '{script_name}' 已挂载到对象!") self.updateMountedScriptsList(selected_object) # 同时更新属性面板 if self.treeWidget and self.treeWidget.currentItem(): self.world.updatePropertyPanel(self.treeWidget.currentItem()) else: QMessageBox.warning(self, "错误", f"挂载脚本失败!") except Exception as e: QMessageBox.critical(self, "错误", f"挂载脚本时出错: {str(e)}") def onUnmountScript(self): """卸载脚本事件""" selected_object = getattr(self.world.selection, 'selectedObject', None) if not selected_object: QMessageBox.warning(self, "错误", "请先选择一个对象!") return current_item = self.mountedScriptsList.currentItem() if not current_item: QMessageBox.warning(self, "错误", "请选择要卸载的脚本!") return # 解析脚本名称(移除状态标记) item_text = current_item.text() script_name = item_text[2:] # 移除 "✓ " 或 "✗ " 前缀 try: success = self.world.removeScript(selected_object, script_name) if success: QMessageBox.information(self, "成功", f"脚本 '{script_name}' 已从对象卸载!") self.updateMountedScriptsList(selected_object) # 同时更新属性面板 if self.treeWidget and self.treeWidget.currentItem(): self.world.updatePropertyPanel(self.treeWidget.currentItem()) else: QMessageBox.warning(self, "错误", f"卸载脚本失败!") except Exception as e: QMessageBox.critical(self, "错误", f"卸载脚本时出错: {str(e)}") def closeEvent(self, event): """处理窗口关闭事件""" try: print("🔄 正在关闭应用程序...") # 清理工具管理器中的进程 if hasattr(self.world, 'tool_manager') and self.world.tool_manager: print("🧹 清理工具管理器进程...") self.world.tool_manager.cleanup_processes() # 停止更新定时器 if hasattr(self, 'updateTimer') and self.updateTimer: self.updateTimer.stop() print("⏹️ 更新定时器已停止") # 清理Panda3D资源 if hasattr(self, 'pandaWidget') and self.pandaWidget: print("🧹 清理Panda3D资源...") self.pandaWidget.cleanup() print("✅ 应用程序清理完成") event.accept() except Exception as e: print(f"⚠️ 关闭应用程序时出错: {e}") event.accept() # 即使出错也要关闭 def onCreateFlatTerrain(self): """创建平面地形""" dialog = QDialog(self) 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) width_layout = QHBoxLayout() width_layout.addWidget(QLabel("宽度:")) width_spin = QDoubleSpinBox() width_spin.setRange(0,10000) width_spin.setValue(0.3) width_layout.addWidget(width_spin) layout.addLayout(width_layout) # 高度 height_layout = QHBoxLayout() height_layout.addWidget(QLabel("高度:")) height_spin = QDoubleSpinBox() height_spin.setRange(0, 10000) height_spin.setValue(0.3) height_layout.addWidget(height_spin) layout.addLayout(height_layout) # 分辨率 resolution_layout = QHBoxLayout() resolution_layout.addWidget(QLabel("分辨率:")) resolution_spin = QSpinBox() resolution_spin.setRange(16, 2048) resolution_spin.setValue(256) resolution_spin.setSingleStep(16) resolution_layout.addWidget(resolution_spin) layout.addLayout(resolution_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) # 显示对话框 if dialog.exec_() == QDialog.Accepted: width = width_spin.value() height = height_spin.value() resolution = resolution_spin.value() # 调用世界对象创建地形 terrain_info = self.world.createFlatTerrain((width, height), resolution) if terrain_info: QMessageBox.information(self, "成功", "平面地形创建成功!") else: QMessageBox.warning(self, "错误", "平面地形创建失败!") def onCreateHeightmapTerrain(self): """从高度图创建地形""" dialog = self.createStyledFileDialog( self, "选择高度图文件", "", "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga);;所有文件 (*)" ) 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) 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) # 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) # 连接信号 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() # 调用世界对象创建地形 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 if path: openProjectForPath(path,main_window) return app, main_window