The background to this question (and my overall goal) is to structure a Python GTK application in a nice way. I am trying to bind widget properties to model properties using GTK's bidirectional data bindings.
My expectation is that the bidirectional binding should keep two properties in sync. I find instead that changes propagate in one direction only, even though I am using the GObject.BindingFlags.BIDIRECTIONAL flag. I created the following minimal example and the failing test case test_widget_syncs_to_model to illustrate the problem. Note that in a more realistic example, the model object could be an instance of Gtk.Application and the widget object could be an instance of Gtk.Entry.
import gi
gi.require_version('Gtk', '3.0')
from gi.repository import Gtk, GObject
import unittest
class Obj(GObject.Object):
"""A very simple GObject with a `txt` property."""
name = "default"
txt = GObject.Property(type=str, default="default")
def __init__(self, name, *args, **kwargs):
super().__init__(*args, **kwargs)
self.name = name
self.connect("notify", self.log)
def log(self, source, parameter_name):
print(
f"The '{self.name}' object received a notify event, "
f"its txt now is '{self.txt}'."
)
class TestBindings(unittest.TestCase):
def setUp(self):
"""Sets up a bidirectional binding between a model and a widget"""
print(f"\n\n{self.id()}")
self.model = Obj("model")
self.widget = Obj("widget")
self.model.bind_property(
"txt", self.widget, "txt", flags=GObject.BindingFlags.BIDIRECTIONAL
)
@unittest.skip("suceeds")
def test_properties_are_found(self):
"""Verifies that the `txt` properties are correctly set up."""
for obj in [self.model, self.widget]:
self.assertIsNotNone(obj.find_property("txt"))
@unittest.skip("suceeds")
def test_model_syncs_to_widget(self, data="hello"):
"""Verifies that model changes propagate to the widget"""
self.model.txt = data
self.assertEqual(self.widget.txt, data)
def test_widget_syncs_to_model(self, data="world"):
"""Verifies that widget changes propagate back into the model"""
self.widget.txt = data
self.assertEqual(self.widget.txt, data) # SUCCEEDS
self.assertEqual(self.model.txt, data) # FAILS
if __name__ == "__main__":
unittest.main()
The above program outputs:
ssF
======================================================================
FAIL: test_widget_syncs_to_model (__main__.TestBindings)
Verifies that widget changes propagate back into the model
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/jh/.config/JetBrains/PyCharmCE2021.1/scratches/scratch_14.py", line 52, in test_widget_syncs_to_model
self.assertEqual(self.model.txt, data) # FAILS
AssertionError: 'default' != 'world'
- default
+ world
----------------------------------------------------------------------
Ran 3 tests in 0.001s
FAILED (failures=1, skipped=2)
__main__.TestBindings.test_widget_syncs_to_model
The 'widget' object received a notify event, its txt now is 'world'.
Process finished with exit code 1
My specific question is, how can I get the bidirectional data bindings to work?... I would be glad if someone could fix my example or provide another working example.
In a broader sense, are bidirectional bindings the way to go for syncing UI state and model state in a well-structured Python GTK application? What is the intended and well-supported way to do this? thanks!
GObject::notifysignal being emitted for yourtxtproperty when it’s changed? Property bindings rely on thenotifysignal to work.print("received notify event", self.txt)in thelog()function. Theparamargument is actually aGParamSpecinstance, which describes the property type, but not its current value. So I think thetxtwhich is being printed is actually just the property name.