4

I'm currently working on a GUI which is based on the thread How to get variable data from a class. Since there will be a lot of data to handle, I would like to use a Model-Class, which get's its updates via Observer.

Right now, changes in the ttk.Combobox on Page One are registered via <<ComboboxSelect>>, pulled into the variable self.shared_data of the Controller and passed to the Model. This way, no Oberserver/Observable logic is used. Instead, the data in Model is changed, whenever the user takes a corresponding action in the GUI.

I, however, would love not to have to use bindings like <<ComboboxSelect>> to change the corresponding data in the Model, but an Observer/Observable logic, which detects, that i.e. the entry "Inputformat" in the dictionary self.shared_data within the Controller was changed, which in turn refreshes the data in the Model, i.e. self.model_data, where the actual state of the ttk.Combobox is saved.

In short, I want to achieve the following, by using an Observer:

User selects i.e. "Entry 01" in the ttk.Combobox --> self.shared_data["Inputformat"] in the Controller is now filled with "Entry 01" --> an Observer/Observable logic detects this --> the corresponding variable in the Model is beeing changed.

For you to have something to work with, here is the code.

# -*- coding: utf-8 -*-

import csv
import Tkinter as tk   # python2
import ttk
import tkFileDialog


# Register a new csv dialect for global use.
# Its delimiter shall be the semicolon:
csv.register_dialect('excel-semicolon', delimiter = ';')

font = ('Calibri', 12)

''' 
############################################################################### 
#                                 Model                                       # 
###############################################################################
'''

class Model:
    def __init__(self, *args, **kwargs):
        # There shall be a variable, which is updated every time the entry
        # of the combobox is changed
        self.model_keys = {}
        self.model_directories = {}

    def set_keys(self, keys_model):
        self.model_keys = keys_model
        keys = []
        keyentries = []
        for key in self.model_keys:
            keys.append(key)
        for entry in self.model_keys:
            keyentries.append(self.model_keys[entry].get())

        print "model_keys: {0}".format(keys) 
        print "model_keyentries: {0}".format(keyentries)

    def get_keys(self):
        keys_model = self.model_keys
        return(keys_model)

    def set_directories(self, model_directories):
        self.model_directories = model_directories
        print "Directories: {0}".format(self.model_directories)

    def get_directories(self):
        model_directories = self.model_directories
        return(model_directories)


''' 
############################################################################### 
#                               Controller                                    # 
###############################################################################
'''

# controller handles the following: shown pages (View), calculations 
# (to be implemented), datasets (Model), communication
class PageControl(tk.Tk):

    ''' Initialisations '''
    def __init__(self, *args, **kwargs):
        tk.Tk.__init__(self, *args, **kwargs) # init
        tk.Tk.wm_title(self, "MCR-ALS-Converter") # title

        # initiate Model
        self.model = Model()

        # file dialog options
        self.file_opt = self.file_dialog_options()

        # stores checkboxstatus, comboboxselections etc.
        self.shared_keys = self.keys()

        # creates the frames, which are stacked all over each other
        container = self.create_frame()
        self.stack_frames(container)

        #creates the menubar for all frames
        self.create_menubar(container)

        # raises the chosen frame over the others
        self.frame = self.show_frame("StartPage")      


    ''' Methods to show View'''
    # frame, which is the container for all pages
    def create_frame(self):        
        # the container is where we'll stack a bunch of frames
        # on top of each other, then the one we want visible
        # will be raised above the others
        container = ttk.Frame(self)
        container.pack(side="top", fill="both", expand=True)
        container.grid_rowconfigure(0, weight=1)
        container.grid_columnconfigure(0, weight=1)
        return(container)

    def stack_frames(self, container):
        self.frames = {}
        for F in (StartPage, PageOne, PageTwo):
            page_name = F.__name__
            frame = F(parent = container, controller = self)
            self.frames[page_name] = frame
            # put all of the pages in the same location;
            # the one on the top of the stacking order
            # will be the one that is visible.
            frame.grid(row=0, column=0, sticky="nsew")

    # overarching menubar, seen by all pages
    def create_menubar(self, container):       
        # the menubar is going to be seen by all pages       
        menubar = tk.Menu(container)
        menubar.add_command(label = "Quit", command = lambda: app.destroy())
        tk.Tk.config(self, menu = menubar)

    # function of the controller, to show the desired frame
    def show_frame(self, page_name):
        #Show the frame for the given page name
        frame = self.frames[page_name]
        frame.tkraise()
        return(frame)


    ''' Push and Pull of Data from and to Model ''' 
    # calls the method, which pushes the keys in Model (setter)
    def push_keys(self):
        self.model.set_keys(self.shared_keys)

    # calls the method, which pulls the key data from Model (getter)    
    def pull_keys(self):
        pulled_keys = self.model.get_keys()
        return(pulled_keys)

    # calls the method, which pushes the directory data in Model (setter) 
    def push_directories(self, directories):
        self.model.set_directories(directories)

    # calls the method, which pulls the directory data from Model (getter)
    def pull_directories(self):
        directories = self.model.get_directories()
        return(directories)


    ''' Keys '''
    # dictionary with all the variables regarding widgetstatus like checkbox checked    
    def keys(self):
        keys = {}
        keys["Inputformat"] = tk.StringVar()
        keys["Outputformat"] = tk.StringVar() 
        return(keys)


    ''' Options '''  
    # function, which defines the options for file input and output     
    def file_dialog_options(self):
        #Options for saving and loading of files:
        options = {}
        options['defaultextension'] = '.csv'
        options['filetypes'] = [('Comma-Seperated Values', '.csv'), 
                                ('ASCII-File','.asc'), 
                                ('Normal Text File','.txt')]
        options['initialdir'] = 'C//'
        options['initialfile'] = ''
        options['parent'] = self
        options['title'] = 'MCR-ALS Data Preprocessing'
        return(options)


    ''' Methods (bindings) for PageOne '''
    def open_button(self):
        self.get_directories()


    ''' Methods (functions) for PageOne '''
    # UI, where the user can selected data, that shall be opened
    def get_directories(self):
        # open files
        file_input = tkFileDialog.askopenfilenames(** self.file_opt)
        file_input = sorted(list(file_input))
        # create dictionary 
        file_input_dict = {}
        file_input_dict["Input_Directories"] = file_input
        self.push_directories(file_input_dict) 


''' 
############################################################################### 
#                                   View                                      # 
###############################################################################
'''


class StartPage(ttk.Frame):

    ''' Initialisations '''
    def __init__(self, parent, controller):
        ttk.Frame.__init__(self, parent)
        self.controller = controller

        self.labels()
        self.buttons()


    ''' Widgets '''        
    def labels(self):
        label = tk.Label(self, text = "This is the start page", font = font)
        label.pack(side = "top", fill = "x", pady = 10)

    def buttons(self):
        button1 = ttk.Button(self, text = "Go to Page One",
                            command = lambda: self.controller.show_frame("PageOne"))
        button2 = ttk.Button(self, text = "Go to Page Two",
                            command = lambda: self.controller.show_frame("PageTwo"))
        button_close = ttk.Button(self, text = "Close",
                                command = lambda: app.destroy())                    
        button1.pack(side = "top", fill = "x", pady = 10)
        button2.pack(side = "top", fill = "x", pady = 10)
        button_close.pack(side = "top", fill = "x", pady = 10)


class PageOne(ttk.Frame):

    ''' Initialisations '''
    def __init__(self, parent, controller):
        ttk.Frame.__init__(self, parent)
        self.controller = controller

        self.labels()
        self.buttons()
        self.combobox()

    ''' Widgets '''
    def labels(self):
        label = tk.Label(self, text = "On this page, you can read data", font = font)
        label.pack(side = "top", fill = "x", pady = 10)

    def buttons(self): 
        button_open = ttk.Button(self, text = "Open", 
                                 command = lambda: self.controller.open_button())
        button_forward = ttk.Button(self, text = "Next Page >>",
                                command = lambda: self.controller.show_frame("PageTwo"))
        button_back = ttk.Button(self, text = "<< Go back",
                                command = lambda: self.controller.show_frame("StartPage"))
        button_home = ttk.Button(self, text = "Home",
                                command = lambda: self.controller.show_frame("StartPage"))
        button_close = ttk.Button(self, text = "Close",
                                command = lambda: app.destroy())
        button_open.pack(side = "top", fill = "x", pady = 10)
        button_forward.pack(side = "top", fill = "x", pady = 10)
        button_back.pack(side = "top", fill = "x", pady = 10)
        button_home.pack(side = "top", fill = "x", pady = 10)
        button_close.pack(side = "top", fill = "x", pady = 10)

    def combobox(self):                                  
        entries = ("", "Inputformat_01", "Inputformat_02", "Inputformat_03") 
        combobox = ttk.Combobox(self, state = 'readonly', values = entries,
                                     textvariable = self.controller.shared_keys["Inputformat"])
        combobox.current(0)
        combobox.bind('<<ComboboxSelected>>', self.updater)
        combobox.pack(side = "top", fill = "x", pady = 10)


    ''' Bindings '''
    # wrapper, which notifies the controller, that it can update keys in Model
    def updater(self, event):
        self.controller.push_keys()



