EG/core/imgui_style_manager.py
2026-02-25 11:49:31 +08:00

431 lines
17 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.

"""
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'):
# 转换路径为Panda3D兼容格式 (Windows下: D:\... -> /d/...)
# 注意: p3dimgui.loadTexture 仅支持 str 或 Texture不支持 Filename 对象
fn = Filename.fromOsSpecific(str(icon_path))
return base.imgui.loadTexture(fn.getFullpath())
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)