Gtk3 animation inside the progress bar

About programming and getting involved with Linux Mint development
Forum rules
Topics in this forum are automatically closed 6 months after creation.
Locked
Misko_2083

Gtk3 animation inside the progress bar

Post by Misko_2083 »

Those of you who remember how it looked ike in Gtk2, remember having text inside the progress bar.
This function to show the text inside the progress bar deprecated in Gtk3+.
But we will not display text inside the progress bar.
We will take it the next level.
Not only we'll put image inside the progress bar but we will make it move away from it as as the "progress progresses".
No pun intended there.

For this experiment we will use Vladimir's avatar, Image
because it's a cool image and Vladimir is a cool fellow with a great sence of humor and administrator of foss.rs (Free and Open Source Serbia).
And because if I did that with xenopeek's avatar he would kick my a... cough, cough... lover back.
He probably wouldn't, but it's better safe than sorry people. :lol:

We will need an animated gif and since we will use this only for fun, we need to find a quick animation.
I found this website that creates a simple animation from a static image and exports it to a video or animated gif.
https://www.kapwing.com/
Doesn't even requires to upload an image only a url of that image.
And so, after choosing the vibration animation and the adjusting the size of animation it's time to export.
But the image was not as I expected it to be.
It had a white background behind the animation.
After some trial and error, the setting for transparent background appears only after going back to edit the exported animation.

Here's the final result 36x36 pixel jumping avatar.
Image
The appropriate file name for this animated image is vlada.gif
Why, because Vlada is usually a nickname for people that their parents name Vladimir.
The complex Slavic name Vladimir with the meaning "to rule in peace". :)

Now we need one A.C.M.E. Gtk3 window, one GtkProgressBar, one GtkLabel, one GtkImage and a couple of other widgets.
First we need a file to write the code.
Folks let's name it vlada-bar.c because we want to have a uniform naming and because we will literaly put vlada in the bar. :)

GtkWindow will be our main container widget. It will a parent of all the other widgets.

Code: Select all

  /*Create a window with a title, and a default size*/
  window = gtk_application_window_new (app);
  gtk_window_set_title (GTK_WINDOW (window),
                               "Vladimir Fries the Guitar");
  gtk_window_set_default_size (GTK_WINDOW (window),
                               420, 60);
It will also become a child widget of the root window. Think of root window as a grandad of all the other windows.
Great-great-great-great-great-great-great-great-great-great-great-great-great-great-grandfather, 16th male ancestor.
We call him "beli orao" (the white eagle). And yes, there is a word for every ancestor in Serbian. https://familypedia.wikia.org/wiki/Serbian_kinship

Next, we need another container, GtkGrid.
Grid will host all the others and adjust their placement.

Code: Select all

  /*Create a grid container to store the widgets*/
  grid = gtk_grid_new ();
  gtk_grid_set_row_spacing (GTK_GRID (grid),
                                 10);
  gtk_grid_set_column_spacing (GTK_GRID (grid),
                                 10);
  gtk_grid_set_column_homogeneous (GTK_GRID (grid),
                                TRUE);
  gtk_grid_set_row_homogeneous (GTK_GRID (grid),
                                FALSE);
Let's plan the widget position(s) in a window.
------------------------------------window---------------------------
|-----------------------progress bar--------------------------------|
|---progress label----------action button---cancel button--|
-------------------------------------------------------------------------
Progress will be in the first row of the window.
In the second row there will be a GtkLabel to show the actual progress and two GtkButtons.

Now progress bar is going to need another container because we want our image on top of it.
We will use GtkOverlay for this and store progress as it's child widget.
window---grid---overlay---progress
Now about an image, the dancing guitar player avatar of our rock star admin.
It will have to have another container.
I know, "not another container please!". :roll:
Sorry pals, we need one pixel which will separate the progress from the image and not resize the window to infinity later on.
The image widget must be smaller than the progress bar.

Code: Select all

gtk_box_pack_start (GTK_BOX(box), progress_animation, FALSE, FALSE, 1);
// this one pixel is essential
window---grid---overlay---box---image
Now let's attach them.
Overlay and it's children progress bar and image in the first row.
While the label and two buttons go to the lower row.

Code: Select all

 
  gtk_grid_attach (GTK_GRID (grid), progress_overlay, 0, 0, 4, 1);
  gtk_grid_attach (GTK_GRID (grid), progress_label, 0, 1, 2, 1);
  gtk_grid_attach (GTK_GRID (grid), button1, 2, 1, 1, 1);
  gtk_grid_attach_next_to (GTK_GRID (grid), button2, button1,  GTK_POS_RIGHT, 1, 1);
  /*And attach grid to the window.*/
  gtk_container_add (GTK_CONTAINER (window), grid);
Phew, that wasn't too complicated.
We need a size group to ensure the progress bar does not cut the image.
The image dictates the size of of the progress bar, not the way around.
We have to connect to the size-allocate signal so that whenever the image is resized, the progress bar would increase in height.

We also have to connect to the size-allocate signal of the progress bar so that whenever the window is resized, the image would be positioned correctly.

Code: Select all

  g_signal_connect (progress_animation,
                    "size-allocate",
                    G_CALLBACK (progress_animation_size_allocate),
                     widgets);
  g_signal_connect (progress_bar,
                    "size-allocate",
                    G_CALLBACK (progress_bar_size_allocate),
                     widgets);
Now we connect to the "clicked" signals of the buttons.
The first button starts and stops a new thread at regular intervals of 100 miliseconds.
g_timeout_add_full calls function "fill" which does a few things:
- increments the progress
- sets the porogress bar
- sets the text of the label
There is also a callback function which is called when the fill function returns FALSE or when we click the button1 again.

Code: Select all

guint threadID = 0;

void button1_clicked_cb
(GtkButton *button, app_widgets *widgets)
{
    if (threadID == 0)
    {
        threadID = g_timeout_add_full (0, 100, fill,
                      widgets, stop_progress_cb);
        gtk_button_set_label (button,
                      "\xe2\x8f\xb8");
    }
    else
    {
        GSource *source = g_main_context_find_source_by_id(NULL,
                                                       threadID);
         if (source)
         {
            g_source_destroy (source);
         }
         threadID = 0;
         gtk_button_set_label (button,
                      "\xe2\x8f\xaf");
    }
}
The play and pause characters from unicode that fits the theme of this application.
I hope you don't mind there are unicode characters in the button label.
We want to keep those small because GtkButton size will increase if the text label size is increased.
And maybe the window size will increase then.
One event leads to another and next thing we wake up having a big hangover in Mexico with nothing but the shirt.
Oh wait, that's only in the movies.
Or is it?! Right, carry on, nothing to see here.

Now we have vlada.gif Image and vlada-bar.c
Full code here:

Code: Select all

#include <stdio.h>
#include <string.h>
#include <gtk/gtk.h>
#include <gdk/gdk.h>
#include <glib.h>

#define ANIMATION "vlada.gif"

/*  Vladin bar animated progressbar
                                
    gcc -Wall -Wextra -o vlada-bar vlada-bar.c `pkg-config --cflags --libs gtk+-3.0`

                                 depends: gcc libgtk-3-dev (3.24)
*/

guint threadID = 0;

typedef struct {
    GtkWidget           *progress_bar;
    GtkWidget           *button1;
    GtkWidget           *progress_animation;
    GtkWidget           *progress_label;
    GtkStyleProvider    *progressbar_style_provider;
} app_widgets;


void destroy_handler (GtkApplication* app, gpointer data)
{
    (void) app;
    g_application_quit(G_APPLICATION (data));
}


void
stop_progress_cb (gpointer user_data)
{
  app_widgets *widgets = (app_widgets *) user_data;
  gdouble fraction;

  fraction = gtk_progress_bar_get_fraction (GTK_PROGRESS_BAR(widgets->progress_bar));
  g_print("Action: %.0f %%\n", fraction*100);
}


static gboolean
fill (gpointer  user_data)
{
  app_widgets *widgets = (app_widgets *) user_data;
  GtkAllocation *alloc = g_new(GtkAllocation, 1);
  gdouble fraction;
  
  /*Get the current progress*/
  fraction = gtk_progress_bar_get_fraction (GTK_PROGRESS_BAR(widgets->progress_bar));

   if (fraction > 0.999) {
      fraction = 0;
   }

  /*Increase the bar by 1% each time this function is called*/
  if (fraction < 0.999)
    fraction += 0.01;

  gtk_widget_get_allocation (widgets->progress_bar, alloc);
  
  gtk_widget_set_margin_start(widgets->progress_animation,
                          alloc->width*fraction);

  /*Fill in the bar with the new fraction*/
  gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR(widgets->progress_bar),
                                fraction);
  gchar temp[256];
  memset(&temp, 0x0, 256);
  if (fraction > 0.999) {
     snprintf(temp, 255, "Over and Out: %.0f %%", fraction*100);

     gtk_button_set_label (GTK_BUTTON(widgets->button1),
                          "Repeat");
     threadID = 0;
   }
   else {
     snprintf(temp, 255, "Action: %.0f %%", fraction*100);
   }
  gtk_label_set_text (GTK_LABEL(widgets->progress_label), temp);

  g_free(alloc);

  /*Ensures that the fraction stays below 1.0*/
  if (fraction < 0.999)
    return TRUE;

  return FALSE;
}


void
button1_clicked_cb (GtkButton *button,
                    app_widgets *widgets)
{
    if (threadID == 0)
    {
        threadID = g_timeout_add_full (0, 100, fill,
                      widgets, stop_progress_cb);
        gtk_button_set_label (button,
                      "\xe2\x8f\xb8");
    }
    else
    {
        GSource *source = g_main_context_find_source_by_id(NULL,
                                                       threadID);
         if (source)
         {
            g_source_destroy (source);
         }
         threadID = 0;
         gtk_button_set_label (button,
                      "\xe2\x8f\xaf");
    }
}

static void
progress_bar_size_allocate (GtkWidget       *progress_bar,
                              GdkRectangle    *allocation,
                              gpointer         user_data)
{
  (void) progress_bar;
  app_widgets *widgets = (app_widgets *) user_data;

  gdouble fraction;
  
  /*Get the current progress*/
  fraction = gtk_progress_bar_get_fraction
              (GTK_PROGRESS_BAR(widgets->progress_bar));
  if (fraction == 0)
  { 
     fraction = 0.01;
  }
  /*Set the margin of animation when the window width changes*/
  gtk_widget_set_margin_start(widgets->progress_animation,
                               allocation->width*fraction);
  if (fraction > 0.999)
     gtk_widget_set_margin_start(widgets->progress_animation,
                                                           1);
}


static void
progress_animation_size_allocate (GtkWidget   *animation,
                                  GdkRectangle    *allocation,
                                  gpointer         user_data)
{
  (void) animation;
  app_widgets   *widgets = (app_widgets *) user_data;
  GtkAllocation *progress_bar_allocation = g_new(GtkAllocation, 1);
  char          *css_text;

  /*Get progress bar allocation*/
  gtk_widget_get_allocation (widgets->progress_bar,
                             progress_bar_allocation);
  /*weird but working*/
  if ((progress_bar_allocation->height = allocation->height) ||
      (progress_bar_allocation->height < allocation->height))
  {
     css_text = g_strdup_printf ("progressbar trough,\n"
                                 "progressbar progress\n"
                                 "{\n"
                                 "  min-height: %dpx;\n"
                                 "}\n",
                                 allocation->height);

     gtk_css_provider_load_from_data (GTK_CSS_PROVIDER
                                      (widgets->progressbar_style_provider),
                                      css_text, -1, NULL);

    }

  g_free(progress_bar_allocation);
  g_free (css_text);
}

