Skip to content

System.Windows.Forms tutorial

Alex Zimin edited this page Jul 11, 2011 · 3 revisions

Graphical User Interface (GUI) with Windows.Forms

Windows.Forms is the standard GUI toolkit for the .NET Framework. It is also supported by Mono. Like every other thing in the world it has its strong and weak points. Another GUI supported by .NET is Gtk#. Which outguns which is up to you to judge. But before you can do that, get a feel for Windows.Forms by going throught this tutorial.

Note 1: To compile programs using Windows.Forms you will need to include -r:System.Windows.Forms in the compile command. E.g. if you save the examples in the file MyFirstForm.n, compile them with:

ncc MyFirstForm.n -r:System.Windows.Forms -o MyFirstForm.exe

Note 2: I am not going to go too much into details of every single thing you can do with Windows.Forms. These can be pretty easily checked in the Class reference. I will just try to show you how to put all the bricks together.

Table of Contents

The first step

using System.Drawing;
using System.Windows.Forms;

class MyFirstForm : Form {
   public static Main() : void {
      Application.Run(MyFirstForm());
   }
}
This is not too complex, is it? And it draws a pretty window on the screen, so it is not utterly useless, either :)

Let us now try to customize the window a little. All the customization should be placed in the class constructor.

using System.Drawing;
using System.Windows.Forms;

class MyFirstForm : Form {
   public this() {
      Text="My First Form";        //title bar
      ClientSize=Size(300,300);    //size (without the title bar) in pixels
      StartPosition=FormStartPosition.CenterScreen;     //I'm not telling
      FormBorderStyle=FormBorderStyle.FixedSingle;      //not resizable
   }

   public static Main() : void {
      Application.Run(MyFirstForm());
   }
}

An empty Windows.Forms form.

Buttons, labels and the rest

Wonderful. Now the time has come to actually display something in our window. Let us add a slightly customized button. All you need to do is to add the following code to the constructor of your Form class (i.e. somewhere between public this() { and the first } after it).

def button=Button();
   button.Text="I am a button";
   button.Location=Point(50,50);
   button.BackColor=Color.Khaki;
   Controls.Add(button);

def label=Label();
   label.Text="I am a label";
   label.Location=Point(200,200);
   label.BorderStyle=BorderStyle.Fixed3D;
   label.Cursor=Cursors.Hand;
   Controls.Add(label);

I hope it should pretty doable to guess what is which line responsible for. Perhaps you could only pay a little more attention to the last one. Buttons, labels, textboxes, panels and the like are all controls, and simply defining them is not enough, unless you want them to be invisible. Also, the button we have added does actually nothing. But fear not, we shall discuss a bit of events very soon.

A simple menu

Menus are not hard. Just take a look at the following example:

def mainMenu=MainMenu();

def mFile=mainMenu.MenuItems.Add("File");
def mFileDont=mFile.MenuItems.Add("Don't quit");
def mFileQuit=mFile.MenuItems.Add("Quit");

def mHelp=mainMenu.MenuItems.Add("Help");
def mHelpHelp=mHelp.MenuItems.Add("Help");

Menu=mainMenu;
It will create a menu with two drop-down submenus: File (with options Don't Quit and Quit) and Help with just one option, Help.

A button, a label and a menu.

It is also pretty easy to add context menus to controls. Context menus are the ones which become visible when a control is right-clicked. First, you need to define the whole menu, and than add a reference to it to the button definition:

def buttonCM=ContextMenu();
   def bCMWhatAmI=buttonCM.MenuItems.Add("What am I?");
   def bCMWhyAmIHere=buttonCM.MenuItems.Add("Why am I here?");
...
def button=Button();
   ...
   button.ContextMenu=buttonCM;

A context menu.

Basic events

Naturally, however nice it all might be, without being able to actually serve some purpose, it is a bit pointless. Therefore, we will need to learn handling events. There are in fact two ways to do it. Let us take a look at the easier one for the begnning.

The first thing you will need to do is to add the System namespace:

using System;

You can skip this step but you will have to write System.EventArgs and so on instead of EventArgs.

Then, you will need to add a reference to the event handler to your controls. It is better to do it first as it is rather easy to forget about it after having written a long and complex handler.

button.Click+=button_Click;
...
mFileDont.Click+=mFileDont_Click;
mFileQuit.Click+=mFileQuit_Click;
...
mHelpHelp.Click+=mHelpHelp_Click;

Mind the += operator instead of = used when customizing the controls.

Finally, you will need to write the handlers. Do not forget to define the arguments they need, i.e. an object and an EventArgs. Possibly, you will actually not use them but you have to define them anyway. You can prefix their names with a _ to avoid warnings at compile time.

private button_Click(_sender:object, _ea:EventArgs):void{
   Console.WriteLine("I was clicked. - Your Button");
}

private mFileQuit_Click(_sender:object, _ea:EventArgs):void{
   Application.Exit();
}

This is the way you will generally want to do it. But in this very case the handlers are very short, and like all handlers, are not very often used for anything else than handling events. If so, we could rewrite them as lambda expressions which will save a good bit of space and clarity.

button.Click+=fun(_sender:object, _ea:EventArgs){
   Console.WriteLine("I wrote it. - button_Click as a lambda");
};

mFileQuit.Click+=fun(_sender:object, _ea:EventArgs){
   Application.Exit();
};

Cleaning up

After you are done with the whole application, you should clean everything up. In theory, the system would do it automatically anyway but it certainly is not a bad idea to help the system a bit. Especially that it is not a hard thing to do at all, and only consists of two steps.

The first step is to add a reference to System.ComponentModel and define a global variable of type Container:

using System.ComponentModel;
...

class MyFirstForm : Form {
   components : Container = null;
...

In the second and last step, you will need to override the Dispose method. It could even fit in one line if you really wanted it to.

protected override Dispose(disposing : bool) : void {
   when (disposing) when (components!=null) components.Dispose();
   base.Dispose(d);
}

The second step

You should now have a basic idea as to what a form looks like. The time has come to do some graphics.

Painting

There is a special event used for painting. It is called Paint and you will need to create a handler for it.

using System;
using System.Drawing;
using System.Windows.Forms;

class MySecondForm : Form {
   public this() {
      Text="My Second Form";
      ClientSize=Size(300,300);

      Paint+=PaintEventHandler(painter);
   }

   private painter(_sender:object, pea:PaintEventArgs):void{
      def graphics=pea.Graphics;
      def penBlack=Pen(Color.Black);
      graphics.DrawLine(penBlack, 0, 0 , 150, 150);
      graphics.DrawLine(penBlack, 150, 150, 300, 0);
   }

   public static Main():void{
      Application.Run(MySecondForm());
   }
}

A reference to the painter method should be added in the form class constructor (it affects the whole window), but this time it is a PaintEventHandler. Same as PaintEventArgs in the handler itself. In the handler, you should first define a variable of type PaintEventArgs.Graphics. This is the whole window. This is where you will be drawing lines, circles, strings, images and the like (but not pixels; we will discuss bitmaps later on). To draw all those things you will need pens and brushes. In our example we used the same pen twice. But sometimes you will only need it once. In such case, there is no point in defining a separate variable for it. Take a look at the next example.

Painting in Windows.Forms.

Overriding event handlers

Another way to define a handler is to override the default one provided by the framework. It is not at all complicated, it is not dangerous and in many cases it will prove much more useful. Take a look at the example overriding the OnPaint event handler:

protected override OnPaint(pea:PaintEventArgs):void{
   def g=pea.Graphics;
   g.DrawLine(Pen(Color.Black), 50, 50, 150, 150);
}
Naturally, when you override the handler, there is no need any longer to add a reference to it in the class constructor.

By the same token can you override other events. One of the possible uses is to define quite complex clickable areas pretty easily:

protected override OnPaint(pea:PaintEventArgs):void{
   def g=pea.Graphics;
   for (mutable x=0; x<300; x+=10) g.DrawLine(Pen(Color.Blue), x, 0, x, 300);
}

protected override OnMouseDown(mea:MouseEventArgs):void{
   when (mea.X%10<4 && mea.Button==MouseButtons.Left)
      if (mea.X%10==0) Console.WriteLine(&quot;You left-clicked a line.&quot;)
      else Console.WriteLine($&quot;You almost left-clicked a line ($(mea.X%10) pixels).&quot;);
}

Easily creating a complex clickable image map.

A simple animation with double-buffering

What is an animation without double-buffering? Usually, a flickering animation. We do not want that. Luckily enough, double-buffering is as easy as abc in Windows.Forms. In fact, all you need to do is to set three style flags and override the OnPaint event handler instead of writing your own handler and assigning it to the event.

class Animation:Form{
   mutable mouseLocation:Point;         //the location of the cursor

   public this(){
      Text=&quot;A simple animation&quot;;
      ClientSize=Size(300,300);
      SetStyle(ControlStyles.UserPaint | ControlStyles.AllPaintingInWmPaint |
               ControlStyles.DoubleBuffer, true);         //double-buffering on
   }

   protected override OnPaint(pea:PaintEventArgs):void{
      def g=pea.Graphics;
      g.FillRectangle(SolidBrush(Color.DimGray),0,0,300,300);  //clearing the window

      def penRed=Pen(Color.Red);
      g.FillEllipse(SolidBrush(Color.LightGray),
                    mouseLocation.X-15, mouseLocation.Y-15, 29, 29);
      g.DrawEllipse(penRed, mouseLocation.X-15, mouseLocation.Y-15, 30, 30);
      g.DrawLine(penRed, mouseLocation.X, mouseLocation.Y-20,
                 mouseLocation.X, mouseLocation.Y+20);
      g.DrawLine(penRed, mouseLocation.X-20, mouseLocation.Y,
                 mouseLocation.X+20, mouseLocation.Y);
   }

   protected override OnMouseMove(mea:MouseEventArgs):void{
      mouseLocation=Point(mea.X,mea.Y);
      Invalidate();       //redraw the screen every time the mouse moves
   }

   public static Main():void{
      Application.Run(Animation());
   }
}
As you can see, the basic structure of a window with animation is pretty easy. Main starts the whole thing as customized in the constructor. The OnPaint event handler is called immediately. And then the whole thing freezes waiting for you to move the mouse. When you do, the OnMouseMove event handler is called. It checks the new position of the mouse, stores it in mouseLocation and tells the window to redraw (Invalidate();).

Try turning double-buffering off (i.e. comment the SetStyle... line) and moving the mouse slowly. You will see the viewfinder flicker. Now turn double-buffering back on and try again. See the difference?

Well, in this example you have to move the mouse slowly to actually see any difference because the viewfinder we are drawing is a pretty easy thing to draw. But you can be sure that whenever anything more complex comes into play, the difference is very well visible without any extra effort.

A simple animation with double-buffering.

Bitmaps and images

I promised I would tell you how to draw a single pixel. Well, here we are.

protected override OnPaint(pea:PaintEventArgs):void{
   def g=pea.Graphics;
   def bmp=Bitmap(256,1);   //one pixel height is enough; we can draw it many times
   for (mutable i=0; i<256; ++i) bmp.SetPixel(i, 0, Color.FromArgb(i,0,i,0));
   for (mutable y=10; y<20; ++y) g.DrawImage(bmp, 0, y);
}

So, what do we do here? We define a bitmap of size 256,1. We draw onto it 256 pixels in colours defined by four numbers: alpha (transparency), red, green, and blue. But as we are only manipulating alpha and green, we will get a nicely shaded green line. Finally, we draw the bitmap onto the window ten times, one below another.

Mind the last step, it is very important. The bitmap we had defined is an offset one. Drawing onto it does not affect the screen in any way.

Now, that you know the DrawImage method nothing can stop you from filling your window with all the graphic files you might be having on your disk, and modifying them according to your needs.

class ImageWithAFilter:Form{
   img:Image=Image.FromFile(&quot;/home/johnny/pics/doggy.png&quot;);  //load an image
   bmp:Bitmap=Bitmap(img);       //create a bitmap from the image

   public this(){
      ClientSize=Size(bmp.Width*2, bmp.Height);
   }

   protected override OnPaint(pea:PaintEventArgs):void{
      def g=pea.Graphics;
      for (mutable x=0; x<bmp.Width; ++x) for (mutable y=0; y<bmp.Height; ++y)
         when (bmp.GetPixel(x,y).R>0){
            def g=bmp.GetPixel(x,y).G;
            def b=bmp.GetPixel(x,y).B;
            bmp.SetPixel(x,y,Color.FromArgb(255,0,g,b));
         }
      g.DrawImage(img,0,0);            //draw the original image on the left
      g.DrawImage(bmp,img.Width,0);    //draw the modified image on the right
   }

   ...
}

This example draws two pictures: the original one on the left and the modified one on the right. The modification is a very simple filter entirely removing the red colour.

A simple graphic filter.

Adding icons to the menu

Adding icons is unfortunately a little bit more complicated than other things in Windows.Forms.

You will need to provide handlers for two events: MeasureItem and DrawItem, reference them in the right place, and set the menu item properly.

mFileQuit.Click+=EventHandler( fun(_) {Application.Exit();} );
mFileQuit.OwnerDraw=true;
mFileQuit.MeasureItem+=MeasureItemEventHandler(measureItem);
mFileQuit.DrawItem+=DrawItemEventHandler(drawItem);
private measureItem(sender:object, miea:MeasureItemEventArgs):void{
   def menuItem=sender:>MenuItem;
   def font=Font(&quot;FreeMono&quot;,8);   //the name and the size of the font
   miea.ItemHeight=(miea.Graphics.MeasureString(menuItem.Text,font).Height+5):>int;
   miea.ItemWidth=(miea.Graphics.MeasureString(menuItem.Text,font).Width+30):>int;
}

private drawItem(sender:object, diea:DrawItemEventArgs):void{
   def menuItem=sender:>MenuItem;
   def g=diea.Graphics;
   diea.DrawBackground();
   g.DrawImage(anImageDefinedEarlier, diea.Bounds.Left+3, diea.Bounds.Top+3, 20, 15);
   g.DrawString(menuItem.Text, diea.Font, SolidBrush(diea.ForeColor),
                diea.Bounds.Left+25, diea.Bounds.Top+3);
}

A menu with an icon.

Clone this wiki locally