Using PAGE

PAGE is built upon the program Visual Tcl, but is different because of differing objectives of the two programs. Visual Tcl was conceived as complete build environment for Tcl. It included facilities for managing projects with multiple windows, creating of GUI’s, binding actions with callback procedures, creating menus, writing functions, and testing the application, all the while supporting many different widget sets and several geometry managers. PAGE is limited to defining a single GUI window using Tk and ttk widgets and the placer geometry manager; there are better environments for building and debugging Python programs once you have code for the GUI.

PAGE makes use of the Virtual Tcl facilities for creating a single GUI window, assigning attributes to widgets, binding events to callback procedures, and creating menus. PAGE also automatically generates skeletal callback functions and supplies much of the boilerplate code for running Tkinter.

When PAGE generates the code for a window, all of the code for causing Python to create and map that window is generated - a Python class for the toplevel window with all the code necessary to instantiate the class (i. e., display the window), import statements, Tk mainloop and initialization, a main procedure, etc. Therefore, if you have provided the skeletal callbacks the generated code is executable and you can see just how the GUI will look in a Python environment. PAGE even attempts to generate skeletal callbacks. As mentioned above the generated code resides in two modules.

The generated code can be used as the skeleton for your application. However, the generated GUI may be a secondary GUI and all that you really want to do is instantiate the toplevel class, use support functions implied within the GUI and possibly to destroy that window. You can do that by importing the code as a module and invoking functions within that module including automatically generated functions for creation and destruction of the window. This will be discussed in more detail later.

I often use the generated GUI classes as starting points for customization. When you are not sure how to start or want to quickly try out some GUI ideas, PAGE is very useful because it generates all of the boilerplate necessary.

PAGE is invoked by executing the “page” script located in the page directory. I go to that directory and issue the command:

./page [filename]

I preface the command with ”./” to be sure I am executing the page script in the page directory. By including “page” in the PATH environmental variable, one can work in any directory.

PAGE can be invoked with zero or one file names. If supplied the file name should have an extension of ”.tcl” and the file should exist. If another extension is supplied that is interpreted as an error and PAGE terminates. If no extension is given in the file name or the file name ends in ”.” , an extension of ”.tcl” is assumed by PAGE. If the file cannot be found PAGE terminates because PAGE expects a file generated by PAGE. If a file name is specified, the file should be a tcl file saved during a previous PAGE session; it will be opened as PAGE begins executing as an alternative to File->Open for proceeding from a saved PAGE session. Near the top of a ”.tcl” design file created in PAGE there is a comment containing version information and a timestamp. If that version information is not present then PAGE will refuse to open the file.

As you use PAGE to build a GUI, you can save the current state of the GUI at any time from the File->Save of File->Save As menu. In fact, it is a good idea to save your status often in case PAGE fails or dies.

I am sometimes exasperated in PAGE because so many functions can be performed several ways.

Overview

This section attempts to describe the main functionality of PAGE when building an application with a single root window. It will yield a Python module which implements the application interface.

One uses PAGE to generate a GUI as follows:

  • Start PAGE by executing “page” or activating the PAGE icon on the Windows desktop. In Windows one may also start PAGE from the command line by going to the installation directory and executing winpage.bat.
  • Create the new top window by selecting the top left button in the widget toolbar.
  • Drag the toplevel window to where you want it.
  • Resize the toplevel window by dragging a handle at the corners or edges.
  • Change the title by changing the title attribute in the Attribute Editor.
  • Add a menu to the top level using the Menu Editor entered from Widget->Edit Menu in the main menu.
  • Drag appropriate widgets from the tool bar to the toplevel window or other previously placed container widgets.
  • As desired to make the generated code more readable, specify an unique alias for the widget.
  • Adjust properties of the widgets and the toplevel window as desired. Some of those properties will include specification of functions to support the GUI such as to load list box, or to respond to mouse selection, etc. Included can be the specifications of event bindings.
  • Use the function definition facilities to specify the necessary functions and use the bind specification facilities to bind events to functions.
  • When the window has the appearance that you want, select Gen_Python->Generate Import Module. A new window will appear and fill with the skeleton functions and the definitions of the Tkinter variables. Save the source window of the python console.
  • Then select Gen_Python->Generate Python GUI from the main menu. A new window, a Python Console will open and fill with the Python code, which I call the GUI module. To generate the support module select Gen_Python->Generate Support Module, which creates and fills another Python Console with the generated skeleton support module.
  • You can save the code in one or both of the Python modules and then try executing the GUI module to see if you like what you have designed.

Placing and Modifying Widgets

Toplevel Geometry

PAGE generates a single Toplevel Tk window which is the users GUI for his application. (One can have multiple GUI windows; see Applications with Multiple Top-Level Windows.) As elsewhere with Tk and PAGE, there is more than one way to specify where the GUI window will appear on the application user’s screen.

Prior to release 4.5, the PAGE user would select the Toplevel button in the Widget Toolbar and a toplevel window would appear on the screen. As well as filling in subwidgets the user could drag the toplevel widget to the desired screen location and also could adjust its size. When the Python code was generated it would contain code that would place the GUI at the exact spot specified. If you had placed it at pixel specification +1000+300, it would end up 1000 pixels from the upper left corner and 300 pixels down.

With version 4.5, the Python code generation addresses the toplevel placement with a new attribute - “default origin”.

The “default origin” preference, if selected will cause the generated Python GUI window to placed on the screen at the default location as determined by the system window manager. If false, the location will be determined from where the toplevel window is placed in PAGE. The default for the this attribute is false, but can be changed by means of the Preferences mechanism. A default of false will cause PAGE to behave as before. Unfortunately, I was faced with an unclear mnemonic in the Attribute Editor or a double negative; I chose the latter

Adding Widgets

All that is necessary to add a widget to the GUI is to select it with Button-1 from the Widget Toolbar, position the cursor inside the destination container and again press the Button-1. A small version of the widget will appear with its upper left-hand corner at the point of the Button-1 click.

Aliases

Often algorithmically generated names can be difficult to understand in computer generated code. To reduce the problem in PAGE, users can specify more easily understood names called aliases.

An alias is a user specified identifier of a widget in the generated code. Obviously, aliases must be unique within the class which will be generated in the python code to realize the GUI window.

  • An easy way to specify an alias is to select the target widget with a Button-3 click and then select “Set Alias ...”. Another small window will appear and one can add the alias. Finally close that window by selecting the “check” or with the Enter key.
  • Another way to enter an alias is to select a widget with Button-1 and then select “Set Alias ...” from the Option menu in the main PAGE window.
  • Also, one may specify an alias by selecting a widget and editing the “Alias” field in the top section of the Attribute Editor.

An alias must be a legal python identifier. PAGE does allow blanks in an alias but they will be changed to “_”. Also Alias has no meaning for top level windows. For top level windows the the variable names are generated from the title attribute. There are numerous schemes for generating such names but one that has been suggested is based on CamelCase. It is:

  • Buttons could start with ‘btn’, i.e. btnQuit, btnNew, etc.
  • Entry boxes could start with ‘txt’, i.e. txtFirstName, etc.
  • Check boxes could start with ‘chk’ I.E. chkDoThis, etc.
  • Radio Buttons could start with ‘rdo’ or ‘rbtn’.

Note that PAGE has an option to automatically generate aliases. These aliases are algorithmically generated name but are more readable than the default generated names. This is controlled by a new field in the Preference window. I recommend that you go into Preferences, check the value and save the preferences. Why the original Visual Tcl program had two methods of generating names, one comprehensible and one not, but chose the less readable one as the default is baffling.

Selecting and Modifying a Widget

There are several ways to select a widget for modification and I don’t want to keep repeating the variations through out this document.

For simple widgets like buttons or text boxes you can select the widget either by selecting the widget with Button-1 in the GUI or in the Widget Tree.

With more complex widgets like notebooks, paned windows, or scrolled widgets, clicking Button-1 inside the widget will select a child widget rather than the whole widget. The widget may be selected with Control-Button-1. Alternatively, the widget may be selected with Button-1 in the Widget Tree. When dragging the widget or a handle hold down the control key.

Once selected there are several ways to modify the widget. :

  • A selected widget may be moved by dragging the widget or resized by dragging a handle. When the mouse is over a handle, it turns red.
  • Attributes can be changed in the Attribute Editor. Geometric attributes can be changed Attribute Editor also
  • Some changes can be made using the Widget Menu.

If you are working with standard Tcl widgets, there are many options that can be modified whereas the ttk widgets have very few options, their appearance being governed principally by the specified theme.

Fill Container

“Fill Container” is a feature that was added in version 4.8 and causes the selected widget to expand to fill its container provided that there are no other widgets already in the container. This function is restricted to those widgets for which I think it makes sense, like frames, notebooks, scrolled widgets, canvases as well as text and list boxes.

To use this feature activate the Widget popup menu with Button-3 over the widget itself or an entry in the Widget Tree and select “Fill Container” from the widget submenu.

Cut, Copy, and Paste

After much work I think that there is now a useful cut, copy, and paste feature in PAGE. The basic way it works:

  1. Select the widget you want to paste in a new spot or container by selecting it as you would for just plain moving it.
  2. With Widget Menu and select with Button-1 Cut or Copy as you want.
  3. In the Widget Menu select Paste or use Control-V or select Paste from the Main menu -> Edit.
  4. Move the mouse to the desired insertion point and click Button-1.

Copy and Paste are important features because one cannot drag a widget from one container widget into another but one can cut or copy and paste to get that effect. For instance if one creates a button in a top level window and then decides that it should be moved to a frame or a notebook tab, that cannot be accomplished that just by selecting the button and dragging it to the frame. However it can be done with cut and paste.

When doing cut-copy-paste I make fewer mistakes by selecting the widget the Widget Tree than trying to grab it in the top level window. Correct selection is crucial important when trying to select and copy nested widgets.

I have implemented cut and it appears to work; however, I never use it. There is a sequence of several steps between the selecting copy from the menu and selecting the location of the paste. If you get it wrong following a cut, you can’t go back and retry the operation; the source for the copying is gone. So, I stick to copy and paste and then finish with a delete.

One thing that cut, copy, and paste cannot do is copy from one module’s GUI and paste in another module’s GUI.

Widget Menu

The Widget Menu, which appears in more than one place including the main menu as well as the drop menu in the Widget Tree or a top level window, provides a convenient way to modify some of the widget attributes. The drop menu is activated with Button-3 applied to the selected widget and its features are dependent on and apply to the widget selected. It is seen below:

_images/widget-menu.png

In the case above the widget submenu allows one to easily specify the text attribute and for specifying multiple line text labels.

Linking Events to Actions

I will not try to explain event binding fully for Tk and Tkinter. I have read several books and many web pages on the subject but feel little mastery.

The point of building a GUI is to link actions (the execution of specific code) to some event within the GUI like selecting a button with a mouse key, typing a particular character into a text field, or resizing window a widget. Binding events to widgets is a very confusing aspect of using Tk and Tkinter. Tk implements a global binding hierarchy in that Tk allows one to create bindings between

  1. events and a particular widgets such mouse selection and a particular button,
  2. events and particular classes of widgets such as mouse selection and all buttons in an application,
  3. events and all widgets in a toplevel window, and
  4. events and all the widgets in an application.

PAGE really assists in only the first type of binding. At least one respected documenter recommends against using the other three.

