540 lines
24 KiB
Python
540 lines
24 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
GUI预览窗口 - 使用现有ShowBase创建额外窗口,专门用来显示GUI元素
|
||
解决Qt集成中2D GUI显示问题,避免多个ShowBase实例错误
|
||
"""
|
||
|
||
import warnings
|
||
warnings.filterwarnings("ignore", category=DeprecationWarning)
|
||
|
||
from direct.gui.DirectGui import *
|
||
from panda3d.core import *
|
||
from direct.showbase.ShowBase import ShowBase
|
||
|
||
class GUIPreviewWindow:
|
||
"""使用现有ShowBase的额外窗口进行GUI预览"""
|
||
|
||
def __init__(self):
|
||
self.preview_window = None
|
||
self.chinese_font = None
|
||
self.gui_elements = []
|
||
self.main_world = None # 主程序world对象的引用
|
||
self.sync_timer = None
|
||
|
||
# 预览窗口的渲染节点
|
||
self.preview_render2d = None
|
||
self.preview_camera2d = None
|
||
self.preview_aspect2d = None
|
||
|
||
print("✓ GUI预览窗口类已初始化")
|
||
|
||
def set_main_world(self, world):
|
||
"""设置主程序world引用并初始化预览窗口"""
|
||
self.main_world = world
|
||
if world:
|
||
self.init_preview_window()
|
||
self.start_sync_timer()
|
||
|
||
def init_preview_window(self):
|
||
"""使用现有ShowBase创建预览窗口"""
|
||
try:
|
||
if not self.main_world:
|
||
print("错误: 没有主程序world引用")
|
||
return False
|
||
|
||
# 使用主程序的base创建额外窗口
|
||
base = self.main_world # MyWorld继承自ShowBase
|
||
|
||
# 尝试从Qt部件获取渲染区域的实际尺寸
|
||
main_width = 800 # 默认宽度
|
||
main_height = 600 # 默认高度
|
||
|
||
# 检查是否有Qt应用程序实例
|
||
try:
|
||
from PyQt5.QtWidgets import QApplication
|
||
app = QApplication.instance()
|
||
if app:
|
||
# 查找主窗口
|
||
for widget in app.topLevelWidgets():
|
||
if hasattr(widget, 'centralWidget'):
|
||
central_widget = widget.centralWidget()
|
||
if central_widget and hasattr(central_widget, 'world'):
|
||
# 找到了Panda3D部件
|
||
qt_width = central_widget.width()
|
||
qt_height = central_widget.height()
|
||
if qt_width > 0 and qt_height > 0:
|
||
main_width = qt_width
|
||
main_height = qt_height
|
||
print(f"从Qt部件获取尺寸: {main_width} x {main_height}")
|
||
break
|
||
else:
|
||
print("未找到Qt Panda3D部件,使用默认尺寸")
|
||
else:
|
||
print("未找到Qt应用程序实例,使用默认尺寸")
|
||
except ImportError:
|
||
print("未找到PyQt5,使用默认尺寸")
|
||
except Exception as e:
|
||
print(f"从Qt获取尺寸失败: {str(e)},使用默认尺寸")
|
||
|
||
# 如果Qt方法失败,尝试从Panda3D窗口获取
|
||
if main_width == 800 and main_height == 600:
|
||
main_window = base.win
|
||
if main_window and main_window.hasSize():
|
||
panda_width = main_window.getXSize()
|
||
panda_height = main_window.getYSize()
|
||
if panda_width > 0 and panda_height > 0:
|
||
main_width = panda_width
|
||
main_height = panda_height
|
||
print(f"从Panda3D窗口获取尺寸: {main_width} x {main_height}")
|
||
else:
|
||
print(f"Panda3D窗口尺寸无效,使用默认尺寸: {main_width} x {main_height}")
|
||
|
||
# 设置窗口属性,使用获取到的尺寸
|
||
wp = WindowProperties()
|
||
wp.setSize(main_width, main_height)
|
||
wp.setTitle("GUI预览窗口 - 2D GUI专用显示")
|
||
wp.setOrigin(main_width + 50, 50) # 放在主窗口右侧
|
||
wp.setUndecorated(False) # 保持窗口装饰
|
||
# 注意:WindowProperties没有setMaximized和setMinimized方法
|
||
|
||
print(f"设置预览窗口属性: 尺寸={main_width}x{main_height}, 位置=({main_width + 50}, 50)")
|
||
|
||
# 获取图形管道
|
||
pipe = base.pipe
|
||
if not pipe:
|
||
print("错误: 无法获取图形管道")
|
||
return False
|
||
|
||
# 创建帧缓冲属性
|
||
fbp = FrameBufferProperties.getDefault()
|
||
|
||
# 使用makeOutput创建窗口,强制要求窗口类型
|
||
self.preview_window = base.graphicsEngine.makeOutput(
|
||
pipe, "GUI预览窗口", 0,
|
||
fbp, wp, GraphicsPipe.BFRequireWindow
|
||
)
|
||
|
||
if not self.preview_window:
|
||
print("错误: 无法创建预览窗口")
|
||
return False
|
||
|
||
print(f"预览窗口创建成功,类型: {type(self.preview_window).__name__}")
|
||
|
||
# 强制打开窗口
|
||
base.graphicsEngine.openWindows()
|
||
|
||
# 验证创建的窗口尺寸
|
||
if hasattr(self.preview_window, 'getXSize'):
|
||
actual_width = self.preview_window.getXSize()
|
||
actual_height = self.preview_window.getYSize()
|
||
print(f"实际创建的预览窗口尺寸: {actual_width} x {actual_height}")
|
||
|
||
if actual_width != main_width or actual_height != main_height:
|
||
print(f"警告: 窗口尺寸不匹配!期望: {main_width}x{main_height}, 实际: {actual_width}x{actual_height}")
|
||
|
||
# 尝试重新设置尺寸
|
||
new_props = WindowProperties()
|
||
new_props.setSize(main_width, main_height)
|
||
self.preview_window.requestProperties(new_props)
|
||
base.graphicsEngine.renderFrame()
|
||
|
||
# 再次检查
|
||
final_width = self.preview_window.getXSize()
|
||
final_height = self.preview_window.getYSize()
|
||
print(f"重新设置后的尺寸: {final_width} x {final_height}")
|
||
|
||
# 设置预览窗口的背景色
|
||
base.setBackgroundColor(0.15, 0.15, 0.25, 1.0, self.preview_window)
|
||
|
||
# 创建预览窗口的2D渲染节点
|
||
self.preview_render2d = NodePath('preview_render2d')
|
||
|
||
# 为预览窗口创建2D相机,使用正确的宽高比
|
||
aspect_ratio = float(main_width) / float(main_height)
|
||
self.preview_camera2d = base.makeCamera2d(self.preview_window)
|
||
self.preview_camera2d.reparentTo(self.preview_render2d)
|
||
|
||
# 设置2D渲染属性
|
||
self.preview_render2d.setDepthWrite(0)
|
||
self.preview_render2d.setMaterialOff(1)
|
||
self.preview_render2d.setTwoSided(1)
|
||
|
||
# 检查窗口类型并设置输入
|
||
print(f"预览窗口类型: {type(self.preview_window)}")
|
||
print(f"是否为GraphicsWindow: {hasattr(self.preview_window, 'getNumInputDevices')}")
|
||
|
||
# 只为真正的GraphicsWindow设置输入
|
||
if hasattr(self.preview_window, 'getNumInputDevices') and self.preview_window.getNumInputDevices() > 0:
|
||
# 设置鼠标和键盘输入
|
||
mk = base.dataRoot.attachNewNode(MouseAndKeyboard(self.preview_window, 0, 'previewMouse'))
|
||
mw = mk.attachNewNode(MouseWatcher('previewMouseWatcher'))
|
||
self.preview_aspect2d = self.preview_render2d.attachNewNode(PGTop('previewAspect2d'))
|
||
self.preview_aspect2d.node().setMouseWatcher(mw.node())
|
||
print("✓ 已设置预览窗口的鼠标和键盘输入")
|
||
else:
|
||
print("⚠ 跳过鼠标/键盘设置(窗口不支持或没有输入设备)")
|
||
# 创建没有输入的aspect2d节点
|
||
self.preview_aspect2d = self.preview_render2d.attachNewNode(PGTop('previewAspect2d'))
|
||
|
||
# 加载中文字体
|
||
self.load_font()
|
||
|
||
# 创建界面说明
|
||
self.create_ui_info()
|
||
|
||
# 最终验证
|
||
if hasattr(self.preview_window, 'getXSize'):
|
||
final_width = self.preview_window.getXSize()
|
||
final_height = self.preview_window.getYSize()
|
||
print(f"✓ GUI预览窗口已成功创建(最终尺寸: {final_width}x{final_height})")
|
||
else:
|
||
print(f"✓ GUI预览窗口已成功创建(期望尺寸: {main_width}x{main_height})")
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
print(f"创建GUI预览窗口失败: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return False
|
||
|
||
def load_font(self):
|
||
"""加载中文字体"""
|
||
try:
|
||
if self.main_world and hasattr(self.main_world, 'loader'):
|
||
self.chinese_font = self.main_world.loader.loadFont('/usr/share/fonts/truetype/wqy/wqy-microhei.ttc')
|
||
if not self.chinese_font:
|
||
print("预览窗口: 无法加载中文字体,将使用默认字体")
|
||
except:
|
||
print("预览窗口: 无法加载中文字体,将使用默认字体")
|
||
self.chinese_font = None
|
||
|
||
def create_ui_info(self):
|
||
"""创建界面说明"""
|
||
if not self.preview_aspect2d:
|
||
return
|
||
|
||
# # 创建欢迎信息
|
||
# self.welcome_text = OnscreenText(
|
||
# text="GUI预览窗口\n\n这个窗口使用现有的ShowBase\n创建额外的显示窗口\n\n可以正确显示2D GUI元素",
|
||
# parent=self.preview_aspect2d,
|
||
# pos=(0, 0.2),
|
||
# scale=0.08,
|
||
# fg=(1, 1, 1, 1),
|
||
# align=TextNode.ACenter,
|
||
# font=self.chinese_font if self.chinese_font else None
|
||
# )
|
||
|
||
# # 创建提示信息
|
||
# self.tip_text = OnscreenText(
|
||
# text="这个窗口与主程序共享ShowBase,避免了多实例错误",
|
||
# parent=self.preview_aspect2d,
|
||
# pos=(0, -0.7),
|
||
# scale=0.05,
|
||
# fg=(0.8, 0.8, 0.8, 1),
|
||
# align=TextNode.ACenter,
|
||
# font=self.chinese_font if self.chinese_font else None
|
||
# )
|
||
|
||
# # 创建同步状态显示
|
||
# self.sync_text = OnscreenText(
|
||
# text="同步状态: 等待检测",
|
||
# parent=self.preview_aspect2d,
|
||
# pos=(-0.9, 0.8),
|
||
# scale=0.04,
|
||
# fg=(0, 1, 0, 1),
|
||
# align=TextNode.ALeft,
|
||
# font=self.chinese_font if self.chinese_font else None
|
||
# )
|
||
|
||
def start_sync_timer(self):
|
||
"""启动同步定时器"""
|
||
if self.main_world and hasattr(self.main_world, 'taskMgr'):
|
||
# 每0.5秒检查一次GUI元素变化
|
||
self.main_world.taskMgr.doMethodLater(0.5, self.sync_gui_elements, 'sync_preview_gui')
|
||
print("✓ GUI同步定时器已启动")
|
||
|
||
def sync_gui_elements(self, task=None):
|
||
"""同步主程序的GUI元素到预览窗口"""
|
||
try:
|
||
if not self.main_world or not self.preview_aspect2d:
|
||
return task.again if task else None
|
||
|
||
# 获取主程序中的GUI元素
|
||
main_gui_elements = getattr(self.main_world, 'gui_elements', [])
|
||
|
||
# 检查是否需要重新创建所有元素
|
||
current_count = len(main_gui_elements)
|
||
preview_count = len(self.gui_elements)
|
||
|
||
need_full_sync = False
|
||
|
||
# 如果数量有变化,需要完全重新同步
|
||
if current_count != preview_count:
|
||
need_full_sync = True
|
||
print(f"GUI数量变化: {preview_count} -> {current_count}")
|
||
else:
|
||
# 检查现有元素的属性是否有变化
|
||
for i, main_element in enumerate(main_gui_elements):
|
||
if i < len(self.gui_elements):
|
||
if self.has_element_changed(main_element, self.gui_elements[i]):
|
||
need_full_sync = True
|
||
print(f"GUI元素 {i} 属性发生变化")
|
||
break
|
||
|
||
# 执行同步
|
||
if need_full_sync:
|
||
print("执行完整同步...")
|
||
self.clear_preview_elements()
|
||
self.create_preview_elements(main_gui_elements)
|
||
|
||
# 更新同步状态显示
|
||
if hasattr(self, 'sync_text'):
|
||
self.sync_text.setText(f"同步状态: 已同步 {current_count} 个GUI元素")
|
||
|
||
print(f"同步完成: {current_count} 个GUI元素")
|
||
|
||
return task.again if task else None
|
||
|
||
except Exception as e:
|
||
print(f"同步GUI元素失败: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return task.again if task else None
|
||
|
||
def has_element_changed(self, main_element, preview_element):
|
||
"""检查主程序GUI元素是否发生了变化"""
|
||
try:
|
||
if not hasattr(main_element, 'getTag') or not hasattr(preview_element, 'getTag'):
|
||
return True
|
||
|
||
# 检查文本是否变化
|
||
main_text = main_element.getTag("gui_text")
|
||
preview_text = getattr(preview_element, '_last_synced_text', "")
|
||
if main_text != preview_text:
|
||
return True
|
||
|
||
# 检查位置是否变化
|
||
if hasattr(main_element, 'getPos') and hasattr(preview_element, 'getPos'):
|
||
main_pos = main_element.getPos()
|
||
preview_pos = getattr(preview_element, '_last_synced_pos', Point3(999, 999, 999))
|
||
if (abs(main_pos.getX() - preview_pos.getX()) > 0.001 or
|
||
abs(main_pos.getY() - preview_pos.getY()) > 0.001 or
|
||
abs(main_pos.getZ() - preview_pos.getZ()) > 0.001):
|
||
return True
|
||
|
||
# 检查缩放是否变化
|
||
if hasattr(main_element, 'getScale') and hasattr(preview_element, 'getScale'):
|
||
main_scale = main_element.getScale().getX()
|
||
preview_scale = getattr(preview_element, '_last_synced_scale', -1)
|
||
if abs(main_scale - preview_scale) > 0.001:
|
||
return True
|
||
|
||
return False
|
||
|
||
except Exception as e:
|
||
print(f"检查元素变化失败: {str(e)}")
|
||
return True # 出错时强制同步
|
||
|
||
def clear_preview_elements(self):
|
||
"""清空预览窗口中的GUI元素"""
|
||
for element in self.gui_elements:
|
||
try:
|
||
if hasattr(element, 'removeNode'):
|
||
element.removeNode()
|
||
elif hasattr(element, 'destroy'):
|
||
element.destroy()
|
||
except:
|
||
pass
|
||
|
||
self.gui_elements.clear()
|
||
|
||
# 如果没有GUI元素了,显示说明文本
|
||
if hasattr(self, 'welcome_text') and self.welcome_text:
|
||
self.welcome_text.show()
|
||
if hasattr(self, 'tip_text') and self.tip_text:
|
||
self.tip_text.show()
|
||
|
||
def create_preview_elements(self, main_gui_elements):
|
||
"""在预览窗口中创建GUI元素"""
|
||
if not main_gui_elements or not self.preview_aspect2d:
|
||
return
|
||
|
||
# 隐藏说明文本
|
||
if hasattr(self, 'welcome_text') and self.welcome_text:
|
||
self.welcome_text.hide()
|
||
if hasattr(self, 'tip_text') and self.tip_text:
|
||
self.tip_text.hide()
|
||
|
||
for main_element in main_gui_elements:
|
||
try:
|
||
preview_element = self.create_preview_element(main_element)
|
||
if preview_element:
|
||
self.gui_elements.append(preview_element)
|
||
except Exception as e:
|
||
print(f"创建预览元素失败: {str(e)}")
|
||
|
||
def create_preview_element(self, main_element):
|
||
"""根据主程序GUI元素创建对应的预览元素"""
|
||
try:
|
||
if not hasattr(main_element, 'getTag'):
|
||
return None
|
||
|
||
gui_type = main_element.getTag("gui_type")
|
||
gui_text = main_element.getTag("gui_text")
|
||
|
||
if not gui_type:
|
||
return None
|
||
|
||
# 获取位置和缩放
|
||
pos = main_element.getPos() if hasattr(main_element, 'getPos') else (0, 0, 0)
|
||
scale = main_element.getScale().getX() if hasattr(main_element, 'getScale') else 0.1
|
||
|
||
preview_element = None
|
||
|
||
if gui_type == "button":
|
||
preview_element = DirectButton(
|
||
parent=self.preview_aspect2d,
|
||
text=gui_text or "按钮",
|
||
pos=pos,
|
||
scale=scale,
|
||
frameColor=(0.2, 0.6, 0.8, 1),
|
||
text_font=self.chinese_font if self.chinese_font else None,
|
||
command=lambda: print(f"预览按钮点击: {gui_text}")
|
||
)
|
||
|
||
elif gui_type == "label":
|
||
preview_element = DirectLabel(
|
||
parent=self.preview_aspect2d,
|
||
text=gui_text or "标签",
|
||
pos=pos,
|
||
scale=scale,
|
||
frameColor=(0, 0, 0, 0),
|
||
text_fg=(1, 1, 1, 1),
|
||
text_font=self.chinese_font if self.chinese_font else None
|
||
)
|
||
|
||
elif gui_type == "entry":
|
||
preview_element = DirectEntry(
|
||
parent=self.preview_aspect2d,
|
||
text="",
|
||
pos=pos,
|
||
scale=scale,
|
||
initialText=gui_text or "输入框",
|
||
numLines=1,
|
||
width=12,
|
||
focus=0,
|
||
command=lambda text: print(f"预览输入框: {text}")
|
||
)
|
||
|
||
elif gui_type == "slider":
|
||
preview_element = DirectSlider(
|
||
parent=self.preview_aspect2d,
|
||
pos=pos,
|
||
scale=scale,
|
||
range=(0, 100),
|
||
value=50,
|
||
frameColor=(0.6, 0.6, 0.6, 1),
|
||
thumbColor=(0.2, 0.8, 0.2, 1),
|
||
command=lambda: print("预览滑块值变化")
|
||
)
|
||
|
||
elif gui_type == "3d_text":
|
||
# 3D文本需要特殊处理,在3D空间中显示
|
||
textNode = TextNode(f'preview-3d-text-{len(self.gui_elements)}')
|
||
textNode.setText(gui_text or "3D文本")
|
||
textNode.setAlign(TextNode.ACenter)
|
||
if self.chinese_font:
|
||
textNode.setFont(self.chinese_font)
|
||
|
||
# 需要一个3D渲染节点
|
||
if not hasattr(self, 'preview_render3d'):
|
||
self.preview_render3d = NodePath('preview_render3d')
|
||
# 为预览窗口创建3D相机
|
||
camera3d = self.main_world.makeCamera(self.preview_window)
|
||
camera3d.reparentTo(self.preview_render3d)
|
||
camera3d.setPos(0, -10, 0)
|
||
camera3d.lookAt(0, 0, 0)
|
||
|
||
preview_element = self.preview_render3d.attachNewNode(textNode)
|
||
preview_element.setPos(*pos)
|
||
preview_element.setScale(scale)
|
||
preview_element.setColor(1, 1, 0, 1)
|
||
preview_element.setBillboardAxis()
|
||
|
||
elif gui_type == "virtual_screen":
|
||
# 虚拟屏幕也需要3D渲染
|
||
if not hasattr(self, 'preview_render3d'):
|
||
self.preview_render3d = NodePath('preview_render3d')
|
||
camera3d = self.main_world.makeCamera(self.preview_window)
|
||
camera3d.reparentTo(self.preview_render3d)
|
||
camera3d.setPos(0, -10, 0)
|
||
camera3d.lookAt(0, 0, 0)
|
||
|
||
cm = CardMaker(f"preview-virtual-screen-{len(self.gui_elements)}")
|
||
cm.setFrame(-1, 1, -0.5, 0.5)
|
||
preview_element = self.preview_render3d.attachNewNode(cm.generate())
|
||
preview_element.setPos(*pos)
|
||
preview_element.setColor(0.2, 0.2, 0.2, 0.8)
|
||
preview_element.setTransparency(TransparencyAttrib.MAlpha)
|
||
|
||
# 在虚拟屏幕上添加文本
|
||
screenText = TextNode(f'preview-screen-text-{len(self.gui_elements)}')
|
||
screenText.setText(gui_text or "虚拟屏幕")
|
||
screenText.setAlign(TextNode.ACenter)
|
||
if self.chinese_font:
|
||
screenText.setFont(self.chinese_font)
|
||
screenTextNP = preview_element.attachNewNode(screenText)
|
||
screenTextNP.setPos(0, 0.01, 0)
|
||
screenTextNP.setScale(0.3)
|
||
screenTextNP.setColor(0, 1, 0, 1)
|
||
|
||
# 为预览元素记录同步状态,用于变化检测
|
||
if preview_element:
|
||
preview_element._last_synced_text = gui_text or ""
|
||
preview_element._last_synced_pos = Point3(pos) if hasattr(main_element, 'getPos') else Point3(0, 0, 0)
|
||
preview_element._last_synced_scale = scale
|
||
print(f"创建预览元素: {gui_type} - {gui_text} (位置: {pos}, 缩放: {scale})")
|
||
|
||
return preview_element
|
||
|
||
except Exception as e:
|
||
print(f"创建预览元素失败: {str(e)}")
|
||
import traceback
|
||
traceback.print_exc()
|
||
return None
|
||
|
||
def destroy(self):
|
||
"""销毁预览窗口"""
|
||
try:
|
||
# 停止同步任务
|
||
if self.main_world and hasattr(self.main_world, 'taskMgr'):
|
||
self.main_world.taskMgr.remove('sync_preview_gui')
|
||
|
||
# 清空GUI元素
|
||
self.clear_preview_elements()
|
||
|
||
# 移除界面元素
|
||
if hasattr(self, 'welcome_text') and self.welcome_text:
|
||
self.welcome_text.removeNode()
|
||
if hasattr(self, 'tip_text') and self.tip_text:
|
||
self.tip_text.removeNode()
|
||
if hasattr(self, 'sync_text') and self.sync_text:
|
||
self.sync_text.removeNode()
|
||
|
||
# 关闭窗口
|
||
if self.preview_window and self.main_world:
|
||
self.main_world.closeWindow(self.preview_window)
|
||
self.preview_window = None
|
||
|
||
print("预览窗口已销毁")
|
||
|
||
except Exception as e:
|
||
print(f"销毁预览窗口失败: {str(e)}")
|
||
|
||
# 测试代码
|
||
if __name__ == "__main__":
|
||
print("这是GUI预览窗口模块")
|
||
print("需要与主程序集成使用,不能独立运行") |