Custom widgets without .ui file

I am trying to port from GTK3 to GTK4 a custom widget of mine derived from GtkOverlay. I am able to do it via .ui template and gtk_widget_class_set_template_from_resource(), but the problem is that method is really for composite widgets, whereas mine is truly only a simple GtkOverlay that behaves in a particular way (I don’t really need a .ui file for programming one single GtkOverlay).

Is it possible to find a simple guide on how to create a GtkMyCustomThing widget as a subclass of an official GTK4 widget without using .ui files?

Do what your apparently already doing, but without calling any template methods :slight_smile:

The “official guide” is the GObject tutorial.

Though, of course, not every GTK widget can be derived: anything that is documented as a “final” class is not derivable—and GtkOverlay is a final type.

In general, you should not derive GTK widgets directly. The typical suggestion is to derive from GtkWidget and then pack your widgets into your custom widget.

Thanks both @zbrown and @ebassi for your answers. Unfortunately I am still confused. Therefore I will try to be more precise.

Back then (GTK3) I needed something quite similar to GtkFlowBox, but without aligning things in columns – I really wanted a “pure” flow of widgets. So I created my own GtkFlow custom widget, derived from GtkOverlay. I will post the code below. The result of the test will look like the following picture:

gtk-flow-example

(please check also the effect of resizing the window)

The GTK3 code was also a draft, because I knew I would eventually have to port it to GTK4 – but it was a functioning draft.

Here is the code:

gtk_flow.h:

/*  -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*-  */

#ifndef __GTK_FLOW_H__
#define __GTK_FLOW_H__


#include <glib.h>
#include <glib-object.h>
#include <gtk/gtk.h>

G_BEGIN_DECLS

#define GTK_FLOW_TYPE \
	(gtk_flow_get_type())

#define GTK_FLOW(obj) \
	(G_TYPE_CHECK_INSTANCE_CAST((obj), GTK_FLOW_TYPE, GtkFlow))

#define GTK_FLOW_CLASS(klass) \
	(G_TYPE_CHECK_CLASS_CAST((klass), GTK_FLOW_TYPE, GtkFlowClass))

#define IS_GTK_FLOW(obj) \
	(G_TYPE_CHECK_INSTANCE_TYPE((obj), GTK_FLOW_TYPE))

#define IS_GTK_FLOW_CLASS(klass) \
	(G_TYPE_CHECK_CLASS_TYPE((klass), GTK_FLOW_TYPE))

#define GTK_FLOW_GET_CLASS(obj) \
	(G_TYPE_INSTANCE_GET_CLASS ((obj), GTK_TYPE_FLOW, GtkFlowClass))


typedef struct _GtkFlow GtkFlow;

typedef struct _GtkFlowClass GtkFlowClass;


extern GType gtk_flow_get_type (void);

extern GtkWidget * gtk_flow_new (void);

extern void gtk_flow_add_widget (
	GtkFlow * flow,
	GtkWidget * widget
);

extern void gtk_flow_remove_widget (
	GtkFlow * flow,
	GtkWidget * widget
);

void gtk_flow_set_underlay (
	GtkFlow * flow,
	GtkWidget * widget
);

G_END_DECLS

#endif

gtk_flow.c:

/*  -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*-  */

#include <gtk/gtk.h>
#include "gtk_flow.h"

typedef struct {
	GtkWidget * widget;
	GdkRectangle allocation;
} GtkFlowAllocatedChild;

enum {
  TEST_SIGNAL = 0,
  LAST_SIGNAL
};

struct _GtkFlow {
	GtkOverlay the_flow;
	GtkAlign valign;
	GtkAlign halign;
	GtkRequisition flow_shape;
	GList * allocated_children;
};

struct _GtkFlowClass {
	GtkOverlayClass parent_class;
	void (* gtk_flow) (GtkFlow * flow);
	void (* set_underlay) (GtkFlow * flow, GtkWidget * widget);
};

static guint SIGNALS[LAST_SIGNAL] = { 0 };

typedef enum {
  PROP_HALIGN = 1,
  PROP_VALIGN,
  N_PROPERTIES
} GtkFlowProperty;

static GParamSpec * obj_properties[N_PROPERTIES] = { NULL };

static void gtk_flow_set_property (
	GObject * object,
	guint property_id,
	const GValue * value,
	GParamSpec * pspec
) {

	GtkFlow * self = GTK_FLOW(object);

	switch ((GtkFlowProperty) property_id) {

		case PROP_HALIGN:

			self->halign = g_value_get_enum(value);
			g_print ("halign: %s\n", self->halign);
			break;

		case PROP_VALIGN:

			self->valign = g_value_get_enum(value);
			g_print ("valign: %s\n", self->valign);
			break;

		default:

			/*  We don't have any other property...  */
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
			break;

	}

}

static void gtk_flow_get_property (
	GObject * object,
	guint property_id,
	GValue * value,
	GParamSpec * pspec
) {

	GtkFlow * self = GTK_FLOW(object);

	switch ((GtkFlowProperty) property_id) {

		case PROP_HALIGN:

			g_value_set_enum(value, self->halign);
			break;

		case PROP_VALIGN:

			g_value_set_enum(value, self->valign);
			break;

		default:

			/* We don't have any other property... */
			G_OBJECT_WARN_INVALID_PROPERTY_ID (object, property_id, pspec);
			break;
	}
}

static gboolean pos_callback (
	GtkFlow * flow,
	GtkWidget * child,
	GdkRectangle * allocation,
	gpointer v_unused_data
) {

	GList * iter;

	for (
		iter = flow->allocated_children;
			iter && ((GtkFlowAllocatedChild *) iter->data)->widget != child;
		iter = iter->next
	);

	if (!iter) {

		return FALSE;

	}

	GdkRectangle last_pos =
		iter->prev ?
			((GtkFlowAllocatedChild *) iter->prev->data)->allocation
		:
			(GdkRectangle) { 0 };

	GtkRequisition child_min_size;
	GtkRequisition parent_min_size;

	gint
		parent_w = gtk_widget_get_allocated_width(GTK_WIDGET(flow)),
		parent_h = gtk_widget_get_allocated_height(GTK_WIDGET(flow)),
		child_w = gtk_widget_get_allocated_width(child),
		child_h = gtk_widget_get_allocated_height(child);

	gtk_widget_get_preferred_size(child, &child_min_size, NULL);
	gtk_widget_get_preferred_size(GTK_WIDGET(flow), &parent_min_size, NULL);
	allocation->width = child_min_size.width;
	allocation->height = child_min_size.height;

	allocation->x =
		last_pos.x + last_pos.width + child_w > parent_w ?
			0
		:
			last_pos.x + last_pos.width;

	allocation->y =
		last_pos.x + last_pos.width + child_w > parent_w ?
			last_pos.y + last_pos.height
		:
			last_pos.y;

	((GtkFlowAllocatedChild *) iter->data)->allocation = *allocation;

	if (iter->prev) {

		if (allocation->width > flow->flow_shape.width) {

			flow->flow_shape.width = allocation->width;

			gtk_widget_set_size_request(
				GTK_WIDGET(flow),
				flow->flow_shape.width,
				parent_min_size.height
			);

		}

		if (allocation->y + allocation->height > flow->flow_shape.height) {

			flow->flow_shape.height = allocation->y + allocation->height;


		}

		gtk_widget_set_size_request(
			GTK_WIDGET(flow),
			flow->flow_shape.width,
			flow->flow_shape.height
		);

	} else {

		flow->flow_shape.width = allocation->width;
		flow->flow_shape.height = allocation->height;

	}

	g_signal_emit(G_OBJECT(flow), SIGNALS[TEST_SIGNAL], 0);
	return TRUE;


}

GtkWidget * gtk_flow_new () {

	return GTK_WIDGET(g_object_new(gtk_flow_get_type(), NULL));

}

void gtk_flow_remove_widget (
	GtkFlow * flow,
	GtkWidget * widget
) {

	// DO SOMETHING

}

void gtk_flow_set_underlay (
	GtkFlow * flow,
	GtkWidget * widget
) {

	GTK_FLOW_GET_CLASS(flow)->set_underlay(flow, widget);

}

void gtk_flow_add_widget (
	GtkFlow * flow,
	GtkWidget * widget
) {

	GtkFlowAllocatedChild * child_allocation = g_new(GtkFlowAllocatedChild, 1);

	*child_allocation = (GtkFlowAllocatedChild) {
		.widget = widget,
		.allocation = { 0 }
	};

	flow->allocated_children = g_list_append(
		flow->allocated_children,
		child_allocation
	);

	gtk_overlay_add_overlay(GTK_OVERLAY(flow), widget);

}

static void gtk_flow_clear (
	GtkFlow * flow
) {

	g_list_free_full(flow->allocated_children, g_free);

}

static void gtk_flow_free_memory (GObject * object) {

	g_list_free_full(g_steal_pointer(&GTK_FLOW(object)->allocated_children), g_free);

}

static void gtk_flow_class_init (
	GtkFlowClass * klass
) {

	GObjectClass * object_class;
	GtkWidgetClass * widget_class;
	GtkContainerClass * parent_class;

	object_class = (GObjectClass*) klass;
	widget_class = (GtkWidgetClass*) klass;
	parent_class = (GtkContainerClass *) klass;

	klass->set_underlay = (void (*) (GtkFlow *, GtkWidget *)) parent_class->add;
	object_class->dispose = gtk_flow_free_memory;

	parent_class->add = (void (*) (GtkContainer *, GtkWidget *)) gtk_flow_add_widget;
	parent_class->remove = (void (*) (GtkContainer *, GtkWidget *)) gtk_flow_remove_widget;

	object_class->set_property = gtk_flow_set_property;
	object_class->get_property = gtk_flow_get_property;

	obj_properties[PROP_HALIGN] = g_param_spec_enum(
		"halign",
		"Horizontal Alignment",
		"How to position in extra horizontal space",
		GTK_TYPE_ALIGN,
		GTK_ALIGN_START,
		G_PARAM_READWRITE
	);

	obj_properties[PROP_VALIGN] = g_param_spec_enum(
		"valign",
		"Vertical Alignment",
		"How to position in extra vertical space",
		GTK_TYPE_ALIGN,
		GTK_ALIGN_START,
		G_PARAM_READWRITE
	);

	g_object_class_install_properties(
		object_class,
		N_PROPERTIES,
		obj_properties
	);

	g_signal_override_class_handler(
		"get-child-position",
		GTK_TYPE_OVERLAY,
		G_CALLBACK(pos_callback)
	);

	// TODO: add a GList and two GdkRectangle to parameters
	SIGNALS[TEST_SIGNAL] = g_signal_new(
		"test-signal",
		G_TYPE_FROM_CLASS(klass),
		G_SIGNAL_RUN_FIRST | G_SIGNAL_ACTION,
		G_STRUCT_OFFSET (GtkFlowClass, gtk_flow),
		NULL, 
		NULL,                
		g_cclosure_marshal_VOID__VOID,
		G_TYPE_NONE, 0
	);

}

static void gtk_flow_init (
	GtkFlow * flow
) {

	flow->allocated_children = NULL;

}

GType gtk_flow_get_type (void) {

	static GType flow_type = 0;

	if (!flow_type)  {

		static const GTypeInfo flow_info = {
			sizeof(GtkFlowClass),
			(GBaseInitFunc) NULL,
			(GBaseFinalizeFunc) NULL,
			(GClassInitFunc) gtk_flow_class_init,
			(GClassFinalizeFunc) NULL,
			(gconstpointer) NULL,
			sizeof(GtkFlow),
			0,
			(GInstanceInitFunc) gtk_flow_init,
			(const GTypeValueTable *) NULL

		};

		flow_type = g_type_register_static(GTK_TYPE_OVERLAY, "GtkFlow", &flow_info, 0);
	}

	return flow_type;

}

test.c:

/*  -*- Mode: C; indent-tabs-mode: t; c-basic-offset: 4; tab-width: 4 -*-  */

#include <gtk/gtk.h>
#include "gtk_flow.h"

static void activate (
	GtkApplication * app,
	gpointer user_data
) {

	GtkWidget
		* const window = gtk_application_window_new(app),
		* const my_gtk_flow = gtk_flow_new();

	gtk_window_set_title(GTK_WINDOW(window), "Window");
	gtk_window_set_default_size(GTK_WINDOW(window), 200, 200);

	gtk_widget_set_valign(GTK_WIDGET(my_gtk_flow), GTK_ALIGN_FILL);

	gtk_flow_add_widget(GTK_FLOW(my_gtk_flow), gtk_label_new("One"));
	gtk_flow_add_widget(GTK_FLOW(my_gtk_flow), gtk_label_new("Two"));
	gtk_flow_add_widget(GTK_FLOW(my_gtk_flow), gtk_label_new("Three"));
	gtk_flow_add_widget(GTK_FLOW(my_gtk_flow), gtk_label_new("Four"));
	gtk_flow_add_widget(GTK_FLOW(my_gtk_flow), gtk_label_new("Five"));
	gtk_flow_add_widget(GTK_FLOW(my_gtk_flow), gtk_label_new("Six"));
	gtk_flow_add_widget(GTK_FLOW(my_gtk_flow), gtk_label_new("Seven"));
	gtk_flow_add_widget(GTK_FLOW(my_gtk_flow), gtk_label_new("Eight"));
	gtk_flow_add_widget(GTK_FLOW(my_gtk_flow), gtk_label_new("Nine"));
	gtk_flow_add_widget(GTK_FLOW(my_gtk_flow), gtk_label_new("Ten"));

	gtk_container_add(GTK_CONTAINER(window), my_gtk_flow);

	gtk_widget_show_all(window);

}

