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

C++项目BuildIncrement2012插件

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.86/5 (5投票s)

2013年7月3日

CPOL

13分钟阅读

viewsIcon

20912

downloadIcon

281

BuildIncrement2012 C++ 项目插件。

 

引言

本文的真正且最终目的是为了获得关于此插件的反馈,以便我能修复其中可能存在的任何错误。您知道,我从未进行过 C# 开发。事实上,我也从未在 VS2012 上工作过。至今,由于工作要求,我仍然在使用 VC6。但我刚刚接触了 VS2012,而这个插件将作为我的“Hello World”项目……或者说解决方案(?)。

我一边写演示项目,一边写这篇文章。这是我试图提供一份我希望找到的那种极其详细的教程……但有一个重要的提醒:我什么都解释不了。我对自动化/扩展性和 C# 的理解完全为零。

免责声明和限制 - 阅读本文!

所有可能发生的灾难都适用,包括但不限于,您在使用此插件的任何项目上都可能确定无疑地导致项目损坏。

该插件仅在包含单个项目的 C++ 解决方案上进行了测试。它试图实现 如何在 Visual C++ 中在每次生成后递增版本信息,并以笨拙但顽固的无能为之。例如,该插件会在每次生成之前递增,而不是之后,因为我敢于梦想。

您已经被警告过,一次又一次。我不敢确定它是否会在您的计算机拿起剃刀自我了断以进行先发制人的投降之前执行一次。

另外请注意,为了正常运行,该插件需要我自己的计算机上那种精细的实验条件(普通大众无法获得)。一旦投入使用,该插件将破坏其路径上的一切,同时弹出数十条令人不快的错误消息。误导是这里的关键。它将迷惑,它将摧毁。

背景

天哪。我真诚地希望您比我懂得多。所以,是的,有自动化/扩展性和 C# 背景会很有用。

分步教程

第一幕。项目启动

启动 VS2012,然后点击“新建项目”->“模板”->“其他项目类型”->“Extensibility”->“Visual Studio Add-in”。输入名称,选择位置,然后点击“确定”。

Add-in 向导应该会出现(敲木头)。选择“使用 Visual C# 创建 Add-in”。点击“下一步”两次。填写“What is the name ...”和“What is the description ...”,然后点击“下一步”。

选中“Yes, create a 'Tools' menu ...”和“I would like my ...”。点击“下一步”两次,然后点击“完成”。

