0

Initially I wrote this tkinter app functionally, but now that I'm converting it into OOP due to how bloated its become, the major issue I'm having is understanding a streamlined way to pass variables between a large number of classes.

Below is a minimum test version of the start of my code, with two variables defined in Mainframe that I want to first pass to FileSelect to update them and then on to LoadFiles. The big area of confusion for me is why file_list is seemingly updating correctly when running print_files, but the import_state variable I want to use to enable a button in LoadFiles does not.

import os
import tkinter as tk
from tkinter import filedialog


class MainFrame:

    def __init__(self, master):
        self.master = master

        # Variables
        self.file_list = []  # Files list
        self.import_state = tk.DISABLED  # Enable import on correct fp

        # Class Frames
        FileSelect(master, self.file_list, self.import_state)
        LoadFiles(master, self.file_list, self.import_state)


class FileSelect:

    def __init__(self, master, files, button_state):
        self.files = files
        self.button_state = button_state

        # Frame
        self.frame = tk.LabelFrame(master, text='File Selection')
        self.frame.grid(row=0, column=0)

        # Select directory button
        tk.Button(self.frame, text='Open:', command=self.directory_path).grid(row=0, column=0)

        # Tkinter Elements...

    # Directory Selection
    def directory_path(self):

        # Select filepath
        directory = filedialog.askdirectory(initialdir="/", title="Select a directory")
        os.chdir(directory)

        # Populate list of files
        for tb in os.listdir(directory):
            self.files.append(tb)

        # Update GUI, activate button in different class if condition met
        if len(os.listdir(directory)) == 0:
            # Update GUI Labels...
            self.button_state = tk.DISABLED
            print(self.files, self.button_state)
        elif len(self.files) == len(os.listdir(directory)):
            # Update GUI Labels...
            self.button_state = tk.NORMAL
            print(self.files, self.button_state)


class LoadFiles:
    def __init__(self, master, files, button_state):
        self.files = files
        self.button_state = button_state

        # Frame
        self.frame = tk.LabelFrame(master, text='File loading')
        self.frame.grid(row=1, column=0)
        # Load button
        self.import_files_button = tk.Button(self.frame, text='Import TB', command=self.print_files)
        self.import_files_button.grid(row=0, column=0, sticky="EW")

        # Tkinter Elements...

    def print_files(self):
        print(self.files, self.button_state)


if __name__ == "__main__":
    root = tk.Tk()
    MainFrame(root)
    root.mainloop()

Aside from the issue of variable passing between classes, is there a more streamlined way of doing this in general? One of the main reasons I've seen OOP advocated is how much it improves readability, organisation, and isolating tkinter elements, but the variable handling seems to be a major lynchpin when converting from functional.

1
  • 2
    I has to do with mutability. tk.DISABLED is a string and they're immutable, so all that happens in the called method is a value is assigned to the class' button_state attribute and when that's changed it only affect the value associated with the attribute. On the other hand file_list is a list which are mutable. So changes made to it through its append() method actually change its value (contents). See Facts and myths about Python names and values. Commented Jan 6, 2022 at 22:43

2 Answers 2

2

Instead of passing the individual variables, pass the instance of the class that owns the variables. This reduces the number of things you have to pass back and forth, and it makes the code more self-documenting since it's clear that self.main.files.append(...) is appending to the list managed by the main program instead of a list managed locally.

class MainFrame:
    def __init__(self, master):
        ...
        FileSelect(master, main=self)
        LoadFiles(master, main=self)

class FileSelect:
    def __init__(self, master, main):
        self.main = main
        ...

    # Directory Selection
    def directory_path(self):

        # Select filepath
        directory = filedialog.askdirectory(initialdir="/", title="Select a directory")
        os.chdir(directory)

        # Populate list of files
        for tb in os.listdir(directory):
            self.main.files.append(tb)

        # Update GUI, activate button in different class if condition met
        if len(os.listdir(directory)) == 0:
            # Update GUI Labels...
            self.main.button_state = tk.DISABLED
            print(self.main.files, self.main.button_state)
        elif len(self.files) == len(os.listdir(directory)):
            # Update GUI Labels...
            self.main.button_state = tk.NORMAL
            print(self.main.files, self.main.button_state)
Sign up to request clarification or add additional context in comments.

Comments

0

Just to follow up, Bryan and Martineau's answers helped to resolve my issue, but for the sake of others that may be having issues with handling updates to tkinter widgets, I've included my updated code below since self.main.file_load.import_files_button is a method that explicitly resolves the changing of button states between classes, and one of my major issues understanding this was a lack of clear minimised examples:

import os
import tkinter as tk
from tkinter import filedialog


class MainFrame:
    def __init__(self, master):
        self.file_selection = FileSelect(master, main=self)
        self.file_load = LoadFiles(master, main=self)


class FileSelect:
    def __init__(self, master, main):
        self.main = main
        self.files = []

        # Frame
        self.frame = tk.LabelFrame(master, text='File Selection')
        self.frame.grid(row=0, column=0)

        # Select directory button
        tk.Button(self.frame, text='Open:', command=self.directory_path).grid(row=0, column=0)

    # Directory Selection
    def directory_path(self):

        # Select filepath
        directory = filedialog.askdirectory(initialdir="/", title="Select a directory")
        os.chdir(directory)

        # Populate list of files
        for tb in os.listdir(directory):
            self.files.append(tb)

        # Update GUI, activate button in different class if condition met
        if len(os.listdir(directory)) == 0:
            self.main.file_load.import_files_button['state'] = tk.DISABLED

        else:
            self.main.file_load.import_files_button['state'] = tk.NORMAL


class LoadFiles:
    def __init__(self, master, main):
        self.main = main

        # Frame
        self.frame = tk.LabelFrame(master, text='File loading')
        self.frame.grid(row=1, column=0)
        # Load button
        self.import_files_button = tk.Button(self.frame, text='Import TB', command=self.print_files,
                                             state=tk.DISABLED)
        self.import_files_button.grid(row=0, column=0, sticky="EW")

        # Tkinter Elements...

    def print_files(self):
        print(self.main.file_selection.files)


if __name__ == "__main__":
    root = tk.Tk()
    MainFrame(root)
    root.mainloop()

Hopefully someone finds this useful!

2 Comments

Note that self.main.file_load.import_files_button isn't a method. It's the name of a Button widget which has a state option. What your code is doing by assigning different values to that option is simply changing how it looks on the screen.
Apologies that was bad wording on my part, I wasn't intentionally referring to "method" in the pythonic sense, just that it was the solution I used.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.