65.9K
CodeProject 正在变化。 阅读更多。
Home

无冲突共享字符串资源

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2015年5月8日

BSD

14分钟阅读

viewsIcon

21586

downloadIcon

211

在不发生冲突资源 ID 的风险下共享 Win32 字符串资源

引言

本文提出了一种解决两个问题的直接方法。

  1. 无忧地共享 Win32 字符串资源,避免资源 ID 号冲突。
  2. 使用 Microsoft Excel 高效管理只读 Win32 字符串资源,无需手动编辑资源脚本。

即使这些资料似乎不适用于您的需求,其中也包含许多有用的技巧,让这篇文章至少值得快速浏览一下。

2016 年 7 月 10 日,我用一篇后续文章中提供的、大大改进的代码和演示替换了原始示例, 改进的资源字符串生成器实战:你就吃自己的狗粮,而且喜欢它! 读完本文后,请阅读那篇文章,并遵循其 大大简化的操作说明。

背景

多年来,我一直对建议的共享 Win32 字符串资源给应用程序的方法所需的记录保存感到沮丧。为什么这种记录保存没有自动化呢?有一天,我有一个特定的应用程序,如果它所有的 字符串都存在于一个卫星 DLL 中,那么编码就会变得无限简单。在我完成它的时候,我意识到同样的方法也可以用来打包任何一组 字符串,这样你就可以拥有像代码库一样易于使用的 字符串库。

由于导致此需求的项目是一个库,每个使用它的应用程序都需要自定义的 字符串库,因此我还需要一种方法来高效地生成它们。这催生了 Excel 应用程序,它使用工作表来存储 字符串及其关联的 ID,并驱动一个 VBA 宏,该宏生成只读资源脚本及其关联的头文件,并将两者集成到 Visual Studio 项目的默认资源脚本中。当你修改一组 字符串时,生成器会识别出辅助脚本已包含在主脚本中,并保持主资源脚本不变。

Using the Code

演示包包含以下四个目录。

  1. Excel 包含 Microsoft Excel 工作簿,以及其 3 个 VBA 模块的导出副本和一个包含其使用的自定义日期格式器的自定义 DLL。 如果移动 Excel 工作簿,请一并带走 DLL。重要提示:2016 年 3 月 12 日星期六,我用一个新版本替换了 Excel 工作簿,该版本无需解锁 ResGen Parameters工作表即可编辑或添加配置文件标签。如果您下载了原始版本,请获取新版本;您会对新 Excel 工作簿的行为方式感到满意。
  2. SampleDLL 包含一个由 Excel 工作簿生成的示例 字符串资源 DLL,以及一个展示其运行的程序。
  3. WWStandardErrorMessages是我为自己的项目创建的活动卫星 DLL 的当前版本。
  4. _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。您可以通过两种方式在此工作表的此部分添加自己的数据集。

  1. 在 G 列或 H 列的左侧插入新列。
  2. 覆盖这三列中任何一列的值。

尽管它们不是超链接,但第 5 行和第 6 行中的值是范围名称,都具有工作簿范围,并且是通过选择工作表 Example 的单元格 G2 到 G5 和 F2 到 F5(分别)来创建的,并使用顶部单元格作为名称,其余作为内容来定义命名范围。

要生成资源脚本和头文件,请填充配置文件,使用单元格 D4 选择它,然后按 Ctrl-Shift-G 执行宏。对于中小型库,它可以在一秒钟内完成工作,然后会显示一个消息框,如图 1 所示。

Figure 1: Message box displayed when resource script has been generated and integrated

图 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 作为调用 LoadStringhInstance 参数的值。

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 的过程。

  1. PROC_AllocFNBuff 是在 ProcessInfo.H 中定义的宏,它分配一个大小为 MAX_PATH 个字符(TCHAR)的缓冲区和一个指向它的指针,并将指针初始化为缓冲区的地址。
  2. PROC_GetModuleDirectoryName(也定义在 ProcessInfo.H 中)用启动当前进程的程序的目录名称填充缓冲区。使用加载到进程的第一个 EXE 的目录,意味着 DLL 中的一个辅助例程(它可能从其他地方加载)可以找到该原始 EXE 加载的目录。PROC_GetModuleDirectoryName 返回终止路径 stringnull 字符的地址,该地址被放入 lpModulePath,并馈给 LoadString
  3. 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 项目中有三个模块。

  1. m_RCImport.BAS 将只读资源脚本及其符号导入应用程序的主资源脚本。由于它从另一个模块调用,因此函数 ImportReadOnlyResources 被标记为 public
  2. m_ResGen.BAS 暴露了一个单独的子例程 ResGen,该例程是当 Excel 文档具有焦点时激活热键组合 CTRL-SHIFT-G 时运行的例程。
  3. m_WorksheetFunctions.BAS 暴露了两个函数 ActiveDocumentDirectoryNameParentDirectoryName,它们使包含活动文档的目录名称以及任何目录的父目录名称可用于工作表。

m_RCImport.BAS 中的函数 ImportReadOnlyResources 实现了一个简单的有限状态机,由自定义枚举 enmStateState 枚举的一个实例)驱动,定义如下。

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 例程 PathCombinepstrPath 中命名的目录和 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

正如 MakeFQFNPathCombine 的包装器一样,Ptr2StrUCopyMemory(实际上是 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 日星期四 - 文章完成
© . All rights reserved.