Xfce Wiki

Sub domains
 

Site Tools

Translations of this page?:

Monochrome icon

This step-by-step tutorial explains how to include a monochrome icon inside a GTK+ application. It starts with a minimal solution and evolves up to a more solid one with Cairo. All the examples are written in Vala language.

Author: Mike Massonnet
Date: 2010-03-11

Table of contents

When, why and what

When would you use a monochrome icon? Because it's cool, because it looks nice in your application, or because you are looking for a custom style. I wanted to replace an old button used in a “close-window” context with a cool icon. The window has a custom GTK+ RC style, so it is themed like other applications but with specific colors, and using the “close” themed icon from the stock items doesn't fit because of its static color.

Why did I write this tutorial? Why not! I liked doing my icon and I think this kind of content is interesting.

This tutorial introduces into a very simple solution by reusing a custom image. To get a monochrome result you can use an XPM image, there are techniques to customize the colors but this tutorial doesn't cover it. Better solutions are PNG or SVG images with alpha channel and a semi-transparent monochrome color. The color should be of grayscale so it can fit well over the background color of the window. As this technique doesn't work for dark backgrounds, the tutorial goes further and introduces into a short Cairo example but enough to get you started. At the end it is extended with an example on abstract classes.

It is expected to know about GTK+ and Vala (cf. Annexe, GTK+ Kick-Start Tutorial for Vala).

Simple image

The first solution is to use a GtkImage and put it inside a GtkEventBox to make it clickable.

Minimal window

Before starting we need a minimal GtkWindow in which we are going to load an image. We set a default size for the window so when it appears it isn't tiny.

public class IconWindow : Gtk.Window {
 
        private Gtk.Image image;
 
        /* The constructor sets the default window size */
        construct {
                default_height = 120;
                default_width = 120;
                border_width = 2;
 
                /* Pack a GtkImage inside the window */
                image = new Gtk.Image();
                image.show();
                add(image);
 
                load_image();
 
                destroy.connect(() => { Gtk.main_quit(); });
        }
 
        public IconWindow() {
        }
 
        private void load_image() {
                image.set_from_file("icon.xpm");
        }
}
 
static int main(string[] args) {
        Gtk.init(ref args);
        var win = new IconWindow();
        win.show();
        Gtk.main();
        return 0;
}

Clickable image

To get the image clickable we have to put it inside a GtkEventBox widget. This widget by default will paint an opaque background, but usually we don't want this and set the “visible-window” property to false.

                var evbox = new Gtk.EventBox();
                evbox.visible_window = false;
                image = new Gtk.Image(); 
                evbox.add(image);
                evbox.show_all();
                add(evbox);

And now we can connect to the signal “button-press-event” and attach a callback.

                evbox.button_press_event.connect(on_button_press_event);
 
                ...
 
        private bool on_button_press_event(Gdk.EventButton event) {
                debug("Icon pressed!");
                return false;
        }

We will see later how to install a new signal “clicked” in order to make it behave like a GtkButton.

Highlighted image

To give a better user interaction and make it obvious that the image is clickable, we can highlight it while passing the mouse over the image. This works by attaching callbacks on the GtkWidget enter/leave notify events and what we do in these can vary. We can load the same image inside a GdkPixbuf object to modify it programmatically, or we can use a new image which can actually contain more modifications.

We are going to rewrite the “load_image” method to accept a boolean value and load one or another image.

                evbox.enter_notify_event.connect(on_enter_notify_event);
                evbox.leave_notify_event.connect(on_leave_notify_event);
 
                load_image(false);
 
                ...
 
        private void load_image(bool active) {
                if (active) {
                        image.set_from_file("icon-active.xpm");
                }
                else {
                        image.set_from_file("icon.xpm");
                }
        }
 
        private bool on_enter_notify_event(Gdk.EventCrossing event) {
                load_image(true);
                return false;
        }
 
        private bool on_leave_notify_event(Gdk.EventCrossing event) {
                load_image(false);
                return false;
        }

To load the same image through a GdkPixbuf we would be using the set_from_pixbuf method.

var pixbuf = new Gdk.Pixbuf.from_file("icon.xpm");
pixbuf.saturate_and_pixelate(pixbuf, (float)0.5, false);
image.set_from_pixbuf(pixbuf);

The saturate_and_pixelate method from GdkPixbuf lets us turn the image to grayscale. It is possible to modify a GdkPixbuf in multiple ways but since GdkPixbuf doesn't provide many convenience functions, one would need to write them inside a separate “gdk-pixbuf-extension” source. Writing a GdkPixbuf convenience function is not covered by this tutorial (cf. Annexe, Exo extensions to GdkPixbuf).

Split everything into classes

