Create popups continuously in a loop without blocking the code - python

My program streams data and I want to create a popup displaying some text whenever a condition is met. I tried to create a simple tkinter window and ctypes window, but both seem to block my code, preventing it from continuing until the window has been closed. How can I create simple popup window functionality in for example a loop?
What I have so far is something of this structure.
import tkinter as tk
for i in range(11):
if i%5 == 0: # Any condition
popup = tk.Tk()
label = ttk.Label(popup, text='hi', font=('Verdana', 12))
label.pack(side='top', padx=10, pady=10)
popup.mainloop()
and
import ctypes
for i in range(11):
if i%5 == 0: # Any condition
popup = ctypes.windll.user32.MessageBoxW
popup(None, 'hi', 'title', 0)
However, in both cases the loop will not proceed until I close the popup.

Case 1 - Tkinter:
You are using mainloop() which is no different than a true while loop. You can make it run continuously by removing it.
import tkinter as tk
from tkinter import ttk
for i in range(11):
if i%5 == 0: # Any condition
popup = tk.Tk()
label = ttk.Label(popup, text='hi', font=('Verdana', 12))
label.pack(side='top', padx=10, pady=10)
Case 2 - Ctypes:
To make it run continuously, you will have to use threading.
import ctypes, threading
for i in range(11):
if i%5 == 0: # Any condition
popup = ctypes.windll.user32.MessageBoxW
threading.Thread(target = lambda :popup(None, 'hi', 'title', 0)).start()

Not too familiar with ctypes, but for tkinter the UI will always be blocking your main code during the mainloop.
You can somewhat bypass it if you just instantiate your Tk() without invoking mainloop and use the after function:
import tkinter as tk
from tkinter.messagebox import showinfo
root = tk.Tk()
root.withdraw()
for i in range(10):
if i in (5, 8):
root.after(ms=1, func=lambda x=i: showinfo('Message!', f'Stopped at {x}'))
# ... do something here
The root.after queues the message box to be displayed after 1 millisecond (ms=1).
A better way might be to create a modeless message/dialog box in ctypes but as mentioned, I'm not too familiar and a quick search didn't yield any simple solution.

Related

How do I destroy a tkinter frame?

I am trying to make a tkinter frame that will contain an entry field and a submit button. When the submit button is pressed, I want to pass the entry string to the program and destroy the frame. After many experiments, I came up with this script:
from tkinter import *
from tkinter import ttk
import time
root = Tk()
entryframe = ttk.Frame(root)
entryframe.pack()
par = StringVar('')
entrypar = ttk.Entry(entryframe, textvariable=par)
entrypar.pack()
submit = ttk.Button(entryframe, text='Submit', command=entryframe.quit)
submit.pack()
entryframe.mainloop()
entryframe.destroy()
parval = par.get()
print(parval)
time.sleep(3)
root.mainloop()
When the "Submit" button is pressed, the parameter value is passed correctly to the script and printed. However, the entry frame is destroyed only after 3 seconds (set by the time.sleep function).
I want to destroy the entry frame immediately.
I have a slightly different version of the script in which the entry frame does get destroyed immediately (although the button itself is not destroyed), but the value of par is not printed:
from tkinter import *
from tkinter import ttk
import time
root = Tk()
entryframe = ttk.Frame(root)
entryframe.pack()
par = StringVar('')
entrypar = ttk.Entry(entryframe, textvariable=par)
entrypar.pack()
submit = ttk.Button(root, text='Submit', command=entryframe.destroy)
submit.pack()
entryframe.mainloop()
# entryframe.destroy()
parval = par.get()
print(parval)
time.sleep(3)
root.mainloop()
How can I get both actions, namely the entry frame destroyed immediately and the value of par printed?
Note 100% sure what you are trying to do but look at this code:
from tkinter import *
from tkinter import ttk
def print_results():
global user_input # If you want to access the user's input from outside the function
# Handle the user's input
user_input = entrypar.get()
print(user_input)
# Destroy whatever you want here:
entrypar.destroy()
submit.destroy()
# If you want you can also destroy the window: root.destroy()
# I will create a new `Label` with the user's input:
label = Label(root, text=user_input)
label.pack()
# Create a tkitner window
root = Tk()
# Create the entry
entrypar = ttk.Entry(root)
entrypar.pack()
# Create the button and tell tkinter to call `print_results` whenever
# the button is pressed
submit = ttk.Button(root, text="Submit", command=print_results)
submit.pack()
# Run tkinter's main loop
# It will stop only when all tkinter windows are closed
root.mainloop()
# Because of the `global user_input` now we can use:
print("Again, user_input =", user_input)
I defined a function which will destroy the entry and the button. It also creates a new label that displays the user's input.
I was able to accomplish what I wanted using the wait_window method. Here is the correct script:
from tkinter import *
from tkinter import ttk
root = Tk()
entryframe = ttk.Frame(root)
entryframe.pack()
entrypar = ttk.Entry(entryframe)
entrypar.pack()
submit = ttk.Button(entryframe, text='Submit', command=entryframe.destroy)
submit.pack()
entrypar.wait_window()
parval = entrypar.get()
print(parval)
close_button = ttk.Button(root, text='Close', command=root.destroy)
close_button.pack()
root.mainloop()
My intention was not fully apparent in my original question, and I apologize for that. Anyway, the answers did put me on the right track, and I am immensely thankful.

