Multiple interact() commands duplicate widgets in IPython - python

I am using an IPython Jupyter notebook. In the following situation, I call a function using interact(), which in turns calls a second function again using interact().
def fun1(dataset_id):
dataset = read_dataset(dataset_id)
interact(fun2, data=dataset, var=(0,dataset.property,0.1))
def fun2(data, var):
# something
interact(fun1, dataset_id=(0,5,1))
When first running this, it display 2 slider widgets: one for dataset_id, and one for the variable var. But if I vary the dataset_id slider once, a second slider for var is added below the first var slider, so now I have 3 sliders in total. How can I avoid this?

This is only one step less hacky, but at least you don't have to have a button:
from ipywidgets import *
from IPython.display import display
datasets=[{"property":1},{"property":2},{"property":3},{"property":4},{"property":5}]
def read_dataset(dataset_id):
return datasets[dataset_id]
def fun1(dataset_id):
global sliders
try:
sliders.close()
except NameError:
pass
dataset = read_dataset(dataset_id)
sliders = interactive(fun2, data=fixed(dataset), var=(0,dataset["property"],0.1)) # note I am now using interactive, instead of interact, because I need the close() function
display(sliders)
def fun2(data, var):
print var
interact(fun1, dataset_id=(0,5,1))

After a frustrating day, I came up with a totally hacky way to solve this (but at least it achieves 100% what I want). I am adding a button which, when clicked, invokes .close() on the second slider, as well as on the button itself. Therefore, before each time I need to move the first slider, I press this button to clear up.
Here is a fully-functioning code based on the snippet in the question, that you can copy-paste in your interpreter.
from ipywidgets import *
from IPython.display import display
datasets=[{"property":1},{"property":2},{"property":3},{"property":4},{"property":5}]
def read_dataset(dataset_id):
return datasets[dataset_id]
def fun1(dataset_id):
dataset = read_dataset(dataset_id)
sliders = interactive(fun2, data=fixed(dataset), var=(0,dataset["property"],0.1)) # note I am now using interactive, instead of interact, because I need the close() function
close_button = widgets.Button(description="Remove sliders")
def remove_sliders(b):
sliders.close()
b.close()
close_button.on_click(remove_sliders)
display(sliders)
display(close_button)
def fun2(data, var):
print
# something
interact(fun1, dataset_id=(0,5,1))

Here is another solution, you could create two sliders, and make the "max" of the second slider dependent on the property selected with the first slider:
import ipywidgets as widgets
from ipywidgets import *
from IPython.display import display
datasets=[{"property":1},{"property":2},{"property":3},{"property":4},{"property":5}]
def read_dataset(dataset_id):
return datasets[dataset_id]
w_slider1 = IntSlider(min=0, max=len(datasets)-1, step=1)
w_slider2 = FloatSlider(min=0, step=0.1)
def fun1(dataset_id):
dataset = read_dataset(dataset_id)
#you could get rid of function "read_dataset"
#dataset = datasets[dataset_id]
w_slider2.max = dataset['property']
def fun2(data, var):
#call fun1 to update the size of 2nd slider
fun1(data)
#do something
print(data, var)
interact(fun2, data=w_slider1, var=w_slider2)

Related

Tkinter automatically update Label after Entry in Python

I made an tkinter window for some kind of calculation from the data entered. I simplified my code below to illustrate my question. Currently, the result will show up once I click search. However, I want the result to show up automatically once text are entered in the entry box without the "search" button. I tried "after" using window.after(100, self.searchbarcode) but it did not work for me. Appreciate your inputs. Thank you!
from tkinter import *
import pandas as pd
import tkinter as tk
import os.path
import numpy as np
class searchloc:
def __init__(self):
window=tk.Tk()
window.geometry("800x300")
window.title("Search Location")
Label(window, text="Scan",font="Helvetica 24").grid(row=1,column=1,sticky=W)
self.barcode=StringVar()
self.outcomes=StringVar()
self.text1=tk.Entry(window,textvariable=self.barcode,font="Helvetica 36")
self.text1.grid(row=1,column=2,padx=(0,5))
Label(window,textvariable=self.outcomes,font="Helvetica 68 bold").grid(row=7,column=2,sticky=E)
wsheet1 = gsheet.worksheet("Sheet2")
mydata1 = wsheet1.get_values()
mydata2=mydata1[1:]
cool=mydata1[0]
look = pd.DataFrame(mydata2, dtype=str)
#window.after(1, self.searchbarcode())
#self.text1.bind('<Enter>', self.searchbarcode())
Button(window,text='search',command=self.searchbarcode,font="Helvetica 38").grid(row=5,column=2,padx=(100,5),pady=5,sticky=W)
#Button(window,text='clear',command=self.clear_text,font="Helvetica 38").grid(row=5,column=2,padx=(100,5),pady=5,sticky=E)
window.mainloop()
#def clear_text(self):
def searchbarcode(self):
bar = self.barcode.get()
outtt=bar[1:5]
self.outcomes.set(outtt)
self.text1.delete(0, 'end')
searchloc()
If you want to happen when you press the return key, you almost had it right. The function has to accept an event parameter even if you don't use it, and you need to make sure you pass the function itself, not the result of calling the function (ie: self.searchbarcode instead of self.searchbarcode()).
Also, the event is <Return>. <Enter> is for when the mouse enters the widget.
def __init__(self):
...
self.text1.bind('<Return>', self.searchbarcode)
...
def searchbarcode(self, event):
...
If you want to call searchbarcode both with or without the event parameter, give it a default value of None:
def searchbarcode(self, event=None):
...

Dynamically add and remove list items in an IPython widget

I'm trying to achieve a basic todo list app in Ipython Jupyter Notebook using ipywidgets.
I can easily achieve the functionality of adding items to my list, however, I can't properly handle removing of existing items if the 'Remove' button is clicked. The entire code is run in a single cell.
import ipywidgets as widgets
from ipywidgets import VBox, HBox, Text, Button
from IPython.display import display
todo = []
def completed_sentence(sentence):
""" To display existing notes with a 'Remove' button """
sentenceField = Text(value=sentence)
removeButton = Button(description='Remove',
button_style='danger')
return HBox([sentenceField, removeButton])
def render_sentences(_):
""" To update the view """
global a,b
if a.value != '':
todo.append(a.value)
a.value = ''
todoWidget.children = tuple\
([VBox([VBox([completed_sentence(each)
for each in todo]),
HBox([a, b])])])
# Setting up a basic view- with an empty field and a button
a = widgets.Text(value='')
b = widgets.Button(description='Add')
b.on_click(render_sentences)
todoWidget = widgets.HBox([a, b])
display(todoWidget)
Now, in order to enable the removal of sentences, I update the definition of the function completed_sentence as follows:
def completed_sentence(sentence):
""" To display existing notes """
def remove_sentence(_):
global render_sentences
try:
if todo.index(sentenceField.value) >= 0:
todo.remove(sentenceField.value)
render_sentences()
except:
return
sentenceField = Text(value=sentence)
removeButton = Button(description='Remove', button_style='danger')
removeButton.on_click(remove_sentence)
return HBox([sentenceField, removeButton])
But now, this has the issue that its call to render_sentences is ignored! What is the optimal way to deal with such a kind of 'reactive' programming, if you will, using Ipython Widgets.
Updating the definition of completed_sentence seems to do the job. But it still remains a mystery why the original definition didn't work.
def completed_sentence(sentence):
def remove_sentence(_):
global render_sentences
try:
if todo.index(sentenceField.value) >= 0:
todo.remove(sentenceField.value)
except:
pass
render_sentences(_)
sentenceField = Text(value=sentence)
removeButton = Button(description='Remove', button_style='danger')
removeButton.on_click(remove_sentence)
sentence_view = HBox([sentenceField, removeButton])
return sentence_view

sikuli observing multiple images with priority

I've defined a sikuli module which is used to click on an image when something appears inside a region.
# observer.py
from sikuli import *
class Observer:
# When "observedImage" appears inside "region", double click on "reactImage"
def __init__(self, region, observedImage, reactImage):
self.region = region
self.observedImage = observedImage
self.reactImage = reactImage
def start(self):
self.region.onAppear(self.observedImage, self.appearHandler)
self.region.observe(FOREVER, background = True)
def appearHandler(self, event):
doubleClick(self.reactImage)
event.repeat()
def stop(self):
self.region.stopObserver()
Here's how to use it:
import observer
import time
observer.Observer(Region(111,222,333,444), "imageToBeDetected1.png", "imageToBeClicked1.png").start()
observer.Observer(Region(555,666,66,666), "imageToBeDetected2.png", "imageToBeClicked2.png").start()
while True:
print('waiting')
time.sleep(1)
The problem with the above code is that when imageToBeDetected1 and imageToBeDetected2 both appear in Region(111,222,333,444) and Region(555,666,66,666) respectively, my mouse will move between imageToBeClicked1 and imageToBeClicked2. I want only imageToBeDetected1 to be clicked in this situation.
imageToBeDetected2 should be ignored when imageToBeDetected1 and imageToBeDetected2 both appear in Region(111,222,333,444) and Region(555,666,66,666), respectively.
How can I modify my code so that imageToBeDetected1 has a higher priority over imageToBeDetected2?
Or is there a better way to observe multiple images with sikuli?

custom Tkinter button won't grid properly inside Frame

I have created a custom Tkinter Button widget which inherits from the standard Button, purely for aesthetic purposes so that I can repeat the same style throughout all my program without have to configure it manually every time. The code for my custom Button is here:
import tkinter as tk
class gameButton(tk.Button):
def __init__(self,*args,**kwargs):
super(gameButton,self).__init__()
self.root=args[0]
self.configure(text=kwargs["text"])
self.configure(fg=kwargs["fg"],
activeforeground=kwargs["fg"])
try:
self.configure(command=kwargs["command"])
except KeyError:
pass
self.configure(relief="flat",cursor="hand2")
self.bind("<Enter>",self.hover)
self.bind("<Leave>",self.leave)
self.old=kwargs["bg"]
self.dark=self.darken(self.old)
self.configure(bg=self.old,activebackground=self.dark,
bd=1,relief="solid")
def getRGB(self,h):
it=tuple(int(h[i:i+2], 16) for i in (0, 2 ,4))
return it
def getHex(self,h):
it='#%02x%02x%02x' % h
return it
def darken(self,h):
currentHex=self.old.replace("#","")
currentRGB=self.getRGB(currentHex)
currentR=currentRGB[0]
currentG=currentRGB[1]
currentB=currentRGB[2]
if currentR>30:
newR=round(currentR-30)
else:
newR=0
if currentG>30:
newG=round(currentG-30)
else:
newG=0
if currentB>30:
newB=round(currentB-30)
else:
newB=0
newRGB=(newR,newG,newB)
newHex=self.getHex(newRGB)
return newHex
def hover(self,event):
self.configure(bg=self.dark)
def leave(self,event):
self.configure(bg=self.old)
The section of the code where I'm trying to grid them in a Frame is here (I imported gameButton as GB):
game=Frame(notebook,bg=bg)
notebook.add(game,text="Game")
mapb=GB(game,text="Map",compound="left",bg=bg,fg=fg)
travelb=GB(game,text="Travel",compound="left",bg=bg,fg=fg)
bagb=GB(game,text="Bag",compound="left",bg=bg,fg=fg)
pokedexb=GB(game,text="Pokédex",compound="left",bg=bg,fg=fg)
partyb=GB(game,text="Party",compound="left",bg=bg,fg=fg)
saveb=GB(game,text="Save",compound="left",bg=bg,fg=fg)
mapb.grid(row=0,column=0,padx=5,pady=5,sticky="nesw")
travelb.grid(row=0,column=1,padx=5,pady=5,sticky="nesw")
bagb.grid(row=0,column=2,padx=5,pady=5,sticky="nesw")
pokedexb.grid(row=1,column=0,padx=5,pady=5,sticky="nesw")
partyb.grid(row=1,column=1,padx=5,pady=5,sticky="nesw")
saveb.grid(row=1,column=2,padx=5,pady=5,sticky="nesw")
The result is this:
The button grids fine in a normal root window, but it refuses to behave properly inside a Frame inside a root.

Problems with a bind function from tkinter in Python

I am working on an application that is supposed to support both running from a console and from a GUI. The application has several options to choose from, and since in both running modes the program is going to have the same options obviously, I made a generalisation:
class Option:
def __init__(self, par_name, par_desc):
self.name = par_name
self.desc = par_desc
class Mode():
def __init__(self):
self.options = []
self.options.append(Option('Option1', 'Desc1'))
self.options.append(Option('Option2', 'Desc2'))
self.options.append(Option('Option3', 'Desc3'))
self.options.append(Option('Option4', 'Desc4'))
self.options.append(Option('Option5', 'Desc5'))
#And so on
The problem is that in GUI, those options are going to be buttons, so I have to add a new field to an Option class and I'm doing it like this:
def onMouseEnter(par_event, par_option):
helpLabel.configure(text = par_option.desc)
return
def onMouseLeave(par_event):
helpLabel.configure(text = '')
return
class GUIMode(Mode):
#...
for iOption in self.options:
iOption.button = Button(wrapper, text = iOption.name, bg = '#004A7F', fg = 'white')
iOption.button.bind('<Enter>', lambda par_event: onMouseEnter(par_event, iOption))
iOption.button.bind('<Leave>', lambda par_event: onMouseLeave(par_event))
#...
There is also a "help label" showing the description of the option every time a mouse hovers over it, so there I am binding those functions.
What is happening is that while I am indeed successfully adding a new field with a button, the bind function seems to mess up and the result is this:
Help label is always showing the description of the last option added, no matter over which button I hover. The problem seems to go away if I directly modify the Option class instead, like this:
class Option:
def __init__(self, par_name, par_desc):
self.name = par_name
self.desc = par_desc
self.button = Button(wrapper, text = self.name, bg = '#004A7F', fg = 'white')
self.button.bind('<Enter>', lambda par_event: onMouseEnter(par_event, self))
self.button.bind('<Leave>', lambda par_event: onMouseLeave(par_event))
But I obviously can't keep it that way because the console mode will get those fields too which I don't really want. Isn't this the same thing, however? Why does it matter if I do it in a constructor with self or in a loop later? I therefore assume that the problem might be in a way I dynamically add the field to the class?
Here is the full minimal and runnable test code or whatever it is called, if you want to mess with it: http://pastebin.com/0PWnF2P0
Thank you for your time
The problem is that the value of iOption is evaluated after the
for iOption in self.option:
loops are complete. Since you reset iOption on each iteration, when the loop is completed iOption has the same value, namely the last element in self.options. You can demonstrate this at-event-time binding with the snippet:
def debug_late_bind(event):
print(iOption)
onMouseEnter(event, iOption)
for iOption in self.options:
iOption.button = Button(wrapper, text = iOption.name,
bg = '#004A7F', fg = 'white')
iOption.button.bind('<Enter>', debug_late_bind)
which will show that all events that iOption has the same value.
I split out the use of iOption to debug_late_bind to show that iOption comes in from the class scope and is not evaluated when the bind() call is executed. A more simple example would be
def print_i():
print(i)
for i in range(5):
pass
print_i()
which prints "4" because that is the last value that was assigned to i. This is why every call in your code to onMouseEnter(par_event, iOption) has the same value for iOption; it is evaluated at the time of the event, not the time of the bind. I suggest that you read up on model view controller and understand how you've tangled the view and the controller. The primary reason this has happened is that you've got two views (console and tk) which should be less coupled with the model.
Extracting the .widget property of the event is a decent workaround, but better still would be to not overwrite the scalar iOption, but instead use list of individual buttons. The code
for n, iOption in enumerate(self.options):
would help in creating a list. In your proposed workaround, you are encoding too much of the iOption model in the tkinter view. That's bound to bite you again at some point.
I don't know what the actual problem was with my original code, but I kind of just bypassed it. I added a dictionary with button as a key and option as a value and I just used the par_event.widget to get the option and it's description, which is working fine:
buttonOption = {}
def onMouseEnter(par_event):
helpLabel.configure(text = buttonOption[par_event.widget].desc)
return
def onMouseLeave(par_event):
helpLabel.configure(text = '')
return
class GUIMode(Mode):
def run(self):
#...
for iOption in self.options:
iOption.button = Button(wrapper, text = iOption.name, bg = '#004A7F', fg = 'white')
iOption.button.bind('<Enter>', lambda par_event: onMouseEnter(par_event))
iOption.button.bind('<Leave>', lambda par_event: onMouseLeave(par_event))
buttonOption[iOption.button] = iOption
#...

Categories