Saving and Loading Window State

Use cases

Saving and restoring the window state across sessions is a useful feature for some applications.

Since applications know best what their state and geometry are – and whether, or how, those should be applied once the application is launched – it’s better to have this function near the code that creates the application window itself.

Where should the state be stored

The are two options for storing the window state:

  • a file under $XDG_CACHE_HOME/your.application.id

  • GSettings

Both options have their own advantages and trade offs.

The first option requires you to write the serialization and deserialization code, but it's portable and does not require ancillary files, like a GSettings schema. This means that you need to write more code, but it allows, for instance, to run your application uninstalled.

The second option gives you the serialization and deserialization code, but it requires you to define the setting keys schema and install it in a known location. This means that you will keep your code to a minimum, but it will also require installation.

It is generally preferred, everything being equal, to use GSettings over a custom file. The GSettings API is nicer, more reliable, and faster.

For more information on GSettings and how to use it in your project, read HowDoI/GSettings.

Saving the window state

There are few window states that should generally be saved:

  • the window size
  • whether the window is maximized, if the window supports it
  • whether the window is full screen, if the window supports it

The position of the window is best left to the window manager. Other states can be saved along with the ones above, using the same technique, so they will be left as an exercise for the reader.

The best practice is to create a GtkApplicationWindow class containing the states you wish to save:

  typedef struct {
    GtkApplicationWindow parent_instance;

    int current_width;
    int current_height;
    gboolean is_maximized;
    gboolean is_fullscreen;
  } MyApplicationWindow;

GTK 4

The window geometry should be updated by getting notifications from the GtkWindow:default-width and GtkWindow:default-height properties; window states, like "maximized" and "fullscreen", are also mapped to GtkWindow properties. You can use the GObject::notify signal:

static void
on_window_state_notify (GObject *gobject,
                        GParamSpec *pspec,
                        gpointer user_data)
{
  MyApplicationWindow *self = MY_APPLICATION_WINDOW (gobject);

  if (g_str_equal (pspec->name, "default-width"))
    g_object_get (gobject, "default-width", &(self->current_width), NULL);
  else if (g_str_equal (pspec->name, "default-height"))
    g_object_get (gobject, "default-height", &(self->current_height), NULL);
  else if (g_str_equal (pspec->name, "maximized"))
    g_object_get (gobject, "maximized", &(self->is_maximized), NULL);
  else if (g_str_equal (pspec->name, "fullscreened"))
    g_object_get (gobject, "fullscreened", &(self->is_fullscreen), NULL);
}

Saving state into GSettings

If you are using GSettings, you can map those properties to keys:

static void
my_application_window_init (MyApplicationWindow *self)
{
  GSettings *settings = g_settings_new ("your.application.id.window-state");

  // update the settings when the properties change and vice versa
  g_settings_bind (settings, "width",
                   self, "default-width",
                   G_SETTINGS_BIND_DEFAULT);
  g_settings_bind (settings, "height",
                   self, "default-height",
                   G_SETTINGS_BIND_DEFAULT);
  g_settings_bind (settings, "is-maximized",
                   self, "maximized",
                   G_SETTINGS_BIND_DEFAULT);
  g_settings_bind (settings, "is-fullscreen",
                   self, "fullscreened",
                   G_SETTINGS_BIND_DEFAULT);
}

Loading window state

Loading the window state

The window state can be loaded and applied when the application window instance is constructed:

static void
on_window_constructed (GObject *object)
{
  MyApplicationWindow *app_window = MY_APPLICATION_WINDOW (object);

  // set up the initial state; these are our fallback values
  app_window->current_width = -1;
  app_window->current_height = -1;
  app_window->is_maximized = FALSE;
  app_window->is_fullscreen = FALSE;

  // load the saved state, if any
  my_application_window_load_state (app_window);

  // apply the loaded state
  gtk_window_set_default_size (GTK_WINDOW (app_window),
                               app_window->current_width,
                               app_window->current_height);

  if (app_window->is_maximized)
    gtk_window_maximize (GTK_WINDOW (app_window));

  if (app_window->is_fullscreen)
    gtk_window_fullscreen (GTK_WINDOW (app_window));

  G_OBJECT_CLASS (my_application_window_parent_class)->constructed (object);
}

