In the previous tutorial, we covered Tkinter and TTK widgets. With the help of these widgets, we’ll create user interfaces to control embedded electronics. We have already created a blank GUI window for our Raspberry Pi (RPi) embedded electronics control app.
In this tutorial, we’ll learn about TTK menus and create one for our RPi app. We’ll also discuss layout management and events in Tkinter/TTK, as well as multi-threading.
This should provide enough foundation for our RPi recipes that will run from the GUI interfaces and will use threads for the implementation of embedded tasks.
Tkinter/TTK menus
Menubars and pop-up menus are typical in graphic interfaces and are used to navigate through different sections of an application. Menus give organized access to different functionalities or user options. They are typically drop-down lists of items that might include other nested menus (sub-menus or cascading menus) as items. The sub-menus are usually populated as nested drop-down lists.
When clicking an item of a menu, a callable can be executed. Generally, the callable is used to populate the interface for some functionality or to offer additional user options within the current interface.
For example, in our RPi app, we’ll develop different programs, some of which will handle:
- Display devices
- Sensors
- Application-specific modules
- Communication interfaces
- And possibly more
To access the different programs in our app, the best option is to create menus for each of the respective categories.
In the Tk GUI toolkit, menus are implemented as widgets. The menus are also classes that are created by instantiating menu objects. The parent of a menu object can be a window (main window or a child window), a frame, or another menu. When instantiating a menu object with a window as a parent, a menubar is automatically created.
A window cannot have more than one menubar. When displayed, the menubar sits below the title bar of the window.
When instantiating a menu object whose parent is another menu, it becomes a sub-menu (cascaded menu). So, there should be a menubar in a window and the menubar should contain other items.
The items of any menu can be command items, other menus (sub-menus), separators, Radiobutton items, or Checkbutton items. The menubar can contain command items, other menus (submenus), Radiobutton items, or Checkbutton items. The items of sub-menus can be other items (command, Radiobutton, Checkbutton), nested menus, or separators. Each menu item has some attributes like text/image for the item, keyboard accelerator, and command to invoke (associated callable).
A menubar can be created by instantiating a menu object whose parent is a window (main window or a child window). The menu objects are instantiated using the Menu method of Tkinter/TTK class.
This a valid example of creating a menubar for the main window:
root = Tk()
root.title(“RPi Embedded Electronics Control APP”)
root.minsize(480, 480)
root.resizable(0, 0)
main_menu = Menu(root)
The command items can be added to a menubar (which itself is a menu) or any menu using the add_command() method of the Menu class. A submenu (menu item) can be added to a menubar or a menu using the add_cascade() method of the Menu class.
However, a submenu must be instantiated as a menu object whose parent is the parent menu object. (And the parent menu must have been created already.)
The add_cascade() method takes the child menu as one of its arguments and the label for that child menu as the other argument. It should be noted that, by default, the tkinter/ttk starts each submenu by a separator, which appears as dashed lines. To get rid of this starting separator, the “tearoff” attribute should be set to False in the definition of the submenu.
The following is a valid example of creating sub-menus as items of the menubar, using the above code snippet:
displays_menu = Menu(main_menu, tearoff=0)
main_menu.add_cascade(label=”Displays”, menu=displays_menu)
sensors_menu = Menu(main_menu, tearoff=0)
main_menu.add_cascade(label=”Sensors”, menu=sensors_menu)
modules_menu = Menu(main_menu, tearoff=0)
main_menu.add_cascade(label=”Modules”, menu=modules_menu)
motors_menu = Menu(main_menu, tearoff=0)
main_menu.add_cascade(label=”Motors”, menu=motors_menu)
comm_menu = Menu(main_menu, tearoff=0)
main_menu.add_cascade(label=”Communication”, menu=comm_menu)
help_menu = Menu(main_menu, tearoff=0)
main_menu.add_cascade(label=”Help”, menu=help_menu)
The add_command() method is used to add command items to menus (menubar, menus, or submenus). It takes the label of the item as one argument and uses the command to invoke the other argument. The command argument can be assigned a callable (function or class method), which must be executed on clicking the menu item.
This is a valid example of adding command items to our submenus, using the above code:
displays_menu.add_command(label=”LEDs”, command=donothing)
displays_menu.add_command(label=”SSDs”, command=donothing)
displays_menu.add_command(label=”Character LCD”, command=donothing)
displays_menu.add_command(label=”Graphic LCD”, command=donothing)
displays_menu.add_separator()
displays_menu.add_command(label=”Touch Screen”, command=donothing)
sensors_menu.add_command(label=”LDR”, command=donothing)
sensors_menu.add_command(label=”IR”, command=donothing)
sensors_menu.add_command(label=”HC-SR04 Ultrasonic”, command=donothing)
sensors_menu.add_command(label=”LM35″, command=donothing)
sensors_menu.add_command(label=”DHT-11″, command=donothing)
sensors_menu.add_command(label=”Motion Sensor”, command=donothing)
sensors_menu.add_command(label=”Moisture Sensor”, command=donothing)
sensors_menu.add_command(label=”Accelerometer”, command=donothing)
sensors_menu.add_command(label=”Accelero + Gyro”, command=donothing)
modules_menu.add_command(label=”GPS”, command=donothing)
modules_menu.add_command(label=”GSM”, command=donothing)
modules_menu.add_command(label=”Fingerprint”, command=donothing)
modules_menu.add_command(label=”RFID”, command=donothing)
modules_menu.add_command(label=”RTC”, command=donothing)
motors_menu.add_command(label=”DC”, command=donothing)
motors_menu.add_command(label=”Stepper”, command=donothing)
motors_menu.add_command(label=”Servo”, command=donothing)
comm_menu.add_command(label=”RF”, command=donothing)
comm_menu.add_command(label=”USB”, command=donothing)
comm_menu.add_command(label=”Ethernet”, command=donothing)
comm_menu.add_command(label=”Bluetooth”, command=donothing)
comm_menu.add_command(label=”Wi-Fi”, command=donothing)
help_menu.add_command(label=”Index”, command=donothing)
help_menu.add_command(label=”About…”, command=donothing)
help_menu.add_command(label=”Exit”, command=root.quit)
Along with the command items and menu-type items, separators, Radiobutton items, and Checkbutton items can be added to a menu. A separator can be added to a menu using the add_separator() method. This method does not require any arguments. The separator appears as a horizontal line between other menu items.
Here’s a valid example of adding a separator to a menu:
displays_menu.add_separator()
A Checkbutton item can be added to a menu using the add_checkbutton() method from the Menu class. The method takes the label for the Checkbutton, the control variable, as well as the onvalue and offvalue as arguments.
When clicking a Checkbutton item, the value of associated control variable is either set to onvalue or offvalue. This variable can then be used somewhere in the code. It’s also possible to assign a command argument for the Checkbutton that executes a callable whenever the Checkbutton is clicked.
This is a valid example of adding a Checkbutton item to a menu:
any_menu.add_checkbutton(label=’Check’, variable=check, onvalue=1, offvalue=0)
The Radiobutton items can be added to a menu using the add_radiobutton() method of the Menu class. The method takes the label for the Radiobutton, variable, and value as arguments.
When clicking a Radiobutton, the value associated with that Radiobutton is assigned to the associated variable. This variable can then be used somewhere in the code. It’s also possible to assign a command argument for Radiobutton that executes a callable whenever the Radiobutton is toggled.
Here are valid examples of adding Radiobutton items to a menu:
any_menu.add_radiobutton(label=’Option1′, variable=variable, value=”Option1″)
any_menu.add_radiobutton(label=’Option1′, variable=variable, value=”Option1″)
any_menu.add_radiobutton(label=’Option1′, variable=variable, value=”Option1″)
any_menu.add_radiobutton(label=’Option1′, variable=variable, value=”Option1″)
A menubar will not display in the parent window until it’s configured as the menu option for the window object. The menu option can be set using the config() method of the window object.
Here’s a valid example of setting a menu as the menubar for the main window, using the above code:
root.config(menu = main_menu)
root.mainloop()
It’s also possible to assign accelerator keys (shortcut keys) to menu items by using the accelerator argument in the item definition. This argument should be assigned a (keyboard) key combination.
To underline a character in the label text of the menu items, use the underline argument. Images can also be used in place of label texts in the menu items by using the image and compound arguments. These arguments must be assigned an image object. Additionally, it’s possible to set the state of a menu as “normal” or “disabled” using a state argument.
In the above example, the command argument for the command items of the menus have been assigned a function donothing().
This function does nothing but has this body:
def do-nothing():
pass
Through the above code example, we’ve created a menu for our app. As we develop the app, we’ll replace the donothing() function with callable(s) to properly implement the embedded system programs for different electronic components, interfaces, and operations.
The callable(s) can be functions or class methods. We prefer to use functions as they’re always faster than the class methods.
Here are the screenshots of the menubar we just created for our app.
Tkinter/TTK layout management
The widgets provide interaction with the user, which should occur in an efficient and organized manner. To do so, the widgets must be placed out in the parent window in an orderly fashion. This order is referred to as the layout of the window.
Tkinter/TTK provides three types of layouts: absolute positioning, pack, and grid.
For the absolute positioning of widgets, the place() method is used. This method needs to be called on for each widget. Absolute positioning means that each widget is positioned with respect to the window by specifying its position in pixels. The height and width of the widgets can also be set as arguments in the place() method.
For positioning, the “x” and “y” arguments should be passed as values in the pixels. For specifying the height and width, the relheight and relwidth arguments must be passed as values in the pixels, respectively. The relheight and relwidth arguments take pixel values as infractions.
When the window is resized, fonts are changed, or the window is viewed on different platforms (Mac OS, Microsoft Windows, or X11), the dimensions and positions of the widgets never change. The absolute positioning is the most basic layout but should be avoided for a better user experience.
For the pack layout, the pack() method is used. In this layout, widgets are placed in the window as vertical or horizontal boxes. The pack() method takes three arguments: side, fill, and expand.
The side argument determines which side of the parent widget that the widget should be placed. It can be assigned values, such as TOP, BOTTOM, LEFT, or RIGHT.
The fill argument determines how the widget fills in any extra space available in the parent widget. It can have values, such as:
- NONE (note: with the default value, the widget will not expand to fill extra space)
- X – to fill horizontally
- Y – fill only vertically
- BOTH – to fill both horizontally and vertically
If the expand argument is set to True, the widget occupies all of the extra space available in the parent widget. The pack layout is better than absolute positioning, but is still only suitable for simpler interfaces.
For the grid layout, the grid() method is used and the widgets are organized in equally spaced rows and columns. A widget can be set to span over multiple rows and/or columns using the rowspan and columnspan arguments, respectively.
The position of the widget is specified using the row and column arguments, which places the widget in the specified row-column position. This is the most organized way of positioning widgets and it can be used to design an interface of any complexity.
It should be noted that until a packer method (place, pack, or grid) is called on a widget, it does not appear on the window. We’ll be using the grid layout for the GUI of all our RPi recipes.
Tkinter/TTK events
Widgets can bind to several keyboard or mouse events. A callable can be executed when that event happens with the widget. For example, a callable might be executed when the cursor hovers over a widget or a user clicks or double-clicks the widget.
This table lists the events supported by Tkinter/TTK.
For binding a callable to an event for a widget, the bind() method is used. It has this syntax:
widget_name.bind(event, callable)
This is a valid example of event binding:
label_LED_ON.bind(<Enter>, changeColor) #changeColor is a function/method
Aside from the keyboard and mouse events, some widgets support virtual events. These are widget-specific events that trigger a change of the state or content of the widget. For example, Combobox supports a <<ComboboxSelected>> virtual event, which is triggered when a user selects a value in Combobox.
Here’s a valid example of binding with a virtual event:
combo_LED_op.bind(‘<<ComboboxSelected>>’, active_blink) # active_blink is callable
Multi-threading
Infinite loops are quite common in an embedded scenario. For example, we may need to continuously fetch data from a sensor or keep populating messages on a display or run an actuator until some external interrupt or conditions are matched.
However, if we try to run infinite loops from the GUI, it will freeze. This is because the GUI is running as a process.
So, the solution is to run such loops without getting our GUI freeze and this is called multi-threading — meaning it’s possible to run code for electronics in separate threads. These threads will run parallel to the GUI and will not freeze or interrupt the control application. Essentially, we’ll be running all our RPi recipes, the microcontroller-way, and the control application independently.
Multi-threading is a great way to carry out several embedded tasks simultaneously, which is not possible on any microcontroller. The microcontrollers execute code sequentially and do have multi-threading features.
For multi-threading, we’ll first need to import the threading and sys modules as follows:
import threading
import sys
We must also create threads as global objects so that they can be used anywhere in the GUI app.
This is a global variable that’s defined, which will later be assigned to a threading object:
thread_LED_driver = None
Unfortunately, Python’s threading module does not have any method to kill a thread. But we can solve this with a little coding trick. We can override the threading module and write a user-defined kill event.
For this, we’ll write a class with overriding methods as follows:
class MyThread(threading.Thread):
def __init__(self, *args, **kwargs):
super(MyThread, self).__init__(*args, **kwargs)
self._stop = threading.Event(
def stop(self):
self._stop.set()
def stopped(self):
self._stop.isSet()
def run(self):
while 1:
if self.stopped():
print(“Thread Closed”)
x = 1
return
else:
self._target(*self._args, **self._kwargs)
Later, we can use this class to define threads for various embedded functions. For example, the following function uses a global variable (defined to be assigned to a thread object) to start a thread. The thread will execute a callable, which will in turn, execute the desired embedded task.
def thread_generate_LED_signal():
global thread_LED_driver
thread_LED_driver = MyThread(target = func_gen_LED_signal)
thread_LED_driver.start()
Since we have already created a class that overrides methods of the threading module, this class has an event defined to stop/kill the thread. This user-defined method can be used to stop the thread virtually.
This function kills the thread created in the above function:
def shutdown_LED_signal():
global thread_LED_driver
thread_LED_driver.stop()
As mentioned, functions are faster than the class methods, so we’ll wrap all of the embedded application codes in functions most of the time. Python allows for the defining of functions inside functions.
So, we’ll use this feature to wrap up all of our embedded code in single functions for each embedded task. For instance, if the LCD is interfaced with RPi, we will confine all of the embedded code to control and populate messages to the LCD in a single function. Then we’ll run that function in a separate thread. This way, we can exert control on each component independently and in a concurrent manner.
Tkinter/TTK Limitation as GUI tool
Tk GUI toolkit is great at creating graphic interfaces of any complexity. It also simplifies layout management and has enough widgets to create most of the controls and user interaction. However, one major limitation of this toolkit is that it only creates static interfaces. The widgets cannot be created and populated dynamically at runtime.
Ideally, the Tk toolkit would have supported dynamic interfaces but it does not. If it did, we could have dynamically added controls for new hardware components that interfaced with the RPi system and we could’ve managed to operate those components using variable arrays.
You’ll experience this limitation in the next tutorial where we present our first Raspberry Pi recipe — an LED driver.
Stay connected
From the next tutorial onwards, we’ll be implementing various recipes on hardware components. We’ll compare the differences when controlling the same component on a microcontroller. We’ll also discuss limitations, advantages, and drawbacks imposed due to the SBC-Vs-Microcontroller, Python, or Linux factor.
Filed Under: Featured, Raspberry pi
Questions related to this article?
👉Ask and discuss on EDAboard.com and Electro-Tech-Online.com forums.
Tell Us What You Think!!
You must be logged in to post a comment.