简体   繁体   中英

Iteration within tkinter Frame

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()   

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:

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()   

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()   

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM