GTK provides a lot of useful widgets, but it inevitably can’t cover every possible use case. What if, for example, you need an image widget that resizes itself based on the available width, and clips its corners to fit in a popover? Fortunately, GTK makes it easy to create your own widgets that do whatever you want.
The Example Code
Here’s the complete source code, straight from GNOME Maps. In this post I’ll be using a condensed version:
const Cairo = imports.cairo;
const Gdk = imports.gi.Gdk;
const GObject = imports.gi.GObject;
const Gtk = imports.gi.Gtk;
var PlaceBubbleImage = GObject.registerClass(
class PlaceBubbleImage extends Gtk.DrawingArea {
_init(params) {
this._pixbuf = params.pixbuf;
delete params.pixbuf;
super._init(params);
}
vfunc_draw(cr) {
let [{x, y, width, height}, baseline] = this.get_allocated_size();
/* I've skipped a bunch of code here that caches the scaled image to
* increase performance. See the full source code if you want to learn
* how that works.*/
cr.scale(width / this._pixbuf.width,
height / this._pixbuf.height);
/* Also skipped the popover clipping code. */
Gdk.cairo_set_source_pixbuf(cr, this._pixbuf, 0, 0);
cr.paint();
return false;
}
vfunc_get_request_mode() {
return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
}
vfunc_get_preferred_height_for_width(width) {
let height = (this._pixbuf.height / this._pixbuf.width) * width;
return [height, height];
}
});
Subclassing and Virtual Functions
GTK’s custom widgets work by subclassing. In GJS, we do that with these two lines:
var PlaceBubbleImage = GObject.registerClass(
class PlaceBubbleImage extends Gtk.DrawingArea {
This registers a class called PlaceBubbleImage with the GLib type system. It extends Gtk.DrawingArea, a generic class designed for this purpose.
Now that we have our own widget class, we customize it using virtual methods, which
override the default functionality of Gtk.DrawingArea. In GJS, all you have to
do is create a method titled vfunc_
followed by the name of the function
you’re overriding. So this code:
vfunc_get_request_mode() {
return Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH;
}
overrides the get_request_mode
vfunc.
Implementing the Widget
Every widget needs to do at least two things:
- Calculate its size
- Draw itself (for widgets like PlaceBubbleImage) or arrange its children (for containers)
Size Calculation
To calculate size, there are a number of vfuncs you can override. In this case,
we only need vfunc_get_request_mode()
and vfunc_get_preferred_height_for_width()
.
vfunc_get_request_mode()
, as you saw above, just returns Gtk.SizeRequestMode.HEIGHT_FOR_WIDTH
.
This indicates that if GTK tells us how much horizontal space is available, we
can tell it how much vertical space we need. For a more detailed explanation
of height-for-width geometry management, see the GtkContainer docs.
Meanwhile, vfunc_get_preferred_height_for_width()
does the actual size
calculation. It first finds the aspect ratio of the pixbuf (this._pixbuf.height / this._pixbuf.width
),
then multiplies by the width we’re given to calculate the appropriate height.
We return it twice in an array–the first is the minimum height, and the second
is the “natural” or preferred height. In this case, they’re the same.
Drawing
The biggest function here is vfunc_draw()
. It takes a Cairo context
and draws the widget’s contents to it.
First, it gets the final size of the widget using Gtk.Widget.get_allocated_size().
Then it scales the image by width / this._pixbuf.width
and
height / this._pixbuf.height
. So if the image is 100x100 but the widget
is only 50x50, we get 50/100=0.5, and the image is drawn at half size.
The next line, Gdk.cairo_set_source_pixbuf(cr, this._pixbuf, 0, 0)
, tells the
Cairo context to draw from the pixbuf, and then cr.paint()
does the actual
drawing. Finally, the method returns false
to let the draw call continue to
propagate. If you return true
, things like the blue highlight in the GTK
inspector won’t work.
It doesn’t show up in this simplified bit of code, but you should be aware that
GJS’s Cairo bindings convert all function names to camelCase. Thus, where you
might read set_source_rgb
and set_line_width
in the C documentation, it’s
actually setSourceRgb
and setLineWidth
in GJS. This only applies to Cairo.
Conclusion
If you’re making a GTK app and you can’t make what you want with the available widgets, don’t be afraid to write your own! It’s not very difficult and you’ll have full control to achieve your vision for your app, exactly the way you want it.