472 lines
14 KiB
Python
Executable File
472 lines
14 KiB
Python
Executable File
#
|
|
# Copyright (c) 2011. All rights reserved.
|
|
#
|
|
# This library is free software; you can redistribute it and/or
|
|
# modify it under the terms of the GNU Lesser General Public
|
|
# License as published by the Free Software Foundation; either
|
|
# version 2.1 of the License, or (at your option) any later version.
|
|
#
|
|
# This library is distributed in the hope that it will be useful,
|
|
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
# Lesser General Public License for more details.
|
|
#
|
|
# You should have received a copy of the GNU Lesser General Public
|
|
# License along with this library; if not, write to the Free Software
|
|
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
|
|
# MA 02110-1301 USA
|
|
#
|
|
"""
|
|
Module for easy creation of Tkinter GUIs.
|
|
|
|
Created on Apr 21, 2011
|
|
|
|
@author: tmetsch
|
|
"""
|
|
|
|
try:
|
|
# python 3
|
|
import tkinter
|
|
except ImportError:
|
|
# python 2
|
|
import Tkinter as tkinter
|
|
|
|
try:
|
|
# python 3
|
|
from tkinter import ttk
|
|
except ImportError:
|
|
# python 2
|
|
import ttk
|
|
|
|
import json
|
|
import os
|
|
import traceback
|
|
import yaml
|
|
|
|
|
|
def _contains_dict(items):
|
|
"""
|
|
Checks if a set of items contains a dictionary.
|
|
|
|
items -- The set of items to test.
|
|
"""
|
|
result = False
|
|
for item in items.keys():
|
|
if isinstance(items[item], dict):
|
|
result = True
|
|
break
|
|
return result
|
|
|
|
|
|
def _contains_list(items):
|
|
"""
|
|
Checks if a set of items contains a list.
|
|
|
|
items -- The set of items to test.
|
|
"""
|
|
result = False
|
|
for item in items.keys():
|
|
if isinstance(items[item], list) and item is not item.lower():
|
|
# the .lower check ensures that I can have attribute lists
|
|
result = True
|
|
break
|
|
return result
|
|
|
|
|
|
class TkJson(tkinter.Tk):
|
|
"""
|
|
Simple class which wraps Tk and uses some JSON to contruct a GUI.
|
|
"""
|
|
|
|
menu = None
|
|
widgets = {}
|
|
|
|
def __init__(self, filename, title='Tk', prefer_tk=True, file_type='json'):
|
|
"""
|
|
Initialize a Tk root and created the UI from a JSON file.
|
|
|
|
Returns the Tk root.
|
|
"""
|
|
# Needs to be done this way - because base class do not derive from
|
|
# object :-(
|
|
tkinter.Tk.__init__(self)
|
|
self.prefer_tk = prefer_tk
|
|
self.title(title)
|
|
|
|
if file_type == 'json':
|
|
user_interface = json.load(open(filename)) if os.path.isfile(
|
|
filename) else json.loads(filename)
|
|
elif file_type == 'yaml':
|
|
user_interface = yaml.load(open(filename)) if os.path.isfile(
|
|
filename) else yaml.loads(filename)
|
|
else:
|
|
raise ValueError('Only json or yaml file definitions are'
|
|
'supported.')
|
|
|
|
self.create_widgets(self, user_interface)
|
|
|
|
def create_widgets(self, parent, items):
|
|
"""
|
|
Creates a set of Tk widgets.
|
|
"""
|
|
for name in items.keys():
|
|
current = items[name]
|
|
if isinstance(current, dict) and not _contains_list(
|
|
current) and not _contains_dict(current):
|
|
self._create_widget(name, parent, current)
|
|
|
|
elif isinstance(current, dict) and _contains_list(current):
|
|
widget = self._create_widget(name, parent, current)
|
|
if not widget:
|
|
break
|
|
|
|
self.create_widgets(widget, current)
|
|
elif isinstance(current, dict) and _contains_dict(current):
|
|
widget = self._create_widget(name, parent, current)
|
|
if not widget:
|
|
break
|
|
|
|
self.create_widgets(widget, current)
|
|
elif isinstance(current, list):
|
|
for item in current:
|
|
self.create_widgets(parent, {name: item})
|
|
|
|
def _create_widget(self, name, parent, desc):
|
|
"""
|
|
Tries to resolve the widget from Tk and create it.
|
|
|
|
Returns the newly created widget.
|
|
|
|
name -- Name of the widget.
|
|
parent -- The parent widget.
|
|
desc -- Dictionary containing the description for this widget.
|
|
"""
|
|
position, weight, padding, opt = self._get_options(desc)
|
|
|
|
try:
|
|
widget_factory = getattr(
|
|
tkinter, name) if self.prefer_tk else getattr(ttk, name)
|
|
except AttributeError:
|
|
try:
|
|
widget_factory = getattr(
|
|
ttk, name) if self.prefer_tk else getattr(tkinter, name)
|
|
except AttributeError:
|
|
traceback.print_exc()
|
|
raise AttributeError(
|
|
'Neither Tkinter nor ttk have a widget named: ', name)
|
|
|
|
while True:
|
|
try:
|
|
widget = widget_factory(parent, **opt)
|
|
break
|
|
except Exception as exp:
|
|
if len(opt) == 0:
|
|
break
|
|
del opt[str(exp).split()[2][2:-1]]
|
|
# widget = widget_factory(parent,**opt)
|
|
|
|
# todo: tkinter Label for debug
|
|
#tkinter.Label(text=position[0],relief=tkinter.SUNKEN).grid(row=position[0],column=0,sticky='news')
|
|
#tkinter.Label(text=position[1],relief=tkinter.SUNKEN).grid(row=0,column=position[1],sticky='news')
|
|
widget.grid(row=position[0],
|
|
column=position[1],
|
|
columnspan=weight[0],
|
|
rowspan=weight[1],
|
|
sticky=padding[2],
|
|
padx=padding[0],
|
|
pady=padding[1])
|
|
|
|
# propaget size settings when needed.
|
|
if 'width' in opt or 'height' in opt:
|
|
widget.grid_propagate(0)
|
|
|
|
# set parent weight of the cells
|
|
if weight[2] > 0:
|
|
parent.rowconfigure(position[0], weight=weight[2])
|
|
if weight[3] > 0:
|
|
parent.columnconfigure(position[1], weight=weight[3])
|
|
|
|
self.widgets[widget._name] = widget
|
|
|
|
return widget
|
|
|
|
def _get_options(self, dictionary):
|
|
"""
|
|
Extracts the needed options from a dictionary.
|
|
|
|
dictionary -- Dictionary with all the options in it.
|
|
"""
|
|
options = {}
|
|
|
|
row = 0
|
|
column = 0
|
|
|
|
colspan = 1
|
|
rowspan = 1
|
|
rowweight = 0
|
|
colweight = 0
|
|
|
|
padx = 2
|
|
pady = 2
|
|
|
|
sticky = 'news'
|
|
|
|
if 'row' in dictionary:
|
|
row = dictionary['row']
|
|
dictionary.pop('row')
|
|
if 'column' in dictionary:
|
|
column = dictionary['column']
|
|
dictionary.pop('column')
|
|
|
|
if 'columnspan' in dictionary:
|
|
colspan = dictionary['columnspan']
|
|
dictionary.pop('columnspan')
|
|
if 'rowspan' in dictionary:
|
|
rowspan = dictionary['rowspan']
|
|
dictionary.pop('rowspan')
|
|
if 'rowweight' in dictionary:
|
|
rowweight = dictionary['rowweight']
|
|
dictionary.pop('rowweight')
|
|
if 'colweight' in dictionary:
|
|
colweight = dictionary['colweight']
|
|
dictionary.pop('colweight')
|
|
if 'weight' in dictionary:
|
|
colweight = dictionary['weight']
|
|
rowweight = dictionary['weight']
|
|
dictionary.pop('weight')
|
|
|
|
if 'padx' in dictionary:
|
|
padx = dictionary['padx']
|
|
dictionary.pop('padx')
|
|
if 'pady' in dictionary:
|
|
pady = dictionary['pady']
|
|
dictionary.pop('pady')
|
|
if 'sticky' in dictionary:
|
|
sticky = dictionary['sticky']
|
|
dictionary.pop('sticky')
|
|
|
|
for key in dictionary.keys():
|
|
if not isinstance(dictionary[key],
|
|
dict) and not isinstance(dictionary[key], list):
|
|
options[str(key)] = str(dictionary[key])
|
|
elif isinstance(dictionary[key], list) and key == key.lower():
|
|
# so we have an attribute list...
|
|
options[str(key)] = dictionary[key]
|
|
|
|
return [row, column], [colspan, rowspan, rowweight, colweight], \
|
|
[padx, pady, sticky], options
|
|
|
|
##
|
|
# Rest is public use :-)
|
|
##
|
|
|
|
def button(self, name, cmd, focus=False):
|
|
"""
|
|
Associate a Tk widget with a function.
|
|
|
|
name -- Name of the widget.
|
|
cmd -- The command to trigger.
|
|
focus -- indicates wether this item has the focus.
|
|
"""
|
|
item = self.get(name)
|
|
item.config(command=cmd)
|
|
|
|
if focus:
|
|
item.focus_set()
|
|
|
|
def checkbox(self, name, focus=False):
|
|
"""
|
|
Associates a IntVar with a checkbox.
|
|
|
|
name -- Name of the Checkbox.
|
|
focus -- indicates wether this item has the focus.
|
|
"""
|
|
var = tkinter.IntVar()
|
|
item = self.get(name)
|
|
item.config(variable=var)
|
|
item.var = var
|
|
|
|
if focus:
|
|
item.focus_set()
|
|
|
|
return var
|
|
|
|
def entry(self, name, key=None, cmd=None, focus=False):
|
|
"""
|
|
Returns the text of a TK widget.
|
|
|
|
name -- Name of the Tk widget.
|
|
key -- Needed if a key should be bound to this instance.
|
|
cmd -- If key is defined cmd needs to be defined.
|
|
focus -- Indicates wether this entry should take focus.
|
|
"""
|
|
var = tkinter.StringVar()
|
|
|
|
item = self.get(name)
|
|
item.config(textvariable=var)
|
|
item.var = var
|
|
|
|
if focus:
|
|
item.focus_set()
|
|
|
|
if key is not None and cmd is not None:
|
|
item.bind(key, cmd)
|
|
|
|
return var
|
|
|
|
def label(self, name):
|
|
"""
|
|
Associate a StringVar with a label.
|
|
|
|
name -- Name of the Label.
|
|
"""
|
|
var = tkinter.StringVar()
|
|
item = self.get(name)
|
|
item.config(textvariable=var)
|
|
item.var = var
|
|
return var
|
|
|
|
def get(self, name):
|
|
"""
|
|
Find a Tk widget by name and return it.
|
|
"""
|
|
if name in self.widgets.keys():
|
|
return self.widgets[name]
|
|
else:
|
|
raise KeyError('Widget with the name ` ' + name + ' ` not found.')
|
|
|
|
def xscroll(self, widget_name, scrollbar_name):
|
|
"""
|
|
Add a horizontal scrollbar to a widget.
|
|
|
|
widget_name -- name of the widget.
|
|
scollbar_name -- name of the scrollbar.
|
|
"""
|
|
widget = self.get(widget_name)
|
|
scrollbar = self.get(scrollbar_name)
|
|
|
|
widget.config(xscrollcommand=scrollbar.set)
|
|
scrollbar.config(command=widget.xview)
|
|
|
|
def yscroll(self, widget_name, scrollbar_name):
|
|
"""
|
|
Add a vertical scrollbar to a widget.
|
|
|
|
widget_name -- name of the widget.
|
|
scollbar_name -- name of the scrollbar.
|
|
"""
|
|
widget = self.get(widget_name)
|
|
scrollbar = self.get(scrollbar_name)
|
|
|
|
widget.config(yscrollcommand=scrollbar.set)
|
|
scrollbar.config(command=widget.yview)
|
|
|
|
def create_menu(self, commands, name=None, parent=None, popup=False):
|
|
"""
|
|
Creates a menu(entry) if non is available. Returns the created menu so
|
|
you can define submenus.
|
|
|
|
commands -- dict with 'label':'command' structure.
|
|
name -- Needs to be provided if it is a dropdown or submenu.
|
|
parent -- Needs to be provided if it is a submenu.
|
|
popup -- indicates if it is an popup menu or not (Default: False)
|
|
"""
|
|
if self.menu is None and popup is False:
|
|
# If no menu exists create one and add it to the Tk root.
|
|
self.menu = tkinter.Menu(self, tearoff=0)
|
|
self.config(menu=self.menu)
|
|
|
|
if name is None and parent is None and popup is False \
|
|
and len(commands.keys()) > 0:
|
|
# Just create a Menu entry.
|
|
for key in commands:
|
|
self.menu.add_command(label=key, command=commands[key])
|
|
return self.menu
|
|
elif name is not None and popup is False and len(commands.keys()) > 0:
|
|
if parent is None:
|
|
# Create a top-level drop down menu.
|
|
tmp_menu = tkinter.Menu(self.menu, tearoff=0)
|
|
self.menu.add_cascade(label=name, menu=tmp_menu)
|
|
else:
|
|
# Create a submenu.
|
|
tmp_menu = tkinter.Menu(parent, tearoff=0)
|
|
parent.add_cascade(label=name, menu=tmp_menu)
|
|
|
|
for key in commands:
|
|
tmp_menu.add_command(label=key, command=commands[key])
|
|
|
|
return tmp_menu
|
|
elif popup is True and len(commands.keys()) > 0:
|
|
tmp_menu = tkinter.Menu(self, tearoff=0)
|
|
for key in commands:
|
|
tmp_menu.add_command(label=key, command=commands[key])
|
|
|
|
return tmp_menu
|
|
else:
|
|
raise AttributeError('Invalid parameters provided')
|
|
|
|
# Move?
|
|
|
|
def create_from_file(self, parent, filename):
|
|
"""
|
|
Create a set of widgets and add them to the given parent.
|
|
|
|
parent -- The parent of the to be created widgets.
|
|
filename -- The JSON definition file.
|
|
"""
|
|
ui_file = open(filename)
|
|
definition = json.load(ui_file)
|
|
self.create_widgets(parent, definition)
|
|
|
|
def notebook(self, parent, filename, name='Tab'):
|
|
"""
|
|
Add a tab to a tkk notebook widget.
|
|
|
|
parent -- The parent notebook widget instance.
|
|
filename -- The file which describes the content of the tab.
|
|
name -- The name of the tab.
|
|
"""
|
|
frame = tkinter.Frame()
|
|
self.create_from_file(frame, filename)
|
|
parent.add(frame, text=name)
|
|
|
|
def toplevel(self, filename, title='Dialog'):
|
|
"""
|
|
Open a Toplevel widget.
|
|
|
|
parent -- The parent notebook widget instance.
|
|
title -- The title for the dialog.
|
|
"""
|
|
dialog = tkinter.Toplevel()
|
|
dialog.title(title)
|
|
self.create_from_file(dialog, filename)
|
|
dialog.grid()
|
|
return dialog
|
|
|
|
def treeview(self, treeview, name, values, parent='', index=0):
|
|
"""
|
|
Adds an item to a treeview.
|
|
|
|
treeview -- The treeview to add the items to.
|
|
name -- The name of the value.
|
|
values -- The values itself.
|
|
parent -- Default will create root items, if specified create a leaf.
|
|
index -- If index < current # of items - insert at the top.
|
|
"""
|
|
return treeview.insert(parent, index, text=name, values=values)
|
|
|
|
|
|
class TkYaml(TkJson):
|
|
"""
|
|
Wrapper class for parsing yaml files.
|
|
"""
|
|
|
|
def __init__(self, filename, title='Tk', prefer_tk=True):
|
|
"""
|
|
Initialize a Tk root and created the UI from a YAML file.
|
|
|
|
Returns the Tk root.
|
|
"""
|
|
super(TkYaml, self).__init__(filename, title, prefer_tk,
|
|
file_type='yaml')
|