Since it's wrong to write all the code inside the main function we split the program into several classes. This keeps the code clean and maintainable instead of a bad cooked tangle of pasta. We already have a custom GtkWindow class, but now we also want a distinctive class for the icon. We are going to call this class “IconButton” that will inherit from GtkEventBox, and we will make it behave like a casual button.

IconWindow class

The IconWindow class does only pack the custom composite-widget IconButton detailed in the next section.

public class IconWindow : Gtk.Window {
 
        /* The constructor sets the default window size */
        construct {
                default_height = 120;
                default_width = 120;
                border_width = 2;
 
                /* Pack an IconButton inside the window */
                var icon = new IconButton();
                icon.show();
                add(icon);
 
                destroy.connect(() => { Gtk.main_quit(); });
        }
 
        public IconWindow() {
        }
}

IconButton class

The IconButton class includes the enter/leave notify event callbacks and the code to load the image.

public class IconButton : Gtk.EventBox {
 
        private Gtk.Image image;
 
        /* The constructor sets the visible window to false and packs an image */
        construct {
                visible_window = false;
 
                image = new Gtk.Image();
                image.show();
                add(image);
 
                load_image(false);
 
                enter_notify_event.connect(on_enter_notify_event);
                leave_notify_event.connect(on_leave_notify_event);
        }
 
        public IconButton() {
        }
 
        private void load_image(bool active) {
                image.set_from_file(active ? "icon-active.xpm" : "icon.xpm");
        }
 
        private bool on_enter_notify_event(Gdk.EventCrossing event) {
                load_image(true);
                return false;
        }
 
        private bool on_leave_notify_event(Gdk.EventCrossing event) {
                load_image(false);
                return false;
        }
}

Clicked signal

Earlier we used the signal “button-press-event” on the image but it doesn't behave the same as a “clicked” signal from a GtkButton. To introduce the same behavior we have to connect to the “button-release-event” signal and check if the mouse pointer is over the image. We can get coordinates from every widget and the button release event exposes the coordinates from the mouse at the time it was released via a GtkEventButton structure.

        /* Install "clicked" signal in the class */
        public signal void clicked();
 
        construct {
                ...
 
                button_release_event.connect(on_button_release_event);
        }
 
        private bool on_button_release_event(Gdk.EventButton event) {
                /* Accept only left mouse button */
                if (event.button != 1)
                        return false;
 
                /* Retrieve position of mouse pointer relative to the Gdk.Window */
                int cur_x = (int)event.x;
                int cur_y = (int)event.y;
 
                /* Retrieve size of the Gdk.Window */
                int width = allocation.width;
                int height = allocation.height;
 
                /* Emit clicked signal only if the mouse was still over the image */
                if (cur_x >= 0 && cur_x < width && cur_y >= 0 && cur_y < height) {
                        clicked();
                }
                return false;
        }

Now we can connect to the “clicked” signal, and if the mouse is released outside the icon it will act as if the action was cancelled just like a normal button.

public class IconWindow : Gtk.Window {
 
        construct {
                ...
 
                icon = new IconButton();
                icon.clicked.connect(icon_clicked);
                add(icon);
 
                ...
        }
 
        ...
 
        private void icon_clicked() {
                debug("Icon clicked!");
        }
}

Custom drawing with Cairo

The second solution is to draw the icon with Cairo. This can also be done on top of GtkEventBox, to make easy use of button clicks and the ability to draw on the parent window (the visible window turned off), but it isn't best suited as it is a container meant to include a single composite-widget although we can override the add method to disallow any widget packing — in the prior solution a GtkImage was packed inside it. Instead the GtkDrawingArea widget will be used, and to get it clickable we just have to add the appropriate event masks.

The advantage of using Cairo is that it makes it possible in a nice and easy way to draw a monochrome icon by picking up colors from the GTK+ theme. This makes for perfect contrast between the icon and the application.

Drawing area

The first code to start with is the minimal IconButton class. From now on we will be using the expose handler to draw on the GdkWindow from the GtkDrawingArea widget. The expose handler is called every time the window needs to be refreshed, and this happens when a part of the window or the whole area was hidden and gets visible again. On a side note window managers with a compositor draw the whole window area off-screen which means less calls on the expose handler.

public class IconButton : Gtk.DrawingArea {
 
        private bool active = false;
 
        public signal void clicked();
 
        construct {
                /* Set minimal size of the icon */
                set_size_request(22, 22);
 
                /* Add event masks to get the widget clickable */
                add_events(Gdk.EventMask.BUTTON_PRESS_MASK
                           |Gdk.EventMask.BUTTON_RELEASE_MASK
                           |Gdk.EventMask.ENTER_NOTIFY_MASK
                           |Gdk.EventMask.LEAVE_NOTIFY_MASK);
 
                enter_notify_event.connect(on_enter_notify_event);
                leave_notify_event.connect(on_leave_notify_event);
                button_release_event.connect(on_button_release_event);
        }
 
        public IconButton() {
        }

We override the expose handler from GtkWidget, it is developed in the next section.

        private override bool expose_event(Gdk.EventExpose event) {
                return false;
        }

And then we include the same callbacks as previously that completes our minimal class. As we no more load an image we keep track of the active state inside an internal boolean value. And in order to enforce a call on the expose handler we invalidate the GdkWindow.

        private bool on_enter_notify_event(Gdk.EventCrossing event) {
                active = true;
                window.invalidate_rect(null, false);
                return false;
        }
 
        private bool on_leave_notify_event(Gdk.EventCrossing event) {
                active = false;
                window.invalidate_rect(null, false);
                return false;
        }
 
        private bool on_button_release_event(Gdk.EventButton event) {
                /* Accept only left mouse button */
                if (event.button != 1)
                        return false;
 
                /* Retrieve position of mouse pointer at release time */
                int cur_x = (int)event.x;
                int cur_y = (int)event.y;
 
                /* Retrieve size of the Gdk.Window */
                int width = allocation.width;
                int height = allocation.height;
 
                /* Emit clicked signal only if the mouse was still over the image */
                if (cur_x >= 0 && cur_x < width && cur_y >= 0 && cur_y < height) { 
                        clicked();
                } 
                return false; 
        }
}

Expose handler

Before we can actually use any Cairo draw functions, we need to set up the Cairo requirements. We will retrieve a Cairo drawing context and use it for a small icon of 22 pixels.

        private override bool expose_event(Gdk.EventExpose event) {
                int icon_width = 22;
                int icon_height = 22;
                int x = allocation.width / 2 - icon_width / 2 + allocation.x;
                int y = allocation.height / 2 - icon_height / 2 + allocation.y;
 
                /* Get the Cairo context directly out from the Gdk.Window */
                var cr = Gdk.cairo_create(window);

Until here we have a Cairo context from the GdkWindow on which we can draw. Now we are going to clip the context in a rectangle right in the middle with the wished size of the icon.

                /* Clip the context */
                cr.rectangle(x, y, icon_width, icon_height);
                cr.clip();

To avoid drawing on a bigger surface than the final icon size we use an empty Cairo surface. Once we are finished drawing the icon we can use the surface as a source on the GdkWindow Cairo context.

                /* Create empty Cairo surface for the icon and get a context on it */
                var icon_surface = new Cairo.ImageSurface(Cairo.Format.ARGB32,
                                                          icon_width, icon_height);
                var cr_ = new Cairo.Context(icon_surface);
 
                /* Draw a dummy cross on the Cairo context cr_ */
                cr_.set_line_cap(Cairo.LineCap.ROUND);
                cr_.set_line_width(5);
                cr_.set_source_rgba(1, 1, 1, active ? 0.95 : 0.4);
 
                cr_.move_to(6, 6);
                cr_.line_to(16, 16);
                cr_.move_to(16, 6);
                cr_.line_to(6, 16);
                cr_.stroke();
 
                /* Copy our icon on top of the Gdk.Window context */
                cr.set_source_surface(icon_surface, x, y);
                cr.paint();
 
                return false;
        }

To learn more about Cairo a very good starting point is the Cairo project website. It has a short but useful FAQ and many samples with interesting drawing techniques (cf. Annexe, Cairo FAQ & Samples).

Dynamic size

Drawing a first static sized icon can be fun. But drawing it within the requested size is better, the constructor already requests a minimum size, and from outside the class we can always request a new size.

var icon = new IconButton();
icon.set_size_request(48, 48);

The changes needed to use the requested size is short.

                int padding = 5;
                int icon_width = allocation.width - padding;
                int icon_height = allocation.height - padding;
 
                ...
 
                cr_.move_to(padding + x, padding + y);
                cr_.line_to(icon_width - padding, icon_height - padding);
                cr_.move_to(icon_width - padding, padding + y);
                cr_.line_to(padding + x, icon_height - padding);
                cr_.stroke();

We include a small padding to avoid drawing too near at the borders of the clipped area.

GTK+ colors

Reusing GTK+ colors is ultra simple. Every widget has a style that contains color indications for a multitude of different states, it goes from “normal” to “selected” but in sum there are five states. On top of the states are four categories, this time ranging from “background” to “text”. Accessing one of these colors works as following:

some_widget.style.fg[Gtk.StateType.NORMAL];

This returns a GdkColor structure. Gdk provides convenience functions for Cairo and one of them eases the selection of the Cairo source color with a GdkColor structure. And it works like this:

Gdk.cairo_set_source_color(some_cairo_context, some_gdk_color);

Now we can hook this into the IconButton class.

                /* Draw a dummy cross on the Cairo context cr_ */
                cr_.set_line_cap(Cairo.LineCap.ROUND);
                cr_.set_line_width(5);
                Gdk.cairo_set_source_color(cr_, active ? style.base[Gtk.StateType.NORMAL]
                                                       : style.fg[Gtk.StateType.INSENSITIVE]);

Abstract class

Writing a custom icon class with Cairo needs us to put code for the mouse handling and the icon drawing. And every time we want a new icon, either we handle all the icons in the same class, with the facility of an icon chooser method, or we copy/paste code for each new icon class but there is better. The simple use of an abstract class will make a good separation between the icon behavior and the icon drawing. We will take the example where we have an abstract class IconButton and a child class TitleBarButton that contains code to draw icons useful for a window title bar.

We will be using an abstract method too. Just like for the expose_event method from GtkWidget where we have been using the Vala keyword “override” we will be using it within our child class for a draw_icon method.

Parent class

This new class looks a lot like the old IconButton class, the only change will be the use of the “abstract” and “protected” keywords and an intermediate call to the method draw_icon in the expose handler.

public abstract class IconButton : Gtk.DrawingArea {
 
        protected bool active = false;
 
        protected abstract void draw_icon(Cairo.Context cr, int width, int height);
 
        ...
 
        private override bool expose_event(Gdk.EventExpose event) {
 
                ...
 
                /* Create empty Cairo surface for the icon and get a context on it */
                var icon_surface = new Cairo.ImageSurface(Cairo.Format.ARGB32,
                                                          icon_width, icon_height);
                var cr_ = new Cairo.Context(icon_surface);
 
                draw_icon(cr_, icon_width, icon_height);
 
                /* Copy our icon on top of the Gdk.Window context */
                cr.set_source_surface(icon_surface, x, y);
                cr.paint();
 
                return false;
        }
 
        ...
 
}

Child class

To implement a child class on top of IconButton we will have to override the draw_icon method. We will be using an enumeration as an example to show the possibility of drawing different icons.

public enum TitleBarButtonType {
        EMPTY,
        CLOSE,
}
 
public class TitleBarButton : IconButton {
 
        /* Install property "icon_type" */
        public TitleBarButtonType icon_type {
                default = TitleBarButtonType.EMPTY;
                get;
                construct set;
        }

Cf. Annexe, Vala Properties.

        /* Default new method */
        public TitleBarButton(TitleBarButtonType icon_type) {
                Object(icon_type: icon_type);
        }

Cf. Annexe, Vala Gobject-Style Construction.

        /* Implement draw_icon */
        private override void draw_icon(Cairo.Context cr, int width, int height) {
                switch (icon_type) {
                        case TitleBarButtonType.CLOSE:
                                draw_close_button(cr, width, height);
                                break;
                        default:
                                break;
                }
        }
 
        private void draw_close_button(Cairo.Context cr, int width, int height) {
                int padding = 6;
                int x1 = padding;
                int x2 = width - padding;
                int y1 = padding;
                int y2 = height - padding;
 
                if (x2 <= x1 || y2 <= y1) {
                        warning("Can't draw icon, size request is too small");
                        return;
                }
 
                cr.set_line_cap(Cairo.LineCap.ROUND);
                cr.set_line_width(5);
                Gdk.cairo_set_source_color(cr,
                                active ? style.base[Gtk.StateType.NORMAL]
                                       : style.fg[Gtk.StateType.INSENSITIVE]);
 
                cr.move_to(x1, y1);
                cr.line_to(x2, y2);
                cr.move_to(x2, y1);
                cr.line_to(x1, y2);
                cr.stroke();
        }
 
}

The TitleBarButton class is now very short and clean, and it is trivial to extend it with more icons. Creating an object inside an application will look like this:

var icon = new TitleBarButton(TitleBarButtonType.CLOSE);
icon.set_size_request(24, 24);
icon.show();

Annexe

License

This document is licensed under the Creative Common by-sa 3.0.

That means you are free:

  • To share – to copy, distribute and transmit the work.
  • To remix – to adapt the work.

Under the following conditions:

  • Attribution – You must attribute the work in the manner specified by the author or licensor (but not in any way that suggests that they endorse you or your use of the work).
  • Share Alike – If you alter, transform, or build upon this work, you may distribute the resulting work only under the same or similar license to this one.
howto/monochrome-icon.txt · Last modified: 2010/10/02 17:26 (external edit)