diff --git a/import_vba.py b/import_vba.py index 09848a5..8a9354b 100644 --- a/import_vba.py +++ b/import_vba.py @@ -1,17 +1,14 @@ """ -VBA代码导入工具 (最终修正版) -1. 修复路径识别问题 -2. 修复中文乱码问题 (GB18030) -3. 修复类模块(Class)被错误导入为标准模块的问题 (补全 VERSION 1.0 CLASS 头) +VBA代码导入工具 +使用 .env 配置直接导入 VBA 代码,无需元数据文件 """ import os import sys -import json import shutil import tempfile from pathlib import Path -from typing import Dict, Any +from typing import Dict, List, Tuple # 加载 .env 配置文件 try: @@ -29,44 +26,68 @@ except ImportError: sys.exit(1) # ==================== 配置区域 ==================== -# 从 .env 文件读取配置,如果未设置则使用 None(默认模式) -TARGET_METADATA_FILE = os.getenv("TARGET_METADATA_FILE", "").strip() or None +# 从 .env 文件读取配置 +# 目标 xlsm 文件路径(用于导入VBA代码) +TARGET_XLSM_FILE = os.getenv("TARGET_XLSM_FILE", "").strip() or None +# VBA代码输出目录(如果未设置,则使用源文件同目录下的VBA文件夹) +VBA_OUTPUT_DIR = os.getenv("VBA_OUTPUT_DIR", "").strip() or None # ================================================= # 常量定义 -METADATA_FILE = "vba_metadata.json" -VBA_DIR_NAME = "VBA" +STANDARD_MODULE_DIR = "Modules" +CLASS_MODULE_DIR = "ClassModules" +DOCUMENT_MODULE_DIR = "DocumentModules" +FORMS_DIR = "Forms" 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) + def __init__(self, vba_dir: str, target_file: str): + """ + 初始化VBA导入器 - if not candidate_path.is_absolute(): - candidate_path = self.project_root / candidate_path + Args: + vba_dir: VBA代码目录(包含 Modules, ClassModules 等子目录) + target_file: 目标 Excel 文件路径 + """ + self.vba_dir = Path(vba_dir).resolve() + self.target_file = Path(target_file).resolve() - self.target_file = candidate_path + def _scan_modules(self) -> List[Dict]: + """ + 扫描 VBA 目录,收集所有模块信息 - 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() + Returns: + 模块信息列表,每个元素包含: + - name: 模块名称 + - type: 模块类型目录 + - file_path: 源文件完整路径 + - ext: 文件扩展名 + """ + modules = [] + + # 定义扫描目录和对应的扩展名 + scan_dirs = [ + (STANDARD_MODULE_DIR, ".bas"), + (CLASS_MODULE_DIR, ".cls"), + (DOCUMENT_MODULE_DIR, ".cls"), + (FORMS_DIR, ".frm"), + ] + + for dir_name, ext in scan_dirs: + dir_path = self.vba_dir / dir_name + if not dir_path.exists(): + continue + + for file_path in dir_path.glob(f"*{ext}"): + modules.append({ + "name": file_path.stem, # 文件名不含扩展名 + "type": dir_name, + "file_path": file_path, + "ext": ext + }) + + return modules def _clean_component_name(self, name: str) -> str: """去除模块名称中的扩展名""" @@ -76,7 +97,7 @@ class VBAImporter: 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: + def _reconstruct_file_content(self, code_path: Path, module_name: str, module_type: str) -> Path: """ 读取纯代码文件,重建完整的导入文件 关键逻辑: @@ -90,39 +111,31 @@ class VBAImporter: # 创建临时文件 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": + if module_type == CLASS_MODULE_DIR: 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("") content_lines.append(code_body) - + # 4. 写入临时文件 (GB18030 防止乱码) try: with open(temp_file_path, 'w', encoding='gb18030', errors='replace') as f: @@ -131,7 +144,7 @@ class VBAImporter: 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): @@ -140,17 +153,21 @@ class VBAImporter: print(f"错误: 找不到目标 Excel 文件: {self.target_file}") return False + if not self.vba_dir.exists(): + print(f"错误: 找不到 VBA 代码目录: {self.vba_dir}") + 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: @@ -158,22 +175,19 @@ class VBAImporter: return False print("开始导入模块...\n") - - modules = self.metadata.get("modules", {}) + + # 扫描所有模块 + modules = self._scan_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 + if not modules: + print("警告: 未找到任何 VBA 模块文件") + return False + + for module_info in modules: + module_name = module_info["name"] + module_type_dir = module_info["type"] + source_code_path = module_info["file_path"] component = None try: @@ -182,8 +196,8 @@ class VBAImporter: component = None # 标准模块和类模块支持删除重建 - is_reloadable = module_type_dir in ["Modules", "ClassModules"] - + is_reloadable = module_type_dir in [STANDARD_MODULE_DIR, CLASS_MODULE_DIR] + # --------------------------------------------------------- # 策略 A: 导入文件模式 (Modules, ClassModules) # --------------------------------------------------------- @@ -193,33 +207,32 @@ class VBAImporter: vb_project.VBComponents.Remove(component) except Exception as e: print(f" [警告] 无法移除 {module_name}: {e},将尝试仅更新代码") - is_reloadable = False - + 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_file = self._reconstruct_file_content(source_code_path, 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": + if module_type_dir == FORMS_DIR: print(f" [警告] 无法恢复 UserForm '{module_name}',跳过。") continue - elif module_type_dir == "DocumentModules": + elif module_type_dir == DOCUMENT_MODULE_DIR: print(f" [警告] 找不到文档对象 '{module_name}',跳过。") continue try: - component = vb_project.VBComponents.Add(1) + component = vb_project.VBComponents.Add(1) component.Name = module_name except: print(f" [错误] 无法创建组件 {module_name}") @@ -230,14 +243,14 @@ class VBAImporter: 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}") @@ -267,7 +280,7 @@ class VBAImporter: import traceback traceback.print_exc() return False - + finally: if workbook: try: workbook.Close(SaveChanges=False) @@ -278,47 +291,52 @@ class VBAImporter: def main(): print("=" * 60) - print("VBA代码导入工具 (V3.0 最终版)") + print("VBA代码导入工具 (V4.0 - 基于配置)") print("=" * 60) print() script_dir = Path(__file__).parent - # 检查是否配置了目标元数据文件 - if TARGET_METADATA_FILE and TARGET_METADATA_FILE.strip(): - # 使用配置的元数据文件路径 - target_path = Path(TARGET_METADATA_FILE) + # 检查配置 + if not TARGET_XLSM_FILE: + print("错误: 未配置 TARGET_XLSM_FILE") + print("请在 .env 文件中设置目标 Excel 文件路径") + return - # 如果是相对路径,则相对于脚本所在目录 - if not target_path.is_absolute(): - target_path = script_dir / target_path + # 确定目标文件路径 + target_path = Path(TARGET_XLSM_FILE) + if not target_path.is_absolute(): + target_path = script_dir / target_path - if not target_path.exists(): - print(f"错误: 配置的元数据文件不存在: {target_path}") - return + if not target_path.exists(): + print(f"错误: 配置的文件不存在: {target_path}") + return - if not target_path.name == METADATA_FILE: - print(f"警告: 文件名不是 {METADATA_FILE}: {target_path.name}") + if not target_path.suffix.lower() == '.xlsm': + print(f"警告: 文件扩展名不是 .xlsm: {target_path.name}") - metadata_path = target_path - print(f"使用配置的元数据文件: {metadata_path.name}") - print() + # 确定 VBA 代码目录 + if VBA_OUTPUT_DIR: + vba_path = Path(VBA_OUTPUT_DIR) + if not vba_path.is_absolute(): + vba_path = script_dir / vba_path else: - # 默认模式:查找脚本所在目录下的 VBA 文件夹 - vba_dir = script_dir / VBA_DIR_NAME - metadata_path = vba_dir / METADATA_FILE + # 使用目标文件同目录下的 VBA 文件夹 + vba_path = target_path.parent / "VBA" - if not metadata_path.exists(): - print(f"错误: 找不到元数据文件: {metadata_path}") - print() - print("提示:") - print(" 1. 确保已运行 extract_vba.py 提取 VBA 代码") - print(" 2. 或在脚本顶部配置 TARGET_METADATA_FILE 指定元数据文件路径") - return + if not vba_path.exists(): + print(f"错误: VBA 代码目录不存在: {vba_path}") + print() + print("提示:") + print(" 1. 确保已运行 extract_vba.py 提取 VBA 代码") + print(" 2. 或在 .env 文件中设置 VBA_OUTPUT_DIR 指定代码目录") + return - print(f"读取元数据: {metadata_path}") + print(f"目标 Excel 文件: {target_path.name}") + print(f"VBA 代码目录: {vba_path}") + print() - # 显示目标文件信息 + # 确认操作 print("=" * 60) print("警告: 此操作将覆盖目标 Excel 文件中的 VBA 代码。") choice = input("\n确认继续? (y/n): ").lower().strip() @@ -327,13 +345,11 @@ def main(): print("操作已取消") return - importer = VBAImporter(str(metadata_path)) + importer = VBAImporter(str(vba_path), str(target_path)) - # 显示导入目标信息 - print() - print(f"目标 Excel 文件: {importer.target_file.name}") - print(f"VBA 代码目录: {importer.vba_dir}") - print(f"模块数量: {len(importer.metadata.get('modules', {}))}") + # 显示模块数量 + modules = importer._scan_modules() + print(f"找到 {len(modules)} 个模块文件") print() print("=" * 60) print() @@ -349,4 +365,4 @@ def main(): print("=" * 60) if __name__ == "__main__": - main() \ No newline at end of file + main()