0

I would like to update a data frame displayed in tkinter getting the data from another class.

In my app, I have defined the frames using classes. By changing an input parameter in a class, the data frame should update in another class.

For instance, I select the multiplier in the OptionMenu within the class Commands and the column B in the data frame displayed in the class Table should be updated by this multiplier. But as I change the multiplier, the data frame does not update. I am using here Treeview to display the data frame.

enter image description here

As I start the app, the GUI appears like the following. The narrow stripe on the right is the empty data frame. I initialize it as empty and it remains empty even if I change the multiplier. While instead it should look like the previous screenshot.

enter image description here

Of course the full app is much more sophisticated, but here I simplify things for the sake of the question. Actually, the data frame contains many columns and the calculation is quite complex.

I try to pass the object through a controller method. For instance, in the class Table (which displays the data frame), I define:

class Table(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller  # save the reference to the controller in each class

        ...
        get_metrics = self.controller.get_page(Commands)
        self.metrics = get_metrics.metrics

Where I (wrongly) suppose that the data frame self.metrics to be displayed in this frame gets updated by calling the function get_page() via the controller object. The function get_page gets the object metrics from the class Commands and is defined in the main class sampleApp as simple as:

def get_page(self, page_class):
    return self.frames[page_class]

The other class Commands contains the same method parent / controller to allow the objects to be passed among classes.

class Commands(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller  # save the reference to the controller in each class

        self.metrics = pd.DataFrame()

In the class Commands, I initialize the data frame self.metrics as empty initially (but this is not important). Later in the class Commands, I update the data frame with the function calculate_df, which is a function that does not return anything and should remain so. Therefore, the data frame object calculated there is defined as global.

I update the data frame self.metrics in the class Commands using an OptionMenu method that calls the following two lines:

calculate_df(mult = self.mult)
self.metrics = df

Here df is global and I intentionally not define with the direct command self.metrics = calculate_df(...).

The function calculate_df creates a data frame of two columns where the column B gets multiplied. For example with a multiplier of 2 df becomes:

   A  B
0  0  0
1  1  2
2  2  4
3  3  6
4  4  8

I post the full code in the following.

import numpy as np
import pandas as pd

pd.set_option('display.max_columns', 20)
pd.set_option('display.width', 200)

import tkinter as tk
from tkinter import ttk


def calculate_df(mult = 1.0):
    global df
    columns = [np.arange(5), np.arange(5) * mult]
    df = pd.DataFrame(data=np.array(columns).T, columns=['A', 'B'])


class sampleApp(tk.Tk):

    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs)
        tk.Tk.wm_title(self, "sampleApp")

        container = tk.Frame(self)
        container.pack(side="top", fill="both", expand=True)
        container.grid_rowconfigure(0, weight=1)
        container.grid_columnconfigure(0, weight=1)

        self.frames = {}
        frame = Commands(parent=container, controller=self)
        self.frames[Commands] = frame
        frame.grid(row=0, column=0)
        # frame.pack()
        self.show_frame(Commands)

        frame = Table(parent=container, controller=self)
        self.frames[Table] = frame
        frame.grid(row=0, column=1)
        # frame.pack()
        self.show_frame(Table)


    def show_frame(self, frame_name):
        frame = self.frames[frame_name]
        frame.tkraise()

    def get_page(self, page_class):
        return self.frames[page_class]


class Table(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller  # save the reference to the controller in each class

        table_frame = tk.Frame(self, bd=1, relief=tk.RIDGE)
        table_frame.pack(fill='x')

        get_metrics = self.controller.get_page(Commands)
        self.metrics = get_metrics.metrics

        # columns = [np.arange(5), np.arange(5) * 2]
        # df = pd.DataFrame(data=np.array(columns).T, columns=['A', 'B'])
        # self.metrics = df

        tv1 = ttk.Treeview(table_frame)
        tv1.pack()

        def display_metrics():
            tv1.delete(*tv1.get_children())
            tv1["column"] = list(self.metrics.columns)
            tv1["show"] = "headings"
            for column in tv1["columns"]:
                tv1.heading(column, text=column)  # set column heading

            df_rows = self.metrics.to_numpy().tolist()  # convert dataframe to list
            for row in df_rows:
                # inserts each list into the treeview.
                tv1.insert("", "end", values=row)

        display_metrics()


class Commands(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller  # save the reference to the controller in each class

        self.metrics = pd.DataFrame()

        side_frame = tk.Frame(self, relief=tk.RIDGE)
        side_frame.pack()

        buttons_frame = tk.Frame(side_frame, bd=1, relief=tk.RIDGE)
        buttons_frame.pack(fill='x')

        Lab1 = tk.Label(buttons_frame, text="multiplier", anchor=tk.W)
        Lab1.grid(row=1, column=0)

        def set_multiplier(*args):
            self.mult = float(mult_var.get())
            print("multiplier", self.mult)

            calculate_df(mult = self.mult)
            self.metrics = df

        mult_var = tk.StringVar(self)
        mult_var.set('')
        mult_var.trace("w", set_multiplier)

        opt_mult = tk.OptionMenu(buttons_frame, mult_var, *[1, 2, 3, 4])
        opt_mult.grid(row=2, column=2, columnspan=1)


if __name__ == '__main__':

    app = sampleApp()
    app.geometry("+35+35")
    app.mainloop()

2 Answers 2

0

First change tv1 to instance variable self.tv1 and nested function display_metrics() to class function of Table. Also pass the required metrics to display_metrics() as an argument.

Then you can call Table.display_metrics() directly inside nested function set_multiplier() inside Commands class:

...
class Table(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller  # save the reference to the controller in each class

        table_frame = tk.Frame(self, bd=1, relief=tk.RIDGE)
        table_frame.pack(fill='x')

        # changed tv1 to instance variable self.tv1
        self.tv1 = ttk.Treeview(table_frame, show="headings")
        self.tv1.pack()

    # change display_metrics() to class function with added argument metrics
    def display_metrics(self, metrics):
        self.tv1.delete(*self.tv1.get_children())
        self.tv1["column"] = list(metrics.columns)
        for column in self.tv1["columns"]:
            self.tv1.heading(column, text=column)  # set column heading

        df_rows = metrics.to_numpy().tolist()  # convert dataframe to list
        for row in df_rows:
            # inserts each list into the treeview.
            self.tv1.insert("", "end", values=row)


class Commands(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)
        self.parent = parent
        self.controller = controller  # save the reference to the controller in each class

        self.metrics = pd.DataFrame()

        side_frame = tk.Frame(self, relief=tk.RIDGE)
        side_frame.pack()

        buttons_frame = tk.Frame(side_frame, bd=1, relief=tk.RIDGE)
        buttons_frame.pack(fill='x')

        Lab1 = tk.Label(buttons_frame, text="multiplier", anchor=tk.W)
        Lab1.grid(row=1, column=0)

        def set_multiplier(*args):
            self.mult = float(mult_var.get())
            print("multiplier", self.mult)

            calculate_df(mult = self.mult)
            self.metrics = df
            
            # update table
            self.controller.get_page(Table).display_metrics(self.metrics)

        mult_var = tk.StringVar(self)
        mult_var.set('')
        mult_var.trace("w", set_multiplier)

        opt_mult = tk.OptionMenu(buttons_frame, mult_var, *[1, 2, 3, 4])
        opt_mult.grid(row=2, column=2, columnspan=1)
...

Note that it is better to change nested function set_multiplier() to class method as well.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you @acw1668. It was a good idea to change display_metrics to a class function. I have also changed set_multiplier to class method as well and it works very well. For the case of set_multiplier it works both ways: my original code and your suggestion. But what exactly is better when using the class method?
0

You can create a static method to achieve this.

You will have to read a bit about the topic.

But in short. You define a static method in Table class and then you can call that function in Commands class without initializing an instance of Table class. The static method can be called using Table.my_method(value).

Comments

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.