GUIbits is a Graphical User Interface package, consisting of independent contractors, which provides a simple and flexible Application Programmer Interface for applications involving a keyboard, mouse and screen.
Version 1.0 of GUIbits is implemented in Python using PyQt and retains the platform-independence of this approach.
The overall design principles have been:
To this end, all types are made completely opaque, that is, their internal structure is unknown to the client code, and they can only be manipulated using the supplied procedures. Subtypes and supertypes are not used. Each Python module of the package implements a contractor, a collection of types and procedures which covers an area of activity within the overall GUI.
Each procedure in a contractor is completely specified by its parameters and the precondition and postcondition relating to them. Furthermore, each procedure is diagnostically correct: not only is it guaranteed that, if the precondition is satisfied, the procedure will terminate in a state satisfying the postcondition, but in addition, it is guaranteed that if the precondition is not satisified, the procedure will terminate with a (hopefully useful) diagnostic message. In this way, the client can quickly amend their code, without having to delve into GUIbits's implementation details.
To read more on contractors, minimizing dependencies and diagnostic correctness see my monograph "The Minimum Dependency Principle".
GUIbits is incomplete. It is hoped that clients will help to extend and improve the package, using the principles outlined above. In part, GUIbits is an experiment to see just how effective this approach is for producing flexible and robust software.
In the following sections, each contractor of GUIbits is described in detail. For each procedure, the precondition and postcondition are specified, and where appropriate, examples of use with screenshots are given.
An application communicates with the user through a window on the screen. A window is a rectangular area of the screen, delimited by a coloured frame and having a title bar at the top. The contents of a window appear inside this frame and consist of an optional menu bar at the top, a set of three zoom buttons on the left-hand side, and two scrollbars on the right-hand side and the bottom respectively. The window contains a viewport through which the user can view the pane of the window. The pane is transparent and the background of the window shows through it. The user can manipulate the pane in three dimensions by the use of the scrollbars and zoom buttons.
When first displayed on the screen, the window is maximized. The window can be manipulated by the user in the standard way (for example, it can be iconized or normalized, and when normalized its shape can be altered).
The client can perform initialization on the window by writing a window_opening callback procedure. This procedure will be executed as soon as the window is shown on the screen by the show procedure.
If the user closes the window, the show procedure terminates the app. If you wish to change this default action, it is possible to specify a window_closing procedure. This procedure will be executed before the show procedure terminates.
All dimensions are in points, where a point is 1/72 inches, expressed as a Python float.
The program fragment:
win = windowing.new_window(20.0,"Demo Window",600.0,450.0,1.0) windowing.show(win,None,None) |
produces the following window on the screen:
This group of procedures allows a client to find the current normalized bounds of a window, save them externally and restore them. The bounds of a window are the x- and y-offset of the top left-hand corner of its contents from the top left-hand corner of the screen, and the width and height of its contents. All measurements are in points. The current bounds are those of the latest normalized window, i.e. intermediate in size between iconized and full-screen.
A memento variable of type WindowBounds is used to hold the window's bounds.
font_styling is a contractor that defines the font styles the client can use, and provides a container for storing a set of font styles.
coloring is a contractor that defines the colors used in GUIbits. Colors are represented as a triple (red,green,blue) where each color coordinate is a float between 0.0 and 1.0. This format is consistent with the Python standard. The standard library module colorsys.py defines bidirectional conversions of color values between colors expressed in the RGB (Red Green Blue) color space and other coordinate systems.
This class permits the client to display text on the pane of a window by using the appropriate write method. It is also possible to clear all text from the pane.
Here is an example of a program that uses writing.write_string:
import coloring import font_styling import windowing import writing # author R.N.Bosworth # version 26 Jul 2021 16:12 """ Demo of writing.write_string. Copyright (C) 2014,2015,2019,2020,2021 R.N.Bosworth This program is free software: you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License (gpl.txt) for more details. """ # private members # --------------- _PLAIN_STYLE = font_styling.new_font_styles() _BLACK = coloring.new_color(0.0,0.0,0.0) _FONT_SIZE = 20.0 # test program # ------------ def window_opening(win): writing.write_string(win,"hello","Times New Roman",_PLAIN_STYLE,24.0,20.0,30.0,_BLACK) def _test(): print("Demo of writing.write_string") # create a window win = windowing.new_window(_FONT_SIZE,"Demo window 2",600.0,450.0,1.0) windowing.show(win,window_opening,None) if __name__ == "__main__": import sys _test() print("All tests OK") |
When run, the following window is shown on the screen:
By manipulation of the scrollbars and zoom buttons the appearance of the pane can be modified to the following, for example:
The demonstration program that produces this pane, demo_window2.py, is included with this manual.
The painting contractor allows the client to paint a rectangular section of the pane in any desired color, and also to erase all rectangles from the pane.
This contractor allows the client to display a flashing cursor anywhere in the pane, ensuring the cursor is visible to the user, and also to remove it from the pane.
These procedures allow you to display menus of any length on the menu bar of a window or as a pop-up in the window, and to nest menus to any depth. All sizes and positions are in points. Positions are measured relative the top-left-hand corner of the window contents, i.e. the point just below the left-hand end of the title bar.
The following program fragment displays a window with two menu buttons in its menu bar:
... def file_menu_item_hit(win,x,y,l): print("File menu item hit at "+str(x)+","+str(y)); print("Label="+l) def font_menu_item_hit(win,x,y,l): print("Font menu item hit at "+str(x)+","+str(y)); print("Label="+l) w = windowing.new_window(font_size,"Menu Example",800.0,600.0,1.0) menuing.add_menu_bar_item(w,font_size,"File",file_menu_item_hit) menuing.add_menu_bar_item(w,font_size,"Font",font_menu_item_hit) windowing.show(w,None,None) ... |
This produces the following window:
Note that when run, the program prints the position of the mouse-hit relative to the top left-hand corner of the menu bar. For example:
File menu item hit at 29.25,25.5 Label=File Font menu item hit at 70.5,23.25 Label=Font |
We can easily add menus to the menu buttons. We need to modify the file_menu_item_hit callback function to set up and display a popup menu, for example:
def file_menu_item_hit(win,x,y,l): """ pre: menu item associated with this callback procedure has been hit by user win = windowing.Window where mouse-hit occurred x,y = x and y offsets of mouse hit from top left-hand corner of window's contents, in points as float post: file menu has been displayed and user has responded test: once thru """ print("File menu item hit at "+str(x)+","+str(y)); m = menuing.new_menu(win) menuing.add_menu_item(m,font_size,"Open",file_open_menu_item_listener) menuing.add_separator(m) menuing.add_menu_item(m,font_size,"Save",None) menuing.add_menu_item(m,font_size,"Save As",file_save_as_menu_item_listener) menuing.add_separator(m) menuing.add_menu_item(m,font_size,"Close",file_close_menu_item_listener) menuing.display(m,win,x,y) |
As the labels on the Font menu need to be tagged to show which value is current, we need a couple of small procedures:
def tag_of(b): """ pre: b = true, if bullet point is to be returned else blank string is returned post: appropriate string has been returned test: b = True b = False """ tag = "" if b: tag += '\u2022' # unicode bullet point tag += ' ' else: tag += " " return tag def tagged_label_of(l,sv): """ pre: l = label to be tagged, as string sv = value of label which is currently selected. as string post: tagged version of label has been returned, as string test: l is selected l is not selected """ return tag_of(l == sv) + l |
Then we can modify the font_menu_item_hit callback function as follows:
def font_menu_item_hit(win,x,y,l): """ pre: menu item associated with this callback procedure has been hit by user win = windowing.Window where mouse-hit occurred x,y = x and y offsets of mouse hit from top left-hand corner of window's contents, in points as float post: font menu has been displayed and user has responded """ print("Font menu item hit at "+str(x)+","+str(y)) print(" font_name="+font_name) m = menuing.new_menu(win) menuing.add_menu_item(m,font_size,tagged_label_of("Courier New",font_name), \ font_courier_menu_item_listener) menuing.add_menu_item(m,font_size,tagged_label_of("Times New Roman",font_name), \ font_times_menu_item_listener) menuing.display(m,win,x,y) |
The following picture shows the result when the Font menu button is hit, and "Courier New" is the current font.
We can also easily make a drop-down menu appear wherever the user performs a right mouse-click. We need to create a MouseListener which is derived from mousing.MouseListener:
class MyMouseListener(mousing.MouseListener): ... def mouse_popup(self,x,y,win,window_x,window_y): """ pre: the user has gestured that a popup menu should be displayed. On some platforms, this is done by clicking the right mouse button. self = pane for this mouse event (x,y) = position in points at which the mouse pointer was clicked, relative to the top left-hand corner of the window's pane win = window in which the gesture was made (window_x,window_y) = position in points required for popup menu, relative to the top left-hand corner of the window's contents post: the action (if any) required by the user has been carried out """ print("mouse_popup hit") print("window_x=" + str(window_x)) print("window_y=" + str(window_y)) _m = menuing.new_menu(win) menuing.add_menu_item(_m,12.0,"Action 1",None) menuing.add_menu_item(_m,12.0,"Action 2",None) menuing.display(_m,win,window_x,window_y) |
and then attach this callback procedure to our window, in the window_opening callback:
def window_opening(win): """ pre: win = Window on which the opening event occurred win has just appeared on the screen post: client's initiation has been carried out """ mousing.attach(MyMouseListener(),win) |
We can then display the window with this window_opening callback:
win = windowing.new_window(font_size,"Menu Example 3",800.0,600.0,1.0) windowing.show(win,window_opening,None) |
The following picture shows what happens when the user right-clicks at (155.4,72.0) on the window:
Each GUIbits dialog is exposed by the dialoging interface as a single procedure. When the client calls this procedure, a dialog box is displayed in the centre of the screen, blocking all other currently displayed windows. Each dialog thus demands a response from the user, which is transmitted to the client via the return type of the dialog procedure.
There are three general dialog procedures:
and three special-purpose file dialogs:
This is the simplest possible dialog. Use it when you want to output a message to the user, and give them time to read it.
The following program fragment:
filename = "my_program.py" dialoging.show_message_dialog(16.0,"File Save","File \"" + \ filename + "\" was saved successfully") ) |
will produce the following dialog on the screen:
Use this dialog to ask the user a question with a Yes or No answer.
The following program fragment:
userReply = \ dialoging.show_confirm_dialog(16.0,"Global delete", \ "Delete entire contents of hard drive?") |
will produce the following dialog on the screen:
If the user presses the Yes button (inadvisable in this case), the variable userReply will be set to the value True.
If the user presses the No button or the Close button, or hits the Esc key, the variable userReply will be set to the value False.
Use this dialog when you want to prompt the user for some information.
The following program fragment:
user_reply = \ dialoging.show_input_dialog(16.0,"Address query","Please enter your postcode:") |
will produce the following dialog on the screen:
If the user replies as shown and presses the OK button, the variable user_reply will be set to the value "BN99 9ZZ".
If the user replies as shown and presses the Cancel button or the Close button or hits the Esc key, the variable user_reply will be set to the value None.
This contractor exposes procedures to display the special-purpose file dialogs.
Use this dialog to ask the user to select a file to be opened.
The following program fragment:
user_reply = \ file_dialoging.show_open_file_dialog(16.0,"Open","C:\\Users\\",file_dialoging.SortMode.ALPHABETIC) |
could produce the following dialog on the screen:
The user can navigate around the file system for a suitable file, and click on it to enter it into the "File name" box. Alternatively, the user can enter a name manually.
If the user then presses the Open button, the variable userReply will be set to the selected file. (If the "File name" box is empty, the Open button is ineffective).
If the user presses the Cancel button or the Close button or the Esc key, the variable userReply will be set to the value None.
Use this dialog to ask the user to select a filename under which a file is to be saved. The user is warned if they are about to overwrite a file of the same name which already exists.
The following program fragment:
exs = ["txt","py"] sm = file_dialoging.SortMode.ALPHABETIC user_reply = \ file_dialoging.show_save_file_dialog(16.0,"Save","C:\\Users\\User\\Documents\\hello.mle",exs,sm) |
could produce the following dialog on the screen:
The user can accept the default filename, or navigate around the file system for a suitable file, and click on it to enter it into the "File name" box. The user can also create a new directory, and enter the filename manually.
If the user then presses the Save button, the variable userReply will be set to the selected file. (If the "File name" box is empty, the Save button is ineffective).
If the user presses the Cancel button or the Close button or the Esc key, the variable userReply will be set to the value None.
Use this dialog to give the user an opportunity to create a new folder (directory). The user is not allowed to create a folder with an invalid name, or a folder that already exists.
The following program fragment:
user_reply = \ file_dialoging.show_new_folder_dialog(16.0,"Create New Folder","C:\\Users\\User") |
could produce the following dialog on the screen:
If the user now enters a valid name and presses the OK button, the folder will be created in the current directory, and the variable userReply will be set to the name of the created folder.
If the user presses the Cancel button, the Close button or the Esc key, the variable userReply will be set to the value None.
This is an adapter which constructs composite mouse gestures from the atomic mouse gestures performed by the user. The composite mouse gestures are:
The position of the mouse is returned in points relative to the top left hand corner of the window's pane. For convenience, the mouse_popup gesture also returns the position in points relative to the window's contents, i.e. just below the left-hand end of the title bar of the window.
To make your app responsive to mouse events, first declare a class which derives from mousing.MouseListener, and fill the methods with the code required for your app's response:
class MyMouseListener(mousing.MouseListener): def double_clicked(self,x,y): """ your app's response to mouse double-clicked here """ ... |
Then attach an instance of the MouseListener to the main window of the app. This is conveniently done in the window_opening procedure of the main window:
def window_opening(win): ... mml = MyMouseListener() mousing.attach(mml,win) ... |
The printing contractor allows the client to print text on the currently selected printer.
A print job is started by the client invoking printing.set_page_dimensions. Thereafter, zero or more printing.print_string calls are made, interspersed as necessary with calls of printing.throw_page. The print job is terminated (and the printer released for other apps) by a call of printing.end_printing.
A miscellaneous collection of contractors that have proved useful in graphical user interfaces.
The latest_listing contractor exposes a ListOfLatest type and procedures to manipulate the list. A ListOfLatest is a limited-size list containing no duplicate elements. If a element is pushed onto the list which duplicates one already there, the duplicated element is removed. When the list reaches its maximum size, pushing a new element onto the list causes one element to be lost from the bottom.
A ListOfLatest resembles the arrivals board at an airport (back-to-front!).
A ListOfLatest is useful for "remembering" the most recent values of combo boxes.
class ListOfLatest
The stacking contractor exposes a StackTop type and procedures to manipulate the stack. A StackTop is a bounded structure representing the top n elements of a generic stack. When the stack reaches its maximum size, pushing a new element onto the stack causes one element to be lost from the bottom. The stack may contain duplicates.
stacking exposes the normal stack manipulation procedures: size, clear, pop and push.
class StackTop
The unicoding3_0 contractor supports mutable UTF-32 strings, which complement the non-mutable Python str type.
The main advantage of UTF-32 is that the Unicode code points are directly indexable. Finding the nth code point in a sequence of code points is a constant time operation. (In contrast, a variable-length code such as UTF-16 or UTF-8 requires sequential access to find the nth code point in a sequence.) This property allows algorithms which use non-sequential access (e.g. efficient search algorithms) to be employed. It is guaranteed that the result of an indexed access is a valid Unicode code point.
The main disadvantage of UTF-32 is that it is space-inefficient, using four bytes per code point. But this can be mitigated to some extent by translating to a variable-length format (e.g. UTF-8) when non-sequential access is not required, for instance when storing Unicode text as a file.
A code point is represented in the unicoding3_0 contractor as a 32-bit integer, using the Python int type. This means that the standard Python character-literal conventions can be used, and also that code points can be compared using the standard Python integer comparison operators (==,!=,<=,>=).
Code points can be strung together to make a unicode3_0.String, which is an indexed sequence of code points. The indexing starts at zero, as for Python str.
Facilities are provided to convert between a unicoding3_0.String and a str. The Python API provides facilities for converting between a str and the Unicode UTF-8 format for external media.
Unlike a str, a unicoding3_0.String is dynamic and can be modified at any time, either by appending code points or by inserting or deleting code points.
The append_a_copy(s1,s2) procedure can be used to append a copy of s2 to s1, making a new version of s1.
If you want to create a completely new String which is the concatenation of s1 and s2, while leaving s1 and s2 intact, use the sequence
s3 = unicoding3_0.new_string() unicoding3_0.append_a_copy(s3,s1) unicoding3_0.append_a_copy(s3,s2)
If you want to make a copy (clone) s2 of a String s1, use the sequence
s2 = unicoding3_0.new_string() unicoding3_0.append_a_copy(s2,s1)
Wikipedia gives the following definitions:
In the above procedure definitions, if the client ensures that the precondition is true, the postcondition is guaranteed. If the client does not ensure that the precondition holds, the procedure will raise a diagnostic exception to indicate this failure in the client's code.
Preconditions and postconditions are written in a rigorous but informal notation. Postconditions should be written in terms of the changes that have occurred since the procedure was invoked, but to reduce the number of "has beens" they are often written in the present tense, e.g.
rather than
Variables that have not changed their value since the procedure was invoked are not mentioned in the postcondition (except to emphasize that their value has not changed).
See the Wikipedia Precondition article and the Wikipedia Postcondition article for more details.
GUIbits version 1.0 is licenced under version 3 of the GNU General Public License (GPL). The documentation, including this document, is licenced under the GNU Free Documentation License (FDL). This download package contains a copy of the General Public License (GPL) and the FDL. For more details of these licences, see www.gnu.org/licences/.
Don Ho, for the superb Notepad++ editor. James Gosling, for inventing Java, in which the original version of GUIbits was written. The anonymous designers of Swing, on which the original version of GUIbits was implemented. John English, whose JEWL system demonstrated that Swing could be simplified. Richard Mitchell, for introducing me to Design-by-Contract and the work of Bertrand Meyer. Aidan Delaney, for helping me to understand the true meaning of free and open source software. Peter Naur, for giving me the courage to think outside the box. Nassim Taleb, for identifying the concept of antifragility. Guido van Rossum, for inventing Python. The Python Community, for the amazing supporting software and documentation provided for the language, including of course PyQt. And the "usual suspects": Edsger Dijkstra, Tony Hoare and Niklaus Wirth.
Version: RNB 10th January 2023 15:08