If you are using GSettings to store the window state, it will be restored as soon as the GSettings bindings have been created.

GTK 3

/!\ It is important to note that you should not use the GtkWidget::destroy signal to retrieve the state of a GtkWindow in order to store it somewhere. The GtkWidget::destroy signal is meant to be used to release external references, which means that the widget is not guaranteed to be in the same state as to when the window's close button was pressed, or the "Quit" action was called. You can still use the GtkWidget::destroy signal to save the state, but you should update the state to be saved only in response to state changes, like new size allocations or window state changes. /!\

The window geometry should be updated inside a GtkWidget::size-allocate signal handler:

static void
on_window_size_allocate (GtkWidget *widget, GtkAllocation *allocation)
{
  MyApplicationWindow *app_window = MY_APPLICATION_WINDOW (widget);

  // chain up to the parent's implementation
  GTK_WIDGET_CLASS (my_application_window_parent_class)->size_allocate (widget,
                                                                        allocation);

  // save the window geometry only if we are not maximized of fullscreen
  if (!(app_window->is_maximized || app_window->is_fullscreen)) {
    gtk_window_get_size (GTK_WINDOW (widget),
                         &app_window->current_width,
                         &app_window->current_height);
  }
}

/!\ It is important to note that you have to use gtk_window_get_size() to obtain the size. Using the allocation directly can lead to growing windows with client-side decorations.

The window state should be updated inside a GtkWidget::window-state-event signal handler:

static gboolean
on_window_state_event (GtkWidget *widget, GdkEventWindowState *event)
{
  MyApplicationWindow *app_window = MY_APPLICATION_WINDOW (widget);
  gboolean res = GDK_EVENT_PROPAGATE;

  // chain up to the parent's implementation, if any
  if (GTK_WIDGET_CLASS (my_application_window_parent_class)->window_state_event != NULL) {
    res =
      GTK_WIDGET_CLASS (my_application_window_parent_class)->window_state_event (widget,
                                                                                 event);
  }

  app_window->is_maximized =
    (event->new_window_state & GDK_WINDOW_STATE_MAXIMIZED) != 0;
  app_window->is_fullscreen =
    (event->new_window_state & GDK_WINDOW_STATE_FULLSCREEN) != 0;

  return res;
}

The window state should be saved inside a GtkWidget::destroy signal handler:

static void
on_window_destroy (GtkWidget *widget)
{
  MyApplicationWindow *app_window = MY_APPLICATION_WINDOW (widget);

  // store the state here
  my_application_window_store_state (app_window);

  // chain up to the parent's implementation
  GTK_WIDGET_CLASS (my_application_window_parent_class)->destroy (widget);
}

Saving the window state in a file

A trivial way to save the window state in a location on the file system is to use a GKeyFile, as provided by GLib. There are other choices for a file format — like XML or JSON — but key/value files are probably the simplest and less invasive choice.

static void
my_application_window_store_state (MyApplicationWindow *self)
{
  GKeyFile *keyfile = g_key_file_new ();

  g_key_file_set_integer (keyfile, "WindowState", "Width", self->current_width);
  g_key_file_set_integer (keyfile, "WindowState", "Height", self->current_height);
  g_key_file_set_boolean (keyfile, "WindowState", "IsMaximized", self->is_maximized);
  g_key_file_set_boolean (keyfile, "WindowState", "IsFullscreen", self->is_fullscreen);

  // we want a file under the $XDG_CACHE_HOME directory; we use the
  // application id to namespace the file.
  //
  // in this example, our application only has one window; if we had
  // more than one then we'd need to generate a UUID for each window
  // and use the UUID to create a unique file.

  const char *appid = g_application_get_application_id (g_application_get_default ());
  char *path = g_build_filename (g_get_user_cache_dir (), appid, NULL);

  // we don't really care about error handling; the ancillary file is
  // optional, and the application *must* work without it
  if (g_mkdir_with_parents (path, 0700) < 0) {
    goto out;
  }

  char *file = g_build_filename (path, "state.ini", NULL);

  // same as above, we don't do error handling
  g_key_file_save_to_file (keyfile, file, NULL);

  g_free (file);

out:
  g_key_file_unref (file);
  g_free (path);
}

