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, should those 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.

/!\ 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 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;
    bool is_maximized;
    bool is_fullscreen;
  } MyApplicationWindow;

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

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);
}

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);
}

Additional tips

When using GSettings it's possible to use the g_settings_bind() function to bind the state settings to a property on an object class holding the window state. For instance, if we move all the window state settings into a WindowState object:

typedef struct {
  GObject parent_instance;

  int current_width;
  int current_height;
  bool is_maximized;
  bool is_fullscreen;
} WindowState;

typedef struct {
  GtkApplicationWindow parent_instance;

  WindowState *state;
} MyApplicationWindow;

And we install the corresponding GObject properties mapping to the fields in the instance data structure, we can use g_settings_bind() to update the GSettings keys when the properties change, and vice versa, thus removing the need for explicit save and load functions:

static void
on_window_size_allocate (GtkWidget *widget, GtkAllocation *allocation)
{
  MyApplicationWindow *self = MY_APPLICATION_WINDOW (widget);
  int window_width, window_height;

  gtk_window_get_size (GTK_WINDOW (widget), &window_width, &window_height);

  if (!(self->state->is_maximized || self->state->is_fullscreen)) {
    g_object_set (self->state,
                  "current-width", window_width,
                  "current-height", window_height,
                  NULL);
  }
}

static void
on_window_state_event (GtkWidget *widget, GdkEventWindowState *event)
{
  MyApplicationWindow *self = MY_APPLICATION_WINDOW (widget);

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

  g_object_set (self->state,
                "is-maximized", is_maximized,
                "is-fullscreen", is_fullscreen,
                NULL);
}

static void
my_application_window_constructed (GObject *object)
{
  MyApplicationWindow *self = MY_APPLICATION_WINDOW (object);

  self->state = g_object_new (window_state_get_type (), NULL);

  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->state, "current-width",
                   G_SETTINGS_BIND_DEFAULT);
  g_settings_bind (settings, "height",
                   self->state, "current-height",
                   G_SETTINGS_BIND_DEFAULT);
  g_settings_bind (settings, "is-maximized",
                   self->state, "is-maximized",
                   G_SETTINGS_BIND_DEFAULT);
  g_settings_bind (settings, "is-fullscreen",
                   self->state, "is-fullscreen",
                   G_SETTINGS_BIND_DEFAULT);

  gtk_window_set_default_size (GTK_WINDOW (self),
                               self->state->current_width,
                               self->state->current_height);

  if (self->state->is_maximized)
    gtk_window_maximize (GTK_WINDOW (self));

  if (self->state->is_fullscreen)
    gtk_window_fullscreen (GTK_WINDOW (self));

The Application Class example in gtk3-demo shows this code in action.

HowDoI/SaveWindowState (last edited 2016-03-01 16:33:09 by EmmanueleBassi)