GTK GUI Programming on Linux
Build, run, and package GTK4 applications on Linux using C and Python. Covers the GObject model, signals, Meson builds, .desktop files, and Flatpak.
Before you start
- ▸Familiarity with C compilation (gcc, make/ninja) or Python 3.10+
- ▸A working desktop session (X11 or Wayland) for running the GUI
- ▸sudo or root access to install development packages
- ▸Basic understanding of event-driven programming concepts
GTK 4 is the current stable toolkit underpinning GNOME and a wide range of Linux desktop applications. It brings a retained-mode rendering pipeline (via GSK/Cairo), a strict ownership model for widgets, and first-class Wayland support. This guide walks through the GTK object model, builds a minimal but complete application in both C and Python, then shows how to package it for distribution.
The GTK Object Model
GTK is built on GObject, a C-level object system that provides single inheritance, interfaces, signals, and properties without requiring C++. Every widget is ultimately a GObject subclass. Understanding a few key concepts makes the API far less mysterious.
- Type system: Types are registered at runtime via
g_type_register_static()(or theG_DEFINE_TYPEmacro). Each type has a class struct (shared across instances) and an instance struct. - Reference counting: GObjects are ref-counted. GTK 4 tightened ownership rules — when you add a widget to a parent, the parent takes ownership. You should not manually
g_object_unref()a widget after adding it. - Signals: A publish-subscribe mechanism built into GObject. You connect a callback with
g_signal_connect(). Signals likeclickedandactivateare how user events reach your code. - Properties: Named, typed attributes on any GObject. Readable and writable via
g_object_get()/g_object_set(), or through language bindings. - Widget hierarchy: In GTK 4,
GtkWidgetis both aGObjectand aGInitiallyUnowned. Layouts are composed usingGtkBox,GtkGrid,GtkOverlay, and similar containers. There is no explicitgtk_container_add()anymore — each container has its own append/prepend API.
Installing the Development Dependencies
Debian / Ubuntu
sudo apt update
sudo apt install build-essential libgtk-4-dev pkg-config python3-gi gir1.2-gtk-4.0 meson ninja-build
Fedora / RHEL / Rocky
sudo dnf install gcc gtk4-devel pkg-config python3-gobject gtk4 meson ninja-build
Arch Linux
sudo pacman -S gtk4 python-gobject pkg-config meson ninja base-devel
Building a GTK4 App in C
The application below creates a window with a label and a button. Clicking the button updates the label text. It is intentionally minimal but covers activation, widget construction, signal connection, and clean shutdown.
Source: hello.c
cat > hello.c <<'EOF'
#include <gtk/gtk.h>
static void on_button_clicked(GtkButton *btn, GtkLabel *label) {
gtk_label_set_text(label, "Button clicked!");
}
static void activate(GtkApplication *app, gpointer user_data) {
GtkWidget *window = gtk_application_window_new(app);
gtk_window_set_title(GTK_WINDOW(window), "GTK4 Hello");
gtk_window_set_default_size(GTK_WINDOW(window), 320, 120);
GtkWidget *box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 12);
gtk_widget_set_margin_top(box, 16);
gtk_widget_set_margin_bottom(box, 16);
gtk_widget_set_margin_start(box, 16);
gtk_widget_set_margin_end(box, 16);
gtk_window_set_child(GTK_WINDOW(window), box);
GtkWidget *label = gtk_label_new("Hello, GTK4!");
gtk_box_append(GTK_BOX(box), label);
GtkWidget *button = gtk_button_new_with_label("Click me");
g_signal_connect(button, "clicked", G_CALLBACK(on_button_clicked), label);
gtk_box_append(GTK_BOX(box), button);
gtk_window_present(GTK_WINDOW(window));
}
int main(int argc, char **argv) {
GtkApplication *app = gtk_application_new("org.linuxjunkies.hello",
G_APPLICATION_DEFAULT_FLAGS);
g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
int status = g_application_run(G_APPLICATION(app), argc, argv);
g_object_unref(app);
return status;
}
EOF
Compile and Run
gcc hello.c -o hello $(pkg-config --cflags --libs gtk4)
./hello
pkg-config --cflags --libs gtk4 expands to the correct include paths and linker flags for the installed GTK version. On GTK 4.10+ you may see a deprecation warning about G_APPLICATION_FLAGS_NONE — use G_APPLICATION_DEFAULT_FLAGS as shown above.
Building a GTK4 App in Python
PyGObject provides introspection-based bindings. The Python API mirrors the C API closely; method names drop the type prefix (gtk_box_append becomes box.append()).
Source: hello.py
cat > hello.py <<'EOF'
import gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk
def on_button_clicked(btn, label):
label.set_text('Button clicked!')
def on_activate(app):
window = Gtk.ApplicationWindow(application=app, title='GTK4 Hello')
window.set_default_size(320, 120)
box = Gtk.Box(orientation=Gtk.Orientation.VERTICAL, spacing=12,
margin_top=16, margin_bottom=16,
margin_start=16, margin_end=16)
window.set_child(box)
label = Gtk.Label(label='Hello, GTK4!')
box.append(label)
button = Gtk.Button(label='Click me')
button.connect('clicked', on_button_clicked, label)
box.append(button)
window.present()
app = Gtk.Application(application_id='org.linuxjunkies.hello')
app.connect('activate', on_activate)
app.run(None)
EOF
python3 hello.py
Packaging with Meson and a .desktop File
Production GTK apps use Meson as the build system and ship a .desktop file so the application appears in the launcher. Here is a minimal project layout for the C app.
Project Layout
mkdir -p gtk4hello/src
mv hello.c gtk4hello/src/
cd gtk4hello
meson.build
cat > meson.build <<'EOF'
project('gtk4hello', 'c', version: '1.0.0', default_options: ['c_std=c11'])
gtk_dep = dependency('gtk4')
executable('gtk4hello',
'src/hello.c',
dependencies: gtk_dep,
install: true)
install_data('data/org.linuxjunkies.hello.desktop',
install_dir: get_option('datadir') / 'applications')
EOF
.desktop File
mkdir data
cat > data/org.linuxjunkies.hello.desktop <<'EOF'
[Desktop Entry]
Name=GTK4 Hello
Exec=gtk4hello
Icon=application-x-executable
Type=Application
Categories=Utility;
EOF
Build and Install
meson setup builddir
cd builddir
ninja
ninja install # installs to /usr/local by default; prefix with sudo if needed
To install to a local prefix instead of system directories, pass --prefix=$HOME/.local to meson setup. Ensure $HOME/.local/bin is in your PATH.
Flatpak: The Modern Distribution Format
For distributing to end users across distros, Flatpak is the standard approach for GTK apps. Install flatpak-builder and create a manifest:
sudo apt install flatpak-builder # Debian/Ubuntu
# or: sudo dnf install flatpak-builder
cat > org.linuxjunkies.hello.json <<'EOF'
{
"app-id": "org.linuxjunkies.hello",
"runtime": "org.gnome.Platform",
"runtime-version": "46",
"sdk": "org.gnome.Sdk",
"command": "gtk4hello",
"modules": [{
"name": "gtk4hello",
"buildsystem": "meson",
"sources": [{"type": "dir", "path": "."}]
}]
}
EOF
flatpak-builder --force-clean build-flatpak org.linuxjunkies.hello.json
Verification
Confirm the binary links correctly against GTK4 and that the .desktop entry is valid:
ldd ./hello | grep gtk
desktop-file-validate data/org.linuxjunkies.hello.desktop
The ldd output should show libgtk-4.so.1. desktop-file-validate exits silently on success.
Troubleshooting
- "Cannot open display" on a headless server: GTK4 requires a display server. Use
Xvfbor run tests viaxvfb-run. On Wayland-only systems, setGDK_BACKEND=waylandorGDK_BACKEND=x11explicitly. - PyGObject import error / wrong version: Run
python3 -c "import gi; gi.require_version('Gtk','4.0'); from gi.repository import Gtk; print(Gtk.MAJOR_VERSION)". If it prints3, you have GTK3 bindings active. Reinstallgir1.2-gtk-4.0(Debian) orgtk4with gobject-introspection (Fedora/Arch). - Deprecation warnings in GTK 4.10+: GTK 4.10 deprecated several widget classes (e.g.,
GtkFileChooserDialog). SetGTK_DEBUG=deprecationsto surface them at runtime:GTK_DEBUG=deprecations ./hello. - Meson can't find gtk4: Confirm
pkg-config --modversion gtk4returns a version string. If it fails, the-dev/-develpackage is missing. - Flatpak runtime not found: Add the Flathub remote first:
flatpak remote-add --if-not-exists flathub https://flathub.org/repo/flathub.flatpakrepo, then install the GNOME SDK:flatpak install org.gnome.Sdk//46.
Frequently asked questions
- What is the difference between GTK3 and GTK4 for application developers?
- GTK4 removes the generic gtk_container_add() API in favor of widget-specific methods like gtk_box_append(). It also introduces a new rendering pipeline (GSK), stricter widget ownership semantics, and improved Wayland and HiDPI support. Porting a GTK3 app requires updating container calls, replacing deprecated widgets, and adjusting event handling.
- Can I use GTK4 with languages other than C and Python?
- Yes. GObject introspection generates bindings for many languages. Actively maintained options include Rust (gtk4-rs crate), JavaScript (GJS, used by GNOME Shell extensions), Vala (compiles to C with GObject), and C++ (gtkmm). All use the same underlying GTK4 library.
- How do I design complex UIs without writing layout code by hand?
- Use GNOME Builder with its built-in UI editor, or the standalone Cambalache tool, to produce .ui XML files in the GtkBuilder format. Load them at runtime with gtk_builder_new_from_file() in C or Gtk.Builder.new_from_file() in Python, then fetch widgets by ID.
- Why does my app crash with 'Gtk-CRITICAL' assertions in GTK4?
- GTK4 promotes many previously soft warnings to critical assertions. Common causes are passing NULL where a widget is required, calling widget methods after the widget has been disposed, or forgetting to call gtk_window_set_child() before presenting. Run the app with G_DEBUG=fatal-criticals to get a backtrace at the first failure.
- Is GTK4 usable on Wayland without any extra configuration?
- Yes. GTK4's GDK backend auto-detects Wayland and uses it by default when WAYLAND_DISPLAY is set. You can force a backend with GDK_BACKEND=wayland or GDK_BACKEND=x11. Features like screen capture and global shortcuts may require additional Wayland portal integration via libportal.
Related guides
Bash Arrays and Associative Arrays
Master bash indexed and associative arrays: declaration, element access, looping, mapfile, namerefs, and practical patterns for real scripting work.
Bash Functions and Variable Scoping
Master Bash function scoping with local variables, source-based libraries, correct use of return codes, and array passing techniques including namerefs.
Bash Loops: for, while and until
Learn all three Bash loop types — for, while, and until — with practical, copy-paste examples covering file iteration, counting, polling, and safe line reading.
Bash Scripting for Beginners
Learn Bash scripting from scratch: shebang lines, variables, conditionals, loops, and arguments, plus a real backup script to tie it all together.