$linuxjunkies
>

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.

AdvancedUbuntuDebianFedoraArch12 min readUpdated June 7, 2026

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 the G_DEFINE_TYPE macro). 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 like clicked and activate are 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, GtkWidget is both a GObject and a GInitiallyUnowned. Layouts are composed using GtkBox, GtkGrid, GtkOverlay, and similar containers. There is no explicit gtk_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 Xvfb or run tests via xvfb-run. On Wayland-only systems, set GDK_BACKEND=wayland or GDK_BACKEND=x11 explicitly.
  • 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 prints 3, you have GTK3 bindings active. Reinstall gir1.2-gtk-4.0 (Debian) or gtk4 with gobject-introspection (Fedora/Arch).
  • Deprecation warnings in GTK 4.10+: GTK 4.10 deprecated several widget classes (e.g., GtkFileChooserDialog). Set GTK_DEBUG=deprecations to surface them at runtime: GTK_DEBUG=deprecations ./hello.
  • Meson can't find gtk4: Confirm pkg-config --modversion gtk4 returns a version string. If it fails, the -dev / -devel package 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.
tested on:Ubuntu 24.04Fedora 40Arch rollingDebian 12

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