From c80e03e1eae2a777cd6f6cfb7ab5809f48ab67c6 Mon Sep 17 00:00:00 2001 From: Misaka_Company Date: Mon, 19 Jan 2026 17:06:15 +0800 Subject: [PATCH] add import_vba.py for VBA code import tool with path recognition and encoding fixes --- import_vba.py | 291 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 291 insertions(+) create mode 100644 import_vba.py diff --git a/import_vba.py b/import_vba.py new file mode 100644 index 0000000..b316ff9 --- /dev/null +++ b/import_vba.py @@ -0,0 +1,291 @@ +""" +VBA代码导入工具 (最终修正版) +1. 修复路径识别问题 +2. 修复中文乱码问题 (GB18030) +3. 修复类模块(Class)被错误导入为标准模块的问题 (补全 VERSION 1.0 CLASS 头) +""" + +import os +import sys +import json +import shutil +import tempfile +from pathlib import Path +from typing import Dict, Any + +try: + import win32com.client as win32 +except ImportError: + print("错误: 未安装 pywin32 库") + print("请运行: pip install pywin32") + sys.exit(1) + +# 常量定义 +METADATA_FILE = "vba_metadata.json" +VBA_DIR_NAME = "VBA" + +class VBAImporter: + """VBA代码导入器""" + + def __init__(self, metadata_path: str): + self.metadata_path = Path(metadata_path).resolve() + self.vba_dir = self.metadata_path.parent + self.project_root = self.vba_dir.parent + + with open(self.metadata_path, 'r', encoding='utf-8') as f: + self.metadata = json.load(f) + + # 处理 Excel 文件路径 + raw_path = self.metadata.get("source_file", "") + candidate_path = Path(raw_path) + + if not candidate_path.is_absolute(): + candidate_path = self.project_root / candidate_path + + self.target_file = candidate_path + + if not self.target_file.exists(): + print(f"提示: 在路径 {self.target_file} 未找到文件,尝试搜索...") + excel_dir = self.project_root / "Excel" + if excel_dir.exists(): + files = list(excel_dir.glob("*.xlsm")) + if files: + print(f" -> 找到替代文件: {files[0].name}") + self.target_file = files[0] + + self.target_file = self.target_file.resolve() + + def _clean_component_name(self, name: str) -> str: + """去除模块名称中的扩展名""" + lower_name = name.lower() + for ext in ['.cls', '.bas', '.frm']: + if lower_name.endswith(ext): + return name[:-len(ext)] + return name + + def _reconstruct_file_content(self, code_path: Path, attributes: Dict[str, str], module_name: str, module_type: str) -> Path: + """ + 读取纯代码文件,重建完整的导入文件 + 关键逻辑: + 1. ClassModules 需要 'VERSION 1.0 CLASS' 头部,否则会被识别为标准模块。 + 2. 使用 GB18030 编码写入,防止中文乱码。 + """ + # 1. 读取源代码 (UTF-8) + with open(code_path, 'r', encoding='utf-8') as f: + code_body = f.read() + + # 创建临时文件 + temp_dir = Path(tempfile.gettempdir()) / "vba_import_temp" + temp_dir.mkdir(exist_ok=True) + + # 确定扩展名 + orig_ext = code_path.suffix + temp_file_path = temp_dir / f"{module_name}{orig_ext}" + + content_lines = [] + + # ----------------------------------------------------------- + # 【关键修复】如果是类模块,必须添加 VERSION 头部块 + # ----------------------------------------------------------- + if module_type == "ClassModules": + content_lines.append("VERSION 1.0 CLASS") + content_lines.append("BEGIN") + content_lines.append(" MultiUse = -1 'True") + content_lines.append("END") + # 注意:Attribute VB_Name 必须紧跟在 END 之后 + + # 2. 重建 Attribute VB_Name + content_lines.append(f'Attribute VB_Name = "{module_name}"') + + # 3. 重建其他 Attribute + for key, value in attributes.items(): + if key == "VB_Name": + continue + if value.lower() in ['true', 'false']: + line = f'Attribute {key} = {value}' + else: + line = f'Attribute {key} = "{value}"' + content_lines.append(line) + + content_lines.append("") + content_lines.append(code_body) + + # 4. 写入临时文件 (GB18030 防止乱码) + try: + with open(temp_file_path, 'w', encoding='gb18030', errors='replace') as f: + f.write('\n'.join(content_lines)) + except Exception as e: + print(f" [警告] 编码转换失败,尝试回退到 utf-8: {e}") + with open(temp_file_path, 'w', encoding='utf-8') as f: + f.write('\n'.join(content_lines)) + + return temp_file_path + + def import_vba(self): + """执行导入过程""" + if not self.target_file.exists(): + print(f"错误: 找不到目标 Excel 文件: {self.target_file}") + return False + + print(f"正在打开 Excel 文件: {self.target_file.name} ...") + + excel = None + workbook = None + try: + excel = win32.Dispatch("Excel.Application") + excel.Visible = False + excel.DisplayAlerts = False + + workbook = excel.Workbooks.Open(str(self.target_file)) + + try: + vb_project = workbook.VBProject + except Exception: + print("错误: 无法访问 VBA 项目。请确保信任对 VBA 工程对象模型的访问。") + return False + + print("开始导入模块...\n") + + modules = self.metadata.get("modules", {}) + temp_files_created = [] + + for file_key, info in modules.items(): + raw_module_name = info["name"] + module_name = self._clean_component_name(raw_module_name) + module_type_dir = info["type"] # e.g., "Modules", "ClassModules" + rel_path = info["file"] + attributes = info.get("attributes", {}) + + source_code_path = self.vba_dir / rel_path + + if not source_code_path.exists(): + print(f" [跳过] 找不到源文件: {rel_path}") + continue + + component = None + try: + component = vb_project.VBComponents(module_name) + except: + component = None + + # 标准模块和类模块支持删除重建 + is_reloadable = module_type_dir in ["Modules", "ClassModules"] + + # --------------------------------------------------------- + # 策略 A: 导入文件模式 (Modules, ClassModules) + # --------------------------------------------------------- + if is_reloadable: + if component: + try: + vb_project.VBComponents.Remove(component) + except Exception as e: + print(f" [警告] 无法移除 {module_name}: {e},将尝试仅更新代码") + is_reloadable = False + + if is_reloadable: + # 生成临时导入文件 + # 【修改】传入 module_type_dir 以判断是否需要加 Class 头 + temp_file = self._reconstruct_file_content(source_code_path, attributes, module_name, module_type_dir) + temp_files_created.append(temp_file) + + try: + vb_project.VBComponents.Import(str(temp_file)) + print(f" [导入] {module_name} ({module_type_dir})") + except Exception as e: + print(f" [错误] 导入 {module_name} 失败: {e}") + + # --------------------------------------------------------- + # 策略 B: 字符串注入模式 (Sheet, Workbook, Forms) + # --------------------------------------------------------- + if not is_reloadable: + if not component: + if module_type_dir == "Forms": + print(f" [警告] 无法恢复 UserForm '{module_name}',跳过。") + continue + elif module_type_dir == "DocumentModules": + print(f" [警告] 找不到文档对象 '{module_name}',跳过。") + continue + try: + component = vb_project.VBComponents.Add(1) + component.Name = module_name + except: + print(f" [错误] 无法创建组件 {module_name}") + continue + + try: + code_module = component.CodeModule + num_lines = code_module.CountOfLines + if num_lines > 0: + code_module.DeleteLines(1, num_lines) + + # 直接读取 UTF-8 字符串到内存 + with open(source_code_path, 'r', encoding='utf-8') as f: + new_code = f.read() + + if new_code.strip(): + code_module.AddFromString(new_code) + + print(f" [更新] {module_name} ({module_type_dir}) - 代码已更新") + except Exception as e: + print(f" [错误] 更新代码 {module_name} 失败: {e}") + + # 清理 + for p in temp_files_created: + try: + if p.exists(): p.unlink() + except: pass + try: + temp_dir = Path(tempfile.gettempdir()) / "vba_import_temp" + if temp_dir.exists(): shutil.rmtree(temp_dir) + except: pass + + print("\n正在编译 VBA 项目...") + try: + workbook.Save() + print("已保存更改。") + except Exception as e: + print(f"保存文件时出错: {e}") + + print(f"\n导入完成!目标文件: {self.target_file.name}") + return True + + except Exception as e: + print(f"\n发生未处理的错误: {e}") + import traceback + traceback.print_exc() + return False + + finally: + if workbook: + try: workbook.Close(SaveChanges=False) + except: pass + if excel: + try: excel.Quit() + except: pass + +def main(): + print("=" * 60) + print("VBA代码导入工具 (V3.0 最终版)") + print("=" * 60) + print() + + script_dir = Path(__file__).parent + vba_dir = script_dir / VBA_DIR_NAME + metadata_path = vba_dir / METADATA_FILE + + if not metadata_path.exists(): + print(f"错误: 找不到元数据文件: {metadata_path}") + return + + print(f"读取元数据: {metadata_path}") + print("警告: 此操作将覆盖目标 Excel 文件中的 VBA 代码。") + choice = input("\n确认继续? (y/n): ").lower().strip() + + if choice != 'y': + return + + importer = VBAImporter(str(metadata_path)) + importer.import_vba() + +if __name__ == "__main__": + main() \ No newline at end of file