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 !
Related
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)]))
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.
I have two buttons on my interface. I want both of them to be able to call their respective functions when I either click on them or a hit the Enter Key.
The problem I'm having is that only the last button in the traveral focus gets activated when I hit the Enter Key, even if the preceeding one has the focus. What can I do to resolve this problem.
Useful answer are welcome and appreciated.
This is the problem in question:
from tkinter import *
w = Tk()
def startProgram(event = None):
print('Program Starting')
def readyContent(event = None):
print('Content being prepared')
# Buttons
Button(text='Prepare', command=readyContent).grid(row=10,column=2)
w.bind('<Return>',readyContent) # Binds the Return key to a Function
Button(text='Start', command=startProgram).grid(row=10,column=3)
w.bind('<Return>',startProgram) # Binds the Return key to a Function
w.mainloop()
When you click on the Prepare or Start button, in return you get either Content being prepared or Program Starting repectively. Nothing like that happens when you use the Tab Key to give focus to one button or the other. Even if the focus is on the Prepare button, when you hit Enter you get: Program Starting
This is the solution to my problem.
I hope it helps anyone else having the same problem as me.
from tkinter import *
w = Tk()
def startProgram(event = None):
print('Program Starting')
def readyContent(event = None):
print('Content being prepared')
# Buttons
btn1 = Button(text='Prepare', command=readyContent)
btn1.grid(row=10,column=2)
btn1.bind('<Return>',readyContent) # Binds the Return key to a Function
btn2 = Button(text='Start', command=startProgram)
btn2.grid(row=10,column=3)
btn2.bind('<Return>',startProgram) # Binds the Return key to a Function
w.mainloop()
Have a good day! :)
I want to get the cursor position (line and column) of the insertion point of a Tkinter.Text, but for the specific situation below.
PROBLEM: My text editor project requires a custom undo/redo for Tkinter.Text. I put in the same string for both Test One and Test Two below, but undo does not act consistently due to a inconsistent column variable in KeyRelease event handler given by Tkinter. The problem seems to be that I type too fast for second test which produces a bad column value. Can you help me find the problem?
TWO TEST PROCESS TO REPRODUCE THE ERROR:
TEST ONE
Type this string slowly: 'one two three'
Press F1 to see each word undo.
Result: Works fine. (For me atleast. Ephasis: type slowly.)
TEST TWO
Type the same string as fast as you can: 'one two three'
Press F1 to see each word undo.
Result: Gets the wrong column and does not undo properly. (Restart script and repeat this step if you don't see the error at first, it sometimes works fine with fast typing. I usually get it with 3 to 4 tries at the most.)
QUESTION: Is this a bug in Tkinter, or am I not understanding something specific within Tkinter that would produce consistent columns for my undo/redo records?
from Tkinter import *
class TextView(Text):
def __init__(self, root):
Text.__init__(self, root)
self.history = History(self)
self.bind("<KeyRelease>", self.keyRelease)
# used to capture a char at a time in keyRelease. If space char is pressed it creates a Word object and adds it to undo/redo history.
self.word = ""
def keyRelease(self, event):
if event.keysym == "space":
self.word += " "
self.makeWordRecord()
else:
self.word += event.char
def makeWordRecord(self, ):
if len(self.word):
index = self.index(INSERT)
wordEvent = Word(index, self.word)
self.history.addEvent(wordEvent)
self.word = ""
def undo(self, event):
self.makeWordRecord()
self.history.undo()
def redo(self, event):
self.history.redo()
class History(object):
def __init__(self, text):
self.text = text
self.events = []
self.index = -1
# create blank document record, line one, column one, no text
self.addEvent(Word("1.0", ""))
def addEvent(self, event):
if self.index +1 < len(self.events):
self.events = self.events[:self.index +1]
self.events.append(event)
self.index +=1
def undo(self):
if self.index > 0:
self.events[self.index].undo(self.text)
self.index -=1
def redo(self):
if self.index +1 < len(self.events):
self.index +=1
self.events[self.index].redo(self.text)
class Word(object):
def __init__(self, index, word):
self.index = index
self.word = word
def undo(self, text):
line = self.index.split(".")[0]
column = int(self.index.split(".")[-1])
startIndex = line + "." + str(column - len(self.word))
endIndex = line + "." + str(int(column))
text.delete(startIndex, endIndex)
def redo(self, text):
line = self.index.split(".")[0]
column = int(self.index.split(".")[-1])
startIndex = line + "." + str(column - len(self.word))
text.insert(startIndex, self.word)
if __name__ == "__main__":
root = Tk()
root.geometry("400x200+0+0")
textView = TextView(root)
textView.pack()
root.bind("<F1>", textView.undo)
root.bind("<F2>", textView.redo)
root.mainloop()
I finally figured out what was going on and has nothing to do with Tkinter, but all Toolkits. I can now honestly say that I can add something to Best Programming Practices and :
Do Not Process Key Events by Binding to a Key Release Method, Instead Process Keys with a Key Press Method
Why?
It's not really a programming issue, it's a hardware issue. When a key is pressed, the physical key goes down. There is a spring that pushes the key back up. If that spring or anything about your keyboard causes a key to be even 200ths of second slower, typing even 60 words a minute may cause keys that were typed in one order to come back in another order. This may happen because a spring may be slightly thicker, stiffer, over used, or even sticky mocha cause more resistance.
Capitalization can be affected as well. Pressing the shift key and another key to get an upper case must be simultaneously pressed down. But if you process keys on key release, it is possible the shift key springs up faster than the character key you are capitalizing, which will result in a lower case. This also allows characters to be inverted. If you check on positions of a character when it's typed, you can get the wrong position due to this as well.
Stick with Key Press.
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.