Let's try something a bit fancier. Add a menu choice to the Welcome form's menu named mnuFilesFileCopier. Set its Text to File Copier. The event handler for that menu choice will open the frmFilesCopier form that you'll create to copy files from a group of directories selected by the user to a single target directory or device, such as a floppy or backup hard drive. Although you won't implement every possible feature, you can imagine programming this form so that you can mark dozens of files and have them copied to multiple disks. Begin by creating the frmFilesCopier form, then extending it to a size of 570,740. Next, drag on three labels, a text box, two tree view controls, four buttons, and a checkbox, as shown in Figure 3-15. Figure 3-15. File Copier designDrag a StatusStrip on to the form at the bottom. Click on the status strip's drop down (on the form) and chose StatusLabel. Set the label's name to lblStatus and set its Text to Ready. You want checkboxes next to the directories and files in the source selection window but not in the target (where only one directory will be chosen). Set the CheckBoxes property on tvwSource to TRue, and on tvwTarget to false. Once you've done this, double-click the Cancel button to create its event handler. The entire implementation for this event handler is to close the form without taking further action, as shown in Example 3-21. Example 3-21. Cancel button Click event handler Private Sub btnCancel_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnCancel.Click Me.Close( ) End Sub 3.5.1. Populating the TreeView ControlsThe two treeView controls work identically, except that the left control, tvwSource, lists the directories and files, whereas the right control, tvwTarget, lists only directories. Also, although tvwSource will allow multiselect, which is the default for treeView controls, you will enforce single selection for tvwTarget. Before you begin, please add these three Imports statements to the top of your code file: Imports System.Collections.Generic Imports System.Collections Imports System.IO Factor the common code for both treeView controls into a shared method FillDirectoryTree , passing in the target tree view and a flag indicating whether to get the files, as shown in Example 3-22. Example 3-22. FillDirectoryTree helper methodPrivate Sub FillDirectoryTree( _ ByVal tvw As TreeView, _ ByVal getFiles As Boolean) End Sub You'll call this method from the Form's Load event handler, once for each of the two controls, as shown in Example 3-23. Example 3-23. FilesCopier form Load event handler Private Sub frmFilesCopier_Load( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load Me.Cursor = Cursors.WaitCursor Me.FillDirectoryTree(Me.tvwSource, True) Me.FillDirectoryTree(Me.tvwTarget, False) Me.Cursor = Cursors.Default End Sub
3.5.1.1. TreeNode objectsThe TReeView control has a property, Nodes, which gets a TReeNodeCollection object. The treeNodeCollection is a collection of treeNode objects, each of which represents a node in the tree. The first thing you'll do in FillDirectoryTree is empty that collection: tvw.Nodes.Clear( ) You are ready to fill the treeView's Nodes collection by recursing through the directories of all the drives. You'll implement a method called GetSubDirectoryNodes that does exactly that.
3.5.2. Displaying the DirectoriesBefore calling GetSubDirectoryNodes, FillDirectoryTree needs to get all the logical drives on the system. To do so, call a shared method of the Environment object, GetLogicalDrives. The Environment class provides information about and access to the current platform environment. You can use the Environment object to get the Figure 3-16. Recursionmachine name, OS version, system directory, and so forth, from the computer on which you are running your program. Dim strDrives As String( ) = Environment.GetLogicalDrives( ) GetLogicalDrives returns an array of strings, each of which represents the root directory of one of the logical drives. You will iterate over that collection, adding nodes to the treeView control as you go. For Each rootDirectoryName As String In strDrives You process each drive within the For Each loop. The very first thing you need to determine is whether the drive is ready. One hack for doing that is to get the list of top-level directories from the drive by calling GeTDirectories on a DirectoryInfo object you create for the root directory, like this: Try Dim dir As DirectoryInfo = New DirectoryInfo(rootDirectoryName) dir.GetDirectories( ) The DirectoryInfo class exposes instance methods for creating, moving, and enumerating through directories, their files, and their subdirectories. The Getdirectories method throws an exception if the drive is not ready (e.g., the A: drive does not have a floppy in it). Your goal here is just to skip over those drives; you don't actually care about the directories returned. Wrap the call in a TRy block and take no action in the catch block. The effect is that if an exception is thrown, the drive is skipped. Continuing in the TRy block (if you're still there, the drive is ready), create a TReeNode to hold the root directory of the drive and add that node to the TReeView control, like this: Dim ndRoot As TreeNode = New TreeNode(rootDirectoryName) tvw.Nodes.Add(ndRoot) To get the plus signs right in the TReeView, you must find at least two levels of directories (so the TReeView knows which directories have subdirectories and can write the plus sign next to them). You do not want to recurse through all the subdirectories, however, because that would be too slow. The job of the GetSubDirectoryNodes method is to recurse two levels deep, as shown schematically in Figure 3-16. You pass it:
Here's the code for doing these steps: If (getFiles = True) Then GetSubDirectoryNodes(ndRoot, ndRoot.Text, True, 1) Else GetSubDirectoryNodes(ndRoot, ndRoot.Text, False, 1) End If You will see why you need to pass in ndRoot.Text when you recurse back into GetSubDirectoryNodes. 3.5.2.1. Recursing through the subdirectoriesGetSubDirectoryNodes begins by once again calling Getdirectories, this time stashing away the resulting array of DirectoryInfo objects: Private Sub GetSubDirectoryNodes( _ ByVal parentNode As TreeNode, _ ByVal fullName As String, _ ByVal getFileNames As Boolean, _ ByVal level As Int32) Dim dir As DirectoryInfo = New DirectoryInfo(fullName) Notice that the node passed in is named parentNode. The current level of nodes will be considered children to the node passed in. This is how you map the directory structure to the hierarchy of the tree view. Iterate over each subdirectory within a try block (forbidden files and directories will throw an exception that you can safely ignore). Here's some code for doing that: Try Dim dirSubs As DirectoryInfo( ) = dir.GetDirectories( ) For Each dirsub As DirectoryInfo In dirSubs ''... Catch ex As Exception ' ignore exceptions End Try Create a treeNode with the directory name and add it to the Nodes collection of the node passed in to the method (parentNode), like this: Dim subNode As TreeNode = New TreeNode(dirsub.Name) parentNode.Nodes.Add(subNode) Now you check the current level (passed in by the calling method) against a constant defined for the class: Private Const MaxLevel As Integer = 2 so as to recurse only two levels deep: If level < MaxLevel Then 'recursion GetSubDirectoryNodes( _ subNode, _ dirsub.FullName, _ getFileNames, _ level + 1) End If You pass in the node you just created as the new parent, the full path as the full name of the parent, and the flag you received (getFileNames), along with one greater than the current level (thus, if you started at level 1, this next call will set the level to 2).
3.5.2.2. Getting the files in the directoryOnce you've recursed through the subdirectories, it's time to get the files for the directory if the getFileNames flag is true. To do so, call the GetFiles method on the DirectoryInfo object. An array of FileInfo objects is returned: If getFileNames = True Then Dim files As FileInfo( ) = dir.GetFiles( ) The FileInfo class provides instance methods for manipulating files. You can now iterate over this collection, accessing the Name property of the FileInfo object and passing that name to the constructor of a TReeNode, which you then add to the parent node's Nodes collection (thus creating a child node). There is no recursion this time because files do not have subdirectories: For Each file As FileInfo In files Dim fileNode As TreeNode = New TreeNode(file.Name) parentNode.Nodes.Add(fileNode) Next That's all it takes to fill the two tree views. Run the program and see how it works so far.
3.5.3. Handling TreeView EventsYou must handle a number of events for this page to work properly. For example, the user might click Cancel, Copy, Clear, or Delete. She might click one of the checkboxes in the left treeView, one of the nodes in the right treeView, or one of the plus signs in either view. Let's consider the clicks on the TReeViews first, as they are the most interesting, and potentially the most challenging. 3.5.3.1. Clicking the source TreeViewThere are two TReeView objects , each with its own event handlers. Consider the source treeView object first. The user checks the files and directories he wants to copy from. Each time the user clicks the checkbox indicating a file or directory, a number of events are raised. The event you must handle is AfterCheck . Your implementation of AfterCheck will delegate the work to a recursive method named SetCheck that you'll also write. The SetCheck method will recursively set the check mark for all the contained folders. To add the AfterCheck event, select the tvwSource control, click the Events icon in the Properties window, then double-click AfterCheck. This will add the event, wire it, and place you in the code editor where you can add the body of the method, shown in Example 3-24. Example 3-24. AfterCheck event handlerPrivate Sub tvwSource_AfterCheck( _ ByVal sender As System.Object, _ ByVal e As System.Windows.Forms.TreeViewEventArgs) _ Handles tvwSource.AfterCheck SetCheck (e.Node, e.Node.Checked) End Sub The event handler passes in the sender object and an object of type treeViewEventArgs. It turns out that you can get the node from this treeViewEventArgs object (e). Call SetCheck, passing in the node and its checked state. Each node has a Nodes property, which gets a TReeNodeCollection containing all the subnodes. Your SetCheck method recurses through the current node's Nodes collection, setting each subnode's check mark to match that of the node that was checked. In other words, when you check a directory, all its files and subdirectories are checked, recursively, all the way down. For each treeNode in the Nodes collection, set the checked property to the Boolean value passed in. A node is a leaf if its own Nodes collection has a count of zero; if the current node is not a leaf, recurse. Code for the SetCheck method is shown in Example 3-25. Example 3-25. SetCheck methodPrivate Sub SetCheck( _ ByVal node As TreeNode, _ ByVal check As Boolean) For Each n As TreeNode In node.Nodes n.Checked = check If n.Nodes.Count <> 0 Then SetCheck(n, check) End If Next End Sub This propagates the check mark (or clears the check mark) down through the entire structure. In this way, the user can indicate that he wants to select all the files in all the subdirectories by clicking a single directory. 3.5.3.2. Expanding a directoryEach time you click on a plus sign next to a directory in the source (or in the target) you want to expand that directory. To do so, you'll need an event handler for the BeforeExpand event. Since the event handlers will be identical for both the source and the target tree views, you'll create a shared event handler (assigning the same event handler to both), as shown in Example 3-26. Example 3-26. BeforeExpand event handler BeforeExpand event handler Private Sub tvwExpand( _ ByVal sender As System.Object, _ ByVal e As System.Windows.Forms.TreeViewCancelEventArgs) _ Handles tvwSource.BeforeExpand, tvwTarget.BeforeExpand Dim tvw As TreeView = CType(sender, TreeView) Dim getFiles As Boolean = (tvw.Name = "tvwSource") Dim currentNode As TreeNode = e.Node Dim fullName As String = currentNode.FullPath currentNode.Nodes.Clear( ) GetSubDirectoryNodes(currentNode, fullName, getFiles, 1) End Sub
The first line of tvwExpand casts sender from System.Object to treeView, which is safe since you know that only a treeView can trigger this event. You must determine whether you want to get the files in the directory you are opening. You want to get the files only if the name of the TReeView that triggered the event is tvwSource. You determine which node's plus mark was checked by getting the Node property from the TReeViewCancelEventArgs that is passed in as the second argument. Dim currentNode As TreeNode = e.Node Once you have the current node, you get its full path name (which you will need as a parameter to GetSubDirectoryNodes). You then clear its collection of subnodes; you are going to refill that collection by calling GetSubDirectoryNodes. currentNode.Nodes.Clear( ) Why do you clear the subnodes and then refill them? Because this time you will go another level deep so that the subnodes know if they, in turn, have subnodes, and thus will know if they should draw a plus mark next to their subdirectories. 3.5.3.3. Clicking the target TreeViewThe second event handler for the target TReeView (in addition to BeforeExpand) is somewhat trickier. The event itself is AfterSelect. (Remember that the target TReeView does not have checkboxes.) This time, you want to take the one directory chosen and put its full path into the text box at the upper-left corner of the form. To do so, you must work your way up through the nodes, finding the name of each parent directory and building the full path. An event handler for AfterSelect that does all this is shown in Example 3-27. Example 3-27. AfterSelect event handler AfterSelect event handlerPrivate Sub tvwTarget_AfterSelect( _ ByVal sender As System.Object, _ ByVal e As System.Windows.Forms.TreeViewEventArgs) _ Handles tvwTarget.AfterSelect Dim theFullPath As String = GetParentString (e.Node) (You'll see GetParentString in just a moment.) Once you have the full path, you must lop off the backslash (if any) on the end, and then you can fill the text box, like this: If theFullPath.EndsWith("\") Then theFullPath = theFullPath.Substring(0, theFullPath.Length - 1) End If Me.txtTarget.Text = theFullPath The GetParentString method takes a node and returns a string with the full path. To do so, it recurses upward through the path, adding the backslash after any node that is not a leaf, as shown in Example 3-28. Example 3-28. GetParentString methodPrivate Function GetParentString(ByVal node As TreeNode) As String If node.Parent Is Nothing Then Return node.Text Else Dim endString As String = String.Empty If node.Nodes.Count <> 0 Then endString = "\" Return GetParentString(node.Parent) + node.Text + endString End If End Function The recursion stops when there is no parent; that is, when you hit the root directory. 3.5.3.4. Handling the Clear button eventGiven the SetCheck method developed earlier, handling the Clear button's Click event is trivial, as shown in Example 3-29. Example 3-29. Clear button Click event handlerPrivate Sub btnClear_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles btnClear.Click For Each node As TreeNode In tvwSource.Nodes SetCheck(node, False) Next End Sub Just call the SetCheck method on the root nodes and tell them to recursively uncheck all their contained nodes. 3.5.4. Implementing the Copy Button EventNow that you can check the files and pick the target directory, you're ready to handle the Copy button's Click event. The very first thing you need to do is to get a list of which files were selected. This will be represented as a collection of FileInfo objects. Delegate responsibility for filling the list to a method called GetFileList as the first step executed by the event handler: Private Sub btnCopy_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) _ Handles btnCopy.Click Dim fileList As List(Of FileInfo) = GetFileList( ) Let's examine the GetFileList method before returning to the event handler. 3.5.4.1. Getting the selected filesStart by instantiating a new List(Of string) object to hold the strings representing the names of all the files selected: Private Function GetFileList( ) As List(Of FileInfo) Dim fileNames As List(Of String) = New List(Of String) To get the selected filenames, you can walk through the source treeView control: For Each theNode As TreeNode In tvwSource.Nodes GetCheckedFiles (theNode, fileNames) Next To see how this works, look at the GetCheckedFiles method, shown in Example 3-30. This method is pretty simple: it examines the node it was handed. If that node has no children, it is a leaf. If that leaf is checked, get the full path (by calling GetParentString on the node) and add it to the List(Of String) passed in as a parameter. Example 3-30. GetCheckedFiles method Private Sub GetCheckedFiles( _ ByVal node As TreeNode, _ ByVal fileNames As List(Of String)) If node.Nodes.Count = 0 Then If node.Checked Then fileNames.Add(GetParentString(node)) End If Else For Each n As TreeNode In node.Nodes GetCheckedFiles(n, fileNames) Next End If End Sub Notice that if the node is not a leaf, you recurse down the tree, finding the child nodes. This will return the List filled with all the filenames. Back in GetFileList, create a second List, this time to hold the actual FileInfo objects: Dim fileList As List(Of FileInfo) = New List(Of FileInfo) Notice the use of type-safe List objects to ensure that the compiler will flag any objects added to the collection that are not of type FileInfo. You can now iterate through the filenames in fileNames, picking out each name and instantiating a FileInfo object with it. You can detect if it is a file or a directory by calling the Exists property, which will return False if the File object you created is actually a directory. If it is a File, you can add it to the new List(Of FileInfo), as shown in the following snippet: For Each fileName As String In fileNames Dim file As FileInfo = New FileInfo(fileName) If file.Exists Then fileList.Add(file) End If Next That done, you can return fileList to the calling method: Return fileList The calling method was btnCopy_Click. Remember, you went off to GetFileList in the first line of the event handler! At this point, you've returned with a list of FileInfo objects, each representing a file selected in the source TReeView. You can now iterate through the list, copying the files and updating the UI, as shown in the completed Click event handler in Example 3-31. Example 3-31. Copy button Click event handlerPrivate Sub btnCopy_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnCopy.Click Dim fileList As List(Of FileInfo) = GetFileList( ) For Each file As FileInfo In fileList Try lblStatus.Text = "Copying " + txtTarget.Text + "\" + file.Name + "..." Application.DoEvents( ) file.CopyTo(txtTarget.Text + "\" + file.Name, cbOverwrite.Checked) Catch ex As Exception MessageBox.Show(ex.Message) End Try Next lblStatus.Text = "Done" Application.DoEvents( ) End Sub As you go, write the progress to the lblStatus label and call Application.DoEvents to give the UI an opportunity to redraw. Then call CopyTo on the file, passing in the target directory obtained from the text field, and a Boolean flag indicating whether the file should be overwritten if it already exists. You'll notice that the flag you pass in is the value of the cbOverwrite checkbox. The Checked property evaluates to TRue if the checkbox is checked and False if not. The copy is wrapped in a try block because you can anticipate any number of things going wrong when copying files. For now, handle all exceptions by popping up a dialog box with the error; you might want to take corrective action in a commercial application. That's it; you've implemented file copying! 3.5.5. Handling the Delete Button EventThe code to handle the delete event is even simpler. The very first thing you do is make sure the user really wants to delete the files. You can use the MessageBox static Show method, passing in the message you want to display, the title "Delete Files" as a string, and flags:
When the user chooses Yes or No, the result is passed back as a System.Windows.Forms.DialogResult enumerated value. You can test this value to see if the user selected Yes, as shown in the following code snippet: Private Sub btnDelete_Click( _ ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles btnDelete.Click Dim result As DialogResult = _ MessageBox.Show( _ "Are you quite sure?", _ "Delete Files", _ MessageBoxButtons.YesNo, _ MessageBoxIcon.Exclamation, _ MessageBoxDefaultButton.Button2) If result = Windows.Forms.DialogResult.Yes Then Dim fileNames As List(Of FileInfo) = GetFileList( ) For Each file As FileInfo In fileNames Try lblStatus.Text = "Deleting " + txtTarget.Text + "\" + file.Name + "..." Application.DoEvents( ) file.Delete( ) Catch ex As Exception MessageBox.Show(ex.Message) End Try Next lblStatus.Text = "Done." Application.DoEvents( ) End If End Sub Assuming the value you get back from the DialogResult is Yes, you get the list of fileNames and iterate through it, deleting each as you go: The final working version of FilesCopier window is shown in Figure 3-17. Figure 3-17. Working version of the FilesCopier |