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:
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 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 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.
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.
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.
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.)
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.