├── example2.bat ├── screenshot.png ├── example2.command ├── setup.py ├── example1.py ├── example3.py ├── example2.py ├── readme.md └── tkform.py /example2.bat: -------------------------------------------------------------------------------- 1 | python example2.py %1 %2 %3 2 | -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/boscoh/tkform/HEAD/screenshot.png -------------------------------------------------------------------------------- /example2.command: -------------------------------------------------------------------------------- 1 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 2 | cd $DIR 3 | python example2.py $* 4 | if [ "$(uname)" == "Darwin" ]; then 5 | echo -n -e "]0;example2.command" 6 | osascript -e 'tell application "Terminal" to close (every window whose name contains "example2.command")' & 7 | fi 8 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from setuptools import setup 3 | 4 | description = "Docs at http://github.com/boscoh/tkform" 5 | 6 | setup( 7 | name='tkform', 8 | version='1.0', 9 | author='Bosco Ho', 10 | author_email='boscoh@gmail.com', 11 | url='http://github.com/boscoh/tkform', 12 | description='a tkinter form-based GUI that wraps python scripts', 13 | long_description=description, 14 | license='BSD', 15 | install_requires=[], 16 | py_modules=['tkform'], 17 | scripts=[] 18 | ) 19 | -------------------------------------------------------------------------------- /example1.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: iso-8859-1 -*- 3 | 4 | 5 | import tkform 6 | 7 | 8 | def make_index(params): 9 | """ 10 | Our example run function that takes the params, converts 11 | it json, constructs a webpage, opens the webpage, and 12 | exits the original form. 13 | """ 14 | import os 15 | import json 16 | import webbrowser 17 | fname = os.path.abspath('tkform_ex2.html') 18 | 19 | # make the webpage 20 | template = "
%s" 21 | open(fname, 'w').write(template % json.dumps(params, indent=2)) 22 | 23 | # open the webpage 24 | webbrowser.open('file://' + fname) 25 | 26 | # to quit the form, must use this function 27 | tkform.exit() 28 | 29 | 30 | # Create the form with title, width and height 31 | title = "tkform example: bridge to webpage with json" 32 | width, height = 700, 900 33 | form = tkform.Form(title, width, height) 34 | 35 | # Add some text 36 | form.push_text(title, 20) 37 | 38 | # And a loader for multiple files, registers a 39 | # param with the dictionary key 'files_and_labels' 40 | # this will be returned as params in the 'run' funtion 41 | form.push_file_list_param('files_and_labels', '+ files') 42 | 43 | # Must register the submit button 44 | form.push_submit() 45 | 46 | # When pushed the submit button will trigger `run` 47 | # with parameters setup from above, replace with 48 | # our own function that takes a params dictionary 49 | form.run = make_index 50 | 51 | # All set up? Trigger app. 52 | form.mainloop() 53 | -------------------------------------------------------------------------------- /example3.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: iso-8859-1 -*- 3 | 4 | 5 | import os 6 | import json 7 | import webbrowser 8 | 9 | import Tkinter as tk 10 | import tkFileDialog 11 | 12 | import tkform 13 | 14 | 15 | class ExampleForm(tkform.Form): 16 | 17 | """ 18 | Example tkform with Reorderable List 19 | """ 20 | 21 | def __init__(self, width=700, height=800, parent=None): 22 | tkform.Form.__init__(self, parent, width, height) 23 | 24 | self.title('Reorderable List Example') 25 | self.push_text("Reorderable List Example", 30) 26 | self.push_line() 27 | self.push_spacer() 28 | self.push_text(u"Drag \u2630 to reorder filenames") 29 | self.push_custom_loader('filenames_and_labels', '+ files') 30 | self.push_spacer(2) 31 | self.push_text("Output", 16) 32 | self.push_line() 33 | self.push_submit() 34 | self.push_output() 35 | 36 | def push_custom_loader(self, param_id, button_text): 37 | self.reorderable_list = tkform.ReorderableList(self.interior) 38 | self.datas = [] 39 | 40 | def load_peptagram(): 41 | fnames = tkFileDialog.askopenfilenames(title=button_text) 42 | try: 43 | self.print_output('Loading %d filenames... ' % len(fnames)) 44 | except: 45 | self.print_exception() 46 | 47 | for fname in fnames: 48 | basename = os.path.basename(fname) 49 | self.reorderable_list.add_entry_label(fname, basename) 50 | 51 | load_button = tk.Button( 52 | self.interior, 53 | text=button_text, 54 | command=load_peptagram) 55 | self.push_row(load_button) 56 | 57 | self.push_row(self.reorderable_list) 58 | self.mouse_widgets.append(self.reorderable_list) 59 | self.param_entries[param_id] = self.reorderable_list 60 | 61 | def run(self, params): 62 | 63 | self.print_output('\nForm parameters:\n') 64 | 65 | self.print_output(json.dumps(params, indent=2)) 66 | self.print_output('\n\n') 67 | 68 | 69 | ExampleForm(800, -150).mainloop() 70 | -------------------------------------------------------------------------------- /example2.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: iso-8859-1 -*- 3 | 4 | 5 | import os 6 | import glob 7 | import json 8 | 9 | import tkform 10 | 11 | 12 | def size_of_fnames(*fnames): 13 | size = 0 14 | for fname in fnames: 15 | if os.path.isdir(fname): 16 | sub_fnames = glob.glob(os.path.join(fname, '*')) 17 | size += size_of_fnames(*sub_fnames) 18 | else: 19 | size += os.path.getsize(fname) 20 | return size 21 | 22 | 23 | def size_str(*fnames): 24 | size = size_of_fnames(*fnames) 25 | if size < 1E6: 26 | return "%.3f MB" % (size / 1E6) 27 | else: 28 | return "%.f MB" % (size / 1E6) 29 | 30 | 31 | class ExampleForm(tkform.Form): 32 | 33 | def __init__(self, width=700, height=800): 34 | 35 | tkform.Form.__init__(self, 'Example Form', width, height) 36 | 37 | # add text 38 | self.push_text("Example with tkform to calculate filesize", 30) 39 | # add dividing line 40 | self.push_line() 41 | # add space between rows 42 | self.push_spacer() 43 | 44 | # add a file loading list object 45 | self.push_text("Params for Reorderable Lists", 16) 46 | self.push_file_list_param('files_and_labels', '+ files') 47 | self.push_dir_list_param('dirs_and_labels', '+ directory') 48 | 49 | self.push_spacer() 50 | self.push_line() 51 | 52 | # some text labels 53 | self.push_text("More Params", 16) 54 | self.push_labeled_param( 55 | 'label1', 'Enter label1', 'label', width=50) 56 | self.push_labeled_param( 57 | 'file2', 'Enter file2', 'default_file', load_file_text='select') 58 | self.push_labeled_param( 59 | 'dir3', 'Enter dir3', 'default_dir', load_dir_text='select') 60 | self.push_checkbox_param('check4', '(Un)check this checkbox') 61 | self.push_text("Choose a radio button:") 62 | self.push_radio_param( 63 | 'radio5', 64 | ['choice 0', 65 | 'choice 1', 66 | 'choice 2', 67 | 'choice 3']) 68 | 69 | self.push_spacer() 70 | self.push_line() 71 | 72 | self.push_text("Output", 16) 73 | self.push_submit() 74 | 75 | # Must register the output if you want to 76 | # display output during the execution of your commands 77 | self.push_output() 78 | 79 | def run(self, params): 80 | """ 81 | Override the command that is run when "submit" is 82 | pressed. 83 | """ 84 | 85 | self.clear_output() 86 | 87 | self.print_output('\nForm parameters:\n') 88 | 89 | self.print_output(json.dumps(params, indent=2)) 90 | self.print_output('\n\n') 91 | 92 | fnames = [e[0] for e in params['files_and_labels']] 93 | 94 | self.print_output('Calculating size of files:\n') 95 | self.print_output('%s' % fnames) 96 | self.print_output('\n') 97 | self.print_output('Size: %s' % size_str(*fnames)) 98 | self.print_output('\n\n') 99 | 100 | dirs = [e[0] for e in params['dirs_and_labels']] 101 | 102 | self.print_output('Calculating size of directories:\n') 103 | self.print_output('%s' % dirs) 104 | self.print_output('\n') 105 | self.print_output('Size: %s' % size_str(*dirs)) 106 | 107 | self.print_output('\n\n') 108 | 109 | # add a text link 110 | self.print_output('Example of link: ') 111 | self.print_output('Close window', tkform.exit) 112 | 113 | 114 | ExampleForm(700, 900).mainloop() 115 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # tkform 4 | 5 | a cross-platform form-based GUI to run command-line utilities using standard Python. 6 | 7 |  8 | 9 | ## Quick Start 10 | 11 | ### Installation instructions 12 | 13 | To install tkform: 14 | 15 | pip install tkform 16 | 17 | Or download and unzip: 18 | 19 | [tkform-master.zip](https://github.com/boscoh/tkform/archive/master.zip) 20 | 21 | Then install: 22 | 23 | > python setup.py install 24 | 25 | ### Examples 26 | 27 | 1. `python example1.py` - loads a filename and sends it a webpage 28 | 2. `python example2.py` - is more involved which involve displaying output 29 | 3. `python example3.py` - shows how to customize reoderable lists 30 | 31 | ### Making a Quick-and-Dirty Form 32 | 33 | Here's how to make a simple form (this is similar to `example1.py`). 34 | 35 | First some house-keeping: 36 | 37 | import tkform, os, webbrowser 38 | 39 | Let's define a function that receives a `params` dictionary: 40 | 41 | def my_run(params): 42 | open('index.html', 'w').write(params['files_and_labels']) 43 | webbrowser.open('file://' + os.path.abspath('index.html') 44 | 45 | Notice the function expects a key of `files_and_labels` in `params` . 46 | 47 | A quick-and-dirty form can be instantiated and populated on the fly. : 48 | 49 | form = tkform.Form('Window title of form', 700, 900) 50 | 51 | Let's populate the form with a header, file/dir list parameter: 52 | 53 | form.push_text(title, 20) 54 | form.push_file_list_param('files_and_labels', '+ files') 55 | 56 | Then add the crucial submit button: 57 | 58 | form.push_submit() 59 | 60 | The submit button will trigger the `run` method, so let's override it with our own: 61 | 62 | form.run = my_run 63 | 64 | Then go: 65 | 66 | form.mainloop() 67 | 68 | ## What is it? 69 | 70 | You want to wrap a simple GUI around a command-line utility. As the idea of a GUI is to make it easy for end-users, you probably also want it to be: 71 | 72 | 1. cross-platform - useful for as many end-users as possible 73 | 2. open files with native file-chooser - familiarity is important for usability 74 | 3. easy to install - which is as important as being easy-to-use 75 | 76 | No existing solution quite satisifiy all three requirements. One solution is [`gooey`](https://github.com/chriskiehl/Gooey), which cleverly wraps a cross-platform `wxPython` GUI around Python scripts. But sadly, it requires `wxPython`, which is difficult to install for end-users. You either need to match binary versions of `wxPython` to your Python package, or install a C compiler ecosystem. 77 | 78 | Another solution is to run a local webbrowser. This is powerful except for one hobbling limitation. Due to the security model of the webbrowser, your native open-file dialog will never give you full pathnames to your scripts. This severely constrains your scripts. 79 | 80 | Instead, we have built `tkform` which provides a base for building a simple GUI around Python scripts that is designed for a non-technical end-user. It is designed against `tkinter`, which is bundled with Python. Your user has only to install Python, which is easy. `tkinter` provides native file-choosers, and as it's all in Python, your GUI will be cross-platform. 81 | 82 | ## How does it work? 83 | 84 | Typically, you will want to sub-class `tkform.Form` to use all the features: callback buttons, integrated text output, filename processing etc. The following is similar to the example `example2.py`. 85 | 86 | ### 1. Subclassing your own Form 87 | 88 | First import the library: 89 | 90 | import tkform 91 | 92 | Then subclass it with your ow: 93 | 94 | class MyForm(tkform.Form): 95 | def __init__(self): 96 | tkform.Form.__init__(self, title, width, height) 97 | 98 | The window will appear in the top-left of the screen. The `width` and the `height` define the size of the window. You can use negative values which will set, for the `width`, to the width of the screen subtracted by the negative value. Similarly for `height`. If you want to maximize the height of the window, a value of -150 seems to work across all platforms. 99 | 100 | ### 2. Creating the form - the grid model 101 | 102 | The way the form is built is with a vertical grid layout, similar to how many HTML forms are laid out. Internally, a tkinter grid is used and all widgets are left-aligned. A vertical scroll-bar will be applied that lets your form grow. 103 | 104 | The form will return a dictionary where each key/value pair is determined by what objects are pushed onto the form. The key/value pairs are determined by what paramaters you push onto the form in the initialization via the `push*_param` methods. 105 | 106 | _Parameters_. Available param types are defined by these methods: 107 | 108 | - multiple file list loader 109 | 110 | push_file_list_param(param_id, load_file_text, is_label=True) 111 | 112 | This creates a button, which, when clicked, triggers an open file dialog box to chose multiple files. Files chosen here will pop up in a table of files. This may include an optional label for each file as determined by the `is_label` flag. This table of files can be reordered, or removed. When the submit button is pressed, the widget will return a list of tuples in `params` of the run function. In the tuple, the first element is the filename, with an optional second element corresponding to the label. 113 | 114 | - directory list loader 115 | 116 | push_file_dir_param(param_id, load_dir_text, is_label=True) 117 | 118 | This is similar to `push_file_list_param` above except it opens directories. Unfortunately tkinter only allows you to select one directory at a time. 119 | 120 | - check box 121 | 122 | push_checkbox_param(param_id, text, init_val='1') 123 | 124 | This creates a checkbox that will return a string `1` if selected, and `0` otherwise. 125 | 126 | - radio button 127 | 128 | push_radio_param(param_id, text_list, init_val=0) 129 | 130 | This creates a radio button from a list of options determined by a list of strings in `text_list`. The return value is an integer that refers to entries in `text_list`. 131 | 132 | - text parameter, optionally as single file/directory 133 | 134 | push_labeled_param(param_id, text, entry_text='', load_file_text=None, load_dir_text=None) 135 | 136 | This creates a text entry that is passed to the `params` dictionary. This is also used for loading a single filename or a single directory. If `load_file_text` is not empty, a button will be created with that label text, which will trigger a file-open dialog box. Similarly for `load_dir_text`. 137 | 138 | _Decorators_. Of course you need other things to make the form readable: 139 | 140 | - text at different sizes: `push_text(text, fontsize=12)` 141 | - lines to divide sections: `push_line(width=500, height=1, color="#999999")` 142 | - and white-space: `push_spacer(self, height=1)` 143 | 144 | _Extra buttons and callbacks_. 145 | 146 | - Although `tkform` is conceived around a single submit button at the end, sometimes you might want to trigger some action before the form is submitted. To do this, first define the action you want to take: 147 | 148 | def callback(): 149 | # do something 150 | 151 | Then push the button onto the form, and link the callback: 152 | 153 | self.push_button('text', callback) 154 | 155 | ### 3. Submitting the job 156 | 157 | Most importantly you need to add a submit button, probably at the bottom of the form: 158 | 159 | push_submit() 160 | 161 | When the submit button is clicked, `tkform` will extract the parameters from all the widgets with `*_param*`, and put them in the `params` dictionary. This will be sent to the `self.run(params)` method. 162 | 163 | In the `Form` class, this is a dummy method that needs to be overriden: 164 | 165 | self.run(params): 166 | # Do your thing with params or run an external function 167 | 168 | At the end of `run` you have a choice: 169 | 170 | 1. you can exit the form with `tkform.exit()` 171 | 2. or the window stays open and the user can submit again _ad nauseum_. 172 | 173 | A nice thing is that if you've carried out some external action, say opening a web-page, that will stay open if you've closed the form. 174 | 175 | When `run` is called, it is wrapped in a try/except clause to catch errors that are optionally sent to the ouptut widget, which will be discussed next. 176 | 177 | 178 | ### 4. Handling output (optional) 179 | 180 | The form provides an (optional) output widget to provide feedback to the user within the form itself. The output area is registered on the form via the method with an optional width parameter: 181 | 182 | self.push_output(width=50) 183 | 184 | During the processing in `run`, you can add text: 185 | 186 | self.print_output('Processing...\n') 187 | 188 | This `print_output` does not include an implicit carriage-return, this allows you to display progressive text ouptut. The form will automatically increase in size to display the output. You can flush the output at different stages of the processing with: 189 | 190 | self.clear_output() 191 | 192 | The `print_output` also has simple hyperlink facility. Simply adding a callback function as a second parameter will turn the text into a hyperlink. 193 | 194 | For instance, if you have written results to a webpage `/user/result/index.html`, you can define a display-results callback: 195 | 196 | import webbrowser 197 | def show_results(): 198 | webbrowser.open('file://user/result.html') 199 | 200 | This will produce a hyperlink that will open the local web-page to your results: 201 | 202 | self.print_output('results page', show_results) 203 | 204 | ### 5. Execute the form 205 | 206 | Instantiate your form: 207 | 208 | form = Myform() 209 | 210 | And run it: 211 | 212 | form.mainloop() 213 | 214 | ## Customizing Forms 215 | 216 | If you want to customize your own widgets then have a look at the widgets instantiated in the `Form` class. 217 | 218 | Any `tkinter` widgets can be displayed in the form by `self.push_row(my_tkinter_widget)`. This could include buttons that trigger functions or any other kind of actions. 219 | 220 | To add key-value pairs to the `params` that is generated when the submit button is pressed, you must add an object to the `self.param_entries` dictionary: 221 | 222 | self.param_entries[param_id] = entry 223 | 224 | The value `entry` must have a `get` method that returns a json compatible data structure (lists, dictionaries, strings and numbers). 225 | 226 | ## Customizing Reorderable Lists 227 | 228 | One feature that `tkform` provides is the ability to preload a list of items before the `submit` button is pressed. An example is the widget generated with the `push_file_list_param` method. This list can be reordered, or truncated, and labels can be attached to them. 229 | 230 | To generated your own editable list, you can create your own widgets based on the `ReorderableList` class. On initialization, you must instantiate an `ReorderableList` on the page. Then you create a button that that triggers an action (say an open file dialog), which will populate your `ReorderableList`. This shows up on the page instantaneously. As well, you must provide an object to return in the `self.param_entries` dictionary. The `ReorderableList` serves this function, with its default `get` method. But you can certainly substitute your own. 231 | 232 | Anyway, check out `example3.py` to see how a customized `ReorderableList` is built. 233 | 234 | ## Processing the submit button 235 | 236 | It's very easy to work the GUI as optional feature within a command-line Python script. If you parameterise the main function in the form: 237 | 238 | def main_processing(params, print_output=std.write.out): 239 | # do something with params 240 | print_output('sucess...') 241 | 242 | This takes a params dictionary, and for output, it writes to the function print_output. This can be put in the `run` method of the form: 243 | 244 | class Myform(tkform.Form): 245 | ... 246 | def run(self, params): 247 | main_processing(params, self.output) 248 | 249 | And `main_processing` lends itself to take in arguments from the command-line parameters. 250 | 251 | ## Making scripts clickable for the End User 252 | 253 | It's useful to wrap the python script with a unix shell script or a Windows batch file so that the end-user can double-click in the file manager. 254 | 255 | For instance we can make for `example1.py`, a batch file `example1.bat`: 256 | 257 | python tkform1_ex1.py %1 %2 %3 258 | 259 | And in *nix, we ca make `example1.command`: 260 | 261 | DIR=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd ) 262 | cd $DIR 263 | python example1.py $*example 264 | 265 | Make sure the file is `chmod +c example1.command`. The extension of .command allows the MacOS finder to allow double-clicking. Indeed, to close the window that pops after the form is closed, you need to also add, for MacOS: 266 | 267 | if [ "$(uname)" == "Darwin" ]; then 268 | echo -n -e "\033]0;example1.command\007" 269 | osascript -e 'tell application "Terminal" to close (every window whose name contains "example1.command")' & 270 | fi 271 | 272 | ## Changelog 273 | 274 | 1.0 (June 2015) 275 | 276 | - ReorderableList abstraction 277 | - Output auto-sizing 278 | 279 | © Bosco K ho. 280 | 281 | -------------------------------------------------------------------------------- /tkform.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # -*- coding: iso-8859-1 -*- 3 | 4 | """ 5 | tkform provides an HTML-inspired form-based GUI 6 | for Python scripts. 7 | 8 | Includes 9 | - scrollable page 10 | - simple entry of params 11 | - reordable lists of files or directories 12 | """ 13 | 14 | 15 | import os 16 | import traceback 17 | import re 18 | import collections 19 | 20 | import Tkinter as tk 21 | import tkFileDialog 22 | from idlelib.WidgetRedirector import WidgetRedirector 23 | 24 | 25 | class VerticalScrolledFrame(tk.Frame): 26 | 27 | """ 28 | A Tkinter scrollable frame: 29 | 30 | - place widgets in the 'interior' attribute 31 | - construct and pack/place/grid normally 32 | - only allows vertical scrolling 33 | - adapted from http://stackoverflow.com/a/16198198 34 | """ 35 | 36 | def __init__(self, parent, *args, **kw): 37 | tk.Frame.__init__(self, parent, *args, **kw) 38 | 39 | # create a canvas object and a vertical scrollbar for scrolling it 40 | vscrollbar = tk.Scrollbar(self, orient=tk.VERTICAL) 41 | vscrollbar.pack(fill=tk.Y, side=tk.RIGHT, expand=tk.FALSE) 42 | hscrollbar = tk.Scrollbar(self, orient=tk.HORIZONTAL) 43 | hscrollbar.pack(fill=tk.X, side=tk.BOTTOM, expand=tk.FALSE) 44 | 45 | self.canvas = tk.Canvas( 46 | self, bd=0, highlightthickness=0, 47 | yscrollcommand=vscrollbar.set, 48 | xscrollcommand=hscrollbar.set) 49 | self.canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=tk.TRUE) 50 | vscrollbar.config(command=self.canvas.yview) 51 | hscrollbar.config(command=self.canvas.xview) 52 | 53 | # reset the view 54 | self.canvas.xview_moveto(0) 55 | self.canvas.yview_moveto(0) 56 | 57 | # create a frame inside the canvas which will be scrolled 58 | self.interior = tk.Frame(self.canvas) 59 | self.interior_id = self.canvas.create_window( 60 | 0, 0, window=self.interior, anchor=tk.NW) 61 | 62 | # track changes to canvas, frame and updates scrollbar 63 | self.interior.bind('