Gtk.Widget.get_allocated_size() returns zero-size Gdk.Rectangle for object created after Gtk.Window.show_all() is called

The following example creates a center-aligned button in a Window. Two seconds later this button is deleted and replaced by another button.

import gi

gi.require_version("Gtk", "3.0")
from gi.repository import Gtk, GLib


class ButtonWindow(Gtk.Window):
    def __init__(self):
        super().__init__()
        self.set_default_size(500, 200)
        
        button = Gtk.Button.new_with_label("one")
        button.set_halign(Gtk.Align.CENTER)
        button.set_valign(Gtk.Align.CENTER)
        self.add(button)
        
        self.timeout_id = GLib.timeout_add(2000, self.on_timeout, button)

    def on_timeout(self, button):
        rect, _ = button.get_allocated_size()
        print(rect.x, rect.y, rect.width, rect.height)
        
        self.remove(button)
        button = Gtk.Button.new_with_label("two")
        button.set_halign(Gtk.Align.CENTER)
        button.set_valign(Gtk.Align.CENTER)
        self.add(button)
        self.show_all()
        
        rect, _ = button.get_allocated_size()
        print(rect.x, rect.y, rect.width, rect.height)
    
win = ButtonWindow()
win.connect("destroy", Gtk.main_quit)
win.show_all()
Gtk.main()

Gtk.Widget.get_allocated_size() returns an incorrect size rect 0 0 500 200 (size of the Window) for the old button but 0 0 0 0 for the new button even after Gtk.Window.show_all() is called.

What am I doing wrong here?

Update

The behaviour is identical in the native GTK3 API

#include <gtk/gtk.h>

guint timeout_id;
GtkWidget *window;
GtkWidget *button;

static gboolean on_timeout(gpointer user_data)
{
	int baseline;
	GtkAllocation rect;
	GtkWidget *button2;
	
	gtk_widget_get_allocated_size(button, &rect, &baseline);
	printf("x: %i y: %i width: %i height: %i\n", rect.x, rect.y, rect.width, rect.height);
	
	gtk_container_remove(GTK_CONTAINER(window), button);
	//g_object_unref (button);  // segfault???
	
	button2 = gtk_button_new_with_label ("two");
	gtk_widget_set_size_request (button2, 200, 100);
	gtk_widget_set_halign (button2, GTK_ALIGN_CENTER);
	gtk_widget_set_valign (button2, GTK_ALIGN_CENTER);
	gtk_container_add (GTK_CONTAINER(window), button2);
	gtk_widget_show_all (window);
	
	gtk_widget_get_allocated_size(button2, &rect, &baseline);
	printf("x: %i y: %i width: %i height: %i\n", rect.x, rect.y, rect.width, rect.height);
	
	g_source_remove(timeout_id);
	return 0;
}

static void activate(GtkApplication* app, gpointer user_data)
{
  window = gtk_application_window_new (app);
  gtk_window_set_default_size (GTK_WINDOW (window), 500, 200);
  
  button = gtk_button_new_with_label ("one");
  gtk_widget_set_size_request (button, 100, 50);
  gtk_widget_set_halign (button, GTK_ALIGN_CENTER);
  gtk_widget_set_valign (button, GTK_ALIGN_CENTER);
  gtk_container_add (GTK_CONTAINER(window), button);
  gtk_widget_show_all (window);
  
  timeout_id = g_timeout_add(2000, on_timeout, NULL);
}

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

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

The output

x: 0 y: 0 width: 500 height: 200
x: 0 y: 0 width: 0 height: 0

Inspired by the output of Accerciser’s displaying the correct extents

I tried the following:

def on_timeout(self, button):
	# Get dimensions of old button. WORKS!
	accessible = button.get_accessible()
	(x, y, width, height) = accessible.get_extents(Atk.CoordType.WINDOW)
	print(x, y, width, height)
	
	#rect, _ = button.get_allocated_size()
	#print(rect.x, rect.y, rect.width, rect.height)
	#print()
	
	self.remove(button)
	self.set_default_size(500, 200)
	button = Gtk.Button.new_with_label("two")
	button.set_size_request(200, 100)
	button.set_halign(Gtk.Align.CENTER)
	button.set_valign(Gtk.Align.CENTER)
	button.set_visible(True)
	self.add(button)
	self.show_all()
	
	#time.sleep(1) # Does not help
	
	# Get dimensions of new button the expected way. FAILS!
	# Probably too soon after object was created.
	accessible = button.get_accessible()
	(x, y, width, height) = accessible.get_extents(Atk.CoordType.WINDOW)
	print(x, y, width, height)
	
	# Getting the dimensions of new button via a timeout 1 millisecond later, SUCCEEDS
	GLib.timeout_add(1, self.on_timeout2, button)
	
def on_timeout2(self, button):
	accessible = button.get_accessible()
	(x, y, width, height) = accessible.get_extents(Atk.CoordType.WINDOW)
	print(x, y, width, height)
	print()

Result

200 75 100 50
-2147483648 -2147483648 1 1
150 50 200 100

Oddly, moving on_timeout2 forward as shown below

# FAILS
GLib.timeout_add(1, self.on_timeout2, button)

# SUCCEEDS
accessible = button.get_accessible()
(x, y, width, height) = accessible.get_extents(Atk.CoordType.WINDOW)
print(x, y, width, height)

Produced the same result. The first attempt failed, the second succeeded.

What is going on???

Just some things about GTK you may want to know:

  • The size of any widget is determined recursively by its parent container(s)
  • The topmost container is usually a window
  • Windows cannot know their size until the window system gives GTK a surface to draw on
  • When creating/removing/updating widgets, GTK will try to queue any new window system events until at least the next return to the event loop
  • GTK is single threaded and uses a polling event loop, so calling sleep will stall the whole program, including stalling any events to/from the window system

With those in mind, in GTK3 you should be able to get the correct size by connecting to the size-allocate signal and getting the size in your signal handler. (Note that in GTK4 the signal no longer exists, so you have to use other methods there)

1 Like

Do you mean like this?

class MyButton(Gtk.Button):
    def __init__(self, label, width, height):
        super().__init__()
        self.set_label(label)
        self.set_size_request(width, height)
        self.set_halign(Gtk.Align.CENTER)
        self.set_valign(Gtk.Align.CENTER)
        self.connect("size-allocate", self.my_size)
        
    def my_size(self, obj, user_data):
        rect, _ = obj.get_allocated_size()
        print(rect.x, rect.y, rect.width, rect.height)
        
class ButtonWindow(Gtk.Window):
    def __init__(self):
        super().__init__()
        self.set_default_size(500, 200)
        
        button = MyButton("one", 100, 50)
        self.add(button)

    def on_timeout(self, button):       
        self.remove(button)
        self.set_default_size(500, 200)
        button = MyButton("two", 200, 100)
        self.add(button)

I get 0 0 500 200 (the window size) both times.

Looks like both buttons expand to fill the window; you could try setting their hexpand and vexpand to FALSE.

The real question is: why are you trying to get, or override, the size of widgets? What are you trying to achieve?

Querying the size of a widget outside the widget’s layout phase is typically an indication of some fundamental misunderstanding of how GTK works.

Both buttons do not expand to fill the window as you will see when you run either version of the program.

That may be the case.

What I am trying to do is to draw connector lines between a series buttons in an overriden Gtk.Widget.draw(ctx). Therefore I need to know the extents of the buttons in order to determine the starts and ends of the connectors.

Solution.

The rect (dimensions) of a widget are passed in to the size-allocate signal hander.

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