Change right click event of tkinter command

I want to detect the right click event on tkinter Menu command.
Consider code below.
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
menu_button = ttk.Menubutton(root, text="MENU")
menu_button.grid()
m = tk.Menu(menu_button, tearoff=False, activeborderwidth=0)
menu_button["menu"] = m # To avoid garbage collection
m.add_command(label="an option", command=lambda: print("option1"))
m.add_command(label="another option", command=lambda: print("option2"))
root.mainloop()
When I click an option or another option, the commands are called as expected. But want I want to do is catch right click event. Can anyone knows that how can I detect it?
use button.bind("<Button-3>", event). Consider this code:
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
button = tk.Button(root, text='right click this')
button.pack()
button.bind("<Button-3>", lambda e: print('You right clicked'))
root.mainloop()

Using Python, how do you call a tkinter GUI from another GUI?

I created a couple of GUIs using tkinter. But now I am interested in combining them into one caller GUI. So the caller GUI would have buttons that, when clicked, would open the other GUIs. However, I cannot get it to work. I've done the imports correctly (I think), edited the main functions in the subGUIs, and added the command=GUI.main in my buttons. I get it to load but I get errors about missing files...but when I run a GUI by itself it works fine.
In my research, I read that there can only be one mainloop in a Tkinter program. Basically, I cannot use a Tkinter GUI to call another Tkinter GUI. Do you know what I can do different, for instance, can I create the caller GUI using wxPython and have it call all other GUIs that use Tkinter?
Thank you!
You can't "call" another GUI. If this other GUI creates its own root window and calls mainloop(), your only reasonable option is to spawn a new process. That's a simple solution that requires little work. The two GUIs will be completely independent of each other.
If you have control over the code in both GUIs and you want them to work together, you can make the base class of your GUI a frame rather than a root window, and then you can create as many windows as you want with as many GUIs as you want.
For example, let's start with a simple GUI. Copy the following and put it in a file named GUI1.py:
import tkinter as tk
class GUI(tk.Frame):
def __init__(self, window):
tk.Frame.__init__(self)
label = tk.Label(self, text="Hello from %s" % __file__)
label.pack(padx=20, pady=20)
if __name__ == "__main__":
root = tk.Tk()
gui = GUI(root)
gui.pack(fill="both", expand=True)
tk.mainloop()
You can run that GUI normally with something like python GUI1.py.
Now, make an exact copy of that file and name it GUI2.py. You can also run it in the same manner: python GUI2.py
If you want to make a single program that has both, you can create a third file that looks like this:
import tkinter as tk
import GUI1
import GUI2
# the first gui owns the root window
win1 = tk.Tk()
gui1 = GUI1.GUI(win1)
gui1.pack(fill="both", expand=True)
# the second GUI is in a Toplevel
win2 = tk.Toplevel(win1)
gui2 = GUI2.GUI(win2)
gui2.pack(fill="both", expand=True)
tk.mainloop()
Depending on your OS and window manager, one window might be right on top of the other, so you might need to move it to see both.
Thank you for the ideas. At first, your code wouldn't print the text on the toplevel window. So I edited it a little and it worked! Thank you. GUI1 and GUI2 look like:
import tkinter as tk
def GUI1(Frame):
label = tk.Label(Frame, text="Hello from %s" % __file__)
label.pack(padx=20, pady=20)
return
if __name__ == "__main__":
root = tk.Tk()
GUI1(root)
root.mainloop()
And then the caller looks like this:
from tkinter import *
import GUI1
import GUI2
def call_GUI1():
win1 = Toplevel(root)
GUI1.GUI1(win1)
return
def call_GUI2():
win2 = Toplevel(root)
GUI2.GUI2(win2)
return
# the first gui owns the root window
if __name__ == "__main__":
root = Tk()
root.title('Caller GUI')
root.minsize(720, 600)
button_1 = Button(root, text='Call GUI1', width='20', height='20', command=call_GUI1)
button_1.pack()
button_2 = Button(root, text='Call GUI2', width='20', height='20', command=call_GUI2)
button_2.pack()
root.mainloop()

Tkinter threading and return to text widget

I've read some post on stack overflow,Issues intercepting subprocess output in real time, Redirect command line results to a tkinter GUI, i know i have to use threading and queue in tkinter, but I am still can't do the same thing because I am a beginner in program,please help.
The goal: When press a button, getting the 'top' command output and realtime display in tkinter text widget
The issue: I've tried to follow the code, but still cannot get the output, but I have not idea how to make it work.
from tkinter import *
import tkinter as tk
import subprocess
from threading import Thread
from queue import Queue
window = tk.Tk()
window.title('realtime')
window.geometry('800x400')
text = tk.Text(window)
text.pack()
button = tk.Button(window, text= 'Press')
button.pack()
window.mainloop()
This is only the gui outlook, please help
top refreshes itself now and then and I'm guessing that's the behavior you want to capture with threading and whatnot. However in this case it would be much easier to ask top to only run once, and have tkinter do the timing and refreshing:
import tkinter as tk
from sh import top
def update_text():
text.delete(0.0, tk.END)
text.insert(0.0, top('-b', n=1))
window.after(1000, update_text) # call this function again in 1 second
window = tk.Tk()
window.title('realtime')
window.geometry('800x400')
text = tk.Text(window)
text.pack()
button = tk.Button(window, text= 'Press', command=update_text)
button.pack()
window.mainloop()
You may need to install sh to run top like I did, or use subprocess.check_output if you want.
text.insert(0.0, subprocess.check_output(['top', '-b', '-n 1']))

How to use a <ComboboxSelected> virtual event with tkinter

I am using a tkk.Combobox themed widget in Python 3.5.2. I want an action to happen when a value is selected.
In the Python docs, it says:
The combobox widgets generates a <<ComboboxSelected>> virtual event when the user selects an element from the list of values.
Here on the Stack, there are a number of answers (1, 2, etc) that show how to bind the event:
cbox.bind("<<ComboboxSelected>>", function)
However, I can't make it work. Here's a very simple example demonstrating my non-functioning attempt:
import tkinter as tk
from tkinter import ttk
tkwindow = tk.Tk()
cbox = ttk.Combobox(tkwindow, values=[1,2,3], state='readonly')
cbox.grid(column=0, row=0)
cbox.bind("<<ComboboxSelected>>", print("Selected!"))
tkwindow.mainloop()
I get one instance of "Selected!" immediately when I run this code, even without clicking anything. But nothing happens when I actually select something in the combobox.
I'm using IDLE in Windows 7, in case it makes a difference.
What am I missing?
The problem is not with the event <<ComboboxSelected>>, but the fact that bind function requires a callback as second argument.
When you do:
cbox.bind("<<ComboboxSelected>>", print("Selected!"))
you're basically assigning the result of the call to print("Selected!") as callback.
To solve your problem, you can either simply assign a function object to call whenever the event occurs (option 1, which is the advisable one) or use lambda functions (option 2).
Here's the option 1:
import tkinter as tk
from tkinter import ttk
tkwindow = tk.Tk()
cbox = ttk.Combobox(tkwindow, values=[1,2,3], state='readonly')
cbox.grid(column=0, row=0)
def callback(eventObject):
print(eventObject)
cbox.bind("<<ComboboxSelected>>", callback)
tkwindow.mainloop()
Note the absence of () after callback in: cbox.bind("<<ComboboxSelected>>", callback).
Here's option 2:
import tkinter as tk
from tkinter import ttk
tkwindow = tk.Tk()
cbox = ttk.Combobox(tkwindow, values=[1,2,3], state='readonly')
cbox.grid(column=0, row=0)
cbox.bind("<<ComboboxSelected>>", lambda _ : print("Selected!"))
tkwindow.mainloop()
Check what are lambda functions and how to use them!
Check this article to know more about events and bindings:
http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm
Thanks you for the posts. I tried *args and it workes with bind and button as well:
import tkinter as tk
from tkinter import ttk
tkwindow = tk.Tk()
cbox = ttk.Combobox(tkwindow, values=[1,2,3], state='readonly')
def callback(*args):
print(eventObject)
cbox.bind("<<ComboboxSelected>>", callback)
btn = ttk.Button(tkwindow, text="Call Callback", command=callback);
tkwindow.mainloop()

Categories

Resources