Adding a menu bar to our text editor

Since the menu bar will sit directly in our Tk widget we can put all of the menu logic in our texteditor.py file. Open this file up and add the following into the __init__ method underneath the creation of our Highlighter:

self.menu = tk.Menu(self, bg="lightgrey", fg="black")

This line creates us a Menu widget, which we will store a reference to under a menu attribute. We configure the colors to specific values for now, but this will change later.

After the creation of our main Menu widget we could define several more here in the __init__ method. However, this will quickly get very cluttered, not to mention it will require a lot of new code each time we want to add a new submenu into our menu bar.

Instead of this approach, we will write a method that automatically figures out what menu commands we want from just a list of strings representing the submenu labels:

sub_menu_items = ["file", "edit", "tools", "help"]
self.generate_sub_menus(sub_menu_items)
self.configure(menu=self.menu)

We decide in advance what menu options we want as cascades in our top menu bar. We then store a list of these in the variable sub_menu_items. These should look familiar to those of you who use graphical applications a lot.

This list is then passed to a method called generate_sub_menus, which will search our TextEditor class for methods that start with the given submenu name and add them in as commands to the relevant cascade.

Let's have a look at how this function will work:

 def generate_sub_menus(self, sub_menu_items):
window_methods = [method_name for method_name in dir(self)
if callable(getattr(self, method_name))]
tkinter_methods = [method_name for method_name in dir(tk.Tk)
if callable(getattr(tk.Tk, method_name))]

We begin with two variables called window_methods and tkinter_methods.  These are defined as list comprehensions, which may seem a little confusing. Allow me to break them down for clarity:

  • method_name for method_name in dir(self): The dir function, as we have seen when looking at imports, lists all available attributes. We use the self argument to get each attribute of our TextEditor instance instead of the whole application. This part will therefore return each attribute of our TextEditor instance and store it under method_name.
  • if callable(getattr(self, method_name)): We want to filter only methods, not regular variable attributes, so we add an if statement to our list comprehension. Elements will only be returned if they are callable, which we check for using the callable function.

In order to get an attribute using a string variable, we need to pass the instance and the string to the getattr method. This attribute is then passed to the callable function to check that it is a method and not simply a variable.

We perform this list comprehension again, this time for the Tk widget itself. This gives us a list of everything available to a subclass of the Tk widget, since we want to ignore this when searching our TextEditor instance for methods to put in the menu:

my_methods = [method for method in set(window_methods) - set(tkinter_methods)]
my_methods = sorted(my_methods)

To filter out only the methods that we have defined in this file we need to cast both lists to sets and then we can take the Tkinter methods away from our methods to get only the newly-defined ones. Again a list comprehension is used to cast this set back to a list.

To ensure that our commands will appear in the same order each time we call the sorted method on this newly returned list to ensure they are ordered alphabetically.

Now that we have all of the methods defined in this file we can begin building Menus and commands:

for item in sub_menu_items:
sub_menu = tk.Menu(self.menu, tearoff=0, bg="lightgrey", fg="black")
matching_methods = []

It is now time to iterate over our sub_menu_items list and create a new cascade for each one.

Each cascade has the main menu bar as its parent and the previously mentioned color scheme for its foreground and background (which again will change later).

The tearoff argument is used to indicate whether or not the user can grab the menu with their mouse and move it around the screen. We do not want this to happen, so we set it to 0 and prevent that behavior.

A list of all methods that begin with the word that defines our cascade now need to be found. We create a list called matching_methods to hold them. We now need to use a loop to search through our filtered my_methods and check to see if they belong in this particular cascade:

for method in my_methods:
if method.startswith(item):
matching_methods.append(method)

The startswith method of a string will allow us to check whether each of our methods begins with the relevant string of the cascade, for example, file.

If this method returns True then we append it to our list of matching_methods.

Once we have built up this list, we can convert each method to a command in our cascade. We can do this by adding another loop that runs after the for method in my_methods loop:

for match in matching_methods:
actual_method = getattr(self, match)
method_shortcut = actual_method.__doc__.strip()
friendly_name = ' '.join(match.split('_')[1:])
sub_menu.add_command(label=friendly_name.title(),
command=actual_method, accelerator=method_shortcut)

The getattr function is once again used to get the actual attribute of our TextEditor instance to pass as the command argument when calling add_command.

Since most applications will display a keyboard shortcut for commands within their top menu, we are going to do the same in our text editor. Since there is no automatic way of achieving this we will set up a convention. The convention will be to put the keyboard shortcut as the docstring of each relevant method.

To parse this, we can use the __doc__ attribute of the found method to grab its docstring, calling strip to remove any excess whitespace.

The label for this command will be generated from the method name. This line does a few things in one go, so I will separate them to better explain each:

  • The method name is split on an underscore to receive a list of each word
  • We use a slice of [1:] to remove the first word from the list, since this word will be the cascade's label
  • We re-join each piece on a space character

This process will convert a method name of file_open_file to open file, which is much nicer to read.

We pass these three variables to the add_command method on our cascade to complete the process. The title method is used on our friendly_name in order to capitalize each word for neatness.

The accelerator argument is responsible for displaying the keyboard shortcut next to a menu item.

The accelerator only displays the keyboard shortcuts, it does not create them. They still need to be bound using the bind or bind_all methods that we have been using.

Now that our cascade has all of its commands added we just need to place it into the menu bar:

self.menu.add_cascade(label=item.title(), menu=sub_menu)

Again the title method is used for neatness.

That is all that we need to do in order to get our top menu into our application. Give this version of the code a run and check out our new menu:

You will notice, however, that the menus are currently all blank. Don't worry, as we go through the chapter we will be adding methods which begin with one of our cascade words, and these will automatically appear in the relevant menu. By the end of this chapter each option will have some commands in it.

With one type of menu out of the way, let's move on to the context menu.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset