Week 8: Notes

There was no lecture or tutorial this week.

Our C# language topics this week are the using statement and events. In addition, this week we will begin to learn to write graphical interfaces in C# using the Windows Forms or GTK toolkits.

You can read about our C# topics in Essential C# 7.0:

They are also covered in Programming C# 8.0:

You will need to choose one graphical interface toolkit to learn: either Windows Forms or GTK. You certainly do not need to learn both. Your choice will probably depend on the operating system you are running:

Here are some more notes on these topics.

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 
}

events

An event is a class member that lets callers register event handlers that will receive notifications. Each event handler is a delegate. When an event is raised (i.e. fires), a notification is sent to each registered event handler. Each notification includes arguments matching the event's delegate type.

Events are useful for implementing the observer pattern, in which one or more observers may want to hear about changes to an object. A common example of this pattern is a model-view architecture, in which the view observes the model and displays the model's data. In such an architecture we want the model to be unaware of the view. Using an event, a view can register to find out when the model has changed, without giving the model specific knowledge of the view class.

Here is an array class including an event that is raised whenever any array element changes:

delegate void Notify(int index, int old, int now);

class WatchableArray {
  int[] a;
  
  public WatchableArray(int n) {
    a = new int[n];
  }
  
  public event Notify changed;
  
  public int this[int i] {
    get => a[i];
    set {
      int prev = a[i];
      a[i] = value;
      changed(i, prev, a[i]);  // fire the event to notify observers
    }
  }
}

Notice that the event declaration includes a delegate type, and that we can raise an event using method call syntax.

Use the += operator to register a handler with an event. For example, we can create an instance of the WatchableArray class and register an event handler:

  void onChange(int index, int old, int now) {
    WriteLine($"a[{index}] changed from {old} to {now}");
  }
  
  public void Main() {
    WatchableArray a = new WatchableArray(5);
    a.changed += onChange;


  }

If some method later calls

  a[3] = 4;

then the above event handler will run, and will print a message such as

  a[3] changed from 0 to 4

events with no handlers

Be warned: if you attempt to raise an event that has no registered handlers, you will get a NullPointerException. In my opinion this is a weakness in the C# event system: if would certainly be nicer if raising such an event did nothing. However, this is how it works. So in the example above, instead of

changed(i, prev, a[i]);  // fire the event to notify observers

really we should write

if (changed != null)
    changed(i, prev, a[i]);  // fire the event to notify observers

which will guard against this condition.

It's worth noting that C# has a null-conditional operator ?. that can make this slightly easier. This operator invokes a method if the value is non-null. If the value is null, the operator returns null. For example:

    string s = abc();

string t = s?.Trim();

We can use the null-conditional operator when firing an event, though we must add the special method name Invoke to make it work with events:

changed?.Invoke(i, prev, a[i]);  // fire the event to notify observers

This syntax is more compact than the if statement above, thoughs arguably harder to read. You may use whichever syntax you prefer.

graphical interfaces

