have seen the future, and it has Microsoft® .NET written all over it. In case you haven't heard, Microsoft .NET is a new platform that sits atop Windows®. It's a whole new programming paradigm that will change the way you think about writing Windows-based software. And it's a bold new way of thinking in which the Windows API, MFC, ATL, and other tools that you've come to rely on take a back seat to a new API embodied in a set of classes called the .NET Framework class library. What do you gain from it? A more unified programming model, improved security, and a better way to write rich, full-featured Web apps. And that's just for starters.
One of the features of Microsoft .NET that interests me most is Windows Forms. If you're an old MFC (or Windows API) hack like me, Windows Forms are a great way to get started with the .NET Framework class library because they enable you to write traditional GUI applications that create windows, process user input, and the like. And once you learn Windows Forms, you'll find learning other parts of the .NET Framework easier, too.
The chief benefit of writing Windows-based applications the Windows Forms way is that Windows Forms homogenize the programming model and eliminate many of the bugs, quirks, and inconsistencies that plague the Windows API. For example, every experienced Windows-based programmer knows that certain window styles can only be applied to a window when that window is created. Windows Forms largely eliminate such inconsistencies. If you apply a style that's only meaningful at creation time to an existing window, Windows Forms will quietly destroy the window and recreate it with the specified style. In addition, the .NET Framework class library is much richer than the Windows API, and when you write applications using Windows Forms, you have the full power of the framework at your disposal. More often than not, applications written using Windows Forms require less code than applications that use the Windows API or MFC.
Another benefit of Windows Forms is that you use the same API, regardless of which programming language you choose. In the past, your choice of programming language drove your choice of API. If you programmed in Visual Basic® you used one API (the one embodied in the Visual Basic language), while C programmers used the Win32® API, and C++ programmers, by and large, used MFC. It was tough for an MFC programmer to switch to Visual Basic, or vice versa. No more. All applications that use Windows Forms are written to one API—that of the .NET Framework class library. The language is little more than a lifestyle choice, and knowledge of one API is sufficient to allow a programmer to write applications in virtually any language they choose.
Windows Forms are nothing less than a modern-day programming model for GUI applications. Unlike the Win32 programming model, much of which dates all the way back to Windows 1.0, this one was designed with the benefit of hindsight.
The purpose of this article is to introduce the Windows Forms programming model. To compile and run the code samples, you must have the Microsoft .NET Framework SDK installed on your computer. You can download the .NET Framework SDK Beta 1 from the Microsoft .NET Developer Center, located at https://msdn.microsoft.com/net.
In Windows Forms, the term "form" is a synonym for a top-level window. An application's main window is a form. Any other top-level windows the application has are forms also. Dialog boxes are also considered forms. Despite their name, applications using Windows Forms don't have to look like forms. Like traditional Windows-based applications, apps exercise full control over what appears in their windows.
Programmers see Microsoft .NET through the lens of the .NET Framework class library. Think of MFC one order of magnitude larger and you'll have an accurate picture of the breadth and depth of the .NET Framework class library. To mitigate naming collisions and lend organization to its many hundreds of classes, the .NET Framework class library is partitioned into hierarchical namespaces. The root namespace, System, defines the fundamental data types used by all .NET applications.
Applications using Windows Forms rely heavily upon classes found in the System.WinForms namespace. That namespace includes classes such as Form, which models the behavior of windows or forms; Menu, which represents menus; and Clipboard, which enables Windows Forms applications to access the clipboard. It also contains numerous classes representing controls with names like Button, TextBox, ListView, and MonthCalendar. These classes can be referred to using their class names only, or using fully qualified names such as System.WinForms.Button.
At the heart of nearly every Windows Forms-based application is a class derived from System.WinForms.Form. An instance of that class represents the application's main window. System.WinForms.Form has scores of properties and methods that comprise a rich programmatic interface to forms. Want to know the dimensions of a form's client area? In Windows, you'd call the GetClientRect API function. In Windows Forms, you read the form's ClientRectangle or ClientSize property. Many properties can be written as well as read. For example, you can change a form's border style by writing to its BorderStyle property, or resize a form using its Size or ClientSize property.
Windows Forms-based applications that use pushbuttons, listboxes, and other Windows control types rely on the control classes in System.WinForms. You'll come to love these classes because they vastly simplify control programming. Want to create a stylized button with a bitmapped background? No problem. Just wrap an image in a System.Drawing.Bitmap object and assign it to the button's BackgroundImage property. What about control colors? Have you ever tried to customize the background color of an edit control? I know other developers have, because I get e-mail asking about it all the time. In Windows Forms, it's easy: you just write the color to a property named BackColor and let the framework do the rest.
Another important building block of an application that uses Windows Forms is a System.WinForms class named Application. That class contains a static method named Run that gets a Windows Forms-based application up and running by displaying a window and providing a message loop. Of course you don't see the message loop. The very existence of messages is abstracted away in the .NET environment. But it's there, and it's one more detail you don't have to worry about because the platform sweats these details for you.
But wait a minute—if applications that are Windows Forms don't process messages, how do they respond to user input or know when to paint? For starters, many classes have virtual methods that you can override to respond to paint messages, mouse messages, and the like. For example, System.WinForms.Form contains a virtual method named OnPaint that's called when a form's client area needs updating. OnPaint is one of many virtual methods you can override in a derived class to build interactive forms; OnMouseDown, OnKeyDown, and OnClosing are some of the others. If all else fails, Windows Forms even provides a mechanism for responding to Windows messages by tapping into a form's internal window procedure.
Another important facet of the Windows Forms programming model is the mechanism that forms use to respond to input from menus, controls, and other GUI application elements. Traditional Windows-based applications process WM_COMMAND and WM_NOTIFY messages using Windows Forms process events. In C# and in other languages that support the .NET Common Language Runtime (CLR), events are first-class type members on par with methods, fields, and properties. Virtually all Windows Forms control classes (and many non-control classes, too) fire events. For example, button controls—instances of System.WinForms.Button—fire Click events when they're clicked. A form that wants to respond to button clicks can use the following syntax to wire a button to a Click handler:
MyButton.Click += new EventHandler (OnButtonClicked);
•••
private void OnButtonClicked (object sender, EventArgs e)
{
MessageBox.Show ("Click!");
}
EventHandler is a delegate defined in the System namespace. A delegate is the CLR's equivalent of a type-safe function pointer. This example wraps an EventHandler around the OnButtonClicked method and adds it to the list of event handlers called when MyButton fires a Click event. OnButtonClick's first parameter identifies the object that fired the event. The second parameter is basically meaningless for Click events but is used by certain other event types to pass additional information about the event.
You're a programmer, so when it comes to learning a new platform, nothing speaks to you better than a Hello World application. (Entire companies, especially dot-com startups, have been built around Hello World applications.) Figure 1 contains the Windows Forms version of a Hello World app. All the samples in this article are written in C#, but you can write Windows Forms-based applications in any language for which a .NET compiler is available. Today, your choice of languages includes C#, Visual Basic, JScript®, and managed C++.
First things first. The using statements at the top of the file allow you to reference classes in the System, System.WinForms, and System.Drawing namespaces using just their class names. Without the following statement, for example,
using System.WinForms;
you'd have to write
public class MyForm : System.WinForms.Form
instead of
public class MyForm : Form
In an application using Windows Forms, each window—or form—is represented by an instance of a class derived from System.WinForms.Form. In Figure 1, that class is MyForm. MyForm's constructor sets the form's caption bar text to "Windows Forms Demo" by writing to MyForm's Text property. Text is one of more than 100 properties which the form inherits from System.WinForms.Form. I'll use more of these properties later on, but for now, Text is the only one you need.
As you know, windows receive WM_PAINT messages, and most screen rendering is performed in response to these messages. In Windows Forms, the equivalent of a WM_PAINT message is a virtual method named OnPaint. A derived form class can override this method to paint itself in response to WM_PAINT messages.
Notice the override keyword in Figure 1 which the C# compiler interprets as confirmation that you intend to override a virtual method inherited from a base class. The OnPaint override here writes "Hello, world" in the form's client area. When OnPaint is called, it's passed a PaintEventArgs (System.WinForms.PaintEventArgs) object, which contains properties named Graphics and ClipRectangle. The Graphics property holds a reference to a Graphics (System.Drawing.Graphics) object, which is the Windows Forms equivalent of the device context. ClipRectangle holds a Rectangle (System.Drawing.Rectangle) that describes what part of the form is invalid.
MyForm's OnPaint method uses Graphics.DrawString to render its output. The first parameter to DrawString is the string itself. The second is a Font object (System.Drawing.Font) that describes the font in which the text should be rendered. MyForm.OnPaint uses the form's default font, a reference to which is stored in a Form property named Font. The third parameter is a Brush (System.Drawing.Brush) object specifying the text color. MyForm.OnPaint creates a black SolidBrush (System.Drawing.SolidBrush) object for this parameter. The fourth and final parameter is a formatting rectangle describing where the text should be positioned. MyForm.OnPaint uses the form's entire client area—a description of which is found in the Form property named ClientRectangle—as the formatting rectangle.
The final member of MyForm is a static method named Main. Main is the application's entry point. Every .NET application must have a Main method. Main can be declared in any of the following ways:
public static void Main ()
public static int Main ()
public static void Main (string[] args)
public static int Main (string[] args)
The args parameter passed to Main is an array of strings representing the application's command-line arguments. args[0] holds the first command-line parameter, args[1] the second, and so on. Main generally appears only once in every application, although it's possible to have multiple Main methods defined if you tell the compiler which one is the entry point. (The Microsoft C# compiler accepts a /main switch specifying which class contains the Main method that serves as the application's entry point. A /main switch is only required if multiple classes within the application include Main methods.) Main can be a member of any class defined in the application.
Displaying your form on the screen is a simple matter of instantiating MyForm and passing a reference to it to Application.Run. Application is another class defined in the System.WinForms namespace. Run is the method you call to create a form, display it on the screen, and service it with a message loop. In Figure 1, the following statement
Application.Run (new MyForm ());
instantiates MyForm and displays the form.
Once you've entered the code in Figure 1 and saved it to a file named Hello.cs, you'll need to compile it. To do so, open a command prompt window, go to the folder where Hello.cs is stored, and type:
csc /target:winexe /out:Hello.exe /reference:System.dll
/reference:System.WinForms.dll /reference:System.Drawing.dll
/reference:Microsoft.Win32.Interop.dll Hello.cs
The csc command invokes the Microsoft C# compiler. Hello.cs identifies the file to be compiled. The /target:winexe switch tells the compiler to produce a GUI Windows-based application (as opposed to a console application), and /out:Hello.exe specifies the name of the resulting executable. (You can omit this switch if you like and the EXE will still be named Hello.exe since the CS file is named Hello.cs). The /reference switches identify the assemblies in which external types such as System.WinForms.Form and System.Drawing.Size are defined. For brevity, you can replace /target and /reference with /t and /r if you'd like. Still, there's no getting around the fact that creating command-line builds of Windows Forms-based applications requires verbose csc commands.
Hello.exe isn't an ordinary Windows-based EXE file; it's a .NET executable containing the following important elements:
- The Microsoft Intermediate Language (MSIL) produced from your C# source code
- Metadata describing the types (classes) defined in the application, and the types (such as System.WinForms.Form) that are referenced in your application, but found in other assemblies (for example, in MsCorLib.dll and System.WinForms.dll)
- A manifest describing the files that make up your application's assembly
In the language of .NET, an assembly is a collection of one or more files that are deployed as a unit. Your assembly contains just one file—Hello.exe—and that fact is noted in the manifest embedded within the executable. The manifest is physically stored as part of the metadata, but I treat it as a separate entity here to underscore its importance. Every managed executable—that is, any PE file that contains MSIL—is part of an assembly, and every managed executable has metadata inside it. One of the files in a .NET assembly contains the manifest identifying the files that comprise the assembly, the public data types that this assembly makes available to other assemblies, and other assemblies upon which this assembly depends. The C# compiler produces all this necessary infrastructure for you, even though you didn't explicitly ask it to. For more information on assemblies, manifests, and metadata, and about the roles that they play in the operation of .NET applications, see Part 1 and Part 2 of Jeffrey Richter's articles on the .NET Framework in the September and October 2000 issues of MSDN® Magazine.
Once Hello.exe is compiled, you can run it by typing Hello
at the command prompt. Take a look at Figure 2 to see how the result looks on screen.
.gif)
Figure 2 Hello.exe Running
The ImageView Application
Hello World is good for a start, but you'll have a hard time making any money with it. (OK, I was joking when I said that entire companies have been built around Hello World applications.) So let's get serious and write a real application using Windows Forms—a bitmap viewer that can crack open JPEG files, GIF files, and various other types of image files. I'll call it ImageView, and I'll use the .NET Framework class library's System.Drawing.Bitmap class to make short work of handling the plethora of image file formats that exist today. The equivalent application written using the Windows API or MFC would require several thousand lines of code. With Windows Forms, I'll do it in approximately 100 lines, bells and whistles included.
The Main.cs file shown in Figure 3 contains the initial version of ImageView. It creates a form, sets the text in the form's caption bar to "Image Viewer", and sizes the form so that its client area measures 640 by 480 pixels. Sizing is performed by writing a Size value (System.Drawing.Size) to the ClientSize property that MyForm inherits from System.WinForms.Form.
To compile, go to the folder where Main.cs is stored and type:
csc /target:winexe /out:ImageView.exe /reference:System.dll
/reference:System.WinForms.dll /reference:System.Drawing.dll
/reference:Microsoft.Win32.Interop.dll Main.cs
This time the /out switch prevents the EXE file from being named Main.exe. After ImageView.exe is compiled, run it by typing
ImageView
at the command prompt. The window you'll see looks rather plain at the moment, lacking traditional GUI application elements such as a menu, but you'll fix that in the next exercise.
The next step is to add an Options menu containing an Exit command that closes the application. In an application that uses Windows Forms, a top-level menu—the menu bar that appears underneath the window's title bar—is an instance of System.WinForms.MainMenu. A MainMenu is attached to a form by assigning it to the form's Menu property, which the form inherits from System.WinForms.Form. Items in the menu are represented by MenuItem (System.WinForms.MenuItem) objects.
Figure 4 shows the necessary modifications to Main.cs. New code is shown in green type. The statement
MainMenu menu = new MainMenu ();
creates a MainMenu object and stores a reference to it in the variable named menu. The statement
MenuItem item = menu.MenuItems.Add ("&Options");
adds a menu item titled &Options and stores a reference to it in the variable named item. MenuItems is a collection representing all the menu items contained in the menu; calling Add on this collection adds an item to the menu and returns a MenuItem object. Finally, the statement
item.MenuItems.Add (new MenuItem ("E&xit",
new EventHandler (OnExit));
adds an Exit command to the Options menu and connects it to a handler named OnExit. When executed, OnExit calls the form's Close method (which MyForm acquires through inheritance), which closes the form and ultimately causes the application to terminate.
Step 3: Add an Open Command
The revised version of Main.cs in Figure 5 adds an Open command to the Options menu. Adding the menu item is a simple matter of calling Add on the Options menu's MenuItems collection. Another call to Add—this time with "-" as the menu item text—adds a horizontal separator. Note the third parameter passed to MenuItem's constructor:
Shortcut.CtrlO
Shortcut is an enumeration defined in System.WinForms. CtrlO is an element of that enumeration that corresponds to the key combination Ctrl+O. Including Shortcut.CtrlO in the parameter list does two things: it defines Ctrl+O as a shortcut for the Open command, and it appends "CTRL+O" to the menu item text.
The work of opening an image file falls to OnOpenImage. The .NET Framework class library's OpenFileDialog class, which is part of the System.WinForms namespace, encapsulates the functionality of the Windows Open File dialog and enables that dialog to be used by managed code. OnOpenImage creates an OpenFileDialog object, initializes it with a filter string specifying what should appear in the dialog's "Files of type" field, and displays the dialog by calling OpenFileDialog.ShowDialog. If the user enters a file name and clicks OK, OnOpenImage extracts the file name by reading it from the OpenFileDialog's FileName property. Then it opens the file with the statement:
_MyBitmap = new Bitmap (fileName);
Bitmap is shorthand for System.Drawing.Bitmap; it's a powerful class that encapsulates bitmaps and allows image files of almost any type to be opened simply by inputting a file name. Of course, you must account for the fact that a user might select a file that isn't an image file at all. That's why this statement is contained in a try block. If the user selects a non-image file, Bitmap's class constructor throws an exception that's caught by your catch handler, which displays an error message warning the user that the file isn't an image file. OnOpenImage displays the error message by calling a static MessageBox method named Show. MessageBox (System.WinForms.MessageBox) is the Framework class that wraps message boxes.
Notice the call to Invalidate that's executed if the image file is successfully opened. Invalidate is a System.WinForms.Form method that forces a repaint by invalidating a form's client area. Like the InvalidateRect function in the Windows API (and MFC's CWnd::InvalidateRect function), Form.Invalidate can be used to invalidate an entire form or just part of it. Here, it's used to invalidate the entire client area.
Now that I've implemented an Open command, you would hope that I'd be able to view an image. However, ImageView still lacks code to render the image onto the form. For the moment, you can at least prove that the try...catch logic in Figure 5 works by attempting to open a non-image file. And you can verify that if you select an image file, the file name appears in the form's title bar. Still, ImageView is of limited utility if it can't display the images that the user opens. Let's fix that with the addition of painting logic.
Step 4: Override OnPaint
To add painting logic, you override OnPaint in your derived form class and use the supplied Graphics object. The OnPaint method in Figure 6 renders _MyBitmap to the form's client area. (_MyBitmap, you'll recall, is a protected field initialized by OnOpenImage that stores a reference to the image selected by the user.) The rendering is performed with two simple statements:
Graphics g = e.Graphics;
g.DrawImage (_MyBitmap, 0, 0,
_MyBitmap.Width, _MyBitmap.Height);
The result is shown in Figure 7. The painting is done by DrawImage, which is one of many methods defined in System.Drawing.Graphics. Painting bitmaps on the screen is far easier in .NET than it is in Windows, primarily because the Windows GDI lacks high-level functions for drawing and manipulating bitmapped images.
.gif)
Figure 7 Image View Displaying a JPEG File
When you use a Graphics object to render graphical output, you're using a component of the .NET platform called GDI+. Like the Windows GDI, GDI+ is an API for producing 2D graphics. But unlike the Windows GDI, GDI+ isn't constrained by the limited set of functions exported from Gdi32.dll. GDI+ is considerably richer, containing support for gradient fills, floating point coordinates, antialiasing, and much more. It also employs a stateless programming model that makes it more suitable for use in distributed, connectionless applications.
ImageView now has the ability to display images, but if the image size exceeds the form size, only a portion of the image can be viewed. An obvious solution to this problem is a set of scroll bars—a horizontal scroll bar that appears when the image width exceeds the form width, and a vertical scroll bar that appears when the image height exceeds the form height.
System.WinForms.Form inherits a pair of properties from System.WinForms.ScrollableControl that make scrolling a breeze. AutoScroll is a Boolean property that turns autoscrolling on and off. When autoscrolling is enabled, scroll bars automatically appear when the form is too small to display everything you want in its client area. How does the form know what you want it to display? You give it the dimensions of the desired viewing area by writing a Size value to the form's AutoScrollMinSize property.
The version of Main.cs shown in Figure 8 adds scrolling support to ImageView. Two lines of code have been added to OnOpenImage: one to enable autoscrolling, and another to set AutoScrollMinSize equal to the width and height of the bitmap. OnPaint has also been modified to offset the bitmap by an amount that corresponds to the horizontal and vertical scroll positions. These simple changes enable any image to be viewed in its entirety, no matter how large the image or how small the form.
Step 6: Add a Size-to-Fit Option
Adding scroll bars to the form is one way to handle large images. Another option is to ignore the physical size of the image and render it so that it fits the form exactly. Let's let the user decide which way to handle this by adding a pair of commands to the Options menu. One command, Size Image to Fit Window, will fit the image to the form. The other, Show Image in Native Size, will display images in their native resolution, but provide scroll bars (if needed) to permit viewing of large images.
Figure 9 shows how to go about it. First you add a field (_NativeSize) to store the user's preference. Next, you modify MyForm's constructor to add two new menu items (three if you count the extra separator bar). Third, you add command handlers that set _NativeSize to true or false, turn scrolling on or off, and call Invalidate to force a repaint. Finally, you modify OnPaint to match the size of the rendered image to the form size if _NativeSize is false. Matching the image size to the form size is a simple matter of passing a Rectangle containing the dimensions of the form's client area in DrawImage's second parameter. That Rectangle is readily available as a System.WinForms.Form property named ClientRectangle.
The only new code in Figure 9 that might raise an eyebrow is a pair of calls to SetStyle. Here's why I added them. By default, a resizing operation doesn't invalidate the entire form; it only invalidates the portion of the form exposed by the resizing operation. That's OK if you're not scaling the image to fit the form size, but if you are, then you want the entire client area to be invalidated when its size changes so the entire form will be repainted. In Windows, you'd accomplish that by including CS_HREDRAW and CS_ VREDRAW flags in the WNDCLASS style. In Windows Forms, you do it by calling the form's SetStyle method with a ControlStyles.ResizeRedraw parameter. ControlStyles is an enumeration defined in System. WinForms; ResizeRedraw is a member of that enumeration.
Step 7: Add Code to Check and Uncheck Menu Items
All that remains now is to place a check mark next to either the Size Image to Fit Window or Show Image in Native Size command, depending on whether _NativeSize is true or false. There are two ways you can go about it.
The first option is to set each MenuItem's Checked property to true or false when either item is selected from the menu. Setting Checked equal to true places a check mark next to the corresponding menu item; setting it to false unchecks it. The other option is to check or uncheck the menu items in the microsecond between the time the user clicks Options in the main menu and the Options menu appears on the screen—in other words, write the equivalent of an MFC update handler. The latter has the distinct advantage of decoupling the code that updates the menu from the code that sets _NativeSize to true or false. That's the approach in Figure 10.
In this, the final version of Main.cs, references to the MenuItem objects representing the Size Image to Fit Window and Show Image in Native Size commands are cached in fields named _itemFitToWindow and _itemNativeSize, respectively. In addition, a handler for Popup events—events signifying that a menu has been pulled down—is connected to the Options menu with the following statement:
item.Popup += new EventHandler
(OnPopupOptionsMenu);
OnPopupOptionsMenu, which is called whenever the Options menu is displayed, checks or unchecks the menu items represented by _itemFitToWindow and _itemNativeSize based on the current value of _NativeSize. Consequently, the check mark always appears next to the correct menu item no matter what the current value of _NativeSize is or how that value was acquired. The finished product is shown in Figure 11.
.gif)
Figure 11 Show Image in Native Size Checked
ImageView in Review
You've now seen firsthand the important roles that the .NET Framework class library's Form, Application, and Graphics classes play in the operation of an application using Windows Forms. At this point, it might be helpful to review some of the framework classes that ImageView uses. A partial list of these classes—the ones that figure most prominently into ImageView's design and operation—is shown in Figure 12. Becoming a Microsoft .NET programmer is largely a matter of getting to know the .NET Framework class library as well as you probably already know the Windows API, MFC, or the Visual Basic API. In a very real sense, the .NET Framework class library is the .NET API. It's the key that unlocks the box called Microsoft .NET, and if Microsoft has any say in the matter, it's the substrate that will form the foundation for future generations of Windows-based applications and Web applications alike.
The TuneTown Application
Writing an application like ImageView is a good way to get started with Windows Forms, but it's just that—a start. ImageView uses only a handful of the Windows Forms classes available in the .NET Framework, and it's not a traditional form-based application in the sense that it doesn't use pushbuttons or controls of any type. Let's make up for that by writing a second application that does use controls—an application that lets a user catalog his or her CD collection (see Figure 13). This time, you'll bring the full power of Microsoft Visual Studio .NET to bear on the problem by using its IDE to design your forms and to help generate the code that backs them up.
.gif)
Figure 13 CD Collection Catalog
The following exercises were written using the Beta 1 version of Visual Studio .NET that Microsoft made available for downloading in November 2000. It's possible that the screens you see here, and even the mechanics of the exercises themselves, will change before Visual Studio .NET is finalized and released.
Choose the New Project command from the Visual Studio® File menu to create a new project. Choose Windows Application from the C# Projects folder as the project type (see Figure 14). For the project name, enter TuneTown.
.gif)
Figure 14 Create New Project
Step 2: Design the Main Form
Once the new project is created, Visual Studio will drop you into the Visual Studio forms editor and present you with a blank form. Before you begin work on the form, change the file name Main1.cs to MainForm.cs in the Solution Explorer window. While you're at it, use the Class View window to change the form class's name from Form1 to MainForm.
Now go back to the forms editor and add a list view control and three push buttons to the form, as shown in Figure 15. Then, one by one, select each of the controls you added and use the Properties window to modify the control properties as described in the following paragraphs.
.gif)
Figure 15 TuneTown's Main Form
For the list view control, edit the control properties as follows:
- Set the FullRowSelect property to True.
- Set the GridLines property to True.
- Set the View property to Report.
- Edit the Columns collection to add three columns to the header at the top of the list view: one whose Name is "TitleHeader", whose Text is "Title", and whose Width is 100; another whose Name is "ArtistHeader", Text is "Artist", and Width is 100; and a third whose Name is "CommentHeader", Text is "Comment", and Width is 200.
- Set Multiselect to False.
- Set HideSelection to False.
- Set Sorting to Ascending.
- Set TabIndex to 0.
- Set Name to "TuneView".
Edit the properties of the push button controls as follows:
- Set the Text property to "&Add", "&Edit", and "&Remove", respectively.
- Set the Name property to "AddButton", "EditButton", and "RemoveButton", respectively.
- Set the TabIndex property to 1, 2, and 3, respectively.
Finally, change the Text property of the form itself to "TuneTown". Doing so will change the title in the form's caption bar.
You'll need a second form that you can use to solicit input when the user clicks the Add or Edit button. In effect, this form will serve as a dialog box. To add the form to the project, go to the Project menu, select the Add Windows Form command, and choose Windows Form from the ensuing dialog (see Figure 16). In the Name box, type AddEditForm.cs.
.gif)
Figure 16 Add New Windows Form
Edit the new form in the Visual Studio forms editor, so that it resembles the one shown in Figure 17. Modify the label controls' properties as follows:
.gif)
Figure 17 Forms Editor
- Set the Text property to "&Title", "&Artist", and "&Comment", respectively.
- Set the Name property to "TitleLabel", "ArtistLabel", and "CommentLabel", respectively.
- Set TabIndex to 0, 2, and 4, respectively.
Modify the edit controls' properties this way:
- Set all three controls' Text properties to null.
- Set Name to "TitleBox", "ArtistBox", and "CommentBox", respectively.
- Set TabIndex to 1, 3, and 5, respectively.
- Change the third edit control's Multiline property from False to True.
Next, modify the properties of the two push buttons:
- Set Text to "OK" and "Cancel", respectively.
- Set DialogResult to OK and Cancel, respectively.
- Set Name to "OKButton" and "NotOKButton", respectively.
- Set TabIndex to 6 and 7, respectively.
Finally, edit the properties of the form itself:
- Set BorderStyle to FixedDialog.
- Set AcceptButton to OKButton.
- Set CancelButton to NotOKButton.
- Set MaximizeBox and MinimizeBox to False.
- Set ShowInTaskbar to False.
The forms are complete; now it's time to write some code.
Open AddEditForm.cs and add the statements shown in green type in Figure 18. The bulk of the code shown in Figure 18 was created by Visual Studio. That code performs three important tasks:
- It declares Button (System.WinForms.Button), TextBox (System.WinForms.TextBox), and Label (System.WinForms.Label) fields in the AddEditForm class to represent the form's controls. It also initializes these fields with references to instances of Button, TextBox, and Label.
- It initializes the properties of each control and of the form itself.
- It physically adds the controls to the form by calling Add on the form's Controls collection.
Most of this code is located in a method named InitializeComponent, which is called from the class constructor. The "Component" in InitializeComponent refers to the form itself. Most of the code that you see in the InitializeComponent method was generated at the time you added the controls to the form and changed the controls' properties.
The statements you entered add Title, Artist, and Comment properties to the form class. These properties permit callers to access the text in the form's edit controls.
Step 6: Add Event Handlers to MainForm
The next step is to add event handlers to the MainForm class—handlers that will be called when the Add, Edit, or Remove button is clicked, or when an item in the list view control is double-clicked (see Figure 19).
In Windows Forms, controls fire events in response to user input. MainForm connects each pushbutton's Click event to an XxxButtonClicked method, and the ListView's DoubleClick event to OnItemDoubleClicked. The handlers for the Add and Edit buttons instantiate AddEditForm and display it on the screen by calling its ShowDialog method. ShowDialog's return value tells you how the dialog was dismissed: DialogResult.OK if the OK button was clicked, DialogResult.Cancel if Cancel was clicked instead. To get data in and out of the dialog, the handlers read and write the properties you added to AddEditForm in Step 5. Once again, the forms editor provides the code that defines the form's appearance, and you provide the code that implements its behavior.
Now's a good time to build the project if you haven't already. You don't have to go out to the command line; instead, you can select the Build command from the Visual Studio Build menu. Then you can run TuneTown.exe by selecting one of the Debug menu's Start commands.
Step 7: Add Anchors
TuneTown is almost complete, but there's an important element that's still missing—an element that highlights one of the coolest features in Windows Forms. To see what I mean, run the application and resize its main window. The window resizes just fine (that's because the form's BorderStyle property is set to Sizable; to prevent resizing, you could change the BorderStyle to FixedDialog), but the controls stay put. Wouldn't it be nice if the controls flowed with the form, automatically resizing and repositioning themselves to utilize all the real estate available to them? To do that in a traditional Windows-based application, you'd have to handle WM_SIZE messages and programmatically move and resize the controls. In Windows Forms, you can do it with about one tenth of the effort. In fact, you don't have to write a single line of code.
Every Windows Form control inherits a property named Anchor from System.WinForms.RichControl. The Anchor property describes which edges of its parent a control's own edges should be glued to. If, for example, you set a control's Anchor property to AnchorStyles.Right, then the distance between the control's right edge and the right edge of the form will remain constant, even if the form is resized. By setting the Anchor properties of the controls in MainForm, you can easily configure the push buttons to move with the form's right edge and the list view control to stretch both vertically and horizontally to fill the remaining space in the form. Here's how.
Open MainForm in the forms editor and select the ListView's Anchor property. Initially, Anchor is set to TopLeft; change it to All. Then, one at a time, set the pushbuttons' Anchor properties to TopRight. These actions result in the following statements being added to MainForm's InitializeComponent method:
AddButton.Anchor = System.WinForms.AnchorStyles.TopRight;
EditButton.Anchor = System.WinForms.AnchorStyles.TopRight;
RemoveButton.Anchor = System.WinForms.AnchorStyles.TopRight;
TuneView.Anchor = System.WinForms.AnchorStyles.All;
Now, rebuild the application and resize the form again. This time, the controls should flow with the form. I'm sure you've been frustrated by dialogs whose controls are too small to show everything that you want them to show. Frustrations such as these will be a thing of the past when developers take advantage of Windows Forms anchoring.
Step 8: Add Persistence
The final step is to make the data entered into TuneTown persistent by writing the contents of the list view control to a disk file when TuneTown closes, and reading those contents back the next time TuneTown starts up. I'll use the .NET Framework class library's StreamWriter and StreamReader classes (members of the System.IO namespace) to make short work of inputting and outputting text strings.
The necessary changes are highlighted in Figure 20. OnClosing is a virtual method inherited from System.WinForms.Form that's called just before a form closes. MainForm's OnClosing implementation creates a text file named TuneTownData.ttd in the local user's application data path (SystemInformation.GetFolderPath (SpecialFolder.LocalApplicationData)). Then it writes the text of each item and subitem in the list view control to the file. Next time TuneTown starts up, MainForm's InitializeListView method opens the file, reads the strings, and writes them back to the list view.
Notice the calls to the StreamWriter and StreamReader objects' Close methods. The reason I included these calls has to do with deterministic versus nondeterministic destruction. Languages such as C++ employ deterministic destruction, in which objects go out of scope (and are destroyed) at precise points in time. However, the .NET CLR, which uses a garbage collector to reclaim resources, uses nondeterministic destruction, meaning that there's no guarantee when (or if) the garbage collector will kick in. Normally you don't care when garbage collection is performed. But if an object encapsulates a non-memory resource such as a file handle, you very much want destruction to be deterministic because you want that file handle closed as soon as it's no longer needed. For reasons that I won't go into here, the problem is exacerbated when StreamWriter objects are involved because a StreamWriter's Finalize method, which is called when a StreamWriter object is destroyed, doesn't bother to close any file handles or flush buffered data to disk. If you don't call Close, data could be lost. I took the extra step of using finally blocks to enclose TuneTown's calls to Close to be sure that Close is called, even after inopportune exceptions.
Parting Thoughts
There's more that could be done to make TuneTown a full-featured application. But it's time to draw this discussion to a close.
The Windows Forms programming model is a portent of things to come. It's entirely conceivable that at some date in the not-too-distant future, the vast majority of Windows-based programming will be done the .NET way. Will the industry join Microsoft in fulfilling this vision? It's too early to tell, but there's one thing that you can be certain about: it's going to be an interesting next couple of years.
|