pyside / pyqt: Getting values from dynamically created qlineedits on button clicked - python

I have a program that creates a number of qlineedits and buttons depending on the users input:
On the image above 4 lines have been added with a button after the grayed out "Next" button was clicked. Now I want to get the input from the user into a function when the corresponding button is clicked (Click "Create Shot 1! --> goto a function with "exShot1" passed as an argument).
The thing is I have no idea how to get the names of each qline and button when they are created in a loop. I guess I could create unique variables in the loop but that doesn't feel right. I have tried using setObjectName but I can't figure out how I can use that to call the text. I also made an unsuccessful attempt with Lamdba (which I have a feeling might be the right way to go somehow) I believe it's a combination of having to fetch the name and tracking when the user input is changed.
I have experimented with textChanged and I got it to work on the last entry of the loop but not for the other qlines and buttons)
Relevant code:
while i <= int(seqNum):
#create each widget
self.createShotBtn = QtGui.QPushButton("Create Shot %s!" %str(self.shotNumberLst[i-1]))
self.labelName = QtGui.QLabel(self)
self.labelName.setText("Enter Name Of Shot %s!" %str(self.shotNumberLst[i-1]))
self.shotName = QtGui.QLineEdit(self)
self.shotName.setObjectName("shot"+str(i))
#add widget to layout
self.grid.addWidget(self.labelName, 11+shotjump,0)
self.grid.addWidget(self.shotName,11+shotjump,1)
self.grid.addWidget(self.createShotBtn, 11+shotjump,2)
#Press button that makes magic happen
self.createShotBtn.clicked.connect(???)
i += 1
edit: It would also be fine if the user entered input on all the lines and just pressed one button that passed all those inputs as a list or dict (there will be more lines added per "shot")

The problem is that on each run through the values of self.createShotBtn, self.labelName and self.shotName are being overridden.
So on the last run through, they are fixed, but only for the last iteration.
Instead, you want to use a locally scoped variable in the loop, and potentially store it in an array for later use.
This code should come close to what you need, but I can see where self.shotNumberLst (which returns a number?) and shotjump (which is an offest, or equal to to i) are declared.
self.shots = []
for i in range(seqNum): # Changed while to for, so you don't need to increment
#create each widget
createShotBtn = QtGui.QPushButton("Create Shot %s!" %str(self.shotNumberLst[i-1]))
labelName = QtGui.QLabel(self)
labelName.setText("Enter Name Of Shot %s!" %str(self.shotNumberLst[i-1]))
shotName = QtGui.QLineEdit(self)
self.shots.append({"button":createShotBtn,
"name":shotName)) # Store for later if needed.
#add widget to layout
self.grid.addWidget(labelName, 11+shotjump,0)
self.grid.addWidget(shotName,11+shotjump,1)
self.grid.addWidget(createShotBtn, 11+shotjump,2)
#Press button that makes magic happen
createShotBtn.clicked.connect(self.createShot(i))
#elsewhere
def createShot(self,index):
print self.shots[index]["name"].text

Try this,
while i <= int(seqNum):
#create each widget
createShotBtn = "ShotBtn"+str(i)
self.createShotBtn = QtGui.QPushButton("Create Shot %s!" %str(self.shotNumberLst[i-1]))
labelName = "labName"+str(i)
self.labelName = QtGui.QLabel(self)
self.labelName.setText("Enter Name Of Shot %s!" %str(self.shotNumberLst[i-1]))
shotName = "shtName"+str(i)
self.shotName = QtGui.QLineEdit(self)
#add widget to layout
self.grid.addWidget(self.labelName, 11+shotjump,0)
self.grid.addWidget(self.shotName,11+shotjump,1)
self.grid.addWidget(self.createShotBtn, 11+shotjump,2)
#Press button that makes magic happen
self.createShotBtn.clicked.connect(self.printText)
i += 1
def printText(self):
print(self.shotName.text())
This will print the text when you push the button on the same line.

Related

Tkinter : Text highlight appears while pressed but disappear next

I've been working on a Tkinter (Python) project that displays a list of strings using the Text widget recently, but I ran into an issue I couldn't manage to solve :
On startup, I want the first line to be highlighted, and when I click on up/down arrows, the highlight goes up/down, as a selection bar.
I succeed to do that, but the problem is that the highlight only appears when arrows are pressed, and when they are released, it disappear. I'd like it to stay even when I'm not pressing any key.
Here is my code :
class Ui:
def __init__(self):
# the list I want to display in Text
self.repos = repos
# here is the entry keys are bind to
self.entry = Entry(root)
self.entry.pack()
self.bind('<Up>', lambda i: self.changeIndex(-1))
self.bind('<Down>', lambda i: self.changeIndex(1))
# here is the Text widget
self.lists = Text(root, state=NORMAL)
self.lists.pack()
# inits Text value
for i in self.repos:
self.lists.insert('insert', i + '\n')
self.lists['state'] = DISABLED
# variable I use to navigate with highlight
self.index = 0
self.lists.tag_add('curr', str(self.index) + '.0', str(self.index + 1) + '.0') # added + '.0' to make it look '0.0' instead of '0'
self.lists.tag_config('curr', background='#70fffa', background='#000000')
self.root.mainloop()
def changeIndex(self, n):
# error gestion (if < 0 or > len(repos), return)
self.lists.tag_delete('curr')
self.lists.tag_add('curr', str(self.index) + '.0', str(self.index + 1) + '.0')
self.index = self.index + n
# to make it scroll if cannot see :
self.lists.see(str(self.index) + '.0')
I haven't seen any similar problem on Stack, so I asked, but do not hesitate to tell me if it is a duplicate.
Do you guys could help me please ? Thanks !
EDIT: Here is the full code if you want to give it a try : https://github.com/EvanKoe/stack_tkinter.git
EDIT : I added the main.py file (the """backend""" file that calls ui.py) to the demo repository. This way, you'll be able to run the project (be careful, there are "YOUR TOKEN" and "YOUR ORGANIZATION" strings in main.py you'll have to modify with your own token/organization. I couldn't push mine or Github would've asked me to delete my token)
The following code should do what you expect. Explanation below code
from tkinter import *
repos = ["one","two","three","four"]
class Ui:
def __init__(self, parent):
# the list I want to display in Text
self.repos = repos
# here is the entry keys are bind to
self.entry = Entry(parent)
self.entry.pack()
self.entry.bind('<Up>', lambda i: self.changeIndex(-1))
self.entry.bind('<Down>', lambda i: self.changeIndex(1))
# here is the Text widget
self.lists = Text(parent, state=NORMAL)
self.lists.pack()
# inits Text value
for i in self.repos:
self.lists.insert('insert', i + '\n')
self.lists['state'] = DISABLED
# variable I use to navigate with highlight
self.index = 1
self.lists.tag_add('curr', str(self.index) + '.0', str(self.index + 1) + '.0') # added + '.0' to make it look '0.0' instead of '0'
self.lists.tag_config('curr', background='#70fffa', foreground='#000000')
def changeIndex(self, n):
print(f"Moving {n} to {self.index}")
self.index = self.index + n
self.index = min(max(self.index,0),len(self.repos))
self.lists.tag_delete('curr')
self.lists.tag_config('curr', background='#70fffa', foreground='#000000')
self.lists.tag_add('curr', str(self.index) + '.0', str(self.index + 1) + '.0')
# to make it scroll if cannot see :
self.lists.see(str(self.index) + '.0')
root = Tk()
ui = Ui(root)
root.mainloop()
Few changes made to your code
Changed the Ui function to accept the parent tk object as a parameter
Changed self.index to be initialised to 1 rather than 0 since the first line on a text box is 1 not 0
Bound the Up/Down keys to the entry box. Not sure why this is what you are going for but this seems to be what your comments indicate
Added some checking code to limit the index value between 1 and len(repos)
Re-created the tag style each time it is set since you delete the tag (this is why it wasn't showing)
I'd suggest that you look to bind the up/down button press to the text box rather than the entry box. Seems a bit strange to have to select a blank entry box to scroll up and down in a list.
Also, why aren't you just using the build in Tkinter list widget?
I finally managed to solve the problem, and it was due to my event bindings. I made the decision (to improve the UX) to bind up/down arrows on the top Entry instead of binding em on the Text widget. I bind 4 events :
Up arrow => move highlight up,
Down arrow => move highlight down,
Return key => calls get_name(), a function that returns the selected option,
Any other Key => calls repo_filter(), a function that updates the displayed options in the Text widget, according to what has been typed in the Entry.
The problem was that pressing the up/down arrow was triggering "up/down key" event AND "any other key" event, so the highlight was removed since the Text value was refreshed.
To solve this problem, I just had to verify that the pressed key was neither up nor down arrow in the "any other key" event callback :
def repo_filter(evt):
if evt.keysym == 'Up' or evt.keysym == 'Down': # verify that pressed key
return # isn't '<Down>' or '<Up>'
# filter Text widget
Also, I am sorry I didn't give you all the code at the beginning, because, indeed you couldn't guess about those event bindings.
Thanks to everyone who tried to help me !

Pass dynamically created button information to a function when they are pressed. kivymd, kivy, python

I am trying to delete information about the button when the user presses the trash can on the button.
My problem is that when the user presses the trash can of any button, only the information of the button that is lastly created gets passed to the function, and therefore only the last created button get deleted instead of the one of the button that is pressed.
Please see the picture below.
picture
docs = users_ref.collection(u'Education').stream()
education_lst = []
education_btn = []
for doc in docs:
dict = doc.to_dict()
education_lst.append(dict['Graduation'])
primary = str(dict['University'])
secondary = str(dict['Degree']) + ' in ' + str(dict['Major'])
tertiary = 'Graduation year: ' + dict['Graduation']
btn = ThreeLineAvatarIconListItem(text=primary, secondary_text=secondary, tertiary_text=tertiary)
education_btn.append(btn)
for btn in education_btn:
pic = IconRightWidget(icon='trash-can')
pic.bind(on_release=lambda *args: Education().delete(education_lst[education_btn.index(btn)]))
btn.add_widget(pic)
sm.get_screen('profile').ids.profile_grid.add_widget(btn)
That's a common problem when defining a lambda function in a loop. The loop variable used in the lambda function isn't evaluated until the lambda is actually executed. So, in your case, the btn argument ends up being the last value of btn. A fix is to use a new variable to hold the value of the loop variable, like this:
pic.bind(on_release=lambda *args, x=btn: self.delete(education_lst[education_btn.index(x)]))

How to make a cursor in a PyQt5 label

I have a PyQt5 window that has a label with text in it.
The text that appears there is from a python string variable. The string is built by the user clicking on on-screen pushbuttons and then the string is outputted to the window in that label.
As of now I have a backspace button which deletes the last character in the string, but I would also like the user to be able to click on a spot in front of a character in the label, and then be able to delete that character.
So, I would like to know how to do two things.
how to get the character location in the string based on the user click
I've seen some examples for this, but I'd like to also show a cursor in that spot once the user has clicked there.
I would like to do this with a label widget - not with a text input field.
Anyone have any ideas?
Making a QLabel work like that is hard (QLabel is more complex than it looks).
It is possible to show the cursor after clicking, and that's achieved by setting the textInteractionFlags property:
self.label.setTextInteractionFlags(
QtCore.Qt.TextSelectableByMouse | QtCore.Qt.TextSelectableByKeyboard)
The first flag is to allow handle of mouse events, while the second allows displaying the cursor as soon as the label has focus (for instance, after clicking it).
Unfortunately, this doesn't allow you to get the cursor position (nor to change it); there are ways (using QFontMetrics and QTextDocument), but you need a complex implementation in order to make it really reliable.
The solution is to use a QLineEdit and override the keyPressEvent, which is the function that is always called on a widget whenever a key press happens (and it has input focus). Considering that number input seems still required, just ensure that the event.key() corresponds to a Qt.Key enum for numbers, and in that case, call the base implementation.
You can even make it look exactly like a QLabel by properly setting its stylesheet.
class CommandLineEdit(QtWidgets.QLineEdit):
allowedKeys = (
QtCore.Qt.Key_0,
QtCore.Qt.Key_1,
QtCore.Qt.Key_2,
QtCore.Qt.Key_3,
QtCore.Qt.Key_4,
QtCore.Qt.Key_5,
QtCore.Qt.Key_6,
QtCore.Qt.Key_7,
QtCore.Qt.Key_8,
QtCore.Qt.Key_9,
)
def __init__(self):
super().__init__()
self.setStyleSheet('''
CommandLineEdit {
border: none;
background: transparent;
}
''')
def keyPressEvent(self, event):
if event.key() in self.allowedKeys:
super().keyPressEvent(event)
Then, if you want to set the text programmatically, also based on the cursor, here's a basic usage:
from functools import partial
class CommandTest(QtWidgets.QWidget):
def __init__(self):
super().__init__()
layout = QtWidgets.QVBoxLayout(self)
self.commandLineEdit = CommandLineEdit()
layout.addWidget(self.commandLineEdit)
keys = (
'QWERTYUIOP',
'ASDFGHJKL',
'ZXCVBNM'
)
backspaceButton = QtWidgets.QToolButton(text='<-')
enterButton = QtWidgets.QToolButton(text='Enter')
self.shiftButton = QtWidgets.QToolButton(text='Shift', checkable=True)
for row, letters in enumerate(keys):
rowLayout = QtWidgets.QHBoxLayout()
rowLayout.addStretch()
layout.addLayout(rowLayout)
for letter in letters:
btn = QtWidgets.QToolButton(text=letter)
rowLayout.addWidget(btn)
btn.clicked.connect(partial(self.typeLetter, letter))
rowLayout.addStretch()
if row == 0:
rowLayout.addWidget(backspaceButton)
elif row == 1:
rowLayout.addWidget(enterButton)
else:
rowLayout.addWidget(self.shiftButton)
spaceLayout = QtWidgets.QHBoxLayout()
layout.addLayout(spaceLayout)
spaceLayout.addStretch()
spaceButton = QtWidgets.QToolButton(minimumWidth=200)
spaceLayout.addWidget(spaceButton)
spaceLayout.addStretch()
backspaceButton.clicked.connect(self.commandLineEdit.backspace)
spaceButton.clicked.connect(lambda: self.typeLetter(' '))
def typeLetter(self, letter):
text = self.commandLineEdit.text()
pos = self.commandLineEdit.cursorPosition()
if not self.shiftButton.isChecked():
letter = letter.lower()
self.commandLineEdit.setText(text[:pos] + letter + text[pos:])
import sys
app = QtWidgets.QApplication(sys.argv)
w = CommandTest()
w.show()
sys.exit(app.exec_())
As you see, you can call backspace() in order to clear the last character (or the selection), and in the typeLetter function there are all the remaining features you required: getting/setting the text and the cursor position.
For anything else, just study the full documentation.

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
#...

Tkinter, saving functions to a list and then running them

I'm working on a GUI for a project in school. All the buttons that I have in my GUI are bound with functions that I have created. These functions call for already predefined functions. For some of the predefined functions, I need one or two arguments and I have solved that with entries. I type in the arguments in the right entries that are connected to the specific button and when I press the button, the function will run with the corresponding arguments.
The thing I want to do is to in some way when I press a button, the function should be saved to a list instead of being executed right away. And when I push the "run" button(a new button that I will create) everything in my list will be executed. I have been thinking about using a list box but I don't know exactly how they work or if its even possible to run a list box that contains a number of functions. Does someone have any ideas or solutions for me? Can I use the list box for this or is there something else that is better to use?
class App:
def __init__(self, master):
frame = Frame(master)
frame.pack()
self.entry1 = IntVar()
self.entry2 = IntVar()
def do_something():
value1 = self.entry1.get()
value2 = self.entry2.get()
self.listbox.insert(END, "predefined_function(value1, value2)")
def run_listbox_contents():
pass
self.button = Button(frame, text="Move", command=lambda: do_something())
self.button.pack(side=TOP)
self.entry1.set("value1")
self.entry = Entry(frame, textvariable=self.entry1)
self.entry.pack(side=TOP)
self.entry2.set("value2")
self.entry = Entry(frame, textvariable=self.entry2)
self.entry.pack(side=TOP)
self.listbox = Listbox(master)
self.listbox.pack(side=TOP)
root = Tk()
app = App(root)
root.title("Mindstorms GUI")
root.geometry("800x1200")
root.mainloop()
root.destroy()
Just use a standard list.
something like this
def hest(txt):
print "hest: " +txt
def horse(txt):
print "horse: " + txt
funcList = []
funcList.append(hest)
funcList.append(horse)
for x in funcList:
x("Wow")
This outputs
hest: Wow
horse: Wow
Was this what you wanted?
If I were you, I wouldn't want to save functions to a list. I would suggest another solution for you.
I suppose you have heard of the principle of MVC (Model-View-Controller). In your case, the list box is a part of view, and the process that saves functions and then calls them at once is a part of controller. Separate them.
You might want to save and display any string in the list box to let the users know that the corresponding functions have been enlisted and ready to run. For example, save a string "Function1 aug1 aug2 aug3" or "Funtion2 aug1 aug2" or whatever you like as a handle of the corresponding function.
And for the controller part, write a function (let's say conductor()). It reads the handle strings from the list, parses them and calls the corresponding functions. Where you want to run the enlisted functions, there you just call conductor().
Update:
Due to your comment I understand that you are pretty new to program. Let me show you how to write a simplest parser with your given variable names.
def run_listbox():
to_do_list = #get the list of strings
for handle_string in to_do_list:
#Let's say you got
#handle_string = "Predfined_function1 value1 value2"
#by here
handle = handle_string.split(" ")
#Split the string by space, so you got
#handle = ["Predfined_function1", "value1", "value2"]
#by here
if handle[0] == "Predfined_function1":
Predfined_function1(handle[1], handle[2]) #Call Predfined_function1(value1, value2)
elif handle[0] == "Predfined_function2":
Predfined_function2(handle[1], handle[2])
#elif ...
#...
#elif ...
#...
#elif ...
#...
This is not a perfect parser, but I hope it could let you know what does a parser look like.

Categories