A control is a class that derives from the System.Windows.Forms.Control base (either directly or indirectly) and is responsible for drawing a chunk of the container , which is either a form or another control. WinForms comes with several standard controls available by default on the Toolbox in VS.NET. These controls can be broken into the following ad hoc categories:
Although Appendix D: Standard WinForms Components and Controls lists and shows each of the standard controls, it's useful to consider each category for the features that the controls share in common. Action ControlsThe action controls are Button, ToolBar, MenuBar, and ContextMenu. [1] These controls exist to provide something for the user to click on to trigger an action in the application. Each of the available actions is labeled and, in the case of ToolBar, can have an optional image. The major event the action controls is the Click event:
Sub button1_Click(sender As Object, e As EventArgs) MessageBox.Show("Ouch!") End Sub Except for Button, the rest of the action controls are actually containers of multiple subobjects that the user interacts with. For example, a MainMenu object contains one or more MenuItem objects, one for each menu item that can fire a Click event: Sub exitMenuItem_Click(sender As Object, e As EventArgs) Me.Close() End Sub The ToolBar control also contains a collection of objects, of type ToolBarButton. However, when the user clicks, the event is sent for ToolBar itself, so the event handler is responsible for using the Button property of the ToolBarButtonClickEventArgs to figure out which button was pressed: Sub toolBar1_ButtonClick(_ sender As Object, e As ToolBarButtonClickEventArgs) If e.Button = fileExitToolBarButton Then Me.Close() ElseIf e.Button = helpAboutToolBarButton Then MessageBox.Show("The standard controls are cool") End If End Sub Because menu items and toolbar buttons often result in the same action, such as showing the About box, it's good practice to centralize the code and call it from both event handlers: Sub FileExit() ... End Sub Sub HelpAbout() ... End Sub Sub fileExitMenuItem_Click(sender As Object, e As EventArgs) FileExit() End Sub Sub helpAboutMenuItem_Click(sender As Object, e As EventArgs) HelpAbout() End Sub Sub toolBar1_ButtonClick( _ Sender As Object, e As ToolBarButtonClickEventArgs) If e.Button = fileExitToolBarButton Then FileExit() ElseIf e.Button = helpAboutToolBarButton Then HelpAbout() End If End Sub If you centralize the handling of an action, you don't have to worry about which controls trigger it; no matter how many do, all of them will get the same behavior. While we're on the topic of ToolBar control, you may be curious as to how images are assigned to each button. Assigning an image to a toolbar button involves creating and indexing into an ImageList, which is a component that holds a list of images for use by controls that display images. Image lists are discussed later in this chapter. Value ControlsThe value controls make up the set of controls that show and optionally allow editing of a single value. They can be broken down further by the data type of the value:
The string value controls expose a property called Text that contains the value of the control in a string format. The Label control merely displays the text. The LinkLabel control displays the text as if it were an HTML link, firing an event when the link is clicked. The StatusBar control displays the text in the same way a Label does (but, by default, docked to the bottom of the container), but it also allows for multiple chunks of text separated into panels. In addition to displaying text, the TextBox control allows the user to edit the text in single or multiline mode (depending on the value of Multiline property). The RichTextBox control allows for editing like TextBox but also supports RTF (Rich Text Format) data, which includes font and color information as well as graphics. When the Text value of either of these controls changes, the TextChanged event is fired . All the numeric value controls expose a numeric Value property, whose value can range from the Minimum to the Maximum property. The difference is only a matter of which UI you'd like to show to the user. When the Value properties change, the ValueChanged property is fired. The Boolean value controls ”CheckBox and RadioButton ”expose a Checked property that reflects whether or not they're checked. Both Boolean value controls can also be set to a third, "indeterminate" state, which is one of the three possible values exposed from the CheckState property. When the CheckState is changed, the CheckedChanged and CheckStateChanged events are fired. The date value controls allow the user to pick one or more instances of the DateTime type. MonthCalendar allows the choice of beginning and ending dates as exposed by the SelectionRange property (signaled by the SelectionRangeChanged event). DateTimePicker allows the user to enter a single date and time as exposed by the Value property (signaled by the Value-Changed event). The graphical value controls show images, although neither allows the images to be changed. The PictureBox control shows any image as set by the Image property. PrintPreviewControl shows, one page at a time, a preview of print data generated from a PrintDocument object (as described in Chapter 7: Printing). List ControlsIf one value at a time is good, then several values at a time must be better. The list controls ”ComboBox, CheckedListBox, ListBox, DomainUpDown, ListView, DataGrid, and TreeView ”can show more than one value at a time. Most of the list controls ”ComboBox, CheckedListBox, ListBox, and DomainUpDown ”show a list of objects exposed by the Items collection. To add a new item you use this collection: Sub Form1_Load(sender As Object, e As EventArgs) listBox1.Items.Add("an item") End Sub This sample adds a string object to the list of items, but you can add any object: Sub Form1_Load(sender As Object, e As EventArgs) Dim bday As DateTime = DateTime.Parse("1995-08-30 6:02pm") listBox1.Items.Add(bday) End Sub To come up with a string to display, the list controls that take objects as items call the ToString method. To show your own custom items in a list control, you simply implement the ToString method: Class Person Dim myname As String Dim myage As Integer Public Sub New(name As String, age As Integer) myname = name myage = age End Sub Property Name() As String Get Return myname End Get Set myname = Value End Set End Property Property Age() As Integer Get Return myage End Get Set myage = Value End Set End Property Overrides Function ToString() As String Return String.Format("{0} is {1} years old", Name, Age) End Function End Class Sub Form1_Load(sender As Object, e As EventArgs) Dim boys() As Person = {New Person("Tom", 7), _ New Person("John", 8)} Dim boy As Person For Each boy In boys listBox1.Items.Add(boy) Next End Sub Figure 8.1 shows the instances of the custom type shown in a ListBox control. Figure 8.1. Custom Type Shown in a ListBox Control
Because the ListView control can show items with multiple columns and states, its Items collection is populated with instances of the ListView-Item class. Each item has a Text property, which represents the text of the first column, and then a collection of subitems that represent the rest of the columns: Sub Form1_Load(sender As Object, e As EventArgs) Dim boys() As Person = { New Person("Tom", 7), _ New Person("John", 8)} Dim boy As Person For Each boy In boys ' NOTE: Assumes Columns collection already has 2 columns Dim item As ListViewItem = New ListViewItem() item.Text = boy.Name item.SubItems.Add(boy.Age.ToString()) listView1.Items.Add(item) Next End Sub Figure 8.2 shows the multicolumn ListView filled via this code. Figure 8.2. Multicolumn ListView
The TreeView control shows a hierarchy of items that are instances of the TreeNode type. Each TreeNode object contains the text, some optional images, and a Nodes collection containing subnodes. Which node you add to determines where the newly added node will show up in the hierarchy: Sub Form1_Load(sender As Object, e As EventArgs) Dim parentNode As TreeNode = New TreeNode() parentNode.Text = "Chris" ' Add a node to the root of the tree treeView1.Nodes.Add(parentNode) Dim childNode As TreeNode = New TreeNode() childNode.Text = "John" ' Add a node under an existing node parentNode.Nodes.Add(childNode) End Sub Figure 8.3 shows the result of filling a TreeView control using this sample code. Figure 8.3. A Parent Node and a Child Node in a TreeView Control
The DataGrid control gets its data from a collection set by using the DataSource property: Sub Form1_Load(sender As Object, e As EventArgs) Dim boys() As Person = {new Person("Tom", 7), _ New Person("John", 8)} dataGrid1.DataSource = boys End Sub The DataGrid shows each public property of the objects in the collection as a column, as shown in Figure 8.4. Figure 8.4. A DataGrid Showing a Collection of Custom Types
A DataGrid can also show hierarchical data and do all kinds of other fancy things. You'll find many more details about the DataGrid control in Chapter 13: Data Binding and Data Grids. List Item SelectionEach of the list controls exposes a property to report the current selection (or a list of selections, if the list control supports multiple selections) and fires an event when the selection changes. For example, the following code handles the SelectedIndexChanged event of the ListBox control and uses the SelectedIndex property to pull out the currently selected object: Sub listBox1_SelectedIndexChanged(sender As Object, e As EventArgs) ' Get the selected object Dim selection As Object = listBox1.Items(listBox1.SelectedIndex) MessageBox.Show(selection.ToString()) ' The object is still the same type as when we added it Dim boy As Person = CType(selection, Person) MessageBox.Show(boy.ToString()) End Sub Notice that the SelectedIndex property is an offset into the Items collection that pulls out the currently selected item. The item comes back as the "object" type, but a simple cast allows us to treat it as an instance of exactly the same type as when it was added. This is useful when a custom type shows data using ToString but has another characteristic, such as a unique identifier, that is needed programmatically. In fact, for the list controls that don't take objects, such as TreeView and ListView, each of the items supports a Tag property for stashing away unique ID information: Sub Form1_Load(sender As Object, e As EventArgs) Dim parentNode As TreeNode = New TreeNode() parentNode.Text = "Chris" parentNode.Tag = "555-12-4545" ' Put in extra info treeView1.Nodes.Add(parentNode) End Sub Sub treeView1_AfterSelect(sender As Object, e As TreeViewEventArgs) Dim selection As TreeNode = treeView1.SelectedNode Dim tag As Object = selection.Tag ' Pull out extra info MessageBox.Show(tag.ToString()) End Sub List controls support either custom types or the Tag property but not both. The idea is that because the lists contain instances of custom types, any extra information can simply be kept as needed. Unfortunately, the lack of a Tag property makes it more difficult to associate ID information with simple types, such as strings. However, a simple wrapper will allow you to add a tag to a list item of any type: Class TaggedItem Dim Item As Object Dim Tag As Object Public Sub New(item As Object, tag As Object) Me.Item = item Me.Tag = tag End Sub Override Function ToString() As String Return Item.ToString() End Function End Class Sub Form1_Load(sender As Object, e As EventArgs) ' Add two tagged strings comboBox1.Items.Add(New TaggedItem("Tom", "555-12-4547")) comboBox1.Items.Add(New TaggedItem("John", "555-12-4546")) End Sub Sub comboBox1_SelectedIndexChanged(sender As Object, e As EventArgs) Dim selection As TaggedItem = _ CType(comboBox1.Items(comboBox1.SelectedIndex), TaggedItem) Dim tag As Object = selection.Tag MessageBox.Show(tag.ToString()) End Sub The TaggedItem wrapper keeps track of an item and a tag. The ToString method lets the item decide how it should be displayed, and the Item and Tag properties expose the parts of the TaggedItem object for use in processing the current selection. Container ControlsWhereas the list controls hold multiple objects, the job of the container controls (GroupBox, Panel, and TabControl) is to hold multiple controls. The Splitter control is not itself a container, but it can be used with container controls docked to a container's edge for sizing. All the anchoring, docking, splitting, and grouping principles covered in Chapter 2: Forms also apply to container controls. Figure 8.5 shows examples of container controls in action. Figure 8.5. Container Controls in Action
Figure 8.5 shows a GroupBox on the left, docked to the left edge of the containing form, and a TabControl with two TabPage controls on the right, split with a Splitter control in the middle. The GroupBox sets the caption of the group using its Text property. The Panel has no label. The TabControl is really a container of TabPage controls. It's the TabPage controls that contain other controls, and the Text property shows up as the label of the tab. The only other interesting member of a container control is the Controls collection, which holds the list of contained controls. For example, the list box in Figure 8.5 is contained by the Controls collection of the group box: Sub InitializeComponent() ... ' groupBox1 Me.groupBox1.Controls.AddRange( _ New System.Windows.Forms.Control() { Me.listBox1 }) ... ' Form1 Me.Controls.AddRange( _ New System.Windows.Forms.Control() { _ Me.tabControl1, _ Me.splitter1, _ Me.groupBox1 }) ... End Sub Notice in the form's InitializeComponent that the group box's Controls collection is used to contain the list box and that the form's Controls collection is used to contain the tab control, the splitter, and the group box. It's a child control's container that determines how a control is arranged. For example, when the list box's Dock property is set to Fill, the docking is relative to its container (the group box) and not to the form that actually creates the control. When a control is added to a container's Controls collection, the container control becomes the child control's parent. A child control can discover its container by using its Parent property. ImageListsIn addition to showing text data, several of the controls ”including TabPage, ToolBarButton, ListView, and TreeView ”can show optional images. These controls get their images from an instance of the ImageList component. The ImageList component provides Designer support for adding images at design time and then exposes them by index number to controls that use them. Each image-capable control exposes one or more properties of type ImageList. This property is named "ImageList" if the control supports a single set of images. But if the control supports more than one list of images, the property name contains a phrase that includes the term "ImageList." For example, the TabControl exposes the ImageList property for use by all the contained TabPage controls, whereas the ListView control exposes the LargeImageList, SmallImageList, and StateImageList properties for the three kinds of images it can display. Regardless of the number of ImageList properties a control supports, when an item requires a certain image from the ImageList, the item exposes an index property to offset into the ImageList component's list of images. The following is an example of adding an image to each of the items in a TreeView control: Sub InitializeComponent() ... Me.treeView1 = New TreeView() Me.imageList1 = New ImageList(Me.components) ... ' ImageList associated with the TreeView Me.treeView1.ImageList = Me.imageList1 ... ' Images read from Form's resources Me.imageList1.ImageStream = ... ... End Sub Sub Form1_Load(sender As Object, e As EventArgs) Dim parentNode As TreeNode = New TreeNode() parentNode.Text = "Chris" parentNode.ImageIndex = 0 ' Dad image parentNode.SelectedImageIndex = 0 treeView1.Nodes.Add(parentNode) Dim childNode As TreeNode = New TreeNode() childNode.Text = "John" childNode.ImageIndex = 1 ' Son image childNode.SelectedImageIndex = 1 parentNode.Nodes.Add(childNode) End Sub Using the Designer to associate images with the ImageList component causes the images themselves to be stored in form-specific resources. [2] InitializeComponent pulls them in at run time by setting the image list's ImageStream property; InitializeComponent also associates the image list with the tree view by setting the tree view's ImageList property. Each node in a tree view supports two image indexes: the default image and the selected image. Each of these properties indexes into the image list associated with the tree view. Figure 8.6 shows the result.
Figure 8.6. A TreeView Using an ImageList
When you collect related images in an ImageList component, setting images in a control is as simple as associating the appropriate image list (or image lists) with the control and then setting each image index as appropriate. The control itself handles the work of drawing the image. Owner-Draw ControlsImage lists allow you to augment the display of certain controls with an image. If you'd like to take over the drawing of a control, owner-draw controls support this very thing. An owner-draw control provides events that allow a control's owner (or the control itself) to take over the drawing chores from the control in the underlying operating system. Controls that allow owner draw ”such as menus , some of the list controls, the tab page control, and status bar panel control ”expose a property that turns owner drawing on and then fires events to let the container know that it should do the drawing. For example, the ListBox control exposes the DrawMode property, which can be one of the following values from the DrawMode enumeration: Enum DrawMode Normal ' Control draws its own items (default) OwnerDrawFixed ' Fixed-size custom drawing of each item OwnerDrawVariable ' Variable-size custom drawing of each item End Enum Figure 8.7 shows an owner-draw ListBox control that changes the style to Italics when it's drawing the selected item. Figure 8.7. Owner-Drawn List Box
To handle the drawing for a ListBox, you first set the DrawMode property to something other than Normal (the default), and then you handle the ListBox control's DrawItem event: Sub InitializeComponent() ... Me.listBox1.DrawMode = DrawMode.OwnerDrawFixed ... End Sub Sub listBox1_DrawItem(sender As Object, e As DrawItemEventArgs) ' Draw the background e.DrawBackground() ' Get the default font Dim drawFont As Font = e.Font Dim ourFont As Boolean = False ' Draw in italics if selected If (e.State And DrawItemState.Selected) = _ DrawItemState.Selected Then ourFont = True drawFont = New Font(drawFont, FontStyle.Italic) End If ' Draw the listbox item e.Graphics.DrawString(listBox1.Items(e.Index).ToString(), _ drawFont, New SolidBrush(e.ForeColor), e.Bounds) If ourFont Then drawFont.Dispose() ' Draw the focus rectangle e.DrawFocusRectangle() End Sub The DrawItem method comes with the DrawItemEventArgs object: Class DrawItemEventArgs Inherits EventArgs ' Properties Property BackColor() As Color Property Bounds() As Rectangle Property Font() As Font Property ForeColor() As Color Property Graphics() As Graphics Property Index() As Integer Property State() As DrawItemState ' Methods Overridable Sub DrawBackground() Overridable Sub DrawFocusRectangle() End Class The DrawItem event is called whenever the item is drawn or when the item's state changes. The DrawItemEventArgs object provides all the information you'll need to draw the item in question, including the index of the item being drawn, the bounds of the rectangle to draw in, the preferred font, the preferred color of the foreground and background, and the Graphics object to do the drawing on. DrawItemEventArgs also provides the selection state so that you can draw selected items differently (as our example does). DrawItemEventArgs also gives you a couple of helper methods for drawing the background and the focus rectangle if necessary. You'll usually use the latter to bracket your own custom drawing. When you set DrawMode to OwnerDrawFixed, each item's size is set for you. If you'd like to influence the size, too, you can set DrawMode to OwnerDrawVariable, and, in addition to doing the drawing in the DrawItem handler, you can specify the height in the MeasureItem handler: Sub InitializeComponent() ... Me.listBox2.DrawMode = OwnerDrawVariable ... End Sub Sub listBox2_MeasureItem(sender As Object, e As MeasureItemEventArgs) ' Make every even item twice as high If ( e.Index Mod 2 = 0 ) Then e.ItemHeight = e.ItemHeight * 2 End Sub The MeasureItem event provides an instance of the MessageItemEvent-Args class, which gives you useful properties for getting and setting each item's height: Class MeasureItemEventArgs Inherits EventArgs ' Properties Property Graphics() As Graphics Property Index() As Integer Property ItemHeight() As Integer Property ItemWidth() As Integer End Class Figure 8.8 shows the effects of doubling the heights of the even items (as well as continuing to show the selection in italics). Figure 8.8. An Owner-Draw List Box Using Variable Height
Unlike the DrawItem event, the MeasureItem event is called only once for every item in the control, so things such as selection state can't be a factor when you decide how big to make the space for the item. ControlPaintOften, owner drawing is used to draw a control that looks just like an existing Windows control but has one minor addition, such as an image added to a menu item. In those cases, you'd like to avoid spending any time duplicating the way every version of Windows draws its controls, and you can use the ControlPaint helper class for that purpose. The ControlPaint class has static members for drawing common controls, lines, grids, and types of text: NotInheritable Class ControlPaint ' Properties Shared Property ContrastControlDark() As Color ' Methods Shared Function CreateHBitmap16Bit(...) As IntPtr Shared Function CreateHBitmapColorMask(...) As IntPtr Shared Function CreateHBitmapTransparencyMask(...) As IntPtr Shared Function Dark(...) As Color Shared Function DarkDark(...) As Color Shared Sub DrawBorder(...) Shared Sub DrawBorder3D(...) Shared Sub DrawButton(...) Shared Sub DrawCaptionButton(...) Shared Sub DrawCheckBox(...) Shared Sub DrawComboButton(...) Shared Sub DrawContainerGrabHandle(...) Shared Sub DrawFocusRectangle(...) Shared Sub DrawGrabHandle(...) Shared Sub DrawGrid(...) Shared Sub DrawImageDisabled(...) Shared Sub DrawLockedFrame(...) Shared Sub DrawMenuGlyph(...) Shared Sub DrawMixedCheckBox(...) Shared Sub DrawRadioButton(...) Shared Sub DrawReversibleFrame(...) Shared Sub DrawReversibleLine(...) Shared Sub DrawScrollButton(...) Shared Sub DrawSelectionFrame(...) Shared Sub DrawSizeGrip(...) Shared Sub DrawStringDisabled(...) Shared Sub FillReversibleRectangle(...) Shared Function Light(...) As Color Shared Function LightLight(...) As Color End Class For example, you can use ControlPaint to draw disabled text in an owner-draw status bar panel: Sub statusBar1_DrawItem(sender As Object, e As StatusBarDrawItemEventArgs) ' Panels don't draw with their BackColor ' so it's not set to something reasonable and ' therefore e.DrawBackground() isn't helpful. ' Instead, use the BackColor of the StatusBar, which is the sender Dim statusBar As StatusBar = CType(sender, StatusBar) Dim mybrush As Brush = New SolidBrush(statusBar.BackColor) e.Graphics.FillRectangle(SystemBrushes.Control, e.bounds) ' Draw text as disabled Dim format As StringFormat = New StringFormat() format.LineAlignment = StringAlignment.Center format.Alignment = StringAlignment.Center ControlPaint.DrawStringDisabled( _ e.Graphics, "Hi!', Me.Font, Me.ForeColor, e.Bounds, format) End Sub What makes the ControlPaint class handy is that it takes into account the conventions between versions of the operating system about the latest way to draw whatever it is you're trying to draw. So, instead of manually trying to duplicate how Windows draws disabled text this time, we can let ControlPaint do it for us, as shown in Figure 8.9. Figure 8.9. An Owner-Drawn Status Bar Panel Using ControlPaint
As nifty as ControlPaint is, as of .NET 1.1 it doesn't take theming into account. If you are using a themed operating system (such as Windows XP or Windows 2003 Server), the artifacts drawn by ControlPaint will not be themed. But even though ControlPaint doesn't support themed drawing, WinForms has some support for it in the standard controls, as discussed in Chapter 2: Forms. |