VC++实战:打造工业级OCX/DLL注册工具,解决32/64位注册难题
1. 项目概述:为什么需要自己写一个注册程序?
在Windows平台上搞开发,尤其是用VC++做桌面应用或者ActiveX控件,你肯定遇到过这个场景:辛辛苦苦写好的程序,发给用户或者部署到客户机器上,一运行就弹窗报错,什么“类未注册”、“找不到指定的模块”、“0x80040154”之类的。这时候,你多半会告诉用户:“哥们儿,以管理员身份打开命令行,cd到那个目录,然后输入regsvr32 MyControl.ocx注册一下”。运气好点,用户会照做;但更多时候,你会收到一连串的追问:“命令行是啥?”、“怎么管理员运行?”、“cd是啥意思?”。
这就是我们为什么要自己动手,用VC++写一个OCX/AX/DLL注册程序的根本原因。它解决的远不止是“注册”这个动作本身,而是用户体验和部署可靠性的最后一公里问题。一个图形化的、一键式的注册工具,能把技术细节完全隐藏起来,让最终用户(甚至是不懂技术的实施人员)也能轻松完成组件部署。更进一步,一个专业的注册程序还能在后台智能判断系统架构(32位还是64位)、自动处理依赖、记录注册日志,并在卸载时干净地清理注册表,这才是工业级软件该有的样子。
从技术角度看,Regsvr32.exe是微软提供的标准工具,但它只是个命令行程序,功能单一,缺乏容错和友好提示。当你的软件需要注册多个组件、或者需要根据环境进行条件注册时,手动调用Regsvr32就显得笨拙且容易出错。自己实现注册逻辑,意味着你可以把组件注册作为软件安装包中的一个自动化步骤,或者提供一个独立的“环境修复工具”,极大提升产品的专业度和用户满意度。
2. 核心原理:Windows组件注册到底在做什么?
在动手写代码之前,我们必须搞清楚,当我们在说“注册一个OCX或DLL”时,操作系统底层究竟发生了哪些变化。这不仅仅是调用一个API那么简单,理解原理才能写出健壮的程序。
2.1 COM组件的注册表“户口本”
OCX(OLE Control Extension)和AX(ActiveX)本质上都是COM(Component Object Model)组件。DLL如果提供了COM接口,也属于此类。COM组件的核心思想是“二进制重用”,为了让应用程序能在运行时找到并加载这些二进制文件,系统需要一个中央目录——这就是Windows注册表。
注册一个COM组件,主要是在注册表的特定位置写入以下几类关键信息:
- CLSID(类标识符):这是一个128位的全局唯一标识符(GUID),例如
{00024500-0000-0000-C000-000000000046}。它是组件的“身份证号”。注册时,会在HKEY_CLASSES_ROOT\CLSID\{你的CLSID}下创建一个项。 - InprocServer32(进程内服务器):在上述CLSID项下,会创建
InprocServer32子项,其默认值被设置为该OCX/DLL文件的完整绝对路径。这就是系统寻找组件二进制文件的依据。 - TypeLib(类型库):如果组件包含了类型库(通常存在于OCX或DLL的资源段中),会注册类型库的GUID(LibID),并在
HKEY_CLASSES_ROOT\TypeLib\{LibID}下记录其路径和版本信息。这对于VBScript、JavaScript等脚本语言或VB调用组件至关重要。 - 接口(Interface)和程序ID(ProgID):注册IID(接口ID),并将可读的程序ID(如
”MyControl.Application”)映射到对应的CLSID,方便通过名字创建对象。
Regsvr32.exe的工作,就是调用目标文件中的一个标准导出函数(DllRegisterServer),由这个函数内部执行上述所有的注册表写入操作。同理,卸载时调用DllUnregisterServer来删除这些条目。
2.2 32位与64位的注册表重定向(Registry Redirection)陷阱
这是手动注册和编写注册程序时最容易踩的坑。在64位Windows系统上,存在两个regsvr32.exe:%windir%\System32\regsvr32.exe(64位) 和%windir%\SysWOW64\regsvr32.exe(32位)。为了兼容32位应用程序,系统采用了注册表重定向机制:
- 64位进程访问的
HKEY_CLASSES_ROOT\CLSID实际是HKEY_LOCAL_MACHINE\SOFTWARE\Classes\CLSID。 - 32位进程访问的
HKEY_CLASSES_ROOT\CLSID会被重定向到HKEY_LOCAL_MACHINE\SOFTWARE\**WOW6432Node**\Classes\CLSID。
这意味着:如果你用64位的regsvr32注册了一个32位的DLL,它的CLSID会被写入64位视图的注册表。当一个32位的应用程序(运行在WOW64下)尝试创建这个对象时,它去32位视图(WOW6432Node下)里找,结果就是找不到,报“类未注册”。
关键心得:注册程序必须能够判断目标DLL的位数(32位PE文件还是64位PE文件),并启动对应位数的注册进程。一个取巧但实用的方法是:无论自身是32位还是64位程序,都尝试启动
%windir%\SysWOW64\regsvr32.exe来注册,如果失败(比如文件不是32位),再尝试用%windir%\System32\regsvr32.exe。更底层的做法是直接调用LoadLibrary和GetProcAddress获取DllRegisterServer函数指针来执行,此时你的调用进程位数必须与目标DLL位数匹配,否则会加载失败。
2.3 权限要求:为什么总是要“以管理员身份运行”?
向HKEY_LOCAL_MACHINE (HKLM)下的注册表项写入数据,属于系统级操作,需要管理员权限。如果只是为当前用户注册,可以写入HKEY_CURRENT_USER (HKCU)下的对应位置,这通常不需要提升权限。Regsvr32默认注册到HKLM,所以需要管理员权限。
在自制的注册程序中,我们可以通过清单文件(Manifest)声明requestedExecutionLevel level=”requireAdministrator”,这样程序启动时就会自动请求UAC提权。或者,我们可以设计得更友好:先尝试注册到HKCU(用户级),如果因为某些原因(如组件设计时只支持系统级注册)失败,再提示用户需要管理员权限并重启程序。
3. 实战:用VC++打造一个健壮的注册程序
理解了原理,我们开始动手。我们将创建一个基于MFC对话框的应用程序,它不仅能注册/卸载单个文件,还能批量处理,并智能应对位数和权限问题。
3.1 项目创建与基础框架
打开Visual Studio,创建一个新的MFC应用程序项目,选择基于对话框的类型。为了界面清晰,我们在对话框资源上放置以下控件:
- 一个列表框(List Box)或列表控件(List Control):用于显示待注册的文件列表。
- “添加文件”按钮:支持多选,添加.ocx, .dll, .ax等文件。
- “移除”按钮:从列表中移除选中项。
- “注册”按钮:执行注册操作。
- “卸载”按钮:执行卸载操作。
- “清除日志”按钮。
- 一个多行编辑框(Edit Control)或静态文本区域:用于显示操作日志,设置为只读(Read-only)。
- 几个单选按钮(Radio Button):让用户选择注册到“所有用户(HKLM)”还是“当前用户(HKCU)”。
3.2 核心功能一:判断DLL/OCX的位数
这是正确注册的前提。我们不能依赖文件扩展名,必须解析PE(Portable Executable)文件头。
// 函数:判断PE文件是32位还是64位 BOOL Is64BitPE(const CString& strFilePath) { HANDLE hFile = CreateFile(strFilePath, GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, 0, NULL); if (hFile == INVALID_HANDLE_VALUE) return FALSE; HANDLE hMapping = CreateFileMapping(hFile, NULL, PAGE_READONLY, 0, 0, NULL); if (!hMapping) { CloseHandle(hFile); return FALSE; } LPVOID pBase = MapViewOfFile(hMapping, FILE_MAP_READ, 0, 0, 0); if (!pBase) { CloseHandle(hMapping); CloseHandle(hFile); return FALSE; } PIMAGE_DOS_HEADER pDosHeader = (PIMAGE_DOS_HEADER)pBase; if (pDosHeader->e_magic != IMAGE_DOS_SIGNATURE) { UnmapViewOfFile(pBase); CloseHandle(hMapping); CloseHandle(hFile); return FALSE; } PIMAGE_NT_HEADERS pNtHeaders = (PIMAGE_NT_HEADERS)((BYTE*)pBase + pDosHeader->e_lfanew); if (pNtHeaders->Signature != IMAGE_NT_SIGNATURE) { UnmapViewOfFile(pBase); CloseHandle(hMapping); CloseHandle(hFile); return FALSE; } BOOL bIs64Bit = FALSE; #ifdef _WIN64 // 我们自己是64位程序,可以直接使用64位结构 PIMAGE_NT_HEADERS64 pNtHeaders64 = (PIMAGE_NT_HEADERS64)pNtHeaders; if (pNtHeaders64->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) { bIs64Bit = TRUE; } #else // 我们是32位程序,但也要能判断64位PE文件 if (pNtHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR64_MAGIC) { // 需要将指针转换为64位头结构,这里需要小心计算偏移 // 简化处理:通过Magic值判断。IMAGE_NT_OPTIONAL_HDR64_MAGIC 表示64位 bIs64Bit = TRUE; } else if (pNtHeaders->OptionalHeader.Magic == IMAGE_NT_OPTIONAL_HDR32_MAGIC) { bIs64Bit = FALSE; } #endif UnmapViewOfFile(pBase); CloseHandle(hMapping); CloseHandle(hFile); return bIs64Bit; }注意事项:直接内存映射并解析PE文件是高效的方法,但代码略显复杂。对于新手,一个更简单但稍慢的替代方案是使用
LoadLibraryEx配合LOAD_LIBRARY_AS_DATAFILE标志来加载DLL(不执行代码),然后使用GetProcAddress尝试获取一个已知的64位特有导出函数(但这方法不通用)。或者,可以调用命令行工具dumpbin /headers Your.dll | findstr “machine”并解析输出,但这会创建新进程,开销较大。在生产环境中,推荐使用上述的PE头解析法。
3.3 核心功能二:实现注册与卸载逻辑
我们有两条路可以走:1) 创建进程调用regsvr32;2) 动态加载DLL并调用其导出函数。这里我们展示更可控、更专业的第二种方法。
// 函数:直接调用DLL中的注册/卸载函数 CString RegisterDllDirect(const CString& strDllPath, BOOL bRegister, BOOL bAllUsers) { CString strLog; HMODULE hModule = LoadLibraryEx(strDllPath, NULL, LOAD_WITH_ALTERED_SEARCH_PATH); if (!hModule) { strLog.Format(_T("[错误] 无法加载模块: %s, 错误码: %d\r\n"), strDllPath, GetLastError()); return strLog; } // 根据操作获取函数指针 FARPROC pFunc = NULL; if (bRegister) { pFunc = GetProcAddress(hModule, "DllRegisterServer"); } else { pFunc = GetProcAddress(hModule, "DllUnregisterServer"); } if (!pFunc) { strLog.Format(_T("[警告] 模块 %s 没有找到 %s 函数。它可能不是COM组件。\r\n"), strDllPath, bRegister ? _T("DllRegisterServer") : _T("DllUnregisterServer")); FreeLibrary(hModule); return strLog; } // 在调用前,可以临时修改注册表映射,实现“注册到当前用户” // 这是一个高级技巧,涉及使用 RegOverridePredefKey 函数。 // 简单起见,我们先实现系统级注册。 HRESULT hr = S_OK; __try { hr = ((HRESULT (STDAPICALLTYPE *)(void))pFunc)(); } __except(EXCEPTION_EXECUTE_HANDLER) { hr = E_UNEXPECTED; strLog += _T("[异常] 调用注册函数时发生异常。\r\n"); } if (SUCCEEDED(hr)) { strLog.Format(_T("[成功] %s: %s\r\n"), bRegister ? _T("注册") : _T("卸载"), strDllPath); } else { strLog.Format(_T("[失败] %s: %s, HRESULT=0x%08X\r\n"), bRegister ? _T("注册") : _T("卸载"), strDllPath, hr); } FreeLibrary(hModule); return strLog; }为什么使用LOAD_WITH_ALTERED_SEARCH_PATH?这个标志告诉系统,在解析DLL的依赖项时,优先使用目标DLL所在目录作为搜索路径,而不是调用者(即我们的注册程序)的目录。这能避免因依赖的DLL不在系统路径而导致的“模块加载失败”错误。
关于“按用户注册”的实现: 要实现注册到HKCU,不能简单地调用DllRegisterServer,因为大多数组件的这个函数内部写死了HKLM路径。一个可行的方法是使用RegOverridePredefKeyAPI,在调用DllRegisterServer前,将HKEY_CLASSES_ROOT (HKCR)这个预定义键临时重定向到HKCU\Software\Classes。调用结束后再恢复。这需要仔细处理,并且要确保你的进程有足够的权限操作HKCU。
// 伪代码示例:重定向HKCR到HKCU以实现用户级注册 HKEY hOrigHKCR = NULL; HKEY hRedirectedHKCR = NULL; // 打开或创建 HKCU\Software\Classes RegCreateKeyEx(HKEY_CURRENT_USER, _T("Software\\Classes"), 0, NULL, 0, KEY_ALL_ACCESS, NULL, &hRedirectedHKCR, NULL); // 重定向 RegOverridePredefKey(HKEY_CLASSES_ROOT, hRedirectedHKCR); // 保存原始键值(需要事先获取,比较复杂,此处简化) // ... 调用 DllRegisterServer ... // 恢复重定向 RegOverridePredefKey(HKEY_CLASSES_ROOT, hOrigHKCR); // 或传入NULL恢复系统默认 RegCloseKey(hRedirectedHKCR);3.4 核心功能三:处理位数匹配与进程创建
如果我们选择通过创建regsvr32进程的方式(对于初学者更简单,且能利用系统工具的完整性),就需要处理位数匹配。
CString RegisterViaRegsvr32(const CString& strDllPath, BOOL bRegister, BOOL bAllUsers) { CString strSysDir, strRegsvrPath; CString strLog; BOOL bIs64BitDll = Is64BitPE(strDllPath); // 决定使用哪个 regsvr32 if (bIs64BitDll) { // 需要64位的regsvr32,它在System32目录下 GetSystemDirectory(strSysDir.GetBuffer(MAX_PATH), MAX_PATH); strSysDir.ReleaseBuffer(); strRegsvrPath = strSysDir + _T("\\regsvr32.exe"); } else { // 32位DLL,在64位系统上要用SysWOW64下的regsvr32,在32位系统上就是System32下的 // 一个兼容性更好的方法是:总是先尝试在SysWOW64下找(64位系统),如果找不到再用System32 TCHAR szWinDir[MAX_PATH]; GetWindowsDirectory(szWinDir, MAX_PATH); CString strWow64Path; strWow64Path.Format(_T("%s\\SysWOW64\\regsvr32.exe"), szWinDir); if (PathFileExists(strWow64Path)) { strRegsvrPath = strWow64Path; } else { // 可能是32位系统,回退到System32 GetSystemDirectory(strSysDir.GetBuffer(MAX_PATH), MAX_PATH); strSysDir.ReleaseBuffer(); strRegsvrPath = strSysDir + _T("\\regsvr32.exe"); } } // 构建命令行 CString strCmdLine; strCmdLine.Format(_T("\"%s\" /s %s \"%s\""), strRegsvrPath, bRegister ? _T("") : _T("/u"), // /u 表示卸载 strDllPath); // 如果是“当前用户”注册,regsvr32没有直接参数,我们需要用重定向技巧,或者放弃此方式。 // 这里我们仅演示系统级注册。 if (!bAllUsers) { strLog += _T("[信息] regsvr32方式暂不支持用户级注册,已按系统级处理。\r\n"); } STARTUPINFO si = { sizeof(si) }; PROCESS_INFORMATION pi; si.dwFlags = STARTF_USESHOWWINDOW; si.wShowWindow = SW_HIDE; // 隐藏黑窗口,提升体验 BOOL bSuccess = CreateProcess(NULL, strCmdLine.GetBuffer(), NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi); strCmdLine.ReleaseBuffer(); if (bSuccess) { WaitForSingleObject(pi.hProcess, INFINITE); // 等待注册完成 DWORD exitCode; GetExitCodeProcess(pi.hProcess, &exitCode); CloseHandle(pi.hProcess); CloseHandle(pi.hThread); if (exitCode == 0) { strLog.Format(_T("[成功] 通过regsvr32 %s: %s\r\n"), bRegister ? _T("注册") : _T("卸载"), strDllPath); } else { strLog.Format(_T("[失败] regsvr32执行失败,退出码: %d, 文件: %s\r\n"), exitCode, strDllPath); } } else { strLog.Format(_T("[错误] 无法启动regsvr32进程,错误码: %d\r\n"), GetLastError()); } return strLog; }4. 高级功能与异常处理
一个基本的注册程序已经成型,但要达到“工业级”,还需要考虑更多细节。
4.1 批量处理与日志记录
我们的程序支持文件列表,批量处理就是遍历列表,对每个文件调用上述注册函数。关键在于良好的日志记录和错误恢复。我们需要一个线程或至少在一个循环中处理,并将每一步的结果(成功、失败及原因)实时追加到日志编辑框中。为了避免界面假死,对于批量操作,强烈建议使用工作线程(Worker Thread)。
MFC中后台线程处理示例:
// 在对话框类中声明 CWinThread* m_pWorkThread; static UINT WorkerThreadProc(LPVOID pParam); // “批量注册”按钮响应函数 void CRegToolDlg::OnBnClickedBatchRegister() { // 禁用界面按钮,防止重复操作 GetDlgItem(IDC_BATCH_REG)->EnableWindow(FALSE); // 清空或初始化日志 m_strLog.Empty(); UpdateData(FALSE); // 传递必要的参数到线程(例如文件列表指针、注册选项) THREAD_PARAMS* pParams = new THREAD_PARAMS; pParams->pFileList = &m_listFiles; // 假设m_listFiles是文件列表CStringArray pParams->bRegister = TRUE; pParams->bAllUsers = m_bAllUsers; // 从单选按钮获取 pParams->pDlg = this; // 用于回调更新UI m_pWorkThread = AfxBeginThread(WorkerThreadProc, pParams); } UINT CRegToolDlg::WorkerThreadProc(LPVOID pParam) { THREAD_PARAMS* pParams = (THREAD_PARAMS*)pParam; // 遍历文件列表,逐个注册 for (int i = 0; i < pParams->pFileList->GetSize(); i++) { CString strFile = pParams->pFileList->GetAt(i); CString strResult = RegisterDllDirect(strFile, pParams->bRegister, pParams->bAllUsers); // 或使用regsvr32方式 // 通过消息或回调,将strResult发送到主线程更新UI ::PostMessage(pParams->pDlg->m_hWnd, WM_UPDATE_LOG, (WPARAM)new CString(strResult), 0); // 可以加入Sleep(10)避免过快 } // 发送线程完成消息 ::PostMessage(pParams->pDlg->m_hWnd, WM_WORKER_FINISHED, 0, 0); delete pParams; return 0; } // 在主对话框消息映射中处理自定义消息 ON_MESSAGE(WM_UPDATE_LOG, OnUpdateLog) ON_MESSAGE(WM_WORKER_FINISHED, OnWorkerFinished) LRESULT CRegToolDlg::OnUpdateLog(WPARAM wParam, LPARAM lParam) { CString* pStrLog = (CString*)wParam; m_editLog.SetSel(-1, -1); // 移动光标到末尾 m_editLog.ReplaceSel(*pStrLog); delete pStrLog; return 0; }4.2 依赖项检查与自动注册
有些OCX/DLL依赖于其他的DLL(如MSVCRT, MFC DLL等)。如果依赖项缺失,注册会失败。一个增强功能是,在注册主组件前,先检查其依赖。可以用dumpbin /dependents Your.ocx命令来获取依赖列表,然后检查这些DLL是否在系统的搜索路径(应用程序目录、系统目录、PATH等)中存在。对于常见的VC运行库(如msvcp140.dll, vcruntime140.dll),可以提示用户安装对应的Visual C++ Redistributable,或者在你的安装包中附带并静默安装它们。
4.3 静默注册与命令行支持
为了让你的注册程序能被其他安装工具(如Inno Setup, InstallShield)或脚本调用,应该支持命令行参数。例如:
MyRegTool.exe /register "C:\MyControls\Control1.ocx" "C:\MyControls\Control2.dll" /silent MyRegTool.exe /unregister "C:\MyControls\Control1.ocx" /allusers在InitInstance()函数中,可以解析__argc和__targv,如果发现命令行参数,则不显示GUI,直接执行相应操作后退出。
5. 常见问题排查与实战心得
即使程序写得再完善,在实际部署中还是会遇到各种奇怪的问题。这里分享一些我踩过的坑和排查思路。
5.1 典型错误与解决方案速查表
| 错误现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 0x8007007E / 0x800700C1 | 找不到指定的模块 / %1 不是有效的 Win32 应用程序。 | 1.位数不匹配:最常见!用PE工具检查DLL和调用进程位数。确保用对应位数的regsvr32或匹配的进程加载。 2.依赖缺失:使用 Dependency Walker或dumpbin /dependents检查缺少的DLL,并确保它们位于搜索路径。 |
| 0x80040154 (REGDB_E_CLASSNOTREG) | 类未注册。 | 1.注册失败但未报错:可能注册到了错误的注册表视图(32/64位)。检查WOW6432Node下是否有你的CLSID。 2.路径错误:注册表里InprocServer32的路径与实际DLL路径不一致(比如移动了文件)。重新注册。 |
| 0x80004005 (E_FAIL) | 未指定的错误。 | 最棘手的错误。通常发生在DllRegisterServer函数内部。1. 使用Process Monitor过滤你的DLL进程,查看它在注册过程中访问了哪些注册表项、文件,失败在哪一步。2. 检查DLL是否依赖其他COM组件,那些组件是否已正确注册。 3. 组件代码本身可能有bug(如内存访问违规)。 |
| Regsvr32 成功但程序仍报错 | 1. 程序缓存了旧的类型库信息。 2. 需要重启资源管理器或应用程序。 3. 注册到了HKCU但程序在系统服务或其他用户账户下运行。 | 1. 尝试重启应用程序,甚至重启计算机。 2. 对于Explorer Shell扩展,可能需要 taskkill /f /im explorer.exe & start explorer.exe。3. 确保注册位置(HKLM/HKCU)与应用程序运行上下文匹配。 |
| 权限不足 (5) | 尝试向HKLM写数据但没有管理员权限。 | 1. 确保你的注册程序以管理员身份运行(清单文件设置)。 2. 或者,修改代码尝试注册到HKCU。 |
5.2 必备调试与排查工具
- Process Monitor (ProcMon):来自SysInternals的瑞士军刀。设置过滤器(
Process Name包含regsvr32.exe或你的程序名,Operation包含RegOpenKey,RegCreateKey,LoadImage),它能让你看清注册过程中所有文件、注册表、网络的访问细节,是定位“E_FAIL”类模糊错误的终极武器。 - Dependency Walker:老牌但经典的依赖分析工具。可以图形化显示DLL的导入、导出函数及依赖树,快速定位缺失的DLL。注意,它在分析64位DLL时可能需要在64位系统下运行64位版本。
- Visual Studio 调试器:如果你是组件的开发者,在
DllRegisterServer函数入口处下断点,然后用你的注册程序(附加进程或直接启动调试)去触发,可以单步跟踪注册逻辑,查看内部错误。 - Registry Editor:手动搜索你的CLSID或ProgID,检查注册表项是否正确、完整。对比注册成功和失败前后的注册表差异,有时能发现线索。
5.3 我个人的几点实操心得
- “先诊断,后操作”:在注册按钮的逻辑里,可以先调用
Is64BitPE判断文件位数,并在日志中显示“目标文件为32位/64位”,让用户心里有数。如果检测到当前程序是32位而目标是64位DLL,直接给出明确错误提示,避免无谓的尝试。 - 备份注册表:在实现卸载功能时,特别是对于自己不太了解的第三方组件,可以在删除注册表项前,先将其导出备份到临时文件。这能给你一个“后悔药”。
- 超时处理:对于通过创建
regsvr32进程的方式,一定要设置超时等待(比如30秒),使用WaitForSingleObject配合超时参数,防止某个DLL的注册函数卡死导致你的程序界面无响应。 - 日志是生命线:日志不仅要记录成功失败,还要尽可能记录详细信息:尝试的路径、使用的regsvr32路径、返回的错误码(HRESULT)、系统错误码(GetLastError)。将这些信息格式化输出,用户在求助时,你一眼就能看出问题所在。
- 考虑提供“修复”模式:对于“DllRegisterServer”返回失败的情况,可以尝试先强制调用
DllUnregisterServer清理可能存在的残破注册项,然后再尝试注册。有时旧的错误注册信息会阻碍新的注册。
最后,这个工具的价值会随着你遇到的注册问题越多而越显珍贵。把它打磨好,不仅能方便自己,也能成为你交付给客户或团队的一个贴心小工具。代码的健壮性就体现在对这些边界情况和异常流程的处理上,多测试,多踩坑,你的注册程序就会越来越可靠。
