18

I am using Python 2.7 and Tkinter. I am almost new to Object Oriented programs. I have a long program with many Tkinter windows and at some point I ask the user to load an Excel file that I read with Pandas, and want to permanently use and update that value (of a data variable). The way that I am doing it now is with global variables but I know that it is dangerous, inefficient and not elegant at all.

Even though I could do controller.show_frame(framename) given the way my gui class is built, I ended up building some of the frames myself just so the data variable would update itself.

I read and tried some answers in Stack Overflow but may have implemented them wrong:

  • Tried creating a dictionary inside the gui class, something like self.app_data = {data=[],filename=""} and updating it from other windows, the thing here is that I think that the class gui is instanced only once and it kind of creates all of the other window classes so this did not work. Maybe I did something wrong there. (not shown on the code).
  • Tried to do something as what was suggested here but I could just not make it work.

Main frame is some sort of intermediate step that I need for other purposes; the following code is a simplification of my program.

I know this is an awful nightmare code! Thank you :)

import Tkinter as tk
import pandas as pd 
import tkFileDialog
import tkMessageBox
global data, strat_columns, filename
data = pd.DataFrame([])
strat_columns = []
filename = ""

class gui(tk.Tk):

    data = pd.DataFrame([])
    filename = ""
    def __init__(self):
        tk.Tk.__init__(self)
        container = tk.Frame(self)
        container.pack(side="top",fill="both",expand=True)
        self.frames = {}

        for F in (main_frame, first_frame):
            frame = F(container, self)
            self.frames[F] = frame
            frame.grid(row=0, column=0, sticky="nsew")
        self.show_frame(main_frame)

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

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


class main_frame(tk.Frame):

    def __init__(self,parent,controller):

        tk.Frame.__init__(self,parent)
        self.parent = parent 
        self.controller = controller
        button_new = tk.Button(self,
                           text="New window",
                           command=lambda: self.button_new_callback())
        button_new.pack()

    def button_new_callback(self,*args,**kwargs):
        self.controller.show_frame(first_frame)


class first_frame(tk.Frame):

    def __init__(self,parent,controller):
        tk.Frame.__init__(self,parent)
        self.controller = controller
        self.parent = parent
        self.show_frame = controller.show_frame
        statusText.set("Press Browse button and browse for file, then press the Go button")
        label = tk.Label(self, text="Please load a file: ")
        label.pack()
        entry = tk.Entry(self, width=50)
        entry.pack()
        button_go = tk.Button(self,
                           text="Go",
                           command=lambda: self.button_go_callback(entry,statusText,message))
        button_browse = tk.Button(self,
                               text="Browse",
                               command=lambda: self.button_browse_callback(entry))
        button_go.pack()
        button_browse.pack()
        message = tk.Label(self, textvariable=statusText)
        message.pack()

    def button_browse_callback(self,entry):
        global filename
        filename = tkFileDialog.askopenfilename()
        entry.delete(0, tk.END)
        entry.insert(0, filename)

    def button_go_callback(self,entry,statusText,message):
        global data
        input_file = entry.get()
        data = pd.read_excel(filename)
        sf = second_frame(self.parent, self)
        sf.grid(row=0, column=0, sticky="nsew")
        sf.tkraise()

class second_frame(tk.Frame):
     pass

if __name__ == "__main__":

    my_gui = gui()
    my_gui.mainloop()
    my_gui.title("TEST")
sarangof
  • 181
  • 4
  • 2
    You can initiate `data`, `strat_columns` and `filename` in `gui.__init__` and access them with the `controller` object in `main_frame` and `first_frame`, eg: `self.controller.filename = '/some/file'`. Alternatively you could build a small class, eg: `my_data` with 3 class variables: `data`, `strat_columns` and `filename`. Then you can access them directly ( without creating a new instance ) eg: `my_data.filename = '/some/file'` – t.m.adam Aug 14 '17 at 01:59
  • 2
    As t.m.adam mentioned you should move the 2 variables `data` and `filename` inside of the `__init__` section. Don't use global in classes. Instead make your variables into class attributes with the `self.` prefix. This will eliminate the need for global. A class attribute can be accessed from anywhere inside the class or its methods. – Mike - SMT Aug 14 '17 at 13:03
  • Thank you! I created the my_data class as @t.m.adam suggested and it succeeds passing information between windows as long as I create them as `sf = second_frame(self.parent, self); sf.grid(row=0, column=0, sticky="nsew"); sf.tkraise()`. If I try to do it as `self.controller.show_frame(second_frame)`, windows will be rendered with the initial values for the variables, and their corresponding classes will not be "instanced" again (not sure if that is the right word). Any clue as to why this happens? Thanks! – sarangof Aug 14 '17 at 18:45
  • Why don't you try with instance variables? I only mentioned the class variables method as an alternative to globals. – t.m.adam Aug 15 '17 at 13:29
  • Just tried with instance variables but keep having the same problem. Somehow I think it has to do with `self.controller.show_frame(first_frame/second_frame)`, it seems like that is done only once at the beginning, so even if I press the buttons that are supposed to "execute" it again, this will not "re-render" the windows as they were created with the initial assignment of variables. It will work if I create the frames as `sf = second_frame(self.parent, self); sf.grid(row=0, column=0, sticky="nsew"); sf.tkraise()`, which is not as elegant (I think) as using `show_frame()` – sarangof Aug 16 '17 at 16:02
  • What is `statusText` in your code? The code just does not run. – aristotll Aug 19 '17 at 05:39
  • Sorry @aristotll, my bad. statusText = tk.StringVar(self) should go before. This is a trimmed version of a very long program and I did not run the code that I selected before posting it here. Ooops! For the purposes of this question, both lines could be ignored. – sarangof Aug 19 '17 at 14:34
  • 1
    I just wanted to point out that `self.app_data = {data=[],filename=""}` will not work as it is not a valid format for a dict. it would need to be like `self.app_data = {data : [], filename : ""}` Dictionaries use a colon with a key on the left side and the data on the right side. – Mike - SMT Aug 22 '17 at 21:10
  • You use single-way methods, `build` never store a data. Make a function with some variable and made some element + sub_main_frame accessible. @SierraMountainTech comments touched this subject. You can't save all data in a application, another point is how rebuild `sub_frame`? Don't store any data cos you got GUI elements, check elements variables/definiations. – dsgdfg Aug 23 '17 at 07:51

2 Answers2

3

There are a few things that are causing issues with your program from running properly.

The first thing I noticed is the use of global variables. This can be avoided with the use of class attributes.

For the 2 variables you have just below the line class gui(tk.Tk): you need to move them to the __init__ section so those variables can be instantiated. Also you need to make them into class attributes so other methods or even other classes can interact with them. We can do this by adding the self. prefix to the variable names.

Something like the below:

self.data = pd.DataFrame([])
self.filename = ""

To access the methods/attributes of the gui class you need to pass the object of the gui class to the other classes working with it.

statusText in your code is not defined so set() wont work here. Just add self.statusText as a class attribute.

Some of your widgets do not need to be assigned to a variable name as no editing is being done to them. For example:

label = tk.Label(self, text="Please load a file: ")
label.pack()

This can be simply changed to:

tk.Label(self, text="Please load a file: ").pack()

This will help reduce the amount of code you are writing and keep the name space cleaner.

There are a few ways to correct all this but the easiest way is to move this code into one class. There is not a good reason with the code you have presented to have several frames separated from the main gui class.

The below code is a rewritten version of your code using one class to accomplish what appears to be the task your code is trying to accomplish and reducing the amount of code needed by around 30+ lines. I am sure it can be refined further but this example should be helpful.

import Tkinter as tk
import pandas as pd
import tkFileDialog