Grayson, in his book talks about invoking callbacks directly or indirectly. Specifying a command attribute leads an indirect invocation while specifying a bind command leads to a direct invocation. The difference is that the direct invocation passes an event object to the callback and the callback function must have an argument list which includes a parameter for the event. To see examples of direct and indirect invocation of callbacks see the vrex example or the bind example. One could also characterize them as clear and confusing, or easy and complex. Let me discuss the easy case first, that of specifying a a command attribute.

Many widgets have a command attribute which specifies the code to executed when the widget is selected with Button-1. This is a simplified way of binding for the common case of selecting the widget with Button-1. While Tk allows one to specify a block of code, one must stick with a function name in Python. For example, setting the command attribute of a button to “foo” so that selecting the button will cause the invocation of the function “foo” with no arguments.

If you want to invoke a function and pass parameters to it, you use a lambda expression. Please see section 6.4 of Grayson’s book for a fine explanation of the use of lambda expressions in this context. (In a nutshell, if Python see a function name followed by parenthesis it will try to execute immediately, whereas the execution is desired when the event occurs.) Let say that if you want to call the function foo and pass it 3 as an argument, what you enter as command is

lambda : foo(3)

not

foo(3)

or to pass a variable parameter

lambda x: foo(x)

not

foo(x)

To specify such a command, one selects the widget and then in the Attribute Editor enters the command in the Command field. In keeping with the Virtual Tcl style of having too many ways of doing most things, an alternative is to select the widget in the Widget Tree with Button-3 and then Widget->Set Command. I don’t use the alternate method very often. I would probably forget it but for this paragraph.

So much for the binding of the easy case which essentially defines a configuration command for setting the command attribute of a widget in the generated Python. For binding other events to widgets I point you to the Bindings Window which essentially builds a bind command. Of course, the bind command could be manually coded in init function of the support module.

Bindings Window

There are many events that can be linked to code. See the Tk man pages and Chapter 6 of Grayson. They include responding to the different mouse button pressings or releases, a window getting or loosing focus, etc. as well as virtual events. Bind is the command for linking one of these events to code and is accomplished by using the Bindings Window. Here the code must be a lambda expression because it pass an event object containing much useful information to the callback. Consequently, the callback function must have an event parameter in its argument list.

The Bindings Window can be opened by selecting the widget with Button-3 and the selecting Bindings... from the popup menu. Basically,

  1. select the widget in the left column if not already selected,
  2. Insert the desired event from the Insert menu,
  3. select the item in the left column, and
  4. fill in prototype lambda expression in the right hand column.

The Bindings Window can be opened either from the popup menus in response to selecting the widget with Button-3 or selecting the widget with Button-1 and typing Alt-b. Then select Insert from the menu to put the event in to the left pane. Finally insert the desired code in the right pane where a template will appear.

Let’s clarify that and look again at

_images/binding.png

In the left column you will see the Tcl name of the selected widget and below the name a list, initially empty) of the events already bound to the widget. In the image two events have already been bound, one of which is a user defined virtual event, “<<Bingo>>”. Following the event information of the selected widget is a bunch of information about the bind hierarchy related to the selected widget. I have not found any use for that stuff.

Two things can be accomplished with the Binding Window are

  1. define a new binding for the selected widget The key action for the first is to insert a new event. It is done by clicking Insert Menu and selecting one of the actions there.
  2. modify the lambda expression of an existing binding in the obvious way.

Selecting Insert yields

_images/insert-bind.png

If you do not find the action you can select Advanced ...

_images/advanced-bind.png

Here you are presented with a wide selection of possibilities.

First, if you want the event to be the pressing of the key c, select the entry box at the top and type “c”; the event “<Key-c>” appears in the Event entry box near the bottom. If you want to modify the event to require the control key at the same time, select Control from the right column and the event becomes “<Control-Key-c>”.

For events other than a key press event, then select an event from the left column. The list of events there is more or less every event Tk knows about. I know of no way of listing only those events to which a particular widget will respond, so I can’t prune the list.

Again, the selected event may be modified by picking a modifier from the right column. Notice that the left column contains all of the virtual events that Tk know about. If you are going to generate a new virtual event, say <<Bingo>>, you can add that to the Event entry box manually.

Having now composed your event, you can select the “Add” button which will close the insert window and add the event to the left column of the Binding Window.

The penultimate step is to appropriately change the skeletal code in the right column. Basically, just add parameters and supply the callback function name in place of “xxx”. Finally, save the result by selecting the check button.

The big difference here is that the callback is always passed a parameter and so a lambda

There is a difference here in the lambda expression from what was discussed above in connection with the command attribute in that an event object is passed to the callback which is the event object. This means that the command in the simplest form (no user parameters) is something:

lambda e: foo_bar(e)

or if passing user parameters:

lambda e, x: foo_bar(e,5)

The parameter e above is the event object which contains much information about the event. The object has the following attributes (from the documentation found in Tkinter.py):

serial - serial number of event
num - mouse button pressed (ButtonPress, ButtonRelease)
focus - whether the window has the focus (Enter, Leave)
height - height of the exposed window (Configure, Expose)
width - width of the exposed window (Configure, Expose)
keycode - keycode of the pressed key (KeyPress, KeyRelease)
state - state of the event as a number (ButtonPress, ButtonRelease,
                        Enter, KeyPress, KeyRelease,
                        Leave, Motion)
state - state as a string (Visibility)
time - when the event occurred
x - x-position of the mouse
y - y-position of the mouse
x_root - x-position of the mouse on the screen
         (ButtonPress, ButtonRelease, KeyPress, KeyRelease, Motion)
y_root - y-position of the mouse on the screen
         (ButtonPress, ButtonRelease, KeyPress, KeyRelease, Motion)
