""" 主窗口设置模块 负责主窗口的界面构建和事件绑定: - 菜单栏、工具栏创建 - 停靠窗口设置 - 事件连接和信号处理 """ WEB_BROWSER_PROCESS_CODE = r""" import sys import os from PyQt5.QtWidgets import QApplication, QWidget, QVBoxLayout from PyQt5.QtCore import Qt, QUrl, QTimer from PyQt5.QtGui import QPalette, QColor # 禁用硬件加速 os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu --disable-software-rasterizer --disable-gpu-compositing --in-process-gpu" os.environ["QT_WEBENGINE_DISABLE_GPU"] = "1" os.environ["QT_OPENGL"] = "software" try: from PyQt5.QtWebEngineWidgets import QWebEngineView, QWebEngineSettings WEB_ENGINE_AVAILABLE = True except ImportError: WEB_ENGINE_AVAILABLE = False print("⚠️ QtWebEngine 不可用") class EmbeddedWebBrowser(QWidget): def __init__(self, parent=None, url="https://www.baidu.com", parent_window_id=0): super().__init__(parent) self.setWindowTitle("Web 浏览器") self.parent_window_id = parent_window_id self.url = url # 设置窗口标志 - 如果要嵌入,使用子窗口样式 if parent_window_id > 0: # 使用 FramelessWindowHint 去除边框,便于嵌入 self.setWindowFlags(Qt.FramelessWindowHint) else: self.setWindowFlags(Qt.Window) self.resize(800, 600) # 设置背景色 palette = self.palette() palette.setColor(QPalette.Window, QColor(25, 25, 27)) self.setPalette(palette) self.setAutoFillBackground(True) # 创建布局 layout = QVBoxLayout(self) layout.setContentsMargins(0, 0, 0, 0) layout.setSpacing(0) if WEB_ENGINE_AVAILABLE: # 创建 WebEngine 视图 self.web_view = QWebEngineView() # 设置样式 self.web_view.setStyleSheet("background-color: #19191b;") # 禁用硬件加速 settings = self.web_view.settings() settings.setAttribute(QWebEngineSettings.Accelerated2dCanvasEnabled, False) settings.setAttribute(QWebEngineSettings.WebGLEnabled, False) settings.setAttribute(QWebEngineSettings.PluginsEnabled, False) settings.setAttribute(QWebEngineSettings.JavascriptEnabled, True) settings.setAttribute(QWebEngineSettings.AutoLoadImages, True) settings.setAttribute(QWebEngineSettings.LocalContentCanAccessRemoteUrls, True) # 连接加载信号 self.web_view.loadStarted.connect(self.on_load_started) self.web_view.loadProgress.connect(self.on_load_progress) self.web_view.loadFinished.connect(self.on_load_finished) layout.addWidget(self.web_view) print("[WebEngine] WebEngine 视图已创建") # 延迟加载网页,确保窗口完全初始化 QTimer.singleShot(500, lambda: self.load_url(url)) else: from PyQt5.QtWidgets import QLabel label = QLabel("QtWebEngine 不可用\n请安装: pip install PyQtWebEngine") label.setAlignment(Qt.AlignCenter) label.setStyleSheet("color: white; font-size: 14px;") layout.addWidget(label) def on_load_started(self): print("[WebEngine] 开始加载网页...") def on_load_progress(self, progress): if progress % 20 == 0: # 每20%打印一次 print(f"[WebEngine] 加载进度: {progress}%") def on_load_finished(self, success): if success: print("[WebEngine] 网页加载成功") else: print("[WebEngine] 网页加载失败") def load_url(self, url): if WEB_ENGINE_AVAILABLE and hasattr(self, 'web_view'): print(f"[WebEngine] 准备加载 URL: {url}") qurl = QUrl(url) print(f"[WebEngine] QUrl 有效性: {qurl.isValid()}") print(f"[WebEngine] QUrl 内容: {qurl.toString()}") self.web_view.load(qurl) print(f"[WebEngine] load() 方法已调用") else: print(f"[WebEngine] 无法加载 URL - WEB_ENGINE_AVAILABLE={WEB_ENGINE_AVAILABLE}, has_web_view={hasattr(self, 'web_view')}") def get_native_window_id(self): return int(self.winId()) def embed_into_parent(self): if self.parent_window_id > 0: try: print(f"[WebEngine] 开始嵌入窗口到父窗口 ID: {self.parent_window_id}") # Windows 平台使用 win32 API 嵌入窗口 if sys.platform == 'win32': import ctypes from ctypes import wintypes # 获取窗口句柄 hwnd = int(self.winId()) parent_hwnd = self.parent_window_id print(f"[WebEngine] 子窗口句柄: {hwnd}") print(f"[WebEngine] 父窗口句柄: {parent_hwnd}") user32 = ctypes.windll.user32 # 先获取父窗口大小 rect = wintypes.RECT() user32.GetClientRect(parent_hwnd, ctypes.byref(rect)) width = rect.right - rect.left height = rect.bottom - rect.top print(f"[WebEngine] 父窗口大小: {width}x{height}") # 设置父窗口 result = user32.SetParent(hwnd, parent_hwnd) print(f"[WebEngine] SetParent 结果: {result}") # 设置窗口样式为子窗口 GWL_STYLE = -16 WS_CHILD = 0x40000000 WS_VISIBLE = 0x10000000 WS_CLIPCHILDREN = 0x02000000 WS_CLIPSIBLINGS = 0x04000000 old_style = user32.GetWindowLongW(hwnd, GWL_STYLE) print(f"[WebEngine] 旧窗口样式: {hex(old_style)}") # 设置为子窗口样式,移除边框和标题栏 new_style = (WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN | WS_CLIPSIBLINGS) user32.SetWindowLongW(hwnd, GWL_STYLE, new_style) print(f"[WebEngine] 新窗口样式: {hex(new_style)}") # 调整窗口位置和大小,填充整个父窗口 SWP_FRAMECHANGED = 0x0020 SWP_SHOWWINDOW = 0x0040 SWP_NOZORDER = 0x0004 result = user32.SetWindowPos( hwnd, 0, # HWND_TOP 0, 0, # x, y width, height, # width, height SWP_FRAMECHANGED | SWP_SHOWWINDOW | SWP_NOZORDER ) print(f"[WebEngine] SetWindowPos 结果: {result}") # 强制刷新窗口 user32.UpdateWindow(hwnd) user32.InvalidateRect(hwnd, None, True) print(f"[WebEngine] 已成功嵌入到父窗口 (Windows)") return True else: # 其他平台使用 Qt 的方式 from PyQt5.QtGui import QWindow parent_window = QWindow.fromWinId(self.parent_window_id) if parent_window: self.windowHandle().setParent(parent_window) print(f"[WebEngine] 已嵌入到父窗口 (Qt): {self.parent_window_id}") return True except Exception as e: print(f"[WebEngine] 嵌入窗口失败: {e}") import traceback traceback.print_exc() return False def main(): # 注释掉标准输出重定向,让输出正常显示在终端 # if sys.platform == 'win32': # import io # sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding='utf-8', errors='replace') # sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding='utf-8', errors='replace') print("=" * 60) print("[WebEngine] 启动独立 WebEngine 进程") print("=" * 60) # 创建应用 app = QApplication(sys.argv) # 从命令行参数获取 URL 和窗口 ID url = sys.argv[1] if len(sys.argv) > 1 else "https://www.baidu.com" parent_window_id = int(sys.argv[2]) if len(sys.argv) > 2 else 0 print(f"[WebEngine] 参数:") print(f" - URL: {url}") print(f" - 父窗口 ID: {parent_window_id}") # 创建浏览器窗口 browser = EmbeddedWebBrowser(url=url, parent_window_id=parent_window_id) # 显示窗口 browser.show() # 输出窗口 ID 供父进程使用 window_id = browser.get_native_window_id() print(f"WINDOW_ID:{window_id}") sys.stdout.flush() # 等待网页开始加载后再嵌入窗口 if parent_window_id > 0: # 延迟2秒,确保网页已经开始加载和渲染 QTimer.singleShot(10, browser.embed_into_parent) print("[WebEngine] 进程已就绪") print("=" * 60) sys.exit(app.exec_()) if __name__ == "__main__": main() """ import os import sys # ==================== 禁用 QtWebEngine 硬件加速 ==================== # 必须在导入 QtWebEngine 之前设置环境变量 os.environ["QTWEBENGINE_CHROMIUM_FLAGS"] = "--disable-gpu --disable-software-rasterizer --disable-gpu-compositing --in-process-gpu" os.environ["QT_WEBENGINE_DISABLE_GPU"] = "1" os.environ["QT_OPENGL"] = "software" print("✓ QtWebEngine 硬件加速已禁用,使用软件渲染模式") # ==================== 结束配置 ==================== 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, QRadioButton, QTextEdit, QCheckBox, QTabWidget, QSizePolicy, QListWidgetItem) from PyQt5.QtCore import Qt, QDir, QTimer, QSize, QPoint, QUrl, QRect, QCoreApplication from direct.showbase.ShowBaseGlobal import aspect2d from panda3d.core import OrthographicLens from ui.widgets import (CustomMeta3DWidget, CustomFileView, CustomTreeWidget, CustomAssetsTreeWidget, CustomConsoleDockWidget, UniversalMessageDialog) from ui.icon_manager import get_icon_manager, get_icon # import yaml from PyQt5.QtWidgets import QDialog, QVBoxLayout, QListWidget, QPushButton, QDialogButtonBox, QCheckBox, QLabel, QHBoxLayout from PyQt5.QtCore import Qt class StyledTerrainDialog(QDialog): """与新建项目对话框风格一致的参数输入对话框""" def __init__(self, parent, title, fields, primary_text="确认", secondary_text="取消"): super().__init__(parent) self.setWindowTitle(title) self.setObjectName("styledTerrainDialog") self.setModal(True) self.resize(508, 241) self.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) self.setAttribute(Qt.WA_TranslucentBackground, True) self.dragging = False self.drag_position = QPoint() self.icon_manager = get_icon_manager() self._title_icon_size = QSize(18, 18) self._icon_close = self.icon_manager.get_icon('close_icon', self._title_icon_size) self.field_widgets = {} self.setStyleSheet(""" QDialog#styledTerrainDialog { background-color: transparent; border: none; } QFrame#baseFrame { background-color: #000000; border: 1px solid #3E3E42; border-radius: 5px; } QWidget#titleBar { background-color: transparent; border-radius: 5px 5px 0px 0px; min-height: 32px; max-height: 32px; } QLabel#titleLabel { color: #FFFFFF; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 14px; font-weight: 500; letter-spacing: 0.7px; } QWidget#controlButtons QPushButton { background-color: transparent; border: none; color: #EBEBEB; font-size: 14px; min-width: 18px; max-width: 18px; min-height: 18px; max-height: 18px; padding: 0px; border-radius: 3px; } QWidget#controlButtons QPushButton:hover { background-color: #2A2D2E; color: #FFFFFF; } QPushButton#closeButton { border-radius: 0px 5px 0px 0px; } QPushButton#closeButton:hover { background-color: #2A2D2E; color: #FFFFFF; } QWidget#contentWidget { background-color: transparent; border-radius: 0px 0px 5px 5px; } QFrame#contentContainer { background-color: #19191B; border: 1px solid #2C2F36; border-radius: 5px; } QLabel[role="fieldLabel"] { color: #EBEBEB; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 12px; font-weight: 400; letter-spacing: 0.6px; } QLabel[role="hint"] { color: rgba(235, 235, 235, 0.6); font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 11px; font-weight: 300; letter-spacing: 0.55px; padding: 0px; } QDoubleSpinBox, QSpinBox { background-color: rgba(89, 100, 113, 0.2); color: #EBEBEB; border: 1px solid rgba(76, 92, 110, 0.6); border-radius: 2px; padding: 0px 10px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 11px; font-weight: 300; letter-spacing: 0.55px; min-height: 30px; max-height: 30px; } QDoubleSpinBox:focus, QSpinBox:focus { border: 1px solid #3067C0; background-color: rgba(48, 103, 192, 0.1); } QDoubleSpinBox:hover, QSpinBox:hover { border: 1px solid #3067C0; background-color: rgba(89, 100, 113, 0.3); } QDoubleSpinBox:disabled, QSpinBox:disabled { background-color: rgba(89, 100, 113, 0.1); color: rgba(235, 235, 235, 0.4); border: 1px solid rgba(76, 92, 110, 0.2); } QPushButton { background-color: rgba(89, 98, 118, 0.5); color: #EBEBEB; border: none; border-radius: 2px; padding: 0px 12px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-weight: 300; font-size: 10px; letter-spacing: 0.5px; min-width: 90px; min-height: 30px; max-height: 30px; } QPushButton:hover { background-color: #3067C0; color: #FFFFFF; } QPushButton:pressed { background-color: #2556A0; color: #FFFFFF; } QPushButton:disabled { background-color: rgba(89, 98, 118, 0.3); color: rgba(235, 235, 235, 0.4); } QPushButton#primaryButton { min-width: 120px; max-width: 120px; } QPushButton#secondaryButton { min-width: 120px; max-width: 120px; } """) main_layout = QVBoxLayout(self) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) base_frame = QFrame() base_frame.setObjectName('baseFrame') base_frame.setFrameShape(QFrame.NoFrame) base_frame.setAttribute(Qt.WA_StyledBackground, True) base_layout = QVBoxLayout(base_frame) base_layout.setContentsMargins(0, 0, 0, 0) base_layout.setSpacing(0) self.createTitleBar() base_layout.addWidget(self.title_bar) content_widget = QWidget() content_widget.setObjectName('contentWidget') content_layout = QVBoxLayout(content_widget) content_layout.setContentsMargins(15, 10, 15, 10) content_layout.setSpacing(0) content_container = QFrame() content_container.setObjectName('contentContainer') content_container.setFrameShape(QFrame.NoFrame) content_container.setAttribute(Qt.WA_StyledBackground, True) container_layout = QVBoxLayout(content_container) container_layout.setContentsMargins(15, 10, 15, 10) container_layout.setSpacing(10) for field in fields: row_widget = QWidget() row_layout = QHBoxLayout(row_widget) row_layout.setContentsMargins(0, 0, 0, 0) row_layout.setSpacing(10) label = QLabel(field.get("label", "")) label.setProperty('role', 'fieldLabel') label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) label.setMinimumWidth(field.get("label_width", 80)) label.setMaximumWidth(field.get("label_width", 120)) row_layout.addWidget(label) widget_type = field.get("type", "double") if widget_type == "int": widget = QSpinBox() widget.setRange(field.get("min", 0), field.get("max", 1000)) widget.setSingleStep(field.get("step", 1)) widget.setValue(field.get("value", field.get("default", 0))) elif widget_type == "text": widget = QLineEdit() widget.setText(field.get("value", field.get("default", ""))) if field.get("placeholder"): widget.setPlaceholderText(field.get("placeholder")) widget.setClearButtonEnabled(True) else: widget = QDoubleSpinBox() widget.setRange(field.get("min", 0.0), field.get("max", 1000.0)) widget.setSingleStep(field.get("step", 0.1)) widget.setDecimals(field.get("decimals", 2)) widget.setValue(field.get("value", field.get("default", 0.0))) widget.setFixedHeight(30) widget.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) row_layout.addWidget(widget, 1) if field.get("suffix"): suffix_label = QLabel(field["suffix"]) suffix_label.setProperty('role', 'fieldLabel') row_layout.addWidget(suffix_label) self.field_widgets[field.get("name", field.get("label", ""))] = widget container_layout.addWidget(row_widget) separator = QFrame() separator.setFrameShape(QFrame.HLine) separator.setFrameShadow(QFrame.Plain) separator.setFixedHeight(1) separator.setStyleSheet("background-color: #2C2F36; border: none;") container_layout.addWidget(separator) button_row_widget = QWidget() button_row_layout = QHBoxLayout(button_row_widget) button_row_layout.setContentsMargins(0, 0, 0, 0) button_row_layout.setSpacing(10) button_row_layout.addStretch() self.confirmButton = QPushButton(primary_text) self.confirmButton.setObjectName('primaryButton') self.confirmButton.setFixedSize(120, 30) self.confirmButton.clicked.connect(self.accept) button_row_layout.addWidget(self.confirmButton) self.cancelButton = QPushButton(secondary_text) self.cancelButton.setObjectName('secondaryButton') self.cancelButton.setFixedSize(120, 30) self.cancelButton.clicked.connect(self.reject) button_row_layout.addWidget(self.cancelButton) container_layout.addWidget(button_row_widget) content_layout.addWidget(content_container, 0, Qt.AlignTop) base_layout.addWidget(content_widget) main_layout.addWidget(base_frame) def createTitleBar(self): self.title_bar = QFrame() self.title_bar.setObjectName("titleBar") title_layout = QHBoxLayout(self.title_bar) title_layout.setContentsMargins(12, 0, 12, 0) title_layout.setSpacing(0) controls = QWidget() controls.setObjectName("controlButtons") controls_layout = QHBoxLayout(controls) controls_layout.setContentsMargins(0, 0, 0, 0) controls_layout.setSpacing(0) self.close_button = QPushButton() self.close_button.setObjectName("closeButton") self.close_button.clicked.connect(self.reject) self.close_button.setFocusPolicy(Qt.NoFocus) controls_layout.addWidget(self.close_button) self._applyTitleBarIcons() left_placeholder = QWidget() left_placeholder.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Preferred) title_layout.addWidget(left_placeholder) self.title_label = QLabel(self.windowTitle()) self.title_label.setObjectName("titleLabel") self.title_label.setAlignment(Qt.AlignCenter) title_layout.addWidget(self.title_label, 1) title_layout.addWidget(controls) left_placeholder.setFixedWidth(controls.sizeHint().width()) def _applyTitleBarIcons(self): if self._icon_close: self.close_button.setIcon(self._icon_close) self.close_button.setIconSize(self._title_icon_size) self.close_button.setText("") self.close_button.setToolTip("关闭") def setWindowTitle(self, title): super().setWindowTitle(title) if hasattr(self, "title_label"): self.title_label.setText(title) def mousePressEvent(self, event): if event.button() == Qt.LeftButton and hasattr(self, "title_bar"): if self.title_bar.geometry().contains(event.pos()): self.dragging = True self.drag_position = event.globalPos() - self.frameGeometry().topLeft() event.accept() super().mousePressEvent(event) def mouseMoveEvent(self, event): if event.buttons() == Qt.LeftButton and self.dragging: self.move(event.globalPos() - self.drag_position) event.accept() super().mouseMoveEvent(event) def mouseReleaseEvent(self, event): if event.button() == Qt.LeftButton: self.dragging = False super().mouseReleaseEvent(event) def get_value(self, name): widget = self.field_widgets.get(name) if isinstance(widget, (QDoubleSpinBox, QSpinBox)): return widget.value() if isinstance(widget, QLineEdit): return widget.text() return None 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.clipboard = [] self.clipboard_mode = None # 初始化图标管理器并打印调试信息 self.icon_manager = get_icon_manager() print("🔧 图标管理器初始化完成") self.icon_manager.debug_info() self.setStyleSheet(""" QMainWindow { background-color: #000000; } QMenuBar { background-color: #000000; color: #ffffff; border-bottom: 1px solid #4c5c6e; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 12px; font-weight: 400; } QMenuBar::item { background-color: transparent; padding: 6px 12px; color: #D4D4D4; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 12px; } QMenuBar::item:selected { background-color: rgba(48, 103, 192, 0.4); } QMenuBar::item:pressed { background-color: rgba(48, 103, 192, 0.6); } QMenu { background-color: #2E3035; color: #ebebeb; border: 1px solid #4c5c6e; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 12px; } QMenu::item { padding: 6px 20px; } QMenu::item:selected { background-color: rgba(48, 103, 192, 0.4); } QPushButton { background-color: rgba(89, 98, 118, 0.4); color: #ebebeb; border: none; padding: 6px 12px; border-radius: 2px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-weight: 300; font-size: 10px; } QPushButton:hover { background-color: #3067c0; color: #ffffff; } QPushButton:pressed { background-color: #2556a0; } QPushButton:disabled { background-color: #394560; color: rgba(235, 235, 235, 0.5); } QComboBox { background-color: rgba(89, 100, 113, 0.2); color: #ebebeb; border: 1px solid rgba(76, 92, 110, 0.6); border-radius: 2px; padding: 4px 8px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; } QComboBox::drop-down { border: none; border-left: 1px solid rgba(76, 92, 110, 0.6); background-color: rgba(89, 100, 113, 0.3); width: 16px; } QComboBox::down-arrow { image: url(icons/down_arrows.png); width: 10px; height: 10px; } QComboBox QAbstractItemView { background-color: #596471; color: #ebebeb; selection-background-color: rgba(48, 103, 192, 0.4); } QLineEdit { background-color: rgba(89, 100, 113, 0.2); color: #ebebeb; border: 1px solid rgba(76, 92, 110, 0.6); border-radius: 2px; padding: 4px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; } QSpinBox, QDoubleSpinBox { background-color: rgba(89, 100, 113, 0.2); color: #ebebeb; border: 1px solid rgba(76, 92, 110, 0.6); border-radius: 2px; padding: 4px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; } QScrollBar:vertical { background-color: #19191b; width: 12px; border: none; } QScrollBar::handle:vertical { background-color: #596471; border-radius: 2px; min-height: 20px; } QScrollBar::handle:vertical:hover { background-color: #3067c0; } QScrollBar:horizontal { background-color: #19191b; height: 12px; border: none; } QScrollBar::handle:horizontal { background-color: #596471; border-radius: 2px; min-width: 20px; } QScrollBar::handle:horizontal:hover { background-color: #3067c0; } """) # 设置 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: #19191b; color: #ebebeb; border: 1px solid #4c5c6e; } QMessageBox QLabel { color: #ebebeb; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 12px; font-weight: 300; } QMessageBox QPushButton { background-color: rgba(89, 98, 118, 0.4); color: #ebebeb; border: none; padding: 6px 12px; border-radius: 2px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-weight: 300; font-size: 10px; min-width: 80px; } QMessageBox QPushButton:hover { background-color: #3067c0; color: #ffffff; } QMessageBox QPushButton:pressed { background-color: #2556a0; } QMessageBox QPushButton:disabled { background-color: #394560; color: rgba(235, 235, 235, 0.5); } """ # 应用全局样式 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: #19191b; color: #ebebeb; } QLabel { color: #ffffff; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-weight: 500; font-size: 14px; } QLineEdit { background-color: rgba(89, 100, 113, 0.2); color: #ebebeb; border: 1px solid rgba(76, 92, 110, 0.6); border-radius: 2px; padding: 6px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; } QPushButton { background-color: rgba(89, 98, 118, 0.4); color: #ebebeb; border: none; padding: 6px 12px; border-radius: 2px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-weight: 300; font-size: 10px; min-width: 80px; } QPushButton:hover { background-color: #3067c0; color: #ffffff; } QPushButton:pressed { background-color: #2556a0; } QPushButton:disabled { background-color: #394560; color: rgba(235, 235, 235, 0.5); } """) return dialog def createStyledFileDialog(self, parent, caption, directory="", filter=""): """创建带有统一主题样式的 QFileDialog""" dialog = QFileDialog(parent) dialog.setWindowTitle(caption) # 设置样式表 dialog.setStyleSheet(""" QFileDialog { background-color: #19191b; color: #ebebeb; } QLabel { color: #ebebeb; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; } QListView { background-color: #19191b; color: #ebebeb; border: 1px solid #4c5c6e; alternate-background-color: #19191b; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; } QListView::item:hover { background-color: #394560; } QListView::item:selected { background-color: rgba(48, 103, 192, 0.4); color: #ffffff; } QTreeView { background-color: #19191b; color: #ebebeb; border: 1px solid #4c5c6e; alternate-background-color: #19191b; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; } QTreeView::item:hover { background-color: #394560; } QTreeView::item:selected { background-color: rgba(48, 103, 192, 0.4); color: #ffffff; } QComboBox { background-color: rgba(89, 100, 113, 0.2); color: #ebebeb; border: 1px solid rgba(76, 92, 110, 0.6); border-radius: 2px; padding: 4px 8px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; } QComboBox::drop-down { border: none; } QComboBox QAbstractItemView { background-color: #596471; color: #ebebeb; selection-background-color: rgba(48, 103, 192, 0.4); } QPushButton { background-color: rgba(89, 98, 118, 0.4); color: #ebebeb; border: none; padding: 6px 12px; border-radius: 2px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-weight: 300; font-size: 10px; } QPushButton:hover { background-color: #3067c0; color: #ffffff; } QPushButton:pressed { background-color: #2556a0; } QPushButton:disabled { background-color: #394560; color: rgba(235, 235, 235, 0.5); } QLineEdit { background-color: rgba(89, 100, 113, 0.2); color: #ebebeb; border: 1px solid rgba(76, 92, 110, 0.6); border-radius: 2px; padding: 4px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; } """) return dialog def setupCenterWidget(self): """设置窗口基本属性""" self.setWindowTitle("引擎编辑器") # 使用图标管理器设置窗口图标 app_icon = get_icon('app_logo') if not app_icon.isNull(): self.setWindowIcon(app_icon) print("✅ 应用图标设置成功") else: print("⚠️ 应用图标设置失败,使用默认图标") # 使用自定义的 Panda3D 部件作为中央部件 self.pandaWidget = CustomMeta3DWidget(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: transparent; border: none; padding: 0px; } QToolButton { background-color: #1e1e1f; border: 1px solid rgba(184, 211, 241, 0.2); color: #ebebeb; padding: 0px; border-radius: 2px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; min-width: 31px; max-width: 31px; min-height: 30px; max-height: 30px; } QToolButton:hover { background-color: rgba(89, 100, 113, 0.3); border: 1px solid rgba(184, 211, 241, 0.3); } QToolButton:checked { background-color: #3067c0; color: #ffffff; border: 1px solid rgba(48, 103, 192, 0.6); } QToolButton:pressed { background-color: #2556a0; } """) # 水平布局 self.toolbarLayout = QHBoxLayout(self.embeddedToolbar) self.toolbarLayout.setContentsMargins(0, 0, 0, 0) self.toolbarLayout.setSpacing(4) # 创建工具按钮组 self.toolGroup = QButtonGroup() # 选择工具 self.selectTool = QToolButton() select_icon = get_icon('select_tool', QSize(20, 20)) if not select_icon.isNull(): self.selectTool.setIcon(select_icon) self.selectTool.setText('选择') # 如果没有图标则显示文字 self.selectTool.setIconSize(QSize(20, 20)) 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() icon_path = self.get_icon_path("move_tool.png") if icon_path and os.path.exists(icon_path): self.moveTool.setIcon(QIcon(icon_path)) self.moveTool.setText("移动") self.moveTool.setIconSize(QSize(20, 20)) 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() rotate_icon = get_icon('rotate_tool', QSize(20, 20)) if not rotate_icon.isNull(): self.rotateTool.setIcon(rotate_icon) self.rotateTool.setText("旋转") self.rotateTool.setIconSize(QSize(20, 20)) 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() scale_icon = get_icon('scale_tool', QSize(20, 20)) if not scale_icon.isNull(): self.scaleTool.setIcon(scale_icon) self.scaleTool.setText("缩放") self.scaleTool.setIconSize(QSize(20, 20)) 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.toolsMenu.addSeparator() self.sunsetAction = self.toolsMenu.addAction('光照编辑') self.pluginAction = self.toolsMenu.addAction('图形编辑') # self.toolsMenu.addSeparator() # self.iconManagerAction = self.toolsMenu.addAction('图标管理器') # self.iconManagerAction.triggered.connect(self.onOpenIconManager) # VR子菜单 self.vrMenu = self.toolsMenu.addMenu('VR') self.enterVRAction = self.vrMenu.addAction('进入VR模式') self.exitVRAction = self.vrMenu.addAction('退出VR模式') self.vrMenu.addSeparator() self.vrStatusAction = self.vrMenu.addAction('VR状态') self.vrSettingsAction = self.vrMenu.addAction('VR设置') self.vrMenu.addSeparator() # VR调试子菜单 self.vrDebugMenu = self.vrMenu.addMenu('VR调试') self.vrDebugToggleAction = self.vrDebugMenu.addAction('启用调试输出') self.vrDebugToggleAction.setCheckable(True) self.vrDebugToggleAction.setChecked(False) # 默认关闭(节省资源) self.vrShowPerformanceAction = self.vrDebugMenu.addAction('立即显示性能报告') self.vrDebugMenu.addSeparator() # 调试模式切换 self.vrDebugModeMenu = self.vrDebugMenu.addMenu('输出模式') self.vrDebugBriefAction = self.vrDebugModeMenu.addAction('简短模式') self.vrDebugDetailedAction = self.vrDebugModeMenu.addAction('详细模式') self.vrDebugBriefAction.setCheckable(True) self.vrDebugDetailedAction.setCheckable(True) self.vrDebugDetailedAction.setChecked(True) # 默认详细模式 # 创建调试模式动作组(单选) from PyQt5.QtWidgets import QActionGroup self.vrDebugModeGroup = QActionGroup(self) self.vrDebugModeGroup.addAction(self.vrDebugBriefAction) self.vrDebugModeGroup.addAction(self.vrDebugDetailedAction) self.vrDebugMenu.addSeparator() # 性能监控选项 self.vrPerformanceMonitorAction = self.vrDebugMenu.addAction('启用性能监控') self.vrPerformanceMonitorAction.setCheckable(True) self.vrPerformanceMonitorAction.setChecked(False) # 默认关闭(节省资源) self.vrGpuTimingAction = self.vrDebugMenu.addAction('启用GPU时间监控') self.vrGpuTimingAction.setCheckable(True) self.vrGpuTimingAction.setChecked(False) # 默认关闭(节省资源) # 管线监控选项 self.vrPipelineMonitorAction = self.vrDebugMenu.addAction('启用管线监控') self.vrPipelineMonitorAction.setCheckable(True) self.vrPipelineMonitorAction.setChecked(False) # 默认关闭(节省资源) self.vrDebugMenu.addSeparator() # 姿态策略选项 self.vrPoseStrategyMenu = self.vrDebugMenu.addMenu('姿态策略') self.vrPoseRenderCallbackAction = self.vrPoseStrategyMenu.addAction('渲染回调策略') self.vrPoseUpdateTaskAction = self.vrPoseStrategyMenu.addAction('更新任务策略') self.vrPoseRenderCallbackAction.setCheckable(True) self.vrPoseUpdateTaskAction.setCheckable(True) self.vrPoseRenderCallbackAction.setChecked(True) # 默认策略 # 创建姿态策略动作组(单选) self.vrPoseStrategyGroup = QActionGroup(self) self.vrPoseStrategyGroup.addAction(self.vrPoseRenderCallbackAction) self.vrPoseStrategyGroup.addAction(self.vrPoseUpdateTaskAction) self.vrDebugMenu.addSeparator() # 测试功能 self.vrTestPipelineAction = self.vrDebugMenu.addAction('测试管线监控') # VR测试模式 self.vrTestModeAction = self.vrDebugMenu.addAction('VR测试模式') self.vrTestModeAction.setCheckable(True) self.vrTestModeAction.setChecked(False) # 默认关闭 # VR测试模式调试子菜单 self.vrTestDebugMenu = self.vrDebugMenu.addMenu('测试模式调试') # 渐进式功能开关 self.vrTestSubmitTextureAction = self.vrTestDebugMenu.addAction('启用纹理提交') self.vrTestSubmitTextureAction.setCheckable(True) self.vrTestSubmitTextureAction.setChecked(False) # 默认关闭 self.vrTestWaitPosesAction = self.vrTestDebugMenu.addAction('启用姿态等待') self.vrTestWaitPosesAction.setCheckable(True) self.vrTestWaitPosesAction.setChecked(False) # 默认关闭 self.vrTestDebugMenu.addSeparator() # 快捷测试预设 self.vrTestStep1Action = self.vrTestDebugMenu.addAction('步骤1: 只启用纹理提交') self.vrTestStep2Action = self.vrTestDebugMenu.addAction('步骤2: 只启用姿态等待') self.vrTestStep3Action = self.vrTestDebugMenu.addAction('步骤3: 同时启用两者') self.vrTestResetAction = self.vrTestDebugMenu.addAction('重置: 禁用所有功能') self.vrDebugSettingsAction = self.vrDebugMenu.addAction('调试设置...') # 初始状态下禁用退出VR选项 self.exitVRAction.setEnabled(False) # 统一创建菜单 - 关键修改 self.createMenu = menubar.addMenu('创建') self.setupCreateMenuActions() # 统一创建菜单动作 #添加地形菜单 self.createTerrainMenu = self.createMenu.addMenu('地形') self.createFlatTerrainAction = self.createTerrainMenu.addAction('创建平面地形') self.createHeightmapTerrainAction = self.createTerrainMenu.addAction('从高度图创建地形') self.createTerrainMenu.addSeparator() # self.terrainEditModeAction = self.createTerrainMenu.addAction('地形编辑模式') # 帮助菜单 self.helpMenu = menubar.addMenu('帮助') self.aboutAction = self.helpMenu.addAction('关于') self.aboutAction.triggered.connect(self.showAboutDialog) tool_menu = self.menuBar().addMenu("配置") plugin_config_action = tool_menu.addAction("渲染管线插件配置") plugin_config_action.triggered.connect(self.open_plugin_config_dialog) def showAboutDialog(self): msgBox = QMessageBox() msgBox.setWindowTitle("关于") msgBox.setText(f'元泰引擎系统\nMetaCore\n版本:v1.0') msgBox.setIcon(QMessageBox.NoIcon) msgBox.exec_() def open_plugin_config_dialog(self): """打开插件配置对话框""" try: dialog = PluginConfigDialog(self) dialog.exec_() except Exception as e: print(f"打开插件配置对话框失败: {e}") 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.createScriptMenu = self.createMenu.addMenu('脚本') self.createScriptAction = self.createScriptMenu.addAction('创建脚本...') self.loadScriptAction = self.createScriptMenu.addAction('加载脚本文件...') self.loadAllScriptsAction = self.createScriptMenu.addAction('重载所有脚本') self.createScriptMenu.addSeparator() self.toggleHotReloadAction = self.createScriptMenu.addAction('启用热重载') self.toggleHotReloadAction.setCheckable(True) self.toggleHotReloadAction.setChecked(True) # 默认启用 self.createScriptMenu.addSeparator() self.openScriptsManagerAction = self.createScriptMenu.addAction('脚本管理器') self.createInfoPanelMenu = self.createMenu.addMenu('信息面板') self.createSamplePanelAction = self.createInfoPanelMenu.addAction('创建2D示例面板') self.create3DSamplePanelAction = self.createInfoPanelMenu.addAction('创建3D实例面板') self.createInfoPanelMenu.addSeparator() self.webBrowserAction = self.createInfoPanelMenu.addAction("Web面板") # 统一连接信号到处理方法 self.connectCreateMenuActions() def setupViewMenuActions(self): """设置视图菜单动作""" # 连接视图菜单事件 self.viewPerspectiveAction.triggered.connect(self.onViewPerspective) #self.viewTopAction.triggered.connect(self.onViewTop) #self.viewFrontAction.triggered.connect(self.onViewFront) self.viewOrthographicAction = self.viewMenu.addAction('正交视图') # 添加正交视图动作 #self.viewOrthographicAction.triggered.connect(self.onViewOrthographic) #self.viewGridAction.triggered.connect(self.onViewGrid) # 添加网格显示的信号连接 # 保存原始相机设置 self._original_camera_fov = 80 self._original_camera_pos = (0, -50, 20) self._original_camera_lookat = (0, 0, 0) self._grid_visible = False def onViewPerspective(self): """切换到透视视图""" try: lens = self.world.cam.node().getLens() lens.setFov(self._original_camera_fov) except Exception as e: print(f"切换到透视视图失败{e}") def onViewOrthographic(self): """切换到正交视图""" try: # 保存当前相机设置(如果是透视模式) lens = self.world.cam.node().getLens() if not hasattr(self, '_saved_perspective_settings'): self._saved_perspective_settings = { 'fov': lens.getFov()[0], 'pos': self.world.cam.getPos(), 'hpr': self.world.cam.getHpr() } # 获取窗口尺寸 win_width, win_height = self.world.getWindowSize() aspect_ratio = win_width / win_height if win_height != 0 else 16 / 9 # 修改现有镜头为正交投影 if not isinstance(lens, OrthographicLens): # 保存当前镜头类型 self._original_lens = lens # 创建正交镜头并替换现有镜头 ortho_lens = OrthographicLens() ortho_lens.setFilmSize(20 * aspect_ratio, 20) # 设置正交镜头大小 ortho_lens.setNearFar(-1000, 1000) # 设置较大的近远裁剪面 # 应用正交镜头 self.world.cam.node().setLens(ortho_lens) else: # 如果已经是正交镜头,则调整其参数 film_height = 20 film_width = film_height * aspect_ratio lens.setFilmSize(film_width, film_height) print("切换到正交视图") except Exception as e: print(f"切换正交视图失败: {e}") def onViewTop(self): """切换到俯视图(正交)""" try: # 保存当前设置 self._saveCurrentCameraSettings() # 设置正交投影 self._setupOrthographicLens() # 设置摄像机位置(从上方俯视) self.world.cam.setPos(0, 0, 30) self.world.cam.lookAt(0, 0, 0) self.world.cam.setHpr(0, -90, 0) # 朝下看 # 更新菜单项文本 self._updateViewMenuText() print("切换到俯视图") except Exception as e: print(f"切换俯视图失败: {e}") def onViewFront(self): """切换到前视图(正交)""" try: # 保存当前设置 self._saveCurrentCameraSettings() # 设置正交投影 self._setupOrthographicLens() # 设置摄像机位置(从前方向看) self.world.cam.setPos(0, -30, 0) self.world.cam.lookAt(0, 0, 0) self.world.cam.setHpr(0, 0, 0) # 正面朝向 # 更新菜单项文本 self._updateViewMenuText() print("切换到前视图") except Exception as e: print(f"切换前视图失败: {e}") def onViewGrid(self): """切换网格显示/隐藏""" try: # 切换网格显示状态 self._grid_visible = not self._grid_visible # 查找网格节点 grid_node = self.world.render.find("**/grid") if grid_node.isEmpty(): # 如果网格不存在则创建 self._createGridView() grid_node = self.world.render.find("**/grid") # 设置网格可见性 if not grid_node.isEmpty(): if self._grid_visible: grid_node.show() self.viewGridAction.setText("隐藏网格") print("网格已显示") else: grid_node.hide() self.viewGridAction.setText("显示网格") print("网格已隐藏") else: print("网格节点未找到") except Exception as e: print(f"切换网格显示失败: {e}") def _createGridView(self): """创建网格视图""" try: from panda3d.core import LineSegs,Vec3 grid_node = self.world.render.attachNewNode("grid") lines = LineSegs() lines.setThickness(1.0) lines.setColor(0.3,0.3,0.3,1.0) grid_size = 20 grid_step = 1 for i in range(-grid_size,grid_size+1,grid_step): lines.moveTo(Vec3(-grid_size,i,0)) lines.drawTo(Vec3(grid_size,i,0)) grid_node.attachNewNode(lines.create()) # 添加中心轴线(红色X轴,绿色Y轴) axis_lines = LineSegs() axis_lines.setThickness(2.0) # X轴(红色) axis_lines.setColor(1.0,0.0,0.0,1.0) axis_lines.moveTo(Vec3(0,0,0)) axis_lines.drawTo(Vec3(grid_size,0,0)) # Y轴(绿色) axis_lines.setColor(0.0, 1.0, 0.0, 1.0) axis_lines.moveTo(Vec3(0, 0, 0)) axis_lines.drawTo(Vec3(0, grid_size, 0)) grid_node.attachNewNode(axis_lines.create()) print("网格已创建") except Exception as e: print(f"创建网格失败{e}") def _saveCurrentCameraSettings(self): """保存当前相机设置""" try: lens = self.world.cam.node().getLens() self._saved_camera_settings = { 'lens_type': 'perspective' if not isinstance(lens, OrthographicLens) else 'orthographic', 'fov': lens.getFov()[0] if hasattr(lens, 'getFov') else None, 'film_size': (lens.getFilmSize()[0], lens.getFilmSize()[1]) if hasattr(lens, 'getFilmSize') else None, 'pos': self.world.cam.getPos(), 'hpr': self.world.cam.getHpr() } except Exception as e: print(f"保存相机设置失败: {e}") def _setupOrthographicLens(self): """设置正交镜头""" try: win_width, win_height = self.world.getWindowSize() aspect_ratio = win_width / win_height if win_height != 0 else 16 / 9 from panda3d.core import OrthographicLens ortho_lens = OrthographicLens() ortho_lens.setFilmSize(20 * aspect_ratio, 20) # 设置正交镜头大小 ortho_lens.setNearFar(-1000, 1000) # 设置较大的近远裁剪面 self.world.cam.node().setLens(ortho_lens) except Exception as e: print(f"设置正交镜头失败: {e}") def _updateViewMenuText(self): """更新视图菜单文本""" try: lens = self.world.cam.node().getLens() from panda3d.core import OrthographicLens # 更新正交/透视视图动作文本 if isinstance(lens, OrthographicLens): self.viewOrthographicAction.setText("切换到透视视图") self.viewOrthographicAction.triggered.disconnect() self.viewOrthographicAction.triggered.connect(self.onViewPerspective) else: self.viewOrthographicAction.setText("切换到正交视图") self.viewOrthographicAction.triggered.disconnect() self.viewOrthographicAction.triggered.connect(self.onViewOrthographic) except Exception as e: print(f"更新视图菜单文本失败: {e}") # 如果需要在窗口大小改变时调整正交镜头,可以添加以下方法 def _onWindowResized(self): """窗口大小改变时的处理""" try: lens = self.world.cam.node().getLens() from panda3d.core import OrthographicLens # 如果当前是正交镜头,需要根据新窗口大小调整 if isinstance(lens, OrthographicLens): win_width, win_height = self.world.getWindowSize() if win_height != 0: aspect_ratio = win_width / win_height film_height = 20 film_width = film_height * aspect_ratio lens.setFilmSize(film_width, film_height) except Exception as e: print(f"窗口大小调整失败: {e}") 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.createScriptAction.triggered.connect(self.onCreateScript) self.loadScriptAction.triggered.connect(self.onLoadScript) #self.loadAllScriptsAction.triggered.connect(self.onLoadAllScripts) self.toggleHotReloadAction.triggered.connect(self.onToggleHotReload) self.openScriptsManagerAction.triggered.connect(self.onOpenScriptsManager) self.createSamplePanelAction.triggered.connect(self.world.info_panel_manager.onCreateSampleInfoPanel) self.create3DSamplePanelAction.triggered.connect(self.onCreate3DSampleInfoPanel) #self.webBrowserAction.triggered.connect(self.openWebBrowser_win) if sys.platform == 'win32': self.webBrowserAction.triggered.connect(self.openWebBrowser_win) else: self.webBrowserAction.triggered.connect(self.openWebBrowser) 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: #19191b; color: #ffffff; border: none; } QDockWidget::title { background-color: #19191b; padding: 8px 10px; border-bottom: none; text-align: left; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 12px; font-weight: 500; } QDockWidget::close-button { background-color: rgba(89, 100, 113, 0.3); border: 1px solid rgba(184, 211, 241, 0.2); icon-size: 10px; border-radius: 2px; width: 16px; height: 16px; right: 6px; top: 6px; } QDockWidget::float-button { background-color: rgba(89, 100, 113, 0.3); border: 1px solid rgba(184, 211, 241, 0.2); icon-size: 10px; border-radius: 2px; width: 16px; height: 16px; right: 26px; top: 6px; } QDockWidget::close-button:hover, QDockWidget::float-button:hover { background-color: rgba(89, 100, 113, 0.5); border: 1px solid rgba(184, 211, 241, 0.3); } QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { background-color: #3067c0; border: 1px solid rgba(48, 103, 192, 0.6); } QTreeView { background-color: #19191b; color: #ebebeb; border: none; padding: 0px 0px 0px 8px; alternate-background-color: #19191b; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 400; outline: none; } QTreeView::item { padding: 3px 0px; border: none; } QTreeView::item:hover { background-color: rgba(89, 100, 113, 0.3); } QTreeView::item:selected { background-color: rgba(48, 103, 192, 0.4); color: #ffffff; } QTreeView::branch { background-color: #19191b; } QTreeView::branch:has-children:!has-siblings:closed, QTreeView::branch:closed:has-children:has-siblings { image: url(icons/solid_right_arrows.png); width: 12px; height: 12px; } QTreeView::branch:open:has-children:!has-siblings, QTreeView::branch:open:has-children:has-siblings { image: url(icons/solid_down_arrows.png); width: 12px; height: 12px; } """) self.treeWidget = CustomTreeWidget(self.world) self.world.setTreeWidget(self.treeWidget) # 设置树形控件引用 # 创建包装容器,添加标题下方的分隔线 leftDockContainer = QWidget() leftDockContainer.setStyleSheet("QWidget { background-color: #19191b; }") leftDockLayout = QVBoxLayout(leftDockContainer) leftDockLayout.setContentsMargins(0, 0, 0, 0) leftDockLayout.setSpacing(0) # 添加带左右间距的分隔线 leftSeparator = QFrame() leftSeparator.setFrameShape(QFrame.HLine) leftSeparator.setFrameShadow(QFrame.Plain) leftSeparator.setStyleSheet(""" QFrame { background-color: rgba(77, 116, 189, 0.4); max-height: 1px; margin-left: 8px; margin-right: 8px; border: none; } """) leftDockLayout.addWidget(leftSeparator) leftDockLayout.addWidget(self.treeWidget) self.leftDock.setWidget(leftDockContainer) self.addDockWidget(Qt.DockWidgetArea.LeftDockWidgetArea, self.leftDock) # 创建右侧停靠窗口(属性窗口) self.rightDock = QDockWidget("属性", self) self.rightDock.setStyleSheet(""" QDockWidget { background-color: #19191b; color: #ffffff; border: none; } QDockWidget::title { background-color: #19191b; padding: 8px 10px; border-bottom: none; text-align: left; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 14px; font-weight: 500; } QDockWidget::close-button { background-color: rgba(89, 100, 113, 0.3); border: 1px solid rgba(184, 211, 241, 0.2); icon-size: 10px; border-radius: 2px; width: 16px; height: 16px; right: 6px; top: 6px; } QDockWidget::float-button { background-color: rgba(89, 100, 113, 0.3); border: 1px solid rgba(184, 211, 241, 0.2); icon-size: 10px; border-radius: 2px; width: 16px; height: 16px; right: 26px; top: 6px; } QDockWidget::close-button:hover, QDockWidget::float-button:hover { background-color: rgba(89, 100, 113, 0.5); border: 1px solid rgba(184, 211, 241, 0.3); } QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { background-color: #3067c0; border: 1px solid rgba(48, 103, 192, 0.6); } QScrollArea { background-color: #19191b; border: none; } QWidget#PropertyContainer { background-color: #19191b; } QLabel { color: #ebebeb; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; } /* 现代化的输入控件样式 */ QLineEdit { background-color: rgba(89, 100, 113, 0.15); color: #ebebeb; border: 1px solid rgba(76, 92, 110, 0.4); border-radius: 4px; padding: 6px 8px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; min-height: 16px; } QLineEdit:focus { border: 1px solid #4d74bd; background-color: rgba(77, 116, 189, 0.1); } QLineEdit:hover { border: 1px solid rgba(77, 116, 189, 0.6); background-color: rgba(89, 100, 113, 0.2); } QDoubleSpinBox, QSpinBox { background-color: rgba(89, 100, 113, 0.15); color: #ebebeb; border: 1px solid rgba(76, 92, 110, 0.4); border-radius: 4px; padding: 6px 8px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; min-height: 16px; } QDoubleSpinBox:focus, QSpinBox:focus { border: 1px solid #4d74bd; background-color: rgba(77, 116, 189, 0.1); } QDoubleSpinBox:hover, QSpinBox:hover { border: 1px solid rgba(77, 116, 189, 0.6); background-color: rgba(89, 100, 113, 0.2); } QDoubleSpinBox::up-button, QSpinBox::up-button { background-color: rgba(77, 116, 189, 0.2); border: none; border-radius: 2px; width: 16px; subcontrol-origin: border; subcontrol-position: top right; } QDoubleSpinBox::down-button, QSpinBox::down-button { background-color: rgba(77, 116, 189, 0.2); border: none; border-radius: 2px; width: 16px; subcontrol-origin: border; subcontrol-position: bottom right; } QDoubleSpinBox::up-button:hover, QSpinBox::up-button:hover, QDoubleSpinBox::down-button:hover, QSpinBox::down-button:hover { background-color: rgba(77, 116, 189, 0.4); } QDoubleSpinBox::up-arrow, QSpinBox::up-arrow { image: url(icons/up_arrows.png); width: 12px; height: 12px; } QDoubleSpinBox::down-arrow, QSpinBox::down-arrow { image: url(icons/down_arrows.png); width: 12px; height: 12px; } QDoubleSpinBox::up-arrow:hover, QSpinBox::up-arrow:hover { image: url(icons/up_arrows.png); } QDoubleSpinBox::down-arrow:hover, QSpinBox::down-arrow:hover { image: url(icons/down_arrows.png); } QComboBox { background-color: rgba(89, 100, 113, 0.15); color: #ebebeb; border: 1px solid rgba(76, 92, 110, 0.4); border-radius: 4px; padding: 6px 8px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; min-height: 16px; } QComboBox:focus { border: 1px solid #4d74bd; background-color: rgba(77, 116, 189, 0.1); } QComboBox:hover { border: 1px solid rgba(77, 116, 189, 0.6); background-color: rgba(89, 100, 113, 0.2); } QComboBox::drop-down { border: none; border-left: 1px solid rgba(76, 92, 110, 0.4); background-color: rgba(77, 116, 189, 0.2); border-radius: 0 4px 4px 0; width: 20px; } QComboBox::drop-down:hover { background-color: rgba(77, 116, 189, 0.4); } QComboBox::down-arrow { image: url(icons/down_arrows.png); width: 12px; height: 12px; } QComboBox QAbstractItemView { background-color: #2a2a2e; color: #ebebeb; selection-background-color: rgba(77, 116, 189, 0.4); border: 1px solid rgba(76, 92, 110, 0.6); border-radius: 4px; padding: 4px; } QPushButton { background-color: rgba(89, 100, 113, 0.4); color: rgba(255, 255, 255, 0.8); border: 1px solid rgba(76, 92, 110, 0.4); border-radius: 4px; padding: 8px 12px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 400; min-height: 16px; } QPushButton:hover { background-color: rgba(89, 100, 113, 0.6); border: 1px solid rgba(77, 116, 189, 0.6); color: #ffffff; } QPushButton:pressed, QPushButton:checked { background-color: rgba(77, 116, 189, 0.8); border: 1px solid #4d74bd; color: #ffffff; } QPushButton:disabled { background-color: rgba(89, 100, 113, 0.2); color: rgba(235, 235, 235, 0.4); border: 1px solid rgba(76, 92, 110, 0.2); } QCheckBox { color: #ebebeb; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; spacing: 8px; } QCheckBox::indicator { width: 16px; height: 16px; border: 1px solid rgba(76, 92, 110, 0.6); border-radius: 3px; background-color: rgba(89, 100, 113, 0.15); } QCheckBox::indicator:hover { border: 1px solid rgba(77, 116, 189, 0.6); background-color: rgba(89, 100, 113, 0.2); } QCheckBox::indicator:checked { background-color: #4d74bd; border: 1px solid #4d74bd; image: none; } QCheckBox::indicator:checked:hover { background-color: rgba(77, 116, 189, 1.0); } QGroupBox { /* 减少上边距,使相邻组更紧凑 */ margin-top: 0px; /* 移除边框 */ border: none; /* 设置内边距,为标题和内容留出空间,左右内边距与主标题对齐 */ padding-top: 20px; /* 为标题留出足够空间 */ padding-left: 0px; /* Align subtitle with dock title baseline */ padding-right: 0px; /* 右内边距与主标题分隔线对齐 */ /* 减少下边距,使相邻组更紧凑 */ background-color: transparent; margin-bottom: 6px; /* 使用边框创建样式线,与主标题分隔线保持相同的8px左右间距 */ border-top: 1px solid rgba(77, 116, 189, 0.4); margin-left: 0px; margin-right: 8px; } QGroupBox::title { /* 将标题定位到控件内部,在样条线下方 */ subcontrol-origin: padding; subcontrol-position: top left; /* 标题样式,左边距为0因为已经通过padding-left处理了对齐 */ padding: 8px 3px 8px 0px; /* 左边距设为0,与内容对齐 */ margin-top: 0px; /* 标题紧贴内边距顶部 */ /* 移除标题上的边框 */ border: none; /* 字体和颜色样式 */ color: rgba(255, 255, 255, 0.8); font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 12px; font-weight: 500; } QGroupBox[groupRole="first"] { padding-top: 16px; margin-top: 0px; border-top: none; /* 明确移除顶部边框 */ } QGroupBox[groupRole="first"]::title { padding: 6px 3px 8px 0px; } """) # 创建属性面板的主容器和布局 self.propertyContainer = QWidget() self.propertyContainer.setObjectName("PropertyContainer") try: self.propertyLayout = QVBoxLayout(self.propertyContainer) # Keep subtitles aligned with the dock title left padding self.propertyLayout.setContentsMargins(10, 0, 10, 12) self.propertyLayout.setSpacing(12) print(f"✓ 属性布局创建完成: {self.propertyLayout}") # 添加初始提示信息 tipLabel = QLabel("选择对象以查看属性") tipLabel.setStyleSheet("color: gray; padding: 10px 0px;") # 使用灰色字体 self.propertyLayout.addWidget(tipLabel) print("✓ 提示标签添加完成") # 创建滚动区域并设置属性 self.scrollArea = QScrollArea() self.scrollArea.setWidgetResizable(True) self.scrollArea.setWidget(self.propertyContainer) print("✓ 滚动区域设置完成") # 创建包装容器,添加标题下方的分隔线 rightDockContainer = QWidget() rightDockContainer.setStyleSheet("QWidget { background-color: #19191b; }") rightDockLayout = QVBoxLayout(rightDockContainer) rightDockLayout.setContentsMargins(0, 0, 0, 0) rightDockLayout.setSpacing(0) # 添加带左右间距的分隔线 rightSeparator = QFrame() rightSeparator.setFrameShape(QFrame.HLine) rightSeparator.setFrameShadow(QFrame.Plain) rightSeparator.setStyleSheet(""" QFrame { background-color: rgba(77, 116, 189, 0.4); max-height: 1px; margin-left: 8px; margin-right: 8px; border: none; } """) rightDockLayout.addWidget(rightSeparator) rightDockLayout.addWidget(self.scrollArea) # 设置包装容器为停靠窗口的主部件 self.rightDock.setWidget(rightDockContainer) 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: #19191b; color: #ffffff; border: none; } QDockWidget::title { background-color: #19191b; padding: 8px 10px; border-bottom: none; text-align: left; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 14px; font-weight: 500; } QDockWidget::close-button { background-color: rgba(89, 100, 113, 0.3); border: 1px solid rgba(184, 211, 241, 0.2); icon-size: 10px; border-radius: 2px; width: 16px; height: 16px; right: 6px; top: 6px; } QDockWidget::float-button { background-color: rgba(89, 100, 113, 0.3); border: 1px solid rgba(184, 211, 241, 0.2); icon-size: 10px; border-radius: 2px; width: 16px; height: 16px; right: 26px; top: 6px; } QDockWidget::close-button:hover, QDockWidget::float-button:hover { background-color: rgba(89, 100, 113, 0.5); border: 1px solid rgba(184, 211, 241, 0.3); } QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { background-color: #3067c0; border: 1px solid rgba(48, 103, 192, 0.6); } QScrollArea { background-color: #19191b; border: none; } QWidget#ScriptContainer { background-color: #19191b; } QLabel { color: #ebebeb; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; } QGroupBox { /* 减少上边距,使相邻组更紧凑 */ margin-top: 0px; /* 移除边框 */ border: none; /* 设置内边距,为标题和内容留出空间,同时与主标题左侧对齐 */ padding-top: 20px; /* 为标题留出足够空间 */ padding-left: 0px; padding-right: 8px; /* 右内边距与主标题分隔线对齐 */ /* 减少下边距,使相邻组更紧凑 */ background-color: transparent; margin-bottom: 6px; /* 使用边框创建样式线,与主标题分隔线保持相同的8px右侧间距 */ border-top: 1px solid rgba(77, 116, 189, 0.4); margin-left: 0px; margin-right: 8px; } QGroupBox::title { /* 将标题定位到控件内部,在样条线下方 */ subcontrol-origin: padding; subcontrol-position: top left; /* 标题样式,左边距为0因为已经通过padding-left处理了对齐 */ padding: 8px 3px 8px 0px; /* 左边距设为0,与内容对齐 */ margin-top: 0px; /* 标题紧贴内边距顶部 */ /* 移除标题上的边框 */ border: none; /* 字体和颜色样式 */ color: rgba(255, 255, 255, 0.8); font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 12px; font-weight: 500; } QGroupBox[groupRole="first"] { padding-top: 16px; margin-top: 0px; border-top: none; /* 明确移除顶部边框 */ } QGroupBox[groupRole="first"]::title { padding: 6px 3px 8px 0px; } """) # 创建脚本面板的主容器和布局(与属性面板相同结构) self.scriptContainer = QWidget() # 设置脚本容器的背景色 self.scriptContainer.setStyleSheet(""" QWidget#ScriptContainer { background-color: #19191b; } """) self.scriptContainer.setObjectName("ScriptContainer") self.scriptLayout = QVBoxLayout(self.scriptContainer) # Match the dock title horizontal padding for consistent alignment self.scriptLayout.setContentsMargins(10, 0, 10, 12) self.scriptLayout.setSpacing(12) # 创建滚动区域并设置属性 self.scriptScrollArea = QScrollArea() self.scriptScrollArea.setWidgetResizable(True) self.scriptScrollArea.setWidget(self.scriptContainer) # 创建包装容器,添加标题下方的分隔线 scriptDockContainer = QWidget() scriptDockContainer.setStyleSheet("QWidget { background-color: #19191b; }") scriptDockLayout = QVBoxLayout(scriptDockContainer) scriptDockLayout.setContentsMargins(0, 0, 0, 0) scriptDockLayout.setSpacing(0) # 添加带左右间距的分隔线 scriptSeparator = QFrame() scriptSeparator.setFrameShape(QFrame.HLine) scriptSeparator.setFrameShadow(QFrame.Plain) scriptSeparator.setStyleSheet(""" QFrame { background-color: rgba(77, 116, 189, 0.4); max-height: 1px; margin-left: 8px; margin-right: 8px; border: none; } """) scriptDockLayout.addWidget(scriptSeparator) scriptDockLayout.addWidget(self.scriptScrollArea) # 设置包装容器为停靠窗口的主部件 self.scriptDock.setWidget(scriptDockContainer) self.scriptDock.setMinimumWidth(300) # 设置脚本面板内容 - 这里添加调用 self.setupScriptPanel(self.scriptLayout) self.addDockWidget(Qt.RightDockWidgetArea, self.scriptDock) # 创建底部停靠窗口(资源窗口) self.bottomDock = QDockWidget("资源", self) self.bottomDock.setStyleSheet(""" QDockWidget { background-color: #19191b; color: #ffffff; border: none; } QDockWidget::title { background-color: #19191b; padding: 8px 10px; border-bottom: none; text-align: left; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 14px; font-weight: 500; } QDockWidget::close-button { background-color: rgba(89, 100, 113, 0.3); border: 1px solid rgba(184, 211, 241, 0.2); icon-size: 10px; border-radius: 2px; width: 16px; height: 16px; right: 6px; top: 6px; } QDockWidget::float-button { background-color: rgba(89, 100, 113, 0.3); border: 1px solid rgba(184, 211, 241, 0.2); icon-size: 10px; border-radius: 2px; width: 16px; height: 16px; right: 26px; top: 6px; } QDockWidget::close-button:hover, QDockWidget::float-button:hover { background-color: rgba(89, 100, 113, 0.5); border: 1px solid rgba(184, 211, 241, 0.3); } QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { background-color: #3067c0; border: 1px solid rgba(48, 103, 192, 0.6); } """) self.fileView = CustomAssetsTreeWidget(self.world) # 为资源树添加样式 self.fileView.setStyleSheet(""" QTreeWidget { background-color: #19191b; color: #ebebeb; border: none; alternate-background-color: #19191b; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 400; outline: none; } QTreeWidget::item { padding: 3px 0px; border: none; } QTreeWidget::item:hover { background-color: rgba(89, 100, 113, 0.3); } QTreeWidget::item:selected { background-color: rgba(48, 103, 192, 0.4); color: #ffffff; } QTreeWidget::branch { background-color: #19191b; } QTreeWidget::branch:has-children:!has-siblings:closed, QTreeWidget::branch:closed:has-children:has-siblings { image: url(icons/solid_right_arrows.png); width: 12px; height: 12px; } QTreeWidget::branch:open:has-children:!has-siblings, QTreeWidget::branch:open:has-children:has-siblings { image: url(icons/solid_down_arrows.png); width: 12px; height: 12px; } QHeaderView::section { background-color: #19191b; color: #ebebeb; border: none; border-bottom: 1px solid #4c5c6e; padding: 6px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 12px; font-weight: 500; } QGroupBox[groupRole="first"] { border-top: none; margin-top: 0px; padding-top: 8px; } QGroupBox[groupRole="first"]::title { padding: 8px 5px 8px 0px; } """) # 创建包装容器,添加标题下方的分隔线 bottomDockContainer = QWidget() bottomDockContainer.setStyleSheet("QWidget { background-color: #19191b; }") bottomDockLayout = QVBoxLayout(bottomDockContainer) bottomDockLayout.setContentsMargins(0, 0, 0, 0) bottomDockLayout.setSpacing(0) # 添加带左右间距的分隔线 bottomSeparator = QFrame() bottomSeparator.setFrameShape(QFrame.HLine) bottomSeparator.setFrameShadow(QFrame.Plain) bottomSeparator.setStyleSheet(""" QFrame { background-color: rgba(77, 116, 189, 0.4); max-height: 1px; margin-left: 8px; margin-right: 8px; border: none; } """) bottomDockLayout.addWidget(bottomSeparator) # Wrap the resource view to add consistent left padding resourceContentWrapper = QWidget() resourceContentLayout = QVBoxLayout(resourceContentWrapper) resourceContentLayout.setContentsMargins(8, 0, 0, 0) resourceContentLayout.setSpacing(0) resourceContentLayout.addWidget(self.fileView) bottomDockLayout.addWidget(resourceContentWrapper) self.bottomDock.setWidget(bottomDockContainer) self.addDockWidget(Qt.BottomDockWidgetArea, self.bottomDock) # 创建底部停靠控制台 self.consoleDock = QDockWidget("控制台", self) self.consoleDock.setStyleSheet(""" QDockWidget { background-color: #19191b; color: #ffffff; border: none; } QDockWidget::title { background-color: #19191b; padding: 8px 10px; border-bottom: none; text-align: left; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 14px; font-weight: 500; } QDockWidget::close-button { background-color: rgba(89, 100, 113, 0.3); border: 1px solid rgba(184, 211, 241, 0.2); icon-size: 10px; border-radius: 2px; width: 16px; height: 16px; right: 6px; top: 6px; } QDockWidget::float-button { background-color: rgba(89, 100, 113, 0.3); border: 1px solid rgba(184, 211, 241, 0.2); icon-size: 10px; border-radius: 2px; width: 16px; height: 16px; right: 26px; top: 6px; } QDockWidget::close-button:hover, QDockWidget::float-button:hover { background-color: rgba(89, 100, 113, 0.5); border: 1px solid rgba(184, 211, 241, 0.3); } QDockWidget::close-button:pressed, QDockWidget::float-button:pressed { background-color: #3067c0; border: 1px solid rgba(48, 103, 192, 0.6); } """) self.consoleView = CustomConsoleDockWidget(self.world) # 为控制台添加样式 self.consoleView.setStyleSheet(""" QTextEdit { background-color: #19191b; color: #ebebeb; border: none; font-family: 'Consolas', 'Monaco', 'Microsoft YaHei', monospace; font-size: 10px; font-weight: 300; } """) # 创建包装容器,添加标题下方的分隔线 consoleDockContainer = QWidget() consoleDockContainer.setStyleSheet("QWidget { background-color: #19191b; }") consoleDockLayout = QVBoxLayout(consoleDockContainer) consoleDockLayout.setContentsMargins(0, 0, 0, 0) consoleDockLayout.setSpacing(0) # 添加带左右间距的分隔线 consoleSeparator = QFrame() consoleSeparator.setFrameShape(QFrame.HLine) consoleSeparator.setFrameShadow(QFrame.Plain) consoleSeparator.setStyleSheet(""" QFrame { background-color: rgba(77, 116, 189, 0.4); max-height: 1px; margin-left: 8px; margin-right: 8px; border: none; } """) consoleDockLayout.addWidget(consoleSeparator) consoleDockLayout.addWidget(self.consoleView) self.consoleDock.setWidget(consoleDockContainer) self.addDockWidget(Qt.BottomDockWidgetArea, self.consoleDock) # 配置Dock区域的角落归属 # 使右侧和下侧的Dock面板可以占据右下角区域 # setCorner(角落位置, 归属的Dock区域) self.setCorner(Qt.TopRightCorner, Qt.RightDockWidgetArea) self.setCorner(Qt.BottomRightCorner, Qt.RightDockWidgetArea) # self.setCorner(Qt.TopLeftCorner, Qt.LeftDockWidgetArea) # self.setCorner(Qt.BottomLeftCorner, Qt.LeftDockWidgetArea) # 设置Dock标签栏位置为上方(North) self.setTabPosition(Qt.RightDockWidgetArea, QTabWidget.North) self.setTabPosition(Qt.LeftDockWidgetArea, QTabWidget.North) self.setTabPosition(Qt.TopDockWidgetArea, QTabWidget.North) self.setTabPosition(Qt.BottomDockWidgetArea, QTabWidget.North) # 将右侧的属性面板和脚本管理面板合并为标签页 # self.tabifyDockWidget(self.rightDock, self.scriptDock) # 设定默认显示的标签(属性面板在前) self.rightDock.raise_() # 确保其他面板也正常显示 self.bottomDock.raise_() self.consoleDock.raise_() self.leftDock.raise_() # ========================================================================= # ↓↓↓ 为停靠窗口的标签栏(QTabBar)设置统一样式(根据Figma设计)↓↓↓ # ========================================================================= # 这段样式会应用到主窗口内的所有 QTabBar,特别是停靠区域的标签栏。 tab_bar_style = """ /* QTabBar 的整体样式 */ QTabBar { qproperty-drawBase: 0; qproperty-expanding: 0; background-color: #19191b; border: none; } /* 标签的基础样式 - 未选中状态 */ QTabBar::tab { background-color: #394560; color: rgba(255, 255, 255, 0.7); border: none; border-radius: 2px; padding: 7px 16px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-weight: 500; font-size: 12px; margin-right: 4px; min-height: 20px; max-height: 20px; min-width: 80px; /* 增加一个最小宽度,根据您的标签文字长度调整 */ } /* 鼠标悬停在标签上时的样式 */ QTabBar::tab:hover { background-color: #4a5568; } /* 当前选中的标签页的样式 */ QTabBar::tab:selected { background-color: #3067c0; color: #ffffff; font-weight: 500; } /* 未选中的标签 */ QTabBar::tab:!selected { margin-top: 0px; } /* 标签栏的对齐方式 */ QTabBar::tab-bar { alignment: left; } /* 标签栏的关闭按钮(如果有) */ QTabBar::close-button { image: none; subcontrol-position: right; } /* 移除标签栏的滚动按钮样式冲突 */ QTabBar::scroller { width: 0px; } """ # 获取主窗口现有的样式表,并附加我们新的样式规则 # 这样可以避免覆盖掉其他可能存在的全局样式 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("脚本系统状态") statusGroup.setProperty("groupRole", "first") statusLayout = QVBoxLayout() # 脚本系统状态行 scriptSystemLayout = QHBoxLayout() scriptSystemLabel = QLabel("脚本系统:") scriptSystemLabel.setStyleSheet(""" QLabel { color: #ebebeb; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; } """) scriptSystemLayout.addWidget(scriptSystemLabel) self.scriptStatusLabel = QLabel("已启动") self.scriptStatusLabel.setStyleSheet(""" QLabel { background-color: rgba(45, 255, 196, 0.17); border: 1px solid #2dffc4; color: #2dffc4; border-radius: 2px; padding: 2px 8px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; } """) scriptSystemLayout.addWidget(self.scriptStatusLabel) scriptSystemLayout.addStretch() statusLayout.addLayout(scriptSystemLayout) # 热重载状态行 hotReloadLayout = QHBoxLayout() hotReloadLabel = QLabel("热重载:") hotReloadLabel.setStyleSheet(""" QLabel { color: #ebebeb; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; } """) hotReloadLayout.addWidget(hotReloadLabel) self.hotReloadLabel = QLabel("已启用") self.hotReloadLabel.setStyleSheet(""" QLabel { background-color: rgba(45, 136, 255, 0.17); border: 1px solid #289eff; color: #289eff; border-radius: 2px; padding: 2px 8px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; } """) hotReloadLayout.addWidget(self.hotReloadLabel) hotReloadLayout.addStretch() statusLayout.addLayout(hotReloadLayout) statusGroup.setLayout(statusLayout) layout.addWidget(statusGroup) # 脚本创建组 createGroup = QGroupBox("创建脚本") createLayout = QVBoxLayout() # 脚本名称输入 nameLayout = QHBoxLayout() nameLabel = QLabel("脚本名称:") nameLabel.setStyleSheet(""" QLabel { color: #ebebeb; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; min-width: 60px; } """) nameLayout.addWidget(nameLabel) self.scriptNameEdit = QLineEdit() self.scriptNameEdit.setPlaceholderText("输入脚本名称...") self.scriptNameEdit.setStyleSheet(""" QLineEdit { background-color: rgba(89, 100, 113, 0.2); color: #ebebeb; border: 1px solid rgba(76, 92, 110, 0.6); border-radius: 2px; padding: 4px 8px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; min-height: 18px; } QLineEdit::placeholder { color: rgba(235, 235, 235, 0.7); } QLineEdit:focus { border: 1px solid #289eff; } """) nameLayout.addWidget(self.scriptNameEdit) createLayout.addLayout(nameLayout) # 模板选择 templateLayout = QHBoxLayout() templateLabel = QLabel("模板:") templateLabel.setStyleSheet(""" QLabel { color: #ebebeb; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; min-width: 60px; } """) templateLayout.addWidget(templateLabel) self.templateCombo = QComboBox() self.templateCombo.addItems(["basic", "movement", "animation"]) self.templateCombo.setStyleSheet(""" QComboBox { background-color: rgba(89, 100, 113, 0.2); color: #ebebeb; border: 1px solid rgba(76, 92, 110, 0.6); border-radius: 2px; padding: 4px 8px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; min-height: 18px; } QComboBox::drop-down { border: none; border-left: 1px solid rgba(76, 92, 110, 0.6); background-color: rgba(89, 100, 113, 0.3); width: 16px; } QComboBox::down-arrow { image: url(icons/down_arrows.png); width: 10px; height: 10px; } QComboBox QAbstractItemView { background-color: #596471; color: #ebebeb; selection-background-color: rgba(48, 103, 192, 0.4); border: 1px solid rgba(76, 92, 110, 0.6); } """) templateLayout.addWidget(self.templateCombo) createLayout.addLayout(templateLayout) # 创建按钮 self.createScriptBtn = QPushButton("创建脚本") self.createScriptBtn.clicked.connect(self.onCreateScript) self.createScriptBtn.setStyleSheet(""" QPushButton { background-color: rgba(89, 98, 118, 0.4); color: #ebebeb; border: none; padding: 6px 12px; border-radius: 2px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-weight: 300; font-size: 10px; letter-spacing: 0.5px; min-height: 18px; } QPushButton:hover { background-color: #3067c0; color: #ffffff; } QPushButton:pressed { background-color: #2556a0; } QPushButton:disabled { background-color: #394560; color: rgba(235, 235, 235, 0.5); } """) createLayout.addWidget(self.createScriptBtn) createGroup.setLayout(createLayout) layout.addWidget(createGroup) # 可用脚本组 scriptsGroup = QGroupBox("可用脚本") scriptsLayout = QVBoxLayout() # 脚本列表 self.scriptsList = QListWidget() self.scriptsList.setStyleSheet(""" QListWidget { background-color: rgba(89, 100, 113, 0.15); color: #ebebeb; border: 1px solid rgba(76, 92, 110, 0.6); border-radius: 2px; alternate-background-color: rgba(89, 100, 113, 0.15); selection-background-color: rgba(48, 103, 192, 0.4); selection-color: #ffffff; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; outline: none; min-height: 120px; max-height: 120px; } QListWidget::item { padding: 6px 8px; border: none; border-bottom: 1px solid rgba(76, 92, 110, 0.3); } QListWidget::item:last { border-bottom: none; } QListWidget::item:hover { background-color: rgba(89, 100, 113, 0.3); } QListWidget::item:selected { background-color: rgba(48, 103, 192, 0.4); color: #ffffff; } """) self.scriptsList.itemDoubleClicked.connect(self.onScriptDoubleClick) scriptsLayout.addWidget(self.scriptsList) # 脚本操作按钮 scriptButtonsLayout = QHBoxLayout() self.loadScriptBtn = QPushButton("加载脚本") self.loadScriptBtn.clicked.connect(self.onLoadScript) self.loadScriptBtn.setStyleSheet(""" QPushButton { background-color: rgba(89, 98, 118, 0.4); color: #ebebeb; border: none; padding: 6px 12px; border-radius: 2px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-weight: 300; font-size: 10px; letter-spacing: 0.5px; min-height: 18px; } QPushButton:hover { background-color: #3067c0; color: #ffffff; } QPushButton:pressed { background-color: #2556a0; } """) scriptButtonsLayout.addWidget(self.loadScriptBtn) self.reloadAllBtn = QPushButton("重载全部") self.reloadAllBtn.clicked.connect(self.onReloadAllScripts) self.reloadAllBtn.setStyleSheet(""" QPushButton { background-color: rgba(89, 98, 118, 0.4); color: #ebebeb; border: none; padding: 6px 12px; border-radius: 2px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-weight: 300; font-size: 10px; letter-spacing: 0.5px; min-height: 18px; } QPushButton:hover { background-color: #3067c0; color: #ffffff; } QPushButton:pressed { background-color: #2556a0; } """) scriptButtonsLayout.addWidget(self.reloadAllBtn) scriptsLayout.addLayout(scriptButtonsLayout) scriptsGroup.setLayout(scriptsLayout) layout.addWidget(scriptsGroup) # 脚本挂载组 mountGroup = QGroupBox("脚本挂载") mountLayout = QVBoxLayout() # 当前选中对象显示 self.selectedObjectLabel = QLabel("未选择对象") self.selectedObjectLabel.setStyleSheet(""" QLabel { color: #2dffc4; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; padding: 2px 0px; } """) mountLayout.addWidget(self.selectedObjectLabel) # 脚本选择和挂载 mountControlLayout = QHBoxLayout() self.mountScriptCombo = QComboBox() self.mountScriptCombo.setEnabled(False) self.mountScriptCombo.setStyleSheet(""" QComboBox { background-color: rgba(89, 100, 113, 0.2); color: #ebebeb; border: 1px solid rgba(76, 92, 110, 0.6); border-radius: 2px; padding: 4px 8px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; min-height: 18px; } QComboBox:disabled { background-color: rgba(89, 100, 113, 0.1); color: rgba(235, 235, 235, 0.5); } QComboBox::drop-down { border: none; border-left: 1px solid rgba(76, 92, 110, 0.6); background-color: rgba(89, 100, 113, 0.3); width: 16px; } QComboBox::down-arrow { image: url(icons/down_arrows.png); width: 10px; height: 10px; } QComboBox QAbstractItemView { background-color: #596471; color: #ebebeb; selection-background-color: rgba(48, 103, 192, 0.4); border: 1px solid rgba(76, 92, 110, 0.6); } """) mountControlLayout.addWidget(self.mountScriptCombo) self.mountBtn = QPushButton("挂载") self.mountBtn.setEnabled(False) self.mountBtn.clicked.connect(self.onMountScript) self.mountBtn.setStyleSheet(""" QPushButton { background-color: rgba(89, 98, 118, 0.4); color: #ebebeb; border: none; padding: 6px 12px; border-radius: 2px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-weight: 300; font-size: 10px; letter-spacing: 0.5px; min-height: 18px; } QPushButton:hover { background-color: #3067c0; color: #ffffff; } QPushButton:pressed { background-color: #2556a0; } QPushButton:disabled { background-color: #394560; color: rgba(235, 235, 235, 0.5); } """) mountControlLayout.addWidget(self.mountBtn) mountLayout.addLayout(mountControlLayout) # 已挂载脚本列表 self.mountedScriptsList = QListWidget() self.mountedScriptsList.setStyleSheet(""" QListWidget { background-color: rgba(89, 100, 113, 0.15); color: #ebebeb; border: 1px solid rgba(76, 92, 110, 0.6); border-radius: 2px; alternate-background-color: rgba(89, 100, 113, 0.15); selection-background-color: rgba(48, 103, 192, 0.4); selection-color: #ffffff; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; outline: none; min-height: 80px; max-height: 80px; } QListWidget::item { padding: 4px 8px; border: none; border-bottom: 1px solid rgba(76, 92, 110, 0.3); } QListWidget::item:last { border-bottom: none; } QListWidget::item:hover { background-color: rgba(89, 100, 113, 0.3); } QListWidget::item:selected { background-color: rgba(48, 103, 192, 0.4); color: #ffffff; } """) self.mountedScriptsList.setMaximumHeight(100) mountedLabel = QLabel("已挂载脚本:") mountedLabel.setStyleSheet(""" QLabel { color: #ebebeb; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; padding: 2px 0px; } """) mountLayout.addWidget(mountedLabel) mountLayout.addWidget(self.mountedScriptsList) # 卸载按钮 self.unmountBtn = QPushButton("卸载选中脚本") self.unmountBtn.clicked.connect(self.onUnmountScript) self.unmountBtn.setStyleSheet(""" QPushButton { background-color: rgba(89, 98, 118, 0.4); color: #ebebeb; border: none; padding: 6px 12px; border-radius: 2px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-weight: 300; font-size: 10px; letter-spacing: 0.5px; min-height: 18px; } QPushButton:hover { background-color: #3067c0; color: #ffffff; } QPushButton:pressed { background-color: #2556a0; } """) 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.saveAction.setShortcut(QKeySequence.Save) # 连接工具事件 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.undoAction.triggered.connect(self.onUndo) self.redoAction.triggered.connect(self.onRedo) self.cutAction.triggered.connect(self.onCut) self.copyAction.triggered.connect(self.onCopy) self.pasteAction.triggered.connect(self.onPaste) self.undoAction.setShortcut(QKeySequence.Undo) self.redoAction.setShortcut(QKeySequence.Redo) self.cutAction.setShortcut(QKeySequence.Cut) self.copyAction.setShortcut(QKeySequence.Copy) self.pasteAction.setShortcut(QKeySequence.Paste) #连接视图菜单事件 #self.setupViewMenuActions() # 连接工具切换信号 #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) # 连接VR菜单事件 self.enterVRAction.triggered.connect(self.onEnterVR) self.exitVRAction.triggered.connect(self.onExitVR) self.vrStatusAction.triggered.connect(self.onShowVRStatus) self.vrSettingsAction.triggered.connect(self.onShowVRSettings) # 连接VR调试菜单事件 self.vrDebugToggleAction.triggered.connect(self.onToggleVRDebug) self.vrShowPerformanceAction.triggered.connect(self.onShowVRPerformance) self.vrDebugBriefAction.triggered.connect(lambda: self.onSetVRDebugMode('brief')) self.vrDebugDetailedAction.triggered.connect(lambda: self.onSetVRDebugMode('detailed')) self.vrPerformanceMonitorAction.triggered.connect(self.onToggleVRPerformanceMonitor) self.vrGpuTimingAction.triggered.connect(self.onToggleVRGpuTiming) self.vrPipelineMonitorAction.triggered.connect(self.onToggleVRPipelineMonitor) self.vrPoseRenderCallbackAction.triggered.connect(lambda: self.onSetVRPoseStrategy('render_callback')) self.vrPoseUpdateTaskAction.triggered.connect(lambda: self.onSetVRPoseStrategy('update_task')) self.vrTestPipelineAction.triggered.connect(self.onTestVRPipeline) self.vrTestModeAction.triggered.connect(self.onToggleVRTestMode) # 连接VR测试模式调试菜单事件 self.vrTestSubmitTextureAction.triggered.connect(self.onToggleVRTestSubmitTexture) self.vrTestWaitPosesAction.triggered.connect(self.onToggleVRTestWaitPoses) self.vrTestStep1Action.triggered.connect(lambda: self.onSetVRTestStep(1)) self.vrTestStep2Action.triggered.connect(lambda: self.onSetVRTestStep(2)) self.vrTestStep3Action.triggered.connect(lambda: self.onSetVRTestStep(3)) self.vrTestResetAction.triggered.connect(lambda: self.onSetVRTestStep(0)) self.vrDebugSettingsAction.triggered.connect(self.onShowVRDebugSettings) def onCopy(self): """复制操作""" try: selected_item = self.treeWidget.currentItem() if not selected_item: UniversalMessageDialog.show_warning( self, "提示", "请先选择要复制的节点", show_cancel=False, confirm_text="确认" ) return # 获取选中的节点 selected_node = getattr(selected_item, 'node_path', None) if not selected_node: selected_node = getattr(selected_item, 'node', None) if not selected_node and hasattr(self.world, 'selection'): selected_node = getattr(self.world.selection, 'selectedNode', None) if not selected_node or selected_node.isEmpty(): UniversalMessageDialog.show_warning( self, "错误", "无法获取选中节点", show_cancel=False, confirm_text="确认" ) return # 检查是否是根节点 if selected_node.getName() == "render": UniversalMessageDialog.show_warning( self, "错误", "不能复制根节点", show_cancel=False, confirm_text="确认" ) return # 序列化节点数据 node_data = self.world.scene_manager.serializeNodeForCopy(selected_node) if not node_data: UniversalMessageDialog.show_warning( self, "错误", "无法序列化选中节点", show_cancel=False, confirm_text="确认" ) return # 存储到剪切板 self.clipboard = [node_data] self.clipboard_mode = "copy" UniversalMessageDialog.show_success( self, "成功", "节点已复制到剪切板", show_cancel=False, confirm_text="确认" ) except Exception as e: UniversalMessageDialog.show_error( self, "错误", f"复制操作失败: {str(e)}", show_cancel=False, confirm_text="确认" ) def onCut(self): """剪切操作""" try: selected_item = self.treeWidget.currentItem() if not selected_item: UniversalMessageDialog.show_warning( self, "提示", "请先选择要剪切的节点", show_cancel=False, confirm_text="确认" ) return # 获取选中的节点 selected_node = getattr(selected_item, 'node_path', None) if not selected_node: selected_node = getattr(selected_item, 'node', None) if not selected_node and hasattr(self.world, 'selection'): selected_node = getattr(self.world.selection, 'selectedNode', None) if not selected_node or selected_node.isEmpty(): UniversalMessageDialog.show_warning( self, "错误", "无法获取选中节点", show_cancel=False, confirm_text="确认" ) return # 检查是否是根节点或特殊节点 if selected_node.getName() in ["render", "camera", "ambientLight", "directionalLight"]: UniversalMessageDialog.show_warning( self, "错误", "不能剪切根节点或系统节点", show_cancel=False, confirm_text="确认" ) return # 序列化节点数据 node_data = self.world.scene_manager.serializeNodeForCopy(selected_node) if not node_data: UniversalMessageDialog.show_warning( self, "错误", "无法序列化选中节点", show_cancel=False, confirm_text="确认" ) return # 存储到剪切板 self.clipboard = [node_data] self.clipboard_mode = "cut" # 删除原节点 self.treeWidget.delete_items([selected_item]) UniversalMessageDialog.show_success( self, "成功", "节点已剪切到剪切板", show_cancel=False, confirm_text="确认" ) except Exception as e: UniversalMessageDialog.show_error( self, "错误", f"剪切操作失败: {str(e)}", show_cancel=False, confirm_text="确认" ) def onPaste(self): """粘贴操作""" try: if not self.clipboard: UniversalMessageDialog.show_warning( self, "提示", "剪切板为空", show_cancel=False, confirm_text="确认" ) return # 获取粘贴目标节点 parent_item = self.treeWidget.currentItem() parent_node = None # 如果选中了节点,将其作为父节点 if parent_item: parent_node = getattr(parent_item, 'node_path', None) if not parent_node: parent_node = getattr(parent_item, 'node', None) # 确保获取到有效的父节点 if parent_node and not parent_node.isEmpty(): print(f"将粘贴到选中的节点: {parent_node.getName()}") else: parent_node = None # 如果没有选中有效节点,默认粘贴到render节点下 if not parent_node: print("未选中有效节点,将粘贴到根节点下") # 查找render节点 for i in range(self.treeWidget.topLevelItemCount()): item = self.treeWidget.topLevelItem(i) if item.text(0) == "render": parent_item = item break # 如果找到了render节点项,获取对应的节点 if parent_item: parent_node = getattr(parent_item, 'node_path', None) if not parent_node: parent_node = getattr(parent_item, 'node', None) # 如果仍然没有找到父节点项,直接使用world.render if not parent_node: parent_node = self.world.render # 检查父节点有效性 if not parent_node or parent_node.isEmpty(): UniversalMessageDialog.show_warning( self, "错误", "无法获取有效的父节点", show_cancel=False, confirm_text="确认" ) return # 检查目标节点是否为允许的父节点类型 parent_name = parent_node.getName() if parent_name in ["camera", "ambientLight", "directionalLight"]: UniversalMessageDialog.show_warning( self, "错误", "不能粘贴到该类型节点下", show_cancel=False, confirm_text="确认" ) return # 粘贴节点 pasted_nodes = [] for node_data in self.clipboard: print(f"正在粘贴节点数据:{node_data.get('name','Unknown')}") new_node = self.world.scene_manager.recreateNodeFromData(node_data, parent_node) if new_node: pasted_nodes.append(new_node) print(f"成功粘贴节点: {new_node.getName()}") else: print(f"粘贴节点失败: {node_data.get('name', 'Unknown')}") # 如果是剪切操作,清空剪切板 if self.clipboard_mode == "cut": self.clipboard.clear() self.clipboard_mode = None UniversalMessageDialog.show_success( self, "成功", f"已粘贴 {len(pasted_nodes)} 个节点", show_cancel=False, confirm_text="确认" ) except Exception as e: UniversalMessageDialog.show_error( self, "错误", f"粘贴操作失败: {str(e)}", show_cancel=False, confirm_text="确认" ) def _serializeNode(self, node): """序列化节点数据""" try: if not node or node.isEmpty(): return None node_data = { 'name': node.getName(), 'type': type(node.node()).__name__, 'pos': (node.getX(), node.getY(), node.getZ()), 'hpr': (node.getH(), node.getP(), node.getR()), 'scale': (node.getSx(), node.getSy(), node.getSz()), 'tags': {}, 'children': [] } # 保存所有标签 try: # 使用更安全的方式获取标签 if hasattr(node, 'getTagKeys'): for tag_key in node.getTagKeys(): node_data['tags'][tag_key] = node.getTag(tag_key) except Exception as e: print(f"获取标签时出错: {e}") # 递归序列化子节点(跳过辅助节点) try: if hasattr(node, 'getChildren'): for child in node.getChildren(): # 跳过辅助节点 child_name = child.getName() if hasattr(child, 'getName') else "" if not child_name.startswith(('gizmo', 'selectionBox', 'grid')): child_data = self._serializeNode(child) if child_data: node_data['children'].append(child_data) except Exception as e: print(f"序列化子节点时出错: {e}") return node_data except Exception as e: print(f"序列化节点失败: {e}") return None def _deserializeNode(self, node_data, parent_node): """反序列化节点数据""" try: if not node_data or not parent_node or parent_node.isEmpty(): return None # 创建新节点 node_name = node_data.get('name', 'node') new_node = parent_node.attachNewNode(node_name) # 设置变换 try: pos = node_data.get('pos', (0, 0, 0)) hpr = node_data.get('hpr', (0, 0, 0)) scale = node_data.get('scale', (1, 1, 1)) new_node.setPos(*pos) new_node.setHpr(*hpr) new_node.setScale(*scale) except Exception as e: print(f"设置变换时出错: {e}") # 恢复标签 try: for tag_key, tag_value in node_data.get('tags', {}).items(): new_node.setTag(tag_key, str(tag_value)) # 确保标签值是字符串 except Exception as e: print(f"恢复标签时出错: {e}") # 递归创建子节点 try: for child_data in node_data.get('children', []): self._deserializeNode(child_data, new_node) except Exception as e: print(f"创建子节点时出错: {e}") return new_node except Exception as e: print(f"反序列化节点失败: {e}") return None def _deleteNode(self, node, tree_item): """删除节点""" try: if not node or node.isEmpty(): return # 特殊处理选中节点 if hasattr(self.world, 'selection') and self.world.selection.selectedNode == node: self.world.selection.clearSelection() # 从场景中删除节点 node.removeNode() # 从树形控件中删除项目 if tree_item: try: parent = tree_item.parent() if parent: parent.removeChild(tree_item) else: index = self.treeWidget.indexOfTopLevelItem(tree_item) if index >= 0: self.treeWidget.takeTopLevelItem(index) except Exception as e: print(f"从树形控件删除项目时出错: {e}") except Exception as e: print(f"删除节点失败: {e}") # 添加撤销/重做功能的基础实现 def onUndo(self): """撤销操作""" if hasattr(self.world,'command_manager'): if self.world.command_manager.can_undo(): success = self.world.command_manager.undo() if success: print("成功操作") else: print("撤销失败") UniversalMessageDialog.show_warning( self, "提示", "撤销操作失败", show_cancel=False, confirm_text="确认" ) else: print("没有可撤销的操作") UniversalMessageDialog.show_warning( self, "提示", "没有可撤销的操作", show_cancel=False, confirm_text="确认" ) else: print("命令管理器未初始化") UniversalMessageDialog.show_warning( self, "提示", "命令系统未初始化", show_cancel=False, confirm_text="确认" ) def onRedo(self): """重做操作""" if hasattr(self.world,'command_manager'): if self.world.command_manager.can_redo(): success = self.world.command_manager.redo() if success: print("成功重做") else: print("重做失败") UniversalMessageDialog.show_warning( self, "提示", "重做操作失败", show_cancel=False, confirm_text="确认" ) else: print("没有可重做的操作") UniversalMessageDialog.show_warning( self, "提示", "没有可重做的操作", show_cancel=False, confirm_text="确认" ) else: print("命令管理器未初始化") UniversalMessageDialog.show_warning( self, "提示", "命令系统未初始化", show_cancel=False, confirm_text="确认" ) def onCreateCesiumView(self): if hasattr(self.world,'gui_manager') and self.world.gui_manager: self.world.gui_manager.createCesiumView() else: UniversalMessageDialog.show_warning( self, "错误", "GUI管理器不可用", show_cancel=False, confirm_text="确认" ) def onToggleCesiumView(self): """切换 Cesium 视图显示状态""" if hasattr(self.world, 'gui_manager') and self.world.gui_manager: self.world.gui_manager.toggleCesiumView() else: UniversalMessageDialog.show_warning( self, "错误", "GUI 管理器不可用", show_cancel=False, confirm_text="确认" ) def onRefreshCesiumView(self): """刷新 Cesium 视图""" if hasattr(self.world, 'gui_manager') and self.world.gui_manager: self.world.gui_manager.refreshCesiumView() else: UniversalMessageDialog.show_warning( self, "错误", "GUI 管理器不可用", show_cancel=False, confirm_text="确认" ) 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: UniversalMessageDialog.show_warning( self, "错误", "GUI 管理器不可用", show_cancel=False, confirm_text="确认" ) 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: result = UniversalMessageDialog.show_info( self, "提示", "Cesium 地图视图尚未打开,是否先打开地图视图?", show_cancel=True, confirm_text="打开视图", cancel_text="取消" ) if result == QDialog.Accepted: 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: UniversalMessageDialog.show_success( self, "成功", f"模型已成功添加到地图!\n模型ID: {model_id}", show_cancel=False, confirm_text="确认" ) else: UniversalMessageDialog.show_warning( self, "失败", "添加模型失败,请检查控制台输出", show_cancel=False, confirm_text="确认" ) except Exception as e: UniversalMessageDialog.show_error( self, "错误", f"添加模型时发生错误:\n{str(e)}", show_cancel=False, confirm_text="确认" ) 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): """加载 Cesium 3D Tiles""" fields = [ { "name": "url", "label": "Tileset URL", "type": "text", "value": "https://assets.ion.cesium.com/96128/tileset.json", "placeholder": "输入 tileset.json 地址", "label_width": 110 }, { "name": "longitude", "label": "经度(°)", "type": "double", "min": -180.0, "max": 180.0, "step": 0.1, "decimals": 6, "value": 0.0, "label_width": 110 }, { "name": "latitude", "label": "纬度(°)", "type": "double", "min": -90.0, "max": 90.0, "step": 0.1, "decimals": 6, "value": 0.0, "label_width": 110 }, { "name": "height", "label": "高度(米)", "type": "double", "min": -10000.0, "max": 10000.0, "step": 1.0, "decimals": 2, "value": 0.0, "label_width": 110 } ] dialog = StyledTerrainDialog(self, "加载 Cesium 3D Tiles", fields, primary_text="加载", secondary_text="取消") if dialog.exec_() == QDialog.Accepted: url = dialog.get_value("url") if not url or not url.strip(): UniversalMessageDialog.show_warning( self, "提示", "请输入有效的 tileset URL", show_cancel=False, confirm_text="确认" ) return longitude = dialog.get_value("longitude") latitude = dialog.get_value("latitude") height = dialog.get_value("height") try: import uuid tileset_name = f"tileset_{uuid.uuid4().hex[:8]}" if hasattr(self.world, 'addCesiumTileset'): success = self.world.addCesiumTileset( tileset_name, url.strip(), (longitude, latitude, height) ) if success: UniversalMessageDialog.show_success( self, "成功", f"Cesium 3D Tiles 已加载到场景中!\n名称: {tileset_name}", show_cancel=False, confirm_text="确认" ) else: UniversalMessageDialog.show_warning( self, "失败", "加载 Cesium 3D Tiles 失败", show_cancel=False, confirm_text="确认" ) except Exception as e: UniversalMessageDialog.show_error( self, "错误", f"加载 Cesium 3D Tiles 时发生错误:\n{str(e)}", show_cancel=False, confirm_text="确认" ) 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_win(self): """打开Web浏览器面板(使用独立进程避免与Panda3D冲突)""" try: from PyQt5.QtWidgets import QDockWidget, QWidget, QVBoxLayout from PyQt5.QtCore import QProcess, Qt import subprocess import sys 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("✗ 无法获取主窗口引用") UniversalMessageDialog.show_warning( self, "错误", "无法获取主窗口引用", False, "确定" ) 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") # 创建容器 Widget container_widget = QWidget() container_layout = QVBoxLayout(container_widget) container_layout.setContentsMargins(0, 0, 0, 0) # 创建一个占位 Widget,用于嵌入外部进程窗口 embed_widget = QWidget() embed_widget.setMinimumSize(400, 300) container_layout.addWidget(embed_widget) browser_dock.setWidget(container_widget) # 添加到主窗口 print("📍 将浏览器视图添加到主窗口") main_window.addDockWidget(Qt.RightDockWidgetArea, browser_dock) # 获取嵌入窗口的 ID embed_window_id = int(embed_widget.winId()) print(f"🔑 嵌入窗口 ID: {embed_window_id}") # 启动独立进程 print("🚀 启动独立 WebEngine 进程...") #web_process_script = os.path.join(os.path.dirname(__file__), "web_browser_process.py") import tempfile temp_dir = tempfile.gettempdir() web_process_script = os.path.join(temp_dir, "web_browser_process_temp.py") with open(web_process_script, "w", encoding="utf-8") as f: f.write(WEB_BROWSER_PROCESS_CODE) # ----------------------------------------------------- # 创建 QProcess(注意:必须在 start 之前) # ----------------------------------------------------- # ----------------------------------------------------- # 创建 QProcess(注意:必须在 start 之前) # ----------------------------------------------------- process = QProcess(browser_dock) browser_dock.web_process = process # 保存进程引用 # ----------------------------------------------------- # 输出信号 # ----------------------------------------------------- def on_process_output(): output = bytes(process.readAllStandardOutput()).decode('utf-8', errors='ignore') if output.strip(): print(f"[WebProcess] {output.strip()}") def on_process_error(): error = bytes(process.readAllStandardError()).decode('utf-8', errors='ignore') if error.strip(): print(f"[WebProcess Error] {error.strip()}") def on_process_finished(exit_code, exit_status): print(f"⚠️ WebEngine 进程退出: {exit_code} {exit_status}") process.readyReadStandardOutput.connect(on_process_output) process.readyReadStandardError.connect(on_process_error) process.finished.connect(on_process_finished) # ----------------------------------------------------- # 写入临时脚本 # ----------------------------------------------------- import tempfile temp_dir = tempfile.gettempdir() web_process_script = os.path.join(temp_dir, "web_browser_process_temp.py") with open(web_process_script, "w", encoding="utf-8") as f: f.write(WEB_BROWSER_PROCESS_CODE) # ----------------------------------------------------- # 启动子进程(这里顺序不能错) # ----------------------------------------------------- python_exe = sys.executable url = "https://baidu.com" # 更可控的测试站点 print(f"🚀 启动: {python_exe} {web_process_script} {url} {embed_window_id}") process.start(python_exe, [web_process_script, url, str(embed_window_id)]) if not process.waitForStarted(3000): print("✗ 启动 WebEngine 失败") return None print("✓ WebEngine 进程已启动") # 添加到GUI元素列表以便管理 self.world.gui_elements.append(browser_dock) # 添加清理函数 def cleanup_process(): if hasattr(browser_dock, 'web_process') and browser_dock.web_process: print("🧹 清理 WebEngine 进程...") browser_dock.web_process.terminate() if not browser_dock.web_process.waitForFinished(2000): browser_dock.web_process.kill() browser_dock.destroyed.connect(cleanup_process) print("✓ 网页浏览器视图已创建并集成到项目中(独立进程模式)") return browser_dock except Exception as e: print(f"✗ 创建浏览器视图失败: {str(e)}") import traceback traceback.print_exc() UniversalMessageDialog.show_error( self, "错误", f"创建浏览器视图失败:\n{str(e)}", False, "确定" ) return None 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")) self.web_view.load(QUrl("https://www.baidu.com")) # 设置内容 browser_dock.setWidget(self.web_view) # 添加到主窗口 print("📍 将浏览器视图添加到主窗口") main_window.addDockWidget(Qt.RightDockWidgetArea, browser_dock) # 添加到GUI元素列表以便管理 self.world.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 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)}" 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 ) weather_panel.setTag("name",unique_id) # 更新面板标题 info_manager.update3DPanelContent(unique_id, title="3D北京天气") # 添加到场景树 self.addInfoPanelToTree(weather_panel, "3D天气信息面板") # 显示加载中信息 info_manager.update3DPanelContent(unique_id, content="正在获取天气数据...") info_manager.registerDataSource(unique_id, info_manager.getRealWeatherData, update_interval=5.0) print("✓ 3D示例天气信息面板已创建") except Exception as e: print(f"✗ 创建3D示例天气信息面板失败: {e}") import traceback traceback.print_exc() UniversalMessageDialog.show_error(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() UniversalMessageDialog.show_error(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() UniversalMessageDialog.show_error(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() UniversalMessageDialog.show_error(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 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 # 更新热重载标签文本和样式 if hot_reload_enabled: self.hotReloadLabel.setText("已启用") self.hotReloadLabel.setStyleSheet(""" QLabel { background-color: rgba(243, 157, 120, 0.15); border: 1px solid #f39d78 ; color: #f39d78 ; border-radius: 2px; padding: 2px 8px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; } """) else: self.hotReloadLabel.setText("已禁用") self.hotReloadLabel.setStyleSheet(""" QLabel { background-color: rgba(45, 136, 255, 0.17) ; border: 1px solid #289eff ; color: #289eff ; border-radius: 2px; padding: 2px 8px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; } """) # 更新热重载菜单状态 self.toggleHotReloadAction.setChecked(hot_reload_enabled) # 更新脚本系统状态(确保使用正确的样式) self.scriptStatusLabel.setText("已启动") self.scriptStatusLabel.setStyleSheet(""" QLabel { background-color: rgba(45, 255, 196, 0.17); border: 1px solid #2dffc4; color: #2dffc4; border-radius: 2px; padding: 2px 8px; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; } """) # 更新选中对象信息 selected_object = getattr(self.world.selection, 'selectedObject', None) if selected_object: self.selectedObjectLabel.setText(f"选中对象:{selected_object.getName()}") self.selectedObjectLabel.setStyleSheet(""" QLabel { color: #2dffc4; font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; padding: 2px 0px; } """) self.mountScriptCombo.setEnabled(True) self.mountBtn.setEnabled(True) # 更新已挂载脚本列表 self.updateMountedScriptsList(selected_object) else: self.selectedObjectLabel.setText("未选择对象") self.selectedObjectLabel.setStyleSheet(""" QLabel { color: rgba(235, 235, 235, 0.5); font-family: 'Microsoft YaHei', 'Inter', sans-serif; font-size: 10px; font-weight: 300; letter-spacing: 0.5px; padding: 2px 0px; 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: UniversalMessageDialog.show_warning( self, "警告", "请输入脚本名称!", show_cancel=False, confirm_text="确认" ) return template = self.templateCombo.currentText() try: success = self.world.createScript(script_name, template) if success: UniversalMessageDialog.show_success( self, "成功", f"脚本 '{script_name}' 创建成功!", show_cancel=False, confirm_text="确认" ) self.scriptNameEdit.clear() self.refreshScriptsList() else: UniversalMessageDialog.show_warning( self, "警告", f"脚本 '{script_name}' 创建失败!", show_cancel=False, confirm_text="确认" ) except Exception as e: UniversalMessageDialog.show_error( self, "错误", f"创建脚本时发生错误: {str(e)}", show_cancel=False, confirm_text="确认" ) 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: UniversalMessageDialog.show_success( self, "成功", f"脚本 '{script_name}' 创建成功!", show_cancel=False, confirm_text="确认" ) self.refreshScriptsList() else: UniversalMessageDialog.show_warning( self, "警告", f"脚本 '{script_name}' 创建失败!", show_cancel=False, confirm_text="确认" ) except Exception as e: UniversalMessageDialog.show_error( self, "错误", f"创建脚本时发生错误: {str(e)}", show_cancel=False, confirm_text="确认" ) def onLoadScript(self): """加载脚本按钮事件""" current_item = self.scriptsList.currentItem() if not current_item: UniversalMessageDialog.show_warning( self, "警告", "请选择要加载的脚本!", show_cancel=False, confirm_text="确认" ) return script_name = current_item.text() try: success = self.world.reloadScript(script_name) if success: UniversalMessageDialog.show_success( self, "成功", f"脚本 '{script_name}' 加载成功!", show_cancel=False, confirm_text="确认" ) else: UniversalMessageDialog.show_warning( self, "警告", f"脚本 '{script_name}' 加载失败!", show_cancel=False, confirm_text="确认" ) except Exception as e: UniversalMessageDialog.show_error( self, "错误", f"加载脚本时发生错误: {str(e)}", show_cancel=False, confirm_text="确认" ) 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: UniversalMessageDialog.show_success( self, "成功", "脚本文件加载成功!", show_cancel=False, confirm_text="确认" ) self.refreshScriptsList() else: UniversalMessageDialog.show_warning( self, "警告", "脚本文件加载失败!", show_cancel=False, confirm_text="确认" ) except Exception as e: UniversalMessageDialog.show_error( self, "错误", f"加载脚本文件时发生错误: {str(e)}", show_cancel=False, confirm_text="确认" ) def onReloadAllScripts(self): """重新加载所有脚本事件""" try: scripts_loaded = self.world.loadAllScripts() UniversalMessageDialog.show_success( self, "成功", f"更新完成,共加载 {len(scripts_loaded)} 个脚本!", show_cancel=False, confirm_text="确认" ) self.refreshScriptsList() except Exception as e: UniversalMessageDialog.show_error( self, "错误", f"加载脚本时发生错误: {str(e)}", show_cancel=False, confirm_text="确认" ) def onToggleHotReload(self): """切换热更新状态""" enabled = self.toggleHotReloadAction.isChecked() self.world.enableHotReload(enabled) status = "已开启" if enabled else "已关闭" UniversalMessageDialog.show_info( self, "脚本热更", f"脚本热更{status}", show_cancel=False, confirm_text="确认" ) def onOpenScriptsManager(self): """打开脚本管理器""" # 显示脚本管理停靠窗口 self.scriptDock.show() self.scriptDock.raise_() def onScriptDoubleClick(self, item): """脚本列表双击事件""" script_name = item.text() UniversalMessageDialog.show_info( self, "提示", f"双击脚本: {script_name}\n\n请使用外部编辑器编辑脚本文件。", show_cancel=False, confirm_text="确认" ) def onMountScript(self): """挂载脚本事件""" selected_object = getattr(self.world.selection, 'selectedObject', None) if not selected_object: UniversalMessageDialog.show_warning( self, "警告", "请选择一个对象!", show_cancel=False, confirm_text="确认" ) return script_name = self.mountScriptCombo.currentText() if not script_name: UniversalMessageDialog.show_warning( self, "警告", "请选择要挂载的脚本!", show_cancel=False, confirm_text="确认" ) return try: success = self.world.addScript(selected_object, script_name) if success: UniversalMessageDialog.show_success( self, "成功", f"脚本 '{script_name}' 已挂载到对象!", show_cancel=False, confirm_text="确认" ) self.updateMountedScriptsList(selected_object) if self.treeWidget and self.treeWidget.currentItem(): self.world.updatePropertyPanel(self.treeWidget.currentItem()) else: UniversalMessageDialog.show_warning( self, "警告", "挂载脚本失败!", show_cancel=False, confirm_text="确认" ) except Exception as e: UniversalMessageDialog.show_error( self, "错误", f"挂载脚本时发生错误: {str(e)}", show_cancel=False, confirm_text="确认" ) def onUnmountScript(self): """卸载脚本事件""" selected_object = getattr(self.world.selection, 'selectedObject', None) if not selected_object: UniversalMessageDialog.show_warning( self, "警告", "请选择一个对象!", show_cancel=False, confirm_text="确认" ) return current_item = self.mountedScriptsList.currentItem() if not current_item: UniversalMessageDialog.show_warning( self, "警告", "请选择要卸载的脚本!", show_cancel=False, confirm_text="确认" ) return item_text = current_item.text() script_name = item_text[2:] try: success = self.world.removeScript(selected_object, script_name) if success: UniversalMessageDialog.show_success( self, "成功", f"脚本 '{script_name}' 已从对象卸载!", show_cancel=False, confirm_text="确认" ) self.updateMountedScriptsList(selected_object) if self.treeWidget and self.treeWidget.currentItem(): self.world.updatePropertyPanel(self.treeWidget.currentItem()) else: UniversalMessageDialog.show_warning( self, "警告", "卸载脚本失败!", show_cancel=False, confirm_text="确认" ) except Exception as e: UniversalMessageDialog.show_error( self, "错误", f"卸载脚本时发生错误: {str(e)}", show_cancel=False, confirm_text="确认" ) def onOpenIconManager(self): """打开图标管理器""" try: from ui.icon_manager_gui import show_icon_manager self.icon_manager_dialog = show_icon_manager(self) print("🎨 图标管理器已打开") except Exception as e: print(f"❌ 打开图标管理器失败: {e}") UniversalMessageDialog.show_warning(self, "警告", f"打开图标管理器失败: {str(e)}", show_cancel=False, confirm_text="确认") def closeEvent(self, event): """处理窗口关闭事件""" try: print("🔄 正在关闭应用程序...") # 关闭拆装交互相关的弹窗 if hasattr(self.world, 'assembly_interaction') and self.world.assembly_interaction: print("🧹 关闭拆装交互弹窗...") if hasattr(self.world.assembly_interaction, 'step_dialog') and self.world.assembly_interaction.step_dialog: self.world.assembly_interaction.step_dialog.close() self.world.assembly_interaction.step_dialog = None # 停止交互模式 if self.world.assembly_interaction.is_active: self.world.assembly_interaction.stop_interaction_mode() # 清理工具管理器中的进程 if hasattr(self.world, 'tool_manager') and self.world.tool_manager: print("🧹 清理工具管理器进程...") if hasattr(self.world.tool_manager, 'cleanup_processes'): self.world.tool_manager.cleanup_processes() else: print("✓ 工具管理器无需清理进程") # 停止更新定时器 if hasattr(self, 'updateTimer') and self.updateTimer: self.updateTimer.stop() print("⏹️ 更新定时器已停止") # 清理VR资源 if hasattr(self.world, 'vr_manager') and self.world.vr_manager: print("🧹 清理VR资源...") self.world.vr_manager.cleanup() # 清理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): """创建平面地形""" fields = [ { "name": "width", "label": "宽度:", "type": "double", "min": 0.0, "max": 10000.0, "step": 0.1, "decimals": 2, "value": 0.30, "label_width": 60 }, { "name": "height", "label": "高度:", "type": "double", "min": 0.0, "max": 10000.0, "step": 0.1, "decimals": 2, "value": 0.30, "label_width": 60 }, { "name": "resolution", "label": "分辨率:", "type": "int", "min": 16, "max": 2048, "step": 16, "value": 256, "label_width": 60 }, ] dialog = StyledTerrainDialog(self, "创建平面地形", fields, primary_text="创建", secondary_text="取消") if dialog.exec_() == QDialog.Accepted: width = dialog.get_value("width") height = dialog.get_value("height") resolution = int(dialog.get_value("resolution")) terrain_info = self.world.createFlatTerrain((width, height), resolution) if terrain_info: UniversalMessageDialog.show_success( self, "成功", "平面地形创建成功!", show_cancel=False, confirm_text="确认" ) else: UniversalMessageDialog.show_warning( self, "警告", "平面地形创建失败!", show_cancel=False, confirm_text="确认" ) def onCreateHeightmapTerrain(self): """从高度图创建地形""" file_dialog = self.createStyledFileDialog( self, "选择高度图文件", "", "图像文件 (*.png *.jpg *.jpeg *.bmp *.tga);;所有文件 (*)" ) if file_dialog.exec_() == QDialog.Accepted: file_path = file_dialog.selectedFiles()[0] if file_path: fields = [ { "name": "x_scale", "label": "X缩放:", "type": "double", "min": 0.1, "max": 1000.0, "step": 0.1, "decimals": 2, "value": 0.30, "label_width": 70 }, { "name": "y_scale", "label": "Y缩放:", "type": "double", "min": 0.1, "max": 1000.0, "step": 0.1, "decimals": 2, "value": 0.30, "label_width": 70 }, { "name": "z_scale", "label": "Z缩放:", "type": "double", "min": 0.1, "max": 1000.0, "step": 1.0, "decimals": 2, "value": 50.0, "label_width": 70 }, ] params_dialog = StyledTerrainDialog(self, "设置地形参数", fields, primary_text="创建", secondary_text="取消") if params_dialog.exec_() == QDialog.Accepted: x_scale = params_dialog.get_value("x_scale") y_scale = params_dialog.get_value("y_scale") z_scale = params_dialog.get_value("z_scale") terrain_info = self.world.createTerrainFromHeightMap( file_path, (x_scale, y_scale, z_scale) ) if terrain_info: UniversalMessageDialog.show_success( self, "成功", "高度图地形创建成功!", show_cancel=False, confirm_text="确认" ) else: UniversalMessageDialog.show_warning( self, "警告", "高度图地形创建失败!", show_cancel=False, confirm_text="确认" ) def onOpenAssemblyDisassemblyConfig(self): """打开拆装配置界面""" try: from ui.assembly_disassembly_config_simple import AssemblyDisassemblyConfigDialog config_dialog = AssemblyDisassemblyConfigDialog(self, self.world) config_dialog.show() except Exception as e: UniversalMessageDialog.show_error(self, "错误", f"打开拆装配置界面失败: {str(e)}") import traceback traceback.print_exc() def onStartAssemblyInteraction(self): """开始拆装交互""" try: # 显示模式选择对话框 mode_dialog = AssemblyModeSelectionDialog(self) if mode_dialog.exec_() != QDialog.Accepted: return selected_mode = mode_dialog.get_selected_mode() print(f"🎯 用户选择的拆装模式: {selected_mode}") from core.assembly_interaction import AssemblyInteractionManager # 检查是否已有交互管理器实例 if not hasattr(self.world, 'assembly_interaction'): self.world.assembly_interaction = AssemblyInteractionManager(self.world) # 启动交互模式,传递模式参数 self.world.assembly_interaction.start_interaction_mode(mode=selected_mode) except Exception as e: UniversalMessageDialog.show_error(self, "错误", f"启动拆装交互失败: {str(e)}") import traceback traceback.print_exc() def onOpenMaintenanceSystem(self): """打开维修系统""" try: # 导入简化的登录界面 from ui.simple_maintenance_login import SimpleMaintenanceLoginDialog from ui.maintenance_system import MaintenanceSubjectDialog, MaintenanceSystemManager print("🔧 启动维修系统...") # 显示登录界面 login_dialog = SimpleMaintenanceLoginDialog(self) if login_dialog.exec_() == QDialog.Accepted: print("✅ 登录成功,显示科目选择界面") # 获取当前项目路径 project_path = None if hasattr(self.world, 'project_manager') and self.world.project_manager: project_path = self.world.project_manager.getCurrentProjectPath() # 显示科目选择界面 subject_dialog = MaintenanceSubjectDialog(project_path, self) def on_subject_selected(subject_path, mode): """处理科目选择""" try: print(f"🎯 启动维修科目: {subject_path}") print(f"📝 模式: {mode}") # 加载科目配置 import json with open(subject_path, 'r', encoding='utf-8') as f: subject_config = json.load(f) # 初始化拆装交互系统(如果还没有) from core.assembly_interaction import AssemblyInteractionManager if not hasattr(self.world, 'assembly_interaction') or not self.world.assembly_interaction: print("🔧 初始化拆装交互系统...") self.world.assembly_interaction = AssemblyInteractionManager(self.world) # 设置配置并启动交互模式 self.world.assembly_interaction.config_data = subject_config # 启动交互模式 success = self.world.assembly_interaction.start_interaction_mode(mode=mode) if success: print(f"✅ 维修科目启动成功") else: print("❌ 维修科目启动失败") UniversalMessageDialog.show_warning(self, "错误", "维修科目启动失败",False,"确认") except Exception as e: print(f"❌ 启动维修科目失败: {e}") UniversalMessageDialog.show_error(self, "错误", f"启动维修科目失败: {str(e)}") import traceback traceback.print_exc() subject_dialog.subject_selected.connect(on_subject_selected) subject_dialog.exec_() else: print("ℹ️ 用户取消了登录") except Exception as e: print(f"❌ 打开维修系统失败: {e}") UniversalMessageDialog.show_error(self, "错误", f"打开维修系统失败: {str(e)}") import traceback traceback.print_exc() # ==================== VR事件处理 ==================== def onEnterVR(self): """进入VR模式""" try: if hasattr(self.world, 'vr_manager') and self.world.vr_manager: success = self.world.vr_manager.enable_vr() if success: # 更新菜单状态 self.enterVRAction.setEnabled(False) self.exitVRAction.setEnabled(True) UniversalMessageDialog.show_info( self, "成功", "VR模式已启用!\n请确保您的VR头显已正确连接。" , False, "确认") else: UniversalMessageDialog.show_warning( self, "错误", "无法启用VR模式!\n请检查:\n1. SteamVR是否正在运行\n2. VR头显是否已连接\n3. OpenVR库是否已正确安装" , False, "确认") else: UniversalMessageDialog.show_warning( self, "错误", "VR管理器不可用!" , False, "确认") except Exception as e: UniversalMessageDialog.show_error( self, "错误", f"启用VR模式时发生错误:\n{str(e)}" , False, "确认") def onExitVR(self): """退出VR模式""" try: if hasattr(self.world, 'vr_manager') and self.world.vr_manager: self.world.vr_manager.disable_vr() # 更新菜单状态 self.enterVRAction.setEnabled(True) self.exitVRAction.setEnabled(False) UniversalMessageDialog.show_info(self, "成功", "已退出VR模式" , False, "确认") else: UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!" , False, "确认") except Exception as e: UniversalMessageDialog.show_error(self, "错误", f"退出VR模式时发生错误:\n{str(e)}" , False, "确认") def onShowVRStatus(self): """显示VR状态""" try: if hasattr(self.world, 'vr_manager') and self.world.vr_manager: status = self.world.vr_manager.get_vr_status() status_text = f"""VR系统状态: 可用性: {'✅ 可用' if status['available'] else '❌ 不可用'} 初始化: {'✅ 已初始化' if status['initialized'] else '❌ 未初始化'} 启用状态: {'✅ 已启用' if status['enabled'] else '❌ 未启用'} 渲染分辨率: {status['eye_resolution'][0]}x{status['eye_resolution'][1]} 追踪设备数: {status['device_count']} 提示: - 如果VR不可用,请确保已安装SteamVR并连接VR头显 - 如果OpenVR库未安装,请运行:pip install openvr """ UniversalMessageDialog.show_info(self, "VR状态", status_text , False, "确认") else: UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!" , False, "确认") except Exception as e: UniversalMessageDialog.show_error(self, "错误", f"获取VR状态时发生错误:\n{str(e)}" , False, "确认") def onShowVRSettings(self): """显示VR设置对话框""" try: if hasattr(self.world, 'vr_manager') and self.world.vr_manager: dialog = self.createVRSettingsDialog() dialog.exec_() else: UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!" , False, "确认") except Exception as e: UniversalMessageDialog.show_error(self, "错误", f"打开VR设置时发生错误:\n{str(e)}" , False, "确认") def createVRSettingsDialog(self): """创建VR设置对话框(统一为 NewProjectDialog 风格)""" dialog = QDialog(self) dialog.setWindowTitle("VR设置") dialog.setModal(True) dialog.resize(508, 420) dialog.setObjectName("newProjectDialog") dialog.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) dialog.setAttribute(Qt.WA_TranslucentBackground, True) dialog.setStyleSheet(""" QDialog#newProjectDialog { background-color: transparent; color: #EBEBEB; border: none; } QFrame#baseFrame { background-color: #000000; border: 1px solid #3E3E42; border-radius: 5px; } QWidget#titleBar { background-color: transparent; border: none; border-radius: 5px 5px 0 0; min-height: 32px; max-height: 32px; } QWidget#titleBar QWidget { background-color: transparent; border: none; } QLabel#titleLabel { color: #FFFFFF; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 14px; font-weight: 500; letter-spacing: 0.7px; } QWidget#controlButtons QPushButton { background-color: transparent; border: none; color: #EBEBEB; font-size: 14px; min-width: 18px; max-width: 18px; min-height: 18px; max-height: 18px; padding: 0px; border-radius: 3px; } QWidget#controlButtons QPushButton:hover { background-color: #2A2D2E; color: #FFFFFF; } QPushButton#closeButton { border-radius: 0px 5px 0px 0px; } QPushButton#closeButton:hover { background-color: #2A2D2E; color: #FFFFFF; } QWidget#contentWidget { background-color: transparent; border-radius: 0 0 5px 5px; } QFrame#contentContainer { background-color: #19191B; border: 1px solid #2C2F36; border-radius: 5px; } QLabel[role="sectionTitle"] { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 12px; font-weight: 500; letter-spacing: 0.6px; } QLabel[role="fieldLabel"] { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 12px; font-weight: 400; letter-spacing: 0.6px; } QLabel { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; letter-spacing: 0.55px; } QComboBox, QSpinBox, QDoubleSpinBox { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; } QCheckBox { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; } QLineEdit { background-color: rgba(89,100,113,0.2); color: #EBEBEB; border: 1px solid rgba(76,92,110,0.6); border-radius: 2px; padding: 6px 10px; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; letter-spacing: 0.55px; min-height: 14px; max-height: 30px; } QLineEdit:focus { border: 1px solid #3067C0; background-color: rgba(48,103,192,0.1); } QLineEdit:hover { border: 1px solid #3067C0; background-color: rgba(89,100,113,0.3); } QLineEdit:disabled { background-color: rgba(89,100,113,0.1); color: rgba(235,235,235,0.4); border: 1px solid rgba(76,92,110,0.2); } QTextEdit { background-color: rgba(89,100,113,0.1); color: #EBEBEB; border: 1px solid rgba(76,92,110,0.4); border-radius: 4px; font-size: 11px; } QPushButton { background-color: rgba(89,98,118,0.5); color: #EBEBEB; border: none; border-radius: 2px; padding: 0px 0px; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-weight: 300; font-size: 10px; letter-spacing: 0.5px; min-width: 90px; min-height: 30px; max-height: 30px; } QPushButton:hover { background-color: #3067C0; color: #FFFFFF; } QPushButton:pressed { background-color: #2556A0; color: #FFFFFF; } QPushButton:disabled { background-color: rgba(89,98,118,0.3); color: rgba(235,235,235,0.4); } QPushButton#primaryButton { background-color: rgba(89,98,118,0.5); border: none; color: #EBEBEB; font-weight: 300; min-width: 120px; max-width: 120px; } QPushButton#primaryButton:hover { background-color: #2556A0; } QPushButton#primaryButton:pressed { background-color: #1E4A8C; } """) main_layout = QVBoxLayout(dialog) main_layout.setContentsMargins(0, 0, 0, 0) main_layout.setSpacing(0) base_frame = QFrame() base_frame.setObjectName('baseFrame') base_frame.setFrameShape(QFrame.NoFrame) base_frame.setAttribute(Qt.WA_StyledBackground, True) base_layout = QVBoxLayout(base_frame) base_layout.setContentsMargins(0, 0, 0, 0) base_layout.setSpacing(0) # Title bar title_bar = QWidget() title_bar.setObjectName('titleBar') tb_layout = QHBoxLayout(title_bar) tb_layout.setContentsMargins(8, 0, 8, 0) tb_layout.setSpacing(6) control_buttons = QWidget() control_buttons.setObjectName('controlButtons') cb_layout = QHBoxLayout(control_buttons) cb_layout.setContentsMargins(0, 0, 0, 0) cb_layout.setSpacing(0) close_btn = QPushButton() close_btn.setObjectName('closeButton') try: close_btn.setIcon(get_icon('close_icon', QSize(18, 18))) close_btn.setIconSize(QSize(18, 18)) except Exception: pass close_btn.clicked.connect(dialog.reject) cb_layout.addWidget(close_btn) left_placeholder = QWidget() left_placeholder.setFixedWidth(control_buttons.sizeHint().width()) tb_layout.addWidget(left_placeholder) title_label = QLabel("VR设置") title_label.setObjectName('titleLabel') title_label.setAlignment(Qt.AlignCenter) tb_layout.addWidget(title_label, 1) tb_layout.addWidget(control_buttons) base_layout.addWidget(title_bar) # drag handlers dragging_state = {'dragging': False, 'pos': QPoint()} def _tb_press(event): if event.button() == Qt.LeftButton: dragging_state['dragging'] = True dragging_state['pos'] = event.globalPos() - dialog.frameGeometry().topLeft() event.accept() else: event.ignore() def _tb_move(event): if event.buttons() & Qt.LeftButton and dragging_state['dragging']: dialog.move(event.globalPos() - dragging_state['pos']) event.accept() else: event.ignore() def _tb_release(event): if event.button() == Qt.LeftButton: dragging_state['dragging'] = False event.accept() else: event.ignore() title_bar.mousePressEvent = _tb_press title_bar.mouseMoveEvent = _tb_move title_bar.mouseReleaseEvent = _tb_release # Content content_widget = QWidget() content_widget.setObjectName('contentWidget') content_layout = QVBoxLayout(content_widget) content_layout.setContentsMargins(10, 10, 10, 10) content_layout.setSpacing(0) content_container = QFrame() content_container.setObjectName('contentContainer') content_container.setFrameShape(QFrame.NoFrame) content_container.setAttribute(Qt.WA_StyledBackground, True) container_layout = QVBoxLayout(content_container) container_layout.setContentsMargins(15, 10, 15, 10) container_layout.setSpacing(10) # 左侧对齐辅助:统一字段列宽,并让小标题与控件左侧对齐 label_column_width = 110 def add_section_title(text): title = QLabel(text) title.setProperty('role', 'sectionTitle') container_layout.addWidget(title) # VR 状态 add_section_title("VR状态") status_widget = QWidget() status_vlayout = QVBoxLayout(status_widget) status_vlayout.setContentsMargins(0, 0, 0, 0) status_vlayout.setSpacing(6) if hasattr(self.world, 'vr_manager') and self.world.vr_manager: status = self.world.vr_manager.get_vr_status() available_label = QLabel(f"VR可用性: {'是' if status['available'] else '否'}") available_label.setStyleSheet(f"color: {'#2dffc4' if status['available'] else 'red'};") status_vlayout.addWidget(available_label) enabled_label = QLabel(f"VR状态: {'已启用' if status['enabled'] else '未启用'}") enabled_label.setStyleSheet(f"color: {'#2dffc4' if status['enabled'] else 'gray'};") status_vlayout.addWidget(enabled_label) resolution_label = QLabel(f"渲染分辨率: {status['eye_resolution'][0]}x{status['eye_resolution'][1]}") status_vlayout.addWidget(resolution_label) container_layout.addWidget(status_widget) # 渲染模式 add_section_title("渲染模式") render_mode_button_group = QButtonGroup(dialog) row_mode1 = QWidget(); row_mode1_layout = QHBoxLayout(row_mode1); row_mode1_layout.setContentsMargins(0,0,0,0); row_mode1_layout.setSpacing(10) normal_render_radio = QRadioButton("普通渲染模式") normal_render_radio.setStyleSheet(""" QRadioButton { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 12px; font-weight: 400; letter-spacing: 0.6px; } """) row_mode1_layout.addWidget(normal_render_radio) row_mode1_layout.addStretch(); container_layout.addWidget(row_mode1) row_mode2 = QWidget(); row_mode2_layout = QHBoxLayout(row_mode2); row_mode2_layout.setContentsMargins(0,0,0,0); row_mode2_layout.setSpacing(10) pipeline_render_radio = QRadioButton("RenderPipeline高级渲染(推荐)") pipeline_render_radio.setStyleSheet(""" QRadioButton { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 12px; font-weight: 400; letter-spacing: 0.6px; } """) row_mode2_layout.addWidget(pipeline_render_radio) row_mode2_layout.addStretch(); container_layout.addWidget(row_mode2) render_mode_button_group.addButton(normal_render_radio, 0) render_mode_button_group.addButton(pipeline_render_radio, 1) if hasattr(self.world, 'vr_manager') and self.world.vr_manager: try: from core.vr import VRRenderMode current_mode = self.world.vr_manager.get_vr_render_mode() if current_mode == VRRenderMode.RENDER_PIPELINE: pipeline_render_radio.setChecked(True) else: normal_render_radio.setChecked(True) except Exception: normal_render_radio.setChecked(True) else: normal_render_radio.setChecked(True) info_text = QTextEdit() info_text.setReadOnly(True) info_text.setMaximumHeight(60) info_text.setPlainText("• 普通渲染:性能最优,适合低配置\n• RenderPipeline:高级图形效果(阴影、AO等),需要较高性能") container_layout.addWidget(info_text) dialog.render_mode_button_group = render_mode_button_group # 载入配置 vr_config = {} if hasattr(self.world, 'vr_manager') and self.world.vr_manager and self.world.vr_manager.config_manager: vr_config = self.world.vr_manager.config_manager.load_config() # 渲染设置 add_section_title("渲染设置") row_quality = QWidget() row_quality_layout = QHBoxLayout(row_quality) row_quality_layout.setContentsMargins(0,0,0,0) row_quality_layout.setSpacing(10) quality_label = QLabel("渲染质量:") quality_label.setProperty('role','fieldLabel') quality_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) row_quality_layout.addWidget(quality_label) quality_combo = QComboBox() quality_combo.addItems(["低","中","高"]) quality_preset = vr_config.get("quality_preset", "balanced") quality_map={"performance":"低","balanced":"中","quality":"高"} quality_combo.setCurrentText(quality_map.get(quality_preset,"中")) row_quality_layout.addWidget(quality_combo,1) container_layout.addWidget(row_quality) row_aa = QWidget(); row_aa_layout = QHBoxLayout(row_aa) row_aa_layout.setContentsMargins(0,0,0,0) row_aa_layout.setSpacing(10) aa_label = QLabel("抗锯齿:") aa_label.setProperty('role','fieldLabel') aa_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) row_aa_layout.addWidget(aa_label) aa_combo = QComboBox() aa_combo.addItems(["无","2x","4x","8x"]) aa_combo.setCurrentText(vr_config.get("anti_aliasing","4x")) row_aa_layout.addWidget(aa_combo,1) container_layout.addWidget(row_aa) # 性能设置 add_section_title("性能设置") row_refresh = QWidget() row_refresh_layout = QHBoxLayout(row_refresh) row_refresh_layout.setContentsMargins(0,0,0,0) row_refresh_layout.setSpacing(10) refresh_label = QLabel("刷新率:") refresh_label.setProperty('role','fieldLabel') refresh_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) row_refresh_layout.addWidget(refresh_label) refresh_combo = QComboBox() refresh_combo.addItems(["72Hz","90Hz","120Hz","144Hz"]) refresh_combo.setCurrentText(vr_config.get("refresh_rate","90Hz")) row_refresh_layout.addWidget(refresh_combo,1) container_layout.addWidget(row_refresh) row_async = QWidget() row_async_layout = QHBoxLayout(row_async) row_async_layout.setContentsMargins(0,0,0,0) row_async_layout.setSpacing(10) async_check = QCheckBox("启用异步重投影") async_check.setChecked(vr_config.get("async_reprojection",True)) row_async_layout.addWidget(async_check) row_async_layout.addStretch() container_layout.addWidget(row_async) # Save refs dialog.quality_combo = quality_combo dialog.aa_combo = aa_combo dialog.refresh_combo = refresh_combo dialog.async_check = async_check # Separator before buttons separator_buttons = QFrame() separator_buttons.setFrameShape(QFrame.HLine) separator_buttons.setFrameShadow(QFrame.Plain) separator_buttons.setFixedHeight(1) separator_buttons.setStyleSheet("background-color: #2C2F36; border: none;") container_layout.addWidget(separator_buttons) # Buttons button_layout = QHBoxLayout() apply_button = QPushButton("应用") ok_button = QPushButton("确定") # ok_button.setObjectName("primaryButton") cancel_button = QPushButton("取消") button_layout.addWidget(apply_button) button_layout.addStretch() button_layout.addWidget(ok_button) button_layout.addWidget(cancel_button) container_layout.addLayout(button_layout) content_layout.addWidget(content_container, 0, Qt.AlignTop) base_layout.addWidget(content_widget) main_layout.addWidget(base_frame) # signals apply_button.clicked.connect(lambda: self.applyVRSettings(dialog)) ok_button.clicked.connect(lambda: self.onVRSettingsOK(dialog)) cancel_button.clicked.connect(dialog.reject) return dialog def onVRSettingsOK(self, dialog): """确定按钮 - 应用设置并关闭对话框""" # 先应用设置 self.applyVRSettings(dialog) # 关闭对话框 dialog.accept() def applyVRSettings(self, dialog): """应用VR设置""" try: if not hasattr(self.world, 'vr_manager') or not self.world.vr_manager: UniversalMessageDialog.show_warning(dialog, "错误", "VR管理器不可用!",False,"确定") return if not self.world.vr_manager.config_manager: UniversalMessageDialog.show_warning(dialog, "错误", "VR配置管理器不可用!",False,"确定") return # 1️⃣ 读取所有UI控件的值 # 渲染模式 selected_mode_id = dialog.render_mode_button_group.checkedId() new_mode = "render_pipeline" if selected_mode_id == 1 else "normal" mode_name = "RenderPipeline高级渲染" if selected_mode_id == 1 else "普通渲染" # 渲染质量 quality_text = dialog.quality_combo.currentText() quality_map_reverse = {"低": "performance", "中": "balanced", "高": "quality"} quality_preset = quality_map_reverse.get(quality_text, "balanced") # 其他设置 anti_aliasing = dialog.aa_combo.currentText() refresh_rate = dialog.refresh_combo.currentText() async_reprojection = dialog.async_check.isChecked() # 2️⃣ 加载当前配置 config = self.world.vr_manager.config_manager.load_config() # 3️⃣ 更新配置 config["quality_preset"] = quality_preset config["anti_aliasing"] = anti_aliasing config["refresh_rate"] = refresh_rate config["async_reprojection"] = async_reprojection # 4️⃣ 检查渲染模式是否改变 from core.vr import VRRenderMode current_mode = self.world.vr_manager.get_vr_render_mode() mode_changed = (current_mode.value != new_mode) # 5️⃣ 如果渲染模式改变,询问用户确认 if mode_changed: reply = UniversalMessageDialog.show_info(dialog, "确认切换", f"确定要切换到{mode_name}模式吗?\n\n注意:切换渲染模式将重新创建VR缓冲区,可能需要几秒钟。",True,"取消","确定") if reply == QDialog.Rejected: # 用户取消渲染模式切换,但仍然保存其他设置 self.world.vr_manager.config_manager.save_config(config) UniversalMessageDialog.show_info(dialog, "提示", "已保存其他设置(未切换渲染模式)", False, "确定") return # 应用渲染模式切换 success = self.world.vr_manager.set_vr_render_mode(new_mode) if not success: UniversalMessageDialog.show_warning(dialog, "失败", f"切换到{mode_name}模式失败!\n请查看控制台输出了解详情。", False, "确定") return # 6️⃣ 保存配置(如果模式改变,set_vr_render_mode已经保存了,但我们需要确保其他设置也被保存) self.world.vr_manager.config_manager.save_config(config) # 7️⃣ 应用质量预设到VR管理器 if hasattr(self.world.vr_manager, 'current_quality_preset'): self.world.vr_manager.current_quality_preset = quality_preset # 8️⃣ 显示成功消息 if mode_changed: UniversalMessageDialog.show_info(dialog, "成功", f"VR设置已应用!\n• 渲染模式: {mode_name}\n• 渲染质量: {quality_text}\n配置已自动保存。", False, "确定") else: UniversalMessageDialog.show_info(dialog, "成功", f"VR设置已保存!\n• 渲染质量: {quality_text}", False, "确定") except Exception as e: UniversalMessageDialog.show_critical(dialog, "错误", f"应用VR设置时发生错误:\n{str(e)}", False, "确定") import traceback traceback.print_exc() # ==================== VR调试事件处理 ==================== def onToggleVRDebug(self): """切换VR调试输出""" try: if hasattr(self.world, 'vr_manager') and self.world.vr_manager: enabled = self.world.vr_manager.toggle_debug_output() self.vrDebugToggleAction.setChecked(enabled) status = "启用" if enabled else "禁用" UniversalMessageDialog.show_info(self, "提示", f"已{status}VR调试输出", False, "确定") else: UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", False, "确定") except Exception as e: UniversalMessageDialog.show_error(self, "错误", f"切换VR调试时发生错误:\n{str(e)}", False, "确定") def onShowVRPerformance(self): """立即显示VR性能报告""" try: if hasattr(self.world, 'vr_manager') and self.world.vr_manager: if self.world.vr_manager.vr_enabled: self.world.vr_manager.force_performance_report() UniversalMessageDialog.show_info(self, "提示", "已立即显示VR性能报告", False, "确定") else: UniversalMessageDialog.show_warning(self, "提示", "请先启用VR模式", False, "确定") else: UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", False, "确定") except Exception as e: UniversalMessageDialog.show_error(self, "错误", f"显示VR性能报告时发生错误:\n{str(e)}", False, "确定") def onSetVRDebugMode(self, mode): """设置VR调试模式""" try: if hasattr(self.world, 'vr_manager') and self.world.vr_manager: self.world.vr_manager.set_debug_mode(mode) # 更新菜单状态 if mode == 'brief': self.vrDebugBriefAction.setChecked(True) self.vrDebugDetailedAction.setChecked(False) else: self.vrDebugBriefAction.setChecked(False) self.vrDebugDetailedAction.setChecked(True) mode_name = "简短" if mode == 'brief' else "详细" UniversalMessageDialog.show_info(self, "提示", f"已设置VR调试模式为:{mode_name}", False, "确定") else: UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", False, "确定") except Exception as e: UniversalMessageDialog.show_error(self, "错误", f"设置VR调试模式时发生错误:\n{str(e)}", False, "确定") def onToggleVRPerformanceMonitor(self): """切换VR性能监控""" try: if hasattr(self.world, 'vr_manager') and self.world.vr_manager: enabled = self.vrPerformanceMonitorAction.isChecked() if enabled: self.world.vr_manager.enable_performance_monitoring() else: self.world.vr_manager.disable_performance_monitoring() status = "启用" if enabled else "禁用" UniversalMessageDialog.show_info(self, "提示", f"已{status}VR性能监控", False, "确定") else: UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", False, "确定") self.vrPerformanceMonitorAction.setChecked(False) except Exception as e: UniversalMessageDialog.show_error(self, "错误", f"切换VR性能监控时发生错误:\n{str(e)}", False, "确定") def onToggleVRGpuTiming(self): """切换VR GPU时间监控""" try: if hasattr(self.world, 'vr_manager') and self.world.vr_manager: enabled = self.vrGpuTimingAction.isChecked() if enabled: self.world.vr_manager.enable_gpu_timing_monitoring() else: self.world.vr_manager.disable_gpu_timing_monitoring() status = "启用" if enabled else "禁用" UniversalMessageDialog.show_info(self, "提示", f"已{status}VR GPU时间监控", False, "确定") else: UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", False, "确定") self.vrGpuTimingAction.setChecked(False) except Exception as e: UniversalMessageDialog.show_error(self, "错误", f"切换VR GPU时间监控时发生错误:\n{str(e)}", False, "确定") def onToggleVRPipelineMonitor(self): """切换VR管线监控""" try: if hasattr(self.world, 'vr_manager') and self.world.vr_manager: enabled = self.vrPipelineMonitorAction.isChecked() self.world.vr_manager.enable_pipeline_monitoring = enabled status = "启用" if enabled else "禁用" print(f"✓ VR管线监控已{status}") else: UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", False, "确定") self.vrPipelineMonitorAction.setChecked(False) except Exception as e: UniversalMessageDialog.show_error(self, "错误", f"切换VR管线监控时发生错误:\n{str(e)}", False, "确定") def onSetVRPoseStrategy(self, strategy): """设置VR姿态策略""" try: if hasattr(self.world, 'vr_manager') and self.world.vr_manager: success = self.world.vr_manager.set_pose_strategy(strategy) if success: strategy_names = { 'render_callback': '渲染回调策略', 'update_task': '更新任务策略' } UniversalMessageDialog.show_info(self, "提示", f"已设置VR姿态策略为:{strategy_names.get(strategy, strategy)}", False, "确定") else: UniversalMessageDialog.show_warning(self, "错误", "设置VR姿态策略失败!", False, "确定") else: UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", False, "确定") except Exception as e: UniversalMessageDialog.show_error(self, "错误", f"设置VR姿态策略时发生错误:\n{str(e)}", False, "确定") def onTestVRPipeline(self): """测试VR管线监控功能""" try: if hasattr(self.world, 'vr_manager') and self.world.vr_manager: self.world.vr_manager.test_pipeline_monitoring() UniversalMessageDialog.show_info(self, "提示", "VR管线监控测试已开始,请稍等...", False, "确定") else: UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", False, "确定") except Exception as e: UniversalMessageDialog.show_error(self, "错误", f"测试VR管线监控时发生错误:\n{str(e)}", False, "确定") def onShowVRDebugSettings(self): """显示VR调试设置对话框""" try: if hasattr(self.world, 'vr_manager') and self.world.vr_manager: dialog = self.createVRDebugSettingsDialog() dialog.exec_() else: UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", False, "确定") except Exception as e: UniversalMessageDialog.show_error(self, "错误", f"打开VR调试设置时发生错误:\n{str(e)}", False, "确定") def createVRDebugSettingsDialog(self): """创建VR调试设置对话框(统一为 NewProjectDialog 风格)""" from PyQt5.QtWidgets import QCheckBox, QSlider from PyQt5.QtCore import Qt dialog = QDialog(self) dialog.setWindowTitle("VR调试设置") dialog.setModal(True) dialog.resize(508, 460) dialog.setObjectName("newProjectDialog") dialog.setWindowFlags(Qt.Dialog | Qt.FramelessWindowHint) dialog.setAttribute(Qt.WA_TranslucentBackground, True) dialog.setStyleSheet(""" QDialog#newProjectDialog { background-color: transparent; color: #EBEBEB; border: none; } QFrame#baseFrame { background-color: #000000; border: 1px solid #3E3E42; border-radius: 5px; } QWidget#titleBar { background-color: transparent; border: none; border-radius: 5px 5px 0 0; min-height: 32px; max-height: 32px; } QWidget#titleBar QWidget { background-color: transparent; border: none; } QLabel#titleLabel { color: #FFFFFF; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 14px; font-weight: 500; letter-spacing: 0.7px; } QWidget#controlButtons QPushButton { background-color: transparent; border: none; color: #EBEBEB; font-size: 14px; min-width: 18px; max-width: 18px; min-height: 18px; max-height: 18px; padding: 0px; border-radius: 3px; } QWidget#controlButtons QPushButton:hover { background-color: #2A2D2E; color: #FFFFFF; } QPushButton#closeButton { border-radius: 0px 5px 0px 0px; } QPushButton#closeButton:hover { background-color: #2A2D2E; color: #FFFFFF; } QWidget#contentWidget { background-color: transparent; border-radius: 0 0 5px 5px; } QFrame#contentContainer { background-color: #19191B; border: 1px solid #2C2F36; border-radius: 5px; } QLabel[role="sectionTitle"] { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 12px; font-weight: 500; letter-spacing: 0.6px; } QLabel[role="fieldLabel"] { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 12px; font-weight: 400; letter-spacing: 0.6px; } QLabel { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; letter-spacing: 0.55px; } QComboBox, QSpinBox, QDoubleSpinBox { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; } QCheckBox { color: #EBEBEB; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; } QLineEdit { background-color: rgba(89,100,113,0.2); color: #EBEBEB; border: 1px solid rgba(76,92,110,0.6); border-radius: 2px; padding: 6px 10px; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-size: 11px; font-weight: 300; letter-spacing: 0.55px; min-height: 14px; max-height: 30px; } QLineEdit:focus { border: 1px solid #3067C0; background-color: rgba(48,103,192,0.1); } QLineEdit:hover { border: 1px solid #3067C0; background-color: rgba(89,100,113,0.3); } QLineEdit:disabled { background-color: rgba(89,100,113,0.1); color: rgba(235,235,235,0.4); border: 1px solid rgba(76,92,110,0.2); } QPushButton { background-color: rgba(89,98,118,0.5); color: #EBEBEB; border: none; border-radius: 2px; padding: 0px 0px; font-family: 'Inter', 'Microsoft YaHei', sans-serif; font-weight: 300; font-size: 10px; letter-spacing: 0.5px; min-width: 90px; min-height: 30px; max-height: 30px; } QPushButton:hover { background-color: #3067C0; color: #FFFFFF; } QPushButton:pressed { background-color: #2556A0; color: #FFFFFF; } QPushButton:disabled { background-color: rgba(89,98,118,0.3); color: rgba(235,235,235,0.4); } QPushButton#primaryButton { background-color: rgba(89,98,118,0.5); border: none; color: #EBEBEB; font-weight: 300; min-width: 120px; max-width: 120px; } QPushButton#primaryButton:hover { background-color: #2556A0; } QPushButton#primaryButton:pressed { background-color: #1E4A8C; } """) main_layout = QVBoxLayout(dialog) main_layout.setContentsMargins(0,0,0,0) main_layout.setSpacing(0) base_frame = QFrame() base_frame.setObjectName('baseFrame') base_frame.setFrameShape(QFrame.NoFrame) base_frame.setAttribute(Qt.WA_StyledBackground, True) base_layout = QVBoxLayout(base_frame) base_layout.setContentsMargins(0,0,0,0) base_layout.setSpacing(0) title_bar = QWidget() title_bar.setObjectName('titleBar') tb_layout = QHBoxLayout(title_bar) tb_layout.setContentsMargins(8,0,8,0) tb_layout.setSpacing(6) control_buttons = QWidget() control_buttons.setObjectName('controlButtons') cb_layout = QHBoxLayout(control_buttons) cb_layout.setContentsMargins(0,0,0,0) cb_layout.setSpacing(0) close_btn = QPushButton() close_btn.setObjectName('closeButton') try: close_btn.setIcon(get_icon('close_icon', QSize(18, 18))) close_btn.setIconSize(QSize(18, 18)) except Exception: pass close_btn.clicked.connect(dialog.reject) cb_layout.addWidget(close_btn) left_placeholder = QWidget() left_placeholder.setFixedWidth(control_buttons.sizeHint().width()) tb_layout.addWidget(left_placeholder) title_label = QLabel("VR调试设置") title_label.setObjectName('titleLabel') title_label.setAlignment(Qt.AlignCenter) tb_layout.addWidget(title_label,1) tb_layout.addWidget(control_buttons) base_layout.addWidget(title_bar) dragging_state={'dragging':False,'pos':QPoint()} def _tb_press(e): if e.button()==Qt.LeftButton: dragging_state['dragging']=True; dragging_state['pos']=e.globalPos()-dialog.frameGeometry().topLeft(); e.accept() else: e.ignore() def _tb_move(e): if e.buttons() & Qt.LeftButton and dragging_state['dragging']: dialog.move(e.globalPos()-dragging_state['pos']); e.accept() else: e.ignore() def _tb_release(e): if e.button()==Qt.LeftButton: dragging_state['dragging']=False; e.accept() else: e.ignore() title_bar.mousePressEvent=_tb_press; title_bar.mouseMoveEvent=_tb_move; title_bar.mouseReleaseEvent=_tb_release content_widget = QWidget() content_widget.setObjectName('contentWidget') content_layout = QVBoxLayout(content_widget) content_layout.setContentsMargins(10,10,10,10) content_layout.setSpacing(0) content_container = QFrame() content_container.setObjectName('contentContainer') content_container.setFrameShape(QFrame.NoFrame) content_container.setAttribute(Qt.WA_StyledBackground, True) container_layout = QVBoxLayout(content_container) container_layout.setContentsMargins(15,10,15,10) container_layout.setSpacing(10) # 获取当前设置 vr_manager = self.world.vr_manager debug_status = vr_manager.get_debug_status() perf_config = vr_manager.get_performance_monitoring_config() # 左侧对齐辅助:统一字段列宽,并让小标题与控件左侧对齐 label_column_width = 110 def add_section_title(text): title = QLabel(text) title.setProperty('role','sectionTitle') container_layout.addWidget(title) # 调试状态 add_section_title("调试状态") status_widget = QWidget() status_layout = QVBoxLayout(status_widget) status_layout.setContentsMargins(0,0,0,0) status_layout.setSpacing(6) debug_enabled_label = QLabel(f"调试输出: {'启用' if debug_status['debug_enabled'] else '禁用'}") debug_enabled_label.setStyleSheet(f"color: {'#2dffc4' if debug_status['debug_enabled'] else 'red'};") status_layout.addWidget(debug_enabled_label) debug_mode_label = QLabel(f"调试模式: {debug_status['debug_mode']}") status_layout.addWidget(debug_mode_label) performance_label = QLabel(f"性能监控: {'启用' if debug_status['performance_monitoring'] else '禁用'}") performance_label.setStyleSheet(f"color: {'#2dffc4' if debug_status['performance_monitoring'] else 'red'};") status_layout.addWidget(performance_label) container_layout.addWidget(status_widget) # 报告设置 add_section_title("报告设置") row_interval = QWidget() row_interval_layout = QHBoxLayout(row_interval) row_interval_layout.setContentsMargins(0,0,0,0) row_interval_layout.setSpacing(10) interval_text_label = QLabel("报告间隔:") interval_text_label.setProperty('role','fieldLabel') interval_text_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) row_interval_layout.addWidget(interval_text_label) interval_slider = QSlider(Qt.Horizontal) interval_slider.setMinimum(5) interval_slider.setMaximum(120) interval_slider.setValue(int(debug_status['report_interval_seconds'])) interval_slider.setTickPosition(QSlider.TicksBelow) interval_slider.setTickInterval(15) interval_label = QLabel(f"{int(debug_status['report_interval_seconds'])}秒") interval_slider.valueChanged.connect(lambda v: interval_label.setText(f"{v}秒")) inner = QHBoxLayout() inner.setContentsMargins(0,0,0,0) inner.setSpacing(6) inner.addWidget(interval_slider) inner.addWidget(interval_label) row_interval_layout.addLayout(inner,1) container_layout.addWidget(row_interval) row_check = QWidget() row_check_layout = QHBoxLayout(row_check) row_check_layout.setContentsMargins(0,0,0,0) row_check_layout.setSpacing(10) check_label = QLabel("性能检查间隔:") check_label.setProperty('role','fieldLabel') check_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) row_check_layout.addWidget(check_label) check_interval_combo = QComboBox() check_interval_combo.addItems(["0.1秒","0.5秒","1.0秒","2.0秒"]) current_check_interval = perf_config['check_interval'] if current_check_interval == 0.1: check_interval_combo.setCurrentIndex(0) elif current_check_interval == 0.5: check_interval_combo.setCurrentIndex(1) elif current_check_interval == 1.0: check_interval_combo.setCurrentIndex(2) else: check_interval_combo.setCurrentIndex(3) row_check_layout.addWidget(check_interval_combo,1) container_layout.addWidget(row_check) row_hist = QWidget(); row_hist_layout = QHBoxLayout(row_hist) row_hist_layout.setContentsMargins(0,0,0,0) row_hist_layout.setSpacing(10) hist_label = QLabel("帧时间历史:") hist_label.setProperty('role','fieldLabel') hist_label.setAlignment(Qt.AlignRight|Qt.AlignVCenter) row_hist_layout.addWidget(hist_label) frame_history_spin = QSpinBox() frame_history_spin.setMinimum(10) frame_history_spin.setMaximum(1000) frame_history_spin.setValue(perf_config['frame_history_size']) frame_history_spin.setSuffix(" 帧") row_hist_layout.addWidget(frame_history_spin,1) container_layout.addWidget(row_hist) # 监控项目 add_section_title("监控项目") monitor_widget = QWidget() monitor_layout = QVBoxLayout(monitor_widget) monitor_layout.setContentsMargins(0,0,0,0) monitor_layout.setSpacing(6) cpu_check = QCheckBox("CPU使用率") cpu_check.setChecked(perf_config['psutil_available']) cpu_check.setEnabled(perf_config['psutil_available']) monitor_layout.addWidget(cpu_check) memory_check = QCheckBox("内存使用率") memory_check.setChecked(perf_config['psutil_available']) memory_check.setEnabled(perf_config['psutil_available']) monitor_layout.addWidget(memory_check) gpu_check = QCheckBox("GPU使用率") gpu_check.setChecked(perf_config['gputil_available'] or perf_config['nvidia_ml_available']) gpu_check.setEnabled(perf_config['gputil_available'] or perf_config['nvidia_ml_available']) monitor_layout.addWidget(gpu_check) frame_time_check = QCheckBox("帧时间统计") frame_time_check.setChecked(True) monitor_layout.addWidget(frame_time_check) container_layout.addWidget(monitor_widget) separator_buttons = QFrame() separator_buttons.setFrameShape(QFrame.HLine) separator_buttons.setFrameShadow(QFrame.Plain) separator_buttons.setFixedHeight(1) separator_buttons.setStyleSheet("background-color: #2C2F36; border: none;") container_layout.addWidget(separator_buttons) button_layout = QHBoxLayout() apply_button = QPushButton("应用") reset_button = QPushButton("重置计数器") ok_button = QPushButton("确定") # ok_button.setObjectName("primaryButton") cancel_button = QPushButton("取消") button_layout.addWidget(apply_button) button_layout.addWidget(reset_button) button_layout.addStretch() button_layout.addWidget(ok_button) button_layout.addWidget(cancel_button) container_layout.addLayout(button_layout) content_layout.addWidget(content_container, 0, Qt.AlignTop) base_layout.addWidget(content_widget); main_layout.addWidget(base_frame) def apply_settings(): try: new_interval_seconds = interval_slider.value() new_interval_frames = int(new_interval_seconds * 60) vr_manager.set_performance_report_interval(new_interval_frames) check_intervals = [0.1, 0.5, 1.0, 2.0] new_check_interval = check_intervals[check_interval_combo.currentIndex()] vr_manager.set_performance_check_interval(new_check_interval) vr_manager.set_frame_time_history_size(frame_history_spin.value()) UniversalMessageDialog.show_success(dialog, "成功", "VR调试设置已应用!", False, "确定") except Exception as e: UniversalMessageDialog.show_error(dialog, "错误", f"应用设置时发生错误:\n{str(e)}", False, "确定") def reset_counters(): try: vr_manager.reset_performance_counters() UniversalMessageDialog.show_success(dialog, "成功", "性能计数器已重置!", False, "确定") except Exception as e: UniversalMessageDialog.show_error(dialog, "错误", f"重置计数器时发生错误:\n{str(e)}", False, "确定") apply_button.clicked.connect(apply_settings) reset_button.clicked.connect(reset_counters) ok_button.clicked.connect(lambda: (apply_settings(), dialog.accept())) cancel_button.clicked.connect(dialog.reject) return dialog # ==================== VR测试模式事件处理 ==================== def onToggleVRTestMode(self): """切换VR测试模式""" try: if hasattr(self.world, 'vr_manager') and self.world.vr_manager: if self.vrTestModeAction.isChecked(): # 启用VR测试模式 success = self.world.vr_manager.enable_vr_test_mode(display_mode='stereo') if success: UniversalMessageDialog.show_info(self, "VR测试模式", "VR测试模式已启用!\n\n现在VR渲染内容将直接显示在PC屏幕上,无需VR头显。\n\n特点:\n- 显示VR左右眼视图\n- 实时性能监控HUD\n- 复用完整VR渲染管线\n- 可测量纯渲染性能", False, "确定") print("✅ VR测试模式已启用") # 可选:自动开启性能测试 self.world.vr_manager.run_vr_performance_test(duration_seconds=10) else: self.vrTestModeAction.setChecked(False) UniversalMessageDialog.show_warning(self, "错误", "启用VR测试模式失败!", False, "确定") else: # 禁用VR测试模式 self.world.vr_manager.disable_vr_test_mode() UniversalMessageDialog.show_info(self, "VR测试模式", "VR测试模式已禁用!", False, "确定") print("✅ VR测试模式已禁用") else: self.vrTestModeAction.setChecked(False) UniversalMessageDialog.show_warning(self, "错误", "VR管理器不可用!", False, "确定") except Exception as e: self.vrTestModeAction.setChecked(False) UniversalMessageDialog.show_error(self, "错误", f"切换VR测试模式时发生错误:\n{str(e)}", False, "确定") def onToggleVRTestSubmitTexture(self): """切换VR测试模式纹理提交功能""" try: if hasattr(self.world, 'vr_manager') and self.world.vr_manager: enabled = self.vrTestSubmitTextureAction.isChecked() self.world.vr_manager.set_test_mode_features(submit_texture=enabled) except Exception as e: UniversalMessageDialog.show_error(self, "错误", f"设置纹理提交功能时发生错误:\n{str(e)}", False, "确定") def onToggleVRTestWaitPoses(self): """切换VR测试模式姿态等待功能""" try: if hasattr(self.world, 'vr_manager') and self.world.vr_manager: enabled = self.vrTestWaitPosesAction.isChecked() self.world.vr_manager.set_test_mode_features(wait_poses=enabled) except Exception as e: UniversalMessageDialog.show_error(self, "错误", f"设置姿态等待功能时发生错误:\n{str(e)}", False, "确定") def onSetVRTestStep(self, step): """设置VR测试步骤""" try: if hasattr(self.world, 'vr_manager') and self.world.vr_manager: if step == 0: # 重置 self.world.vr_manager.set_test_mode_features(submit_texture=False, wait_poses=False) self.vrTestSubmitTextureAction.setChecked(False) self.vrTestWaitPosesAction.setChecked(False) UniversalMessageDialog.show_info(self, "VR测试", "已重置为基线状态:两个功能都禁用", False, "确定") elif step == 1: # 只启用纹理提交 self.world.vr_manager.set_test_mode_features(submit_texture=True, wait_poses=False) self.vrTestSubmitTextureAction.setChecked(True) self.vrTestWaitPosesAction.setChecked(False) UniversalMessageDialog.show_info(self, "VR测试", "步骤1:只启用纹理提交\n观察FPS变化来判断submit_texture是否影响性能", False, "确定") elif step == 2: # 只启用姿态等待 self.world.vr_manager.set_test_mode_features(submit_texture=False, wait_poses=True) self.vrTestSubmitTextureAction.setChecked(False) self.vrTestWaitPosesAction.setChecked(True) UniversalMessageDialog.show_info(self, "VR测试", "步骤2:只启用姿态等待\n观察FPS变化来判断waitGetPoses是否影响性能", False, "确定") elif step == 3: # 同时启用两者 self.world.vr_manager.set_test_mode_features(submit_texture=True, wait_poses=True) self.vrTestSubmitTextureAction.setChecked(True) self.vrTestWaitPosesAction.setChecked(True) UniversalMessageDialog.show_info(self, "VR测试", "步骤3:同时启用两者\n这应该完全复现普通VR模式的36FPS问题", False, "确定") except Exception as e: UniversalMessageDialog.show_error(self, "错误", f"设置VR测试步骤时发生错误:\n{str(e)}", False, "确定") class PluginConfigDialog(QDialog): def __init__(self,parent=None): super().__init__(parent) self.setWindowTitle("渲染管线插件配置") self.setGeometry(200,200,400,500) self.plugins_config_path = os.path.join("RenderPipelineFile", "config", "plugins.yaml") self.init_ui() self.load_plugins_config() def init_ui(self): layout = QVBoxLayout() # 标题 title_label = QLabel("启用的插件:") layout.addWidget(title_label) # 插件列表 self.plugins_list = QListWidget() layout.addWidget(self.plugins_list) # 按钮布局 button_layout = QHBoxLayout() # 全选/全不选按钮 self.select_all_btn = QPushButton("全选") self.select_all_btn.clicked.connect(self.select_all_plugins) button_layout.addWidget(self.select_all_btn) self.deselect_all_btn = QPushButton("全不选") self.deselect_all_btn.clicked.connect(self.deselect_all_plugins) button_layout.addWidget(self.deselect_all_btn) button_layout.addStretch() # 对话框按钮 self.button_box = QDialogButtonBox( QDialogButtonBox.Ok | QDialogButtonBox.Cancel | QDialogButtonBox.Apply ) self.button_box.accepted.connect(self.accept) self.button_box.rejected.connect(self.reject) self.button_box.button(QDialogButtonBox.Apply).clicked.connect(self.apply_changes) button_layout.addWidget(self.button_box) layout.addLayout(button_layout) self.setLayout(layout) def load_plugins_config(self): """加载插件配置""" try: # 检查文件是否存在 if not os.path.exists(self.plugins_config_path): print(f"插件配置文件不存在: {self.plugins_config_path}") return with open(self.plugins_config_path, 'r', encoding='utf-8') as f: config = yaml.safe_load(f) enabled_plugins = config.get('enabled', []) # 清空列表 self.plugins_list.clear() # 添加启用的插件 for plugin in enabled_plugins: item = QListWidgetItem(plugin) item.setFlags(item.flags() | Qt.ItemIsUserCheckable) item.setCheckState(Qt.Checked) self.plugins_list.addItem(item) except Exception as e: print(f"加载插件配置失败: {e}") def select_all_plugins(self): """全选所有插件""" for i in range(self.plugins_list.count()): item = self.plugins_list.item(i) item.setCheckState(Qt.Checked) def deselect_all_plugins(self): """取消选择所有插件""" for i in range(self.plugins_list.count()): item = self.plugins_list.item(i) item.setCheckState(Qt.Unchecked) def apply_changes(self): """应用更改到配置文件,保留原有配置结构""" try: # 检查文件是否存在 if not os.path.exists(self.plugins_config_path): print(f"插件配置文件不存在: {self.plugins_config_path}") return # 读取完整配置 with open(self.plugins_config_path, 'r', encoding='utf-8') as f: config = yaml.safe_load(f) or {} # 获取选中的插件 enabled_plugins = [] for i in range(self.plugins_list.count()): item = self.plugins_list.item(i) if item.checkState() == Qt.Checked: enabled_plugins.append(item.text()) # 只更新enabled部分,保留其他配置 config['enabled'] = enabled_plugins # 保存配置时保留原有格式(尽可能) with open(self.plugins_config_path, 'w', encoding='utf-8') as f: yaml.dump(config, f, default_flow_style=False, allow_unicode=True, sort_keys=False, indent=4) print("插件配置已保存") except Exception as e: print(f"保存插件配置失败: {e}") def accept(self): """点击确定按钮时保存并关闭""" self.apply_changes() super().accept() 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 class AssemblyModeSelectionDialog(QDialog): """拆装模式选择对话框""" def __init__(self, parent=None): super().__init__(parent) self.selected_mode = "training" # 默认选择训练模式 self.setupUI() def setupUI(self): self.setWindowTitle("选择拆装模式") self.setFixedSize(400, 300) self.setModal(True) layout = QVBoxLayout(self) # 标题 title_label = QLabel("请选择拆装交互模式") title_label.setStyleSheet("font-size: 16px; font-weight: bold; color: #2E86C1; margin: 10px;") title_label.setAlignment(Qt.AlignCenter) layout.addWidget(title_label) # 模式选择组 mode_group = QGroupBox("模式选择") mode_layout = QVBoxLayout(mode_group) # 训练模式 self.training_radio = QRadioButton("训练模式") self.training_radio.setChecked(True) # 默认选中 self.training_radio.setStyleSheet("font-size: 14px; margin: 5px;") mode_layout.addWidget(self.training_radio) training_desc = QTextEdit() training_desc.setMaximumHeight(60) training_desc.setReadOnly(True) training_desc.setPlainText("• 显示详细的步骤描述和操作提示\n• 提供工具选择的正确性提示\n• 播放语音指导") training_desc.setStyleSheet("background-color: #f0f8ff; border: 1px solid #ccc; margin-left: 20px;") mode_layout.addWidget(training_desc) # 考核模式 self.exam_radio = QRadioButton("考核模式") self.exam_radio.setStyleSheet("font-size: 14px; margin: 5px;") mode_layout.addWidget(self.exam_radio) exam_desc = QTextEdit() exam_desc.setMaximumHeight(60) exam_desc.setReadOnly(True) exam_desc.setPlainText("• 不显示步骤描述\n• 工具选择错误时不提示,直接扣分\n• 不播放语音指导") exam_desc.setStyleSheet("background-color: #fff5f5; border: 1px solid #ccc; margin-left: 20px;") mode_layout.addWidget(exam_desc) layout.addWidget(mode_group) # 按钮 button_layout = QHBoxLayout() self.ok_button = QPushButton("开始") self.ok_button.setStyleSheet(""" QPushButton { background-color: #27AE60; color: white; font-size: 14px; font-weight: bold; padding: 8px 20px; border: none; border-radius: 4px; } QPushButton:hover { background-color: #2ECC71; } """) self.ok_button.clicked.connect(self.accept) self.cancel_button = QPushButton("取消") self.cancel_button.setStyleSheet(""" QPushButton { background-color: #95A5A6; color: white; font-size: 14px; padding: 8px 20px; border: none; border-radius: 4px; } QPushButton:hover { background-color: #BDC3C7; } """) self.cancel_button.clicked.connect(self.reject) button_layout.addStretch() button_layout.addWidget(self.ok_button) button_layout.addWidget(self.cancel_button) layout.addLayout(button_layout) # 连接单选按钮信号 self.training_radio.toggled.connect(self.on_mode_changed) self.exam_radio.toggled.connect(self.on_mode_changed) def on_mode_changed(self): """模式改变时的处理""" if self.training_radio.isChecked(): self.selected_mode = "training" elif self.exam_radio.isChecked(): self.selected_mode = "exam" print(f"🔄 模式选择改变: {self.selected_mode}") def get_selected_mode(self): """获取选中的模式""" return self.selected_mode def setup_main_window(world,path = None): """设置主窗口的便利函数""" app = QApplication.instance() if app is None: # 修复 Windows 下 WebEngine 与 OpenGL 上下文共享问题 QCoreApplication.setAttribute(Qt.AA_ShareOpenGLContexts) if sys.platform == 'win32': QCoreApplication.setAttribute(Qt.AA_UseDesktopOpenGL) # 添加 Chromium 参数禁用硬件加速,避免与 Panda3D 的 OpenGL 冲突 sys.argv.extend([ '--disable-gpu', '--disable-software-rasterizer', '--disable-gpu-compositing', '--disable-accelerated-2d-canvas', '--num-raster-threads=1' ]) app = QApplication(sys.argv) print("✓ QApplication 已创建,QtWebEngine 硬件加速已禁用") main_window = MainWindow(world) main_window.show() from main import openProjectForPath if path: openProjectForPath(path,main_window) return app, main_window