-1

Short description: I made home window where I'm entering TopLevel window where buttons are created in loop with data from database, let's say that 4 buttons are created and names 1,2,3,&4 for example.

So when you click on one of those buttons there will be some kind of counter like 0/4, there is button "Plati odabrano" when you press it will add 1/4 and on.. after it hit 4/4 button is deleted and it works fine, but when I exit TopLevel window and enter in again from home window, button that was beside that button we deleted took deleted button spot and stayed also on it's last spot, so I still got 4 buttons but they are now named 1,2,4,&4.

So I recreated code in simpler version and it works just fine without that bug, but I can't find bug in my main code to fix it. Here I will paste both codes and if someone can test it in their interpreter and check for bug I would be so grateful.

Here is my main code:

from tkinter import *
import sqlite3

conn = sqlite3.connect('Financije.db')
c = conn.cursor()

c.execute('''CREATE TABLE IF NOT EXISTS PRIMANJA
             ([ID] INTEGER PRIMARY KEY,[Opis] text, [Iznos] integer, [Datum] text)''')

c.execute('''CREATE TABLE IF NOT EXISTS OTPLATA_NA_RATE
             ([ID] INTEGER PRIMARY KEY,[Opis] text, [Iznos_rate] integer, [Broj_rata] text, [Datum_kupnje] text)''')

c.execute('''CREATE TABLE IF NOT EXISTS SKIDANJE_S_RAČUNA
             ([ID] INTEGER PRIMARY KEY,[Opis] text, [Iznos] integer, [Date] text)''')

c.execute('''CREATE TABLE IF NOT EXISTS KASICA
             ([ID] INTEGER PRIMARY KEY, [Stanje_računa] integer)''')

c.execute('SELECT * FROM KASICA')
check = c.fetchone()
if check is None:
    c.execute('INSERT INTO KASICA VALUES (?, ?);', (None, 0.0))
c.execute('SELECT * FROM OTPLATA_NA_RATE')
check = c.fetchone()
if check is None:
    c.execute('INSERT INTO OTPLATA_NA_RATE VALUES (?, ?, ?, ?, ?);', (None, "Test1", 600, "0/2", "12/08/2021"))
    c.execute('INSERT INTO OTPLATA_NA_RATE VALUES (?, ?, ?, ?, ?);', (None, "Test2", 600, "0/3", "12/08/2021"))
    c.execute('INSERT INTO OTPLATA_NA_RATE VALUES (?, ?, ?, ?, ?);', (None, "Test3", 600, "0/4", "12/08/2021"))
    c.execute('INSERT INTO OTPLATA_NA_RATE VALUES (?, ?, ?, ?, ?);', (None, "Test4", 600, "0/5", "12/08/2021"))
    c.execute('INSERT INTO OTPLATA_NA_RATE VALUES (?, ?, ?, ?, ?);', (None, "Test5", 600, "0/6", "12/08/2021"))
# Save (commit) the changes
conn.commit()


