428 lines
17 KiB
Python
428 lines
17 KiB
Python
"""
|
||
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) |