Classic Controls

This chapter considers some of the most common types of controls, like menus, text boxes, and buttons. Many of these controls have existed since the dawn of Windows programming and don't need much description. To keep things interesting I'll present some related .NET variants. For example, at the same time that you look at the label, list box, and domain controls, you will learn about the hyperlink label, checked list box, and rich date controls. I'll also describe menus in detail, and show you a few new tricks, including how you can add thumbnail images to owner-drawn menu items, and attach context menus to other controls.

The final part of this chapter demonstrates two advanced .NET features. First, you learn how you can add support for drag-and-drop operations to your .NET applications, and allow the user to move controls and transfer information. Then you examine different ways to implement validation to gracefully handle invalid input before it becomes a problem.

Types of Controls

Not all controls are created equal. .NET uses specialized controls that don't appear on forms, enhance other controls, and provide backward compatibility with legacy ActiveX controls. The next few sections provide a whirlwind tour of these specialized control types, and how they fit into the .NET class library.

Invisible Controls

Invisible controls don't require a portion of form real estate. These include controls that never have a visual appearance, like the Timer and the ErrorProvider. They also include others that appear in special circumstances or in windows of their own, like the ContextMenu and the common dialog controls (OpenFileDialog, SaveFileDialog, ColorDialog, and so on).

When you drag an invisible control onto the form surface, a special icon appears for it in the component tray (see Figure 4-1). You configure its properties through this icon. If you look at the automatically generated code for the form, you'll see that the code for creating the invisible control is added as it would be for a normal control. However, the invisible control is not added to the form's Controls collection.

Figure 4-1: The component tray

In some cases, it's worth asking whether invisible controls are really controls at all. For example, the Timer is just a special way to automate user interface changes. In some respects, it's simpler than true multithreaded programming because it uses safe task switching and automatically performs its work on the user interface thread, ensuring that you don't need to marshal calls when interacting with a control. (In other words, there is no need to use the Control.Invoke() method, because the controls in the form are always on the same thread as the timer.) However, it's a far stretch to call the Timer a true control in the sense that the text box and label are.

  Note 

Invisible controls don't derive from the Control class. Instead, they derive from System.ComponentModel.Component (the Control class also derives from this class). The Component class adds the basic features needed for an item to be hosted in a container and provides a Dispose() method that causes it to release its resources immediately.

Often, invisible controls can be created more flexibly at runtime in your own code. In some cases (like when you want to share one invisible control between forms), it's a necessity. For the most part, it's a matter of preference. The common dialog controls which represent one example of invisible controls, are described in the following chapter. Menus are another example of invisible controls, and are described later in this chapter.

Provider Controls

Providers are a special type of invisible control. They extend the properties of other controls on the current form. For example, the ToolTipProvider allows any control to display a tooltip when the mouse hovers over it. To use the ToolTipProvider, drag an instance of it onto the form. You can then set a tooltip in one of two ways:

  Tip 

There really isn't any difference between using the SetToolTip() method and the extended ToolTip property provided by the designer.With providers, Visual Studio .NET simply translates what you type in the Properties window into the appropriate method call, and adds the code to the form class. So when you set the ToolTip property, you are still in fact using the SetToolTip() method.

You can also configure some generic tooltip settings by adjusting the properties of the ToolTipProvider control, as detailed in Table 4-1.

Table 4-1: ToolTipProvider Members

Member

Purpose


Active

The same as Enabled for most controls. When set to false, no tooltips are shown.


AutomaticDelay, AutoPopDelay, InitialDelay, ReshowDelay

These settings specify the number of milliseconds before the tooltip appears, the time that it remains visible if the mouse is stationary, and the time required to make it reappear. Generally, you should use the default values.


ShowAlways

If set to true, tooltips appear when the mouse hovers over a control even if the window containing the control does not currently have focus.


SetToolTip(), GetToolTip(), and RemoveAll()

These methods allow you to attach a descriptive string to a control and retrieve it. To remove a tooltip, you can either attach an empty string, or use RemoveAll() to clear all tooltips at once.

.NET does not provide many provider controls, as they are generally used for specialized features. Toward the end of this chapter, you'll see an example that uses the ErrorProvider with control validation. Later, in Chapter 7, where you see how you can create custom provider controls.

ActiveX Controls

.NET includes excellent interoperability features that allow you to continue using COM components and ActiveX controls in your current applications. If you are using Visual Studio .NET, the process is even automated for you.

To add an ActiveX control to one of your projects in Visual Studio .NET, right-click the toolbox and select Customize Toolbox. Select the COM Components tab, and find the appropriate control on the list, and put a check mark next to it (see Figure 4-2).

Figure 4-2: Adding a COM reference

Nothing happens until you add an instance of this control to a form. The first time you do this, Visual Studio .NET automatically creates a special interop assembly for you. For example, if you add the MSChart control, which has no direct .NET equivalent, it creates a file with a name like AxInterop.MSChart20Lib_2_0.dll.

The "Ax" at the beginning of the name identifies the fact that this interop assembly derives from System.Windows.Forms.AxHost. This class is used to create any .NET wrapper for an ActiveX control. It works "in between" your .NET code and the ActiveX component, as shown in Figure 4-3.

Figure 4-3: AxHost interaction

The control on your form is a legitimate .NET control, as you can see by examining the automatically generated designer code that defines and instantiates it. For example, consider an automatically generated interop class that supports the MSChart control:

AxMSChart20Lib.AxMSChart AxMSChart1;

Here's the code used to configure the control, in true .NET fashion:

this.AxMSChart1 = new AxMSChart20Lib.AxMSChart(); this.AxMSChart1.Location = new System.Drawing.Point(36, 24); this.AxMSChart1.Name = "AxMSChart1"; this.axMSChart1.OcxState = ((AxHost.State)(resources.GetObject("axMSChart1.OcxState"))); this.AxMSChart1.Size = new System.Drawing.Size(216, 72); this.AxMSChart1.TabIndex = 4;

You can see that this control supports basic .NET properties like Size and Location. It also uses a special OcxState property (inherited from the AxHost class) that retrieves the persisted state of an ActiveX control. From your program's point of view, you can communicate with a normal .NET control that supports .NET event handling and the basic set of features in the Control class. The AxHost-based control quietly communicates with the original ActiveX control, and mimics its behavior on the form. You can even dynamically resize the control and modify its properties using the built-in property pages, and it will respond exactly as it should.

In some cases, the new class may introduce changes. For example, when the MSFlexGrid control is imported, it changes the syntax used to set some properties into method calls:

grid.set_ColWidth(1, 3000); // This was grid.ColWidth(1) = 3000; grid.set_ColAlignment(0, 1); // This was grid.ColAlightment(0) = 1;

Fortunately, you can always use the Object Browser to get to the bottom of any new changes.

If you are a war-hardened COM veteran, you can create interop controls by hand. However, this process is time-consuming and error-prone, and generally won't produce a better result than Visual Studio .NET's automatic support. Instead, you might want to subclass the interop control that Visual Studio .NET creates. In other words, you could create a custom control that inherits from the interop control. This extra layer gives you the chance to add additional .NET features, and won't hamper performance.

Should You Import ActiveX Controls?

Importing controls is easy, and it most cases it works without a hitch. Right now, it might be required to convert existing programs without rewriting large pieces of functionality. And while it is possible to recreate .NET controls for the MSChart or Internet Explorer Web Browser components, it can be time consuming.

You should also be aware of some of the potential problems:

.NET controls will always be the best solution, and in the coming months there will be a proliferation of new third-party options that surpass most of the ActiveX controls used today. Until that time, you may want to use the built-in ActiveX interop, particularly if you have custom controls and don't have the time or budget to redesign them for .NET.

Some of ActiveX controls that you still need to use in the .NET world (at least for the time being) include:

 
Chapter 4 - Classic Controls
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

The Classic Control Gallery

Now that you've learned about control fundamentals, it's time to look at some of the familiar controls every programmer knows and loves.

Labels

Label controls are used to place static text on a form. The text is contained in Text property, and aligned according the TextAlign property. Some other label properties are listed in Table 4-2.

Table 4-2: Label Properties

Property

Description


RenderTransparent

When set to true, makes the label's background transparent, allowing you to superimpose text on other controls like picture boxes. Of course, you can't control it if text displayed in a label is antialiased, often making it insufficient for splash screens and other situations where you need professional graphics. (In these cases, GDI+ features or prerendered bitmaps are the ticket.)