static void
activate (GtkApplication *app,
          gpointer        user_data)
{
  (void) user_data;
  GtkSizeGroup    *size_group;
  GtkWidget       *window;
  GtkWidget       *grid;
  GtkWidget       *button2;
  GtkWidget       *progress_overlay;
  GdkPixbufAnimation *animation;
  GError      *error = NULL;
  GtkWidget       *box;

  app_widgets *widgets = g_slice_new(app_widgets);

  gdouble fraction = 0.0;

  /*Create a window with a title, and a default size*/
  window = gtk_application_window_new (app);
  gtk_window_set_title (GTK_WINDOW (window),
                               "Vladimir Fries the Guitar");
  gtk_window_set_default_size (GTK_WINDOW (window),
                               420, 60);
  /*Create a grid container to store the widgets*/
  grid = gtk_grid_new ();
  gtk_grid_set_row_spacing (GTK_GRID (grid),
                                 10);
  gtk_grid_set_column_spacing (GTK_GRID (grid),
                                 10);
  gtk_grid_set_column_homogeneous (GTK_GRID (grid),
                                TRUE);
  gtk_grid_set_row_homogeneous (GTK_GRID (grid),
                                FALSE);
  /*Create a progressbar*/
  widgets->progress_bar = gtk_progress_bar_new();
  gtk_progress_bar_set_inverted (GTK_PROGRESS_BAR(widgets->progress_bar),
                                 FALSE);
  gtk_progress_bar_set_show_text (GTK_PROGRESS_BAR(widgets->progress_bar),
                                 FALSE);            
  /*Fill in the given fraction of the bar.
    It has to be between 0.0-1.0 inclusive*/
  gtk_progress_bar_set_fraction (GTK_PROGRESS_BAR (widgets->progress_bar), fraction);


  widgets->progress_label = gtk_label_new ("Ready to Rock");
  /*Create buttons*/
  widgets->button1 = gtk_button_new_with_label ("\xe2\x96\xb6");
  button2 = gtk_button_new_with_label ("Cancel");

  /*Create an overlay*/
  progress_overlay = gtk_overlay_new ();
  gtk_widget_set_hexpand (progress_overlay, TRUE);
  gtk_widget_set_vexpand (progress_overlay, FALSE);
  gtk_container_add (GTK_CONTAINER (progress_overlay),
                     widgets->progress_bar);
  /*Create an animation pixbuf*/
  animation = gdk_pixbuf_animation_new_from_file(ANIMATION, &error);
  if (error) {
      g_warning("No image found\n*ERROR %s\n", error->message);
      destroy_handler(NULL, app);
  }
  widgets->progress_animation = gtk_image_new_from_animation (animation);
  gtk_widget_set_vexpand(widgets->progress_bar, FALSE);
  gtk_widget_set_name (widgets->progress_animation,
                         "progress-animation");

   /*Image has to be in a (box) container with a one pixel padding*/
     
  /*create a box container for the image*/
  box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 0);
  gtk_box_pack_start (GTK_BOX(box), widgets->progress_animation, FALSE, FALSE, 1); // this one pixel is essential
  gtk_widget_set_halign (widgets->progress_animation,
                        GTK_ALIGN_START);
  gtk_overlay_add_overlay (GTK_OVERLAY (progress_overlay),
                           box);
  gtk_overlay_set_overlay_pass_through (GTK_OVERLAY (progress_overlay),
                           box, TRUE);
  /*Size group and add widgets*/
  size_group = gtk_size_group_new (GTK_SIZE_GROUP_VERTICAL);
  gtk_size_group_add_widget (size_group, widgets->progress_bar);
  gtk_size_group_add_widget (size_group, box);

  /*Create a style provider for the progessbar*/
  widgets->progressbar_style_provider = GTK_STYLE_PROVIDER (gtk_css_provider_new ());
  gtk_style_context_add_provider (gtk_widget_get_style_context (widgets->progress_bar),
                                  widgets->progressbar_style_provider,
                                  GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
  g_signal_connect (widgets->progress_animation,
                    "size-allocate",
                    G_CALLBACK (progress_animation_size_allocate),
                     widgets);
  g_signal_connect (widgets->progress_bar,
                    "size-allocate",
                    G_CALLBACK (progress_bar_size_allocate),
                     widgets);
  gtk_grid_attach (GTK_GRID (grid), progress_overlay, 0, 0, 4, 1);
  gtk_grid_attach (GTK_GRID (grid), widgets->progress_label, 0, 1, 2, 1);
  gtk_grid_attach (GTK_GRID (grid), widgets->button1, 2, 1, 1, 1);
  gtk_grid_attach_next_to (GTK_GRID (grid), button2, widgets->button1,  GTK_POS_RIGHT, 1, 1);

  gtk_container_add (GTK_CONTAINER (window), grid);
  gtk_container_set_border_width(GTK_CONTAINER(grid),12);
  gtk_container_set_border_width(GTK_CONTAINER(window),5);
  
  g_signal_connect (G_OBJECT(window), "destroy",
                    G_CALLBACK (destroy_handler), app);
  g_signal_connect (widgets->button1, "clicked",
                    G_CALLBACK (button1_clicked_cb), widgets);
  g_signal_connect (button2, "clicked",
                    G_CALLBACK (destroy_handler), app);

  gtk_widget_show_all (window);
}


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

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

  return status;
}
Brace yourselves peeps, it's time to compile.
Install dependancies:

Code: Select all

apt-install gcc libgtk-3-dev
gcc is a C compiler and libgtk-3-dev is the gtk3 development library.
Make sure it's at least version 3.24, or 3.22 maybe I didn't test it much... :)

Anyway, cd to the path where you keep the files and run:

Code: Select all

 gcc -Wall -Wextra -o vlada-bar vlada-bar.c `pkg-config --cflags --libs gtk+-3.0`
and start the app from the same dir.

Code: Select all

./vlada-bar
Image

P.S.
Playing "Twisted Sister - I Wanna Rock" is obligatory with this.
Last edited by LockBot on Wed Dec 28, 2022 7:16 am, edited 1 time in total.
Reason: Topic automatically closed 6 months after creation. New replies are no longer allowed.
User avatar
AndyMH
Level 21
Level 21
Posts: 13503
Joined: Fri Mar 04, 2016 5:23 pm
Location: Wiltshire

Re: Gtk3 animation inside the progress bar

Post by AndyMH »

As a lazarus user, thanks for this. At some point I want to start transitioning to C and this is a useful 'primer'.
Thinkcentre M720Q - LM21.3 cinnamon, 4 x T430 - LM21.3 cinnamon, Homebrew desktop i5-8400+GTX1080 Cinnamon 19.0
Locked

Return to “Programming & Development”