0

I would like to use a tkinter GUI to iterate through a dictionary (for example) and allow the user to take actions on its values.

For example, my boss might want to iterate through departments and select which employees to fire. The below code works (mostly) for the first department, but I don't understand how to advance to the next department (self.advance below) .

This question is related but just updates values of existing widgets. The number of employees in each department varies, so I can't just update the names, and I also have to allow vertical scrolling.

The iteration occurs within a frame (innerFrame) and the rest of the UI is mostly static. Should I be destroying and recreating that innerFrame, or just all of the widgets inside it? Either way, how can I advance to the next iteration?

# Example data
emp = {'Sales':['Alice','Bryan','Cathy','Dave'],
       'Product':['Elizabeth','Frank','Gordon','Heather',
                  'Irene','John','Kristof','Lauren'],
       'Marketing':['Marvin'],
       'Accounting':['Nancy','Oscar','Peter','Quentin',
                     'Rebecca','Sally','Trevor','Umberto',
                     'Victoria','Wally','Xavier','Yolanda',
                     'Zeus']}

import tkinter as tk
from tkinter import messagebox

class bossWidget(tk.Frame):
    def __init__(self, root):
        """
        Scrollbar code credit to Bryan Oakley:
        https://stackoverflow.com/a/3092341/2573061
        """
        super().__init__()     
        self.canvas = tk.Canvas(root, borderwidth=0)
        self.frame  = tk.Frame(self.canvas)
        self.scroll = tk.Scrollbar(root, orient="vertical", command=self.canvas.yview)
        self.canvas.configure(yscrollcommand=self.scroll.set)
        self.scroll.pack(side="right", fill="y")
        self.canvas.pack(side="left", fill="both", expand=True)
        self.canvas.create_window((4,4), window=self.frame, anchor="nw", 
                                  tags="self.frame")
        self.frame.bind("<Configure>", self.onFrameConfigure)
        self.initUI()        

    def initUI(self):
        """
        Creates the static UI content and the innerFrame that will hold the
        dynamic UI content (i.e., the Checkbuttons for the copies)
        """
        self.master.title("Boss Interface")
        self.instructLabel = tk.Label( self.frame, justify='left',
                                      text = "Select the employees you wish to FIRE")
        self.skipButton   = tk.Button( self.frame, text="Skip Department", 
                                      command = self.advance)
        self.deleteButton = tk.Button( self.frame, text="Fire employees", fg = 'red',
                                       command = self.executeSelection )
        self.quitButton   = tk.Button( self.frame, text="Exit", command=self.frame.quit)
        self.innerFrame   = tk.Frame( self.frame)
        self.instructLabel.pack(anchor = 'nw', padx=5,pady=5)
        self.innerFrame.pack(anchor='nw', padx=5, pady=20, expand=True)
        self.deleteButton.pack(side='left', padx=5,pady=5)
        self.skipButton.pack(side='left', padx=5,pady=5)
        self.quitButton.pack(side='left', padx=5,pady=5)

    def populateUI(self, title, labelList):
        """
        Creates and packs a list of Checkbuttons (cbList) into the innerFrame
        By default, the first Checkbutton will be unchecked, all others checked.
        You should help the boss out by passing the best employee at the head of the list
        """
        self.instructLabel.config(text = title + ' department:\nSelect the employees you wish to FIRE')
        self.cbList = [None] * len(labelList)
        self.cbValues = [tk.BooleanVar() for i in range(len(labelList))]
        for i in range(len(labelList)):
            self.cbList[i] = tk.Checkbutton( self.innerFrame, 
                                        text=labelList[i], 
                                        variable = self.cbValues[i])
            if i: self.cbList[i].select() # Check subsequent buttons by default
            self.cbList[i].pack(anchor = 'w', padx=5,pady=5) 

    def advance(self):
        # -------------> this is what I don't understand how to do <-------------
        self.innerFrame.destroy()  # this destroys everything! 
        # how to advance to next iteration?

    def querySelection(self):
        return [x.get() for x in self.cbValues]

    def executeSelection(self):
        fired = self.querySelection()

        if ( not all(x for x in fired) or 
             messagebox.askokcancel(message='Fire ALL the employees in the department?') 
           ):       
            for i in range(len(self.cbList)):
                empName = self.cbList[i].cget('text') 
                if fired[i]:
                    print('Sorry, '+ empName + ', but we have to let you go.', flush=True)
                else:    
                    print('See you Monday, '+ empName, flush=True)    
            self.advance()   

    def onFrameConfigure(self, event):
        """Reset the scroll region to encompass the inner frame"""
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

def main(): 
    root = tk.Tk()   
    root.geometry("400x250+250+100") # width x height + xOffset + yOffset 
    app = bossWidget(root)
    while emp:    
        department, employees = emp.popitem()
        app.pack(side='top',fill='both',expand=True)
        app.populateUI(title = department, labelList = employees)
        root.mainloop()
        try:
            root.destroy()
        except tk.TclError:
            pass # if run in my IDE, the root already is destroyed

if __name__ == '__main__':
    main()   
C8H10N4O2
  • 18,312
  • 8
  • 98
  • 134

3 Answers3

1

The basic pattern is to have a class or a function for each frame. Each of these classes or functions creates a single Frame, and places all of its widgets in that frame.

Then, all you need to do to switch frames is delete the current frame, and call the function or object to create the new frame. It's as simple as that.

Some examples on this site:

Bryan Oakley
  • 370,779
  • 53
  • 539
  • 685
  • OK, but it's the same function each time, called on new data – C8H10N4O2 Jan 05 '18 at 20:53
  • @C8H10N4O2: that's not a problem. Just create a new instance of the class, with different data. The point is, destroy the old frame, create the new frame. It doesn't have to be complicated. By putting everything inside a single frame, you only have to delete one thing since child widgets are automatically deleted. – Bryan Oakley Jan 05 '18 at 20:55
  • Thanks and I get that, sort of was trying to do that with `populateUI` on the `innerFrame` -- where I was stuck is how does the new instance know what the next value of the iterable is – C8H10N4O2 Jan 05 '18 at 20:59
1

Here's a short rework of your code to handle updating the checkboxes on firing employees and switching frames to display the new employees from the department. I didn't handle advancing if all employees have been fired. There's also a small bug, but I'll leave that to you to figure out.

