Note: this page is a work in progress. I'll try to add more examples when I have time.
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 Gir.Core that lets us call GTK 4 from C#, and can be used on any platform (Linux, macOS, Windows).
First create a console application:
$ dotnet new console
Now add a reference to the GtkSharp package:
$ dotnet add package GirCore.Gtk-4.0
Now you can write GTK code in your program. Here is a "hello, world" program:
using Gtk; class MyWindow : ApplicationWindow { public MyWindow(Application app) { Application = app; Title = "hello, world"; SetDefaultSize(300, 200); } } class Hello : Application { public Hello() : base([]) { OnActivate += on_activate; } void on_activate(Gio.Application app, EventArgs args) { MyWindow w = new((Application) app); w.Show(); } static void Main(string[] args) { new Hello().Run(args.Length, args); } }
The program displays a blank window with title "hello, world".
The program includes a class MyWindow
that is a subclass of ApplicationWindow
, and a class
Hello
that is a subclass of Application
.
The Main()
method creates an instance
of Hello
, and runs it.
In the Hello()
constructor we
register an event handler for the Activate
event, which
will start the application.
The on_activate()
handler creates an
instance of MyWindow
, then calls its Show()
method to make it visible.
Cairo is a library for drawing 2-dimensional graphics. GTK integrates closely with Cairo, and GTK applications commonly 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 Gtk; class MyWindow : ApplicationWindow { public MyWindow(Application app) { Application = app; Title = "hello, cairo"; SetDefaultSize(400, 400); DrawingArea area = new(); area.SetDrawFunc(draw); Child = area; } void draw(DrawingArea area, Context c, int width, int height) { // draw a triangle c.SetSourceRgb(0, 0, 0); // 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.SetSourceRgb(0.56, 0.93, 0.56); // light green c.Fill(); // fill the inside // draw a circle c.SetSourceRgb(0, 0, 1); // blue c.Arc(xc: 300, yc: 100, radius: 50, angle1: 0.0, angle2: 2 * Math.PI); c.Fill(); // draw a rectangle c.SetSourceRgb(0, 0, 0); // 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); c.TextExtents(s, out TextExtents te); c.MoveTo(cx - (te.Width / 2 + te.XBearing), cy - (te.Height / 2 + te.YBearing)); c.ShowText(s); } } class Hello : Application { public Hello() : base([]) { OnActivate += on_activate; } void on_activate(Gio.Application app, EventArgs args) { MyWindow w = new((Application) app); w.Show(); } static void Main(string[] args) { new Hello().Run(args.Length, args); } }
The output looks like this:
The function GLib.Functions.TimeoutAdd()
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 window class
MyWindow
handles keyboard input, and has a timeout
handler that updates the model and redraws the view.
using Cairo; using static Gdk.Constants; using static Gdk.Functions; using static GLib.Functions; using Gtk; using Pixbuf = GdkPixbuf.Pixbuf; class Game { public int player_x = 200, player_y = 400; 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 >= 400) { // hit the ground dy = 0; // stop falling player_y = 400; } else dy += 2; // accelerate downward } public void jump() { if (player_y == 400 && dy == 0) dy = -20; } } class MyWindow : ApplicationWindow { Game game = new(); DrawingArea area; Pixbuf player = Pixbuf.NewFromFile("player.png")!; HashSet<uint> keys = []; public MyWindow(Application app) { Application = app; Title = "game"; SetDefaultSize(600, 600); area = new(); area.SetDrawFunc(draw); Child = area; EventControllerKey key_controller = new(); key_controller.OnKeyPressed += on_key_pressed; key_controller.OnKeyReleased += on_key_released; AddController(key_controller); TimeoutAdd(0, 30, on_timeout); // 1 tick per 30 ms } void draw(DrawingArea area, Context c, int width, int height) { c.SetSourceRgb(0, 0, 0); // black c.MoveTo(0, 400); c.LineTo(600, 400); c.Stroke(); CairoSetSourcePixbuf(c, player, game.player_x, game.player_y - player.Height); c.Paint(); } bool on_key_pressed(EventControllerKey sender, EventControllerKey.KeyPressedSignalArgs args) { if (args.Keyval == KEY_space) game.jump(); else keys.Add(args.Keyval); return true; } void on_key_released(EventControllerKey sender, EventControllerKey.KeyReleasedSignalArgs args) { keys.Remove(args.Keyval); } bool on_timeout() { game.tick(keys.Contains(KEY_Left), keys.Contains(KEY_Right)); area.QueueDraw(); return true; } } class Hello : Application { public Hello() : base([]) { OnActivate += on_activate; } void on_activate(Gio.Application app, EventArgs args) { MyWindow w = new((Application) app); w.Show(); } static void Main(string[] args) { new Hello().Run(args.Length, args); } }
Here is a screenshot of the player jumping:
using Gtk; using static Gtk.Orientation; class MyWindow : ApplicationWindow { public MyWindow(Application app) { Application = app; Title = "containers"; Box top_hbox = Box.New(Horizontal, 5); top_hbox.Append(Label.New("one")); top_hbox.Append(Button.NewWithLabel("button 1")); top_hbox.Append(Label.New("two")); top_hbox.Append(Button.NewWithLabel("button 2")); Box left_vbox = Box.New(Vertical, 5); left_vbox.Append(Label.New("three")); left_vbox.Append(Button.NewWithLabel("button 3")); left_vbox.Append(Label.New("four")); Button button4 = Button.NewWithLabel("button 4"); button4.Vexpand = true; left_vbox.Append(button4); Grid grid = new(); grid.ColumnSpacing = 5; grid.RowSpacing = 3; grid.Attach(Label.New("field 1:"), 0, 0, 1, 1); grid.Attach(new Entry(), 1, 0, 1, 1); grid.Attach(Label.New("field 2:"), 0, 1, 1, 1); grid.Attach(new Entry(), 1, 1, 1, 1); Picture cube = Picture.NewForFilename("cube.png"); cube.Halign = cube.Valign = Align.Center; grid.Attach(cube, 0, 2, 2, 1); Box hbox2 = Box.New(Horizontal, 5); hbox2.Append(left_vbox); hbox2.Append(Separator.New(Vertical)); hbox2.Append(grid); Box vbox = Box.New(Vertical, 5); vbox.Append(top_hbox); vbox.Append(Separator.New(Horizontal)); vbox.Append(hbox2); vbox.MarginTop = vbox.MarginBottom = vbox.MarginStart = vbox.MarginEnd = 8; Child = vbox; } } class Hello : Application { public Hello() : base([]) { OnActivate += on_activate; } void on_activate(Gio.Application app, EventArgs args) { MyWindow w = new((Application) app); w.Show(); } static void Main(string[] args) { new Hello().Run(args.Length, args); } }
Here is a snapshot of the running program:
using Gdk; using Gtk; using Menu = Gio.Menu; using SimpleAction = Gio.SimpleAction; using AGtk; class MyWindow : ApplicationWindow { TextView text_view = new(); List<SimpleAction> transformActions = []; public MyWindow(Application app) { Application = app; Title = "edit"; SetDefaultSize(600, 400); Menu topMenu = create_menu(); PopoverMenuBar bar = PopoverMenuBar.NewFromModel(topMenu); create_actions(); Box vbox = Box.New(Orientation.Vertical, 0); vbox.Append(bar); text_view.Monospace = true; text_view.Buffer!.OnChanged += on_text_changed; ScrolledWindow scrolled = new(); scrolled.Child = text_view; scrolled.Vexpand = true; vbox.Append(scrolled); Child = vbox; } Menu create_menu() { Menu fileMenu = new(); fileMenu.Append("New", "win.new"); fileMenu.Append("Open...", "win.open"); // Append a section with no label, which will draw a separator before the section. Menu exitSection = new(); exitSection.Append("Exit", "app.exit"); fileMenu.AppendSection(null, exitSection); Menu viewMenu = new(); viewMenu.Append("Monospace", "win.monospace"); Menu transformMenu = new(); Menu caseSection = new(); caseSection.Append("Uppercase", "win.uppercase"); caseSection.Append("Lowercase", "win.lowercase"); Menu orderingSection = new(); orderingSection.Append("Reverse Words", "win.reverse_words"); orderingSection.Append("Reverse Characters", "win.reverse_chars"); transformMenu.AppendSection("Case", caseSection); transformMenu.AppendSection("Ordering", orderingSection); Menu documentMenu = new(); documentMenu.Append("Information", "win.information"); Menu aboutMenu = new(); aboutMenu.Append("About", "win.about"); Menu topMenu = new(); topMenu.AppendSubmenu("File", fileMenu); topMenu.AppendSubmenu("View", viewMenu); topMenu.AppendSubmenu("Transform", transformMenu); topMenu.AppendSubmenu("Document", documentMenu); topMenu.AppendSubmenu("Help", aboutMenu); return topMenu; } void create_actions() { this.AddAction("new", on_new); this.AddAction("open", on_open); transformActions = [ this.AddAction("uppercase", on_uppercase), this.AddAction("lowercase", on_lowercase), this.AddAction("reverse_words", on_reverse_words), this.AddAction("reverse_chars", on_reverse_chars) ]; foreach (SimpleAction a in transformActions) a.Enabled = false; this.AddToggleAction("monospace", true, on_monospace); this.AddAction("information", on_information); this.AddAction("about", on_about); } async void on_new() { TextBuffer buf = text_view.Buffer!; if (buf.Text != "") { AlertDialog d = new(); d.Message = "Really clear the buffer?"; d.Buttons = ["Clear", "Cancel"]; d.DefaultButton = 0; // index of button chosen if user presses Enter d.CancelButton = 1; // index of button chosen if user presses Escape int result = await d.ChooseAsync(this); if (result == 0) // user chose "Clear" buf.Text = ""; } } async void on_open() { FileDialog dialog = new(); Gio.File? file = null; try { file = await dialog.OpenAsync(this); } catch (GLib.GException) { // user cancelled dialog } if (file != null) using (StreamReader sr = new(file.GetPath()!)) text_view.Buffer!.Text = sr.ReadToEnd(); } void on_monospace(bool val) { text_view.Monospace = val; } void transform(Func<string, string> f) { TextBuffer buf = text_view.Buffer!; buf.Text = f(buf.Text!); } void on_uppercase() { transform(text => text.ToUpper()); } void on_lowercase() { transform(text => text.ToLower()); } void on_reverse_words() { transform(text => string.Join(" ", text.Split().Reverse())); } void on_reverse_chars() { transform(text => string.Join("", text.ToCharArray().Reverse())); } void on_information() { string text = text_view.Buffer!.Text!; int lines = text.Split('\n').Length; int words = text.Split().Where(w => w != "").Count(); AlertDialog d = new(); d.Message = $"{lines} lines, {words} words, {text.Length} characters"; d.Show(this); } void on_about() { AboutDialog d = new(); d.TransientFor = this; d.Authors = ["Adam Dingle"]; d.ProgramName = "Menu Demo"; d.Version = "0.1.0"; d.Comments = "Program to demonstrate GTK menus"; d.Copyright = "© 2025 Adam Dingle"; if (Resources.GetTexture("logo.png") is Texture t) d.Logo = t; d.Show(); } void on_text_changed(TextBuffer sender, EventArgs args) { // Enable the Transform menu items if the buffer is non-empty. bool enable = text_view.Buffer!.Text! != ""; foreach (SimpleAction a in transformActions) a.Enabled = enable; } } class Hello : Application { public Hello() : base([]) { OnActivate += on_activate; } void on_activate(Gio.Application app, EventArgs args) { this.AddAction("exit", Quit); MyWindow w = new((Application) app); w.Show(); } static void Main(string[] args) { new Hello().Run(args.Length, args); } }
The code above adds actions using AddAction()
and
AddToggleAction()
, which are convenient extension
methods provided by the AGtk package. To use these methods, add the
AGtk package to
your project:
$ dotnet add package AGtk
You will also need to write "using AGtk;
" at
the top of your program.
The code for the About dialog calls
Resources.GetTexture()
to retrieve an image from an
embedded resource in the program's assembly. This is a helper method
found in the AGtk package. To embed an image (such as logo.png)
as an resource, include text such as this in your .csproj file:
<ItemGroup> <EmbeddedResource Include="logo.png" /> </ItemGroup>
Here is a screenshot of the running program:
using Gtk; using static Gtk.Orientation; using Menu = Gio.Menu; using AGtk; class MyWindow : ApplicationWindow { AGtk.ColView col_view; SearchEntry entry; public MyWindow(Application app) { Application = app; Title = "movies"; col_view = new AGtk.ColView(["year", "title", "director"]); col_view.FilterColumn = 1; add_movies(); col_view.OnSelectionChanged += on_selection_changed; col_view.OnActivate += on_activate; col_view.OnRightClick += on_right_click; entry = new SearchEntry(); entry.OnSearchChanged += on_search_changed; ScrolledWindow scrolled = ScrolledWindow.New(); scrolled.Child = col_view; scrolled.Vexpand = true; Box vbox = Box.New(Orientation.Vertical, 0); vbox.Append(scrolled); vbox.Append(entry); Child = vbox; SetDefaultSize(500, 400); this.AddAction("edit", on_edit); this.AddAction("delete", on_delete); } void on_selection_changed(uint rowIndex) { int year = (int) col_view.Rows[rowIndex].Values[0]; string title = (string) col_view.Rows[rowIndex].Values[1]; Console.WriteLine($"selection changed: {year} - {title}"); } void on_activate(ColumnView sender, ColumnView.ActivateSignalArgs args) { uint rowIndex = args.Position; int year = (int) col_view.Rows[rowIndex].Values[0]; string title = (string) col_view.Rows[rowIndex].Values[1]; Console.WriteLine($"double click: {year} - {title}"); } private void on_right_click(int x, int y, uint rowIndex) { col_view.SelectedIndex = rowIndex; Menu menu = new(); menu.Append("Edit", "win.edit"); menu.Append("Delete", "win.delete"); PopoverMenu pop = PopoverMenu.NewFromModel(menu); pop.SetParent(col_view); // Display the popup at the position where the user clicked the mouse. Gdk.Rectangle rect = new() { X = x, Y = y, Width = 1, Height = 1 }; pop.SetPointingTo(rect); pop.Popup(); } void on_edit() { EditWindow w = new(this, col_view.Rows[col_view.SelectedIndex]); w.Show(); } void on_delete() { col_view.DeleteRow(col_view.SelectedIndex); } void on_search_changed(SearchEntry sender, EventArgs args) { col_view.FilterText = entry.GetText(); } void add_movies() { col_view.Add([1981, "Raiders of the Lost Ark", "Steven Spielberg"]); col_view.Add([1994, "The Shawshank Redemption", "Frank Darabont"]); col_view.Add([1995, "Clueless", "Amy Heckerling"]); col_view.Add([2001, "Memento", "Christopher Nolan"]); col_view.Add([2002, "Spirited Away", "Hayao Miyazaki"]); col_view.Add([2004, "Eternal Sunshine of the Spotless Mind", "Michel Gondry"]); col_view.Add([2008, "The Dark Knight", "Christopher Nolan"]); col_view.Add([2014, "Whiplash", "Damien Chazelle"]); } } class EditWindow : Window { AGtk.Row row; Entry year = new(), title = new(), director = new(); public EditWindow(Window parent, AGtk.Row row) { Title = "edit"; TransientFor = parent; // Set this window to be modal: while it is active, the user cannot // interact with other windows. Modal = true; this.row = row; Grid grid = new(); grid.Attach(Label.New("year:"), 0, 0, 1, 1); grid.Attach(Label.New("title:"), 0, 1, 1, 1); grid.Attach(Label.New("director:"), 0, 2, 1, 1); year.Text_ = row.Values[0].ToString(); title.Text_ = (string) row.Values[1]; director.Text_ = (string) row.Values[2]; grid.Attach(year, 1, 0, 1, 1); grid.Attach(title, 1, 1, 1, 1); grid.Attach(director, 1, 2, 1, 1); Box hbox = Box.New(Horizontal, 1); Button ok = Button.NewWithLabel("OK"); ok.OnClicked += on_ok; Button cancel = Button.NewWithLabel("Cancel"); cancel.OnClicked += on_cancel; hbox.Append(ok); hbox.Append(cancel); Box vbox = Box.New(Vertical, 1); vbox.Append(grid); vbox.Append(hbox); Child = vbox; } void on_ok(Button sender, EventArgs args) { row.Values[0] = int.Parse(year.Text_!); row.Values[1] = title.Text_!; row.Values[2] = director.Text_!; Close(); } void on_cancel(Button sender, EventArgs args) { Close(); } } class Hello : Application { public Hello() : base([]) { OnActivate += on_activate; } void on_activate(Gio.Application app, EventArgs args) { MyWindow w = new((Application) app); w.Show(); } static void Main(string[] args) { new Hello().Run(args.Length, args); } }
This example displays a multi-column list view using the AGtk.ColView
class. The Gtk.ColumnView
class has a complicated API.
AGtk.ColView
is a subclass of Gtk.ColumnView
that is much easier to use.
To use AGtk.ColView
in your program,
add the AGtk package
to your project:
$ dotnet add package AGtk
Here is a snapshot of the running program: