Introduction to GTK 3 in C#

Contents

Introduction

GTK is a cross-platform user interface toolkit that first appeared in 1998. 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 will also run on macOS and Windows.

In our class we'll use a library called GtkSharp that lets us call GTK 3 from C#, and can be used on any platform (Linux, macOS, Windows). GtkSharp does not support the GTK 4, the latest version, but GTK 3 is more than adequate for our purposes. (Another library is under development that will allow access to GTK 4 from C#, though apparently it's not yet ready for production use.)

Unfortunately there is no reference documentation for GTK 3 in C#. However, I have written a quick reference that lists fundamental classes and methods in GTK 3. Below, this page explains how to build and run GTK programs, and gives examples showing how to use various GTK features.

Installing GTK

On macOS only, you'll first need to install GTK 3 using the Homebrew package manager:

  1. Install Homebrew:

      $ /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

    This step will take several minutes.

  2. Install GTK:

      $ brew install gtk+3

    This step will take approximately 5 minutes.

  3. If you have a Mac with an M1 (not Intel) chip, then as a workaround for a bug involving library paths, add the following line to the .zprofile file in your home directory (creating it if necessary):

      export DYLD_FALLBACK_LIBRARY_PATH=/opt/homebrew/lib

    Then close your terminal window and open a new one, so that the command you added to .zprofile in the previous step will be read.

Hello, GTK

First create a console application:

$ dotnet new console

Now add a reference to the GtkSharp package:

$ dotnet add package GtkSharp

Now you can write GTK code in your program. Here is a "hello, world" program:

using Gdk;
using Gtk;

class MyWindow : Gtk.Window {
    public MyWindow() : base("hello, world") {
    }

    protected override bool OnDeleteEvent(Event e) {
        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".

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 see that our class MyWindow is a subclass of Gtk.Window.

After we create an instance of MyWindow, we call its ShowAll() method to make it visible. The MyWindow class includes a handler for the event DeleteEvent, which occurs when the user closes the window. In response to this event, we quit the application.

Events

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;

As another approach, you can write a subclass of a Window or other type of widget. In addition to C# events, each GTK class in GtkSharp contains virtual methods that will run automatically in response to each event type. You can override these methods in your subclass to add code that will run in response to events.

Drawing with Cairo

Cairo is a library for drawing 2-dimensional graphics. GTK integrates closely with Cairo, and most GTK applications use Cairo for visual output.

A DrawingArea is a canvas, i.e. a widget where you can draw any graphics you like using Cairo.

Here is a program that uses Cairo to draw a couple of geometric shapes and some text.

using Cairo;
using Gdk;
using Gtk;
using Color = Cairo.Color;

class Area : DrawingArea {
    Color black = new Color(0, 0, 0),
          blue = new Color(0, 0, 1),
          light_green = new Color(0.56, 0.93, 0.56);

    protected override bool OnDrawn (Context c) {
        // draw a triangle
        c.SetSourceColor(black);
        c.LineWidth = 5;
        c.MoveTo(100, 50);
        c.LineTo(150, 150);
        c.LineTo(50, 150);
        c.ClosePath();
        c.StrokePreserve();     // draw outline, preserving path
        c.SetSourceColor(light_green);
        c.Fill();               // fill the inside

        // draw a circle
        c.SetSourceColor(blue);
        c.Arc(xc: 300, yc: 100, radius: 50, angle1: 0.0, angle2: 2 * Math.PI);
        c.Fill();

        // draw a rectangle
        c.SetSourceColor(black);
        c.Rectangle(x: 100, y: 200, width: 200, height: 100);
        c.Stroke();

        // draw text centered in the rectangle
        (int cx, int cy) = (200, 250);  // center of rectangle
        string s = "hello, cairo";
        c.SetFontSize(30);
        TextExtents te = c.TextExtents(s);
        c.MoveTo(cx - (te.Width / 2 + te.XBearing), cy - (te.Height / 2 + te.YBearing));
        c.ShowText(s);

        return true;
    }
}

class MyWindow : Gtk.Window {
    public MyWindow() : base("cairo") {
        Resize(400, 400);
        Add(new Area());  // add an Area to the window
    }

    protected override bool OnDeleteEvent(Event e) {
        Application.Quit();
        return true;
    }
}

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

The output looks like this:

Gradients

using Cairo;
using Gdk;
using Gtk;
using Color = Cairo.Color;

class Area : DrawingArea {
    Color black = new Color(0, 0, 0),
          blue = new Color(0, 0, 1),
          light_green = new Color(0.56, 0.93, 0.56),
          red = new Color(1, 0, 0);

    protected override bool OnDrawn (Context c) {
        using (LinearGradient g = new LinearGradient(x0: 150, y0: 100, x1: 250, y1: 100)) {
            g.AddColorStop(0.0, red);
            g.AddColorStop(1.0, light_green);

            c.SetSource(g);
            c.Rectangle(x: 100, y: 50, width: 200, height: 100);
            c.Fill();
        }

        using (RadialGradient r = new RadialGradient(cx0: 200, cy0: 300, radius0: 25,
                                                     cx1: 200, cy1: 300, radius1: 75)) {
            r.AddColorStop(0.0, black);
            r.AddColorStop(1.0, light_green);

            c.SetSource(r);
            c.Arc(xc: 200, yc: 300, radius: 75, angle1: 0, angle2: 2 * Math.PI);
            c.Fill();
        }

        return true;
    }
}

class MyWindow : Gtk.Window {
    public MyWindow() : base("gradients") {
        Resize(400, 400);
        Add(new Area());  // add an Area to the window
    }

    protected override bool OnDeleteEvent(Event e) {
        Application.Quit();
        return true;
    }
}

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

The output looks like this:

Mouse and keyboard input

When the user clicks or releases a mouse button, GTK will deliver a ButtonPress or ButtonRelease event to the widget under the mouse cursor. Similarly, when the user moves the mouse, GTK will deliver a PointerMotion event. If you want your widget to receive these events, you must register to receive them by calling the AddEvents method.

When the user types or releases a key, GTK will deliever a Key event to the widget that currently has the input focus. The top-level window has focus by default. If the user clicks in a control such a Gtk.Entry, that control will receive the focus so that the user can type there.

The program below allows the user to draw lines and rectangles using the mouse. The user can type 'l' or 'r' to switch between the line and rectangle drawing tools. The program handles ButtonPress, ButtonRelease and PointerMotion events in a DrawingArea, and handles Key events in the top-level Window.

using Cairo;
using Gdk;
using Gtk;
using Color = Cairo.Color;
using Key = Gdk.Key;
using static Gdk.EventMask;

enum Tool { Line, Rectangle };

class Area : DrawingArea {
    Color black = new Color(0, 0, 0),
          white = new Color(1, 1, 1),
          transparent_green = new Color(0.56, 0.93, 0.56, 0.5);

    ImageSurface canvas;
    bool dragging = false;
    double start_x, start_y;    // start position of mouse drag
    double end_x, end_y;        // end position of drag
    public Tool tool = Tool.Line;

    public Area() {
        canvas = new ImageSurface(Format.Rgb24, 400, 400);
        using (Context c = new Context(canvas)) {
            c.SetSourceColor(white);
            c.Paint();
        }

        AddEvents((int) (ButtonPressMask | ButtonReleaseMask | PointerMotionMask));
    }

    void draw(Context c) {
        c.SetSourceColor(black);
        c.LineWidth = 3;
        if (tool == Tool.Line) {
            c.MoveTo(start_x, start_y);
            c.LineTo(end_x, end_y);
            c.Stroke();
        } else {
            c.Rectangle(x: start_x, y: start_y,
                        width: end_x - start_x, height: end_y - start_y);
            c.StrokePreserve();
            c.SetSourceColor(transparent_green);
            c.Fill();
        }
    }

    protected override bool OnDrawn (Context c) {
        c.SetSourceSurface(canvas, 0, 0);
        c.Paint();
        
        if (dragging)
            draw(c);
        return true;
    }

    protected override bool OnButtonPressEvent (EventButton e) {
        dragging = true;
        (start_x, start_y) = (end_x, end_y) = (e.X, e.Y);
        QueueDraw();
        return true;
    }

    protected override bool OnMotionNotifyEvent(EventMotion e) {
        if (dragging) {
            (end_x, end_y) = (e.X, e.Y);
            QueueDraw();
        }
        return true;
    }

    protected override bool OnButtonReleaseEvent (EventButton e) {
        dragging = false;
        using (Context c = new Context(canvas))
            draw(c);
        QueueDraw();
        return true;
    }
}

class MyWindow : Gtk.Window {
    Area area;

    public MyWindow() : base("draw") {
        Resize(400, 400);
        area = new Area();
        Add(area);
    }

    protected override bool OnKeyPressEvent (EventKey e) {
        switch (e.Key) {
            case Key.l: area.tool = Tool.Line; break;
            case Key.r: area.tool = Tool.Rectangle; break;
        }
        return true;
    }

    protected override bool OnDeleteEvent(Event e) {
        Application.Quit();
        return true;
    }
}

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

Here is what the window looks like after I've drawn a few rectangles and lines:

Timeouts and animation

The class GLib.Timeout lets you register a timeout handler that will be called at regular intervals. To perform animation, you can register a timeout handler to be called many times per second. In your handler, typically you will update a model, then call QueueDraw() to redraw the view.

The program below displays a player that can move left, move right, and jump. It uses a model-view architecture: the Game class holds the model, which is just the player's current position and velocity. The top-level class MyWindow handles keyboard input, and has a timeout handler that updates the model and redraws the view.

using Cairo;
using Gdk;
using Gtk;
using Key = Gdk.Key;
using Timeout = GLib.Timeout;

class Game {
    public int player_x = 200, player_y = 600;
    int dy = 0;     // vertical velocity in pixels/tick

    public void tick(bool move_left, bool move_right) {
        if (move_left)
            player_x -= 5;
        else if (move_right)
            player_x += 5;

        player_y += dy;
        if (player_y >= 600) {  // hit the ground
            dy = 0;     // stop falling
            player_y = 600;
        }
        else
            dy += 2;    // accelerate downward
    }

    public void jump() {
        if (player_y == 600 && dy == 0)
            dy = -20;
    }
}

class View : DrawingArea {
    Game game;
    ImageSurface player = new ImageSurface("player.png");

    public View(Game game) {
        this.game = game;
    }

    protected override bool OnDrawn (Context c) {
        c.SetSourceRGB(0, 0, 0);
        c.MoveTo(0, 600);
        c.LineTo(800, 600);
        c.Stroke();

        c.SetSourceSurface(player, game.player_x, game.player_y - player.Height);
        c.Paint();

        return true;
    }
}

class MyWindow : Gtk.Window {
    Game game = new Game();
    HashSet<Key> keys = new HashSet<Key>();

    public MyWindow() : base("game") {
        Resize(800, 800);
        Add(new View(game));
        Timeout.Add(30, on_timeout);    // 1 tick per 30 ms
    }

    bool on_timeout() {
        game.tick(keys.Contains(Key.Left), keys.Contains(Key.Right));
        QueueDraw();
        return true;
    }

    protected override bool OnKeyPressEvent(EventKey e) {
        if (e.Key == Key.space)
            game.jump();
        else
            keys.Add(e.Key);

        return true;
    }
    
    protected override bool OnKeyReleaseEvent(EventKey e) {
        keys.Remove(e.Key);
        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();
    }
}

Here is a screenshot of the player jumping:

Containers

using Gdk;
using Gtk;
using static Gtk.Orientation;

class MyWindow : Gtk.Window {
    public MyWindow() : base("containers") {
        Box top_hbox = new Box(Horizontal, 5);
        top_hbox.Add(new Label("one"));
        top_hbox.Add(new Button("button 1"));
        top_hbox.Add(new Label("two"));
        top_hbox.Add(new Button("button 2"));

        Box left_vbox = new Box(Vertical, 5);
        left_vbox.Add(new Label("three"));
        left_vbox.Add(new Button("button 3"));
        left_vbox.Add(new Label("four"));
        left_vbox.PackStart(new Button("button 4"), true, true, 0);

        Grid grid = new Grid();
        grid.ColumnSpacing = 5;
        grid.RowSpacing = 3;
        grid.Attach(new Label("field 1:"), 0, 0, 1, 1);
        grid.Attach(new Entry(), 1, 0, 1, 1);
        grid.Attach(new Label("field 2:"), 0, 1, 1, 1);
        grid.Attach(new Entry(), 1, 1, 1, 1);
        grid.Attach(new Image("cube.png"), 0, 2, 2, 1);

        Box hbox2 = new Box(Horizontal, 5);
        hbox2.Add(left_vbox);
        hbox2.Add(new Separator(Vertical));
        hbox2.Add(grid);

        Box vbox = new Box(Vertical, 5);
        vbox.Add(top_hbox);
        vbox.Add(new Separator(Horizontal));
        vbox.Add(hbox2);
        vbox.Margin = 8;
        Add(vbox);
    }

    protected override bool OnDeleteEvent(Event e) {
        Application.Quit();
        return true;
    }
}

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

Here is a screenshot of the running program:

Buttons, labels, and text boxes

using Gdk;
using Gtk;
using static Gtk.Orientation;

class MyWindow : Gtk.Window {
    Entry text_box = new Entry();
    Button generate_button = new Button("Generate");
    CheckButton uppercase = new CheckButton("Uppercase");
    Button exit_button = new Button("Exit");

    public MyWindow() : base("password selector") {
        Box row = new Box(Horizontal, 0);
        row.Add(new Label("password: "));
        row.Add(text_box);

        Box row2 = new Box(Horizontal, 3);
        row2.Add(generate_button);
        generate_button.Clicked += on_generate;
        row2.Add(uppercase);

        Box row3 = new Box(Horizontal, 0);
        row3.Add(exit_button);
        exit_button.Clicked += on_exit;

        Box vbox = new Box(Vertical, 5);
        vbox.Add(row);
        vbox.Add(row2);
        vbox.Add(row3);
        Add(vbox);
        vbox.Margin = 5;
    }

    void on_generate(object? sender, EventArgs args) {
        Random rand = new Random();
        string consonants = "bcdfghjklmnoqrstuwxyz";
        string vowels = "aeiou";
        string password = "";
        for (int i = 0 ; i < 5 ; ++i) {
            password += consonants[rand.Next(consonants.Length)];
            password += vowels[rand.Next(vowels.Length)];
        }
        if (uppercase.Active)
            password = password.ToUpper();
        text_box.Text = password;
    }

    void on_exit(object? sender, EventArgs args) {
        using (MessageDialog d = new MessageDialog(this,
            DialogFlags.Modal, MessageType.Warning, ButtonsType.Ok,
            "the password is '{0}'", text_box.Text)) {
            d.Run();
            Application.Quit();
        }
    }

    protected override bool OnDeleteEvent(Event e) {
        Application.Quit();
        return true;
    }
}

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

Here is a screenshot of the running program:

Radio buttons

using Cairo;
using Gdk;
using Gtk;
using Color = Cairo.Color;

class Area : DrawingArea {
    public Color color = new Color(1, 0, 0);

    public Area() {
        SetSizeRequest(0, 100);
    }

    public void setColor(Color c) {
        color = c;
        QueueDraw();
    }

    protected override bool OnDrawn(Context c) {
        c.SetSourceColor(color);
        c.Paint();
        return true;
    }
}

class MyWindow : Gtk.Window {
    Area area = new Area();

    public MyWindow() : base("colors") {
        Box hbox = new Box(Gtk.Orientation.Horizontal, 5);
        RadioButton r = new RadioButton("red");
        RadioButton g = new RadioButton(r, "green");
        RadioButton b = new RadioButton(r, "blue");
        r.Clicked += on_red_clicked;
        g.Clicked += on_green_clicked;
        b.Clicked += on_blue_clicked;
        hbox.Add(r);
        hbox.Add(g);
        hbox.Add(b);
        hbox.Margin = 5;

        Box vbox = new Box(Gtk.Orientation.Vertical, 5);
        vbox.Add(hbox);
        vbox.Add(area);

        Add(vbox);
    }

    void on_red_clicked(object? sender, EventArgs e) {
        area.setColor(new Color(1, 0, 0));
    }

    void on_green_clicked(object? sender, EventArgs e) {
        area.setColor(new Color(0, 1, 0));
    }

    void on_blue_clicked(object? sender, EventArgs e) {
        area.setColor(new Color(0, 0, 1));
    }

    protected override bool OnDeleteEvent(Event e) {
        Application.Quit();
        return true;
    }
}

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

Here is a screenshot of the running program:

Combo boxes

using Gdk;
using Gtk;

class MyWindow : Gtk.Window {
    static (string, string)[] capitals = {
        ("Austria", "Vienna"),
        ("the Czech Republic", "Prague"),
        ("Germany", "Berlin"),
        ("Hungary", "Budapest"),
        ("Slovakia", "Bratislava"),
    };

    ComboBoxText country_box = new ComboBoxText();
    ComboBoxText capital_box = new ComboBoxText();

    public MyWindow() : base("capitals") {
        foreach ((string country, string capital) in capitals) {
            country_box.AppendText(country);
            capital_box.AppendText(capital);
        }

        Box hbox = new Box(Orientation.Horizontal, 5);
        hbox.Add(new Label("The capital of"));
        hbox.Add(country_box);
        hbox.Add(new Label("is"));
        hbox.Add(capital_box);
        hbox.Margin = 8;
        Add(hbox);

        country_box.Changed += on_country_changed;
        capital_box.Changed += on_capital_changed;
    }

    void on_country_changed(object? sender, EventArgs args) {
        capital_box.Active = country_box.Active;
    }

    void on_capital_changed(object? sender, EventArgs args) {
        country_box.Active = capital_box.Active;
    }

    protected override bool OnDeleteEvent(Event e) {
        Application.Quit();
        return true;
    }
}

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

Here's a screenshot of the running program:

Spin buttons and sliders

using Gdk;
using Gtk;
using static Gtk.Orientation;

class MyWindow : Gtk.Window {
    string[] nums = { "",
        "one", "two", "three", "four", "five",
        "six", "seven", "eight", "nine", "ten" };

    Scale horiz = new Scale(Horizontal, 1, 10, 1);
    Scale vert = new Scale(Vertical, 1, 10, 1);
    SpinButton spinner = new SpinButton(1, 10, 1);
    Label label = new Label("one");

    public MyWindow() : base("sliders") {
        Box row = new Box(Horizontal, 0);
        row.PackStart(horiz, true, true, 0);
        horiz.ValueChanged += on_horiz_changed;
        row.Add(vert);
        vert.ValueChanged += on_vert_changed;

        Box row2 = new Box(Horizontal, 8);
        row2.Add(spinner);
        spinner.ValueChanged += on_spinner_changed;
        row2.Add(label);

        Box vbox = new Box(Vertical, 0);
        vbox.PackStart(row, true, true, 0);
        vbox.Add(row2);
        Add(vbox);
        vbox.Margin = 8;
    }

    void update(double val) {
        horiz.Value = vert.Value = spinner.Value = val;
        label.Text = nums[(int) val];
    }

    void on_horiz_changed(object? sender, EventArgs args) {
        update(horiz.Value);
    }

    void on_vert_changed(object? sender, EventArgs args) {
        update(vert.Value);
    }

    void on_spinner_changed(object? sender, EventArgs arg) {
        update(spinner.Value);
    }

    protected override bool OnDeleteEvent(Event e) {
        Application.Quit();
        return true;
    }
}

class Hello {
    static void Main() {
        Application.Init();
        MyWindow w = new MyWindow();
        w.Resize(150, 150);
        w.ShowAll();
        Application.Run();
    }
}

Here is a screenshot of the running program:

Dialogs

using Gdk;
using Gtk;
using Window = Gtk.Window;
using static Gtk.Orientation;

class City {
    public string name;
    public int population;
    public bool is_capital;

    public City(string name, int population, bool is_capital) {
        this.name = name; this.population = population; this.is_capital = is_capital;
    }
}

class CityDialog : Dialog {
    public Entry name = new Entry();
    public SpinButton population = new SpinButton(0, 5_000_000, 100_000);
    public CheckButton capital = new CheckButton("Capital city");

    public CityDialog(Window parent, City city)
            : base("Edit", parent, DialogFlags.Modal,
                   "OK", ResponseType.Ok, "Cancel", ResponseType.Cancel) {
        name.Text = city.name;
        population.Value = city.population;
        capital.Active = city.is_capital;

        Grid grid = new Grid();

        Label name_label = new Label("Name");
        name_label.Halign = Align.End;
        grid.Attach(name_label, 0, 0, 1, 1);
        grid.Attach(name, 1, 0, 1, 1);

        Label pop_label = new Label("Population");
        pop_label.Halign = Align.End;
        grid.Attach(pop_label, 0, 1, 1, 1);
        grid.Attach(population, 1, 1, 1, 1);

        grid.Attach(capital, 1, 2, 1, 1);

        grid.ColumnSpacing = 10;
        grid.RowSpacing = 5;
        grid.Margin = 5;
        ContentArea.Add(grid);
        ShowAll();
    }
}

class MyWindow : Window {
    City city;
    Label name = new Label(), population = new Label(), is_capital = new Label();

    public MyWindow() : base("hello, world") {
        city = new City("Paris", 2_100_000, true);
        Grid grid = new Grid();
        grid.Attach(new Label("name"), 0, 0, 1, 1);
        grid.Attach(name, 1, 0, 1, 1);
        grid.Attach(new Label("population"), 0, 1, 1, 1);
        grid.Attach(population, 1, 1, 1, 1);
        grid.Attach(new Label("capital"), 0, 2, 1, 1);
        grid.Attach(is_capital, 1, 2, 1, 1);
        grid.ColumnSpacing = 10;
        grid.RowSpacing = 5;
        foreach (Widget w in grid)
            w.Halign = Align.Start;

        Box row = new Box(Horizontal, 5);
        Button edit = new Button("Edit");
        row.Add(edit);
        edit.Clicked += on_edit;
        Button quit = new Button("Quit");
        row.Add(quit);
        quit.Clicked += on_quit;

        Box vbox = new Box(Vertical, 5);
        vbox.Add(grid);
        vbox.Add(row);
        Add(vbox);
        vbox.Margin = 5;

        update_labels();
    }

    void update_labels() {
        name.Text = city.name;
        population.Text = city.population.ToString();
        is_capital.Text = city.is_capital ? "yes" : "no";
    }

    void on_edit(object? sender, EventArgs args) {
        using (CityDialog d = new CityDialog(this, city))
            if (d.Run() == (int) ResponseType.Ok) {
                city.name = d.name.Text;
                city.population = (int) d.population.Value;
                city.is_capital = d.capital.Active;
                update_labels();
            }
    }

    void on_quit(object? sender, EventArgs args) {
        Application.Quit();
    }

    protected override bool OnDeleteEvent(Event e) {
        Application.Quit();
        return true;
    }
}

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

Here are screenshots of the main window and of the dialog that appears when you click the Edit button:

List and tree views

using Gdk;
using Gtk;
using Key = Gdk.Key;

class Movie {
    public int year;
    public string title;
    public string director;

    public Movie(int year, string title, string director) {
        this.year = year; this.title = title; this.director = director;
    }
}

class MyWindow : Gtk.Window {
    List<Movie> movies = new List<Movie>();

    ListStore store = new ListStore(typeof(int), typeof(string), typeof(string));
    TreeView tree_view;

    public MyWindow() : base("movies") {
        read_movies();
        fill();

        tree_view = new TreeView(store);
        string[] fields = { "year", "title", "director" };
        for (int i = 0 ; i < 3 ; ++i) {
            TreeViewColumn c = new TreeViewColumn(fields[i], new CellRendererText(), "text", i);
            c.SortColumnId = i;     // make column sortable
            tree_view.AppendColumn(c);
        }
        
        ScrolledWindow scrolled = new ScrolledWindow();
        scrolled.Add(tree_view);
        Add(scrolled);

        Resize(600, 400);
    }

    void read_movies() {
        using (StreamReader sr = new StreamReader("movies"))
            while (sr.ReadLine() is string line) {
                string[] fields = line.Split(',').Select(s => s.Trim()).ToArray();
                movies.Add(new Movie(int.Parse(fields[0]), fields[1], fields[2]));
            }
    }

    void fill() {
        store.Clear();
        foreach (Movie m in movies) {
            TreeIter i = store.Append();
            store.SetValues(i, m.year, m.title, m.director);
        }
    }

    protected override bool OnKeyPressEvent(EventKey e) {
        if (e.Key == Key.Delete) {
            TreePath[] rows = tree_view.Selection.GetSelectedRows();
            if (rows.Length > 0) {
                int row_index = rows[0].Indices[0];
                movies.RemoveAt(row_index);
                fill();
            }
        }
        return true;
    }
    
    protected override bool OnDeleteEvent(Event e) {
        Application.Quit();
        return true;
    }
}

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

Here's a screenshot of the running program:

Menus

using Gdk;
using Gtk;

class MyWindow : Gtk.Window {
    TextView text_view = new TextView();
    MenuItem uppercase_item, lowercase_item;

    public MyWindow() : base("edit") {
        Menu fileMenu = new Menu();
        fileMenu.Append(item("Open...", on_open));
        fileMenu.Append(item("Exit", on_exit));

        Menu transformMenu = new Menu();
        uppercase_item = item("Uppercase", on_uppercase);
        transformMenu.Append(uppercase_item);
        lowercase_item = item("Lowercase", on_lowercase);
        transformMenu.Append(lowercase_item);
        uppercase_item.Sensitive = lowercase_item.Sensitive = false;

        Menu documentMenu = new Menu();
        documentMenu.Append(item("Information", on_information));

        MenuBar bar = new MenuBar();
        bar.Append(submenu("File", fileMenu));
        bar.Append(submenu("Transform", transformMenu, on_open_transform_menu));
        bar.Append(submenu("Document", documentMenu));

        Box vbox = new Box(Orientation.Vertical, 0);
        vbox.Add(bar);
        ScrolledWindow scrolled = new ScrolledWindow();
        scrolled.Add(text_view);
        vbox.PackStart(scrolled, true, true, 0);
        Add(vbox);

        text_view.Monospace = true;

        Resize(600, 400);
    }

    static MenuItem item(string name, EventHandler handler) {
        MenuItem i = new MenuItem(name);
        i.Activated += handler;
        return i;
    }

    static MenuItem submenu(string name, Menu menu, EventHandler? handler = null) {
        MenuItem i = new MenuItem(name);
        i.Submenu = menu;
        if (handler != null)
            i.Activated += handler;
        return i;
    }

    void on_open(object? sender, EventArgs args) {
        using (FileChooserDialog d = new FileChooserDialog(
                "Open...", this, FileChooserAction.Open,
                "Cancel", ResponseType.Cancel, "Open", ResponseType.Ok))
            if (d.Run() == (int) ResponseType.Ok)
                using (StreamReader sr = new StreamReader(d.Filename))
                    text_view.Buffer.Text = sr.ReadToEnd();
    }

    void on_exit(object? sender, EventArgs args) {
        Application.Quit();
    }

    void on_open_transform_menu(object? sender, EventArgs args) {
        uppercase_item.Sensitive = lowercase_item.Sensitive = text_view.Buffer.HasSelection;
    }

    void replace(Func<string, string> f) {
        TextBuffer buf = text_view.Buffer;

        if (buf.GetSelectionBounds(out TextIter start, out TextIter end)) {
            string text = buf.GetText(start, end, false);
            buf.Delete(ref start, ref end);
            buf.Insert(ref start, f(text));
        }
    }

    void on_uppercase(object? sender, EventArgs args) {
        replace(s => s.ToUpper());
    }

    void on_lowercase(object? sender, EventArgs args) {
        replace(s => s.ToLower());
    }

    void on_information(object? sender, EventArgs args) {
        string text = text_view.Buffer.Text;
        int lines = text.Split('\n').Length;
        int words = text.Split().Where(w => w != "").Count();
        using (MessageDialog d =
            new MessageDialog(this, DialogFlags.Modal, MessageType.Info,
                              ButtonsType.Ok, "{0} lines, {1} words, {2} characters",
                              lines, words, text.Length))
            d.Run();
    }

    protected override bool OnDeleteEvent(Event e) {
        Application.Quit();
        return true;
    }
}

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

Here's a screenshot of the running program:

Toolbars

using Cairo;
using Gdk;
using Gtk;

enum Tool { Square, Circle, Triangle };

class Area : DrawingArea {
    ImageSurface canvas = new ImageSurface(Format.Argb32, 400, 400);
    public Tool tool = Tool.Square;

    public Area() {
        AddEvents((int) EventMask.ButtonPressMask);
        SetSizeRequest(400, 400);
    }

    public void clear() {
        canvas = new ImageSurface(Format.Argb32, 400, 400);
        QueueDraw();
    }

    protected override bool OnButtonPressEvent(EventButton e) {
        using (Context c = new Context(canvas)) {
            c.SetSourceRGB(0, 0, 0);    // black
            c.LineWidth = 4;

            switch (tool) {
                case Tool.Square:
                    c.Rectangle(e.X - 50, e.Y - 50, 100, 100);
                    break;
                case Tool.Circle:
                    c.Arc(xc: e.X, yc: e.Y, radius: 50, angle1: 0, angle2: 2 * Math.PI);
                    break;
                case Tool.Triangle:
                    c.MoveTo(e.X, e.Y - 60);
                    c.LineTo(e.X + 60, e.Y + 30);
                    c.LineTo(e.X - 60, e.Y + 30);
                    c.ClosePath();
                    break;
            }

            c.StrokePreserve();
            Random rand = new Random();
            c.SetSourceRGB(rand.NextDouble(), rand.NextDouble(), rand.NextDouble());
            c.Fill();
        }

        QueueDraw();
        return true;
    }

    protected override bool OnDrawn(Context c) {
        c.SetSourceSurface(canvas, 0, 0);
        c.Paint();
        return true;
    }
}

class MyWindow : Gtk.Window {
    Toolbar toolbar = new Toolbar();
    Area area = new Area();
    bool toggling;

    public MyWindow() : base("shapes") {
        toolbar.Style = ToolbarStyle.Icons;

        ToggleToolButton square_button = toggle_button(square_icon(), Tool.Square, "Square");
        square_button.Active = true;
        toolbar.Add(square_button);
        toolbar.Add(toggle_button(circle_icon(), Tool.Circle, "Circle"));
        toolbar.Add(toggle_button(triangle_icon(), Tool.Triangle, "Triangle"));

        toolbar.Add(new SeparatorToolItem());
        ToolButton b = new ToolButton(new Image(Gtk.Stock.Clear, IconSize.SmallToolbar), "clear");
        b.Clicked += on_clear;
        b.TooltipText = "Clear";
        toolbar.Add(b);

        Box vbox = new Box(Orientation.Vertical, 0);
        vbox.Add(toolbar);
        vbox.PackStart(area, true, true, 0);
        Add(vbox);
    }

    Image icon(Action<Context> draw) {
        ImageSurface s = new ImageSurface(Format.Argb32, 16, 16);
        using (Context c = new Context(s)) {
            c.SetSourceRGB(0, 0, 0);    // black
            c.LineWidth = 4;
            draw(c);
            c.StrokePreserve();
            c.SetSourceRGB(1, 1, 1);    // white
            c.Fill();
        }
        return new Image(s);
    }

    Image square_icon() => icon(c => c.Rectangle(2, 2, 12, 12));

    Image circle_icon() => icon(c => c.Arc(xc: 8, yc: 8, radius: 6,
                                           angle1: 0, angle2: 2 * Math.PI));

    void draw_triangle_icon(Context c) {
        c.MoveTo(8, 4);
        c.LineTo(14, 13);
        c.LineTo(2, 13);
        c.ClosePath();
    }

    Image triangle_icon() => icon(draw_triangle_icon);

    void toggle(object? sender, Tool tool) {
        if (toggling)
            return;     // prevent recursive invocations

        toggling = true;
        area.tool = tool;
        
        foreach (ToolItem b in toolbar)
            if (b != sender && b is ToggleToolButton ttb)
                ttb.Active = false;  // will fire Clicked event
        toggling = false;
    }

    ToggleToolButton toggle_button(Image icon, Tool tool, string name) {
        ToggleToolButton b = new ToggleToolButton();
        b.IconWidget = icon;
        b.Clicked += (obj, args) => toggle(obj, tool);
        b.TooltipText = name;
        return b;
    }

    void on_clear(object? o, EventArgs args) {
        area.clear();
    }

    protected override bool OnDeleteEvent(Event e) {
        Application.Quit();
        return true;
    }
}

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

Here is a screenshot of the running program:

Accessing low-level pixel data

using System.Runtime.InteropServices;
using Cairo;
using Gdk;
using Gtk;
using static Gdk.EventMask;

class Area : DrawingArea {
    ImageSurface image;
    bool dragging = false;
    double start_x, start_y;    // start position of mouse drag
    double end_x, end_y;        // end position of drag

    public Area() {
        image = new ImageSurface(Format.Rgb24, 400, 400);
        using (Context c = new Context(image)) {
            c.SetSourceRGB(1, 1, 1);    // white
            c.Paint();
        }

        AddEvents((int) (ButtonPressMask | ButtonReleaseMask | PointerMotionMask));
    }

    void line(Context c) {
        c.SetSourceRGB(0, 0, 0);
        c.LineWidth = 3;
        c.MoveTo(start_x, start_y);
        c.LineTo(end_x, end_y);
        c.Stroke();
    }

    (int, int)[] dirs = { (-1, 0), (1, 0), (0, -1), (0, 1) };

    void flood(int start_x, int start_y) {
        image.Flush();

        byte[] b = new byte[image.Stride * image.Height];
        Marshal.Copy(image.DataPtr, b, 0, b.Length);

        var stack = new Stack<(int, int)>();

        // choose a random color
        Random rand = new Random();
        byte[] color = new byte[3];
        rand.NextBytes(color);

        void visit(int x, int y) {
            int i = y * image.Stride + 4 * x;
            if (b[i] == 255 && b[i + 1] == 255 && b[i + 2] == 255) {
                for (int j = 0 ; j < 3 ; ++j)
                    b[i + j] = color[j];
                stack.Push((x, y));
            }
        }

        visit(start_x, start_y);

        while (stack.TryPop(out (int x, int y) p)) {
            foreach ((int dx, int dy) in dirs) {
                int x1 = p.x + dx, y1 = p.y + dy;
                if (0 <= x1 && x1 < image.Width && 0 <= y1 && y1 < image.Height)
                    visit(x1, y1);
            }
        }

        Marshal.Copy(b, 0, image.DataPtr, b.Length);

        image.MarkDirty();
    }

    protected override bool OnDrawn (Context c) {
        c.SetSourceSurface(image, 0, 0);
        c.Paint();
        
        if (dragging)
            line(c);
        return true;
    }

    protected override bool OnButtonPressEvent (EventButton e) {
        if (e.Button == 1) {
            dragging = true;
            (start_x, start_y) = (end_x, end_y) = (e.X, e.Y);
        } else if (e.Button == 3)
            flood((int) e.X, (int) e.Y);
        QueueDraw();
        return true;
    }

    protected override bool OnMotionNotifyEvent(EventMotion e) {
        if (dragging) {
            (end_x, end_y) = (e.X, e.Y);
            QueueDraw();
        }
        return true;
    }

    protected override bool OnButtonReleaseEvent (EventButton e) {
        if (e.Button == 1) {
            dragging = false;
            using (Context c = new Context(image))
                line(c);
            QueueDraw();
        }
        return true;
    }
}

class MyWindow : Gtk.Window {
    Area area;

    public MyWindow() : base("draw") {
        Resize(400, 400);
        area = new Area();
        Add(area);
    }

    protected override bool OnDeleteEvent(Event e) {
        Application.Quit();
        return true;
    }
}

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

Here is a screenshot of the running program: