无冲突共享字符串资源





5.00/5 (6投票s)
在不发生冲突资源 ID 的风险下共享 Win32 字符串资源
引言
本文提出了一种解决两个问题的直接方法。
- 无忧地共享
Win32 字符串
资源,避免资源 ID 号冲突。 - 使用 Microsoft Excel 高效管理只读
Win32 字符串
资源,无需手动编辑资源脚本。
即使这些资料似乎不适用于您的需求,其中也包含许多有用的技巧,让这篇文章至少值得快速浏览一下。
2016 年 7 月 10 日,我用一篇后续文章中提供的、大大改进的代码和演示替换了原始示例, 改进的资源字符串生成器实战:你就吃自己的狗粮,而且喜欢它! 读完本文后,请阅读那篇文章,并遵循其 大大简化的操作说明。
背景
多年来,我一直对建议的共享 Win32 字符串
资源给应用程序的方法所需的记录保存感到沮丧。为什么这种记录保存没有自动化呢?有一天,我有一个特定的应用程序,如果它所有的 字符串
都存在于一个卫星 DLL 中,那么编码就会变得无限简单。在我完成它的时候,我意识到同样的方法也可以用来打包任何一组 字符串
,这样你就可以拥有像代码库一样易于使用的 字符串
库。
由于导致此需求的项目是一个库,每个使用它的应用程序都需要自定义的 字符串
库,因此我还需要一种方法来高效地生成它们。这催生了 Excel 应用程序,它使用工作表来存储 字符串
及其关联的 ID,并驱动一个 VBA 宏,该宏生成只读资源脚本及其关联的头文件,并将两者集成到 Visual Studio 项目的默认资源脚本中。当你修改一组 字符串
时,生成器会识别出辅助脚本已包含在主脚本中,并保持主资源脚本不变。
Using the Code
演示包包含以下四个目录。
Excel
包含 Microsoft Excel 工作簿,以及其 3 个 VBA 模块的导出副本和一个包含其使用的自定义日期格式器的自定义 DLL。 如果移动 Excel 工作簿,请一并带走 DLL。重要提示:2016 年 3 月 12 日星期六,我用一个新版本替换了 Excel 工作簿,该版本无需解锁ResGen Parameters
工作表即可编辑或添加配置文件标签。如果您下载了原始版本,请获取新版本;您会对新 Excel 工作簿的行为方式感到满意。SampleDLL
包含一个由 Excel 工作簿生成的示例字符串
资源 DLL,以及一个展示其运行的程序。WWStandardErrorMessages
是我为自己的项目创建的活动卫星 DLL 的当前版本。_ResourceStringLib_Template
是一个您可以作为起点用于自己的卫星 DLL 的项目。
构建代码
本文包含了我在上述后续文章中包含的简化的 CPPTimeZoneLab
示例项目。与文章原始版本附带的示例不同,CPPZonaLab
是独立的。但是,由于它演示了一个独特的用例,我保留了原始项目。
构建 CPPTimeZoneLab
CPPTimeZoneLab
不过是一个“hello, world”项目;事实上,我开始时创建了一个这样的项目,并向其中添加了一个 .RC2
文件中的只读资源字符串,这在 改进的资源字符串生成器实战:你就吃自己的狗粮,而且喜欢它! 中有更详细的解释。
构建您的卫星 DLL
复制 _ResourceStringLib_Template 目录,重命名文件(如有必要),并将其设为新配置文件的目标(下文讨论)。Visual Studio 6 项目 (_ResourceStringLib_Template.dsp) 和 Visual Studio 2013 项目 (_ResourceStringLib_Template..vcxproj) 都已配置并准备就绪。使用 Excel 工作簿生成您的 字符串
和符号,然后在 Visual Studio 6 或 Visual Studio 2013 中打开项目,按 F7 构建一个零售版库。由于它只包含数据,因此调试配置是多余的,所以我从两个项目中都删除了它。
使用 Excel 应用程序
Excel 工作簿 Win32_ResGen.XLSM
包含一个未签名的 VBA 项目。在尝试打开它之前,请调整您的 Microsoft Excel 副本的安全设置。如果您有代码签名证书(无论是自签名还是来自 CA),您可能希望使用它来签名 VBA 项目。
工作簿打开时,ResGen Parameters
工作表是活动工作表。单元格 D4 决定右侧列中定义的哪个配置文件驱动资源生成器;其值将根据一个自我维护的选择列表进行验证。生成新库必须更改的单元格用浅绿色高亮显示。其他所有内容要么是 VBA 例程所需的数据,要么是公式,在正常使用中都应保持不变,并受密码保护。如果您需要,密码是 CodeProject
。
在值列(D 列)的右侧,用一个空白列分隔,是三列值,每一列都是不同 字符串
库的参数配置文件。G 列,Example_String_Resource_Script
,对应于 SampleDLL
。名称输入在单元格 G5。您可以通过两种方式在此工作表的此部分添加自己的数据集。
- 在 G 列或 H 列的左侧插入新列。
- 覆盖这三列中任何一列的值。
尽管它们不是超链接,但第 5 行和第 6 行中的值是范围名称,都具有工作簿范围,并且是通过选择工作表 Example
的单元格 G2 到 G5 和 F2 到 F5(分别)来创建的,并使用顶部单元格作为名称,其余作为内容来定义命名范围。
要生成资源脚本和头文件,请填充配置文件,使用单元格 D4
选择它,然后按 Ctrl-Shift-G
执行宏。对于中小型库,它可以在一秒钟内完成工作,然后会显示一个消息框,如图 1 所示。
图 1 是 VBA 宏完成工作后显示的示例消息框。
提示: 如果您单击消息框,然后按 Ctrl-C
,文本和标题将捕获到 Windows 剪贴板。此技巧适用于任何标准消息框。我用它来捕获包含我需要精确文本的错误消息,例如状态码或文件名。
Standard Messages
工作表是用于生成库 WWStandardErrorMessages.dll
中存储的 字符串
并对其进行编号的输入源。该工作表包含另一个魔法范围 Standard_Error_Status_Codes
,从中生成 WWStandardErrorMessages.H 中定义的了状态码。ResGen Parameters
工作表第 7 行的单元格 F7
命名了这个范围,它在资源头模板中替换了令牌 $$StandardErrorStatusCodes$$
。
Read Only Resource Templates
工作表包含只读资源脚本及其配套资源符号头文件的模板。除非活动配置文件第 7 行的单元格已填充,否则 $$StandardErrorStatusCodes$$
令牌将被删除。
演示程序
演示库 SampleDLLDemo.exe
附带的程序故意使用了重叠的 字符串
ID 号。主程序文件中嵌入的 字符串
的 ID 号在其常规位置 resource.H
中定义。字符串
从 1 到 12 编号。存在于卫星 DLL 中的 字符串
的 ID 号在 SampleDLL.H
中定义,编号从 1 到 3。由于 SampleDLL.H
定义了一个保护变量 SAMPLEDLL_DEFINED
,因此它可以安全地包含在任何其他头文件中。保护变量名由一个令牌组成,因此每个库都有自己的保护变量。
防止冲突的魔术是 DLL 获得了自己的实例句柄,并且 LoadString
例程同时使用实例句柄和 字符串
ID 来唯一标识一个 字符串
。 LoadString 函数 的原型如下。
int WINAPI LoadString(
_In_opt_ HINSTANCE hInstance,
_In_ UINT uID,
_Out_ LPTSTR lpBuffer,
_In_ int nBufferMax
);
调用 LoadLibrayEx 函数,为 dwFlags
指定 LOAD_LIBRARY_AS_DATAFILE
(0x00000002
),以加载卫星库,然后使用返回的 HMODULE
作为调用 LoadString
时 hInstance
参数的值。
HMODULE WINAPI LoadLibraryEx(
_In_ LPCTSTR lpFileName,
_Reserved_ HANDLE hFile,
_In_ DWORD dwFlags
);
使用完库后,调用 FreeLibrary 卸载它。它的签名尽可能简单。
BOOL WINAPI FreeLibrary(
_In_ HMODULE hModule
);
我通常将对 FreeLibrary
的调用包装在一个 IF
块中,其中 True
块执行接下来的任何操作,而 False
块调用 GetLastError
,进行报告,然后关闭。
关注点
Excel 文档中的 VBA 代码和演示程序中有几个有趣的功能。
演示程序
第一个值得关注的地方是 stdafx.h,我在其中放置了宏,包括四个函数样式的宏,它们简化了对 LoadLibraryEx
的调用,以加载卫星 DLL,以及大多数对 FB_LoadString
的调用,FB_LoadString
是我用来将 字符串
加载到导出它的 DLL 的五个静态缓冲区之一的自定义例程。 FB_LoadString 由 FixedStringBuffers.dll 导出,我在另一篇 CodeProject 文章 使用静态缓冲区提高错误报告成功率 中对此进行了讨论。宏的目标是隐藏所有调用中具有相同值的参数,以便工作代码可以专注于随每次调用而变化的值。虽然这种技术隐藏了在调试器反汇编视图中工作时可能需要的详细信息,但在正常使用中,我认为它们就像现成的宏(如 MAKELANGID
)一样是黑箱。
三个功能协同工作,简化了从程序加载目录加载卫星 DLL 的过程。
PROC_AllocFNBuff
是在 ProcessInfo.H 中定义的宏,它分配一个大小为MAX_PATH
个字符(TCHAR
)的缓冲区和一个指向它的指针,并将指针初始化为缓冲区的地址。PROC_GetModuleDirectoryName
(也定义在 ProcessInfo.H 中)用启动当前进程的程序的目录名称填充缓冲区。使用加载到进程的第一个 EXE 的目录,意味着 DLL 中的一个辅助例程(它可能从其他地方加载)可以找到该原始 EXE 加载的目录。PROC_GetModuleDirectoryName
返回终止路径string
的null
字符的地址,该地址被放入lpModulePath
,并馈给LoadString
。LoadString
开始写入 lpModulePath,用卫星 DLL 名称的第一个字符覆盖 null 字符,并在最后一个字符后追加一个新的null
字符,因此生成的string
是一个完整的路径名,可以馈给LoadLibraryEx
。
整个主模块非常简短。
// SampleDLLDemo.cpp : Defines the entry point for the console application.
//
#include "stdafx.h"
#include "resource.H"
#include "..\SampleDLL.H"
int ReportEr5rorAndQuit ( FB_RES_STRING_ID puintHintID ) ;
PROC_AllocFNBuff ( szPgmDir , lpPgmDir ) ;
// ----------------------------------------------------------------------------
// FBGetStringLocalDflt is a macro, defined in stdafx.h, that hides the invariant
// arguments required to ask library routine FB_LoadString to load a specified
// string resource into its first static buffer, whose address is its return
// value.
// ----------------------------------------------------------------------------
int _tmain ( int argc , _TCHAR* argv [ ] )
{
_tprintf ( FBGetStringLocalDflt ( IDS_MSG_BOJ ) ,
ProgramIDFromArgV ( argv [ ARGV_PROGRAM_NAME_P6C ] ) ) ;
if ( LPTSTR lpModulePath = PROC_GetModuleDirectoryName ( NULL , lpPgmDir ) )
{
_tprintf ( FBGetStringLocalDflt ( IDS_MSG_SATELLITE_PATH ) ,
lpPgmDir ) ;
if ( LoadString ( FB_LOOK_IN_THIS_EXE ,
IDS_SATELLITE_DLLNAME ,
lpModulePath ,
MAX_PATH - ByteOffsetToTCHARsP6C ( lpPgmDir , lpModulePath ) ) )
{
_tprintf ( FBGetStringLocalDflt ( IDS_MSG_SATELLITE_FQFN ) ,
lpPgmDir ) ;
if ( HMODULE hSatelliteDll = DllLoadAsData_WW ( lpPgmDir ) )
{
for ( unsigned int uintNId = IDS_EXAMPLE_1 ;
uintNId <= IDS_EXAMPLE_3;
uintNId++ )
{
_tprintf ( FBGetStringLocalDflt ( IDS_MSG_STRING_FROM_SATELLITE ) ,
uintNId ,
FBGetStringLocalAlt1 ( LabelIDFromValueID ( uintNId ) ) ,
FB_LoadString ( hSatelliteDll ,
uintNId ,
FB_ALTERNATE_BUFFER_2 ,
FB_HIDE_LENGTH ) ) ;
} // for ( unsigned int uintNId = IDS_EXAMPLE_1 ; uintNId <= IDS_EXAMPLE_3; uintNId++ )
FreeLibrary ( hSatelliteDll );
} // TRUE (expected outcome) block,
// if ( HMODULE hSatelliteDll = DllLoadAsData_WW ( lpPgmDir ) )
else
{
return ReportEr5rorAndQuit ( IDS_ERR_HINT_LOADLIBRARYEX );
} // FALSE (UNexpected outcome) block,
// if ( HMODULE hSatelliteDll = DllLoadAsData_WW ( lpPgmDir ) )
} // TRUE (expected outcome) block, if ( LoadString
// ( FB_LOOK_IN_THIS_EXE , IDS_SATELLITE_DLLNAME , lpModulePath ,
// MAX_PATH - ByteOffsetToTCHARsP6C ( lpPgmDir , lpModulePath ) ) )
else
{
return ReportEr5rorAndQuit ( IDS_ERR_HINT_LOADSTRING ) ;
} // FALSE (UNexpected outcome) block, if ( LoadString
// ( FB_LOOK_IN_THIS_EXE , IDS_SATELLITE_DLLNAME , lpModulePath ,
// MAX_PATH - ByteOffsetToTCHARsP6C ( lpPgmDir , lpModulePath ) ) )
} // TRUE (expected outcome) block,
// if ( LPTSTR lpModulePath = PROC_GetModuleDirectoryName ( NULL , lpPgmDir ) )
else
{
return ReportEr5rorAndQuit ( IDS_ERR_HINT_GETMODDIRNAME ) ;
} // FALSE (UNexpected outcome) block,
// if ( LPTSTR lpModulePath = PROC_GetModuleDirectoryName ( NULL , lpPgmDir ) )
_tprintf ( FBGetStringLocalDflt ( IDS_MSG_EOJ ) ) ;
return 0 ;
} // int _tmain
int ReportEr5rorAndQuit ( FB_RES_STRING_ID puintHintID )
{
DWORD dwLastError = GetLastError ( );
_tprintf ( FB_FormatMessage ( FBGetStringLocalDflt ( puintHintID ) ,
dwLastError ,
SCF2_HEXADECIMAL ) ) ;
return dwLastError;
} // int ReportEr5rorAndQuit
演示程序中的另一个例程 ProgramIDFromArgV(定义在 ProgramIDFromArgV.CPP 中)也因其在不依赖轻量级 Shell API 的情况下从 argV [0]
中提取程序名称的方式而值得注意。
/*
============================================================================
File Name: ProgramIDFromArgV.C
File Synopsis: This file defines function ProgramIDFromArgV.
Function Synopsis: ProgramIDFromArgV provides a portable mechanism to
identify the name of a program, for display on its
console. Hence, it depends exclusively on CRT routines.
Author: David A. Gray
----------------------------------------------------------------------------
Revision History
----------------------------------------------------------------------------
Date By Synopsis
---------- --- -------------------------------------------------------------
2015/01/04 DAG Function created and tested to meet an immediate need.
2015/05/06 DAG Replace all char entities with TCHAR entities, so that this
routine works in either ANSI or Unicode.
============================================================================
*/
#include "stdafx.h"
TCHAR chrArg0IsNull [ ] = TEXT
( "ERROR: The first string in the argument list passed into routine ProgramIDFromArgV
is a null reference.\n" ) ;
TCHAR chrArg0IsBlank [ ] = TEXT
( "ERROR: The first string in the argument list passed into routine ProgramIDFromArgV
is the empty string.\n" ) ;
TCHAR chrPathDlm [ ] = TEXT ( "\\" ) ;
TCHAR * lpchrArg0IsNull = ( TCHAR * ) &chrArg0IsNull ;
TCHAR * lpchrArg0IsBlank = ( TCHAR * ) &chrArg0IsBlank ;
TCHAR * lpchrPathDlm = ( TCHAR * ) &chrPathDlm ;
TCHAR * ProgramIDFromArgV ( const TCHAR * ppgmptr )
{
TCHAR * lpLastPathDelimiter = NULL ;
TCHAR * lpLastDelimiterScan = NULL ;
if ( ppgmptr )
{
if ( _tcslen ( ppgmptr ) )
{
lpLastDelimiterScan = ( TCHAR * ) ppgmptr ;
lpLastPathDelimiter = ( TCHAR * ) ppgmptr ;
do
{
lpLastDelimiterScan = _tcsstr ( lpLastDelimiterScan ,
lpchrPathDlm ) ;
if ( lpLastDelimiterScan )
{
lpLastPathDelimiter = lpLastDelimiterScan ;
lpLastDelimiterScan++ ;
} // TRUE block, if ( lpLastDelimiterScan )
else
{
lpLastPathDelimiter++ ;
} // FALSE block, if ( lpLastDelimiterScan )
} while ( lpLastDelimiterScan ) ;
return lpLastPathDelimiter ;
} // TRUE (expected outcome) block, if ( strlen ( ppgmptr ) )
else
{
return lpchrArg0IsBlank ;
} // FALSE (UNexpected outcome) block, if ( strlen ( ppgmptr ) )
} // TRUE (expected outcome) block, if ( ppgmptr )
else
{
return lpchrArg0IsNull ;
} // FALSE (UNexpected outcome) if ( ppgmptr )
} // LPTSTR ProgramIDFromArgV
我刚刚注意到花形框仍然说它是 .C 文件,但我会保留它,因为这种疏忽对现有实现没有危害,并提醒读者它可以像纯 C 一样编译。另一个项目中的 ANSI 实现是纯 ANSI C。我已将其转换为 C++ 以用于此应用程序,以便它可以使用从 stdafx.h 生成的预编译头文件。
VBA 代码
VBA 项目中有三个模块。
m_RCImport.BAS
将只读资源脚本及其符号导入应用程序的主资源脚本。由于它从另一个模块调用,因此函数ImportReadOnlyResources
被标记为public
。m_ResGen.BAS
暴露了一个单独的子例程ResGen
,该例程是当 Excel 文档具有焦点时激活热键组合CTRL-SHIFT-G
时运行的例程。m_WorksheetFunctions.BAS
暴露了两个函数ActiveDocumentDirectoryName
和ParentDirectoryName
,它们使包含活动文档的目录名称以及任何目录的父目录名称可用于工作表。
m_RCImport.BAS
中的函数 ImportReadOnlyResources
实现了一个简单的有限状态机,由自定义枚举 enmState
(State
枚举的一个实例)驱动,定义如下。
Enum State
FindingTextinclude2Marker
FindingTextinclude2BlockEnd
FindingTextInclude2Begin
FindingTextinclude2End
FindingTextInclude3Begin
FindingTextInclude3End
FindingTextinclude3Marker
FindingTextinclude3BlockEnd
FindingEndOfFile
End Enum ' Type State
Private
函数 LoadFileIntoString
使用 Scripting.FileSystemObject
从名为 pstrMainRCFQFN
的文件中读取主资源脚本(函数 ImportReadOnlyResources
的三个参数中的第一个),将其读入一个长 string
,然后将其拆分为一个行数组。我将其放入一个函数中,以便其错误处理程序可以解决与文件 I/O 操作相关的特定问题,并提供更精确的错误消息。报告错误后,它返回空 string
以向调用例程发出失败信号。
由于 Basic String 的长度存储在字符串本身前面的一个四字节整数中,因此检查空 string
的最简单方法是评估其长度。如果长度大于零,则两次调用另一个 private
函数 CreateTextInclude
会将传递给 LoadFileIntoString
的第二个和第三个参数的头文件和脚本文件名转换为有效的 #include
指令。这个单语句函数隐藏了构建它所需的有些晦涩的格式。(这样,如果我出错了,我只需要修复一个语句。)
Private Function CreateTextInclude(ByRef pstrIncludeFN As String) As String
CreateTextInclude = Space$(4) & Chr$(34) & "#include " _
& Chr$(34) & Chr$(34) _
& pstrIncludeFN _
& Chr$(34) & Chr$(34) _
& "\r\n" & Chr$(34) _
End Function
在继续之前,会扫描资源脚本 string
以查找两个 #include
指令。如果这两个指令已存在,则例程将退出,不再采取进一步行动,资源脚本将保持原样。测试由以下复合 IF
语句组成。
If InStr(strMainData, strHeaderTextInclude) = INSTR_NOT_FOUND And InStr
(strMainData, strScriptTextInclude) = INSTR_NOT_FOUND Then
INSTR_NOT_FOUND
在模块 m_ResGen.BAS
中定义为 public
常量 long
整数,值为零。
以下语句通过将每个 CR/LF
对替换为 LF
字符(成为拆分的分隔符)来将现有资源脚本拆分为行数组。以嵌入 Replace
及其相关内存消耗为代价,此方法将标准 Windows 文本文件内容拆分为行数组减少为单个语句。
Dim avarLines As Variant: avarLines = Split(Replace(strMainData, _
vbCrLf, _
vbLf), _
vbLf)
上面语句返回的数组包含每行一个元素。每一行都通过常规的 For
循环进行处理。每次迭代都会执行以下 Select Case
块,该块实现了状态机。
Select Case enmState
Case FindingTextinclude2Marker
strNewScript = strNewScript & strCurrLine & vbCrLf
If strCurrLine = TEXTINCLUDE_2_MARKER Then
enmState = FindingTextinclude2BlockEnd
End If ' If strCurrLine = TEXTINCLUDE_2_MARKER Then
Case FindingTextinclude2BlockEnd
If Len(strCurrLine) > LENGTH_OF_EMPTY_STRING Then
If strCurrLine = AFX_RC_BOUNDARY Then
strNewScript = strNewScript _
& "#include " & Chr$(34) & pstrHeaderFN & Chr$(34) _
& vbCrLf _
& vbCrLf _
& strCurrLine _
& vbCrLf
enmState = FindingTextInclude2Begin
Else
strNewScript = strNewScript & strCurrLine & vbCrLf
End If ' If strCurrLine = AFX_RC_BOUNDARY Then
End If ' If Len(strCurrLine) > LENGTH_OF_EMPTY_STRING Then
Case FindingTextInclude2Begin
strNewScript = strNewScript & strCurrLine & vbCrLf
If strCurrLine = DLM_TEXTINCLUDE_2 Then
enmState = FindingTextinclude2End
End If ' If strCurrLine = DLM_TEXTINCLUDE_2 Then
Case FindingTextinclude2End
If strCurrLine = AFX_EMPTY_LINE Then
strNewScript = strNewScript _
& strHeaderTextInclude & vbCrLf _
& strCurrLine & vbCrLf
enmState = FindingTextInclude3Begin
Else
strNewScript = strNewScript & strCurrLine & vbCrLf
End If ' If strCurrLine = AFX_EMPTY_LINE Then
Case FindingTextInclude3Begin
strNewScript = strNewScript & strCurrLine & vbCrLf
If strCurrLine = DLM_TEXTINCLUDE_3 Then
enmState = FindingTextInclude3End
End If ' If strCurrLine = DLM_TEXTINCLUDE_2 Then
Case FindingTextInclude3End
If strCurrLine <> AFX_NEWLINE Then
If strCurrLine = AFX_EMPTY_LINE Then
strNewScript = strNewScript _
& strScriptTextInclude & vbCrLf _
& strCurrLine & vbCrLf
enmState = FindingTextinclude3Marker
Else
strNewScript = strNewScript & strCurrLine & vbCrLf
End If ' If strCurrLine = AFX_EMPTY_LINE Then
End If ' If strCurrLine <> AFX_NEWLINE Then
Case FindingTextinclude3Marker
strNewScript = strNewScript & strCurrLine & vbCrLf
If strCurrLine = TEXTINCLUDE_3_MARKER Then
enmState = FindingTextinclude3BlockEnd
End If ' If strCurrLine = TEXTINCLUDE_2_MARKER Then
Case FindingTextinclude3BlockEnd
If Len(strCurrLine) > LENGTH_OF_EMPTY_STRING Then
If strCurrLine = AFX_RC_BOUNDARY Then
strNewScript = strNewScript _
& "#include " & Chr$(34) & pstrScriptFN & Chr$(34) _
& vbCrLf _
& vbCrLf _
& strCurrLine _
& vbCrLf
enmState = FindingEndOfFile
Else
strNewScript = strNewScript & strCurrLine & vbCrLf
End If ' If strCurrLine = AFX_RC_BOUNDARY Then
End If ' If Len(strCurrLine) > LENGTH_OF_EMPTY_STRING Then
Case FindingEndOfFile
strNewScript = strNewScript & strCurrLine & vbCrLf
End Select ' Select Case enmState
在更新现有资源脚本文件之前,通过调用另一个 private
函数 MakeBackupFile
创建备份,该函数从输入文件名构建备份文件名,然后调用内置的 VBA 子例程 FileCopy
来创建备份文件。
模块 m_ResGen.BAS
中许多更有趣的事情发生在其主要的 public
例程 ResGen
之外,ResGen
是一个简单的嵌套 IF
块,由一个通用的错误处理块保护,该块会显示一个消息框,然后关闭宏。
接下来是对函数 MakeFQFN
的三次调用之一,它使用轻量级 Shell API 例程 PathCombine
从 pstrPath
中命名的目录和 pstrFileName
中命名的文件构建一个完全限定的文件名。
Public Function MakeFQFN(pstrPath As String, _
pstrFileName As String) _
As String
Dim strBuff As String * MAX_PATH
MakeFQFN = Ptr2StrU(PathCombine(StrPtr(strBuff), _
StrPtr(pstrPath), _
StrPtr(pstrFileName)))
End Function
正如 MakeFQFN 是 PathCombine
的包装器一样,Ptr2StrU
是 CopyMemory
(实际上是 RtlMoveMemory
)的包装器。
函数 LoadTemplateFromRange
从两个命名范围之一返回资源脚本或符号头文件的模板。过程很简单:读取范围中的每个单元格,并将其附加到 string
后面,后跟一个 CR/LF 对,除了最后一行没有终止符。在尝试将其用作范围名称之前,会调用函数 RangeNameExists
来根据 ThisWorkbook.Names
属性检查名称。
在大多数情况下,LoadTemplateFromRange
是一个简单的 For
循环,我选择它而不是 For Each
循环是因为我需要知道何时到达最后一行,以便可以省略最后的换行符。 在进入主循环之前,rngTemplate
.Rows.Count
被复制到局部变量 lngLastRow
中,这样就无需在每次迭代中查询它了。
Private Function LoadTemplateFromRange(pstrRangeName As String, _
Optional ByVal pfLastNewlineDisp = LAST_LINE_DLM_KEEP) _
As String
Const THE_ONE_AND_ONLY_COLUMN As Integer = 1
LoadTemplateFromRange = vbNullString
On Error GoTo LoadTemplateFromRange_Err
If RangeNameExists(pstrRangeName) Then
Dim rngTemplate As Range: Set rngTemplate = ThisWorkbook.Names(pstrRangeName).RefersToRange
Dim strWork As String: strWork = vbNullString
If rngTemplate.Columns.Count = THE_ONE_AND_ONLY_COLUMN Then
Dim lngLastRow As Long: lngLastRow = rngTemplate.Rows.Count
Dim lngCurrRow As Long
For lngCurrRow = RANGE_FIRST_ROW To lngLastRow
Dim strLine As String: strLine = CStr(rngTemplate.Cells(lngCurrRow, _
THE_ONE_AND_ONLY_COLUMN).Value)
If lngCurrRow < lngLastRow Then
strWork = strWork & strLine & vbCrLf
Else
If pfLastNewlineDisp = LAST_LINE_DLM_KEEP Then
strWork = strWork & strLine & vbCrLf
Else
strWork = strWork & strLine
End If ' If pfLastNewlineDisp = LAST_LINE_DLM_KEEP Then
End If ' If lngCurrRow < lngLastRow Then
Next ' For lngCurrRow = RANGE_FIRST_ROW To lngLastRow
LoadTemplateFromRange = strWork
Else ' If rngTemplate.Columns.Count = THE_ONE_AND_ONLY_COLUMN Then
MsgBox "Worksheet Format Error: Template range " _
& pstrRangeName _
& " contains too many columns." _
& vbLf & _
"Only " _
& THE_ONE_AND_ONLY_COLUMN _
& " column of cells is permitted.", _
vbApplicationModal Or vbExclamation, _
ThisWorkbook.Name
Set rngTemplate = Nothing
End If ' If rngTemplate.Columns.Count = THE_ONE_AND_ONLY_COLUMN Then
Else
If Len(pstrRangeName) > LENGTH_OF_EMPTY_STRING Then
MsgBox pstrRangeName & " is invalid as a range name in " & ActiveWorkbook.FullName, _
vbApplicationModal Or vbExclamation, _
ActiveWorkbook.Name
End If ' If Len(pstrRangeName) > LENGTH_OF_EMPTY_STRING Then
LoadTemplateFromRange = vbNullString ' Regardless, return the empty string.
End If ' If RangeNameExists(pstrRangeName) Then
LoadTemplateFromRange_End:
Exit Function
LoadTemplateFromRange_Err:
MsgBox VBA_RT_ERRMSG_PREFIX & Err.Number & " - " & Err.Description, _
vbApplicationModal Or vbExclamation, _
ActiveWorkbook.Name
Err.Clear
LoadTemplateFromRange = vbNullString
Resume LoadTemplateFromRange_End
End Function
由于它是一个最终会出现在库中的实用函数,因此函数 RangeNameExists
被标记为 public。由于它不需要计数,因此此例程使用 For Each
循环来枚举 ThisWorkbook.Names
集合的成员。
Public Function RangeNameExists(ByRef pstrName As String) As Boolean
If Len(pstrName) > LENGTH_OF_EMPTY_STRING Then
Dim fFound As Boolean: fFound = False
Dim wbAllNames As Names: Set wbAllNames = ThisWorkbook.Names
Dim wbCurrName As Name
For Each wbCurrName In wbAllNames
If wbCurrName.Name = pstrName Then
fFound = True
Exit For ' Done!
End If ' If wbCurrName.Name = pstrName Then
Next ' For Each wbCurrName In wbAllNames
RangeNameExists = fFound
Else
RangeNameExists = False
End If ' If Len(pstrName) > LENGTH_OF_EMPTY_STRING Then
End Function
接下来遇到的有趣的 private
函数是 LookupParameterValue
,它在 ResGen Parameters
工作表的 Substitution_Token_Data
范围中查找参数。
Public Function LookupParameterValue(ByRef pstrToken As String, _
ByRef putpColumns As utpParameterColumns) _
As String
On Error GoTo LookupParameterValue_Err
LookupParameterValue = vbNullString
Dim rngParams As Range: Set rngParams = ActiveWorkbook.Names(RN_RESGEN_PARAMETER_TABLE).RefersToRange
If rngParams.Columns.Count >= putpColumns.ColLiteral Then
Dim lngCurrRow As Long: lngCurrRow = RANGE_FIRST_ROW
Dim lngLastRow As Long: lngLastRow = rngParams.Rows.Count
Dim fDone As Boolean: fDone = False
Do ' Until fDone
If pstrToken = CStr(rngParams.Cells(lngCurrRow, putpColumns.ColValue).Value) Then
LookupParameterValue = CStr(rngParams.Cells(lngCurrRow, putpColumns.ColLiteral).Value)
fDone = True
Else
lngCurrRow = lngCurrRow + ARRAY_NEXT_ELEMENT
fDone = lngCurrRow > lngLastRow
End If ' If pstrToken = CStr(rngParams.Cells(lngCurrRow, putpColumns.ColValue).Value) Then
Loop Until fDone
Else
MsgBox "Error report from VBA function LookupParameterValue, " _
& "on behalf of workbook Macro ResGen:" & vbLf & vbLf _
& "Named worksheet range " & rngParams.Name _
& ", located at " & rngParams.AddressLocal _
& " in worksheet " & rngParams.Worksheet.Name & "." & vbLf _
& "The range contains too few columns." & vbLf _
& "It contains " & rngParams.Columns.Count _
& " columns; it must contain at least " _
& putpColumns.ColValue & " columns.", _
vbExclamation, _
ThisWorkbook.Name
End If ' If rngParams.Columns.Count >= putpColumns.ColLiteral Then
LookupParameterValue_End:
Exit Function
LookupParameterValue_Err:
MsgBox "Error report from VBA function LookupParameterValue, " _
& "on behalf of workbook Macro ResGen:" & vbLf & vbLf _
& "Error " & Err.Number & " - " & Err.Description, _
vbExclamation, _
ThisWorkbook.Name
Err.Clear
Resume LookupParameterValue_End
End Function
此例程最重要的特点是 rngParams.Rows.Count
被复制到局部变量 lngLastRow
中,因此无需在每次迭代中查询它,并且工作是由一个 Do
循环完成的,该循环执行直到布尔变量 fDone
变为 True
。由于循环至少必须执行一次,因此 fDone
的测试被推迟到循环底部。
还有许多其他有趣的函数,留给好奇的读者练习。
值得重用的工作表函数
两个工作表函数中的第一个 ActiveDocumentDirectoryName
非常简单,因为它只是公开了 Excel 对象模型中 ActiveWorkbook
对象的一个属性。
Public Function ActiveDocumentDirectoryName( _
Optional pFAppendBackslash As Boolean = True) _
As String
' ------------------------------------------------------------------------
' Abstract: Return the name of the directory from which the active
' document loaded.
'
' In: pFAppendBackslash = TRUE (default) to have a backslash
' appended to the returned name, or
' FALSE to omit it.
'
' Out: Fully qualified name of parent of pstrChildDirectoryName.
' ------------------------------------------------------------------------
If pFAppendBackslash Then
ActiveDocumentDirectoryName = ActiveWorkbook.Path & PATH_DELIMITER_WINDOWS
Else
ActiveDocumentDirectoryName = ActiveWorkbook.Path
End If
End Function
第二个函数 ParentDirectoryName
几乎同样简单;它从 Scripting.FileSystemObject
读取属性。
Public Function ParentDirectoryName( _
ByRef pstrChildDirectoryName As String, _
Optional pFAppendBackslash As Boolean = True) _
As String
' ------------------------------------------------------------------------
' Abstract: Return the name of the parent of a specified directory.
'
' In: pstrChildDirectoryName = Fully qualified directory nme from
' which to extract name of parent.
' Path pstrChildDirectoryName must
' exist.
'
' pFAppendBackslash = TRUE (default) to have a backslash
' appended to the returned name, or
' FALSE to omit it.
'
' Out: Fully qualified name of parent of pstrChildDirectoryName.
' ------------------------------------------------------------------------
If Len(pstrChildDirectoryName) > LENGTH_OF_EMPTY_STRING Then
Dim fso As FileSystemObject: Set fso = New FileSystemObject
If fso.FolderExists(pstrChildDirectoryName) Then
If pFAppendBackslash Then
ParentDirectoryName = fso.GetParentFolderName(pstrChildDirectoryName) _
& PATH_DELIMITER_WINDOWS
Else
ParentDirectoryName = fso.GetParentFolderName(pstrChildDirectoryName)
End If ' If pFAppendBackslash Then
Else
ParentDirectoryName = "PathNotFound"
End If ' If fso.FolderExists(pstrChildDirectoryName) Then
Else
ParentDirectoryName = "Unspecified"
End If ' If Len(pstrChildDirectoryName) > LENGTH_OF_EMPTY_STRING Then
End Function请随意将这些例程导入您自己的工作簿。
历史
- 2016 年 7 月 12 日,更新了工作簿,增加了一个更健壮的资源生成器宏,该宏可以优雅地处理资源脚本中 TEXTINCLUDE 标记行已被截断空格的情况。
- 2016 年 7 月 10 日 - 整合了随 改进的资源字符串生成器实战:你就吃自己的狗粮,而且喜欢它! 一起提供的、大大改进的示例存档。
- 2015 年 5 月 9 日星期六 - 使演示项目存档可见。
- 2015 年 5 月 8 日星期五 - 修复了损坏的图像标签
- 2015 年 5 月 7 日星期四 - 文章完成