How do I use ListModel's with custom objects in GTK4?

In Gtk3, I had a ListStore (I am using Python), which had two columns. The combo box that was using the ListStore would display the data from one of the columns, while the other would be used in some other processig.

With Gtk4, that ability has been changed and I can’t wrap my head around how I can replicate it.

From what I can tell, the new ListStore accepts only a single object type. If I want to store something that has two properties, I have to create my own type. OK, fine. I can create a GObject subclass that holds what I need.

However, how do I display the data? There seem to be a pile of new widgets, which have made this simple taks much more complicated.

Does anyone know of a tutorial or examples on how to create a DropDown using custom types?

Refer https://blog.gtk.org/tag/lists/.

It’s not any more complicated that GtkTreeView, GtkTreeViewColumn, and GtkCellRenderer:

  • GTK4 models only contains rows of GObject instances, and those objects expose their data as GObject properties
  • GTK4 list widgets are populated from the models, and use factories to generate row widgets from row items
  • Row widgets bind their state to the row items via properties and methods
  • List widgets are “recycling views”: widgets are created when needed, and updated when the visible area changes

A very simple DropDown example that takes the ISO codes country data and turns it into a drop down:

import gi
import json

gi.require_version('Adw', '1')
gi.require_version('Gtk', '4.0')

from gi.repository import Adw, Gio, GObject, Gtk  # noqa


class Country(GObject.Object):
    """A simple data type that contains a country code and its name"""
    __gtype_name__ = 'Country'

    def __init__(self, country_id, country_name):
        super().__init__()

        self._country_id = country_id
        self._country_name = country_name

    @GObject.Property(type=str)
    def country_id(self):
        return self._country_id

    @GObject.Property(type=str)
    def country_name(self):
        return self._country_name

    def __repr__(self):
        return f"Country(country_id={self.country_id}, country_name={self.country_name})"  # noqa

    def __str__(self):
        return f"{self._country_name} ({self._country_id})"


class ExampleWindow(Gtk.ApplicationWindow):
    def __init__(self, app):
        super().__init__(application=app, title="DropDown", default_width=300)

        # Load the ISO country codes
        nodes = {}
        with open("/usr/share/iso-codes/json/iso_3166-1.json", "r") as f:
            iso3661 = json.load(f)
            for country in iso3661.get("3166-1", []):
                nodes[country.get("alpha_2").lower()] = country.get("name")

        # Create the model from the list of countries
        self.model = Gio.ListStore(item_type=Country)
        for n in nodes.keys():
            self.model.append(Country(country_id=n, country_name=nodes[n]))

        # Factories are used to create the UI mapping to the model
        factory = Gtk.SignalListItemFactory()
        factory.connect("setup", self._on_factory_setup)
        factory.connect("bind", self._on_factory_bind)

        self.dd = Gtk.DropDown(model=self.model, factory=factory, hexpand=True)
        self.dd.connect("notify::selected-item", self._on_selected_item_notify)

        box = Gtk.Box(spacing=12, hexpand=True, vexpand=True, valign=Gtk.Align.CENTER)
        box.props.margin_start = 12
        box.props.margin_end = 12
        box.props.margin_top = 6
        box.props.margin_bottom = 6
        box.append(Gtk.Label(label="Select Country:"))
        box.append(self.dd)

        self.set_child(box)

    def _on_factory_setup(self, factory, list_item):
        """Set up the row widget"""
        label = Gtk.Label()
        list_item.set_child(label)

    def _on_factory_bind(self, factory, list_item):
        """Bind a row item to its corresponding row widget; we use a property
        binding to connect the Country object's country_name property to the
        Label's label property; we also use a transformation function to show
        that it's possible to operate on the binding at run time."""
        label = list_item.get_child()
        country = list_item.get_item()
        country.bind_property("country_name",
                              label, "label",
                              GObject.BindingFlags.SYNC_CREATE,
                              self._transform_to_country)

    def _on_selected_item_notify(self, dropdown, _):
        """Print out the selected item"""
        country = dropdown.get_selected_item()
        print(f"Selected item: {country}")

    def _transform_to_country(self, binding, src_value):
        """Transforms the Country object into a string"""
        return str(binding.dup_source())


class ExampleApp(Adw.Application):
    def __init__(self):
        super().__init__()
        self.window = None

    def do_activate(self):
        if self.window is None:
            self.window = ExampleWindow(self)
        self.window.present()


app = ExampleApp()
app.run([])
1 Like

First and foremost, thank you for the example. It does make things clearer.

Having said that, when it comes to Gtk.ComboBox’es, you can’t tell me that your example is the same as (even after striping the code for the “complete” application):

model = Gtk.ListSTore(GObject.TYPE_UINT64, str)
for i, s in enumerate(("this", "that", "the", "other")):
    model.append((i, o))
combo = Gtk.ComboBox.new_with_model_and_entry(model)
combo.set_entry_test_column(1)

Of course, tree views are more complex but all I wanted is the equivalent to a ComboBox. :smile:

I never said it would be.

GtkDropDown does not have an entry by design, and it does not have the combinatorial explosion of features that GtkComboBox has, which made the widget barely maintainable as it was.

If you are using an entry to provide some search functionality, GtkDropDown has an equivalent that is shown in the popup if you:

An example would be to take the code above, and add:

        def get_country_name(country: Country) -> str:
            return str(country)

        self.dd = Gtk.DropDown(model=self.model, factory=factory, hexpand=True)
        self.dd.connect("notify::selected-item", self._on_selected_item_notify)
        self.dd.set_expression(Gtk.PropertyExpression.new(Country, None, 'country_name'))
        self.dd.set_enable_search(True)

Again, thank you for all your help. Hopefully, with time I’ll get a better understanding of the structure and relationships between all of these parts.

First time I ported a treeview to gtk4 it took me almost a friggin week!! You have already got answers for basically everything but here comes a “dropdown for dummies” example. :slight_smile: So basically how this works is that you update your model (ie gtkstringlist) and then the factory starts working. In the “setup” callback you create your row and in the “bind” callback you set values. This is my understanding of it all.

import os, gi
gi.require_version('Gtk', '4.0')
from gi.repository import Gtk

class Main:
  def __init__(self, app):
    self.mainwin = Gtk.ApplicationWindow.new(app)
    self.box = Gtk.Box.new(Gtk.Orientation.VERTICAL, 0)
    button = Gtk.Button.new_with_label("Populate")
    button.connect("clicked", self.button_cb)
    self.box.append(button)
    button = Gtk.Button.new_with_label("Empty")
    button.connect("clicked", self.button_cb)
    self.box.append(button)

    self.stringlist = Gtk.StringList.new(["Start"])
    self.dropdown = Gtk.DropDown.new(self.stringlist)
    self.factory = Gtk.SignalListItemFactory.new()
    self.factory.connect("setup", self.factory_setup)
    self.factory.connect("bind", self.factory_bind)
    self.dropdown.set_factory(self.factory)
    self.dropdown.connect("notify::selected", self.dropdown_cb)

    self.box.append(self.dropdown)
    self.mainwin.set_child(self.box)
    self.mainwin.present()

  def factory_setup(self, _factory, _item):
    _label = Gtk.Label.new()
    _item.set_child(_label)

  def factory_bind(self, _factory, _item):
    _str = _item.get_item().get_string()
    _item.get_child().set_text(_str)

  def button_cb(self, _button):
    if _button.get_label() == "Populate":
      _list = os.listdir()
    else:
      _list = []
    self.stringlist.splice(0, self.stringlist.get_n_items(), _list)

  def dropdown_cb(self, _dropdown, _2):
    print(self.stringlist.get_string(_dropdown.get_selected()))

app = Gtk.Application()
app.connect('activate', Main)
app.run(None)