This commit is contained in:
ayuan9957 2026-02-28 15:11:22 +08:00
parent 036b68ef41
commit 5a9e31a195
16 changed files with 342 additions and 4809 deletions

View File

@ -496,7 +496,7 @@ class MoveGizmo(DirectObject):
def undo_last_move(self):
"""
Undo the last committed move.
Default hotkey (from Qt -> Panda3D translation): Ctrl+Z => 'control-z'.
Default hotkey: Ctrl+Z => 'control-z'.
"""
if not self._move_undo_stack:
self._log("undo_last_move: stack empty")
@ -530,7 +530,7 @@ class MoveGizmo(DirectObject):
# --- Mouse helpers -------------------------------------------------
def _get_normalized_mouse(self, extra) -> Optional[Point3]:
"""
Convert Qt pixel coordinates (from QPanda3DWidget) to Panda's
Convert external UI pixel coordinates to Panda's
normalized device coordinates (-1..1), or fall back to
mouseWatcherNode when available.
"""
@ -538,14 +538,14 @@ class MoveGizmo(DirectObject):
mouse = self.world.mouseWatcherNode.getMouse()
return Point3(mouse.x, mouse.y, 0)
# 1) Extra payload from QPanda3DWidget (pixels)
# 1) Extra payload from external UI (pixels)
if isinstance(extra, dict) and "x" in extra and "y" in extra:
parent = getattr(self.world, "parent", None)
if parent is not None:
w = max(parent.width(), 1)
h = max(parent.height(), 1)
nx = (extra["x"] / w) * 2.0 - 1.0
# Qt origin is topleft, Panda origin is center with +Y up
# Pixel origin is top-left; Panda origin is center with +Y up.
ny = 1.0 - (extra["y"] / h) * 2.0
return Point3(nx, ny, 0)

View File

@ -54,10 +54,10 @@ from .events import GizmoEvent
- 控件会根据摄像机距离自动缩放屏幕大小保持近似不变
集成方式示例
from QPanda3D.Panda3DWorld import Panda3DWorld
from QPanda3DExamples.rotate_gizmo import RotateGizmo
from direct.showbase.ShowBase import ShowBase
from TransformGizmo.rotate_gizmo import RotateGizmo
world = Panda3DWorld()
world = ShowBase()
model_np = world.render.attachNewNode("Box")
# ... 在 model_np 下加载模型
@ -72,7 +72,7 @@ from .events import GizmoEvent
鼠标事件要求
- 本类默认监听 Panda3D "mouse1" / "mouse1-up" / "mouse-move" 事件
- 如果从 Qt / 自定义 UI 传递鼠标像素坐标可以在发送事件时传入 extra 字典
- 如果从外部 UI 传递鼠标像素坐标可以在发送事件时传入 extra 字典
messenger.send("mouse1", [{"x": mouse_x, "y": mouse_y}])
本类会自动将像素坐标转换到 [-1, 1] 的标准化设备坐标
"""
@ -955,7 +955,7 @@ class RotateGizmo(DirectObject):
"""
将鼠标转换到 Panda3D 的标准化设备坐标 [-1, 1]
优先使用 Qt / 外部 UI 传入的像素坐标extra 字典
优先使用外部 UI 传入的像素坐标extra 字典
如果没有则回退到 Panda3D mouseWatcherNode
"""
if self.world.mouseWatcherNode.hasMouse():
@ -969,7 +969,7 @@ class RotateGizmo(DirectObject):
w = max(parent.width(), 1)
h = max(parent.height(), 1)
nx = (extra["x"] / w) * 2.0 - 1.0
# Qt 原点在左上Panda3D 原点在中心Y 轴向上
# 像素坐标原点在左上Panda3D 原点在中心Y 轴向上
ny = 1.0 - (extra["y"] / h) * 2.0
return Point3(nx, ny, 0.0)

View File

@ -48,10 +48,10 @@ from .events import GizmoEvent
- 控件会根据摄像机距离自动缩放屏幕大小保持近似不变
集成方式示例
from QPanda3D.Panda3DWorld import Panda3DWorld
from QPanda3DExamples.scale_gizmo import ScaleGizmo
from direct.showbase.ShowBase import ShowBase
from TransformGizmo.scale_gizmo import ScaleGizmo
world = Panda3DWorld()
world = ShowBase()
model_np = world.render.attachNewNode("Box")
# ... 在 model_np 下加载模型
@ -66,7 +66,7 @@ from .events import GizmoEvent
鼠标事件要求
- 本类默认监听 Panda3D "mouse1" / "mouse1-up" / "mouse-move" 事件
- 如果从 Qt / 自定义 UI 传递鼠标像素坐标可以在发送事件时传入 extra 字典
- 如果从外部 UI 传递鼠标像素坐标可以在发送事件时传入 extra 字典
messenger.send("mouse1", [{"x": mouse_x, "y": mouse_y}])
本类会自动将像素坐标转换到 [-1, 1] 的标准化设备坐标
"""

View File

@ -1,18 +1,16 @@
# 修改后的 InfoPanelManager.py
from xml.sax.handler import property_encoding
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox
from direct.gui.DirectGui import DirectFrame, DirectLabel
from direct.showbase.ShowBaseGlobal import aspect2d
from panda3d.core import TextNode, Vec4, NodePath
from direct.showbase.DirectObject import DirectObject
import threading
# 修改后的 InfoPanelManager.py
from direct.gui.DirectGui import DirectFrame, DirectLabel
from direct.showbase.ShowBaseGlobal import aspect2d
from panda3d.core import TextNode, Vec4, NodePath
from direct.showbase.DirectObject import DirectObject
import threading
import json
import time
from urllib.parse import urlparse
import requests
import time
from urllib.parse import urlparse
import requests
from scene.tree_roles import TREE_USER_ROLE
class InfoPanelManager(DirectObject):
"""信息面板管理器用于创建和管理2D信息面板"""
@ -215,9 +213,15 @@ class InfoPanelManager(DirectObject):
root_item = None
for i in range(tree_widget.topLevelItemCount()):
item = tree_widget.topLevelItem(i)
if item.text(0) == "render" or item.data(0, Qt.UserRole) == self.world.render:
root_item = item
break
item_node = None
if hasattr(item, "data"):
try:
item_node = item.data(0, TREE_USER_ROLE)
except Exception:
item_node = None
if item.text(0) == "render" or item_node == self.world.render:
root_item = item
break
if root_item:
# 使用现有的 add_node_to_tree_widget 方法添加节点
@ -1306,11 +1310,12 @@ class InfoPanelManager(DirectObject):
print("✓ 示例天气信息面板已创建")
except Exception as e:
print(f"✗ 创建示例天气信息面板失败: {e}")
import traceback
traceback.print_exc()
QMessageBox.critical(self, "错误", f"创建示例天气信息面板时出错: {str(e)}")
except Exception as e:
print(f"✗ 创建示例天气信息面板失败: {e}")
import traceback
traceback.print_exc()
if hasattr(self.world, "add_error_message"):
self.world.add_error_message(f"创建示例天气信息面板时出错: {str(e)}")
def getRealWeatherData(self):
"""获取真实天气数据"""

View File

@ -11,7 +11,7 @@ from direct.showbase.ShowBaseGlobal import globalClock
from panda3d.core import (Vec3, Point3, Point2, LineSegs, ColorAttrib, RenderState,
DepthTestAttrib, CollisionTraverser, CollisionHandlerQueue,
CollisionNode, CollisionRay, GeomNode, BitMask32, Material, LColor, DepthWriteAttrib,
TransparencyAttrib, Vec4, CollisionCapsule)
TransparencyAttrib, Vec4, CollisionCapsule, WindowProperties)
from direct.task.TaskManagerGlobal import taskMgr
import math
from core.selection_outline import SelectionOutlineManager
@ -93,38 +93,16 @@ class SelectionSystem:
# ==================== 光标设置 ====================
def _setCursor(self,cursor_type):
try:
from PyQt5.QtCore import Qt
if self._current_cursor == cursor_type:
return
if hasattr(self.world,'main_window') and self.world.main_window:
main_window = self.world.main_window
else:
from PyQt5.QtWidgets import QApplication
main_window = QApplication.activeWindow()
if not main_window:
windows = QApplication.topLevelWindows()
for window in windows:
if hasattr(window,'isVisible') and window.isVisible():
main_window = window
break
if main_window:
if cursor_type == "crosshair":
main_window.setCursor(Qt.CrossCursor)
elif cursor_type == "size_hor":
main_window.setCursor(Qt.SizeHorCursor)
elif cursor_type == "size_ver":
main_window.setCursor(Qt.SizeVerCursor)
elif cursor_type == "size_all":
main_window.setCursor(Qt.SizeAllCursor)
elif cursor_type == "pointing_hand":
main_window.setCursor(Qt.PointingHandCursor)
else:
main_window.unsetCursor()
self._current_cursor = cursor_type
#print(f"光标已设置:{cursor_type}")
self._current_cursor = cursor_type
else:
print("警告:无法获取主窗口,光标设置失败")
if not hasattr(self.world, "win") or not self.world.win:
return
# Panda3D 原生接口不支持直接切换系统光标形状,这里保留状态并确保光标可见。
props = WindowProperties()
props.setCursorHidden(False)
self.world.win.requestProperties(props)
self._current_cursor = cursor_type
except Exception as e:
print(f"设置光标失败{e}")
def _resetCursor(self):

View File

@ -91,10 +91,10 @@ class VRTestMode:
self.vr_manager._disable_main_cam()
# 设置高帧率用于测试
if hasattr(self.vr_manager.world, 'qtWidget') and self.vr_manager.world.qtWidget:
if hasattr(self.vr_manager.world.qtWidget, 'synchronizer'):
self.vr_manager.world.qtWidget.synchronizer.setInterval(int(1000/144))
print("✓ 测试模式:Qt Timer设置为144Hz")
host_widget = getattr(self.vr_manager.world, "host_widget", None)
if host_widget and hasattr(host_widget, "synchronizer"):
host_widget.synchronizer.setInterval(int(1000 / 144))
print("✓ 测试模式:渲染同步器设置为144Hz")
# 初始化测试显示系统
if not self._initialize_test_display():

View File

@ -1893,12 +1893,11 @@ class VRManager(DirectObject):
print(" 注意Submit后立即调用WaitGetPoses是错误实现")
self.set_prediction_time(11.0) # 11ms预测时间 - OpenVR标准值平衡准确性和延迟
# 🚀 动态调整Qt Timer频率以支持VR
if hasattr(self.world, 'qtWidget') and self.world.qtWidget:
if hasattr(self.world.qtWidget, 'synchronizer'):
# 设置为144Hz让OpenVR控制实际渲染节奏
self.world.qtWidget.synchronizer.setInterval(int(1000/144))
print("✓ Qt Timer调整为144Hz让OpenVR控制VR渲染节奏")
# 🚀 动态调整宿主同步器频率以支持VR
host_widget = getattr(self.world, "host_widget", None)
if host_widget and hasattr(host_widget, "synchronizer"):
host_widget.synchronizer.setInterval(int(1000 / 144))
print("✓ 渲染同步器调整为144Hz让OpenVR控制VR渲染节奏")
# 🔧 关键修复检测并重建缺失的手柄visualizer
# 当渲染模式切换时visualizer可能被清理但控制器对象仍存在
@ -1952,11 +1951,11 @@ class VRManager(DirectObject):
# 恢复主相机
self._enable_main_cam()
# 恢复Qt Timer到60FPS
if hasattr(self.world, 'qtWidget') and self.world.qtWidget:
if hasattr(self.world.qtWidget, 'synchronizer'):
self.world.qtWidget.synchronizer.setInterval(int(1000/60))
print("Qt Timer恢复为60Hz")
# 恢复渲染同步器到60FPS
host_widget = getattr(self.world, "host_widget", None)
if host_widget and hasattr(host_widget, "synchronizer"):
host_widget.synchronizer.setInterval(int(1000 / 60))
print("渲染同步器恢复为60Hz")
print("✅ VR模式已禁用手柄模型已隐藏")

View File

@ -40,7 +40,7 @@ class CoreWorld(ShowBase):
global _global_render_pipeline
# 初始化基础属性
self.qtWidget = None # Qt部件引用用于获取准确的渲染区域尺寸)
self.host_widget = None # 外部宿主窗口引用(用于获取准确渲染区域尺寸)
# 设置基本配置
loadPrcFileData("", "show-frame-rate-meter 0")
@ -686,16 +686,17 @@ class CoreWorld(ShowBase):
print(f"警告: 加载中文字体时发生错误: {e}")
self.chinese_font = None
def setQtWidget(self, widget):
"""设置Qt部件引用"""
self.qtWidget = widget
print(f"✓ 设置Qt部件引用: {widget}")
def setHostWidget(self, widget):
"""设置外部宿主窗口引用。"""
self.host_widget = widget
print(f"✓ 设置宿主窗口引用: {widget}")
def getWindowSize(self):
"""获取准确的窗口尺寸"""
if self.qtWidget:
# 优先使用Qt部件的实际尺寸
width, height = self.qtWidget.getActualSize()
widget = self.host_widget
if widget and hasattr(widget, "getActualSize"):
# 优先使用宿主窗口的实际尺寸
width, height = widget.getActualSize()
if width > 0 and height > 0:
return width, height
@ -754,30 +755,28 @@ class CoreWorld(ShowBase):
self.lastMouseX = evt['x']
self.lastMouseY = evt['y']
#
# # 通过 Qt 窗口隐藏光标并捕获鼠标
# # 通过宿主窗口隐藏光标并捕获鼠标
# try:
# if hasattr(self, 'qtWidget') and self.qtWidget:
# from PyQt5.QtCore import Qt
# self.qtWidget.setCursor(Qt.BlankCursor)
# if hasattr(self, 'host_widget') and self.host_widget:
# self.host_widget.setCursorHidden(True)
# # 捕获鼠标,使其无法离开窗口
# self.qtWidget.grabMouse()
# self.host_widget.grabMouse()
# except Exception as e:
# print(f"通过 Qt 隐藏光标时出错: {e}")
# print(f"隐藏光标时出错: {e}")
def mouseReleaseEventRight(self, evt):
"""处理鼠标右键释放事件"""
#print("右键释放")
self.mouseRightPressed = False
# # 恢复 Qt 窗口光标并释放鼠标捕获
# # 恢复宿主窗口光标并释放鼠标捕获
# try:
# if hasattr(self, 'qtWidget') and self.qtWidget:
# from PyQt5.QtCore import Qt
# self.qtWidget.unsetCursor() # 恢复默认光标
# if hasattr(self, 'host_widget') and self.host_widget:
# self.host_widget.setCursorHidden(False) # 恢复默认光标
# # 释放鼠标捕获
# self.qtWidget.releaseMouse()
# self.host_widget.releaseMouse()
# except Exception as e:
# print(f"恢复 Qt 光标时出错: {e}")
# print(f"恢复光标时出错: {e}")
def mouseMoveEvent(self, evt):
"""处理鼠标移动事件 - 只处理相机旋转"""
@ -966,7 +965,7 @@ class CoreWorld(ShowBase):
return None
# 创建材质编辑器组件
# 使用纯 Panda3D 实现,不依赖 PyQt5
# 使用纯 Panda3D 实现,不依赖外部 GUI 框架
print("材质编辑器组件已创建(使用 Panda3D 原生实现)")
material_widget.setLayout(layout)

View File

@ -64,13 +64,13 @@ python main.py
- 选择虚拟环境中的Python路径
3. **验证环境**检查状态栏显示正确的Python版本
## 📦 项目依赖说明
- **Panda3D**: 3D图形引擎
- **PyQt5/PySide6**: GUI框架
- **Pillow**: 图像处理
- **python-dotenv**: 环境变量管理
- **pyassimp**: 3D模型加载
## 📦 项目依赖说明
- **Panda3D**: 3D图形引擎
- **imgui-bundle / p3dimgui**: ImGui GUI框架
- **Pillow**: 图像处理
- **python-dotenv**: 环境变量管理
- **pyassimp**: 3D模型加载
## 🌍 跨平台注意事项
@ -83,8 +83,8 @@ python main.py
**Q: 无法安装Panda3D**
A: 确保系统有OpenGL支持或使用conda安装
**Q: PyQt5无法运行**
A: 可能需要安装系统级GUI依赖
**Q: ImGui界面显示异常**
A: 检查显卡驱动/OpenGL支持并确认已安装 `imgui-bundle`
**Q: 导入错误?**
A: 检查Python版本需要3.10+
A: 检查Python版本需要3.10+

View File

@ -1,6 +1,5 @@
Panda3D>=1.10.15
PyQt5>=5.15.9
PySide6>=6.8.1
Pillow>=9.0.1
python-dotenv>=1.0.1
pyassimp>=5.2.5
Panda3D>=1.10.15
imgui-bundle
Pillow>=9.0.1
python-dotenv>=1.0.1
pyassimp>=5.2.5

View File

@ -5,11 +5,10 @@ channels:
dependencies:
- python=3.10
- pip
- pip:
- Panda3D>=1.10.15
- PyQt5>=5.15.9
- PySide6>=6.8.1
- Pillow>=9.0.1
- python-dotenv>=1.0.1
- pyassimp>=5.2.5
- pip:
- Panda3D>=1.10.15
- imgui-bundle
- Pillow>=9.0.1
- python-dotenv>=1.0.1
- pyassimp>=5.2.5
- six

View File

@ -58,33 +58,21 @@ PyGObject==3.42.1
PyJWT==2.3.0
pymacaroons==0.13.0
PyNaCl==1.5.0
pyparsing==2.4.7
PyQt5==5.15.9
pyqt5-plugins==5.15.9.2.3
PyQt5-Qt5==5.15.2
pyqt5-tools==5.15.9.3.3
PyQt5_sip==12.16.1
pyRFC3339==1.1
PySide6==6.8.1
PySide6_Addons==6.8.1
PySide6_Essentials==6.8.1
python-apt==2.4.0+ubuntu4
pyparsing==2.4.7
pyRFC3339==1.1
python-apt==2.4.0+ubuntu4
python-dateutil==2.8.1
python-debian==0.1.43+ubuntu1.1
python-dotenv==1.0.1
pytz==2022.1
pyxdg==0.27
PyYAML==5.4.1
QPanda3D==0.2.10
qt5-applications==5.15.2.2.3
qt5-tools==5.15.2.1.3
reportlab==3.6.8
PyYAML==5.4.1
reportlab==3.6.8
requests==2.25.1
SceneEditor==22.5
screen-resolution-extra==0.0.0
SecretStorage==3.3.1
shiboken6==6.8.1
six==1.16.0
SecretStorage==3.3.1
six==1.16.0
soupsieve==2.3.1
systemd-python==234
ubuntu-drivers-common==0.0.0

View File

@ -39,28 +39,45 @@ class SceneManagerConvertTilesMixin:
def _convertToGLBWithProgress(self, filepath):
"""带进度显示的GLB转换"""
try:
from PyQt5.QtWidgets import QProgressDialog, QApplication
from PyQt5.QtCore import Qt
# 创建进度对话框
progress = QProgressDialog("正在转换模型格式以获得更好的动画支持...", "取消", 0, 100)
progress.setWindowTitle("模型格式转换")
progress.setWindowModality(Qt.WindowModal)
progress.show()
QApplication.processEvents()
try:
result = self._convertToGLB(filepath, progress)
progress.hide()
return result
except Exception as e:
progress.hide()
print(f"转换过程出错: {e}")
class _ConsoleProgress:
def __init__(self, label):
self._value = 0
self._label = label
def setWindowTitle(self, title):
print(f"[GLB转换] {title}")
def setWindowModality(self, _):
return None
except ImportError:
# 如果没有 PyQt5直接转换
def show(self):
print(f"[GLB转换] {self._label}")
def hide(self):
print("[GLB转换] 完成")
def setValue(self, value):
value = int(value)
if value != self._value:
self._value = value
print(f"[GLB转换] 进度: {self._value}%")
def setLabelText(self, text):
if text != self._label:
self._label = text
print(f"[GLB转换] {self._label}")
progress = _ConsoleProgress("正在转换模型格式以获得更好的动画支持...")
progress.setWindowTitle("模型格式转换")
progress.show()
try:
result = self._convertToGLB(filepath, progress)
progress.hide()
return result
except Exception as e:
progress.hide()
print(f"转换过程出错: {e}")
return self._convertToGLB(filepath)
def _convertToGLB(self, filepath, progress=None):
@ -71,8 +88,6 @@ class SceneManagerConvertTilesMixin:
if progress:
progress.setValue(10)
progress.setLabelText("准备转换...")
from PyQt5.QtWidgets import QApplication
QApplication.processEvents()
# 准备输出路径
base_name = os.path.splitext(os.path.basename(filepath))[0]
@ -93,7 +108,6 @@ class SceneManagerConvertTilesMixin:
if progress:
progress.setValue(20)
progress.setLabelText("尝试 Blender 转换...")
QApplication.processEvents()
# 方法1: 使用 Blender 进行转换
if self._convertWithBlender(filepath, glb_path, progress):
@ -102,7 +116,6 @@ class SceneManagerConvertTilesMixin:
if progress:
progress.setValue(60)
progress.setLabelText("尝试 FBX2glTF 转换...")
QApplication.processEvents()
# 方法2: 使用 FBX2glTF (如果可用)
if self._convertWithFBX2glTF(filepath, glb_path, progress):
@ -111,7 +124,6 @@ class SceneManagerConvertTilesMixin:
if progress:
progress.setValue(80)
progress.setLabelText("尝试 Assimp 转换...")
QApplication.processEvents()
# 方法3: 使用 Assimp
if self._convertWithAssimp(filepath, glb_path, progress):

View File

@ -1,5 +1,5 @@
"""Shared tree data-role constants (Qt-independent)."""
"""Shared tree data-role constants."""
# Qt.ItemDataRole.UserRole
# Keep this value stable for legacy tree-item data payloads.
TREE_USER_ROLE = 256

View File

