EG/ui/main_window.py

3441 lines
137 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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