How to create a for loop that proceeds to the next item when a Button widget is pressed?
The Button description should be updated in the for loop.
In the official documentation of Asynchronous widgets, they show how to stop a for loop and then proceed to the next item when the user moves an IntSlider widget. It is mentioned that a similar result can be achieved with a Button, even though Buttons have a different interface than other widgets.
What I want is the same desired outcome of an existing question. Even though the question has an accepted answer, from the comments it is clear that the answer does not produce the desired outcome.
This code works, and it is based on the example from Asynchronous widgets documentation mentioned in the answer.
import asyncio
from IPython.display import display
import ipywidgets as widgets
out = widgets.Output()
def wait_for_click(btn, n_clicked=0):
future = asyncio.Future()
def on_button_clicked(btn):
btn.description = f"Clicked {n_clicked} times!"
future.set_result(btn.description)
btn.on_click(on_button_clicked)
return future
n_changes = 5
btn = widgets.Button(description="Never Clicked")
async def f():
for i in range(n_changes):
out.append_stdout(f'did work {i}\n')
btn_descr = await wait_for_click(btn, n_clicked=i + 1)
out.append_stdout(f'\nasync function continued with button description "{btn_descr}"\n')
asyncio.ensure_future(f())
display(btn, out)
The most important part is wait_for_click, a function that is called in each iteration of the for loop, and updates the Button description from "Never Clicked" to "Clicked N times!".
wait_for_click also returns the Button description as a string.
wait_for_click is a modification of wait_for_change from the documentation: instead of waiting for a modification of the IntSlider, it waits for a click of the Button.
Related
I set up a Jupyter Notebook where I'm running a recursive function that clears the output like so, creating an animated output:
from IPython.display import clear_output
active = True
def animate:
myOutput = ""
# do stuff
clear_output(wait=True)
print(myOutput)
sleep(0.2)
if active:
animate()
and that's working perfectly.
But now I want to add in one more step: A speed toggle. What I'm animating is a debugging visualization of a cursor moving through interpreted code as an interpreter I'm writing parses that code. I tried conditional slow-downs to have more time to read what's going on as the parsing continues, but what I really need is to be able to click a button to toggle the speed between fast and slow. Maybe I'll use a slider, but for now I just want a button for proof of concept.
This sounds simple enough. Note that I'm writing this statefully as a class because I need to read / write the state from within another imported class.
Jupyter block 1:
import ipywidgets as widgets
from IPython.display import display
out = widgets.Output()
class ToggleState():
def __init__(self):
self.button = widgets.Button(description="Toggle")
self.button.on_click(self.toggle)
display(self.button)
self.toggleState = False
print("Toggle State:", self.toggleState)
def toggle(self, arg): # arg has to be accepted here to meet on_click requirements
self.toggleState = not self.toggleState
print("Toggle State:", self.toggleState)
def read(self):
return self.toggleState
toggleState = ToggleState()
Then, in Jupyter block 2, note I decided to to do this in a separate block because the clear_output I'm doing with the animate func clears the button if it's in the same block, and therein lies the problem:
active = True
def animate:
myOutput = ""
# do stuff
clear_output(wait=True)
print(myOutput)
if toggleState.read():
sleep(5)
else:
sleep(0.2)
if active:
animate()
But the problem with this approach was that two blocks don't actually run at the same time (without using parallel kernels which is way more complexity than I care for) so that button can't keep receiving input in the previous block. Seems obvious now, but I didn't think about it.
How can I clear input in a way that doesn't delete my button too (so I can put the button in the animating block)?
Edit:
I thought I figured the solution, but only part of it:
Using the Output widget:
out = widgets.Output()
and
with out:
clear_output(wait=True) # clears only the logged output
# logging code
We can render to two separate stdouts within the same block. This works to an extent, as in the animation renders and the button isn't cleared. But still while the animation loop is running the button seems to be incapable of processing input. So it does seem like a synchronous code / event loop blocking problem. What's the issue here?
Do I need an alternative to sleep that frees up the event loop?
Edit 2:
After searching async code in Python, I learned about asyncio but I'm still struggling. Jupyter already runs the code via asyncio.run(), but the components obviously have to be defined as async for that to matter. I defined animate as async and tried using async sleeps, but the event loops still seems to be locked for the button.
In a published Jupyter notebook is there a way to insert a widget that says "running" or something similar when I am running a function.
I am aware of the tqdm function but to the best of my knowledge this is only when the function / process contains a for-loop.
I currently have a series of dropdown widgets with a submit button but some of the functions take a while for the calcs to run so i have no way of telling if the're running or not
Cheers
The way I have done this in the past is to have a function as a context manager, that displays some value to a Text widget to indicate that the function is running. You could also use an Output widget to display a indeterminate 'progress' bar like the one below:
https://www.iselect.com.au/content/themes/iselect/images/post/loader.gif
import ipywidgets as ipyw
import time
from contextlib import contextmanager
label = ipyw.Text('Ready')
button = ipyw.Button(description='Click me')
#contextmanager
def show_loading():
label.value = 'Running...'
yield
label.value = 'Ready'
def long_running_function(self):
with show_loading():
time.sleep(2)
button.on_click(long_running_function)
display(button)
display(label)
I'm using PySimpleGUI in which I want to update a radio button. According to the documentation the radio button has an update method. But somehow it doesn't work properly.
I wrote the following code which should update the value of the radio button from Test to NewTest. The result is still Test.
Code used below:
import PySimpleGUI as sg
layout1 = [[sg.Radio('Test', "RADIO1", key='_RADIO1_', default=True, font=50)],
[sg.Button('Ok', font=50), sg.Button('Stop', font=50)]]
window = sg.Window('Read').Layout(layout1).Finalize()
while True:
window.Element('_RADIO1_').Update('NewTest')
button, values = window.Read()
exit()
Sounds like you're trying to change the text that's next to an specific radio button.
The problem is that each of the PySimpleGUI Elements has a slightly different Update method. Simply put, the things you can change in a Radio Element are:
Update(self, value=None, disabled=None, visible=None)
While the documentation on the Radio Button element's Update is brief in the documentation, it is described there https://pysimplegui.readthedocs.io/#radio-button-element
Update(value=None, disabled=None, visible=None)
value - bool - if
True change to selected
disabled - if True disables the element
There are 3 things you can currently change in a Radio button, the "state" (true/false), disabled and visibility.
I would suggest logging this as a feature request Issue on the GitHub site (http://www.PySimpleGUI.com). These requests are often implemented quite quickly.
following Problem. I created a Tkinter GUI. There is a button, a text widget and a label which shows my progress status.
There is a function that clears the text widget, a function that writes in the text widget and a function that updates the progress status.
At first the status label says: "-"
When I click the button. I want the label to say "Work in progress" as long as the funtion that writes in the text widget is working. after the text is shown in the widget (takes a while) the status should say: "done".
The problem is, that I currently click the button. after a few seconds the text is shown in the text widget and the status just switches from "-" to "done".
def show_working_status(self):
self.status["text"] = "Work in progress"
def show_done_status(self):
self.status["text"] = "Done"
def cleartext(self):
.......
def write(self, columns)
.......
def write_text(self, columns)
self.cleartext()
self.show_working_status()
self.write(columns)
self.show_done_status()
I hope i made my problem as clear as possible :)
Greetings,
Tom
You should use multithreading:
import threading
t1 = threading.Thread(target=your_first_function)
t1.start()
t2 = threading.Thread(target=your_second_function)
t2.start()
t3 = threading.Thread(target=your_third_function)
t3.start()
This will make the functions your_first_function, your_second_function and your_third_function run in parallel. This makes it so that your window won't freeze, since the Tkinter Mainloop is just an infinite while loop.
You can also make one function that will call all of yours in order.
I have the following problem when using tkinter to create a very simple window containing a matrix of buttons: When one of the buttons is clicked, the event handler changes the text of that button using the configure method on the button widget. This works. But I also want to change the text in one of the other buttons, which does not work. The method I use is that on creating the button, I store the object returned by the Button method before I use the grid geometry manager to place it. This object looks like ".123456789L" when printed and seems to be a pointer to the widget. I also use configure on this to change the button text. But somehow it seems to be wrong, because it works sometimes, and most of the times not. There's unfortunately no error message, just nothing happens when calling configure. I checked and it seems to be the correct pointer to the widget. Do I have to use a special way to affect a widget other that the one that called the event handler? These are the relevant parts of the code:
# CREATING THE BUTTONS:
buttons={} # global
for i in range(3):
for j in range(3):
button = Tkinter.Button(self,text='foo')
buttons[button]=(i,j)
button.grid(column=j,row=i)
button.bind( "<Button-1>", self.OnButtonClick )
# CHANGING BUTTONS:
def find_button(i,j):
"""Return the pointer to the other button to be changed when a button has been clicked."""
for button,key in buttons.items():
if key==(i,j): return button
def OnButtonClick(self,event):
print "You clicked the button",buttons[event.widget]
i,j=buttons[event.widget]
old_button=find_button(i,j) # This is simplified, I don't actually pass i,j, but other values. But I checked this and it returns the reference to the correct button. But this simplified version works the same way, just assume a different button that the one pressed would be returned.
old_button.configure(text = 'blabla') # THIS DOES NOT WORK
event.widget.configure(text = 'something') # THIS WORKS
I have the same problem and i solve it with:
buttons[button]=(i,j,button)
and in the function OnButtonClicK:
i,j,old_button=buttons[event.widget]
old_button.configure(text = 'blabla') # THIS DOES NOT WORK