Saving the window state in GSettings

Assuming you created a GSettings schema with the proper keys, the code for saving the window state is remarkably similar to the file case, except we don't have to care about saving the data ourselves:

static void
my_application_window_store_state (MyApplicationWindow *self)
{
  GSettings *settings = g_settings_new ("your.application.id.window-state");

  g_settings_set_int (settings, "width", self->current_width);
  g_settings_set_int (settings, "height", self->current_height);
  g_settings_set_boolean (settings, "is-maximized", self->is_maximized);
  g_settings_set_boolean (settings, "is-fullscreen", self->is_fullscreen);
}

Loading the window state

Loading window state in GTK3 works just like in GTK4:

static void
on_window_constructed (GObject *object)
{
  MyApplicationWindow *app_window = MY_APPLICATION_WINDOW (object);

  // set up the initial state; these are our fallback values
  app_window->current_width = -1;
  app_window->current_height = -1;
  app_window->is_maximized = FALSE;
  app_window->is_fullscreen = FALSE;

  // load the saved state, if any
  my_application_window_load_state (app_window);

  // apply the loaded state
  gtk_window_set_default_size (GTK_WINDOW (app_window),
                               app_window->current_width,
                               app_window->current_height);

  if (app_window->is_maximized)
    gtk_window_maximize (GTK_WINDOW (app_window));

  if (app_window->is_fullscreen)
    gtk_window_fullscreen (GTK_WINDOW (app_window));

  G_OBJECT_CLASS (my_application_window_parent_class)->constructed (object);
}

Loading the window state from a file

The main difference between the file-based approach and the GSettings-based approach is that the format must be more resilient against missing values. The window state file may not exist, or may be an older version, if you decided to add more states to it.

static void
my_application_window_load_state (MyApplicationWindow *self)
{
  const char *appid = g_application_get_application_id (g_application_get_default ());
  char *file = g_build_filename (g_get_user_cache_dir (), appid, "state.ini", NULL);
  GKeyFile *keyfile = g_key_file_new ();

  if (!g_key_file_load_from_file (keyfile, file, NULL)) {
    goto out;
  }

  GError *error;

  error = NULL;
  self->current_width = g_key_file_get_integer (keyfile, "WindowState", "Width", &error);
  if (error != NULL) {
    g_clear_error (&error);
    self->current_width = -1;
  }

  self->current_height = g_key_file_get_integer (keyfile, "WindowState", "Height", &error);
  if (error != NULL) {
    g_clear_error (&error);
    self->current_height = -1;
  }

  self->is_maximized = g_key_file_get_boolean (keyfile, "WindowState", "IsMaximized", &error);
  if (error != NULL) {
    g_clear_error (&error);
    self->is_maximized = FALSE;
  }

  self->is_fullscreen = g_key_file_get_boolean (keyfile, "WindowState", "IsFullscreen", &error);
  if (error != NULL) {
    g_clear_error (&error);
    self->is_fullscreen = FALSE;
  }

out:
  g_key_file_unref (keyfile);
  g_free (file);
}

Loading the window state from GSettings

As with saving the window state, loading the window state is generally easier to do with GSettings:

static void
my_application_window_load_state (MyApplicationWindow *self)
{
  GSettings *settings = g_settings_new ("your.application.id.window-state");

  self->current_width = g_settings_get_int (settings, "width");
  self->current_height = g_settings_get_int (settings, "height");
  self->is_maximized = g_settings_get_boolean (settings, "is-maximized");
  self->is_fullscreen = g_settings_get_boolean (settings, "is-fullscreen);
}

Examples

The Application Class example in the GTK Demo application (available in both GTK3 and GTK4) shows this code in action.