char - pressed character (KeyPress, KeyRelease)
send_event - see X/Windows documentation
keysym - keysym of the the event as a string (KeyPress, KeyRelease)
keysym_num - keysym of the event as a number (KeyPress, KeyRelease)
type - type of the event as a number
widget - widget in which the event occurred
delta - delta of wheel movement (MouseWheel)

A couple of points need mentioning. First, the Binding Window contains a lot of cruft below the event entries for the selected widget which I don’t understand and can’t use. It seems to be a listing default events for the binding hierarchy of the widgets alluded to above. I am toying with just removing it. In addition I have never done anything good with the buttons below the menubar except for the check button. I might dispense with them also. Stay tuned.

With version 4.8, Custom Widgets were introduced. Since PAGE knows nothing of those widgets including binding possibilities, the mechanism just described cannot be used to specified bindings for such widgets. You will have to do that in the support module.

Defining Functions

If you name a function in a menu, an event binding, command attribute, etc., then definition of that function is required before trying to execute the GUI in PAGE.

Executing the GUI means to execute the generated Python code within PAGE to demonstrate how the GUI will appear in the completed application by selecting the Run button on the Python Console. To satisfy requirements of the Python interpreter Tkinter variables and callback functions have to be defined. Skeleton functions will be satisfactory since at that state of development you really don’t expect the application to usefully function.

One path for the user is to do nothing, letting PAGE create a skeleton for you by just noticing that the a name is specified in a binding or an attribute in the support module as described in Rework. This is the recommended approach as of version 4.2.

I usually just let PAGE generate the skeleton functions, run the resulting code to see what my window will look like and then do the rest of my programming in emacs. When PAGE stores the python modules, several layers of backup files are retained. If I need to regress to one of them, I find that Meld is a wonderful utility for managing the differences between versions.

Special Widget Processing

Toplevel Widget

Toplevel widgets don’t really have a title property. It was added in Visual Tcl and the Attribute Editor originally displayed it as one of the geometry attributes. That didn’t seem like a good place so I moved it to the Attributes section. The title is displayed at top of the window and is used with “_“‘s replacing blanks, as the name of the generated Python class.

Among the attributes listed for a toplevel widget is ‘menu’ which in PAGE allows one to easily create a menubar at the top of the widget. Click on the attribute and follow the procedure below.

An improvement in version 3.5 was the ability to change other properties such as the colors and the cursor of Toplevel widgets. One property of Toplevel widgets is relief. However, I was totally unable to change that property at either the Python or Tcl/Tk level. I simply don’t know how.

With version 4.8.6, the Toplevel widget may be deleted by closing the window in PAGE with the delete control button in the window title bar or Alt-F4. Since only one Toplevel widget is allow in PAGE this has the effect of restarting the design session.

Relative Placement

Following the paradigm I am familiar with from VB, PAGE uses the place window manager to fix location within the generated GUI. Thanks to George Tellalov, who suggested using relative placement for widgets within the GUI thus allowing one to build stretchable GUI’s. They allow one to grab an edge or corner of the executing Python GUI and change its size and while maintaining relative positions and sizes of the internal widgets. This is the default behavior. Keep in mind that with relative placement, widgets may change size as the toplevel window is resized, but fonts do not change size.

Since I don’t think buttons or labels should change size in step with the change in size of the window, relative placement for buttons does not change the size of buttons but the relative placement is maintained.

Tkinter Variable Classes

With several of the widgets, it is necessary to have linkage between tkinter variables and Python variables. For instance, when one moves a slider of a scale in the GUI window, he wants the value to be reflected in a Python variable or conversely changing that variable should change the position of the slider. This is done by means of the Tkinter variable classes: BooleanVar, DoubleVar, IntVar, and StringVar.

You need an instance of one of these classes. PAGE guesses the variable type which you may need. You can then use the get method in python to determine the value of the variable in tk and use the set method to set the value of the tk variable. There are examples below. For more information see the Tkinter Variable Class on the effbot.org web page.

For instance if you are using a TScale widget to be coupled with the tk variable variable “val”:

def set_Tk_var():
    # These are Tkinter variables used passed to Tkinter and must be
    # defined before the widgets using them are created.
    global val
    val = DoubleVar()
    val.set(5)   # Initial value to be displayed.

then you need to set the TScale attribute to “val”. The rule is that the Tkinter variable must exist before it the widget class is instantiated. It may appear in the toplevel class definition like:

self.che26.configure(variable=self.var)

When val is changed

val.set(14)

the TScale will move to that value.

If the TScale is changed in the GUI, you can read the new value with

val.get()

PAGE tries to help out by generating an instance of the appropriate global class variables as needed in the support module. Again, this is skeletal code to help the generated Python code run from within the Python Console. It recognizes that a variable starting with “self.” is a class variable. Again, I do not use these support variables because I think that the generated Python is less clear.

For more discussion of Tkinter Variable Classes see Tkinter 8.5 reference.

Ttk Widgets

There are some aspects of the ttk widget set that have presented me with some significant difficulties mainly due to “styles”, it may be merely that I don’t understand the ttk widgets well enough or to problems with their implementation and documentations. I will try to explain my problems below. I would welcome any suggestions.

Scrolled Widgets

For some reason that I don’t understand, the Tk folks have never seen fit to implement scrolled widgets such as a Scrolled Text widget. I certainly don’t want to fuss around with all the separate programming tasks required for build a scrolled text widget when building a GUI.

I am especially pleased that Guilherme Polo in his Pyttk-samples package shows how to build Scrolledtext and Scrolledtreeview widgets. Borrowing that code, I was able to include such widgets as well as a Scrolledlistbox in PAGE.

One can select a scrolled widget from the Widget Toolbar and place it in the GUI and PAGE will include all of the Python support coded necessary to realize the scrolled widget. The scrolled widgets that I added are not named with an T because they are not official ttk widgets. The Scrolledtextbox has a normal text subwidget in which I have chosen to make the “wrap” default “none”.

