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

构建 CVS Root 文件更改实用程序

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.60/5 (8投票s)

2005 年 11 月 13 日

BSD

16分钟阅读

viewsIcon

66021

downloadIcon

1198

从收集需求到实现和优化,构建一个临时更改 CVS/Root 文件以实现远程 CVS 访问的工具的过程。

引言

出差的 Concurrent Versions System (CVS) 用户可能会发现自己正在与源代码树中存在的 CVS/Root 文件内容作斗争。因为用于解析计算机的主机名可能因一个 LAN 到另一个 LAN 而异,所以 CVS/Root 文件中存储的值必须反映这一点。本文介绍了构建一个可视化工具来更改 CVS/Root 文件内容,并在完成后将其恢复到原始状态的过程。

本文采用比说教更具对话性的语气。但是,我希望仍有一些读者觉得这个叙述很有启发性。它涵盖了从问题分析等抽象主题到将图像嵌入程序集并将其用于 `TreeView` 等详细解决方案。文章中的代码示例以及源代码中的代码都包含大量注释。如果您属于我们作为开发人员应该编写自注释代码的阵营,那么我希望您能容忍过多的 //。

从前...

在一个比平时温暖的冬日早晨,我坐在我借来的小办公室外面的一张相当舒适的长椅上。我的意式浓缩咖啡在稀薄的空气中静静地冒着热气,我正在思考我的问题:我必须想办法临时更改我笔记本电脑上的 CVS 客户端如何连接到我的 CVS 服务器。通常,我通过 SSH 隧道连接到我的 CVS 服务器。但是,在我的防火墙之外,我必须使用我的 SSH 隧道来传输我的 IMAP 连接。

这个担忧确实不算什么大问题。我的 CVS 客户端 WinCVS 内置了一个宏,可以精确地完成我需要的功能。但是,如果我运行它,设置就会永久化。直到我再改回来。而且,因为我很懒惰,容易忘记这样的事情,所以我想要一个不同的解决方案。本文的主题就是开发该解决方案的过程。

CVS 及其文件的简要说明

如果您了解 CVS 和 `Root` 文件,那么您可以跳到下一节。否则,请允许我解释。

来自维基百科:Concurrent Versions System,“[CVS] 跟踪一组文件(通常是软件项目的实现)中的所有工作和所有更改,并允许多个(可能相距甚远)开发人员进行协作”。作为一名承包商,我使用 CVS 来跟踪我客户代码的所有更改,并允许我项目中的其他开发人员在同一代码库上进行开发。

当您通过正常的检出过程从 CVS 获取代码时,您的 CVS 客户端很可能会在代码的源代码树的所有目录中创建子目录。在这些恰当地命名为“CVS”的子目录中,客户端将创建描述检索到的文件、检索它们的模块名称以及(对本文的存在至关重要)名为“`Root`”的文件,该文件包含 CVS 客户端自动重新连接到 CVS 服务器以获取文件更新的连接信息。在本节左侧的图像中,您可以看到我检查出的代码的一些目录的 Windows Explorer 裁剪视图。名为“`Test Area`”和“`Test Area/deader`”目录下的“`CVS`”子目录是由我的 CVS 客户端在我检出源代码文件时创建的。尽管您在树视图中看不到,但那些“`CVS`”子目录具有“隐藏”属性。

在这些“`CVS`”子目录内部存在 `Root` 文件。如前一段所述,`Root` 文件包含 CVS 客户端与 CVS 服务器通信的连接信息。通常,我“`CVS`”子目录中的 `Root` 文件包含字符串 `:ssh:curtis@cvs.grayiris.com:/var/cvs`,这意味着“使用 SSH 连接到 cvs.grayiris.com 上的 CVS 服务器,用户名是'curtis',并在路径 `/var/cvs` 中查找源代码树。”

需求收集

如最后一段所述,我的 CVS 控制目录中的 `Root` 文件包含字符串 `:ssh:curtis@cvs.grayiris.com:/var/cvs`。但是,因为我已经使用 SSH 连接打开了 IMAP 连接的隧道,所以我必须通过现有 SSH 连接来隧道化 CVS 客户端的 SSH 连接。我通过将本地主机的 22 端口上的请求转发到 cvs.grayiris.com:22 来做到这一点。因此,我必须将所有 `Root` 文件的内容更改为 `:ssh:curtis@localhost:/var/cvs`。然后,当我完成工作时,我希望它们能在没有我的干预的情况下恢复到正常状态。我希望这能发生而不会创建备份文件。我只是不太喜欢它们。我不喜欢我的文件系统变得混乱。

此外,我不想花很多时间编写这个工具。我的意思是,如果这需要我大量的时间投入,那么我只需要忍受运行宏或编写 Perl 脚本来处理它,并忽略创建的临时文件。

拿出笔记本,迅速列出需求

  1. 开发时间短:最多 1 或 2 小时。
  2. 自动将 CVS/Root 文件内容恢复为旧连接字符串。
  3. 能够选择性地递归遍历目录,以更改存在的每个 CVS/Root 文件。
  4. 文件系统的高效树视图,顶层包含
    • “`我的文档`”文件夹。
    • 所有本地、逻辑、不可移动驱动器。
  5. 树视图必须在其表示中区分 CVS 控制的目录。

我还草绘了用户界面的外观。下一张图像包含使用计算机绘图工具重现的草图

分析与设计

我真的很想在上班前完成这个工具。我翻过笔记本的一页,在长椅上坐下。我看了看手表,注意到离我需要进楼还有几分钟。

描述 CVS/Root 文件修改的流程图

我决定画一个快速的流程图来模拟代码更改 CVS/Root 文件内容的流程。我画出了以下图像中的流程图

文件系统的高效 TreeView

在查看树视图时,我决定按需加载方案对我来说是最好的。我不想遍历整个目录结构来填充表示我可用路径的 `TreeView`。从“`我的文档`”目录和本地驱动器的根节点开始,`TreeView` 将只包含用户已主动展开的节点。

之后,我走进大楼去工作。

午餐时间

软件开发人员在午餐时间做什么?开发软件!早上我考虑了这个小项目。它很有趣。当其他人去餐馆和微波炉时,我把笔记本电脑带到所有这一切开始的长椅上。我有一些很好的书面想法,还有一个帮助我编码的流程图。我启动了 Visual Studio™ 并开始工作。

我创建了一个“Windows 应用程序”项目,并首先构建了用户界面。我真的很讨厌调整大小不好的应用程序,所以我花时间确保这个应用程序可以。下图显示了我主窗体上的项目布局。设置了这些属性后,如果我在宽屏笔记本上最大化窗口,所有内容都会相应地调整大小。

现在,我有三个实际的代码块需要完成:管理 `TreeView` 的例程,将修改 CVS/Root 文件的代码,以及将文件恢复的代码。

加载 TreeView

我决定首先解决上面列表中的第四个问题。最初填充 `TreeView` 必须在应用程序开始时发生。由于我的大部分工作都位于“`我的文档`”目录中,所以我决定先添加它。

既然我之前决定使用按需加载方案创建目录结构的视图,我需要一种方法来存储 `TreeNode` 表示的路径,以及应用程序是否已加载其子节点(如果存在)。在我的窗体类声明的底部,我创建了以下结构来保存该信息

/// <summary>
/// A structure that contains information for a <see cref="TreeNode"/>.
/// </summary>
private struct NodeInfo
{
  /// <summary>
  /// Initializes the structure with the specified <see cref="bool"/>
  /// and <see cref="string"/> values.
  /// </summary>
  /// <param name="init">Denotes if the node has been initialized.</param>
  /// <param name="path">
  /// The path to the CVS-controlled directory represented by the node.
  /// </param>
  public NodeInfo( bool init, string path )
  {
    Initialized = init;
    Path = path;
  }
  
  /// <summary>
  /// A <see cref="bool"/> that contains the flag representing if the node
  /// has been initialized.
  /// </summary>
  public bool Initialized;
  
  /// <summary>
  /// A <see cref="string"/> containing the path represented by the node.
  /// </summary>
  public string Path;
}

添加“我的文档”TreeNode

然后,在窗体构造函数中的 `InitializeComponent` 方法调用之后,我添加了以下行。请注意,`TreeView` 控件的名称是 `dirTree`

// Change the font of the tree to a fixed-width 
// font that I can read from a distance
// without using my glasses.
dirTree.Font = new Font( "Courier New", 10.0f );

// Create a bold font to mark CVS-controlled subdirectories.
bold = new Font( dirTree.Font.FontFamily.Name, 
                        dirTree.Font.Size, FontStyle.Bold );

// Get the path for My Documents.
string myDocDir = 
  Environment.GetEnvironmentVariable( "USERPROFILE" ) + "\\My Documents";

// Add the node representing the "My Documents" directory, if it exists.
if( Directory.Exists( myDocDir ) )
{
  // Create the My Documents tree node.
  TreeNode myDocNode = new TreeNode( "My Documents" );
  
  // Check to see if the My Documents directory has subdirectories.
  if( Directory.GetDirectories( myDocDir ).Length > 0 )
  {
    // The My Documents has subdirectories. Let's add the loading node.
    myDocNode.Nodes.Add( new TreeNode( "Loading..." ) );
    
    // Also, we tag the node with the pertinent information that we'll need later
    // to populate the tree on demand.
    myDocNode.Tag = new NodeInfo( false, myDocDir );
  }
  
  // Add the My Documents node to the tree.
  dirTree.Nodes.Add( myDocNode );
}

添加逻辑驱动器的 TreeNodes

我只想添加逻辑的、不可移动的驱动器到我的 `TreeView`。`Environment.GetLogicalDrives()` 方法返回所有逻辑驱动器,但没有任何驱动器类型信息。有一会儿,我以为我别无选择,只能全部添加。然后,我想起了我去年写的一些脚本中使用的 Windows Management Instrumentation 接口。我查看了 .NET 文档,发现 `System.Management` 程序集中有我需要的东西。我在项目中添加了对它的引用,在文件中添加了 using System.Management 指令,并在添加到上一节的代码之后添加了以下代码

// Create a management class to get the logical disks 
// that we can add to the directory tree.
ManagementClass c = new ManagementClass( "Win32_LogicalDisk" );

// Get the instances of the logical drives.
ManagementObjectCollection moc = c.GetInstances();

// Iterate over the logical drives.
foreach( ManagementObject mo in moc )
{
  try
  {
    // If the drive is a hard drive, then add 
    // it to the tree as a root item.
    if( mo[ "DriveType" ] != null && 
           Int32.Parse( mo[ "DriveType" ].ToString() ) == 3 )
    {
      // Get the name of the drive 
      // ("C:", for example) and append the path
      // separator character to it.
      string title = 
          mo[ "Name" ].ToString() + Path.DirectorySeparatorChar;
      
      // Create a new node that contains that 
      // represents that drive letter.
      TreeNode tn = new TreeNode( title );
      
      // If subdirectories exist for that drive letter, then
      // add a "Loading..." node.
      if( Directory.GetDirectories( title ).Length > 0 )
      {
        // Create and add the "Loading..." node.
        tn.Nodes.Add( new TreeNode( "Loading..." ) );
        
        // Tag the node with the pertinent information.
        tn.Tag = new NodeInfo( false, title );
      }
      
      // Add the node that represents that drive letter.
      dirTree.Nodes.Add( tn );
    }
  }
  catch( Exception ) {}
}

我构建了项目并检查了我所做的。果然,`TreeView` 包含了合适的节点,并具有合适的展开功能。

添加按需加载 TreeView 展开

