This commit is contained in:
2025-05-11 12:30:41 +08:00
parent bdc873d295
commit eea67c91c8
3 changed files with 564 additions and 47 deletions

View File

@@ -73,6 +73,10 @@ class RiggingUI(ui_utils.BaseUI):
self.main_widget = QtWidgets.QWidget()
self.main_widget.setObjectName("riggingMainWidget")
# 初始化DNA文件列表和按钮尺寸
self.dna_files = []
self.dna_button_size = (120, 150)
# 初始化UI
self.create_widgets()
self.create_layouts()
@@ -81,6 +85,29 @@ class RiggingUI(ui_utils.BaseUI):
# 更新UI文本
self.update_language()
# 加载DNA文件和预览图 - 移到最后执行确保所有控件已创建
self.load_dna_files()
def closeEvent(self, event):
"""
窗口关闭事件处理
清理缓存和资源
"""
try:
# 清理图片缓存
from scripts.utils import utils_rigging
utils_rigging.clear_pixmap_cache()
# 调用父类的关闭事件
super().closeEvent(event)
except Exception as e:
print(f"窗口关闭处理失败: {str(e)}")
import traceback
traceback.print_exc()
# 确保窗口正常关闭
event.accept()
#========================================= WIDGET =======================================
def create_widgets(self):
"""
@@ -121,47 +148,6 @@ class RiggingUI(ui_utils.BaseUI):
self.controls["presets_flow_layout"].setContentsMargins(5, 5, 5, 5)
self.controls["presets_flow_layout"].setSpacing(10)
# 添加测试预设项
for i in range(6): # 添加6个预设项如图中所示
col = i % 6
row = i // 6
preset_widget = QtWidgets.QWidget()
preset_widget.setObjectName(f"preset_{i}")
preset_widget.setMinimumSize(120, 150) # 设置预设项大小
preset_widget.setMaximumSize(120, 150)
# 预设布局
preset_layout = QtWidgets.QVBoxLayout(preset_widget)
preset_layout.setContentsMargins(0, 0, 0, 0)
preset_layout.setSpacing(2)
# 预设图片
preset_image = QtWidgets.QLabel()
preset_image.setObjectName(f"preset_image_{i}")
preset_image.setMinimumSize(120, 120)
preset_image.setMaximumSize(120, 120)
preset_image.setScaledContents(True)
preset_image.setStyleSheet("background-color: #333333; border: 1px solid #555555;")
# 加载测试图片
pixmap = QtGui.QPixmap(os.path.join(ASSETS_PATH, "metahuman_placeholder.png"))
if not pixmap.isNull():
preset_image.setPixmap(pixmap)
# 预设标签
preset_label = QtWidgets.QLabel("METAHUMAN")
preset_label.setObjectName(f"preset_label_{i}")
preset_label.setAlignment(QtCore.Qt.AlignCenter)
preset_label.setStyleSheet("color: white; background-color: rgba(0, 0, 0, 128);")
# 添加到布局
preset_layout.addWidget(preset_image)
preset_layout.addWidget(preset_label)
# 添加到流布局
self.controls["presets_flow_layout"].addWidget(preset_widget, row, col)
# 设置滚动区域内容
self.controls["presets_scroll_area"].setWidget(self.controls["presets_content"])
@@ -171,7 +157,7 @@ class RiggingUI(ui_utils.BaseUI):
self.controls["presets_slider_layout"].setSpacing(5)
# 数量显示
self.controls["presets_count_label"] = QtWidgets.QLabel("99")
self.controls["presets_count_label"] = QtWidgets.QLabel("0")
self.controls["presets_count_label"].setObjectName("presetsCountLabel")
self.controls["presets_count_label"].setAlignment(QtCore.Qt.AlignCenter)
self.controls["presets_count_label"].setStyleSheet("font-weight: bold;")
@@ -486,14 +472,15 @@ class RiggingUI(ui_utils.BaseUI):
# 设置分割器的伸缩因子
for i in range(self.splitters["main_splitter"].count()):
self.splitters["main_splitter"].setStretchFactor(i, 1)
# 监听预设内容区域大小变化更新DNA网格布局
self.controls["presets_content"].resizeEvent = lambda event: utils_rigging.on_presets_content_resize(event, self)
#======================================= FUNCTIONS ======================================
def create_connections(self):
"""
创建信号连接设置UI控件的交互行为
"""
# 导入绑定工具函数
from scripts.utils import utils_rigging
# 预设导入和导出按钮
self.buttons["export_presets"].clicked.connect(utils_rigging.export_dna)
@@ -506,4 +493,25 @@ class RiggingUI(ui_utils.BaseUI):
# 底部按钮连接
self.buttons["remove_all"].clicked.connect(utils_rigging.remove_all)
self.buttons["import_skeleton"].clicked.connect(utils_rigging.import_skeleton)
self.buttons["build_rigging"].clicked.connect(utils_rigging.build_rigging)
self.buttons["build_rigging"].clicked.connect(utils_rigging.build_rigging)
def load_dna_files(self):
"""
加载DNA文件和预览
在所有UI控件创建完成后执行
"""
try:
# 从utils_rigging加载DNA文件和预览图
from scripts.utils import utils_rigging
self.dna_files = utils_rigging.get_dna_files()
# 更新预设数量标签
if 'presets_count_label' in self.controls:
self.controls['presets_count_label'].setText(str(len(self.dna_files)))
# 调用utils_rigging中的加载预览按钮函数
utils_rigging.load_dna_preview_buttons(self)
except Exception as e:
import traceback
print(f"加载DNA文件失败: {str(e)}")
traceback.print_exc()

View File

@@ -20,6 +20,34 @@ QLabel#mainTitleLabel {
padding: 5px;
}
/* ==================== DNA预览按钮样式 ==================== */
QPushButton.dna-preview-button {
background-color: #2D2D30;
border: 1px solid #3E3E42;
border-radius: 8px;
padding: 0;
margin: 4px;
text-align: center;
}
QPushButton.dna-preview-button:hover {
border: 1px solid #007ACC;
background-color: #3E3E42;
}
QPushButton.dna-preview-button:pressed {
border: 1px solid #0062A3;
background-color: #1E1E1E;
}
QPushButton.dna-preview-button QLabel {
color: #FFFFFF;
font-size: 11px;
border-radius: 0;
border-bottom-left-radius: 7px;
border-bottom-right-radius: 7px;
}
/* ==================== 标签页样式 ==================== */
QTabWidget::pane {
border: 1px solid #1E1E1E;

View File

@@ -57,6 +57,477 @@ original_controller_properties = {}
#========================================== FUNCTIONS ========================================
#------------------------------------ DNA PREVIEW ------------------------------------
def get_dna_files():
"""
获取所有DNA文件及其对应的预览图
Returns:
list: 包含DNA文件信息的字典列表每个字典包含:
- name: DNA文件名不含扩展名
- dna_path: DNA文件的完整路径
- image_path: 预览图的完整路径
"""
dna_files = []
default_preview = os.path.join(DNA_IMG_PATH, "Preview.png")
# 调试信息
print(f"加载DNA文件目录: {DNA_FILE_PATH}")
print(f"加载DNA图片目录: {DNA_IMG_PATH}")
# 确保目录存在
if not os.path.exists(DNA_FILE_PATH):
print(f"错误: DNA文件目录不存在: {DNA_FILE_PATH}")
return dna_files
if not os.path.exists(DNA_IMG_PATH):
print(f"错误: DNA图片目录不存在: {DNA_IMG_PATH}")
if not os.path.exists(default_preview):
print(f"错误: 默认预览图不存在: {default_preview}")
# 列出所有DNA文件
try:
dna_file_list = os.listdir(DNA_FILE_PATH)
print(f"找到 {len(dna_file_list)} 个文件在 DNA目录")
except Exception as e:
print(f"列出DNA文件失败: {str(e)}")
return dna_files
# 获取所有可用的预览图文件名(不带扩展名),只查询一次目录提高性能
available_images = set()
try:
img_files = os.listdir(DNA_IMG_PATH)
for img_file in img_files:
if img_file.lower().endswith('.png'):
base_name = os.path.splitext(img_file)[0]
available_images.add(base_name)
except Exception as e:
print(f"列出图片文件失败: {str(e)}")
# 处理每个DNA文件
for file_name in dna_file_list:
if file_name.lower().endswith('.dna'):
# 获取不含扩展名的文件名
name = os.path.splitext(file_name)[0]
dna_path = os.path.join(DNA_FILE_PATH, file_name).replace("\\", "/")
# 检查预览图是否存在,不重复检查文件系统提高性能
if name in available_images:
image_path = os.path.join(DNA_IMG_PATH, f"{name}.png").replace("\\", "/")
else:
image_path = default_preview
print(f"未找到预览图: {name}.png, 使用默认预览图")
dna_files.append({
"name": name,
"dna_path": dna_path,
"image_path": image_path
})
print(f"成功加载 {len(dna_files)} 个DNA文件")
return dna_files
def load_selected_dna(dna_path, ui_instance=None):
"""
加载用户选择的DNA文件
Args:
dna_path (str): DNA文件路径
ui_instance: UI实例用于更新UI状态
Returns:
bool: 是否成功加载
"""
try:
if not os.path.exists(dna_path):
print(f"DNA文件不存在: {dna_path}")
return False
print(f"加载DNA文件: {dna_path}")
# 更新UI中的DNA路径输入框如果有UI实例
if ui_instance and hasattr(ui_instance, 'controls') and 'presets_dna_input' in ui_instance.controls:
ui_instance.controls['presets_dna_input'].setText(dna_path)
# 这里可以添加实际的DNA文件加载逻辑
# 例如通过MetaHuman DNA Calibration库读取DNA文件
return True
except Exception as e:
print(f"加载DNA文件失败: {str(e)}")
return False
def calculate_grid_layout(parent_width, item_width, spacing):
"""
根据父容器宽度计算每行可以容纳的项目数量
Args:
parent_width (int): 父容器宽度
item_width (int): 每个项目的宽度
spacing (int): 项目间的间距
Returns:
int: 每行可容纳的项目数量
"""
if parent_width <= 0 or item_width <= 0:
return 1
# 计算每行可容纳的项目数量
available_width = parent_width - spacing # 减去左边距
items_per_row = max(1, int(available_width / (item_width + spacing)))
return items_per_row
# 预缓存的图片缓存,减少重复加载
_pixmap_cache = {}
_pixmap_cache_max_size = 50 # 最多缓存50张图片
_pixmap_cache_lru = [] # 最近最少使用列表,用于清理缓存
def get_pixmap(image_path, width, height):
"""
从缓存获取QPixmap或创建新的
Args:
image_path: 图片路径
width: 所需宽度
height: 所需高度
Returns:
QPixmap: 图片对象
"""
cache_key = f"{image_path}_{width}_{height}"
# 检查缓存
if cache_key in _pixmap_cache:
# 更新LRU列表
global _pixmap_cache_lru
if cache_key in _pixmap_cache_lru:
_pixmap_cache_lru.remove(cache_key)
_pixmap_cache_lru.append(cache_key)
return _pixmap_cache[cache_key]
try:
# 加载原始图片
pixmap = QtGui.QPixmap(image_path)
if not pixmap.isNull():
# 缩放图片
if width > 0 and height > 0:
pixmap = pixmap.scaled(width, height, QtCore.Qt.KeepAspectRatio, QtCore.Qt.SmoothTransformation)
# 缓存清理 - 如果超出最大缓存大小,删除最不常用的项目
global _pixmap_cache_max_size
if len(_pixmap_cache) >= _pixmap_cache_max_size and _pixmap_cache_lru:
oldest_key = _pixmap_cache_lru.pop(0)
if oldest_key in _pixmap_cache:
del _pixmap_cache[oldest_key]
# 存入缓存
_pixmap_cache[cache_key] = pixmap
_pixmap_cache_lru.append(cache_key)
return pixmap
else:
# 创建一个纯色背景作为替代
fallback_pixmap = QtGui.QPixmap(width, height)
fallback_pixmap.fill(QtGui.QColor(60, 60, 60))
return fallback_pixmap
except Exception as e:
print(f"加载图像失败 ({image_path}): {str(e)}")
fallback_pixmap = QtGui.QPixmap(width, height)
fallback_pixmap.fill(QtGui.QColor(60, 60, 60))
return fallback_pixmap
def create_dna_preview_button(dna_info, item_size, on_click_callback):
"""
创建DNA预览按钮
Args:
dna_info (dict): DNA文件信息
item_size (tuple): 按钮尺寸 (宽, 高)
on_click_callback (function): 点击回调函数
Returns:
QPushButton: 创建的DNA预览按钮
"""
try:
# 创建按钮
button = QtWidgets.QPushButton()
button.setObjectName(f"dna_button_{dna_info['name']}")
button.setFixedSize(item_size[0], item_size[1])
button.setProperty("class", "dna-preview-button")
# 创建垂直布局
layout = QtWidgets.QVBoxLayout(button)
layout.setContentsMargins(0, 0, 0, 0)
layout.setSpacing(2)
# 创建图像标签
image_label = QtWidgets.QLabel()
image_label.setObjectName(f"image_{dna_info['name']}")
image_label.setFixedSize(item_size[0], item_size[0]) # 正方形图像区域
image_label.setScaledContents(True)
# 使用预缓存加载图像
pixmap = get_pixmap(dna_info['image_path'], item_size[0], item_size[0])
image_label.setPixmap(pixmap)
# 创建名称标签
name_label = QtWidgets.QLabel(dna_info['name'])
name_label.setObjectName(f"name_{dna_info['name']}")
name_label.setAlignment(QtCore.Qt.AlignCenter)
name_label.setStyleSheet("background-color: rgba(0, 0, 0, 128); padding: 2px;")
# 添加到布局
layout.addWidget(image_label)
layout.addWidget(name_label)
# 设置按钮数据
button.setProperty("dna_path", dna_info['dna_path'])
# 连接点击信号
button.clicked.connect(lambda: on_click_callback(dna_info['dna_path']))
return button
except Exception as e:
print(f"创建DNA预览按钮失败 ({dna_info.get('name', 'unknown')}): {str(e)}")
import traceback
traceback.print_exc()
# 返回一个简单的替代按钮
fallback_button = QtWidgets.QPushButton(dna_info.get('name', 'DNA'))
fallback_button.setFixedSize(item_size[0], item_size[1])
if on_click_callback:
fallback_button.clicked.connect(lambda: on_click_callback(dna_info.get('dna_path', '')))
return fallback_button
# 缓存上一次的布局信息,避免不必要的重新计算
_last_layout_info = {
"parent_width": 0,
"item_size": (0, 0),
"spacing": 0,
"items_per_row": 0
}
def load_dna_preview_buttons(ui_instance):
"""
为UI加载DNA预览按钮
Args:
ui_instance: UI实例
"""
try:
# 检查UI实例是否有效
if not ui_instance or not hasattr(ui_instance, 'controls'):
print("无效的UI实例无法加载DNA预览按钮")
return False
# 获取DNA文件列表
dna_files = ui_instance.dna_files
# 检查是否有DNA文件
if not dna_files:
# 如果还没有加载DNA文件则先加载
ui_instance.dna_files = get_dna_files()
dna_files = ui_instance.dna_files
if not dna_files:
print("没有找到DNA文件无法加载预览按钮")
if 'presets_count_label' in ui_instance.controls:
ui_instance.controls['presets_count_label'].setText("0")
return False
# 更新预设数量标签
if 'presets_count_label' in ui_instance.controls:
ui_instance.controls['presets_count_label'].setText(str(len(dna_files)))
# 获取滚动区域宽度
if 'presets_content' in ui_instance.controls:
scroll_width = ui_instance.controls['presets_content'].width()
else:
scroll_width = 400 # 默认宽度
# 更新DNA网格布局
update_dna_grid_layout(
ui_instance.controls["presets_flow_layout"],
scroll_width,
ui_instance.dna_button_size,
10, # 间距
dna_files,
lambda dna_path: on_dna_button_clicked(dna_path, ui_instance)
)
return True
except Exception as e:
import traceback
print(f"加载DNA预览按钮失败: {str(e)}")
traceback.print_exc()
return False
def update_dna_grid_layout(flow_layout, parent_width, item_size, spacing, dna_files, on_dna_clicked):
"""
更新DNA网格布局根据父容器宽度动态调整每行显示的DNA项目数量
Args:
flow_layout (QGridLayout): 网格布局
parent_width (int): 父容器宽度
item_size (tuple): 项目尺寸 (宽, 高)
spacing (int): 项目间的间距
dna_files (list): DNA文件信息列表
on_dna_clicked (function): DNA按钮点击回调函数
"""
try:
# 检查参数有效性
if flow_layout is None:
print("错误: flow_layout 为空")
return
if not dna_files:
print("警告: 没有DNA文件可以显示")
return
if parent_width <= 0:
print(f"警告: 父容器宽度无效 ({parent_width})")
parent_width = 400 # 使用默认宽度
global _last_layout_info
# 计算每行可容纳的项目数量
items_per_row = calculate_grid_layout(parent_width, item_size[0], spacing)
# 如果布局信息与上次相同,则不需要重新布局
if (_last_layout_info["parent_width"] == parent_width and
_last_layout_info["item_size"] == item_size and
_last_layout_info["spacing"] == spacing and
_last_layout_info["items_per_row"] == items_per_row and
flow_layout.count() > 0):
return
# 更新布局信息缓存
_last_layout_info = {
"parent_width": parent_width,
"item_size": item_size,
"spacing": spacing,
"items_per_row": items_per_row
}
print(f"布局信息: 宽度 = {parent_width}, 项目宽度 = {item_size[0]}, 每行项目数 = {items_per_row}")
# 先移除所有现有控件
for i in reversed(range(flow_layout.count())):
item = flow_layout.itemAt(i)
if item and item.widget():
item.widget().setParent(None) # 从父窗口分离
item.widget().deleteLater() # 稍后删除控件
# 重新添加DNA预览按钮
for i, dna_info in enumerate(dna_files):
try:
row = i // items_per_row
col = i % items_per_row
# 创建DNA预览按钮
dna_button = create_dna_preview_button(dna_info, item_size, on_dna_clicked)
# 添加到网格布局
flow_layout.addWidget(dna_button, row, col)
except Exception as e:
print(f"添加DNA按钮失败 ({dna_info['name']}): {str(e)}")
except Exception as e:
print(f"更新DNA网格布局失败: {str(e)}")
import traceback
traceback.print_exc()
# 保存上次调整大小的时间,用于限制调整频率
_last_resize_time = 0
_resize_throttle = 200 # 毫秒
def on_presets_content_resize(event, ui_instance):
"""
预设内容区域大小变化事件,限制更新频率以提高性能
Args:
event: 调整大小事件
ui_instance: UI实例
"""
try:
# 检查UI实例是否有效
if not ui_instance or not hasattr(ui_instance, 'controls') or "presets_content" not in ui_instance.controls:
# 如果UI实例无效只调用原始的resizeEvent
QtWidgets.QWidget.resizeEvent(event.widget(), event)
return
global _last_resize_time
# 调用父类的resizeEvent
QtWidgets.QWidget.resizeEvent(ui_instance.controls["presets_content"], event)
# 获取当前时间
current_time = QtCore.QTime.currentTime().msecsSinceStartOfDay()
# 如果距离上次更新不足阈值时间,则跳过更新
if current_time - _last_resize_time < _resize_throttle:
return
# 更新上次调整时间
_last_resize_time = current_time
# 确保所有必要的控件和属性都存在
required_keys = ["presets_flow_layout"]
required_attrs = ["dna_button_size", "dna_files"]
for key in required_keys:
if key not in ui_instance.controls:
print(f"警告: '{key}' 未在UI控件中找到")
return
for attr in required_attrs:
if not hasattr(ui_instance, attr):
print(f"警告: '{attr}' 属性未在UI实例中找到")
return
# 更新DNA网格布局
update_dna_grid_layout(
ui_instance.controls["presets_flow_layout"],
event.size().width(),
ui_instance.dna_button_size,
10, # 间距
ui_instance.dna_files,
lambda dna_path: on_dna_button_clicked(dna_path, ui_instance)
)
except Exception as e:
import traceback
print(f"调整DNA预览布局失败: {str(e)}")
traceback.print_exc()
def on_dna_button_clicked(dna_path, ui_instance):
"""
DNA按钮点击事件
Args:
dna_path: DNA文件路径
ui_instance: UI实例
"""
try:
# 检查UI实例是否有效
if not ui_instance or not hasattr(ui_instance, 'controls'):
print("警告: UI实例无效无法更新DNA路径")
# 仍然尝试加载DNA文件即使无法更新UI
load_selected_dna(dna_path)
return
# 加载选中的DNA文件
load_selected_dna(dna_path, ui_instance)
# 更新UI中的DNA路径输入框
if "presets_dna_input" in ui_instance.controls:
ui_instance.controls["presets_dna_input"].setText(dna_path)
else:
print("警告: 'presets_dna_input' 未创建")
except Exception as e:
import traceback
print(f"点击DNA按钮处理失败: {str(e)}")
traceback.print_exc()
#------------------------------------ UI UTILITIES ------------------------------------
def browse_file(parent_widget, title, line_edit, file_type=None):
"""
@@ -265,4 +736,14 @@ def build_rigging(*args):
return True
except Exception as e:
print(f"构建绑定系统失败: {str(e)}")
return False
return False
def clear_pixmap_cache():
"""
清除图片缓存
在不再需要缓存时调用,例如关闭窗口时
"""
global _pixmap_cache, _pixmap_cache_lru
_pixmap_cache.clear()
_pixmap_cache_lru.clear()
print("图片缓存已清除")