417 lines
14 KiB
Python
417 lines
14 KiB
Python
"""
|
||
自定义Qt部件模块
|
||
|
||
包含所有自定义的Qt界面组件:
|
||
- NewProjectDialog: 新建项目对话框
|
||
- CustomPanda3DWidget: 自定义Panda3D显示部件
|
||
- CustomFileView: 自定义文件浏览器
|
||
- CustomTreeWidget: 自定义场景树部件
|
||
"""
|
||
|
||
import os
|
||
import re
|
||
from PyQt5.QtWidgets import (QDialog, QVBoxLayout, QGroupBox, QHBoxLayout,
|
||
QLineEdit, QPushButton, QLabel, QDialogButtonBox,
|
||
QTreeView, QTreeWidget, QTreeWidgetItem, QWidget,
|
||
QFileDialog, QMessageBox)
|
||
from PyQt5.QtCore import Qt, QUrl
|
||
from PyQt5.QtGui import QDrag, QPainter, QPixmap
|
||
from PyQt5.sip import wrapinstance
|
||
from QPanda3D.QPanda3DWidget import QPanda3DWidget
|
||
|
||
|
||
class NewProjectDialog(QDialog):
|
||
"""新建项目对话框"""
|
||
|
||
def __init__(self, parent=None):
|
||
super().__init__(parent)
|
||
self.setWindowTitle("新建项目")
|
||
self.setMinimumWidth(500)
|
||
|
||
# 创建布局
|
||
layout = QVBoxLayout(self)
|
||
|
||
# 创建路径选择部分
|
||
pathGroup = QGroupBox("项目路径")
|
||
pathLayout = QHBoxLayout()
|
||
self.pathEdit = QLineEdit()
|
||
self.pathEdit.setReadOnly(True)
|
||
browseButton = QPushButton("浏览...")
|
||
browseButton.clicked.connect(self.browsePath)
|
||
pathLayout.addWidget(self.pathEdit)
|
||
pathLayout.addWidget(browseButton)
|
||
pathGroup.setLayout(pathLayout)
|
||
layout.addWidget(pathGroup)
|
||
|
||
# 创建项目名称部分
|
||
nameGroup = QGroupBox("项目名称")
|
||
nameLayout = QVBoxLayout()
|
||
self.nameEdit = QLineEdit()
|
||
self.nameEdit.setText("新项目")
|
||
self.nameEdit.selectAll()
|
||
nameLayout.addWidget(self.nameEdit)
|
||
# 添加提示标签
|
||
self.tipLabel = QLabel("项目名称只能包含字母、数字、下划线、中划线和中文")
|
||
self.tipLabel.setStyleSheet("color: gray;")
|
||
nameLayout.addWidget(self.tipLabel)
|
||
nameGroup.setLayout(nameLayout)
|
||
layout.addWidget(nameGroup)
|
||
|
||
# 添加按钮
|
||
buttonBox = QDialogButtonBox(
|
||
QDialogButtonBox.Ok | QDialogButtonBox.Cancel
|
||
)
|
||
buttonBox.accepted.connect(self.validate)
|
||
buttonBox.rejected.connect(self.reject)
|
||
layout.addWidget(buttonBox)
|
||
|
||
# 存储结果
|
||
self.projectPath = ""
|
||
self.projectName = ""
|
||
|
||
def browsePath(self):
|
||
"""浏览选择项目路径"""
|
||
path = QFileDialog.getExistingDirectory(self, "选择项目路径")
|
||
if path:
|
||
self.pathEdit.setText(path)
|
||
|
||
def validate(self):
|
||
"""验证输入并关闭对话框"""
|
||
# 获取并验证路径
|
||
self.projectPath = self.pathEdit.text()
|
||
if not self.projectPath:
|
||
QMessageBox.warning(self, "错误", "请选择项目路径!")
|
||
return
|
||
|
||
# 获取并验证项目名称
|
||
self.projectName = self.nameEdit.text().strip()
|
||
if not self.projectName:
|
||
QMessageBox.warning(self, "错误", "请输入项目名称!")
|
||
return
|
||
|
||
# 验证项目名称格式
|
||
if not re.match(r'^[a-zA-Z0-9_\-\u4e00-\u9fa5]+$', self.projectName):
|
||
QMessageBox.warning(self, "错误",
|
||
"项目名称只能包含字母、数字、下划线、中划线和中文!")
|
||
return
|
||
|
||
# 检查项目是否已存在
|
||
full_path = os.path.join(self.projectPath, self.projectName)
|
||
if os.path.exists(full_path):
|
||
QMessageBox.warning(self, "错误", "项目已存在!")
|
||
return
|
||
|
||
self.accept()
|
||
|
||
|
||
class CustomPanda3DWidget(QPanda3DWidget):
|
||
"""自定义Panda3D显示部件"""
|
||
|
||
def __init__(self, world, parent=None):
|
||
if parent is None:
|
||
parent = wrapinstance(0, QWidget)
|
||
super().__init__(world, parent)
|
||
self.world = world
|
||
self.setFocusPolicy(Qt.StrongFocus) # 允许接收键盘焦点
|
||
self.setAcceptDrops(True) # 允许接收拖放
|
||
self.setMouseTracking(True) # 启用鼠标追踪
|
||
|
||
# 让world引用这个widget,以便获取准确的尺寸
|
||
if hasattr(world, 'setQtWidget'):
|
||
world.setQtWidget(self)
|
||
|
||
def getActualSize(self):
|
||
"""获取Qt部件的实际渲染尺寸"""
|
||
return (self.width(), self.height())
|
||
|
||
def dragEnterEvent(self, event):
|
||
"""处理拖拽进入事件"""
|
||
# 检查是否是文件拖拽
|
||
if event.mimeData().hasUrls():
|
||
# 检查是否包含支持的模型文件
|
||
for url in event.mimeData().urls():
|
||
filepath = url.toLocalFile()
|
||
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
|
||
event.acceptProposedAction()
|
||
return
|
||
event.ignore()
|
||
|
||
def dragMoveEvent(self, event):
|
||
"""处理拖拽移动事件"""
|
||
if event.mimeData().hasUrls():
|
||
event.acceptProposedAction()
|
||
else:
|
||
event.ignore()
|
||
|
||
def dropEvent(self, event):
|
||
"""处理拖放事件"""
|
||
if event.mimeData().hasUrls():
|
||
for url in event.mimeData().urls():
|
||
filepath = url.toLocalFile()
|
||
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
|
||
self.world.importModel(filepath)
|
||
event.acceptProposedAction()
|
||
else:
|
||
event.ignore()
|
||
|
||
def wheelEvent(self, event):
|
||
"""处理滚轮事件"""
|
||
if event.angleDelta().y() > 0:
|
||
# 滚轮向前滚动
|
||
self.world.wheelForward()
|
||
else:
|
||
# 滚轮向后滚动
|
||
self.world.wheelBackward()
|
||
event.accept()
|
||
|
||
def mousePressEvent(self, event):
|
||
"""处理 Qt 鼠标按下事件"""
|
||
if event.button() == Qt.LeftButton:
|
||
self.world.mousePressEventLeft({
|
||
'x': event.x(),
|
||
'y': event.y()
|
||
})
|
||
elif event.button() == Qt.RightButton:
|
||
self.world.mousePressEventRight({
|
||
'x': event.x(),
|
||
'y': event.y()
|
||
})
|
||
elif event.button() == Qt.MiddleButton: # 添加滑轮按下事件处理
|
||
self.world.mousePressEventMiddle({
|
||
'x': event.x(),
|
||
'y': event.y()
|
||
})
|
||
event.accept()
|
||
|
||
def mouseReleaseEvent(self, event):
|
||
"""处理 Qt 鼠标释放事件"""
|
||
if event.button() == Qt.LeftButton:
|
||
self.world.mouseReleaseEventLeft({
|
||
'x': event.x(),
|
||
'y': event.y()
|
||
})
|
||
elif event.button() == Qt.RightButton:
|
||
self.world.mouseReleaseEventRight({
|
||
'x': event.x(),
|
||
'y': event.y()
|
||
})
|
||
elif event.button() == Qt.MiddleButton: # 添加滑轮释放事件处理
|
||
self.world.mouseReleaseEventMiddle({
|
||
'x': event.x(),
|
||
'y': event.y()
|
||
})
|
||
event.accept()
|
||
|
||
def mouseMoveEvent(self, event):
|
||
"""处理 Qt 鼠标移动事件"""
|
||
if hasattr(self, 'world') and self.world is not None:
|
||
self.world.mouseMoveEvent({
|
||
'x': event.x(),
|
||
'y': event.y()
|
||
})
|
||
event.accept()
|
||
|
||
def cleanup(self):
|
||
"""清理Panda3D资源"""
|
||
try:
|
||
print("🧹 清理CustomPanda3DWidget资源...")
|
||
|
||
# 清理world资源
|
||
if hasattr(self, 'world') and self.world:
|
||
# 如果world有cleanup方法,调用它
|
||
if hasattr(self.world, 'cleanup'):
|
||
self.world.cleanup()
|
||
# 清理world引用
|
||
self.world = None
|
||
|
||
# 调用父类的清理方法(如果存在)
|
||
if hasattr(super(), 'cleanup'):
|
||
super().cleanup()
|
||
|
||
print("✅ CustomPanda3DWidget资源清理完成")
|
||
|
||
except Exception as e:
|
||
print(f"⚠️ 清理CustomPanda3DWidget资源时出错: {e}")
|
||
|
||
|
||
class CustomFileView(QTreeView):
|
||
"""自定义文件浏览器"""
|
||
|
||
def __init__(self, world, parent=None):
|
||
if parent is None:
|
||
parent = wrapinstance(0, QWidget)
|
||
super().__init__(parent)
|
||
self.world = world
|
||
self.setDragEnabled(True) # 启用拖拽
|
||
self.setSelectionMode(QTreeView.ExtendedSelection) # 允许多选
|
||
self.setDragDropMode(QTreeView.DragOnly) # 只允许拖出,不允许拖入
|
||
|
||
def startDrag(self, supportedActions):
|
||
"""开始拖拽操作"""
|
||
# 获取选中的文件
|
||
indexes = self.selectedIndexes()
|
||
if not indexes:
|
||
return
|
||
|
||
# 只处理文件名列
|
||
indexes = [idx for idx in indexes if idx.column() == 0]
|
||
|
||
# 创建 MIME 数据
|
||
mimeData = self.model().mimeData(indexes)
|
||
|
||
# 检查是否包含支持的模型文件
|
||
urls = []
|
||
for index in indexes:
|
||
filepath = self.model().filePath(index)
|
||
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
|
||
urls.append(QUrl.fromLocalFile(filepath))
|
||
|
||
if not urls:
|
||
return
|
||
|
||
# 设置 URL 列表
|
||
mimeData.setUrls(urls)
|
||
|
||
# 创建拖拽对象
|
||
drag = QDrag(self)
|
||
drag.setMimeData(mimeData)
|
||
|
||
# 设置拖拽图标(可选)
|
||
pixmap = QPixmap(32, 32)
|
||
pixmap.fill(Qt.transparent)
|
||
painter = QPainter(pixmap)
|
||
painter.drawText(pixmap.rect(), Qt.AlignCenter, str(len(urls)))
|
||
painter.end()
|
||
drag.setPixmap(pixmap)
|
||
|
||
# 执行拖拽
|
||
drag.exec_(supportedActions)
|
||
|
||
def mouseDoubleClickEvent(self, event):
|
||
"""处理双击事件"""
|
||
index = self.indexAt(event.pos())
|
||
if index.isValid():
|
||
model = self.model()
|
||
filepath = model.filePath(index)
|
||
|
||
# 检查是否是模型文件
|
||
if filepath.lower().endswith(('.egg', '.bam', '.obj', '.fbx', '.gltf', '.glb')):
|
||
self.world.importModel(filepath)
|
||
else:
|
||
print("不支持的文件类型")
|
||
super().mouseDoubleClickEvent(event)
|
||
|
||
|
||
class CustomTreeWidget(QTreeWidget):
|
||
"""自定义场景树部件"""
|
||
|
||
def __init__(self, world, parent=None):
|
||
if parent is None:
|
||
parent = wrapinstance(0, QWidget)
|
||
super().__init__(parent)
|
||
self.world = world
|
||
self.setDragEnabled(True)
|
||
self.setAcceptDrops(True)
|
||
self.setDragDropMode(QTreeWidget.InternalMove)
|
||
|
||
def dropEvent(self, event):
|
||
"""处理拖放事件"""
|
||
# 获取拖动的项和目标项
|
||
dragged_item = self.currentItem()
|
||
target_item = self.itemAt(event.pos())
|
||
|
||
if not dragged_item or not target_item:
|
||
event.ignore()
|
||
return
|
||
|
||
# 获取节点引用
|
||
dragged_node = dragged_item.data(0, Qt.UserRole)
|
||
|
||
# 如果目标是模型根节点,使用 render 作为新父节点
|
||
if target_item.text(0) == "模型":
|
||
target_node = self.world.render
|
||
else:
|
||
target_node = target_item.data(0, Qt.UserRole)
|
||
|
||
if not dragged_node or not target_node:
|
||
event.ignore()
|
||
return
|
||
|
||
# 检查是否是有效的父子关系
|
||
if self.isValidParentChild(dragged_item, target_item):
|
||
# 保存当前的世界坐标
|
||
world_pos = dragged_node.getPos(self.world.render)
|
||
|
||
# 更新场景图中的父子关系
|
||
dragged_node.wrtReparentTo(target_node)
|
||
|
||
# 接受拖放事件,更新树形控件
|
||
super().dropEvent(event)
|
||
|
||
# 更新属性面板
|
||
self.world.updatePropertyPanel(dragged_item)
|
||
else:
|
||
event.ignore()
|
||
|
||
def isValidParentChild(self, dragged_item, target_item):
|
||
"""检查是否是有效的父子关系"""
|
||
# 不能拖放到自己上
|
||
if dragged_item == target_item:
|
||
return False
|
||
|
||
# 不能拖放到自己的子节点上
|
||
parent = target_item
|
||
while parent:
|
||
if parent == dragged_item:
|
||
return False
|
||
parent = parent.parent()
|
||
|
||
# 检查目标项
|
||
if target_item.text(0) == "场景":
|
||
return False # 不能拖放到场景根节点
|
||
|
||
# 允许拖放到模型根节点或其他模型节点
|
||
if target_item.text(0) == "模型":
|
||
return True
|
||
|
||
# 检查目标项的父节点
|
||
target_parent = target_item.parent()
|
||
if not target_parent:
|
||
return False
|
||
|
||
# 允许在模型节点下的任何位置调整父子关系
|
||
while target_parent:
|
||
if target_parent.text(0) == "模型":
|
||
return True
|
||
target_parent = target_parent.parent()
|
||
|
||
return False
|
||
|
||
def dragEnterEvent(self, event):
|
||
"""处理拖入事件"""
|
||
if event.source() == self:
|
||
event.accept()
|
||
else:
|
||
event.ignore()
|
||
|
||
def dragMoveEvent(self, event):
|
||
"""处理拖动事件"""
|
||
if event.source() == self:
|
||
event.accept()
|
||
else:
|
||
event.ignore()
|
||
|
||
def keyPressEvent(self, event):
|
||
"""处理键盘按键事件"""
|
||
if event.key() == Qt.Key_Delete:
|
||
currentItem = self.currentItem()
|
||
if currentItem and currentItem.parent():
|
||
# 检查是否是模型节点或其子节点
|
||
if self.world.isModelOrChild(currentItem):
|
||
nodePath = currentItem.data(0, Qt.UserRole)
|
||
if nodePath:
|
||
print("正在删除节点...")
|
||
self.world.deleteNode(nodePath, currentItem)
|
||
print("删除完成")
|
||
else:
|
||
super().keyPressEvent(event) |