当然,当我展开 `TreeView` 中的根节点时,我只看到一个显示“Loading...”的节点。唉,我还有一些工作要做。我查看了 `TreeView` 控件的文档,注意到 `AfterExpand` 事件符合我的要求。在添加到构造函数的最后一段代码之后,我添加了以下行,并允许 Visual Studio 为我创建适当的方法

  // Create an event handler that will load 
  // subdirectories into the tree when
  // it expands.
  dirTree.AfterExpand += 
         new TreeViewEventHandler( dirTree_AfterExpand );

在 `dirTree_AfterExpand` 中,我用以下代码填充它

/// <summary>
/// The event handler that will populate the 
/// tree's node with subdirectories, if
/// they exist.
/// </summary>
/// <param name="sender">The <see cref="object"/> 
/// that invoked the event.</param>
/// <param name="e">Some <see cref="TreeViewEventArgs"/>.</param>
private void dirTree_AfterExpand( object sender, TreeViewEventArgs e )
{
  // The information for the node.
  NodeInfo ni = ( NodeInfo ) e.Node.Tag;
  
  // If the code has not initialized the node, 
  // yet, with subdirectories, then
  // do that.
  if( !ni.Initialized )
  {
    // Get rid of the "Loading..." node.
    e.Node.Nodes.Clear();
    
    string[] dirs = null;
    try
    {
      // Get the subdirectories assigned to the path of the node.
      dirs = Directory.GetDirectories( ni.Path );
    }
    catch( DirectoryNotFoundException )
    {
      // Somebody's gone and removed the directory 
      // that once existed. We need to
      // inform the user and remove it from the tree.
      MessageBox.Show( 
         "That directory no longer exists and I will remove it from the tree.",
         "Invalid Directory",
         MessageBoxButtons.OK,
         MessageBoxIcon.Exclamation );
      e.Node.Remove();
      return;
    }
    
    // For each subdirectory, check its attributes and, 
    // if they meet the specified criteria,
    // add it to the tree.
    for( int i = 0; i < dirs.Length; i++ )
    {
      try
      {
        // Get the directory's information.
        DirectoryInfo di = new DirectoryInfo( dirs[ i ] );
        
        // If the directory does not have the 
        // System nor Hidden attributes set,
        // then add it to the tree.
        if( ( di.Attributes & FileAttributes.System ) == 0 && 
                   ( di.Attributes & FileAttributes.Hidden ) == 0 )
        {
          // Create a tree node that represents the subdirectory.
          int lastDirSepChar = 
                  dirs[ i ].LastIndexOf( Path.DirectorySeparatorChar );
          string nodeTitle = dirs[ i ].Substring( lastDirSepChar + 1 );
          TreeNode n = new TreeNode( nodeTitle );
          
          // Count the subdirectories and, if more than one exists, add a
          // "Loading..." node.
          if( CountSubDirs( Directory.GetDirectories( dirs[ i ] ) ) > 0 )
          {
            n.Nodes.Add( new TreeNode( "Loading..." ) );
          }
          
          // If the CVS subdirectory exists and 
          // the Root file exists in it, then
          // make the font for the node bold.
          string p = String.Format( "{0}{1}CVS{1}Root", 
                          di.FullName, Path.DirectorySeparatorChar );
          if( File.Exists( p ) )
          {
            n.NodeFont = bold;
          }
          
          // Tag the subdirectory's node with the pertinent information.
          n.Tag = new NodeInfo( false, dirs[ i ] );
          
          // Add the new subdirectory node to the recently expanded node.
          e.Node.Nodes.Add( n );
        }
      }
      catch( Exception ) {}
    }
    
    // Mark the node as initialized.
    ni.Initialized = true;
    
    // Reset the node's tag.
    e.Node.Tag = ni;
  }
}

现在 `TreeView` 的工作方式完全符合我的要求。我看了看手表,注意到我的午餐时间只剩下大约 25 分钟了。如果我想要完成这个,我需要稍微加快速度。

修改 CVS/Root 文件

当应用程序修改文件时,它需要记住它对哪个文件做了什么。然后,它需要在 `ListBox` 中插入一个条目来显示它做了什么。我查看了 `ListBox.Add` 方法的文档,看到它接受任何 `System.Object`。然后 `ListBox` 使用 `Object` 的 `ToString` 方法来显示条目。我滚动到类声明的底部,输入了以下结构定义

/// <summary>
/// A structure that contains the path and content of modified
/// CVS/Root files.
/// </summary>
private struct PathSelectInfo
{
  /// <summary>
  /// Initializes the structure with the given <see cref="string"/>
  /// values.
  /// </summary>
  /// <param name="path">The path to the modified directory.</param>
  /// <param name="root">The value of the Root file.</param>
  public PathSelectInfo( string path, string root )
  {
    Root = root.Trim();
    Path = path;
  }
  
  /// <summary>
  /// A <see cref="string"/>-based representation of the struct
  /// used by the ListBox.
  /// </summary>
  /// <returns>A <see cref="string"/>-based 
  /// representation of the struct.</returns>
  public override string ToString()
  {
    return Path + " (" + Root + ")";
  }
  
  /// <summary>
  /// A <see cref="string"/> that contains 
  /// the value for the Root file.
  /// </summary>
  public string Root;
  
  /// <summary>
  /// A <see cref="string"/> that contains 
  /// the path to the CVS-controlled
  /// directory.
  /// </summary>
  public string Path;
}

现在,我有了存储已修改文件相关信息的方法,当点击“Apply”按钮时,需要发生一些事情。幸运的是,我今天早些时候在我的流程图中定义了过程。我双击“Form View”中的按钮,Visual Studio 添加了 `btnApply_Click` 事件处理程序,并向其中添加了以下代码

/// <summary>
/// The event handler for the "Apply" <see cref="Button"/>.
/// </summary>
/// <param name="sender">The <see cref="object"/> 
/// that invoked the event.</param>
/// <param name="e">Some <see cref="EventArgs"/>.</param>
private void btnApply_Click( object sender, System.EventArgs e )
{
  // Disable the controls.
  cbRecurse.Enabled = false;
  btnApply.Enabled = false;
  btnRevertAll.Enabled = false;
  
  // Get the current node's information.
  NodeInfo ni = ( NodeInfo ) dirTree.SelectedNode.Tag;
  
  // Change the CVS/Root files.
  ChangeCvsRoot( ni.Path );
  
  // Enable the controls.
  cbRecurse.Enabled = true;
  btnApply.Enabled = true;
  btnRevertAll.Enabled = true;
}

我没有在方法中进行任何文件修改,因为如果用户选择递归遍历子目录,那么我将需要一个可以递归调用的函数来执行这些操作。因此,我定义了 `ChangeCvsRoot` 方法

/// <summary>
/// Changes the Root file in the CVS subdirectory and adds the file's
/// information to the <see cref="ListBox"/>.
/// </summary>
/// <param name="path">The current path to investigate.</param>
private void ChangeCvsRoot( string path )
{
  try
  {
    // Create a string that will contain the CVS path.
    string cvsPath = String.Format( "{0}{1}CVS{1}Root", path, 
                                       Path.DirectorySeparatorChar );
    // Open a StreamReader to read the file's contents.
    StreamReader sr = File.OpenText( cvsPath );
    
    // Read the file's contents.
    string root = sr.ReadToEnd();
    
    // Close the StreamReader.
    sr.Close();
    
    // Open a StreamWriter to overwrite the file just read.
    StreamWriter sw = new StreamWriter( cvsPath );
    
    // Write the new root to the file.
    sw.Write( txtRoot.Text + Environment.NewLine );
    
    // Close the StreamWriter
    sw.Close();
    
    // If all that went well, add the modified file's path and old
    // value to the ListBox.
    lbFilePaths.Items.Add( new PathSelectInfo( path, root ) );
    
    // If the user has specified that she would like to descend
    // recursively into subdirectories, then do that.
    if( cbRecurse.Checked )
    {
      // Get the subdirectories of the current directory.
      string[] paths = Directory.GetDirectories( path );
      
      // For each subdirectory, change its CVS/Root file.
      foreach( string p in paths )
      {
        ChangeCvsRoot( p );
      }
    }
  }
  catch( Exception ) {}
}

现在,当我输入一个新的 Root 值,从 `TreeView` 中选择一个 CVS 控制的目录,然后点击“Apply”按钮时,一切“Just Works”™。

恢复文件

当然,我想把文件改回原来的样子。幸运的是,我将所有信息都存储在了 `ListBox` 中!所以,我双击窗体视图中的“Revert All”按钮,Visual Studio 创建了事件处理程序 `btnRevertAll_Click`,我填充了它

/// <summary>
/// The "Revert All" <see cref="Button"/> that sets the modified CVS/Root
/// files to their original state.
/// </summary>
/// <param name="sender"></param>
/// <param name="sender">The <see cref="object"/> 
/// that invoked the event.</param>
/// <param name="e">Some <see cref="EventArgs"/>.</param>
private void btnRevertAll_Click( object sender, System.EventArgs e )
{
  // For each item in the ListBox starting with the bottom, revert the
  // file to its original state.
  for( int i = lbFilePaths.Items.Count - 1; i >= 0; i-- )
  {
    try
    {
      // Get the item out of the ListBox.
      PathSelectInfo psi = ( PathSelectInfo ) lbFilePaths.Items[ i ];
      
      // Open a StreamWriter to write the original value.
      string p = String.Format( "{0}{1}CVS{1}Root", psi.Path, 
                                       Path.DirectorySeparatorChar );
      StreamWriter sw = new StreamWriter( p );
      
      // Write the original value plus a new line character.
      sw.Write( psi.Root + Environment.NewLine );
      
      // Close the StreamWriter.
      sw.Close();
      
      // If no error has occurred, remove the item from the ListBox.
      lbFilePaths.Items.RemoveAt( i );
    }
    catch( Exception ) {}
  }
}

就这样。我的实用程序工作了!我带着满意的微笑走回大楼。

让它更漂亮

当晚...

孩子们上床睡觉后,我和妻子在后院放松。我带着笔记本电脑在那里,给她看我的新程序。她不写软件,也不太关心 CVS、C# 或我的防火墙烦恼。但她喜欢我为自己写一些小程序。我真的很看重她。所以,她的评价对我来说意义重大。她说:“这很酷。但是,有点丑。而且加载很慢。”

好吧,我不得不承认。所以,我决定解决眼前的审美缺陷。我还没有在树中添加图像,而且不可否认,它看起来有点单调。所以我翻阅了Tango 图标库,找到了四张 16x16 大小的图像,它们对我来说效果很好

  • devices/drive-harddisk.png 用于逻辑驱动器的节点。
  • apps/system-file-manager.png 用于“`我的文档`”节点。
  • mimetypes/x-directory-normal.png 用于普通子目录节点。
  • mimetypes/x-directory-remote.png 用于 CVS 控制下的子目录节点。

我把它们都添加到了我的项目中。

将图像嵌入程序集并使用它们

对于像这样的小型实用程序,我喜欢嵌入静态资源,例如这些图像。它会使可执行文件变大,但我不再需要担心这些文件的路径。这需要使用 .NET 反射功能。所以,我遵循这三个简单的步骤

  1. 将图像文件的“Build Action”属性值从“Content”更改为“Embedded Resource”;
  2. using System.Reflection 语句添加到文件中;并且,
  3. 编写代码以将图像添加到树中。

步骤 1:更改图像文件的生成操作

正如您从右侧的屏幕截图所看到的,`system-file-manager.png` 文件在“解决方案资源管理器”窗格下方的“属性”窗格中有一个“Build Action”属性。请注意,我已将其值更改为“Embedded Resource”。这指示 Visual Studio 指示编译器将此资源嵌入到生成的程序集中,在这种情况下是可执行文件。这样,在步骤 3 中,我可以编写代码从可执行文件中提取图像并在 `TreeView` 中使用它。

步骤 2:添加 using 指令

不言自明:我在类文件的顶部输入了 using System.Reflection

步骤 3:使用嵌入式资源

要在树中使用图像,`TreeView` 会公开 `TreeView.ImageList` 属性,可以为其分配 `System.Windows.Forms.ImageList`。因此,要将我新找到的图像添加到 `TreeView`,我需要创建一个 `ImageList`,将图像添加到其中,将该列表分配给树,然后指定要使用哪些图像用于不同的节点。

为此,我回到我的窗体构造函数,在 `InitializeComponent` 调用之后,我添加了以下行来创建 `ImageList` 并填充它

// Create an image list for the tree.
ImageList iList = new ImageList();

// Populate the image list by getting the 
// executing assembly, getting each image
// from the manifest resources, then 
// adding that image to the image list.
Assembly a = Assembly.GetExecutingAssembly();
Stream imageStream = 
    a.GetManifestResourceStream( "CvsRootChanger.drive-harddisk.png" );
iList.Images.Add( Image.FromStream( imageStream ) );
imageStream = 
    a.GetManifestResourceStream( "CvsRootChanger.system-file-manager.png" );
iList.Images.Add( Image.FromStream( imageStream ) );
imageStream = 
    a.GetManifestResourceStream( "CvsRootChanger.x-directory-normal.png" );
iList.Images.Add( Image.FromStream( imageStream ) );
imageStream = 
    a.GetManifestResourceStream( "CvsRootChanger.x-directory-remote.png" );
iList.Images.Add( Image.FromStream( imageStream ) );

// Set the tree's image list.
dirTree.ImageList = iList;

如果您在程序集中使用嵌入式资源,则在想从其嵌入性质中检索文件名时,不能忘记在文件名之前加上命名空间。您将在添加图像到列表的四行中看到这一点;我将它们都称为“`CvsRootChanger.filename`”,因为我的项目具有“`CvsRootChanger`”命名空间。

现在我已经将图像分配给了 `TreeView`,我必须指定要使用哪些图像。我搜索所有创建新的 `TreeNode` 的地方,并通过 `ImageList` 中的零基索引指定使用哪个图像。我发现三个适用实例,我添加了读取“Loading...”以外的节点,并对它们进行了更改

// In the portion where I create the "My Documents" TreeNode
// Replaces this line.
TreeNode myDocNode = new TreeNode( "My Documents", 1, 1 );
// In the portion where I create the TreeNodes for the logical drives
// Replaces this line.
TreeNode tn = new TreeNode( title, 0, 0 );
// In the portion where I create the TreeNodes for subdirectories
// Replaces this line.
int lastDirSepChar = 
        dirs[ i ].LastIndexOf( Path.DirectorySeparatorChar );
string nodeTitle = dirs[ i ].Substring( lastDirSepChar + 1 );
TreeNode n = new TreeNode( nodeTitle, 2, 2 );

现在,我不会为 CVS 控制下的子目录创建新的 `TreeNode`。我只是将它们设为粗体。所以我寻找粗体设置,并添加使用第四个图像用于该节点的说明

// In the dirTree_AfterExpand method, after I specify that I want to use a bold
// font for the node.
// Inserted after this line.
n.ImageIndex = n.SelectedImageIndex = 3;

我编译了项目并开始展开树。果然,图像在每个节点中都正确显示。

克服加载缓慢的问题

没有什么比启动屏幕更能体现专业精神了。

好吧,那不全是。但是,每当我启动应用程序时,我都必须等待逻辑驱动器被检索并添加到 `TreeView`。这个过程可能需要长达五秒钟。我不喜欢这样。所以我决定使用一个单独的线程来加载主窗体,同时向用户显示一个辅助窗体。我不想在这个上面投入太多时间,所以我保持了设计的简单性。我再次拿出我的笔记本,并快速写下一些伪代码来模拟设计。

  • 显示加载窗体。
  • 加载窗体激活时,启动一个线程来加载主窗体。
  • 如果线程因为主窗体已正确加载而结束,
    • 关闭加载窗体。
    • 显示主窗体。

    否则,

    • 显示错误消息。
    • 显示一个关闭窗体的按钮。

我在项目中创建了另一个 Windows 窗体,将其 `ControlBox` 属性设置为“False”,将窗体的 `Text` 属性设置为“Loading Application...”,在底部添加了一个停靠的 `Panel`,其中包含一个 `Button`,在窗体上添加了一个停靠并设置为“Fill”的 `Label`,并将 `Label` 的 `Text` 属性设置为“Loading the drive information for the application”。下图显示了该窗体的窗体设计器视图的屏幕截图

现在,我需要一种方法让加载窗体创建一个主窗体,该主窗体在加载窗体关闭后不会消失。所以,在我的主窗体类声明的末尾,我添加了以下行

/// <summary>
/// A public static variable for the loading form to set.
/// </summary>
public static MainAppForm f;

有了这个,我就可以将窗体存储在该 static 变量中,并在加载窗体关闭时使用它。由于我计划为加载主窗体使用单独的线程,所以我将 using System.Threading 指令添加到了加载窗体类文件的顶部。在加载窗体的构造函数中,在 `InitializeComponent` 调用之后,我添加了以下行,并允许 Visual Studio 为我创建事件处理程序

// An event handler that will load the application's main form.
this.Activated += new EventHandler( LoadingForm_Activated );

在事件处理程序中,我写下了以下代码行

/// <summary>
/// An event handler that will load the application's main form in
/// another <see cref="Thread"/>.
/// </summary>
/// <param name="sender">The <see cref="object"/> 
/// that invoked the event.</param>
/// <param name="e">Some <see cref="EventArgs"/>.</param>
private void LoadingForm_Activated( object sender, EventArgs e )
{
  // Create the object that contains the data for the thread.
  Foo f = new Foo();
  
  // Give the other thread a reference to this form so that it can
  // close it when it completes its work.
  f.f = this;
  
  // Create a thread to load the form.
  Thread t = new Thread( new ThreadStart( f.LoadMainAppForm ) );
  
  // Start the thread.
  t.Start();
}

最后,在类声明的末尾,我需要创建处理加载应用程序主窗体的 `Foo` 类

/// <summary>
/// A utility class that loads the application's main form.
/// </summary>
private class Foo
{
  /// <summary>
  /// A reference to the form that shows that we're loading the
  /// application's main form.
  /// </summary>
  public LoadingForm f;
  
  /// <summary>
  /// The method used to load the application's main form.
  /// </summary>
  public void LoadMainAppForm()
  {
    try
    {
      // Load the main form.
      MainAppForm.f = new MainAppForm();
      
      // Close the loading form.
      f.Close();
    }
    catch( Exception e )
    {
      // Change the alignment of the label's content.
      f.lblMessage.TextAlign = ContentAlignment.TopLeft;
      
      // Catch the exception and informa the user.
      f.lblMessage.Text = 
             "An exception occurred while loading the application"
             + Environment.NewLine
             + Environment.NewLine
             + "Message: "
             + e.Message;
      
      // Show the "OK" button.
      f.cancelPanel.Height = 40;
    }
  }
}

最后,我想让加载窗体在应用程序启动时首先显示。我回到主窗体的代码,找到 `Main` 方法。我根据以下代码片段对其进行了更改

/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main() 
{
  // First, show the loading form.
  Application.Run( new LoadingForm() );
  
  // If the loading form did its job, then run the application with
  // the application's main form.
  if( MainAppForm.f != null )
  {
    Application.Run( MainAppForm.f );
  }
}

我坐下,喝了一口茶,向妻子展示了我所做更改的效果。当她竖起大拇指时,我知道我完成了我的应用程序。

请求反馈

如果您有关于本文及其关联源代码的意见,请随时提出您的赞赏或异议。

历史

  • 2005-11-13
    • 撰写并提交文章。
  • 2005-11-14
    • 更改了 `
      ` 标签的宽度,以更好地适应 Firefox。
© . All rights reserved.