class PageTwo(ttk.Frame):

    ''' Initialisations '''
    def __init__(self, parent, controller):
        ttk.Frame.__init__(self, parent)
        self.controller = controller

        self.labels()
        self.buttons()
        self.combobox()


    ''' Widgets '''        
    def labels(self):
        label = tk.Label(self, text = "This is page 2", font = font)
        label.pack(side = "top", fill = "x", pady = 10)

    def buttons(self):
        button_back = ttk.Button(self, text = "<< Go back",
                                command = lambda: self.controller.show_frame("PageOne"))
        button_home = ttk.Button(self, text = "Home",
                                command = lambda: self.controller.show_frame("StartPage"))
        button_close = ttk.Button(self, text = "Close",
                                command = lambda: app.destroy())                        
        button_back.pack(side = "top", fill = "x", pady = 10)
        button_home.pack(side = "top", fill = "x", pady = 10)
        button_close.pack(side = "top", fill = "x", pady = 10)

    def combobox(self):
        entries = ("Outputformat_01", "Outputformat_02") 
        combobox = ttk.Combobox(self, state = 'readonly', values = entries,
                                     textvariable = self.controller.shared_keys["Outputformat"])
        combobox.bind('<<ComboboxSelected>>', self.updater)
        combobox.pack(side = "top", fill = "x", pady = 10)


    ''' Bindings '''
    # wrapper, which notifies the controller, that it can update keys in Model
    def updater(self, event):
        self.controller.push_keys()



if __name__ == "__main__":
    app = PageControl()
    app.mainloop()
Community
  • 1
  • 1
MrPadlog
  • 115
  • 1
  • 8
  • Your question is very hard to understand. I recommend removing the "edit" and "re-edit" and "re-re-edit" parts. I don't care how many times you've edited it. Just restate the whole question rather than tacking on addendums. You can probably remove 3/4 of the words in the question and still get your point across. – Bryan Oakley Aug 17 '16 at 12:57

1 Answers1

2

Since I wasn't able to implement an Observer to watch widgets like the ttk.Combobox, I've decided to create a workaround. Here are the steps I took, in order to achieve a MVC architecture from Bryan Oakleys example (link is in the question), which refreshes its model class via the controller class, whenever a user takes an action in the view (GUI).

Step 1: Add a model class

First, in order to use a MVC architecture, we have to seperate the code into model, view and control. In this example, model is class Model:, control is class PageControl(tk.Tk): and view are the pages class StartPage(tk.Frame), PageOne(tk.Frame) and PageTwo(tk.Frame).

Step 2: Set up your model class

Now we have to decide on which variables we want to have in the model class. In this example, we have directories and keys (status of the comboboxes), which we want to save in dictionaries. After setting them up empty, all we have to do is add setters and getters for each variable, so we can refresh data in model and also retrieve some, if we want. Additionally, we could implement delet methods for each variable, if we wanted to.

Step 3: Add push and pull methods to the control class

Now that there is a model class, we can refrence it via e. g. self.model = Model() in PageControl(tk.Tk) (control). Now we have the basic tools to set data in Model via e. g. self.model.set_keys(self.shared_keys) and also get data from Model. Since we want our control class to do that, we need some methods, that can achieve this. So we add the push and pull methods to the PageControl (e. g. def push_key(self)), which in turn can be refrenced from view (StartPage, PageOne, PageTwo) via controller.

Step 4: Add your widgets to the view class

Now we have to decide on which widgets shall be on which page and what you want them to do. In this example, there are buttons for navigation, which for the sake of the task can be ignored, two comboboxes and a button, which opens a file dialog.

Here, we want the comboboxes to refresh their status whenever it is changed and send the new status via controller to the model. Whereas the Open button of PageOne shall open a file dialog, where the user then selects files he/she wants to open. The directories we got from this interaction then shall be send via controller to model.

Step 5: Get all your functionality into the controller class

Since there is a controller variable, we can use it to refrence methods, which are in the controller class. This way, we can outsource all our methods from the pages into the controller and reference them via self.controller.function_of_controller_class. But we have to be aware, that methods, which are bound to commands via lambda: can't return any values, but they are also not called on programme startup. So keep that in mind.

Step 6: Set up your bindings and wrappers

Here we have to set up the .bind() for our comboboxes. Since the controller allready is set up to store data and the comboboxes have a textvariable, we can use this to gather information about the status of the comboboxes via combobox.bind(<<ComboboxSelect>>). All we have to do is to set up a wrapper which is called, whenever combobox.bind(<<ComboboxSelect>>) is throwing an event.

Closing statement

Now we have it, a programme based on Bryan Oakleys example of "How to get variable data from a class", which utilises a model, which is updated via controller whenever the user takes a corresponding action in the view. Unfortunately it doesn't utilise a Observer class, as first intended, but I'll keep working on it and update this, when I've found a satisfying solution.

MrPadlog
  • 115
  • 1
  • 8