使用事件:Filerenamer II 项目
这是一个用于批量重命名文件的实用程序,它演示了如何使用和创建事件。
引言
这个项目是为了批量重命名文件而创建的,源于我试图手动重命名大量文件时的沮丧。它演示了几个方便的方法,例如如何枚举驱动器和文件,然后如何批量重命名文件。该项目还演示了如何在程序中创建、生成和处理事件,包括用于磁盘驱动器和文件的内置实用程序事件。
为了使驱动器、文件夹和文件的显示保持最新,我连接了系统事件,并创建了一些自己的事件来向程序发出信号,要求重新扫描文件名、文件夹和驱动器。有了这些事件,程序就可以实时保持驱动器和文件的显示更新。我将解释这些事件类的创建和用法,以及它们如何“知道”发生了更改。
背景
如今,我和大多数人一样使用数码相机。相机在内存芯片上命名文件时会使用然后重复使用相同的普通通用文件名——P1010078.JPG、P1010078.JPG、P1010078.JPG 等等。当您从另一次假期或场合下载下一组照片时,您会遇到名称冲突,而且您根本不知道诸如这样的名称与照片有什么关系。
我曾尝试手动重命名有此问题的照片和文件,但当文件数量达到约五十个时,这是一件非常烦人的事。这种烦恼常常是创造解决方案的动力。这是我通过一个易于使用的 C# Windows 窗体应用程序界面来解决这个问题的方法。
该项目使用了以下库
- 系统
- System.Collections.Generic
- System.IO
- System.Management
- System.Windows.Forms
目录
应用程序代码
该项目包含以下类/窗体
frm_Main
- 包含启动和用户界面代码Actions
- 用于更改文件名的函数GettingDrivesNotice
- 在扫描驱动器时使用的弹出窗口DiskChangeAlerter
- 用于检测 USB 驱动器连接/断开的事件委托和处理程序代码FileChangeWatcher
- 用于检测文件更改的事件委托和处理程序代码
代码中第一个不寻常之处是程序的启动。在 frm_Main()
构造函数运行后,我在 frm_Main_Shown()
事件处理程序中设置了一个短暂的超时。这是为了防止目录和驱动器列表显示控件尚未完全渲染完毕时出现的竞态条件。关于这一点,似乎有两种观点。一类人喜欢启动新线程并使用 Thread.Sleep() 来创建延迟。在此示例中,我选择了使用一个 timer 对象并使用 timer_Tick()
事件处理程序来执行我接下来需要执行的操作。
private void frm_Main_Shown(object sender, EventArgs e)
{
p.frmParent = this;
x.frmParent = this;
chkUSBdrives.Checked = Properties.Settings.Default.DisplayUSBdrives;
frmNotice.Show(this);
tTime = new Timer();
tTime.Interval = 100;
tTime.Tick += new EventHandler(tTime_Tick);
tTime.Start();
}
void tTime_Tick(object sender, EventArgs e)
{
try
{
tTime.Stop();
tTime.Tick -= new EventHandler(tTime_Tick);
tTime.Dispose();
tTime = null;
DAlerter = new DiskChangeAlerter(); // must come before getdrives()
GetDrives();
frmNotice.Hide();
if (chkUSBdrives.Checked)
DAlerter.DiskChangeEvent += new EventChangedAlertHandler(DAlerter_DiskChangeEvent);
FileWatcher = new FileChangeWatcher();
FileWatcher.FolderPath = sCurrentDirectoryPath;
FileWatcher.eFileChanged += new FileChangedEventHandler(FileWatcher_eFileChanged);
FileWatcher.Start();
}
catch (Exception te)
{
Debug.WriteLine("tTime_Tick(): " + te);
}
}
GetDrives()
方法使用 System.IO.Directory
对象枚举驱动器,并将它们列在组合框中,同时将它们添加到字典:LastDirListByDrive
。System.IO.DriveInfo
对象用于收集有关驱动器的有用信息,例如驱动器类型、名称和卷标,这些信息会显示在组合框驱动器列表中。
GetDrives()
接着调用 GetFolders()
方法将所有文件夹枚举到 treeview 对象中。然后调用 DrillDownToCurrentDir()
方法将树展开到当前目录。
最后,它调用 GetFiles()
,该方法将当前目录的内容列入 System.Windows.Forms.DataGridView
对象。
从这一点开始,用户操作会触发主窗体中的方法。如果您选择不同的驱动器,则会调用 GetFolders()
和 GetFiles()
序列来更新显示。新单击的文件夹将调用 GetFiles()
方法。
用户然后选择他们想要的重命名类型,并通过单击“预览”按钮来查看预期的结果,而无需实际修改文件名。这是一个例子
单击“执行”按钮会执行相应操作。如果您认为文件列表已过时,则可以手动刷新文件列表,并且还有用于全选和全不选的按钮。我在 DataGridView_CellContentClick()
事件处理程序中添加了一段代码,以便更容易地选择多个文件。当您使用鼠标或空格键“单击”复选框时,处理程序会将焦点移动到下面的单元格。这样,您就可以按住空格键来选择任意数量的文件。
private void DataGridView1_CellContentClick(object sender, DataGridViewCellEventArgs e)
{
if (e.ColumnIndex != 0) return;
int iIndex = e.RowIndex + 1;
if (iIndex == DataGridView1.Rows.Count)
iIndex = 0;
DataGridView1[0, iIndex].Selected = true;
}
也许您已经注意到,我们刚刚在示例代码中使用了/处理了多个事件。现在,我们将深入了解如何在程序中使用和创建事件,并提供两个实际的用法示例。
链接
事件
现在让我们谈谈事件。总的来说,事件及其消费者需要以下部分
- 一个参数类
- 一个委托
- 一个事件类型变量
- 一个事件处理程序方法
您会在代码或引发事件的类的代码中的某个地方找到所有这些。如果缺少任何一个,您将无法引发和捕获事件并进行处理。
事件到底是什么
表面上看,事件就像有人挥舞旗帜,传递一些信息,然后一个远程对象抓住旗帜并执行其相应的处理程序。嗯,事实并非如此。在 .NET 世界中,事件就像在其声明范围内(即局部或全局)的一个抽象方法。它本身不做任何事情。要引发一个事件,一个对象会像调用任何其他方法一样调用这个方法。
侦听器对象通过将其处理程序方法的地址列在事件方法中来订阅这个抽象方法,这样就存在了一个实际的处理程序方法,当事件方法被调用时就可以执行。将其视为重写抽象方法,提供实际的实现,但这些方法可以是全局的,而不限于包含在一个类或其子类中。
考虑到这一点,如果您没有侦听器重写抽象事件方法,并且一个对象尝试引发事件,就像一个实际的抽象方法一样,您会收到一个空引用错误和一个异常。我将向您展示一种非常简单的方法来处理这个问题。
参数类
这是一个参数类声明的示例
public class DriveChangedArgs
{
public string DriveLetter = "";
public string InterfaceType = "";
public DriveChangeType ChangeType = DriveChangeType.Other;
}
您不必声明一个唯一的参数类,但实际上有必要使其易于区分一个事件和另一个事件。因为“事件”实际上是方法,所以您需要一个唯一的签名。您可以将所有事件命名为“EventHandler”,只要您的参数不同即可。在 C# 和 C++ 中,整个头包括参数列表都是签名的一部分。如果您创建了一个唯一的参数类来作为事件的参数传递,那么事件方法将是唯一的。
它只需要声明参数变量的代码,所以编程时间开销不大。当然,如果您不通过事件传递任何信息,您可以始终使用 System.EventArgs.Empty
内置类并在其他声明中使用它。
参数类必须能够被所有将引发事件或处理事件的对象看到。通常这意味着一个全局声明——在命名空间内,但在任何类之外。如果您的事件和处理程序都在一个类对象内,例如一个 Windows 窗体对象,那么您可以将声明放在该类对象内部。
Enums
您会注意到此声明中的 DriveChangeType
。使用枚举来限制返回值到您期望的值始终是个好主意。否则,用户会构造各种各样的值,您将遇到处理错误。这是在此实例中创建的枚举
public enum DriveChangeType { Create, Remove, MediaChange, Other };
委托
委托声明是事件处理程序方法的声明原型,用于声明将被引发的事件。它类似于抽象方法声明,它本身不做任何事情,但会模式化方法并为其提供一个签名。这是一个例子
public delegate void EventChangedAlertHandler(DriveChangedArgs e);
必须声明委托,以便所有需要引发和处理事件的类对象都能看到它。通常,您会将它们声明为全局对象,在命名空间内但任何类之外。再次,如果整个事件和处理程序代码都发生在一个单独的类中,您可以将声明放在类内部。
引发事件
现在是最精彩的部分!首先,我们必须引发一个事件,以便有人可以捕获它。event
在将要引发它的类中声明。如果您的侦听器在声明“event”的类之外的代码中,那么您需要将其设为“public
”,以便它可见。如果您希望由不包含事件所在类的实例化副本的对象来处理事件,那么该事件将需要是“static
”。这是此代码中事件的声明
public event EventChangedAlertHandler DiskChangeEvent;
请注意,它使用了先前声明的委托的名称。这一点很重要。您必须为您的事件声明一个委托,否则您无法引发它。(有道理,是吧?)因此,我们声明了一个类型为“event”的对象,并将委托名称作为类型传递,然后为这个事件变量命名——DiskChangeEvent
。
现在,我们该如何实际引发事件呢?这是一个例子
if (DiskChangeEvent != null)
DiskChangeEvent(a1);
传递给事件的参数 a1
只是 DriveChangedArgs
类的一个实例。
DriveChangedArgs a1 = new DriveChangedArgs();
事情变得有点奇怪了。您声明了一个事件类型并将其命名为 DiskChangeEvent
。那么它怎么会为空呢?嗯,就像抽象方法一样,委托没有实现。对于事件,您通过让侦听器将其实现附加到事件来实现这一点,这就像重写方法一样。“是否为空”的检查就是这样做的。有侦听器吗?如果没有,而您继续引发事件,您将收到一个异常错误。您的事件引发代码应始终包含此“是否为空”的陷阱。请记住,事件的生命周期开始时就像一个抽象方法。直到您附加实际方法,它才会为空。
事件处理程序
好了,现在您已经引发了一个事件,我们需要捕获并处理它。可能有多个对象侦听事件,然后执行某些操作。此外,事件通常不在与捕获事件的对象相同的线程上发生。我们现在将讨论这些问题。
首先,我们需要创建一个处理程序方法,然后才能将其附加到事件。它必须遵循与委托相同的签名,但是它将有一个唯一的名称——不是委托的名称,而是签名。这是我们的例子
void DAlerter_DiskChangeEvent(DriveChangedArgs e)
{
.
.
.
}
稍后我们将讨论它的作用。但首先,我们必须让事件知道那里有一个侦听器,方法是将我们的侦听器附加到事件。事件由类对象引发。在我们的代码中的某个地方,我们必须至少实例化一个事件引发类对象,以便它能够根据需要引发事件。在我们的示例程序中,我们在主窗体程序启动时这样做的。这是代码
DAlerter = new DiskChangeAlerter();
DiskChangeAlerter
类包含所有事件引发代码以及 event
对象。现在我们已经实例化了一个类对象,我们将把我们的处理程序附加到它的事件上。这是要执行的代码
DAlerter.DiskChangeEvent += new EventChangedAlertHandler(DAlerter_DiskChangeEvent);
记住,我们的事件引发类将其事件声明为“public”。这样,任何持有事件引发类实例的对象都可以“看到”该事件。然后我们使用“+=”运算符将我们的处理程序添加到引发者的侦听器列表中。但是,如果您希望所有侦听器“看到”相同的事件,那么您应该考虑将事件变量设为“static”。
注意!
如果您实例化事件引发类的独立实例,它们将引发事件的不同实例,因此侦听器将听到独立的事件,而不是“事件”。如果您希望所有侦听器看到相同的事件,请在引发类中使用静态事件,并让所有侦听器附加到它。
跨线程事件处理程序
由于 .Net 中类实例化方式的原因,不同的类对象经常在不同的线程上运行。如果是这种情况,那么您就不能修改另一个线程上的对象的值。您的处理程序方法实际上是**从**事件引发类调用并运行的。如果您附加事件处理程序,并在事件被引发时尝试修改像文本框这样的显示对象的值,您会很容易看到这一点。如果您收到线程安全错误,那么您就遇到了棘手的跨线程安全问题。
并非所有希望都渺茫。有一个非常简单的代码技巧可以解决这个问题。这是一个在事件处理程序中的示例
void DAlerter_DiskChangeEvent(DriveChangedArgs e)
{
//MessageBox.Show(e.DriveLetter + ", " + e.InterfaceType + ", " + e.ChangeType);
try
{
if (this.InvokeRequired)
{
// this is what we do if the event comes in from another thread
MethodInvoker del = delegate { DAlerter_DiskChangeEvent(e); };
this.Invoke(del);
return;
}
else
{
// this is what we do as a result of the invoke() or if it's
// on the same thread as the handler
.
-some code here-
.
}
}
catch (Exception de)
{
frmNotice.Hide();
Application.DoEvents();
MessageBox.Show("rescan error: " + de);
}
if (frmNotice != null)
{
frmNotice.Hide();
Application.DoEvents();
}
}
正如您所见,我们实际上使用 'this.invoke(del)
' 命令启动了对处理程序方法的本地调用,并将参数传递给它。现在它在本地线程中运行,因此我们可以修改任何我们想要的内容,而不会出现可怕的跨线程异常。这有点笨拙,但它有效。
评估
好了,现在您已经是事件专家了。只需记住,在 .Net 世界中,事件实际上就像一个抽象方法。它声明了一个原型但没有实现。直到您将实现附加到事件,否则它将返回一个错误,因此您需要在事件引发代码中捕获这一点。
事件由四部分组成:参数、委托、事件、处理程序方法。在应用程序的某个地方,您会找到所有这四项。
链接
磁盘更改事件类
此类及其事件的目的是通知应用程序磁盘驱动器已被挂载或卸载。传统上,这是针对 USB 内存棒,但任何进出的驱动器都会触发此事件。
检测驱动器挂载/卸载
这有点复杂,但我的代码会为您指明方向。我将解释,同时尽量不深入细节。像大多数程序员一样,我只是想检测事件然后使用它。不幸的是,Windows 中没有什么是如此直接的。我们将监视系统管理事件,然后挑出磁盘驱动器事件进行检查。所以我们需要实例化 ManagementEventWatcher
类,然后将我们的事件处理程序附加到我们想要侦听的事件上。
ManagementEventWatcher Watcher = new ManagementEventWatcher();
接下来,我们在 DiskChangeAlerter
类的构造函数中进行设置
public DiskChangeAlerter()
{
WqlEventQuery q1 = new WqlEventQuery("SELECT * FROM __InstanceOperationEvent WITHIN 1 "
+ "WHERE TargetInstance ISA 'Win32_DiskDrive' Or TargetInstance isa 'Win32_MappedLogicalDisk'");
Watcher.Query = q1;
Watcher.EventArrived += new EventArrivedEventHandler(Watcher_EventArrived);
Watcher.Start();
}
查询告诉监视器我们感兴趣的内容,然后我们通过(名称)告诉监视器我们的事件处理程序在哪里。我们的处理程序当然必须匹配此事件的委托签名。
我不会在此处发布所有相关代码,但如果您下载源代码,您可以沿着事件解构的路径进行跟踪,以查看发生了什么以及涉及哪个驱动器号。完成所有这些的确切代码有点晦涩难懂,但网上还有其他示例,我的示例相当直接。一旦我们收到了有驱动器更改的事件,并且我们深入挖掘并获取了所需信息,我们就将其放入我们的 args 创建中,并引发我们的事件——DiskChangeEvent(a1)
。
我们的应用程序具有位于 DiskChangeAlerter
类代码之上的命名空间中的以下代码(因此是全局范围的)
public enum DriveChangeType { Create, Remove, MediaChange, Other };
public class DriveChangedArgs
{
public string DriveLetter = "";
public string InterfaceType = "";
public DriveChangeType ChangeType = DriveChangeType.Other;
}
public delegate void EventChangedAlertHandler(DriveChangedArgs e);
在类内部,我们有这些类范围的变量
ManagementEventWatcher Watcher = new ManagementEventWatcher();
public event EventChangedAlertHandler DiskChangeEvent;
一旦我们在管理事件中用我们的处理程序解构了信息,我们就初始化 DriveChangedArgs
变量 a1 的值,然后调用事件方法
if (DiskChangeEvent != null)
DiskChangeEvent(a1);
回到我们的应用程序类,在实例化了这个 DiskChangeAlerter
类的副本后,我们将其 EventChangedAlertHandler
事件 DiskChangeEvent
附加了一个处理程序
if (chkUSBdrives.Checked)
DAlerter.DiskChangeEvent += new EventChangedAlertHandler(DAlerter_DiskChangeEvent);
当 ManagementWatcher
引发其更改事件,然后 DiskChangeAlerter
类引发其更改事件时,我们需要清除用户表示对象,然后调用方法来重新列出驱动器、文件夹和文件。您会注意到我们加入了处理跨线程问题的代码。
void DAlerter_DiskChangeEvent(DriveChangedArgs e)
{
//MessageBox.Show(e.DriveLetter + ", " + e.InterfaceType + ", " + e.ChangeType);
try
{
if (this.InvokeRequired)
{
// this is what we do if the event comes in from another thread
MethodInvoker del = delegate { DAlerter_DiskChangeEvent(e); };
this.Invoke(del);
return;
}
else
{
// this is what we do as a result of the invoke() or if its
// on the same thread as the handler
frmNotice.Show();
Application.DoEvents();
bDriveRescan = true;
cbDriveList.Text = "";
cbDriveList.Items.Clear();
tvFolderTree.Nodes.Clear();
DataGridView1.Rows.Clear();
GetDrives();
bDriveRescan = false;
}
}
catch (Exception de)
{
frmNotice.Hide();
Application.DoEvents();
MessageBox.Show("rescan error: " + de);
}
if (frmNotice != null)
{
frmNotice.Hide();
Application.DoEvents();
}
}
监视器的关键是 WqlEventQuery
。语法有点晦涩难懂。您可以在网上和 Visual Studio 帮助文件中查找有关 ManagementEventWatcher
类的信息,但真正的核心在于您创建的查询。如果您搜索 WqlEventQuery
,网上有很多很好的示例。
就是这样。您创建一个 ManagementEventWatcher
类实例并将其处理程序附加到它。您告诉监视器您对磁盘驱动器事件(或其他您喜欢的)感兴趣。当它引发事件时,您根据传递给您的事件参数来深入挖掘您需要的信息。在我的情况下,我创建了另一个类来包装它,从而隐藏了获取我真正需要的信息的晦涩代码。然后我引发自己的事件,并将此信息传递给应用程序,由应用程序的处理程序擦除旧的显示数据,获取最新的数据并进行显示。
链接
文件更改事件
让我们看看如何检测文件夹内容或文件信息更改,以便我们更新用户显示。有一个非常有用的内置类用于检测文件更改——System.IO.FileSystemWatcher
。它会告诉您文件是否被:创建、删除或重命名。在我们的例子中,我们需要知道这些,以便保持显示最新。
我们将该事件的引发者包装在我们自己的类中,并引发一个自定义事件,以隐藏捕获和过滤信息,并只发送我们需要知道的应用程序运行所需的信息。我的应用程序中的 FileChangeWatcher
类包含所有这些代码。这是我们自定义事件的全局声明
public class FileChangeEventArgs
{
public string FolderPath = "";
public string ChangeType = "";
}
public delegate void FileChangedEventHandler(object sender, FileChangeEventArgs e);
在我们的自定义引发者类内部,我们声明了 FileSystemWatcher
类的一个实例和我们 FileChangedEventHandler
委托的一个事件实例
FileSystemWatcher Watcher = new FileSystemWatcher();
public event FileChangedEventHandler eFileChanged;
在我们的类构造函数中,我们初始化了一些值并将我们的侦听器附加到 FileSystemWatcher
Watcher.Filter = "*.*";
Watcher.IncludeSubdirectories = true;
Watcher.NotifyFilter = NotifyFilters.DirectoryName | NotifyFilters.FileName;
Watcher.Created += new FileSystemEventHandler(Watcher_Created);
Watcher.Deleted += new FileSystemEventHandler(Watcher_Deleted);
Watcher.Renamed += new RenamedEventHandler(Watcher_Renamed);
从这一点开始,如果文件监视器引发事件,我们就会处理发生了什么,将有用的信息放入我们的参数类,然后执行我们的自定义事件。我不打算在此列出所有代码。您可以轻松地通过查看源代码文件来了解发生了什么。
回到应用程序类,我们处理自定义文件更改事件。它实际上所做的就是擦除当前文件显示,然后获取文件信息的最新副本并显示它。这是代码。请注意,我们使用了我们的跨线程事件处理程序代码来防止任何跨线程安全问题。
void FileWatcher_eFileChanged(object sender, FileChangeEventArgs e)
{
//Debug.WriteLine("!!** File Change **!!");
if (this.InvokeRequired)
{
MethodInvoker del = delegate { FileWatcher_eFileChanged(sender, e); };
try
{
this.Invoke(del);
}
catch { };
return;
}
else
GetFiles();
}
评估
有一个非常方便的内置文件更改监视器类——System.IO.FileSystemWatcher
。有了它,您可以实时收到任何给定目录/文件夹中文件变化的通知。我使用它来驱动一个自定义事件,以隐藏繁琐的代码,并简化主应用程序类需要处理的内容。自定义事件会过滤掉发生的事情,并发送一个为应用程序量身定制的简化参数集。应用程序进而利用此来触发文件显示的刷新。