Forms

Windows are the basic ingredients in any desktop application—so basic that the operating system itself is named after them. However, there's a fair deal of subtlety in exactly how you use a window, not to mention how you resize its content. This subtlety is what makes windows (or forms, to use .NET terminology) one of the most intriguing user interface topics.

This chapter explores how forms interact and take ownership of one another, and how forms are used to manage events. It also examines the basic classes involved, and considers the far from trivial problem of resizable windows. You learn how to design split-window interfaces, use the dazzling Windows XP control styles, and create irregularly shaped windows that will amaze your programming colleagues. Finally, the end of this chapter considers the advantages and limitations of visual inheritance, which offers an elegant way to create form templates.

The Form Class

The Form class is a special type of control that represents a complete window. It almost always contains other controls. The Form class does not derive directly from Control; instead, it acquires additional functionality through two extra layers, as shown in Figure 5-1.

click to expand
Figure 5-1: The Form class lineage

The Form class provides a number of basic properties that determine appearance and window style. Many of these properties (listed in Table 5-1) will be familiar if you are a seasoned Windows programmer because they map to styles defined by the Windows API.

Table 5-1: Basic Style Properties

Member

Description


FormBorderStyle

Specifies a value from the FormBorderStyle enumeration that identifies the type of window border, including its appearance and whether or not it can be resized.


ControlBox

Boolean property that determines whether the window has the system menu box at the top left corner.


MaximizeBox

Boolean property that determines if the window has the maximize box at the top right corner.


MinimizeBox

Boolean property that determines if the window has the minimize box at the top right corner.


HelpButton

Boolean property that determines if the window has the Help question mark icon at the top right corner. This button, previously used to trigger context-sensitive Help, has fallen into disuse in recent years.


Icon

References the System.Drawing.Icon object that is used to draw the window icon in the top left corner.


ShowInTaskBar

Boolean property that determines whether the window has an associated box that appears in the Windows task bar. This is generally used for main forms, but is not required for secondary windows like configuration forms, About boxes, and modal dialogs.


SizeGripStyle

Determines whether the sizing grip is shown on the bottom right corner of the window.


WindowState

Identifies (and allows you to configure) the current state of a resizable window. Possible values are Normal, Maximized, and Minimized.


TopMost

When set to true, this window is always displayed on top of every other window in your application, regardless of form ownership. This can be a useful setting for tool windows.


Opacity

A percentage value that makes a form partially transparent if set to a value less than 100 percent. For example, if you set this to 10 percent, the form and all its controls are almost completely invisible, and the background window clearly shows through. This feature is only supported on Windows 2000 or later operating system. While interesting, this feature should not be used by the application programmer.


TransparencyKey

Identifies a color that becomes transparent. Any occurrence of this color becomes invisible whether it is in the form background, another control, or even a picture contained inside a control. These transparent settings act like "holes" in your window. You can even click to activate another window if you see it through a transparent region. This feature is only supported on Windows 2000 or later. Generally, if you need this type of functionality you are better off creating an irregularly shaped form, as described later in this chapter.

The Form class defines references to two special buttons, as shown in Table 5-2. These properties add automatic support for the Enter and Escape keys.

Table 5-2: Special Form Buttons

Member

Description


AcceptButton

The button referenced by this property is automatically "clicked" when the user presses the Enter key. This is also sometimes known as the default button. It should always be the least threatening button. Typically, this is a form's OK or Close button, unless that button could accidentally commit irreversible changes.


CancelButton

The button referenced by this property is automatically "clicked" when the user presses the Esc key. This is usually a Cancel button.

As you saw in Chapter 2.

The Form class also defines some events of its own. These events (shown in Table 5-3) allow you to react when the form acquires focus, is about to be closed, or is first loaded into memory.

Table 5-3: Form Events

Event

Description


Activated and Deactivated

These events are the form equivalent of the LostFocus and GotFocus events for a control. Deactivated occurs when the user clicks a different form in the application, or moves to another application. Activated occurs when the user switches to the window. You can also programmatically set the active form with the Activate() method, and you can retrieve the active form by inspecting the static ActiveForm property.


Load

Occurs when the form first loads. It gives you the chance to perform additional control initialization (like accessing a database and filling a list control).


Closing

Occurs when the form is about to close as a result of the user clicking the close button or the programmatic use of the Close() method. The CancelEventArgs object provides a Cancel property that you can set to true to force the form to remain open. Event handlers for this event often provide a message box prompting the user to save the document. This message box typically provides Yes, No, and Cancel buttons. If Cancel is selected, the operation should be cancelled and the form should remain open.


Closed

Occurs when the form has closed.

Finally, every form has a special designer region, which contains the constructor and an InitializeComponent() method that is executed immediately when the form object is created. The code in the designer region creates all the controls and sets all the properties that you have configured at design time. Even for a simple window, this code is quite lengthy, and shouldn't be modified directly (as Visual Studio .NET may become confused, or simply overwrite your changes). However, the hidden designer region is a great place to learn how to dynamically create and configure a control. For example, you can create a control at design time, set all its properties, and then simply copy the relevant code, almost unchanged, into another part of your code to create the control dynamically at runtime.

The previous chapters have presented the skeleton structure of a custom Form class, which is detailed here:

public class myForm : System.Windows.Forms.Form
{
 // (Control member variable declarations go here.)

 public myForm()
 {
 InitializeComponent();
 }
#region " Windows Form Designer generated code "
 private void InitializeComponent()
 {
 // (Control and form initialization code goes here.)
 }
#endregion

 // (Your custom form-level variables go here)

 // (Your control event handling code goes here.)
}

In the next few sections, you examine more advanced properties of the Form class and the classes it inherits from. You also learn the basic approaches for showing and interacting with forms.

Form Size and Position

The Form class provides the same Location and Size properties that every control does, but with a twist. The Location property determines the distance of the top left corner of the window from the top left corner of the screen (or desktop area). Furthermore, the Location property is ignored unless the StartPosition property is set to Manual. The possible values from the FormStartPosition enumeration are shown in Table 5-4.

Table 5-4: StartPosition Values

Value (from the FormStartPosition enumeration)

Description


CenterParent

If the form is displayed modally, the form is centered relative to the form that displayed it. If this form doesn't have a parent form (for example, if it's displayed modelessly), this setting is the same as WindowsDefaultLocation.


CenterScreen

The form is centered in the middle of the screen.


Manual

The form is displayed in the location specified by the Location property, relative to the top left corner of the desktop area.


WindowsDefaultLocation

The form is displayed in the Windows default location. In other words, there's no way to be sure exactly where it will end up.


WindowsDefaultBound

The form is displayed in the Windows default location, and with a default size (the Size property is ignored). This setting is rarely used, because you usually want exact control over a form's size.

Sometimes you need to take a little care in choosing an appropriate location and size for your form. For example, you could accidentally create a window that is too large to be accommodated on a low-resolution display. If you are working with a single-form application, the best solution is to create a resizable form. If you are using an application with several floating windows the answer is not as simple.

You could just restrict your window positions to locations that are supported on even the smallest monitors, but that's likely to frustrate higher-end users (who have purchased better monitors for the express purpose of fitting more information on their screen at a time). In this case, you usually want to make a runtime decision about the best window location. To do this, you need to retrieve some basic information about the available screen real estate using the Screen class.

Consider the following example that manually centers the form when it first loads using the Screen class. It retrieves information about the resolution of the screen using the Screen.PrimaryScreen property.

private void dynamicSizeForm_Load(System.Object sender,
 System.EventArgse)
{
 Screen scr = Screen.PrimaryScreen;
 this.Left = (scr.WorkingArea.Width - this.Width) / 2;
 this.Top = (scr.WorkingArea.Height - this.Height) / 2;
}

The members of the Screen class are listed in Table 5-5.

Table 5-5: Screen Members

Member

Description


AllScreens (static)

Returns an array of Screen objects, with one for each display on the system. This method is useful for systems that use multiple monitors to provide more than one desktop (otherwise, it returns an array with one Screen object).


Primary (static)

Returns the Screen object that represents the primary display on the system.


Bounds

Returns a Rectangle structure that represents the bounds of the display area for the current screen.


GetBounds() (static)

Accepts a reference to a control, and returns a Rectangle representing the size of the screen that contains the control (or the largest portion of the control if it is split over more than one screen).


WorkingArea

Returns a Rectangle structure that represents the bounds of the display area for the current screen, minus the space taken for the taskbar and any other docked windows.


GetWorkingArea() (static)

Accepts a reference to a control, and returns a Rectangle representing the working area of the screen that contains the control (or the largest portion of the control, if it is split over more than one screen).


DeviceName

Returns the device name associated with a screen as a string.

A common requirement for a form is to remember its last location. Usually, this information is stored in the registry. The code that follows shows a helper class that automatically stores information about a form's size and position using a key based on the name of a form.


using System;
using Microsoft.Win32;

public class RegistryForm
{
 public static string RegPath = @"SoftwareApp";

 public static void SaveSize(System.Windows.Forms.Form frm)
 {
 // Create or retrieve a reference to a key where the settings
 // will be stored.
 RegistryKey key;
 key = Registry.LocalMachine.CreateSubKey(RegPath + frm.Name);

 key.SetValue("Height", frm.Height);
 key.SetValue("Width", frm.Width);
 key.SetValue("Left", frm.Left);
 key.SetValue("Top", frm.Top);
 }

 public static void SetSize(System.Windows.Forms.Form frm)
 {
 RegistryKey key;
 key = Registry.LocalMachine.OpenSubKey(RegPath + frm.Name);

 // If the value isn't found the value from the passed in form object is
 // used instead, which effectively leaves the size and location
 // unchanged.
 frm.Height = (int)key.GetValue("Height", frm.Height);
 frm.Width = (int)key.GetValue("Width", frm.Width);
 frm.Left = (int)key.GetValue("Left", frm.Left);
 frm.Top = (int)key.GetValue("Top", frm.Top);
 }
}
  Note 

This example uses the HKEY_LOCAL_MACHINE branch of the registry, which means that changes are global for the current computer.You might want to use HKEY_CURRENT_USER instead to allow user-specific window settings. In this case, just use the Registry.CurrentUser value instead of Registry.LocalMachine.

To use this class in a form, you call the SaveSize() method when the form is closing:

private void MyForm_Closing(object sender,
 System.ComponentModel.CancelEventArgs e)
{
 registryForm.SaveSize(this);
}

and call the SetSize() method when the form is first opened:

private void MyForm_Load(object sender, System.EventArgs e)
{
 registryForm.SetSize(this);
}

In each case, you pass a reference to the form you want the Helper class to inspect.

Scrollable Forms

The Form class inherits some built-in scrolling support from the ScrollableControl class. Generally, forms do not use these features directly. Instead, you will probably use scrollable controls like rich text boxes to display scrollable document windows. However, these features are still available, rather interesting, and effortless to use.

Figure 5-2 shows a form that has its AutoScroll property set to true. This means that as soon as a control is added to the form that does not fit in its visible area, the required scrollbars will be displayed. The scrolling process takes place automatically.

click to expand
Figure 5-2: A scrollable form

  Note 

To get a scrollable form to display correctly, you may need to call the form's Refresh() method to update the window. In some tests, problems occurred when this method was not used.

If Figure 5-2 looks a little strange, that's because it is. Scrollable forms make a few appearances in Windows applications (Microsoft Access is one example) but are relatively rare. They should be discouraged as unconventional. Instead, it probably makes more sense to use another class that derives from ScrollableControl, like Panel (see Figure 5-3).

click to expand
Figure 5-3: A scrollable panel

By default, scrollbars aren't shown unless a control is off the edge of the form, or you explicitly set the Boolean HScroll and VScroll properties. However, you can configure an AutoScrollMinSize, which specifies the required space, in pixels, between each control and the window border. If this minimum space is not provided, scroll bars are shown.

The Form class doesn't derive directly from ScrollableControl. Instead, it derives from the ContainerControl (which, in turn, derives from ScrollableControl). Like the ScrollablControl class, the ContainerControl class doesn't provide many members that you are likely use. It includes a ProcessTabKey() method that the .NET framework uses transparently to manage focus, a ParentForm property that identifies the form that contains this control, and an ActiveControl property, that identifies or sets the control that currently has focus.

Showing a Form

To display a form, you need to create an instance of the Form class and use the Show() or ShowDialog() method.

MainForm frmMain = new MainForm();
frmMain.Show();

The Show() method creates a modeless window, which doesn't stop code from executing in the rest of your application. That means you can create and show several modeless windows, and the user can interact with them all at once. When using modeless windows, synchronization code is sometimes required to make sure that changes in one window update the information in another window to prevent a user from working with invalid information.

The ShowDialog() method, on the other hand, interrupts your code. Nothing happens on the user interface thread of your application until the user closes the window (or the window closes in response to a user action). The controls for all other windows are "frozen," and attempting to click a button or interact with a control has no effect (other than an error chime, depending on Windows settings). This makes the window ideal for presenting the user with a choice that needs to be made before an operation can continue. For example, consider Microsoft Word which shows its Options and Print windows modally, forcing you to make a decision before continuing. On the other hand, the windows used to search for text or check the spelling in a document are shown modelessly, allowing the user to edit text in the main document window while performing the task. You can also use a different version of the ShowDialog() method that accepts a reference to a form in your application. The code in that form will be halted until the modal window is closed, but the user can use other windows in your application, if they exist.

And of course, you are probably already keenly aware that your application needs an entry point, typically in the form of a static Main method. This method creates an instance of the initial form and uses the Application class to start a message loop, ensuring that your application stays alive until this window is closed.

public static void Main()
{
 Application.Run(new MyForm());
}

This represents a basic piece of form infrastructure. In the code examples in this book, I rarely include the entry point or the Windows designer code, both of which can cloud the example at hand.

  Tip 

If you are designing a multithreaded application, it is possible to show windows on more than one thread. In this case, a modal window only stops the code on its thread, and your application may show more than one modal window at once. However, it is strongly recommended that multithreaded applications use one thread for user interface code. This prevents synchronization problems that can occur if a thread tries to access a user interface control that it did not create (and therefore does not legitimately own).

Custom Dialog Windows

Often when you show a dialog window, you are offering the user a choice. The code that displays the window waits for the result of that choice, and then acts on it.

