|
Authors | Duncan Mackenzie, Andy Baron, Erik Porter, Joel Semeniuk | Title | Microsoft Visual Basic .NET 2003 Kick Start | Publisher | Sams | Published | NOV 21, 2003 | ISBN | 0672325497 | Price | US$ 34.99 | Pages | 336 |
|
Building Windows Applications
In This Chapter
The Way Things Were
For most of Visual Basic's history, you did not need to specify you were building a rich-client application—all the applications you built were rich-client apps. Web development has never been the purpose of Visual Basic. This focus on developing stand-alone or client/server applications with a Windows user interface created a very tight bond between the VB language and the forms engine within it. There was no need to distinguish between the language and the tools for building an interface in VB6, but there certainly is in .NET.
In Visual Basic .NET, the technologies that enable you to create "standard" Windows applications are part of the .NET Framework, available to any .NET language. This is a huge change from the way things were. In each of the following sections, before going into detail on how the new Forms technology works in Visual Basic .NET, I briefly describe some of the relevant details about Visual Basic 6.0 forms.
The Windows Forms Model
Forms in Visual Basic 6.0 were distinct files from other types of code (such as modules and classes) stored in two parts—a .FRM file that contained the code and the layout of the form and a .FRX file, which was a special kind of resource file that held any embedded resources needed by the form. When you designed a form using the visual tools, controls were added to forms and properties were set (such as the size and position of various controls) without any visible effect on your code. Changing the position of a button would change some hidden (not shown in the IDE at least) text (shown below for a simple one button "Hello World" application) that you could access and change using a text editor, but all of these properties were not part of your code. Setting a property in code was therefore very different than setting it through the visual interface.
Listing 3.1 The Code Behind a Visual Basic 6.0 Form
VERSION 5.00
Begin VB.Form Form1
Caption = "Form1"
ClientHeight = 3090
ClientLeft = 60
ClientTop = 450
ClientWidth = 4680
LinkTopic = "Form1"
ScaleHeight = 3090
ScaleWidth = 4680
StartUpPosition = 3
Begin VB.CommandButton Command1
Caption = "Hello World"
Default = -1
Height = 495
Left = 840
TabIndex = 0
Top = 480
Width = 1335
End
End
Attribute VB_Name = "Form1"
Attribute VB_GlobalNameSpace = False
Attribute VB_Creatable = False
Attribute VB_PredeclaredId = True
Attribute VB_Exposed = False
This special area of the form would also contain object references (in the form of GUIDs) for any ActiveX controls you used. Although editing the .FRM file directly was not encouraged, that was exactly what you had to do to fix a corrupt form.
Forms in .NET change everything just described. Forms are no longer "special" files; they are just code (.VB files in .NET), although they certainly can have associated resource files with them. Editing the properties of the form or of the controls on the form does not add hidden text to the form file; it generates real VB .NET code that sets the properties in the same way that you would in your own code. If you follow along with me and create a sample VB .NET form, you can explore the new forms designer and browse the code generated by your visual editing.
Building a Hello World Sample
First, go ahead and fire up Visual Studio .NET. Although it is possible to create Visual Basic .NET applications without using any IDE (another example of how VB has changed in its move to .NET), this book assumes that you have at least VB .NET Standard Edition and are therefore using the Visual Studio IDE.
Once it is loaded, you should see a start page. This start page has a few tabs and several different areas of information, but the main area (which is displayed by default) has all of the functionality you need at this point, including a list of recently opened projects and a New Project button. Click the New Project button, or select File, New, Project from the menu to bring up the New Project dialog box.
If you are new to Visual Studio .NET, this dialog box contains a lot more options than the equivalent from Visual Basic 6.0, including the capability to create Web Services, Windows Services, Console Applications, and more. For now though, pick the standard project for creating a new Windows Application: Windows Application. Selecting this project type (and picking a name and location, and then clicking OK) creates a new solution containing a single project that contains one blank Windows form and a code file called AssemblyInfo.vb. At this point, if you look around the Visual Studio .NET IDE, you will see an interface that is somewhat similar to Visual Basic 6.0, but many things have changed. In the first chapter of this book, I covered the IDE, detailing the key changes from Visual Basic 6.0 and many of the important features.
Moving along with this quick sample, you will build the application's user interface by placing a button onto the form. To work with the form, you need to use its design view, which is accessed through the same steps as in VB6. Double-click the form (in the Solution Explorer) to bring it up in the design view or right-click it and pick View Designer (this was View Object in VB6, but the meaning is the same) from the context menu.
Once you have the form's design view up, you can use the toolbox to select controls and place them onto your form. The toolbox looks a little different from the one in VB6. It has sliding groups to categorize all the different types of controls and lists the controls with an icon and text by default. It works a little differently as well. When you worked with the toolbox in Visual Basic 6.0, you had two choices of how to interact with it:
- You could double-click a control and a new instance of that type of control would be added to the form with a default size and location.
- You could also select a control and then click the form to set the top-left location and drag an outline out to represent the size of the control.
In the Windows forms designer, you have those two methods of placing a control and an additional option:
- You can drag a control from the toolbox onto the desired location on your form and it will be placed at that position with a default size.
Using any one of the three possible methods, place a single button from the toolbox onto the form. It is a minor change, but it is worth noting that the CommandButton
control from Visual Basic 6.0 has changed to the Button
control in VS .NET 2002 and 2003. Just like in Visual Basic 6.0, you can double-click this new button to start writing an event handler for its most common event (in this case Click
). All you want to do is pop up a message box, which you can do with the exact same line of code that you would use in Visual Basic 6.0 (see Listing 3.2).
Listing 3.2 Hello World Does Not Look Too Different in .NET
Private Sub Button1_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles Button1.Click
MsgBox("Hello World!")
End Sub
You can run the project at this point if you want; F5 works fine for that purpose or you can select Start from the Debug menu. The real reason for building this form was to look at the code generated by your actions in the designer.
Exploring the Designer Generated Code
Switch to the code for the form (right-click the form in the designer or the Solution Explorer and select View Code) and you will see your button's click procedure and an area called Windows Form Designer generated code, as shown in Figure 3.1.
Figure 3.1
Regions allow you to hide blocks of code.
That area is a region, a new feature of the code editor in Visual Studio .NET that allows you to collapse areas of code down to a single line to simplify the experience of browsing code. We discussed regions in Chapter 1, but all you have to know now is that the designer (the visual tool for creating forms) has used this feature to hide all of the code that it generated as you built the form. As a rule, you are not supposed to go into this area of code, which is why it is hidden by default, but you should at least understand the code in this area. In some cases, you might even need to change it. You can expand the designer code region by clicking the plus symbol next to the region name.
Inside the region, you will find a few key elements:
- A constructor for the form (a
Sub New()
)
- A
Dispose
procedure
- Declarations of all of the controls on the form
- A sub called
InitializeComponent
The Constructor and the Dispose
routines are new to Visual Basic .NET, but they are relatively equivalent to the Class_Initialize
and Class_Terminate
events of Visual Basic 6.0 classes. The constructor is called when an instance of your form is created; the Dispose
is called before the form is destroyed. The real meat of the designer-generated code is the other two code elements—the list of declarations for all of the controls on the form and the InitializeComponent
routine that sets up the properties of the controls on the form and of the Form
itself. You did not set up many controls or even change many properties when creating this simple sample, but let's take a look at the generated code, shown in Listing 3.3.
Listing 3.3 In VB .NET, You Can View and Even Edit All the Code that Builds Your Form's Interface
Friend WithEvents Button1 As System.Windows.Forms.Button
<System.Diagnostics.DebuggerStepThrough()> _
Private Sub InitializeComponent()
Me.Button1 = New System.Windows.Forms.Button
Me.SuspendLayout()
Me.Button1.Location = New System.Drawing.Point(96, 88)
Me.Button1.Name = "Button1"
Me.Button1.TabIndex = 0
Me.Button1.Text = "Button1"
Me.AutoScaleBaseSize = New System.Drawing.Size(5, 13)
Me.ClientSize = New System.Drawing.Size(292, 273)
Me.Controls.Add(Me.Button1)
Me.Name = "Form1"
Me.Text = "Form1"
Me.ResumeLayout(False)
End Sub
The comment on the top of this routine lets you know that you can modify anything in here by using the visual designer, and that you should not change the code directly. This is pretty good advice, but let's ignore it for a moment and see what happens. If you look at lines 12-15 in Listing 3.3, you can see that they are setting the properties of the button, including the size. If you add some of your own code in there, even something as simple as outputting some text to the debug window, it could produce surprising results.
In this case, just adding a line (see Listing 3.4) causes the Windows Form Designer to fail when trying to parse the code, producing the helpful little error message shown in Figure 3.2.
Listing 3.4 Editing the Windows Forms Designer Generated Code May Produce Unexpected Results
Me.Button1.Location = New System.Drawing.Point(96, 88)
Me.Button1.Name = "Button1"
Debug.WriteLine("Testing!")
Me.Button1.TabIndex = 0
Me.Button1.Text = "Button1"
Figure 3.2
The Windows Forms Designer does not deal well with someone changing its code.
Other, less fatal, changes to the code are often undone the next time you change something in the designer (because the InitializeComponent
procedure is regenerated), which can be quite frustrating. Code generators (such as the designers in Visual Studio .NET) work well in many situations, but when they have to support round tripping (where the code isn't just generated once; it is read back in and used by the designer whenever the form needs to be displayed), they tend to be very fragile. In this case, you cannot make code changes to most of the designer-generated area. The place where you can change code is in the constructor; you can freely add code after the call to InitializeComponent
as long as you do not remove that line! The best bet is to ignore all of the code inside this special region, other than the constructor, and make all of your property changes through the visual interface. Now let's go back to the button's click procedure and take a closer look at how .NET is handling events.
Handling Events in .NET
Event handling has changed, for the better, in .NET. I never really thought anything of it, but event procedures in Visual Basic 6.0 were attached to the appropriate object and event based solely on the procedure's name. This reliance on names meant that if you had already created and written code for CommandButton1_Click
and you renamed your button to the more meaningful saveFile
, your event procedure would no longer be attached to this button.
If event handling is no longer based on procedure names, how do you connect a specific event handler with an object's event? There are two ways; one combines the familiar WithEvents
keyword in your variable declarations with a new keyword Handles
, and the other dynamically connects an object's event to an event handling procedure at runtime.
Using WithEvents and the Handles Keyword
Although event handling in VB .NET has changed, as I described, using the Handles
keyword is the closest model to pre-.NET Visual Basic and it is the model used by default by the Windows forms designer. Objects, such as controls, that expose events are declared using the WithEvents
keyword, like they were in Visual Basic 6.0, and event procedures provide a Handles myObj.myEvent
clause after the procedure declaration. Those two parts, the Handles
and WithEvents
, wire up a specific event handling procedure.
Returning to the first example, a simple button on a form, look at the code generated by the Windows forms designer to see how the designer wired the Button
's Click
event to a code procedure. The first line to look at is the declaration of the button itself:
Friend WithEvents Button1 As System.Windows.Forms.Button
The button is declared using the WithEvents
keyword, which is required if you want to use the Handles
keyword on the event handling procedure. Only having both will make the proper connection between this object (the button) and the event procedure. Next, you double-click the Button1
and the Windows forms designer automatically creates a new event procedure for the default event (Click
). Controls can specify one of their events to be the default. If they do, the Windows forms designer will assume you want to work with that event when you double-click the control. The designer knows that the button was declared using the WithEvents
keyword, so it creates the procedure declaration with Handles Button1.Click
appended to the end (see Listing 3.5).
Listing 3.5 Designer Generated Event Handlers Use the Handles Keyword
Private Sub Button1_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles Button1.Click
MsgBox("Hello World!")
End Sub
Up to this point, the syntax might be a little different, but the general idea is the same as in Visual Basic 6.0. The only change, the addition of the Handles
keyword, is just a way to get around relying on the name of your event handling procedure. Even using WithEvents
and Handles
though, there is a new feature built into this method of handling events. It is possible to specify that a specific event procedure handles more than one event. If you were to add a second button to the form, (automatically named Button2
) and to double-click it, the Windows forms designer would create a second event procedure (see Listing 3.6).
Listing 3.6 The Windows Forms Designer Automatically Creates a Handler for the Default Event When You Double-Click a Control
Private Sub Button2_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles Button2.Click
End Sub
This procedure declaration is followed by Handles Button2.Click
, but you could instead modify the first Click
procedure to handle both buttons' Click
events, as shown in Listing 3.7.
Listing 3.7 Listing More than One Object's Events After a Procedure Declaration Allows You to Consolidate Code
Private Sub Button1_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles Button1.Click, Button2.Click
MsgBox("Hello World!")
End Sub
Now, when either button is clicked, the same code will run. Any number of events can be handled by the same event procedure, assuming that all of the events have the same signature (the same set of arguments to their event procedures). It is also possible to have a single event handled by multiple procedures (see Listing 3.8) by having that event specified in multiple Handles
clauses.
Listing 3.8 One Event, Such as a Button's Click, Can be Handled by Multiple Routines
Private Sub ClickEvent1( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles Button2.Click
MsgBox("ClickEvent1")
End Sub
Private Sub ClickEvent2( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles Button2.Click
MsgBox("ClickEvent2")
End Sub
When Button2
is clicked, the code in both ClickEvent1
and ClickEvent2
is executed. You will use the first concept (multiple events being handled by a single event handler) more often than the second (multiple handlers for a single event), but it is good to know that the functionality exists if you need it. In the case of multiple events all being handled by a single event handler, you will likely need to know which specific control is raising (firing or causing) the event. For most events, the object that raises the event is passed in as the first parameter (usually named "sender
") to the event procedure. The sender
parameter is typed as an Object
, which means that you have to convert it into a more useful type (such as a Control
or a Button
) before you can work with it. Listing 3.9 shows a sample event procedure that is handling the Click
event of 10 buttons; it casts (views an object as a different type) the sender parameter from Object
to Button
to determine the Button
's text (caption).
Listing 3.9 If You Handle the Events of Many Different Objects, the Sender Parameter Tells You which Object Caused the Current Event
Private Sub LotsOfButtons( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles Button1.Click, Button2.Click, _
Button3.Click, Button4.Click, _
Button5.Click, Button6.Click, _
Button7.Click, Button8.Click, _
Button9.Click, Button10.Click
Dim clickedBtn As Button
If TypeOf sender Is Button Then
clickedBtn = DirectCast(sender, Button)
MsgBox(clickedBtn.Text)
End If
End Sub
It is worth noting, although perhaps a bit confusing, that you could have also written this routine using Control
instead of Button
, and this alternative is shown in Listing 3.10.
Listing 3.10 All Windows Forms Controls Inherit from Control, so a Variable of that Type Can Be Used to Hold Any Control on Your Form
Private Sub LotsOfButtons( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles Button1.Click, Button2.Click, _
Button3.Click, Button4.Click, _
Button5.Click, Button6.Click, _
Button7.Click, Button8.Click, _
Button9.Click, Button10.Click
Dim clickedCtrl As Control
If TypeOf sender Is Control Then
clickedCtrl = DirectCast(sender, Control)
MsgBox(clickedCtrl.Text)
End If
End Sub
The reason this works, and it does work, is due to the way in which Windows Forms controls have been written. All Windows Forms controls share the same base class, System.Windows.Forms.Control
, and that class provides them with a set of common properties, events, and methods. Text
is one of those common properties, so you can cast to Control
instead of Button
and everything will still work. What does that mean to you? It means you can write code to handle any control on a form and you never have to know what type of control it is. Without casting it to a specific type of control, you can work with any of the common properties available on the Control
class including position and size properties, color properties, and many more. Combining the new flexibility in event handling with this common control class, there is a lot you can accomplish using WithEvents
and Handles
. There are a few cases though, when even more flexibility is required, and that is where the other method of event handling comes in.
Wiring Up Events Manually with AddHandler/ RemoveHandler
The WithEvents
/Handles
method of event handling is designed for when you know at design time which controls and which event handling procedures you are dealing with. If you are going to be working with objects and event procedures in a more dynamic fashion, you can use the AddHandler
and RemoveHandler
statements to connect an object's event to an event procedure at runtime. This is more of an advanced technique, and it will not likely be necessary in most applications, but here is a basic example to illustrate how these statements could be used. In this example, there is a Button
and a CheckBox
(set to look like a button) on an otherwise empty form (see Figure 3.3). When the CheckBox
is pressed (see Listing 3.11), the button's Click
event is attached using AddHandler
to a simple event handler that displays a MsgBox
. When the CheckBox
is in its up position, the button is detached from its event handler using RemoveHandler
.
Figure 3.3
The check box on this form, which is using the new toggle button appearance, turns event handling on and off for the button.
Listing 3.11 The CheckedChanged Event of the Check Box Is Used to Add and Remove an Event Handler for the Button's Click Event
Private Sub CheckBox1_CheckedChanged( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles CheckBox1.CheckedChanged
If CheckBox1.Checked Then
AddHandler Button1.Click, _
AddressOf myClickHandler
Else
RemoveHandler Button1.Click, _
AddressOf myClickHandler
End If
End Sub
Private Sub myClickHandler( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs)
MsgBox("Event Handled!")
End Sub
Adding and removing an event handler is a more useful technique with non-form classes that raise events, but the procedure is the same.
Dynamic Event Handling - The AddHandler
/RemoveHandler
statements open up a completely new form of event handling, one that wasn't available in Visual Basic 6.0. There are many occasions where you will find yourself dynamically creating objects such as controls, but also including such things as forms, database objects, socket objects for TCP/IP communication, and more. By using AddHandler
, you can still trap events from these objects even though you never declared them using WithEvents
.
Behind the scenes, AddHandler
and RemoveHandler
are doing work with the delegate related Framework classes, wrapping the functionality of adding and removing additional methods (event handlers) to the same delegate (event). If you need to, you can still work natively with the System.Delegate
class instead of, or in addition to, using AddHandler
and RemoveHandler
.
Coding Without Control Arrays
One of the more noticeable changes for a Visual Basic 6.0 developer who is getting started in Windows forms is the lack of control arrays. This feature of pre-.NET VB allowed you to configure a set of controls with a single name by assigning index values to each control. Once a control array was created, a single event procedure could be used to handle the events of all the controls in the array and you could also loop through the controls in an array based on index. These features made control arrays useful for a great many projects, but I mostly remember using this feature to handle groups of option buttons (radio buttons in .NET). Consider this form in VB6 (see Figure 3.4), which displays a six-item option button (radio button) collection using a control array. A single event handler (see Listing 3.12) can be written for this array that will run whenever any of the options are selected, or the array can be looped through to find the one option button with a Value = True
.
Listing 3.12 VB6 Event Handler for an Option Button Control Array
Private Sub optFruit_Click(Index As Integer)
MsgBox "Selected Item: " & optFruit(Index).Caption
End Sub
Figure 3.4
A group of option buttons on a VB6 form can all be part of a control array.
Due to the changes in event handling, allowing multiple objects' events to be mapped to the same event handler, achieving the same effect in Windows forms is not impossible, but it is not quite as simple as it was in VB6. Combining the event handling features of .NET with the use of a standard array, here is a walkthrough of handling some radio buttons on a .NET Windows form. This example creates an array to hold a set of radio buttons and shows you how you can use that array along with a shared event handler to determine which option button is currently selected. If you follow along with these steps, you can try out the code for yourself.
- Create a new Visual Basic .NET Windows application.
- A blank
Form1
will be created and opened in Design View. Select the form and then drag a new radio button onto it from the toolbox.
- Copy and paste the radio button four times onto the form. This is likely the fastest way to end up with five radio buttons, but you can also drag four more radio buttons from the toolbox.
- Add a label to your form.
- Double-click one of the radio buttons to jump into its
CheckedChanged
event (see Listing 3.13) and modify the Handles
clause to include all five of the radio buttons' events.
Listing 3.13 Creating an Event Handler for All Five Radio Buttons
Private Sub RadioButton1_CheckedChanged( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles RadioButton1.CheckedChanged, _
RadioButton2.CheckedChanged, _
RadioButton3.CheckedChanged, _
RadioButton4.CheckedChanged, _
RadioButton5.CheckedChanged
End Sub
- Declare an array of radio buttons as a private member variable in your form (put the declaration anywhere in your code outside of a procedure):
Dim radioButtons(4) As RadioButton
- Declare an integer private member variable as well:
Dim selectedOption As Integer = 0
- Add code into the constructor of the form (the
sub New()
procedure contained within the Windows Forms Designer generated area) to fill up the array with the radio buttons (see Listing 3.14).
Listing 3.14 Filling Up Your Own Radio Button Array
Public Sub New()
MyBase.New()
InitializeComponent()
radioButtons(0) = RadioButton1
radioButtons(1) = RadioButton2
radioButtons(2) = RadioButton3
radioButtons(3) = RadioButton4
radioButtons(4) = RadioButton5
End Sub
- Finally, write code into the shared
CheckedChanged
event handler that will loop through the radio button array and determine the currently selected option (shown in Listing 3.15). Store the appropriate array index into selectedOption
and change the Text
property of the Label
control.
Listing 3.15 Loop Through the Radio Buttons and Determine which One Is Clicked
Private Sub RadioButton1_CheckedChanged( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles RadioButton1.CheckedChanged, _
RadioButton2.CheckedChanged, _
RadioButton3.CheckedChanged, _
RadioButton4.CheckedChanged, _
RadioButton5.CheckedChanged
Dim i As Integer = 0
Dim found As Boolean = False
While i < radioButtons.GetLength(0) And Not found
If radioButtons(i).Checked Then
found = True
selectedOption = i + 1
Label1.Text = CStr(selectedOption)
End If
i += 1
End While
End Sub
If you were to run this code, you would see the label changing every time a different option was selected. The flexibility of event handling in .NET allows you to work with a set of five radio buttons without having to have five different event handlers.
Configuring Your Form for Resizing
Every time I built a resizable form in Visual Basic 6.0 (which includes every form I built except for the simplest of dialog boxes), I had to write code into the form's Resize
event.
I had to decide how I wanted each control to adjust to the new size of the form, what size was too small for my interface to be usable, and I had to remember to check if the form had been minimized before trying to do any resizing. This code was not particularly hard to write, and after 50 or 60 forms I could do it without much thought, but it still took time and effort for every form I created. In Windows forms, a variety of features have come together to allow you to configure your controls for auto-resizing without having to write a single line of code. By combining the docking and anchoring features with the careful use of panels, you can set your form up to resize in almost any way you can imagine. If you add in the auto-scroll feature, you can even have parts of the form that do not resize at all, but instead extend right off the visible area of the form.
Using the Layout Event in Windows Forms - This section is all about avoiding the code you used to have to write into your Resize
event handler, but even if you did write code, it wouldn't be in the same event handler in .NET. A new event, Layout
, is available in Windows forms, and if you need to write code that adjusts the form after a resize, the Layout
event is the best place for it.
The following sections explain how each of these three features works and how the use of panels can increase your flexibility of control arrangement. Once you have learned about all the features, the chapter will detail a few of the more common form layout scenarios, showing you how to set them up for proper resizing.
Using Docking to Handle Resizing
Docking windows and toolbars have been in use for quite some time, so the general concept is not new; a docked item is attached to one of the edges of its container (such as a form). Consider a form that contains only a single ListBox
control. If you set the ListBox
's Dock
property to Left
(see Figure 3.5), the height of the control will be fixed to the height of the form's client area (causing the list box to fill the form from top to bottom), while the position of the control will be locked to the left side of the form. The only thing you can change about the size of the list box at this point is its width, controlling how far out from the left side it extends. Docking to the right is essentially the same; the list box becomes attached to the right side of the form, but you can still change its width as desired. Docking to the top or bottom will cause the width to be fixed to fill the width of the form, but the height of the control can still be modified.
Figure 3.5
Docking a control to the left makes it stick to the left side, while filling the vertical space of the container.
In addition to docking to one of the four sides, controls also support a fifth (sixth if you count None
for no docking) docking setting, Fill
. If you set the Dock
property to Fill
, that control becomes attached to all four sides of the container, adjusting itself automatically as the form is resized. You cannot adjust any size or position settings for a control that has been docked to fill the container.
Remember that you are docking the control to its container, which in this example is the Form
, but could be a container control such as a GroupBox
or Panel
. This flexibility leads to more layout options, as you will see later in the section on "Using Panels."
The container (Form
, Panel
, or other type of container controls) has a DockPadding
property, which allows it to specify a certain amount of padding between it and docked controls. The padding values can be specified individually for the four sides of the container, or an overall padding value that applies to all of the sides at once. If a container has specified a DockPadding
value of 10, for example, a control docked to the left will be positioned 10 pixels away from the left edge. The DockPadding
setting is great for creating a more visually pleasing user interface as it results in a border around the form while still enabling the automatic resizing of docked controls (see Figure 3.6).
Figure 3.6
DockPadding
allows you to dock, without sacrificing a bit of white space around the edge of your controls.
Docking gets a little more complicated when multiple docked controls are involved. If you dock more than one control to the same edge, the second control will dock alongside the first instead of directly to the container. Going back to the example with the ListBox
on a form, you can try multiple docked controls to see what happens. If you docked the ListBox
to the bottom and then added a new DataGrid
to the form, setting its Dock
property also to Bottom
, you would have produced an interface similar to Figure 3.7, where the ListBox
appears below the DataGrid
.
Figure 3.7
Multiple controls docked to the same side will stack instead of overlap.
Both of the controls are docked to the form and will resize as the form is resized. If you have controls docked to one or more sides of your container, and then you set another control's Dock
property to Fill
, the control set to Fill
will be automatically sized to use all of the remaining area of the container. If you have multiple controls docked on a form, you might want to use the Splitter
control. Splitter
is a special Windows Forms control that, when docked between two other controls, allows you to resize the two controls at runtime. Using the Splitter
control and a few other key controls, you can create a standard Explorer view form in a matter of minutes.
To add a splitter to your form, you need to be careful of the order in which you add your controls. Try adding a ListBox
to an empty form, and docking it to the left. Then add a Splitter
control, and dock it to the left as well (it is by default). Finally, add a DataGrid
control, dock it to the left as well, or set its dock property to Fill
, and you will have a working example of using a splitter!
A little confusing? It can appear very complex, but the best bet is to try it out on your own, adding a variety of controls to a blank form and playing around with the various Dock
/DockPadding
settings.
Anchoring as an Alternative Resizing Technique
After docking, this has to be the coolest layout feature. Anchoring is a little simpler than docking, but it can be a powerful tool. Using a graphical property editor (see Figure 3.8), you can set the Anchor
property for a control to any combination of Top
, Left
, Bottom
, and/or Right
.
Figure 3.8
The property editor for anchoring is a nice little graphical control.
To anchor a control to a specific side means that the distance between the control and that side of its container becomes fixed. Therefore, if you anchor a control to a specific side and then resize the form, the control's distance from the anchored side(s) will not change. To maintain a constant position relative to one or more sides of your container, the control might have to be resized when the form's size changes.
By default, controls are anchored to the top and left, which makes them behave exactly as controls did in previous versions of Visual Basic. When you resize the form, they do not move or resize. If you want to create a TextBox
that grows as you make your form wider, you can anchor it to the top, left, and right sides. If you want the TextBox
to grow in height as well as width, anchor it to the bottom as well. Figure 3.9 shows a form with some anchored controls, and Figure 3.10 shows what happens when that form is resized. You will see a few more examples of how anchoring can be used to lay out your form in the samples throughout the rest of this book.
Figure 3.9
Anchored controls maintain a fixed distance between themselves and the container edge(s) they are anchored to.
Figure 3.10
As the form changes size, the controls move and resize automatically.
AutoScrolling Forms
Docking and anchoring are designed to resize your controls when the form is resized, but resizing the contents of your form is not always appropriate. In some cases there is a minimum size at which your form is usable, so resizing below that needs to be avoided. There are also situations when the content on your form is a fixed size, making resizing inappropriate. Windows forms provides a few additional features to allow you to deal with these situations. Forms have minimum/maximum height and width properties (allowing you to constrain resizing to a specific range of sizes) and the AutoScroll
feature. AutoScroll
allows a form to be resized by the users, but instead of shrinking the controls on the form, scroll bars appear to allow the users to view the entire form area even if they have resized the window. The form shown in Figure 3.11 is a perfect candidate for AutoScroll
; it contains a large number of controls and buttons and cannot be resized using docking or anchoring.
Figure 3.11
This form would be hard to resize, so the solution is to allow users to scroll.
If the user were to resize this form, making it smaller than the area required for all of its controls, the AutoScroll
feature of Windows forms will save the day by adding horizontal and/or vertical scroll bars as required (see Figure 3.12).
Figure 3.12
AutoScroll
automatically adds scroll bars when the form becomes small enough to hide any part of any of the controls on the form.
In addition to the AutoScroll
property, which you set to True
to enable auto scrolling, there are two other properties, AutoScrollMargin
and AutoScrollMinSize
, that are used to configure exactly how scrolling occurs.
Using Panels
Panels enhance all of the other layout features discussed so far because they can act as a container for other controls, much like a form does. Because they are containers, panels have their own DockPadding
property and all of the auto-scroll features described for forms. A control can be placed into a panel and anchored to the bottom-right corner of that panel, while the panel itself was docked or anchored to a specific location on a form. By combining panels with forms, you can design more complicated layouts that still support resizing. Panels are used in several of the examples in the next section.
Some Common Resizing Scenarios
Now that you have been told what docking, anchoring, and auto-scrolling are, here are a few forms that demonstrate using the new layout features.
A Standard One-Large-Control Dialog Box
A large control, such as TextBox
, ListBox
, or DataGrid
, needs to resize properly on a Form
with two buttons (OK and Cancel), as shown in Figure 3.13.
Figure 3.13
Allowing the users to resize your form makes your application work better on a range of screen sizes.
If it were not for those two buttons, docking would be a possible answer, but anchoring saves the day here. Assuming a desired border around the form's contents of 10 pixels:
- Position the large control with its top, left, and right edges 10 pixels in from the corresponding form edges. Note that you don't have to be exact about these positions, anchoring will lock the control in whatever place you put it. A precise border is just for aesthetics.
- Add your two buttons to the bottom of the form, placing their bottom edge 10 pixels up from the bottom of the form. The right edge of one button should be 10 pixels in from the right edge of the form. Place the other button so that its right edge is five pixels away from the left edge of the first button.
- Adjust the height of the large control so that its bottom edge is 10 pixels from the top of the two buttons.
- Now, set the large control's
Anchor
property to "Top, Bottom, Left, Right
" and both buttons' Anchor
property to "Bottom, Right
".
A Long List of Questions
You have a long list of questions that must be answered before a user can submit an order, and additional questions keep being added. There also needs to be an OK and Cancel button on the form; OK to move onto submitting the order, and Cancel to close the form and cancel the order.
AutoScroll
is the key for this layout, either for the entire form or for a panel, depending on whether you want to keep the OK and Cancel buttons visible at all times or if you want the user to have to scroll down to find them.
A Multi-Column Form
You have a form with multiple columns of text boxes, each with associated labels, a single large text box, and the OK and Cancel buttons at the bottom (see Figure 3.16).
Figure 3.16
Multiple columns are trickier to resize.
This gets a little trickier because of the two columns at the top of the form, but panels can make it all work out.
Note that I used the Layout
event, which is a new event in Windows forms that was not available in Visual Basic 6.0. In Visual Basic 6.0, I would have used the Resize
event, but Layout
is actually more precise as it occurs when the position and size of controls within a container might need adjusting, as opposed to occurring upon every resize of the form. If a form were set to AutoScroll
, for example, the Resize
event would fire whenever the form was resized (as it should), but the controls inside the form would not need to be rearranged.
An Explorer-Style Application
You are attempting to produce an interface in the format of the Windows Explorer, Outlook, and other applications. This interface will have a TreeView
along one side of the form and a ListView
on the other, and have the capability to move the dividing line between the two controls to resize them (see Figure 3.17).
Figure 3.17
The standard Explorer style of form layout.
This style of layout is easy to accomplish with the new features of Windows forms, but the specific order in which you add items to your form is important.
- Starting with a blank form, add a
TreeView
control and set its Dock
property to Left
.
- Add a
Splitter
control; it will automatically dock to the left and take its place along the right side of the TreeView
.
- Add a
ListView
control and set its Dock
property to Fill
.
That's it. The ListView
will fill whatever space isn't being used by the TreeView
and the Splitter
will allow you to adjust the relative size of the two areas.
That is all of the sample resizing scenarios covered in this chapter, but that certainly is not the extent of layouts that can be created.
Programming Without Default Form Instances
Classes and objects existed in Visual Basic 6.0, but they were not as pervasive as they are in .NET. Objects are everywhere when you are developing in Visual Basic .NET, and that large change has lead to many small changes that can trip you up when you are coming from a VB6 background. One of the more commonly encountered problems for a VB6 developer is the lack of default form instances in .NET, but the term default form instance is confusing enough that I will need to go into more detail about the problem before we look at any solutions.
When you are dealing with classes and objects, it is helpful to think of a class as the blueprint of a house, whereas an object is an instance of a specific class and is more like an actual house that was built from the blueprint. In general, you don't work with classes, just like you don't live in blueprints. You create instances from your classes and it is those instances that get used throughout your applications. This is not a .NET issue; this is just how classes and objects work, even in VB6. Even in VB6, forms are considered classes, which is why you can create multiple copies (instances) of a single form in your VB6 code if you wish. The two snippets of code in Listings 3.17 and 3.18, assuming you have a Form1
in your project, would produce the same result (11 open copies of Form1
) in VB6 or VB .NET. The only differences between these two examples are syntax issues; they are otherwise identical.
Listing 3.17 VB6 Code to Display 11 Copies of a Form
Dim i As Integer
Dim myFrm As Form1
For i = 0 To 10
Set myFrm = New Form1
myFrm.Show
Next
Listing 3.18 The Very Similar VB .NET Code for Displaying 11 Copies of a Form
Dim i As Integer
Dim myFrm As Form1
For i = 0 To 10
myFrm = New Form1()
myFrm.Show()
Next
In each case, the code examples created 11 new instances of the class Form1
and then called a method (Show
) on each of those new instances. Code you wrote in VB6 that worked with multiple instances of a single Form
class will probably work in .NET without too many changes, but consider this VB6 code:
Form2.Show
Form2.TextBox1.Text = "dfsdf"
That code would not work in .NET, because it is treating the class Form2
as if it were an instance of Form2
. In Visual Basic 6.0 and earlier versions, a special default instance of each form is automatically created, and allows you to use the form's name to access this instance. What this means is that the Visual Basic 6.0 code Form2.Show
has the effect of showing the "default" instance of Form2
, but it doesn't work in Visual Basic .NET. If you want to show a form in VB .NET, you need to create an instance, writing code like this to make Form2
visible:
Dim myFrm As New Form2()
myFrm.Show()
This is a major behavioral change from VB6, but the difference in code is not extreme. The removal of default instances changes more than just the code to show a form though. The second part of the problem is that these special default form instances were global to your entire project in Visual Basic 6.0. Taking the two facts together means that (in Visual Basic 6.0 or earlier) you can refer to any form in your project from anywhere in your code and you will always be referring to the same instance of that form.
In Visual Basic .NET, if you need to access an instance of Form2
from some other part of your application (such as another form or a module), you need to pass the reference to Form2
around your application. The next section explores some of the ways in which you can work with multiple forms.
Working with Multiple Forms in VB .NET
The previous section described a major change in the forms model from VB6 to VB .NET—the removal of default form instances. This is not a major technical problem; programmers (including VB6 programmers) have been working without default instances of non-form classes for a very long time, but it is a big change if you are used to building Windows applications using VB6. The result of this change is that you need a reference to a particular instance of a form to be able to use it. I will start with a very simple example to illustrate the concept. I will create a new Windows application that contains two forms. Form1
, which will be shown automatically when the project runs, will create and display an instance of Form2
when you click a button. To illustrate communication from one form to another, Form2
will also has a button, and it will change the caption of Form1
whenever it is clicked. To get things started, create the new project and add a new form.
Select File, New, Project from the main menu in Visual Studio .NET, and then pick a Visual Basic Windows Application to create. A form will be created with a default name of Form1
. Add a second form by right-clicking the project and selecting Add, Add Windows Form from the menu that appears. Accept the default name of Form2
for the new form and click the Open button to finish the wizard.
Creating and Displaying an Instance of a Form
The first step is to add code to Form1
to create and display an instance of Form2
. Add a button to Form1
, leaving it with a default name of Button1
. Now, double-click the button to enter its event procedure for the Click
event. If you used the code shown in Listing 3.19, a new form would open every time you click the button, which is likely not the desired result.
Listing 3.19 Simple Code for Displaying a Form
Private Sub Button1_Click(ByVal sender As System.Object, _
ByVal e As System.EventArgs) Handles Button1.Click
Dim myForm As New Form2
myForm.Show()
End Sub
Instead, you will move the Form
variable to a module level value, and then determine if it already exists in the Click
event (Listing 3.20). Then, you will create the form if it does not already exist and show it either way.
Listing 3.20 Code That Will Create and Display Only One Copy of a Form
Dim myForm As Form2
Private Sub Button1_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles Button1.Click
If myForm Is Nothing Then
myForm = New Form2
End If
myForm.Show()
End Sub
Using the myForm
variable allows you to hang on to a reference to your newly created form. Hanging onto the reference returned from creating a new form is useful so that you can talk to this second form if need be. The main reason for using this variable though, is so that you can create and track a single instance of Form2
, instead of creating a new one on every button click. Now, let's make Form2
talk back to Form1
.
Communicating Between Two Forms
If you want Form2
to be able to communicate with Form1
, you need to supply a reference to Form1
. Once you do this, you will be set up for two-way communication, as both forms will be holding a reference to the other. The simplest way to accomplish this is to add a Public
variable (of type Form1
) to Form2
, like this:
Public Class Form2
Inherits System.Windows.Forms.Form
Public myCaller As Form1
Then, right after you create an instance of Form2
in the button click event (see Listing 3.21), you can set this property.
Listing 3.21 You Can Pass a Form Reference with a Property
Dim myForm As Form2
Private Sub Button1_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles Button1.Click
If myForm Is Nothing Then
myForm = New Form2
myForm.myCaller = Me
End If
myForm.Show()
End Sub
If code in Form2
needs to access Form1
, it can now do so through the myCaller
variable. Add a button to Form2
and put this code into it, as shown in Listing 3.22.
Listing 3.22 Now that Form2 Has a Reference to Form1, it Can Access Form1's Properties
Private Sub Button1_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles Button1.Click
If Not myCaller Is Nothing Then
myCaller.Text = Now.ToLongTimeString
End If
End Sub
Clicking the button on Form1
will create an instance of Form2
and populate Form2
's myCaller
variable with a reference to Form1
. Clicking the button on Form2
will access Form1
through the myCaller
variable (if it was set) and change its window title. This was a very simple example of communicating between multiple forms, but there will be additional examples as part of the sample applications in Chapters 4 and 5. The next section covers creating and using a form as a dialog box.
Creating and Using a Form as a Dialog Box
Technically speaking, every window/form in an application could be called a dialog box. When I use the term, however, I am referring specifically to a window that is displayed to request some information from the users and return that information back to the application that displayed the dialog box. For the most part, the calling application does not care what happens between displaying the form and the user clicking OK or Cancel to close it; all it is concerned with is the information gathered by the dialog box. To illustrate how you can create a standard dialog box using Visual Basic .NET, this section walks you through the creation of a dialog box that is designed to allow the users to enter in an address (see Figure 3.18).
Figure 3.18
An Address entry dialog box.
Setting Up Your Form
First, you create the sample application and form. Open a new project, a Visual Basic Windows Application, and add a new form named GetAddress
. There should now be two forms in your project, which is exactly what you want because we will launch the GetAddress
dialog box from Form1
. Now, you need to set up the look of GetAddress
to match the expected appearance of a dialog box. Set up four text boxes named txtStreet
, txtCity
, txtPostalCode
, and txtCountry
on your form and arrange them somewhat like the form shown in Figure 3.18. Now, add two buttons to your form, saveAddress
and cancelAddress
, with captions of OK and Cancel, respectively. The two buttons should be positioned in the lower-right corner. If you are planning to make your dialog box resizable, you will want to anchor the two buttons to bottom and right. Select the form itself (click any empty area of the design surface) and set its AcceptButton
and CancelButton
properties to the saveAddress
and cancelAddress
buttons. Setting these properties allows the users to use the Enter and Escape keys as the equivalent of OK and Cancel. The AcceptButton
property also makes the saveAddress
button into the default for the form, which causes it to be highlighted. So that you can tell what button was pressed to exit the form, you should also set the DialogResult
property of both buttons. Set the DialogResult
property for saveAddress
to OK
, and set it to Cancel
for cancelAddress
.
If you want your dialog box to be resizable, select the Sizable
option for the FormBorderStyle
property. For a fixed sized dialog box, you select FixedDialog
for FormBorderStyle
and set MinimizeBox
and MaximizeBox
both to False
.
Once you have created your dialog box, the key to using it is to determine a method for putting starting data into the dialog box and for pulling out the information the user entered. You could access the various controls (the text boxes) directly, but I strongly advise against it. If you work directly with the controls, you will have to change that code if you ever modify the dialog box. Instead, I suggest one of two approaches. Either create a property procedure for each of the values you are exchanging (street, city, postal code, and country in this example) or create a new class that holds all of these values and then create a single property for that object.
This section shows you both methods; you can use whichever one you prefer. For the first case, using multiple properties, create a property for each of the four values you are dealing with, as shown in Listing 3.23.
Listing 3.23 You Can Insert and Remove Values from Your Dialog Box Using Properties
Public Property Street() As String
Get
Return Me.txtStreet.Text
End Get
Set(ByVal Value As String)
Me.txtStreet.Text = Value
End Set
End Property
Public Property City() As String
Get
Return Me.txtCity.Text
End Get
Set(ByVal Value As String)
Me.txtCity.Text = Value
End Set
End Property
Public Property PostalCode() As String
Get
Return Me.txtPostalCode.Text
End Get
Set(ByVal Value As String)
Me.txtPostalCode.Text = Value
End Set
End Property
Public Property Country() As String
Get
Return Me.txtCountry.Text
End Get
Set(ByVal Value As String)
Me.txtCountry.Text = Value
End Set
End Property
For now, these property procedures are just working with the text boxes directly. The value in using property procedures instead of direct control access is that you can change these procedures later, without affecting the code that calls this dialog box. The properties are all set up now, but to make the dialog box work correctly, you also need some code (shown in Listing 3.24) in the OK and Cancel buttons.
Listing 3.24 Don't Forget to Provide a Way to Close Your Forms
Private Sub saveAddress_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles saveAddress.Click
Me.Close()
End Sub
Private Sub cancelAddress_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles cancelAddress.Click
Me.Close()
End Sub
Although both button click events do the same thing, close the dialog box, the DialogResult
property of each button is set appropriately so it will result in the correct result value being returned to the calling code. In the calling procedure, a button click event on the first form, you populate the dialog box using the property procedures, display it using ShowDialog()
, and then retrieve the property settings back into the local variables. ShowDialog
is the equivalent of showing a form in VB6 passing in vbModal
for the modal parameter, but with the added benefit of a return value. ShowDialog
returns a DialogResult
value to indicate how the user exited the dialog box. The example in Listing 3.25 checks for the OK result code before retrieving the properties from the dialog box.
Listing 3.25 If Users Click OK, You Must Copy the Values Back, Otherwise You Don't Want to Change Anything Because They Must Have Clicked Cancel
Private Sub Button1_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles Button1.Click
Dim address As New addressDialog
Dim Street As String = "124 First Street"
Dim City As String = "Redmond"
Dim Country As String = "USA"
Dim PostalCode As String = "98052"
address.Street = Street
address.City = City
address.Country = Country
address.PostalCode = PostalCode
If address.ShowDialog = DialogResult.OK Then
Street = address.Street
City = address.City
Country = address.Country
PostalCode = address.PostalCode
End If
End Sub
The other method I mentioned, using a class to hold a set of values instead of passing each value individually, requires just a few modifications to the code. First, you need to create the class. Add a new class to your project, named Address
, and enter the class definition shown in Listing 3.26.
Listing 3.26 With a Class being Used to Hold Multiple Values, You Can Add New Properties to it without Having to Change the Code to Pass it Around
Public Class address
Dim m_Street As String
Dim m_City As String
Dim m_PostalCode As String
Dim m_Country As String
Public Property Street() As String
Get
Return m_Street
End Get
Set(ByVal Value As String)
m_Street = Value
End Set
End Property
Public Property City() As String
Get
Return m_City
End Get
Set(ByVal Value As String)
m_City = Value
End Set
End Property
Public Property PostalCode() As String
Get
Return m_PostalCode
End Get
Set(ByVal Value As String)
m_PostalCode = Value
End Set
End Property
Public Property Country() As String
Get
Return m_Country
End Get
Set(ByVal Value As String)
m_Country = Value
End Set
End Property
End Class
This class is nothing more than a convenient way to package data into a single object, but it allows you to have only a single property procedure (Listing 3.27) in the dialog box and to simplify the calling code in Form1
(see Listing 3.28).
Listing 3.27 The Number of Properties Is Reduced to One if You Switch to Using a Class Versus Individual Properties on Your Form
Public Property Address() As address
Get
Dim newAddress As New address
newAddress.City = txtCity.Text
newAddress.Country = txtCountry.Text
newAddress.PostalCode = txtPostalCode.Text
newAddress.Street = txtStreet.Text
Return newAddress
End Get
Set(ByVal Value As address)
txtCity.Text = Value.City
txtCountry.Text = Value.Country
txtPostalCode.Text = Value.PostalCode
txtStreet.Text = Value.Street
End Set
End Property
Listing 3.28 A Single Property to Exchange Data Simplifies the Calling Code as well as the Form
Private Sub Button1_Click( _
ByVal sender As System.Object, _
ByVal e As System.EventArgs) _
Handles Button1.Click
Dim addr As New address
Dim address As New addressDialog
Dim Street As String = "124 First Street"
Dim City As String = "Redmond"
Dim Country As String = "USA"
Dim PostalCode As String = "98052"
addr.Street = Street
addr.City = City
addr.Country = Country
addr.PostalCode = PostalCode
address.Address = addr
If address.ShowDialog = DialogResult.OK Then
addr = address.Address
End If
End Sub
The simple addition of a return code when displaying a modal form makes it easier to implement dialog boxes within Visual Basic .NET, but the lack of default form instances can make all multiple form applications difficult.
In Brief
This chapter presented a detailed overview of the new Windows client development model in Visual Basic .NET. Here are the important points of this chapter:
- Windows forms is the new forms model for Visual Basic. Although it is similar to the forms model for Visual Basic 6.0, it supports many new features.
- In .NET, you can see and sometimes edit all of the code that creates and configures your user interface.
- Event handling in .NET is no longer based on the name of the event-handler procedure, instead a new
Handles
keyword has been added.
- Form layout is more powerful in .NET than in VB6 due to the addition of new features for docking and anchoring.
- There are no default instances of forms in VB .NET, which means you cannot work with the form's name as if it were always available.
This member has not yet provided a Biography. Assume it's interesting and varied, and probably something to do with programming.