""" ImGui样式管理器 用于统一管理ImGui UI样式,与主程序Qt UI保持一致 """ from imgui_bundle import imgui, imgui_ctx import os import platform from pathlib import Path from panda3d.core import Filename class ImGuiStyleManager: """ImGui样式管理器 - 负责UI样式的统一管理""" def __init__(self, imgui_backend, world=None): """ 初始化样式管理器 Args: imgui_backend: ImGui后端对象 """ self.imgui_backend = imgui_backend self.world = world self.io = imgui_backend.io self.style = None # 延迟初始化,在apply_style中设置 # 颜色定义 - 与Qt UI保持一致 self.colors = { # 主要颜色 'primary': (0.188, 0.404, 0.753, 1.0), # #3067C0 - 主色调 'primary_dark': (0.149, 0.337, 0.627, 1.0), # #2556A0 - 按钮按下 'background': (0.0, 0.0, 0.0, 1.0), # #000000 - 主背景 'secondary_bg': (0.096, 0.096, 0.106, 1.0), # #19191B - 次背景 'panel_bg': (0.18, 0.188, 0.208, 1.0), # #2E3035 - 面板背景 'text': (0.922, 0.922, 0.922, 1.0), # #EBEBEB - 主文字 'text_secondary': (0.922, 0.922, 0.922, 0.6), # #EBEBEB - 次文字 'border': (0.243, 0.243, 0.259, 1.0), # #3E3E42 - 主要边框 'border_secondary': (0.173, 0.184, 0.212, 1.0), # #2C2F36 - 次要边框 'button_bg': (0.349, 0.384, 0.463, 0.5), # 半透明按钮背景 'input_bg': (0.349, 0.392, 0.443, 0.2), # 输入框背景 'input_border': (0.298, 0.361, 0.431, 0.6), # 输入框边框 'success': (0.176, 1.0, 0.769, 1.0), # #2dffc4 - 成功状态 'warning': (0.953, 0.616, 0.471, 1.0), # #f39d78 - 警告状态 'info': (0.157, 0.620, 1.0, 1.0), # #289eff - 信息状态 } # 尺寸定义 self.sizes = { 'font_size': 12.0, # 与Qt一致的字体大小 'window_padding': (8.0, 8.0), 'window_rounding': 2.0, 'item_spacing': (6.0, 6.0), 'item_inner_spacing': (4.0, 4.0), 'frame_padding': (4.0, 3.0), 'frame_rounding': 2.0, 'indent_spacing': 20.0, 'scrollbar_size': 14.0, } # 字体设置在demo.py中直接处理,这里不再初始化 def _setup_fonts(self): """设置字体,优先确保中文正常显示""" try: # 获取中文字体路径 chinese_font_path = self._get_chinese_font_path() if chinese_font_path: # 使用原始的字体加载方式 self._load_chinese_font_simple(chinese_font_path) else: print("⚠ 未找到中文字体,使用默认字体") self._load_default_font() except Exception as e: print(f"⚠ ImGui字体设置失败: {e}") # 备用方案:使用默认字体 self._load_default_font() def _get_chinese_font_path(self): """获取中文字体路径""" system = platform.system().lower() project_root = Path(__file__).resolve().parent.parent # 候选字体路径 if system == "windows": win_dir = os.environ.get("WINDIR") or r"C:\Windows" font_candidates = [ project_root / "RenderPipelineFile" / "data" / "font" / "msyh.ttc", Path(win_dir) / "Fonts" / "msyh.ttc", Path(win_dir) / "Fonts" / "msyh.ttf", Path(win_dir) / "Fonts" / "simhei.ttf", ] elif system == "darwin": font_candidates = [ Path("/System/Library/Fonts/PingFang.ttc"), Path("/System/Library/Fonts/STHeiti.ttc"), ] else: # Linux font_candidates = [ Path("/usr/share/fonts/truetype/wqy/wqy-microhei.ttc"), Path("/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc"), ] # 尝试找到存在的字体文件 for font_path in font_candidates: if font_path.exists(): return str(font_path) return None def _get_emoji_font_path(self): """获取Emoji字体路径""" system = platform.system().lower() # 候选Emoji字体路径 if system == "windows": win_dir = os.environ.get("WINDIR") or r"C:\Windows" font_candidates = [ Path(win_dir) / "Fonts" / "seguiemj.ttf", # Segoe UI Emoji Path(win_dir) / "Fonts" / "NotoColorEmoji.ttf", ] elif system == "darwin": font_candidates = [ Path("/System/Library/Fonts/Apple Color Emoji.ttc"), Path("/System/Library/Fonts/AppleColorEmoji.ttf"), ] else: # Linux font_candidates = [ Path("/usr/share/fonts/truetype/noto/NotoColorEmoji.ttf"), Path("/usr/share/fonts/opentype/noto/NotoColorEmoji.ttf"), Path("/usr/share/fonts/truetype/emoji/Emoji.ttf"), ] # 尝试找到存在的字体文件 for font_path in font_candidates: if font_path.exists(): return str(font_path) return None def _load_merged_fonts(self, chinese_font_path, emoji_font_path): """合并加载中文字体和Emoji字体""" try: # 简化的字体加载:先确保中文正常显示 font_config = self.imgui_backend.io.fonts # 清除现有字体 font_config.clear() # 添加中文字体(使用默认字符范围) chinese_font = font_config.add_font_from_file_ttf( chinese_font_path, self.sizes['font_size'] ) # 尝试添加Emoji字体(如果支持的话) try: font_config.merge_mode = True font_config.add_font_from_file_ttf( emoji_font_path, self.sizes['font_size'] ) font_config.merge_mode = False print(f"✓ ImGui合并字体加载成功: 中文={chinese_font_path}, Emoji={emoji_font_path}") except Exception as emoji_error: print(f"⚠ Emoji字体合并失败,仅使用中文字体: {emoji_error}") print(f"✓ ImGui中文字体加载成功: {chinese_font_path}") # 构建字体图集 font_config.build() except Exception as e: print(f"⚠ 字体加载失败,使用备用方案: {e}") self._load_simple_chinese_font(chinese_font_path) def _load_font_with_emoji_range(self, font_path): """加载单一字体但包含Emoji字符范围""" # 简化实现,直接调用简单字体加载 self._load_simple_chinese_font(font_path) def _load_chinese_font_simple(self, font_path): """简单加载中文字体""" try: # 清除现有字体 self.imgui_backend.io.fonts.clear() # 使用标准的中文字符范围 glyph_ranges = imgui.get_io().fonts.get_glyph_ranges_chinese_full() # 添加中文字体 self.imgui_backend.io.fonts.add_font_from_file_ttf( font_path, self.sizes['font_size'], None, glyph_ranges ) print(f"✓ ImGui中文字体加载成功: {font_path}") except Exception as e: print(f"⚠ 中文字体加载失败: {e}") self._load_default_font() def _load_default_font(self): """加载默认字体""" try: # 清除现有字体 self.imgui_backend.io.fonts.clear() # 添加默认字体 self.imgui_backend.io.fonts.add_font_default() print("✓ 使用默认字体") except Exception as e: print(f"⚠ 默认字体加载失败: {e}") def _get_chinese_glyph_ranges(self): """获取中文字符范围""" # 基本拉丁 + 中文扩展 ranges = [ 0x0020, 0x00FF, # 基本拉丁 0x2000, 0x206F, # 标点符号 0x3000, 0x30FF, # 中文符号 0xFF00, 0xFFEF, # 半角/全角 0x4E00, 0x9FAF, # CJK统一汉字 ] return ranges def _get_emoji_glyph_ranges(self): """获取Emoji字符范围""" # Emoji字符范围 ranges = [ 0x1F300, 0x1F5FF, # 杂项符号和象形文字 0x1F600, 0x1F64F, # 表情符号 0x1F680, 0x1F6FF, # 交通和地图符号 0x1F700, 0x1F77F, # 炼金术符号 0x1F780, 0x1F7FF, # 几何形状扩展 0x1F800, 0x1F8FF, # 补充箭头-C 0x1F900, 0x1F9FF, # 补充符号和象形文字 0x2600, 0x26FF, # 杂项符号 0x2700, 0x27BF, # 装饰符号 ] return ranges def _get_combined_glyph_ranges(self): """获取合并的字符范围(中文+Emoji)""" # 合并所有字符范围 chinese_ranges = self._get_chinese_glyph_ranges() emoji_ranges = self._get_emoji_glyph_ranges() # 将两个范围合并 combined = chinese_ranges + emoji_ranges return combined def apply_style(self): """应用与Qt UI一致的样式""" # 先应用深色主题作为基础 imgui.style_colors_dark() # 获取样式对象 self.style = imgui.get_style() style = self.style # 应用自定义颜色 style.colors[imgui.COLOR_WINDOW_BG] = self.colors['background'] style.colors[imgui.COLOR_CHILD_BG] = self.colors['secondary_bg'] style.colors[imgui.COLOR_POPUP_BG] = self.colors['panel_bg'] style.colors[imgui.COLOR_BORDER] = self.colors['border'] style.colors[imgui.COLOR_BORDER_SHADOW] = self.colors['border_secondary'] # 文本颜色 style.colors[imgui.COLOR_TEXT] = self.colors['text'] style.colors[imgui.COLOR_TEXT_DISABLED] = self.colors['text_secondary'] style.colors[imgui.COLOR_TEXT_SELECTED_BG] = self.colors['primary'] # 菜单栏颜色 style.colors[imgui.COLOR_MENU_BAR_BG] = self.colors['background'] # 按钮颜色 style.colors[imgui.COLOR_BUTTON] = self.colors['button_bg'] style.colors[imgui.COLOR_BUTTON_HOVERED] = self.colors['primary'] style.colors[imgui.COLOR_BUTTON_ACTIVE] = self.colors['primary_dark'] # 复选框和单选按钮 style.colors[imgui.COLOR_CHECK_MARK] = self.colors['primary'] style.colors[imgui.COLOR_RADIO_BUTTON_HOVERED] = self.colors['primary'] style.colors[imgui.COLOR_RADIO_BUTTON_ACTIVE] = self.colors['primary_dark'] # 滑块 style.colors[imgui.COLOR_SLIDER_GRAB] = self.colors['primary'] style.colors[imgui.COLOR_SLIDER_GRAB_ACTIVE] = self.colors['primary_dark'] # 进度条 style.colors[imgui.COLOR_PROGRESS_BAR_FG] = self.colors['primary'] # 标题栏 style.colors[imgui.COLOR_TITLE_BG] = self.colors['panel_bg'] style.colors[imgui.COLOR_TITLE_BG_ACTIVE] = self.colors['primary'] style.colors[imgui.COLOR_TITLE_BG_COLLAPSED] = self.colors['border_secondary'] # 输入框 style.colors[imgui.COLOR_FRAME_BG] = self.colors['input_bg'] style.colors[imgui.COLOR_FRAME_BG_HOVERED] = self.colors['input_border'] style.colors[imgui.COLOR_FRAME_BG_ACTIVE] = ( self.colors['primary'][0], self.colors['primary'][1], self.colors['primary'][2], 0.1 # 低透明度 ) # 标签页 style.colors[imgui.COLOR_TAB] = self.colors['button_bg'] style.colors[imgui.COLOR_TAB_HOVERED] = self.colors['primary'] style.colors[imgui.COLOR_TAB_ACTIVE] = self.colors['primary'] style.colors[imgui.COLOR_TAB_UNFOCUSED] = self.colors['button_bg'] style.colors[imgui.COLOR_TAB_UNFOCUSED_ACTIVE] = self.colors['primary_dark'] # 选择高亮 style.colors[imgui.COLOR_HEADER] = self.colors['primary'] style.colors[imgui.COLOR_HEADER_HOVERED] = self.colors['primary'] style.colors[imgui.COLOR_HEADER_ACTIVE] = self.colors['primary_dark'] # 分隔符 style.colors[imgui.COLOR_SEPARATOR] = self.colors['border_secondary'] style.colors[imgui.COLOR_SEPARATOR_HOVERED] = self.colors['border'] style.colors[imgui.COLOR_SEPARATOR_ACTIVE] = self.colors['primary'] # 调整尺寸和间距 style.window_padding = self.sizes['window_padding'] style.window_rounding = self.sizes['window_rounding'] style.window_min_size = (200, 100) style.child_rounding = self.sizes['frame_rounding'] style.frame_padding = self.sizes['frame_padding'] style.frame_rounding = self.sizes['frame_rounding'] style.item_spacing = self.sizes['item_spacing'] style.item_inner_spacing = self.sizes['item_inner_spacing'] style.indent_spacing = self.sizes['indent_spacing'] style.scrollbar_size = self.sizes['scrollbar_size'] style.scrollbar_rounding = self.sizes['frame_rounding'] style.grab_min_size = 10.0 style.grab_rounding = self.sizes['frame_rounding'] # 禁用一些ImGui的默认效果,使其更像Qt style.window_border_size = 1.0 style.child_border_size = 1.0 style.popup_border_size = 1.0 style.frame_border_size = 1.0 print("✓ ImGui样式已应用,与Qt UI保持一致") def get_window_flags(self, window_type="default"): """获取不同类型窗口的标志(支持docking)""" base_flags = 0 if window_type == "main_menu": return imgui.WindowFlags_.menu_bar elif window_type == "dockable": # 移除NO_MOVE和NO_RESIZE以支持docking return (base_flags | imgui.WindowFlags_.no_title_bar | imgui.WindowFlags_.no_collapse) elif window_type == "toolbar": # 工具栏允许docking但保持无标题栏 return (base_flags | imgui.WindowFlags_.no_title_bar | imgui.WindowFlags_.no_collapse | imgui.WindowFlags_.no_scrollbar) elif window_type == "panel": # 面板窗口完全支持docking return base_flags # 移除NO_COLLAPSE限制 return base_flags def begin_styled_window(self, name, open=True, flags=None, window_type="default"): """开始一个带样式的窗口""" if flags is None: flags = self.get_window_flags(window_type) return imgui_ctx.begin(name, open, flags) def styled_button(self, label, size=(0, 0)): """绘制带样式的按钮""" return imgui.button(label, size) def styled_input_text(self, label, value, buffer_size=256): """绘制带样式的文本输入框""" changed, new_value = imgui.input_text(label, value, buffer_size) return changed, new_value def styled_slider_float(self, label, value, min_val, max_val, format="%.3f"): """绘制带样式的浮点滑块""" changed, new_value = imgui.slider_float(label, value, min_val, max_val, format) return changed, new_value def load_icon(self, icon_name): """加载图标纹理为ImGui可用的格式""" try: # 构建图标路径 project_root = Path(__file__).resolve().parent.parent icon_path = project_root / "icons" / f"{icon_name}.png" if icon_path.exists(): # 使用base.imgui.loadTexture方法 if hasattr(base, 'imgui') and hasattr(base.imgui, 'loadTexture'): return base.imgui.loadTexture(str(icon_path)) else: print(f"⚠ ImGui后端未初始化") return None else: print(f"⚠ 图标文件不存在: {icon_path}") return None except Exception as e: print(f"⚠ 加载图标失败: {e}") return None def image_button(self, texture_id, size=(32, 32), bg_col=(0, 0, 0, 0), tint_col=(1, 1, 1, 1)): """绘制图像按钮""" return imgui.image_button(texture_id, size, bg_col, tint_col) def get_icon_text_button(self, icon_texture, text, size=(0, 0)): """绘制带图标的文本按钮""" if icon_texture: # 先绘制图标 imgui.image(icon_texture, (16, 16)) imgui.same_line() # 再绘制文本按钮 return imgui.button(text, size)