DEV Community

Diego Crespo
Diego Crespo

Posted on

Experimenting with GUIs on the Pi Zero

tkinter gui for querying openai api

I recently brought an old Raspberry Pi Zero back to life. With the oldest files dating back to the summer of 2020, it was safe to say that it was time for a fresh install. Thankfully there is still a version of Raspberry Pi OS that supports 32 bit Raspberry Pi's like the Zero W

Raspberry Pi OS download screen

It comes with Bookworm which is a recent Debian release, which means we get nice goodies like Python 3.11, version 6+ of the Linux kernel (up from 4.19 in the old install), and newer versions of lots of other software. Not that I'll be using the RPI Zero much like a Desktop. A 32-bit single core CPU clocked at 1 Ghz, with 512Mb of RAM, is basically abandonware at this point as a Desktop platform. Even the default installer from the Raspberry Pi foundation installs software you can't use. It asks whether you want to install Chromium and or Firefox, but both programs say this when you try to use them

Chromium/Firefox is not supported on the Raspberry PI Zero W

Whether it's actually realistic to use the Raspberry Pi Zero as a Desktop is a boring question. Who cares! It's fun. But anyway, I'm more interested in using the Zero W to test the baseline performance of some apps that I'm writing anyways, so this isn't a problem. Let's start by looking at a couple of programs on the Pi Zero and see how they perform out of the box. The answer... Pretty slow, but not as bad as I was expecting. Opening up basic software like the terminal, and the file browser take anywhere from 3-6 seconds. That's definitely slow, but even on my beefy desktop with an SSD, most of the apps I use take at least 1-3 seconds to start up. That's not even an order of magnitude faster on a machine that is much more powerful. But complaining about software performance in 2024 is so passé so I'll just say it sucks. It did pique my interest in performing some highly unscientific testing though which is what you will get below.

Being nerdsniped by timing random apps opening, I decided to write some basic GUI apps that did nothing more than open an 800x600 window, and time them on my phone. Starting off with GTK, I modified the basic GTK4 tutorial example

#include <gtk/gtk.h>

static void
activate (GtkApplication* app,
          gpointer        user_data)
{
  GtkWidget *window;

  window = gtk_application_window_new (app);
  gtk_window_set_title (GTK_WINDOW (window), "Window");
  gtk_window_set_default_size (GTK_WINDOW (window), 800, 600);
  gtk_window_present (GTK_WINDOW (window));
}

int
main (int    argc,
      char **argv)
{
  GtkApplication *app;
  int status;

  app = gtk_application_new ("org.gtk.example", G_APPLICATION_DEFAULT_FLAGS);
  g_signal_connect (app, "activate", G_CALLBACK (activate), NULL);
  status = g_application_run (G_APPLICATION (app), argc, argv);
  g_object_unref (app);

  return status;
}
Enter fullscreen mode Exit fullscreen mode

Compiling the tutorial with the -O2 optimization set, I was astonished to see the app take between 12-14 seconds to open a blank window. Now GTK4 does a lot for us out of the box, and that comes with an upfront cost. But for a blank window that is pretty bad. I looked around for some examples of deploying apps on resource constrained systems with GTK, and even asked in the GNOME forums but couldn't find any solutions. If you know of any please let me know.

With that shocking result, I decided to get a baseline for how fast a window could open up. I did this by making as simple X application that opens a window, and then timing the result...

#include <X11/Xlib.h>
#include <stdio.h>
#include <stdlib.h> // For exit function

