1
0
forked from Rowland/EG
EG/gui_preview_window.py
2025-08-13 09:30:16 +08:00

540 lines
23 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.

#!/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("需要与主程序集成使用,不能独立运行")