1

I'm trying to do some real-time updating of my GUI, but the variables seem to update AFTER the event happens, meaning it updates the previous button press. I know it has something to do with the wait_variable() function but the documentation is hazy, and the other post about it here didn't seem to help me. Here's the relevant code:

EDIT: Working example

from tkinter import *
import json
import os

class GUI:
    def __init__(self,master):
        self.master = master
        self.frame = Frame(master)
        master.title("Catalogue")
        master.geometry("500x300")
        self.categories = ["Top","Bottom","Dress","Outerwear"]
        self.setup_filters()

    def setup_filters(self):
        self.filter_categs_vars = []
        self.filter_checks = []
        for i in range(len(self.categories)):
            self.filter_categs_vars.append(IntVar())
            self.filter_checks.append(Checkbutton(root,variable=self.filter_categs_vars[i],text=self.categories[i]))
            self.filter_checks[i].grid(row=7+i,column=0,sticky=W)
            self.filter_checks[i].select()
            self.filter_checks[i].bind("<ButtonRelease-1>", self.filter_categ)

    def filter_categ(self, event):
        for i in range(len(self.filter_categs_vars)):
            #self.filter_checks[i].wait_variable(self.filter_categs_vars[i])
            print(self.filter_categs_vars[i].get()) #Debug print

#START PROGRAM
global catalogue 
root = Tk()
GUI(root)
root.mainloop()
davide m.
  • 452
  • 4
  • 19
Elle
  • 21
  • 1
  • 3
  • 1
    Please provide a [mcve] as opposed to the relevant code. – Nae Jan 22 '18 at 00:22
  • All right, thank you! Done. – Elle Jan 22 '18 at 00:35
  • Thanks for the update, but I think you can still provide a better example that fits [mcve]. – Nae Jan 22 '18 at 02:01
  • 1
    I'll keep that in mind for the future! Your first suggestion is exactly what I was looking for (and used previously in my code but forgot about!) Thanks for the help. – Elle Jan 22 '18 at 02:35

1 Answers1

2

Analysis

What I believe to be a Minimal, Complete, and Verifiable example for the above-mentioned issue:

import tkinter as tk


def callback(event):
    #checkbutton.wait_variable(var)
    checkbutton['text'] = var.get()

if __name__ == '__main__':
    root = tk.Tk()
    var = tk.BooleanVar()
    checkbutton = tk.Checkbutton(root, text="Check", variable=var,
                                        onvalue=True, offvalue=False)
    checkbutton.pack()
    checkbutton.bind("<ButtonRelease-1>", callback)

    root.mainloop()

This happens simply because the virtual events, for example, the one that sets a Checkbutton's value based on its state (checked / unchecked), are handled after events attached with bind are handled(See the very last MCVE provided).

For more info see: https://stackoverflow.com/a/3513906/7032856


For the code you've provided, it means that Checkbutton's value won't change until the bind event handle filter_categ method is finished. But filter_categ won't move on unless Checkbutton's value is changed.

Which makes your program to be stuck at a local event loop, waiting for a 'break', a 'break' comes only if the loop is completed. Feels like a paradox.


Review the following example:

import tkinter as tk
import random


def modify_var(event):
    var.set(random.choice((True, False)))

def callback(event):
    checkbutton.wait_variable(var)
    checkbutton['text'] = var.get()

if __name__ == '__main__':
    root = tk.Tk()
    var = tk.BooleanVar()
    checkbutton = tk.Checkbutton(root, text="Check", variable=var,
                                        onvalue=True, offvalue=False)
    checkbutton.pack(fill='both', expand=True)
    checkbutton.bind("<ButtonRelease-1>", callback)

    root.bind_all("<Escape>", modify_var)

    root.mainloop()

It has the same paradoxical behavior your code does, but with the only exception, when Escape is hit, the variable wait_variable waits, var, is modified, so the local event loop is broken.


Solutions

By using command option in Checkbutton

Replace:

self.filter_checks[i].bind("<ButtonRelease-1>", self.filter_categ)

with:

self.filter_checks[i]['command'] = self.filter_categ

This is by far the simplest. Also, you can overwrite your method definition to:

def filter_categ(self):
    ...

unless it would be used later on by other events.

Its MCVE:

# By using command option in Checkbutton MCVE
import tkinter as tk


def callback():
    checkbutton['text'] = var.get()

if __name__ == '__main__':
    root = tk.Tk()
    var = tk.BooleanVar()
    checkbutton = tk.Checkbutton(root, text="Check", variable=var,
                                        onvalue=True, offvalue=False)

    checkbutton['command'] = callback

    checkbutton.pack(fill='both', expand=True)
    root.mainloop()

By using Tkinter Variable Class and, trace_add

replace:

self.filter_checks[i].bind("<ButtonRelease-1>", self.filter_categ)

with:

self.filter_categs_vars[i].trace_add('write', self.filter_categ)

with the above line, trace_add will call its callback, self.filter_categ with 3 arguments, for which your method needs to accept those arguments as well, whenever the variable it is attached to, self.filter_categs_vars[i], gets modified. Replace:

def filter_categ(self, event):

with:

def filter_categ(self, *args):

Its MCVE:

# By using Tkinter Variable Class and, trace_add
import tkinter as tk


def callback(*args):
    checkbutton['text'] = var.get()

if __name__ == '__main__':
    root = tk.Tk()
    var = tk.BooleanVar()
    checkbutton = tk.Checkbutton(root, text="Check", variable=var,
                                        onvalue=True, offvalue=False)
    
    var.trace_add('write', callback)

    checkbutton.pack(fill='both', expand=True)
    root.mainloop()

By shifting the order of event handle sequence

self.filter_checks[i].bind("<ButtonRelease-1>", self.filter_categ) # this line is not modified
self.filter_checks[i].bindtags((self.filter_checks[i].bindtags()[1:] + self.filter_checks[i].bindtags()[:1]))

This makes it so that "<ButtonRelease-1>" event is handled the latest, as in Checkbutton's variable value will change before self.filter_categ is executed.

Its MCVE:

# By shifting the order of event handle sequence MCVE
import tkinter as tk


def callback(event):
    checkbutton['text'] = var.get()

if __name__ == '__main__':
    root = tk.Tk()
    var = tk.BooleanVar()
    checkbutton = tk.Checkbutton(root, text="Check", variable=var,
                                        onvalue=True, offvalue=False)
    checkbutton.pack(fill='both', expand=True)
    checkbutton.bind("<ButtonRelease-1>", callback)

    # comment the line below out to see the difference
    checkbutton.bindtags((checkbutton.bindtags()[1:] + checkbutton.bindtags()[:1]))

    root.mainloop()
Community
  • 1
  • 1
Nae
  • 14,209
  • 7
  • 52
  • 79
  • Thank you for the explanation! I tried binding the callback to the button press and the button release to the modify variable, but it doesn't seem to work reliably. Is there another way of automating this without requiring user input? – Elle Jan 22 '18 at 01:09