1

My app consists of this one big frame (cp2) that contains two smaller frames: the top one contains the buttons that allow the user to switch between the different screens of the app and the bottom one contains the screens themselves (also frames). I initialize all screens at the beginning and then switch between them using their lift method.

The thing is, if the user clicks on a button to go to a different screen, unsaved changes made at the current one will be lost. I want to show a warning with a confirmation message, but I only see two ways of triggering the warning and I can't seem to figure out how to implement either.

Option 1 would be to add the warning to the exibirTela method: if the user clicks on a button that calls this method, the method will compare the values on the screen with the database before lifting another frame. Problem is, the method knows which frame is about to be lifted (t), but it doesn't know which frame is on top right now (the screen the user is about to leave).

Option 2 would be to issue the warning on a separate method, triggered by the event of the frame losing its place on top. The problem with this option is that no such event seems to exist for me to bind a method to it.

Either one works for me. Unless, of course, there's a reason I shouldn't use them, in which case, feel free to let me know. And, of course, any other suggestions are welcome, too.

The code is below. Sorry for the names in Portuguese. I added a few comments in English: hope this helps.

from tkinter import *
from tkinter.messagebox import *
import sqlite3

class cp2(Frame):
    def __init__(self, *args, **kwargs):
        Frame.__init__(self, *args, **kwargs)
        root.title(string="Controle de Produtos e Pedidos")
        self.columnconfigure(0, weight=1)
        self.rowconfigure(1, weight=1)

# Here I create two frames: one for the buttons, one for the screens

        barraOpcoes = Frame(self, bd=10, bg="yellow")
        barraOpcoes.grid(row=0, column=0, sticky=EW)
        barraOpcoes.columnconfigure(2, weight=1)
        areaPrincipal = Frame(self, bd=10)
        areaPrincipal.grid(row=1, column=0, sticky=NSEW)
        areaPrincipal.columnconfigure(0, weight=1)

# Here I create a set (telasAplicacao) to which I'll add all the screens (frames)

        self.telasAplicacao = {}

        for tela in (telaInicial,
                     telaCrudFabricante, telaCrudRevista,
                     telaInclusaoFabricante, telaInclusaoRevista,
                     telaAlteracaoFabricante, telaAlteracaoRevista):
            novaTela = tela(areaPrincipal, self)
            self.telasAplicacao[tela] = novaTela
            novaTela.grid(row=0, column=0, sticky=NSEW)

# Here I add the buttons that switch between frames

        btInicio = Button(barraOpcoes, text="Início", command=self.exibirTelaInicial)
        btInicio.grid(row=0, column=0, sticky=W)
        btFabricantes = Button(barraOpcoes, text="Fabricantes", command=self.exibirTelaCrudFabricante)
        btFabricantes.grid(row=0, column=1, sticky=W)
        btRevistas = Button(barraOpcoes, text="Revistas", command=self.exibirTelaCrudRevista)
        btRevistas.grid(row=0, column=2, sticky=W)
        btSair = Button(barraOpcoes, text="SAIR", command=lambda: sairCP2(self))
        btSair.grid(row=0, column=3, sticky=E)

# Always start with telaInicial on top: this is the welcome screen

        self.exibirTelaInicial()

    def exibirTela(self, t):
        minhaTela = self.telasAplicacao[t]
        minhaTela.exibirDados(codigo=kwargs["codigo"])
        minhaTela.lift()

    def exibirTelaInicial(self):
        self.exibirTela(telaInicial)

    def exibirTelaCrudFabricante(self):
        self.exibirTela(telaCrudFabricante)

    def exibirTelaCrudRevista(self):
        self.exibirTela(telaCrudRevista)

class telaAlteracaoFabricante(Frame):
    def __init__(self, parent, controller):
# lots of widgets on this frame
# also, all the other methods of this class

# and so on to all the other frames in telasAplicacao...
  • By-pass all exit condition for asking user ! Here methods : http://stackoverflow.com/questions/3295270/overriding-tkinter-x-button-control-the-button-that-close-the-window – dsgdfg Aug 30 '15 at 10:41
  • 1
    Thanks for the reply, but my problem isn't the "X" button: I already have this one covered with the WM_DELETE_WINDOW protocol, as suggested on the link you provided. The event I need to keep track is the lifting/lowering of frames: when the user switches from one frame to the other through the exibirTela method, I need to check if changes were made on the current frame before I lift the next. – Raquel Peres da Silva Aug 30 '15 at 23:08
  • Why do you say that all the data on a page will be lost when switching pages? If you don't destroy the widgets, the data should still be there. I don't see anything in your code that would destroy the data. – Bryan Oakley Aug 31 '15 at 14:35
  • Yes, technically the data will still be there, but if the user ever tries to return to that page to click on the "Save" button, the exibirDados method will replace it with the values currently in the database, so any unsaved changes will be lost when the frame is lifted. – Raquel Peres da Silva Sep 03 '15 at 02:57

1 Answers1

1

I think I found a partial solution for your option one. There's a method called winfo_children, and according to the effbot.org documentation it:

Returns a list containing widget instances for all children of this widget. The windows are returned in stacking order from bottom to top.

Now, I have created an example that shows how this mechanism works. The example is based on the "famous" Bryan Oakley's program on how to switch between frames. I have added a Text widget which displays the stacking order of self.container. To do this I have used the winfo_children method mentioned above, which returns the widgets of self.container in stacking order. The top of the stack (the visible widget) is the last element of this list (as mentioned above), but I have used reversed function to show it as the first element.

import tkinter as tk
from tkinter import ttk


LARGE_FONT= ("Verdana", 12)


class MainWindowController(tk.Tk):

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

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

        self.frames = {}

        for F in (StartPage, PageOne, PageTwo):
            frame = F(self.container, self)
            self.frames[F] = frame
            frame.grid(row=0, column=0, sticky="nsew")

        self.so = tk.Text(self.container, state="disabled", font=("Arial", 14),
                          width=70, height=12, bd=1, relief="groove")
        self.so.grid(row=1, column=0, sticky="nsew", pady=(60, 0))
        self.update_stack_order()

        self.show_frame(StartPage)

    def update_stack_order(self):
        self.so.config(state="normal")
        self.so.delete("1.0", "end")
        self.so.insert("end", "Stack order of the widgets of self.container (0 means top)\n\n\n")
        for i, child in enumerate(reversed(self.container.winfo_children())):
            self.so.insert("end", str(i) + "\t" + repr(child) + "\n\n")
        self.so.config(state="disabled")

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


class StartPage(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)

        label = ttk.Label(self, text="START PAGE", font=LARGE_FONT)
        label.pack(pady=10,padx=10)

        button = ttk.Button(self, text="Visit Page 1",
                            command=lambda: controller.show_frame(PageOne))
        button.pack()

        button2 = ttk.Button(self, text="Visit Page 2",
                            command=lambda: controller.show_frame(PageTwo))
        button2.pack()


class PageOne(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)

        label = ttk.Label(self, text="PAGE ONE", font=LARGE_FONT)
        label.pack(pady=10,padx=10)

        button1 = ttk.Button(self, text="Back to Home",
                            command=lambda: controller.show_frame(StartPage))
        button1.pack()

        button2 = ttk.Button(self, text="Page Two",
                            command=lambda: controller.show_frame(PageTwo))
        button2.pack()


class PageTwo(tk.Frame):

    def __init__(self, parent, controller):
        tk.Frame.__init__(self, parent)

        label = ttk.Label(self, text="PAGE TWO", font=LARGE_FONT)
        label.pack(pady=10,padx=10)

        button1 = ttk.Button(self, text="Back to Home",
                            command=lambda: controller.show_frame(StartPage))
        button1.pack()

        button2 = ttk.Button(self, text="Page One",
                            command=lambda: controller.show_frame(PageOne))
        button2.pack()


app = MainWindowController()
app.mainloop()

While you change frames, you will certainly notice that the stack order changes from the output of the Text widget. (Notice that the Text widget is never at the top). So, now you know which frame is at the top of the container, and you can proceed with the ideas of your first option.

If you want to know more about how "lifts" and "lowers" work in tkinter, check out the Tk's official documentation, which contains a section dedicated to this topic:

http://wiki.tcl.tk/12752

The code is not in Python, since Tk is a Tcl library, but the concepts are applied to tkinter as well.

Community
  • 1
  • 1
nbro
  • 15,395
  • 32
  • 113
  • 196
  • 1
    Thank you! That was all I needed: now that I know which frame is about to lose its "current screen" status, I can check if there are unsaved changes and warn the user. – Raquel Peres da Silva Sep 03 '15 at 02:51