int main() {
    Display *display;
    Window window;
    XEvent event;
    int screen;

    // Open connection to the X server
    display = XOpenDisplay(NULL);
    if (display == NULL) {
        fprintf(stderr, "Cannot open display\n");
        exit(1);
    }

    screen = DefaultScreen(display);


    window = XCreateSimpleWindow(display, RootWindow(display, screen), 10, 10, 800, 600, 1,
                                 BlackPixel(display, screen), WhitePixel(display, screen));


    XSelectInput(display, window, ExposureMask | KeyPressMask);


    XMapWindow(display, window);


    while (1) {
        XNextEvent(display, &event);

        // Break out of the loop if the user presses any key
        if (event.type == KeyPress)
            break;
    }


    XCloseDisplay(display);
    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Except, I almost didn't have a chance to time the result, as the window opened up in ~0.5 seconds. I'll be the first to tell you this is not an Apples to Apples comparison, and writing a GUI using just X11 would be an exercise in suffering. (I mean but that's basically what Casey Muratori did in his Handmade Hero Series for Windows). But if all apps opened at that speed, my RPI Zero W wouldn't feel slow at all. Armed with this new knowledge, I decided to look at a couple of other ways of opening a window.

Since I've been on an SDL2 kick recently, it only made sense to do the same test with that library. You make recognize this code as essentially the example I wrote in SDL Tutorial Part 1: Opening A Window from a couple weeks ago

#include <SDL2/SDL.h>
#include <stdio.h>
#include <stdlib.h>

#define SCREEN_WIDTH 800
#define SCREEN_HEIGHT 600

int main(){
  if (SDL_Init(SDL_INIT_VIDEO) < 0){
    printf("Couldn't initialize SDL: %s\n", SDL_GetError());
    return EXIT_FAILURE;
  }

  SDL_Window *window = SDL_CreateWindow("Example: 0", SDL_WINDOWPOS_UNDEFINED,
                    SDL_WINDOWPOS_UNDEFINED, SCREEN_WIDTH, SCREEN_HEIGHT, 0);
  if (!window){
    printf("Failed to open %d x %d window: %s\n", SCREEN_WIDTH, SCREEN_HEIGHT, SDL_GetError());
    return EXIT_FAILURE;
  }

  SDL_Renderer *renderer = SDL_CreateRenderer(window, -1, 0);
  SDL_SetRenderDrawColor(renderer, 255, 255, 255, 250);
  SDL_RenderClear(renderer);
  SDL_RenderPresent(renderer);
  SDL_Delay(2000);
  SDL_DestroyWindow(window);
  SDL_DestroyRenderer(renderer);
  SDL_Quit();
  return EXIT_SUCCESS;
}
Enter fullscreen mode Exit fullscreen mode

And our results... A little over 3.1 seconds. While SDL2 is not a retained mode GUI, I could certainly do more with it than I could with just a raw X app. So, so long as people didn't need a screen reader for the program, this might be a viable option. (I'm not even sure you could run any sufficiently complex app, and a screen reader on the RPI Zero, at the same time anyway though).

I recently made a simple GUI app in Python with Tkinter, and since it's already built into Python I wrote my example in it next to see how it would perform

import tkinter as tk

app = tk.Tk()
app.title("Simple 800x600 Window")

app.geometry("800x600")

app.mainloop()
Enter fullscreen mode Exit fullscreen mode

~2.9 seconds. For an language that has to interpret the code upfront, that is not bad. I think that Tkinter is an underrated gem of the Python world, and I'll be writing more about that soon. But, while the Tk integration with Python is great, I wanted to look at two more implementations. One using the de facto Tcl bindings to Tk, and the other using just raw C and Tk

#!/usr/bin/env wish

wm title . "Simple Window"
wm geometry . 800x600

tkwait window .
Enter fullscreen mode Exit fullscreen mode

Tcl is an interesting language that warrants its own discussion at a later date, but on my RPI Zero I was able to open up the window in ~2.0 seconds which is a great improvement. Considering we are entering the territory of as fast, or faster than the default apps, I feel like we have reached acceptable speeds. But Python and Tcl are just binding the underlying C Tk library, so let's look at C next.

#include <tk.h>

int main(int argc, char **argv) {
    Tcl_Interp *interp;
    Tk_Window window;

    // Initialize Tcl and the Tk toolkit
    Tcl_FindExecutable(argv[0]);
    interp = Tcl_CreateInterp();

    if (Tcl_Init(interp) == TCL_ERROR) {
        return -1;
    }

    if (Tk_Init(interp) == TCL_ERROR) {
        return -1;
    }

    // Create a new top-level Tk window
    window = Tk_MainWindow(interp);

    // Set the title of the window using Tcl command
    Tcl_Eval(interp, "wm title . \"Simple Tk C App\"");

    // Set the size of the window
    Tk_GeometryRequest(window, 800, 600);

    // Start the Tk event loop
    Tk_MainLoop();

    return 0;
}
Enter fullscreen mode Exit fullscreen mode

Honestly maybe a tenth of a second faster than Tcl example but much less ergonomic. I did compile everything with -O2 when I could, but I think that is just a testament to how well Tcl and Tk are integrated.

Next I looked at the Fast Light Toolkit (FLTK), a cross platform GUI library I used many years ago when I first attempted to learn C++. Fun fact this is also the GUI library used to create the Eureka Doom Level editor, a cross platform tool I have extensive experience with.

Eureka doom map editor

It's certainly got the right name for this test, so let's see if it can live up to it.

#include <FL/Fl.H>
#include <FL/Fl_Window.H>

int main() {
    // Create a window with dimensions 800x600
    Fl_Window *window = new Fl_Window(800, 600);

    // Set the window label
    window->label("My FLTK Window");

    // Show the window
    window->show();

    // Run the FLTK event loop
    return Fl::run();
}
Enter fullscreen mode Exit fullscreen mode

Aaand at 1.1 seconds we have a new winner. That was really impressive. I would be curious to see how much of it's speed it could retain with a more complex application.

To finish this test off, I'll add two late additions. A simple Qt6 app using QT Widgets and C++

#include <QApplication>
#include <QWidget>

int main(int argc, char *argv[])
{
    QApplication app(argc, argv);

    QWidget window;
    window.resize(800, 600);
    window.setWindowTitle("Simple Qt6 App on Raspberry Pi Zero");
    window.show();

    return app.exec();
}
Enter fullscreen mode Exit fullscreen mode

and a QtQuick example with qml

import QtQuick 2.15
import QtQuick.Window 2.15

Window {
    width: 800
    height: 600
    visible: true
}
Enter fullscreen mode Exit fullscreen mode

Compiling these apps made my little Pi Zero chug, but it was worth it. At a little under 2.5 seconds for QT widgets, and 2.75 seconds for the QML example, that's not bad. QT is used a lot in the auto industry for infotainment systems which are not known for their specs, so it's not surprising that we could get something running on our RPI Zero. Qt also comes with all the same goodies that GTK and other fully featured cross platform GUIs would need to build any type of app, so its performance here indicates it would be pretty snappy on any computer more powerful than a Raspberry Pi Zero. It's not my favorite cross platform GUI for a few reasons, but every non electron based desktop GUI developer job I've seen uses it, so that's a strong point in its favor.

Now, I'll be the first one to tell you that there are a lot more that goes into making a GUI app than how fast it opens. There is the set of widgets that it provides, how cross platform it is, what its memory footprint looks, its ability to provide a native "look and feel" etc. All things we didn't test here. But I hope that I've at least convinced you to give one of these great libraries a try

Call To Action 📣

Hi 👋 my name is Diego Crespo and I like to talk about technology, niche programming languages, and AI. I have a Twitter and a Mastodon, if you’d like to follow me on other social media platforms. If you liked the article, consider checking out my Substack. And if you haven’t why not check out another article of mine listed below! Thank you for reading and giving me a little of your valuable time. A.M.D.G

Top comments (0)