How to disable Entry input until double-clicked? - python

I'm developing a GUI using python 3.6 but I need the user to double-click on a tkinter Entry widget to allow input (to prevent modification of fields by accident), instead of just press any key to input text.
Is there any way to prevent the user input by keystroke until double
click on Entry?
I've tried by first to override events with the methods below to unbind keystrokes but none of them worked, so re-definition of new bind (double click) is not yet implemented.
Entry.unbind_all('<Key>')
Entry.unbind_all('<KeyPress>')
Entry.unbind_all('<KeyRelease>')

One simple way of doing preventing the user input by keystrokes until a double-click, is simply manipulating the state of an Entry with double click and focus out events. As in by default every widget is read-only, when double-clicked a single one gets enabled, and when lost focus it's read-only again:
try: # In order to be able to import tkinter for
import tkinter as tk # either in python 2 or in python 3
except ImportError:
import Tkinter as tk
def on_double_click(widget):
widget['state'] = 'normal'
def on_lose_focus(widget):
widget['state'] = 'readonly'
def main():
root = tk.Tk()
entries = list()
for i in range(3):
entries.append(tk.Entry(root, state='readonly'))
entries[-1].bind('<Double-Button-1>',
lambda e, w=entries[-1]: on_double_click(w))
entries[-1].bind('<FocusOut>',
lambda e, w=entries[-1]: on_lose_focus(w))
entries[-1].pack()
tk.mainloop()
if __name__ == '__main__':
main()

Related

Python 3, Keyboard module -How to stop the default action when adding a hotkey (workaround found)

I'm trying to overwrite an existing keyboard function on the enter key with a custom hotkey. The problem is, I cannot stop the default action from occurring also. Worse yet, it occurs after the custom action, so I don't have the chance to retroactively correct it as well.
Here are the relevant parts of the code:
from tkinter import *
from tkinter import ttk
from keyboard import *
root = Tk()
root.geometry('400x300')
'''This is the problematic function called by the hotkey'''
def entr_key(text):
'''Enter key results in printing of the line to the right of cursor'''
curs_pos = text.index(INSERT)
right_hand = text.get(curs_pos,END)
right_hand = right_hand.split('\n')[:1] #lines -> strs in a list, selects the first
print (right_hand)
return
'''THIS IS THE MAIN WIDGET FOR THIS FRAME'''
text_view = ttk.Frame(root, padding=10)
text_view.grid()
text_box = Text(text_view, width=45, wrap=WORD)
text_box.grid(column=0, row=1)
text_box.insert('1.0', 'This is a Text widget demo,\nThis text is aimed at testing the enter key function')
add_hotkey("enter", lambda: entr_key(text_box))
root.mainloop()
(I've changed up some variable names to be more understandable, apologies if I missed any but it's not the source of the problem!)
I've also (unsuccesfully) tried other ways of doing this, eg:
while True:
if is_pressed('enter'):
entr_key(text_box)
'''AND ALSO'''
on_press_key("enter", lambda x=None: entr_key(text_box))
Just to be clear, I don't want the default action of enter key moving the text to a new line.
I need either a way to "break" the key event so that only the custom action takes place, or a way for the custom action to occur after default action, so I can retroactively edit it out
EDIT!!!
I've found a workaround: at the start of entr_key() I call time.sleep(0.01). During this, the default action of the enter key occurs first, and I can retroactively edit it out when the custom function resumes. The delay is slight enough to not be noticeable at all.
But still, if anyone knows how to prevent the default action from occurring completely, I would really appreciate it.

Combobox enable ctrl+a to select text

I would like to enable ctrl+a to select the text within a combobox. Instead of selecting all it does <end> (more or less at least).
minimal example:
#!/usr/bin/env python3
import tkinter as tk
from tkinter import ttk
from tkinter.messagebox import showinfo
root = tk.Tk()
def month_changed(event):
msg = f'You selected {month_cb.get()}!'
showinfo(title='Result', message=msg)
# month of year
months = ['Jan', 'Feb']
# create a combobox
selected_month = tk.StringVar()
month_cb = ttk.Combobox(root, textvariable=selected_month)
month_cb['values'] = months
month_cb.pack(fill='x', padx=5, pady=5)
month_cb.bind('<<ComboboxSelected>>', month_changed)
month_cb.bind('<Control-a>', doSomeSelecting) #doSomeSelcting tbd
root.mainloop()
I stole the example and minimized it from here to get a quick example: https://www.pythontutorial.net/tkinter/tkinter-combobox/
So what you are doing is overriding the default bindings for your platform. On X11 Tk sets up a default binding for Control-Key-slash to generate the <<SelectAll>> virtual event. On Win32 this is extended with Control-Key-a as well. On X11 Control-a is bound to <<LineStart>>.
So the platfrom correct thing is to leave it alone and learn to use Control-slash to select all. To override this you need to bind Control-a to a function that generates the SelectAll virtual event and also prevents the default event handler from then moving the insertion point to the start of the line. For that:
def selall(ev):
ev.widget.event_generate('<<SelectAll>>')
return 'break'
month_cb.bind('<Control-a>', selall)
The return 'break' is important here otherwise the event handlers will continue being called and our selection will be undone when something generates the <<LineStart>> event after our <<SelectAll>>.
This can be investigated in IDLE using month_cb.bindtags() to find that it's class bindings are TCombobox. Then month_cb.bind_class('TCombobox') to see all the events that are bound to this class. For the virtual events, root.event_info('<<SelectAll>>') shows the set of events that cause this virtual event to be raised.

Python: How do you obtain variables from another script which are constantly being updated

I've made a script that uses a while True loop to constantly update a series of variables based on UDP packets that I am constantly recieving. I want to ultimately create a GUI that displays that data and updates the screen constantly, which I plan to do with tkinter (using my_label.after in a function which then calls itself, not sure if this is a good plan).
Here is some testing scripts that I can't get to work properly:
GUI2.py (my test looping script)
import time
var = 0
while True:
var += 1
time.sleep(0.1)
GUI Testing.py (the script that would be accessing those variables)
from GUI2 import *
import time
print('never')
print(var)
time.sleep(1)
The second script never reaches the print('never') line, I think because it gets stuck in the other script's while True loop and never returns.
How should I go about this? I have one script that I want in a constant loop to update my variables to the correct values based on incoming packets, and then another script updating a tkinter window. I went this way as most examples I could find using Tkinter didn't use any sort of while True loops. Could I just put my packet recieving code inside the Tkinter mainloop, and would that effectively act as a while True?
EDIT (added Tkinter loop that I can't get working):
This opens a Tkinter window, but the label stays at 99, then reopens a window when I close it with the new x value (ie. 98, 97, etc). I want the label to update every second.
import tkinter as tk
import time
x = 99
while True:
root = tk.Tk()
label = tk.Label(root, text=x)
label.pack()
x -= 1
time.sleep(1)
root.mainloop()
Below is a sample script to show you how you can update the value in the label widget at a certain time interval. I have provided you the hyperlinks to help you understand tkinter's methods. Best regards.
Key points:
use the textvariable option of the tk.Label widget.
use tkinter's control variable. I have shown you how to set and get it's value.
you can use tkinter's widget method called .after() without having to explicitly use a while-statement and time.sleep() method. Tkinter has it's own event loop that you can use.
writing your tkinter GUI as a class makes it easier to implement what you need.
Example Script:
import tkinter as tk
class App(tk.Frame):
def __init__( self, master, *args, **kw ):
super().__init__( master )
self.master = master
self.create_label()
self.update_label()
def create_label( self ):
self.var = tk.IntVar() # Holds an int; default value 0
self.label = tk.Label(self, textvariable=self.var ) # Use textvariable not text
self.label.pack()
def update_label( self ):
value = self.get_value()
self.var.set( value ) # Set the label widget textvariable value.
self.after(1000, self.update_label) # Call this method after 1000 ms.
def get_value( self ):
'''To simulate calling a function to return a value'''
value = self.var.get() + 1
return value
if __name__ == "__main__":
root = tk.Tk()
root.geometry('100x100+0+24')
app = App( root )
app.pack()
root.mainloop() #This command activates tkinter's event loop
Edit:
As a clarification, this answer shows how to utilize the .after() and .mainloop() methods in GUI Testing.py, i.e. using tkinter event loop and not use two while-loops, to achieve what you wanted to do. This is a way to simplify your GUI script.
For more sophisticated algorithms, e.g. more than one while-loop is involved, you have to look into using threads(note it has its issues) or more recently I found a way of using python's Asyncio approach to do it. The learning curve for these two approaches is a lot steeper. To use the asyncio approach, you can explore modifying my answer to do what you want.
Best solution is to use threads however If you plan to do in simplest possible manner then implement the main loop inside your Tkinter GUI and once you read the packet simply update it on your GUI in same loop. Here is the Updated and working Code.
import tkinter as tk
import time
def setvalue(self, x):
self.label.config(text=x, )
root.update()
time.sleep(1)
def changevalues(self):
x = 99
self.label = tk.Label(root, text=x)
self.label.pack()
while x >0:
x -= 1
setvalue(root,x)
root = tk.Tk()
changevalues(root)
root.mainloop()

window freezes in Python 3

I am using python3 on a mac and run scripts with the IDLE which comes automatically with the python3 installation.
I am trying to make an alert to the user and found the command
tkinter.messagebox.showinfo("title","some text")
So I i tried a minimal script to check if I can get along with that command
import tkinter
tkinter.messagebox.showinfo("test" , "blabla")
The window is displayed correctly but it doesn't respond when I click on the "OK" button.
Addtionally there is a second empty window which appears when I start the script.
What is the explanation for this or at least how can I fix that?
tkinter isn't designed to work this way. Every tkinter requires a root window. If you don't explicitly create one (and you didn't), one will be created for you. That's what the blank window is.
Also, a tkinter GUI can't function properly unless it has a running event loop. This is necessary because some functions, such as responding to buttons and redrawing the window, only happens in response to events. If the event loop isn't running, events can't be processed.
Bottom line: the dialogs aren't designed to be used outside of the context of a proper tkinter app.
Wrapper for standalone use
The following code can be used to display one of the dialogs in standalone mode. It works by creating and hiding a root window, displaying the dialog, and then destroying the root window.
import tkinter as tk
from tkinter import messagebox
def show_dialog(func, *args, **kwargs):
# create root window, then hide it
root = tk.Tk()
root.withdraw()
# create a mutable variable for storing the result
result = []
# local function to call the dialog after the
# event loop starts
def show_dialog():
# show the dialog; this will block until the
# dialog is dismissed by the user
result.append(func(*args, **kwargs))
# destroy the root window when the dialog is dismissed
# note: this will cause the event loop (mainloop) to end
root.destroy()
# run the function after the event loop is initialized
root.after_idle(show_dialog)
# start the event loop, then kill the tcl interpreter
# once the root window has been destroyed
root.mainloop()
root.quit()
# pop the result and return
return result.pop()
To use it, pass the dialog you want as the first option, followed by dialog-specific options.
For example:
result = show_dialog(messagebox.askokcancel, "title", "Are you sure?")
if result:
print("you answered OK")
else:
print("you cancelled")

Button behaviour

I work with Python 3.5 and TKinter.
I defined a label and file dialog that updates this label.
A button is responsible to launch this dialog.
self.sel_folder_val = the label that will be updated.
The code:
self.sel_folder_val['text']=filedialog.askdirectory()
After pressing the button in order to launch this dialog, the button stays pressed. Any dialog that a button is responsible to open cause the button to stay low (pressed) after closing this dialog.
I have tried this also with no help...:
self.select_folder_btn.config(relief=RAISED)
Code example:
self.select_folder_btn = Button(self.top)
self.select_folder_btn.place(relx=0.07, rely=0.57, height=34, width=187)
self.select_folder_btn.configure(activebackground="#d9d9d9")
self.select_folder_btn.configure(activeforeground="#000000")
self.select_folder_btn.configure(background="#d9d9d9")
self.select_folder_btn.configure(disabledforeground="#a3a3a3")
self.select_folder_btn.configure(font=self.font3)
self.select_folder_btn.configure(foreground="#000000")
self.select_folder_btn.configure(highlightbackground="#d9d9d9")
self.select_folder_btn.configure(highlightcolor="black")
self.select_folder_btn.configure(pady="0")
self.select_folder_btn.configure(text='''Select destination folder''')
self.select_folder_btn.bind('<Button-1>',self.update_folder_value)
def update_folder_value(self,event):
self.sel_folder_val['text']=filedialog.askdirectory()
return
After executing update_folder_value() function, self.select_folder_btn stays down.
I used the command:
self.select_folder_btn.configure(command=self.update_folder_value)
Instead of bind:
self.select_folder_btn.bind('<Button-1>',self.update_folder_value)
It solved my problem.
Thanks
First for future reference this is a minimal working example:
from Tkinter import *
import tkFileDialog as filedialog
class app:
def __init__(self):
self.top = Tk()
self.select_folder_btn = Button(self.top)
self.select_folder_btn.place(relx=0.07, rely=0.57, height=34, width=187)
self.select_folder_btn.configure(activebackground="#d9d9d9")
self.select_folder_btn.configure(activeforeground="#000000")
self.select_folder_btn.configure(background="#d9d9d9")
self.select_folder_btn.configure(disabledforeground="#a3a3a3")
#self.select_folder_btn.configure(font=self.font3)
self.select_folder_btn.configure(foreground="#000000")
self.select_folder_btn.configure(highlightbackground="#d9d9d9")
self.select_folder_btn.configure(highlightcolor="black")
self.select_folder_btn.configure(pady="0")
self.select_folder_btn.configure(text='''Select destination folder''')
self.select_folder_btn.configure(command=self.update_folder_value)
self.sel_folder_val = {}
self.top.mainloop()
def update_folder_value(self):
self.sel_folder_val['text']=filedialog.askdirectory()
self.top.update_idletasks()
app()
and even that's not minimal. Second your problem is hard to find since this isn't minimal- you're doing something really weird - binding the button to a click. You're overriding the built-in binding, and apparently it still affects the state of the button on press, but not going back. What you wanted is:
self.select_folder_btn.configure(command=self.update_folder_value)
instead of your:
self.select_folder_btn.bind('<Button-1>',self.update_folder_value)
You could also define that in the Button command. What you did is bypassed the button mechanism, so apparently only half of it is executed, and the relief is not raised. Note you have to remove the event parameter your method accepts.

Categories

Resources