I am working on a program that needs a GUI with buttons to do certain things, as is usually the case when having questions about Buttons, but I have ran into difficulties because while you can activate functions with buttons, you cannot test wether they are currently being pressed with an if statement. I know how to use check buttons and radio buttons, but I have not found anything else remotely useful. I need to be able to tell how long they are pressed, and to do things as they are being pressed that stop when they are released. I need a way of assigning a variable that will be true while you are still holding click down over the button, and false any other time, with a normal button, not one that toggles each time you press.
It's not clear to me what you're having trouble with, so I took the liberty of coding up a little GUI that times how long a button is pressed.
import tkinter as tk
import time
class ButtonTimer:
def __init__(self, root):
self.master = root
self.button = tk.Button(self.master, text="press me") # Notice I haven't assigned the button a command - we're going to bind mouse events instead of using the built in command callback.
self.button.bind('<ButtonPress>', self.press) # call 'press' method when the button is pressed
self.button.bind('<ButtonRelease>', self.release) # call 'release' method when the button is released
self.label = tk.Label(self.master)
self.startTime = time.time()
self.endTime = self.startTime
self.button.grid(row=1, column=1)
self.label.grid(row=2, column=1)
def press(self, *args):
self.startTime = time.time()
def release(self, *args):
self.endTime = time.time()
self.label.config(text="Time pressed: "+str(round(self.endTime - self.startTime, 2))+" seconds")
root = tk.Tk()
b = ButtonTimer(root)
root.mainloop()
Note: I tested this in python 2.7 then changed the import from Tkinter to tkinter. It will probably work in 3.x, but I haven't tested it with that version.
Related
I have a problem similar to this post: Exit program within a tkinter class
My variation on the problem involves the wait_variable being used on a button to control "stepping forward" in an app, but also allowing the app to close cleanly.
See my code below:
# To see output unbuffered:
# python -u delete_win_test.py
import tkinter as tk
from tkinter import *
class GUI(Tk):
def __init__(self):
super().__init__()
# Close the app when the window's X is pressed
self.protocol("WM_DELETE_WINDOW", self.closing)
# When this var is set to 1, the move function can continue
self.var = tk.IntVar()
# Close the app if the button is pressed
button = tk.Button(self, text="Exit",
command=self.destroy)
button.place(relx=.5, rely=.5, anchor="c")
# Step forward
self.step_button = tk.Button(self, text="Step",
command=lambda: self.var.set(1))
self.step_button.place(relx=.5, rely=.75, anchor="c")
def move(self):
print("doing stuff") # simulates stuff being done
self.step_button.wait_variable(self.var)
self.after(0, self.move)
def closing(self):
self.destroy()
app = GUI()
app.move()
app.mainloop()
The window shows correctly
"Stepping forward" works because "doing stuff" prints to terminal on button click
Exiting the window by both pressing X or using the "exit" button both work
The problem: the Python app never exits from the terminal and requires a closing of the terminal.
How can I make the Python program exit cleanly so the user does not need to close and re-open a new terminal window?
Related references for animation, etc:
Animation using self.after: moving circle using tkinter
Button wait: Making Tkinter wait untill button is pressed
The original "exit" code: Exit program within a tkinter class
UPDATE (the solution):
(Credit to both response answers below)
# Close the app if the button is pressed
button = tk.Button(self, text="Exit",
- command=self.destroy)
+ command=self.closing)
button.place(relx=.5, rely=.5, anchor="c")
# Step forward
...
def closing(self):
self.destroy()
+ self.var.set("")
+ exit(0)
This allows the native window's "X" to close the window and the Tk button to close the window while still closing the Python app cleanly in the terminal.
Your closing function needs to set the variable to cause the app to stop waiting.
def closing(self):
self.destroy()
self.var.set("")
in the closing function, you need to call exit to exit the program.
def closing(self):
self.destroy() #closes tkinkter window
exit(0) #exits program
I have multiple buttons in tkinter window, but on a computer we only have one mouse pointer at one location at a time, how can I multi-press buttons simultaneously just like on a touch screen?
Here is a sample code of what I am trying to do,
#!/usr/bin/python
import Tkinter as Tk
def button_pressed_cb(event):
print("You pressed " + event.widget['text'])
def button_release_cb(event):
print("You released " + event.widget['text'])
root = Tk.Tk()
button1 = Tk.Button(root, text="button1")
button1.pack()
button2 = Tk.Button(root, text="button2")
button2.pack()
button1.bind("<ButtonPress>", button_pressed_cb)
button1.bind("<ButtonRelease>", button_release_cb)
button2.bind("<ButtonPress>", button_pressed_cb)
button2.bind("<ButtonRelease>", button_release_cb)
root.mainloop()
After executing this, I get a sample output of something like this,
You pressed button1
You released button1
You pressed button2
You released button2
What I want to achieve is to be able to generate events which occur in this sequence,
You pressed button1
You pressed button2
You released button2
You released button1
Does anybody know what's the best way to achieve this? Thank you so much.
If you just want to call the callbacks, you can do that—they're ordinary Python functions, after all.
If you're asking how to create an event and dispatch it, you can use the event_generate method. Unfortunately, I don't know of any tkinter documentation for this, so you have to look at the Tcl/Tk docs instead. But the basic idea is:
def hit_the_buttons():
button1.event_generate('<ButtonPress>', when='tail')
button2.event_generate('<ButtonPress>', when='tail')
button2.event_generate('<ButtonRelease>', when='tail')
button1.event_generate('<ButtonRelease>', when='tail')
The when argument puts the events at the end of the event queue rather than processing them immediately, so they won't interfere with things like redraws or actual mouse events. It isn't really necessary here; I included it mainly to show how to map one of those Tcl arguments (-when tail) to tkinter.
If you want this to go through the normal dispatch instead of directly to the buttons, you can call root.event_generate and pass x and y arguments that will hit-test as inside the buttons.
Ok, I found a solution. I put in extra checks that if user presses and holds ctrl before clicking a button, that button will latch on to that pressed state. Then user can go and click other button etc. User can click that button again to unlatch it.
I also added a hover message as well for user to tell the user they can use ctrl to latch a button and achieve multikey presses.
Here is the new code,
#!/usr/bin/python
import Tkinter as Tk
class HoverInfo(Tk.Menu):
def __init__(self, parent, text):
Tk.Menu.__init__(self, parent, tearoff=0,
background='light goldenrod yellow')
self.__displayed = False
for line in text.split('\n'):
self.add_command(label=line)
parent.bind("<Enter>", self.__display)
parent.bind("<Leave>", self.__remove)
def __display(self, event):
if event.widget['state'] == Tk.NORMAL:
if not self.__displayed:
self.__displayed = True
self.post(event.x_root+5, event.y_root+5)
def __remove(self, event):
if self.__displayed:
self.__displayed = False
self.unpost()
CONTROL = 4
def button_pressed_cb(event):
if event.widget['state'] != Tk.DISABLED:
print("You pressed " + event.widget['text'])
if (event.state & CONTROL) == CONTROL:
event.widget.config(state=Tk.DISABLED)
event.widget.config(relief=Tk.SUNKEN)
def button_release_cb(event):
if (event.state & CONTROL) != CONTROL:
print("You released " + event.widget['text'])
event.widget.config(state=Tk.NORMAL)
event.widget.config(relief=Tk.RAISED)
root = Tk.Tk()
button1 = Tk.Button(root, text="button1")
button1.pack()
button2 = Tk.Button(root, text="button2")
button2.pack()
button1.bind("<ButtonPress>", button_pressed_cb)
button1.bind("<ButtonRelease>", button_release_cb)
button2.bind("<ButtonPress>", button_pressed_cb)
button2.bind("<ButtonRelease>", button_release_cb)
HoverInfo(button1, "Hint:\nYou can hold Ctrl key before\nclicking a button to latch it.")
root.mainloop()
Now, this is how the hover message looks,
https://ibb.co/dEb6Bn
And when user holds ctrl key and clicks button 1 and then button 2, then this is how it looks,
https://ibb.co/eVzRBn
Now, I can easily generate this sequence of events,
You pressed button1
You pressed button2
You released button2
You released button1
The idea was show in label where my cursor(line.column) is. Which work with .index(INSERT) well but if i bind the right mouse button with text it returns the previous cursor position not the current.
It seems that callback is executed after event?
from tkinter import Tk, Text, Frame, Label, StringVar, constants, END, INSERT
EXPL_TEXT = "I know that dress is karma. Perfume regret\nYou got me thinking bout"
class App(Frame):
def __init__(self,master):
Frame.__init__(self,master)
self.pack()
self.var = StringVar()
self.init_widgets()
def init_widgets(self):
self.text = Text(self)
self.text.bind('<Button-1>',self.callback_index)
self.text.pack()
self.text.insert(END,EXPL_TEXT)
self.label = Label(self, textvariable=self.var)
self.label.pack()
def callback_index(self,event):
x = self.text.index(INSERT)
self.var.set(x)
if __name__ == '__main__':
root = Tk()
app = App(root)
root.mainloop()
The issue I believe you are seeing is that when you click your mouse down the event fires to read INSERT. The problem is before you lift your mouse the location will still have the value of the previous INSERT. So in order for you to get the update after the event has completed we can use after() to wait for the event to finish and then set the value for self.var.
Change your callback_index method to:
def callback_index(self,event):
root.after(0, lambda: self.var.set(self.text.index(INSERT)))
What we are doing is telling python to schedule something to happen after a set time. I believe (Correct me if I am wrong) Because an event is in progress it waits until that event finishes to perform the action in the after() method.
We use lambda to create an anonymous function to update your self.var variable and all should work as intended.
Ok, so this is from a larger project that I am working on, so I apologize if it looks messy.
The issue is that when I click the 'Exit Program' Button on the GUI, the window remains active.
I know that the button is working as when I hit the 'x' on the top right corner the window; the program closes, so the run variable has been set back to 0, which stops the code from looping.
My question is how do I get the window to be closed automatically when the exit button is clicked, because the root.destroy() method isn't doing it.
#imports
from tkinter import *
import random, pickle, shelve
#global vars
run = 0
class Window(Frame):
#the class that manages the UI window
def __init__(self, master, screen_type = 0):
"""Initilize the frame"""
super(Window, self).__init__(master)
self.grid()
if screen_type == 1:
self.log_in_screen()
def log_in_screen(self):
#Program Exit Button
self.exit = Button(self, text = " Exit Program ", command = self.end)
self.exit.grid(row = 3, column = 0, columnspan = 2, sticky = W)
def end(self):
global run, root
run = 0
root.destroy()
#Main Loop
def main():
global run, root
run = 1
while run != 0:
root = Tk()
root.title("Budget Manager - 0.6.1")
root.geometry("400x120")
screen = Window(root, screen_type = run)
root.mainloop()
store = shelve.open("store.dat", "c")
main()
store.close()
My question is how do I get the window to be closed automatically when
the exit button is clicked, because the root.destroy() method isn't
doing it.
The answer is: call destroy() on the root window. You say it isn't working, but the code you posted seems to work, and what destroy() is documented to do is exactly what you describe you want to have happen: it will destroy the window. Your code creates new toplevel windows in a loop, so maybe it only appears to not work since the old window id destroyed and the new window is created in the blink of an eye.
It seems like what you're really asking is "how can I make clicking on the "x" do the same as clicking on the "Exit program" button?". If that is the case, the answer is very straight-forward, even with your unconventional code that creates root windows in a loop.
To get the "x" button on the window frame to call a function instead of destroying the window, use the wm_protocol method with the "WM_DELETE_WINDOW" constant and the function you want it to call.
For example:
while run != 0:
root = Tk()
...
screen = Window(root, screen_type = run)
root.wm_protocol("WM_DELETE_WINDOW", screen.end)
...
root.mainloop()
you could do something like the below. I've used it in my own projects etcand it works.
Mywin =tkinter.Tk()
def exit():
Mywin.quit()
# etc.
I'm using the slider to update my visualization, but the command updateValue is sent everytime I move the slider thumb, even for intermediate values.
Instead I want to trigger it only when I release the mouse button and the interaction is complete.
self.slider = tk.Scale(self.leftFrame, from_=0, to=256, orient=tk.HORIZONTAL, command=updateValue)
How can I trigger the function only once, when the interaction is ended ?
This is quite an ancient question now, but in case anyone stumbles upon this particular problem just use the bind() function and the "ButtonRelease-1" event like so:
import Tkinter as tk
class App:
def __init__(self):
self.root = tk.Tk()
self.slider = tk.Scale(self.root, from_=0, to=256,
orient="horizontal")
self.slider.bind("<ButtonRelease-1>", self.updateValue)
self.slider.pack()
self.root.mainloop()
def updateValue(self, event):
print self.slider.get()
app=App()
Hope this helps anyone!
You can't.
What you can do instead is have your command delay any real work for a short period of time using 'after'. Each time your command is called, cancel any pending work and reschedule the work. Depending on what your actual requirements are, a half second delay might be sufficient.
Another choice is to not use the built-in command feature and instead use custom bindings. This can be a lot of work to get exactly right, but if you really need fine grained control you can do it. Don't forget that one can interact with the widget using the keyboard in addition to the mouse.
Here's a short example showing how to schedule the work to be done in half a second:
import Tkinter as tk
#create window & frames
class App:
def __init__(self):
self.root = tk.Tk()
self._job = None
self.slider = tk.Scale(self.root, from_=0, to=256,
orient="horizontal",
command=self.updateValue)
self.slider.pack()
self.root.mainloop()
def updateValue(self, event):
if self._job:
self.root.after_cancel(self._job)
self._job = self.root.after(500, self._do_something)
def _do_something(self):
self._job = None
print "new value:", self.slider.get()
app=App()