ASCII Designer Manual¶
What is this?¶
A library that:
- creates GUI from ASCII-art (with well-defined syntax)
- maps widgets to virtual class attributes
- relieves you from the boring parts of Form building while leaving you in control.
The workhorse class is the AutoFrame
:
from ascii_designer import AutoFrame
The created widgets are “raw”, native widgets. You do not get wrappers; instead, the library focuses on specific tasks - building the layout, event-/value binding - and lets you do everything else with the API you know and (maybe) love.
AutoFrame overview¶
AutoFrame
is used by subclassing it. Then, define the special attribute
f_body
to define the design:
class MyForm(AutoFrame):
f_body = '''
| |
Hello World!
[Close]
'''
That’s it: A working form. Show it by calling f_show()
. If necessary, it
will set up an application object and main loop for you; the boilerplate code
reduces to:
if __name__ == '__main__':
MyForm().f_show()
You can set the f_title
attribute for a custom window title. Otherwise, your
class named is turned into a title by space-separating on uppercase characters.
If you like menus, f_menu
can be used for concise definition of menu
structures.
Finally, there is the f_build()
method, which does the actual form
generation. This is the method to override for custom building and
initialization code.
Grid slicing, stretching and anchors¶
ASCII designer generates grid layouts. The first step of processing f_body
is to cut it up into text “cells”. Each line of the f_body
string is
converted into one grid layout row.
Before processing, leading and trailing whitespace lines are cropped. Also, common leading whitespace is removed.
Slicing¶
The first line is used to define the split
points by means of the pipe character (|
). The lines below are split exactly
underneath the pipe signs, IF the respective text-column is either space or pipe
character. If, on the other hand, any other character is present in that line
and text-column, a column-spanning cell is created, containing the joint text
of both cells.
If you want to join but have a space character at this point, you can use the
tilde ~
character instead of the space. It will be converted to space in the
subsequent processing.
Row-spans are created by prepending the cell content with a brace {
character. No matching close-brace is needed. The brace characters must be
aligned, i.e. exactly above each other.
Stretch¶
By default, column widths will “shrinkwrap” the content. To make a column
stretchable, insert one or more minus -
signs in the first line between the
pipe chars:
| - | |
stretches fixed width
If you want it nice-looking, you can make a double arrow like so:
<-->
; however to the program only the minus characters count.
If you define multiple stretchable columns, the stretch proportion of each column is equal to the number of minus chars above.
Row-stretch is defined similarly. You need to create a special “first text-column” by having a pipe char before any content underneath:
| | |
<- special text-column
column 1 column 2
In this text-column, put a capital I
before rows that you want to stretch.
Stretch proportion is equal for all stretchable rows. Use row-span to have some
widgets stretch more than others vertically.
Anchoring¶
Anchoring refers to the positioning and stretching of the widget within its grid cell.
Horizontal anchoring of widgets within the grid cell is controlled by whether the text is space-padded at the beginning and/or end of its text cell:
- No space at beginning nor end makes the widget full-width.
- Space at only beginning gives right-, at end gives left-align.
- Space at both begin and end gives center alignment.
In graphical form:
| |
Alignment:
[Fill ]
[Left] ~
[Right]
[Center] ~
[also center ] |
Note how tilde character is used as space substitute. This is because trailing space is badly visible, and also removed by some text editors automatically. The last row shows another possibility by explicitly putting a pipe sign at the end.
Vertical anchoring is not controllable. It defaults to “fill”, which is the right thing most of the time. If not, you can use toolkit-native methods to change the anchoring afterwards.
Widget specification¶
To create a: | Use the syntax: |
---|---|
Label | blah blah (just write plain text),label_id: Text or.Text |
Button | [ ] or[Text] or[control_id: Text] .(From here on simplified as
id_and_text ). |
Text field | [id_and_text_] (single-line) or[id_and_text__] (multi-line) |
Dropdown Chooser | [id_and_text v] or[id_and_text (choice1, choice2) v] |
Combobox | [id_and_text_ v] or[id_and_text_ (choice1, choice2) v] |
Checkbox | [ ] id_and_text or[x] id_and_text |
Radio button | ( ) id_and_text or(x) id_and_text |
Slider (horizontal) | [id: 0 -+- 100] |
List/Tree view (only in Tk for now) | [= id_and_text] or[= id_and_text (Column1, Column2)] |
Placeholder (empty or framed box) | <name> for empty box;<name:Text> for framed box |
Control ID¶
Each control gets an identifier which is generated as follows:
If a control id is explicitly given, it has of course precedence.
Otherwise, the control Text is converted to an identifier by
- replacing space with underscore
- lower-casing
- removing all characters not in (a-z, 0-9,
_
)- prepending
x
if the result starts with a number.- Special-Case: Labels get
label_
prepended.If that yields no ID (e.g. Text is empty), the ID of a preceding Label (without
label_
prefix) is used. This requires the label to be left of the control in question.If that fails as well, an ID of the form
x1
,x2
, … is assigned.
Examples:
[ Hello ]
gives idhello
[ Hello World! ]
gives idhello_world
Hello World: | [ ]
gives a label with idlabel_hello_world
and a button with idhello_world
[ $%&§§% ]
gives a button with idx1
(assuming this is the first control withoud id).
The control id can be used to get/set the control value or the control object from the form - see below.
Notes about specific widgets¶
Dropdown and combobox without values can be populated after creation.
All radio buttons on one form are grouped together. For multiple radio groups, create individiual AutoFrames for the group, and embed them in a box.
Slider: only supported with horizontal orientation. For a vertical slider, change orientation afterwards; or use a placeholder box and create it yourself.
Listview: The first column will have the text as heading. The subsequent columns have the given column headings. If Text is empty (or only id given), only the named columns are there. This makes a difference when using value-binding (see below).
Value and event binding¶
Control objects¶
Usually you will access your controls from methods in your AutoFrame
subclass. So let us assume that your AutoFrame
variable is called self
.
Then, access the generated controls by using self["control_id"]
or
self.f_controls["control_id"]
. The result is a toolkit-native widget, i.e. a
QWidget
subclass in Qt case, a tkinter
widget in Tk case.
For Tk widgets, if there is an associated Variable object (StringVar
or
similar), you can find it as self["control_id"].variable
attribute on the
control.
Event binding¶
If you define a method named after a control-id, it will be automatically called (“bound”, “connected”) as follows:
- Button: When user clicks the button; without arguments (except for
self
).- Any other widget type: When the value changes; with one argument, being the new value.
Example:
class EventDemo(AutoFrame):
f_body = '''
| |
[ My Button ]
[ Text field_ ]
'''
def my_button(self):
print('My Button was clicked')
def text_field(self, val):
print('Text "%s" was entered'%val)
In case of the ListView, the method is called on selection (focus) of a row.
As second option, you can name the method on_<control-id>
(e.g.:
on_text_field
). Thus the handler can easily coexist with the virtual value
attribute (read on).
Virtual value attribute¶
If the control is not bound to a function, you can access the value of the control by using it like a class attribute:
class AttributeDemo(AutoFrame):
f_body = '''
| |
[ Text field_ ]
'''
def some_function(self):
x = self.text_field
self.text_field = 'new_text'
For label and button, the value is the text of the control.
Boxes are a bit special. An empty box’s value is the box widget itself. A framed box contains an empty box, which is returned as value.
You can set the virtual attribute to another (any) widget the toolkit understands. In this case, the original box is destroyed, and the new “value” takes its place. For a framed box, the inner empty box is replaced. So you can use the box as a placeholder for a custom widget (say, a graph) that you generate yourself.
Note
The new widget must have the same parent as the box you replace.
A second possibility is to use the box as parent for one or more widgets that you add later. For instance, you can render another AutoFrame into the box. (see under Extending).
Value of List / Tree View¶
Note
Lists and tree views are considerably more complex than the other widgets. I am still experimenting with how to make handling as convenient as possible. Be prepared for changes here if you update.
The general picture is this: The Listview has a value, which on the python side looks mostly like a list. You can slice it, insert/remove items and so on.
Inserted items are displayed in the list view in textual form. The value list is attached to the actual list view. I.e. if you update the list, the changes immediately reflect in the ListView widget.
The value list or its items can become detached if you replace the list or pop nodes of it. You can still use it like a normal python object, but it will not have an onscreen representation anymore.
The sources
method of the list can be used to configure how values are
read from the given objects into the predefined columns. By default we look for
attributes matching the column names. If you have a first column (defined via
the “Text”, not the “Columns” list in parens), it gets the object’s string
representation.
That means that the simplemost way of using the List is this:
class SimpleList(AutoFrame):
f_body = '''
|
[= Some Items]
'''
def f_build(self, parent, body):
super().f_build(parent, body)
# populate the list
self.some_items = ['First', 'Second', 'Fifth']
A more complex example to showcase how additional columns work:
# RankRow is a stand-in for a "real" class.
RankRow = namedtuple('RankRow', 'name points rank')
class TreeDemo(AutoFrame):
f_body = '''
| <-> |
I[= Players (,Name, Points, Rank)]
'''
def f_build(self, parent, body):
super().f_build(parent, body)
self.players = [
RankRow('CaptainJack', 9010, 1),
RankRow('MasterOfDisaster', 3010, 2),
RankRow('LittleDuck', 12, 3),
]
# Replacing items triggers updating of the displayed data
self.players[2] = RankRow('BigDuck', 24, 3)
# change the data binding:
self.players.sources(
lambda obj: 'ItsLikeMagic', # unnamed arg: sets the default text (first column)
name=['foo'], points=['bar'], # use __getitem__ for those
# custom callback
rank=lambda obj: obj['baz'],
)
self.players.append({'foo': 'Last', 'bar': -1, 'baz': 4})
When working with the list, keep in mind that it can be changed by user interaction (like any other widget’s value). Currently the only possible change is to re-sort the list, but more (edit, add, remove items) might come.
Note
Currently Tk and Qt toolkit behave notably different concerning lists. Tk retrieves the “source” values once to build all the list items. Meaning that changes in the underlying items do not reflect in the list unless explicitly updated.
Qt on the other hand queries the items permanently (e.g. on mouse-over). This means that changes are immediately visible onscreen, but that you should not do complicated calculations or I/O to retrieve column values.
Trees are created by using the ObsList.children_source
method,
which works similar to sources
. Here you can define two sources, one
for has_children
(bool) and one for children
(list).
The tree is lazy-loading, i.e. children are only retrieved when a node is expanded. On repeated expansion, children are reloaded.
has_children
is queried to determine whether expanders should be drawn on
each item. If not given, we assume that each entry might have children, and they
all get expanders initially.
The children
property, if retrieved, is again a special list like the “root” one.
To identify items in the tree, the two methods ObsList.find
and
ObsList.find_by_toolkit_id
are provided, which yield container list
and index given the item or its toolkit-native identifier, respectively.
For Tk, the toolkit-native identifier is the iid
value of the tree item.
For Qt it is unset; only parent_toolkit_id
is set to the parent
QModelIndex
. Given a QModelIndex
, its internalPointer()
refers to
the containing list and row()
gives the index of the item.
Extending / integrating¶
In any real-world scenario, you will hit the limits of this library pretty soon. Usually it boils down to one of the questions:
- How do I use toolkit-native methods on the widgets?
- How can I embed generated controls into a “3rd-party” window?
- How can include “3rd-party” controls in the generated grid?
Toolkit-native methods¶
Having an AutoFrame self
, access the toolkit-native controls by using
self["control_id"]
or self.f_controls["control_id"]
. Do whatever you
like with them.
Embedding AutoFrame
into a 3rd-party host window¶
The AutoFrame.f_build
method takes a parent window as argument. You can
use this to “render” the AutoFrame into a custom container.
- The container can be any widget taking children. It must be preconfigured to have a grid layout. I.e. for
tk
toolkit,.pack()
must not have been used; in case ofqt
toolkit, aQGridLayout
must have been set via.setLayout()
.- Already-existing children are ignored and left in place. However, row/column stretching is modified.
- Automatic method / property binding works as usual.
Including 3rd-party controls into an AutoFrame
¶
This is what the <placeholder>
control is for. It creates an empty Frame /
Widget / Panel which you can either use as parent, or replace with your own
control.
For the former, get the placeholder object (via its value attribute) and use it as parent. You must do the layout yourself.
For the latter, set its virtual value attribute to your widget. This destroys the placeholder. The layout of the placeholder (Grid position and stretching) is copied onto the new widget.
Nesting AutoFrame
¶
Combining both methods, you can also embed one AutoFrame into another. The following example showcases everything:
class Host(AutoFrame):
f_body = '''
|
<placeholder>
'''
def f_build(self, parent, body=None):
super().f_build(parent, body)
# self.placeholder.setLayout(QGridLayout()) # only for Qt
# create instance
af_embedded = Embedded()
# render widgets as children of self.placeholder
af_embedded.f_build(parent=self.placeholder)
# store away for later use
self._embedded = af_embedded
class Embedded(AutoFrame):
f_body = '''
|
<another placeholder>
'''
def f_build(self, parent, body=None):
super().f_build(parent, body)
parent = self.another_placeholder.master
self.another_placeholder = tk.Button(parent, text='3rd-party control')