Windows Controls

Overview

The Microsoft .NET Framework includes a rich object model for creating and configuring Windows Forms. These types, which are found in the System.Windows.Forms namespace, abstract away most of the headaches of the Win32 API and also make it possible to perform tasks that would otherwise be extremely complex. They also mean that most of the solutions you might have used in the Microsoft Visual Basic 6 world no longer apply. In fact, some (such as control arrays, which are replaced in recipe 11.3) might even be counterproductive.

In this chapter you'll learn how to take charge of the .NET control classes. For example, you'll learn a few fundamentals such as adding controls programmatically (recipe 11.1), using drag-and-drop operations (recipe 11.11), and saving window positions and sizes (recipe 11.18). You'll also see how to enhance text boxes and combo boxes (recipes 11.8 and 11.9), use form inheritance (recipe 11.15) and context menus (recipes 11.12 and 11.13), work in multiple languages (recipe 11.14), and apply the Windows XP styles (recipe 11.21).

Add a Control at Runtime

Problem

You need to add a new control programmatically.

Solution

Create an instance of the appropriate control class, and then add the control object to a form or a container control.

Discussion

In the world of .NET, there really isn't any difference between creating a control at design time and creating one at runtime. When you add a design-time control, Microsoft Visual Studio .NET adds the required .NET code to the form's InitializeComponent subroutine. Alternatively, you can create the control with the same .NET code later, after the form has been displayed.

To do so, you simply instantiate a control class, configure the properties accordingly (particularly the size and position coordinates), and then add the control to the form or another container. Every control provides a Controls property that references a ControlCollection that contains all the child controls. To add a child control, you invoke the ControlCollection.Add method. In addition, if you need to handle the events for the new control, you can connect them to existing subroutines using the AddHandler statement.

The following application generates buttons dynamically and attaches their Click event handlers. Buttons are placed in a random position on the window, are numbered using a form-level counter variable, and are tracked in an ArrayList. A Clear button allows the user to remove all the dynamically generated buttons from the form using the ControlCollection.Remove method.

Public Class ButtonGenerator Inherits System.Windows.Forms.Form ' (Designer code omitted.) Private ButtonCounter As Integer = 0 Private DynamicButtons As New ArrayList() Private Sub cmdCreateNew_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdCreateNew.Click ' Generate the new button. Dim NewButton As New Button() ' Configure the properties of the button. Dim Rand As New Random() NewButton.Size = New System.Drawing.Size(88, 28) NewButton.Left = Rand.Next(150, Me.Width - NewButton.Width) NewButton.Top = Rand.Next(100, Me.Height - NewButton.Height) ButtonCounter += 1 NewButton.Text = "New Button " & ButtonCounter.ToString() ' Add the button to the form. Me.Controls.Add(NewButton) ' Attach an event handler to the Click event. AddHandler NewButton.Click, AddressOf NewButton_Click ' Store the button in a collection. DynamicButtons.Add(NewButton) End Sub Private Sub NewButton_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) ' Retrieve a reference to the button that was clicked. Dim Button As Button = CType(sender, Button) MessageBox.Show("You clicked: " & Button.Text) End Sub Private Sub cmdClear_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdClear.Click Dim Button As Button For Each Button In DynamicButtons ' Remove the button. Me.Controls.Remove(Button) Next ' Empty the collection. DynamicButtons.Clear() ButtonCounter = 0 End Sub End Class

The result of clicking the button several times is shown in Figure 11-1.

Figure 11-1: Generating buttons programmatically.

Store Arbitrary Data in a Control

Problem

You want to link a piece of data to a control.

Solution

Use the Control.Tag property.

Discussion

Every class that derives from System.Windows.Forms.Control provides a Tag property that can be used to store an instance of any type of object. The Tag property isn't used by the control or the .NET Framework. Instead, it's reserved as a convenient storage place for application-specific information. When retrieving data from the Tag property, you'll need to use the CType function to cast the object from the generic System.Object type to its original type.

Many other classes that are used with .NET controls also provide a Tag property. Notable examples include the ListViewItem and TreeNode classes (which represent items in a ListView or TreeView control). One class that does not provide a Tag property is MenuItem.

As an example, consider the custom Person class, which stores information about a single individual:

Public Class Person Private _FirstName As String Private _LastName As String Private _BirthDate As Date 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 Property BirthDate() As Date Get Return _BirthDate End Get Set(ByVal Value As Date) _BirthDate = Value End Set End Property Public Sub New(ByVal firstName As String, ByVal lastName As String, _ ByVal birthDate As Date) Me.FirstName = firstName Me.LastName = lastName Me.BirthDate = birthDate End Sub End Class

To test the Tag property, you can create a form with a TreeView and add several nodes. Each node will represent a separate person. Some information will be shown in the node text, but the full Person instance will be stored in the Tag property.

Private Sub TagTestForm_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load ' Fill the TreeView with three items. Dim NewPerson As New Person("John", "Smith", DateTime.Now) Dim NewNode As New TreeNode(NewPerson.LastName) NewNode.Tag = NewPerson treePersons.Nodes.Add(NewNode) NewPerson = New Person("Gustavo", "Camargo", DateTime.Now) NewNode = New TreeNode(NewPerson.LastName) NewNode.Tag = NewPerson treePersons.Nodes.Add(NewNode) NewPerson = New Person("Douglas", "Groncki", DateTime.Now) NewNode = New TreeNode(NewPerson.LastName) NewNode.Tag = NewPerson treePersons.Nodes.Add(NewNode) End Sub