Although Polo implemented his code with ttk widgets, I have used his ideas to implement scrolled tk widgets as well. In fact, where possible I have used tk widgets in preference to ttk widgets. For instance, I have based only the ScrolledTreeview and Scrolledcombobox on ttk widgets. I am indebted to Polo for his ideas but he bears no responsibility for any errors I make interpreting his ideas.

The scrolled widgets are compound widgets containing as-needed scrollbars and an elementary widget. As such, to set attributes or apply bindings, first select the elementary internal widget.

When the scrolled widgets are placed into a container window, the image shown displays vertical scroll bars to facilitate identification. The best is that the appearance is similar to that which is shown when the GUI is executed because as implemented in the Python code the scroll bars appear only when required. I had a lot of trouble with background colors in the ttk::scrollbars used in the Python GUI.

These widgets are complex widgets, so to move or resize them, use Control with Button-1. The scrollbars are ttk::scrollbar widgets which appear only when needed, i. e., when an item extends beyond the allotted space.

When inserting text inside a Scrolledtext widget, treat it like a text widget. For example, uses code like

obj = self.Scrolledtext1
obj.insert(END, "This is text to be inserted")

When cutting, or copying scrolled widgets, I use the widget tree for easily selecting the whole widget rather than just the interior widget.

Ttk Notebook

If you select, place, and resize a TNotebook in your GUI window it will display two pages and look like:

_images/notebook.jpg

To change the attributes of the TNotebook, select the notebook editor either by invoking Control-Button-1 in the widget and then selecting edit page from the Widget menu or by selecting it in the Widget Tree with Button-3 and then the Widget entry in the popup menu.

_images/edit-pages.jpg

Here you can do all sorts of interesting things like change the text in the tab and select the tab you want to activate for adding widgets or changing attributes of widgets already added to that page. If you go to the menu Item->Add, it will create a new page to the notebook, in the above example, between Page 1 and Page 2. The Move menu as well as the up and down buttons will change the order of the pages. The move operation will move the selected page one position up or down. The move is circular in the sense that moving the bottom item down will move it to the top, etc..

Another way of navigating the pages is the pages attribute in the Attribute Editor.

Ttk Panedwindow

The TPanedwindow can be added to the GUI by selecting it from the Widget Toolbar and then Button-1 in the container. There are two entries for the TPainedwindow, one for vertical separators and another for horizontal, in the Widget Toolbar. It can be moved around by Control-Button-1 and resized by dragging one of the handles. It is sometimes a bit difficult grabbing a handle unless you select the TPanedwindow widget from the Widget Tree window. Each pane of the paned window contains a TLableframe which fills the pane. Using the label frame was the only way I could find to actually get the a TPanedwindow to appear on my screen. Again, I had few examples to work from.

The paned window is configured by invoking Widget->Edit Panes menu item bringing up the following editor.

_images/edit-panes.jpg

This editor allows users to move among the panes, change the text in the label frame, and to manipulate the position of the sash between the current frame and the next one. You can set the sash position as a percentage of the total size of paned window. Again, one saves the changes by clicking on the green check.

With version 3.2, one can select the TLableframe defining the pane and drag the edge of it to change the sash position. In other words, select a pane with Button-1 and drag one of the interior handle; that will move the sash between the selected pane and the adjacent pane.

