Last active
September 29, 2021 13:16
-
-
Save ikus060/0ffd017776cdf3ff2b8d88a58d75f731 to your computer and use it in GitHub Desktop.
Proof-of-concept to create Tkinter graphical user interface in a declarative way using XML template.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import tkinter | |
from html.parser import HTMLParser | |
from tkinter import TclError, ttk | |
import yaml | |
_components = {} | |
class Label(ttk.Label): | |
""" | |
Custom Label to easily support text wrapping using `wrap="true"` in layout. | |
""" | |
def __init__(self, *args, **kwargs): | |
wrap = kwargs.pop('wrap', 'false') | |
super().__init__(*args, **kwargs) | |
if wrap.lower() in ['true', '1']: | |
self.bind('<Configure>', lambda e: self.config(wraplen=self.winfo_width())) | |
def getwidget(name): | |
if name == 'tk': | |
return tkinter.Tk | |
elif name == 'label': | |
return Label | |
# Handle widget namespace | |
namespace = ttk | |
# lookup widget by name | |
for a in dir(namespace): | |
if a.lower() == name.lower(): | |
func = getattr(namespace, a) | |
if hasattr(func, '__call__'): | |
return func | |
# lookup in components. default to None | |
return _components.get(name, None) | |
class TkParser(HTMLParser): | |
""" | |
(X)HTML parser used to create the Tkinter widgetr structure. | |
""" | |
theme_name = 'default' | |
def __init__(self, component, master): | |
assert component | |
self.component = component | |
self.stack = [] | |
self.lastwidget = None | |
self.stack.append(master) | |
super().__init__() | |
def handle_starttag(self, tag, attrs): | |
# Create list of arguments used to create widget | |
print(tag, attrs) | |
kwargs = {k: v for k, v in attrs if not k.startswith('pack:')} | |
pack = {k[5:]: v for k, v in attrs if k.startswith('pack:')} | |
# Handle the command attribute | |
if 'command' in kwargs: | |
func = getattr(self.component, kwargs['command'], None) | |
assert func, 'cannot bind the command %s, component %s has not matching attribute, on line: %s' % (kwargs['command'], self.component.__class__.__name__, self.get_starttag_text()) | |
kwargs['command'] = func | |
# Handle the id attribute | |
id = kwargs.pop('id', None) | |
# Create widget to represent the node. | |
Widget = getwidget(tag) | |
assert Widget, 'cannot find widget matching tag name: ' + tag | |
# Get constructor attributes | |
if Widget == tkinter.Tk: | |
widget = Widget( | |
screenName=kwargs.pop('screenName', None), | |
baseName=kwargs.pop('baseName', None), | |
className=kwargs.pop('className', 'Tk')) | |
# Call functions e.g. geometry, title | |
for k, v in kwargs.items(): | |
func = getattr(widget, k, None) | |
assert func, 'cannot find function name: ' + k | |
func(v) | |
else: | |
assert self.stack[-1], 'widget or component cannot be created without a master' | |
try: | |
widget = Widget(master=self.stack[-1], **kwargs) | |
except TclError as e: | |
raise ValueError(str(e) + ', %s, line: %s' % (self.get_starttag_text(), self.lineno)) | |
# Assign widget to variables. | |
if id: | |
setattr(self.component, id, widget) | |
# Pack widget | |
if tag != 'tk': | |
widget.pack(**pack) | |
self.stack.append(widget) | |
def handle_endtag(self, tag): | |
print('end', tag) | |
self.lastwidget = self.stack.pop() | |
def component(cls): | |
""" | |
Class decorator to register component. | |
""" | |
_components[cls.__name__.lower()] = cls | |
orig_init = cls.__init__ | |
orig_getattr = getattr(cls, '__getattr__', None) | |
def __init__(self, *args, **kwargs): | |
# Get the parent widget if any. | |
master = kwargs.pop('master', None) | |
# Create the wdiget from template | |
parser = TkParser(self, master=master) | |
parser.feed(cls.template) | |
assert len(parser.stack) == 1, "one or more tag are not properly closed" | |
self.root = parser.lastwidget | |
# Load the style if any. | |
style = getattr(cls, 'style', '') | |
if style: | |
data = yaml.load(style) | |
s = ttk.Style(self.root) | |
# TODO Make this theme configurable | |
s.theme_use('clam') | |
s.theme_settings('clam', data) | |
orig_init(self, *args, **kwargs) | |
def mainloop(self): | |
if self.root.__class__ != tkinter.Tk: | |
raise AttributeError('mainloop') | |
return self.root.mainloop() | |
def pack(self, *args, **kwargs): | |
return self.root.pack() | |
cls.__init__ = __init__ | |
cls.mainloop = mainloop | |
cls.pack = pack | |
return cls | |
@component | |
class HomeView(): | |
template = """ | |
<Frame> | |
<Frame pack:fill="x"> | |
<Label text="Backup is healthy" pack:side="left" style="H1.TLabel"/> | |
<Button command="start_stop_backup" text="Start backup" pack:side="right" style="Success.TButton" cursor="hand2"/> | |
</Frame> | |
<Separator orient="horizontal" pack:fill="x" pack:pady="25" /> | |
<Frame pack:fill="x"> | |
<Label style="Strong.TLabel" text="Last Backup" width="15" pack:side="left"/> | |
<Label text="Complete Successfully on Sun, 26 Sep 2021 15:47:12.\nNo background jobs using system resources." pack:expand="true" pack:side="left" pack:fill="x" wrap="true"/> | |
</Frame> | |
<Separator orient="horizontal" pack:fill="x" pack:pady="25" /> | |
<Frame pack:fill="x"> | |
<Label style="Strong.TLabel" text="Remote" width="15" pack:side="left"/> | |
<Button style="Link.TButton" text="http://www.minarca.net" cursor="hand2" pack:side="left"/> | |
<Button style="Nav.TButton" text="Disconnect" cursor="hand2" pack:side="right"/> | |
</Frame> | |
</Frame> | |
""" | |
def start_stop_backup(self): | |
pass | |
@component | |
class ScheduleView(): | |
template = """ | |
<Frame> | |
<Frame pack:fill="x"> | |
<Label text="Schedule" pack:side="left" style="H1.TLabel"/> | |
</Frame> | |
<Frame pack:fill="x"> | |
<Label text="Select how often you want your backup to be performed." pack:side="left"/> | |
</Frame> | |
<Separator orient="horizontal" pack:fill="x" pack:pady="25" /> | |
<Frame pack:fill="x"> | |
<Label style="Strong.TLabel" text="Frequency" width="15" pack:side="left"/> | |
<Combobox values="value1 value2" pack:side="right" /> | |
</Frame> | |
</Frame> | |
""" | |
@component | |
class HomeDialog(): | |
style = """ | |
".": | |
configure: | |
font: ["Helvetica", "16"] | |
foreground: "#787878" | |
TLabel: | |
configure: | |
background: white | |
TFrame: | |
configure: | |
background: white | |
TButton: | |
configure: | |
background: "#1c4c72" | |
foreground: "#fff" | |
padding: 10 | |
relief: flat | |
map: | |
foreground: | |
- ['disabled', '#aaa'] | |
- ['pressed', '#fff'] | |
background: | |
- ['pressed', '#0b1e2c'] | |
- ['active', '#123149'] | |
relief: | |
- ['pressed', '!focus', 'flat'] | |
Nav.TButton: | |
configure: | |
background: "#ebebeb" | |
foreground: "#787878" | |
padding: 5 | |
cursor: hand2 | |
map: | |
foreground: | |
- ['pressed', '#555'] | |
background: | |
- ['pressed', '#ababab'] | |
- ['active', '#bdbdbd'] | |
Success.TButton: | |
configure: | |
background: "#88a944" | |
map: | |
background: | |
- ['pressed', '#566b2b'] | |
- ['active', '#6b8535'] | |
Link.TButton: | |
configure: | |
font: ["Helvetica", "16", "underline"] | |
background: "white" | |
foreground: "#1c4c72" | |
padding: 1 | |
relief: flat | |
map: | |
foreground: | |
- ['pressed', '#a5221f'] | |
- ['active', '#d02b27'] | |
background: | |
- ['pressed', 'white'] | |
- ['active', 'white'] | |
relief: | |
- ['pressed', '!focus', 'flat'] | |
H1.TLabel: | |
configure: | |
font: ["Helvetica", "36", "bold"] | |
foreground: "#1c4c72" | |
Strong.TLabel: | |
configure: | |
font: ["Helvetica", "16", "bold"] | |
""" | |
template = """ | |
<Tk geometry="970x500" title="CustomTkinter Test"> | |
<Frame pack:fill="both" pack:expand="true"> | |
<Frame pack:fill="y" pack:side="left" pack:pady="3"> | |
<Button style="Nav.TButton" command="show_home_view" pack:fill="x" pack:padx="4" pack:pady="2" width="22" text="Home" cursor="hand2"/> | |
<Button style="Nav.TButton" command="show_patterns_view" pack:fill="x" pack:padx="4" pack:pady="2" width="22" text="Select files" cursor="hand2"/> | |
<Button style="Nav.TButton" command="show_schedule_view" pack:fill="x" pack:padx="4" pack:pady="2" width="22" text="Schedule" cursor="hand2"/> | |
</Frame> | |
<Frame pack:fill="both" pack:side="right" pack:expand="1" pack:padx="3" pack:pady="3"> | |
<HomeView id="homeview" pack:fill="both" pack:expand="1" /> | |
</Frame> | |
</Frame> | |
</Tk> | |
""" | |
def show_home_view(self): | |
pass | |
def show_patterns_view(self): | |
pass | |
def show_schedule_view(self): | |
pass | |
if __name__ == "__main__": | |
dlg = HomeDialog() | |
dlg.mainloop() |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
This python file contains the templating engine and the template.
HomeDialog Templates:
HomeView template:
The resulting graphical user interface: