disable tkinter keyboard shortcut (2) - python

I'm proposing a continuation of the discussion in disable tkinter keyboard shortcut: I have an event handler for an event that Tkinter also uses, so that my prog & Tkinter interact badly.
Since it is a problem that I've been unable to solve I'm re-proposing here, where I tried to boil it down to the simplest form in the following code:
#!/usr/bin/env python
from Tkinter import *
import tkFont
def init():
global root,text
root = Tk()
root.geometry("500x500+0+0")
dFont=tkFont.Font(family="Arial", size=10)
text=Text(root, width=16, height=5, font=dFont)
text.pack(side=LEFT, fill=BOTH, expand = YES)
root.bind("<Control-b>", setbold)
text.tag_config("b",font=('Verdana', '10', 'bold' ))
text.tag_config("i",font=('Verdana', '10', 'italic' ))
def removeformat(event=None):
text.tag_remove('b',SEL_FIRST,SEL_LAST)
text.tag_remove('i',SEL_FIRST,SEL_LAST)
def setbold(event=None):
removeformat()
text.tag_add('b', SEL_FIRST,SEL_LAST)
text.edit_modified(True)
def main():
init()
mainloop()
if __name__ == '__main__':
main()
What it should do is simply to produce a text window where you write into.
Selecting some text and pressing Ctrl+B the program should remove any preexisting tag, then assign to it the 'b' tag that sets the text to bold.
What instead happens is an exception at the first tag_remove, telling me that text doesn't contain any characters tagged with "sel".
The suggestion to use a return 'break' is of no use, since the selection disappears before setbold() has any chance to act...

