Title: Add an MRU list to a program in Python and tkinter
There are many ways that you can store configuration information for a Python program, none of them perfect. This example stores its MRU (most recently used) file information in a JSON file that uses the following format.
{"MRU_0": "C:/Users/rod/pysrc/tkinter_mru_list/Addresses.txt",
"MRU_1": "C:/Users/rod/pysrc/tkinter_mru_list/Names.txt",
"MRU_2": "C:/Users/rod/pysrc/tkinter_mru_list/Places.txt"}
When the program starts, it executes the following code.
class MruApp:
# Create and manage the tkinter interface.
def __init__(self):
# Load the MRU list.
self.max_mru_length = 4
self.load_mru_list()
...
This code sets the maximum number of files that should be in the MRU list and then calls the following load_mru_list method to load the list from the JSON file.
def load_mru_list(self):
'''Load the MRU list.'''
self.mru_list = []
try:
# Load the MRU list JSON file.
with open('mru_list.json', 'r') as f:
file_dict = json.load(f)
# Pull the file names out of the loaded data.
for i in range(self.max_mru_length):
key = f'MRU_{i}'
if key in file_dict:
self.mru_list.append(file_dict[key])
except:
pass
This code initializes the mru_list variable to an empty list. It then tries to open the file mru_list.json. If the file isn't present, the method just exits so the initial MRU list is empty.
Otherwise the code loops through the numbers 1, 2, 3, for the number of items in the dictionary. For each value i, it searches the dictionary for a key with the format MRU_i and adds its file name to the MRU list.
After it loads the MRU list, the program builds its menus. While doing that, it executes the following code.
# MRU menu
self.file_menu = file_menu # Save so we can enable/disable the Recent Files command.
self.mru_menu = tk.Menu(file_menu, tearoff=False)
self.mru_list_photo_image = ImageTk.PhotoImage(file='mru_list.png')
file_menu.add_cascade(label='Recent Files', menu=self.mru_menu,
compound='left', image=self.mru_list_photo_image)
# Make the initial MRU list.
self.document_photo_image = ImageTk.PhotoImage(file='document.png')
self.build_mru_list()
This code saves the file menu in variable self.file_menu so it can use the menu later. It then adds the Recent Files cascading menu to the File menu.
The code then loads the image in document.png so it can use that image later to make recent file menu items. It then calls the following build_mru_list method to build the MRU menu items.
def build_mru_list(self):
'''Add files to the MRU menu.'''
# Delete existing items.
self.mru_menu.delete(0, tk.END)
# Add the files.
# Note how the lambda function renames the variable.
# This forces it to bind the value when this code is
# executed rather than trying to use the value of
# filename when the lambda executes.
for filename in self.mru_list:
label = os.path.basename(filename)
self.mru_menu.add_command(label=label, compound='left',
command=lambda bound_name=filename: self.mru_file_selected(bound_name),
image=self.document_photo_image)
# Enable the Recent Files command if there are no recent files.
if len(self.mru_list) > 0:
state = 'normal'
else:
state = 'disabled'
self.file_menu.entryconfigure('Recent Files', state=state)
# Save the MRU list.
self.save_mru_list()
This code first deletes any menu items that are in the MRU menu. It then loops through the file names in self.mru_list. For each file name, it uses os.path.basename to get the file's name without the full path. It then uses the shortened file name to make the menu item.
The item's command is a lambda function that calls the mru_file_selected method, passing it the file's full name.
When I initially tried to just pass the full name into the method, the lambda function tried to evaluate the file name when it was executed so every menu item used the same file name. (The last one in the list.) To prevent that, the code sets bound_name equal to the file name and then passes that into the method call. That binds the file name value when the lambda method is created so it uses the corretc file name.
Next the code enables the MRU list's menu item in the File menu if the number of files in the MRU list is greater than 0.
Finally, the code calls the following save_mru_list method to save the current MRU list. (This isn't required when the program initially loads the list, but it's handy when the list is updated later.)
def save_mru_list(self):
'''Save the MRU list.'''
# Build a dictionary holding the MRU file names.
file_dict = {}
for i in range(len(self.mru_list)):
key = f'MRU_{i}'
file_dict[key] = self.mru_list[i]
# Save the JSON file.
with open('mru_list.json', 'w') as f:
json.dump(file_dict, f)
This code creates a dictionary a then adds each of the MRU list's file names to it. When it's finished, it opens the file mru_list.json and dumps the dictionary into it.
The program includes two methods that let you add and remove items from the MRU list. The following code shows the mru_add_file method.
This code simply removes the item from the list (if it is present) and then calls build_mru_list to rebuild the MRU menu items.
Those are the basic MRU list maintenance methods. The rest of this post talks about the code that executes when you open or save a file.
The following code handles file saving.
def mnu_save_as(self):
'''Save a file.'''
filetypes = [
('Text files', '*.txt'),
('Python Files', '*.py *.ipynb'),
('All Files', '*,*')]
filename = asksaveasfilename(filetypes=filetypes)
if len(filename) > 0:
# Save the file.
msg.showinfo('Save File', filename)
# If the save succeeds, update the MRU list.
save_succeeded = True
if save_succeeded:
self.mru_add_file(filename)
This method displays a file save dialog. If you select a file name, the program saves the file with the new name. (This example just displays the name.) If the sace succeeds, the program calls mru_add_file to add the new file name to the MRU list.
I know that's a lot of pieces, but each one is fairly simple if you look at it in isolation. Download the example to see all of the details.
|