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.
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);
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.
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;
}
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
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:
Cairo – for drawing 2D graphics
Gdk – low-level functionality including mouse/keyboard events
GLib – has lots of functionality, but we will use this library only to request timeout callbacks
Gtk – the main user interface toolkit
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.
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:
A Label
is a a fixed piece of text, typically
used as a label for some other widget. Above, "x", "y"
and "sum" are all Label
widgets.
An Entry
is a text box, optionally editable by
the user. You can get or set its text via the Text
property.
A Button
is a push button such as the "add"
and "clear" buttons above. When the user pushes it, it
fires a Clicked
event.
A Table
is a container widget that holds a
grid of child widgets. The interface depicted above contains a Table
with 5 rows and 2 columns.
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
+=
.
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 Fill
– if
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.
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 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.
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#.