When a node is selected, the corresponding Person instance is retrieved, and the information is used to refresh a label control. Figure 11-2 shows this program in action.

Figure 11-2: Storing data in the Tag property.

Private Sub treePersons_AfterSelect(ByVal sender As System.Object, _ ByVal e As System.Windows.Forms.TreeViewEventArgs) _ Handles treePersons.AfterSelect Dim SelectedPerson As Person = CType(e.Node.Tag, Person) lblSelected.Text = "You selected " & SelectedPerson.FirstName & _ " " & SelectedPerson.LastName & " (born " & _ SelectedPerson.BirthDate.ToString() & ")" End Sub

You could use a similar approach to store much more complex data (such as a reference to a DataRow representing the node) or much simpler information (such as a unique ID number that allows you to fetch additional information if needed).

Replace a Control Array

Problem

You want to provide the same functionality as a Visual Basic 6 control array in Visual Basic .NET.

Solution

Create an event handler that handles multiple controls, and examine the sender parameter to identify which control fired the event.

Discussion

It's possible to create a control array in a Visual Basic .NET project by making heavy use of the VisualBasic.Compatibility namespace. However, there are other solutions using only native .NET features that are better performing, more elegant, and easier to manage than using these legacy features.

One of the easiest approaches is simply to handle multiple events with a single event handler. You can then determine which control fired the event by examining the sender event parameter. The only limitation to this approach is that all the events handled by a single event handler must have the same signature.

The following example shows a single event handler that handles the CheckedChanged event from three radio button controls. Depending on which radio button is clicked, a different text box is enabled. The program is shown in action in Figure 11-3.

Figure 11-3: Handling multiple events with the same event handler.

Public Class MultipleControlHandlerTest Inherits System.Windows.Forms.Form ' (Designer code omitted.) Private Sub RadioButton_CheckedChanged( _ ByVal sender As System.Object, ByVal e As System.EventArgs) _ Handles RadioButton1.CheckedChanged, RadioButton2.CheckedChanged, _ RadioButton3.CheckedChanged ' Determine which control fired the event. Dim RadioButton As RadioButton = CType(sender, RadioButton) ' Disable all textboxes. TextBox1.Enabled = False TextBox2.Enabled = False TextBox3.Enabled = False ' Enable the associated textbox. If RadioButton Is RadioButton1 Then TextBox1.Enabled = True ElseIf RadioButton Is RadioButton2 Then TextBox2.Enabled = True ElseIf RadioButton Is RadioButton3 Then TextBox3.Enabled = True End If End Sub End Class

In some cases, you might not need to examine the control sender. Instead, you might simply be able to cast the sender parameter to a Control object and then retrieve the information you need. For example, consider a case where you want to display help text in another control when a user moves the mouse over a button. You can store the help text for each button in the button's Tag property. In the MouseMove event, you simply need to retrieve this text and display it accordingly:

Private Sub Button_MouseMove(ByVal sender As Object, _ ByVal e As System.Windows.Forms.MouseEventArgs) _ Handles Button1.MouseMove, Button2.MouseMove, _ Button3.MouseMove, Button4.MouseMove Dim ctrl As Control = CType(sender, Control) lblHelpText.Text = CType(ctrl.Tag, String) End Sub

Clear All Controls on a Form

Problem

You want to clear all the input controls on a form.

Solution

Iterate recursively through the collection of controls, and clear the Text property whenever you find an input control.

Discussion

You can iterate through the controls on a form using the Form.Controls collection, which includes all the controls that are placed directly on the form surface. However, if any of these controls are container controls (such as a group box, a panel, or a tab page), they might contain more controls. Thus, it's necessary to use recursive logic that searches the Controls collection of every control on the form.

The following example shows a form that calls a ClearControls function recursively to clear all text boxes. Figure 11-4 shows the form.

Figure 11-4: A self-clearing form.

