|
|||||||||||||
|
|
Chapter 4
Windows Forms
Solutions in this chapter:
· Introducing Windows Forms
· Writing a Simple Windows Forms Application
· Writing a Simple Text Editor
· Using the ListView and TreeView Controls
· Creating Controls
· Summary
· Solutions Fast Track
· Frequently Asked Questions
Introduction
With so much focus on Web-based user interfaces, it’s easy to dismiss the traditional Windows architecture when developing for the Internet. The recent popularity, however, of peer-to-peer file sharing and online chat programs demonstrates that the “rich client” can work extremely well over the Internet, and provide features unavailable in thin client model. The .NET platform provides a modern solution for developing Windows applications, with the following key features:
· A revamped object-oriented model, with a focus on consistency and extensibility
· A rapid application development environment in Visual Studio
· Easy access to the Internet through .NET networking libraries and Web Services
· Managed execution environment that allows custom controls to be hosted in a Web page
· Compilation to a small executable
And, of course, you no longer have any installation worries—you just need to copy a small executable to the target machine and run it. Rich client has become thin.
The components provided in the .NET library for writing Windows applications can broadly be divided into two groups: Windows Forms (the components that manage windows and controls) and the graphics device interface known as GDI+ (the classes that encapsulate the lower-level graphics functions). This chapter covers Windows Forms in some detail, also touching upon GDI+, and it takes you step by step through the process of creating typical rich client applications.
Introducing Windows Forms
In essence, Windows Forms is a collection of classes and types that encapsulate and extend the Win32 API in a tidy object model. In other words, the components used to create Windows GUI applications are provided as .NET classes and types that form part of an orderly hierarchy.
This hierarchy is defined by inheritance: Simple reusable classes such as Component are provided, and then used as a base from which more sophisticated classes are derived. We can draw a useful overview by representing the inheritance hierarchy in a treelike diagram. Figure 4.1 summarizes at a high level the classes that comprise Windows Forms and GDI+.
Figure 4.1 A Summary of Window Forms and GDI+ Classes
The arrows represent inheritance: Control assumes all the functionality of Component, which assumes all the functionality of Object. Table 4.1 provides a quick and pragmatic summary of the four essential classes on which the Windows Forms types are based.
Table 4.1 Core Classes
Class |
What It Does |
Why We Need It |
Object |
Acts as a base class for all types in the .NET Framework. |
For a tidy unified type system, and to provide core functionality available to all types (such as ToString). |
Component |
Provides the basics of containership, facilitates hosting in a visual designer, and defines a protocol for resource disposal. |
So Visual Studio’s Designer can host a wide variety of controls and components in a generic way, to provide a base from which you can write nonvisual components, and to allow the cleanup of Windows handles and file handles in a timely and reliable manner. |
Control |
Provides the core functionality for a visual control that responds to mouse and keyboard messages, accepts focus, and can participate in drag-and-drop operations. |
As a common superclass for all controls, such as textboxes, labels, and buttons, allowing them to be treated in a consistent manner, as well as providing a base from which you can derive your own custom controls. |
Form |
Defines a class representing a window to which you can add controls. |
To provide a base class with standard windowing and containership functionality that you can subclass to create forms in your application. |
Creating a Windows Forms application is largely just a matter of instantiating and extending the Windows Forms and GDI+ classes. In a nutshell, you typically complete the following steps:
1. Create a new project defining the structure of a Windows Forms application.
2. Define one or more Forms (classes derived from the Form class) for the windows in your application.
3. Use the Designer to add controls to your forms (such as textboxes and checkboxes), and then configure the controls by setting their properties and attaching event handlers.
4. Add other Designer-managed components, such as menus or image lists.
5. Add code to your form classes to provide functionality.
6. Write custom controls to meet special requirements, using GDI+ classes to handle low-level graphics.
In this chapter, we cover each of these steps through a series of walkthroughs. Starting with a new Windows Forms project, we visually add controls to a simple form, add an event handler, and then demonstrate how controls can be added at runtime. In the next walkthrough, we write a simple text editor, illustrating menus, single and multiple-document interfaces, dialog forms, and visual inheritance. In the following example, we introduce the ListView and TreeView controls, going step-by-step through the process of setting up a splitter, adding a context menu, and enabling drag and drop between the controls. In the final walkthrough, we write our own controls—starting with a simple address container and finishing with a scrolling text banner. We then show how custom controls can be hosted on an HTML page—demonstrating how C# and Windows Forms can be used to write Java-like Internet applets.
Writing a Simple Windows Forms Application
The first step to building a Windows Forms application is creating a project. A Windows Forms project is just like any other type of project in that it consists of a grouping of source code files, a list of references to required .NET code libraries, and an appropriate configuration of compilation and debugging options. When you use Visual Studio to create a project from a template, it sets all of this up for you, providing a “skeleton” appropriate to the template you’ve selected. In the case of Windows Forms, this consists of the following:
· A project of Output Type Windows Application. You can view or change this in the Project | Properties dialog box.
· References to the .NET assemblies required for typical Windows Forms applications (covering most of the types in the Windows Forms namespace). You can see a list of the project references in the Solution Explorer.
· A blank form, called Form1 (a C# class with the structure required for a visually editable form).
· A Main method in Form1 that instantiates and displays the form.
Let’s start the walkthrough by creating a new Windows Forms project. From the main menu, choose File | New | Project, click Visual C# Projects, and choose the Windows Application template (see Figure 4.2). Change the project name to SimpleApp and click OK.
Figure 4.2 Creating a New Windows Forms Project
Adding Controls
Once we’ve created the project, Visual Studio opens the main form (Form1) in the Designer—the visual editor for our C# form class. Basically, a form created in Visual Studio is just a C# file, defining a class based on System.Windows.Forms.Form, containing code to add and configure the controls created visually. Visual Studio is a “two-way tool” meaning that we can work with the same code either visually (using the Designer) or programmatically (in the Code Editor).
Let’s use the Designer to add a few controls to Form1. We can add controls and components from the toolbox window and then configure them using the Properties window.
1. From the toolbox, add a Label control to the form. By default, Visual Studio will name the control Label1.
2. From the Properties Window (F4) change label1’s Text property to Favorite CD, and change its AutoSize property to True (see Figure 4.3). This tells the control to size itself according to the metrics of the font and width of the text.
Figure 4.3 Adding and Configuring a Label Control
3. Now add a TextBox from the toolbox onto the form, and position it below the label. Enlarge it horizontally and clear its Text property.
4. Add another label to the form, setting its Text property to Favorite Style, and AutoSize property to True.
5. Add a ComboBox and position it below the Favorite Style label. Clear its Text property.
6. Select the combo’s Items property, and then click the ellipses on the right to open the String Collection Editor. Type in a few styles of music—each on a separate line, as shown in Figure 4.4.
Figure 4.4 Populating a ComboBox Items Collection
7. Click OK, and then press F5 to save, compile, and run the application.
Developing & Deploying…
Working with Controls: Using TextBoxes
To create and work with textboxes having more than one line:
· Set MultiLine to True and AutoSize to False.
· Set AcceptsTab and AcceptsReturn to True to allow tabs and new lines to be entered via the keyboard.
· Set the ScrollBars property to Vertical (or Both if WordWrap is false).
· Use the Lines property to access the control’s text one line at a time.
· Use \r\n for a new line, for example, Flat 18\r\nQueen St.
To use the control for entering a password:
· Set the PasswordChar property to *.
To read or update selected text:
· Use the SelectionStart, SelectionLength, and SelectedText properties.
Adding an Event Handler
Let’s add some functionality to the form.
1. Add a Button and ListBox to the form.
2. Select the button, and change its Text property to Update. Then click the lightning icon in the Properties window to switch to the Events View (see Figure 4.5).
Figure 4.5 Properties Window Events View
Think of these events as “hooks” into which we can attach our own methods. You can either double-click on an event to create a new event-handling method, or use the drop-down list to connect into an existing compatible method.
3. Double-click on the Click event. Visual Studio will write a skeleton event-handling method, wiring it to the event. It will then place you in the Code Editor, inside the empty method definition:
private void button1_Click(object sender, System.EventArgs e)
{
}
The .NET convention for event handling requires two parameters: a sender parameter of type object, and an event arguments parameter of type EventArgs—or a descendant of EventArgs. The sender parameter tells us which control fired the event (this is useful when many controls have been wired to the same event-handling method). The second parameter is designed to supply special data about the event. In the case of Click, we have a standard EventArgs object, and this contains no useful information—it’s just there to meet the protocol required to support more sophisticated events (such as KeyPress or MouseDown).
The actual name for this method (button1_Click) is just a convenient identifier generated by Visual Studio; Windows Forms doesn’t impose any particular naming convention.
4. Add the following code to the event handler:
private void button1_Click(object sender, System.EventArgs e)
{
listBox1.Items.Clear();
listBox1.Items.Add ("Fav CD: " + textBox1.Text);
listBox1.Items.Add ("Fav Style: " + comboBox1.Text);
}
Here we’re manipulating our list box through its Items property. Items returns a collection object, having methods to add and remove items from its list. Note how we access each control through its name—this is possible because the Designer creates class fields matching the names of each control. You can see these declarations at the top of the class definition.
5. Press F5 to compile and run the program (see Figure 4.6).
Figure 4.6 Running a Simple Windows Forms Application
Developing & Deploying…
Working with Controls: Using the ComboBox and ListBox Controls
To add items to the controls’ selection lists programmatically:
· Call the Item property’s Add method to append to the end of the list, for example:
myControl.Items.Add ("My New Item");
· Use the Item property’s Insert method to insert within the list.
· Because these methods expect an Object type, the item you add can be of any class, including your own (this is polymorphism in action—one of the benefits of a working in an object-oriented language). The control simply calls the item’s ToString method to determine what to display.
To get the currently selected item:
· Use the Text property to return a string.
· Use SelectedIndex to get a numeric position within the list.
· Use SelectedItem to get an object reference. If the item is of your own custom class, you’ll need to explicitly cast the returned value back to your type.
To allow the user to select only from items in a ComboBox list, set the DropDownStyle property to DropDownList.
Adding Controls at Runtime
Sometimes it’s necessary to add controls without the help of the Designer. For instance, you might want some controls to appear on a form only when a particular button is clicked.
In learning how to programmatically add controls, it’s very helpful to examine a visually created form in the Code Editor. If you expand the Designer Generated Code region, you’ll see a method called InitializeComponent containing all the code that creates and configures each of the form’s visual components.
Warning
Although reading Designer-generated code is useful in understanding how components are instantiated and configured, you shouldn’t make manual changes to this code without exercising some caution. In particular, you should check that the control renders as expected in the Designer before saving the form. You should also check your code after making some visual change—Visual Studio completely rewrites the Designer-generated code section, so your modifications may not appear as originally entered.
Here are the four steps to programmatically adding a control or component:
1. Add a class field declaration for the new control.
2. Instantiate the control.
3. Configure the control by setting its properties and adding event handlers, if required.
4. Add the control to the form’s Controls collection (or alternatively, to the Controls collection of a container control, such as a GroupBox).
Let’s work through an example: we’ll create a new form, add a button, and then have a textbox appear when the user clicks the button:
1. Create a new Windows Forms project called SimpleApp2 and add a Button control from the toolbox onto the new form.
2. Press F7 to open the Code Editor, and locate button1’s declaration. Below this, add a similar declaration for our new textbox, as follows (you can exclude the System.Windows.Forms prefix if your form has the appropriate using statement):
private System.Windows.Forms.Button button1;
private System.Windows.Forms.TextBox myTextBox;
You need to understand that this declaration doesn’t actually create a textbox. All it does is instruct the compiler, once our form is instantiated, to create a field that can reference (point to) a textbox object—one that does not yet exist. This declaration exists so as to provide a convenient way to refer to the control throughout the lifetime of the form. In the cases where we don’t need to explicitly reference the control after its been created, we can do away with this declaration.
3. Return to the Designer, and double-click on the button. This is a quick way to attach an event handler to the button’s default event (Click).
4. Add the following code to the button’s event handler:
private void button1_Click(object sender, System.EventArgs e)
{
// Create the actual textbox and assign its reference to myTextBox
this.myTextBox = new TextBox();
// Position the control
myTextBox.Location = new Point (30, 20);
// Put the control on the form.
this.Controls.Add (myTextBox);
}
5. Press F5 to test the application (illustrated in Figure 4.7).
Figure 4.7 Adding Controls at Runtime
You might have noticed that we created a Point object to position the control. Point, Size, and Rectangle are three “helper types” defined in the System.Drawing namespace, and are used extensively in Windows Forms—as well as other parts of the .NET Framework. Table 4.2 illustrates how these types are most commonly applied in Windows Forms.
Table 4.2 Helper Types for Positioning and Sizing
Type |
Example |
Notes |
Point struct |
button1.Location = new Point (100, 80); |
Sets button1’s position 100 pixels across and 80 pixels down. |
button1.Left = 100; button1.Top = 80; |
Equivalent to the above. |
|
Console.WriteLine (button1.Location.X); |
Equivalent to outputting button1.Left. |
|
button1.Location.X = 100; |
Not permitted because of the way structs are marshaled in C#. |
|
Size struct |
button1.Size = new Size (75, 25); |
Resizes button1 to 75 by 25 pixels |
button1.Width = 75; button1.Height = 25; |
Equivalent to the above. |
|
// Assuming "this" is our form this.Size = new Size (button1.Right, button1.Bottom); |
Attempts to resize the form so it just fits button1. However, the form’s Size property includes the title bar and borders—its usable space is less, and button1 won’t quite fit. |
|
this.ClientSize = new Size (button1.Right, button1.Bottom); |
ClientSize excludes title bars and borders so this works correctly. |
|
Rectangle struct |
button1.Bounds = new Rectangle (100, 80, 50, 20); |
Rectangle combines Point and Size. |
button1.Bounds = new Rectangle (0, 0, this.ClientSize.Width, this.ClientSize.Height); |
Moves and sizes button1 to fill the whole client area of our form (later we’ll see that docking provides a better solution to achieving this). |
Developing & Deploying…
Working With Controls: Using Controls Collections
The form class is an example of a control that hosts other controls. Windows Forms manages this containership by providing a Controls property, returning a ControlCollection object that has methods to add, remove, and access the child controls. Like other .NET collections, it implements standard interfaces such as ICollection and IList—and this means we can work with them all in a similar way.
To access an individual control by its position in the collection, use its Indexer—for example:
Controls[0].Hide() // hide the first control in the collection
To iterate through every control, use the foreach operator—for example:
// Write the Text property of each control on the form
foreach (Control c in Controls)
Console.WriteLine (c.Text);
To remove a control from the collection, use the Remove method—for example:
Controls.Remove (txtMiddleName);
To reparent a control to another collection:
· Change the control’s Parent property.
· A control’s position in the collection determines its z-order (front-to-back order), where position 0 is at the front. When you use Bring To Front and Send To Back in the Designer, you’re actually changing the control’s position in its parent’s Controls collection. You can also achieve the same thing at runtime by calling the object’s BringToFront and SendToBack methods, or by using the parent collection’s SetChildIndex method.
Here are some other commonly used container-style controls that offer the same property:
· Panel A simple container for other controls.
· GroupBox A container with a border and caption text, used for visually grouping controls on a form. It’s also often used to host RadioButton controls (only one radio button can be checked at a time inside each group box).
· TabPage A TabControl contains a collection of TabPage controls—each of which acts as a container for child controls, with its own Controls property.
Attaching an Event Handler at Runtime
Let’s suppose we want to set up our newly created textbox so that when it’s right-clicked, a message box appears. We need to add an event handler to the textbox at runtime, and there are two steps to this:
· Writing the event-handling method.
· Attaching the method to the control’s event.
In our case, we’ll need to attach to the textbox’s MouseDown event (because there’s no specific right-click event). First, we need to write the event-handling method, with parameters of the correct type for a MouseDown event. You can determine an event’s signature in two ways:
· Look for the event in the Microsoft documentation, and then click on its delegate (in our case, MouseEventHandler).
· Using the Designer, add a dummy control of the type we’re attaching to, create an appropriate event handler, and then delete the dummy control. The event-handling method will still be there—with the correct signature. All we need to do is rename it.
Here’s how we do it:
1. Using either approach, add a method to our form, as follows:
void myTextBox_MouseDown (object sender, MouseEventArgs e)
{
if (e.Buttons == MouseButtons.Right)
// Show is a static method of System.Windows.Forms.MessageBox
MessageBox.Show ("Right Click!");
}
2. Next, we attach this method to myTextBox’s MouseDown event. Return to the button1_Click method and add the following line of code:
myTextBox.MouseDown += new MouseEventHandler (myTextBox_MouseDown)
On the left-hand side, myTextBox.MouseDown is the event to which we’re attaching, using the += operator. On the right-hand side, we’re creating a new MouseEventHandler delegate instance: in other words, an object containing a pointer to a method (myTextBox_MouseDown) conforming to MouseEventHandler’s signature.
3. Test the application.
Developing & Deploying…
Why We Need Delegates
It’s often asked, “why can’t we simply assign a target method (for example, myTextBox_MouseDown) directly to an event?” C# doesn’t allow this because the language is strongly typed, and the event needs to pass parameters to the target method. If we could assign a method directly to an event, there would be no place to formalize the number and types of these parameters (the method signature). We need a way of describing an agreed method signature, and for this we have delegates. The easiest way to think of a delegate is in two parts:
· The delegate definition This simply describes a method signature.
· A delegate instance This is an object containing a pointer to a method conforming to the signature.
Most of the delegate definitions you’ll come across are part of the .NET Framework—although sometimes you define your own—usually when writing custom controls. Delegate instances, however, are created whenever you hook up to an event.
Here’s an example of a complete delegate definition:
public delegate void EventHandler (object sender, EventArgs e)
As you can see, all this does is set out a signature: two parameters, one of type object, and the other of type EventArgs, and a void return type. EventHandler is the “plain vanilla” delegate used extensively in the .NET Framework. Events are declared of this type if they don’t require any special information sent to the target.
Here’s an example of a delegate instance:
EventHandler eh = new EventHandler (textBox1_Click);
This simply contains a reference (pointer) to textBox1_Click. The compiler will check that the target method’s signature agrees with the delegate definition (EventHandler). The following line of code attaches eh to myTextBox’s click event:
myTextBox.Click += eh;
Review Chapter 2 for more information on delegates and events.
Goto Page 2 >>Contact Us | | Site Guide | About PerfectXML | Advertise | Privacy | |