class gui(tk.Frame):


    def __init__(self, master, *args, **kwargs):
        tk.Frame.__init__(self, master, *args, **kwargs)

        self.master = master
        self.data = pd.DataFrame([])
        self.filename = ""
        self.strat_columns = []

        self.main_frame()
        self.first_frame()
        self.mainframe.tkraise()

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

    def main_frame(self):
        self.mainframe = tk.Frame(self.master)
        self.mainframe.grid(row=0, column=0, sticky="nsew")

        tk.Button(self.mainframe, text="New window", 
                  command=lambda: self.firstframe.tkraise()).pack()


    def first_frame(self):
        self.firstframe = tk.Frame(self.master)
        self.firstframe.grid(row=0, column=0, sticky="nsew")

        self.statusText = tk.StringVar()
        self.statusText.set("Press Browse button and browse for file, then press the Go button")
        label = tk.Label(self.firstframe, text="Please load a file: ").pack()
        self.first_frame_entry = tk.Entry(self.firstframe, width=50)
        self.first_frame_entry.pack()
        tk.Button(self.firstframe, text="Go", command=self.button_go_callback).pack()
        tk.Button(self.firstframe, text="Browse", command=self.button_browse_callback).pack()
        self.message = tk.Label(self.firstframe, textvariable=self.statusText)
        self.message.pack()


    def button_browse_callback(self):
        self.filename = tkFileDialog.askopenfilename()
        self.first_frame_entry.delete(0, tk.END)
        self.first_frame_entry.insert(0, self.filename)

    def button_go_callback(self):
        self.data = pd.read_excel(self.filename)


if __name__ == "__main__":
    root = tk.Tk()
    root.title("TEST")
    my_gui = gui(root)
    root.mainloop()
Mike - SMT
  • 14,784
  • 4
  • 35
  • 79
  • Thank you! So far it is working. As I expand my program, inside one of the `callback` methods I want to open a window `second_frame` after doing transformations to the data. `def second_frame(self): self.secondframe = tk.Frame(self.master) self.secondframe.grid(row=0, column=0, sticky="nsew") ...` I am doing `self.secondframe.tkraise()` from inside one of the `callback` methods. The problem now is that a window is opening but nothing else is happening (even if I am just trying to add some buttons and text). What would be the right way to open that new window? – sarangof Sep 15 '17 at 19:07
  • @sarangof You should be able to add to add any widget as normal to `self.secondframe` after it has been created. – Mike - SMT Sep 15 '17 at 20:37
  • @sarangof it sounds like you may have a new problem that should be on a new question. – Mike - SMT Sep 15 '17 at 20:38
  • Thank you @mike-smt, you were right, the problem was different and it's solved. – sarangof Oct 18 '17 at 15:07
  • @sarangof so your issue with the new frame is fixed now? – Mike - SMT Oct 18 '17 at 15:19
2

in my opinion your are tying too much the data and the GUI. What if in the future you want to display something else? I would use a more generic approach: I would a create a DataProvider class that would read and return the data for you:

Data Provider

import pandas as pd

    class DataProvider(object):

        def __init__(self):
            self._excel = {}
            self._binary = {}

        def readExcel(self, filename):
            self._excel[filename] = pd.read_excel(filename)

        def excel(self):
            return self._excel

        def readBinary(self, filename):
            self._binary[filename] = open(filename,"rb").read()

        def binary(self):
            return self._binary

Using this class you can obtain the data that you can present in your GUI:

gui.py

from Tkinter import *
from DataProvider import *
import binascii

#example data
dp = DataProvider()
dp.readExcel("file.xlsx")
dp.readBinary("img.png")


root = Tk()
frame = Frame(root)
frame.pack()

bottomframe = Frame(root)
bottomframe.pack( side = BOTTOM )

w = Label(bottomframe, text=dp.excel()["file.xlsx"])
w.pack()


w = Label(bottomframe, text=binascii.hexlify(dp.binary()["img.png"][:5]))
w.pack()

root.mainloop()

If all works well you should have a small GUI showing the content of the Excel file and the first few bytes of the image.

Gui Example

Let me know if all works fine or if you need more info.

Thanks

Alberto
  • 446
  • 5
  • 14