Week 10: Notes

Our C# language topics this week are exceptions and the using statement. We also began to learn to write graphical interfaces in C# using the GTK or Windows Forms toolkits.

You can read about the 'using' statement in our Essential C# textbook (Chapter 10 "Well-Formed Types": Resource Cleanup). Here are some more notes:

using

Certain classes in the standard C# library implement the interface IDisposable, which has a single method:

void Dispose ();

This method frees any external resources associated with an object. You should call it when you are finished using an object.

Fort example, the StreamReader and StreamWriter classes implement IDisposable. The Dispose() method in these classes performs the same task as the Close() method: it closes a file or other stream. It is especially important to call Close() or Dispose() when writing to a file - if you do not, some output may not be written! So to be sure that the file is closed in any case, you can write

StreamWriter w = new StreamWriter("output");
try {
   write to w 
} finally {
  w.Dispose();
}

This pattern is so common that C# includes a statement called using that can be used for the same task. using takes a object that implements IDisposable and executes a block of code, disposing the object when the block exits:

StreamWriter w = new StreamWriter("output");
using (w) {
   write to w 
}

Alternatively, you can bind a new variable inside a using statement:

using (StreamWriter w = new StreamWriter("output")) {
   write to w 
}

graphical interfaces

Graphical programs contain many common user interface elements: push buttons, check boxes, scrollbars, dialog boxes and so on. In this course we will learn to write programs in C# that contain these kinds of elements, which are typically called widgets in the Unix programming world and controls in the Windows world.

There are many user interface toolkits that let you write graphical programs using a variety of widgets/controls. Some of these toolkits are specific to one particular operating system, and others are cross-platform. Also, some of them are specific to a particular programming language, and others have bindings to many different languages. In this course we will study GTK and Windows Forms. Your choice of toolkit might depend on which operating system you run.

See my page about building programs using Gtk# or Windows Forms, which has a table showing which toolkits will work with which operating systems and C# implementations.

GTK

GTK is popular in the Linux world, where it forms the foundation of many desktop applications. For example, Ubuntu includes the GNOME desktop, whose applications are written using GTK. GTK was originally a Linux toolkit, but it has been ported to macOS and Windows and is sometimes used on those platforms as well.

The current version is GTK 4, but it's not yet possible to call GTK 4 from C#. However a library called GtkSharp supports GTK 3, and can be used on any platform (Linux, macOS, Windows) when building a C# program with .NET. In addition, Mono supports GTK 2 through its Gtk# library.

You can learn about Gtk# 2 by reading the documentation on the Mono project site. There are some tutorials there, though their scope is pretty limited. The Mono site also includes extensive API reference documentation for GTK 2 and many other related libraries.

GTK is enormous: it contains dozens of controls that you can use to build an application. We can't study all of these in our course. I have written a quick reference page that lists the most important classes and methods that we'll be using in GTK 3.

Hello, GTK

Here is a "hello, world" program in GTK 3:

using Gdk;
using Gtk;

class MyWindow : Gtk.Window {
    public MyWindow() : base("hello, world") {
    }
    
    protected override bool OnDeleteEvent(Event ev) {
        Application.Quit();
        return true;
     }
}

class Hello {
    static void Main() {
        Application.Init();
        MyWindow w = new MyWindow();
        w.ShowAll();
        Application.Run();
    }
}

The program displays a blank window with title "hello, world".

Make sure that you can build and run this program on your computer. Here is a page describing how to build GtkSharp programs on various operating systems.

At the beginning, the program imports the Gdk and Gtk namespaces. GDK is a lower-level graphics library that underlies GTK. Some of the classes we will use live in the Gdk namespace, so you should always import both Gdk and Gtk.

We next see that our class MyWindow is a subclass of Window. In this subclass, we have an event handling method OnDeleteEvent. This method runs automatically when the user closes the window, which causes a Delete event to occur. In response to this event, we quit the application.

drawing graphics

Like most application toolkits, GTK is an event-driven system. Typically in your Main() method you will call the Application.Run() method, which runs the main event loop. Then, when actions occur such as the user clicking a mouse button, the system fires an event that your code can respond to.

There are two ways to enable your code to run in response to a GTK event. One way is to add a handler to a C# event object. For example, Window is a subclass of Widget, and Widget has a event called ButtonPressEvent that fires when the user presses a mouse button. So if you write an event handler method such as

void buttonHandler(object o, ButtonPressEventArgs args) { … }

then you can register it to run in response to the event, like this:

w.ButtonPressEvent += buttonHandler;

However it is often more convenient to write a subclass of a Window or other type of widget. In addition to C# events, each GtkSharp class also contains virtual methods that will run automatically in response to each event type. You can override these methods to add code that will run in response to events, and that is easier than hooking up event handlers using +=. This is the approach we took in the "hello, world" program above, where MyWindow is a subclass of Window.

