How to Add New Frames¶
When adding new features to the GUI, you’ll likely either need to add new Widgets to existing Frames or you’ll need to create and place whole new Frames. Adding a Widget to an existing Frame is fairly simple (just find the custom Frame’s file, build and place the widget in the Frame’s __init__()
method, and add any new logic or callbacks necessary as private methods on the custom Frame class). In this article, we’ll instead go over how to add an entirely new custom Frame to the GUI, which will be necessary when adding completely new features to the GUI.
We’ll use our custom DecoderFrame
defined in python/src/gui/frames/content_frames/decoder_frame.py
as an example, so inspect that file if you struggle through any of these steps.
Implementing your custom tk.Frame
class¶
To create your new Frame, we will create a new class that implements tk.Frame
. To do this, create a new file for your Frame class in the python/src/gui/frames/content_frames/
directory. Add these imports to the top of your file:
import tkinter as tk
from tkinter import ttk
from gui.app import GUI
Next, define your class with an __init__()
as shown below. This is copied from our custom DecoderFrame
class. Make sure your __init__()
has the same signature as the one shown.
class DecoderFrame(tk.Frame):
# build the interface
def __init__(self, master: tk.Frame, gui: GUI, **kwargs):
super().__init__(master=master, **kwargs)
self.gui = gui
Next, define any private instance variables your widgets will need. For example, a dropdown will require a tk.StringVar
and a list of options that starts with a blank string, a checkbox will require a tk.IntVar
, etc. Here are the variables that the DecoderFrame
uses for its Decoder dropdown selector and its align checkbox:
self.decoder_selected = tk.StringVar()
decoder_types = ['']
decoder_types.extend([item.value for item in DecoderType])
self.align = tk.IntVar()
You can see that the values of the dropdown are pulled directly from the DecoderType
Enum values. Note that the currently selected enum value can be retrieved with self.decoder_selected.get()
. Similarly, the value of the align checkbox can be retrieved with self.align.get()
.
Next, we add any validation functions if necessary. These are not used in DecoderFrame
, but you can use these in your Frame if you must ensure that a text Entry box only allows ints or floats. See our OutputSettingsFrame
for an example of how to use these.
# register the validation for entry floats
val_float = (self.register(self._validate_float), '%P')
val_int = (self.register(self._validate_int), '%P')
Next, configure the layout grid for your Frame. This is not necessary unless you want specific rows or columns of your layout to have a specific padding. In the case of DecoderFrame
, we add the line self.grid_rowconfigure(1, pad=5)
to vertically pad the second row by 5 pixels. Note that all our Frames use Tk’s grid layout. More info on this layout type can be found online. We use the grid layout because it is the most customizable layout; however, this does make the layout code a bit more verbose.
Finally, we can begin adding Widgets to your Frame. This typically starts with a Title Label, as shown before for the DecoderFrame
. Note that we must (1) create the Widget and then (2) add it to the grid layout. The first argument of all the Widget constructors is the parent Frame, which is self
in this case. Also note that the we are using the GUI’s predefined label_style_bold
as the style and the GUI’s predefined sizes.DECODER_TITLE_WIDTH
as the width of the label. Please see Tk’s or Tkinter’s online documentation on how to use the grid layout and the grid
function.
self.title_label = tk.Label(self, text='Decoders', **gui.label_style_bold, width=gui.sizes.DECODER_TITLE_WIDTH)
self.title_label.grid(row=0, column=0, sticky='w', padx=(0, 5))
Continue adding all your widgets using whichever code organization you like. Depending on the Frame, I like to organize the code by either rows or columns. See the rest of DecoderFrame.__init__()
and any of our other predefined Frames for examples of how to create other Widgets such as Entry
, OptionMenu
, Button
, and Checkbutton
. You can define callbacks either as lambda functions or as private methods on your custom Frame
class.
After adding all your widgets inside __init__()
, the last thing to do is add any right click menus or tooltips. To add right click menus, use self.gui.build_right_click_menu()
and to add a tooltip use self.gui.tooltip.bind()
. See DecoderFrame
for examples on their usage.
Now we are done with your Frame’s layout and the __init__()
method! There are still a few last things to implement in your custom Frame
class though.
Firstly, implement any custom private methods you need for logic or callbacks.
And lastly, implement the update_ui(self, kwargs)
method. This method gets called whenever the GUI has new data from the Backend to update all its Frames with. The kwargs
argument in this case is a dict whose keys are either custom strings or names of the UpdateType
Enum. Use these keys to access any data from the Backend you need to update your custom Frame. Note that this data comes from the Backend.get()
method, so if you aren’t getting all the data you need, you’ll need to edit that Backend method to add that data to the returned dict
. And for your assistance, DecoderFrame
has a few good examples of ways to update your Frame in its update_ui()
method.
Now you are done implementing your custom Frame! Let’s continue
Adding your custom tk.Frame
to the GUI¶
Now that you’ve implemented your custom Frame, let’s go back to python/src/gui/app.py
to add your new Frame to the main GUI window.
Firstly, import your custom Frame at the BOTTOM of python/src/gui/app.py
. This is to make sure Python linters still work.
Secondly, inside GUI._build_ui()
initialize your Frame object and place in the GUI using the grid layout. Note that most of the content Frames in the GUI are placed inside the main tabbed window of the GUI, which corresponds to the self.tabbed_content
variable. To add a new tab, create a Frame whose parent is self.tabbed_content
and add that Frame to the tabbed window. The order the Frames are added to the tabbed Notebook is the order of the displayed tabs. Here’s some example code of just that:
self.raw_processing_tab_frame = tk.Frame(self.tabbed_content, **self.frame_style)
self.tabbed_content.add(self.raw_processing_tab_frame, text="Raw Processing")
You can use that dummy Frame (self.raw_processing_tab_frame
in this case), as the parent Frame for your custom Frame when you initialize it at the bottom of GUI._build_ui()
. Just follow the existing code layout inside GUI._build_ui()
and you should be fine.
Finally, inside GUI.update_ui_callback(self, kwargs)
, call your new Frame’s update_ui()
method as we already do for all the existing Frames.
Now if you’ve done everything correctly, your new Frame should show up in the GUI!
Sending Backend Commands¶
The last step you might need to complete to get your new Frame working is to add any necessary methods to the GUI class to send commands to Backend. To send a command to the Backend, use the GUI._send_command()
. We typically do this from within GUI methods - not from within methods in your custom Frame class. See the [[GUI]] overview article to get more info on commonly used GUI methods.