0

I'm almost new to making applications in Python with Tkinter and I decided to use ttk so I'm able to code nice-looking modern apps with the sun_valley theme for Windows (win11).

Optional part to read (if you want to know more about my problem): Everything was going well till I found a difference between normal Windows apps and my app which was made by ttk. The problem is that in most applications when you press buttons (or any other widgets except entries) it won't cause to rise any border (selection border as in the image) but Not in ttk-made apps! I made a function (root_focus) to focus on the root when we click somewhere on the app screen and it works well for buttons but in entries case they must stay active () until we press somewhere else (because widgets are parts of the root and when we press a button or entry it also runs the function and removes the border). So I made another function (entry_focus) to duel with the previous one and cancel that, but there was a problem with binding it to entries...

Main problem description: I used nested loops to detect entries between other widgets and bind the command (entry_focus) to them, but all function arguments seem to be set to the last entry in the loop and I don't even know why is this happening! I'd be so grateful if you could help me, Thank you in advance

(Python version: 3.11)

Code lines:

import time
import threading as thrd
import tkinter as tk
from tkinter.ttk import *

padding = {'padx': 10, 'pady': 10}


def root_focus():
    root.focus()
    print('focused to root')  # temp!


def entry_focus(ent):
    time.sleep(0.025)
    print(ent['state'])  # temp?
    if ent['state'] == 'normal':
        ent.focus()
        print(f'focused to {ent}')  # temp!


root = tk.Tk()
root.geometry('160x300')
root.resizable(False, False)
Label(root, text='Entry Focus Control').pack(side='top', **padding)
Button(root, text='test button', width=20).pack(side='top', **padding)
for i in range(4):
    Entry(root).pack(side='top', **padding, expand=True)
for widget in root.winfo_children():  # bind functions to entries
    if type(widget) == Entry:
        print(widget)  # temp!
        widget.bind('<ButtonPress>', lambda *args: thrd.Thread(target=entry_focus, args=(widget,)).start())
root.bind('<ButtonPress>', lambda *args: thrd.Thread(target=root_focus).start())  # bind function to root
root.mainloop()

Output after click each widget (from the label on the top to the last entry on the bottom respectively):

.!entry
.!entry2
.!entry3
.!entry4
focused to root
focused to root
focused to root
normal
focused to .!entry4
focused to root
normal
focused to .!entry4
focused to root
normal
focused to .!entry4
focused to root
normal
focused to .!entry4

with selection borderwithout selection border my actual project before some small changes (light) my actual project before some small changes (dark)

I added some prints to see what is happening (lines with "# temp!" comment) And that line with "# temp?" as the comment is one of the other challenges I faced while coding this project, I don't know why but if I don't print ent['state'] before using it as an if/else statement condition, Python will run the else code block always! If anyone could help me with this also, it's going to be so cool and helpful :)

smabedi
  • 3
  • 3
  • Does this answer your question? It's about the command option rather than bind, but the root problem and the solution is the same. https://stackoverflow.com/questions/10865116/tkinter-creating-buttons-in-for-loop-passing-command-arguments – Bryan Oakley Jul 20 '23 at 23:38
  • Wow, A huge thanks to Bryan Oakley and d'Elbreil Clément, both answers are so useful! – smabedi Jul 21 '23 at 10:10

1 Answers1

0

i didn't check everything but if you just want to correct this specific problem here's the answer:

The thing is that in tkinter, the "bind" method call the function binded with an "event argument" which contains some informations about the event which in your case is not usefull. So for example the basic structure of a bind event is:

widget.bind("<ButtonPress>", lambda event: your_fct())

See how event tho the "your_fct" function doesn't take any argument, you still have to put an argument in front of the lambda to recieve the argument given by the bind method. So you have to remove your *args argument which is not accurate, instead the solution is to replace:

widget.bind('<ButtonPress>', lambda *args: thrd.Thread(target=entry_focus, args=(widget,)).start())

with:

widget.bind('<ButtonPress>', lambda event, _widget = widget: thrd.Thread(target=entry_focus, args=(_widget,)).start())

See how first you put an event arg to recieve the arg of the bind method even tho it is useless in your case, then the main problem you had was that your bind was made on the widget variable but the matter is that you have to add an argument in your lambda so that the lambda knows that calling the function he'll use the widget given in arg when calling the function. Instead here the lambda call the function with widget var pointing to the widget variable at the time of the call which is wrong because when you're clicking, the last value of the widget will always be the last the widget that has been given (this is why when clicking an entry the focus is made on the last entry). In conclusion, you have to give a memory of which entry the lambda will call with an extra argument.