I don't know about you, but every time I consider developing a GUI,I get this uneasy feeling, knowing that I am about to engagein a tedious and protracted fight with whatever GUI library I choose.
Of course, no matter which library is used, there is a learning curve,but learning the library's details is not the source of my frustration. The frustration is to get the library to place the widgets on the screen to look as it does on the design I put on paper. Unless the GUI I am designing is super-simple, this just never happens. I can spend days, if not weeks, just to get widgets to align on the screen as I designed. They always initially look as if I coded the GUI after having one glass of wine too many. The reason is simple. I normally rely on a GUI library's algorithmic layout utilities called layout or geometry managers to place the widgets on the screen.
Layout ManagersLet's take a look at the GUI layout managers. Using Tkinter as an example,you are offered 3 different layout managers to choose from. The first is the place manager. With the place manager, according to Effbot,
"It allows you to explicitly set the position and size of a window,either in absolute terms, or relative to another window."
Almost every text on Tkinter GUI design tells you to avoid using the place manager. And for a good reason, you manually need to know each widget's exact position, and that usually involves some calculations on your part to get things positioned correctly. Suppose you need to add additional widgets or change the position of a widget. In that case, you may find yourself in a maze of twisty little mazes tweaking multiple widget positions.
The second layout manager that Tkinter offers is the pack layout manager. This is an improvement over the place manager. Here is how Effbot defines the pack manager:
"The Pack geometry manager packs widgets in rows or columns. You can use options like fill, expand, and side to control this geometry manager.
The manager handles all widgets that are packed inside the same master widget. The packing algorithm is simple, but a bit tricky to describe in words; imagine a sheet of some elastic material, with a very small rectangular hole in the middle. For each widget, in the order they are packed, the geometry manager makes the hole large enough to hold the widget, and then place it against a given inner edge (default is the top edge). It then repeats the process for all widgets. Finally, when all widgets have been packed into the hole, the manager calculates the bounding box for all widgets, makes the master widget large enough to hold all widgets, and moves them all to the master."
The pack manager is great for very simple GUI designs but falls short whenyour GUI contains a complex set of widgets.
The third and clearly the most complicated of all the layout manager is the grid manager. Again, let's use Effbot to define what a grid manager is.
"The Grid geometry manager puts the widgets in a 2-dimensional table. The master widget is split into a number of rows and columns,and each “cell” in the resulting table can hold a widget."
It sounds simple enough, but getting things to look quite right requires tweaking the code with things such as padding and "sticky" positions and iterating many, many times. This is because it is difficult to accurately predict what thegrid manager will do for a complicated set of widgets. The grid layout manager will deliver a screen similar to my original intent, but somewhat off. It needs a lot of tedious, iterative tweaking to overcome the manager's shortcomings.
So what to do? Just resign myself to a week or two of GUI drudgery, or is there a tool that can make GUI implementation quick, simple, and dare I say, fun? Since I decided upon using Tkinter, I decided to do a web search for such a tool. By the way, I chose Tkinter over some other GUI library because it is packaged with Python. It is portable across Windows, macOS, and Linux operating systems without having to install specific OS versions of the library.
Usually, the Tkinter module comes preinstalled with a Python distribution, butsome operating systems do not include it and require a manual installation. I am looking at you, Ubuntu!
To install Tkinter for Ubuntu, you can use the following command:
sudo apt-get install python3-tk
To verify if you have Tkinter for Python3 installed, open a command window, and start Python3 from the command line. Then type:
import tkinter
If no ModuleNotFoundError is reported, then you have Tkinter installed for Python3.
By the way, if you need to learn or brush up on Tkinter concepts, I can recommend the Tkdocs.org free online tutorial.
Now back to the tool. My search yielded several candidates, and the one that fit my needs was Page. Page is a "What you see Is what you get" (WYSIWYG) Tkinter GUI editor and code generator.With Page, you visually place widgets on the screen, align them as you like, press a button, and voila. You have Python code that implements the GUI's visual aspects. But wait, there is more! Page will also generate the Python stubs to handle widget events. With Page no more tweaking for days on end. What isPage's secret? It uses the place geometry manager. Yes, the dreaded place manager. Since you are visually laying out your GUI, Page captures the widgets' physical screen locations. If you need to add or move widgets around, Page has got your back. After all, it is WYSIWYG.
But is it efficient? Take a look at the RoboHAT GUI screens I created. It contains a notebook widget with seven populated tabs. There are quite a few widgets that need to be laid out. Based upon my previous experience of hand-coding Tkinter GUI's, I estimate the coding and layout tweaking would take me about a week to accomplish. Using Page I was able to layout the entire GUI and generate allthe code in about 4 hours. And that is pretty efficient if you ask me. And by the way, it was fun.
But is Page easy to use? Let's install Page and build a very simple GUI to find out.
Creating A Simple GUI With PageThe purpose of the GUI we are going to build is to toggle the RoboHAT LED on and off.
Don't own a RoboHAT MM1, but you have an Arduino? This demo program will also toggle an Arduino's LED.
Installing PageYou can download Page from its homepage. Installation instructions are provided here.The installation instructions suggest downloading and installing ActiveStateTcl. Please use that version of TCL, since other versions may cause some issues if used with Page.
Page comes with extensive documentation. I strongly recommend that you read the documentation. Yes, it is rather lengthy, but one or two hours of your time is well worth it. It will pay in dividends in understanding the thoughtfulness that went into creating Page, and you will ultimately save time in understanding howto use Page's menuing system.
Using PageWhen you start Page, you will see 5 windows. The center window is the Toplevel window used as the visual base of the application. This window contains the title bar for our application.
Let's begin by changing the title from "New Toplevel" to something more meaningful for the application. Let's enter "RoboHAT LED Toggler." We can modify or set a widget's attributes in the Attribute Editor window on the screen's right-hand side.
In the title field, type in the new title for the Toplevel window.
Note that the Toplevel window title immediately changes.
Next, we are going to add widgets to the Toplevel window. First, I am going to add a Frame widget.
To do so, select the Frame widget selection from the Widget Toolbar window.
Now, click somewhere within the Toplevel window, and a Frame appears.
You will be able to select the Frame and drag it anywhere within the Toplevel, and you can resize it by using the drag handles.
After positioning and resizing the Frame, let's change the look of the Frame's border. In the Attribute Editor, find the field called relief, and select raised from the drop-down menu.
The Frame immediately appears with a raised border.
Notice that the Frame now appears in the Widget Tree window. The Widget Tree window allows you to quickly navigate to any widget in your project.
Now let's add a second Frame within the first, and this time, we will change its relief to be sunken.
Finally, select a Button from the Widgets Toolbar and place it within the inner Frame.
With the Button selected, let's modify some of its attributes to meet our needs.
First, let's change the default Alias for this widget to ButtonLedToggle. The alias allows one to easily refer to the widget if you need to refer to it in your code.To create a stub to receive button pressed messages, let's set its name to toggle_led in the command field.Let's change the text that the Button displays to LED On. To do this, find the text field and modify it.Finally, we will add a text variable and will call it led_button_text. We can dynamically modify the Button's text by manipulating this variable.
Now, If you look at the Widget Tree, you will see all the Widgets that we have added.
The visual aspects for this demo GUI are now complete so let's save our project. From the Main window menu, select File/Save and give your project a name.
Page saves projects as.tcl files. You may view the file, but you never have to manually edit this file, so if it may look like gibberish. Don't worry, you will not need to learn TCL.
Generating The CodeThe next step is to have Page generate the code for our application. In the Main window, select Gen_Python and select Generate Python Gui. A GUI console willopen, displaying the Python code to instantiate all the widgets. Press Save at the bottom of this window.
Now from the Gen_Python menu, select Generate Support Module, and the support console opens with some additional Python code.
Why two Python files? Page uses the MVC or Model-View-Controller design pattern when generating code. It separates the visual aspects or View for the project in the GUI file and the interactive parts of the program in the support file, which acts as the Controller for the MVC pattern. Page manages the GUI file for us, so we do not edit it. The support file is where we add our code to interact with the GUI. The model, which typically is the data model for the application, is not managed at all by Page, so we will not discuss that here.By separating things into two files, we are free to modify our own code in the support file without fear of breaking the GUI. If you need to make changes to the GUI, such as adding a new widget, you generate the GUI file again and then generate the support file to reflect those changes. Page gives you the option to retain any changes you may have made in the support file and will merge them into the new code. By separating things out and having automatic merging available for the support file, you are free to make changes without having to recode things again and again.Since Page manages the GUI file, it won't be discussed here, but let's look at the support file.
Working With The Support FileBelow is the modified support file. It adds the functionality to toggle the button text when the button is pressed.
#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# Support module generated by PAGE version 5.4
# in conjunction with Tcl version 8.6
# Sep 04, 2020 07:12:34 PM EDT platform: Linux
import sys
try:
import Tkinter as tk
except ImportError:
import tkinter as tk
try:
import ttk
py3 = False
except ImportError:
import tkinter.ttk as ttk
py3 = True
def set_Tk_var():
global led_button_text
led_button_text = tk.StringVar()
led_button_text.set('LED On')
def init(top, gui, *args, **kwargs):
global w, top_level, root
w = gui
top_level = top
root = top
# additional
# add a reference variable for the MyCode class
global my_code
# instantiate the MyCode class
# pass in the StringVar to the button text
my_code = MyCode(led_button_text)
def toggle_led():
# additional
# associate the handler in the class for this event.
my_code.handle_button_click()
def destroy_window():
# Function which closes the window.
global top_level
top_level.destroy()
top_level = None
# additional
class MyCode:
"""
This class houses the code to process the GUI events
"""
def __init__(self, button_text):
"""
:param button_text: A tk.StringVar used to adjust the button
text.
"""
self.led_button_text = button_text
# a variable to hold the current toggle state
self.toggle_state = False
def handle_button_click(self):
if self.toggle_state:
self.led_button_text.set('LED On')
else:
self.led_button_text.set('LED Off')
# Toggle the toggle state
self.toggle_state = self.toggle_state ^ 1
if __name__ == '__main__':
import toggle_led
toggle_led.vp_start_gui()
To identify all of the code modifications I made, I begin those sections with the comment "# additional."A note about my coding style. I prefer to encapsulate my changes within a class whenever possible. I find that this makes my changes more readable and easier to modify. Let's look at the changes one by one.In the init function generated by Page, I append the code to instantiate my class.
def init(top, gui, *args, **kwargs):
global w, top_level, root
w = gui
top_level = top
root = top
# additional
# add a reference variable for the MyCode class
global my_code
# instantiate the MyCode class
# pass in the StringVar to the button text
my_code = MyCode(led_button_text)
The toggle_led function generated by Page is called whenever the button is pressed. I redirect the call to a method in my class to handle the button clicks.
def toggle_led():
# additional
# associate the handler in the class for this event.
my_code.handle_button_click()
Finally, I implement my class.
# additional
class MyCode:
"""
This class houses the code to process the GUI events
"""
def __init__(self, button_text):
"""
:param button_text: A tk.StringVar used to adjust the button
text.
"""
self.led_button_text = button_text
# a variable to hold the current toggle state
self.toggle_state = False
def handle_button_click(self):
if self.toggle_state:
self.led_button_text.set('LED On')
else:
self.led_button_text.set('LED Off')
# Toggle the toggle state
self.toggle_state = self.toggle_state ^ 1
In the __init__ method, the button text StringVar is saved. A state variable, called toggle_state, is created to remember the button's current toggle state.The handle_button_click method toggles the button text to reflect the state of the LED with the next button click.With all the changes to the support file in place, we are ready to test things out by running the code. You can run the code by using the Page GUI and Support consoles, or directly using python3:
For macOS and Linux:
python3 toggle_led.py
And for Windows:
python toggle_led.py
With each click of the button, the button's text should toggle between LED On and LED Off.
After we test the changes and are satisfied that everything is working as planned, it is time to associate the GUI with the module that will actually turn the LED on and off. First, you will need to install the pymata-rh module. Follow these instructions to install and also make sure that you install FirmataExpress on your device.
Now, let's look at the code that actually controls the hardware. As mentioned earlier, If you do not own a RoboHAT, this demo program will also work with any standard Arduino. Here is the fully updated support file:
#! /usr/bin/env python
# -*- coding: utf-8 -*-
#
# Support module generated by PAGE version 5.4
# in conjunction with Tcl version 8.6
# Sep 04, 2020 07:12:34 PM EDT platform: Linux
import sys
try:
import Tkinter as tk
except ImportError:
import tkinter as tk
try:
import ttk
py3 = False
except ImportError:
import tkinter.ttk as ttk
py3 = True
# additional
# bring in the module to control the board
from pymata_rh import pymata_rh
# import the tkinter messagebox widget
if py3:
from tkinter import messagebox
else:
# noinspection PyUnresolvedReferences
import tkMessageBox as messagebox
def set_Tk_var():
global led_button_text
led_button_text = tk.StringVar()
led_button_text.set('LED On')
def init(top, gui, *args, **kwargs):
global w, top_level, root
w = gui
top_level = top
root = top
# additional
# add a reference variable for the MyCode class
global my_code
# instantiate the MyCode class
# pass in the StringVar to the button text
my_code = MyCode(led_button_text)
def toggle_led():
# additional
# associate the handler in the class for this event.
my_code.handle_button_click()
def destroy_window():
# Function which closes the window.
global top_level
top_level.destroy()
top_level = None
# additional
class MyCode:
"""
This class houses the code to process the GUI events
"""
def __init__(self, button_text):
"""
:param button_text: A tk.StringVar used to adjust the button
text.
"""
self.led_button_text = button_text
# a variable to hold the current toggle state
self.toggle_state = False
# instantiate PyMataRh to provide device communication
# If the board is not plugged in it will issue an exception.
try:
self.board = pymata_rh.PymataRh()
except AttributeError:
print('Board Not Found')
messagebox.showerror("Board Not Found", "Do you have FirmataExpress installed and is the board plugged in?")
sys.exit(0)
# enable pin 13 for digital output
self.board.set_pin_mode_digital_output(13)
def handle_button_click(self):
if self.toggle_state:
self.led_button_text.set('LED On')
self.board.digital_write(13, 0)
else:
self.led_button_text.set('LED Off')
self.board.digital_write(13, 1)
# Toggle the toggle state
self.toggle_state = self.toggle_state ^ 1
if __name__ == '__main__':
import toggle_led
toggle_led.vp_start_gui()
When testing if the board is not plugged in or FirmataExpress is not installed, an exception will be thrown by pymata_rh. To handle this exception, a Tkinter messagebox will provide some guidance.Page does not directly support the Tkinter messagebox widget, but it is easily imported.
# additional
# bring in the module to control the board
from pymata_rh import pymata_rh
# import the tkinter messagebox widget
if py3:
from tkinter import messagebox
else:
import tkMessageBox as messagebox
All other changes are localized to the MyCode class.
# additional
class MyCode:
"""
This class houses the code to process the GUI events
"""
def __init__(self, button_text):
"""
:param button_text: A tk.StringVar used to adjust the button
text.
"""
self.led_button_text = button_text
# a variable to hold the current toggle state
self.toggle_state = False
# instantiate PyMataRh to provide device communication
# If the board is not plugged in it will issue an exception.
try:
self.board = pymata_rh.PymataRh()
except AttributeError:
print('Board Not Found')
messagebox.showerror("Board Not Found", "Do you have FirmataExpress installed and is the board plugged in?")
sys.exit(0)
# enable pin 13 for digital output
self.board.set_pin_mode_digital_output(13)
def handle_button_click(self):
if self.toggle_state:
self.led_button_text.set('LED On')
self.board.digital_write(13, 0)
else:
self.led_button_text.set('LED Off')
self.board.digital_write(13, 1)
# Toggle the toggle state
self.toggle_state = self.toggle_state ^ 1
We add an instantiation of PymataRh to the __init__ method. We alsoadd an exception handler if that should fail. We open a messagebox if the exception is called and then exit the program when the messagebox is dismissed.Pin 13, which is connected to the LED, is configured as a digital output in the __init__ method.In the handle_button method, we add the code to turn the LED on and off with the call to self.board.digital_write. The first parameter is the pin number, and the second is the value to be written.Now when we test, not only does the text in the button change with each button press, but the LED also toggles.
Concluding Remarks.That's it! Creating a GUI with Page is that simple and quick. Page not only manages the visual aspects of your GUI but also creates all the code to quickly get you started.
So if you are thinking about creating a GUI for your project, I hope you will consider using Page and have some fun with GUI creation.
Comments