Making widgets appear in two locations

I am trying to create an interface where widgets appear in two notebook pages. One page is a “favorites” page for quick access to a subset of tools found in the other pages. I want to provide users with an option to show favorite tools in their original page as well. Currently, I am achieving this with the notebook page change signal and removing then adding the affected widgets, e.g.:

// Simplified code.
src_container.remove(tool);
dest_container.pack_start(tool, false, false);

Although this works, it is slow if the tool is large (many child widgets). One particularly large tool takes 1/8th of a second to remove and another 1/8th to add. In the extreme case where all tools must move, it takes nearly half a second to complete the process. In typical use, there is a small but noticeable lag when switching notebook pages.

Can the speed be increased? If not, is there a better way to achieve the same result?

Using GTK 3 with gtkmm in RawTherapee

void Gtk::widget::reparent (Widget& new_parent)
Moves a widget from one Gtk::Container to another, handling
reference count issues to avoid destroying the widget.

Thanks for the suggestion. I tried using reparent, but it did not improve the speed. Regarding the reference counting, shouldn’t remove followed by pack_start work too? The API documentation implies the remove method maintains the reference count.

If widget is managed with Gtk::manage(), and you don’t want to use widget again then you should delete widget, because there will no longer be any parent container to delete it automatically.

It seems you are missing some vital information here, above all how you
are measuring this delay. My wild guesses:

  • you really have thousands of widgets;
  • you have slow signal handlers triggered by this operation, e.g.
    connected to the parent-set signal of the children;
  • what you are seeing are visual artefacts due to the queued drawing;
  • gtkmm is really doing something weird under the hood.

On my computer, reparenting 200 GtkLabels takes between 5 and 20
milliseconds:

/* reparenting.c
 * gcc -o reparenting $(pkg-config --cflags --libs gtk+-3.0) reparenting.c
 */

#include <gtk/gtk.h>

static void move_children(GtkNotebook *notebook)
{
    static gint from_nth = 0;
    static gint to_nth = 1;
    gint tmp_nth;
    GtkWidget *from, *to, *widget;
    GList *children, *child;
    gint64 elapsed;

    elapsed = -g_get_monotonic_time();

    from = gtk_notebook_get_nth_page(notebook, from_nth);
    to = gtk_notebook_get_nth_page(notebook, to_nth);

    children = gtk_container_get_children(GTK_CONTAINER(from));
    for (child = children; child != NULL; child = child->next) {
        widget = GTK_WIDGET(child->data);
        /* or gtk_widget_reparent(widget, to) */
        g_object_ref(widget);
        gtk_container_remove(GTK_CONTAINER(from), widget);
        gtk_box_pack_start(GTK_BOX(to), widget, FALSE, FALSE ,0);
        g_object_unref(widget);
    }
    g_list_free(children);

    tmp_nth = to_nth;
    to_nth = from_nth;
    from_nth = tmp_nth;

    elapsed += g_get_monotonic_time();
    g_print("It took %" G_GINT64_FORMAT " microseconds\n", elapsed);
}


int main()
{
    GtkWidget *window, *notebook, *box, *page, *label, *button;
    gint n;

    gtk_init(NULL, NULL);

    window = gtk_window_new(GTK_WINDOW_TOPLEVEL);

    box = gtk_box_new(GTK_ORIENTATION_VERTICAL, 6);
    gtk_container_add(GTK_CONTAINER(window), box);

    notebook = gtk_notebook_new();
    gtk_container_add(GTK_CONTAINER(box), notebook);

    page = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    for (n = 0; n < 200; ++n) {
        gtk_container_add(GTK_CONTAINER(page), gtk_label_new("x"));
    }
    gtk_notebook_append_page(GTK_NOTEBOOK(notebook), page, gtk_label_new("First"));

    page = gtk_box_new(GTK_ORIENTATION_HORIZONTAL, 0);
    gtk_notebook_append_page(GTK_NOTEBOOK(notebook), page, gtk_label_new("Second"));

    button = gtk_button_new_with_label("Move children to other page");
    g_signal_connect_swapped(button, "clicked", G_CALLBACK(move_children), notebook);
    gtk_container_add(GTK_CONTAINER(box), button);

    gtk_widget_show_all(window);
    gtk_main();

    return 0;
}

Ciao.

There aren’t many widgets being reparented (less than 100), but some widgets have many children mostly nested in other widgets. The largest one does look like it has hundreds if not thousands of widgets. There are no connections to parent changed (or similar) signals, though I’m not 100% sure due to the size of the code base.

I measured the time by taking the difference between two clocks immediately before and after the individual function calls. remove and pack_start take roughly the same amount of time to complete for a given widget. The initial pack_start during startup and before show_all are near instantaneous. Other interactions with the notebook and its children are smooth (scrolling, hide/show widgets, resizing, etc.).

I will do some more testing and try to create a small example and report my findings. This is part of the actual code if anyone is curious. Please let me know if I can provide additional helpful information.

There aren’t many widgets being reparented (less than 100), but some widgets
have many children mostly nested in other widgets. The largest one does look
like it has hundreds if not thousands of widgets.

Reparenting is basically a gtk_container_remove followed by a
gtk_container_add: it does not affect the children (although it can
drastically affect the drawing phase).


[This](GitHub - Beep6581/RawTherapee at a7010d25cd88ce15b558d3c20fb61868798e555d
b61868798e555d/rtgui/toolpanelcoord.cc#L797-L869) is part of the actual code
if anyone is curious.

I don’t know what the above code does but I guess 70 LOCs should do much
more than just reparenting. And near the end there is a recursion, so it
is effectively traversing the whole widget tree: if there are hundreds
of widgets, the delay can be justified.

Ciao.

I may be misunderstanding something here, but if your widgets are factored into their own classes, you should be able to create two instances of the same widget? That seems to be the simplest option and should be much faster than creating and destroying them.

Yes, the function I pointed to does more than just reparenting. There is logic to determine which widgets to move. However, the bottlenecks are the remove function and pack_start within the addPanel function. Just a single call to one of those gtkmm functions can take up to 1/8th of a second to complete. GTK doesn’t draw anything I those functions, does it? I will try to create a small and clearer example to demonstrate this.

The widgets are indeed defined as classes. They contain many sliders, toggles, adjustable custom widgets, etc. and the model is not cleanly separated from the view. I was hoping that by going with the reparenting strategy, I can avoid the tedious task of keeping all the stateful widgets in sync.

I made a simplified example of what I’m doing.

g++ main.cpp -o app $(pkg-config gtkmm-3.0 --cflags --libs)

// main.cpp
#include <chrono>
#include <iostream>

#include <gtkmm.h>


using namespace std::chrono;
using clk = std::chrono::high_resolution_clock;


void printTimeSpan(const char *description, clk::time_point start, clk::time_point end) {
    std::cout
        << description << " took "
        << 1000 * duration_cast<duration<double>>(end - start).count()
        << "ms"
        << std::endl;
}

void onPageChange(guint page_number, Gtk::Box *page1, Gtk::Box *page2, Gtk::Widget *child) {
    Gtk::Container *old_page;
    Gtk::Box *new_page;

    old_page = child->get_parent();
    new_page = (page_number == 0) ? page1 : page2;

    auto start = clk::now();
    old_page->remove(*child);
    auto end = clk::now();
    printTimeSpan("Gtk::Container::remove", start, end);

    start = clk::now();
    new_page->pack_start(*child);
    end = clk::now();
    printTimeSpan("Gtk::Box::pack_start", start, end);
}

int main(int argc, char *argv[]) {
    auto app = Gtk::Application::create(argc, argv, "org.example.app");
    Gtk::Window window;
    Gtk::Notebook notebook;
    Gtk::ScrolledWindow sw1, sw2;
    Gtk::Box page1, page2;
    Gtk::Expander child("Tool");
    Gtk::Box child_contents(Gtk::Orientation::ORIENTATION_VERTICAL);

    // Window with notebook.
    window.set_default_size(640, 480);
    window.add(notebook);

    // Notebook with 2 pages and a connection to the page switch signal.
    notebook.append_page(sw1, "Page 1");
    notebook.append_page(sw2, "Page 2");
    notebook.signal_switch_page().connect(
            sigc::bind(sigc::hide<0>(sigc::ptr_fun(&onPageChange)), &page1, &page2, &child));

    // The actual pages are wrapped in scrolled windows.
    sw1.add(page1);
    sw2.add(page2);

    // Add the child to the first page.
    page1.pack_start(child, true, true);

    // Add widgets to the child.
    child.add(child_contents);
    for (int i = 0; i < 2000; i++) {
        auto widget = Gtk::manage(new Gtk::Scale(Gtk::Orientation::ORIENTATION_HORIZONTAL));
        widget->set_range(0, 10);
        widget->set_value(i % 11);
        child_contents.pack_start(*widget, true, true);
    }

    notebook.show_all();

    return app->run(window);
}

This is a two page notebook of scrolled windows. Each window has a box which holds the child I want to move. The child is an expander (although we use a custom expander implementation in the actual code) containing a box of many widgets. The time it takes to move the expander between the two pages grows as the number of widgets inside increases. For 2000 widgets, remove usually takes 35ms and pack_start takes 30ms. Before the window is shown, both take roughly 1ms. Using the C API does not result in an improvement.

This is not a perfect representation of the real thing. The simplified example is faster when calling remove and even more so with pack_start. I can only match the worst-case latency by using 5000 widgets. At this point, changing the window width becomes laggy which is not the case with the real notebook. Collapsing the expander has no effect on the speed in the example but cuts the reparenting time in half for the real expander. Interestingly, collapsing the expander immediately before reparenting does not help.

The problem you’re having is caused by too many CSS invalidations; containers with more than a few hundred direct children will have issues because the CSS state tracker needs to identify and match rules like “first-child”, “last-child”, and “nth-child”.

The simplest solution is: don’t use 5000 widgets. Also, don’t reparent complex hierarchies: abstract your notebook page into a class and instantiate it on every page you need it. Reparenting is pointless. If you need to reparent because your widgets represent some state, then use a model/view split, and instantiate multiple views of the same data.

For GTK3 we still recommend using GtkTreeView for tabular data, as the cell renderers are not widgets and thus have no real CSS state tracking. For GTK4 we have widgets like GtkListView and GtkGridView, which are model-based and recycle visible items so they can scale up to millions of elements.

Ahh, that makes sense. Thought the real widgets don’t have more than a few direct children, I suppose there is a similar caveat for widgets containing hundreds of indirect children?

In that case, the only real solution is good design that separates view and model. Reparenting is just a suboptimal workaround/hack. Thank to everyone for the help.

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