Introduction
Cross-platform GUI development remains a persistent challenge due to differences in operating system APIs and rendering models. GTK+, an open-source widget toolkit originally developed for the GIMP image editor, provides a unified abstraction layer for building native-looking applications. Paired with Glade, a WYSIWYG interface designer, developers can separate UI design from application logic, streamlining workflows across languages.
The application described here is a simple calculator built with GTK in conjunction with Glade. The UI is the same, the only difference is the selected binding (C++, Python and Rust). The source code can be found here. The operating system is Ubuntu 20.04 running on Hyper-V.
Glade Interface Designer
- Visual UI builder: Allows drag-and-drop design without manual layout code.
- XML-based output: Stores UI definitions in .glade files, which can be loaded at runtime.
- Separation of concerns: Designers focus on interface, developers on logic.
- Cross-language compatibility: Works seamlessly with bindings in C++, Python, and Rust.

Language Bindings
- C++
- gtkmm: Official C++ binding for GTK+.
- Provides type safety and modern C++ features.
- Commonly used in Linux desktop applications and cross-platform projects.
- Python
- PyGObject: Python binding for GTK+.
- Simplifies rapid prototyping and scripting.
- Strong integration with Glade, enabling fast iteration cycles.
- Rust
- gtk-rs: Rust bindings for GTK, Cairo, and GLib.
- Offers memory safety and concurrency advantages.
- Supports Glade-based UI definitions, enabling declarative design.
Comparative Analysis
- C++
- gtkmm: Official C++ binding for GTK+.
- Provides type safety and modern C++ features.
- Commonly used in Linux desktop applications and cross-platform projects.
- Python
- PyGObject: Python binding for GTK+.
- Simplifies rapid prototyping and scripting.
- Strong integration with Glade, enabling fast iteration cycles.
- Rust
- gtk-rs: Rust bindings for GTK, Cairo, and GLib.
- Offers memory safety and concurrency advantages.
- Supports Glade-based UI definitions, enabling declarative design.
| Language | Binding | Strengths | Limitations |
| C++ | gtkmm | Type satefy, mature ecosystem | Verbose syntax, slower iteration |
| Python | PyGObject | Rapid prototyping, ease of learning | Performance overhead |
| Rust | gtk-rs | Memory safety, modern language features | Ecosystem still maturing |
As evidenced by the image above (C++ code represents 44.9%, Rust 28.%, Python 13.1%. CMake is used by C++ to build the solution and it represents 13.5%). We’re compiling/using C++ 17.
Now, let’s dissect some of the code that perform the same operation. In this case, the method responsible to connect UI (buttons) to code.
C++
void MainWindowController::connect_buttons() {
const std::vector<std::string> digits = {
"btn_0", "btn_1", "btn_2", "btn_3", "btn_4",
"btn_5", "btn_6", "btn_7", "btn_8", "btn_9"
};
for (const auto& id : digits) {
Gtk::Button* btn;
builder->get_widget(id, btn);
if (btn) {
btn->signal_clicked().connect([this, btn]() {
on_digit_pressed(btn->get_label());
});
}
}
const std::vector<std::pair<std::string, std::string>> operators = {
{"btn_add", "+"}, {"btn_subtract", "-"},
{"btn_multiply", "*"}, {"btn_divide", "/"}
};
for (const auto& [id, symbol] : operators) {
Gtk::Button* btn;
builder->get_widget(id, btn);
if (btn) {
btn->signal_clicked().connect([this, symbol]() {
on_operator_pressed(symbol);
});
}
}
Gtk::Button* btn_clear;
builder->get_widget("btn_clear", btn_clear);
if (btn_clear) {
btn_clear->signal_clicked().connect(sigc::mem_fun(*this, &MainWindowController::on_clear_pressed));
}
Gtk::Button* btn_equals;
builder->get_widget("btn_equals", btn_equals);
if (btn_equals) {
btn_equals->signal_clicked().connect(sigc::mem_fun(*this, &MainWindowController::on_equals_pressed));
}
}
Rust
// Get the display entry
let entry: Entry = builder.object("entry_display").unwrap();
// Shared state for current expression
let expression = Rc::new(RefCell::new(String::new()));
// Helper to wire a button by id
let connect_button = |id: &str, expr: Rc<RefCell<String>>, entry: Entry| {
let entry_clone = entry.clone();
let button: Button = builder.object(id).unwrap();
let label = button.label().unwrap_or_else(|| "".into());
button.connect_clicked(move |_| {
let mut exp = expr.borrow_mut();
exp.push_str(&label);
entry_clone.set_text(&exp);
});
};
// Wire numeric buttons
for id in &["btn_0","btn_1","btn_2","btn_3","btn_4","btn_5","btn_6","btn_7","btn_8","btn_9"] {
connect_button(id, expression.clone(), entry.clone());
}
// Wire operator buttons
for id in &["btn_add","btn_subtract","btn_multiply","btn_divide"] {
connect_button(id, expression.clone(), entry.clone());
}
// Clear button
{
let button: Button = builder.object("btn_clear").unwrap();
let entry_clone = entry.clone();
let expr = expression.clone();
button.connect_clicked(move |_| {
expr.borrow_mut().clear();
entry_clone.set_text("");
});
}
// Equals button
{
let entry_clone = entry.clone();
let button: Button = builder.object("btn_equals").unwrap();
let expr = expression.clone();
button.connect_clicked(move |_| {
let exp = expr.borrow().clone();
let result = evaluate_expression(&exp);
entry_clone.set_text(&result);
expr.borrow_mut().clear();
});
}
Python
# Wire numeric buttons
for i in range(10):
btn = builder.get_object(f"btn_{i}")
btn.connect("clicked", self.on_button_clicked, entry)
# Wire operator buttons
for op in ["add", "subtract", "multiply", "divide"]:
btn = builder.get_object(f"btn_{op}")
btn.connect("clicked", self.on_button_clicked, entry)
# Clear button
builder.get_object("btn_clear").connect("clicked", self.on_clear_clicked, entry)
# Equals button
builder.get_object("btn_equals").connect("clicked", self.on_equals_clicked, entry)
def on_button_clicked(self, button, entry):
label = button.get_label()
self.expression += label
entry.set_text(self.expression)
def on_clear_clicked(self, button, entry):
self.expression = ""
entry.set_text("")
def on_equals_clicked(self, button, entry):
try:
expr = (self.expression
.replace("×", "*")
.replace("÷", "/")
.replace("−", "-"))
result = eval(expr)
entry.set_text(str(result))
self.expression = ""
except Exception:
entry.set_text("Error")
self.expression = ""
Case Study: Glade + Rust
A simple Rust application can load a .glade file and bind widgets to logic using gtk-rs. For example, a button click can update a label dynamically. This demonstrates how declarative UI design integrates with Rust’s safe concurrency model, offering both productivity and reliability.
Conclusion
GTK+ and Glade provide a robust foundation for cross-platform UI development, enabling developers to choose the language that best fits their project goals.
- C++ offers maturity and performance.
- Python excels in accessibility and rapid iteration.
- Rust introduces safety and modern paradigms.
Together, these bindings ensure GTK+ remains relevant in diverse application domains, from desktop productivity tools to experimental cross-platform projects.
References
GNOME Project. (n.d.). GTK: The GTK Project. Retrieved from https://www.gtk.org
GNOME Project. (n.d.). Glade Interface Designer. Retrieved from https://glade.gnome.org
gtkmm Development Team. (n.d.). gtkmm: C++ interface for GTK. Retrieved from https://www.gtkmm.org
PyGObject Documentation. (n.d.). Python GTK+ 3 Tutorial. Retrieved from
https://pygobject.readthedocs.io
gtk-rs Project. (n.d.). Rust bindings for GTK. Retrieved from https://gtk-rs.org