This could be a lot cleaner. I just didn't want to rewrite all of your code....

   # Example data
    emp = [['Sales', ['Alice','Bryan','Cathy','Dave']],
           ['Product', ['Elizabeth','Frank','Gordon','Heather',
                      'Irene','John','Kristof','Lauren']],
           ['Marketing', ['Marvin']],
           ['Accounting', ['Nancy','Oscar','Peter','Quentin',
                         'Rebecca','Sally','Trevor','Umberto',
                         'Victoria','Wally','Xavier','Yolanda',
                         'Zeus']]]

    import tkinter as tk
    from tkinter import messagebox

    class bossWidget(tk.Frame):
        def __init__(self, root):
            """
            Scrollbar code credit to Bryan Oakley:
            https://stackoverflow.com/a/3092341/2573061
            """
            super().__init__()    

            self.cursor = 0

            self.canvas = tk.Canvas(root, borderwidth=0)
            self.frame  = tk.Frame(self.canvas)
            self.scroll = tk.Scrollbar(root, orient="vertical", command=self.canvas.yview)
            self.canvas.configure(yscrollcommand=self.scroll.set)
            self.scroll.pack(side="right", fill="y")
            self.canvas.pack(side="left", fill="both", expand=True)
            self.canvas.create_window((4,4), window=self.frame, anchor="nw", 
                                      tags="self.frame")
            self.frame.bind("<Configure>", self.onFrameConfigure)
            self.initUI()        

        def initUI(self):
            """
            Creates the static UI content and the innerFrame that will hold the
            dynamic UI content (i.e., the Checkbuttons for the copies)
            """
            self.master.title("Boss Interface")
            self.instructLabel = tk.Label( self.frame, justify='left',
                                          text = "Select the employees you wish to FIRE")
            self.skipButton   = tk.Button( self.frame, text="Skip Department", 
                                          command = self.advance)
            self.deleteButton = tk.Button( self.frame, text="Fire employees", fg = 'red',
                                           command = self.executeSelection )
            self.quitButton   = tk.Button( self.frame, text="Exit", command=self.frame.quit)
            self.innerFrame = tk.Frame(self.frame)
            self.instructLabel.pack(anchor = 'nw', padx=5,pady=5)
            self.innerFrame.pack(anchor = 'nw', padx=5,pady=5)
            self.deleteButton.pack(side='left', padx=5,pady=5)
            self.skipButton.pack(side='left', padx=5,pady=5)
            self.quitButton.pack(side='left', padx=5,pady=5)

            self.populateUI(*self.get_populate_items())

        def get_populate_items(self):

            return (emp[self.cursor][0], emp[self.cursor][1])

        def populateUI(self, title, labelList):
            """
            Creates and packs a list of Checkbuttons (cbList) into the innerFrame
            By default, the first Checkbutton will be unchecked, all others checked.
            You should help the boss out by passing the best employee at the head of the list
            """
            for child in self.innerFrame.winfo_children():
                child.destroy()
            self.instructLabel.config(text = title + ' department:\nSelect the employees you wish to FIRE')
            self.cbList = [None] * len(labelList)
            self.cbValues = [tk.BooleanVar() for i in range(len(labelList))]
            for i in range(len(labelList)):
                self.cbList[i] = tk.Checkbutton( self.innerFrame, 
                                            text=labelList[i], 
                                            variable = self.cbValues[i])
                if i: self.cbList[i].select() # Check subsequent buttons by default
                self.cbList[i].pack(anchor = 'w', padx=5,pady=5) 

        def advance(self):

            if (self.cursor < len(emp) - 1):
                self.cursor += 1
            else:
                self.cursor  = 0
            self.populateUI(*self.get_populate_items())

        def querySelection(self):
            return [x.get() for x in self.cbValues]

        def executeSelection(self):
            fired = self.querySelection()

            if ( not all(x for x in fired) or 
                 messagebox.askokcancel(message='Fire ALL the employees in the department?') 
               ):       
                for i in range(len(self.cbList)):
                    empName = self.cbList[i].cget('text') 
                    if fired[i]:
                        emp[self.cursor][1].remove(empName)
                        print('Sorry, '+ empName + ', but we have to let you go.', flush=True)
                    else:    
                        print('See you Monday, '+ empName, flush=True) 
                self.populateUI(*self.get_populate_items())
                # self.advance()   

        def onFrameConfigure(self, event):
            """Reset the scroll region to encompass the inner frame"""
            self.canvas.configure(scrollregion=self.canvas.bbox("all"))

    def main(): 
        root = tk.Tk()   
        root.geometry("400x250+250+100") # width x height + xOffset + yOffset 
        app = bossWidget(root)
        root.mainloop()
        # while emp:    
        #     department, employees = emp.popitem()
        #     app.pack(side='top',fill='both',expand=True)
        #     app.populateUI(title = department, labelList = employees)
        #     root.mainloop()
        #     try:
        #         root.destroy()
        #     except tk.TclError:
        #         pass # if run in my IDE, the root already is destroyed

    if __name__ == '__main__':
        main()   
Pythonista
  • 11,377
  • 2
  • 31
  • 50
  • so basically, keep `emp` a global variable, and track position with `cursor` instead of the `pop.item` approach -- is that right? – C8H10N4O2 Jan 05 '18 at 21:00
  • the other option would be to pop a department each time and pass the remaining ones on to the next instance, but I couldn't think of how to pass the remaining values from an instance that gets destroyed – C8H10N4O2 Jan 05 '18 at 21:03
  • I'm accepting this because the key insight is to have the function that populates the UI destroy any existing widgets first – C8H10N4O2 Jan 05 '18 at 22:01
0

I accepted Pythonista's answer but eventually wound up doing the following:

  • the UI constructor gets the data as an argument (perhaps better practice than the global data variable)
  • the UI populator deletes any existing labels first (see accepted answer)
  • the UI populator then pops a record off (if remaining, otherwise terminate)
  • the execute button calls the UI populator after doing its other tasks
  • the skip button just calls the UI populator (thus the advance function could be removed entirely)

This is what I wound up using. As Pythonista said, it's messy, but we all have to start somewhere.

# Example data
emp = {'Sales':['Alice','Bryan','Cathy','Dave'],
       'Product':['Elizabeth','Frank','Gordon','Heather',
                  'Irene','John','Kristof','Lauren'],
       'Marketing':['Marvin'],
       'Accounting':['Nancy','Oscar','Peter','Quentin',
                     'Rebecca','Sally','Trevor','Umberto',
                     'Victoria','Wally','Xavier','Yolanda',
                     'Zeus']}

import tkinter as tk
from tkinter import messagebox

class bossWidget(tk.Frame):
    def __init__(self, root, data):
        """
        Scrollbar code credit to Bryan Oakley:
        https://stackoverflow.com/a/3092341/2573061
        """
        super().__init__()     
        self.canvas = tk.Canvas(root, borderwidth=0)
        self.frame  = tk.Frame(self.canvas)
        self.scroll = tk.Scrollbar(root, orient="vertical", command=self.canvas.yview)
        self.canvas.configure(yscrollcommand=self.scroll.set)
        self.scroll.pack(side="right", fill="y")
        self.canvas.pack(side="left", fill="both", expand=True)
        self.canvas.create_window((4,4), window=self.frame, anchor="nw", 
                                  tags="self.frame")
        self.frame.bind("<Configure>", self.onFrameConfigure)
        self.data = data
        self.initUI()        

    def initUI(self):
        """
        Creates the static UI content and the innerFrame that will hold the
        dynamic UI content (i.e., the Checkbuttons for the copies)
        """
        self.master.title("Boss Interface")
        self.instructLabel = tk.Label( self.frame, justify='left',
                                      text = "Select the employees you wish to FIRE")
        self.skipButton   = tk.Button( self.frame, text="Skip Department", 
                                      command = self.populateUI)
        self.deleteButton = tk.Button( self.frame, text="Fire employees", fg = 'red',
                                       command = self.executeSelection )
        self.quitButton   = tk.Button( self.frame, text="Exit", command=self.frame.quit)
        self.innerFrame   = tk.Frame( self.frame)
        self.instructLabel.pack(anchor = 'nw', padx=5,pady=5)
        self.innerFrame.pack(anchor='nw', padx=5, pady=20, expand=True)
        self.deleteButton.pack(side='left', padx=5,pady=5)
        self.skipButton.pack(side='left', padx=5,pady=5)
        self.quitButton.pack(side='left', padx=5,pady=5)
        self.populateUI()

    def populateUI(self):
        """
        Creates and packs a list of Checkbuttons (cbList) into the innerFrame
        By default, the first Checkbutton will be unchecked, all others checked.
        You should help the boss out by passing the best employee at the head of the list
        """
        for child in self.innerFrame.winfo_children():
            child.destroy()
        try:
            title, labelList = self.data.popitem()
            self.instructLabel.config(text = title + ' department:\nSelect the employees you wish to FIRE')
            self.cbList = [None] * len(labelList)
            self.cbValues = [tk.BooleanVar() for i in range(len(labelList))]
            for i in range(len(labelList)):
                self.cbList[i] = tk.Checkbutton( self.innerFrame, 
                                            text=labelList[i], 
                                            variable = self.cbValues[i])
                if i: self.cbList[i].select() # Check subsequent buttons by default
                self.cbList[i].pack(anchor = 'w', padx=5,pady=5) 
        except KeyError:
            messagebox.showinfo("All done", "You've purged all the departments.  Good job, boss.")
            self.frame.quit()

    def querySelection(self):
        return [x.get() for x in self.cbValues]

    def executeSelection(self):
        fired = self.querySelection()

        if ( not all(x for x in fired) or 
             messagebox.askokcancel(message='Fire ALL the employees in the department?') 
           ):       
            for i in range(len(self.cbList)):
                empName = self.cbList[i].cget('text') 
                if fired[i]:
                    print('Sorry, '+ empName + ', but we have to let you go.', flush=True)
                else:    
                    print('See you Monday, '+ empName, flush=True)    
            self.populateUI()   

    def onFrameConfigure(self, event):
        """Reset the scroll region to encompass the inner frame"""
        self.canvas.configure(scrollregion=self.canvas.bbox("all"))

def main(): 
    root = tk.Tk()   
    root.geometry("400x250+250+100") # width x height + xOffset + yOffset 
    app = bossWidget(root, data=emp)
    app.mainloop()
    try:
        root.destroy()
    except tk.TclError:
        pass # if run in my IDE, the root already is destroyed

if __name__ == '__main__':
    main()   
C8H10N4O2
  • 18,312
  • 8
  • 98
  • 134