class FinancialCalc:
    def __init__(self, master):
        self.lista_opisa_rata = list()
        self.dict_for_buttons = dict()

        # Start screen define:
        self.master = master
        self.master.title("Test")
        self.master.geometry("300x300")
        self.master.resizable(False, False)

        self.otplata_na_rate_button = Button(master, text="payment in installments",
                                             command=self.otplata_na_rate_deiconify)
        self.otplata_na_rate_button.place(x=95, y=155)

        #   Otplata_na_rate TopLevel - definiranje
        self.onr_top = Toplevel()
        self.onr_top.withdraw()
        self.onr_top.protocol("WM_DELETE_WINDOW", lambda: (self.master.deiconify(), self.onr_top.withdraw()))
        self.onr_top.title("Test")
        self.onr_label = Label(self.onr_top, text="List of payments:", font=("Helvetica", 11, "underline", "bold"))
        self.onr_opis_label = Label(self.onr_top, text="Payment description:", font=("Helvetica", 11, "underline", "bold"))
        self.onr_oznacena_rata_label = Label(self.onr_top, text="Installment description:", font=("Helvetica", 11))
        self.onr_broj_rata_label = Label(self.onr_top, text="Installments:", font=("Helvetica", 11))
        self.onr_iznos_rate_label = Label(self.onr_top, text="Value:", font=("Helvetica", 11))
        self.onr_datum_isplate_label = Label(self.onr_top, text="Final payment:", font=("Helvetica", 11))

        self.onr_oznacena_rata_txt = Label(self.onr_top, text="", font=("Helvetica", 11))
        self.onr_broj_rata_txt = Label(self.onr_top, text="", font=("Helvetica", 11))
        self.onr_iznos_rate_txt = Label(self.onr_top, text="", font=("Helvetica", 11))
        self.onr_datum_isplate_txt = Label(self.onr_top, text="", font=("Helvetica", 11))

        self.onr_pl_pojed_btn = Button(self.onr_top, text="Pay selected", command=self.pay_selected)

    def pay_selected(self):
        p_description = self.onr_oznacena_rata_txt.cget("text")
        num_of_installments = self.onr_broj_rata_txt.cget("text").split("/")
        if int(num_of_installments[0])+1 >= int(num_of_installments[1]):
            c.execute('DELETE FROM OTPLATA_NA_RATE WHERE Opis = ?', (p_description,))
            conn.commit()
            btn_to_del = self.dict_for_buttons[p_description]
            btn_to_del.destroy()
            self.dict_for_buttons.pop(p_description)
            self.lista_opisa_rata.clear()

        else:
            target_3_novo = str(int(num_of_installments[0])+1) + "/" + num_of_installments[1]
            c.execute('UPDATE OTPLATA_NA_RATE SET Broj_rata = ? WHERE Opis = ?', (target_3_novo, p_description,))
            conn.commit()
            self.get_description(p_description)

    def get_description(self, opis):
        c.execute('SELECT * FROM OTPLATA_NA_RATE WHERE Opis=?;', (opis,))
        for x in c:
            dodaj_godinu = 0
            self.onr_oznacena_rata_txt.config(text=x[1])
            self.onr_iznos_rate_txt.config(text=str(x[2]) + " kn")
            self.onr_broj_rata_txt.config(text=x[3])
            datum_racunanje = int(x[4].split("/")[1]) + int(x[3].split("/")[1])
            while datum_racunanje > 12:
                datum_racunanje -= 12
                dodaj_godinu += 1
            datum_isplate = str(datum_racunanje) + "/" + str(int(x[4].split("/")[2])+dodaj_godinu)
            self.onr_datum_isplate_txt.config(text=datum_isplate)

    def otplata_na_rate_deiconify(self):
        # Otplata_na_rate TopLevel - define window:
        self.master.withdraw()
        self.onr_top.deiconify()
        self.onr_label.grid(row=0, columnspan=3, sticky="NESW")
        self.onr_opis_label.grid(row=0, column=5, sticky="NESW")
        self.onr_oznacena_rata_label.grid(row=1, column=4, padx=5, sticky="E")
        self.onr_broj_rata_label.grid(row=2, column=4, padx=5, sticky="E")
        self.onr_iznos_rate_label.grid(row=3, column=4, padx=5, sticky="E")
        self.onr_datum_isplate_label.grid(row=4, column=4, padx=5, sticky="E")

        self.onr_oznacena_rata_txt.grid(row=1, column=5, padx=5, sticky="NSWE")
        self.onr_broj_rata_txt.grid(row=2, column=5, padx=5, sticky="NSWE")
        self.onr_iznos_rate_txt.grid(row=3, column=5, padx=5, sticky="NSWE")
        self.onr_datum_isplate_txt.grid(row=4, column=5, padx=5, sticky="NSWE")

        c.execute('SELECT Opis FROM OTPLATA_NA_RATE')
        for opis in c:
            self.lista_opisa_rata.append(opis[0])
        column = 0
        row = 1
        for opis in self.lista_opisa_rata:
            # Append function to button
            function = lambda x=opis: self.get_description(x)
            rata_button = Button(self.onr_top, text=opis, width=10, command=function)
            rata_button.grid(row=row, column=column, padx=5, pady=5)
            self.dict_for_buttons[opis] = rata_button
            column += 1
            if column == 3:
                row += 1
                column = 0

        #   Define placement of Button widget - created with loop
        if row < 5:
            row = 5
        if row >= 5:
            row += 1

        self.onr_pl_pojed_btn.grid(row=row, column=5, padx=2, pady=5)

        x = self.master.winfo_x()
        y = self.master.winfo_y()
        self.onr_top.geometry(f"+{x}+{y}")


root = Tk()
my_gui = FinancialCalc(root)
root.mainloop()

and this is code that I recreated so you guys can understand what I'm trying to do:

from tkinter import *

root = Tk()

toplvl = Toplevel()
toplvl.withdraw()
toplvl.protocol("WM_DELETE_WINDOW", lambda: (root.deiconify(), toplvl.withdraw()))
button_id = dict()


