PyQt connect() call affecting multiple widgets - python

Making PushButton widgets for a program. The intent was to create each PushButton, connect it to a function which compared two string values, ap.parse_answer(), then add the PushButton to the appropriate cell of a QGridLayout:
answers = ["this", "that", "the other", "one more"]
correct_answer = "this"
for grid_pos in [(i,j) for i in range(0,2) for j in range(0,2)]:
answer_disp = AnswerDisplay()
current_answer = answers.pop()
answer_disp.setText(current_answer)
answer_disp.clicked.connect(
lambda: self.ap.parse_answer(current_answer, answer))
answer_grid.addWidget(answer_disp, *grid_pos)
Here is the AnswerDisplay class:
class AnswerDisplay(QtGui.QPushButton):
def __init__(self):
super(AnswerDisplay, self).__init__()
answer_font = QtGui.QFont()
answer_font.setWeight(24)
answer_font.setPixelSize(20)
self.setFont(answer_font)
Unfortunately, what happens is the same function gets connected to each button. The last function generated end up on all the buttons, so it seems the connect is being reapplied to previous created buttons. But how do I address this? My approach can't be completely invalid because the setText() function correctly sets the text for each button without overwriting the previous assignments.
I tried to address the issue making an single AnswerDisplay and then copying it with deepcopy():
for grid_pos in [(i,j) for i in range(0,2) for j in range(0,2)]:
disp = AnswerDisplay()
answer_disp = deepcopy(disp)
super(AnswerDisplay, answer_disp).__init__()
...
but it produced the same undesired result.
I've done some searching, but all I've found are questions from people trying to get the kind of result I'm trying not to get. Any help would be appreciated.

Your issue is that you're not capturing the values in the lambda function. Because of the way Python's scoping rules work, you're using the same value (the last one) each time.
Change the lambda line to this to capture the variables you want:
answer_disp.clicked.connect(
lambda ca=current_answer, a=answer: self.ap.parse_answer(ca, a))
There are other related questions/answers that may give you more of an explanation about this (like here)

Related

Python Canvas Click Event - Multiple Choice Answers

I apologize in advance as I've read through a few threads with almost an identical question, but I've yet to definitively find an answer to the difficulty I'm currently experiencing. I'm basically trying to set up a GUI that isn't so... like a GUI - plain and gray. Therefore, I'm using the Canvas widget, creating a background image for a little color and life, and displaying a question with four multiple choice answers.
There's no 'ENTER' button or Entry field, so I'm relying on click events to determine the selected answer. I understand I need to give the created text a tag, then bind to initiate a callback for the event. I used *.find_closest(event.x, event.y) to get the widget's tag, but it's not returning the tag I've assigned it...
In reference to the threads I've already read through, one has a parameter that apparently is not valid in Python 3.10 - I can use a tags parameter, but another post shows tag as the parameter which does not work. The end result I seem to get is a tuple with a three digit number i.e. (297,) and this is from printing the variable I assigned to the .find_closest(event.x, event.y) function.
At first I thought ... okay, if it won't acknowledge the tag I tried to assign, the numbers were sequential for a, b, c, and d which would be good enough! But no... the numbers are not necessarily the same each time I run the program.
I'm not too keen on posting my sloppy code on here, but I understand my rambling on isn't explaining the issue as well as a simple visual component would. So, to go ahead and provide that visual aid:
def update_screen(self, question, choices, answer_index):
self.correct_answer = answer_index
self.display_screen.delete('all')
self.display_screen.create_image((400, 225), image=self.background)
self.display_screen.create_text(self.QUIZ_TEXT, text=question,
fill='#EE82EE', font=self.gui_font)
a = self.display_screen.create_text(self.OPT_A_TEXT, tags='A', text=choices[0],
fill='#DA70D6', font=self.gui_font)
b = self.display_screen.create_text(self.OPT_B_TEXT, tags='B', text=choices[1],
fill='#DA70D6', font=self.gui_font)
c = self.display_screen.create_text(self.OPT_C_TEXT, tags='C', text=choices[2],
fill='#DA70D6', font=self.gui_font)
d = self.display_screen.create_text(self.OPT_D_TEXT, tags='D', text=choices[3],
fill='#DA70D6', font=self.gui_font)
self.display_screen.update()
self.display_screen.tag_bind(a, '<Button-1>', self.check_answer)
self.display_screen.tag_bind(b, '<Button-1>', self.check_answer)
self.display_screen.tag_bind(c, '<Button-1>', self.check_answer)
self.display_screen.tag_bind(d, '<Button-1>', self.check_answer)
return
Obviously, the function is just one part of the root Tk class I created, so if the full code is needed or more information is required, I apologize, and please just inform me.
To reiterate the question, I'm attempting to retrieve the selected answer from the multiple choice options displayed through the Canvas as .create_text() ... I've tried callbacks for the click events using tag bindings with both regular strings and the .create_text() assigned variable - both are represented in the code and the aforementioned threads have demonstrated both.
I only used both in the code I posted to mention that I've tried both ways ... exclusively... I still can't seem to get a valid ID or tag returned. I realize each choice has its own callback bound to the click event, so I suppose I could just use the x, y coordinates alone...
Should this be the method I use?

Connecting multiples signal/slot in a for loop in pyqt

I'm connecting multiple signal/slots using a for loop in PyQt. The code is bellow:
# Connect Scan Callbacks
for button in ['phase', 'etalon', 'mirror', 'gain']:
getattr(self.ui, '{}_scan_button' .format(button)).clicked.connect(
lambda: self.scan_callback(button))
What I expect:
Connect button phase_scan_button clicked signal to the scan_callback slot and send the string phase as a parameter to the slot. The same for etalon, mirror and gain.
What I'm getting:
For some reason my functions is always passing the string gain as parameter for all the buttons. Not sure if I'm being stupid (likely) or it is a bug.
For reference, the slot method:
def scan_callback(self, scan):
print(scan) # Here I always get 'gain'
if self.scanner.isWorking:
self.scanner.isWorking = False
self.scan_thread.terminate()
self.scan_thread.wait()
else:
self.scanner.isWorking = True
self.scan_thread.start()
getattr(self.ui, '{}_scan_button' .format(
scan)).setText('Stop Scan')
getattr(self, '_signal{}Scan' .format(scan)).emit()
My preferred way of iterating over several widgets in pyqt is storing them as objects in lists.
myButtons = [self.ui.phase_scan_button, self.ui.etalon_scan_button,
self.ui.mirror_scan_button, self.ui.gain_scan_button]
for button in myButtons:
button.clicked.connect(lambda _, b=button: self.scan_callback(scan=b))
If you need the strings "phase", "etalon", "mirror" and "gain" separately, you can either store them in another list, or create a dictionary like
myButtons_dict = {"phase": self.ui.phase_scan_button,
"etalon": self.ui.etalon_scan_button,
"mirror": self.ui.mirror_scan_button,
"gain": self.ui.gain_scan_button}
for button in myButtons_dict:
myButtons_dict[button].clicked.connect(lambda: _, b=button self.scan_callback(scan=b))
Note, how I use the lambda expression with solid variables that are then passed into the function self.scan_callback. This way, the value of button is stored for good.
Your lambdas do not store the value of button when it is defined. The code describing the lambda function is parsed and compiled but not executed until you actually call the lambda.
Whenever any of the buttons is clicked, the current value of variable button is used. At the end of the loop, button contains "gain" and this causes the behaviour you see.
Try this:
funcs = []
for button in ['phase', 'etalon', 'mirror', 'gain']:
funcs.append( lambda : print(button))
for fn in funcs:
fn()
The output is:
gain
gain
gain
gain
Extending the example, as a proof that the lambdas don't store the value of button note that if button stops existing, you'll have an error:
del button
for fn in funcs:
fn()
which has output
funcs.append( lambda : print(button))
NameError: name 'button' is not defined
As noted here : Connecting slots and signals in PyQt4 in a loop
Using functools.partial is a nice workaround for this problem.
Have been struggling with same problem as OP for a day.

Adding/Removing QSlider widgets on PyQt5

I want to dynamically change the number of sliders on my application window, in dependence of the number of checked items in a QStandardItemModel structure.
My application window has an instance of QVBoxLayout called sliders, which I update when a button is pressed:
first removing all sliders eventually in there:
self.sliders.removeWidget(slider)
And then creating a new set.
The relevant code:
def create_sliders(self):
if len(self.sliders_list):
for sl in self.sliders_list:
self.sliders.removeWidget(sl)
self.sliders_list = []
for index in range(self.model.rowCount()):
if self.model.item(index).checkState():
slid = QSlider(Qt.Horizontal)
self.sliders.addWidget(slid)
self.sliders_list.append(slid)
The principle seems to work, however what happens is weird as the deleted sliders do not really disappear but it is as they were 'disconnected' from the underlying layout.
When created, the sliders keep their position among other elements while I resize the main window.
However, once they've been removed, they occupy a fixed position and can for instance disappear if I reduce the size of the window.
Unfortunately I'm having difficulties in linking images (it says the format is not supported when I try to link from pasteboard), so I hope this description is enough to highlight the issue.
Do I have to remove the sliders using a different procedure?
EDIT
Thanks to #eyllansec for his reply, it condenses a bunch of other replies around the topic, which I wasn't able to find as I did not know the method deleteLater() which is the key to get rid of widgets inside a QLayout.
I am marking it as my chosen (hey, it's the only one and it works, after all!), however I want to propose my own code which also works with minimal changes w.r.t. to what I proposed in the beginning.
The key point here is that I was using the metod QLayout.removeWidget(QWidget) which I was wrongly thinking, it would..er..Remove the widget! But actually what it does is (if I understood it right) remove it from the layout instance.
That is why it was still hanging in my window, although it seemed disconnected
Manual reference
Also, the proposed code is far more general than what I need, as it is a recursion over layout contents, which could in principle be both other QLayout objects or QWidgets (or even Qspacer), and be organized in a hierarchy (i.e., a QWidget QLayout within a QLayout and so on).
check this other answer
From there, the use of recursion and the series of if-then constructs.
My case is much simpler though, as I use this QVLayout instance to just place my QSliders and this will be all. So, for me, I stick to my list for now as I do not like the formalism of QLayout.TakeAt(n) and I don't need it. I was glad that the references I build in a list are absolutely fine to work with.
In the end, this is the slightly changed code that works for me in this case:
def create_sliders(self):
if len(self.sliders_list):
for sl in self.sliders_list:
sl.deleteLater()
self.sliders_list = []
for index in range(self.model.rowCount()):
if self.model.item(index).checkState():
slid = QSlider(Qt.Horizontal)
self.sliders.addWidget(slid)
self.sliders_list.append(slid)
It is not necessary to save the sliders in a list, they can be accessed through the layout where it is contained. I have implemented a function that deletes the elements within a layout. The solution is as follows:
def create_sliders(self):
self.clearLayout(self.sliders)
for index in range(self.model.rowCount()):
if self.model.item(index).checkState():
slid = QSlider(Qt.Horizontal)
self.sliders.addWidget(slid)
def clearLayout(self, layout):
if layout:
while layout.count():
item = layout.takeAt(0)
widget = item.widget()
if widget:
widget.deleteLater()
else :
self.clearLayout(item.layout())
layout.removeItem(item)

Creating A UI Window For My Previous Script

folks! So, thanks to you guys I was able to figure out what it was I was doing wrong in my previous script of staggering animation for selected objects in a scene. I am now on part two of this little exercise: Creating a UI for it.
This involves creating a window with a button and user input of how much the animation will be staggered by. So, instead of me putting how much the stagger should increment by (which was two in my previous script), I'd now allow the user to decide.
The script I have so far created the window, button, and input correctly, though I am having some trouble with getting the UI to properly execute, meaning when I click on the button, no error pops up; in fact, nothing happens at all to change the scene. I get the feeling it's due to my not having my increment variable in the correct spot, or not utilizing it the right way, but I'm not sure where/how exactly to address it. Any help would be greatly appreciated.
The code I have (with suggested edits) is as follows:
import maya.cmds as cmds
spheres = cmds.ls(selection=True)
stagWin = cmds.window(title="Stagger Tool", wh=(300,100))
cmds.columnLayout()
button = cmds.button(label="My Life For Aiur!")
count = cmds.floatFieldGrp(fieldgroup, query=True, value=True)
fieldgroup = cmds.floatFieldGrp(numberOfFields=1)
cmds.button(button, edit=True, command=lambda _:stagger(fieldgroup))
cmds.showWindow(stagWin)
def stagger(fieldgroup):
for i in spheres:
cmds.selectKey(i)
cmds.keyframe(edit=True, relative=True, timeChange=count)
print "BLAH"
Moving the comments into an answer because I think I've got it all figured out finally:
First of all, the better practice is to pass the stagger object to the button command rather than the string. so that would be:
cmds.button(label="My Life For Aiur!", command=stagger)
Secondly, the count isn't getting updated, so it stays 0 as per your third line. To update that:
count = cmds.floatFieldGrp(fieldgroup, query=True, value=True)
But wait, where did fieldgroup come from? We need to pass it into the function. So go back to your button code and take out the command entirely, also saving the object to a variable:
button = cmds.button(label="My Life For Aiur!")
Now store the object for the fieldgroup when you make it:
fieldgroup = cmds.floatFieldGrp(numberOfFields=1)
Now that you have fieldgroup, you can pass that in the function for the button, like this:
cmds.button(button, edit=True, command=lambda _:stagger(fieldgroup))
I had to wrap the function in a lambda because we're passing fieldgroup, but if I just put stagger(fieldgroup) it would call that and pass the result of that into the command for the button
Also update stagger def with fieldgroup argument:
def stagger(fieldgroup):
One final note that won't actually affect this, but good to know:
when you shift the keyframes inside stagger you're using a DIFFERENT count variable than the one you declared as 0 up above. The outer one is global, and the inner is local scope. Generally it's best to avoid global in the first place, which fortunately for you means just taking out count = 0
Putting that all together:
import maya.cmds as cmds
spheres = cmds.ls(selection=True)
stagWin = cmds.window(title="Stagger Tool", wh=(300,100))
cmds.columnLayout()
button = cmds.button(label="My Life For Aiur!")
fieldgroup = cmds.floatFieldGrp(numberOfFields=1)
cmds.button(button, edit=True, command=lambda _:stagger(fieldgroup))
cmds.showWindow(stagWin)
def stagger(fieldgroup):
count = 0
increment = cmds.floatFieldGrp(fieldgroup, query=True, value=True)[0]
print count
for i in spheres:
cmds.selectKey(i)
cmds.keyframe(edit=True, relative=True, timeChange=count)
count += increment
print "BLAH"

How to update ListStrEditor in TraitsUI, or another way of displaying a list in a Python GUI?

I'm trying to learn how to use Traits for building simple UIs for my Python scripts. I want to make a programme with a list which the user can add words to with one button, or to clear the list completely with another.
So far, I've used traits:
myList = List(Str)
myList = ['Item1','Item2'] #Initial items in list
To display this list in a traits UI, I used ListStrEditor from the traits UI package:
Item('myList', show_label = False, label = 'Data Files', editor = ListStrEditor(auto_add = False)
I have coded 2 buttons:
1) one for ADDing an item to the list (myList.append('item3'))
2) a CLEAR button to empty the list (myList = []). The basic UI is good, the buttons work and my list variable is changed as expected.
However, the problem is, that my list in the GUI doesn't update. If I click on it the new values are displayed, however. Also, I want to potentially add many items to it and wonder if there is a way to add a scrollbar to the side?
I've looked up the ListStrEditor manual and usage, although I'm getting a bit bogged down with terminology. Apparently a refresh() function exists, but I'm not sure how to apply it to ListStrEditor. Also, I'm not sure if I need to use things called "adapters" or "handlers".
Any tips or pointers in the right direction would be much appreciated!
Without seeing more complete code, it is difficult to know why your code is not working. Perhaps you are missing the use of self? E.g. self.myList.append('Item3') instead of just myList.append('Item3')?
The following works for me. The display of the list updates as soon as the buttons are pressed.
import random
from traits.api import HasTraits, List, Str, Button
from traitsui.api import ListStrEditor, View, UItem
class Demo(HasTraits):
my_list = List(Str)
add = Button("ADD")
clear = Button("CLEAR")
traits_view = \
View(
UItem('my_list', editor=ListStrEditor(auto_add=False)),
UItem('add'),
UItem('clear'),
)
def _my_list_default(self):
return ['Item1', 'Item2']
def _add_fired(self):
new_item = "Item%d" % random.randint(3, 999)
self.my_list.append(new_item)
def _clear_fired(self):
self.my_list = []
if __name__ == "__main__":
demo = Demo()
demo.configure_traits()
It works with both the wx and qt4+pyside backends. I'm using the Enthought python distribution, EPD 7.3, which has version 4.2.0 of Traits and TraitsUI.
If your code is significantly different, could you add it to the question?

Categories

Resources