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






4.60/5 (8投票s)
从收集需求到实现和优化,构建一个临时更改 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 或 2 小时。
- 自动将 CVS/Root 文件内容恢复为旧连接字符串。
- 能够选择性地递归遍历目录,以更改存在的每个 CVS/Root 文件。
- 文件系统的高效树视图,顶层包含
- “`我的文档`”文件夹。
- 所有本地、逻辑、不可移动驱动器。
- 树视图必须在其表示中区分 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 反射功能。所以,我遵循这三个简单的步骤
- 将图像文件的“Build Action”属性值从“Content”更改为“Embedded Resource”;
- 将
using System.Reflection
语句添加到文件中;并且, - 编写代码以将图像添加到树中。
步骤 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。
- 更改了 `