def btn_destroy():
    picked_btn = str(lbl_picked_btn.cget("text").split(":")[1]).rstrip()
    if picked_btn != "":
        txt = int(lbl.cget("text").split("/")[0]) + 1
        lbl.config(text=str(txt) + "/3")
        if txt == 3:
            btn_to_del = button_id[int(picked_btn)]
            btn_to_del.destroy()
            lbl.config(text="0/3")


def btn_pick(x):
    lbl_picked_btn.config(text="Button picked:"+str(x))


lbl_desc = Label(toplvl, text="Pick button and click Destroy 3 times to destroy it", font=("Helvetica", 11))
lbl_desc.grid(row=0, columnspan=4)
lbl_picked_btn = Label(toplvl, text="Button picked:", font=("Helvetica", 11))
lbl_picked_btn.grid(row=1, column=1, columnspan=2)
btn_d = Button(toplvl, width=10, text="Destroy", command=btn_destroy)
btn_d.grid(row=1, column=0)
lbl = Label(toplvl, text="0/3", font=("Helvetica", 11))
lbl.grid(row=1, column=3)


row = 2
column = 0
for i in range(5):
    function = lambda x=i: btn_pick(x)
    btn = Button(toplvl, width=10, text=str(i), command=function)
    btn.grid(row=row, column=column)
    button_id[i] = btn
    column += 1


def top():
    root.withdraw()
    toplvl.deiconify()


start_btn = Button(root, text="Start", command=top)
start_btn.pack()


root.mainloop()

Here are pictures of bug:

1st pic is how it looks before deleteing First pic is how window looks before deleting button

Second pic is how window looks like when button is deleted but still didn't leave and enter TopLevel Second pic is how window looks like when button is deleted but still didn't leave and enter TopLevel

Last pic is how window looks like and you can see that part about button Last pic is how window looks like and you can see that part about button

Sorry for this long post and a lot of information, but I just want to explain everything :)

Freezy
  • 89
  • 1
  • 8
  • We need a shorter version that *does* reproduce the problem. – martineau Sep 08 '21 at 21:38
  • @martineau Last time you left same comment, I don't see point in that since I can't make it shorter than this... It's literally minimal amount of code so the app can run as it should and I can't recreate the code, but I'm looking for a bug in the code I currently have – Freezy Sep 08 '21 at 21:40
  • I left a similar messages twice because you seemed to have ignored it the first time. Obviously there's something in the difference between the two code snippets that's the case of the issue. Maybe this will help you narrow it down: [How to debug small programs](https://ericlippert.com/2014/03/05/how-to-debug-small-programs/) – martineau Sep 08 '21 at 21:46
  • Please try to reduce the code down to a [mcve]. For example, you should be able to remove all of the database code and use hard-coded lists of data. – Bryan Oakley Sep 08 '21 at 21:46
  • @BryanOakley Well I posted it with database so someone can check exact code that I'm trying to debug. If I hard-code list of data it won't be the same program anymore, you can see that I posted another code with hard-coded list of data that is working, so problem might be somewhere in database, but I can't find it. – Freezy Sep 08 '21 at 22:08
  • If you replace the call to a query with the exact data you are getting from the query, then for all intents and purposes the code is identical for the purpose of debugging this code. tkinter doesn't care where the data comes from. – Bryan Oakley Sep 08 '21 at 22:12
  • @bryanOakley And that won't reduce amount of lines in code, just to replace them with lists or whatever won't make any difference I guess – Freezy Sep 08 '21 at 22:16
  • I count at least 25 lines of code that could be removed in the first half of the code. – Bryan Oakley Sep 08 '21 at 22:18
  • Also, since the question seems to be about the buttons, you could probably remove most or all of the code related to the payment description. – Bryan Oakley Sep 08 '21 at 22:21
  • @BryanOakley Yeah you can remove all the Database stuff, you can just skip those first 20 lines of code, since problem is somewhere in last 3 functions... – Freezy Sep 08 '21 at 22:21
  • _I'm_ certainly not going to do that. I'm just trying to give advice that when you're asking other people for help, you'll have the most success if you make it as easy as possible to understand the code. As written, this problem seems very complicated. Not too many people are going to want to wade through 150 lines of code. – Bryan Oakley Sep 08 '21 at 22:27
  • Finally, the screenshots don't match the code. Your screenshots show three buttons but I see five when I run the code, and the code has the text translated to english. – Bryan Oakley Sep 08 '21 at 22:28
  • @BryanOakley I don't have time to argue about Labels or is it database that tkinter is using or list data, I made screenshots from my original program that is written on my language, and I made copy of it on English so people here can at least understand some variables. Problem is the same if there are 100 buttons, 4 buttons or 3 buttons, I said that is just example in pics how buttons are messed up when I re-enter that TopLevel window. Doesn't matter mate, I'm gonna just delete this post and figure it out tomorrow, or just rewrite those 3 functions from beginning. Anyway, thanks for help. – Freezy Sep 08 '21 at 22:44
  • I'm not trying to argue, I'm just trying to make you aware of ways to make your question easier to understand. – Bryan Oakley Sep 08 '21 at 23:21

1 Answers1

1

You create new set of buttons whenever otplata_na_rate_deiconify() is executed and put the new set of buttons on the same positions of the last set of buttons.

So, for example, at first run of otplata_na_rate_deiconify(), 4 buttons (1, 2, 3, 4) are created at column 0 to 3. Then you destroy button 3 at column 2 from both the window and database and close (actually hide) the window. At that moment, buttons 1, 2 and 4 still exist in the window.

When you call otplata_na_rate_deiconify() again to open the window, you create new set of buttons (1, 2, 4) based on data in database and put the buttons at column 0 to 2 (same row). That is new button 1 overlaps old button 1 at column 0, new button 2 overlays old button 2 at column 1, new button 4 is put at column 2 (where the deleted button 3 was put) and old button 4 is kept at column 3. That is why you see 4 buttons (1, 2, 4, 4).

So when otplata_na_rate_deiconify() is executed, either delete old buttons and create new set of buttons, or do nothing if there are buttons created already.


Note that your second example code cannot reproduce the issue actually.


Updated: modified OP sample code to simulate the issue:

from tkinter import *

root = Tk()

toplvl = Toplevel()
toplvl.withdraw()
toplvl.protocol("WM_DELETE_WINDOW", lambda: (root.deiconify(), toplvl.withdraw()))
button_id = dict()

def btn_destroy():
    picked_btn = str(lbl_picked_btn.cget("text").split(":")[1]).rstrip()
    if picked_btn != "":
        txt = int(lbl.cget("text").split("/")[0]) + 1
        lbl.config(text=str(txt) + "/3")
        if txt == 3:
            btn_to_del = button_id[int(picked_btn)]
            btn_to_del.destroy()
            lbl.config(text="0/3")
            # simulate deletion from database
            database.remove(int(picked_btn))

def btn_pick(x):
    lbl_picked_btn.config(text="Button picked:"+str(x))

lbl_desc = Label(toplvl, text="Pick button and click Destroy 3 times to destroy it", font=("Helvetica", 11))
lbl_desc.grid(row=0, columnspan=4)
lbl_picked_btn = Label(toplvl, text="Button picked:", font=("Helvetica", 11))
lbl_picked_btn.grid(row=1, column=1, columnspan=2)
btn_d = Button(toplvl, width=10, text="Destroy", command=btn_destroy)
btn_d.grid(row=1, column=0)
lbl = Label(toplvl, text="0/3", font=("Helvetica", 11))
lbl.grid(row=1, column=3)

# simulate database data
database = [1, 2, 3, 4, 5]

def top():
    #global button_id
    root.withdraw()
    toplvl.deiconify()

    """
    # this is the fix
    # remove old buttons
    for b in button_id.values():
        b.destroy()
    button_id.clear()
    """

    row = 2
    column = 0
    for i in database:
        function = lambda x=i: btn_pick(x)
        btn = Button(toplvl, width=10, text=str(i), command=function)
        btn.grid(row=row, column=column)
        button_id[i] = btn
        column += 1

start_btn = Button(root, text="Start", command=top)
start_btn.pack()

root.mainloop()

Uncomment the block comment inside top() will fix the issue.

acw1668
  • 40,144
  • 5
  • 22
  • 34
  • Thanks mate, I will make try/except for buttons when otplata_na_rate_deiconify() is executed, if that works gonna accept your answer. Cheers – Freezy Sep 09 '21 at 07:24
  • If you got time, can you explain me difference in example code and main code? Why I'm not able to induce that bug on example code? Edit: Got it, because I'm not creating buttons all over again, they are created only once program is launched, is that right? – Freezy Sep 09 '21 at 07:35
  • @Freezy Your sample code create those buttons once, not inside `top()` (acts like `otplata_na_rate_deiconify()` in main code). – acw1668 Sep 09 '21 at 07:59
  • @Freezy Added modified sample code to simulate the issue (create buttons inside `top()`). Uncomment the block comment inside `top()` will fix the issue. – acw1668 Sep 09 '21 at 08:09