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