【问题标题】:Tkinter button widget buggingTkinter 按钮小部件窃听
【发布时间】:2021-09-08 21:20:37
【问题描述】:

简短描述:我创建了主窗口,我正在输入 TopLevel 窗口,其中按钮是使用数据库中的数据循环创建的,假设创建了 4 个按钮,名称为 1、2、3 和 4。

因此,当您单击其中一个按钮时,会出现某种计数器,例如 0/4,当您按下按钮“Plati odabrano”时,它将添加 1/4 和 on.. 在达到 4/4 后按钮被删除并且工作正常,但是当我退出 TopLevel 窗口并从主窗口再次进入时,我们删除的那个按钮旁边的按钮占据了已删除的按钮位置并且也停留在它的最后一个位置,所以我仍然有 4 个按钮但它们现在被命名为 1,2,4,&4。

所以我用更简单的版本重新创建了代码,它没有那个错误就可以正常工作,但是我无法在我的主代码中找到错误来修复它。在这里我将粘贴这两个代码,如果有人可以在他们的解释器中对其进行测试并检查错误,我将不胜感激。

这是我的主要代码:

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()

这是我重新创建的代码,所以你们可以理解我想要做什么:

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()

这里是bug的图片:

第一张图片是删除前的样子

第二张图是按钮被删除但仍未离开并进入 TopLevel 时窗口的样子

最后一张图是窗口的样子,你可以看到关于按钮的部分

很抱歉这篇长文和大量信息,但我只想解释一切:)

【问题讨论】:

  • 我们需要一个确实重现问题的较短版本。
  • @martineau 上次你留下同样的评论时,我看不出这有什么意义,因为我不能让它比这更短......它实际上是最少量的代码,所以应用程序可以运行为它应该而且我无法重新创建代码,但我正在寻找我目前拥有的代码中的错误
  • 我留下了两次类似的消息,因为您似乎第一次忽略了它。显然,这两个代码 sn-ps 之间存在差异,这就是问题所在。也许这会帮助你缩小范围:How to debug small programs
  • 请尝试将代码缩减为minimal reproducible example。例如,您应该能够删除所有数据库代码并使用硬编码的数据列表。
  • @BryanOakley 好吧,我将它与数据库一起发布,以便有人可以检查我正在尝试调试的确切代码。如果我对数据列表进行硬编码,它将不再是同一个程序,您可以看到我发布了另一个代码,其中包含正在工作的硬编码数据列表,所以问题可能在数据库中的某个地方,但我不能找到它。

标签: python tkinter tkinter-button


【解决方案1】:

每当执行otplata_na_rate_deiconify() 时,您都会创建一组新的按钮,并将新的一组按钮放在最后一组按钮的相同位置。

因此,例如,otplata_na_rate_deiconify() 第一次运行时,在第 0 到第 3 列创建了 4 个按钮(1、2、3、4)。然后从窗口和数据库中销毁第 2 列的按钮 3,然后关闭(实际上隐藏)窗口。此时,窗口中仍然存在按钮 1、2 和 4。

当您再次调用otplata_na_rate_deiconify() 打开窗口时,您会根据数据库中的数据创建一组新的按钮(1、2、4),并将这些按钮放在第 0 到第 2 列(同一行)。也就是说,新按钮 1 在第 0 列与旧按钮 1 重叠,新按钮 2 在第 1 列覆盖旧按钮 2,新按钮 4 放在第 2 列(已删除按钮 3 所在的位置),旧按钮 4 保留在第 3 列. 这就是为什么你会看到 4 个按钮(1、2、4、4)。

所以当otplata_na_rate_deiconify()被执行时,要么删除旧按钮并创建新的按钮集,要么如果已经创建了按钮则什么都不做。


请注意,您的第二个示例代码实际上无法重现该问题。


更新:修改 OP 示例代码以模拟问题:

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()

取消注释top() 中的块注释将解决此问题。

【讨论】:

  • 谢谢伙计,当执行 otplata_na_rate_deiconify() 时,我会尝试/除了按钮,如果可行的话会接受你的回答。干杯
  • 如果你有时间,你能解释一下示例代码和主代码的区别吗?为什么我无法在示例代码中引发该错误?编辑:知道了,因为我没有重新创建按钮,它们仅在程序启动后创建,对吗?
  • @Freezy 您的示例代码创建这些按钮一次,而不是在 top() 内(在主代码中类似于 otplata_na_rate_deiconify())。
  • @Freezy 添加了修改后的示例代码来模拟问题(在top() 中创建按钮)。取消注释top() 中的块注释将解决此问题。
猜你喜欢
  • 2022-06-18
  • 2013-10-21
  • 1970-01-01
  • 1970-01-01
  • 2016-03-24
  • 1970-01-01
  • 2019-01-04
  • 2013-05-14
  • 1970-01-01
相关资源
最近更新 更多