Last semester we learned how to write graphical programs in Python using the pygame library. In those programs we had to draw all the graphical elements we wanted, since that's how pygame works. But real-world 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. As mentioned above, in this course we will study Windows Forms or GTK, and your choice of toolkit will probably depend on which operating system you run.

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. (You can also find a link to it from our course's home page.)

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.

GTK

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

GTK is a popular toolkit in the Linux world, where it forms the foundation of many desktop applications. For example, the popular Ubuntu distribution includes the GNOME desktop, all of 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.

GTK can be called from many languages, and the C# binding to GTK is called GTK#. The current version of GTK is GTK 3 but we will use Gtk# 2 since it is well supported by the MonoDevelop IDE and by Visual Studio for Mac. (Gtk# 3 does exist, but it is a bit tricky to get it working, especially on macOS.)

Here is a page that describes how to configure your system to build programs with GTK#. As you can see there, the easiest way is to create a Gtk# project using MonoDevelop or Visual Studio for Mac. If you want to use Visual Studio Code, I recommend that you first create your project using MonoDevelop or Visual Studio, and then you can edit it using Visual Studio Code. In this situation you will want to build your project from the command line using msbuild, not dotnet build.

You can learn about Gtk# 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 and many other related libraries.

GTK 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. (You can also find a link to it from our course's home page.)

MonoDevelop includes a visual tool called Stetic that let you build Gtk# interfaces 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 MonoDevelop feature on your own, you are welcome to use it for your work in this class.)

Hello, GTK

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

using Gdk;
using Gtk;
using Window = Gtk.Window;

class MyWindow : 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 install Gtk# and build Gtk# 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.

The next line "using Window = Gtk.Window" is necessary because the namespaces Gdk and Gtk both contain classes called Window, so we must specify which one we want to use.

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 Gtk# 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 Gtk# 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;
using Window = Gtk.Window;

class MyWindow : Window {
    bool draw = true;

    public MyWindow() : base("circle") {
        AddEvents((int) (EventMask.ButtonPressMask));
        Resize(500, 500);
    }
    
    protected override bool OnButtonPressEvent (EventButton e) {
        draw = !draw;
        QueueDraw();
        return true;
    }
    
    protected override bool OnExposeEvent (EventExpose e) {
        if (draw)
            using (Context c = CairoHelper.Create(GdkWindow)) {
                c.SetSourceRGB(0.5, 0.5, 0.0);  // olive color
                c.Arc(250, 250, 150, 0.0, 2 * Math.PI);
                c.Fill();
            }
        return true;
    }
    
    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.

In the MyWindow() constructor we call the important method AddEvents() to tell GTK which input events we wish to receive. In GTK 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 OnExposeEvent() in the near future. This is how a GTK program typically works. Do not attempt to paint anywhere outside the OnExposeEvent() method – instead, whenever the underlying data (i.e. model) changes, call QueueDraw() to cause OnExposeEvent() to redraw the view.

In OnExposeEvent(), the program retrieves a Cairo.Context object by calling CairoHelper.Create(). 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. (If we had passed 0.0 and Math.PI, we would have gotten a half 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.

menus

Like most graphical toolkits, GTK 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 MenuItem class represents a menu item. Each MenuItem 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 Cairo;
using Gdk;
using Gtk;
using System;
using Window = Gtk.Window;

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

class MyWindow : Window {
    Shape shape = Circle;

    public MyWindow() : base("shapes") {
        Resize(500, 500);
        
        MenuItem makeItem(string name, EventHandler handler) {
            MenuItem i = new MenuItem(name);
            i.Activated += handler;
            return i;
        }
        
        MenuItem[] items = {
            makeItem("Square", onSquare),
            makeItem("Circle", onCircle),
            makeItem("Triangle", onTriangle),
            makeItem("Quit", onQuit)
        };
        
        Menu fileMenu = new Menu();
        foreach (MenuItem i in items)
            fileMenu.Append(i);
        MenuItem fileItem = new MenuItem("File");
        fileItem.Submenu = fileMenu;
            
        MenuBar bar = new MenuBar();
        bar.Append(fileItem);
        
        VBox vbox = new VBox();
        vbox.PackStart(bar, false, false, 0);
        Add(vbox);
    }

    void onSquare(object sender, EventArgs e) {
        shape = Square; QueueDraw();
    }

    void onCircle(object sender, EventArgs e) {
        shape = Circle; QueueDraw();
    }

    void onTriangle(object sender, EventArgs e) {
        shape = Triangle; QueueDraw();
    }  

    void onQuit(object sender, EventArgs e) {
        Application.Quit();
    }
      
    protected override bool OnExposeEvent (EventExpose e) {
        using (Context c = CairoHelper.Create(GdkWindow)) {
            c.SetSourceRGB(0.1, 0.1, 0.9);
            switch (shape) {
                case Square:
                    c.Rectangle(100, 100, 300, 300); break;
                case Circle:
                    c.Arc(250, 250, 150, 0.0, 2 * Math.PI); break;
                case Triangle:
                    c.MoveTo(250, 100); c.LineTo(100, 400); c.LineTo(400, 400);
                    c.ClosePath(); break;
                }
            c.Fill();
        }
        return true;
    }  
  
    protected override bool OnDeleteEvent(Event ev) {
        Application.Quit();
        return true;
     }
}

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

Notice how the MyWindow() constructor builds the menu. The top-level MenuBar contains a MenuItem representing the File menu. That MenuItem contains a Menu representing a submenu, which itself contains a series of MenuItem objects (named "Square", "Circle" and so on).

To place the menu at the top of the window, the program places the MenuBar into a VBox, which is a container widget that arranges one or more child widgets in a vertical column. In this case the VBox contains only a single child widget, namely the MenuBar. We add the MenuBar to the VBox by calling PackStart() which places it at the beginning, i.e. at the top. The two false arguments to PackStart() indicate that the MenuBar should not grow to occupy the available space below it, which exists because we don't add any more widgets to the VBox. Finally, the constructor calls Add() to add the VBox to the window itself.

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