Lecture 9

Here are notes about topics we discussed in lecture 9.

For more details about the C# features we discussed, see the Essential C# textbook or the C# reference pages. For more information about GTK and Gtk#, see the Gtk# documentation.

Implicitly typed variables

When you declare a local variable, you can use the var keyword to tell the compiler to infer its type. For example:

    var list = new List<int>(5);

This is equivalent to

    List<int> list = new List<int>(5);

extension methods

You can add extension methods to an existing class. An extension method can be used syntactically as if belonged to a class, even though it is written outside the class.

For example, suppose that we are using a C# library that provides this type:

  class Vector {
    public double dx, dy;
    public Vector(double dx, double dy) { this.dx = dx; this.dy = dy; }
  }

We might wish that the author of the class had provided a length method that calculates the length of a Vector. Since we cannot modify the class, we can write an extension method:

  static class Util {
    public static double length(this Vector v) =>
        Sqrt(v.dx * v.dx + v.dy * v.dy);
  }

Now we can call the method as if it had been defined inside the Vector class itself:

  Vector v = new Vector(3.0, 4.0);
  WriteLine(v.length());  // writes 5.0

Note that an extension method must be static and must be contained in a static class.

delegates

A delegate holds a reference to a method. It is similar to a function pointer in languages such as Pascal or C.

The delegate keyword declares a new delegate type. For example:

  delegate bool IntCondition(int i);

With this declaration, an IntCondition is a type of delegate that takes an integer argument and returns a boolean. We can now declare a variable of type IntCondition, and use it to hold a reference to a method:

  static bool odd(int i) => i % 2 == 1;

  static void Main() {
    IntCondition c = odd;
    …

We can invoke the delegate using method call syntax:

    WriteLine(c(4));    //  writes False

In the example above, the delegate c refers to a static method odd. A delegate may also refer to an instance method, in which case it actually references a particular object on which the method will be invoked. For example:

  class Interval {
    public int low, high;
    public Interval(int low, int high) { this.low = low; this.high = high; }
  
    public bool contains(int i) => low <= i && i <= high;
  }

  static void Main() {
    IntCondition c = new Interval(1, 5).contains;
    IntCondition d = new Interval(3, 7).contains;
    WriteLine(c(2));  // writes True
    WriteLine(d(2));  // writes False
  }

Here is a method that counts how many elements in an array of integers satisfy an arbitrary condition:

  static int count(int[] a, IntCondition cond) {
    int n = 0;
    foreach (int i in a)
      if (cond(i))
        ++n;
    return n;
  }

Delegates may be generic:

  delegate bool Condition<T>(T t);  // maps type T to bool

Here is the count method from above, rewritten to work on an array of any type T:

  static int count<T>(T[] a, Condition<T> cond) {
    int n = 0;
    foreach (T val in a)
      if (cond(val))
        ++n;
    return n;

}

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]);
    }
  }
}

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 foo() {
    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

Introduction to Gtk#

In this course we will learn to build simple graphical interfaces using GTK+ and Gtk#.

There are lots of toolkits available for building graphical user interfaces including GTK+, Cocoa, Qt, Windows Forms, WPF, wxWidgets and many others. All of these have various strengths and weaknesses. Some are tied to one particular platform, and others are cross-platform. Most are similar in various ways. If you get to know one framework such as GTK+, many of the concepts you learn will apply if and when you start using a different framework in the future.

GTK+ is the native toolkit of the GNOME desktop, which is included in various Linux distributions such as Ubuntu. But GTK+ also works reasonably well on other platforms including macOS and Windows. It is a large and powerful library that includes dozens of standard user interface elements such as push buttons, scrollbars, menu bars, toolbars and so on. In this course we will learn only a small subset of GTK+ that is adequate for writing simple applications, including some video games.

Gtk# is the standard C# binding to GTK+. It nicely integrates C# and GTK+, making GTK+ elements available via C# classes, methods, properties and events.

In this course we will use GTK+ version 2. There is a newer version GTK+ 3, but it is not yet well integrated with C# on every platfrom. Fortunately GTK+ 2 will be more than adequate for our purposes.

To set up your computer to build and run programs using Gtk#, follow the instructions here.

The Gtk# quick reference page contains reference documentation for the subset of Gtk# 2 that we will be learning and using. You will want to refer to it often.

Our Gtk# programs will use several related libraries. As you can see in the Gtk# reference, each library has its own namespace:

Windows and widgets

In GTK, a window is a top-level window that appears on the user's desktop, represented by an instance of the Window class. A user can drag a window around, minimize it, maximize it and so on.

Every window contains a hierarchy of widgets. A widget may represent a user interface element such as a scrollbar or push button. Some widgets are invisible containers that are used only for holding and positioning other widgets. Widgets form a hierarchy: every widget has a single parent, and a widget may have any number of children. Each widget is an instance of the abstract Widget class.

A window is itself a widget; thus, Window is a subclass of Widget. A window can have only one child. So if you want to have multiple widgets in a window, the window's child should be a container widget, which can itself hold multiple widgets.

Here is a minimal "hello, world" program in GTK. It creates a window whose title is "hello", and which displays nothing. When the user clicks the window's close button, the application exits.

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

class Frame : Window {
  Frame() : base("hello") {  }
  
  protected override bool OnDeleteEvent(Event ev) {
    Application.Quit();
    return true;
  }
  
  static void Main() {
    Application.Init();
    new Frame().ShowAll();
    Application.Run();
  }
}

All of our Gtk# programs will have this basic structure. Notice that in our Main method we call Application.Run, which runs GTK's main loop. All our further activity occurs in response to events that GTK sends us. This means that our program is event-driven; this is typical for programs that use graphical interfaces.

In the program above, the line

  using Window = Gtk.Window;

is necessary because both the Gdk and Gtk namespaces export a class named Window. This line resolves the ambiguity, indicating that we want to use the Window class from the Gtk namespace.

GTK includes many kinds of events. For example, GTK sends a delete event to a window when the user clicks its close button. The program above illustrates one way to handle a GTK event in a C# program: by overriding an event handler method defined in a widget superclass. Our event handler causes the application to exit.

We will see another way to handle GTK events below.

A simple calculator

We will now build a trivial calculator application in GTK. It will look like this:

The user can enter numbers in the text boxes labelled "x" and "y". When they press the "add" button, the sum appears in the "sum" text box.

Here is the application. It uses the following Widget subclasses:

You can find more information about all of these widgets in the Gtk# Quick Reference.

This application illustrates another way to handle GTK events. Each GTK event is exposed as a C# event, and you can register a handler using the += operator. Here, we register an event handler add_clicked that adds the numbers x and y from the text boxes in the window, and a handler clear_clicked that clears the text boxes.

Alternatively, we could have written two Button subclasses, each of which could override the OnClicked method inherited from the Button class. But this application it was more convenient to handle the events in code in the Adder class, so we wrote handlers there and registered them using +=.

Drawing with Cairo

You may often want to perform your own custom drawing, for example if you are writing a game. GTK provides a DrawingArea widget where you can draw anything we like using the Cairo library.

You can draw on a DrawingArea in response to an Expose event that GTK sends when it is time to draw. Every time you receive this event, you should redraw everything that should be visible to the user. (The redrawing actually happens in an in-memory bitmap, which GTK then copies to the display all at once so there is no visible flicker. This technique is called double buffering.) Do not attempt to perform any drawing outside an Expose event handler.

Sometimes you will want to ask GTK to send a new Expose event, because you want to redraw the window because some underlying data has changed. To do this, call the QueueDraw method. This will cause GTK to send an Expose event in the near future.

You draw using a Cairo context, which contains lots of methods for 2D drawing. When using a context, you will probably first want to set a drawing color using the SetSourceRGB method.

You can construct a path in a context by calling methods that add lines and curves to the path, including MoveTo, LineTo, Rectangle and Arc. You then call either Stroke to draw the path itself as a series of lines/curves, or Fill to fill in the area inside the path. Do not forget to call either Stroke or Fillif you do, nothing will appear at all.

See the Gtk# Quick Reference documentation for details about these methods.

Here is a program that draws a red square. In this program, the View class is a subclass of DrawingArea. It illustrates how we will typically handle an Expose event:

  protected override bool OnExposeEvent (EventExpose ev) {
    Context c = CairoHelper.Create(GdkWindow);
    
    // do some drawing with the Cairo context
    
    c.GetTarget().Dispose();
    c.Dispose();
    return true;
  }

This program never calls QueueDraw: the image it draws never changes, so there is never any need to redraw it.

Mouse and keyboard input

Gtk# will send us events when the user clicks the mouse or types on the keyboard.

When a key is pressed or released, the top-level window will receive an KeyPress or KeyRelease event. It is easiest to handle these by overriding OnKeyPressEvent or OnKeyReleaseEvent in a Window subclass.

When the user moves the mouse or clicks or releases the mouse button, the DrawingArea (or other widget) under the mouse may receive a MotionNotify, ButtonPress or ButtonRelease event. But these events will arrive only if the widget first requests to receive them by calling the AddEvents method. Each of these events arrives with an EventMotion or EventButton object containing the current mouse position.

Here is the red square program from above, extended to let the user drag the square around with the mouse. It uses mouse event handlers to achieve this. Each time the square's position changes, the program calls QueueDraw. That causes a new Expose event to arrive, and the OnExposeEvent handler redraws the square at the new position.

Cairo transformations

Cairo uses device coordinates by default. This means that a position passed to a Cairo method such as LineTo is measured in absolute pixels. The position (50, 100) is 50 pixels to the right of the upper left corner of the window, and is 100 pixels below that corner.

But sometimes it is convenient to use a different coordinate system for drawing. Cairo lets us define a transformation from user coordinates to device coordinates. We can accomplish this by calling the Translate and/or Scale methods on a Cairo context. Translate causes user coordinates to be translated linearly by a fixed offset, and Scale will scale them linearly by fixed factors in each dimension.

If you perform a series of calls to Translate and/or Scale, the transformations are composed in such a way that the last transformation is performed first. For example, suppose that we make these calls:

Cairo.Context c = …;
c.Translate(100, 100);
c.Scale(2, 3)
c.MoveTo(50, 50);

What position will this move to in device coordinates, i.e. in absolute pixels? The user coordinate (50, 50) is first scaled to (100, 150) and then translated to (200, 250), which is the resulting pixel position.

Some larger programs

In the tutorial we saw two larger programs written using Gtk#: a Tic Tac Toe game and an Asteroids video game. These are well worth studying to help you become more familiar programming with Gtk#.