windows forms development

advertisement
WINDOWS FORMS DEVELOPMENT
(with an introduction to ASP.NET) By
Dr Kieran F. Mulchrone,
School of Mathematical Sciences,
UCC
January, 2010.
Page 1 of 55
CONTENTS
Developing Simple Single Form Applications ....................................................................................... 4 Introduction ............................................................................................................................... 4 Getting Started ........................................................................................................................... 5 Delving a little deeper .................................................................................................................. 7 Buttons and messageboxes ....................................................................................................... 7 Adding a pair of numbers .......................................................................................................... 8 Standard and Custom Dialog Boxes .............................................................................................. 10 Standard Dialog Boxes ............................................................................................................ 10 Custom Dialog Boxes .............................................................................................................. 10 Resources ................................................................................................................................ 12 Common Components/Controls ................................................................................................... 13 Button .................................................................................................................................. 14 Checkbox .............................................................................................................................. 14 RadioButton .......................................................................................................................... 14 ListBox, ComboBox and CheckedListBox .................................................................................... 14 DateTimePicker ...................................................................................................................... 16 ErrorProvider ......................................................................................................................... 16 PictureBox............................................................................................................................. 16 ImageList.............................................................................................................................. 16 ListView ................................................................................................................................ 17 ProgressBar........................................................................................................................... 17 Various Other Controls ............................................................................................................ 18 ToolStrips, Menus, Serialization, Data-Binding and MDI Applications..................................................... 19 ToolStrips................................................................................................................................. 19 Menus ..................................................................................................................................... 19 Normal Menus ....................................................................................................................... 20 Context Menus ....................................................................................................................... 20 Serialization ............................................................................................................................. 20 Data Binding ............................................................................................................................. 23 MDI Applications ....................................................................................................................... 25 Introduction .......................................................................................................................... 25 Menu Merging ........................................................................................................................ 25 Document-View Architecture or Model View Controller ................................................................. 27 Data Binding and Enabling Menu Options ...................................................................................... 29 Example Problem ................................................................................................................... 29 Making Menus Bind Data ......................................................................................................... 30 Make the Form and Menu ........................................................................................................ 31 Page 2 of 55
Code the Application ............................................................................................................... 31 Menu Control ......................................................................................................................... 34 Bouncing Balls, Boids and GDI+...................................................................................................... 36 Introduction ............................................................................................................................. 36 Basic GDI+............................................................................................................................... 36 Introduction .......................................................................................................................... 36 A Basic Example .................................................................................................................... 37 Tools of the Trade .................................................................................................................. 38 Coordinate Systems and Transformations ..................................................................................... 40 Introduction .......................................................................................................................... 40 Device Coordinates ................................................................................................................. 41 Page Transformation............................................................................................................... 41 World Transformations ............................................................................................................ 42 Exercise 3.1 Create a graph of the above data in a form. ............................................................... 46 Images, Mice, Keyboards and Panning .......................................................................................... 46 Exercise 3.2 Modify the above code so that the image jars at each end and top and bottom i.e. the
image cannot escape from the panning window. ............................................................................ 49 Animations and Breakout ............................................................................................................ 49 Exercise 3.3 Modify the code so that instead of drawing a line with the mouse move event it can be used
to move the ball into a suitable starting position (i.e. inside the frame) by the user. Add code which
checks to see if the paddle has been moved to a position to take the bounce and if not put up a suitable
message for the user ie. YOU LOSE!! ............................................................................................ 51 The Chaotic Waterwheel and Letting Mathematica to do the Hard Work ................................................ 52 Introduction ............................................................................................................................. 52 Using Mathematica in .NET ......................................................................................................... 52 IMathLink and IKernelLink ....................................................................................................... 52 MathLinkFactory..................................................................................................................... 52 Using IKernelLink ................................................................................................................... 53 Using the MathKernel Object .................................................................................................... 53 Organising our Work in Mathematica ............................................................................................ 54 Solving the Chaotic Waterwheel Problem ...................................................................................... 54 Implementing the Solution of the Chaotic Waterwheel Problem ........................................................ 54 Patterns and the Swift-Hohenberg Equation ...................................................................................... 55 Data Access ................................................................................................................................. 55 A Very Brief Tour of ASP.NET.......................................................................................................... 55 Page 3 of 55
DEVELOPING SIMPLE SINGLE FORM APPLICATIONS
INTRODUCTION
Windows forms is essentially a collection of objects and classes which work together to produce applications
which run primarily on Windows operating systems. The form part of the .NET framework and can be
programmed in any supported language, although here C# is used. Together with the class collection there
is an IDE (Visual Studio) which simplifies much of the development process. In this chapter simple
applications are developed as a means of introduction to the IDE. It is assumed throughout that a firm
grounding in the C# language has been obtained previously by the reader. Much of what the IDE does (e.g.
production of code automatically) behind the scenes should be readily understood in terms of raw C# code,
i.e. you should be able to program this up manually without too much difficulty except perhaps that you
may not be able to remember all of it too easily.
FIGURE 0-1 THE DEFAULT SITUATION AFTER CREATING A WINDOWS FORMS APPLICATION.
Page 4 of 55
There is a great amount of detail associated with Windows Forms programming. This text will not be a direct
transcription of the help file associated with Visual Studio. Key ideas will be introduced here but you may
have to consult supplementary materials.
GETTING STARTED
To begin, let’s create the default Windows Forms project, do nothing and see what we get for free. I’m using
Visual C# 2008 Express Edition (it’s free to download!).
1) Select the File -> New Project menu option.
2) Select the Windows Forms Application and type in a project name (I chose HelloWorld).
3) Click OK.
You are presented with something quite different to what you may be used to with console applications (see
Figure 0-1). Turn your attention to the solution explorer. There are two *.cs files one is called Form1.cs and
one is called Program.cs.
Form1.cs can be viewed in one of two ways 1) designer mode – this is a visual interactive way of coding and
2) code mode – looking directly at the code. These are two views of the same thing. Modifying one modifies
the other and vice versa. Different people find it easier to work in different ways, but at some stage code
has to written. You can switch between the different modes by making sure Form1.cs is selected in the
solution explorer and using the menu option View -> Code (F7) or View -> Designer (Shift + F7). The code
in Form1.cs is as follows:
using
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.ComponentModel;
System.Data;
System.Drawing;
System.Linq;
System.Text;
System.Windows.Forms;
namespace HelloWorld
{
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent();
}
}
}
This seems strange at first sight. There is a method called InitializeComponent(); which is not defined
anywhere. The explanation is simple. Note that the class Form1 is being prefixed by the keyword partial and
this means that the class can be defined over multiple files. Next note that if you click on the X next to the
Form1.cs icon in the solution explorer there is a file called Form1.Designer.cs. This file completes the
definition of the Form1 class.
Open Form1.Designer.cs and click on the “Windows Form Designer Generated Code” text to fully expose all
code in the file. The code is:
namespace HelloWorld
{
partial class Form1
{
/// <summary>
Page 5 of 55
/// Required designer variable.
/// </summary>
private System.ComponentModel.IContainer components = null;
/// <summary>
/// Clean up any resources being used.
/// </summary>
/// <param name="disposing">true if managed resources should be disposed;
otherwise, false.</param>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows Form Designer generated code
/// <summary>
/// Required method for Designer support - do not modify
/// the contents of this method with the code editor.
/// </summary>
private void InitializeComponent()
{
this.components = new System.ComponentModel.Container();
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.Font;
this.Text = "Form1";
}
#endregion
}
}
Now we see the definition of InitializeComponent() and that nothing magical is occurring.
The idea here is subdivide the code into that concerned with visual presentation (Form1.Designer.cs) and
that concerned with functionality or behaviour or real programming (Form1.cs). The recommended way of
editing/creating the visual interface is by using the Designer Interface (although you can directly edit the
code, this is tedious and error-prone). Form1.cs is where the real coding occurs, however in this visual
environment this usually amounts to small segments of code executed in response to events.
The other file Program.cs corresponds to that in a console application and is where the application begins.
Its code is as follows:
using
using
using
using
System;
System.Collections.Generic;
System.Linq;
System.Windows.Forms;
namespace HelloWorld
{
static class Program
{
/// <summary>
/// The main entry point for the application.
/// </summary>
[STAThread]
static void Main()
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
Page 6 of 55
Application.Run(new Form1());
}
}
}
Run the application now as you would any application. We get a simple form which does nothing. However ti
can be resized and moved around the screen and it has a caption along the top of “Form1”.
Exercise 1.1 Can you directly modify the code to change the caption of the Form?
DELVING
BUTTONS
A LITTLE DEEPER
AND MESSAGEBOXES
There are two types of entities that can be added to a form (and we will use both in this section). A control
has a visual appearance at run time such as a button to initiate an action, or a text box for inputting data or
a label for displaying text. On the other hand a component is non-visual (cannot be seen at runtime). Both
entities are object instances and support properties, methods and event. Sometimes I may use the terms
interchangeably as they are essentially identical form a coding perspective.
Following on from the application created previously we add some functionality in this section. First let’s
consider the objects being used in the code listed above. Form1 is derived from a base class called Form.
This automatically provides access to a rich collection of behaviours (some of which we will learn along the
way). In addition there are events generated by the operating system (though the user) which the form may
be interested in subscribing to (it is the programmer who decides on this behaviour).On the other hand in
the Program.cs file we access some methods of the static class Application. For the moment the important
line here is:
Application.Run(new Form1());
This line creates a new Form1 object and then passes it as an argument to Run() to get the application
going.
Let’s now add a component to the form. A component is an object with a visual aspect, properties and
methods. Additionally just like a form each component may wish to subscribe to particular events. Let’s add
a button component to our form and wire up an event.
1)
2)
3)
In the designer view click on the toolbox located on the left hand side (usually). You may want to
pin it.
Here you can see the bewildering array of components that come for free with visual studio (there is
a lucrative business in developing components for software development companies).
Click to select the button control and then drag it into place on the form. It is called button1 by
default. By the way it is good practice to rename components straight away as it can be confusing
dealing with lots of button1, button2 etc. components. Names should be meaningful and it is also
important to change the name immediately because it is used by default in code related to events.
Thus things can get very confusing very quickly if they are changed later on in the development
process.
Take a look at the file Form1.Designer.cs. Code relating to the button has been added to the “Windows Form
Designer Generated Code” section.
We can modify the properties of either the Form1 or Button1 using the properties section. If you cannot see
it then right click button1 and select properties from the menu. If button1 is select it displays it’s properties,
if Form1 is selected it displays it’s properties. There are four buttons at the top of the properties section
which represent from left to right 1) categorised, 2) alphabetical, 3) properties view of the properties; the
fourth button looks like a lightening flash and displays a list of events the component (or form) is usually
interested in. As we saw from the previous course we can hook any object up to any event (i.e. Microsoft
Page 7 of 55
are just guiding us a little here). Change the name property to btHello (it is a useful convention to keep the
first few letters of the name to indicate the type of component being used – bt == button). Change the text
property to ‘Hello’ and note that it updates straight away in the designer, this is WYSIWYG (what you see is
what you get) designing.
Next we need to hook up an event. The most common thing that happens is that the user clicks on it with
the mouse pointer. Make sure the button is selected in the designer and go to the events section of the
properties area (i.e. selected the lightning bolt). A long list of events to which the button can subscribe is
displayed. Find the event called click. Now in the space opposite it you can type the name of the method
(anything you like) or you can double click in the space and the event handler is automatically named as a
combination of the component name and the event (in my case btHello_Click(object sender, EventArgs e)).
You will be automatically taken to the section in Form1.cs where the code to execute when the event occurs
should be written. However, if you look in Form1.Designer.cs you should find the code hooking up the
event:
this.btHello.Click += new System.EventHandler(this.btHello_Click);
this should seem familiar. btHello publishes a Click event and the btHello_Click wants to be informed when it
occurs. Here is my code:
private void btHello_Click(object sender, EventArgs e)
{
DialogResult d = MessageBox.Show("You clicked btHello",
"Click occurred",
MessageBoxButtons.OKCancel,
MessageBoxIcon.Exclamation,
MessageBoxDefaultButton.Button1);
if (d == DialogResult.Cancel)
MessageBox.Show("You Canceller");
}
I am using the static method MessageBox.Show() to display some information for the user. There are 21
overloads to this method and you can have as little or as much control over its appearance as you require.
You should play with it a bit. The action a user takes in regards to a MessageBox is returned as DialogResult
object which simply tells you which button on the MessageBox was clicked. You can then decide what action
to take after that, I have just taken the senseless action of displaying another MessageBox if they press the
Cancel button.
ADDING
A PAIR OF NUMBERS
Let’s create a very simple application now which expects the user to input two numbers and then the result
of adding them together is displayed. This covers basic interactions with users. Next we need to get input
from the user. This involves adding more components (often referred to as controls) to the existing form
and it is probably worthwhile briefly mentioning layout. Good layout of controls on Forms makes the user’s
experience more pleasurable and Visual Studio provides a number of tools to support this activity. You can
use the Tools -> Options Dialog Box to modify how the IDE works. Select “Windows Forms Designer” in the
left panel and in LayoutMode you can specify SnapToGrid (all controls are aligned with the grid) or
SnapToLines (controls are easy to align). You can also specify the grid size (default 8 by 8), whether to snap
to the grid or even show the grid. Annoyingly, changes made here do not show up until you restart visual
studio. There are many options available under the Format menu to help you align, size and space
components is an aesthetically pleasing manner.
Another point is that users expect that by pressing the tab key the focus moves from component to
component in a logical manner (by focus, I mean when a component has focus then any input (e.g.
keyboard) is directed to that component). Ordering of this tabbing process is controlled by select the View > Tab Order menu option and by clicking the components in the order you would like the tabbing to follow,
it is done.
Page 8 of 55
The original form was named Form1 by default. Let’s rename it to frmAddNumbers; this is more descriptive.
Now lets get down to business. Modify btHello to make its text read “Calculate”. Change the Form caption to
“Add Numbers”. Select the TextBox component and draw two onto the form. Name them txtNum1 and
txtNum2. These will take the user input. Create two label controls called lblNum1 and lblNum2 which are
used to describe the purpose of the TextBoxes. Change their text to read First Number: and Second
Number: respectively. Finally we need somewhere for the answer to be placed. Place a suitably name Label
and TextBox on the Form (lblAnswer, txtAnswer). Make the TextBox readonly (properties) so that the
answer can’t be modified. Make sure the tab order is logical.
The code is in response the click event of the Button component. The idea is that the user enters two
numbers and then clicks calculate and the answer appears in the txtAnswer TextBox. There is huge scope
for the user to make mistakes, but let’s not worry about that yet. Here’s the code
private void btHello_Click(object sender, EventArgs e)
{
double num1 = Convert.ToDouble(this.txtNum1.Text);
double num2 = Convert.ToDouble(this.txtNum2.Text);
double answer = num1 + num2;
this.txtAnswer.Text = string.Format("{0:##.00}", answer);
}
Look up composite formatting or formatting types on the help system for more information on formatting
with string.Format.
This code works if the user cooperates. If the user does not cooperate i.e. fails to enter a number then the
code fails. So what can we do? I hate to say it, but you can spend a huge amount of time trying to second
guess the user and cattle-railing them into a particular course of action. Anyway we have a number of
options available to us:
1)
2)
3)
Use the exception mechanism to catch these errors and give appropriate feedback.
Use the MaskedTextBox for user input which essentially control the input side of the task.
Use validation/error controls to catch errors and give feedback.
Exercise 1.2 Implement methods 1 and 2 above for catching user errors.
We will use the validation/error control mechanism. Drop an Error Provider component on to the form.
Notice that there is no visual representation on the form only a red icon on a grey bar beneath it. This is
because this is a component in the strict sense as opposed to a control. Change it’s name to errorProvider.
Next implement the “validated” event for both the txtNum1 and txtNum2 controls. This event occurs (is
raised) after the content of the control has been validated (validation should occur in the Validating event –
no need to worry about this now). In each event put code similar to the following:
double d;
bool bTest = double.TryParse(txtNum1.Text,out d);
if (bTest == true)
this.errorProvider.SetError(txtNum1, "");
else
this.errorProvider.SetError(txtNum1, "This field must contain a number");
here we check to see if the text in the textbox can be converted to a double. If it can use errorProvider
SetError method to assign an empty string associated with txtNum1, if it is erroneous set it to an error
message. Porivde similar code for txtNum2. Play with this app now for a while. Notice that you can still click
the Calculate Button and generate an error. We can avoid this by doing modifying the click code as follows:
if (((errorProvider.GetError(txtNum1)).Length != 0) ||
((errorProvider.GetError(txtNum2)).Length != 0))
{
MessageBox.Show("Input must be corrected before calculation can occur");
Page 9 of 55
return;
}
double num1 = Convert.ToDouble(this.txtNum1.Text);
double num2 = Convert.ToDouble(this.txtNum2.Text);
double answer = num1 + num2;
this.txtAnswer.Text = string.Format("{0:##.00}", answer);
Here we simply check that there has been no error associated with the two boxes before we calculate.
STANDARD
AND
CUSTOM DIALOG BOXES
STANDARD DIALOG BOXES
Dialog boxes are a common feature of applications and are used to interact with the user usually in order to
complete a specific task. For example, suppose the user wants to open a file then a dialog box opens which
allows the user to select the particular file to open. Many of these tasks are standard and .NET provides
components for each one (ColorDialog, FolderBrowserDialog, FontDialog, OpenFileDialog, SaveFileDialog,
PageSetupDialog, PrintDialog, PrintPreviewDialog). These can be created either visually or programmatically.
Let’s do one of each.
In the first case we wish to be able to modify the colour of a form which is grey by default and we will create
the dialog etc. programmatically. Create an application and put a Button named “btChangeColour” on the
form. Wire up the click event and put in the following code:
private void btChangeColour_Click(object sender, EventArgs e)
{
ColorDialog c = new ColorDialog();
c.Color = this.BackColor;
DialogResult d = c.ShowDialog();
if (d == DialogResult.OK)
{
this.BackColor = c.Color;
}
}
In the next suppose we wish to open a file. First place an OpenFileDialog component on the form, name it
openFileDialog and notice that it does not has a visual aspect on the form. Instead it is placed on a grey bar
below the form. This is the container for non-visual components (i.e. the Dialog is only visible when it is
used – it is not persistently visible like a Button). You can modify the properties of the dialog visually
whereas programmatically each modification would take a line of code. For example make the initial
directory “c:\”. Set the filter to be the following: “All Files|*.*|Documents|*.doc”. Place a button named
“btOpenFile” on the form and wire up the click event as follows:
private void btOpenFile_Click(object sender, EventArgs e)
{
DialogResult d = openFileDialog.ShowDialog();
if (d == DialogResult.OK)
{
MessageBox.Show(string.Format("You selected {0}",
openFileDialog.FileName), "Result");
}
}
Usually the standard dialogs a pretty easy to use and there is excellent help available online.
Exercise 1.3 Modify the application to add two numbers such that they can save the calculation to a userselected binary file or read the calculation from a user-selected binary file.
CUSTOM DIALOG BOXES
Page 10 of 55
Naturally when we write our own applications there will be many instances when the standard dialogs will
not fit our purpose. Thus the need arises frequently to create our own custom dialogs. .NET provides plenty
of support for this endeavour. You should have noticed by now that all our dialogs and message boxes
behave in such a way that until the user responds to the dialog nothing else can happen in the application.
This is called modal behaviour. There is also the possibility of modeless behaviour (i.e. you can do other task
while a dialog is open), but this is only warranted quite rarely.
A custom dialog is essentially a form customised to behave in a modal manner. As a simple but contrived
example suppose we wish to have a Get Name Dialog. Create a new form in the last project and name it
dlgGetName (right-clickt he project name and select add new item – select windows form). Add OK (btOK)
and Cancel (btCancel) Buttons. In the form set the AcceptButton property to btOK (this means that pressing
the enter key is the same as clicking on the OK button) and set the CancelButton property to btCancel (this
maps pressing the escape key to pressing the cancel button). Add a text box (txtName) and label. Wire up
the click events of the buttons as follows:
private void btOK_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.OK;
this.Close();
}
private void btCancel_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.Cancel;
this.Close();
}
Note that by default the controls on a form are private. Hence we cannot read the value of the txtName
textbox directly. It is a good idea to wrap it in a property. The code in dlgGetName is:
public string theName
{
get { return txtName.Text; }
set { txtName.Text = value; }
}
public dlgGetName()
{
InitializeComponent();
//initialise to blank string
//must be after InitializeComponent
theName = "";
}
Let’s put a property called name into our dialog. On the original form put a button called btGetName. Wire
up the click event and code as follows:
private void btGetName_Click(object sender, EventArgs e)
{
dlgGetName g = new dlgGetName();
//using ShowDialog makes it modal
DialogResult d = g.ShowDialog();
if (d == DialogResult.OK)
{
MessageBox.Show(string.Format("You entered {0}", g.theName), "Get Name
Report");
}
}
Using ShowDialog() makes it modal. If Show() were used then it would be modeless.
Page 11 of 55
Of course the perennial problem with all of this is making sure the user is doing what they are supposed to
do. Thus a common task in custom dialogs is double checking that the data entered is valid – a process
called validation. We saw earlier how to do this with an ErrorProvider control in the Validated event. The
Validated is really the last part of the validation process and if this event occurs then it means that
validation has been completed successfully. Our previous implementation kind of side stepped the validation
process. Here’s how it is supposed to work.
1)
2)
3)
When moving focus from a one control to another and both controls have CausesValidation set to
true (the default setting) the Validating event is raised.
Implement the Validating event for each control you need to check. If the input is valid do nothing –
if it is incorrect use the incoming e argument (CancelEventArgs e) to specify this e.g. e.Cancel =
true. This prevents the user from progressing and focus remains in the control they have put bad
data into.
Naturally you should always set the Cancel button’s CausesValidation property to false so that the
user can exit the dialog at any time.
Of course this approach can drive the user crazy if they don’t know what’s wrong or they may want to fill in
everything else and then come back to the problematic input. The approach is flexible and you should reflect
on which way is best.
Exercise 1.4 Create a dialog which gets the user’s firstname, surname, date of birth and IQ. Make sure the
data is completely validated.
RESOURCES
This is a fairly advanced topic and could easily be worth a whole chapter to itself. Resources are binary data,
text files, audio or video files, string tables, icons, images, XML files, or any other type of data that your
application requires. The general approach is to store these resources inside the application executable (best
performance) or as a separate file. This all happens when the application is built. This approach also has
implications for software localisation. Using string tables the text assigned to labels, form headings,
message boxes etc. can be changed into a different language simply by changing the associated resource
file. There is excellent support for this type of activity in Visual Studio. However, this is beyond the scope of
the present document. Using resources prevents errors due to images not being present or not being in the
expected place etc.
Let’s add some resources to our project. Key resources for a project are stored under the project name ->
Properties -> Resources.resx, then right click and select open. You should be presented with the following
screen.
Page 12 of 55
FIGURE 0-2 ADDING RESOURCES TO A PROJECT
Concentrating on the central screen area, then on the left hand vertical panel the resource tab should be
selected. Along the top the first button allows you to select the type of resource to be added (above it is set
to images). If you select the arrow on this button the options are Strings, Images, Icons, Audio, Files and
Other. The next button is entitled “Add Resource”. If you click on the arrow the options are Add Existing File,
Add New string, New Image and Add New Text File. Typically most resources are copied from existing files.
Visual Studio (full edition) ships with a large collection of ready to use resources. They are usually located in
c:\Program
Files\Microsoft
Visual
Studio
2009\Common
7\VS2008ImageLibrary\1033\VS2008ImageLibrary.zip. For the moment we will just import an image and
create a string table. Import an image and look at the properties of it in the bottom RHS of the screen.
Importantly, it has a name and a persistence (linked or embedded). Create a few strings. Each string has a
name and value. We will see how to use these in the next section (by the way my image is named tmp and I
created two strings named Hello and Doodle).
COMMON COMPONENTS/CONTROLS
There are lots of components and controls available for use in development. Microsoft provide a default
selection but third party software house provide many more which are customized for particular tasks. For
example controls that simplify the task of interacting with a database or a control which allows users to
input using an excel-style spreadsheet. In this section a very brief introduction to the common controls and
components is given. You are strongly encouraged to play with them all.
Page 13 of 55
BUTTON
Buttons are usually used by the user to indicate a choice or initiate some course of action. On a form with
buttons it is common that pressing the enter key automatically simulates the clicking of one particular
button called the default button. This functionality is provided by setting the AcceptButton property of the
Form to the Button to be automatically pressed. The accept button is then outlined more heavily to indicate
its functionality to the user. Try it out! Buttons can display both text and images. The image associated with
a button is set by modifying the image* properties. This happens by setting a specific image (i.e. image
property) or by selecting an image from an associated imagelist component.
CHECKBOX
Checkboxes have either two or three-state behaviours which is specified by setting the ThreeState property
to false or true respectively. In two-state the CheckState property can either be CheckState.Checked or
CheckState.Unchecked, whereas in three-state it can additionally be in CheckState.Indeterminate. Note
Indeterminate can only be set in code and is usually used to indicate to the user that a choice has not yet
been made. There is also an associated property called Checked which takes Boolean values and
corresponds to the two-state behavior. The CheckedChanged or CheckedStateChanged events occur when
the Checked or CheckedState properties change.
Exercise 1.5 Put three checkboxes on a form and make it so that only one check box can be checked at any
one time. (Note you should actually use RadioButton controls for this they are designed to support this
behavior, it is actually very difficult/impossible reproduce the behavior with check boxes).
RADIOBUTTON
Radio buttons are designed to work in packs. That is two or more radio buttons in a single container item
(i.e. on a form, in a group box) act together such that only one button can be selected at any one time (i.e.
the behavior requested in the last exercise). The value of Checked Boolean property can be used to
determine the choice of the user. There is a CheckedChange event like that of the CheckBox but it is best
practice to use these controls as Microsoft intended.
LISTBOX, COMBOBOX
AND
CHECKEDLISTBOX
These items are designed to manage and display lists of data. Key operations that need to be mastered are
the addition, selection and deletion of list elements. If you require the ability to select multiple items then
either a ListBox or CheckedListBox control are most suitable. On the other hand if space is tight on the form
and only one selection should be made then the combo box may be best. Let’s first concentrate on the
ListBox control.
The elements of the ListBox are managed using the ListBox.ObjectCollection which is exposed as the Items
property. Objects can be added to the list using the Add() method of the Items property. Use the
DisplayMember and DisplayValue properties of the ListBox to specify which property of the objects being
stored are displayed and the value returned.
For a simple example, create a form and put a ListBox named lbTest, a Button named btGetSelection, a
Button named btLoad and two RadioButtons name rbString and rbObject on it. Set the text of
btGetSelection to “Get Selection” and similarly for the other controls. Create a LoadListBox method in the
form class which takes bool argument as follows:
Page 14 of 55
public void LoadListBox(bool stringData)
{
//remove any existing data
lbTest.Items.Clear();
if (stringData)
{
lbTest.Items.Add("Cork");
lbTest.Items.Add("Dublin");
lbTest.Items.Add("Galway");
lbTest.Items.Add("Limerick");
lbTest.Items.Add("Waterford");
}
else
{
lbTest.Items.Add(new city("Cork", "Munster"));
lbTest.Items.Add(new city("Dublin", "Leinster"));
lbTest.Items.Add(new city("Galway", "Connaught"));
lbTest.Items.Add(new city("Limerick", "Munster"));
lbTest.Items.Add(new city("Waterford", "Munster"));
lbTest.DisplayMember = "Name";
lbTest.ValueMember = "Province";
}
}
Where the city class is defined as:
public class city
{
private string name;
public string Name
{
get { return name; }
set { name = value; }
}
private string province;
public string Province
{
get { return province; }
set { province = value; }
}
public city(string name, string province)
{
this.name = name;
this.province = province;
}
}
Implement the click event for btLoad as follows:
private void btLoad_Click(object sender, EventArgs e)
{
LoadListBox(rbString.Checked);
}
Implement the btGetSelection click event as follows:
private void btGetSelection_Click(object sender, EventArgs e)
{
string selection;
if(rbString.Checked)
selection = (string)lbTest.SelectedItem;
else
selection = ((city)lbTest.SelectedItem).Province;
MessageBox.Show(string.Format("You selected: {0}", selection));
}
Page 15 of 55
This implementation is not bulletproof.
Exercise 1.6 Can you figure out how to break this application? Also provide a solution.
Exercise 1.7 Create an application with two ListBoxes and two Buttons (captioned -> and <-) which allows
the user to transfer list elements left and right between the ListBoxes. Load the list boxes with suitable data
to begin.
Note that the ValueMember property only makes sense in the context of data binding (see next chapter).
DATETIMEPICKER
Allows the user to select a date and/or time. Use Format property to specify how the date is displayed
(custom formats can be specified using the CustomFormat property and setting Format to custom. The Text
property returns a string representation of the selected DateTime whereas the Value property returns a
DateTime object.
ERRORPROVIDER
Please refer to the earlier example.
PICTUREBOX
Used for displaying images. Set the image property to specify the image to be displayed. You can select
images form either the file system or from the project resources. Add a few more images to the resources of
the project and select one to be displayed in the PictureBox. Play with the properties of the PictureBox (i.e.
SizeMode) to modify how the image is displayed.
IMAGELIST
This is a component and has a non-visual interface. It is used to store a list of images (as the name would
suggest). Add an ImageList component to the form. You can set the images in the image list from the file
system or programmatically from the project resources as follows:
private void frmAddNumbers_Load(object sender, EventArgs e)
{
imageList1.Images.Add((Image)Properties.Resources.ASSETS);
imageList1.Images.Add((Image)Properties.Resources.CONTACTS);
string tmp = (string)Properties.Resources.Doodle;
}
Both ASSETS and CONTACTS are image resources whereas Doodle is a string resource. The Images property
returns an ImageCollection object which can be used to get at the stored pictures.
ImageList is commonly used in conjunction with other controls.
Page 16 of 55
LISTVIEW
This is essentially a sophisticated ListBox control and works in much the same way. ListView can only display
objects which are inherited from a ListViewItem class. Place a ListView control on a form, name it
lvStudentList and also place a ComboBox call cmbViewType. In the form load event place the following
code:
imageList1.Images.Add((Image)Properties.Resources.ASSETS);
imageList1.Images.Add((Image)Properties.Resources.EXPENSES);
imageList1.Images.Add((Image)Properties.Resources.CONTACTS);
imageList1.Images.Add((Image)Properties.Resources.ORDPROC);
string tmp = (string)Properties.Resources.Doodle;
//add items to ComboBox
cmbViewType.Items.Add(View.LargeIcon);
cmbViewType.Items.Add(View.SmallIcon);
cmbViewType.Items.Add(View.List);
cmbViewType.Items.Add(View.Details);
//make sure first item is selected
cmbViewType.SelectedIndex = 0;
//setup ListView to connect ot image list
lvStudentList.LargeImageList = imageList1;
lvStudentList.SmallImageList = imageList1;
//Add Items to ListView
ListViewItem lvi = new ListViewItem();
lvi.Text = "Kieran";
lvi.SubItems.Add("Mulchrone");
lvi.SubItems.Add("42");
lvStudentList.Items.Add(lvi);
lvStudentList.Items[0].ImageIndex = 0;
lvi = new ListViewItem();
lvi.Text = "John";
lvi.SubItems.Add("MacCarthy");
lvi.SubItems.Add("40");
lvStudentList.Items.Add(lvi);
lvStudentList.Items[1].ImageIndex = 1;
lvi = new ListViewItem();
lvi.Text = "Bobby";
lvi.SubItems.Add("Skywalker");
lvi.SubItems.Add("35");
lvStudentList.Items.Add(lvi);
lvStudentList.Items[2].ImageIndex = 2;
//add columns for the details view
lvStudentList.Columns.Add("First Name", 100);
lvStudentList.Columns.Add("Surname", 100);
lvStudentList.Columns.Add("Age", 100);
Use the ComboBox to switch view types by responding to the SelectedIndexChanged event.
private void cmbViewType_SelectedIndexChanged(object sender, EventArgs e)
{
lvStudentList.View = (View)cmbViewType.SelectedItem;
}
PROGRESSBAR
Exercise 1.8 Figure out how to use the ProgressBar control.
Page 17 of 55
VARIOUS OTHER CONTROLS
A large variety of controls exist. Check out some of the container items such as group boxes and tab
controls as they can make form layouts much more appealing to the user.
Exercise 1.9 Can you figure out how to use the TreeView control?
Page 18 of 55
TOOLSTRIPS, MENUS, SERIALIZATION, DATA-BINDING AND MDI
APPLICATIONS
TOOLSTRIPS
In graphical applications menus and toolstrips (aka toolbars) are essential and expected components of the
interface. The .NET framework provides excellent support for these elements. In this example we write
programme to show off some of its capabilities. Although in a normal application the result of clicking a
menu of toolstrip item is some useful calculation, here we just use MessageBox displays to indicate the
selection has been interpreted correctly.
Begin with a blank form. Place a ToolStrip control on the form and rename it tstrMain. In the properties of
the tstrMain go to the Items property and select the Button with the dots at the RHS. You should see a
dialog box as follows:
Add one of each item type: Button, Label, SplitButton, DropDownButton, Separator, ComboBox, TextBox
and ProgressBar. You can modify the properties of each item here and also remove and change the order in
which the items appear. Click OK to finish.
Each item added is now visible on the tstrMain control on the form. You can select each item and modify its
properties as you would with any control. You can also respond to events. If you care to look in the Designer
code file you will see the programmatic equivalent of your interactive modifications.
Exercise 2.1 Add the items “One”, “Two”, “Three” to the ToolStrip ComboBox. Add three corresponding
menu options to the DropDownButton control.
MENUS
Page 19 of 55
NORMAL MENUS
Simply add a MenuStrip control to the form and you literally type in the name of each menu item. It’s all
very intuitive.
Exercise 2.2 Create a menu with top level items File (lower level items Open, Close, Exit), Edit (lower level
items Undo, Cut, Copy, Paste), View (Graphic, Data, Text). See if you can do flyout menu options as well.
Typically when a menu item is selected then some action is expected to take place. In more complex
applications not every menu option will make sense at all times. That is, depending on what the user is
doing or the state of the application, only a subset of menu options should be available. It can be frustrating
for an option to be apparently available only to find when it is selected that it really wasn’t. It is best
practise to cut these options down as soon as a possible. Each menuitem has an Enabled property and a
checked property which can be used signify the availability of a menu option, or whether or not a selection is
valid. Normally there are a set of Boolean values which specify the state of an application and these can be
checked to determine whether or not a menu option is enabled checked etc. Some care needs to be taken
when implementing this kind of code as you can end up in knots very rapidly. One helpful approach to
consider is using the DropDownOpening Event of a top level menu which is invoked just before the lowerlevel menu is displayed and do a quick check about the enable status of each menu item. An alternative
approach could be taken using databinding of the enabled property
Exercise 2.3 Modify the previous example such that if a bool variable editing is true then cut, copy and
paste are enabled and vice versa. Use a check box to modify the value of the bool variable.
CONTEXT MENUS
A context menu is a floating menu that usually appears in response to a user right-clicking the mouse. By
context is meant that more than one menu may be available (it even be that items may be enabled or
disabled) depending on where the user right-clicks i.e. over different controls. The contextual component of
this functionality is provided by setting the ContextMenuStrip property of each control and form.
Context menus are created in much the same way as ordinary menus and usually only the click event is
coded. You can checked for which items on the context menu should be enabled/disabled by implementing
the Opening event.
SERIALIZATION
As we learn about the fundamentals of Windows Forms application development and the enormity of the
support available, let’s pause to consider how can we persist the data associated with an application? That
is, it is all very well to have our application work with variables and objects stored in RAM but users will also
like to shut down their machine and close all applications (including yours). When they start the application
once more they would reasonably expect to be able to go back to where they were working. At this stage
you are probably thinking about using a file for such a purpose. This is the correct approach but doing so in
an unstructured manner can lead to lots of extra work – work which can be avoided by taking a thought-out
approach. Fortunately .NET provides such a mechanism to make this task easier. Serialization is the
suggested approach and in a nutshell it supports making applications persistent. Serialization does not,
however, force us to arrange the data in our applications in any particular way.
After the next section on MDI Applications and Data Access, we can look at a more complicated context. For
the moment let’s take a simple single form application as our first example. Suppose we wish to maintain a
list of car objects. If we abstract the situation a little, we can imagine that somehow we have a mechanism
that takes the information from our objects and puts it into a suitable control on the form for display and
that any creations/edits/deletions made to the data gets transferred back to our objects. In other words I
am assuming there is a channel between the display layer and the actual data layer (it will come as no
surprise that .NET provides such a mechanism but we’ll worry about this later on). So we are concentrating
on the channel between the data and the persistent file.
Page 20 of 55
0-1 SERIA
ALIZATION AS THE INTERFACE BETWEEN THE DATA
A AND THE FILE STORAGE
In any file
e where Serialization is used
d you must include the follo
owing using sttatements:
using Sys
stem.Runtime
e.Serializat
tion;
using Sys
stem.Runtime
e.Serializat
tion.Formatters.Binary;
In order to
t use our ob
bjects with the Serialization
n mechanism we must ma
ake our objec
cts Serializable
e. For
example consider
c
the fo
ollowing code example:
Page 21 of
o 55
[Serializable()]
public class Car : ISerializable
{
private string make;
private string model;
private int year;
private Owner owner;
public Car()
{
}
public Car(SerializationInfo info, StreamingContext ctxt)
{
this.make = (string)info.GetValue("Make", typeof(string));
this.model = (string)info.GetValue("Model", typeof(string));
this.year = (string)info.GetValue("Year", typeof(int));
this.owner = (Owner)info.GetValue("Owner", typeof(Owner));
}
public void GetObjectData(SerializationInfo info, StreamingContext ctxt)
{
info.AddValue("Make", this.make);
info.AddValue("Model", this.model);
info.AddValue("Make", this.year);
info.AddValue("Owner", this.owner);
}
}
[Serializable()]
public class Owner : ISerializable
{
private string firstName;
private string lastName;
public Owner()
{
}
public Owner(SerializationInfo info, StreamingContext ctxt)
{
this.firstName = (string)info.GetValue("FirstName", typeof(string));
this.lastName = (string)info.GetValue("LastName", typeof(string));
}
public void GetObjectData(SerializationInfo info, StreamingContext ctxt)
{
info.AddValue("FirstName", this.firstName);
info.AddValue("LastName", this.lastName);
}
}
There a couple of features to note (1) each class is preceded by Serializable()] signifying the following
object is to be serializable and (2) each class implements the ISerializable interface which requires a
constructor with a signature like public Owner(SerializationInfo info, StreamingContext ctxt) for
example and a method with signature public void GetObjectData(SerializationInfo info,
StreamingContext ctxt). The constructor allows construction of an object from data in the info object,
whereas GetObjectData stores data into the info object. The typeof operator returns the System.Type of the
object.
Next we create a data object which holds a collection of car objects and some code to do the
Serialization/Deserialization.
Page 22 of 55
public class DataStore
{
private List<Car> cars;
//can use this for adding, editing or subtracting etc.
//should somehow be connected to the visual layer
public List<Car> Cars
{
get { return cars; }
set { cars = value; }
}
public void Serialize(string filename)
{
Stream str = File.Open(filename, FileMode.Create);
BinaryFormatter bf = new BinaryFormatter();
bf.Serialize(str, cars);
}
public void DeSerialize(string filename)
{
Stream str = File.Open(filename, FileMode.Open);
BinaryFormatter bf = new BinaryFormatter();
cars = (List<Car>)bf.Deserialize(str);
}
}
It is worth noting that List<> is also serializable and it knows how to serialize itself (i.e. it tells each item in
the list to serialize itself and this is a list of cars which we have coded to be able to serialize themselves, so
everything is fine). Really all we have to worry about is the name and location of the file where information
gets stored and we have standard dialogs to help manage most of these tasks.
Exercise 2.4 Can you take the above code and make it work with actual data?
DATA BINDING
The previous section used serialization to manage the interface between our data and persistent file storage.
Data binding is the mechanism provided by .NET to manage the interface between the data and the
presentation layer (i.e. the visible bits you can see). We could of course code this from the ground up but it
can become quite involved very quickly and even more than this, it becomes tedious. In many presentations
the Data Binding mechanism becomes mixed up with database access. Although this is probably the most
common commercial application, Data Binding is actually more generic than simply managing a connection
between a database and a form. Data Binding manages the connection between a data source (arrays,
objects, databases or whatever) and the visual display of the data.
Objects of type BindingSource encapsulate much of the work involved in maintaining currency between form
controls and the data source. The default property of the BindingSource is DataSource which expects to
assigned type object (i.e. pretty much anything). The BindingSource object is then used to bind data to the
various controls on the form. For example consider the following simple example.
1)
Create an object of type student
class student
{
private string firstname = null;
public string Firstname
{
get { return firstname; }
set { firstname = value; }
}
private string surname = null;
public string Surname
{
get { return surname; }
set { surname = value; }
}
Page 23 of 55
private int age = 0;
public int Age
{
get { return age; }
set { age = value; }
}
}
2)
Create a layout as follows:
Where the textboxes are named txtFirstname, txtSurname, txtSurname2, txtAge and the button is
btcheck.
3)
Add the following code:
//this is the data and will bound to the controls
private student s = new student();
public DataBinding()
{
InitializeComponent();
//do the data binding
//create a BindingSource
BindingSource bs = new BindingSource();
//assign a DataSource
bs.DataSource = s;
//connect the controls
txtFirstName.DataBindings.Add("Text", bs, "FirstName");
txtSurname.DataBindings.Add("Text", bs, "SurName");
txtSurname2.DataBindings.Add("Text", bs, "SurName");
txtAge.DataBindings.Add("Text", bs, "Age");
}
private void btCheck_Click(object sender, EventArgs e)
{
string message = string.Format("{0} {1} {2}", s.Firstname, s.Surname,
s.Age);
MessageBox.Show(message);
}
When this application runs edit the textboxes and click btCheck. Notice how the two textboxes
connected to the SurName update. This simple example illustrates how databinding manages the
relationship between controls and data and keeps it current.
Page 24 of 55
A slightly more complex example involves using a List<> or student objects as the DataSource. Here is the
code:
//add some data
slist.Add(new student("Kieran", "Mulchrone", 42));
slist.Add(new student("Mick", "McCarthy", 57));
bindingSource1.DataSource = slist;
bindingNavigator1.BindingSource = bindingSource1;
//do the data binding
txtFirstName.DataBindings.Add("Text", bindingSource1, "Firstname");
txtSurname.DataBindings.Add("Text", bindingSource1, "SurName");
txtAge.DataBindings.Add("Text", bindingSource1, "Age");
lstSurnames.DataSource = bindingSource1;
lstSurnames.DisplayMember = "Surname";
dataGridView1.DataSource = bindingSource1;
dataGridView1.Columns[0].DataPropertyName = "Surname";
dataGridView1.Columns[1].DataPropertyName = "Firstname";
dataGridView1.Columns[2].DataPropertyName = "Age";
In this example there is a list as follows:
private List<student> slist = new List<student>();
Additionally, a ListBox and DataGridView control have been added. Two controls namely the bindingSource1
and bindingNavigator1 have been included and hooked up. Play with this application. The BindingNavigator
essentially does all the work in navigating though the list of student objects.
BindingSource objects expose many useful properties and methods such as: MoveFirst(), MoveLast(),
MoveNext(), MovePRevious(), AddNew(), AllowNew etc.
Exercise 2.5 Modify the ListBox example in the previous Chapter to work with databinding.
MDI APPLICATIONS
INTRODUCTION
Multiple Document Interface (MDI) Applications are used when you need an application that can (1) display
multiple instances of the same type of form simultaneously (for example MS Word can display multiple text
documents at the same time) or (2) display multiple views of the same core application data (usually in
different ways – for example in MS Access a single database (core data) can be viewed as a list of tables,
queries, like a spreadsheet etc.). The uniting feature of MDI Applications is that every form in the application
is contained within the boundaries of the main application form.
MENU MERGING
Let’s look at an example. Create a forms application as before. Rename the form MDIParent. Set the
IsMDIContainer property to true. Notice that this effects the display of the form. This form will contain all
other forms created in the application. Add a menuStrip with File, New, Exit and Window, Tile Vertical, Tile
Horizontal, Cascade. Use the ShortCutKeys property to assign Ctrl + N to File->New and Ctrl + E to File>Exit. Put a separator between New and Exit. Select the menuStrip and set the MdiWindowListItem to
windowToolStripMenu. This means that under the window menu an automatic list of all open Child Forms will
be maintained.
Next create the child form (MDI Child) and it should have its own menustrip. In MDI applications you can
have a menu for the parent and one for the child. A complication arises about how to manage the two
menus. Windows Forms provides pretty much automatic support for solving this problem. When the parent
form only is displayed then clearly only the parent menu is to be displayed. However when a child form is
also displayed there should be some mixture of the child and parent menus. Let the child menu have File,
Save and Close and another top level item Edit.
Page 25 of 55
Implement the File->New menu option for MDIParent as follows:
private void newToolStripMenuItem_Click(object sender, EventArgs e)
{
MDIChild m = new MDIChild();
m.MdiParent = this; //a crucial step!
m.Show();
}
Look at what happens to the menus. I hope you agree it’s a bit of a mess. First of all, set the visible
property of the menustrip of the child to false to get rid of the empty blue bar.
We can solve this using the Menu Merge feature. An important related concept is menu level. Top-level
menu items are those always visible such as File, Edit, Window etc. in a standard application. When a top
level menu is selected then a dropdown submenu is displayed. This is the submenu-level. Sometimes a
submenu item may also result in a flyout of more menu options (we could call it a sub-submenu level). Each
and every menu item has a unique level. Forms contructs menus starting at the highest level (the top-level)
and works its way down progressively to the lowest level. That is, every submenu level is constructed before
moving down to the next level.
Each menu item has associated with it a MergeAction and a MergeIndex property. As a menu level is
constructed the contents of these properties determines the positioning of the menu item. The default
values are MergeAction Append and MergeIndex -1. The following table briefly explains the various
MergeAction options.
MergeAction Append Insert MatchOnly Remove Replace Explanation This is the default value and means that the specified menu item is appended to the
end of existing menu items at the same level.
Insert the menu item into the existing menu items at the position specified in
MergeIndex.
If a menu item exists at the same level with the same Text they merge and any
lower level menu items are merged according to their MergeAction and MergeIndex
values.
If a menu item exists at the same level with the same Text then the menu item is
excluded from the merged menu.
If a menu item exists at the same level with the same Text then the pre-existing
menu item is replaced in its entirety (including any sub-levels) by the new menu
item.
The MDIParent menu items should all be set to Append and -1. This means that when only the MDIParent is
to be displayed the menu is created as seen in the designer. For example:
1.
2.
Top-Level
a. Append
b. Append
Sub Level
a. Append
b. Append
c. Append
File (Position 0)
Window (Position 1)
New to File (Position 0)
separator to File (Position 1)
Exit to File (Position 2)
Now consider the situation with the MDIChild form. The proposed values of the relevant properties are:
Menu Item File File­>Save File­>Close Edit Page 26 of 55
MergeAction MatchOnly
Insert
Insert
Insert
MergeIndex -1
1
2
1
Now the detailed procedure for menu creation is as follows:
1.
2.
Top-Level
a. MDIParent
i. Append File (Position 0)
ii. Append Window (Position 1)
b. MDIChild
i. MatchOnly File (Matches the MDIParent File and sub-levels to be merged).
ii. Insert Edit at position 1, to get File (Position 0), Edit (Position 1), Window (Position
2).
Sub Level
a. MDIParent
i. Append New to File (Position 0)
ii. Append separator to File (Position 1)
iii. Append Exit to File (Position 2)
b. MDIChild
i. Insert Save at position 1, to get New (Position 0), Save (Position 1), separator
(Position 2), Exit (Position 3).
ii. Insert Close at position 2, to get New (Position 0), Save (Position 1), Close
(Position 2), separator (Position 3), Exit (Position 4).
Set the properties as advised and run the application. The menu’s are now working as might be expected.
Implement Close and Exit.
A nice feature of MDI Applications is the ability to have multiple forms open at the same time. We can easily
add options to modify how the child forms are arranged.
Exercise 2.6 On the MDIParent Menu make sure that options to Tile Horizontally, Tile Vertically, Cascade
and Arrange Icons are implemented and merge appropriately (hint: see LayoutMDI). Can you achieve the
same functionality from a MDIChild?
DOCUMENT-VIEW ARCHITECTURE
OR
MODEL VIEW CONTROLLER
In previous incarnations of the windows development environment (i.e. pre .NET - Visual C++ and the
Microsoft Foundation Classes were all the rage), the Document-View Architecture was heavily promoted.
Since the arrival of Windows Forms this has been less so, possibly due the convergence of disparate
development environments including Visual Basic. However it is still worth considering the Document View
Architecture (DVA) or its modern design pattern equivalent Model View Controller.
In a nutshell the DVA allows a single data source (i.e. the Document part) to be viewed in one or more
ways. For example, we may have a list of coordinate data and this could be viewed as a spreadsheet or as a
graph. Taking cognisance of Data Binding and Serialization we may come up with the following design:
Page 27 of 55
Furthermore when one view makes a change to the data then that change propagates through to all other
views. Note that windows forms provides no native support for this architecture and we will have to go it
alone. In the next example we try to implement a DVA for a list of coordinates. Suppose that for each set of
coordinates we can have as many graph views or data views of the underlying data as we want and that we
also want any changes to data to be reflected in all the other views.
We will need a class to encapsulate the coordinate:
public class coordinate
{
private float x;
public float X
{
get { return x; }
set { x = value; }
}
private float y;
public float Y
{
get { return y; }
set { y = value; }
}
public coordinate(float x, float y)
{
this.x = x;
this.y = y;
}
}
Let’s use a List<coordinate> object to manage the collection of coordinate objects. Suppose now we have
multiple views displaying the information in List<coordinate> object. We have a few requirements:
1)
There must be only one List<coordinate> object which is referenced by each attached view (i.e.
form).
Page 28 of 55
2)
3)
4)
We need to be able to create a totally new view (and attendant new List<coordinate>).
We need to be able to create a new view based on an existing view (and there may be different
types of new view available).
It would seem sensible to use the existing data binding mechanism to maintain currency between
the different view types.
One way to achieve this functionality is to have different menu items and different constructors for the
MDIChild forms. On the MDIParent menu the New menu option means create a new View (by default it’s a
data view - say) and List<coordinate> data. On the MDIChild New Graph View and New Data View menu
options create the respective view type but no new data. We can write a different constructor depending
on the form type being passed (a bit sloppy) or alternatively we could get our forms to subscribe to an
interface which forces certain properties to be present. In the present case we will need a reference to a
List<coordinate> and also to a BindingSource (to maintain data currency). Other approaches may also
work.
Assignment 1 Provide the implementation of the application discussed in the previous section. An example
application is provided. Use the dataGridView control for displaying data and download the trail version of
Component One Studio for Winforms and use its Chart2D component. As well as providing the functionality
present in the application use the Form text property (i.e. the form caption) to indicate to the user which
child forms are attached to the same data object. Implement serialization for the application so that users
can save to file and open from file (use the filename to modify the caption of the child forms).
The following code is useful for getting chart setup correctly.
chartData.Reset();
//chartData is the chart object
chartData.ChartGroups[0].ChartType = Chart2DTypeEnum.XYPlot;
chartData.DataSource = bs;
//bs is my BindingSource
ChartDataSeriesCollection sc = chartData.ChartGroups[0].ChartData.SeriesList;
sc.RemoveAll();
// Add the data series.
ChartDataSeries s = sc.AddNewSeries();
s.Label = "GraphPlot";
s.X.DataField = "X";
s.Y.DataField = "Y";
DATA BINDING
AND
ENABLING MENU OPTIONS
In many modern applications there are many ways provided for a user to perform the same tasks i.e. menu
options, tool bars, buttons etc. As mentioned earlier on it is often good practise to only available to users’
feasible options at any given time. Maintaining an application in this way can be a headache and may result
in many hundreds of lines of code.
EXAMPLE PROBLEM
Data binding is one way of semi-automating the process of keeping menus enabled or disabled, checked or
unchecked. Let’s consider the following example. Suppose we wish to graphically compare solutions
Verhulst’s logistic population equation for two separate populations. The model is developed first by
considering the Exponential or Malthusian population as follows:
Where N(t) is the size of the population as a function of time t, and r is a constant rate parameter. This
essentially states that the rate of change of the population is proportional to the size of the population. Thus
Page 29 of 55
if r>0, the population grows exponentially and vice versa for r<0. Verhulst proposed that the rate parameter
actually depends on the size of the population as follows:
1
Thus effectively mean that if N=K then r=0, whereas if N>K then r<0 and finally if N<K then r>0. Now r0 is
the rate when N = 0. The differential equation becomes:
1
With solution:
Where N0 is the initial size of the population. Biologically this model encapsulates competition for resources.
K is often termed the carrying capacity and ultimately the population size tends to K.
For whatever reason in our application we wish the user to take a step by step approach:
1)
2)
3)
4)
5)
Begin the Calculation
Enter the parameters for the first population.
Enter the parameters for the second population.
Enter the timescale.
Graph the solutions.
Once a solution exists they may then either:
1)
2)
3)
Modify parameters of first population.
Enter the timescale.
Graph the solutions.
1)
2)
3)
Modify parameters of second population.
Enter the timescale.
Graph the solutions.
1)
Begin a new calculation from the start.
Or
Or
Clearly this is overkill in the current example but it serves as a good background for menu control.
MAKING MENUS BIND DATA
By default menus do not support data-binding. We can modify this by extending the existing
ToolStripMenuItem to make it subscribe to the IBindableComponet interface as follows.
class BindableToolStripMenuItem : ToolStripMenuItem, IBindableComponent
{
#region IBindableComponent Members
private BindingContext bindingContext;
private ControlBindingsCollection dataBindings;
[Browsable(false)] //ignore this
public BindingContext BindingContext
{
get
Page 30 of 55
{
if (bindingContext == null)
{
bindingContext = new BindingContext();
}
return bindingContext;
}
set
{
bindingContext = value;
}
}
[DesignerSerializationVisibility(DesignerSerializationVisibility.Content)] //ignore
this
public ControlBindingsCollection DataBindings
{
get
{
if (dataBindings == null)
{
dataBindings = new ControlBindingsCollection(this);
}
return dataBindings;
}
}
#endregion
}
MAKE
THE
FORM
AND
MENU
Create the menu for you application as before. Create a single form application called frmLogistic and
include the above code. Create a new menu called menuStrip with the following structure:
Top Level File Model Sub Level New Model Exit Enter Pop1 Modify Pop1 Enter Pop2 Modify Pop2 Enter Timescale Generate Graphics Accept the default names generated for the menu items. Finally edit the designer generated code to make
objects of type ToolStripMenuItem be BindableToolStripMenuItem (this could take some time and patience –
the find/replace options of the IDE may come in handy).
Add a component one chart called chartSolution.
CODE
THE
APPLICATION
Let’s ignore the requirements for menu enabling and disabling and code the solution. Let’s grab all of the
relevant data into one object.
public class populationData
{
private float p1_r0;
public float P1_R0
Page 31 of 55
{
get { return p1_r0; }
set { p1_r0 = value; }
}
private float p1_n0;
public float P1_N0
{
get { return p1_n0; }
set { p1_n0 = value; }
}
private float p1_k;
public float P1_K
{
get { return p1_k; }
set { p1_k = value; }
}
private float p2_r0;
public float P2_R0
{
get { return p2_r0; }
set { p2_r0 = value; }
}
private float p2_n0;
public float P2_N0
{
get { return p2_n0; }
set { p2_n0 = value; }
}
private float p2_k;
public float P2_K
{
get { return p2_k; }
set { p2_k = value; }
}
}
Make an instance of this object in the form constructor called popParams.
A custom dialog box is required for setting the parameters for each population. The code for the dialog box
is:
public partial class frmPopDialog : Form
{
private dlgData data;
public dlgData Data
{
get { return data; }
}
public frmPopDialog()
{
InitializeComponent();
data = new dlgData();
//bind the properties to the controls
txtR0.DataBindings.Add("Text", data, "R0");
txtN0.DataBindings.Add("Text", data, "N0");
txtK.DataBindings.Add("Text", data, "K");
}
private void btOK_Click(object sender, EventArgs e)
{
this.DialogResult = DialogResult.OK;
this.Close();
}
private void btCancel_Click(object sender, EventArgs e)
{
Page 32 of 55
this.DialogResult = DialogResult.Cancel;
this.Close();
}
}
public class dlgData
{
private float r0;
public float R0
{
get { return r0; }
set { r0 = value; }
}
private float n0;
public float N0
{
get { return n0; }
set { n0 = value; }
}
private float k;
public float K
{
get { return k; }
set { k = value; }
}
}
In the main form the code is:
private void enterPop1ToolStripMenuItem_Click(object sender, EventArgs e)
{
frmPopDialog f = new frmPopDialog();
f.Text = "Enter Parameters for Population 1";
DialogResult d = f.ShowDialog();
if (d == DialogResult.OK)
{
popParams.P1_R0 = f.Data.R0;
popParams.P1_N0 = f.Data.N0;
popParams.P1_K = f.Data.K;
}
}
private void modifyPop1ToolStripMenuItem_Click(object sender, EventArgs e)
{
frmPopDialog f = new frmPopDialog();
f.Text = "Modify Parameters for Population 1";
//display existing values
f.Data.R0 = popParams.P1_R0;
f.Data.N0 = popParams.P1_N0;
f.Data.K = popParams.P1_K;
DialogResult d = f.ShowDialog();
if (d == DialogResult.OK)
{
popParams.P1_R0 = f.Data.R0;
popParams.P1_N0 = f.Data.N0;
popParams.P1_K = f.Data.K;
}
}
private void enterPop2ToolStripMenuItem_Click(object sender, EventArgs e)
{
frmPopDialog f = new frmPopDialog();
f.Text = "Enter Parameters for Population 2";
DialogResult d = f.ShowDialog();
if (d == DialogResult.OK)
{
popParams.P2_R0 = f.Data.R0;
Page 33 of 55
popParams.P2_N0 = f.Data.N0;
popParams.P2_K = f.Data.K;
}
}
private void modifyPop2ToolStripMenuItem_Click(object sender, EventArgs e)
{
frmPopDialog f = new frmPopDialog();
f.Text = "Modify Parameters for Population 2";
//display existing values
f.Data.R0 = popParams.P2_R0;
f.Data.N0 = popParams.P2_N0;
f.Data.K = popParams.P2_K;
DialogResult d = f.ShowDialog();
if (d == DialogResult.OK)
{
popParams.P2_R0 = f.Data.R0;
popParams.P2_N0 = f.Data.N0;
popParams.P2_K = f.Data.K;
}
}
A custom dialog for the getting and setting the timescale is also required.
Exercise 2.6 Implement the timescale dialog box.
MENU CONTROL
Finally we are ready to implement menu control. Note I have omitted creating a dialog box for the timescale
you can fill in those details. Essentially we create an object with Boolean data member corresponding to
each menu item as follows:
public class menuState
{
private bool enterP1 = false;
public bool EnterP1
{
get { return enterP1; }
set { enterP1 = value; }
}
private bool modifyP1 = false;
public bool ModifyP1
{
get { return modifyP1; }
set { modifyP1 = value; }
}
private bool enterP2 = false;
public bool EnterP2
{
get { return enterP2; }
set { enterP2 = value; }
}
private bool modifyP2 = false;
public bool ModifyP2
{
get { return modifyP2; }
set { modifyP2 = value; }
}
}
Create an object in the Load and bind to the menu items enabled properties.
private void frmLogistic_Load(object sender, EventArgs e)
{
popParams = new populationData();
Page 34 of 55
ms = new menuState();
bs = new BindingSource();
bs.DataSource = ms;
Binding binding = new Binding("Enabled", bs, "ModifyP1");
modifyPop1ToolStripMenuItem.DataBindings.Add(binding);
modifyPop2ToolStripMenuItem.DataBindings.Add("Enabled", bs, "ModifyP2");
enterPop1ToolStripMenuItem.DataBindings.Add("Enabled", bs, "EnterP1");
enterPop2ToolStripMenuItem.DataBindings.Add("Enabled", bs, "EnterP2");
}
We can know set menus to be enabled or disabled according to the values of the menuState ms object. After
a value is changed you must call bs.ResetBindings() to propagate the change. Thus you could bind
menuState with the BindingSource bs to toolstrip options or checkboxes or radiobuttons and changes to ms
will coordinate the display throughout. Note that when new toolstrip items are added then you can reuse the
esisting event handlers coded for the menu items.
Exercise 2.7 Modify the application so that it has a toolstrip items corresponding to the controlled menu
items above. Add control for the generate graphics menu option. Implement code to make the application
restrict the user choices depending on where they are in the process described earlier.
Page 35 of 55
BOUNCING BALLS, BOIDS AND GDI+
INTRODUCTION
In the last two chapters applications were constructed by using a potpourri of pre-existing components and
visual elements. Essentially we were simply tacking it all together and then providing the odd bit of code
here and there. This can be restrictive regarding what you can achieve. For example, suppose you wish to
make a version of that classic computer game tennis (called pong)?
GDI+ is a collection of objects which allow you to take control of the visual output from your program. We
will begin by looking at some static visual outputs and then advance to animated outputs and some
elementary game development.
BASIC GDI+
INTRODUCTION
First there was GDI (Graphics Device Interface) which was a mainstay of Windows programming throughout
the years. Essentially GDI abstracted the connection between a programme and the visual display. This was
achieved through Microsoft providing an interface from the programme to a standard set of drivers, and the
display manufacturers providing the connection from their hardware to the same set of drivers but from a
different direction. GDI+ is an improved version of GDI which works in much the same way. Naturally .NET
provides a selection of objects which abstracts GDI+ and most of these are present in the System.Drawing
namespace.
Page 36 of 55
The key class is the Graphics class. It is a rich and varied class and we will probe some but not all of its
features. An object of type Graphics uses a significant resource essentially managed by the operating
system, it is imperative that when we are finished with such an object all associated resources are given
back to the system (this will be a feature of all our graphical activities).
A BASIC EXAMPLE
Let’s begin with a simple example to draw an ellipse. Create a form and place a single button on it. Add the
following code to its click event handler.
Graphics g = this.CreateGraphics();
try
{
g.FillEllipse(Brushes.Beige, this.ClientRectangle);
}
finally
{
g.Dispose();
}
Run the code and see what happens. Resize the form and see what happens. Notice in the code we use the
try/finally syntax which guarantees the graphics resource is disposed. A shorthand equivalent is:
using(Graphics g = this.CreateGraphics())
{
g.FillEllipse(Brushes.BlueViolet, this.ClientRectangle);
}
Here g is automatically disposed. This is the using statement which is distinct from the using directive which
is used to specify namespaces in which we are interested.
In playing with this simple application you may have notice some behaviour which is not nice. Whenever the
form is resized or covered and uncovered the ellipse disappears and by default is not automatically redrawn.
The windows operating system however triggers an event each time a form needs to be redrawn and this is
the Paint() event. Implement the paint event as follows:
private void frmMain_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.FillEllipse(Brushes.BlueViolet, this.ClientRectangle);
}
This code performs very well with the cover/uncover situation but still it does not do well in the case of a
resize. Notice that here we use the graphics object pre-created for us (and also disposed for us): we are not
creating a new Graphics object, as we had to do in the click event.
During the redraw, windows is only redrawing the new bits that become exposed as the window expands
and does no redrawing at all as the window shrinks in size. We can fix (assuming that we always want the
ellipse to fit into the form snugly) this in one of two ways. First we can simple set a style option for the form
requesting that resize warrants a redraw as follows (put it in the load event or constructor after the
components are initialised):
this.SetStyle(ControlStyles.ResizeRedraw, true);
This works fine but we get loads of flicker (we will fix this in a minute). The second solution is to code up the
resize event:
private void frmMain_Resize(object sender, EventArgs e)
{
Page 37 of 55
//this.Invalidate(true);
//this.Update();
this.Refresh();
}
This works in exactly the same way (flicker included). A few words about the methods listed above.
Invalidate() requests that a paint event be triggered, however because drawing is a resource intensive
operation windows will often take care of other stuff first such as mouse events etc. this can lead to a light
delay. On the other hand Update() causes the paint event to occur straight away. Note Invalidate() and
Update() are called as a pair! Refresh() simply raps up the pair of calls into a single call.
That flicker is very upsetting. It is caused by the fact that paint event is actually the end of a sequence of
activities:
1)
2)
3)
Erase the portion of the form to be redrawn by painting it with a windows level brush.
Sends a paint background event (usually automatically handled by the base-class implementation
using the settings of the BackColor and/or BackgroundImage properties).
Finally the paint handler.
The flicker is cause by constant interference between the background paint and the stuff you actually want
painted.
The solution is simple: set the double buffered property of the form to true. This equivalent to setting the
following Controlstyles on for the form (the older way to achieving the same end):
this.SetStyle(ControlStyles.UserPaint, true);
this.SetStyle(ControlStyles.AllPaintingInWmPaint, true);
this.SetStyle(ControlStyles.DoubleBuffer, true);
The solution is simple and hides quite an amount of work going on behind the scenes. First of all drawing is
condensed into the paint event (no separate background paint). Secondly, double buffering means that all
drawing is first performed into a memory buffer (like to an array, this is very fast) and then the buffer is
transferred directly to the screen. The combination of these two activities removes the flicker.
TOOLS
OF THE
TRADE
In the last section we were really only exposed to one drawing method of the Graphics objects: namely
FillEllipse(). In this section we look at a few other tools we can use for drawing.
C OLOR
Colour is a basic feature of drawing. Many colours are available as static properties of the Color [sic] class.
Sometimes you may wish to have more control (for example colouring a fractal image) and to this end you
can use the static Method FromArgb which has two relevant overloads:
Color FromArgb(int red, int green, int blue);
Color FromArgb(int alpha, int red, int green, int blue);
Each parameter takes and value between 0 and 255 inclusive. For red, green, blue 255 means full on and 0
means full off and alpha is a measure of transparency: 0 is total transparency and 255 is total opacity. You
can also check out two other enumerations related to the operating system preferences KnowColor and
SystemColors.
B RUSHES
Brushes are used for colouring in regions of a form. In the earlier example we used the system collection
Brushes which are by default solid colours. As these are system resource you don’t need to worry about
Page 38 of 55
disposing of them. However there are many sophisticated brushes which are all derived from the Brush base
class. If you create a Brush object then you should use the using() syntax to ensure it is disposed of after
usage. You can check out the following classes: SolidBrush, TextureBrush, HatchBrush,
LinearGradientBrush, PathGradientBrush. Try the following code:
private void frmMain_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
using(Brush b = new HatchBrush(HatchStyle.Divot, Color.DarkBlue, Color.Wheat))
{
g.FillEllipse(b, this.ClientRectangle);
}
}
P ENS
Pens are used just as an artist would use them i.e. for drawing shape outlines or framing shapes. Pens can
be constructed by passing wither a Brush or a Color (more common) and then optionally specifying a width.
Other properties can be specified afterwards such as DashStyle, EndCap, StartCap (see the LineCap
enumeration for options). There is an extreme level of flexibility. On this course we won’t go into the
niceties.
The width of a Pen is related to the graphics units being used (by default it is pixels). Setting the width to 0
means that it will have a width of one of the underlying graphical device basic unit (for most displays this is
one pixel). We will go into units in more detail later. Let’s just draw a line:
private void frmMain_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
using(Brush b = new HatchBrush(HatchStyle.Divot, Color.DarkBlue, Color.Wheat))
{
g.FillEllipse(b, this.ClientRectangle);
}
using (Pen p = new Pen(Color.Tomato, 0))
{
g.DrawLine(p, 0, 0, 50, 50);
}
}
W HAT C AN BE D RAWN ?
Unsurprisingly there is quite an amount of flexibility available and pretty much anything can be drawn.
Here are some of the relevant methods, which tend largely use pens, provided by the Graphics object:
Method
DrawArc
DrawBezier
DrawBeziers
DrawClosedCurve
DrawCurve
DrawEllipse
DrawIcon
DrawIconUnstretched
Page 39 of 55
Description
Overloaded. Draws an arc representing a portion of an ellipse specified by a
pair of coordinates, a width, and a height.
Overloaded. Draws a Bézier spline defined by four Point structures.
Overloaded. Draws a series of Bézier splines from an array of Point
structures.
Overloaded. Draws a closed cardinal spline defined by an array of Point
structures.
Overloaded. Draws a cardinal spline through a specified array of Point
structures.
Overloaded. Draws an ellipse defined by a bounding rectangle specified by a
pair of coordinates, a height, and a width.
Overloaded. Draws the image represented by the specified Icon at the
specified coordinates.
Draws the image represented by the specified Icon without scaling the image.
DrawImage
DrawImageUnscaled
DrawImageUnscaledAnd
Clipped
DrawLine
DrawLines
DrawPath
DrawPie
DrawPolygon
DrawRectangle
DrawRectangles
DrawString
Overloaded. Draws the specified Image at the specified location and with the
original size.
Overloaded. Draws the specified image using its original physical size at the
location specified by a coordinate pair.
Draws the specified image without scaling and clips it, if necessary, to fit in
the specified rectangle.
Overloaded. Draws a line connecting the two points specified by the
coordinate pairs.
Overloaded. Draws a series of line segments that connect an array of Point
structures.
Draws a GraphicsPath.
Overloaded. Draws a pie shape defined by an ellipse specified by a coordinate
pair, a width, a height, and two radial lines.
Overloaded. Draws a polygon defined by an array of Point structures.
Overloaded. Draws a rectangle specified by a coordinate pair, a width, and a
height.
Overloaded. Draws a series of rectangles specified by Rectangle structures.
Overloaded. Draws the specified text string at the specified location with the
specified Brush and Font objects.
Here are the methods which use brushes:
Method
FillClosedCurve
FillEllipse
FillPath
FillPie
FillPolygon
FillRectangle
FillRectangles
FillRegion
Description
Overloaded. Fills the interior of a closed cardinal spline curve defined by an
array of Point structures.
Overloaded. Fills the interior of an ellipse defined by a bounding rectangle
specified by a pair of coordinates, a width, and a height.
Fills the interior of a GraphicsPath.
Overloaded. Fills the interior of a pie section defined by an ellipse specified
by a pair of coordinates, a width, a height, and two radial lines.
Overloaded. Fills the interior of a polygon defined by an array of points
specified by Point structures.
Overloaded. Fills the interior of a rectangle specified by a pair of
coordinates, a width, and a height.
Overloaded. Fills the interiors of a series of rectangles specified by Rectangle
structures.
Fills the interior of a Region.
We will get to use many of these items as we examine some examples in detail.
COORDINATE SYSTEMS
AND
TRANSFORMATIONS
INTRODUCTION
This course we deals only with 2D graphics and thus our discussions will be limited to 2D. 3D graphics will
be dealt with later but a more sophisticated collection of objects need to be used in order to achieve the
performance required for 3D processing.
There are three coordinate systems that need to be understood for GDI+:
•
•
World Coordinates: these are the real-world coordinates of the physical situation that is to be
represented.
Page Coordinates: the world coordinate system is transformed into page coordinates (called the
world transformation). We can have as little or as much control over this as we like. Page
coordinates have their origin (0,0) in the upper left-hand corner of the Form client area (i.e. the bit
you can draw on), x-values increase to the right and y-values increase downwards.
Page 40 of 55
•
Device coordinates: these are the coordinates used by the actual device (for a screen this is pixels).
Page coordinates are transformed into device coordinates (the page transformation) and we are
usually not involved in this process (although there are a few parameters we can tweak).
There are two common devices we wish to draw on: (i) a screen and (ii) a printer (page). Drawing on
screens is usually straight forward as world coordinates can be directly transformed into pixels and there is
no need to tinker with the page transformation. However when drawing on a printer page, things can get
quite hairy as normally we wish to gets items such as fonts, images etc. drawn so as to be at the very least
visible and usually there is an expectation that the user gets an exact copy of what they see on the monitor.
Printing is beyond the scope of this course and forms a specialised area in itself. We will concentrate on
screen output.
Make sure you have the following statement in code, in order to use many features of GDI+:
using System.Drawing.Drawing2D;
DEVICE COORDINATES
On this course we will only be interested in drawing to the screen or display and device units are pixels.
Device coordinates are such that the origin (0,0) is at the upper left hand side of the paintable area of the
form. The x-coordinate increase to the right whereas the y-coordinates increase downwards. We can always
determine the size of the drawable region by using the ClientRectangle property of the form class.
PAGE TRANSFORMATION
Although this could be safely ignored, we will briefly play with this given it can be important in printing. In
this section we ignore the world transformation and concentrate on the effect of the page transformation. By
default when drawing to the screen device the PageUnit property of the graphics object is set to pixels and
the PageScale is set to 1 (i.e. no scaling). PageUnit may be set to any of the members of the GraphicsUnit
enumeration:
Option
World
Display
Description
Specifies the world coordinate system unit as the unit of measure.
Specifies the unit of measure of the display device. Typically pixels for video
displays, and 1/100 inch for printers.
Pixel
Specifies a device pixel as the unit of measure.
Point
Specifies a printer's point (1/72 inch) as the unit of measure.
Inch
Specifies the inch as the unit of measure.
Document Specifies the document unit (1/300 inch) as the unit of measure.
Millimeter Specifies the millimeter as the unit of measure.
Try the following code (note for some reason World causes an exception).
private void frmMain_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
g.PageUnit = GraphicsUnit.Display;
//g.DrawLine(Pens.Blue, 0, 0, 50, 50);
g.PageUnit = GraphicsUnit.Document;
//g.DrawLine(Pens.Blue, 0, 0, 50, 50);
g.PageUnit = GraphicsUnit.Inch;
//g.DrawLine(Pens.Blue, 0, 0, 5, 5);
g.PageUnit = GraphicsUnit.Millimeter;
//g.DrawLine(Pens.Blue, 0, 0, 50, 50);
g.PageUnit = GraphicsUnit.Pixel;
//g.DrawLine(Pens.Blue, 0, 0, 50, 50);
g.PageUnit = GraphicsUnit.Point;
Page 41 of 55
//g.DrawLine(Pens.Blue, 0, 0, 72, 72);
g.PageUnit = GraphicsUnit.World;
g.DrawLine(Pens.Blue, 0, 0, 50, 50);
}
Modify the above code and set the PageScale property as well.
Graphics objects also have the following properties DpiX and DpiY which hold the number of dots per inch in
the horizontal and vertical directions of the target device. Again these are typically used with respect to
printers.
WORLD TRANSFORMATIONS
These are the bread and butter of drawing graphics to the display. Let’s begin with a simple example and
develop an application that draws a collection of points.
Suppose we have a set of points as follows:
20 48 28 19 36 21 31 42 32 44 26 37 35 43 22 29 24 45 41 27 42.2431 97.77975 61.64584 39.92477 68.45782 43.62488 67.93727 81.34073 60.10433 91.45296 51.86696 71.72533 71.50467 87.93271 42.58 58.26773 51.61154 95.76911 83.59582 52.3502 Which plot in excel as follows:
Page 42 of 55
120
100
80
60
Series1
40
20
0
0
10
20
30
40
50
60
We wish to plot these points in our application. Looking at the x-axis the points are between 10 and 55 and
on the y-axis they lie between 30 and 100. A world transformation makes it possible to work in the units of
the points rather than manually calculating the where each one should be place in terms of the device
coordinate system.
We could place a box around our data with bottom-left point (10,30) and upper-right point (55,100). The
question is then, where in the device coordinate system should these points be situated? Let’s look at a
picture.
It would be nice if when using the drawing capabilities of the Graphics class we could specify the position of
the points in their original (world) coordinate system. We seek a transform that maps the world coordinate
Page 43 of 55
Page 44 of 55
system
to
the
device coordinate system. From the above drawing, we wish to map (x1,y1) onto (x1’, y1’) and (x2,y2) onto
(x2’, y2’). It turns out we can achieve this by applying a scaling and a translating transformation. That is:
Where s1 and s2 are the scaling factors and t1 and t2 are the translating factors. We can solve for these in
mathematica to find that:
Even though the transforms dealt with by GDI+ are in 2D they are implemented internally by a 3 by 3
matrix encapsulated in the Matrix class.
For example the transform above can be written in matrix form as:
0
1
0
0
0
0
1
With this representation you can ignore the translational component by setting the third element of the
world vector to 0 instead of 1.
This transformation is applied to the graphics object using the matrix object as follows (for example):
Rectangle crect = this.ClientRectangle;
//specify device points
float x1d = crect.Width * 0.1f;
float y1d = crect.Height * 0.9f;
float x2d = crect.Width * 0.9f;
float y2d = crect.Height * 0.1f;
//world points (depend on application)
float x1 = 0, y1 = -1, x2 = (float)(2 * Math.PI), y2 = 1;
//calculate scaling
float s1 = (x1d – x2d) / (x1 – x2);
float s2 = (y1d – y2d) / (y1 – y2);
//calculate translation
float t1 = (x1 * x2d – x2 * x1d) / (x1 – x2);
float t2 = (y1 * y2d – y2 * y1d) / (y1 – y2);
//identify matrix from default constructor
Matrix m = new Matrix();
//apply transforms
m.Translate(t1,t2);
Page 45 of 55
m.Scale(s1, s2);
//assign transform to the Graphics object
g.Transform = m;
Exercise 3.1 Create a graph of the above data in a form.
IMAGES, MICE, KEYBOARDS
AND
PANNING
These days’ images are an important feature of many applications and even if they do nothing, they often
can give a positive feeling to the user and make an application look modern. Images are easily created using
the Bitmap object and can be loaded from file or from resources. Add the soap_bubbles.bmp to the
resources of your project. Tryout the following code:
Bitmap bmp = GDIPlusSimple.Properties.Resources.Soap_Bubbles;
//alternative
//Bitmap bmp = new Bitmap(@"C:\windows\soap bubbles.bmp");
Rectangle destrect = new Rectangle(10, 10, 100, 100);
Rectangle srcrect = new Rectangle(0, 0, 100, 100);
Rectangle cliprect = new Rectangle(110, 110, 100, 100);
//image scaled to fit in destrect
g.DrawImage(bmp, destrect );
//clip it
g.DrawImage(bmp, cliprect, srcrect, GraphicsUnit.Pixel);
The srcrect above takes a piece of the image (starting at 0,0 of width, height 100,100) and then plonks it
into the cliprect. See what happens if you make:
Rectangle cliprect = new Rectangle(110, 110, 1000, 1000);
When programming in the Console environment most input came in the form of simple string level input and
output. Keyboard input is still a significant means of interaction with programs in windows forms, however
we also have to be able to deal with mouse input. In order to introduce and examine mouse interactions
let’s consider the following problem. Suppose we have the image above but we are only able to see a small
part of it at any one time. We want the user to be able to move the image around by means of the mouse
(this is called panning). The following diagram will aid our thinking.
Page 46 of 55
In one sense we are moving the panning window around the image and displaying what peeks through.
However from the users perspective the panning window is fixed and it appears that the image is sliding
around inside the panning window.
In detail, the typical interface used in this type of interaction is 1) the user presses the left mouse button
and holds it down. 2) the user moves the mouse and the image moves with it 3) the user releases the left
mouse button to signal the end of the interaction. There are three mouse events used in this interaction
namely: MouseDown, MouseMove and MouseUp. Mouse events are channelled into our application by the
operating system and in the normal course of events will only send mouse related events when the mouse is
directly over the application. This can cause difficulties in the current and many other contexts. What if the
user initiates an interaction but releases the mouse button outside of our application? We can mitigate this
circumstance by capturing the mouse, that is requesting that for the period of the transaction we would like
to be informed about all mouse events whether over or outside our application. We do this by setting the
Capture property of the form object to true and unset it by assigning it the value false.
Some of the code and comments follow. First we need to setup the variables etc:
//reference to bitmap no need to keep creating objects
private Bitmap bmp;
//panwindow – where to draw
//imagerect bit of image to draw
private Rectangle panwindow, imagerect;
//top left corner of image relative to panwindow
int imagex = 0, imagey = 0;
Page 47 of 55
//to control the mouse move
Point prev, next;
public frmMain()
{
InitializeComponent();
//set up initial values
bmp = new Bitmap(@"C:\windows\soap bubbles.bmp");
panwindow = new Rectangle(10,10,100,100);
imagerect = new Rectangle(imagex, imagey, 90, 90);
prev = new Point(0, 0);
next = new Point(0, 0);
}
Paint the scene:
private void frmMain_Paint(object sender, PaintEventArgs e)
{
Graphics g = e.Graphics;
imagerect.X = imagex;
imagerect.Y = imagey;
g.DrawImage(bmp, panwindow, imagerect, GraphicsUnit.Pixel);
}
Handle the mouse event which basically modifies imagex and imagey:
private void frmMain_MouseDown(object sender, MouseEventArgs e)
{
prev.X = e.X;
prev.Y = e.Y;
if(panwindow.Contains(prev))
{
this.Capture = true;
}
}
private void frmMain_MouseUp(object sender, MouseEventArgs e)
{
if (this.Capture)
this.Capture = false;
}
private void frmMain_MouseMove(object sender, MouseEventArgs e)
{
if (this.Capture)
{
next.X = e.X;
next.Y = e.Y;
//why is this negative???
imagex -= next.X - prev.X;
imagey -= next.Y - prev.Y;
prev.X = next.X;
prev.Y = next.Y;
this.Refresh();
}
}
Page 48 of 55
For flicker-free display make sure the doublebuffered option of the form is set to true.
Exercise 3.2 Modify the above code so that the image jars at each end and top and bottom i.e. the image
cannot escape from the panning window.
ANIMATIONS
AND
BREAKOUT
Animations essentially give the illusion of motion by changing a static image at around 20 times a second. If
there are only small changes between consecutive images then the human brain can fill in the dots (as it
were) and the result is a smooth motion. A key tool in the arsenal of animation with GDI+ is the Timer
object which appears as a non-visual component in the toolbox. Timers are fairly simple to use. Each one
has an Interval property which is set in milliseconds and a value of 50ms should be good enough in most
cases. There are Start and Stop methods which begin or end the firing of a Tick event. Inside the tick event
you should force the form to repaint (i.e. Invalidate() or Refresh()) and you should also have a mechanism
for knowing the time inside the Paint event handler.
Let’s consolidate these ideas by developing a version of the old arcade game called breakout. Check out the
following code.
using
using
using
using
using
using
using
using
using
System;
System.Collections.Generic;
System.ComponentModel;
System.Data;
System.Drawing;
System.Linq;
System.Text;
System.Windows.Forms;
System.Drawing.Drawing2D;
namespace GDIPlusBreakout
{
public partial class frmMain : Form
{
private Matrix m;
private float s1;
private int leftpaddlex = -10;
private int ballx = -3;
private int bally = -3;
private int xinc = 1;
private int yinc = 1;
private Point prev;
private Point next;
public frmMain()
{
InitializeComponent();
//mouse points
prev = new Point();
next = new Point();
//setup coordinate transformation
float width = this.ClientRectangle.Width;
float height = this.ClientRectangle.Height;
float x1d = width * 0.1f;
float x2d = height * 0.9f;
float x3d = width * 0.9f;
float x4d = height * 0.1f;
float x1 = -80, x2 = -50, x3 = 80, x4 = 50;
Page 49 of 55
s1 = (x1d - x3d) / (x1
float s2 = (x2d - x4d)
float t1 = (x1 * x3d float t2 = (x2 * x4d m = new Matrix();
m.Translate(t1, t2);
m.Scale(s1, s2);
- x3);
/ (x2 - x4);
x1d * x3) / (x1 - x3);
x2d * x4) / (x2 - x4);
}
private void exitToolStripMenuItem_Click(object sender, EventArgs e)
{
this.Close();
}
private void frmMain_Paint(object sender, PaintEventArgs e)
{
//do we need to calculate this every time?
Graphics g = e.Graphics;
g.Transform = m;
using(Pen p = new Pen(Color.Black, 10/s1))
{
//draw outline
g.DrawRectangle(p, -80, -50, 160, 100);
//draw paddle
g.DrawRectangle(p, leftpaddlex, -48, 20, 5);
//draw the ball
g.FillEllipse(Brushes.Blue, ballx, bally, 6, 6);
}
}
private void frmMain_KeyDown(object sender, KeyEventArgs e)
{
switch(e.KeyCode)
{
case Keys.Left:
if(leftpaddlex > -80)
leftpaddlex -= 1;
break;
case Keys.Right:
if(leftpaddlex < 60)
leftpaddlex += 1;
break;
}
}
private void timer_Tick(object sender, EventArgs e)
{
if ((ballx < -80) ||(ballx > 74))
xinc *= -1;
if ((bally < -50) || (bally > 44))
yinc *= -1;
ballx += xinc;
bally += yinc;
this.Refresh();
}
Page 50 of 55
private void startToolStripMenuItem_Click(object sender, EventArgs e)
{
timer.Start();
}
private void stopToolStripMenuItem_Click(object sender, EventArgs e)
{
timer.Stop();
}
private void frmMain_MouseDown(object sender, MouseEventArgs e)
{
this.Capture = true;
prev.X = e.X;
prev.Y = e.Y;
}
private void frmMain_MouseMove(object sender, MouseEventArgs e)
{
if (this.Capture == true)
{
next.X = e.X;
next.Y = e.Y;
using (Graphics g = CreateGraphics())
{
g.DrawLine(Pens.Black, prev, next);
}
prev.X = next.X;
prev.Y = next.Y;
}
}
private void frmMain_MouseUp(object sender, MouseEventArgs e)
{
this.Capture = false;
}
}
}
Exercise 3.3 Modify the code so that instead of drawing a line with the mouse move event it can be used to
move the ball into a suitable starting position (i.e. inside the frame) by the user. Add code which checks to
see if the paddle has been moved to a position to take the bounce and if not put up a suitable message for
the user ie. YOU LOSE!!
Page 51 of 55
Download