Skip to content

Instantly share code, notes, and snippets.

@ikus060
Last active September 29, 2021 13:16
Show Gist options
  • Save ikus060/0ffd017776cdf3ff2b8d88a58d75f731 to your computer and use it in GitHub Desktop.
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.
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()
@ikus060
Copy link
Author

ikus060 commented Sep 29, 2021

This python file contains the templating engine and the template.

HomeDialog Templates:

<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>

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>

The resulting graphical user interface:
image

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment