ipywidgets: Update one widget based on results from another - python

I am using widgets in IPython that allows the user to repeatedly search for a phrase and see the results (different titles) in another widget (a selection widget) and then select one of the results.
In short:
search_text = widgets.Text(description = 'Search')
search_result = widgets.Select(description = 'Select table')
def search_action(sender):
phrase = search_text.value
df = search(phrase) # A function that returns the results in a pandas df
titles = df['title'].tolist()
search_result.options = titles
search_text.on_submit(search_action)
This used to work fine, but after updating to the latest version of ipywidgets (5.1.3 from 4.0.1) it seems like
search_selection.options = titles
Produce the following errors (one or both, it varies):
TraitError: Invalid selection
TypeError: 'list' object is not callable
It still works in the sense that the widget gets updated with the results based on the search from the other widget, but it gives an error.
What is the correct way of setting the options in one widget based on the results from another widget?
(edit: added more detailed error message)

I encountered this exact problem an hour ago. I have hacked together a solution using the minimum example here: Dynamically changing dropdowns in IPython notebook widgets and Spyre, since my own requirements were to have dynamically linked lists. I am sure you'll be able to adapt your requirements using this solution.
The key is to pre-generate all Dropdowns/Select. For some reason, w.options = l only sets w._options_labels but not w.options. Subsequent validation of the selected value of w will then fail horribly.
import ipywidgets as widgets
from IPython.display import display
geo={'USA':['CHI','NYC'],'Russia':['MOW','LED']}
geoWs = {key: widgets.Select(options=geo[key]) for key in geo}
def get_current_state():
return {'country': i.children[0].value,
'city': i.children[1].value}
def print_city(**func_kwargs):
print('func_kwargs', func_kwargs)
print('i.kwargs', i.kwargs)
print('get_current_state', get_current_state())
def select_country(country):
new_i = widgets.interactive(print_city, country=countryW, city=geoWs[country['new']])
i.children = new_i.children
countryW = widgets.Select(options=list(geo.keys()))
init = countryW.value
cityW = geoWs[init]
countryW.observe(select_country, 'value')
i = widgets.interactive(print_city, country=countryW, city=cityW)
display(i)
Note lastly that it is not trivial to obtain the most up-to-date state of the widgets. These are
directly from the children's values, via get_current_state. This can be trusted.
from the interactive instance, via i.kwargs
from the supplied args to print_city
The latter two can sometimes be out of date, for various reasons I don't wish to find out further.
Hope this helps.

You can hold notifications for during the assignment to options:
with search_result.hold_trait_notifications():
search_result.options = titles
Thus:
search_text = widgets.Text(description = 'Search')
search_result = widgets.Select(description = 'Select table')
def search_action(sender):
phrase = search_text.value
df = search(phrase) # A function that returns the results in a pandas df
titles = df['title'].tolist()
with search_result.hold_trait_notifications():
search_result.options = titles
See hmelberg's explanation below
"The root of the error is that the widget also has a value property and the value may not be in the new list of options. Because of this, widget value may be "orphaned" for a short time and an error is produced."

I had a similar problem and solved it Registering callbacks to trait changes in the kernel
caption = widgets.Label(value='The values of range1 and range2 are synchronized')
slider = widgets.IntSlider(min=-5, max=5, value=1, description='Slider')
def handle_slider_change(change):
caption.value = 'The slider value is ' + (
'negative' if change.new < 0 else 'nonnegative'
)
slider.observe(handle_slider_change, names='value')
display(caption, slider)
I guess this solution wasn't available back in 2016 or my problem wasn't as similar as thought.

The root of the error is that the widget also has a value property and the value may not be in the new list of options. Because of this, widget value may be "orphaned" for a short time and an error is produced.
The solution is either to assign the widget value to the option list before assigning it to the widget (and remove the value/option after if desired), or as Dan writes: use create a hold_trait-notifications()
Dan's approach is the best. The above just explains the cause of the problem.

Related

tkinter button destroys frame and removes index from list

I'm relatively new to tkinter and I'm trying to create a program to show some stock values. I have the user input the stock symbol which appends to a list of stocks and displays some information about the stock in a tkinter frame. Within the frame I have a "Remove Stock" button that I want to simultaneously remove the stock from the list of stocks and destroy that frame. Here is my relevant code:
current_stocks = []
stock_frames = []
def update(): #Update the Currently Displayed Frames
print(stock_frames)
for stock in current_stocks:
list_index = current_stocks.index(stock)
stock_frame = LabelFrame(root, text=stock, padx=5, pady=5)
stock_frame.grid(row=list_index+1,column=0,columnspan=3)
if stock_frame not in stock_frames:
stock_frames.append(stock_frame)
stock_info_lbl = Label(stock_frame, text=f'{stock} Current Price')
stock_info_lbl.grid(row=0,column=0)
graph_stock_btn = Button(stock_frame, text="Graph", command=graph_stock)
graph_stock_btn.grid(row=0,column=1)
remove_stock_btn = Button(stock_frame, text="Remove", command=lambda list_index=list_index: remove_stock(list_index))
remove_stock_btn.grid(row=0,column=2)
def remove_stock(i): #Remove Stock From List and Destroy the Frame
current_stocks.pop(i)
stock_frames[i].destroy()
stock_frames.pop(i)
update()
I pass the list index parameter to remove stock because as far as I can see the index position of current_stocks and stock_frames should correspond to the same stock. I tried using grid_forget() instead of destroy() but that doesn't change anything. I should note that as long as I only have one stock displayed I can remove it perfectly fine, however if I add more than one stock things start breaking. This makes me believe the error has something to do with this:
if stock_frame not in stock_frames:
stock_frames.append(stock_frame)
Any help would be greatly appreciated, thanks!
your intuition is correct about where your problematic code lies, it is in if stock_frame not in stock_frames. This will always return false as this is object comparison and two LabelFrame are not equivalent even if they have the same text.
To get around this you would want to compare only the name in the first list (current_stocks) and take it for granted that stock_frames has an associated entry which you can delete.
the more thorough solution would be to use a more associative datastructure such as a dict from stock_name --> tkinter_frame
That would look something like this:
stocks = {
"GE": None, # A default case
"SYM": tkinter.LabelFrame(...), # A populated value
...
}
and your remove method would operate on that dictionary instead:
def remove_stock(stock_name): #Remove Stock From List and Destroy the Frame
stock_frame = stocks[stock_name]
# stock_frame could be None
if stock_frame:
stock_frames[i].destroy()
del stocks[stock_name]

Populate wx.MenuItem from a dictionary and Bind all MenuItems to the same Method (passing a dictionary value)?

Using Python 3.x and wxPython.
I'm working on a GUI tool that will likely need to be expanded and updated over time and I'm trying to reduce the amount of work (long term) as features are added.
I have a method that needs a variable passed to it, and then the method checks the dictionary to see which "ID" is associated with it, then uses that ID to trigger some automation.
job_dictionary = {
'job_name_in_english': 'id_goes_here',
'job_name_in_english': 'id_goes_here'
}
I have a very simple window setup and I'm populating a file menu with the following:
for key in job_dictionary:
newitem = wx.MenuItem(file_menu,wx.ID_NEW, text = key,kind = wx.ITEM_NORMAL)
file_menu.Append(newitem)
Then later on I want to bind them to a method, and pass that method the 'key' value, depending on which item they select from the menu.
Which is where I'm running into some difficulties. I was wondering if there was a way to bind them dynamically based on the key value within the dictionary, because once I've got that part done I can just do call the method and pass it the key:
def job_trigger(key)
id = job_dictionary[key]
#Rest Of Code...
Hopefully what I'm trying to do makes sense. I'm still new to building GUI applications, and this is the first time I'm dealing with a File Menu, and the first time I'm trying to implement something like this.
I know in a batch script I can do something like this:
set x=0
loop of some kind here(
set /A "x=x+1"
set menuItem%x%=foobar
)
Which would make the variable name dynamic, and then I was thinking I could bind them as they're being written. So I guess something like:
x = 0
for key in dictionary
x += 1
menu_item = 'menu_item_' + x
'menu_item_' + x = wx.Menu().Append(wx.ID_FILEDLGG, key)
'menu_item_' + x.Bind(wx.EVT_MENU, self.job_trigger(key), key)
I know that's not the correct syntax for something like this, and I have no idea if that would be the appropriate way of handling this scenario, or if there is something else entirely I should be attempting.
From what I deducted, you might need only simple iteration over dict:
#! /usr/bin/env python3
a_dict = {
"key_0": "value_0",
"key_1": "value_1"
}
for i, v in enumerate(a_dict):
print("current index: {}".format(i))
print("current value: {}".format(v))
# string manipulation
builder_string = "prefix_" + str(i)
print(builder_string)
# rest of code...
If you also need keys, then you can use for key, value in a_dict.items(): and set index outside loop and increment it manually.
I mistakenly confused the File menu with the dropdown menu. So here is my solution.
job_dictionary = {
'job_name1': 'job_id_1',
'job_name2': 'job_id_2'
}
Code to make Window and Panel goes here
#Create Dropdown Menu
jobs = list()
for i in job_dictionary.keys():
jobs.append(i)
self.choice = wx.Choice(panel, pos=(5,5), size=(375,50), choices = jobs)
#Create Buttons
trigger_job_btn = wx.Button(panel, label='Trigger Job', pos=(290,50))
#Button Trigger
trigger_job_btn.Bind(wx.EVT_BUTTON, self.execute_job)
The above creates a list from the dictionary, then uses that list to populate the dropdown menu (wx.Choice). Then I create a button, and bind that button to a method.
Then the method looks like this:
def execute_job(self, evt):
choice = self.choice.GetSelection()
if choice == -1:
wx.MessageBox('Please Make a Selection from the DropDown.',
'Warning', wx.OK | wx.ICON_INFORMATION)
else:
key = self.choice.GetString(choice)
jobTrigger(key, job_dictionary[key])
The above method gets choice number, if it's blank, you get an error message popup informing you to make a selection. If it's a populated choice, it will then look for the string within that choice, and pass that to another method as the dictionary key value.

How to remove multiple selections from a listbox

I got this code that put in the variable ativo what is selected in the listbox
def selecionado(evt):
global ativo
a=evt.widget
b=a.curselection()
if len(b) > 0:
ativo=[a.get(i) for i in a.curselection()]
else:
ativo=[]
How can i create a function that can delete this, because ativo return this:
["Name1\t Code1\n"]
["Name1\t Code1\n", 'Name2\t Code2\n']
But if i print the listbox, this shows up
.59070320.59070384
Even though the listbox is present in my graphic interface with the names and correspodent codes the right way, like:
Name Code
Name Code
So, i have no idea how to delete what is selected, please help me
Just use yourlistboxname.delete(index)
def removeclicked(evt):
w = evt.widget
index = int(w.curselection()[0])
label = w.get(index)
yourlistboxname.delete(index)
To bind the evt, add this where you create the widget
yourlistboxname.bind('<<ListboxSelect>>',removeclicked)
This solution is very easy to find, so maybe I am mistaking your problem. you don't remove the entries in your code, you only get() the entries or change them.
This page should help:
http://effbot.org/tkinterbook/entry.htm
http://effbot.org/tkinterbook/tkinter-events-and-bindings.htm

PyQt treeview edit text on doubleclick

So I've been trying to implement a proper TreeView that displays directories and files depending on user input - I am allowing the user to add directories and files to his "project" either recursively or otherwise, after which I create my own tree view of the contents of said project.
Now, my problem is that even though most of the documentation and other questions I've found on this subject seem to want to disable the editability of treeview items, I am trying ( and failing ) to find a way to enable it. What I would like is for the user to be able to double click on any cell in any column of my treeview and then edit its contents. Does anyone know how to do this?
Below is the code I am using to generate a tab in a tabView Widget, after which i then add the TreeView. The items of the TreeView are later added through the AddParent and AddChild methods.
class treeTab(QtWidgets.QWidget):
def __init__(self,core,main,label):
super (treeTab,self).__init__()
self.label = label
self.core = core
self.sizes = core.UISizes
self.tab_sizePolicy = QtWidgets.QSizePolicy(QtWidgets.QSizePolicy.Expanding,QtWidgets.QSizePolicy.Expanding)
self.tree = QtWidgets.QTreeWidget(self)
self.tree.setColumnCount(len(self.sizes.projectTreeColumnLabels))
self.tree.setHeaderLabels(self.sizes.projectTreeColumnLabels)
self.tree.setSizePolicy(self.tab_sizePolicy)
self.tree_layout = QtWidgets.QGridLayout()
self.tree_layout.objectName = self.label + "TreeGridLayout"
self.tree.setLayout(self.tree_layout)
self.treeroot = self.tree.invisibleRootItem()
self.tree.setSelectionMode(Qt.QAbstractItemView.ContiguousSelection)
def addParent(self, parent, column, title, data):
item = QtWidgets.QTreeWidgetItem(parent, [title])
item.setData(column, QtCore.Qt.UserRole, data)
item.setChildIndicatorPolicy(QtWidgets.QTreeWidgetItem.ShowIndicator)
item.setExpanded (True)
return item
def addChild(self, parent, column, title, data):
item = QtWidgets.QTreeWidgetItem(parent, [title])
item.setData(column, QtCore.Qt.UserRole, data)
item.setText(1,data.print_tags())
item.setText(2,data.category.name)
item.setText(3,data.format)
item.setCheckState (column, QtCore.Qt.Unchecked)
item.setFlags(item.flags() or QtCore.Qt.ItemIsEditable)
return item
You have confused binary operators and Boolean operators. Boolean operators (such as and and or) are used with Boolean values (such as True and False) to produce a single True or False once the expression is evaluated.
However, the flags and not Boolean values. They are integers which are powers of 2 (or binary numbers with only one bit set), such that they can be combined into a single integer that represents whether each flag is enabled or disabled. For instance, 2 is represented in binary as 0b010. 4 is represented as 0b100. If you bitwise or these together, you get 0b110, indicating that both the flag equal to 2 and the flag equal to 4 is set. However the flag equal to 1 is not set (the 0 in the 0b110).
In short, you should set the flags using the bitwise or operator (|):
item.setFlags(item.flags() | QtCore.Qt.ItemIsEditable)
So I managed to figure this one out - it turned out to be quite simple in the end :)
In order to create an 'editable' treeView item where you can double click on the text of a particular item to edit it, you simply need to change the widget contained in that particular column of the item to a QLineEdit Widget that deletes itself upon pressing enter. The code for this looks as follows:
This is the code to connect the double click event to a method that gets the currently selected item and replaces it with a QLineEdit Widget:
self.tree.itemDoubleClicked.connect(self.editItem)
def editItem(self,*args):
itm = self.tree.itemFromIndex(self.tree.selectedIndexes()[0])
column = self.tree.currentColumn()
edit = QtWidgets.QLineEdit()
edit.returnPressed.connect(lambda*_:self.project.setData(column,edit.text(),itm,column,self.tree))
edit.returnPressed.connect(lambda*_:self.update())
self.tree.setItemWidget(itm,column,edit)
Note especially the combination of the following code:
itm = self.tree.itemFromIndex(self.tree.selectedIndexes()[0])
column = self.tree.currentColumn()
This code in effect gives you both the row and the column of the currently selected item, which is useful if you want to edit column items separately.
Now You'll be asking yourself why I'm passing so many parameters to the 'setData' method: this is purely for the purposes of my particular project, so don't worry about it. the 'returnPressed' event simply needs to connect to the proper method to handle whatever data it contains, and then delete itself. in my code, this looks like this:
def setData(self,dataTypeIndex,data,item,column,tree):
if dataTypeIndex == 0:
# filename
self.name = data
elif dataTypeIndex == 1:
# tags
data = data.split(",")
self.tags = []
for tag in data:
self.tags.append(Tag(tag))
elif dataTypeIndex == 2:
# category
self.category.name = data
tree.setItemWidget(item,column,None)
This last line of code ( tree.setItemWidget(item,column,None) ) is where the QlineEdit is unparented and therefore effectively removed.

Switch items between two QListWidgets in python

i am currently designing a QT-gui in Python and i want to allow the user to switch QListWidgetItems between two QListWidgets. Multiple selection (CTRL) is allowed and switching is done by two control buttons.
In the QT4-Designer the lists looks like this
So if the user selects for example two items from the left list and clicks on the '>' Button the items have to be added to the right list and consequently deleted from the left list.
My current triggered Button-Events look like this:
def switchR( self ):
itemlistSel = self.list_left.selectedItems()
for item in itemlistSel:
self.list_right.addItem( item )
self.list_left.removeItemWidget( item )
But nothing happens? Someone got a quick solution?
The removeItemWidget() method doesn't quite do what you're expecting it to do (see docs). Use takeItem(), addItem() and row() instead:
def switch(self):
items = self.left.selectedItems()
for item in items:
n = self.left.row(item) # get the index/row of the item
i = self.left.takeItem(n) # pop
self.right.addItem(i) # add to right QListWidget

Categories

Resources