Set your binding on the text widget, not on the root. (Whole toplevel bindings are processed after widget class bindings – where the standard <Control-Key-b> binding is – and those are processed after the widget instance bindings, which is what you want to use here.) And you need to do that 'break'; it inhibits the subsequent bindings. (If you're having any problems after that, it's probably that the focus is wrong by default, but that's easy to fix.)
The only other alternative is to reconfigure the bindtags so that class bindings are processed after toplevel bindings, but the consequences of doing that are very subtle and far-reaching; you should use the simpler approach from my first paragraph instead as that's the normal way of handling these things.

Bindings are handled in a specific order, defined by the bindtags of that widget. By default this order is:
The specific widget
The widget class
The toplevel window
The special class "all"
If there are conflicting bindings -- for example, a control-b binding on both the widget and class -- they both will fire (in the described order) unless you break the chain by returning "break".
In the case of the code you posted, however, you are binding to the toplevel window (ie: the root window), and the conflicting binding is on the class. Therefore, the binding will fire for the class before it is processed by the toplevel, so even if your binding returned "break" it wouldn't matter since the class binding happens first.
The most straight-forward solution is to move your binding to the actual widget and return "break". That will guarantee your binding fires first, and the return "break" guarantees that the class binding does not fire.
If you really want your binding on the root window, you can remove the binding for the class using the bind_class method with the value of "Text" for the class.
You might find the Events and Bindings page on effbot.org to be useful.

Related

Tkinter, ignore certain lines from a tcl file?

Basically, I just want to remove the mouse scrollwheel binding for scrolling in ttk.Treeview widget. And I found that a specific line in the file found in
Python.framework/Versions/3.9/lib/tk8.6/ttk/treeview.tcl
is responsible for it, that if removed, disables the mouse scrollwheel for scrolling the view:
ttk::copyBindings TtkScrollable Treeview
However, I don't want to affect the built-in files.
My only solution was to create py2app bundle and delete the same line from the treeview.tcl inside the bundle. But is there a direct way to do this inside a script.py file without affecting the tcl file?
You don't have to modify the original code. Tkinter's event handling mechanism is very robust. It's easy to prevent the default bindings from running, bind binding a function that returns the string "break". Returning "break" will prevent any other handlers in the widget's binding tags from being invoked.
Here's a short contrived example:
import tkinter as tk
from tkinter import ttk
root = tk.Tk()
tree = ttk.Treeview(root)
tree.pack(side="top", fill="both", expand=True)
for i in range(20):
tree.insert("", "end", text=f"Item #{i+1}")
tree.bind("<MouseWheel>", lambda event: "break")
root.mainloop()
Note if you're on an x11 based system you might have to disable the bindings for <Button-4> and <Button-5> instead of <MouseWheel>.

How can I make a tkinter text widget unselectable?

I want to make my tkinter Text to be only an output and not an input. Through some research I've found that text.config(state="disabled") disables user input, but it still allows for selecting text, which I do not want.
How can I get my Text widget to be unselectable and unwritable?
The simplest way is to replace the default text bindings that support selection so that they do nothing. There are a couple ways to do this: using binding tags you can remove all default bindings, or you can remove the bindings to only a subset of default bindings.
Removing all default bindings
All bindings on widgets -- including the default bindings -- are associated with binding tags (also called "bindtags"). The binding tag for the the text widget is "Text", and all default bindings for the text widget are associated with this tag. If you remove that binding tag, you remove all Text-specific bindings.
The default binding tags for any widget is a tuple of the string representation of the widget, the internal widget class (in this case, "Text"), the internal name of the toplevel window (in this case, root), and the special tag "all".
In the following example we change the binding tags so that "Text" is not included, effectively removing all default bindings on the text widget:
import tkinter as tk
root = tk.Tk()
text = tk.Text(root)
text.bindtags((str(text), str(root), "all"))
Removing specific bindings
If you prefer to keep some of the default bindings, you can replace just the ones that you don't want. You do that by creating your own bindings, and having those bindings return the string "break". This special return value tells tkinter to stop processing the event any further.
For example, to prevent a double-click from selecting the word under the cursor you could do this:
text.bind("<Double-1>", lambda event: "break")
The downside to this approach is that you have to figure out what all of the bindings are that are related to the selection mechanism. On the other hand, it gives you complete control over what each key or button press does.
A read-only, unselectable text widget.
class Textarea(tkinter.Text):
def __init__(self, master, **kw):
super().__init__(master, **kw)
# disable text alteration
self.configure(state="disabled")
# untag any selection from beginning to end
def unselect(event):
self.tag_remove("sel", "1.0", "end")
# catch different ways selections could be made and unselect before copying or cutting
good = ["<ButtonRelease-1>", "<Leave>", "<Control-c>", "<Control-C>", "<Control-x>", "<Control-X>"]
better = good + ["<Shift-Left>", "<Shift-Up>", "<Shift-Right>", "<Shift-Down>", "<Shift-Home>", "<Shift-End>", "<Shift-Next>", "<Shift-Prior>"]
excessive = better + ["<Shift-KP_1>", "<Shift-KP_2>", "<Shift-KP_3>", "<Shift-KP_4>", "<Shift-KP_6>", "<Shift-KP_7>", "<Shift-KP_8>", "<Shift-KP_9>"]
for sequence in better:
self.bind(sequence, unselect, add="+")
# remove the appearance of selection
self.configure(selectforeground=self.cget("foreground"), selectbackground=self.cget("background"))
# disallow export of selection in case anything gets through
self.configure(exportselection=False)
Tested on python 3.8.2
I believe you will have to replace it with another widget that such as a Label or LabelFrame to accomplish this. As well you could use a from tkinter import messagebox and have the text you want pop up in another window (like an info window or error message window). I think that as far as the Text widget goes, setting the state to disabled is the best you can do for your purposes unfortunately and users will be able to copy that text despite being unable to edit it.
Here is the simplest method to prevent text from being selected/highlighted when you just want the Text widget to be an ordinary log that is disabled and unselectable.
When I had the issue I figured I just needed to set some Text configuration property (highlightbackground, highlightcolor or selectbackground) to "Black". Nothing worked. The Text widget employs tags that can be used to mark up the Text within the control. Configuration for user defined tags as well as the special tag "sel" have a number of settings including foreground (color) and background (color).
tag_config("sel", background="black")
Too easy right? That doesn't work either.
Turns out that the highlight is actually a bitmap overlaid on the text. This is controlled by the bgstipple (bitmap) configuration for the tag. The documentation indicates that there are a number of system bitmaps (shades of grey) that can be used however it is also possible to specify your own. The bitmap needs to be an xbm and it's easy to create your own as it's a text file.
Put the following in a file named transparent.xbm.
#define trans_width 2
#define trans_height 2
static unsigned char trans_bits[] = {
0x00, 0x00
};
Here it is...
class TextLog(tk.Text):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.tag_config("sel", bgstipple="#transparent.xbm",
foreground="white")
self.config(state="disabled")
def write_log(self, text="", clear=False)
self.configure(state='normal')
if clear is True:
self.delete("1.0","end")
self.insert(tk.END, text)
self.see(tk.END)
self.config(state="disabled")
Depending on where the .xbm is relative to the module using the TextLog you may need to prefix it with a path "#path/to/transparent.xbm"

How can I unbind every single binding from a Tkinter root window

So I have an app in Tkinter that has a lot of buttons in the first screen and when you press one you pass into a new "Window" (basically destroying all widgets and drawing the ones that are needed for the 'window'). There is a standard function that uses some commands to destroy every child on the root. I would like to add some code that can unbind all of the bindings that are made in the root. Bindings that are on specific widgets get destroyed but those that are bind on the root stay there and cause error.
Here's the code for destroying the widgets.
#staticmethod
def clear():
for widget in guihandle.root.winfo_children():
widget.destroy()
#staticmethod
def set(db,table):
guihandle.clear()
curW = Window(db,table)
guihandle.current_Window = curW
curW.initialize()
guihandle.windows.push(curW)
(Yes, I make the base GUI from a sqlite3 database :P)
There is nothing in Tkinter to do what you want. Your app will need to keep track of the bindings it wants to remove.
That being said, depending on just how complex your real problem is, there may be other solutions. For example, instead of binding to the root window, bind to a custom binding tag (also called a bind tag or bindtag). You will then need to add that tag to every widget to enable the bindings, and remove the tag from any existing widgets to effectively disable the bindings.
I know I am VERY late but, if you go through your code and replace every widget.bind with the function below, which was taken and modified from here
def bindWidget(widget: Widget, event, all:bool=False, func=None):
# https://stackoverflow.com/a/226141/19581763
'''Set or retrieve the binding for an event on a widget'''
try:
_ = widget.__dict__['bindings']
except KeyError:
has_binding_key = False
else:
has_binding_key = True
if not has_binding_key:
setattr(widget, 'bindings', dict())
if func:
if not all:
widget.bind(event, func)
else:
widget.bind_all(event, func)
widget.bindings[event] = func
else:
return(widget.bindings.setdefault(event, None))
Than the function will keep track of every binding for you by setting a attribute called bindings which has both, the event and the function inside of it.
For example:
label.bind('<Button-1>', label.destroy)
Will become:
bindWidget(label, '<Button-1>', func=label.destroy)
After doing that, you can write a simple function which deletes all of the bindings and widgets:
def clear(self): # Self will be your Tk() instance
"""Clears everything on the window, including bindings"""
for child in self.winfo_children():
child.destroy()
if not hasattr(self, 'bindings'):
self.bindings = {}
for event, func in self.bindings.items():
self.unbind_all(event)
self.bindings = {}
There are only 2 caveats with this approach
Time
You will have to go through your code to replace every widget.bind and if you have lots of bindings, than it will take a lot of time
Readability
bindWidget(label, '<Button-1>', func=label.destroy) is less readable and less clear than label.bind('<Button-1>', label.destroy)
Edit 8/11/2022
Alternatively, you can destroy the entire window and recreate it like this:
window.destroy()
window = Tk()
However, I am not sure if creating a whole Tcl interpreter is good.

Select all text in a Text widget using Python 3 with tkinter

I'm working on my first Python program and have little idea what I'm doing. I want to re-bind ctrl-a (control a) to select all text in a Text widget. The current binding is ctrl-/ (control /). The binding part jumps right to the function but the actual text selection doesn't work. Instead, the cursor jumps to the first character on the first line (like it should) and nothing else happens. I'm sure this is embaressingly easy to fix but after spending hour an hours on it, I can't figure out what's wrong.
Python 3, Windows:
from tkinter import *
# Select all the text in textbox (not working)
def select_all(event):
textbox.tag_add(SEL, "1.0", END)
textbox.mark_set(INSERT, "1.0")
textbox.see(INSERT)
# Open a window
mainwin = Tk()
# Create a text widget
textbox = Text(mainwin, width=40, height=10)
textbox.pack()
# Add some text
textbox.insert(INSERT, "Select some text then right click in this window")
# Add the binding
textbox.bind("<Control-Key-a>", select_all)
# Start the program
mainwin.mainloop()
So the new code is...
from tkinter import *
# Select all the text in textbox
def select_all(event):
textbox.tag_add(SEL, "1.0", END)
textbox.mark_set(INSERT, "1.0")
textbox.see(INSERT)
return 'break'
# Open a window
mainwin = Tk()
# Create a text widget
textbox = Text(mainwin, width=40, height=10)
textbox.pack()
# Add some text
textbox.insert(INSERT, "Select some text then right click in this window")
# Add the binding
textbox.bind("<Control-Key-a>", select_all)
textbox.bind("<Control-Key-A>", select_all) # just in case caps lock is on
# Start the program
mainwin.mainloop()
and yes it works flawlessly. Thank you, very much Bryan Oakley. Steven Rumbalski: that's a VERY good point, I've followed your advice as well.
You need to both do the selection and then inhibit the default action by having your function return the string "break".
This is due to how Tkinter processes events. It uses what it calls "bind tags". Even though it looks like you are binding to a widget, you are actually binding to a tag that is the name of the widget. There can also be bindings to the widget class, to the toplevel window that the widget is in, and the tag "all" (plus, you can invent your own tags if you wish).
The default ordering of these tags is from most-specific to least-specific, and events are processed in that order. Meaning, if you have a binding both on the widget (most specific) and the class (less specific), the binding will fire for the widget first, and then for the class binding (and then for the toplevel, and then for "all").
What this means is that by default, a binding on a widget augments rather than replaces a default binding. The good news is, you can inhibit any further bindings from firing by simply returning the string "break", which stops the chain of bindings from doing any more work.
You can do that with a module named pyautogui
Just run the command where you want to add the event,
import pyautogui
..., command=lambda *awargs:pyautogui.hotkey("ctrl","a")
Make sure you install the module. If you are on windows, install it by
pip install pyautogui

Tkinter Label bound to StringVar is one click behind when updating

The problem I'm running into here is that, when I click on the different file names in the Listbox, the Label changes value one click behind whatever I'm currently clicking on.
What am I missing here?
import Tkinter as tk
class TkTest:
def __init__(self, master):
self.fraMain = tk.Frame(master)
self.fraMain.pack()
# Set up a list box containing all the paths to choose from
self.lstPaths = tk.Listbox(self.fraMain)
paths = [
'/path/file1',
'/path/file2',
'/path/file3',
]
for path in paths:
self.lstPaths.insert(tk.END, path)
self.lstPaths.bind('<Button-1>', self.update_label)
self.lstPaths.pack()
self.currentpath = tk.StringVar()
self.lblCurrentPath = tk.Label(self.fraMain, textvariable=self.currentpath)
self.lblCurrentPath.pack()
def update_label(self, event):
print self.lstPaths.get(tk.ACTIVE),
print self.lstPaths.curselection()
self.currentpath.set(self.lstPaths.get(tk.ACTIVE))
root = tk.Tk()
app = TkTest(root)
root.mainloop()
The problem has to do with the fundamental design of Tk. The short version is, bindings on specific widgets fire before the default class bindings for a widget. It is in the class bindings that the selection of a listbox is changed. This is exactly what you observe -- you are seeing the selection before the current click.
The best solution is to bind to the virtual event <<ListboxSelect>> which is fired after the selection has changed. Other solutions (unique to Tk and what gives it some of its incredible power and flexibility) is to modify the order that the bindings are applied. This involves either moving the widget bindtag after the class bindtag, or adding a new bindtag after the class bindtag and binding it to that.
Since binding to <<ListboxSelect>> is the better solution I won't go into details on how to modify the bindtags, though it's straight-forward and I think fairly well documented.

Categories

Resources