@ -1,342 +1,154 @@
"""
图标管理工具
负责统一管理应用程序中的所有图标
- 图标路径解析
- 图标缓存
- 图标预加载
- 图标错误处理
"""
import os
import sys
from typing import Dict, Optional
from PyQt5.QtGui import QIcon, QPixmap
from PyQt5.QtCore import QSize
class IconManager:
"""图标管理器类"""
def __init__(self):
"""初始化图标管理器"""
self.icon_cache: Dict[str, QIcon] = {}
self.icon_directory = self._get_icon_directory()
self.default_icon = None
# 预定义的图标映射
self.icon_map = {
# 主窗口图标
'app_logo': 'logo.png',
# 工具栏图标
'select_tool': 'select_tool.png',
'move_tool': 'move_tool.png',
'rotate_tool': 'rotate_tool.png',
'scale_tool': 'scale_tool.png',
# 菜单图标(如果有的话)
'new_file': 'new_file.png',
'open_file': 'open_file.png',
'save_file': 'save_file.png',
'exit': 'exit.png',
# 对象类型图标
'object_3d': 'object_3d.png',
'light': 'light.png',
'camera': 'camera.png',
'terrain': 'terrain.png',
'script': 'script.png',
# 状态图标
'success': 'success.png',
'warning': 'warning.png',
'error': 'error.png',
'info': 'info.png',
'up_arrow': 'up_arrows.png',
'down_arrow': 'down_arrows.png',
'left_arrow': 'left_arrows.png',
'right_arrow': 'right_arrows.png',
'solid_down_arrows': 'solid_down_arrows.png',
'solid_right_arrows': 'solid_right_arrows.png',
'minimize_icon': 'minimize_icon.png',
'windowing_icon': 'windowing_icon.png',
'close_icon': 'close_icon.png',
# 弹窗图标
'success_icon': 'success_icon.png',
'warning_icon': 'warning_icon.png',
'fail_icon': 'delete_fail_icon.png',
# 属性面板图标
'property_select_image': 'property_select_image.png',
}
# 初始化默认图标
self._create_default_icon()
# 预加载常用图标
self._preload_icons()
def _get_icon_directory(self) -> str:
"""获取图标目录的绝对路径"""
# 获取当前文件的目录ui目录
current_dir = os.path.dirname(os.path.abspath(__file__))
# 获取项目根目录ui的父目录
project_root = os.path.dirname(current_dir)
# 拼接icons目录路径
icon_dir = os.path.join(project_root, "icons")
print(f"🔍 图标目录路径: {icon_dir}")
# 检查目录是否存在
if not os.path.exists(icon_dir):
print(f"⚠️ 图标目录不存在: {icon_dir}")
# 尝试创建目录
try:
os.makedirs(icon_dir, exist_ok=True)
print(f"✅ 已创建图标目录: {icon_dir}")
except Exception as e:
print(f"❌ 创建图标目录失败: {e}")
return icon_dir
def _create_default_icon(self):
"""创建默认图标"""
# 创建一个简单的默认图标
pixmap = QPixmap(16, 16)
pixmap.fill() # 填充为白色
self.default_icon = QIcon(pixmap)
def _preload_icons(self):
"""预加载常用图标"""
print("🔄 开始预加载图标...")
for icon_name, file_name in self.icon_map.items():
icon_path = os.path.join(self.icon_directory, file_name)
if os.path.exists(icon_path):
try:
icon = QIcon(icon_path)
self.icon_cache[icon_name] = icon
print(f"✅ 已加载图标: {icon_name} -> {file_name}")
except Exception as e:
print(f"❌ 加载图标失败: {icon_name} -> {file_name}, 错误: {e}")
else:
print(f"⚠️ 图标文件不存在: {icon_path}")
print(f"📊 预加载完成,共加载 {len(self.icon_cache)} 个图标")
def get_icon(self, icon_name: str, size: Optional[QSize] = None) -> QIcon:
"""
获取图标
Args:
icon_name: 图标名称可以是预定义名称或文件名
size: 图标尺寸
Returns:
QIcon对象
"""
# 首先检查缓存
if icon_name in self.icon_cache:
icon = self.icon_cache[icon_name]
if size:
# 如果指定了尺寸,返回指定尺寸的图标
pixmap = icon.pixmap(size)
return QIcon(pixmap)
return icon
# 如果不在缓存中,尝试从映射中获取
if icon_name in self.icon_map:
file_name = self.icon_map[icon_name]
icon_path = os.path.join(self.icon_directory, file_name)
else:
# 直接使用文件名
icon_path = os.path.join(self.icon_directory, icon_name)
if not icon_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')):
icon_path += '.png' # 默认添加.png扩展名
# 尝试加载图标
if os.path.exists(icon_path):
try:
icon = QIcon(icon_path)
# 缓存图标
self.icon_cache[icon_name] = icon
print(f"✅ 动态加载图标: {icon_name} -> {os.path.basename(icon_path)}")
if size:
pixmap = icon.pixmap(size)
return QIcon(pixmap)
return icon
except Exception as e:
print(f"❌ 加载图标失败: {icon_path}, 错误: {e}")
else:
print(f"⚠️ 图标文件不存在: {icon_path}")
# 返回默认图标
return self.default_icon
def get_icon_path(self, icon_name: str) -> str:
"""
获取图标文件的完整路径
Args:
icon_name: 图标名称
Returns:
图标文件的完整路径
"""
if icon_name in self.icon_map:
file_name = self.icon_map[icon_name]
else:
file_name = icon_name
if not file_name.endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')):
file_name += '.png'
icon_path = os.path.join(self.icon_directory, file_name)
if os.path.exists(icon_path):
return icon_path
else:
print(f"⚠️ 图标文件不存在: {icon_path}")
return ""
def has_icon(self, icon_name: str) -> bool:
"""
检查图标是否存在
Args:
icon_name: 图标名称
Returns:
是否存在
"""
return bool(self.get_icon_path(icon_name))
def add_icon(self, icon_name: str, icon_path: str) -> bool:
"""
添加新图标到缓存
Args:
icon_name: 图标名称
icon_path: 图标文件路径
Returns:
是否添加成功
"""
try:
if os.path.exists(icon_path):
icon = QIcon(icon_path)
self.icon_cache[icon_name] = icon
print(f"✅ 已添加图标到缓存: {icon_name} -> {icon_path}")
return True
else:
print(f"❌ 图标文件不存在: {icon_path}")
return False
except Exception as e:
print(f"❌ 添加图标失败: {icon_name} -> {icon_path}, 错误: {e}")
return False
def refresh_cache(self):
"""刷新图标缓存"""
print("🔄 刷新图标缓存...")
self.icon_cache.clear()
self._preload_icons()
def get_available_icons(self) -> list:
"""获取所有可用的图标列表"""
available_icons = []
# 添加预定义的图标
available_icons.extend(self.icon_map.keys())
# 扫描图标目录中的所有图标文件
if os.path.exists(self.icon_directory):
for file_name in os.listdir(self.icon_directory):
if file_name.lower().endswith(('.png', '.jpg', '.jpeg', '.svg', '.ico')):
icon_name = os.path.splitext(file_name)[0]
if icon_name not in available_icons:
available_icons.append(icon_name)
return sorted(available_icons)
def get_cache_info(self) -> dict:
"""获取缓存信息"""
return {
'cached_icons': len(self.icon_cache),
'icon_directory': self.icon_directory,
'available_icons': len(self.get_available_icons()),
'cache_keys': list(self.icon_cache.keys())
}
def debug_info(self):
"""打印调试信息"""
print("=" * 50)
print("📋 图标管理器调试信息")
print("=" * 50)
info = self.get_cache_info()
print(f"图标目录: {info['icon_directory']}")
print(f"目录存在: {os.path.exists(info['icon_directory'])}")
print(f"缓存图标数: {info['cached_icons']}")
print(f"可用图标数: {info['available_icons']}")
if info['cache_keys']:
print("\n已缓存的图标:")
for key in info['cache_keys']:
print(f" - {key}")
print("\n图标目录内容:")
if os.path.exists(self.icon_directory):
for file_name in os.listdir(self.icon_directory):
file_path = os.path.join(self.icon_directory, file_name)
size = os.path.getsize(file_path) if os.path.isfile(file_path) else 0
print(f" - {file_name} ({size} bytes)")
else:
print(" 目录不存在")
print("=" * 50)
# 全局图标管理器实例
_icon_manager = None
def get_icon_manager() -> IconManager:
"""获取全局图标管理器实例"""
global _icon_manager
if _icon_manager is None:
_icon_manager = IconManager()
return _icon_manager
def get_icon(icon_name: str, size: Optional[QSize] = None) -> QIcon:
"""便捷函数:获取图标"""
return get_icon_manager().get_icon(icon_name, size)
def get_icon_path(icon_name: str) -> str:
"""便捷函数:获取图标路径"""
return get_icon_manager().get_icon_path(icon_name)
def has_icon(icon_name: str) -> bool:
"""便捷函数:检查图标是否存在"""
return get_icon_manager().has_icon(icon_name)
if __name__ == "__main__":
# 测试代码
print("🧪 测试图标管理器...")
manager = IconManager()
manager.debug_info()
# 测试获取图标
logo_icon = manager.get_icon('app_logo')
print(f"\n📱 应用图标是否有效: {not logo_icon.isNull()}")
move_tool_icon = manager.get_icon('move_tool')
print(f"🔧 移动工具图标是否有效: {not move_tool_icon.isNull()}")
"""
Icon manager compatibility layer for the ImGui-only runtime.
This module keeps the legacy public API so older imports do not break
immediately.
"""
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, Optional, Tuple
@dataclass(frozen=True)
class IconHandle:
"""Lightweight icon placeholder used by legacy call sites."""
path: str = ""
def isNull(self) -> bool:
return not self.path
def pixmap(self, _size: Optional[Tuple[int, int]] = None) -> "IconHandle":
return self
class IconManager:
def __init__(self):
self.icon_cache: Dict[str, IconHandle] = {}
self.icon_directory = self._get_icon_directory()
self.default_icon = IconHandle("")
self.icon_map = {
"app_logo": "logo.png",
"select_tool": "select_tool.png",
"move_tool": "move_tool.png",
"rotate_tool": "rotate_tool.png",
"scale_tool": "scale_tool.png",
"new_file": "new_file.png",
"open_file": "open_file.png",
"save_file": "save_file.png",
"exit": "exit.png",
"object_3d": "object_3d.png",
"light": "light.png",
"camera": "camera.png",
"terrain": "terrain.png",
"script": "script.png",
"success": "success.png",
"warning": "warning.png",
"error": "error.png",
"info": "info.png",
"up_arrow": "up_arrows.png",
"down_arrow": "down_arrows.png",
"left_arrow": "left_arrows.png",
"right_arrow": "right_arrows.png",
"solid_down_arrows": "solid_down_arrows.png",
"solid_right_arrows": "solid_right_arrows.png",
"minimize_icon": "minimize_icon.png",
"windowing_icon": "windowing_icon.png",
"close_icon": "close_icon.png",
"success_icon": "success_icon.png",
"warning_icon": "warning_icon.png",
"fail_icon": "delete_fail_icon.png",
"property_select_image": "property_select_image.png",
}
def _get_icon_directory(self) -> str:
current_dir = Path(__file__).resolve().parent
project_root = current_dir.parent
icon_dir = project_root / "icons"
return str(icon_dir)
def _resolve_icon_path(self, icon_name: str) -> Path:
if icon_name in self.icon_map:
file_name = self.icon_map[icon_name]
else:
file_name = icon_name
if not file_name.lower().endswith((".png", ".jpg", ".jpeg", ".svg", ".ico")):
file_name += ".png"
return Path(self.icon_directory) / file_name
def get_icon(self, icon_name: str, _size: Optional[Tuple[int, int]] = None) -> IconHandle:
if icon_name in self.icon_cache:
return self.icon_cache[icon_name]
icon_path = self._resolve_icon_path(icon_name)
if icon_path.exists():
icon = IconHandle(str(icon_path))
self.icon_cache[icon_name] = icon
return icon
return self.default_icon
def get_icon_path(self, icon_name: str) -> str:
icon_path = self._resolve_icon_path(icon_name)
return str(icon_path) if icon_path.exists() else ""
def has_icon(self, icon_name: str) -> bool:
return bool(self.get_icon_path(icon_name))
def add_icon(self, icon_name: str, icon_path: str) -> bool:
path_obj = Path(icon_path)
if not path_obj.exists():
return False
self.icon_cache[icon_name] = IconHandle(str(path_obj))
return True
def refresh_cache(self):
self.icon_cache.clear()
def get_available_icons(self) -> list:
available_icons = sorted(self.icon_map.keys())
icon_dir = Path(self.icon_directory)
if icon_dir.exists():
for p in icon_dir.iterdir():
if p.suffix.lower() in {".png", ".jpg", ".jpeg", ".svg", ".ico"}:
name = p.stem
if name not in available_icons:
available_icons.append(name)
return sorted(available_icons)
def get_cache_info(self) -> dict:
return {
"cached_icons": len(self.icon_cache),
"icon_directory": self.icon_directory,
"available_icons": len(self.get_available_icons()),
"cache_keys": list(self.icon_cache.keys()),
}
def debug_info(self):
info = self.get_cache_info()
print(f"icon_directory: {info['icon_directory']}")
print(f"cached_icons: {info['cached_icons']}")
print(f"available_icons: {info['available_icons']}")
_icon_manager: Optional[IconManager] = None
def get_icon_manager() -> IconManager:
global _icon_manager
if _icon_manager is None:
_icon_manager = IconManager()
return _icon_manager
def get_icon(icon_name: str, size: Optional[Tuple[int, int]] = None) -> IconHandle:
return get_icon_manager().get_icon(icon_name, size)
def get_icon_path(icon_name: str) -> str:
return get_icon_manager().get_icon_path(icon_name)
def has_icon(icon_name: str) -> bool:
return get_icon_manager().has_icon(icon_name)

File diff suppressed because it is too large Load Diff