int main (
	int argc,
	char ** argv
) {

	int status;

	GtkApplication * app = gtk_application_new(
		"org.gtk.example",
		G_APPLICATION_DEFAULT_FLAGS
	);

	g_signal_connect(app, "activate", G_CALLBACK(activate), NULL);
	status = g_application_run(G_APPLICATION(app), argc, argv);
	g_object_unref(app);

	return status;

}

So the question is: How can I create my GtkFlow custom widget using GTK4?

You cannot directly inherit from GtkOverflow in GTK4. You’re also overloading the size allocation function, so I don’t really think you need an overlay to begin with. There are other bad bits of code in your “working” example:

  • you’re overloading the GTK namespace, something your should NEVER do
  • you’re not using the proper subclassing macros
  • you’re overriding GtkWidget’s halign and valign properties, for some reason
  • in your size allocate you’re setting the size request of the children, which is completely broken

In short, I really have no idea what your code is even trying to achieve.

If you want to write a layout manager that reflows children, then I recommend you derive from GtkLayoutManager so you can avoid the widget entirely; GtkOverlay is implemented with a layout manager, in any case. Then you can use the layout manager in any custom widget class you want.

@ebassi

I am pretty sure there are. As I said, it was only a first experiment (from a year ago, which I did not review), in which my first goal was to implement a “functioning” prototype, knowing that I would have to rewrite it in GTK4.

It’s pretty simple. I would like a widget similar to GtkFlowBox, but in which widgets in different rows are not aligned in the same column. In many ways GtkFlowBox looks like a table. But I don’t want a table, I want a pure flow (more or less like the letters of the text that you are reading right now). If there was a native widget able to do that I would be pretty happy with it.

That sounds a bit like the hard path…

EDIT 1: I believe that the main problem is that while I kind of knew how to “subclass” in GTK3, I do not actually know how to “subclass” in GTK4. So how to subclass GtkLayoutManager is something I would not know how to do right now.

EDIT 2: To explain what I would like to achieve specifically in my program, it is something similar to GitHub’s “Topics” form field:

what-i-want

I need that for letting the user specify custom keywords.

Subclassing is exactly the same in both GTK3 and GTK4: the underpinning of that operation is in GObject.

The issue is that most widgets in GTK4 are final, i.e. non-derivable. You need to derive either from a derivable widget, or from GtkWidget itself, and add children widgets to it.

I started the previous reply with an example of a GtkWidget with a single GtkOverlay as a child, when I realised that you’re really not using GtkOverlay at all—you’re overriding the only thing it does, which is the size allocation of its children. This means you don’t need a GtkOverlay. That’s why I recommended implementing a layout manager.

The other option for you is to derive from GtkWidget and then override the size_allocate and the measure virtual functions, and implement your own layout container. It’s pretty much the same thing as implementing a GtkLayoutManager, except you’re writing a custom widget instead.

In both cases, you will need to measure each child inside the measure virtual function, and allocate each child in the allocation virtual function.

That’s a tagged entry, which is another thing entirely, and it’s not a trivial widget at all.

There’s a draft merge request for libadwaita that tries to implement that design, but it’ll need a reflowing layout manager as well.

Okay, thank you, I will try. I think I got confused by the fact that there is no GtkContainerClass data type in GTK4, and that was normally used while subclassing in GTK3. But I understand now that I won’t need that.

I didn’t post my entire draft (I posted only the GtkFlow part), but the “tagged entry” part was not too hard to implement when I have a GtkFlow.

Oh thank you! I will definitely have a look and in case grab something from that.

It sounds like we need the same thing. It might be worth joining the efforts.

@ebassi

I am trying to derive my custom GTK4 widget from GtkLayoutManager as you suggested. To do that I started to study the code of GtkOverlay as an example of widget derived from GtkLayoutManager.

While reading the code I encountered these two identifiers, _gtk_marshal_BOOLEAN__OBJECT_BOXED and _gtk_marshal_BOOLEAN__OBJECT_BOXEDv, which I could not find defined anywhere in the whole GTK project.

What are they? Where can I find their definitions?

This topic was automatically closed 30 days after the last reply. New replies are no longer allowed.