Connect.cs 文件会自动打开,您会感觉自己的灵魂落到了地上。代码(据说)填满了屏幕,但它是一个完全无法解读、完全无法理解的混乱。向下滚动到 OnConnection 方法(C# 里它们被称为方法吗?)并将断点放在第一行。这是健全性检查。点击“开始调试”。

如果执行没有在断点处停止,那么您就只能靠自己了。

如果停止了,那我们就有戏了。现在停止调试。您可以放手了。

是时候设计 GUI 了。我花了好长时间(您不必感到羞愧)才弄明白如何添加 Windows 窗体。我不会描述上面截图中的 GUI 设计和命名带有曲折标签的控件所花费的时间。您自己去玩吧。

Connect.cs 的顶部添加 using System.Windows.Forms,并修改 Exec 方法如下。编译并调试。我找不到其他方法来测试 GUI,而且,经常运行项目也是一个好习惯。

...
using System.Windows.Forms;
...
public void Exec( string commandName, vsCommandExecOption executeOption, 
       ref object varIn, ref object varOut, ref bool handled )
{
    ...
    handled = true;

    BIForm biForm = new BIForm();

    biForm.ShowDialog();
    ...
}
...

一旦第二个 VS 实例启动,您会发现“工具”菜单下的第一个条目是一个笑脸。没开玩笑。这是插件的默认图标。点击它。您应该会看到我们的对话框弹出。花点时间验证一下按下 TAB 键是否能按照预期顺序在控件之间移动(转到“视图”->“Tab 顺序”来修改它)。

如果您想更改默认图标(我非常想),请参阅 如何:更改插件的默认图标(疯狂的东西)。我选择了 Farm-Fresh 系列中的一个图标。

截图不言自明。它将显示项目的名称(为什么不呢),以及用户可以随意修改的三个版本号。第四个是构建号,将在每次构建之前自动增加。版本文本框下方是第一个定义窗口,证明了我有多么懒惰。它将方便在我们的代码中插入包含版本定义(BI2012_V0, BI2012_V1, BI2012_V2, BI2012_BUILD,...)的头文件(_version.h)。

最后,在第一个定义窗口下方,是第二个定义窗口。这是用于配置用户定义的。那是什么?嗯,我经常有一些切换配置设置的定义,在构建项目之前我需要将它们注释掉/取消注释。正如我所说,我很懒。我把它们放在这里,这样我就不会忘记它们,也不会每次都去搜索。

我现在要去修我妻子的自行车,然后去洗个澡。您可以随意闲逛。

第二幕。我们迷失了方向

更换车把把套比看上去要棘手。我弄断了一个指甲。所以请记住,虽然我英勇地前进,但我是在受伤中前进。

好的。让我们来写一些 C# 代码……什么?!没有头文件? C# 项目是如何组织的?什么东西放在哪里?

我在这里即兴发挥,希望一切顺利……(二十分钟后)……好吧,在某个地方,在深层掩护下,有一个叫做“解决方案资源管理器”的东西。右键点击项目,选择“添加”->“新建项”,然后选择“类”。再次在解决方案资源管理器中,右键点击创建的类(例如 Class1.cs),然后将其重命名为,比如,BIManager.cs,以方便起见。

...
namespace BuildIncrement2012
{
    public class BIManager
    {
    }
}
...

让我们将它与 Connect 类关联起来。

糟糕。当我仔细查看 Connect.cs 时,事情就朝着糟糕的方向发展了。根据我对此主题的了解,我应该添加一个事件处理程序,以便在解决方案打开时收到通知。这对我来说是合理的。然而,事实证明这毫无意义,因为 Exec 方法似乎总是在 Connect 类首次实例化后被调用。

这意味着我无法依赖在 Connect 对象中保留信息,因为这些对象的生命周期对我来说是神秘的。我确信我在这方面是错误的,因为这是一种疯狂、笨拙的做法。

然而,无论对错,这就是我的做法。因此,我只实现 OnBuildBeginEventHandler,并在每次编译时创建一个插件逻辑的新实例……以及每次用户通过“工具”菜单调用插件时……我不喜欢这样,但在这个阶段,不是这样就是什么都没有。

但还有更多。据说事件处理程序会消失在垃圾回收的通道中。太棒了。让我们向类中添加一个成员来记录 BuildEvents。这个怪物看起来是这样的

...
public class Connect : IDTExtensibility2, IDTCommandTarget
{
    ...
    public void OnConnection( object application, ext_ConnectMode connectMode, 
                              object addInInst, ref Array custom )
    {
    ...
        if( connectMode == ext_ConnectMode.ext_cm_Startup )
        {
            m_BuildEvents = _applicationObject.Events.BuildEvents;

            m_BuildEvents.OnBuildBegin += 
              new _dispBuildEvents_OnBuildBeginEventHandler( BuildEvents_OnBuildBegin );
        }
    }
    
    public void OnDisconnection( ext_DisconnectMode disconnectMode, ref Array custom )
    {
        m_BuildEvents.OnBuildBegin -= 
          new _dispBuildEvents_OnBuildBeginEventHandler( BuildEvents_OnBuildBegin );
    }
    ...
    BuildEvents m_BuildEvents;
}
...

我不知道 C# 中的命名约定是什么,所以我将在类成员前加上 m_ 前缀,就像我在礼仪场合那样被教导的那样。

请注意,我们在分配事件处理程序之前会检查 connectMode。这是因为 OnConnection 可能会被调用多次。没有这个检查,我们可能会有重复的事件处理程序和意外的行为。

BuildEvents_OnBuildBegin 的实现如下

...
private void BuildEvents_OnBuildBegin( vsBuildScope Scope, vsBuildAction Action )
{
    System.Array arrProjects = ( System.Array )_applicationObject.ActiveSolutionProjects;
    EnvDTE.Project pjProject = ( EnvDTE.Project )arrProjects.GetValue( 0 );
    
    // Check for the GUID string of C++ projects
    if( pjProject.Kind == "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}" )
    {
        BIManager biManager = new BIManager( pjProject );
    
        biManager.IncrementBuildNumber();
    }
}
...

让我们切换到 BIManager.cs,为类设置一些成员,以保存活动项目的插件信息。

构造函数将是确定项目名称和路径的地方。IncrementBuildNumber 方法仍然无法正常工作,并且只是部分实现,以便插件能够干净地构建(因此,在调试时可以确认事件的行为)。

class BIManager
{
    Project m_Project;
    String m_strFnRC;
    String m_strFnRC2;
    String m_strFnVersion;
    bool m_bBIEnabled;
    int m_iV0;
    int m_iV1;
    int m_iV2;
    int m_iBUILD;
    
    public BIManager( Project pjProject )
    {
        m_Project = pjProject;
        
        String strProjectPath = m_Project.FullName;
        
        strProjectPath = strProjectPath.Remove(1 + strProjectPath.LastIndexOf("\\"));
        
        m_strFnRC = strProjectPath + m_Project.Name + ".rc";
        m_strFnRC2 = strProjectPath + "res\\" + m_Project.Name + ".rc2";
        m_strFnVersion = strProjectPath + "res\\_version.h";
        m_bBIEnabled = false;
    }
    
    public int IncrementBuildNumber()
    {
        m_iBUILD++;
        
        return m_iBUILD;
    }
}

我想结束 Connect.cs 文件。光是看它就让我头疼。

当用户通过“工具”菜单(或者如果手动添加了图标,则通过工具栏)调用插件时,就会调用 Exec 方法。正如我之前提到的,Exec 的行为与其他事件不同。我说不出原因,希望能得到帮助。总之。这意味着我们必须为窗体实现一个 BIManager 类的新实例。

...
handled = true;

if( _applicationObject.ActiveDocument != null )
{
    System.Array arrProjects = ( System.Array )_applicationObject.ActiveSolutionProjects;
    EnvDTE.Project pjProject = ( EnvDTE.Project )arrProjects.GetValue( 0 );

    // Check for the GUID string of C++ projects
    if( pjProject.Kind != "{8BC9CEB8-8B4A-11D0-8D11-00A0C91BC942}" )
    {
        MessageBox.Show( "The BuildIncrement2012 Add-in only works with C++ projects" );
    }
    else
    {
        BIForm biForm = new BIForm( pjProject );

        biForm.ShowDialog();
    }
}

return;
...

切换到 BIForm.cs 来处理这些更改。代码看起来是这样的

...
using EnvDTE;
...
BIManager m_biManager;
bool m_bBlockEvents;
...
public BIForm( Project pjProject )
{
    InitializeComponent();
    
    m_biManager = new BIManager( pjProject );
    
    PopulateControls();    
}

private void PopulateControls()
{
    m_bBlockEvents = true;
    
    cbEnableBI.Checked = m_biManager.IsBIEnabled();
    
    nudV0.Value = m_biManager.GetV0();
    nudV1.Value = m_biManager.GetV1();
    nudV2.Value = m_biManager.GetV2();
    tbBuild.Text = m_biManager.GetBUILD().ToString();
    
    lbBIDefines.Items.Clear();
    lbBIDefines.Items.Add( "#include \"_version.h\"" );
    lbBIDefines.Items.Add( "BI2012_V0" );
    lbBIDefines.Items.Add( "BI2012_V1" );
    lbBIDefines.Items.Add( "BI2012_V2" );
    lbBIDefines.Items.Add( "BI2012_BUILD" );
    lbBIDefines.Items.Add( "BI2012_BUILD_DATE" );
    lbBIDefines.Items.Add( "BI2012_BUILD_TIME" );
    lbBIDefines.Items.Add( "BI2012_STR_V0" );
    lbBIDefines.Items.Add( "BI2012_STR_V1" );
    lbBIDefines.Items.Add( "BI2012_STR_V2" );
    lbBIDefines.Items.Add( "BI2012_STR_BUILD" );
    lbBIDefines.Items.Add( "BI2012_STR_VERSION" );
    
    m_bBlockEvents = false;
}
...

从代码中您也应该能看出我为每个控件命了什么名。欢迎来到我的世界。

m_bBlockEvents 是一个必需的 hack,用于防止 GUI 事件触发,以后也会用到。如果有人知道更优雅的解决方法,请告诉我。

其余的代码是收集 BIManager 类的信息并显示它。插件定义现在可以添加,因为它们不会改变。用于填充用户定义复选框列表的代码将在稍后添加。

现在我们有了几个 getter(?) 方法在 BIManager 类中需要实现。很简单。

...
public bool IsBIEnabled()
{
    return m_bBIEnabled;
}

public int GetV0()
{
    return m_iV0;
}

public int GetV1()
{
    return m_iV1;
}

public int GetV2()
{
    return m_iV2;
}

public int GetBUILD()
{
    return m_iBUILD;
}
...

构建并调试。如果您慷慨地设置了断点,您将获得对所有内容如何协同工作的更好理解。现在也是创建测试 C++ 项目来捣鼓的好时机。当项目加载时,可以通过“工具”菜单访问插件对话框。它什么也不做,但它在那里。

它在那里。

第三幕。我的伦敦伦敦桥想垮下去

如果在窗体上的控件上双击会发生什么?让我们通过点击所有地方来找出答案。先是您。

下面是我点击前几个控件时得到的结果以及我如何实现它们。

...
private void cbEnableBI_CheckedChanged( object sender, EventArgs e )
{
    if( m_bBlockEvents )
    {
        return;
    }
    
    if( cbEnableBI.Checked && m_biManager.LoadVersionInfo() == false )
    {
        m_biManager.ModifyResourceFiles();
        
        m_biManager.CreateVersionFile();
        
        m_biManager.ModifyRC2();
    }

    m_biManager.EnableBI( cbEnableBI.Checked );
    
    PopulateControls();
}

private void nudV0_ValueChanged( object sender, EventArgs e )
{
    if( m_bBlockEvents )
    {
    return;
    }
    
    m_biManager.VersionNumberChange( (int)nudV0.Value );
}

private void nudV1_ValueChanged( object sender, EventArgs e )
{
    if( m_bBlockEvents )
    {
    return;
    }
    
    m_biManager.VersionNumberChange( -1, (int)nudV1.Value );
}

private void nudV2_ValueChanged( object sender, EventArgs e )
{
    if( m_bBlockEvents )
    {
    return;
    }
    
    m_biManager.VersionNumberChange( -1, -1, (int)nudV2.Value );
}
...

上面的代码将 GUI 与 BIManager 关联起来。唯一值得注意的是 cbEnableBI_CheckedChanged,如果 BuildIncrement2012 已启用,我们将修改项目的资源文件并创建项目的版本文件。当然,如果这些修改已经完成,我们只需加载信息然后继续。

好的。现在我们必须实现这些方法中概述的逻辑。切换到 BIManager.cs

我们从 LoadVersionInfo 开始。首先,我们调用 HasTheProjectBeenSetup 来检查项目是否之前已被插件设置。如果没有,则退出。如果设置了,则读取版本文件。对于任何了解 C# 的人来说,这段代码看起来都会很奇怪。但我却爱上了它。

在寻找了一个 Map 类之后,我偶然发现了 Dictionary。谁知道呢?它将存储用户定义以及它们是否被注释掉(或未被注释掉)。我们稍后将修改 PopulateControls,以便将这些定义与所有其他内容一起显示。

...
using System.IO;
...
Dictionary<String, bool> m_dUserDefines = new Dictionary<String, bool>();
...
public BIManager( Project pjProject )
{
    m_Project = pjProject;
    
    String strProjectPath = m_Project.FullName;
    
    strProjectPath = strProjectPath.Remove( 1 + strProjectPath.LastIndexOf( "\\" ) );
    
    m_strFnRC = strProjectPath + m_Project.Name + ".rc";
    m_strFnRC2 = strProjectPath + "res\\" + m_Project.Name + ".rc2";
    m_strFnVersion = strProjectPath + "res\\_version.h";
    m_bBIEnabled = false;
    
    LoadVersionInfo();
}
...
private bool HasTheProjectBeenSetup()
{
    bool bIsRC2_Modified = false;
    StreamReader sr = new StreamReader( m_strFnRC2, true );
    String strLine = sr.ReadLine();
    
    while( strLine != null && !bIsRC2_Modified )
    {
        bIsRC2_Modified = ( strLine.IndexOf( "VS_VERSION_INFO" ) != -1 );
        
        strLine = sr.ReadLine();
    }
    
    sr.Close();
    
    return File.Exists( m_strFnVersion ) && bIsRC2_Modified;
}

public bool LoadVersionInfo()
{
    bool bSuccess = HasTheProjectBeenSetup();

    m_dUserDefines.Clear();
    
    if( bSuccess )
    {
        StreamReader sr = new StreamReader( m_strFnVersion, true );
        String strLine = sr.ReadLine();
        bool bUserDefines = false;
        
        while( strLine != null )
        {
            if( bUserDefines == false && 
                strLine.IndexOf( "// User defines from here on" ) == 0 )
            {
                bUserDefines = true;
            }
            else if( bUserDefines )
            {
                bool bEnabled = ( strLine.IndexOf( "//" ) != 0 );

                m_dUserDefines.Add( bEnabled ? 
                  strLine.Substring( 8 ) : strLine.Substring( 10 ), bEnabled );
            }
            else if( strLine.IndexOf( "#define BI2012_ENABLED" ) == 0 )
            {
                m_bBIEnabled = Convert.ToBoolean( strLine.Substring( 40 ) );
            }
            else if( strLine.IndexOf( "#define BI2012_V0" ) == 0 )
            {
                m_iV0 = Convert.ToInt32( strLine.Substring( 40 ) );
            }
            else if( strLine.IndexOf( "#define BI2012_V1" ) == 0 )
            {
                m_iV1 = Convert.ToInt32( strLine.Substring( 40 ) );
            }
            else if( strLine.IndexOf( "#define BI2012_V2" ) == 0 )
            {
                m_iV2 = Convert.ToInt32( strLine.Substring( 40 ) );
            }
            else if( strLine.IndexOf( "#define BI2012_BUILD " ) == 0 )
            {
                m_iBUILD = Convert.ToInt32( strLine.Substring( 40 ) );
            }
            
            strLine = sr.ReadLine();
        }
        
        sr.Close();
    }
    
    return bSuccess;
}
...

请注意对构造函数的修改,包括调用 LoadVersionInfo。另外,正如您所见,没有任何异常处理或错误检查。这并非偶然。这是一个模式,如果还需要更多证据,就可以确认我不知道我在做什么。

是时候实现修改项目资源文件的那些方法了,这样插件才能发挥其魔力。此时您会庆幸有了一个测试项目。代码如下。

...
public void ModifyResourceFiles()
{
    String strFnTemp = m_strFnRC + ".tmp";
    StreamReader srRC = new StreamReader(m_strFnRC, true);
    TextWriter twTemp = new StreamWriter(strFnTemp, false, Encoding.Unicode);
    TextWriter twRC2 = new StreamWriter(m_strFnRC2, true, Encoding.Unicode);
    String strLine = srRC.ReadLine();
    bool bFound = false;
    
    twRC2.WriteLine();
    
    while (strLine != null)
    {
        bFound |= (strLine.IndexOf("VS_VERSION_INFO") == 0);
        
        if(bFound)
        {
            twRC2.WriteLine(strLine);
            
            if (strLine.IndexOf("END") == 0)
            {
                twRC2.WriteLine();
                
                bFound = false;
            }
        }
        else
        {
            twTemp.WriteLine(strLine);
        }
        
        strLine = srRC.ReadLine();
    }
    
    srRC.Close();
    twTemp.Close();
    twRC2.Close();
    
    File.Delete(m_strFnRC);
    File.Move(strFnTemp, m_strFnRC);
}

public void CreateVersionFile()
{
    m_bBIEnabled = true;
    m_iV0 = 1;
    m_iV1 = 0;
    m_iV2 = 0;
    m_iBUILD = 0;

    TextWriter tw = new StreamWriter( m_strFnVersion, false, Encoding.Unicode );

    tw.WriteLine();
    tw.WriteLine( "#define BI2012_ENABLED                  True" );
    tw.WriteLine( "#define BI2012_V0                       1" );
    tw.WriteLine( "#define BI2012_V1                       0" );
    tw.WriteLine( "#define BI2012_V2                       0" );
    tw.WriteLine( "#define BI2012_BUILD                    0" );
    tw.WriteLine( "#define BI2012_BUILD_DATE               L\"00/00/00\"" );
    tw.WriteLine( "#define BI2012_BUILD_TIME               L\"00:00:00\"" );
    tw.WriteLine();
    tw.WriteLine( "#define BI2012_STR_EXPAND(tok) #tok" );
    tw.WriteLine( "#define BI2012_STR(tok) BI2012_STR_EXPAND(tok)" );
    tw.WriteLine();
    tw.WriteLine( "#define BI2012_STR_V0 _T(BI2012_STR(BI2012_V0))" );
    tw.WriteLine( "#define BI2012_STR_V1 _T(BI2012_STR(BI2012_V1))" );
    tw.WriteLine( "#define BI2012_STR_V2 _T(BI2012_STR(BI2012_V2))" );
    tw.WriteLine( "#define BI2012_STR_BUILD _T(BI2012_STR(BI2012_BUILD))" );
    tw.WriteLine( "#define BI2012_STR_VERSION BI2012_STR" 
      "(BI2012_V0) \".\" BI2012_STR(BI2012_V1) \".\" BI2012_STR(" 
      "BI2012_V2) \".\" BI2012_STR(BI2012_BUILD)" );
    tw.WriteLine();
    tw.WriteLine( "// User defines from here on" );

    tw.Close();

    m_Project.ProjectItems.AddFromFile( m_strFnVersion );
}

public void ModifyRC2()
{
    String strFnTemp = m_strFnRC2 + ".tmp";
    TextWriter tw = new StreamWriter( strFnTemp, false, Encoding.Unicode );
    StreamReader sr = new StreamReader( m_strFnRC2, true );
    String strLine = sr.ReadLine();

    while( strLine != null )
    {
        if( strLine.IndexOf( "VS_VERSION_INFO" ) != -1 )
        {
            tw.WriteLine( "#include \"_version.h\"" );
            tw.WriteLine();
            tw.WriteLine( strLine );
        }
        else if( strLine.IndexOf( "FILEVERSION" ) != -1 )
        {
            tw.WriteLine( "FILEVERSION BI2012_V0,BI2012_V1,BI2012_V2,BI2012_BUILD" );
        }
        else if( strLine.IndexOf( "PRODUCTVERSION" ) != -1 )
        {
            tw.WriteLine( "PRODUCTVERSION BI2012_V0,BI2012_V1,BI2012_V2,BI2012_BUILD" );
        }
        else if( strLine.IndexOf( "VALUE \"FileVersion\"" ) != -1 )
        {
            tw.WriteLine( "VALUE \"FileVersion\", BI2012_STR_VERSION" );
        }
        else if( strLine.IndexOf( "VALUE \"ProductVersion\"" ) != -1 )
        {
            tw.WriteLine( "VALUE \"ProductVersion\", BI2012_STR_VERSION" );
        }
        else
        {
            tw.WriteLine( strLine );
        }

        strLine = sr.ReadLine();
    }

    sr.Close();
    tw.Close();

    File.Delete( m_strFnRC2 );
    File.Move( strFnTemp, m_strFnRC2 );
}
...

编写这段代码就像使用古埃及象形文字创作亚历山大诗。然而,从算法的角度来看,这段代码很简单。为 rcrc2 的修改内容创建临时文件,然后替换原始文件。创建 _version.h 文件则完全直接。

不过,有一个问题,我想提请注意,以防有人知道答案(Google 对我不起作用)。我添加了 m_Project.ProjectItems.AddFromFile( m_strFnVersion ),因为我希望 _version.h 文件能自动包含在项目中。就目前的代码而言,该文件默认包含在“头文件”节点/过滤器/文件夹中。有人知道如何将其包含在其他文件夹中,比如“资源文件”节点/过滤器/文件夹吗?

我正在注释掉仍需实现的方法,以便我能够构建并在调试器中逐行执行代码。让我们验证一下 _version.h 是否已创建,并且 rc/rc2 是否已为测试项目正确修改……

是的。它对我有用。如果对您也有效,那我们就成功了。

第四幕。醉心权力,拿破仑直奔巴黎

现在是全力以赴的时候了。如果我们能适当地更新 _version.h,我们就完成了。最简单的方法是在 BIManager 中实现 EnableBI 方法。反过来,这需要实现 UpdateVersionFile,因为我们希望 _version.h 记录插件是否已启用。代码如下

...
public void EnableBI( bool bEnableBI )
{
    m_bBIEnabled = bEnableBI;

    UpdateVersionFile( false );
}

public void UpdateVersionFile( bool bBuild )
{
    String strBuildDate = "L\"" + 
      DateTime.Now.ToString( "MM/dd/yy" ) + "\"";
    String strBuildTime = "L\"" + 
      DateTime.Now.ToString( "hh:mm:ss" ) + "\"";

    if( bBuild == false )
    {
        // Fetch old values before overwriting the file
        StreamReader sr = new StreamReader( m_strFnVersion, true );
        String strLine = sr.ReadLine();

        while( strLine != null )
        {
            if( strLine.IndexOf( "#define BI2012_BUILD_DATE" ) == 0 )
            {
                strBuildDate = strLine.Substring( 40 );
                strLine = sr.ReadLine();
                strBuildTime = strLine.Substring( 40 );
                break;
            }

            strLine = sr.ReadLine();
        }

        sr.Close();
    }

    TextWriter tw = new StreamWriter( m_strFnVersion, false, Encoding.Unicode );

    tw.WriteLine();
    tw.WriteLine( "#define BI2012_ENABLED                  " + m_bBIEnabled.ToString() );
    tw.WriteLine( "#define BI2012_V0                       " + m_iV0.ToString() );
    tw.WriteLine( "#define BI2012_V1                       " + m_iV1.ToString() );
    tw.WriteLine( "#define BI2012_V2                       " + m_iV2.ToString() );
    tw.WriteLine( "#define BI2012_BUILD                    " + m_iBUILD.ToString() );
    tw.WriteLine( "#define BI2012_BUILD_DATE               " + strBuildDate );
    tw.WriteLine( "#define BI2012_BUILD_TIME               " + strBuildTime );
    tw.WriteLine();
    tw.WriteLine( "#define BI2012_STR_EXPAND(tok) #tok" );
    tw.WriteLine( "#define BI2012_STR(tok) BI2012_STR_EXPAND(tok)" );
    tw.WriteLine();
    tw.WriteLine( "#define BI2012_STR_V0 _T(BI2012_STR(BI2012_V0))" );
    tw.WriteLine( "#define BI2012_STR_V1 _T(BI2012_STR(BI2012_V1))" );
    tw.WriteLine( "#define BI2012_STR_V2 _T(BI2012_STR(BI2012_V2))" );
    tw.WriteLine( "#define BI2012_STR_BUILD _T(BI2012_STR(BI2012_BUILD))" );
    tw.WriteLine( "#define BI2012_STR_VERSION BI2012_STR(" 
      "BI2012_V0) \".\" BI2012_STR(BI2012_V1) \".\" BI2012_STR(" 
      "BI2012_V2) \".\" BI2012_STR(BI2012_BUILD)" );
    tw.WriteLine();
    tw.WriteLine( "// User defines from here on" );

    foreach( var pair in m_dUserDefines )
    {
        tw.WriteLine( ( pair.Value == false ? "//" : "" ) + "#define " + pair.Key );
    }

    tw.Close();
}
...

EnableBI 方法很简单。它记录值的变化,然后调用 UpdateVersionFile

然而,UpdateVersionFile 方法在其同类中具有远见。它展望未来,届时将被调用来记录构建日期和时间。但现在还不是时候。此时,当它从 EnableBI 调用时,它会获取旧的日期/时间值,因为毕竟我们只是记录插件已被启用或禁用。

构建并调试。当您打开 _version.h 文件并在一个足够智能的文本编辑器中(该编辑器能在外部修改时重新加载文件)打开它时,疯狂地点击 cbEnableBI。不要害羞。尽情操作。您将看到 BI2012_ENABLEDTrue 变为 False,再变回来,世界也会随之微笑。

我们势不可挡。现在让我们实现 VersionNumberChange,这是我们之前为了能够构建和调试这个怪物而注释掉的最后一个方法。

...
public void VersionNumberChange( int iV0 = -1, int iV1 = -1, int iV2 = -1, int iBuild = -1 )
{
    if( iV0 != -1 )
    {
        m_iV0 = iV0;
    }

    if( iV1 != -1 )
    {
        m_iV1 = iV1;
    }

    if( iV2 != -1 )
    {
        m_iV2 = iV2;
    }

    bool bBuild = ( iBuild != -1 );

    if( bBuild )
    {
        m_iBUILD = iBuild;
    }

    if( iV0 != -1 || iV1 != -1 || iV2 != -1 || bBuild )
    {
        UpdateVersionFile( bBuild );
    }
}
...

好了。VersionNumberChange 方法在用户手动更改对话框中的版本号时被调用,并且根据 Connect.cs 中的代码,在每次构建之前也会自动调用。

那么,还剩下什么要做?啊,是的。填充用户定义的复选框列表,并编写“添加”、“删除”和“复制到剪贴板”按钮的逻辑。

...
private void PopulateControls()
{
...
    clbUserDefines.Items.Clear();
    Dictionary<String, bool> dUserDefines = 
                 m_biManager.GetUserDefinesDictionary();

    foreach( var pair in dUserDefines )
    {
        clbUserDefines.Items.Add( pair.Key, pair.Value );
    }
...
}

private void btCopytoClipboardBIDefines_Click( object sender, EventArgs e )
{
    String strToClip = "";

    foreach( Object selecteditem in lbBIDefines.SelectedItems )
    {
        String strItem = selecteditem as String;

        strToClip += strItem;

        if( strItem == "#include \"_version.h\"" )
        {
            strToClip += "\n";
        }
        else
        {
            strToClip += ", ";
        }
    }

    if( strToClip != "" )
    {
        char[] charsToTrim = { ' ', ',' };

        Clipboard.SetText( strToClip.TrimEnd( charsToTrim ) );
    }
}

private void btCopyToClipboardUserDefines_Click( object sender, EventArgs e )
{
    Object oSelection = clbUserDefines.SelectedItem;

    if( oSelection != null )
    {
        Clipboard.SetText( oSelection.ToString() );
    }
}

private void btDeleteUserDefine_Click( object sender, EventArgs e )
{
    Object oSelection = clbUserDefines.SelectedItem;

    if( oSelection != null )
    {
        Dictionary<String, bool> dUserDefines = m_biManager.GetUserDefinesDictionary();

        dUserDefines.Remove( oSelection.ToString() );

        clbUserDefines.Items.Remove( oSelection );

        m_biManager.UpdateVersionFile( false );
    }
}

private void btAddUserDefine_Click( object sender, EventArgs e )
{
    AddNewUserDefine();
}

private void tbNewUserDefine_KeyDown( object sender, KeyEventArgs e )
{
if( e.KeyCode == Keys.Enter )
{
    AddNewUserDefine();
}
}

private void AddNewUserDefine()
{
    String strNewUserDefine = tbNewUserDefine.Text;

    if( strNewUserDefine != "" )
    {
        Dictionary<String, bool> dUserDefines = m_biManager.GetUserDefinesDictionary();

        if( dUserDefines.ContainsKey( strNewUserDefine ) == true )
        {
            MessageBox.Show( "This define already exists" );
        }
        else
        {
            m_bBlockEvents = true;
            clbUserDefines.Items.Add( strNewUserDefine, true );
            m_bBlockEvents = false;

            dUserDefines.Add( strNewUserDefine, true );

            m_biManager.UpdateVersionFile( false );
        }
    }
}
...

这似乎有很多代码,但很容易理解。别忘了在 BIManager 类中添加一个简单的 getter hack 来获取用户定义的 Dictionary

...
public Dictionary<String, bool> GetUserDefinesDictionary()
{
    return m_dUserDefines;
}
...

另外,插件被禁用意味着什么?我们可以将 BIForm 中的所有控件变灰,但我只会阻止插件自动更新构建计数。所有其他功能将保持不变。IncrementBuildNumber 方法需要修改如下。

...
public int IncrementBuildNumber()
{
    if( m_bBIEnabled )
    {
        m_iBUILD++;

        VersionNumberChange( -1, -1, -1, m_iBUILD );
    }

    return m_iBUILD;
}
...

完成?还没有。我们必须确保在用户定义被(取消)选中时更新 _version.h。没问题。在属性 -> 事件中,找到用户定义的复选框列表的“行为”下的“ItemCheck”。

...
private void clbUserDefines_ItemChecked( object sender, ItemCheckEventArgs e )
{
    if( m_bBlockEvents )
    {
        return;
    }

    Dictionary<String, bool> dUserDefines = m_biManager.GetUserDefinesDictionary();
    String strKey = clbUserDefines.Items[ e.Index ].ToString();

    if( dUserDefines.ContainsKey( strKey ) == true )
    {
        dUserDefines[ strKey ] = ( e.NewValue == CheckState.Checked );

        m_biManager.UpdateVersionFile( false );
    }
}
...

我认为我们实际上已经完成了整个插件。奇怪的平淡无奇。

测试它,测试它,然后再次测试它。请记住,此插件将修改您珍贵项目的资源文件。别说我没有足够地警告您。

第五幕。我看到了僵尸代码

这是您的代码。您曾经深爱的项目现在已被此插件摧毁。您没有听从我的警告,现在……好吧,您必须从头开始。

附录 

我希望这篇教程能对某些人有所帮助。我只能为我在这个主题上的无知道歉。这是我在几个小时内能为您提供的最好的。

而且,请,如果您确实比我懂得多(这肯定是真的),请告诉我哪些可以改进,哪些是致命的错误。我会更新教程并给您署名。评论区全归您了。

© . All rights reserved.