Input Dialogs¶
From the beginning pyFormex was intended as a framework for creating parametric models. This means that some parameter values need to be fed to the models to instantiate them. Often it is more appropriate to ask these values interactively from the user, rather than hardcoding them into a script.
The pyFormex user has full access to the Qt framework on which the GUI was built. Therefore he can built input dialogs as complex and powerful as he can imagine. However, directly dealing with the Qt libraries requires some skills and, for simple input widgets, more effort than needed.
Therefore pyFormex has a very powerful but still easy to use system for the creation of such dialogs. This document lays out the basic ideas of the dialog creation and use. The examples named in parentheses are available from the pyFormex menu Script->Scripts->userguide.
Modal Dialogs¶
A modal dialog shows an input dialog to let the user enter some value(s), and blocks all other user interaction with the application until the user either accepts valid input or cancels (and rejects) the input. This is the typical operation when the program needs some data from the user in order to continue.
A simple dialog¶
In many cases we can just make use of the gui.draw.askItems()
function. Here’s a simple example (dialog1):
"""Simple input dialog
Creates an (nrows, ncols) array filled with numbers from arange(nrows*ncols).
The values nrows and ncols are asked from the user.
"""
res = askItems([
dict(name='nrows', value=3),
dict(name='ncols', value=6),
])
if res:
nrows = res['nrows']
ncols = res['ncols']
A = np.arange(nrows*ncols).reshape(nrows,ncols)
print(A)
The askItems function takes a list of input items as its first (and only
required) argument. Each input item is a dict where the keys have predefined
meanings. There are many optional keys, but at least the name and value keys
should be present. The whole dict should be acceptable as keyword arguments
for the gui.widgets.InputItem
class. See Input Items
for more details.
The name specifies the key in the result dict that will contain the value
entered by the user. The value is the initial value that will be displayed
when the dialog is first shown.
The value also serves in simple cases to identify the type
of the result: thus a str, int, float given as value will automatically
require that the same type is returned. If you want to get an integer back
as a string, you should specify: value='3'
.
The askItems function constructs a dialog, displays it to the user:
It then lets the user interact with the dialog until he either accepts or rejects the input data. Accepting the data is done by clicking the OK button or pressing the ENTER key. The data can be rejected by pushing the CANCEL button or hitting the ESC key. The dialog created by askItems is a modal one, meaning that all interaction with the rest of the pyFormex interface is blocked until the user either accepts or rejects the data. When the results are accepted or rejected, the dialog is closed and the user can again interact with other pyFormex windows. In Modeless Dialogs we describe how to create Dialogs that can stay open and allow the user to interact with other windows.
The return value of askItems is always a dict. If the user accepts the data, the dict contains a value for each of the items specified, with the name of the item as key. The value will be of the type implied by or specified in the input item. If the user rejects the data, it will be an empty dict. It is good practice to always test the result to see if it contains values, as is done in the example above.
If you run this example and accept the default values, you will see this printed:
[[ 0 1 2 3 4 5]
[ 6 7 8 9 10 11]
[12 13 14 15 16 17]]
Convenience functions¶
To avoid excessive typing and to make the structure of the dialog creating source more clear, pyFormex offers some convenient shorthand functions: _I, _G, _C and _T. We will use these abundantly in this user guide.
The most important is _I
, which creates a single input item, accepting
the name and value as positional arguments.
We will study this function in more detail in Input Items.
Using the _I
function, we can simplify the dialog1 script to (dialog2):
"""Simple input dialog
The same dialog as dialog1, but using the _I function to construct
the items, and globals().update to set the results in global variables.
"""
res = askItems([
_I('nrows', 3),
_I('ncols', 6),
])
if res:
globals().update(res)
A = np.arange(nrows*ncols).reshape(nrows,ncols)
print(A)
The globals().update(res)
is a convenient shortcut to convert
all the values returned from the dialog to global variables. Of course
this only works if all the field names are valid Python variable names.
It is only recommended in small and simple scripts, where you can easily
avoid name clashes in the global name space.
Input validation¶
When an input item uses a string editing field as interactor, it is usually not possible to type anything that would obviously lead to an invalid value. For example, the integer input fields in the above example will not allow you to enter a non-digit (except for the minus sign).
It is however still possible to have an invalid value in the edit field. You can for example delete the input field contents: this has to be possible in order to replace a value with another one. But the empty string is not a valid integer. If this value would be accepted, the remainder of the script would fail.
There are often more restrictions needed on the input values to guarantee a proper operation. For example, in the dialog2 above, it is possible to enter negative integers. If you only enter one negative value, the script will still continue normally, producing an empty array. If you enter both values negative though, the script exits with an exception traceback. We could test the values nrows and ncols in the script, and only proceed if the values are not negative. But pyFormex dialogs offer a better way: specify the value bounds on the input item, and let the pyFormex GUI handle the validation (dialog3):
"""Simple input dialog
Similar dialog as dialog2, but with limits on the input values,
more explicative labels, and a function to print the array.
"""
def print_array(nrows, ncols):
A = np.arange(nrows*ncols).reshape(nrows,ncols)
print(A)
res = askItems([
_I('nrows', 3, min=0, text='Number of rows'),
_I('ncols', 6, min=2, max=10, text='Number of columns'),
])
if res:
print_array(**res)
The min and max values specify the lowest and highest acceptable int value
for the field. For nrows, we accept any non-negative value. For ncols, we
restrict the values to the range 2..10. The text option in the input items
specifies the label displayed before the field. If text is not provided,
the field name is used instead, as in the previous dialogs. A final change
is that we use a print_array
function to create and print the array.
By using the same argument names as the field names, we can easily process
the dialog results by unpacking them with **res
.
The dialog now looks like this:
While it is now impossible to enter negative values, specifying bounds
still leaves possibilities for intermediate invalid values. Take the
case of ncols, which specifies the range 2..10 as valid: it is possible
to enter a single 1
in the field, since that could be the first digit of
the valid number 10. Again, if this value would be accepted, an
invalid value would be produced.
The problem of intermediate invalid values is handled by pyFormex Dialogs as follows: when the user performs an action to accept the dialog (pressing OK or hitting ENTER), a validation of all data is performed, and if any fields hold invalid data, these fields are flagged and the data is not accepted:
Hovering the mouse over the red invalid button will pop up a tip with the reason:
Then the user interaction continues until all data are valid and accepted, or the data are rejected.
Modeless Dialogs¶
Modal Dialogs are a good solution when some information from the user is required to continue. But there are cases where we want to construct a dialog that stays open while allowing to further interact with the other windows. Such modeless dialogs need to be handled somewhat differently. The system can not wait for results. Instead you will have to define actions that are to be executed when activated, usually by clicking some button.
Here is the above dialog3 converted to a modeless dialog (dialog4):
"""Simple input dialog
A modeless dialog with the same input items as dialog3
"""
def print_array(nrows, ncols):
A = np.arange(nrows*ncols).reshape(nrows,ncols)
print(A)
def show():
if dialog.validate():
create_array(**dialog.results)
def close():
dialog.close()
dialog = Dialog([
_I('nrows', 3, min=0, text='Number of rows'),
_I('ncols', 6, min=2, max=10, text='Number of columns'),
], actions=[('Close', close), ('Show', show)])
dialog.show()
The first function, create_array
, is the same as before
The next two functions are the actions that we want to be provided
by the dialog: show
creates and prints the array with the current
input data; close
just closes the dialog. The show
function is
important here: it first validates the current input data. If these are
valid, they become available in a dict attribute results
of the
dialog, so we can use that directly to proceed with our purpose.
Finally, we construct the Dialog. Its first argument specifies the input
items. This is exactly the same as the argument of
askItems
in script_dialog3.
But we have a second keyword argument defining the actions to appear in the
dialog. This is a list of tuples defining pushbuttons (actually it is the
input for a ButtonBox
constructor.
Each button is defined by a tuple of a string and a function.
The string is the text shown on the button. The function should
take no arguments and will be executed when the button is pressed.
Now you can repeatedly change the input fields and click the Show
button and see an array printed with the specified size. And when you’re
done, click Close
to close the window.
Dialog class¶
The Dialog class is the most important class in pyFormex for constructing user input dialogs. It is very versatile and dialogs can range from very simple to extremely comprehensive. In this user guide we will describe the most commonly used features. See the pyFormex reference manual for a full description.
Here is an example that is slightly more complex than the previous (dialog5):
"""Simple input dialog
A modeless dialog with the same input items as dialog3
"""
def create_grid(base, nrows, ncols, shear, color, custom):
F = Formex(base).replic2(ncols, nrows).shear(0, 1, shear)
if color == 'index':
color = arange(nrows*ncols)
elif color == 'custom':
color = custom
F.attrib(color=color)
return F
def show():
if dialog.validate():
F = create_grid(**dialog.results)
clear()
draw(F)
def close():
dialog.close()
flat()
dialog = Dialog([
_I('base', choices=['4:0123', '3:016']),
_I('nrows', 2, min=1, text='Number of rows'),
_I('ncols', 3, min=1, text='Number of columns'),
_I('shear', 0.0, text='Shear dx/dy'),
_I('color', choices=['index', 'random', 'custom'], text='Color'),
_I('custom', value='black', itemtype='color', text='Custom color'),
], actions=[('Close', close), ('Show', show)])
dialog.show()
for c in pf.GUI.children():
print(c)