The Windows Forms Programming Model

The Windows Forms Programming Model

In Windows Forms, the term form is a synonym for window. An application s main window is a form. If the application has other top-level windows, they too are forms. Dialog boxes are also forms. Despite their name, Windows Forms applications don t have to look like forms. They, like traditional Windows applications, exercise full control over what appears in their windows.

Windows Forms applications rely heavily upon classes found in the FCL s System.Windows.Forms namespace. That namespace includes classes such as Form, which models the behavior of windows, or forms ; Menu, which represents menus; and Clipboard, which provides a managed interface to the system s clipboard. The System.Windows.Forms namespace also contains numerous classes representing controls, with names like Button, TextBox, ListView, and MonthCalendar.

At the heart of nearly every Windows Forms application is a class derived from System.Windows.Forms.Form. An instance of the derived class represents the application s main window. It inherits from Form scores of properties and methods that make up 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 to as well as read. For example, you can change a form s border style by writing to its BorderStyle property, resize a form using its Size or ClientSize property, or change the text in a form s title bar with its Text property.

Another important building block of a Windows Forms application is a System.Windows.Forms class named Application. That class contains a static method named Run that drives a Windows Forms application by providing a message pump. You don t see the message pump, of course the very existence of messages is abstracted away by the .NET Framework. But it s there, and it s one more detail you don t have to worry about because the framework sweats such details for you.

Many Windows Forms applications also rely on classes in the System.Drawing namespace. System.Drawing contains classes that wrap the Graphics Device Interface+ (GDI+) portion of Windows. Classes such as Brush and Pen represent logical drawing objects. They define the look of lines, curves, and fills. The Bitmap and Image classes represent images and are capable of importing images from a variety of file types, including BMP files, GIFs, and JPEGs.

But the most important System.Drawing class of all is Graphics. Graphics is the Windows Forms equivalent of a Windows device context; it s the conduit for graphical output. If you want to draw a line in a Windows form, you call DrawLine on a Graphics object. If you want to draw a string of text, you call DrawString. Graphics contains a rich assortment of methods and properties that you can use to write graphical output to a form or any other device (such as a printer) that you can associate with a Graphics object.

Your First Windows Form

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 have been built around Hello, world applications.) Figure 4-1 lists the Windows Forms version of Hello, world. Figure 4-2 shows the resulting application.

Hello.cs

using System; using System.Windows.Forms; using System.Drawing; class MyForm : Form {     MyForm ()     {         Text = "Windows Forms Demo";     }     protected override void OnPaint (PaintEventArgs e)     {         e.Graphics.DrawString ("Hello, world", Font,             new SolidBrush (Color.Black), ClientRectangle);     }     static void Main ()     {         Application.Run (new MyForm ());     } }
Figure 4-1

Hello.cs source code.

Figure 4-2

The Hello, world Windows Forms sample.

In a Windows Forms application, each form is represented by an instance of a class derived from Form. In Hello.cs, the derived class is named MyForm. MyForm s constructor customizes the form s title bar text by assigning a string to MyForm s Text property. Text is one of more than 100 properties that MyForm inherits from Form.

Every Windows programmer knows that windows receive WM_PAINT messages, and that most screen rendering is performed in response to these messages. In Windows Forms, the equivalent of a WM_PAINT message is a virtual Form method named OnPaint. A derived class can override this method to paint itself in response to WM_PAINT messages.

The OnPaint override in MyForm (notice the keyword override, which tells the C# compiler that you re overriding rather than hiding a virtual method inherited from a base class) writes Hello, world to the form s client area the portion of the form bounded by the window border and title bar. OnPaint is passed a PaintEventArgs (System.Windows.Forms.PaintEventArgs) object, which contains properties named Graphics and ClipRectangle. The Graphics property holds a reference to a Graphics object that permits OnPaint to draw in the form s client area. ClipRectangle contains a reference to a Rectangle (System.Drawing.Rectangle) object that describes which part of the client area needs repainting.

MyForm.OnPaint uses Graphics.DrawString to render its output. The first parameter to DrawString is the string itself. The second is a Font (System.Drawing.Font) object 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 the Form property named Font. The third parameter is a Brush (System.Drawing.Brush) object specifying the text color. OnPaint uses a black SolidBrush (System.Drawing.SolidBrush) object, resulting in black text. 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. Because DrawString outputs text in the upper left corner of the formatting rectangle by default, Hello, world appears in the form s upper left corner.

If you wanted Hello, world displayed in the center of the window, you could use an alternate form of DrawString that accepts a StringFormat object as an input parameter and initialize the StringFormat s Alignment and LineAlignment properties to center the string horizontally and vertically. Here s the modified version of OnPaint:

protected override void OnPaint (PaintEventArgs e) { StringFormat format = new StringFormat (); format.Alignment = StringAlignment.Center; format.LineAlignment = StringAlignment.Center; e.Graphics.DrawString ("Hello, world", Font, new SolidBrush (Color.Black), ClientRectangle, format); }

The final member of MyForm is a static method named Main. Main is the application s entry point. Displaying the form on the screen is a simple matter of instantiating MyForm and passing a reference to the resulting object to Application.Run. In Figure 4-1, the statement

Application.Run (new MyForm ());

instantiates MyForm and displays the form.

Once you ve entered the code in Figure 4-1 and saved it to a file named Hello.cs, you ll need to compile it. Open a command prompt window, go to the folder where Hello.cs is stored, and type

csc /target:winexe hello.cs

The /target switch tells the compiler to produce a GUI Windows application (as opposed to a console application or, say, a DLL). The resulting executable is named Hello.exe.

Drawing in a Form: The GDI+

Writing Windows Forms applications that incorporate rich graphics means getting to know Graphics and other classes that expose the Windows GDI+ to managed code. The GDI has existed in Windows since version 1. The GDI+ is an enhanced version of the GDI that s available on any system with Windows XP or the .NET Framework installed.

One of the differences between the GDI and the GDI+ is the latter s support for gradient brushes, cardinal splines, and other drawing aids. But the larger difference lies in their respective programming models. Unlike the GDI, which uses a stateful model, the GDI+ is mostly stateless. A traditional Windows application programs values such as fonts and text colors into a device context. A Windows Forms application doesn t. It passes parameters detailing output characteristics to every Graphics method it calls. You saw an example of this in the previous section s sample program, which passed a Font object and a SolidBrush object to the DrawString method to identify the output font and text color.

Drawing Lines, Curves, and Figures

Font and SolidBrush are graphics primitives used to control the output rendered to a form. But they re not the only primitives the GDI+ places at your disposal. Here are some of the others:

GDI+ Graphics Primitives

Class

Description

Bitmap

Represents bitmapped images

Font

Defines font attributes: size, typeface, and so on

HatchBrush

Defines fills performed with hatch patterns

LinearGradientBrush

Defines fills performed with linear gradients

Pen

Defines the appearance of lines and curves

SolidBrush

Defines fills performed with solid colors

TextureBrush

Defines fills performed with bitmaps

Some of these classes are defined in the System.Drawing namespace; others belong to System.Drawing.Drawing2D. Pens, which are represented by instances of Pen, control the appearance of lines and curves. Font objects control the appearance of text, while brushes, represented by HatchBrush, LinearGradientBrush, SolidBrush, and TextureBrush, control fills. The following OnPaint method draws three different styles of rectangles: one that has no fill, a second that s filled with red, and a third that s filled with a gradient that fades from red to blue:

protected override void OnPaint (PaintEventArgs e) { Pen pen = new Pen (Color.Black); // Draw an unfilled rectangle e.Graphics.DrawRectangle (pen, 10, 10, 390, 90); // Draw a rectangle filled with a solid color SolidBrush solid = new SolidBrush (Color.Red); e.Graphics.FillRectangle (solid, 10, 110, 390, 90); e.Graphics.DrawRectangle (pen, 10, 110, 390, 90); // Draw a rectangle filled with a gradient Rectangle rect = new Rectangle (10, 210, 390, 90); LinearGradientBrush gradient = new LinearGradientBrush (rect, Color.Red, Color.Blue, LinearGradientMode.Horizontal); e.Graphics.FillRectangle (gradient, rect); e.Graphics.DrawRectangle (pen, rect); }

The output is shown in Figure 4-3. The Pen object passed to DrawRectangle borders each rectangle with a black line that s 1 unit wide. (By default, 1 unit equals 1 pixel, so the lines in this example are 1 pixel wide.) The brushes passed to FillRectangle govern the fills in the rectangles interiors. Observe that outlines and fills are drawn separately. Veteran Windows developers will find this especially interesting because Windows GDI functions that draw closed figures draw outlines and fills together.

Figure 4-3

Rectangles rendered in a variety of styles.

The Graphics class includes a variety of public Draw and Fill methods that you can use to draw lines, curves, rectangles, ellipses, and other figures. A partial list appears in the following table. The documentation that comes with the .NET Framework SDK contains an excellent dissertation on the GDI+, and it s packed with examples demonstrating how to draw anything from the simplest line to the most complex filled figure.

Graphics Methods for Drawing Lines, Curves, and Figures

Method

Description

DrawArc

Draws an arc

DrawBezier

Draws a Bezier spline

DrawCurve

Draws a cardinal spline

DrawEllipse

Draws a circle or an ellipse

DrawIcon

Draws an icon

DrawImage

Draws an image

DrawLine

Draws a line

DrawPie

Draws a pie-shaped wedge

DrawPolygon

Draws a polygon

DrawString

Draws a string of text

FillEllipse

Draws a filled circle or ellipse

FillPie

Draws a filled pie-shaped wedge

FillPolygon

Draws a filled polygon

FillRectangle

Draws a filled rectangle

Disposing of GDI+ Objects

As you learned in Chapter 2, classes that wrap unmanaged resources such as file handles and database connections require special handling to ensure that their resources are properly released. Pen, Brush, and other GDI+ classes that represent graphics primitives fall into this category because they wrap GDI+ handles. Failure to close GDI+ handles can result in debilitating resource leaks, especially in applications that run for a very long time. To be safe, you should call Dispose on pens, brushes, and other primitives to deterministically dispose of the resources that they encapsulate. Even Graphics objects should be disposed of if they re created programmatically rather than obtained from a PaintEventArgs.

One way to dispose of GDI+ objects is to call Dispose on them manually:

Pen pen = new Pen (Color.Black); . . . pen.Dispose ();

Some C# programmers prefer the special form of using that automatically generates calls to Dispose and encloses them in finally blocks. One advantage of this technique is that it ensures Dispose is called, even if an exception is thrown:

using (Pen pen = new Pen (Color.Black)) { . . . }

And some programmers do neither, banking on the hope that the garbage collector will kick in before the GDI+ runs out of resource space, or that the application won t run long enough for GDI+ resources to grow critically low.

Another approach is to create pens, brushes, and other graphics primitives when an application starts up and to use and reuse them as needed. This dramatically reduces the number of GDI+ resources that an application consumes, and it all but eliminates the need to call Dispose on each and every GDI+ object. It also improves performance ever so slightly.

Coordinates and Transformations

When you call DrawRectangle and FillRectangle, you furnish coordinates specifying the position of the rectangle s upper left corner and distances specifying the rectangle s width and height. By default, distances are measured in pixels. Coordinates specify locations in a two-dimensional coordinate system whose origin lies in the upper left corner of the form and whose x and y axes point to the right and down, respectively. If the default coordinate system or unit of measure is ill-suited to your needs, you can customize them as needed by adding a few simple statements to your program.

The coordinates passed to Graphics methods are world coordinates. World coordinates undergo two transformations on their way to the screen. They re first translated into page coordinates, which denote positions on a logical drawing surface. Later, page coordinates are translated into device coordinates, which denote physical positions on the screen.

The GDI+ uses a transformation matrix to convert world coordinates to page coordinates. Transformation matrices are mathematical entities that are used to convert x-y coordinate pairs from one coordinate system to another. They re well established in the world of computer graphics and well documented in computer science literature.

The transformation matrices that the GDI+ uses to perform coordinate conversions are instances of System.Drawing.Drawing2D.Matrix. Every Graphics object encapsulates a Matrix object and exposes it through a property named Transform. The default matrix is an identity matrix, which is the mathematical term for a transformation matrix that performs no transformation (just as multiplying a number by 1 yields the same number). You can customize the transformation matrix and hence the way world coordinates are converted to page coordinates in either of two ways. Option number one is to initialize an instance of Matrix with values that produce the desired transformation and assign it to the Transform property. That s no problem if you re an expert in linear algebra, but mere mortals will probably prefer option number two, which involves using Graphics methods such as TranslateTransform and RotateTransform to define coordinate transformations. TranslateTransform moves, or translates, the coordinate system s origin by a specified amount in the x and y directions. RotateTransform rotates the x and y axes. By combining the two, you can place the origin anywhere you want it and orient the x and y axes at any angle.

Confused? Then maybe an example will help. The following OnPaint method draws a rectangle that s 200 units wide and 100 units tall in a form s upper left corner, as seen in Figure 4-4:

protected override void OnPaint (PaintEventArgs e) { SolidBrush brush = new SolidBrush (Color.Red); e.Graphics.FillRectangle (brush, 0, 0, 200, 100); brush.Dispose (); }

Figure 4-4

Rectangle drawn with no translation or rotation.

The next OnPaint method draws the same rectangle, but only after calling TranslateTransform to move the origin to (100,100). Because the GDI+ now adds 100 to all x and y values you input, the rectangle moves to the right and down 100 units, as shown in Figure 4-5:

protected override void OnPaint (PaintEventArgs e) { SolidBrush brush = new SolidBrush (Color.Red); e.Graphics.TranslateTransform (100.0f, 100.0f); e.Graphics.FillRectangle (brush, 0, 0, 200, 100); brush.Dispose (); }

Figure 4-5

Rectangle drawn with translation applied.

The final example uses RotateTransform to rotate the x and y axes 30 degrees counterclockwise after moving the origin, producing the output shown in Figure 4-6:

protected override void OnPaint (PaintEventArgs e) { SolidBrush brush = new SolidBrush (Color.Red); e.Graphics.TranslateTransform (100.0f, 100.0f); e.Graphics.RotateTransform (-30.0f); e.Graphics.FillRectangle (brush, 0, 0, 200, 100); brush.Dispose (); }

Figure 4-6

Rectangle drawn with translation and rotation applied.

TranslateTransform and RotateTransform are powerful tools for positioning the coordinate system and orienting its axes. A related method named ScaleTransform lets you scale the coordinate system as well.

Something to keep in mind when you use transforms is that the order in which they re applied affects the output. In the preceding example, the coordinate system was first translated and then rotated. The origin was moved to (100,100) and then rotated 30 degrees. If you rotate first and translate second, the rectangle appears in a different location because the translation occurs along axes that are already rotated. Think of it this way: if you stand in a room, walk a few steps forward and then turn, you end up in a different place than you would had you turned first and then started walking. The same principle applies to matrix transformations.

Figure 4-8 lists the source code for a sample program named Clock that draws an analog clock face (see Figure 4-7) that shows the current time of day. The drawing is done with FillRectangle and FillPolygon, but the transforms are the real story. TranslateTransform moves the origin to the center of the form by translating x and y coordinates by amounts equal to half the form s width and height. RotateTransform rotates the coordinate system in preparation for drawing the hour and minute hands and the squares denoting positions on the clock face. ScaleTransform scales the output so that the clock face fills the form regardless of the form s size. The coordinates passed to FillRectangle and FillPolygon assume that the form s client area is exactly 200 units wide and 200 units tall. ScaleTransform applies x and y scaling factors that scale the output by an amount that equals the ratio of the physical client area size to the logical client area size.

So much happens in MyForm s OnPaint method that it s easy to miss an important statement in MyForm s constructor:

SetStyle (ControlStyles.ResizeRedraw, true);

This method call configures the form so that its entire client area is invalidated (and therefore repainted) whenever the form s size changes. This step is essential if you want the clock face to shrink and expand as the form shrinks and expands. If it s not clear to you what effect this statement has on the output, temporarily comment it out and recompile the program. Then resize the form and observe that the clock face doesn t resize until an external stimulus (such as the act of minimizing and restoring the form) forces a repaint to occur.

Figure 4-7

Analog clock drawn with the GDI+.

Clock.cs

using System; using System.Windows.Forms; using System.Drawing; class MyForm : Form { MyForm () { Text = "Clock"; SetStyle (ControlStyles.ResizeRedraw, true); } protected override void OnPaint (PaintEventArgs e) { SolidBrush red = new SolidBrush (Color.Red); SolidBrush white = new SolidBrush (Color.White); SolidBrush blue = new SolidBrush (Color.Blue); // Initialize the transformation matrix InitializeTransform (e.Graphics); // Draw squares denoting hours on the clock face for (int i=0; i<12; i++) { e.Graphics.RotateTransform (30.0f); e.Graphics.FillRectangle (white, 85, -5, 10, 10); } // Get the current time
Figure 4-8

Clock source code.

 DateTime now = DateTime.Now; int minute = now.Minute; int hour = now.Hour % 12; // Reinitialize the transformation matrix InitializeTransform (e.Graphics); // Draw the hour hand e.Graphics.RotateTransform ((hour * 30) + (minute / 2)); DrawHand (e.Graphics, blue, 40); // Reinitialize the transformation matrix InitializeTransform (e.Graphics); // Draw the minute hand e.Graphics.RotateTransform (minute * 6); DrawHand (e.Graphics, red, 80); // Dispose of the brushes red.Dispose (); white.Dispose (); blue.Dispose (); } void DrawHand (Graphics g, Brush brush, int length) { // Draw a hand that points straight up, and let // RotateTransform put it in the proper orientation Point[] points = new Point[4]; points[0].X = 0; points[0].Y = -length; points[1].X = -12; points[1].Y = 0; points[2].X = 0; points[2].Y = 12; points[3].X = 12; points[3].Y = 0; g.FillPolygon (brush, points); } void InitializeTransform (Graphics g) { // Apply transforms that move the origin to the center // of the form and scale all output to fit the form's width // or height g.ResetTransform (); g.TranslateTransform (ClientSize.Width / 2, ClientSize.Height / 2); float scale = System.Math.Min (ClientSize.Width, ClientSize.Height) / 200.0f; g.ScaleTransform (scale, scale); } static void Main () { Application.Run (new MyForm ()); } }

Units of Measure

Just as a Graphics object s Transform property governs the conversion of world coordinates to page coordinates, the PageUnit and PageScale properties play important roles in the conversion of page coordinates to device coordinates. PageUnit identifies a system of units and can be set to any value defined in the System.Drawing.GraphicsUnit enumeration: Pixel for pixels, Inch for inches, and so on. PageScale specifies the scaling factor. It can be used in lieu of or in combination with ScaleTransform to scale most output. Some types of output, including fonts, can be scaled only with PageScale.

PageUnit s default value is GraphicsUnit.Display, which means that one unit in page coordinates equals one pixel on the screen. The following OnPaint method sets the unit of measurement to inches and draws the ruler shown in Figure 4-9:

protected override void OnPaint (PaintEventArgs e) { Pen pen = new Pen (Color.Black, 0); SolidBrush yellow = new SolidBrush (Color.Yellow); SolidBrush black = new SolidBrush (Color.Black); // Set the unit of measurement to inches e.Graphics.PageUnit = GraphicsUnit.Inch; // Add 0.5 to all x and y coordinates e.Graphics.TranslateTransform (0.5f, 0.5f); // Draw the body of the ruler e.Graphics.FillRectangle (yellow, 0, 0, 6, 1); e.Graphics.DrawRectangle (pen, 0, 0, 6, 1); // Draw tick marks for (float x=0.25f; x<6.0f; x+=0.25f) e.Graphics.DrawLine (pen, x, 0.0f, x, 0.08f); for (float x=0.5f; x<6.0f; x+=0.5f) e.Graphics.DrawLine (pen, x, 0.0f, x, 0.16f); for (float x=1.0f; x<6.0f; x+=1.0f) e.Graphics.DrawLine (pen, x, 0.0f, x, 0.25f); // Draw numeric labels StringFormat format = new StringFormat (); format.Alignment = StringAlignment.Center; format.LineAlignment = StringAlignment.Center; for (float x=1.0f; x<6.0f; x+=1.0f) { string label = String.Format ("{0}", Convert.ToInt32 (x)); RectangleF rect = new RectangleF (x - 0.25f, 0.25f, 0.5f, 0.25f); e.Graphics.DrawString (label, Font, black, rect, format); } // Dispose of GDI+ objects pen.Dispose (); yellow.Dispose (); black.Dispose (); }

The same ruler could be drawn using the default PageUnit value by using the Graphics object s DpiX and DpiY properties to manually convert between inches and pixels. But expressing coordinates and distances in inches makes the code more readable. Note the call to TranslateTransform offsetting all x and y coordinates by a half-inch to move the ruler out of the upper left corner of the form. Also note the 0 passed to Pen s constructor. The 0 sets the pen width to 1 pixel, no matter what system of units is selected. Without the 0, a pen defaults to a width of 1 unit. In the GraphicsUnit.Inch system of measurement, 1 unit equals 1 inch, so a 1-unit-wide pen would be a very wide pen indeed.

One nuance you should be aware of is that values expressed in inches are logical values, meaning the ruler probably won t measure exactly 6 inches long. Logical values differ slightly from physical values because Windows doesn t know precisely how many pixels per inch your screen can display. To compensate, it uses an assumed resolution of 96 dots per inch.

Figure 4-9

Ruler drawn using inches as the unit of measurement.

Menus

Menus are a staple of GUI applications. Nearly everyone who sits down in front of a computer understands that clicking an item in an overhead menu displays a drop-down list of commands. Even novices quickly catch on once they see menus demonstrated a time or two.

Because menus are such an important part of a user interface, the .NET Framework provides a great deal of support to applications that use them. The System.Windows.Forms classes listed in the following table play a role in menu creation and operation. The next several sections discuss these and other menu-related classes and offer examples demonstrating their use.

System.Windows.Forms Menu Classes

Class

Description

Menu

Abstract base class for other menu classes

MainMenu

Represents main menus

ContextMenu

Represents context menus

MenuItem

Represents the items in a menu

Main Menus

A main menu, sometimes called a top-level menu, is one that appears in a horizontal bar underneath a window s title bar. The following code creates a main menu containing two drop-down menus labeled File and Edit and attaches it to the form:

// Create a MainMenu object MainMenu menu = new MainMenu (); // Add a File menu and populate it with items MenuItem item = menu.MenuItems.Add ("&File"); item.MenuItems.Add ("&New", new EventHandler (OnFileNew)); item.MenuItems.Add ("&Open...", new EventHandler (OnFileOpen)); item.MenuItems.Add ("&Save", new EventHandler (OnFileSave)); item.MenuItems.Add ("Save &As...", new EventHandler (OnFileSaveAs)); item.MenuItems.Add ("-"); // Menu item separator (horizontal bar) item.MenuItems.Add ("E&xit", new EventHandler (OnFileExit)); // Add an Edit menu and populate it, too item = menu.MenuItems.Add ("&Edit"); item.MenuItems.Add ("Cu&t", new EventHandler (OnEditCut)); item.MenuItems.Add ("&Copy", new EventHandler (OnEditCopy)); item.MenuItems.Add ("&Paste", new EventHandler (OnEditPaste)); // Attach the menu to the form Menu = menu;

The first statement creates a MainMenu object and captures the returned MainMenu reference in a variable named menu. All menus, including instances of MainMenu, inherit a property named MenuItems from Menu that represents the items in the menu. Calling Add on the MenuItemCollection represented by MenuItems adds an item to the menu and optionally registers an event handler that s called when a user selects the item. The statements

MenuItem item = menu.MenuItems.Add ("&File"); item = menu.MenuItems.Add ("&Edit");

add top-level items named File and Edit to the main menu. The remaining calls to Add add items to the File and Edit menus. (The ampersand in the menu text identifies a keyboard shortcut. The ampersand in "&File" designates Alt+F as the shortcut for File and automatically underlines the F.) The final statement attaches the MainMenu to the form by assigning the MainMenu to the form s Menu property. All forms inherit the Menu property from System.Windows.Forms.Form.

Processing Menu Commands

Selecting an item from a menu fires a Click event and activates the Click event handler, if any, registered for that item. The code sample in the previous section registers handlers named OnFileNew, OnFileOpen, and so on for its menu items. Click event handlers are prototyped this way:

void HandlerName (Object sender, EventArgs e)

Thus, the OnFileExit handler registered for the File/Exit command might be implemented like this:

void OnFileExit (Object sender, EventArgs e) { Close (); // Close the form }

The first parameter passed to the event handler identifies the menu item that the user selected. The second parameter is a container for additional information about the event that precipitated the call. Menu item event handlers typically ignore this parameter because EventArgs contains no information of interest.

An alternative method for connecting menu items to event handlers is to use C# s += syntax, as shown here:

MenuItem exit = item.MenuItems.Add ("E&xit"); exit.Click += new EventHandler (OnFileExit);

I prefer to identify event handlers in Add simply because doing so makes my code more concise. You should do what works best for you.

Context Menus

Many applications pop up context menus in response to clicks of the right mouse button. Inside a context menu is a context-sensitive list of commands that can be applied to the target of the click. In Windows Forms applications, ContextMenu objects represent context menus. ContextMenus are populated with items in the same way that MainMenus are. One way to display a context menu is to call ContextMenu.Show. Here s an example that creates a context menu containing three items and displays it on the screen:

ContextMenu menu = new ContextMenu (); menu.MenuItems.Add ("&Open", new EventHandler (OnOpen)); menu.MenuItems.Add ("&Rename", new EventHandler (OnRename)); menu.MenuItems.Add ("&Delete", new EventHandler (OnDelete)); menu.Show (this, new Point (x, y));

The first parameter to ContextMenu.Show identifies the form that the menu belongs to. (It s this form s event handlers that are called when items are selected from the context menu.) The second parameter specifies where the menu is to be displayed. The units are pixels, and the coordinates are relative to the upper left corner of the form identified in Show s first parameter.

If an application uses one context menu to serve an entire form that is, if it displays the same context menu no matter where in the form a right-click occurs there s an easier way to display a context menu. Simply create a ContextMenu object and assign it to the form s ContextMenu property, as shown here:

ContextMenu menu = new ContextMenu (); . . . ContextMenu = menu;

The .NET Framework responds by displaying the context menu when the form is right-clicked.

As with items in a main menu, context menu items fire Click events when clicked. And like main menu items, you can identify event handlers when adding items to the menu or wire them up separately with the += operator.

Menu Item States

Many applications change the states of their menu items on the fly to reflect changing states in the application. For example, if nothing is selected in an application that features an Edit menu, protocol demands that the application disable its Cut and Copy commands.

Windows Forms applications control menu item states by manipulating the properties of MenuItem objects. A MenuItem s Checked property determines whether the corresponding item is checked or unchecked. Enabled determines whether the item is enabled or disabled, and Text exposes the text of a menu item. If item is a MenuItem object, the following statement places a check mark next to it:

item.Checked = true;

And this statement removes the check mark:

item.Checked = false;

MFC programmers are familiar with the concept of update handlers, which are functions whose sole purpose is to update menu item states. MFC calls update handlers just before a menu appears on the screen, affording the handlers the opportunity to update the items in the menu before the user sees them. Update handlers are useful because they decouple the logic that changes the state of an application from the logic that updates the state of the application s menu items.

You can write update handlers for Windows Forms menu items, too. The secret is to register a handler for the Popup event that fires when the menu containing the item you want to update is clicked and to do the updating in the Popup handler. The following example registers a Popup handler for the Edit menu and updates the items in the menu to reflect the current state of the application:

MenuItem item = menu.MenuItems.Add ("&Edit"); item.Popup += new EventHandler (OnPopupEditMenu); . . . void OnPopupEditMenu (Object sender, EventArgs e) { MenuItem menu = (MenuItem) sender; foreach (MenuItem item in menu.MenuItems) { switch (item.Text) { case "Cu&t": case "&Copy": item.Enabled = IsSomethingSelected (); break; case "&Paste": item.Enabled = IsSomethingOnClipboard (); break; } } }

IsSomethingSelected and IsSomethingOnClipboard are fictitious methods, but hopefully their intent is clear nonetheless. By processing MenuItem.Popup events, an application can delay updating its menu items until they need to be updated and eliminate the possibility of forgetting to update them at all.

Accelerators

A final note concerning menus regards accelerators. Accelerators are keys or key combinations that, when pressed, invoke menu commands. Ctrl+O (for Open) is an example of a commonly used accelerator; Ctrl+S (for Save) is another. Windows programmers implement accelerators by loading accelerator resources. Windows Forms programmers have it much easier. They new up a MenuItem and identify the accelerator in the MenuItem constructor s third parameter. Then they add the resulting item to the menu with MenuItemCollection.Add:

item.MenuItems.Add (new MenuItem ("&Open...", new EventHandler (OnFileOpen), Shortcut.CtrlO));

CtrlO is a member of the Shortcut enumeration defined in System.Windows.Forms. Shortcut contains a long list of elements representing all the different accelerators you can choose from.

The ImageView Application

The application shown in Figures 4-10 and 4-11 is a Windows Forms image viewer that s capable of displaying a wide variety of image files. The heart of the application is the statement

Bitmap bitmap = new Bitmap (FileName);

which creates a new System.Drawing.Bitmap object encapsulating the image in the file identified by FileName. Having the ability to load an image with a single statement is powerful. The Bitmap class is capable of reading BMPs, GIFs, JPEGs, and other popular image file formats. To accomplish the same thing in a traditional (unmanaged) Windows application, you d have to write reams of code or purchase a third-party graphics library.

Equally interesting is the statement

g.DrawImage (MyBitmap, ClientRectangle);

which draws the bitmap on the form. (MyBitmap is a private field that stores a reference to the currently displayed bitmap.) Windows programmers who want to display bitmaps have to grapple with memory device contexts and BitBlts. Windows Forms programmers simply call Graphics.DrawImage.

ImageView demonstrates other important principles of Windows Forms programming as well. Here are some of the highlights:

  • How to use menus, menu handlers, and menu update handlers

  • How to size a form by writing to its ClientSize property

  • How to use Open File dialog boxes (instances of System.Windows.Forms.OpenFileDialog) to solicit file names from users

  • How to use MessageBox.Show to display message boxes

  • How to create a scrollable form by setting a form s AutoScroll property to true and its AutoScrollMinSize property to the size of the virtual viewing area

  • How to properly dispose of Bitmap objects

The final item in this list is an important one because without it, ImageView would leak memory like a sieve. Rather than open an image file and assign the Bitmap reference to MyBitmap in one statement, like this:

MyBitmap = new Bitmap (FileName);

ImageView does this:

Bitmap bitmap = new Bitmap (FileName); if (MyBitmap != null) MyBitmap.Dispose (); // Important! MyBitmap = bitmap;

See what s happening? Bitmap objects encapsulate bitmaps, which are unmanaged resources that can consume a lot of memory. By calling Dispose on the old Bitmap after opening a new one, ImageView disposes of the encapsulated bitmap immediately rather than wait for the garbage collector to call the Bitmap s Finalize method. Remember the dictum in Chapter 2 that admonished developers to always call Close or Dispose on objects that wrap unmanaged resources? This code is the embodiment of that dictum and a fine example of why programmers must constantly be on their guard to avoid the debilitating side effects of nondeterministic destruction.

Figure 4-10

ImageView showing a JPEG file.

ImageView.cs

using System; using System.Windows.Forms; using System.Drawing; class MyForm : Form { MenuItem NativeSize; MenuItem FitToWindow; bool ShowNativeSize = true; int FilterIndex = -1; Bitmap MyBitmap = null; MyForm () { // Set the form's title Text = "Image Viewer"; // Set the form's size ClientSize = new Size (640, 480); // Create a menu MainMenu menu = new MainMenu (); MenuItem item = menu.MenuItems.Add ("&Options"); item.Popup += new EventHandler (OnPopupOptionsMenu); item.MenuItems.Add (new MenuItem ("&Open...", new EventHandler (OnOpenImage), Shortcut.CtrlO));

 item.MenuItems.Add ("-"); item.MenuItems.Add (FitToWindow = new MenuItem ("Size Image to &Fit Window", new EventHandler (OnFitToWindow)) ); item.MenuItems.Add (NativeSize = new MenuItem ("Show Image in &Native Size", new EventHandler (OnNativeSize)) ); item.MenuItems.Add ("-"); item.MenuItems.Add (new MenuItem ("E&xit", new EventHandler (OnExit))); // Attach the menu to the form Menu = menu; } // Handler for Options menu popups void OnPopupOptionsMenu (object sender, EventArgs e) { NativeSize.Checked = ShowNativeSize; FitToWindow.Checked = !ShowNativeSize; } // Handler for the Open command void OnOpenImage (object sender, EventArgs e) { OpenFileDialog ofd = new OpenFileDialog (); ofd.Filter = "Image Files (JPEG, GIF, BMP, etc.) " +  "*.jpg;*.jpeg;*.gif;*.bmp;*.tif;*.tiff;*.png " +  "JPEG files (*.jpg;*.jpeg) *.jpg;*.jpeg "  +  "GIF Files (*.gif) *.gif "  +  "BMP Files (*.bmp) *.bmp "  +  "TIFF Files (*.tif;*.tiff) *.tif;*.tiff "  +  "PNG Files (*.png) *.png "  +  "All files (*.*) *.*"; if (FilterIndex != -1) ofd.FilterIndex = FilterIndex; if (ofd.ShowDialog () == DialogResult.OK) { String FileName = ofd.FileName; if (FileName.Length != 0) { FilterIndex = ofd.FilterIndex; try { Bitmap bitmap = new Bitmap (FileName); if (MyBitmap != null) MyBitmap.Dispose (); // Important! MyBitmap = bitmap; string[] parts = FileName.Split ('\\'); Text = "Image Viewer - " + parts[parts.Length - 1]; if (ShowNativeSize) { AutoScroll = true; AutoScrollMinSize = MyBitmap.Size; AutoScrollPosition = new Point (0, 0); } Invalidate (); } catch (ArgumentException) { MessageBox.Show (String.Format ("{0} is not " +  "a valid image file", FileName), "Error", MessageBoxButtons.OK, MessageBoxIcon.Error); } } } ofd.Dispose (); } // Handler for the Size Image to Fit Window command void OnFitToWindow (object sender, EventArgs e) { ShowNativeSize = false; SetStyle (ControlStyles.ResizeRedraw, true); if (MyBitmap != null) { AutoScroll = false; Invalidate (); } } // Handler for the Show Image in Native Size command void OnNativeSize (object sender, EventArgs e) { ShowNativeSize = true; SetStyle (ControlStyles.ResizeRedraw, false); if (MyBitmap != null) { AutoScroll = true; AutoScrollMinSize = MyBitmap.Size; AutoScrollPosition = new Point (0, 0); Invalidate (); } } // Handler for the Exit command void OnExit (object sender, EventArgs e) { Close (); } // OnPaint handler protected override void OnPaint (PaintEventArgs e) { if (MyBitmap != null) { Graphics g = e.Graphics; if (ShowNativeSize) g.DrawImage (MyBitmap, AutoScrollPosition.X, AutoScrollPosition.Y, MyBitmap.Width, MyBitmap.Height); else g.DrawImage (MyBitmap, ClientRectangle); } } static void Main () { Application.Run (new MyForm ()); } }

Figure 4-11

ImageView source code.

Mouse and Keyboard Input

ImageView takes all its input from menus, but forms can also process mouse and keyboard input. Windows notifies forms about mouse and keyboard input events using messages. The WM_LBUTTONDOWN message, for example, signifies a press of the left mouse button in a form s client area. Windows forms process mouse and keyboard input by overriding virtual methods inherited from Form. The following table lists the virtual methods that correspond to mouse and keyboard events. The rightmost column identifies the type of event argument passed in the method s parameter list.

Form Methods for Processing Mouse and Keyboard Input

Method

Called When

Argument Type

OnKeyDown

A key is pressed

KeyEventArgs

OnKeyPress

A character is typed on the keyboard

KeyPressEventArgs

OnKeyUp

A key is released

KeyEventArgs

OnMouseDown

A mouse button is pressed

MouseEventArgs

OnMouseEnter

The mouse cursor enters a form

EventArgs

OnMouseHover

The mouse cursor pauses over a form

EventArgs

OnMouseLeave

The mouse cursor leaves a form

EventArgs

OnMouseMove

The mouse cursor moves over a form

MouseEventArgs

OnMouseUp

A mouse button is released

MouseEventArgs

OnMouseWheel

The mouse wheel is rolled

MouseEventArgs

Processing Keyboard Input

The OnKeyDown and OnKeyUp methods correspond to WM_KEYDOWN and WM_KEYUP messages in Windows. If overridden in a derived class, they re called when keys are pressed and released. The KeyCode property of the KeyEventArgs passed to OnKeyDown and OnKeyUp identify the key that generated the event. Here s an example that traps presses of function key F1:

protected override void OnKeyDown (KeyEventArgs e) { if (e.KeyCode == Keys.F1) { // Function key F1 was pressed } }

KeyEventArgs also includes properties named Alt, Control, Shift, and Modifiers that you can use to determine whether the Ctrl, Alt, or Shift key (or some combination thereof) was held down when the keyboard event occurred.

A related method named OnKeyPress corresponds to WM_CHAR messages in Windows. It s called when a character is input from the keyboard. Not all keys generate character input. Some, such as the A through Z keys, do, but others, such as F1 and F2, do not. To process input from noncharacter keys, you override OnKeyDown. To process input from character keys, you generally override OnKeyPress instead. Why? Because OnKeyPress receives a KeyPressEventArgs whose KeyChar property tells you what character was entered, taking into account the state of other keys on the keyboard (such as Shift) that affect the outcome. If a user presses the C key, OnKeyDown tells you that the C key was pressed. OnKeyPress tells you whether the C is uppercase or lowercase. Here s an OnKeyPress handler that responds one way to an uppercase C and another way to a lowercase C:

protected override void OnKeyPress (KeyPressEventArgs e) { if (e.KeyChar == 'C') { // Do something } else if (e.KeyChar == 'c') { // Do something else } }

Processing Mouse Input

Pressing and releasing a mouse button with the cursor over a form calls the form s OnMouseDown and OnMouseUp methods, in that order. Both methods receive a MouseEventArgs containing a Button property identifying the button that was clicked (MouseButtons.Left, MouseButtons.Middle, or MouseButtons.Right), a Clicks property indicating whether an OnMouseDown signifies a single click (Clicks == 1) or double-click (Clicks == 2), and X and Y properties identifying the cursor position. Coordinates are always expressed in pixels and are always relative to the upper left corner of the form in which the click occurred. The coordinate pair (100,200), for example, indicates that the event occurred with the cursor 100 pixels to the right of and 200 pixels below the form s upper left corner.

The following OnMouseDown method draws a small X at the current cursor position each time the left mouse button is pressed:

protected override void OnMouseDown (MouseEventArgs e) { if (e.Button == MouseButtons.Left) { Graphics g = Graphics.FromHwnd (Handle); Pen pen = new Pen (Color.Black); g.DrawLine (pen, e.X - 4, e.Y - 4, e.X + 4, e.Y + 4); g.DrawLine (pen, e.X - 4, e.Y + 4, e.X + 4, e.Y - 4); pen.Dispose (); g.Dispose (); } }

This code sample demonstrates another important Windows Forms programming principle: how to draw in a form outside OnPaint. The secret? Pass the form s window handle, which is stored in Form.Handle, to the static Graphics.FromHwnd method. You get back a Graphics object. Don t forget to call Dispose on the Graphics object when you re finished with it because a Graphics object acquired this way, unlike a Graphics object passed in a PaintEventArgs, is not disposed of automatically.

An alternate way to sense mouse clicks is to override Form methods named OnClick and OnDoubleClick. The former is called when a form is clicked with the left mouse button; the latter is called when the form is double-clicked. Neither method receives information about click coordinates. OnClick and OnDoubleClick are generally more interesting to control developers than to form developers because forms that process mouse clicks typically want to know where the clicks occur.

To track mouse movement over a form, override OnMouseMove in the derived class. OnMouseMove corresponds to Windows WM_MOUSEMOVE messages. The X and Y properties of the MouseEventArgs passed to OnMouseMove identify the latest cursor position. The Button property identifies the button or buttons that are held down. If both the left and right buttons are depressed, for example, Button will equal MouseButtons.Left and MouseButtons.Right logically ORed together.

The OnMouseEnter, OnMouseHover, and OnMouseLeave methods enable a form to determine when the cursor enters it, hovers motionlessly over it, and leaves it. One use for these methods is to update a real-time cursor coordinate display. The code for the MouseTracker application shown in Figure 4-13 demonstrates how to go about it. As the mouse moves over the form, OnMouseMove updates a coordinate display in the center of the form. (See Figure 4-12.) When the mouse leaves the form, OnMouseLeave blanks out the coordinate display. No OnMouseEnter handler is needed because an OnMouseEnter call is always closely followed by an OnMouseMove call identifying the cursor position.

Figure 4-12

MouseTracker displaying real-time cursor coordinates.

MouseTracker.cs

using System; using System.Windows.Forms; using System.Drawing; class MyForm : Form { int cx; int cy; MyForm () { Text = "Mouse Tracker";

 Graphics g = Graphics.FromHwnd (Handle); SizeF size = g.MeasureString ("MMMM, MMMM", Font); cx = (Convert.ToInt32 (size.Width) / 2) + 8; cy = (Convert.ToInt32 (size.Height) / 2) + 8; g.Dispose (); } protected override void OnMouseMove (MouseEventArgs e) { Graphics g = Graphics.FromHwnd (Handle); EraseCoordinates (g); ShowCoordinates (e.X, e.Y, g); g.Dispose (); } protected override void OnMouseLeave (EventArgs e) { Graphics g = Graphics.FromHwnd (Handle); EraseCoordinates (g); g.Dispose (); } void EraseCoordinates (Graphics g) { int x = ClientRectangle.Width / 2; int y = ClientRectangle.Height / 2; SolidBrush brush = new SolidBrush (BackColor); g.FillRectangle (brush, x - cx, y - cy, x + cx, y + cy); brush.Dispose (); } void ShowCoordinates (int x, int y, Graphics g) { StringFormat format = new StringFormat (); format.Alignment = StringAlignment.Center; format.LineAlignment = StringAlignment.Center; string coords = String.Format ("{0}, {1}", x, y); SolidBrush brush = new SolidBrush (Color.Black); g.DrawString (coords, Font, brush, ClientRectangle, format); brush.Dispose (); } static void Main () { Application.Run (new MyForm ()); }

Figure 4-13

MouseTracker source code.

The NetDraw Application

Here s another sample application to chew on, one that processes both mouse and keyboard input. It s called NetDraw, and it s a simple sketching application inspired by the Scribble tutorial that comes with Visual C++. (See Figure 4-14.) To draw, press and hold the left mouse button and begin moving the mouse. To clear the drawing area, press the Del key.

NetDraw s source code appears in Figure 4-15. The OnMouseDown, OnMouseMove, and OnMouseUp methods do most of the heavy lifting. OnMouseDown creates a new Stroke object representing the stroke the user has just begun drawing and records it in CurrentStroke. OnMouseMove adds the latest x-y coordinate pair to the Stroke object that OnMouseDown created. OnMouseUp adds the Stroke to Strokes, which is an ArrayList that records all the strokes that the user draws. When the form needs repainting, OnPaint iterates through the Strokes array calling Stroke.Draw to reproduce each and every stroke.

Stroke is a class defined in NetDraw.cs. It wraps an ArrayList that stores an array of Points. Thanks to the ArrayList, one Stroke object is capable of holding a virtually unlimited number of x-y coordinate pairs. Stroke s Draw method uses Graphics.DrawLine to draw lines connecting the pairs.

Figure 4-14

NetDraw in action.

NetDraw.cs

using System; using System.Collections; using System.Windows.Forms; using System.Drawing; using System.Drawing.Drawing2D; class MyForm : Form { Stroke CurrentStroke = null; ArrayList Strokes = new ArrayList (); MyForm () { Text = "NetDraw"; } protected override void OnPaint (PaintEventArgs e) { // Draw all currently recorded strokes foreach (Stroke stroke in Strokes) stroke.Draw (e.Graphics); } protected override void OnMouseDown (MouseEventArgs e) { if (e.Button == MouseButtons.Left) { // Create a new Stroke and assign it to CurrentStroke CurrentStroke = new Stroke (e.X, e.Y); } } protected override void OnMouseMove (MouseEventArgs e) { if ((e.Button & MouseButtons.Left) != 0 && CurrentStroke != null) { // Add a new segment to the current stroke CurrentStroke.Add (e.X, e.Y); Graphics g = Graphics.FromHwnd (Handle); CurrentStroke.DrawLastSegment (g); g.Dispose (); } } protected override void OnMouseUp (MouseEventArgs e) {

 if (e.Button == MouseButtons.Left && CurrentStroke != null) { // Complete the current stroke if (CurrentStroke.Count > 1) Strokes.Add (CurrentStroke); CurrentStroke = null; } } protected override void OnKeyDown (KeyEventArgs e) { if (e.KeyCode == Keys.Delete) { // Delete all strokes and repaint Strokes.Clear (); Invalidate (); } } static void Main () { Application.Run (new MyForm ()); } } class Stroke { ArrayList Points = new ArrayList (); public int Count { get { return Points.Count; } } public Stroke (int x, int y) { Points.Add (new Point (x, y)); } public void Add (int x, int y) { Points.Add (new Point (x, y)); } public void Draw (Graphics g) {    Pen pen = new Pen (Color.Black, 8);    pen.EndCap = LineCap.Round;    for (int i=0; i<Points.Count - 1; i++)        g.DrawLine (pen, (Point) Points[i], (Point) Points[i + 1]);    pen.Dispose (); } public void DrawLastSegment (Graphics g) { Point p1 = (Point) Points[Points.Count - 2]; Point p2 = (Point) Points[Points.Count - 1]; Pen pen = new Pen (Color.Black, 8); pen.EndCap = LineCap.Round; g.DrawLine (pen, p1, p2); pen.Dispose (); } }

Figure 4-15

NetDraw source code.

Other Form-Level Events

A class that derives from Form inherits a long list of virtual methods whose names begin with On. On methods are called in response to form-level events. They exist to simplify event processing. If there were no On methods, every event handler you write would have to be wrapped in a delegate and manually connected to an event.

OnKeyDown, OnMouseDown, and OnPaint are just a few of the On methods that you can override in a derived class to respond to the various events that take place over a form s lifetime. The following table lists some of the others. If you want to know when your form is resized, simply override OnSizeChanged in the derived class. OnSizeChanged is the Windows Forms equivalent of WM_SIZE messages and is called whenever a form s size changes. Want to know when a form is about to close so you can warn the user if your application contains unsaved data? If so, override OnClosing. If you want, you can even prevent the form from closing by setting the Cancel property of the CancelEventArgs parameter passed to OnClosing to true.

Virtual Methods That Correspond to Form-Level Events

Method

Called When

OnActivated

A form is activated

OnClosed

A form is closed

OnClosing

A form is about to close

OnDeactivate

A form is deactivated

OnGotFocus

A form receives the input focus

OnLoad

A form is created

OnLocationChanged

A form is moved

OnLostFocus

A form loses the input focus

OnPaintBackground

A form s background needs repainting

OnSizeChanged

A form is resized

The sample program shown in Figure 4-16 demonstrates one application for OnClosing. When the user clicks the Close button in the form s upper right corner, Windows begins closing the form and the framework calls OnClosing. This implementation of OnClosing displays a message box asking the user to click Yes to close the form or No to cancel the operation. If the answer is yes, OnClosing sets CancelEventArgs.Cancel to false and the form closes. If, however, the answer is no, OnClosing sets CancelEventArgs.Cancel to true and the form remains on the screen.

CloseDemo.cs

using System; using System.Windows.Forms; using System.ComponentModel; class MyForm : Form { MyForm () { Text = "OnClosing Demo"; } protected override void OnClosing (CancelEventArgs e) { DialogResult result = MessageBox.Show ("Close this form?", "Please Verify", MessageBoxButtons.YesNo, MessageBoxIcon.Question); e.Cancel = (result == DialogResult.No); } static void Main () { Application.Run (new MyForm ()); } }
Figure 4-16

CloseDemo source code.



Programming Microsoft  .NET
Applied MicrosoftNET Framework Programming in Microsoft Visual BasicNET
ISBN: B000MUD834
EAN: N/A
Year: 2002
Pages: 101

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