Working with .NET Windows Forms
In the first two days you learned quite a bit about the way Windows Forms applications work. You can identify and describe each part of a Windows Forms application, and how it works with .NET, and you can infer the usage from those you don't know.
Since you have the basics now on how to construct Windows Forms, it's time to get into the details. Today's lesson focuses on all aspects of the System.Windows.Forms.Form object, and shows you the multitude of ways that you can customize your applications.
Today you will learn
How to use the Object object
How to control a form's layout and appearance
How to control interactivity in your forms
What the message loop is and how it works with the CLR
How to handle keyboard and mouse input
How to make your forms work with drag-and-drop
The Object-Oriented Windows Form
If you haven't started thinking of your forms as objects yet, now is a good time to start. Imagine the Windows Form on your screen as an actual window in your house. You can change the color of the window, the opacity (how transparent the window is), the type of knobs and locks, curtain dressing, type of glass, and on and on. You can do virtually anything you want to your window (assuming you have the necessary resources).
The same statement can be said about Windows Forms. You know from yesterday's lesson that you can change the caption and size of a form. You can also change the title bar, the way objects inside the form are arranged, the color, the opacity and visibility, the borders, the buttons, and so on. Figure 3.1 shows just a few styles your form can take on.
Figure 3.1 Forms can be transparent, have no borders, and be resized.
Thus, it is helpful to think of Windows Forms as generic objects that can be manipulated and customized however you want. You can go to the .NET Class Library "store" and pick out a Form object, and then tailor it as you see fit.
Working with the Object Object
You worked with a few objects in Days 1 and 2, but you haven't yet examined the Object object, the base class for all .NET classes, including Windows Forms. Because it is inherited by every class, you will deal it with from now on, so it's a good idea to learn a bit more about itspecifically, the methods it provides you.
Object is the most generic class you can use in .NET. As such, it is used to represent any otherwise unknown object. For example, if you use the following statement to create a variable, CLR doesn't know what type to create it as, and just creates an Object:
dim MyObjectVariable
Because your variable is just an Object, you lose a lot of functionality that you would gain from declaring an explicit data type. For example, you can add an integer to an integer, but you cannot add Objects together; doing so would result in an error no matter your intentions. Therefore it's a good idea to always declare a specific type, rather than using Object for everything.
NOTE
By default, your applications must always declare a type for your variables to prevent misuse and increase performance. However, you can turn this safety feature off by disabling the Option Strict feature. See the "Compiling Your Windows Forms" section in Day 2 for more information on how to do this.
There are many functions you'll use in Windows Forms programming that create objects for you. For example, the ConfigurationSettings.GetConfig method retrieves configuration settings from a file. This function returns an Object data type, because it doesn't know specifically what you're going to do with the returned results. By returning an Object, you are free to transform the results to any data type you want by casting, a process that transforms one data type into another. In C#, you cast a variable with the following:
myDataType myVariable; //declare variable of myDataType myVariable = (myDataType) ConfigurationSettings.GetConfig ("some setting"); //retrieve settings and cast to myDataType
There are five key methods of the Object object: Equals, ReferenceEquals, GetHashCode, GetType, and ToString. These all come in handy, so let's cover them individually. Remember that every object inherits from Object, so these methods are available to every object you use.
The Equals method is both a static and non-static member. This means that you can call it from either a specific instance of an object, or from the Object type itself. For example, Listing 3.1 shows a snippet using both static and non-static versions of the Equals method.
Listing 3.1 Using the Equals Method
1: int intA; 2: int intB; 3: 4: intA = 4; 5: intB = 5; 6: 7: Console.WriteLine("{0}", intA.Equals(intB)); //non-static method 8: 9: Console.WriteLine("{0}", Object.Equals(intA, intB)); //static method
Lines 7 and 9 will both print "False" to the command line because the intA and intB objects are not equal (intA is 4, and intB is 5). Line 7, however, calls the Equals method from an instance, while line 9 calls Equals from the Object class itself. There are many times you need to compare two objects, so this method will prove useful.
TIP
The Console.Writeline method prints a string to the command line. The first parameter is the string to be printed. A number in brackets means the method should refer to one of the additional supplied parameters: {0} means print the next parameter, {1} means print the parameter after that, and so on. You could also simply use the parameter in place of the numeric identifiers.
To use this method, you must compile your application as a regular executable file (use the /t:exe option when compiling).
The ReferenceEquals method is a static-only member that is similar to the Equals method. The difference is that ReferenceEquals compares two objects to see if they are the same instance, rather than the same value. This can be done because an instance in computer terms is defined by a location in memory. Therefore, two objects that point to the same memory location are the same instance. Two different memory locations can have the same value, but they are not the same instance. (Note that nullor Nothing in VB.NETalways equals the same instance as another null.) Take a look at Listing 3.2, which compares different variables.
Listing 3.2 Comparing References
1: object a = null; 2: object b = null; 3: object c = new Object(); 4: 5: Console.WriteLine(Object.ReferenceEquals(a, b)); 6: Console.WriteLine(Object.ReferenceEquals(a, c)); 7: a = c; 8: Console.WriteLine(Object.ReferenceEquals(a, c));
Line 5 returns true, because all null values equate to the same instance. Line 6 returns false because a and c are two different objects. Line 7 looks like it sets a and c to the same value, but when you do an assignment of one variable to another, you're actually setting the memory locations equal, and therefore, line 8 returns true.
NOTE
If you have previous programming experience, you'll know where the name ReferenceEquals comes from: this method compares the variables' memory locations (or references) rather than simply their values.
Most often, the Equals method suffices, but it is useful to have the ReferenceEquals method around as well.
The next three methods are all non-static. GetHashCode returns a hash code for your object. A hash code is a numeric representation of an object; it doesn't actually hold any meaning. Objects A and B may be different objects but generate the same hash code, for example, the number 7. However, you cannot reverse the process and go from the number 7 to an object. Thus, hashing is a one-way mechanism.
GetType, as the name implies, simply returns the specific data type of an object. This is very useful when you don't know what kind of data you're dealing with; for instance, when a function returns an unidentifiable object:
string a = "hi"; Console.WriteLine(a.GetType());
This snippet will return System.String, the data type of the variable a. Note that this method returns a type of data, rather than just the type's name. In other words, it returns a type, and not a string. This method is often used to compare data types. For example, using the GetConfig method discussed previously:
myType myVariable; if (myVariable.GetType() != ConfigurationSettings.GetConfig ("my settings").GetType()) return false;
This code snippet tests whether the type returned by the GetConfig method (a Type object) is of the same type as myVariable. And in this case, it is not.
Finally, probably the most common method, ToString, returns a string representation of a variable's type. For example, the following code snippet would print "system.object" to the command line:
Object objA = new Object(); Console.WriteLine(objA.ToString());
Note that it does not print out an object's value, but the name of the type. Sometimes the ToString method is overridden for a particular class. The string data type, for instance, overrides ToString so that it prints out the actual value of the variable, rather than the name of the type. The following code prints out "hello world."
string strA = "hello world"; Console.WriteLine(strA.ToString());
Form Properties
The System.Windows.Forms.Form object has 101 properties (no exaggeration!) that let you control nearly every imaginable aspect of the form. That's too many to cover in just one day, so let's just look a few of the more useful ones here, grouping by function.
Controlling Size and Location
You already know two ways to control the size of your form: the Width and Height properties. These properties simply take numeric values to set the size of the form in pixels. For example:
form1.Width = 100 form1.Height = 200
You can also set the size using the Size property and a Size object:
form1.Size = New Size(100, Form1.Size.Height) form1.Size = New Size(100, Form1.Size.Width)
Both methods do the same thing, but you'll typically use the first method because it's simpler.
You can set the size relative to the user's screen as well by using the Screen object. Listing 3.3 shows an example.
Listing 3.3 Setting Form Height Based on Screen Size in VB.NET
1: Imports System 2: Imports System.Windows.Forms 3: 4: Public Class MultiForm : Inherits Form 5: Public Sub New() 6: Me.Text = "Main form" 7: Me.Height = Screen.GetWorkingArea(Me).Height / 2 8: End Sub 9: End Class 10: 11: Public Class StartForm 12: Public Shared Sub Main() 13: Application.Run(New MultiForm) 14: End Sub 15: End Class
This code should be familiar to you by now, so the only line we'll examine is line 7. The GetWorkingArea method of the Screen object returns a Rectangle object that represents the user's screen. You pass in the current form object (using the Me keyword) to tell the CLR which screen to use (this is just in case the user has more than one monitor). Rectangle objects in turn have Height and Width properties that return integers describing the height and width of the screen. We then divide the height value by two. Compile this application and run it; note that the window now takes up half the vertical height of your screen!
To control where the form pops up on the screen, you can use the Location, DesktopLocation, Top, or Left properties. The last two set the position of the upper left corner of your form to a location on the user's desktop. They work just like the Height and Width properties. Note that you can set these properties to some location way off the user's screen, such as Top = 9999 and Left = 9999, but that's generally not a good idea.
The Location and DesktopLocation properties are similar. For a Form object, they do the same thing: set the starting location for the top left corner of your form. For example, both of the following lines do the same thing:
Form1.DesktopLocation = New Point(100,300) Form1.Location = New Point(100,300)
Location is a property inherited from the Form's great-grandparent class, Control. This property is used to set the location of a control within another control. In this case, the containing control for the form is simply the user's desktop. You could use the Location property for any other object that inherits from the Control object, such as the TextBox or Label controls, to set its location within another object, such as your form. The DesktopLocation property, however, only applies to the Form object. For consistency, we'll use the Location property from now on, so you only need to use one property for all Windows Forms objects.
Controlling Appearance
You already know about the Text property, which controls the text displayed in the form's title bar, so let's examine a few things you can do with font (the typeface in which the text appears).
The Font property sets the font that will be used on the form, unless otherwise overridden by a control's Font property. The ForeColor property then sets the color of that text. Take a look at the following code snippet:
Form1.Font = New Font(new FontFamily("Wingdings"), 23) Form1.ForeColor = Color.Blue
The first line creates a new Font object (you're now beginning to see how everything in .NET is an object, even fonts and colors). There are quite a few different ways to create Font objects (in other words, it has many constructors), and this is just one method. You specify the font face by using the FontFamily object, which contains predefined font names. The second parameter is the size of the font. The second line sets the text color to blue, using the Blue property of the Color object. This code, when used in a Windows Form, produces the output shown in Figure 3.2 (you'll have to trust my word that the color is really blue).
Figure 3.2 Font face and colors are specified in properties.
The BackColor and BackgroundImage properties enable you to change the default appearance of the form. BackColor is used just like ForeColor:
Form1.BackColor = Color.LightSalmon
The BackgroundImage property takes an Image object as a parameter. Typically, you'll use the Image's FromFile method to load an image; you must provide a path name. For example:
Form1.BackgroundImage = Image.FromFile("c:\winforms\day3\coffee bean.bmp")
FromFile is a static method, as you've probably inferred since you don't have to create a new Image instance to use it. Using the form from Figure 3.2, we now have a tiled background, as shown in Figure 3.3.
Figure 3.3 You can use a graphic to tile a background in your form.
The background color of the Label is still gray, but you'll learn how to change that in Day 6, "Enhancing Your Windows Forms with More Controls."
Another image property you can customize is the Icon property. This icon is used in the upper left of your form's title bar, as well as in any other representation of your application, such as in the taskbar. Here's how you set this property:
Me.Icon = New Icon("c:\winforms\day3\VSProjectApplication.ico")
You must create a new instance of the Icon object, supplying a valid path to an image. The image you select must be an icon file (.ico) or your application will generate an error.
The mouse cursor image can be controlled via the Cursor property:
Me.Cursor = Cursors.Hand
The Cursors object has quite a few built-in properties for the default set of Windows cursors, such as Arrow, IBeam, WaitCursor, and Help. See the .NET Framework documentation for more cursors. You can also load a custom cursor from a file:
Me.Cursor = New Cursor("path name")
The cursor file must be a .cur file. Animated cursors (those with the .ani extension) are not supported by the CLR.
The ShowInTaskBar property determines if your application should be shown in the Windows taskbar (this doesn't affect the onscreen window, only the taskbar button). The default value is true. Occasionally you may want to change this to not allow a user to select your form. For example, if you create a "splash screen" that displays your company's logo, you probably don't want the user to be able to select it. Simply set the ShowInTaskBar property to false, and the user won't be able to select it from the taskbar.
The FormBorderStyle property represents the type of border around a Windows Form. The main reason you modify this property is to allow or disallow resizing of the form, although changing border styles sometimes also changes the form's appearance. For example:
Form1.FormBorderStyle = FormBorderStyle.Sizable
The FormBorderStyle enumeration (not to be confused with the Form.FormBorderStyle property) has several predefined styles to choose from, as shown in Table 3.1. (An enumeration is simply a collection of properties or styles.) Figure 3.4 shows a collection of the different styles.
Figure 3.4 There are seven predefined border styles available.
Table 3.1 FormBorderStyle Styles
Style |
Description |
Fixed3D |
Non-resizable. 3D border around form. |
FixedDialog |
A thick border, non-resizable. |
FixedSingle |
Thin border, non-resizable. |
FixedToolWindow |
Form with a smaller title bar, useful for displaying ToolTips and Help windows. Non-resizable. Does not include minimize or maximize buttons. |
None |
No border and non-resizable. |
Sizable |
Resizable form. The default style. |
SizableToolWindow |
Form with a smaller title bar, useful for displaying ToolTips and Help windows. Resizable. Does not include minimize or maximize buttons. |
Finally, there are three properties that control how, and if, your form is displayed on screen. First, the Visible property determines if your form is visible to the user. If a form is not visible, the user cannot interact with it. This is a good way to hide things from the userif you want your application to stay open but don't want it interfering with the user's desktop, for example. By default, a form is not visible.
To make a form partially visible (in other words, transparent), you must set the Visible property to true, and use the Opacity property. This property takes a value from 1 (fully opaquein other words, fully visible) to 0.0 (invisible). To perform some really interesting transparency techniques, you can use the TransparencyKey property to specify that only a certain color should be transparent. For example, the following code will make all gray areas on the form invisible, while everything else remains opaque:
Form1.TransparencyKey = Color.Gray
If you set the form's BackColor to gray as well, then you'll end up with a form like the one shown in Figure 3.5 (the form is placed over a command prompt window to show the effect of transparency.
Figure 3.5 Use the TransparencyKey property to make only a certain color invisible.
Controlling Interactivity
When you look at any Windows application, you'll notice that most of them have standard features: Minimize, Maximize, Close, and sometimes Help buttons in the upper right corner of the window, and a "gripper" on the bottom right to resize the form. These control boxes are shown in Figure 3.6.
Each of these control boxes can be hidden or shown on your forms with the following properties:
MaximizeBox
MinimizeBox
HelpButton
ControlBox
SizeGripStyle
Figure 3.6 These control boxes are among standard Windows features.
The first four properties simply take a true or false value. ControlBox determines whether the previous buttons should be shown at all. Be careful, though; if you set ControlBox to false, you may not be able to close your application!
NOTE
The Help button appears only if the Maximize and Minimize buttons are not visible. This is a standard feature of .NET.
The SizeGripStyle property takes a SizeGripStyle enumeration value: Auto, Hide, or Show. Auto displays the sizing grip when necessary (in other words, depending on the FormBorderStyle), while Hide and Show prevent display of and display the sizing grips, respectively.
There are often two special keys associated with applications: the Enter key and the Esc (Escape) key. For example, many applications exit if you press the Esc key. You can control these functions with the AcceptButton and CancelButton properties. Listing 3.4 shows an example in C# of using these properties.
Listing 3.4 The AcceptButton and CancelButton Properties
1: using System; 2: using System.Windows.Forms; 3: using System.Drawing; 4: 5: namespace TYWinForms.Day3 { 6: 7: public class Listing34 : Form { 8: Button btAccept = new Button(); 9: Button btCancel = new Button(); 10: Label lblMessage = new Label(); 11: 12: public Listing34() { 13: lblMessage.Location = new Point(75,150); 14: lblMessage.Width = 200; 15: 16: btAccept.Location = new Point(100,25); 17: btAccept.Text = "Accept"; 18: btAccept.Click += new EventHandler(this.AcceptIt); 19: 20: btCancel.Location = new Point(100,100); 21: btCancel.Text = "Cancel"; 22: btCancel.Click += new EventHandler(this.CancelIt); 23: 24: this.AcceptButton = btAccept; 25: this.CancelButton = btCancel; 26: this.Text = "Accept and Cancel Button Example"; 27: this.Height = 200; 28: this.Controls.Add(lblMessage); 29: this.Controls.Add(btAccept); 30: this.Controls.Add(btCancel);AcceptButton and 31: } 32: 33: public void AcceptIt(Object Sender, EventArgs e) { 34: lblMessage.Text = "Accept button pressed"; 35: } 36: 37: public void CancelIt(Object Sender, EventArgs e) { 38: lblMessage.Text = "Cancel button pressed"; 39: } 40: } 41: 42: public class StartForm { 43: public static void Main() { 44: Application.Run(new Listing34()); 45: } 46: } 47: 48: }
Much of this code should already look familiar, so we can just breeze through most of it. Lines 13 import the necessary namespaces. Line 5 declares the namespace this application belongs to; following our naming scheme, the namespace is TYWinForms.Day3. Line 6 declares the Windows Form class.
Lines 810 declare the controls we'll be using in our form. Note that these are declared outside of any method, but inside the class so that they can be used from every method in the class.
The constructor, beginning on line 12, is where most of the work takes place. The code on lines 1322 simply sets a few properties for our controls. Take special note of lines 18 and 22, which point to methods that will be executed when either button on the form is clicked. These methods, AcceptIt and CancelIt, are on lines 33 and 37, and simply print a message in the Label control.
Lines 2430 set some properties and add the controls to the form. Lines 24 and 25 set the AcceptButton and CancelButton properties to the accept and cancel buttons respectively. Essentially, this means that clicking the Accept and Cancel buttons with your mouse will do the same thing as pressing the Enter and Esc keys.
Finally, lines 4246 contain another class that is used simply to hold our Main method and call the Application.Run method. The output of the application after pressing the Esc key is shown in AcceptButton and Figure 3.7.
NOTE
If the Cancel button on the form gets the focus, then pressing Enter causes that button to be pressed and, consequently, the CancelIt method to execute. In other words, the button receives the input before the form does. Unfortunately, there's no easy way to get around this. Some controls, such as the Button and RichTextBox controls, automatically handle the Enter key press when they have the focus, before anything else can execute. As a result, once you give the Cancel button focus (by clicking it, or tabbing to it), pressing the Enter key always causes the CancelIt method to execute.
If you're curious, you can override this behavior by overriding the Button control itself. We'll cover that in Day 18, "Building Custom Windows Forms Controls."
Note that although the AcceptButton and CancelButton properties need to point to Button controls, those Buttons do not have to necessarily be visible to the user. By setting their Visible properties to false, the buttons will be invisible, but you can still retain their functionality.
Finally, the AllowDrop property specifies whether the form can handle drag-and-drop functionalitythat is, when a user drags some item into the form and releases it there. This property accepts a true or false value. We'll discuss how to make your forms actually do something when this event occurs in the "Form Events" section later today.
Figure 3.7 Pressing the Esc key has the same effect as clicking the Cancel button.
For a complete reference on the properties of the Form object, see Appendix B, "Windows Forms Controls."
Form Methods
The Form object has quite a few methods as well57 of them to be precise. Most of them are inherited from the Object and Control classes, so they will be common to almost all the objects you work with in Windows Forms. Again, we'll cover a few of them here, grouped by category.
In addition to the methods discussed here, don't forget the Object methods such as Equals and ToString that we talked about earlier today. The Form object can take advantage of these as well.
Dealing with Display Issues
The first two methods you should learn are Show and Hide. These two functions make your form visible and invisible, respectively, by modifying the Visible property. These functions don't do anything to the formsuch as removing it from memory or activating other functionsthey only control what the user sees.
The Close method, on the other hand, completely gets rid of a form (and its controls), removing it from memory. Use this method when you want to close your application or simply when you don't need a form anymore.
Since Windows is a multitasking environment, you can have many windows open at once. Each window must compete for the user's attention. The Form object has a few methods that help you deal with this issue.
The Activate method "activates" your form. This can mean two different things:
If your application is the active one (the one the user happens to be using at the moment), Activate brings the form to the front of the screen, ensuring it is on top of all other forms.
If it is not the active application, the title bar and taskbar icon flash, grabbing the user's attention. More than likely you've seen this type of attention grabber before; the most common usage is for instant messaging applications. If someone sends you an instant message while you're working on another application, the IM window pops up in the background and flashes its title bar.
The BringToFront and SendToBack methods are more direct than Activate at getting a user's attention. The first method brings your form to the front of all other windows on screen, forcing the user to look at it. This is useful, for example, when something happens with your application that demands the user's attention (like getting an instant message popup). SendToBack, conversely, places your form behind all others on screen. SendToBack isn't used as often, but it's there just in case.
TIP
You can set the TopMost property to true to have your form always stay on top of other windows. This is especially useful for forms that deliver error or warning messages.
Finally, the Refresh method works much like the Refresh button on your Web browser; it simply redraws everything on the form, updating if necessary. We'll talk more about this method when you deal with GDI+ in Day 13, "Creating Graphical Applications with GDI+."
Resetting Properties
The Form object has a series of reset methods that enable you to change modified properties back to their default values. All of these methods follow the naming scheme Resetproperty. A few of the more common ones are
ResetBackColor
ResetCursor
ResetFont
ResetForeColor
ResetText
These methods are very convenient when you've modified something and need to go back, but don't know or don't care what the original value was. For example, if in a word processor the user changes the font several times, but then wants to go back to default values, you could use ResetFont.