You can easily accommodate this design pattern by creating some sort of public variable on the dialog form. When the user makes a selection in the dialog window, this special variable is set, and the form is closed. Your calling code can then check for this variable and determine what to do next based on its value. (Remember, even when a form is closed, the form object and all its control information still exists until the variable referencing it goes out of scope.)

For example, consider the form shown in Figure 5-4, which provides two buttons: Cancel and OK.

click to expand
Figure 5-4: A simple dialog form

The form class provides a UserSelection property, which uses a custom enumeration to identify the action that was used to close the window:

public class DialogForm : System.Windows.Form
{
 // (Windows designer code omitted.)

 enum SelectionTypes
 {
 OK,
 Cancel
 }

 // This property must be public so the caller can access it.
 public SelectionTypes UserSelection;

 private void cmdOK_Click(object sender, EventArgs e)
 {
 UserSelection = SelectionTypes.OK;
 this.Hide();
 }

 private void cmdCancel_Click(object sender, EventArgs e)
 {
 UserSelection = SelectionTypes.Cancel;
 this.Hide();
 }
}

The code that creates the form shows it modally. It then checks the UserSelection property after the window is closed to determine what action the user selected:

DialogForm frmDialog = new DialogForm();
frmDialog.ShowDialog();

// The code uses a custom enumeration to make the code readable and less
// error-prone.
switch (frmDialog.UserSelection)
{
 case DialogForm.SelectionTypes.OK:
 // (Do something here.)
 break;
 case DialogForm.SelectionTypes.Cancel:
 // (Do something else here.)
 break;
}

// Release the form and all its resources.
frmDialog.Dispose();

This is an effective, flexible design. In some cases, it gets even better: You can save code by using .NET's built-in support for dialog forms. This technique works best if your dialog only needs to return a simple value like Yes, No, OK, or Cancel. It works like this: In your dialog form, you set the DialogResult of the appropriate button control to one of the values from the DialogResult enumeration (found, like all user interface types, in the System.Windows.Forms namespace). For example, you can set the Cancel button's result to DialogResult.Cancel, and the OK button to DialogResult.OK. When the user clicks the appropriate button, the dialog form is immediately closed, and the corresponding DialogResult is returned to the calling code. Best of all, you don't need to write any event handling code to make it happen.

Your calling code would interact with a .NET dialog window like this:

DialogForm frmDialog = new DialogForm();
DialogResult result;
result = frmDialog.ShowDialog();
switch (result)
{
 case DialogResult.OK:
 // The window was closed with the OK button.
 break;
 case DialogResult.Cancel:
 // The window was closed with the Cancel button.
 break;
}

The code is cleaner and the result is more standardized. The only drawback is that you are limited to the DialogResult values shown in the list below (although you could supplement this technique with additional public form variables that would only be read if needed).

  • OK
  • Cancel
  • Yes
  • No
  • Abort
  • Retry
  • Ignore

Form Interaction

Once you create a form, it continues to exist until you end your application or explicitly call the Close() method. As with all controls, even when the control variable goes out of scope and is destroyed, the actual control continues to exist. However, without the form variable, your code has no way to access the form.

This isn't a problem if you code your forms independently, and place all the code that uses the form inside the appropriate Form class. This code can simply use the this reference to access the form (as this always points to the current instance of a class). However, things become a little trickier if you need to allow interaction between forms. For example, if you want to configure a control on one form using the code inside another form, you need to make sure you create and retain the required form variable, so it's available when you need it.

All this raises at least one good question: where should you store the references to a form that you might need later? Once common choice is to create a special global class that does little more than provide a reference to the forms in your application. The following code presents one such example that retains static references to two forms. Remember, static members are always available, so you won't need to create an instance of the AppForms class.

public class AppForms
{
 public static Form frmMain;
 public static Form frmSecondary;
}

You can then refer to the forms you need to use in any code module with the following syntax:

AppForms.frmMain.Show();

Keep in mind that the AppForms class doesn't actually set the form references. You'll need to do that when you create and display the form. One easy way to automate this process is to insert a little code into the Form.Load event handler:

private void MainForm_Load(object sender, EventArgs e)
{
 // Register the newly created form instance.
 AppForms.frmMain = this;
}

This approach works well if every Form class is only created once. If you want to track multiple instances of the same form, you probably want to use a collection object in your AppForms class. The example below uses a Hashtable, which means that every form can be indexed in the collection with a key. If you don't need this ability, you could use the ArrayList object, or even create a custom collection class. Both collection types are found in the System.Collections namespace.

public class AppForms
{
 public static Form frmMain;
 public static Hashtable SecondaryForms = new Hashtable();
}

Forms can add themselves to this collection as needed:

private void SecondaryForm_Load(object sender, EventArgs e)
{
 // Register the newly created form instance.
 AppForms.SecondaryForms.Add(this);
}

When trying to read from variables like frmMain, you should also explicitly check if the value is null (in other words, it hasn't yet been created) before attempting to access the form object.

Of course, you should minimize the need for form interactions, as they complicate code unnecessarily. If you do need to modify a control in one form based on an action in another form (a common requirement when designing wizard-like features), create a dedicated method in the target form. That makes sure that the dependency is well identified, and adds another layer of indirection, making it easier to accommodate changes to the form's interface. Figures 5-5 and 5-6 show two examples for implementing this pattern. Figure 5-5 shows a form that triggers a second form to refresh its data in response to a button click. This form does not directly attempt to modify the second form's user interface; instead, it relies on a custom intermediate method called DoUpdate(). The second example, Figure 5-6, shows a case where more than one form needs to be updated. In this case, the acting form relies on a higher-level application method, which calls the required form update methods (perhaps by iterating through a collection of forms).

click to expand
Figure 5-5: A single form interaction

click to expand
Figure 5-6: A one-to-many form interaction

  Note 

These rules don't apply for MDI applications, which have built-in features that help you track child and parent windows. Chapter 10 presents a few detailed examples of how MDI forms should interact with one another.

Form Ownership

.NET allows a form to "own" other forms. Owned forms are useful for floating toolbox and command windows. One example of an owned form is the Find and Replace window in Microsoft Word. When an owner window is minimized, the owned forms are also minimized automatically. When an owned form overlaps its owner, it is always displayed on top. Table 5-6 lists the properties of the Form class that support owned forms.

Table 5-6: Ownership Members of the Form Class

Member

Description


Owner

Identifies a form's owner. You can set this property to change a form's ownership, or release an owned form.


OwnedForms

Provides an array of all the forms owned by the current form. This array is read-only.


AddOwnedForm() and RemovedOwnedForm()

You can use these methods to add or release forms from an owner. It has the same result as setting the Owner property.

The following example (shown in Figure 5-7) loads two forms, and provides buttons on the owner that acquire or release the owned form. You can try this sample (included in code download for this chapter with the project name FormOwnership) to observe the behavior of owned forms.

click to expand
Figure 5-7: An owned form tester

public class Owner : System.Windows.Forms.Form
{
 // (Windows designer code omitted.)

 private OwnedForm frmOwner = new OwnedForm();

 private void Owner_Load(object sender, System.EventArgs e)
 {
 this.Show();
 frmOwned.Show();
 }

 private void cmdAddOwnership_Click(object sender, System.EventArgs e)
 {
 this.AddOwnedForm(frmOwned);
 frmOwned.lblState.Text = "I'm Owned";
 }

 private void cmdReleaseOwnership_Click(object sender, System.EventArgs e)
 {
 this.RemoveOwnedForm(frmOwned);
 frmOwned.lblState.Text = "I'm Free!";
 }
}

Note that for this demonstration, the lblState control in the owned form has been modified to be publicly accessible (by changing the access modifier from internal to public). As described in the form interaction section, this violates encapsulation and wouldn't be a good choice for a full-scale application.

Windows XP Styles

If you are using the Windows XP visual styles on your computer, you have probably already noticed an anomaly with .NET. Any .NET application you create in Windows XP uses the Windows XP styles for the nonclient area (such as the border and minimize/maximize buttons) but not for the actual form surface. Basic user interface elements, like buttons, check boxes, and radio buttons, still have the antiquated look that they've used since the early days of Windows 95.

Fortunately, you can enable Windows XP styles in your .NET applications. To do so, you need to create a special manifest file. This manifest file is an ordinary text file with the same name as your application, plus the extension .manifest (e.g., TheApp.exe would have the manifest file TheApp.exe.manifest—with what looks like two extensions). This file needs to go in the same directory as your program (typically, the binDebug subdirectory of the project directory, which is where the program is compiled during testing).

All the manifest file does is instruct Windows that your application should use the new version of the Comctl32.dll file, if available. This file is part of Windows XP and is used for the visual styles (it is also not redistributable to non-XP computers).

An example .manifest file is shown in the following, and is provided in the root directory of the code download for this book. You can copy this file exactly for your applications. Ideally, you should modify the name value (currently set to "TheApp"), but this change isn't necessary.



name="TheApp"
 type="win32" />








This gets you part of the way, but you also need to configure all your form's button-style controls, like Button, CheckBox, and RadioButton. These controls all have a FlatStyle property, which you must be set to System (not Standard) for Windows XP to supply its visual styles.

Once you are finished, run your application. The changes won't appear inside the development environment. Figure 5-8 shows the differences between the classic Windows look and Windows XP styles for some common controls.

click to expand
Figure 5-8: Windows XP visual styles

  Tip 

You can safely supply a manifest file for applications that run on older operating systems. If your application is executed on a non-Windows XP computer, the manifest file is harmlessly ignored, and the ordinary control appearance remains.

Prebuilt Dialogs

.NET provides some custom dialog types that you can use to show standard operating system windows. The most common of these is the MessageBox class, which exposes a static Show() method. You can use this code to display a standard Windows message box (see Figure 5-9):

MessageBox.Show("You must enter a name.", "Name Entry Error",
 MessageBoxButtons.OK, MessageBoxIcon.Exclamation);

click to expand
Figure 5-9: A simple message box

The message box icon types are listed in Table 5-7. The button types you can use with a message box are:

  • AbortRetryIgnore
  • OK
  • OKCancel
  • RetryCancel
  • YesNo
  • YesNoCancel
Table 5-7: MessageBoxIcon Values

MessageBoxIcon

Displays


Asterisk or Information

A lowercase letter i in a circle.


Error, Hand, or Stop

A white X in a circle with a red background.


Exclamation or Warning

An exclamation point in a triangle with a yellow background.


None

No icon.


Question

A question mark in a circle.

In addition, .NET provides useful dialogs that allow you to show standard windows for opening and saving files, choosing a font or color, and configuring the printer. These classes all inherit from System.Windows.Forms.CommonDialog. For the most part, you show these dialogs like an ordinary window, and then inspect the appropriate property to find the user selection.

For example, the code for retrieving a color selection is as follows:

ColorDialog myDialog = new ColorDialog();

// Sets the initial color select to the current color,
// so that if the user cancels, the original color is restored.
myDialog.Color = shape.ForeColor;

myDialog.ShowDialog();

shape.ForeColor = myDialog.Color;

The dialogs often provide a few other properties. For example, with a ColorDialog you can set AllowFullOpen to false to prevent users from choosing a custom color, and ShowHelp to true to allow them to invoke Help by pressing F1. (In this case, you need to handle the HelpRequest event.)

The OpenFileDialog provides its own set of properties, which allow you to validate the user's selection, allow multiple files to be selected, and set the filter string used for allowed file extensions:

OpenFileDialog myDialog = new OpenFileDialog();

myDialog.Filter = "Image Files(*.BMP;*.JPG;*.GIF)|*.BMP;*.JPG;*.GIF" +
 "|All files (*.*)|*.*";
myDialog.CheckFileExists = true;
myDialog.Multiselect = true;

myDialog.ShowDialog();

string selectedFiles = "";
foreach (string file in myDialog.FileNames)
{
 selectedFiles += file + " ";
}

lblDisplay.Text = "You chose: " + selectedFiles;

The PageSetupDialog and PrintDialog behave slightly differently, because they allow the user to set multiple printing options at once. With .NET, you do not have to retrieve and apply these settings. Instead, you just set the PrintDocument object for the current document in the Document property, and all the appropriate settings are applied automatically when the user selects them.

PageSetupDialog myDialog = new PageSetupDialog();

myDialog.Document = myDocument;
myDialog.ShowDialog();

Also, with the PrintDialog, you need to make sure to examine the dialog result to decide whether the user has chosen to continue with the print operation.

PrintDialog myDialog = new PrintDialog();

myDialog.Document = myDocument;
DialogResult result = myDialog.ShowDialog();

if (result == DialogResult.OK)
{
 // (Initiate printing here.)
}

Table 5-8 provides an overview of the prebuilt dialog classes. Figure 5-10 shows a small image of each window type.

Table 5-8: Common Dialog Classes

Class

Description


ColorDialog

Displays the system colors and controls that allow the user to define custom colors. The selected color can be found in the Color property.


OpenFileDialog

Allows the user to select a file, which is returned in the FileName property (or the FileNames collection, if you have enabled multiple file select). Additionally, you can use the Filter property to set the file format choices, and use CheckFileExists to enforce validation.


SaveFileDialog

Allows the user to select a file, which is returned in the FileName property. You can also use the Filter property to set the file format choices, and use set the CreatePrompt and OverwritePrompt Boolean properties to instruct .NET to display a confirmation if the user selects a new file or an existing file, respectively.


FontDialog

Allows the user to choose a font face and size, which is provided in the Font property (and its color through the Color property). You can limit the size selection with properties like MinSize and MaxSize, and you can set ShowColor and ShowEffects to configure whether the user changes the font color and uses special styles like underlining and strikeout.


PageSetupDialog

Allows the user to configure page layout, page format, margins, and the printer. To use this dialog, simply place the PrintDocument object for the document you want to print in the PageSetupDialog.Document property. Then, all settings are automatically set in your PrintDocument object when the user accepts them. Additionally, you can use properties like AllowMargins, AllowOrientation, AllowPaper, and AllowPrinter to choose the elements of this dialog that are shown to the user.


PrintDialog

Allows users to select a printer, choose which portions of the document to print, and invoke printing. To use this dialog, simply place the PrintDocument object for the document you want to print in the PrintDialog.Document property.


PrintPreviewDialog

This is the only dialog that is not a part of standard Windows architecture. It provides a painless way to show a print preview—just assign the PrintDocument to the Document property and display the form. The same logic you write for handling the actual printing is used automatically to construct the preview. Alternatively, you can use the PrintPreviewControl to show the same preview inside one of your custom windows.

click to expand
Figure 5-10: Common dialogs

 
Chapter 5 - Forms
 
byMatthew MacDonald  
Apress 2002
has companion web siteCompanion Web Site
 

Resizable Forms

Every week, Windows developers from every programming language spend countless hours trying to solve the problem of resizable windows. Some purchase third-party ActiveX controls designed to transform static forms into resizable wonders automatically. These components are easy to use, but generally provide mediocre results that aren't suitable for professional applications. Other developers ignore the problem, and stubbornly lock interfaces into fixed-size dialog boxes, making them seem unmistakably primitive. Most developers eventually give in, and write lengthy code routines to resize their forms by hand.

In .NET the picture has finally improved. Two new features-anchoring and docking-provide built-in support for resizable forms. These features allow you to configure a few properties, and end up with intelligent controls that adjust themselves automatically. The catch? It's extremely easy to create the equivalent of the mediocre ActiveX resizing control. In other words, you can end up with a window that resizes its controls in an embarrassingly unprofessional way with far less effort than was needed before.

Matching a good resizing approach with a sophisticated interface is possible, but it requires a little more subtlety and a few tricks. This chapter describes these tricks, such as adding container controls and using the DockPadding property. Along the way, you learn how to create scrollable windows and controls, and see a full-fledged Explorer-style application that uses automatic resizing the right way.

The Problem of Size

The resizable forms dilemma stems from the fact that the Windows operating system supports a variety of monitors at several different resolutions. A window that looks decently sized on one computer may shrink to a toy-like box on another, or even stretch beyond the bounds of the desktop, obscuring important controls.

For many simple applications, these types of problems are not serious because programmers usually design their applications for a set minimum standard resolution (such as 640 × 480 or, more commonly today, 800 × 600). It's also generally accepted that users with much larger viewable areas expect to run several programs at once, and purchased larger screens so that they can put different programs side-by-side. They don't expect to use up the extra viewable area with larger fonts or extra white space in a dialog box.

However, a document-based application can't afford to ignore these considerations. Users with more available space expect to be able to use it to see more information at a time. Programs that ignore this consideration (see for example Figure 5-11) are irredeemably frustrating.

click to expand
Figure 5-11: A frustrating window

  Tip 

Generally, a form needs to be resizable as soon as it stores more content than it can show at once. For example, a configuration window with a few settings does not need to be resizable, because all the information is available. On the other hand, a window that shows a scrolled text view or a data grid can benefit from more space, and so should be resizable.

Ever since the dawn of Windows 95 and 32-bit programming, developers have found it increasingly difficult to avoid writing manual code to resize windows (in the older days of Windows programming, they just didn't bother at all). The emphasis on MDI interfaces has also contributed to this change. In an MDI (Multiple Document Interface) application, users expect to be able to resize, maximize, minimize, tile, and otherwise arrange all the child windows.

One common solution is to write procedures that dynamically resize the window, by responding to a resize event or message. Unfortunately, if your window has more than a couple of controls, this code becomes long, repetitive, and ugly. It's also hard to alter or debug when the form changes even slightly, and a nightmarish example of hard-coded values and cryptic lines of code. In .NET, the picture improves considerably.

Minimum and Maximum Size

The first useful feature the Form class introduces for managing size is the MaximumSize and MimimumSize properties. These properties stop users abruptly when they try to resize a form outside its set limits. For example, the Address Book program included with Windows 2000/XP won't let you shrink it any smaller than the size of the toolbar (see Figure 5-12).

click to expand
Figure 5-12: The Address Book's minimum size

If you have the Show window contents while dragging environment setting enabled, the border suddenly becomes fixed when you hit the minimum size, as though it's glued in place. Similarly, you can set a maximum size, although this is less conventional. In this case, even when you try to maximize a window, it won't go beyond the set size, which can confuse the user.

The Visual Studio .NET IDE also stops you from resizing your form to an invalid size at design time when you have these properties set. If you set the form size to an invalid value in code, no error will occur. Instead, your window just automatically shrinks or expands to a valid size if it's outside the bounds of the allowed MinimumSize or MaximumSize properties.

One final caveat: both of these settings are ignored if you make your window a MDI child inside another window. In that case, your window is freely resizable.

Anchoring

Anchoring allows you to latch a control on to one of the form's corners. Anchored controls always stay a fixed distance from the point they are bound to. By default, every control is anchored to the top left corner. That means that when you move the window (and hence the top left corner), the controls follow. If you resize the form, however, the controls stay in place, because the position of the top left corner hasn't changed.

On the other hand, you can use .NET to anchor a control to a different corner. For example, if you chose the top right corner, the control moves as you expanded the window width-wise to stay within a fixed distance of the corner. If you expand the form height-wise, the control stays in place, because it's anchored to the top. It doesn't need to follow the bottom edge.

To anchor a button in .NET, you set the Anchor property using one of the values from the AnchorStyles enumeration. It's almost always easiest to set anchoring at design-time using the Properties window. A special designer lets you select the edges you are anchoring to by clicking them in a miniature picture, as shown in Figure 5-13. You don't need to run your program to test your anchoring settings; the Visual Studio .NET IDE provides the same behavior.

click to expand
Figure 5-13: Setting control anchoring at design-time

Resizing controls with anchoring

Anchoring to one corner works best with controls that don't need to change size, but should remain in a consistent position. This typically includes buttons (for example, OK and Cancel should always remain at the bottom of the window) and simple controls like labels and text boxes. If you use this type of anchoring on every control, you create a window that gradually spreads out as it enlarges, providing that dreaded "third-party component" effect.

Instead, you can anchor a control to more than one side at once. Then, as you expand the window, the control needs to expand to keep a fixed distance from all the anchored sides. Table 5-9 lists some of the ways that you can combine anchor settings for different effects, and Figure 5-14 shows a window that uses fixedcorner anchoring for a button, and side-to-side anchoring for a text box.

Table 5-9: Common Anchoring Choices

Anchoring

Description


Top + Left

Equivalent to no anchoring. Controls remain a fixed distance from the top left corner, but they don't move or expand as the form changes size.


Bottom + Right

The control moves to keep a fixed distance from the bottom right corner.


Bottom + Left

The control moves to stay at the bottom of the form, but it does not move to the side.


Top + Right

The control moves to stay at the right of the form, but it does not move down.


Top + Bottom

The control's height expands as the form lengthens.


Right + Left

The control's width expands as the form widens.


Top + Bottom + Right + Left

The control's width and height expand as the form is enlarged.

click to expand
Figure 5-14: Two ways to use anchoring

  Tip 

When using a resizable ListBox control, be sure to set the IntegralHeight property to false. This ensures that the ListBox can grow evenly. Otherwise, the ListBox is automatically resized to ensure that no list item is partially displayed. This causes it to "jump" awkwardly between valid sizes as its height grows or shrinks.

The controls that benefit the most from anchoring to more than one side are those that contain more information than they can display at once. For example, a DataGrid, RichTextBox, or even ListBox control may present a scrolled view into a large amount of information. It makes sense for these controls to resize to use available screen area. On the other hand, a button should never be set to resize. Doing so impersonates the low-quality add-in controls that performed resizing automatically.

  Note 

The problems I keep mentioning with ActiveX controls for automatic resizing is that most tried to apply the same strategy to every window (there are a couple of rare exceptions). The result is that every control resized itself in pretty much the same way. In a real window, however, some controls are more important and need more space. Other controls don't need to change at all, and can look rather ridiculous if they are sized radically bigger than their contents. Ultimately, resizing is another area where you must follow user interface design conventions from established Windows applications.

Containers and anchoring

Rather than try to anchor every control in a window, you should use one or more container controls to save some work. Containers also make it easier to rearrange portions of user interface at once, or even transplant them from one form to another.

To use anchoring with container controls, you need to understand that anchoring is always relative to the container. That means that if you place a button inside a group box and you anchor it to the bottom right, it will be anchored to the bottom right corner of the group box. It won't move when the size of the form changes; it will move only when the size of the container changes. For example, consider the button shown in Figure 5-15. The form is resized, but the group box doesn't change, and so the button also remains in place.

click to expand
Figure 5-15: Anchored controls follow a corner in the container.

Nothing happens in the previous example, because there's no change in the container. To get around this, you could anchor the group box to all sides of the window. Then, as the group box grows, the button will move to keep a consistent distance from the bottom right corner. This version is shown in Figure 5-16.

click to expand
Figure 5-16: Anchoring the control and its container

Container controls become particularly important when you start to add docking and split windows to your designs.

Docking

Docking allows a control to bind itself to an edge in the form or container control. When you resize the container, the control resizes itself to fit the entire edge. A control can be bound to any one edge, or it can be set to fill the entire available area. The only limitation is that you can't dock and anchor the same control (if you think about it for a moment, you'll realize that it wouldn't make sense anyway).

For example, you can solve the problem you saw with the button in the container control by docking the group box to the right edge of our form. Now, when you resize the window, the group box expands to fit the edge. Because the button inside is anchored to the bottom right corner of the group box, it also moves to the right side as the form is enlarged. Similarly, you could set the group box docking to fill, so that it would automatically resize itself to occupy the entire available area. Figure 5-17 shows an example of this behavior.

click to expand
Figure 5-17: A docked group box

To configure docking, you set the control's Dock property to one value from the DockStyle enumeration. Typically, you use the Property window to choose a setting at design-time.

If you experiment with docking, your initial enthusiasm quickly drains away, as you discover that:

  • Docked controls insist on sitting flush against the docked edge. This results in excessive crowding, and doesn't leave a nice border where you need it.
  • Docked controls always dock to the entire edge. There's no way to tell a docked control to bind to the first half (or 50 percent) of an edge. It automatically takes the full available width, which makes it difficult to design a real interface.

Every control that derives from the ScrollableControl class has an additional feature called dock padding. Dock padding allows you to insert a buffer of empty space between a container and its docked controls. The only important classes that derive from ScrollableControl are Panel, Form, and UserControl. The GroupBox control does not provide any padding.

Figure 5-18 shows another example with a group box and a contained button. Because the Form is the container for the group box, you need to modify the form's padding property by finding DockPadding in the properties window, expanding it, and setting All to 10 (pixels). Now the group box will still bind to all sides, but it will have some breathing room around it.

click to expand
Figure 5-18: A docked group box with padding

At this point you may wonder why you need docking at all. It seems like a slightly more awkward way to accomplish what anchoring can achieve easily. However, in many cases, anchoring alone is not enough. There are two common scenarios:

  • You are using an advanced window design that hides and shows various window elements. In this scenario, docking forces other controls to resize and make room, while anchoring leads to overlapping controls.
  • You want to create a window that the user can resize, like a split window design. In this case, you need to use docking, because it allows controls to resize to fit the available space.

You examine both of these designs in the next section.

  Tip 

The sample code for this chapter includes a program that lets you play with a number of different combinations of anchoring and docking, so you can see how they do or don't solve a problem.

 
Chapter 5 - Forms
 
byMatthew MacDonald  
Apress 2002
has companion web siteCompanion Web Site
 

Splitting Windows

One of the most recognizable user interface styles in applications today is the split window look (arguably popularized by Windows Explorer). In fact, split-window view applications are beginning to replace the former dominant paradigm of MDI, and Microsoft has led the change (although many developers, myself included, still favor MDI design for many large-scale applications).

In .NET, split-window designs are refreshingly easy to create, which is a dramatic departure from most other programming environments. To create a split window requires just three easy steps:

  1. Start by docking a control to one side of the window.
  2. Add the Splitter control, and dock it to the same side.
  3. Add another control, and set its docking to Fill so it occupies the rest of the window.

The example in Figure 5-19 shows this technique with a TreeView and a ListView. By moving the position of the splitter bar at runtime, the user can change the relative size of these two controls.

click to expand
Figure 5-19: A basic splitter bar

In this case, the window is somewhat claustrophobic. To improve the spacing, you can set a buffer using the form's DockPadding property. However, this won't add any extra spacing between the controls and the splitter bar—to add that, you need to anchor the ListView and TreeView in separate docked Panels, as you consider in the next example.

You can also set the SplitterBar.MinSize property to configure the smallest size (in pixels) to which the first docked control can be resized. You can also set the SplitterBar.MinExtra property to configure the minimum size for the controls on the other side of the splitter.

Docking with Panels

Rather than docking specific controls, as in the preceding example, it's more common to dock two Panel controls, one on each side of the splitter bar. That way, you can include several controls together in the Panel, and fine-tune their spacing. You can then use anchoring to cause the controls inside the Panel to resize themselves to fit its contents. This is where docking and container controls really begin to become useful.

Figure 5-20 shows an example taken from Chapter 9, which uses a customized TreeView/ListView explorer.

click to expand
Figure 5-20: Advanced docking

The panel on the left includes a single TreeView, but the panel on the right includes two label controls spaced inside a panel to give a pleasing border around the label text. (If the same window simply used a single label control with a border, the text in the label would sit flush against the border.) The horizontal rule and Close button at the bottom of the window aren't included in the resizable portion of the window. Instead, they are anchored in a separately docked panel, which is attached to the bottom of the form.

To implement this design, a panel control is first docked to the bottom to hold the Close button. Then, the TreeView, SplitterBar, and ListView controls are docked to fill the upper portion of the window. The diagram in Figure 5-21 shows the docking.

click to expand
Figure 5-21: A docking strategy

When designing this user interface, you may find that Visual Studio .NET is a little inflexible. If you dock the panels in the wrong order, you won't achieve the desired effect, and you'll need to copy all the controls to another form, and add the panels back one at a time in the correct docking order. Of course, the professional finished result is worth this minor aggravation.

Other Split Windows

Another reason to split a window is to provide two different views of the same data. Consider the example shown in Figure 5-22, which shows an HTML page using Microsoft's ActiveX web browser control and an ordinary text box. In this case, the splitter is docked to the top of the control, and becomes a horizontal bar.

click to expand
Figure 5-22: A split view on a single document

You could also add another vertical splitter to create a compound view. For example, consider Figure 5-23, which provides a list of HTML files the user can select from.

click to expand
Figure 5-23: Multiple splits

One of the best characteristics of docked designs is that they easily accommodate hidden or modified controls. For example, Figure 5-24 shows an alternate design that allows the file selection panel to be collapsed and then restored to its original size with the click of the button. The contents of the window automatically resize themselves to accommodate the additional portion when it is displayed.

click to expand
Figure 5-24: A collapsible split window

The code for this operation is trivial:

private void cmdHide_Click(object sender, System.EventArgs e)
{
 pnlFileList.Visible = false;
 pnlShow.Visible = true;
}

private void cmdShow_Click(object sender, System.EventArgs e)
{
 pnlShow.Visible = false;
 pnlFileList.Visible = true;
}

This sample is included in the code for this chapter with the project name HTMLSplitWindow.

  Tip 

When designing a split window, it is best to start by creating and docking panel controls. Otherwise, if you need to modify your design, you may need to start over.When using panels, you can always add any controls you require simply by dropping them inside the appropriate panel.

 
Chapter 5 - Forms
 
byMatthew MacDonald  
Apress 2002
has companion web siteCompanion Web Site
 

Irregularly Shaped Forms

Irregularly shaped forms are often the trademark of cutting-edge consumer applications like photo editors, movie makers, and MP3 players. In the past, creating them required a fair bit of API wizardry. Now with .NET, creating a shaped form is almost effortless, thanks to the new GDI+ model that's included as part of the .NET framework.

To create a simple shaped window in .NET, all you need to do is create a form, and assign a new shape to its Region property (this is the same process followed to create a custom shaped control). There is more than one way to create a Region, but the easiest is by using the GraphicsPath class, which allows you to build up a complex shape out of as many subshapes as you need.

First, begin by importing the required GDI+ namespace:

using System.Drawing.Drawing2D;

You can then create and apply the GraphicsPath. The following example code defines an ellipse with the same bounds of the form. Once it's assigned to the Region property of the form, only the part of the form that fits inside the ellipse is displayed (see Figure 5-25).

click to expand
Figure 5-25: A shaped form

private void ShapedForm_Load(object sender, System.EventArgs e)
{
 GraphicsPath path = new GraphicsPath();
 path.AddEllipse(0, 0, this.Width, this.Height);
 this.Region = new Region(path);
}

You can see part of the original window border at the top and bottom of the shaped form, and the single contained button in the middle. However, the form acts completely like an ellipse. For example, if you click in the cutout portion that the original rectangular form occupied (that is, just above the left edge of the ellipse), you will not select the form. Instead, you select whatever application is currently underneath the form. This convenience is a great step forward—with ordinary GDI code, shaped controls looked like shapes but still behaved like the original rectangles in many ways.

You can also create a shaped form made up of a combination of shapes. In fact, these shapes don't even need to overlap! The following example illustrates a more unusual shaped form, shown in Figure 5-26:

private void ShapedForm_Load(object sender, System.EventArgs e)
{
 GraphicsPath path = new GraphicsPath();

 path.AddEllipse(0, 0, this.Width / 2, this.Height / 2);
 path.AddRectangle(new Rectangle(this.Width / 2, this.Top / 2,
 this.Width / 2, this.Top / 2));
 path.AddEllipse(this.Width / 2, this.Height / 2, this.Width / 2,
 this.Height  2);

 this.Region = new Region(path);
}

click to expand
Figure 5-26: A noncontiguous shaped form

Shaped Form Content

There are two problems you will quickly notice with shaped forms:

  • The Region defines a shape, but this shape does not provide any borders. Instead, a shaped form is just a geometric figure that reveals the underlying form.
  • Ordinary controls (like standard windows buttons) aren't well suited for a shaped form—the styles seem to clash.

To handle these problems, you need to create your graphical content from scratch. You can accomplish this by using GDI+ to create owner-drawn controls, as you'll see in 13. An easier approach is just to design the appropriate images in a dedicated graphics program and import them into your .NET controls.

For example, you can set the BackgroundImage property for the form to a picture that has the same shape you want to use, and includes a border. You can also substitute picture box controls for buttons. Figure 5-27 shows one such example.

click to expand
Figure 5-27: An irregular form with graphical content

To make the images behave like buttons, some additional logic is required to offset the button when it is clicked. The event handlers shown in the following are reused for the MouseDown and MouseUp events of all four buttons.

private void pic_MouseDown(object sender, System.Windows.Forms.MouseEventArgs e)
{
 PictureBox pic = (PictureBox)sender;
 pic.Top += 2;
 pic.Left += 2;
}

private void pic_MouseUp(object sender, System.Windows.Forms.MouseEventArgs e)
{
 PictureBox pic = (PictureBox)sender;
 pic.Top -= 2;
 pic.Left -= 2;
}

Additionally, the images are enhanced to support hot tracking. Whenever the mouse pointer positions itself above a button, a different image is displayed. This image looks the same as the normal button image, but has its text highlighted in yellow. The code to implement this technique simply retrieves the required picture from a ListView control.

private void pic_MouseEnter(object sender, System.EventArgs e)
{
 PictureBox pic = (PictureBox)sender;
 int imageIndex = int.Parse((pic.Tag.ToString()));
 pic.Image = imgSelectedButtons.Images[imageIndex];
}

private void pic_MouseLeave(object sender, System.EventArgs e)
{
 PictureBox pic = (PictureBox)sender;
 int imageIndex = int.Parse((pic.Tag.ToString()));
 pic.Image = imgNormalButtons.Images[imageIndex];
}

You can try this code by running the IrregularlyShapedForms project included with the code download for this chapter.

Moving Shaped Forms

Another limitation of shaped forms is that they often omit the nonclient title bar portion, which allows the user to easily drag the form around the desktop. To remedy this problem, you need to add a control that takes over the responsibility for the form's title bar.

For example, you could add a picture box or a label control. This control needs to handle the MouseDown, MouseUp, and MouseMove events, and reposition the form accordingly (Figure 5-28). The code is similar to that used in

click to expand
Figure 5-28: Moving a shaped form

private bool formDragging;
private Point pointClicked;

private void lblDrag_MouseDown(object sender,
 System.Windows.Forms.MouseEventArgs e)
{
 // Set drag mode on.
 formDragging = true;

 // Store the offset where the control was clicked.
 pointClicked = new Point(e.X, e.Y);
}

private void lblDrag_MouseMove(object sender,
 System.Windows.Forms.MouseEventArgs e)
{
 if (formDragging)
 {
 Point pointMoveTo;

 // Find the current mouse position in screen coordinates.
 pointMoveTo = this.PointToScreen(new Point(e.X, e.Y));

 // Compensate for the position the control was clicked.
 pointMoveTo.Offset(-pointClicked.X, -pointClicked.Y);

 // Compensate for the nonclient region (title bar).
 // This code is not necessary if you explicitly hide the title bar
 // by setting the form's BorderStyle to None.
 pointMoveTo.Offset(0, -25);

 // Move the form.
 this.Location = pointMoveTo;
 }
}

private void lblDrag_MouseUp(object sender,
 System.Windows.Forms.MouseEventArgs e)
{
 formDragging = false;
}

Forms with Holes

There's also one other approach to creating irregularly shaped forms. You can define a special color for the form that will automatically be cut out by setting the form's TransparencyKey property. For example, if you choose dark blue, all occurrences of dark blue in your form become invisible. This includes occurrences of the color in an image, a control, or even the form's background. In fact, these holes aren't just invisible—you can even click through them to activate a program underneath!

Figure 5-29 shows an example from the online samples that contains several picture box controls that contain the TransparencyKey color as their background. At runtime, these picture boxes disappear.

click to expand
Figure 5-29: A form (with the desktop showing through)

 
Chapter 5 - Forms
 
byMatthew MacDonald  
Apress 2002
has companion web siteCompanion Web Site
 

Visual Inheritance

Visual inheritance is a fancy name for form inheritance, which is similar to the custom control pattern you use to create specialized .NET controls. Depending on how you use visual inheritance, you can really accomplish two things:

  • Use a common form template (visual appearance) for several different windows. This might be useful to create a wizard or standardized About window.
  • Use form functionality in several different windows. This allows you to create a framework that you might use for different types of view windows in a MDI application. Every window will have its own look, but it might reuse some of the same buttons to close the window or open a file.

And, as with any type of inheritance, visual inheritance gives you many different ways to customize how the descendent class can use, extend, or override the inherited class.

To create a simple example of form inheritance, you might create a wizard form like the one shown in Figure 5-30. It uses a blank header area for title text, a large surface area for additional content, and a Next button at the bottom. In this example (found in the code for this chapter under the project name VisualInheritance), the base form is named Ancestor.

click to expand
Figure 5-30: An ancestor form for a wizard

To create an inherited form that uses this form, you first need to compile the project (unless the form is already stored in a separate assembly from your project). Then, right-click the project item in the Solution Explorer and choose Add → Inherited Form. You'll be prompted to choose a new form name, and select the form you want to derive from (see Figure 5-31).

click to expand
Figure 5-31: Inheriting from a form

Of course, you don't actually need to use the wizard to create an inherited form. All you really need to do is create a Form class, and change the standard class declaration (which inherits from the System.Windows.Forms class) to inherit from your custom class. Make sure you use a fully qualified name that includes both the project namespace (in this case, VisualInheritance) and the form name.

public class Descendent : Inherits VisualInheritance.Ancestor

You'll notice that the inherited form contains all the controls that you defined in the original form, but it doesn't allow you to move them, change their properties, or add event handlers. You can, however, add new controls, write their event handlers, and change the size (or any other property) for your descendant form. In the basic example, this doesn't allow the flexibility you need. For example, the user needs to have some way to configure the label control in the title area and override the function of the Next and Previous buttons. Fortunately, this is all easy if you understand a few basics about inheritance.

  Tip 

Whenever you change the ancestor form, you must recompile the project before you see the appropriate changes in the descendant form. Just right-click the project in the Solution Explorer and choose Build to create the assembly without launching it.

Making an Ancestor Control Available

By default, every control on the original ancestor form is declared with the Friend modifier. This keyword allows access to other forms in the same project, but it doesn't allow any access to your derived form. To change this state of affairs, simply modify the controls you want to configure to use the Protected modifier instead. You can change the declaration by looking through the form code, or you can use the Properties window, and look for the special Modifiers property.

Once you've made this change, you'll find that you can configure any aspect of the inherited controls, including their appearance and position. The values used in the base form become the default values in the derived form, but any changes you make are recorded in the derived form's designer code and applied automatically when the form is created.

Adding an Ancestor Property

In our wizard example, creating protected-level controls may not be the best approach. Quite simply, it allows too much freedom to change the original layout. Take the header text, for example. The creator of the derived form should be able to enter custom text into the control, but other details (like its font, color, and position) shouldn't be modifiable, as they risk compromising the standardized layout you've established.

To code a better solution, you could create a special property in the base form. The client could then use this property to set the header text, without being allowed any greater degree of control.

public string HeaderText
{
 get
 {
 return lblHeader.Text;
 }
 set
 {
 lblHeader.Text = value;
 }
}

In your ancestor form, this property is available through code—and it also appears as value the user can set in the Properties window (see Figure 5-32). In fact, you can add other attributes to this property that configure the description it shows and the category it will appear in, but that topic is explored in Chapter 10.

click to expand
Figure 5-32: A custom property

Overriding an Event Handler

Your base form might also contain event-handling logic. If this logic is generic (for example, it simply closes the form) it is suitable for all descendants. In the case of your Previous and Next buttons, there clearly is no generic code that can be written. Instead, the descendant needs to override the event handling code.

To accomplish this, you need to declare that the event handler can be overridden by marking it virtual in your ancestor form:

protected virtual void cmdNext_Click(object sender, System.EventArgs e)
{
 MessageBox.Show("Ancestor form event handler.")
}

You can then override the routine in your descendant form:

protected override void cmdNext_Click(object sender, System.EventArgs e)
{
 MessageBox.Show("Descendant form event handler.")
}

Note that you do not connect this code by adding an event handler. That's because the original routine (the one you are overriding) is already connected to the event. Another way to get around this situation is just to mark the control in the base form as protected. In this case, you don't need to include an ancestor event handler; instead, the derived form can write event-handling code directly. The side effect is that you lose your protection, and any other aspect of the control can also be modified in the derived form.

In some cases, you might want to execute both the extra code in the descendant form, and the original code. You can accomplish this by using the base reference. The code that follows, for example, results in the display of two message boxes, one from the ancestor form, followed by one from the derived form.

protected override void cmdNext_Click(object sender, System.EventArgs e)
{
 // Call the original version.
 base.cmdNext_Click(sender, e);

 MessageBox.Show("Descendant form event handler.");
}

Finally, in some cases you might want to force an event handler to be overriden. For example, in our example a wizard form can't be considered complete unless it has the necessary event handling logic behind added to its next button. However, it's impossible to code this logic at the ancestor level. To force the derived class to override this event handler (as a precautionary measure), you can declare the event handler with the abstract qualifier. In this case, you can't add a method body.

protected abstract void cmdNext_Click(object sender, System.EventArgs e);

For this to work, the ancestor form class must also be declared abstract.

public abstract class Ancestor : System.Windows.Forms.Form

Be warned that this pattern can confuse the Visual Studio .NET IDE, and could even cause it to stop displaying your derived forms (although the code will work without a hitch). The best approach is to create a separate DLL assembly for your base forms, and add this as a reference to any client applications that want to inherit from these classes.

As you master visual inheritance, you might want to expand the inheritance technique to include your custom controls. Chapter 7 is dedicated to this approach.

 
Chapter 5 - Forms
 
byMatthew MacDonald  
Apress 2002
has companion web siteCompanion Web Site
 

The Last Word

In this chapter you've traveled from the basics of Windows forms-creating them, displaying them, and handling their interactions, to advanced techniques using shaped forms and visual inheritance. In many respects, these new developments hint at two of the themes I take up in the rest of the book. Shaped forms give just a taste of what you encounter in 13, which dive headfirst into GDI+. Visual inheritance provides a preview into some of the custom control designs you explore with control inheritance throughout this book. Both techniques represent the step from ordinary user interface programming to the more advanced techniques of a UI guru.

 
Chapter 6 - Modern Controls
 
byMatthew MacDonald  
Apress 2002
has companion web siteCompanion Web Site
 

Modern Controls



User Interfaces in C#(c) Windows Forms and Custom Controls
User Interfaces in C#: Windows Forms and Custom Controls
ISBN: 1590590457
EAN: 2147483647
Year: 2005
Pages: 142

Flylib.com © 2008-2020.
If you may any questions please contact us: flylib@qtcs.net