Public Class TestClearForm Inherits System.Windows.Forms.Form ' (Designer code omitted.) Private Sub cmdClear_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdClear.Click ClearControls(Me) End Sub Private Sub ClearControls(ByVal ctrl As Control) ' Check if the current control should be cleared. ' Currently, the control is only cleared if it is a textbox. If TypeOf ctrl Is TextBox Then ctrl.Text = "" End If ' Process controls recursively. ' This is required if controls contain other controls ' (for example, if you use panels, group boxes, or other ' container controls). Dim ctrlChild As Control For Each ctrlChild In ctrl.Controls ClearControls(ctrlChild) Next End Sub End Class

Store Objects in a List

Problem

You want to store custom objects in a list, and customize their display.

Solution

You can add custom objects directly to the ListBox.Items collection. However, you must override the ToString method in your custom class to set the text that will appear in the ListBox, or set the ListBox.DisplayMember property.

Discussion

The .NET list controls (the ListBox, ComboBox, and CheckedListBox) can hold any type of object, whether it's an ordinary string or a custom class or structure that contains several pieces of information. However, if you attempt to store custom classes in a list box, you might discover that the text does not appear correctly. Instead, the fully qualified class name will be shown for each item in the list. The reason for this behavior is that the list box calls the contained object's ToString method to retrieve the text it should display. If you haven't added your own ToString method, your class uses the default ToString implementation it inherits from the System.Object class, which simply returns the fully qualified class name.

Here's an example of a custom class that can be used in a list control without problem:

Public Class Person Private _FirstName As String Private _LastName As String Private _BirthDate As Date 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 Property BirthDate() As Date Get Return _BirthDate End Get Set(ByVal Value As Date) _BirthDate = Value End Set End Property Public Sub New(ByVal firstName As String, ByVal lastName As String, _ ByVal birthDate As Date) Me.FirstName = firstName Me.LastName = lastName Me.BirthDate = birthDate End Sub Public Overrides Function ToString() As String Return LastName & ", " & FirstName End Function End Class

You can add Person objects to a list box in much the same way that you would add string information:

Dim NewPerson As New Person("John", "Smith", DateTime.Now) lstPersons.Items.Add(NewPerson) NewPerson = New Person("Gustavo", "Camargo", DateTime.Now) lstPersons.Items.Add(NewPerson) NewPerson = New Person("Douglas", "Groncki", DateTime.Now) lstPersons.Items.Add(NewPerson)

You can also retrieve the Person instance for the selected item just as easily:

Dim SelectedPerson As Person SelectedPerson = CType(lstpersons.SelectedItem, Person)

In the list box, the items will appear as shown in Figure 11-5.

Figure 11-5: Custom objects in a list box.

This technique works best if you are able to tweak the code for the custom class as needed. However, there might be a case where you want to use an object whose code you cannot modify—it might even be a class from the .NET class library. In this case, you can use the DisplayMember property. This takes the string name of a property in the bound object. For example, you could set DisplayMember to "LastName" and the list box would show the last name for each item. In order for this to work, LastName must be implemented as a full property, not just a public variable.

Force a ListBox to Scroll

Problem

You want to scroll a list box programmatically so that certain items in the list are visible.

Solution

Set the ListBox.TopIndex property, which sets the first visible list item.

Discussion

In some cases, you might have a list box that stores a significant amount of information or one that you add information to periodically. It's often the case that the most recent information, which is added at the end of the list, is more important than the information at the top of the list. One solution is to scroll the list box so that recently added items are visible.

The following code example is for a form with two buttons (shown in Figure 11-6). One button adds a batch of 10 items to the form, while the other button adds 10 items and then scrolls the list box to the last full page, using the TopIndex property.

Figure 11-6: Programmatically scrolling a list box.

Public Class ListScrollTest Inherits System.Windows.Forms.Form ' (Designer code omitted.) Private Sub cmdAdd_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdAdd.Click AddTenItems() End Sub Private Sub cmdAddScroll_Click(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles cmdAddScroll.Click AddTenItems() ListBox1.TopIndex = ListBox1.Items.Count - 1 End Sub Private Counter As Integer Private Sub AddTenItems() Dim i As Integer For i = 0 To 9 Counter += 1 ListBox1.Items.Add("Item " & Counter.ToString()) Next End Sub End Class

Use a Hyperlink

Problem

You want to add a hyperlink to a form that, when clicked, performs an action (such as launching Microsoft Internet Explorer and opening your company Web site).

Solution

Use the LinkLabel control.

Discussion

The LinkLabel control is a special type of label that can include hyperlinks. You define where the hyperlinks are and then handle the LinkClicked event to determine what action should be taken.

To create a hyperlink, add a LinkLabel control to the form, and then enter the full text. By default, all the text will be included in the link. To change the link text, find the LinkArea property in the Properties window and click the ellipsis (…) to launch a special editor that allows you to mark the hyperlink text by selecting it (see Figure 11-7).

Figure 11-7: Setting the link text for a LinkLabel control.

However, a more flexible option is to simply add links programmatically using the LinkLabel.Links.Add method. This allows you to specify multiple links for the same LinkLabel. You simply have to identify the zero-based position of the first letter in the link text, and the length of the link text. You can even associate additional data with each link by submitting an optional Object parameter. For example, the following code adds two links and associates each one with a distinct string that identifies the related Web site.

Private Sub LinkTest_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load ' Add the link on the word "www.prosetech.com" lnkSite.Links.Add(22, 17, "http://www.prosetech.com") ' Add the link on the word "Microsoft" lnkSite.Links.Add(71, 9, "http://www.microsoft.com") End Sub

When the LinkClicked event occurs, you can execute any .NET code. If your label has multiple links defined, you might want to examine the LinkLabelLinkClickedEventArgs object to retrieve the link data and determine the appropriate action. The code shown here launches Internet Explorer with the text that is associated with the link.

Private Sub lnkSite_LinkClicked(ByVal sender As System.Object, _ ByVal e As System.Windows.Forms.LinkLabelLinkClickedEventArgs) _ Handles lnkSite.LinkClicked ' Mark the link as visited. e.Link.Visited = True ' Retrieve the related URL. Dim Url As String = CType(e.Link.LinkData, String) ' Launch the default web browser using this link. Process.Start(Url) End Sub

If you would like a clicked link to change color (as in an Internet browser), you can set the LinkLabelLinkClickedEventArgs.Visited property to True when the link is clicked. Figure 11-8 shows a form with two links.

Figure 11-8: A LinkLabel control with two hyperlinks.

Restrict a Text Box to Numeric Input

Problem

You need to create a text box that will reject only all non-numeric keystrokes.

Solution

Add an event handler for the TextBox.KeyPress event, and set the KeyPressEventArgs.Handled property to True to reject an invalid keystroke.

Discussion

The best way to correct invalid input is to prevent it from being entered in the first place. This approach is easy to implement with the .NET text box because it provides a KeyPress event that occurs after the keystroke has been received but before it has been displayed. You can use the KeyPressEventArgs event parameter to effectively "cancel" an invalid keystroke by setting the Handled property to True.

To allow only numeric input, you must allow a keystroke only if it corresponds to a number (0 through 9) or a special control key (such as delete or the arrow keys). The keystroke character is provided to the KeyPress event through the KeyPressEventArgs.KeyChar property. You can use two shared methods of the Char class—IsDigit and IsControl—to quickly test the character.

Here's the complete code you would use to prevent non-numeric input:

Private Sub TextBox1_KeyPress(ByVal sender As Object, _ ByVal e As System.Windows.Forms.KeyPressEventArgs) _ Handles TextBox1.KeyPress If Not Char.IsDigit(e.KeyChar) And Not Char.IsControl(e.KeyChar) Then e.Handled = True End If End Sub

Notice that this code rejects the decimal separator. If you need to allow this character (for example, to permit the user to enter a fractional currency amount), you'll have to modify the code slightly, as shown here:

If Char.IsDigit(e.KeyChar) Or Char.IsControl(e.KeyChar) Then ElseIf e.KeyChar = "." And TextBox1.Text.IndexOf(".") = -1 Then Else e.Handled = True End If

This code allows only a single decimal point, but it makes no restriction about how many significant digits can be used.

Use an Auto Complete Combo Box

Problem

You want to create a combo box that automatically completes what the user is typing based on the item list.

Solution

You can implement a basic auto-complete combo box by handling the KeyPress event.

Discussion

Many professional applications include some type of auto-complete control. This control might fill in values based on a list of recent selections (as Microsoft Excel does when entering cell values) or might display a drop-down list of near matches (as Internet Explorer does when typing a URL). You can create a basic auto-complete combo box by handling the KeyPress and TextChanged events, searching for matching items in the appropriate list, and then filling in the appropriate item. The important step is that after you fill in a matching item, you must programmatically select the characters between the current insertion point and the end of the text. This allows the user to continue typing and replace the auto-complete text as needed.

' Track if a special key is pressed ' (in which case the text replacement operation will be skipped). Private ControlKey As Boolean = False ' Determine whether a special key was pressed. Private Sub TestCombo_KeyPress(ByVal sender As Object, _ ByVal e As System.Windows.Forms.KeyPressEventArgs) _ Handles TestCombo.KeyPress ' Retrieve a reference to the ComboBox that sent this event. Dim Combo As ComboBox = CType(sender, ComboBox) If Asc(e.KeyChar) = Keys.Escape Then ' Clear the text. Combo.SelectedIndex = -1 Combo.Text = "" ControlKey = True ElseIf Char.IsControl(e.KeyChar) Then ControlKey = True Else ControlKey = False End If End Sub ' Perform the text substituion. Private Sub TestCombo_TextChanged(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles TestCombo.TextChanged ' Retrieve a reference to the ComboBox that sent this event. Dim Combo As ComboBox = CType(sender, ComboBox) If Combo.Text <> "" And Not ControlKey Then ' Search for a matching entry. Dim MatchText As String = Combo.Text Dim Match As Integer = Combo.FindString(MatchText) ' If a matching entry is found, insert it now. If Match <> -1 Then Combo.SelectedIndex = Match ' Select the added text so it can be replaced ' if the user keeps typing. Combo.SelectionStart = MatchText.Length Combo.SelectionLength = Combo.Text.Length - Combo.SelectionStart End If End If End Sub

Figure 11-9 shows the auto-complete combo box.

Figure 11-9: An auto-complete combo box.

Sort a ListView Based on Any Column

Problem

You want to sort a ListView, but the Sort method sorts only based on the first column.

Solution

Create a custom IComparer that can sort ListViewItem objects, and pass it to the ListView.Sort method.

Discussion

The ListView control provides a Sort method that orders items alphabetically based on the text in the first column. If you want to sort based on other column values, perform a descending sort, or order items in any other way, you need to create a custom IComparer class that can perform the work.

The IComparer interface was first introduced in recipe 3.9. It defines a single method named Compare, which takes two objects and determines which one should be ordered first. The following example shows a custom ListViewItemComparer class that implements IComparer. It provides two additional properties: Column and Numeric. Column indicates the column that should be used for sorting, and Numeric is a Boolean flag that can be set to True if you want to perform number-based comparisons instead of alphabetic comparisons.

Public Class ListViewItemComparer Implements IComparer Private _Column As Integer Private _Numeric As Boolean = False Public Property Column() As Integer Get Return _Column End Get Set(ByVal Value As Integer) _Column = Value End Set End Property Public Property Numeric() As Boolean Get Return _Numeric End Get Set(ByVal Value As Boolean) _Numeric = Value End Set End Property Public Sub New(ByVal columnIndex As Integer) Column = columnIndex End Sub Public Function Compare(ByVal x As Object, ByVal y As Object) As Integer _ Implements System.Collections.IComparer.Compare Dim ListX As ListViewItem = CType(x, ListViewItem) Dim ListY As ListViewItem = CType(y, ListViewItem) If Numeric Then ' Convert column text to numbers before comparing. ' If the conversion fails, just use the value 0. Dim ListXVal, ListYVal As Decimal Try ListXVal = Decimal.Parse(ListX.SubItems(Column).Text) Catch ListXVal = 0 End Try Try ListYVal = Decimal.Parse(ListY.SubItems(Column).Text) Catch ListYVal = 0 End Try Return Decimal.Compare(ListXVal, ListYVal) Else ' Keep the column text in its native string format ' and perform an alphabetic comparison. Dim ListXText As String = ListX.SubItems(Column).Text Dim ListYText As String = ListY.SubItems(Column).Text Return String.Compare(ListXText, ListYText) End If End Function End Class

Now, to sort the ListView you simply need to create a ListViewItemComparer instance, configure it appropriately, and then set it in the ListView.ListViewItemSorter property before you call the ListView.Sort method.

Here's the code you might add to the ColumnClick event handler to automatically order items when a column header is clicked.

Private Sub ListView1_ColumnClick(ByVal sender As Object, _ ByVal e As System.Windows.Forms.ColumnClickEventArgs) _ Handles ListView1.ColumnClick Dim Sorter As New ListViewItemComparer(e.Column) ListView1.ListViewItemSorter = Sorter ListView1.Sort() End Sub

Use the Drag and Drop Feature

Problem

You want to use the drag-and-drop feature to exchange information between two controls (possibly in separate windows or in separate applications).

Solution

Start a drag-and-drop operation using DoDragDrop, and then respond to the DragEnter and DragDrop events.

Discussion

A drag-and-drop operation allows the user to transfer information from one place to another by clicking an item and "dragging" it to another location. A drag-and-drop operation consists of three basics steps:

  1. The user clicks a control, holds the mouse button down, and begins dragging. If the control supports the drag-and-drop feature, it sets aside some information.
  2. The user drags the mouse over another control. If this control accepts the dragged type of content, the mouse cursor changes to the special drag-and-drop icon (arrow and page). Otherwise, the mouse cursor becomes a circle with a line drawn through it.
  3. When the user releases the mouse button, the data is sent to the control, which can then process it appropriately.

To start a drag-and-drop operation, you call the source control's DoDragDrop method. At this point you submit the data and specify the type of operations that will be supported (copying, moving, and so on). This example initiates a drag-and-drop operation when the user clicks a text box:

Private Sub TextBox_MouseDown(ByVal sender As Object, _ ByVal e As System.Windows.Forms.MouseEventArgs) _ Handles TextBox1.MouseDown, TextBox2.MouseDown Dim txt As TextBox = CType(sender, TextBox) ' Select the text (so the user knows what data is being dragged.) txt.SelectAll() ' Start the drag-and-drop operation. txt.DoDragDrop(txt.Text, DragDropEffects.Copy) End Sub

Controls that can receive dragged data must have their AllowDrop property set to True. These controls will receive a DragEnter event when the mouse drags the data over them. At this point, you can examine the data that is being dragged, decide whether the control can accept the drop, and set the DragEventArgs.Effect property accordingly:

Private Sub TextBox_DragEnter(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DragEventArgs) _ Handles TextBox1.DragEnter, TextBox2.DragEnter ' Allow any text data to be dropped. If (e.Data.GetDataPresent(DataFormats.Text)) Then e.Effect = DragDropEffects.Copy Else e.Effect = DragDropEffects.None End If End Sub

The final step is to respond to the DragDrop event, which occurs when the user releases the mouse button:

Private Sub TextBox_DragDrop(ByVal sender As Object, _ ByVal e As System.Windows.Forms.DragEventArgs) _ Handles TextBox1.DragDrop, TextBox2.DragDrop ' Enter the dropped data into the textbox. Dim txt As TextBox = CType(sender, TextBox) txt.Text = CType(e.Data.GetData(DataFormats.Text), String) End Sub

Using the code we've presented so far, you can create a simple drag-and-drop test application (shown in Figure 11-10) that allows text to be dragged from one text box to the other. You can also drag text from another application and drop it into either text box.

Figure 11-10: A drag-and-drop test application with two text boxes.

Show a Linked Context Menu Generically

Problem

You want to show context menus for multiple controls with a minimum amount of code.

Solution

Write a generic event handler that retrieves the context menu that is associated with a control.

Discussion

You can associate a control with a context menu by settings the control's ContextMenu property. However, this is only a convenience—in order to display the context menu, you must retrieve the menu and call its Show method, supplying both a parent control and a pair of coordinates. Usually, you implement this logic in an event handler for the MouseDown event.

The good news is that the logic for showing context menus is completely generic, no matter what the control is. Every control supports the ContextMenu property (which is inherited from the base Control class), which means you can easily write a generic event handler that will display context menus for all controls.

The event handler shown here handles the MouseDown event for a label, picture box, and text box, and it shows the associated context menu.

Private Sub Control_MouseDown(ByVal sender As System.Object, _ ByVal e As System.Windows.Forms.MouseEventArgs) _ Handles PictureBox1.MouseDown, Label1.MouseDown, TextBox1.MouseDown If e.Button = MouseButtons.Right Then Dim ctrl As Control = CType(sender, Control) If Not ctrl.ContextMenu Is Nothing Then ctrl.ContextMenu.Show(ctrl, New Point(e.X, e.Y)) End If End If End Sub

Use Part of the Main Menu for a Context Menu

Problem

You want to create a context menu that shows the same entries as part of an application main menu.

Solution

Use the CloneMenu method to duplicate a portion of the main menu.

Discussion

In many applications, a control's context-sensitive menu duplicates a portion of the main menu. Unlike Visual Basic 6, .NET differentiates between context menus and main menus, and a menu item can only belong to one menu at a time.

The solution is to make a duplicate copy of a portion of the menu using the CloneMenu method. The CloneMenu method not only copies the appropriate MenuItem items (and any contained submenus), it also registers the MenuItem with the same event handlers. Thus, when a user clicks a cloned menu item in a context menu, the same event handler will be triggered as if the user clicked the duplicate menu item in the main menu.

Here's the code that duplicates all the menu items in a top-level File menu:

Dim mnuContext As New ContextMenu() Dim mnuItem As MenuItem ' Copy the menu items from the File menu into a context menu. For Each mnuItem In mnuFile.MenuItems mnuContext.MenuItems.Add(mnuItem.CloneMenu()) Next ' Attach the context menu to the textbox. TextBox1.ContextMenu = mnuContext

You can now display this context menu as normal:

Private Sub TextBox1_MouseDown(ByVal sender As Object, _ ByVal e As System.Windows.Forms.MouseEventArgs) Handles TextBox1.MouseDown If e.Button = MouseButtons.Right Then TextBox1.ContextMenu.Show(TextBox1, New Point(e.X, e.Y)) End If End Sub

A simple test application is shown in Figure 11-11.

Figure 11-11: Copying part of a main menu to a context menu.

Make a Multilingual Form

Problem

You need to create a localizable form that can be deployed in more than one language.

Solution

Store all locale-specific information in resource files, which are compiled into satellite assemblies.

Discussion

The .NET Framework includes built-in support for localization through its use of resource files. The basic idea is to store information that is locale-specific (for example, button text), in a resource file. You can then create multiple resource files for multiple different cultures and compile them into satellite assemblies. When you run the application, .NET will automatically use the correct satellite assembly based on the locale settings of the current computer.

You can read to and write from resource files manually. However, Visual Studio .NET also includes extensive design-time support for localized forms. It works like this:

  1. First set the Localizable property of the Form to True using the Properties window.
  2. Set the Language property of the Form to the locale for which you would like to enter information (see Figure 11-12). Then configure the localizable properties of all the controls on the form. Instead of storing your changes in the form designer code, Visual Studio .NET will actually create a new resource file to hold your data.

    Figure 11-12: Selecting a language for localizing a form.

  3. Repeat step 2 for each language that you want to support. Each time, a new resource file will be used. If you change the Language property to a locale you have already configured, your previous settings will reappear, and you'll be able to modify them.

You can now compile and test your application on differently localized systems. Visual Studio .NET will create a separate directory and satellite assembly for each resource file in the project. You can select Project | Show All Files from the Visual Studio .NET menu to see how these files are arranged, as shown in Figure 11-13.

Figure 11-13: A French-locale satellite assembly.

As a testing shortcut, you can also force your application to adopt a specific culture by modifying the Thread.CurrentUICulture property of the application thread.

Thread.CurrentThread.CurrentUICulture = New CultureInfo("fr")

However, you must modify this property before the form has loaded. You might need to use a Main startup method for this task, as demonstrated in the downloadable code sample for this recipe.

  Note

You can also use a utility called Winres.exe (included with Visual Studio .NET) to edit resource information. It provides a scaled-down form editor that does not include the capability to modify code, which is ideal for translators and other nonprogramming professionals who might need to enter locale-specific information.

Use Form Inheritance

Problem

You want to create and apply a consistent template to multiple forms.

Solution

Create a base form class, and derive all other forms from this class.

Discussion

Using inheritance with form classes is just as straightforward as using it with any other type of control class. You can use it to standardize visual appearance for multiple similar windows (for example, in a wizard) or in similar windows in multiple applications.

To use form inheritance, follow these three steps:

  1. Create a base form, and configure as you would any other form.
  2. Compile the project.
  3. Create a derived form.

There are two approaches to create your derived form. You can create it automatically with Visual Studio .NET by right-clicking the project item in Solution Explorer and choosing Add | Inherited Form. You'll be prompted to enter the name for the new form and to select the form it should derive from (see Figure 11-14). Alternatively, you can simply add an Inherits statement to an existing form. Just make sure to specify the fully-qualified class name, as shown here:

Figure 11-14: Adding a derived form.

Public Class DerivedForm Inherits MyNamespace.BaseForm

  Note

The easiest way to manage base forms is to place them in a separate class library assembly. You can then reference this assembly DLL in any projects that need to create derived forms. However, you can put base and derived forms in the same project, as long as you remember that you'll need to recompile the project before base form changes will appear in any derived forms.

When you use form inheritance, you'll discover that the controls on the base form cannot be modified. You'll also be prevented from attaching event handlers through the form editor, although you can write the code manually and it'll work perfectly well.

To fine-tune this behavior, you have several options:

Create a Form That Can t Be Moved

Problem

You want to create a form that occupies a fixed location on the screen and cannot be moved.

Solution

Make a borderless form by setting FormBorderStyle to None.

Discussion

You can create a borderless form by setting the FormBorderStyle property to None. Borderless forms cannot be moved. However, they also lack any kind of border—if you want the customary blue border, you'll need to add it yourself either with manual drawing code or by using a background image.

There is one other approach to creating an immovable form that provides a basic control-style border. First, set the ControlBox, MinimizeBox, and MaximizeBox properties to False. Then, set the Text property to an empty string. The form will have a raised gray border or black line (depending on the FormBorderStyle option you use), similar to a button. Figure 11-15 shows both types of immovable forms.

Figure 11-15: Two types of forms that cannot be moved.

Make a Borderless Form Movable

Problem

You want to create a borderless form that can be moved. This might be the case if you are creating a custom window that has a unique look (for example, for a visually rich application such as a game or a media player).

Solution

Create another control that responds to the MouseDown, MouseUp, and MouseMove events and programmatically moves the form.

Discussion

Borderless forms omit the title bar portion, which makes it impossible for them to be moved by the user. You can compensate for this shortcoming by adding a control to the form that serves the same purpose.

For example, Figure 11-16 shows a form that includes a label for dragging. When the label is clicked, the code sets a form-level flag to indicate it's in drag mode, and the current mouse position is recorded.

Figure 11-16: A movable borderless form.

' Tracks whether the form is in drag mode. If it is, mouse movements ' over the label will be translated into form movements. Dim Dragging As Boolean ' Stores the offset where the label is clicked. Dim PointClicked As Point Private Sub lblDrag_MouseDown(ByVal sender As Object, _ ByVal e As System.Windows.Forms.MouseEventArgs) Handles lblDrag.MouseDown If e.Button = MouseButtons.Left Then Dragging = True PointClicked = New Point(e.X, e.Y) Else Dragging = False End If End Sub

Next, as the user moves the mouse over the label, the form is automatically moved correspondingly. The result is that the form appears to be "attached" to the mouse pointer, which the user can move at will.

Private Sub lblDrag_MouseMove(ByVal sender As Object, _ ByVal e As System.Windows.Forms.MouseEventArgs) Handles lblDrag.MouseMove If Dragging Then Dim PointMoveTo As Point ' Find the current mouse position in screen coordinates. PointMoveTo = Me.PointToScreen(New Point(e.X, e.Y)) ' Compensate for the position the control was clicked. PointMoveTo.Offset(-PointClicked.X, -PointClicked.Y) ' Move the form. Me.Location = PointMoveTo End If End Sub

Finally, when the user releases the mouse button, dragging mode is switched off.

Private Sub lblDrag_MouseUp(ByVal sender As Object, _ ByVal e As System.Windows.Forms.MouseEventArgs) Handles lblDrag.MouseUp Dragging = False End Sub

You might want to combine this approach with recipe 12.19, which demonstrates how to create an irregularly shaped (nonrectangular) window.

Save the Size and Location of a Form

Problem

You want to store the size and position of a resizable form and restore it the next time the form is shown.

Solution

Store the Left, Top, Width, and Height form properties in the registry.

Discussion

The registry is an ideal place for storing position and size information for a form. Typically, you'll store each form in a separate key, perhaps using the class name of the form. These keys will be stored under an application-specific key.

To automate this process, it helps to create a dedicated class that saves and retrieves form settings. The FormSettingStore class shown in the following example fills this role. It provides a SaveSettings method that accepts a form and writes its size and position information to the registry, and an ApplySettings method that accepts a form, and applies the settings from the registry. The registry key path and the name of the form subkey are stored as class member variables.

Public Class FormSettingStore Private _RegPath As String Private _FormName As String Private Key As RegistryKey Public ReadOnly Property RegistryPath() As String Get Return _RegPath End Get End Property Public ReadOnly Property FormName() As String Get Return _FormName End Get End Property Public Sub New(ByVal registryPath As String, ByVal formName As String) Me._RegPath = registryPath Me._FormName = formName ' Create the key if it doesn't exist. Key = Registry.LocalMachine.CreateSubKey(registryPath & Me.FormName) End Sub Public Sub SaveSettings(ByVal form As System.Windows.Forms.Form) Key.SetValue("Height", form.Height) Key.SetValue("Width", form.Width) Key.SetValue("Left", form.Left) Key.SetValue("Top", form.Top) End Sub Public Sub ApplySettings(ByVal form As System.Windows.Forms.Form) ' If form settings are not available, the current form settings ' are used instead. form.Height = CType(Key.GetValue("Height", form.Height), Integer) form.Width = CType(Key.GetValue("Width", form.Width), Integer) form.Left = CType(Key.GetValue("Left", form.Left), Integer) form.Top = CType(Key.GetValue("Top", form.Top), Integer) End Sub End Class

To use the FormSettingStore class, simply add the event handling code shown here to any form. This code saves the form properties when the form closes and restores them when the form is loaded.

Private FormSettings As New FormSettingStore("SoftwareMyApp", Me.Name) Private Sub Form1_Load(ByVal sender As System.Object, _ ByVal e As System.EventArgs) Handles MyBase.Load FormSettings.ApplySettings(Me) End Sub Private Sub Form1_Closed(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles MyBase.Closed FormSettings.SaveSettings(Me) End Sub

Synchronize Controls on a Form

Problem

You want to create a record-browser where all control values are updated automatically for the current record.

Solution

Use a custom class to encapsulate the data, add instances of it to an ArrayList collection, and use .NET data binding.

Discussion

.NET data binding is most commonly used in ADO.NET applications, where you need to display one or more records in a DataTable. However, it can be used just as easily in other types of applications by replacing the DataTable or DataSet with a collection of custom objects.

For example, consider an example where you want to use multiple controls to show different pieces of information about a person. The person information is wrapped into a dedicated class, which is shown here.

Public Class Person Public _FirstName As String Public _LastName As String Public _BirthDate As Date 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 Property BirthDate() As Date Get Return _BirthDate End Get Set(ByVal Value As Date) _BirthDate = Value End Set End Property Public Sub New(ByVal firstName As String, ByVal lastName As String, _ ByVal birthDate As Date) Me.FirstName = firstName Me.LastName = lastName Me.BirthDate = birthDate End Sub End Class

To store multiple Person instances, you can use an ArrayList, as shown here:

Dim Persons As New ArrayList() Dim NewPerson As New Person("John", "Smith", New DateTime(1976, 6, 6)) Persons.Add(NewPerson) NewPerson = New Person("Gustavo", "Camargo", New DateTime(1926, 2, 6)) Persons.Add(NewPerson) NewPerson = New Person("Douglas", "Groncki", New DateTime(1980, 3, 30)) Persons.Add(NewPerson)

Finally, you can connect multiple controls to the same ArrayList. To connect a list control, you simply need to set its DataSource property to reference the ArrayList. You should also set the DisplayMember property with the name of the property in the bound object that you want to display. (Instead of using DisplayMember, you can override the Person.ToString method so that it returns a custom text representation for your object, as demonstrated in recipe 11.5.)

lstPersons.DataSource = Persons lstPersons.DisplayMember = "LastName"

Many other .NET controls, like buttons, text boxes, and labels, don't provide any specialized data binding features. Instead, they support data binding through the DataBindings collection, which is inherited from the Control class. The DataBindings collection allows you to link any control property to a property in a custom class. For example, you can bind the Text property of a text box to the Person.FirstName property. Values from the Person object will automatically be inserted into the text box, and changes to the text box will be automatically applied to the Person object.

Here's the data-binding code that connects two text boxes and a DateTimePicker control:

txtFirstName.DataBindings.Add("Text", Persons, "FirstName") txtLastName.DataBindings.Add("Text", Persons, "LastName") dtBirth.DataBindings.Add("Value", Persons, "BirthDate")

Figure 11-17 shows the resulting form. Note that you can change the Person values using the text boxes or DateTimePicker control. However, if you change the last name, the list box information won't be refreshed automatically. For that reason, you should use a piece of read-only information as the list box DisplayMember, or you should detect changes and rebind the list box as needed.

Figure 11-17: Synchronized controls.

Create a System Tray Application

Problem

You want to create an application that displays an icon in the system tray and runs in the background.

Solution

Start your application with a component class that includes the NotifyIcon control. Show other forms if needed when the user clicks on the NotifyIcon control or selects a context menu option.

Discussion

System tray applications are usually long-running applications that run quietly in the background, possibly performing some periodic task or waiting for an event (such as the creation of a file or a notification from the operating system). System tray applications might provide a user interface, but they don't present it on startup. Instead, they create an icon in the system tray and then wait for user interaction.

To create a system tray application, you need to start your application with a Main subroutine, not a form. To make life easy, you should code this Main subroutine in a component class, not an ordinary module. This is because component classes automatically have design-time support, which means you can configure the system tray icon and add a context menu in Visual Studio .NET at design time instead of writing tedious code.

To add a component class, right-click on the project item in Solution Explorer and select Add | Add Component. Then, add a NotifyIcon control to the form, and configure it. As a bare minimum, you must supply an icon for the Icon property. In addition, you might want to add a context menu and link it to the NotifyIcon using the ContextMenu property. The NotifyIcon control automatically displays its context menu when it's right-clicked, unlike other controls.

The design-time component surface that you might have is shown in Figure 11-18.

Figure 11-18: The design-time surface for a system tray application.

  Note

You cannot edit a context menu at design time on a component surface. Instead, you can add the context menu to a form, configure it using the designer, and then copy the context menu to the design-time surface of the component class.

Next, create a startup method in the component class that creates an instance of the component and starts a message loop. You can also start a thread or timer to perform a periodic task, or attach additional event handlers. To handle menu events, you simply need to attach event handlers to the Click event of each menu item. The full component code for a basic system tray application framework is shown here. It can be started and stopped (by clicking the Exit menu item).

Public Class App Inherits System.ComponentModel.Component ' (Component designer code omitted.) Public Shared Sub Main() ' Create the component. ' It's at this point that the system tray icon will appear. Dim App As New App() ' Keep the application running even when this subroutine ends. ' Use Application.Exit() to end the application. Application.Run() End Sub Private Sub mnuExit_Click(ByVal sender As Object, _ ByVal e As System.EventArgs) Handles mnuExit.Click Application.Exit() End Sub End Class

Apply Windows XP Control Styles

Problem

You want your controls to have the updated Windows XP appearance on Windows XP systems.

Solution

In .NET 1.0, you must create a manifest file. In .NET 1.1, you simply need to call the Application.EnableVisualStyles method.

Discussion

Windows XP styles are automatically applied to the non-client area of a form (such as the border and the minimize and maximize buttons). However, they won't be applied to controls such as buttons and group boxes unless you take additional steps.

First of all, you must configure all your form's button-style controls (such as buttons, check boxes, and radio buttons). These controls provide a FlatStyle property, which must be set to System.

The next step depends on the version of .NET that you are using. If you are using .NET 1.1 (provided with Visual Studio .NET 2003), you simply need to call the Application.EnableVisualStyles method before you show any forms. For example, you can start your application with the startup routine shown on the following page.

Public Module Startup Public Sub Main() ' Enable visual styles. Application.EnableVisualStyles() ' Show the main form for your application. Application.Run(New StartForm) End Sub End Module

If you are using .NET 1.0, you don't have the convenience of the Application.EnableVisualStyles method. However, you can still use visual styles—you simply need to create a manifest file for your application. This manifest file (an ordinary text file with XML content) tells Windows XP that your application requires the new version of the Comctl32.dll file. This file, which defines the new control styles, is included on all Windows XP computers. Windows XP will read and apply the settings from the manifest file automatically, provided you deploy it in the application directory and give it the correct name. The manifest file should have the same name as the executable used for your application, plus the extension .manifest (so TheApp.exe would have the manifest file TheApp.exe.manifest—even though this looks like two extensions).

Following is a sample manifest file. You can copy this file for your own applications—just rename it accordingly. It's also recommended that you modify the name value to use your application name, although this step isn't necessary.

To test that this technique is working, run the application. The Windows XP styles won't appear in the Visual Studio .NET design-time environment. Figure 11-19 shows the difference between the Windows XP and non–Windows XP control styles.

Figure 11-19: Control styles with and without Windows XP.

  Note

If you supply a manifest file for an application running on a pre–Windows XP version of Windows, it will simply be ignored, and the classic control styles will be used. For this reason, you might want to test your application both with and without a manifest file.

Категории