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:
Chapter 10 "Well-Formed Types": Resource Cleanup
Chapter 14 "Events"
They are also covered in Programming C# 8.0:
Chapter 7 "Object Lifetime": IDisposable
Chapter 9 "Delegates, Lambdas, and Events": Events
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:
On Linux, GTK is the native toolkit (i.e. many default applications in popular distributions such as Ubuntu are written using GTK). However, Windows Forms under Mono also works well. If you are unsure, I recommend learning Windows Forms since its API is a bit easier to use.
On macOS, Windows Forms will not run, so you must learn GTK.
On Windows, either library can run, but it can sometimes be tricky to get GTK to work correctly. I recommend Windows Forms, which is native to Windows and works well there.
Here are some more notes on these topics.
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 … }
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
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.
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 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.)
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:
On Linux, you cannot build a Windows Forms application using .NET Core. You must use Mono.
On
Windows, if you are using .NET Core, create your project using the
command "dotnet
new winforms
".
("dotnet new
console
"
will not work.) After you do this, you may wish to edit the .csproj
file and change the <OutputType> setting from "WinExe"
to "Exe". That will allow Console.Write() to produce
output, which is useful for debugging.
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).
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.
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:
object
and EventArgs
. (Usually you will just ignore the
arguments that are passed.)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.
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.)
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.
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.
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:
object
and EventArgs
. (Usually you will just ignore the
arguments that are passed.)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.