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?
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([])
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.
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:
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. 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.