# ui_main_window.py
from datetime import datetime
import json
import os
import sys
from pathlib import Path
from PyQt6.QtWidgets import (
QWidget, QVBoxLayout, QHBoxLayout, QLabel,
QLineEdit, QPushButton, QTextEdit, QFileDialog,
QComboBox, QGroupBox, QApplication, QMessageBox,
QDialog, QCheckBox, QRadioButton
)
from PyQt6.QtCore import QObject, pyqtSignal
import logging
print(f" 当前工作目录: {os.getcwd()}")
print(f" 文件所在目录: {Path(__file__).resolve()}")
from excel_to_clm import ExcelToCLMConverter
from channel.range_channel_sync import CLMRangeSynchronizer
from power.power_sync import PowerTableSynchronizer
from rate_set.rate_sync import RateSetSynchronizer
from utils import get_output_dir
# ============= 日志重定向器 ==============
class LogEmitter(QObject):
log_signal = pyqtSignal(str)
# ============= 主窗口类 ==============
class CLMGeneratorUI(QWidget):
# 类顶部定义(靠近 class CLMGeneratorUI(QWidget):)
LOCALE_UI_MAPPING = [
("2_4G", "locale_2g_idx"),
("2_4G_HT", "locale_2g_ht_idx"),
("5G", "locale_5g_idx"),
("5G_HT", "locale_5g_ht_idx"),
("6G", "locale_6g_idx"),
("6G_HT", "locale_6g_ht_idx"),
]
def __init__(self, generator=None):
super().__init__()
self.generator = generator or ExcelToCLMConverter()
self.log_emitter = LogEmitter()
self.log_emitter.log_signal.connect(self.update_log_area)
# 设置日志系统以捕获同步器输出
self._setup_logging_redirect()
self.setWindowTitle("Wi-Fi CLM Generator Tool v1.2")
self.resize(700, 600)
self.init_ui()
self.load_default_config_for_power_tables()
def _setup_module_logger(self, module_name: str):
"""
为指定模块名设置日志转发到 UI
:param module_name: 如 "channel", "power", "rate_set"
"""
logger = logging.getLogger(module_name)
logger.setLevel(logging.INFO)
# 定义统一的日志处理器类(在方法内定义,但会被外部引用)
class QtStreamHandler(logging.Handler):
def __init__(self, emitter):
super().__init__()
self.emitter = emitter
self.setFormatter(
logging.Formatter('%(levelname)s %(name)s: %(message)s')
)
def emit(self, record):
try:
msg = self.format(record)
if msg.strip():
self.emitter.log_signal.emit(msg.strip())
except Exception:
self.handleError(record)
# 移除旧的同类型 handler
for h in logger.handlers[:]:
if isinstance(h, QtStreamHandler):
logger.removeHandler(h)
# 添加新 handler
handler = QtStreamHandler(self.log_emitter)
logger.addHandler(handler)
logger.propagate = False # 防止被父 logger 重复处理
def _setup_logging_redirect(self):
"""统一注册多个模块的日志转发"""
modules = ["channel", "power", "rate_set", "excel_to_clm"]
for mod in modules:
self._setup_module_logger(mod)
class LogStream:
"""伪装成文件对象的日志流,用于 logging.Handler"""
def __init__(self, emitter):
self.emitter = emitter
def write(self, msg):
if msg.strip():
self.emitter.log_signal.emit(msg.strip())
def flush(self):
pass
def init_ui(self):
layout = QVBoxLayout()
# === 1. 输入区:Excel & Config ===
input_group = QGroupBox("输入配置")
input_layout = QVBoxLayout()
# Excel 文件
excel_row = QHBoxLayout()
excel_row.addWidget(QLabel("Excel 文件:"))
self.excel_path = QLineEdit("input/Archer BE900US 2.xlsx")
self.btn_browse_excel = QPushButton("浏览...")
self.btn_browse_excel.clicked.connect(self.select_excel)
excel_row.addWidget(self.excel_path)
excel_row.addWidget(self.btn_browse_excel)
input_layout.addLayout(excel_row)
# Config 文件
config_row = QHBoxLayout()
config_row.addWidget(QLabel("Config 文件:"))
self.config_path = QLineEdit("config/config.json")
self.btn_browse_config = QPushButton("浏览...")
self.btn_browse_config.clicked.connect(self.select_config_file)
config_row.addWidget(self.config_path)
config_row.addWidget(self.btn_browse_config)
input_layout.addLayout(config_row)
# --- C 源文件(目标文件)---
cfile_row = QHBoxLayout()
cfile_row.addWidget(QLabel("目标 C 文件:"))
self.c_file_path = QLineEdit("input/wlc_clm_data_6726b0.c")
self.btn_browse_cfile = QPushButton("浏览...")
self.btn_browse_cfile.clicked.connect(self.select_c_file)
cfile_row.addWidget(self.c_file_path)
cfile_row.addWidget(self.btn_browse_cfile)
input_layout.addLayout(cfile_row) # 关键:放在这里!
# Locale ID(可选,未来可能用于过滤)
locale_row = QHBoxLayout()
locale_row.addWidget(QLabel("默认国家码:"))
self.locale_combo = QComboBox()
self.locale_combo.addItems(["US", "FCC", "EU", "JP", "CN"])
locale_row.addWidget(self.locale_combo)
input_layout.addLayout(locale_row)
input_group.setLayout(input_layout)
layout.addWidget(input_group)
# === 2. 主流程控制区(分步操作)===
step_group = QGroupBox("主流程控制(请按顺序操作)")
step_layout = QVBoxLayout()
# === 0. 全局预览模式开关 ===
self.chk_dry_run = QCheckBox(" 启用预览模式(所有写入操作不实际修改文件)")
self.chk_dry_run.setChecked(True)
self.chk_dry_run.setToolTip("勾选后所有‘写入’操作仅模拟变更,不会保存到磁盘")
layout.addWidget(self.chk_dry_run)
# 步骤 1: 解析 Excel
self.btn_parse = QPushButton(" 1. 解析 Excel 并生成数据")
self.btn_parse.setToolTip("读取Excel并生成内部配置与RANGE宏")
self.btn_parse.clicked.connect(self.run_parse)
step_layout.addWidget(self.btn_parse)
# 步骤 2: 同步选项(互斥选择)
sync_choice_layout = QHBoxLayout()
self.radio_channel = QRadioButton("插入信道范围")
self.radio_power = QRadioButton("注入功率表")
self.radio_channel.setChecked(True) # 默认选中
sync_choice_layout.addWidget(self.radio_channel)
sync_choice_layout.addWidget(self.radio_power)
step_layout.addLayout(sync_choice_layout)
# 执行按钮
self.btn_execute_sync = QPushButton(" 2. 执行选定的同步任务")
self.btn_execute_sync.clicked.connect(self.execute_selected_sync)
step_layout.addWidget(self.btn_execute_sync)
step_group.setLayout(step_layout)
layout.addWidget(step_group)
# === 3. 功率表名称配置(紧凑排布)===
power_table_group = QGroupBox("功率表名称配置")
power_table_layout = QVBoxLayout()
# 提示语
power_table_layout.addWidget(
QLabel("为每个频段指定对应的功率表名称(如 us, eu, cn_5g):")
)
# 使用网格布局限制宽度 + 控件大小
grid = QHBoxLayout() # 改为横向两列布局,节省垂直空间
left_col = QVBoxLayout()
right_col = QVBoxLayout()
table_configs = [
("2_4G", "2.4G"),
("2_4G_HT", "2.4G HT"),
("5G", "5G"),
("5G_HT", "5G HT"),
("6G", "6G"),
("6G_HT", "6G HT"),
]
self.power_table_edits = {}
for idx, (key, label) in enumerate(table_configs):
row_layout = QHBoxLayout()
row_layout.addWidget(QLabel(f"{label}:"), 1)
edit = QLineEdit(key.lower().replace("_ht", "").upper())
edit.setMaximumWidth(120) # 限制编辑框宽度
edit.setObjectName(f"edit_{key.replace('.', '_')}")
self.power_table_edits[key] = edit
row_layout.addWidget(edit, 2)
# 分左右列填充
if idx < len(table_configs) // 2:
left_col.addLayout(row_layout)
else:
right_col.addLayout(row_layout)
grid.addLayout(left_col)
grid.addLayout(right_col)
power_table_layout.addLayout(grid)
power_table_group.setLayout(power_table_layout)
layout.addWidget(power_table_group)
# === 5. 独立功能按钮:速率集同步(随时可用)===
utility_group = QGroupBox("独立工具")
utility_layout = QVBoxLayout()
self.btn_sync_rate_set = QPushButton(" 写入速率集(即时生效)")
self.btn_sync_rate_set.clicked.connect(self.run_rate_set_sync)
self.btn_sync_rate_set.setToolTip("直接将速率集数据写入C文件,不受主流程限制")
utility_layout.addWidget(self.btn_sync_rate_set)
utility_group.setLayout(utility_layout)
layout.addWidget(utility_group)
# === 6. 日志输出 ===
self.log_area = QTextEdit()
self.log_area.setReadOnly(True)
self.log_area.setFontFamily("Consolas") # 更适合日志查看的等宽字体
self.log_area.setStyleSheet("QTextEdit { font-size: 9pt; }")
self.log_area.append("🔧 CLM Generator 已启动,请开始第一步...")
layout.addWidget(self.log_area)
self.setLayout(layout)
# === 初始化状态控制 ===
self.update_button_states()
# 绑定事件以动态更新按钮状态
self.excel_path.textChanged.connect(self.update_button_states)
self.c_file_path.textChanged.connect(self.update_button_states)
def update_button_states(self):
"""根据输入字段和解析状态来启用/禁用相关按钮"""
# 检查 Excel 文件路径是否存在
excel_path = self.excel_path.text().strip()
has_excel = bool(excel_path) and os.path.exists(excel_path)
# 检查 C 文件路径是否填写
c_file_path = self.c_file_path.text().strip()
has_c_file = bool(c_file_path)
# 检查是否已完成解析(last_config 存在且非 None)
has_parsed = (
hasattr(self.generator, 'last_config')
and self.generator.last_config is not None
)
# 所有判断都返回明确的布尔值
self.btn_parse.setEnabled(has_excel)
can_sync = has_parsed and has_c_file
self.btn_execute_sync.setEnabled(bool(can_sync)) # 确保为 bool
def execute_selected_sync(self):
"""根据用户选择执行信道同步或功率表同步"""
if self.radio_channel.isChecked():
self.run_range_sync()
elif self.radio_power.isChecked():
self.run_power_sync()
else:
self.log("请选择要执行的操作类型")
def run_power_sync(self):
"""执行功率表同步:调用 PowerTableSynchronizer 完成注入"""
try:
config_path_str = self.config_path.text().strip() or "config/config.json"
config_path = Path(config_path_str)
if not config_path.exists():
self.log(f"Config 文件不存在: {config_path}")
QMessageBox.critical(self, "错误", f"配置文件不存在:\n{config_path}")
return
dry_run = self.chk_dry_run.isChecked()
# ========== 添加日志文件支持 begin ==========
file_handler = None
log_file_path = None
if not dry_run:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_dir = get_output_dir() / "log"
log_dir.mkdir(parents=True, exist_ok=True)
log_file_path = log_dir / f"power_sync_{timestamp}.log"
file_handler = logging.FileHandler(log_file_path, encoding='utf-8')
file_handler.setFormatter(
logging.Formatter('%(asctime)s [%(levelname)s] %(name)s: %(message)s', datefmt='%H:%M:%S')
)
power_logger = logging.getLogger("power")
power_logger.addHandler(file_handler)
self.log(f" 功率表日志文件已创建: {log_file_path.name}")
# ========== end ==========
self.log(" 初始化功率表同步器...")
synchronizer = PowerTableSynchronizer(
c_file_path=None,
dry_run=dry_run,
config_path=str(config_path),
)
self.log(" 开始同步功率表与国家码...")
was_modified = synchronizer.run()
if was_modified:
if dry_run:
self.log(" 预览模式:检测到变更,但未写入文件")
else:
self.log(" 功率表与国家码同步完成!")
self.log(" 所有修改已保存至目标 C 文件")
else:
self.log(" 所有 Locale 已存在,无需更新")
QMessageBox.information(self, "成功", "功率表同步完成!详情见日志。")
except Exception as e:
self.log(f" 功率表同步失败: {str(e)}")
import traceback
self.log(f"错误详情:\n{traceback.format_exc()}")
QMessageBox.critical(self, "错误", f"同步过程出错:\n{str(e)}")
finally:
# ========== 清理 FileHandler begin =========
if file_handler:
try:
file_handler.flush()
file_handler.close()
logging.getLogger("power").removeHandler(file_handler) # ✅ power
self.log(" 功率表日志文件已关闭")
except Exception as e:
print(f"[WARN] 关闭 power 日志文件失败: {e}")
if not dry_run and log_file_path:
self.log(f" 完整日志已保存至:\n{log_file_path}")
# ========== end ==========
def run_rate_set_sync(self):
"""执行速率集同步"""
try:
config_path_str = self.config_path.text().strip() or "config/config.json"
config_path = Path(config_path_str)
if not config_path.exists():
self.log(f"Config 文件不存在: {config_path}")
QMessageBox.critical(self, "错误", f"配置文件不存在:\n{config_path}")
return
c_file_path_str = self.c_file_path.text().strip()
if not c_file_path_str:
self.log(" 未指定目标 C 文件路径")
QMessageBox.warning(self, "警告", "请先设置目标 C 文件路径")
return
c_file_path = Path(c_file_path_str)
if not c_file_path.exists():
self.log(f"目标 C 文件不存在: {c_file_path}")
QMessageBox.critical(self, "错误", f"C 文件不存在:\n{c_file_path}")
return
dry_run = self.chk_dry_run.isChecked()
# ========== 添加日志文件支持 begin ==========
file_handler = None
log_file_path = None
if not dry_run:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_dir = get_output_dir() / "log"
log_dir.mkdir(parents=True, exist_ok=True)
log_file_path = log_dir / f"rate_sync_{timestamp}.log"
file_handler = logging.FileHandler(log_file_path, encoding='utf-8')
file_handler.setFormatter(
logging.Formatter('%(asctime)s [%(levelname)s] %(name)s: %(message)s', datefmt='%H:%M:%S')
)
rate_logger = logging.getLogger("rate_set")
rate_logger.addHandler(file_handler)
self.log(f" 速率集日志文件已创建: {log_file_path.name}")
# ========== end ==========
self.log(" 初始化 [速率集同步器] ...")
try:
synchronizer = RateSetSynchronizer(
c_file_path=str(c_file_path),
dry_run=dry_run,
config_path=str(config_path)
)
except Exception as e:
self.log(f" ❌ 初始化失败: {e}")
QMessageBox.critical(self, "错误", f"无法创建速率集同步器:\n{str(e)}")
return
self.log(f" 开始同步速率集数据... (模式: {'预览' if dry_run else '执行'})")
result = synchronizer.run()
success = result.get("success", False)
changed = result.get("changed", False)
if success:
if dry_run:
if changed:
self.log(" 📊 预览完成:检测到变更,确认无误后可关闭预览模式执行")
else:
self.log(" ℹ️ 预览完成:无新数据需要注入")
QMessageBox.information(self, "预览完成", "预览模式下未修改文件。")
else:
if changed:
backup = result.get("backup")
self.log(f" ✅ 速率集同步成功!")
self.log(f" 📁 修改文件: {result['file']}")
if backup:
self.log(f" 💾 备份文件: {Path(backup).name}")
QMessageBox.information(self, "成功", f"速率集已写入!\n备份: {Path(backup).name}")
else:
self.log(" ℹ️ 所有速率集均已存在,无需更新")
QMessageBox.information(self, "无需更新", "所有条目已存在,无需修改。")
else:
error = result.get("error", "未知错误")
self.log(f" ❌ 同步失败: {error}")
QMessageBox.critical(self, "失败", f"同步过程中发生错误:\n{error}")
except Exception as e:
self.log(f" ⚠️ 意外错误: {type(e).__name__}: {e}")
import traceback
self.log(f"详细堆栈:\n{traceback.format_exc()}")
QMessageBox.critical(self, "严重错误", f"程序异常终止:\n{str(e)}")
finally:
# ========== 清理 FileHandler begin ==========
if file_handler:
try:
file_handler.flush()
file_handler.close()
logging.getLogger("rate_set").removeHandler(file_handler)
self.log(" 功率表日志文件已关闭")
except Exception as e:
print(f"[WARN] 关闭 power 日志文件失败: {e}")
if not dry_run and log_file_path:
self.log(f" 完整日志已保存至:\n{log_file_path}")
# ========== end ==========
def run_parse(self):
"""解析 Excel 并生成输出(使用真实的 ExcelToCLMConverter.convert 流程)"""
file_handler = None
log_file_path = None
try:
excel_path = self.excel_path.text().strip()
config_path_str = self.config_path.text().strip() or "config/config.json"
dry_run = False # 假设 parse 总是实际执行(非预览)
# ========== 添加日志文件支持 begin ==========
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_dir = get_output_dir() / "log"
log_dir.mkdir(parents=True, exist_ok=True)
log_file_path = log_dir / f"parse_{timestamp}.log"
file_handler = logging.FileHandler(log_file_path, encoding='utf-8')
file_handler.setFormatter(
logging.Formatter('%(asctime)s [%(levelname)s] %(name)s: %(message)s', datefmt='%H:%M:%S')
)
# 使用独立的 logger 名称
parse_logger = logging.getLogger("excel_to_clm")
parse_logger.addHandler(file_handler)
parse_logger.setLevel(logging.INFO)
self.log(f"📝 已创建解析日志文件: {log_file_path.name}")
# ========== end ==========
self.log("INFO 开始解析 Excel 文件...")
if not excel_path:
self.log("ERROR 请先选择 Excel 文件!")
return
if not os.path.exists(excel_path):
self.log(f"ERROR Excel 文件不存在: {excel_path}")
return
if not os.path.exists(config_path_str):
self.log(f"ERROR 配置文件不存在: {config_path_str}")
return
# === Step A: 收集 locale_map ===
import re
locale_map = {}
for display_name, enum_key in self.LOCALE_UI_MAPPING:
text = self.power_table_edits[display_name].text().strip()
if not text:
self.log(f"ERROR [{display_name}] 功率表名称不能为空!")
return
if not re.match(r'^[a-zA-Z0-9_]+$', text):
self.log(f"ERROR [{display_name}] 名称非法:'{text}' → 只能含字母、数字、下划线")
return
locale_map[enum_key] = text
main_locale = locale_map["locale_2g_idx"]
# === Step B: 更新 config.json 中的 assigned_locale ===
saved_names, full_config = self._read_locales_from_config(config_path_str)
if full_config is None:
self.log("ERROR 加载配置文件失败,终止解析")
return
updated_count = 0
for tgt in full_config.get("locale_targets", []):
enum = tgt.get("enum")
if enum in locale_map:
new_loc = locale_map[enum]
old_loc = tgt.get("assigned_locale")
if old_loc != new_loc:
tgt["assigned_locale"] = new_loc
self.log(f"INFO {enum}: '{old_loc}' → '{new_loc}'")
updated_count += 1
if updated_count > 0:
try:
with open(config_path_str, 'w', encoding='utf-8') as f:
json.dump(full_config, f, indent=4, ensure_ascii=False)
self.log(f"INFO 已更新 {updated_count} 个 assigned_locale 并保存到 config.json")
except Exception as e:
self.log(f"ERROR 保存 config 失败: {e}")
import traceback
self.log(f"DEBUG {traceback.format_exc()}")
return
# === Step C: 调用 convert() ===
try:
self.generator = ExcelToCLMConverter(
config_path=config_path_str,
locale_display_name=main_locale
)
self.log("INFO 调用 convert() 开始处理...")
self.generator.convert(excel_path)
self.log("✅ Excel 解析与代码生成完成!")
except Exception as e:
self.log(f"ERROR 执行 convert() 失败: {type(e).__name__}: {e}")
import traceback
self.log(f"DEBUG 详细堆栈:\n{traceback.format_exc()}")
QMessageBox.critical(self, "解析失败", f"转换过程中发生错误:\n{str(e)}")
return
self.update_button_states()
except Exception as e:
self.log(f"❌ 解析流程意外中断: {e}")
import traceback
self.log(f"TRACEBACK:\n{traceback.format_exc()}")
finally:
# ========== 清理 FileHandler begin ==========
if file_handler:
try:
file_handler.flush()
file_handler.close()
logging.getLogger("excel_to_clm").removeHandler(file_handler)
self.log("📁 解析日志文件已关闭")
except Exception as e:
print(f"[WARN] 关闭 parse 日志文件失败: {e}")
if log_file_path:
self.log(f"📄 完整日志已保存至:\n{log_file_path}")
# ========== end ==========
def select_excel(self):
file, _ = QFileDialog.getOpenFileName(
self, "选择 Excel 文件", "", "Excel Files (*.xlsx *.xls)")
if file:
self.excel_path.setText(file)
def select_config_file(self):
file, _ = QFileDialog.getOpenFileName(
self, "选择配置文件", "config/", "JSON 配置文件 (*.json);;All Files (*)")
if file:
self.config_path.setText(file)
self.load_default_config_for_power_tables()
def select_c_file(self):
file, _ = QFileDialog.getOpenFileName(
self, "选择 C 源文件", "input/", "C Source Files (*.c)")
if file:
self.c_file_path.setText(file)
def log(self, msg):
"""发送日志消息到信号队列"""
self.log_emitter.log_signal.emit(msg)
def update_log_area(self, msg):
"""更新日志文本框"""
self.log_area.append(f"> {msg}")
print(f"[LOG] {msg}") # ← 关键:让开发者能看到
def clear_log(self):
self.log_area.clear()
def show_preview(self):
try:
output_file = get_output_dir()
with open(output_file, "r", encoding="utf-8") as f:
content = f.read()
dialog = QDialog(self)
dialog.setWindowTitle(f"预览: {output_file}")
dialog.resize(700, 500)
layout = QVBoxLayout()
text_edit = QTextEdit()
text_edit.setReadOnly(True)
text_edit.setText(content[:2000] + "\n...(后续内容已截断)")
close_btn = QPushButton("关闭")
close_btn.clicked.connect(dialog.accept)
layout.addWidget(text_edit)
layout.addWidget(close_btn)
dialog.setLayout(layout)
dialog.exec()
except FileNotFoundError:
self.log(f" 无法预览: 文件未生成 '{output_file}',请先点击【开始生成】")
except Exception as e:
self.log(f" 预览失败: {str(e)}")
def run_range_sync(self,):
"""运行同步:使用 generator 生成的 manifest 路径自动同步到 C 文件"""
from pathlib import Path
import logging
from datetime import datetime
if not hasattr(self.generator, 'last_config') or not self.generator.last_config:
self.log(" 尚未生成任何 文件,请先点击【解析 Excel】")
return
c_file_path = self.c_file_path.text().strip()
if not c_file_path:
self.log(" 未指定目标 C 文件路径,请在下方输入或选择")
return
if not Path(c_file_path).exists():
self.log(f" 目标 C 文件不存在: {c_file_path}")
return
file_handler = None
log_file_path = None
dry_run = self.chk_dry_run.isChecked()
if not dry_run:
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
log_dir = Path(get_output_dir()) / "log"
log_dir.mkdir(parents=True, exist_ok=True)
log_file_path = log_dir / f"range_sync_{timestamp}.log"
file_handler = logging.FileHandler(log_file_path, encoding='utf-8')
file_handler.setFormatter(
logging.Formatter('%(asctime)s [%(levelname)s] %(message)s', datefmt='%H:%M:%S')
)
target_logger = logging.getLogger("channel")
target_logger.addHandler(file_handler)
self.log(f" 日志文件已创建: {log_file_path}")
try:
mode = "预览模式" if dry_run else "执行模式"
self.log(f" 开始同步... ({mode})")
self.log(f" 修改文件: {c_file_path}")
sync = CLMRangeSynchronizer(
c_file_path=c_file_path,
dry_run=dry_run
)
was_modified = sync.run()
if dry_run:
if was_modified:
self.log("发现需要更新的内容,确认无误后可点击【执行同步】")
else:
self.log("所有 RANGE 已存在,无需修改")
else:
if was_modified:
self.log(f"同步成功!已更新 {c_file_path}")
self.log(f"备份已保存为 {c_file_path}.bak")
else:
self.log("文件已是最新,无需修改")
except Exception as e:
self.log(f"同步失败: {str(e)}")
import traceback
tb_msg = traceback.format_exc()
self.log(f"错误详情:\n{tb_msg}")
raise
finally:
if file_handler:
try:
file_handler.flush()
file_handler.close()
logging.getLogger("channel").removeHandler(file_handler)
self.log("日志文件已关闭")
except Exception as e:
print(f"Warning: failed to close file handler: {e}")
if not dry_run and log_file_path:
self.log(f"完整日志已保存至:\n{log_file_path}")
def load_default_config_for_power_tables(self):
"""从当前 config.json 自动填充空的功率表名称"""
config_path_str = self.config_path.text().strip() or "config/config.json"
saved_names, _ = self._read_locales_from_config(config_path_str)
if not saved_names:
return
filled_count = 0
for display_name, enum_key in self.LOCALE_UI_MAPPING:
edit_widget = self.power_table_edits.get(display_name)
if not edit_widget:
continue
if not edit_widget.text().strip() and enum_key in saved_names:
edit_widget.setText(saved_names[enum_key])
filled_count += 1
if filled_count > 0:
self.log(f" 已从 config.json 自动填充 {filled_count} 个功率表名称")
def _read_locales_from_config(self, config_path_str):
"""从 config.json 读取 enum -> assigned_locale 映射"""
try:
if not os.path.exists(config_path_str):
raise FileNotFoundError(f"Config 文件不存在: {config_path_str}")
with open(config_path_str, 'r', encoding='utf-8') as f:
full_config = json.load(f)
saved_names = {}
for tgt in full_config.get("locale_targets", []):
enum = tgt.get("enum")
loc = tgt.get("assigned_locale")
if enum and loc:
saved_names[enum] = loc.strip()
return saved_names, full_config
except Exception as e:
self.log(f"读取配置失败: {e}")
import traceback
self.log(traceback.format_exc())
return {}, None
def run_full_process(self):
"""一键完成:解析 Excel → 生成 manifest → 同步到 .c 文件"""
reply = QMessageBox.question(
self,
"确认执行",
"是否要执行【解析Excel + 更新C文件】全流程?\n\n"
"建议先使用【预览同步】检查变更。",
QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No
)
if reply != QMessageBox.StandardButton.Yes:
return
self.run_parse()
if hasattr(self.generator, 'last_config') and self.generator.last_config:
self.run_range_sync()
else:
self.log(" 解析失败,无法继续同步")
if __name__ == '__main__':
import sys
import traceback
from PyQt6.QtWidgets import QMessageBox
def handle_exception(exc_type, exc_value, exc_traceback):
if issubclass(exc_type, KeyboardInterrupt):
sys.__excepthook__(exc_type, exc_value, exc_traceback)
return
print("".join(traceback.format_exception(exc_type, exc_value, exc_traceback)))
QMessageBox.critical(None, "未捕获异常", f"{exc_type.__name__}: {exc_value}")
sys.excepthook = handle_exception
app = QApplication([])
try:
window = CLMGeneratorUI()
window.show()
sys.exit(app.exec())
except Exception as e:
msg = QMessageBox()
msg.setWindowTitle("严重错误")
msg.setText(f"程序启动失败:\n\n{type(e).__name__}: {e}\n\n详情查看日志或联系开发者。")
msg.setIcon(QMessageBox.Icon.Critical)
msg.exec()
print(f"Critical error: {e}")
打开打包后的文件双击就显示找不到配置文件退出,问题出在哪,我觉得配置文件要手动选择