For most programmers, development centers on the features (and quirks) of the Microsoft Windows operating system. In this chapter, we'll consider some of the most frequently asked questions about development with Microsoft Visual Basic .NET. You'll learn how to use Windows-specific resources such as environment variables, the registry, the Start menu, the clipboard, and Windows help. You'll also learn how to interact with other currently running Windows processes, handle operating system events, and deploy your application with a Windows Installer setup utility.
It's important to understand that the Microsoft .NET Framework does not attempt to replicate the entire Win32 API, only its core features. To create managed interfaces for every available API function would be a monumental (and counterproductive) undertaking. Most developers will find that .NET provides 99 percent of the most commonly required Windows functionality and makes it easier to use than ever before. However, in order to use some features you will need to delve back into the unmanaged world.
To create the recipes for this chapter, we'll need several different types of solutions, including:
You want to store and retrieve values in the Windows registry.
Use the Registry and RegistryKey classes from the Microsoft.Win32 namespace.
Unlike previous versions of Visual Basic, Visual Basic .NET provides unrestricted access to the Windows registry through the Registry and RegistryKey classes. Registry is the starting point for accessing the Windows registry: it provides shared fields that return RegistryKey objects for first-level registry paths (or registry base keys). The two most important fields are
Once you have a RegistryKey object, you can navigate down to deeper nested levels using a path-like syntax and then set and retrieve individual values. Commonly, an application will store settings in the subpath SoftwareCompanyName ProductName or SoftwareCompanyNameProductNameCategory under a registry base key. To retrieve a RegistryKey object for a nested key, you use the RegistryKey.OpenSubKey method. If you want to open a key in writable mode, you must supply an optional True parameter to the OpenSubKey method.
Dim Key As RegistryKey Key = Registry.LocalMachine.OpenSubKey("SoftwareMyCompanyMyApp", True)
To write a value, you use SetValue, and to retrieve a value, you use GetValue. Values are usually retrieved as strings.
Dim Value As String ' Retrieve the MyValueName value. Value = CType(Key.GetValue("MyValueName"), String) ' Save the value MyValueName. Key.SetValue("MyValueName", Value)
You can implement registry access in your application in several ways. You can retrieve all the values when the application starts and save them when it closes, or you can retrieve and save them just-in-time. The RegistryData class shown in the following example follows the latter approach. A reference to the appropriate RegistryKey is retrieved when the class is instantiated (and the key is created if needed). From that point on, property Get procedures wrap the code needed to retrieve registry values, and property Set procedures wrap the code needed to write to the registry.
Imports Microsoft.Win32 Public Class RegistryData Private Key As RegistryKey Private Const RegistryPath As String = "SoftwareTestCompanyTestApp" Public Property DefaultDocumentPath() As String Get ' If the key is not found, the application ' startup path is used as a default. Return CType(Key.GetValue("DefaultDocumentPath", _ Application.StartupPath), String) End Get Set(ByVal Value As String) Key.SetValue("DefaultDocumentPath", Value) End Set End Property Public Sub New() Key = Registry.CurrentUser.OpenSubKey(RegistryPath, True) If Key Is Nothing Then ' Key does not exist. Create it. Key = Registry.CurrentUser.CreateSubKey(RegistryPath) End If End Sub End Class
The following code demonstrates how you would use the RegistryData class to retrieve information for an Open dialog box in a Windows application:
Public Class RegistryTestForm Inherits System.Windows.Forms.Form ' This creates the key if needed. Public RegistryData As New RegistryData() Private Sub cmdTest_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdTest.Click Dim dlgOpen As New OpenFileDialog() ' Returns the registry value or default. dlgOpen.InitialDirectory = RegistryData.DefaultDocumentPath ' Show the Open dialog box with the default initial directory. dlgOpen.ShowDialog() ' Check if a filename was selected. If dlgOpen.FileName = "" Then ' Cancel was clicked. Do nothing. Else ' Store the directory where this file exists. RegistryData.DefaultDocumentPath = _ System.IO.Path.GetDirectoryName(dlgOpen.FileName) ' (You can now perform an application-specific task ' with the file.) End If End Sub End Class
You want to retrieve information from a Windows environment variable (for example, to find out the computer name, username, logon server, and so on).
Use the GetEnvironmentVariable or GetEnvironmentVariables methods of the System.Environment class.
The Windows operating system stores some commonly used information in environment variables. You can access this information by using the Environment.GetEnvironmentVariable method and supplying the name of the variable. The following code snippet uses this technique to retrieve the name of the current computer:
Dim ComputerName As String ComputerName = Environment.GetEnvironmentVariable("COMPUTERNAME")
Applications can define and set their own environment variables. (Typically, this step is performed by the installation program.) You can retrieve any environment variable in .NET code, provided that you know its name, using the GetEnvironmentVariable method. However, the Environment class doesn't provide any methods for setting environment variables. If you need to perform this task, you should use the Windows Script Host, which is described in recipe 10.5.
In addition, you can retrieve all the environment variables on the current computer using the GetEnvironmentVariables method. This technique is used in the following code to fill a ListView control (as shown in Figure 10-1).
Figure 10-1: A list of environment variables.
Dim Variables As IDictionary Variables = Environment.GetEnvironmentVariables() Dim Variable As System.Collections.DictionaryEntry For Each Variable In Variables Dim listItem As New ListViewItem(Variable.Key.ToString()) listItem.SubItems.Add(Variable.Value.ToString()) listSettings.Items.Add(listItem) Next
You want your code to react to a Windows system event, such as a modification of system or desktop settings.
Add an event handler to one of the shared events provided by the Microsoft.Win32.SystemEvents class.
The SystemEvents class provides references to several global system events, including:
These events are all shared events, which means that you can add an event handler without needing to create an instance of the SystemEvents class. Here's an example that attaches as event handler for the SessionEnding event:
AddHandler SystemEvents.SessionEnding, AddressOf SessionEnding
You should note that the event callbacks take place on a system thread, not on your application thread. Therefore, if you want to update the user interface or modify a shared variable in an event handler for a system event, you'll need to use the synchronization steps explained in Chapter 7 (for example, recipes 7.6 and 7.9). In addition, you shouldn't perform any time-consuming processing in the event handler so that you don't slow down other applications waiting for an event.
The following example handles the SessionEnding event and attempts to cancel the shutdown if it corresponds to a user logoff operation. To test this example properly, you must compile the program and run it outside of the Microsoft Visual Studio .NET development environment.
Public Class ShutdownTestForm Inherits System.Windows.Forms.Form Private Sub ShutdownTestForm_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load AddHandler Microsoft.Win32.SystemEvents.SessionEnding, _ AddressOf SessionEnding End Sub Private Sub SessionEnding(ByVal sender As Object, _ ByVal e As Microsoft.Win32.SessionEndingEventArgs) If e.Reason = Microsoft.Win32.SessionEndReasons.Logoff Then ' It was a user initiated shutdown. Try to cancel it. ' (There is no guarantee that this request will be honored, ' so you may want to respond to the SessionEnded event to ' perform last minute cleanup.) e.Cancel = True MessageBox.Show("Attempting to cancel the logoff operation.") Else MessageBox.Show("System is shutting down.") End If End Sub End Class
Note that if the system is shutting down, your application has a limited amount of time to end. If you don't click the OK button on the message box quickly enough, the application might be terminated forcibly.
You want to access shortcuts or other files on the desktop or the Start menu.
You can access the underlying Start menu and desktop directories directly by retrieving the corresponding environment variables.
.NET does not provide any classes for interacting with the desktop or Start menu. However, you can find the corresponding directories using Windows environment variables.
For example, you can retrieve the current user's profile directory from the environment variable USERPROFILE. This directory contains two important subdirectories: Desktop (which holds the files that are displayed on the current user's desktop) and Start Menu (which holds the user-specific shortcuts on the Start menu). You can also use the environment variable ALLUSERSPROFILE to retrieve the All Users profile directory, which has settings that apply to all users. The All Users directory includes a Start Menu directory with global shortcuts, where most applications are installed.
The following example uses both of these approaches to show the files on the desktop and some of the programs installed on the computer:
Public Module DesktopShortcutTest Public Sub Main() ' Get the desktop directory for the current user. Dim DesktopDir, StartMenuDir As String DesktopDir = Environment.GetEnvironmentVariable("USERPROFILE") & _ "Desktop" StartMenuDir = Environment.GetEnvironmentVariable("ALLUSERSPROFILE") _ & "Start Menu" ' Display the names of the files on the desktop. Console.WriteLine("These are the files on your desktop:") Dim Dir As New System.IO.DirectoryInfo(DesktopDir) Dim File As System.IO.FileInfo For Each File In Dir.GetFiles() Console.WriteLine(File.Name) Next ' Display the shortcuts groups in the first level of the ' all users Start menu (under the Programs group). Console.WriteLine("These are shortcut groups in your Programs menu:") Dir = New System.IO.DirectoryInfo(StartMenuDir & "Programs") Dim ShortcutGroup As System.IO.DirectoryInfo For Each ShortcutGroup In Dir.GetDirectories() Console.WriteLine(ShortcutGroup.Name) Next Console.ReadLine() End Sub End Module
This environment variable technique is useful for retrieving information from these special paths, but it won't help you create shortcuts because shortcuts are special file types that use a proprietary format. However, you can create shortcuts programmatically using the Windows Script Host (WSH) object, as described in recipe 10.5.
The user profile directory is typically a path such as C:Documents and Settings username, while the All Users profile is usually stored in a path such as C:Documents and SettingsAll Users.
You want to add a new shortcut to the desktop or the Start menu.
Use COM Interop to access the Windows Script Host component, which provides a WshShell.CreateShortcut method.
There are several approaches that you can take if you want to programmatically create a shortcut file. You can use an unmanaged call to a legacy API (such as the Visual Basic 6 setup toolkit DLL), create the file by hand (in which case you need an in-depth understanding of its proprietary format), or create a dedicated Windows Installer setup program. The easiest approach is to use the Windows Script Host component, which is included with the Windows operating system. You can interact with this COM component through COM Interop. All you need to do is add a reference, as shown in Figure 10-2.
Figure 10-2: Adding a reference to the Windows Script Host.
The Windows Script Host is built into Microsoft Windows 98, Windows Me, Windows 2000, Windows XP, and Windows Server 2003 through the file wshom.ocx (in the Windows System32 directory). You can also download the most recent version of the Windows Script Host (version 5.6) from http:// msdn.microsoft.com/scripting. The Windows Script Host can also be used to map network drives, connect to printers, retrieve and modify environment variables, and modify registry keys. See http://msdn.microsoft.com/library/en-us/script56/ htm/wsconwhatiswsh.asp for the complete documentation.
Creating a new shortcut with the Windows Script Host is quite easy. First, you create an instance of the WshShell object. You can then use the WshShell.SpecialFolders collection to retrieve a path to any one of the following folders:
Once you have the appropriate path, you can use the WshShell.CreateShortcut method to create an IWshShortcut object. You can then configure the shortcut by modifying the properties of the IWshShortcut object and invoke its Save method to store the final result.
Here's a full example that creates a shortcut to the Windows Notepad application:
Public Module CreateShortcutTest Public Sub Main() ' Create the Windows Script Host shell object. Dim WshShell As New IWshRuntimeLibrary.WshShell() Dim DesktopDir As String = _ CType(WshShell.SpecialFolders.Item("Desktop"), String) Dim Shortcut As IWshRuntimeLibrary.IWshShortcut ' Shortcut files have the (hidden) extension .lnk Shortcut = CType( _ WshShell.CreateShortcut(DesktopDir & "NotepadShortcut.lnk"), _ IWshRuntimeLibrary.IWshShortcut) ' Specify some basic shortcut properties. Shortcut.TargetPath = "C:Windows otepad.exe" Shortcut.WindowStyle = 1 Shortcut.Hotkey = "CTRL+SHIFT+N" Shortcut.Description = "Run Notepad" Shortcut.WorkingDirectory = DesktopDir ' Specify the first icon in the notepad.exe file. Shortcut.IconLocation = "notepad.exe, 0" ' Save the shortcut file. Shortcut.Save() Console.WriteLine("Shortcut created.") Console.ReadLine() End Sub End Module
You can also create shortcuts that reference Web sites (and even insert them in the favorites menu, if desired). Simply alter the TargetPath property, as shown here:
Shortcut = _ CType(WshShell.CreateShortcut(DesktopDir & "Prosetech.lnk"), _ IWshRuntimeLibrary.IWshShortcut) Shortcut.TargetPath = "http://www.prosetech.com" ' (Other configuration omitted.) Shortcut.Save()
You want to start a Windows application without a startup form.
Create a module with a public Main method. Show all forms modally, or use the Application.Run method.
There are several reasons that you might want to start a Windows application without using a startup form, including:
In these cases, you can start your application using a startup method. This startup method is a public method named Main that you will place in any module in your project. Here's one example:
Public Module StartModule Public Sub Main() ' (Code omitted.) End Sub End Module
You can then configure your project to start using this code. Right-click the project, select Properties, and then browse to the Common Properties | General node. Under Startup Object, choose Sub Main, as shown in Figure 10-3.
Figure 10-3: Configuring a startup method.
There's one important fact to note about startup methods: as soon as the Main method finishes executing, the application terminates and any open windows are automatically closed. This behavior is different from that of Visual Basic 6, which keeps the application running until all windows are closed. As a consequence, in your startup method, you should show windows modally, as in the example below.
Public Module StartModule Public Sub Main() Dim frm As New Form1() ' ShowDialog() shows a modal window, which interrupts the code. ' The Main() method does not continue until the window is closed. frm.ShowDialog() ' Show() shows a modeless window, which does not interrupt the code. ' The Main() method code continues, the application terminates ' prematurely, and the window is closed automatically. frm.Show() End Sub End Module
Another option is to use the Application.Run method to create a message loop. For example, if you want to show several windows at once, you can display them all modelessly and then use Application.Run to set up a message loop on the main window. When it's closed, the application will end.
Public Module StartModule Public Sub Main() Dim frmMain As New MainForm() Dim frmSecondary As New Form1() ' Show both windows modelessly. frmMain.Show() frmSecondary.Show() ' Keep the application running until frmMain is closed. Application.Run(frmMain) End Sub End Module
You can also use Application.Run without supplying a window name to start a message loop that continues until you explicitly terminate it.
This approach is useful if you want to decide programmatically when to end the application. It also allows you to show several windows and end the application when any one of these windows is closed. All you need to do is use the Application.Exit method anywhere in your program. The following code snippet ends the message loop when a window is closed:
Private Sub Form1_Closed(ByVal sender As Object, ByVal e As EventArgs) _ Handles MyBase.Closed Application.Exit() End Sub
You need to retrieve the command-line parameters that are used to execute your application.
Create a Main subroutine that accepts an array of strings. This array will be automatically populated with all command-line arguments.
Command-line arguments are most commonly used in Console applications. In fact, many Console utilities require command-line parameters to supply a minimum amount of information. Command-line arguments are often used in document-based Windows-based applications to quickly open specific files. For example, the command winword.exe mydoc.doc could be used to launch Microsoft Word and open the mydoc.doc file in one operation.
To retrieve command-line arguments in a Windows or Console application, your program must start with a Main subroutine. You should modify the Main subroutine so that it accepts an array of strings, as shown here:
Public Module StartModule Public Sub Main(args() As String) ' (Code omitted.) End Sub End Module
The args array will be populated with all the command-line arguments, in order. For example, if you execute the command myapp.exe /a /b /c, there will be three strings in the array, one for each parameter. Depending on the application, order might or might not be important for your parameters. The following example prints out all the supplied parameters:
Public Module CommandLineArgumentTest Public Sub Main(ByVal args() As String) Console.WriteLine("You supplied " & args.Length().ToString() & _ " parameters.") Dim Argument As String For Each Argument In args Console.WriteLine(Argument) Next End Sub End Module
You can test this example without resorting to the command line. Simply right-click the project in Solution Explorer and select Properties. Then browse to the Configuration Properties | Debugging node (shown in Figure 10-4) and supply the parameter list.
Figure 10-4: Using command-line parameters in Visual Studio .NET.
Parameters are separated based on spaces. For example, the command myapp.exe /a/b/c will return a single /a/b/c parameter. If you need to supply a parameter that includes a space, you can use quotation marks. This approach is necessary if you want to specify a filename parameter and the filename includes spaces. Here's an example:
myapp.exe "my file with spaces.txt" /d /e
This command provides three parameters:
my file with spaces.txt /d /e
Note that the quotation marks are stripped out transparently before your program receives the argument list.
You want to launch the application that is registered to handle a specific file type.
Use the Start method of the System.Diagnostics.Process class.
In Visual Basic 6, the only way to execute a program is to directly invoke the executable or use the Win32 API. In Visual Basic .NET, the situation is greatly improved with a specialized Process class that can launch an application based on the file types that are registered with Windows.
The Process class allows you to launch an application such as Microsoft Word (to display .doc files) or Adobe Acrobat Reader (to display .pdf files) without needing to know the exact location of the application on the computer's hard disk. You simply pass the name of the document file to the shared Start method of the System.Diagnostics.Process class.
As an example, consider the following code, which displays a file selection dialog box and then automatically opens the file using the registered application. For this example to work, you must import the System.Diagnostics namespace (which is true by default in Visual Studio .NET).
Dim dlgOpen As New OpenFileDialog() If dlgOpen.ShowDialog() = DialogResult.OK Then Process.Start(dlgOpen.FileName) End If
If you try to launch a file and there's no application registered to handle the corresponding file type, you'll receive a System.ComponentModel.Win32Exception exception. You can catch this exception and inform the user of the problem.
In some cases, you might want to configure the startup settings in more detail. You can do so by first creating a System.Diagnostics.ProcessStartInfo object, which encapsulates the information that will be used to launch the application. You can then pass the ProcessStartInfo instance to the shared Process.Start method.
Dim dlgOpen As New OpenFileDialog() If dlgOpen.ShowDialog() = DialogResult.OK Then Dim ProcessStart As New ProcessStartInfo(dlgOpen.FileName) Process.Start(ProcessStart) End If
You can change how the application will be executed by modifying the properties of the ProcessStartInfo object. For example, you can change the startup parameters or working directory. More interestingly, you can choose to use a different verb. By default, when you use Process.Start, the "open" verb will be used and the document will be loaded in the corresponding application. However, many applications register verbs for other actions, such as printing. The following example checks if a print verb is available for a file type and then uses it. For example, if you use this approach with a .doc file and you have Microsoft Word installed, Word will print the document in the background and then shut down.
Dim dlgOpen As New OpenFileDialog() If dlgOpen.ShowDialog() = DialogResult.OK Then Dim ProcessStart As New ProcessStartInfo(dlgOpen.FileName) Dim CanPrint As Boolean = False Dim Verb As String For Each Verb In ProcessStart.Verbs If Verb.ToLower() = "print" Then ' This is a print-able document. CanPrint = True ' Configure ProcessStart to use the print action. ProcessStart.Verb = Verb Exit For End If Next If CanPrint Then Process.Start(ProcessStart) Else MessageBox.Show("Can't print this type of document.") End If End If
You want to retrieve information about processes that are currently running.
Use the GetProcesses or GetProcessesByName methods of the System.Diagnostics.Process class.
The System.Diagnostics.Process class represents a Windows process. It provides an exhaustive list of properties, which are detailed on MSDN. Using this information, you can
You can also use the Process class methods to end a process (as described in recipe 10.10), and you can handle the Process.Exited event to react when another process terminates for any reason.
You can retrieve an array of Process objects that represent all the currently executing processes on a computer using the Process.GetProcesses method. You can retrieve information about a single process using the Process.GetProcessesByName method and supplying the process name. The process name is usually the same as the executable name, without the file extension.
Both GetProcesses and GetProcessesByName include an overloaded version that allows you to specify a computer name. You can use this method to retrieve information about a process running on another computer.
The following example shows a simple Console application that reports a few statistics about its own process. The information it displays is only a small subset of the total information that the Process class makes available.
Public Module ProcessInfoTest Public Sub Main() Dim Proc As Process ' This gets the current process by name. ' Alternatively, you could use the GetCurrentProcess() method instead. Proc = Process.GetProcessesByName("ConsoleApplication1")(0) Console.WriteLine("Start time: " & Proc.StartTime.ToString()) Console.WriteLine("Memory use: " & Proc.PagedMemorySize.ToString()) Console.WriteLine("Number of threads: " & Proc.Threads.Count) Console.WriteLine("Executable file: " & Proc.MainModule.FileName) Console.WriteLine("Responding: " & Proc.Responding.ToString()) ' Display modules this process has loaded. Console.WriteLine("Loaded modules:") Dim ProcModule As ProcessModule For Each ProcModule In Proc.Modules Console.WriteLine(" " & ProcModule.FileName) Next Console.ReadLine() End Sub End Module
The output for this application is shown in the following code listing. Only part of the list of loaded modules is shown. You'll notice that even though only one thread is in use in the application itself, .NET is using a total of seven threads to manage it.
Start time: 2003-01-13 10:47:35 AM Memory use: 8216576 Number of threads: 7 Executable file: C:TempConsoleApplication1inConsoleApplication1.exe Responding: True Loaded modules: C:TempConsoleApplication1inConsoleApplication1.exe C:WINDOWSSystem32 tdll.dll C:WINDOWSSystem32mscoree.dll C:WINDOWSsystem32KERNEL32.dll C:WINDOWSMicrosoft.NETFrameworkv1.0.3705MSVCR70.dll C:WINDOWSMicrosoft.NETFrameworkv1.0.3705fusion.dll . . .
The first time you access a property of a Process object, all the information will be retrieved and cached. If you want to update the information stored in the Process object with the current values, invoke the Process.Refresh method.
Here's another application that retrieves the full list of processes and displays them in a DataGrid control using data binding:
Public Class ProcessViewForm Inherits System.Windows.Forms.Form ' (Windows designer code omitted.) Private Sub ProcessViewForm_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load gridProcesses.DataSource = System.Diagnostics.Process.GetProcesses() End Sub End Class
A partial view of the result is shown in Figure 10-5.
Figure 10-5: Partial information for currently running processes.
You might experience a security error if you attempt to retrieve information about a process that's privileged. You can catch this error in your code when attempting to access the Process property. However, if you perform data binding with a restricted process, an untrappable error will occur when you navigate to the corresponding row and the DataGrid control attempts to retrieve the information for that process.
You want to end a process that's currently running.
Find the process using Process.GetProcessesByName, and then terminate it using Process.CloseMainWindow or Process.Kill.
The Process class provides two methods for ending a process: CloseMainWindow and Kill. CloseMainWindow sends a close message to the main window of an application and is the equivalent of the user closing the window. CloseMainWindow is preferable to Kill because it allows an orderly shutdown. For example, an application such as Microsoft Word will prompt the user to save any open documents. However, CloseMainWindow might not end an application. Most applications will ask for user verification before exiting.
Kill, on the other hand, immediately terminates the process, which might result in lost data. Kill is the equivalent of terminating the process with the Windows Task Manager. Kill should be used only as a last resort. For example, when shutting down the Windows operating system, Windows attempts to close any open applications and then kills them if the process is still running after a short period of time (approximately 30 seconds). Kill is also the only way to terminate a process that doesn't have a visual interface.
Before you use CloseMainWindow or Kill, you must find the appropriate Process. If you know the process friendly name (which is usually the executable name without the .exe extension), you can use the GetProcessesByName method. Alternatively, you can retrieve all processes with the GetProcesses method and then examine other Process properties to find the correct instance (as described in recipe 10.9).
The following example shows a Console application that attempts to close Microsoft Excel. If the program is still running 30 seconds after the CloseMainWindow request, the application is terminated with the Kill method.
Public Module ProcessKillTest Public Sub Main() ' Use an array, as there may be multiple instances of Excel running. Dim Proc, Processes() As Process Processes = Process.GetProcessesByName("excel") For Each Proc In Processes ' Attempt to close the window. ' If there is an unsaved Excel document, this will bring ' up the save changes prompt. Proc.CloseMainWindow() ' Wait up to 30 seconds. Proc.WaitForExit(30000) ' Kill the process if it is still runnning. If Not Proc.HasExited Then Proc.Kill() Console.WriteLine("Application was terminated forcibly.") Else Console.WriteLine("Application ended peacefully.") End If Next Console.ReadLine() End Sub End Module
You can't use CloseMainWindow or Kill to end processes that are running on remote computers.
You want to ensure that only one instance of your application can be running at once.
In the startup code for your application, check the currently running processes to see if your application is already loaded.
Limiting your application to a single instance is simply a matter of refusing to start if your application detects that another instance is already present. You can examine currently running processes using the System.Diagnostics.Process class, as described in recipe 10.9.
The following Console application provides a simple demonstration. Typically, if you detect more than one running instance, you will simply end the application quietly on startup. However, the example displays a message to facilitate testing.
Public Module OneInstanceTest Public Sub Main() Dim Proc() As Process ' Determine the full name of the current process. Dim ModuleName, ProcName As String ModuleName = Process.GetCurrentProcess.MainModule.ModuleName ProcName = System.IO.Path.GetFileNameWithoutExtension(ModuleName) ' Find all processes with this name. Proc = Process.GetProcessesByName(ProcName) If Proc.Length > 1 Then ' There is more than one process with this name. ' Therefore, this instance should end. Console.WriteLine("This instance should end.") Else Console.WriteLine("This instance can continue.") End If Console.ReadLine() End Sub End Module
Remember that it's only necessary to perform this test once at startup.
If you think that there might be more than one application with your friendly name, you can retrieve the full list of processes using the Process.GetProcesses method. You can then investigate each one in more detail. For example, you might want to examine the Process.MainModule property to determine the executable filename.
You want to interact with an application programmatically by sending keystrokes.
Use the WshShell.SendKeys method from the Windows Script Host. Alternatively, use the SendKeys class in the System.Windows.Forms namespace, in conjunction with the FindWindow and SetForegroundWindow methods from the Win32 API.
Ideally, application interaction should work through known interfaces. For example, you can "drive" Microsoft Office using dedicated Office COM components, as described in Chapter 19. However, many applications don't expose any programmatic interface, in which case, the only way you can interact with the application is by sending keystrokes to the user interface.
There is more than one way to send keys to a running application. The approach in this recipe uses the Windows Script Host COM component, which was introduced in recipe 10.5. Another option is to use the System.Windows.Forms.SendKeys class, which works almost identically. However, the .NET Framework does not provide any classes for activating other windows. Thus, if you want to send keys to another application using the managed SendKeys class, you will also have to use unmanaged functions from the Win32 API such as FindWindow and SetForegroundWindow first.
To use the Windows Script Host, you must add a reference to the Windows Script Host Object Model (as shown in Figure 10-2). You can then create a WshShell instance and use the SendKeys method, which allows you to send any combination of keystrokes. There are three types of keystrokes that you can send:
Both the Windows Script Host and the managed SendKeys class use the same syntax for specifying keystrokes.
Be aware that when using the SendKeys method, it's entirely possible to send messages faster than they can be processed. For that reason, you should be careful to insert short pauses between key presses.
The following example shows a Console application that runs the calculator and uses it to perform a simple calculation. It then copies the result to the clipboard and then displays the result in the Console window (using the clipboard technique from recipe 10.16).
Public Module SendKeyTest Private Shell As IWshRuntimeLibrary.WshShell Public Sub Main() Shell = New IWshRuntimeLibrary.WshShell() ' Start the calculator. Shell.Run("calc") Threading.Thread.Sleep(100) ' Give focus to the calculator, so it will receive keystrokes. Shell.AppActivate("Calculator") ' Send a series of keys (representing a calculation of 101 * 2. SendKeys("101") SendKeys("*") SendKeys("2~") ' Use the calculator's ability to copy results to the clipboard. SendKeys("^c") ' Retrieve the data from the clipboard. Console.Write("The calculator result is: ") Console.WriteLine(Clipboard.GetDataObject().GetData(DataFormats.Text)) Console.ReadLine() End Sub ' Send the key and pause 500 milliseconds. Private Sub SendKeys(ByVal key As String) Shell.SendKeys(key) Threading.Thread.Sleep(500) End Sub End Module
You want to log off or shut down Windows programmatically.
Use the unmanaged ExitWindowsEx API function.
The .NET Framework doesn't include the functionality needed to shut down or restart Windows. However, you can easily do so using the ExitWindowsEx function from the user32.dll library. This function accepts a parameter that indicates whether you want a logoff (value 0), a restart (value 2), or a shutdown (value 1). In addition, you can add a force constant (value 4) to force the system to take the indicated action even if the user attempts to cancel it. This drastic step is usually resented by users and should be used with caution.
The following Console application imports the ExitWindowsEx function, defines the related constants, and uses it to request a system logoff:
Public Module ShutdownTest ' This is the API function for exiting Windows. Private Declare Function ExitWindowsEx Lib "user32" _ (ByVal uFlags As Long, ByVal dwReserved As Long) As Long ' This enumeration holds related constants. Private Enum ExitWindowsFlags ' Use this constant to log the user off without a reboot. Logoff = 0 ' Use this constant to cause a system reboot. Reboot = 2 ' Use this constant to cause a system shutdown ' (and turn of the computer, if the hardware supports it). Shutdown = 1 ' Add this constant to any of the other three ' to force the shutdown or reboot even if the user tries to cancel it. Force = 4 End Enum Public Sub Main() ExitWindowsEx(ExitWindowsFlags.Logoff, 0&) End Sub End Module
You need to play a .wav audio file.
Use the unmanaged sndPlaySoundA API function.
The .NET Framework doesn't include any managed classes for playing audio files. However, the winmm.dll library included with Windows includes a function named sndPlaySoundA that accepts the name of a WAV file and a parameter indicating how to play it. You can choose to play a sound synchronously, asynchronously, or in a continuous background loop. When you play a sound synchronously, the function interrupts the execution of the program until the sound is complete. If you play a sound asynchronously, the function will return immediately, and the sound will play in the background.
The following example form allows a sound to be played in several different ways. The form code is shown here, and the form itself is shown in Figure 10-6 on the following page.
Figure 10-6: A sound test application.
Public Class SoundTestForm Inherits System.Windows.Forms.Form ' (Windows designer code omitted.) ' This function plays a WAV file. Private Declare Function PlaySound Lib "WINMM.DLL" Alias _ "sndPlaySoundA" (ByVal lpszSoundName As String, ByVal uFlags As _ Long) As Long ' This enumeration holds related constants. Private Enum PlaySoundFlags ' This flag pauses the application until the sound finishes playing. Sync = &H0 ' This flag indicates that the sound should be played asynchronously ' in the background while your application continues to execute. Async = &H1 ' Plays the sound continuously in a loop. This flag must be used ' with SND_ASYNC. To stop the play, call sndPlaySound ' again with a 0& as the first argument. [Loop] = &H8 ' By default, if you play a new sound while another sound is still ' playing, the first sound is interrupted. This flag instructs ' the application to wait instead. NoStop = &H10 ' By default, if you attempt to play a file the does not exist, ' Windows plays the default system sound. This flag ' stops the default sound from being playing in this circumstance. NoDefault = &H2 End Enum Private Sub cmdPlaySync_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdPlaySync.Click Me.Cursor = Cursors.WaitCursor PlaySound("testsound.wav", PlaySoundFlags.Sync) Me.Cursor = Cursors.Default End Sub Private Sub cmdPlayAsync_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdPlayAsync.Click Me.Cursor = Cursors.WaitCursor PlaySound("testsound.wav", PlaySoundFlags.Async) Me.Cursor = Cursors.Default End Sub Private Sub cmdPlayLoop_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdPlayLoop.Click PlaySound("testsound.wav", _ PlaySoundFlags.Async Or PlaySoundFlags.Loop) End Sub Private Sub cmdEndLoop_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdEndLoop.Click PlaySound(Nothing, PlaySoundFlags.Async) End Sub End Class
For more powerful control over sound and graphics, you can use Microsoft DirectX 9. Microsoft provides a full set of managed interfaces for DirectX 9. You can download both DirectX and the DirectX SDK at http://msdn.microsoft.com/ downloads/list/directx.asp.
You want to display a standard dialog box for directory selection.
In .NET 1.1, use the FolderBrowserDialog class in the System.Windows.Forms namespace. In .NET 1.0, you must derive a class from System.Windows.Forms.Design.FolderNameEditor so that you can use the protected FolderBrowser class it contains.
The .NET Framework includes several classes that wrap standard dialog boxes, such as the OpenFileDialog and SaveFileDialog classes. However, .NET 1.0 does not include any class for selecting a directory. .NET 1.1 (included with Visual Studio .NET 2003) resolves this problem by adding the FolderBrowserDialog class. This recipe demonstrates how to use this class, and how to create a very similar solution for .NET 1.0 applications.
To use the FolderBrowserDialog, you simply need to create an instance, set the descriptive text and initial path, and then call the ShowDialog method to display the selection window. The code snippet below demonstrates this technique, and the resulting window is shown in Figure 10-7.
Figure 10-7: The standard directory selection dialog box.
Dim dlgDirectory As New FolderBrowserDialog() ' Set the initial path and descriptive text. dlgDirectory.SelectedPath = "C:" dlgDirectory.Description = "Select a folder." ' Show the directory selection window. If dlgDirectory.ShowDialog() = DialogResult.OK Then MessageBox.Show("You chose: " & dlgDirectory.SelectedPath) End If
If you are using .NET 1.0, you don't have the benefit of the FolderBrowserDialog class. You could create your own control, but it isn't easy to create one that closely resembles the Windows default and provides all its functionality. Several solutions are possible, including using the SHBrowseForFolder API function from shell32.dll. However, calling SHBrowseForFolder is complicated by several interoperability issues (the function uses structures and pointers), so it isn't much easier than creating the functionality from scratch.
There is one shortcut, however. The FolderNameEditor class in the System.Windows.Forms.Design namespace provides a managed implementation that wraps the Win32 API in a FolderBrowser class. Unfortunately, FolderBrowser is a protected class, which means that it's available only to the FolderNameEditor code and to any class that derives from FolderNameEditor. One way to access FolderBrowser is to create a class that derives from FolderNameEditor and exposes the FolderBrowser functionality.
The custom class CustomFolderBrowserDialog demonstrates this technique. Be aware that before you can derive a class from FolderNameEditor, you must add a reference to the System.Design.dll assembly where the class is defined.
Public Class CustomFolderBrowserDialog Inherits System.Windows.Forms.Design.FolderNameEditor ' An instance of the protected FolderBrowser class. Private Browser As FolderBrowser Public Sub New() Browser = New FolderBrowser() ' Configure the FolderBrowser properties as needed. ' You could wrap this logic in custom property procedures, ' but you would need to create new enumerations, as the ' FolderBrowserFolder and FolderBrowserStyles enumerations ' are not accessible outside of this class. Browser.StartLocation = FolderBrowserFolder.Desktop Browser.Style = FolderBrowserStyles.RestrictToFilesystem End Sub ' Display the directory selection dialog box. Public Function ShowDialog() As DialogResult Return Browser.ShowDialog() End Function ' The descriptive text that appears in the window. Public Property Description() As String Get Return Browser.Description End Get Set(ByVal Value As String) Browser.Description = Value End Set End Property ' The path the user selected. Public ReadOnly Property SelectedPath() As String Get Return Browser.DirectoryPath End Get End Property End Class
You can now use the custom class to show a directory selection window. The process is almost identical to using the FolderBrowserDialog included with .NET 1.1. The only missing feature is the ability to set the initially selected path.
Dim dlgDirectory As New CustomFolderBrowserDialog() ' Set the initial path and descriptive text. dlgDirectory.Description = "Select a folder." ' Show the directory selection window. If dlgDirectory.ShowDialog() = DialogResult.OK Then MessageBox.Show("You chose: " & dlgDirectory.SelectedPath) End If
The directory selection window is shown in Figure 10-7.
You want to paste data to or retrieve data from the Windows clipboard.
Use the SetDataObject and GetDataObject methods of the System.Windows.Forms.Clipboard class.
The System.Windows.Forms.Clipboard class allows you to place data on the Windows clipboard and retrieve it. You can use the Clipboard class in any type of application, from Windows programs to Console utilities (although you'll need to add a reference to the System.Windows.Forms.dll assembly). Valid clipboard data includes core .NET types (strings, numbers, and so on) and any serializable type, including your own custom classes if they include the Serializable attribute. As an example, consider the PersonData class shown here:
_ Public Class PersonData Private _FirstName As String Private _LastName As String Public Property FirstName() As String Get Return _FirstName End Get Set(ByVal Value As String) _FirstName = Value End Set End Property Public Property LastName() As String Get Return _LastName End Get Set(ByVal Value As String) _LastName = Value End Set End Property Public Sub New(ByVal firstName As String, ByVal lastName As String) Me.FirstName = firstName Me.LastName = lastName End Sub End Class
To place data on the clipboard, you use the shared Clipboard.SetDataObject method:
Dim Person As New PersonData("Bob", "Jones") Clipboard.SetDataObject(Person)
To retrieve data, you use the shared Clipboard.GetDataObject method, which returns an IDataObject object that wraps the data. You can then query the IDataObject to determine if it contains a specific type of data. IDataObject.GetDataPresent checks for a specific type of data and returns True if it exists, IDataObject.GetData retrieves the data itself, and IDataObject.GetFormats retrieves all the data formats currently on the clipboard.
Here's the code you could use to retrieve the PersonData object from the clipboard:
' Retrieve the clipboard data. Dim Data As IDataObject = Clipboard.GetDataObject() ' Check if the clipboard contains a PersonData instance. If Data.GetDataPresent(GetType(PersonData)) Then Dim Person As PersonData Person = CType(Data.GetData(GetType(PersonData)), PersonData) MessageBox.Show("Retrieved: " & Person.FirstName & " " & Person.LastName) Else MessageBox.Show("No PersonData found.") End If
Here's a code snippet that displays all the data formats that are currently on the clipboard:
Dim Data As IDataObject = Clipboard.GetDataObject() Dim Format As String For Each Format In Data.GetFormats() MessageBox.Show(Format) Next
You want to display a specific help file topic depending on the currently selected control.
Use the HelpProvider component, and set the HelpKeyword and HelpNavigator extender properties for each control.
.NET provides support for context-sensitive help through the System.Windows.Forms.HelpProvider class. The HelpProvider class is a special extender control. You add it to the component tray of a form, and it extends all the controls on the form with a few additional properties, including HelpNavigator and HelpKeyword. For example, Figure 10-8 shows a form that has two controls and a HelpProvider named HelpProvider1. The ListBox1 control, which is currently selected, has several help-specific properties that are provided through HelpProvider.
Figure 10-8: The HelpProvider extender properties.
To use context-sensitive help with HelpProvider, you simply need to follow these three steps:
If the user presses the F1 key while a control has focus, the help file will be launched automatically and the linked topic will be displayed in the help window. If the user presses F1 while positioned on a control that doesn't have a linked help topic, the help settings for the containing control will be used (for example, a group box or a panel). If there are no containing controls or the containing control doesn't have any help settings, the form's help settings will be used. If the form's help settings are also lacking, HelpProvider will attempt to launch whatever help file is defined at the project level.
You can also use the HelpProvider methods to set or modify context-sensitive help mapping at run time. The book's sample files include a program that uses context-sensitive help in this way to provide control-specific, frame-specific, and form-specific help.
You want to be notified if your application is about to exit because of an unhandled error, possibly so that you can log the problem or perform some final cleanup.
Create an event handler for the AppDomain.UnhandledException event.
The AppDomain.UnhandledException event fires when an unhandled error occurs, just before the application is terminated. This event doesn't give you the chance to rectify the problem, but it does provide the exception object, which allows you to log the error and perform last-minute cleanup.
The following Console application uses this technique. Before it exits, it displays information about the offending error.
Public Module ErrorHandlerTest Public Sub Main() ' Connect a default unhandled exception handler. AddHandler AppDomain.CurrentDomain.UnhandledException, _ AddressOf UnhandledException ' End the program with an unhandled exception. Dim x As Integer x = x x End Sub Private Sub UnhandledException(ByVal sender As Object, _ ByVal e As UnhandledExceptionEventArgs) Console.WriteLine("*** An error was encountered. ***") Console.WriteLine(e.ExceptionObject.ToString()) Console.WriteLine("*** Press any key to exit. ***") Console.ReadLine() End Sub End Module
The easiest way to test this application is to run it outside of the development environment. The UnhandledException event won't occur while you debug the application in Visual Studio .NET, unless you configure the debugging settings (under Debug | Exceptions in the main menu) to continue on unhandled exceptions.
You want to deploy your application using a setup program that can copy files, create shortcuts, and add registry entries.
Create a Windows Installer setup project using Visual Studio .NET.
Thanks to the .NET zero-touch deployment model, you can copy your compiled application to any other computer without registering components or modifying the registry. However, most professional applications require an automated setup program that can copy files to the appropriate locations and add program shortcuts to the Start menu. Visual Studio .NET allows you to build this type of setup program by creating a setup project.
The setup project is a special type of Visual Studio .NET project. Unlike other project types, it is not language-specific. Instead of writing code or installation scripts, you configure setup options through designers and property windows. The project compiles to a Windows Installer setup application (an .msi file).
To create a setup project, you should begin by opening the project you want to deploy. Then right-click the solution item in Solution Explorer, and choose Add | New Project. Choose Setup Project from the Setup And Deployment Projects group, as shown in Figure 10-9.
Figure 10-9: A Visual Studio .NET setup project.
To create a basic setup, you need to complete at least these steps:
Figure 10-10: The File System Designer.
Initially, the File System Designer displays a short list of commonly used destination folders. You can add links to additional folders by right-clicking in the directory pane and choosing Add Special Folder. There are options that map to the computer's Fonts folder, Favorites folder, Startup folder, and many more, allowing you to install files and shortcuts in a variety of places.
Figure 10-11: Adding a project output (assembly file).
Figure 10-12: Adding a shortcut.
At this point, you have a fully functional setup project that you can compile and deploy. To create the .msi setup file at any time, right-click the setup project and choose Build. An .msi file for your setup project will be created in the bin directory, with the name of your project. You can use other setup designers to configure the setup user interface, add registry settings, install additional files, and more. The book's sample files include a sample application and Windows Installer setup project.
A Visual Studio .NET setup project can't install the .NET Framework. If you need the .NET Framework, you must install it using one of the techniques described in recipe 10.20 before you install your application on a new client.
You want to install the .NET Framework on another computer so that it can run custom .NET applications.
Use the redistributable Dotnetfx.exe executable.
Visual Studio .NET setup projects can't be used to install the .NET Framework. Microsoft recommends that you install the .NET Framework on clients that don't already have it using the Dotnetfx.exe redistributable file before you attempt to install a .NET application. You can obtain Dotnetfx.exe in several ways:
It's possible to create a bootstrapper setup that installs the .NET framework and then launches your setup application automatically. This approach complicates deployment and doesn't add a compelling benefit in most scenarios. However, if you would like to pursue this approach, refer to the MSDN white paper at http:// msdn.microsoft.com/library /en-us/dnnetdep/html/dotnetframedepguid.asp, which describes in detail the process you must follow.
You want to register your application to open automatically when the user selects certain file types in Windows Explorer.
Use the File Types Designer to configure a setup project accordingly.
You can register file types by modifying the registry manually using the techniques explained in recipe 10.1. However, a much better approach is to make these configurations once—at installation time—using the features built in to the Visual Studio .NET setup project.
To use this approach, begin by creating a setup project as described in recipe 10.19. Then follow these steps:
Figure 10-13: The File Types Designer.
In addition, you can specify an icon (using the Icon property) and a two-or three-word description of the format (using the Description property). A completed entry is shown in Figure 10-14.
Figure 10-14: Adding a file type.
Figure 10-15 shows a completed file type action for opening an application.
Figure 10-15: Adding a file type action.
Don't use the File Types Designer to take over basic file types such as .bmp, .html, or .mp3. Almost all computer users have preferred applications for accessing these file types, and trying to override these preferences will only anger your users.