AutoSize, PreferredHeight, and PreferredWidth

.NET labels support automatic resizing, which dynamically adjusts size to fit the text string and font you apply. To use these features, set the AutoSize property to true, and specify the ideal size in the PreferredHeight and PreferredWidth properties.


Image and ImageAlign

Label controls can also contain a picture (referenced in the Image property), although they will not wrap around it. You will have finer-grained control by using separate label and picture controls.


UseMnemonic

When set to true, ampersands in the label's Text are automatically interpreted as Ctrl access keys. The user can press this access key, and the focus is forwarded to the next control in the tab order (for example, a labeled text box).

LinkLabel

This specialty label inherits from the Label class, but adds some properties that make it particularly well suited to represent links. For example, many applications provide a clickable link to a company web site in an About window.

The LinkLabel handles the details of displaying a portion of its text as a hyperlink. This portion is identified in the LinkArea property using a special structure that identifies the first character of the link and the number of characters in the link. Depending on the LinkBehavior property, this linked text may always be underlined, displayed as normal, or it may become underlined when the mouse hovers over it.

Here's the basic code that creates a link on the web site address:

lnkWebSite.Text = "See www.prosetech.com for more information."; // Starts at position 4, and 17 characters long. lnkWebSite.LinkArea = new LinkArea(4, 17); lnkWebSite.LinkBehavior = LinkBehavior.HoverUnderline;

You need to handle the actual LinkClicked event to make the link functional. In this event handler, you should set the LinkVisited property to true so that the color is updated properly, and perform the required action. For example, you might start Internet Explorer with the following code:

private void lnkWebSite_LinkClicked(Object sender, LinkLabelLinkClickedEventArgs e) { // Change the color if needed. lnkWebSite.LinkVisited = true; // Use the Process.Start method to open the default browser with a URL. System.Diagnostics.Process.Start("http://www.prosetech.com"); }

If you need to have more than one link, you can use the Links property, which exposes a special collection of Link objects. Each Link object stores its own Enabled and Visited properties, as well as information about the start and length of the link (Start and Length). You can also use the LinkData object property to associate some additional data with a link. This is useful if the link text does not identify the URL (for example a "click here" link).

lnkBuy.Text = "Buy it at Amazon.com or Barnes and Noble."; lnkBuy.Links.Add(10, 10, "http://www.amazon.com"); lnkBuy.Links.Add(24, 16, "http://www.bn.com");

The LinkClicked event provides you with a reference to the Link object that was clicked. You can then retrieve the LinkData, and use it to decide what web page should be shown.

private void lnkBuy_LinkClicked(Object sender, LinkLabelLinkClickedEventArgs e) { e.Link.Visited = true; System.Diagnostics.Process.Start((string)e.Link.LinkData); }

Figure 4-4 shows both of these LinkLabel examples. Additional information about the LinkLabel and LinkLabel.Link classes is provided in Tables 4-3 and 4-4.

Figure 4-4: Two LinkLabel examples

Table 4-3: LinkLabel Properties

Property

Description


ActiveLinkColor, DisabledLinkColor, LinkColor, and VisitedLinkColor

Sets colors for the links in the LinkLabel (the rest of the text has its color determined by the standard ForeColor property). Links can be visited, disabled, enabled (normal), or active (while they are in the process of being clicked).


LinkArea and Links

LinkArea specifies the position of the link in the text. If you have more than one link, you can use the Links property instead, which is a special LinkCollection class.


LinkBehavior

Specifies the underlining behavior of the link using the LinkBehavior enumeration.


LinkVisited

When set to true, the link appears with the visited link color.

Table 4-4: LinkLabel.Link Properties

Property

Description


Enabled

Allows you to enable or disable a link. Disabled links do not fire the LinkClicked event when clicked.


Length and Start

Identifies the position of the link in the LinkLabel.


LinkData

Provides an object property that can hold additional data, like the corresponding URL. You can retrieve this data in the LinkClicked event handler.


Visited

When set to true, the link appears with the visited link color.

Button

Quite simply, buttons are used to "make things happen." The most important thing to remember about buttons is that their Click event has a special meaning: it occurs when you trigger the button in any way, including with the keyboard, and it is not triggered by right-button mouse clicks. Buttons are old hat to most developers, but Table 4-5 lists a couple of interesting members that may have escaped your attention.

Table 4-5: Special Button Members

Member

Description


PerformClick()

"Clicks" the button programmatically. Useful for wizards and other feature where code "drives" the program.


DialogResult

If set, indicates that this button will close the form automatically and return the indicated result to the calling code, provided the window is shown modally. This technique explained in Chapter 5, in the section about dialog forms.

TextBox

Another staple of Windows development, the text box allows the user to enter textual information. The previous chapter explained how you can react to and modify key presses in the text box. Interestingly, text boxes provide a basic set of built-in functionality that the user can access through a context menu (see Figure 4-5).

Figure 4-5: The built-in TextBox menu

Much of this functionality is also exposed through TextBox class members. See Table 4-6 for a complete rundown.

Table 4-6: TextBox Members

Property

Description


AcceptsReturn and Multiline

If you set Multiline to true, the text box can wrap text over the number of available lines (depending on the size of the control). You can also set AcceptsReturn to true, so that a new line is inserted in the text box whenever the user hits the Enter key (otherwise, pressing the Enter key will probably trigger the form's default button).


AcceptsTab

If true, when the user presses the Tab key it inserts a hard tab in the text box (rather than causing the focus to move to the next control in the tab order).


AutoSize

When set to true, a single line text box's height is adjusted to match the corresponding font size.


CanUndo

Determines whether the text box can undo the last action. An Undo operation can be triggered using the Undo() method, or when the user right-clicks the control and chooses Undo from the context menu.


Cut(), Copy(), Paste(), Clear(), Undo(), Select(), SelectAll()

These methods allow you to select text and trigger operations like copy and cut, which work with the clipboard. The user can also access this built-in functionality through the context menu for the text box.


CharacterCasing

Forces all entered characters to become lowercase or uppercase, depending on the value you use from the CharacterCasing enumeration.


MaxLength

The maximum number of characters or spaces that can be entered in the text box.


PasswordChar

If this property is set to a character, that character appears in place of the text box value, hiding its information. For example, if you set this to an asterisk, the password "sesame" will appear as a series of asterisks (******).


SelectedText, SelectionLength, and SelectionStart

The SelectionStart and SelectionLength properties allow you to set the text that is currently selected in the text box.


ReadOnly

If true, the contents of a read-only text box can be modified in your code, but not by the user. Making a text box read-only instead of disabling it allows the text to remain clearly visible (instead of "greyed out") and it allows the user to scroll through if it does not fit in the display area.

CheckBox and RadioButton

The CheckBox and RadioButton controls provide a Checked property that indicates whether the control is checked or "filled in." After the state is changed, a Checked event occurs.

A special three-state check box can be created by setting the ThreeState property to true. You need to check the CheckState property to examine whether it is Checked, Unchecked, or Indeterminate (shaded but not checked).

By default, the control is checked and unchecked automatically when the user clicks it. You can prevent this by setting AutoCheck to false, and handling the Click event. This allows you to programmatically prevent a check box or radio button from being checked (without trying to "switch it back" after the user has made a change).

PictureBox

A picture box is one of the simplest controls .NET offers. You can set a valid image using the Image property, and configure a SizeMode from the PictureBoxSizeMode enumeration. For example, you can set the picture to automatically stretch to fit the picture box.

pic.Image = System.Drawing.Image.FromFile("mypic.bmp"); pic.SizeMode = PictureBoxSizeMode.StretchImage;

List Controls

.NET provides three basic list controls: ListBox, CheckedListBox, and ComboBox. They all inherit from the abstract ListControl class, which defines basic functionality that allows you to use a list control with data binding. Controls can be bound to objects like the DataSet, arrays, and ArrayList collections, regardless of the underlying data source (as you'll see in Chapter 8).

// Bind a list control to an array of city names. String[] cityChoices = {"Seattle", "New York", "Signapore", "Montreal"}; lstCity.DataSource = cityChoices;

You can access the currently selected item in several ways. You can use the SelectedIndex property to retrieve the zero-based index number identifying the item, or the Text property to retrieve the displayed text. You can also set both of these properties to change the selection:

// Search for the item with "New York" as its text, and select it. lstCity.Text = "New York"; // Select the first item in the list. lstCity.SelectedIndex = 0;

If you are using a multiselect ListBox, you can also use the SelectedIndices or SelectedItems collections. Multiselect listboxes are set based on the Selection-Mode property. You have two multiselect choices: SelectionMode.MultiExtended, which requires the user to hold down Ctrl or Shift while clicking the list to select additional items, and SelectionMode.MultiSimple, which selects and deselects items with a simple mouse click or press of the Space key. The CheckedListBox provides similar CheckedIndices and CheckedItems properties that provide collections of checked items.

Here's an example that iterates through all the checked items in a list, and displays a message box identifying each one:

foreach (string item in chkList.CheckedItems) { // Do something with checked item here. MessageBox.Show("You checked " + Item); }

You can also access all the items in a list control through the Items collection. This collection allows you to count, add, and remove items. Note that this collection is read-only if you are using a data-bound list.

lstFood.Items.Add("Macaroni"); // Added to bottom of list. lstFood.Items.Add("Baguette"); // Added to bottom of list. lstFood.Items.Remove("Macaroni"); // The list is searched for this entry. lstFood.Items.RemoveAt(0); // The first item is removed.

Table 4-7 dissects the properties offered by all list controls.

Table 4-7: List Control Properties

Property

Description


CheckOnClick

If set to true, the check box for an item is toggled with every click. Otherwise, a double-click is required.


IntegralHeight

If set to true, the height is automatically adjusted to the nearest row-multiple height, ensuring no half-visible rows are shown in the list.


ItemHeight

The height of a row with the current font, in pixels.


Items

The full collection of items in the list control.


MultiColumn and HorizontalScrollbar

A multicolumn list control automatically divides the list into columns, with no column longer than the available screen area. Vertical scrolling is thus never required, but you may need to enable the horizontal scroll bar to see all the columns easily.


SelectedIndex, SelectedIndices, SelectedItem, SelectedItems, and Text

Provide different ways to access the currently selected item (an Object type, which is typically a string), its zero-based index number, or its text. The CheckedListBox uses CheckedItems and CheckedIndices properties instead of SelectedItems and SelectedIndices.


SelectionMode

Allows you to configure a multiselect list control using one of the SelectionMode values. Multiple selection is not supported for CheckListBox controls.


Sorted

If set to true, items are automatically sorted alphabetically. This generally means you should not use index-based methods, as item indices change as items are added and removed.


TopIndex

The index number representing the topmost visible item. You can set this property to scroll the list.


ThreeDCheckBoxes

Configures the appearance of check boxes for a CheckedListBox.


UseTabStops

If set to true, embedded tab characters are expanded into spaces.

The ComboBox control provides a few different properties (detailed in Table 4-8). It also supports the same selection properties and Items collection. In addition, it can work in one of three modes, as specified by the DropDownStyle property. In ComBoxStyle.DropDown mode, the combo box acts as a nonlimiting list where the user can type custom information. In ComboBoxStyle.DropDown-List, pressing a key selects the first matching entry. The user cannot enter items that are not in the list.

Table 4-8: Special ComboBox Properties

Property

Description


DropDownStyle

Sets the type of drop-down list box. It can be a restrictive or nonrestrictive list.


DropDownWidth

This specifies the width of the drop-down portion of the list.


DroppedDown

This Boolean property indicates if the list is currently dropped down. You can also set it programmatically.


MaxDropDownItems

This specifies how many items will be shown in the drop-down portion of the list.


MaxLength

For an unrestricted list, this limits the amount of text that can be entered by the user.

  Tip 

You should always make sure to choose the right kind of combo. DropDown style is ideal for a list of selected choices that is not comprehensive (like a field where users can type the name of their operating system). The available list items aren't mandatory, but they will encourage consistency. The DropDownList style is ideal for a database application where a user is specifying a piece of search criteria by using the values in another table. In this case, if the value doesn't exist in the database, it's not valid, and can't be entered by the user.

List controls with objects

In the preceding examples, the Items property was treated like a collection of strings. In reality, it's a collection of objects. To display an item in the list, the list control automatically calls the object's ToString() method. In other words, you could create a custom data object, and add instances to a list control. Just make sure to override the ToString() method, or you will end up with a series of identical items that show the fully-qualified class name.

For example, consider the following Customer class:

public class Customer { public string FirstName; public string LastName; public DateTime BirthDate; public Customer() {} public Customer(string firstName, string lastName, DateTime birthDate) { this.FirstName = firstName; this.LastName = lastName; this.BirthDate = birthDate; } public override string ToString() { return FirstName + " " + LastName; } }

You can add customer objects to the list control natively. Figure 4-6 shows how these Customer objects appear in the list.

Figure 4-6: Filling a list box with objects

lstCustomers.Items.Add(new Customer("Maurice", "Respighi", DateTime.Now)); lstCustomers.Items.Add(new Customer("Sam", "Digweed", DateTime.Now)); lstCustomers.Items.Add(new Customer("Faria", "Khan", DateTime.Now));

Other Domain Controls

Domain controls are controls that restrict user input to a finite set of valid values. The standard ListBox is an example of a domain control, because a user can only choose one of the items in the list. Figure 4-7 shows an overview of the other domain controls provided in .NET.

Figure 4-7: The domain controls

DomainUpDown

This control is similar to a list control in that it provides a list of options. The difference is that the user can only navigate through this list using the up/down arrow buttons, and moving to either the previous or following item. List controls are generally more useful, because they allow multiple items to be shown at once.

To use the DomainUpDown control, add a string for each option to the Items collection. The Text or SelectedIndex property returns the user's choice.

// Add Items. udCity.Items.Add("Tokyo"); udCity.Items.Add("Montreal"); udCity.Items.Add("New York"); // Select the first one. udCity.SelectedIndex = 0;

NumericUpDown

The NumericUpDown list allows a user to choose a number value by using up/down arrow buttons (or typing it in directly). You can set the allowed range using the Maximum, Minimum, and DecimalPlaces properties. The current number in the control is set or returned through the Value property.

// Configure a NumericUpDown control. udAge.Maximum = 120; udAge.Minimum = 18; udAge.Vale = 21;

TrackBar

The track bar allows the user to choose a value graphically by moving a tab across a vertical or horizontal strip (use the Orientation property to specify). The range of values is set through the Maximum and Minimum properties, and the Value property returns the current number. However, the user sees a series of "ticks" and not the exact number. This makes the track bar suitable for a setting that doesn't have an obvious numeric significance or where the units may be arbitrary, such as when setting volume levels or pitch in an audio program.

// Configure a TrackBar. barVolume.Minimum = 0; barVolume.Maximum = 100; barVolume.Value = 50; // Show a tick every 5 units. barVolume.TickFrequency = 5; // The SmallChange is the amount incremented if the user clicks an arrow button // (or presses an arrow key). // The LargeChange is the amount incremented if the user clicks the barVolume // (or presses PageDown or PageUp). tackbar.SmallChange = 5; barVolume.LargeChange = 25;

ProgressBar

The progress bar is quite different than the other domain controls because it doesn't allow any user selection. Instead, you can use it to provide feedback about the progress of a long running task. As with all the number-based domain controls, the current position of the progress bar is identified by the Value property, which is only significant as it compares to the Maximum and Minimum properties that set the bounds of the progress bar. You can also set a number for the Step property. Calling the Step() method then increments the value of the progress bar by that number.

// Configure the progress bar. // In this case we hard-code a maximum, but it would be more likely that this // would correspond to something else (like the number of files in a directory). progress.Maximum = 100; progress.Minimum = 0; progress.Value = 0; progress.Step = 5; // Start a task. for (int i = progress.Minimum; i < progress.Maximum; i += progress.Step) { // (Do work here.) // Increment the progress bar. progress.Step(); }

Organizational Controls

The GroupBox and Panel are two container controls that are used to group related controls. Radio buttons, for example, must be grouped into a container to be associated together as a unit.

The Panel control is similar to the GroupBox control; however, only the Panel control can have scroll bars, and only the GroupBox control displays a caption (set in the Text property). Also, the Panel control supports DockPadding, which makes it a necessary ingredient in complex resizable forms, as you'll see in the next chapter. The GroupBox control does not provide this ability.

 
Chapter 4 - Classic Controls
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

The Date Controls

Retrieving date information is a common task. For example, requiring a date range is a good way to limit database searches. In the past, programmers have used a variety of different controls to retrieve date information, including text boxes that required a specific format of month, date, and year values.

The modern date controls make life much easier. For one thing, they allow dates to be chosen from a graphical calendar view that's easy to use and prevents users from choosing invalid dates (like the 31st day in February, for example). They also allow dates to be displayed in a range of formats.

There are two date controls: DateTimePicker and MonthCalendar. DateTimePicker is ideal for choosing a single date value, and requires the same amount of space as an ordinary drop-down list box. When the user clicks the drop-down button, a full month calendar page is shown. The user can page from month to month (and even year to year) looking for a specific date with the built-in navigational controls. The control handles these details automatically.

The MonthCalendar shows a similar expanded display, with a single month at a time. Unlike the DateTimePicker, it allows the user to choose a range of dates. Both controls are shown in Figure 4-8.

Figure 4-8: The date controls

The DateTimePicker

The DateTimePicker allows a user to choose a single date. One nice thing about the DateTimePicker is that it automatically takes the computer's regional settings into consideration. That means you can specify Short for the DateTimePicker.Format property, and the date might be rendered as yyyy/mm/dd format or dd/mm/yyyy depending on the date settings. Alternatively, you can specify a custom format by assigning a format string to the CustomFormat property, and make sure the date is always presented in the same way on all computers. Figure 4-9 shows the different date formats.

Figure 4-9: Common date formats

The selected date is provided in the Value property. One important detail about date controls is that they always use the System.DateTime date type, which represents a date and time. Depending on your needs, you might configure a date control to show only the day or time portion. In this case, you may need to be careful to retrieve just the appropriate part.

For example, imagine you are using a DateTimePicker control, which allows the user to choose the start date for a database search. The date control is configured to show dates in the long format, which doesn't include time information.

When the form loads, you configure the date control:

dtStart.Value = DateTime.Now; // Sets dtStart to the current date and time.

The user might then click a different date. However, choosing a different date only updates the month, year, and day components of the date. The time component remains, even though it is not displayed!

// The next line performs a search based on date and the original time. // This artificially limits the returned results. string SQLSelect = "SELECT * FROM Orders WHERE Date >'" + dtStart.Value.ToString() + "'";

If you initialized the DateTimePicker at lunchtime, you could lose the first half day from your search.

There are a number of ways to avoid this problem. For example, you can use the DateTime.Date property, which returns another DateTime object that has its time portion set to 0 (midnight).

// This gets the full day. string SQLSelect = "SELECT * FROM Orders WHERE Date >'" + dtStart.Value.Date.ToString() + "'";

You could also use the DateTime.Today property to set the initial value instead of DateTime.Now. This is a good technique for the MonthCalendar control as well. The MonthCalendar automatically sets the time component for the currentValue to 0 when the user selects a date, but if the user leaves the default date unchanged, and you've assigned a date with information, the time portion remains.

You can also use a DateTimePicker to represent a time value with no date component. To do so, set the Format property to Time. You also need to set the UseUpDown property to true. This prevents the drop-down month display from being shown. Use the up/down scroll buttons instead to increment the highlighted time component (hours, minutes, or seconds).

Table 4-9 lists the important properties of the DateTimePicker control.

Table 4-9: DateTimePicker Properties

Properties

Description


CalendarFont, CalendarForeColor, CalendarMonthBackground, CalendarTitleBackColor, CalendarTitleForeColor, and CalendarTrailingForeColor

These properties configure the calendar's font and the color used for parts of its interface. The default colors are provided as static read-only fields for this class (like DefaultTitleForeColor). Note that the CalendarTrailingForeColor changes the color of the "trailing" dates. These are the dates that appear on a month page from the previous month (at the beginning) or from the next month (at the end). They are used to fill in the grid.


ShowCheckBox and Checked

ShowCheckBox displays a small check box inside the drop-down list box. Unless it is checked, the date cannot be modified.


Format and CustomFormat

The Format property specifies a value from the DateTimePickerFormat enumeration. Alternatively, you can manually specify an exact form by assigning a format string to the CustomFormat property (like "yyyy/MM/DD hh:mm:ss").


DropDownAlign

Determines whether the drop-down month page lines up with the left or right of the list box.


MaxDate and MinDate

Sets a maximum and minimum date, beyond which the user cannot select. This is a great tool for preventing error messages by making invalid selections impossible.


ShowUpDown

When set to true, disables the drop-down month pages and uses up/down scroll buttons for incrementing part of the date. This is ideal for time-only values.


Text and Value

Text returns the formatted date as a string, according to how it is currently displayed. Value returns the represented DateTime object.

MonthCalendar

The MonthCalendar control looks like the DateTimePicker, except that it always shows the month page display, and it doesn't allow the user to enter a date by typing it into a list box. That makes the MonthCalendar slightly less useful, except for situations when you need to let the user select a range of contiguous dates.

You set the maximum number of dates that the user can select in the MaxSelectionCount property. The user selects a group of dates by dragging and clicking. Selected dates must always be next to each other. The first and last selected dates are returned as DateTime objects in the SelectionStart and SelectionEnd properties. Figure 4-10 shows a range of four days.

Figure 4-10: Selecting multiple dates

// Set a range of four days. dt.SelectionStart = new DateTime(2003, 01, 17); dt.SelectionEnd = new DateTime(2003, 01, 20);

Depending on your needs, you may still need to perform a significant amount of validation with selected dates to make sure they fit your business rules. Unfortunately, you can't easily use the DateChanged and DateSelected events for this purpose. They only fire after an invalid date has been selected, and you have no way to remove the selection unless you choose a different date range. Information about the original (valid) date range is already lost.

Though the MonthCalendar control looks similar to the DateTimePicker, it provides a different set of properties, adding some features while omitting others. Table 4-10 lists the most important properties.

Table 4-10: MonthCalendar Properties

Property

Description


AnnuallyBoldedDates, MonthlyBoldedDates, and BoldedDates

These properties accept arrays of DateTime objects, which are then shown in bold in the calendar. MonthlyBoldedDates can be set for one month and are repeated for every month, while AnuallyBoldedDates are set for one year and repeated for every year.


FirstDayOfWeek

Sets the day that will be shown in the leftmost column of the calendar.


MaxDate, MinDate, and MaxSelectionCount

Sets the maximum and minimum selectable date in the calendar, and the maximum number of contiguous dates that can be selected at once.


ScrollChange

The number of months that the calendar "scrolls through" every time the user clicks a scroll button.


SelectionEnd, SelectionStart, and SelectionRange

Identifies the selected dates. The SelectionRange property returns a special structure that contains a SelectionEnd and SelectionStart date.


ShowToday and ShowTodayCircle

These properties, when true, show the current day in a special line at the bottom of the control and highlight it in the calendar with a circle, respectively.


ShowWeekNumbers

If true, displays a number next to each week in the year from 1 to 52.


TodayDate and TodayDateSet

TodayDate indicates what date is shown as "today" in the MonthCalendar. If you set this value manually in code, TodayDateSet is true.


TitleBackColor, TitleForeColor, and TrailingForeColor

Sets colors associated with the MonthCalendar. Note that the TrailingForeColor changes the color of the "trailing" dates. These are the dates that appear on a month page from the previous month (at the beginning) or from the next month (at the end). They are used to fill in the grid.

  Caution 

The MonthCalendar control doesn't properly support Windows XP styles. If you try to use this control with a project that uses Windows XP styles, the display does not appear correctly when the user selects more than one date at a time.

 
Chapter 4 - Classic Controls
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

Menus

Applications use two kinds of menus: main and context. Main menus provide a comprehensive set of options just below a window's title bar, and use standard headings like File, Edit, and Help. Context menus are floating or "pop up" menus that provide additional options. Typically, context menus appear when the user right-clicks a user interface element (like an object, or a system tray icon). Like main menus, context menus can contain nested layers of menus, but they tend to be much simpler.

Consider the partial menu shown in Figure 4-11. In .NET, this menu is modeled as a collection of objects, as shown in Figure 4-12. MenuItem objects make up every part of a menu except the top-level container.

Figure 4-11: A sample menu

Figure 4-12: Menu objects in .NET

The next code snippet shows how to create the first part of this menu (all the entries contained under the File heading). Generally, you use the Visual Studio .NET IDE to create the menu and configure items automatically through the custom menu designer.

// Build the menu starting with the deepest nested level // (in this case, New, Open, and Save). MenuItem mnuNew = new MenuItem(); mnuNew.Text = "New"; MenuItem mnuOpen = new MenuItem(); mnuOpen.Text = "Open"; MenuItem mnuSave = new MenuItem(); mnuSave.Text = "Save"; ' Create the top-level File menu. MenuItem mnuFile = new MenuItem(); mnuFile.Text = "File"; ' Add the contained menu items to the File menu. mnuFile.MenuItems.Add(mnuNew); mnuFile.MenuItems.Add(mnuOpen); mnuFile.MenuItems.Add(mnuSave); // Create the main menu container. MainMenu mnuMain = new MainMenu(); // Add the File menu to the main menu. mnuMain.Add(mnuFile); // Attach the main menu to the form. this.Menu = mnuMain;

The next few sections dissect the .NET menu objects.

The Menu Class

In .NET, main menus, context menus, and menu items are all treated slightly differently. All of them inherit from the abstract Menu class (see Figure 4-13).

Figure 4-13: The menu object hierarchy

The Menu class contains a collection of MenuItem objects that corresponds to a single menu level, and provides functionality that lets you clone or merge these items into another menu, and find the top-level menu container. Table 4-11 lists the members of the Menu class.

Table 4-11: Members of the Abstract Menu Class

Member

Purpose


Handle

The internal operating system "handle" (number) that identifies this menu. It could be required for a low-level API call.


IsParent

Returns true if this menu contains other menu items.


MenuItems

The collection of MenuItem objects that represent the next level of the menu.


CloneMenu()

Allows you to copy an array of MenuItem objects into the current menu. This method can be used to transfer items between a MainMenu and a ContextMenu, or vice versa.


GetContextMenu() and GetMainMenu()

Returns the ContextMenu or MainMenu object that contains this menu, even if this menu is nested several layers deep in the hierarchy.

The MainMenu and ContextMenu Classes

At the top level, menu items are always contained in a MainMenu or ContextMenu class. These classes, which inherit from Menu, add a few additional frills. For example, the MainMenu class allows you to determine the form that owns the menu, while the ContextMenu class allows you to find the associated control and react to the Popup event when the menu is displayed (Tables 4-12 and 4-13). As you'll see, MainMenu and ContextMenu can't be treated equivalently in your code. This means that you can't display part of a main menu as a context menu, as you would in a Visual Basic 6 application.

Table 4-12: ContextMenu Members

Member

Purpose


SourceControl

Indicates the control that "owns" this context menu (or null).


Popup event

Allows you to customize the menu when the menu is about to appear but has not yet been displayed. You can use this event to configure the context menu appropriately by hiding, adding, or disabling some items depending on the current state of your application.


Show()

Displays the context menu on the screen, at the indicated position.

Table 4-13: MainMenu Members

Member

Purpose


CloneMenu()

This new (alternative) CloneMenu() method can be called without arguments to create a duplicate copy of the MainMenu object.


GetForm()

Returns the Form object that contains the menu.

  Caution 

Watch out—the MainMenu class adds a new CloneMenu() method for duplicating the menu. If you're transferring a few menu items from one menu to another, you'll want to use the CloneMenu() method inherited from the Menu class instead.

The MenuItem Class

Each command and submenu in a menu is represented by a MenuItem object. The MenuItem class is the most full-featured menu object, with several properties for configuring appearance and reacting to menu events, as described in Table 4-14.

Table 4-14: MenuItem Members

Member

Purpose


BarBreak and Break

These properties allow you to create the unusual menus shown in Figure 4-14. Break, when set to true, instructs .NET to place the menu item in a separate column. For example, if you create an entire submenu of menu items with Break set to true, they appear as a horizontal menu arranged from left to right. BarBreak works identically, except it displays a vertical line as a column separator between menu items. These properties must be set in code—they aren't available in the Properties window.


Checked and RadioCheck

If set to true, a check mark is displayed next to the highlighted menu item. If RadioCheck is set to true, the menu item displays a bullet instead of a check mark when its Checked property is true. Typically the check mark style is used for a value that can be toggled on and off, while the radio button style is used when you provide a mutually exclusive list of menu selections, from which the user can choose only one.


DefaultItem

The default menu item for a menu is displayed in bold type. If the user double-clicks a submenu that contains a default item, the default item is selected automatically, and the submenu is closed. DefaultItem is usually provided as a guide for the user, and indicates the most common choice.


Enabled

Sets whether the menu should be selectable or disabled and displayed in "greyed out" text.


Index

The numeric index of a menu item in its parent's MenuItems collection. You can modify this index to reposition the menu item.


MergeOrder and MergeType

This specifies the behavior for this menu when being merged with another menu, either programmatically through the MergeMenu() method, or automatically with MDI forms. The MergeOrder is a number representing relative position (0, the default, is first in the menu). MergeType defines the merge behavior using one of the values from the MenuMerge enumeration.


OwnerDraw and the DrawItem and MeasureItem events

When OwnerDrawn is set to true, the default menu user interface is not provided for you. Instead, you need to handle the DrawItem and MeasureItem events.


Shortcut and ShowShortcut

Shortcut sets a hotkey from the Shortcut enumeration (like Ctrl+N or F8). ShowShortcut determines whether the shortcut is displayed with the text for the menu item.


Text

Sets the text for menu item. Use the ampersand character to precede the access key (as in E&xit to make "x" the access key).


Visible

If set to false, the menu item is not be shown at all.


CloneMenu()

Duplicates a menu. This method keeps all event handlers intact, and is useful when you need to reuse the same entries in a context menu as in a main menu.


MergeMenu()

Combines the children of two menus into a single list. You could use this technique to create a context menu that contains the entries from two different submenus in the main menu. Just remember to use CloneMenu() first, and then merge the duplicate copy of the menus.


PerformClick() and PerformSelect()

Triggers the appropriate Click or Select event for the MenuItem. These methods are rarely used, except if you are creating wizards or other tools that drive an application by "remote control."


Popup event

Occurs when the menu item's submenu is just about to be displayed. You can use this to dynamically tailor a menu just in time when it is selected. The Popup event is not raised for menus that do not contain submenu items.


Select event

Occurs when a menu item is highlighted, but not selected. This can occur when the menu item is scrolled over by using the keyboard controls, or hovered over with the mouse.


Click event

Occurs when the menu item is clicked with the mouse or triggered with the keyboard.

Figure 4-14: Menu variations

The Visual Studio NET Menu Designer

Visual Studio.NET includes built-in support for creating any type of menus. To start, drag a main menu or context menu onto your form. It will appear in the component tray. To design a menu, click once to select it, and then type text in the onscreen "type here" areas (see Figure 4-15).

Figure 4-15: The Visual Studio .NET menu designer

Every time you add a new menu item, a "type here" area appears for a submenu and a new menu item. You can also drag and drop menu items in any order you want, and configure each menu item's name and properties in the Properties window when it is selected. Right-click a menu item and select Insert Separator to add a horizontal dividing line between menu entries.

Attaching a Menu

Unlike other controls, main menus are not added to the Controls collection of the hosting form. Instead, they are specifically set using the Menu property of the form. Once this link is set, the menu automatically appears at the top of the form.

this.Menu = mnuMain; // Attach the mnuMain MainMenu object.

Context menus use a similar technique with the ContextMenu property.

ctrl.ContextMenu = mnuContext; // Attach the mnuContext ContextMenu object.

Context menus, however, are not automatically shown. Instead, you need to show them manually using the ContextMenu.Show() method. Typically, you perform this task in the MouseUp event for the linked control.

The example that follows displays the control's context menu. It uses the sender parameter, making it completely generic. You could use this event handler for every control with a context menu in your application.

private void lbl_MouseUp(Object sender, System.Windows.Forms.MouseEventArgs e) { // Convert the sender parameter into a valid control reference. Control ctrl = (Control)sender; // If the right mouse button was pressed, show the menu. if (e.Button == MouseButtons.Right) { ctrl.ContextMenu.Show(ctrl, new Point(e.X, e.Y)); } }

Using a control's ContextMenu property is really just a convenience. You can display a context menu at any time, in response to any event, even if you haven't set the ContextMenu property of a nearby control. However, using the ContextMenu property allows you to write a generic method that can handle the MouseUp event for multiple controls. Your code simply needs to retrieve the ContextMenu property of the control that fired the event.

Menu Events

There are two ways you can handle menu selection events. You can write an individual event handler for the Click event of every MenuItem. This is ideal if you are writing all the menu code inside the current form.

private void mnuOpen_Click(System.Object sender, System.EventArgs e) { // (Do something here.) } private void mnuNew_Click(System.Object sender, System.EventArgs e) { // (Do something here.) } private void mnuSave_Click(System.Object sender, System.EventArgs e) { // (Do something here.) }

However, if your menu just hands the task off to another class, it probably makes sense to handle all menu events in the same event handler. (In this case, your event handler acts as a generic switchboard). You can then determine which MenuItem fired the event by converting the sender parameter into a MenuItem object and examining its Text, or just by comparing object references (which is preferred, because the compiler alerts you if you refer to a nonexisting MenuItem, but it doesn't alert you if you enter incorrect menu text). The following code snippet handles the Click event of three MenuItem objects, and compares the event sender to the appropriate form-level variables to determine which item was clicked.

private void mnu_Click(System.Object sender, System.EventArgs e) if (sender == mnuOpen) { MyApp.DoNew(); } else if (sender == mnuNew) { MyApp.DoSave(); } else if (sender == mnuSave) { MyApp.DoOpen(); } }

This approach of handling all menu clicks in one method also provides an easy way to implement the standard MFC logic, where a help string is displayed in another control (typically a status bar) whenever a menu item is highlighted.

Copying and Cloning a Menu

.NET imposes some restrictions on menus. Items cannot belong to more than one menu, and they cannot be shared between types of menus. In many applications, a context menu is actually a subset of a main menu. To set this up with .NET, you need to copy the appropriate branch of the menu.

You might attempt this with the logical-appearing code shown below:

// A flawed approach. ContextMenu mnuContext = new ContextMenu(); // Attempt to copy the menu items from the File menu. foreach (MenuItem mnuItem in mnuFile.MenuItems) { mnuContext.MenuItems.Add(mnuItem.Text); }

Unfortunately, this will copy the items but lose the event handlers. To preserve the event handling logic, you need to use the CloneMenu() method, as shown here:

// A copy operation that preserves event handlers. ContextMenu mnuContext = new ContextMenu(); // Copy the menu items from the File menu into a context menu. foreach (MenuItem mnuItem in mnuFile.MenuItems) { mnuContext.MenuItems.Add(mnuItem.CloneMenu()); }

Merging a Menu

In some cases, you might want to create a context menu that contains the entries from two different submenus. While you could do this by duplicating and manipulating the individual MenuItem objects, the easiest way is by using the built-in MenuItem.MergeMenu() method.

The example below combines the menu items in the top-level File and Edit menus.

// Create a copy of the menus you want to merge. MenuItem mnuMergeFile, mnuMergeEdit; mnuMergeFile = mnuFile.CloneMenu(); mnuMergeEdit = mnuEdit.CloneMenu(); // Merge the duplicate copy of the menus. mnuMergeFile.MergeMenu(mnuMergeEdit); // Now add the merged menu to the appropriate control. this.ContextMenu = mnuMergeFile;

 
Chapter 4 - Classic Controls
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

Owner Drawn Menus

If this is your first look at .NET menus, you may be disappointed to see that they don't support the common Windows convention of using embedded thumbnail bitmaps to help distinguish common items. To solve this problem, you could purchase a third-party control, but luckily the logic is easy to implement on your own. All it requires is a dash of GDI+.

The steps for creating an owner-drawn menu are as follows.

  1. Set the OwnerDraw property for the menu items to true. Note that even when you do this, you can still see the menu item in the design environment and configure its Text property.
  2. Handle the MeasureItem event. This is where you tell .NET how much space you need to display the menu item.
  3. Handle the DrawItem event. This is where you actually write the output for the item to the screen. In our case, this output consists of text and a small thumbnail image.

The first step is easy enough. The second step requires a little bit more work. To specify the required size, you need to set the ItemHeight and ItemWidth properties of the MeasureItemEventArgs class with a value in pixels. However, the size required depends a great deal on the font and text you use. Fortunately, the MeasureItemEventArgs class also provides a reference to the graphics context for the menu, which provides a useful MeasureString() method. This method returns a Size structure that indicates the space required.

To keep your code manageable, you should use a single event handler to measure all the menu items. In our example, this includes a "New," "Open," and "Save" menu entry.

private void mnu_MeasureItem(object sender, System.Windows.Forms.MeasureItemEventArgs e) { // Retrieve current item. MenuItem mnuItem = (MenuItem)sender; Font menuFont = new Font("Tahoma", 8); // Measure size needed to display text. // We add 30 pixels to the width to allow a generous spacing for the image. e.ItemHeight = (int)e.Graphics.MeasureString(mnuItem.Text, menuFont).Height + 5; e.ItemWidth = (int)e.Graphics.MeasureString(mnuItem.Text, menuFont).Width + 30; }

When displaying a drop-down menu, Windows automatically measures the size of each item, and uses the greatest required width for all menu items.

The final step of displaying the menu item is similarly straightforward. You must find the appropriate picture, and write both the text and image to the screen using the graphics context provided by the DrawItem event handler and the DrawString() and DrawImage() methods. The result is shown in Figure 4-16.

Figure 4-16: An owner-drawn menu

private void mnu_DrawItem(object sender, System.Windows.Forms.DrawItemEventArgs e) { // Retrieve current item. MenuItem mnuItem = (MenuItem)sender; // This defaults to the highlighted background if the item is selected. // Otherwise, it is the default grey background. e.DrawBackground(); // Retrieve the image from an ImageList control. Image menuImage = imgMenu.Images[mnuItem.Index]; // Draw the image. e.Graphics.DrawImage(menuImage, e.Bounds.Left + 3, e.Bounds.Top + 2); // Draw the text with the supplied colors and in the set region. e.Graphics.DrawString(mnuItem.Text, e.Font, new SolidBrush(e.ForeColor), e.Bounds.Left + 25, e.Bounds.Top + 3); }

Note that this code uses the Windows standard font and colors, which are provided in properties like e.Font and e.ForeColor. Alternatively, you could create your own Color or Font objects and use them. The next example shows more flexible custom formatting.

  Tip 

You don't need to worry about invisible menu items. If the Visible property is set to false, .NET will not fire the MeasureItem and DrawItem events.You would, however, have to add the drawing logic if you wanted to let your custom menu draw separator items (when the menu text is set to "-"), checkmarks, or greyed out text and images (when the menu item is disabled).

An Owner Drawn Menu Control

Writing the correct code in the MeasureItem and DrawItem event handlers requires some tweaking of pixel offsets and sizes. Unfortunately, in our current implementation there is no easy way to reuse this logic for different windows (not to mention different applications). A far better approach is to perfect your menu as a custom control, and then allow this control to be reused in a variety of projects and scenarios.

The following example adopts this philosophy, and shows a menu control that provides Image, Font, and ForeColor properties. This custom menu item handles its own drawing logic. All the client code needs to do is set the appropriate properties. The code also extends the previous example by correctly drawing disabled menu items.

using System; using System.Windows.Forms; using System.Drawing; using System.Drawing.Text; public class ImageMenuItem : MenuItem { private Font font; private Color foreColor; private Image image; public Font Font { get { return font; } set { font = value; } } public Image Image { get { return image; } set { image = value; } } public Color ForeColor { get { return foreColor; } set { foreColor = value; } } public ImageMenuItem(string text, Font font, Image image, Color foreColor) : base(text) { this.Font = font; this.Image = image; this.ForeColor = foreColor; this.OwnerDraw = true; } public ImageMenuItem(string text, Image image) : base(text) { // Choose a suitable default color and font. this.Font = new Font("Tahoma", 8); this.Image = image; this.ForeColor = SystemColors.MenuText; this.OwnerDraw = true; } protected override void OnMeasureItem( System.Windows.Forms.MeasureItemEventArgs e) { base.OnMeasureItem(e); // Measure size needed to display text. e.ItemHeight = (int)e.Graphics.MeasureString(this.Text, this.Font).Height + 5; e.ItemWidth = (int)e.Graphics.MeasureString(this.Text, this.Font).Width + 30; } protected override void OnDrawItem(System.Windows.Forms.DrawItemEventArgs e) { base.OnDrawItem(e); // Determine whether disabled text is needed. Color textColor; if (this.Enabled == false) { textColor = SystemColors.GrayText; } else { e.DrawBackground(); if ((e.State & DrawItemState.Selected) == DrawItemState.Selected) { textColor = SystemColors.HighlightText; } else { textColor = this.ForeColor; } } // Draw the image. if (this.Image != null) { if (this.Enabled == false) { ControlPaint.DrawImageDisabled(e.Graphics, this.Image, e.Bounds.Left + 3, e.Bounds.Top + 2, SystemColors.Menu); } else { e.Graphics.DrawImage(Image, e.Bounds.Left + 3, e.Bounds.Top + 2); } } // Draw the text with the supplied colors and in the set region. e.Graphics.DrawString(this.Text, this.Font, new SolidBrush(textColor), e.Bounds.Left + 25, e.Bounds.Top + 3); } }

Because this class inherits from MenuItem, you can add instances of it to any MenuItems collection. Here's an example that creates the same menu you considered in your previous example by using the ImageMenuItem control:

mnuFile.MenuItems.Add(new ImageMenuItem("New", imgMenu.Images[0])); mnuFile.MenuItems.Add(new ImageMenuItem("Open", imgMenu.Images[1])); mnuFile.MenuItems.Add(new ImageMenuItem("Save", imgMenu.Images[2]));

Alternatively, you can use the other supplied constructor to configure an unusual font and color combination. For example, the code that follows creates a submenu that lists every font, with each name displayed in its own typeface (see Figure 4-17).

Figure 4-17: Displaying a list of installed fonts

InstalledFontCollection fonts = new InstalledFontCollection(); foreach (FontFamily family in fonts.Families) { try { mnuFonts.MenuItems.Add(new ImageMenuItem(family.Name, new Font(family, 10), null, Color.CornflowerBlue)); } catch { // Catch invalid fonts/styles and ignore them. } }

Unfortunately, there is no easy way to insert an ImageMenu object into a menu using the integrated Visual Studio .NET menu designer. If you want to bridge this gap, you would have to create a custom MainMenu and ContextMenu objects, and then develop custom designers for them that would allow ImageMenu objects to be inserted. Chapter 8 introduces custom designers.

  Tip 

You can mix owner-drawn ImageMenu objects and ordinary MenuItem objects in the same menu without any complications.

Now that you have a grip on the basic suite of .NET controls, the remainder of the chapter dives into two more interesting topics: adding drag-and-drop ability, and creating advanced validation code.

 
Chapter 4 - Classic Controls
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

Drag and Drop

Drag-and-drop operations aren't quite as common today as they were a few years ago, because programmers have gradually settled on other methods of copying information that don't require holding down the mouse button (a technique that many users find difficult to master). For example, a drawing program is likely to use a two-step operation (select an object, and then draw it on) rather than a single drag-and-drop operation. Programs that do support drag-and-drop often use it as a shortcut for advanced users, rather than a standard way of working.

Drag-and-drop is also sometimes confused with the ability to "drag" a picture or piece of user interface around a window. This "fake" drag-and-drop is useful in drawing and diagramming applications (including the drawing application developed in 13), but it needs to be coded manually. In this section, you will learn about both types of dragging operations.

Fake Drag and Drop

True drag-and-drop is a user-initiated way to exchange information between two controls. You don't need to use drag-and-drop events to create objects that the user can move around the form (Figure 4-18). For example, consider the following program that allows a user to click on a picture box, drag it around, and release it somewhere else on the form.

Figure 4-18: Dragging a control around

Conceptually, a control is being dragged and dropped, but all the logic takes place in the appropriate mouse handling events. A Form level isDragging variable keeps track of when fake drag-and-drop mode is currently switched on.

// Keep track of when fake "drag and drop" mode is enabled. private bool isDragging = false; // Store the location where the user clicked the control. private int clickOffsetX, clickOffsetY; // Start dragging. private void lblDragger_MouseDown(System.Object sender, System.Windows.Forms.MouseEventArgs e) { isDragging = true; clickOffsetX = e.X; clickOffsetY = e.Y; } // End dragging. private void lblDragger_MouseUp(System.Object sender, System.Windows.Forms.MouseEventArgs e) { isDragging = false; } // Move the control (during dragging). private void lblDragger_MouseMove(System.Object sender, System.Windows.Forms.MouseEventArgs e) { if (isDragging == true) { // The control coordinates are converted into form coordinates // by adding the label position offset. // The offset where the user clicked in the control is also // accounted for. Otherwise, it looks like the top-left corner // of the label is attached to the mouse. lblDragger.Left = e.X + lblDragger.Left - clickOffsetX; lblDragger.Top = e.Y + lblDragger.Top - clickOffsetY; } }

There are three components that factor into the position calculation:

  • The e.X and e.Y parameters provide the position of the mouse over the control, where (0,0) is the top-left corner of the control.
  • The lblDragger.Left and lblDragger.Top properties give the position between the top-left corner of the control, and the top-left corner of the form.
  • The ClickOffsetX and ClickOffsetY variables give the position between the control's top-left corner and where the user actually clicked to start dragging. By taking this into account, the label acts as though it is "glued" to the mouse at that point.

Authentic Drag and Drop

Real drag-and-drop operations are quite a bit different. Essentially, they work like this:

  1. The user clicks a control and holds the mouse button down. At this point, some information is set aside and a drag-and-drop operation begins.
  2. The user moves the mouse over another control. If this control can accept the current type of content (for example, a picture or text) the mouse cursor changes to a special drag-and-drop icon. Otherwise, the mouse cursor becomes a circle with a line drawn through it.
  3. When the user releases the mouse button, the control receives the information, and decides what to do with it.

Unlike our fake drag-and-drop example, a real drag-and-drop operation can easily take place between controls, or even two different applications, as long as the drag-and-drop contract is followed.

The example program below uses drag-and-drop to take a picture from a label control and draw it onto a picture box control. The actual drawing operation uses GDI+ methods that you examine in more detail later in this book. All other details are generic parts of any drag-and-drop application (Figure 4-19). You'll find the complete code with the samples for this chapter under the project name AuthenticDragAndDrop.

Figure 4-19: A sample drag-and-drop application

The first step is to configure the picture box control to accept dropped information.

picDrawingArea.AllowDrop = true;

To start the drag-and-drop, you can use the DoDragDrop() method of the source control. In this case, it is one of three labels. Dragging is initiated in the MouseDown event for the label.

private void lbl_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e) { Label lbl = (Label)sender; lbl.DoDragDrop(lbl.Image, DragDropEffects.Copy); }

The same event handler handles the MouseDown event for each label. In the event handler, the generic sender reference (which points to the object that sent the event) is converted into a label. Then, a drag-and-drop copy operation is started. The information associated with this operation is the image from the label control.

To allow the picture box to receive information, you need to verify that the information is the correct type in the DragEnter event, and then set a special event argument (e.Effect). DragEnter occurs once when the mouse moves into the bounds of the control.

private void picDrawingArea_DragEnter(object sender, System.Windows.Forms.DragEventArgs e) { if (e.Data.GetDataPresent(DataFormats.Bitmap)) { e.Effect = DragDropEffects.Copy; } else { e.Effect = DragDropEffects.None; } }

The last step is to respond to the information once it is dropped, by handling the DragDrop event. You can do anything you want with the dropped information. In the current example, a GDI+ drawing operation is started (although it could make just as much sense to create a new object in that location and set its Image property).

private void picDrawingArea_DragDrop(object sender, System.Windows.Forms.DragEventArgs e) { Graphics g = picDrawingArea.CreateGraphics(); g.DrawImage((Image)e.Data.GetData(DataFormats.Bitmap), new Point(e.X - this.Left - 12, e.Y - this.Top - 30)); }

Note that the event handler provides screen coordinates, which must be converted into the appropriate coordinates for the picture box.

Practically, you can exchange any type of object through a drag-and-drop operation. However, while this free-spirited approach is perfect for your applications, it isn't wise if you need to communicate with other applications. If you want to drag-and-drop into other applications, you should use data from a managed base class (like String or Image), or an object that implements ISerializable or IDataObject (which allows .NET to transfer your object into a stream of bytes, and reconstruct the object in another application domain).

 
Chapter 4 - Classic Controls
 
byMatthew MacDonald  
2002
Companion Web Site
 

Validation

The best possible way to prevent invalid input is to make it impossible for users to enter it. You accomplish this by forcing users to choose from lists, and creating custom controls that automatically format data and ignore invalid key presses. Of course, sometimes this task is too daunting and you need to settle on the next best thing, which is checking for errors after the fact. If you take this approach, it's important that you report the error as soon as possible, preferably before the user continues to enter more information. The easiest way is to react to validation events.

Validation events were designed to let you check information as soon as it is entered, rather than waiting for the whole form to be submitted. This kind of instantaneous error checking is very useful:

  • Without it, users might be afraid to submit a form because they know there is a possible error.
  • Users might enter several pieces of invalid data at the same time. If you don't check the data until the form is submitted, your program then has to find some way to report about all the mistakes at once.
  • By the time users submit a form, they might have already forgotten about the particular field they entered incorrectly.

Validation solves this information by checking the field as soon as the user is finished entering it and changes focus to another control (either to enter new information, like choosing a text box, or to perform an action, like clicking a button).

In the past, developers have tried to create "do-it-yourself" validation by responding to a control's LostFocus event. The problem with this event is that it occurs after the focus has already moved on. If you reset the focus because of invalid input, another control then receives its own LostFocus event. If both controls have invalid data, they may fight endlessly between each other, trying to move the focus somewhere else.

.NET handles this problem with the Validating and Validated events. These events occur after the user has chosen to move to another control (for example, by pressing the Tab key), but before the focus has been changed, in the following order:

  1. Leave
  2. Validating
  3. Validated
  4. LostFocus

The Validated event allows you to respond to correctly entered data. The Validating event is more useful. It allows you to verify the data and, if it fails the test, stop the focus from moving to the new control.

Validation only takes place if the source control (the control to be validated) has the CausesValidaton property set to true. In addition, the validation won't take place until the focus changes to a control that also has its CausesValidation property set to true. Table 4-15 shows some examples of what can happen when tabbing from one control to another.

Table 4-15: .NET Validation

Source Control

Destination Control

Result


CausesValidation is false

Doesn't matter

Validation code is ignored.


CausesValidation is true

CausesValidation is true

Validation is performed for the source control.


CausesValidation is true

CausesValidation is false

Validation is postponed until the focus changes to a CausesValidation control. At this point, all the controls that need to be validated are validated in order, until one is found with invalid input and the process is cancelled.


A Validation Example

The program shown in Figure 4-20 uses validation to verify that neither text box is left blank. If the user tries to change focus without entering any information, a message box appears, and the focus is reset to the empty text box.

Figure 4-20: A validation example

The validation code for this application is shown here:

private void txtName_Validating(object sender, System.ComponentModel.CancelEventArgs e) { if (((TextBox)sender).Text == "") { MessageBox.Show("You must enter a first and last name.", "Invalid Input", MessageBoxButtons.OK, MessageBoxIcon.Warning); e.Cancel = true; } }

Note that buttons handle validation differently than other controls. They don't validate on a focus change except when they are clicked. If the user tries to click a button and validation fails, the focus is reset, and the Click event is ignored. Attempting to close with the top-right close button (displayed as an "X") also triggers validation. (This creates a problem if you need to let users escape from a form without completing the operation. The solution is to create a Cancel button that closes the form, and has its CausesValidation property set to false.)

Validating with the ErrorProvider

Interrupting users with a message box is a relatively crude way of alerting them to an error. It's better to provide some kind of onscreen indication about the problem, like an explanatory error message next to the incorrect input.

The .NET framework provides an elegant way to accomplish this with its new error provider control. The ErrorProvider displays a special error icon next to an invalid control. If the user hovers the mouse above the control, a detailed message appears (see Figure 4-21).

Figure 4-21: The ErrorProvider

The ErrorProvider is a special provider control. You add it once to your form, and you can use it to display an error icon next to any control. To add the ErrorProvider, drag it into the component tray, or create it manually in code. In the latter case, make sure you keep a form-level reference to use later.

You show the error icon next to a control using the ErrorProvider.SetError() method. The following code segment shows the same text box validating code, but is rewritten to indicate the error using the ErrorProvider control instead of a message box.

private void txtName_Validating(object sender, System.ComponentModel.CancelEventArgs e) { Control ctrl = (Control)sender; if (ctrl.Text == "") { errProvider.SetError(ctrl, "You must enter a first and last name."); } else { errProvider.SetError(ctrl, ""); } }

  Note 

The ErrorProvider control can serve any number of input controls on the same form, and display as many simultaneous error icons and warning messages as needed. Every warning icon automatically appears to the immediate right of the input control; there is no way to place it explicitly.

Note that you must explicitly clear the error message after validation succeeds. In this example, the validation event doesn't cancel the action; it just displays the error icon. This is a more user-friendly alternative, but it means that you need to explicitly check if the form has any errors before allowing users to continue if they have clicked on the OK button.

private void cdmOK_Click(object sender, System.EventArgs e) { if (errProvider.GetError(txtFirstName) == "" && errProvider.GetError(txtLastName) == "") { this.Close(); } else { MessageBox.Show("You still have invalid input.", "Invalid Input", _ MessageBoxButtons.OK, MessageBoxIcon.Warning); } }

If you have a lot of controls, it makes more sense to iterate through the whole collection, rather than writing code checking each control individually. In the following example, the validation controls are all contained inside a single group box named grpValidation, so the code iterates its collection of child controls.

private void cmdOK_Click(object sender, System.EventArgs e) { bool invalidInput = false; foreach (Control ctrl in this.grpValidation.Controls) { if (errProvider.GetError(ctrl) != "") { invalidInput = true; break; } } if (invalidInput) { MessageBox.Show("You still have invalid input.", "Invalid Input", MessageBoxButtons.OK, MessageBoxIcon.Warning); } else { this.Close(); } }

Validating with Regular Expressions

The ErrorProvider control is an ideal way to weave error feedback into your application. However, writing the actual validation code can still be painful and time consuming. One way to streamline your work is to use the .NET regular expression classes, which allow you to search text strings for specific patterns.

Here's an example that validates an email address, by verifying that it contains an "at" symbol (@) and period (.) and doesn't include spaces or special characters. Unlike our previous example, this code is performed in the KeyPress event handler, which ensures that the error provider icon is updated immediately after any change.

private void txtEmail_KeyPress(object sender, System.Windows.Forms.KeyPressEventArgs e) { System.Text.RegularExpressions.Regex regex; regex = new System.Text.RegularExpressions.Regex(@"S+@S+.S+"); Control ctrl = (Control)sender; if (regex.IsMatch(ctrl.Text)) { errProvider.SetError(ctrl, ""); } else { errProvider.SetError(ctrl, "Not a valid email."); } }

  Tip 

In C#, you can precede a string with the "at" symbol (@) in order to indicate that it is a string literal. In this case, all character sequences that start with a backslash () will be interpreted as backslashes, not special escape sequences. This ability is very useful when dealing with regular expressions, which use the backslash character frequently.

Regular expressions almost constitute an entire language of their own, with special characters and metacharacters. Most programmers and organizations create their own regular expression classes that provide commonly used expressions. One possible example is shown below.

public class RegularExpressions { public const string Email = @"S+@S+.S+"; // 4-10 character password that starts with a letter. public const string Password = @"[a-zA-Z]w{3,9}"; // A sequence of 3-2-4 digits, with each group separated by a dash. public const string SSN = @"d{3}-d{2}-d{4}"; }

Once you have created this type of resource class, you can use it easily to create a RegEx object:

Regex expression = new Regex(RegularExpressions.Email);

A brief list of some common regular expression metacharacters is shown in Table 4-16. You can use these characters to create your own regular expressions.

Table 4-16: Regular Expression Metacharacters

Character

Matches


*

Zero or more occurrences of the previous character or subexpression. For example, a*b matches aab or just a.


+

One or more occurrences of the previous character or subexpression. For example, a+b matches aab but not a.


( )

Groups a subexpression that is treated as a single element. For example, (ab)+ matches ab and ababab.


|

Either of two matches. For example, a|b matches a or b.


[ ]

Matches one character in a range of valid characters. For example, [A-C] matches A, B, or C.


[^ ]

Matches a character that is not in the given range. For example, [^A-C] matches any character except A, B, and C.


.

Any character except newline.


s

Any whitespace character (like a tab or space).


S

Any non-whitespace character (like a tab or space).


d

Any digit character.


D

Any character that is not a digit.


w

Any word character (letter, number, or underscore).

However, it's often easier to look up a premade regular expression that suits your data using the Internet or a dedicated book on the subject.

 
Chapter 4 - Classic Controls
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

The Last Word

This chapter has toured through the most common Windows controls, and demonstrated a few .NET twists like owner-drawn menus and the ErrorProvider control. You've also learned about the basic types of controls, the techniques you can use for drag-and-drop support, and the best ways to integrate validation code into your forms. The next chapter continues with the last core topic for Windows user interface programming: Forms.

 
Chapter 5 - Forms
 
byMatthew MacDonald  
Apress 2002
Companion Web Site
 

Forms

Категории