MVVM implementation for the Tkinter GUI Python package.
Supports generating interfaces from XML files.
You can find the complete Calculator sample in the samples folder.
The View in XML
<Window xmlns="https://jonaskloster.dk/xml/tkmvvm"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://raw.githubusercontent.com/Joklost/tkmvvm/master/tkmvvm/schema/tkmvvm.xsd"
height="230" width="150" title="Calculator">
<Entry grid-sticky="nesw"
grid-row="0"
grid-column="0"
grid-columnspan="4"
state="readonly"
text-binding="{{computation}}"
justify="right"/>
<Button width="2"
grid-sticky="nesw"
grid-row="1"
grid-column="1"
command="{clear}"
text="C"/>
...
</Window>
Binding and command attributes attempt to match to a method from the ViewModel.
The text-binding="{{computation}}"
attribute will create a 2-way binding with the computation
property, and the command="{clear}"
attribute means that the clear
method will be invoked when this button is clicked.
The ViewModel
class CalculatorViewModel(ViewModel):
_entry = ""
_computation = ""
def __init__(self):
super(CalculatorViewModel, self).__init__(None)
@property
def entry(self):
return self._entry
@entry.setter
def entry(self, value):
self._entry = value
self.on_property_changed('entry')
@property
def computation(self):
return self._computation
@computation.setter
def computation(self, value):
self._computation = value
self.on_property_changed('computation')
def clear(self):
self.entry = ""
self.computation = ""
Adding a Model to the ViewModel is not strictly necessary. The calculator for example has no data to store, which is why no Model is given. Calling the on_property_changed('computation')
method will force the View to update anything bound to the 'computation' property, and calling the method without any parameters will force the View to update all data bindings.
The View class
class CalculatorView(View):
def __init__(self, parent: tkinter.Tk, context: tkmvvm.viewmodel.ViewModel, height: int, width: int):
super().__init__(parent, context, height, width)
self.window = tkinter.Toplevel(self.parent)
self.center_window(self.window)
# enable quitting when pressing the exit button
self.window.protocol('WM_DELETE_WINDOW', self.window.quit)
Currently, a View class is needed in order to load a View from XML, but in the future, this will not be needed.
The Model
class CalculatorModel(Model):
computation = 0
computation_history = []
The Model is used when you have some data that you want to store, and the Model class will support serialization and deserialization in the future.
Combining Everything
def main():
view_model = CalculatorViewModel()
root = Tk()
root.withdraw()
view = CalculatorView(root, view_model, 600, 400)
view.load_xml('view.xml')
view.resizeable(False, False)
view.mainloop()
if __name__ == '__main__':
main()
These are the widgets with support for either 1-way or 2-way data binding (depending on the widget).
- Entry
- Button
- Listbox
- Checkbutton (not working as intended)
- Label
- Add "state"-binding to widgets to control state from program
- Implement data-binding for more widgets (rest of Tk, and all of ttk)
- Remove the need to creating a View class, when defining the view using XML
- Add serialization and deserialization to Models.