GUIbits Manual version 1.0


A simple contract-based Graphical User Interface package


Contents


1. Introduction

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:

  1. to avoid dependencies between different parts of the codebase, as far as this is possible.
  2. to present as simple and straightforward an interface to the client as is possible.

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.

1.1 How this document is organized

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.


2. Window

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.

Specification of windowing

Exposed types:

Specification of callback procedures:

Exposed procedures:

Example

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:

Demo Window

2.1. Window positioning

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.

Specification of window_bounding

Exposed types:

Exposed procedures:



3. Font styles

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.

Specification of font_styling

Exposed types:

Exposed procedures:



4. Color

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.

Specification of coloring

Exposed constants

Exposed types:

Exposed procedures:



5. Writing

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.

Specification of writing

Exposed procedures:

Example

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:

Demo Window 2

By manipulation of the scrollbars and zoom buttons the appearance of the pane can be modified to the following, for example:

Demo Window 2A

The demonstration program that produces this pane, demo_window2.py, is included with this manual.



6. Painting

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.

Specification of painting

Exposed procedures:



7. Cursor

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.

Specification of cursoring

Exposed procedures:



8. Menus

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.

Specification of menuing

Exposed types:

Exposed callback procedures:

Exposed procedures:

Examples

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:

Menu Example

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.

Menu Example 2

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:

Menu Example 3


9. Dialogs

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:


Specification of dialoging

Exposed procedures:

9.1 show_message_dialog

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.

Example of use

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:

Message Dialog Example

9.2 show_confirm_dialog

Use this dialog to ask the user a question with a Yes or No answer.

Example of use

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:

Confirm Dialog Example

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.


9.3 show_input_dialog

Use this dialog when you want to prompt the user for some information.

Example of use

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:

Input Dialog Example

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.


Specification of file_dialoging

This contractor exposes procedures to display the special-purpose file dialogs.

Exposed types:

Exposed procedures:

9.4 show_open_file_dialog

Use this dialog to ask the user to select a file to be opened.

Example 1

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:

Open File Dialog Example

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.


9.5 show_save_file_dialog

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.

Example

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:

Save File Dialog Example

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.


9.6 show_new_folder_dialog

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.

Example

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:

New Folder Dialog Example

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.



10. Keyboard (Character keys)

Specification of keyboarding

Specification of callback function:

Exposed procedures:



11. Keyboard (Control keys)

Specification of controlling

Exposed constants:

Specification of callback procedure:

Exposed procedures:



12. Mouse

Specification of mousing

This is an adapter which constructs composite mouse gestures from the atomic mouse gestures performed by the user. The composite mouse gestures are:

  1. mouse_click
  2. double_click
  3. mouse_popup
  4. mouse_pressed mouse_dragged* mouse_released (mouse drag)

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.

Exposed types:

Exposed procedures:

Example of use

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)
  ...



13. Printing

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.

Specification of printing

Exposed types:

Exposed procedures:



14. Other facilities

A miscellaneous collection of contractors that have proved useful in graphical user interfaces.


14.1 Specification of latest_listing

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.

Exposed type:

class ListOfLatest

Exposed procedures


14.2 Specification of stacking

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.

Exposed type:

class StackTop

Exposed procedures:


14.3 Specification of unicoding3_0

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)

Exposed types:

Exposed procedures:


15. Precondition and postcondition

Wikipedia gives the following definitions:

precondition
a condition or predicate that must always be true just prior to the execution of some section of code or before an operation in a formal specification.
postcondition
a condition or predicate that must always be true just after the execution of some section of code or after an operation in a formal specification.

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.



16. Licence agreement

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/.



17. Acknowledgements

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