Here is a GtkSharp program that draws a circle. Each time the user clicks the window, the circle toggles on and off:

using Cairo;
using Gdk;
using Gtk;
using System;

class Area : DrawingArea {
    bool draw = true;           // model
    
    public Area() {
        AddEvents((int) EventMask.ButtonPressMask);
    }

    protected override bool OnButtonPressEvent (EventButton e) {
        draw = !draw;
        QueueDraw();
        return true;
    }
    
    protected override bool OnDrawn (Context c) {
        if (draw) {
            c.SetSourceRGB(0.5, 0.5, 0.0);  // olive color
            c.Arc(250, 250, 150, 0.0, 2 * Math.PI);
            c.Fill();
        }
        return true;
    }
}

class MyWindow : Gtk.Window {
    public MyWindow() : base("circle") {
        Resize(500, 500);
        Add(new Area());
    }
    
    protected override bool OnDeleteEvent(Event ev) {
        Application.Quit();
        return true;
     }
}

class Hello {
    static void Main() {
        Application.Init();
        MyWindow w = new MyWindow();
        w.ShowAll();
        Application.Run();
    }
}

At the top of the program we import the Cairo namespace. Cairo is a library for drawing 2-dimensional graphics. GTK integrates closely with Cairo, and most GTK applications use Cairo for visual output.

The program uses a DrawingArea widget for drawing graphics. To produce graphics in your program, I recommend drawing on a DrawingArea rather than trying to draw on the main Gtk.Window, which may cause problems in some environments. You can add the DrawingArea to your Window subclass in its constructor, as in the code above:

Add(new Area());

In the Area() constructor we call the important method AddEvents() to tell GTK which input events we wish to receive. In a GTK DrawingArea you will not receive any input events unless you ask for them by calling AddEvents(), so don't forget to make this call!

When the user clicks the mouse, the program toggles the boolean field 'draw' and then calls the important method QueueDraw(). This method tells the system that it is time to redraw the window's contents, and causes the system to call OnDrawn() in the near future. This is how a GTK program typically works. Do not attempt to paint anywhere outside the OnDrawn() method – instead, whenever the underlying data (i.e. model) changes, call QueueDraw() to cause OnDrawn() to redraw the view.

In OnDrawn(), the program receives a Cairo.Context object. It then calls Arc() to draw a circle. By passing 0.0 and 2 * Math.PI, we tell Arc() that we want it to go all the way around the center point, drawing a complete circle. In the quick reference documentation you will find other methods you can use for drawing.

Be aware that Arc() and other drawing methods do not immediately draw - they merely add a geometric shape to the current path. After you call these methods, you must call either Stroke() or Fill() to draw to the output surface. Stroke() draws an outline of the current path, and Fill() fills it in.

Windows Forms

First: Most graphical interface toolkits are fairly similar, at least at a high level. And so this introduction to Windows Forms is quite similar to the GTK introduction above – in fact I have copied much of the text, but with the code transformed to Windows Forms!

Windows Forms is a popular toolkit for writing graphical programs in C#. You can learn about it by reading the documentation on Microsoft's web site. The same site also includes API reference documentation for the System.Drawing and System.Windows.Forms namespaces that contain the various Windows Forms classes.

Windows Forms is enormous: it contains dozens of controls that you can use to build an application, and some of them are complex. We can certainly not study all of these in our course. I have written a quick reference page that lists the most important classes and methods that we will be using, and I recommend using this quick reference page rather than the full documentation whenever possible.

Microsoft's Visual Studio IDE includes a tool called the Windows Forms Designer that let you build forms visually by dragging and dropping their elements. In this course we will not study this tool: we will construct graphical interfaces manually in code. (However if you want to learn about this Visual Studio capability on your own, you are welcome to use it for your work in this class.)

Hello, Forms

Here is a "hello, world" program in Windows Forms:

using System;

using System.Windows.Forms;

class Hello {
  [STAThread]
  static void Main() {
    Form form = new Form();
    form.Text = "hello, world";
    
    Application.Run(form);
  }
}

The program displays a blank window with title "hello, world".

In Windows Forms, a form means a window, so the call to new Form() creates a new top-level window.

Make sure that you can build and run this program on your computer. Here are some tips:

The attribute [STAThread] above Main() is related to synchronization and multithreading, which are subjects we might discuss later. If you are using Mono on Linux, the attribute is unnecessary. If you are running Windows, be sure to include the [STAThread] attribute before Main() since without it some operations may hang (e.g. running an open file dialog).

drawing graphics

Like most application toolkits, Windows Forms is an event-driven system. Typically in your Main() method you will call the Application.Run() method, which runs the main event loop. Then, when actions occur such as the user clicking a mouse button, the system fires an event that your code can respond to.

There are two ways to enable your code to run in response to a Windows Forms event. One way is to add a handler to a C# event object. For example, Form is a subclass of Control, and Control has a event called MouseDown. So if you write an event handler method such as

void mouseHandler(object sender, MouseEventArgs e) { … }

then you can register it to run in response to the event, like this:

form.MouseDown += mouseHandler;

However it is often more convenient to write a subclass of a Form or other type of control. In addition to C# events, each Windows Forms class also contains virtual methods that will run automatically in response to each event type. You can override these methods to add code that will run in response to events, and that is a little easier than hooking up event handlers using +=.

Here is a Windows Forms program that draws a circle. Each time the user clicks the window, the circle toggles on and off:

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

class MyForm : Form {
  bool draw = true;
  
  public MyForm() {
    Text = "circle";
    ClientSize = new Size(500, 500);
    StartPosition = FormStartPosition.CenterScreen;
  }
  
  protected override void OnMouseDown(MouseEventArgs args) {
    draw = !draw;
    Invalidate();
  }
  
  protected override void OnPaint(PaintEventArgs args) {
    if (draw) {
      Graphics g = args.Graphics;
      g.FillEllipse(Brushes.Olive, 100, 100, 300, 300);
    }
  }
}

class Hello {
  [STAThread]
  static void Main() {
    Form form = new MyForm();
    
    Application.Run(form);
  }
}

When the user clicks the mouse, the OnMouseDown() handler toggles the boolean field 'draw' and then calls the important method Invalidate(). This method tells the system that it is time to redraw the window's contents, and causes the system to call OnPaint() in the near future. This is how a Windows Forms program typically works. Do not attempt to paint anywhere outside the OnPaint() method – instead, whenever the underlying data (i.e. model) changes, call Invalidate() to cause OnPaint() to redraw the view.

In OnPaint(), the program retrieves a Graphics object from the args parameter. It then calls FillEllipse() to draw a circle. In the quick reference documentation you will find other methods you can use for drawing.

menus

Like most graphical toolkits, Windows Forms supports menus. Each menu contains a set of commands that the user can select, and application-specific code runs in response to each command.

The ToolStripMenuItem class represents a menu item. Each ToolStripMenuItem holds a instance of the System.EventHandler delegate, which is defined as follows:

delegate void EventHandler (object sender, EventArgs e);

So when you write a method that will run in response to a menu item, it needs to take these two parameters of type object and EventArgs. (Usually you will just ignore the arguments that are passed.)

Here is a program with a menu that lets the user select a shape to be displayed:
using System;
using System.Drawing;
using System.Windows.Forms;

using static Shape;
enum Shape { Square, Circle, Triangle };

class MyForm : Form {
  Shape shape = Circle;
  
  public MyForm() {
    ClientSize = new Size(500, 500);
    StartPosition = FormStartPosition.CenterScreen;
    
    ToolStripMenuItem[] fileItems = {
        new ToolStripMenuItem("Square", null, onSquare),
        new ToolStripMenuItem("Circle", null, onCircle),
        new ToolStripMenuItem("Triangle", null, onTriangle),
        new ToolStripMenuItem("Quit", null, onQuit)
    };
    
    ToolStripMenuItem[] topItems = {
        new ToolStripMenuItem("File", null, fileItems)
    };
    
    MenuStrip strip = new MenuStrip();
    foreach (var item in topItems)
      strip.Items.Add(item);
    
    Controls.Add(strip);
  }
  
  void onSquare(object sender, EventArgs e) {
    shape = Square; Invalidate();
  }
  
  void onCircle(object sender, EventArgs e) {
    shape = Circle; Invalidate();
  }
  
  void onTriangle(object sender, EventArgs e) {
    shape = Triangle; Invalidate();
  }  
  
  void onQuit(object sender, EventArgs e) {
    Application.Exit();
  }
  
  protected override void OnPaint(PaintEventArgs args) {
    Graphics g = args.Graphics;
    switch (shape) {
        case Square:
            g.FillRectangle(Brushes.Blue, 100, 100, 300, 300); break;
        case Circle:
            g.FillEllipse(Brushes.Blue, 100, 100, 300, 300); break;
        case Triangle:
            g.FillPolygon(Brushes.Blue, new Point[] {
              new Point(250, 100), new Point(100, 400), new Point(400, 400)
            }); break;
    }
  }
}

class Hello {
  [STAThread]
  static void Main() {
    Form form = new MyForm();
    
    Application.Run(form);
  }
}

Exercise: The methods onSquare(), onCircle() and onTriangle() above are all very similar. Figure out how to eliminate this code duplication.