""" 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()