The Edit panes window allows one to add additional panes to the window via the Item menu. The implementation of paned windows sets the initial size of the paned window to 200x200 pixels and the the first pane size to 75 pixels. Adding a new pane adds one at the end (the right end of a horizontal TPanedwindow or the bottom of a vertical TPanedwindow, in either case, taking space from prior end pane. Fortunately you can resize the whole TPanedwindow which changes the size of the end pane. Then resize the others by changing the sash positions as described above.

The editor, by including the Move menu and the little up and down arrows allows one can move the a pane to a new position. Again, the move operation is circular.

To move a paned window select the whole window then a spot in a sash between panes and then you can drag the whole paned window.

Ttk Treeview

It was difficult to provide reasonably good support for the Treeview widget. I actually do not support the ttk::treeview widget, rather I have gone directly to the Scrolledtreeview which embeds the ttk::treeview widget in a ttk::frame with auto-scrolling scrollbars.

What I have been able to do is to support the placement of the widget in a window with a default of one column in addition to the tree column both columns are stretchable. By invoking the column editor you may change many of the characteristics of the widget such as the column size and heading, as well as the number of columns. It also allows one to reorder the columns. Note that the column that contains the tree has the index of “#0” and must remain the first (left-most) column. The column editor is invoked with Button-3 in the widget and going to Widget->Edit Columns ... as shown below:

_images/column-editor.png

If the widget were created or a column is added with the configuration option “stretch” is set to 0 (not stretchable), then when the resulting GUI is stretched then the column width will not change. The enclosing widget with the scrollbars will either have a blank area to the right of the last column if the window is enlarged or the last column will not fully show. To get the more desirable behavior, go into the Column Editor in the Widget menu and make at least one of the columns stretchable.

Ttk Entry

While I do support the ttk entry widget, I don’t see any reason to recommend using it because I am unable to change the widgets font and I don’t like being stuck with the TkDefaultFont. Strange to say the ttk Combobox, below, is rumored to be based on the TEntry widget and I am able to manipulate the font size using the style facility.

Ttk Combobox

The combobox requires a list of selectable values to display in the drop-down listbox. These are easily specified from the Set Values entry of the Widget menu. When invoked from the menu a scrolled text box appears and the values are specified by entering them one per line in the text box and then selecting the check mark. The user should include strings as they should appear in the Python code. That means if they are string literals, then they should be enclosed in quotes as shown in the example below. If the quoting is not present it is assumed that they are variables. I do not want to get into the morass of entry parsing literal strings.

_images/SetValues.png

Values can also be set in the “init” function in support module using code like:

w.TCombobox1['values'] = ('USA', 'Canada', 'Australia')

My style problem with the TCombobox is that while I can use the style mechanism to change the font of the entry field in the combobox I have not found a way to change the font of the drop down area containing the values. In addition, I have not found a mechanism for changing the values in the drop down list after initialization.

Radiobuttons

TRadiobuttons and Radiobuttons act pretty much the same. One specifies several of the widgets which are linked by specifying the same Tkinter variable for all. It is necessary to specify a different value for each radiobutton. One can also specify an initial value for the group. For TRadiobuttons the values and variables can be specified from the Widget menu as well as in the Attribute editor.

Strangeness with Text and Variables

I was surprised to discover a couple of strange things about the way text is handled with ttk widgets. There are several ttk widgets, among them TButton, TMenubutton, Tlabel, TCheckbuttom, and Tradiobutton, which have both properties of “-text” and “-variable”. Using the Attribute Editor you can specify the value of the Text to be displayed in PAGE when you are laying out the GUI. However, if you then specify the variable attribute, the text attribute is changed to the zero length string. I guess that that is because the variable you named is undefined. But also the width of the field containing the text may be set to zero length. So, for instance, Tlables appear to have zero width unless you have changed the width prior to specifying the variable. A Tbutton merely displays blank text and TRadiobuttons squeezes down to just the button. TCheckbuttons behaves similarly to TRadiobuttons.

When you generate the Python code the widgets will again appear to have no text and maybe have no width unless you have set a non blank value into the Tkinter variable. Tk widgets including Button, Message, Label, Checkbutton, and Radiobutton behave much the same way.

Label

Label widgets also treat text in an unexpected way. The justify attribute applies how multiple lines of text are aligned relative to each other, it does not set how text lines are placed relative to the Label widget boundaries. Use the anchor attribute to specify if the text block is up against the left of the widget (anchor ‘e’) or the right (anchor ‘w’).

Listbox

The Listbox widget has the option “listvariable” which one would expect to behave exactly like one of the Tkinter variable classes. However, the Tcl documentation specifies that the listvariable must contain a list of values to be displayed in the listbox and the possible Tkinter variable classes are BooleanVar, StringVar, IntVar, and DoubleVar. By experimentation I have found that by specifying the var to be StringVar and setting its value to a tuple of strings will cause each member of the tuple to appear as an entry in the listbox. I am rather surprised at this but glad to find something that works.

The following code

def set_Tk_var():
    global rrr
    rrr = StringVar()
    rrr.set(('a','b','c','d','e'))

results in a scrolled list box looking like:

_images/Scrolled_List_Box.png

Spinbox

The Spinbox widget has the option values which contains a list of values presented in the widget as the arrows manipulated. They are set using the Widget menu item “Set Values”. When invoked from the menu a scrolled text box appears and the values are specified by entering them one per line in the text box and then selecting the check mark.

Scale and TScale

Tk does strange things when one tries to modify the narrow dimension of a scale widget (the height of a horizontal scale or the width of a vertical one). So PAGE does not allow one to modify the narrow dimension during the design phase and restricts the Relative Placement in the Python code to prevent changes in the narrow dimension.

Sizegrip

Support for the ttk::sizegrip widget was included in Version 4.0. Merely select the widget from the Widget Toolbar and drop it anywhere within the Toplevel frame but not on top of another widget; it will bounce to the lower right corner. Were you to drop it on say a notebook widget, a weird result would occur like landing in the wrong place but doing the right thing.

I had difficulties with using Sizegrip with PAGE windows. It works fine with the Python Console and the Menu Editor, but I never got a truly satisfactory result with Widget Tree or or the Widget Toolbar. I left it with the Widget Toolbar but not with the Widget tree. To be revisited.

Custom Widget

In writing a photo manager, I needed a variation of a scrolled canvas to display photos. I could not find a general purpose candidate for a scrolled canvas widget, but a found several variations on the web that might work. So I implemented a Custom widget that can be manipulate in PAGE but requires the user to supply the Python implementation. So if I have left something out and you can conjure a tkinter implementation of it, page can handle it.

PAGE shows a Custom widget as a Text widget which can be placed and resized like any other widget. However since the widget has not been defined within PAGE, it is meaningless to talk about modifying attributes. (It is shown as a Text widget with the caption “Custom widget”.) The generated Python refers to it as a class defined in the support module. To allow execution of the GUI before the support module is completed, there is included in the support modle the line:

Custom = Frame

which is followed with user code.

The user then inserts his code for the custom widgets as a class with the <class_name> of his choosing and follows that code with the line:

Custom = <class_name>

This is the magic that links the widget that you placed with the code in the support modules. See Custom Widget example.

Generating, Inspecting, and Running the Python GUI

Once the GUI has been defined, the next step is to generate the Python modules.

Creating and Saving Code Modules

This section discusses the creation and saving of the GUI module and the support module. I want to make saving simple and intuitive while reducing the probability of inadvertently overwriting hard to reproduce code, particularly in the support module. At the same time I did not want to bombard the user with “Are you sure ...” dialogues. These goals are somewhat contradictory. I would appreciate comments on this subject.

When one chooses to generate the GUI module (Control-P):

  • The constructed GUI is transformed into a Tcl file and saved if the GUI has changed in the current session.
  • The GUI is also transformed into a Python module called the GUI module and displayed in the Python Console but is not automatically saved. This is to allow the user to peruse the code before committing it to storage. The user may even change the code since the Python Console code window is a Tk text widget which rudimentary editing capability.
  • From the Python Console, the user can select the Save button and the GUI module will be saved if “new” or changed. Repeatedly selecting the Save button without changing the code will not result in additional actual saves.
  • From the Python Console, the user can select the Run button which provides the same function as the Save button but also attempts to execute the GUI module, if there is an existing support module.

When one chooses to generate the Support module (Control-U):

  • The constructed GUI is transformed into a Tcl file and saved if the GUI has changed in the current session.

  • If there is no existing support module, then one is generated and displayed in a Python Console.

  • If there is an existing support module, action is a bit more elaborate. First, the existing support module is analyzed to see what Tkinter variable and functions are defined in the existing module and compared with those which would be defined in a new support module. Next, the user is given the choice

    • use the existing support module, thereby preserving your hand written code,
    • generate a new support module,
    • update the existing support module updated to include the additional Tkinter variable and skeleton functions.
  • From the Python Console, the user can select the Save button and the GUI module will be saved if “new” or “changed”.

  • From the Python Console, the user can select the Run button which provides the same function as the Save button but also attempts to execute the GUI module.

It is important that the support file is not automatically saved when run is invoked. .. Since most of the handwritten code of the application .. will reside in the support module, it is only saved when explicitly .. requested. I don’t want PAGE to inadvertently trash your handwritten application code. The Python Console has a label which will indicates when the code window has been modified. That flag is turned on any key is released over the window and that can indicate changes which may not actually change the text, i. e., a false positive.

Inspecting the Generated Python Modules

Often the user will want to look at the code that exists for a project. To do that, execute page with the project name or open the project and the select Gen_Python->Load Python Consoles. This will open two Python Consoles; one with the GUI module and the other with the support module. The loading of the consoles is from appropriate modules saved to disk. If a Python Console exists with either the GUI or support module, it will not be overwritten. If one or the other has not been saved, then nothing is done with the corresponding Python Console.

My guess that this is most interesting when the user has modified the GUI and generated a new GUI module and wants to see what any existing support module looks like. Again, in this situation, the new GUI code is not automatically saved.

Executing the Python Modules

To see what the GUI looks like, the user can run or execute the GUI module. That can occur in two contexts, one is to execute the code from one of the Python Consoles and the other is to load the modules into an IDE and carry on development from there. For execution within PAGE there has to be a Python Console; the user can generate either the GUI or the support module or load the project into the consoles from the Gen_Python submenu.

To execute the GUI, select the Run button in a Python Console or using the shortcut Control-R when the cursor is over a Python Console.

Let me discuss the skeletal functions first. Function references may be referenced in several ways. If the function name is given the skeletal function will be created in the support module. An example would be to specify the command attribute in PAGE as “george”. In that case, the skeletal support function “george” would be created in the support module. .. If the specification were given as “self.george”, the .. skeletal module would created as a class function within the GUI .. class. If another module were specified as in “app.george” PAGE would not create a skeleton function at all; your on your own to create and import the “app” module. From this you can see the need to create the support module before trying to execute the GUI module.

Similarly, Tkinter variable classes are defined or the GUI class or in the support module depending on the presence or absence of “self.” at the beginning of the specification. If specified in the support module, code is included to insure that the class is created before the GUI execution references the class.

Because the use of “self.” in specifying functions and Tkinter variables will require use code to be added to the GUI module, I avoid them in my usage of PAGE. Such specifications work against the benefits of the rework facilities.

I frequently execute the GUI module to see how the Python version of GUI looks. To that end, the support module is generated with very minimal skeletal functions in order to check the appearance of the GUI by running from the Python console. The final lines of both the GUI module are:

if __name__ == '__main__':
    vp_start_gui()

and the final lines of the support module are:

if __name__ == '__main__':
   import name
   name.vp_start_gui()

which will call vp_start_gui when the either module is executed. The key is vp_start_gui which is generated automatically. It contains some code like the following, where “unknown” is the default project name within PAGE:

import unknown_support

def vp_start_gui():
    '''Starting point when module is the main routine.'''
    global val, w, root
    root = Tk()
    root.title('New_Toplevel_1')
    root.geometry('600x450+650+150')
    unknown_support.set_Tk_var()
    top = New_Toplevel_1 (Interactive Design Environment)(root)
    unknown_support.init(root, top)
    root.mainloop()

The title above reflects the title attribute of the Toplevel window and, of course, the geometry will reflect the location and sizes you specify in placing the toplevel widget.

When you select the toplevel widget and Generate Support Module from the menu, the Python Console will appear, filled with the generated code for the supporting module named “<name>_support.py”. This file will contain skeleton functions and Tkinter variables needed. This file will contain the principal code for the application. .. This .. is generated automatically once per application.

When you select the toplevel widget and Generate Python GUI from the menu, the Python Console will appear, filled with the generated code. You can push the run button and execution will be attempted. This will automatically save the generated code into a ”.py” file where the root name matches that of the tcl file which is also automatically saved. When running from the Python Window, line output from the GUI is directed to the lower window of the Python Console.

Execution of the Python GUI is initiated by either selecting the “Run” button at the bottom of the Python GUI or by typing Control-R. It can also be run directly by the Python interpreter.

The function “init” is the place to initial things after the GUI is mapped.

Loading Python files into an IDE

While it is possible to edit PAGE generated files in Python Consoles and to execute them from there, they don’t really constitute a particularly good development environment. One should move into a well concieved IDE like IdleX, emacs, or any of a host of similar programs.

PAGE can start up an IDE loaded with the Python modules that have been saved. This is done by selecting Gen_Python->Load Project into IDE. This does not automatically save modules from existing Python Consoles.

The IDE is set in the Basics page of the Preferences. PAGE tries to execute a the IDE with two the two file names, <name>.py and <name>_support.py, as arguments. If your favorite IDE can be so invoked then it should work. When running under Linux or OSX, one can enter either the full path name of the IDE or a command name in the execution path. If you are on Windows, then the full path name takes the a form with double backslashes like “C:\Python27\Lib\idlelibidle” I am not an expert in Python IDE’s, but I have successfully tested this facility with emacs, vim, idle, and idlex on Linux; idlex in OSX, and idle in Windows. For instance, on Linux in the IDE command field I enter the full path of the IDE. When I want to use IdleX under Linux I enter /usr/local/bin/idlex. I also found that I could enter idlex for Linux.

Applications with Multiple Top-Level Windows

Often the user will want to build applications which have more than one top level window. Since PAGE is built for the specification of only one such window how does the user proceed? I think that the best approach is to create separate modules for each top level window and have the support module for the main GUI import them using import statements. The windows can then be created using the create function in the modules. This approach was used for the Vrex example above. Here, I used PAGE to build skeletons for two separate program modules, the main vrex program and the vrex_help module, and coupled the two by having vrex, the main module, import vrex_help and invoke the help window when the “Help” button was selected with the statement:

import vrex_help

def help():
    vrex_help.create_Vrex_Help(root)

Of course, there is very little interaction between the two programs which simplifies things in this case. As in the example below, the argument “root” ties the help window to the main window.

The same technique was used in the progress_bar example which shows more interaction between the windows including the control of the progress bar window from inside the main routine. Here the two modules are main.py and progress_bar.py. In main.py we have

import progress_bar

self.bar = progress_bar.create_Progress_Bar(root)

Here pass to the create routine, the parameter root which ties the two windows together and the create routine returns the Progress_Bar object which allows the main GUI to access all of the functions and attributes of the Progress_Bar object, in turn allowing the main GUI to advance the bar and to close the progress bar.

Special functions are created in the python code to facilitate the creation and destruction of the window.

w = None
def create_New_Toplevel_1(root, *args, **kwargs):
    '''Starting point when module is imported by another program.'''
    global w, w_win, rt
    rt = root
    w = Toplevel (root)
    top = New_Toplevel_1 (w)
    unknown_support.init(w, top, *args, **kwargs)
    return (w, top)

def destroy_New_Toplevel_1():
    global w
    w.destroy()
    w = None

One can pass a variable number of parameters to the init function of the support module using standard Python techniques built around the “*args, **kwargs” usage.

Obviously, if one wants to pass parameters to the init function of the window, then one must modify the parameter list to the create function above and to the call to ini in the above:

def create_Progress_Bar (root, other_args, ...)

     w_win = Progress_Bar (w, other_args, ...)

Also, one may pass a parameter to the “init” function in the support module. This parameter may be any Python data type - ,integer, float, etc., as well as lists, tuple, and dictionary. So it is really a variable length parameter list. For instance, you might have code like the following which I used in the Clone example :

p_dict_1 = {'geom': "+200+650", 'instance': 1, 'color' : 'firebrick'}
p_dict_2 = {'geom': "+1000+650", 'instance': 2, 'color' : 'plum'}


def open_two():
         print "open_two starts"
     firebrick = called.create_Called(root,param=p_dict_1)
     plum = called.create_Called(root,param=p_dict_2)

I suggest comparing the directories, progress_bar and rework_progress_bar to see how I modified the program Progress Bar to conform with the new rework scheme.

A common question is how to share global variables across module. A good reference is How do I share global variables across modules?.

Busy Cursors

This section describes how to change the cursor when in long running sections of an application. Changing to a busy cursor gives some feedback to the application user who otherwise may think that the application is hung, not doing anything.

One includes the following code at the module level of the support module:

# Code added to allow one to change default cursor to a busy cursor.
# Variables added manually to indicate long processes based on code by
# Greg Walters in his python programming examples.  These routines
# also can be seen in the Greyson book pg. 158.
busyCursor = 'watch'
preBusyCursors = None

def busyStart(newcursor=None):
    '''We first check to see if a value was passed to newcursor. If
       not, we default to the busyCursor. Then we walk through the
       busyWidgets tuple and set the cursor to whatever we want.'''
    global preBusyCursors
    if not newcursor:
        newcursor = busyCursor
    newPreBusyCursors = {}
    for component in busyWidgets:
        newPreBusyCursors[component] = component['cursor']
        component.configure(cursor=newcursor)
        component.update_idletasks()
    preBusyCursors = (newPreBusyCursors, preBusyCursors)

def busyEnd():
    '''In this routine, we basically reset the cursor for the widgets
       in our busyWidget tuple back to our default cursor.'''
    global preBusyCursors
    if not preBusyCursors:
        return
    oldPreBusyCursors = preBusyCursors[0]
    preBusyCursors = preBusyCursors[1]
    for component in busyWidgets:
        try:
            component.configure(cursor=oldPreBusyCursors[component])
        except KeyError:
            pass
        component.update_idletasks()
# End of busy cursor code.

and the following lines of code are inserted in “init” in the support module:

global busyWidgets

busyWidgets = (top, )

The first line goes in near the top of the function and the assignment to busyWidgets is inserted after the root object is created. In one of my applications the function “init” looks like:

def init(top, gui):
    ''' Function to create the thread for periodically updating the GUI.'''
    global t
    global w, top_level
    global busyWidgets
    w = gui
    top_level = top
    t = threading.Thread(target=fill_window,)
    t.start()
    busyWidgets = (top, w.Scrolledtext1)

The final line above sets the global variable busyWidgets to be a tuple of those widgets I wish to display the busy cursor. This is from the example WCPE where I want the busy cursor to appear in the top level window as well as the Scrolledtextbox.

When starting a section of code which is likely to be long running, a busy section, insert the following at the start:

busyStart()

When leaving a busy section make the following the last statement():

busyEnd()

Obviously, an application could have numerous busy sections and they might coincide with particular functions or not.

This code can be generalized for usage of any of the Tkinter cursors.