I am trying to enhance the Tkinter Text widget and provide additional capabilities.
One of the key features of my new widget is to hack the arrows/keyboard key strokes (syntax highlighting, auto-complete context menus).
I tried a direct approach of binding the Key and Up/Down keystrokes to methods, but this approach failed as my method handlers were executed before the Text event handlers therefore my methods were executed before the last keystroke handled by the text itself.
class Editor(Frame):
def __init__(self, parent, *args, **kwargs):
Frame.__init__(self, parent) # initialize the base class
# The Main Text Widget
self.text = scrolledtext.ScrolledText(self, *args, **kwargs)
self.text.pack(side='left', fill='both', expand=1)
self.text.bind("<Key>", lambda event: self.editor_key_handler())
self.text.bind("<Up>", lambda event:self.editor_arrow_key_handler('Up'))
self.text.bind("<Down>", lambda event: self.editor_arrow_key_handler('Down'))
I then tried to switch the bindtag order, and make the Class handler run first, then my instance handlers - this indeed fixed the original issue:
bindtag = list()
bindtag.append(self.text.bindtags()[1])
bindtag.append(self.text.bindtags()[0])
bindtag.append(self.text.bindtags()[2])
bindtag.append(self.text.bindtags()[3])
self.text.bindtags(bindtag)
But now as my handlers were running after the Text's... my Up / Down handlers were running after the insert cursor has already moved inside the Text losing the original position where the user clicked the Up/Down arrows.
Being an experienced Perl Tk programmer, I moved along to try and derive the Tkinter Text widget to allow me to hijack the Up/Down default handler and provide my own customised methods..
I didn't find a way to simply derive and override the Text widget (something seems trivial to an OOP system.)
How this can be done?
The best way to add custom event handling to a widget is to leave the bind tags alone, and simply add your own bindings that return the literal string "break". Returning "break" will prevent the default bindings from firing.
In the following example I've added a custom handler for the up arrow, inserting "" rather than doing the default behavior. Notice that the handler returns the string "break":
import tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.text = tk.Text(root)
self.text.pack(fill="both", expand=True)
self.text.bind("<Up>", self.handle_up)
def handle_up(self, event):
self.text.insert("insert", "<up>")
return "break"
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
For a lengthy description of what happens when a key is handled, see this answer: https://stackoverflow.com/a/11542200/7432. The answer is to a question about the Entry widget, but event processing is the same for all widgets.
Related
I have been searching for a way to set the tab order in a tkinter application, that I have been working on. Currently the default order seems to be working from top-down, but it requires using CTRL + Tab to cycle through the controls.
Is there any way to customize the order and, more so, change the CTRL + Tab to just Tab?
Tab order is based on the stacking order, which in turn defaults to the order that widgets are created. You can adjust the stacking order (and thus the tab order) using the methods tkraise (or lift) and lower.
This should be working out of the box for you without the need to press CTRL + Tab. Be aware, however, that tab inserts a literal tab in text widgets rather than moving focus to another control. That default behavior can be changed of course.
Here's an example showing how to reverse the tab order. When running the example, pressing tab in the first entry should take you to the last. Pressing tab again takes you to the second, then the first, lather, rinse, repeat
Note that the native Tk commands are raise and lower, but since raise is a reserved word in Python it had to be renamed in tkinter.
import Tkinter as tk
class SampleApp(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
e1 = tk.Entry(self)
e2 = tk.Entry(self)
e3 = tk.Entry(self)
e1.insert(0,"1")
e2.insert(0,"2")
e3.insert(0,"3")
e1.pack()
e2.pack()
e3.pack()
# reverse the stacking order to show how
# it affects tab order
new_order = (e3, e2, e1)
for widget in new_order:
widget.lift()
if __name__ == "__main__":
app = SampleApp()
app.mainloop()
Since you mention you have to do CTRL + Tab, I'm guessing you're trying to have the tab key change focus from a text widget. Normally a tab key inserts a literal tab. If you want it to change the focus, just add a binding to the <Tab> event.
Tkinter has a function that will return the name of the next widget that should get focus. Unfortunately, for older versions of Tkinter that function is buggy. However, it's easy to work around that. Here's a couple of methods you can add to the above code:
def _focusNext(self, widget):
'''Return the next widget in tab order'''
widget = self.tk.call('tk_focusNext', widget._w)
if not widget: return None
return self.nametowidget(widget.string)
def OnTextTab(self, event):
'''Move focus to next widget'''
widget = event.widget
next = self._focusNext(widget)
next.focus()
return "break"
I've been searching for ways to skip some widgets while tabbing and found in tkinter's tk_focusNext function description the following: "A widget is omitted if it has the takefocus resource set to 0."
you can set takefocus on widget initialization as an argument.
I'm building something that uses multiple scrollbars. This class is actually a wrapper for the tk.ScrollBar and it's used to create scrollable frames. What I want is to be able to set a "default" scroll-container.
Assume 90% of the time someone using the application would want to scroll some frame (Which we will call main_frame), and 10% of the time they would want to scroll a different frame. (normal_frame)
It would make sense to have the mousewheel scroll the normal_frame only when the user was hovering over it with the mouse, but to have the mousewheel scroll the main_frame in all instances except when hovering over normal_frame.
The problem is that whenever you call bind_all upon "<Enter>"(ing) the normal_frame, when you "<Leave>" it, the main_frame no longer scrolls. Any suggestions?
class ScrollThing(object):
def __init__(self, some_frame, default=False):
self.default = default
self.canvas = tkinter.Canvas(some_frame)
self.view_window = tkinter.Frame(self.canvas)
#things and stuff to setup scroll bar
def setup_view_window(self):
if self.default:
self.canvas.bind_all('<MouseWheel>', self.on_mousewheel)
else:
self.view_window.bind('<Enter>', self.focus_in)
self.view_window.bind('<Leave>', self.focus_out)
def focus_in(self, *args):
del args
# I'm a seperate ScrollThing from main_frame
# I don't have access to the other ScrollThing because
# we are both dynamically created.
# I want to save the current bound arguments somehow
# but .bindtags() returns only MY bindings
self.canvas.bind_all('<MouseWheel>', self.on_mousewheel)
def focus_out(self, *args):
self.canvas.unbind_all('<MouseWheel>', self.on_mousewheel
def on_mousewheel(self, event):
self.canvas.yview_scroll(int(-1*(event.delta//120)), 'units')
I just found a solution involving handing the function and the canvas back to the Master.
class Master(object)
def __init__(self, *args, **kwargs):
#Things and stuff
self.default_scroll = None
Then by setting default_scroll to hold the default parameters
def setup_view_window(self):
if self.default:
self.canvas.bind_all('<MouseWheel>', self.on_mousewheel)
# HERE
self.dynamic.stuff.MASTER.default_scroll = [self.canvas, self.on_mousewheel]
else:
self.view_window.bind('<Enter>', self.focus_in)
self.view_window.bind('<Leave>', self.focus_out)
You can then access it in the focus_out.
def focus_out(self, *args):
self.canvas.unbind_all('<MouseWheel>', self.on_mousewheel)
# HERE
if self.dynamic.stuff.MASTER.default_scroll is not None:
self.dynamic.stuff.MASTER.default_scroll[0].bind_all('<MouseWheel>', self.dynamic.stuff.MASTER.default_scroll[1])
Although, I would be interested to know if anyone knows of a way to access it in a more concise way, perhaps something in tkinter that lets you see all bound events in the entire application?
Arguably the simplest solution is to set a variable to be the widget you want to scroll, and then set a global binding to scroll that widget.
For example, your app would initialize the binding like this exactly once:
self.root.bind_all("<MouseWheel>", self.scroller)
Then, in scroller you scroll the default window:
def scroller(self, event):
self.default_window.yview(...)
You can also add the binding to the canvases themselves instead of calling bind_all if you prefer. The basic concept still works without having to adjust the bindings on enter/leave events.
I am trying to figure out what the best way to communicate between different widgets is, where the widgets are custom classes inheriting from tkinter widgets and I have several frames present (to help with layout management). Consider for example the following simple gui (written for python 3, change tkinter to Tkinter for python 2):
from tkinter import Frame,Button,Tk
class GUI(Frame):
def __init__(self, root):
Frame.__init__(self,root)
self.upper_frame=Frame(root)
self.upper_frame.pack()
self.lower_frame=Frame(root)
self.lower_frame.pack()
self.upper_btn1 = Button(self.upper_frame, text="upper button 1")
self.upper_btn2 = Button(self.upper_frame, text="upper button 2")
self.upper_btn1.grid(row=0,column=0)
self.upper_btn2.grid(row=0,column=1)
self.lower_btn = CustomButton(self.lower_frame, "lower button 3")
self.lower_btn.pack()
class CustomButton(Button):
def __init__(self,master,text):
Button.__init__(self,master,text=text)
self.configure(command=self.onClick)
def onClick(self):
print("here I want to change the text of upper button 1")
root = Tk()
my_gui = GUI(root)
root.mainloop()
The reason I put them in different frames is because I want to use different layout managers in the two different frames to create a more complicated layout. However, I want the command in lower_btn to change properties of eg upper_btn1.
However, I can not immediately access upper_btn1 from the instance lower_btn of the customized class CustomButton. The parent of lower_btn is frame2, and the frame2 parent is root (not the GUI instance). If I change the parent of lower_btn to the GUI instance, ie
self.lower_btn = CustomButton(self, "lower button")
it will not be placed correctly when using pack(). If I change the parent of frame1/frame2 to the GUI instance, ie
self.upper_frame=Frame(self)
self.upper_frame.pack()
self.lower_frame=Frame(self)
self.lower_frame.pack()
I could in principle access the upper_btn1 from lower_btn by self.master.master.upper_btn1, BUT then the frame1/frame2 are not placed correctly using pack() (this I don't understand why). I can of course pass the GUI instance as separate variable, on top of master, to CustomButton, ie something like
class CustomButton(Button):
def __init__(self,master,window,text):
Button.__init__(self,master,text=text)
self.window=window
self.configure(command=self.onClick)
def onClick(self):
self.window.upper_btn1.configure(text="new text")
and then change the construction of lower_btn to
self.lower_btn = CustomButton(self.lower_frame,self, "lower button 3")
but is that the "correct" way of doing it?
So, what is the best way to reorganize this gui? Should I pass GUI as a separate variable on top of the master/parent argument? Should I change the master/parent of the buttons and/or the frames (and somehow fix the issues with the layout management)? Or should I make the commands of the buttons as methods of the GUI instance so they can communicate with the widgets in that GUI, although this does not really feel like object oriented programming? I can't seem to find a working object oriented design that feels "correct". Also, an explanation why pack() does not work for frame1/frame2 if I make the GUI instance (self) the master, would also be appreciated, as that would have been my intuitive, most object oriented, approach.
Edit: Maybe the best way is to not use frames inside frames at all, and use only grid() and then use colspan/rowspan to give the layout management more flexibility.
A year late, but: One way to communicate between widgets is to instantiate some kind of control center object that receives knowledge about the state of your widgets and then compels other widgets to act on that information. This 'manager' functionality will be independent of the layout of your widgets.
Here's an implementation that's customized to your example. The idea is to add a .manager attribute to the lower button, and to notify this manager when clicked. The GUI class remains unchanged.
from tkinter import Frame,Button,Tk
class Manager(object):
def __init__(self, gui):
self.gui = gui
self.gui.lower_btn.manager = self
def onClick(self):
self.gui.upper_btn2.configure(text="changed text")
class GUI(Frame):
def __init__(self, root):
Frame.__init__(self,root)
self.upper_frame=Frame(root)
self.upper_frame.pack()
self.lower_frame=Frame(root)
self.lower_frame.pack()
self.upper_btn1 = Button(self.upper_frame, text="upper button 1")
self.upper_btn2 = Button(self.upper_frame, text="upper button 2")
self.upper_btn1.grid(row=0,column=0)
self.upper_btn2.grid(row=0,column=1)
self.lower_btn = CustomButton(self.lower_frame, "lower button 3")
self.lower_btn.pack()
class CustomButton(Button):
def __init__(self,master,text):
Button.__init__(self,master,text=text)
self.configure(command=self.onClick)
self.manager = None
def onClick(self):
if self.manager:
self.manager.onClick()
else:
print("here I want to change the text of upper button 1")
root = Tk()
my_gui = GUI(root)
Manager(my_gui)
root.mainloop()
In python, how can I receive keyboard input. I'm well aware of console input with input("...") but I'm more concerned with receiving keyboard input while the console window is not active. For example if I created an instance of a Tkinter screen how could I check to see if let's say "w" was pressed. Then if the statement returned true i could move an object accordingly.
The way you do this with a GUI toolkit like tkinter is to create a binding. Bindings say "when this widget has focus and the user presses key X, call this function".
There are many ways to accomplish this. You can, for example, create a distinct binding for each character. Or, you could create a single binding that fires for any character. With these bindings, you can have them each call a unique function, or you can have all the bindings call a single function. And finally, you can put the binding on a single widget, or you can put the binding on all widgets. It all depends on exactly what you are trying to accomplish.
In a simple case where you only want to detect four keys, four bindings (one for each key) calling a single function makes perhaps the most sense. For example, in python 2.x it would look something like this:
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent, width=400, height=400)
self.label = tk.Label(self, text="last key pressed: ", width=20)
self.label.pack(fill="both", padx=100, pady=100)
self.label.bind("<w>", self.on_wasd)
self.label.bind("<a>", self.on_wasd)
self.label.bind("<s>", self.on_wasd)
self.label.bind("<d>", self.on_wasd)
# give keyboard focus to the label by default, and whenever
# the user clicks on it
self.label.focus_set()
self.label.bind("<1>", lambda event: self.label.focus_set())
def on_wasd(self, event):
self.label.configure(text="last key pressed: " + event.keysym);
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
How would that be done?
I have been unable to find it on here or with Google.
#Refrences
from tkinter import *
class Interface:
def __init__(self,stage):
topLeft = Frame(stage,bg='black')
test = Entry(topLeft,bg='#2a2a2a',fg='white',insertontime=0)
test.config(insertbackground='white', exportselection=0)
test.grid()
topLeft.grid(row=0,column=0)
def launch():
window = Tk()
lobby = Interface(window)
window.mainloop()
launch()
I assume you wish for users to not be able to edit the Entry box? If so, you must use the config parameter of (state="disabled"), eg.
test.config(insertbackground='white', exportselection=0, state="disabled")
be wary though, I could not find a way to keep the background of your entry box black. Hope this helps
You can set the text highlight color to match the background of entry widget. Note that the text in the widget can still be selected but the user will not see it visually which will give the illusion that selection is disabled.
test = Entry(topLeft,bg='#2a2a2a',fg='white',insertontime=0)
test.configure(selectbackground=test.cget('bg'), inactiveselectbackground=test.cget('bg'))
When we select a text we tkinter trigger(fire) 3 events, which are ButtonPress, Motion and ButtonRelease all these 3 events call a event handler fucntion.
The function run select_range(start, end) method which highlight selected text.
To disable this we have to hanlde ButtonPress and Motion events together and call select_clear method on the widget.
If you handle the events and call select_clear method it work but not properly, the text will be highlighted and when another event occured highligh color will be cleared.
This happend because of execution order of events. That's mean you have to tell to tk handle your event after default event handler. We can change order of events execution with bindtags and bin_class methods
example:
from tkinter import *
from tkinter import ttk
def on_select(event):
event.widget.select_clear() # clear selected text.
root = Tk()
name_entry = ttk.Entry(root)
name_entry.pack()
# with PostEvent tag, on_select the event hanlde after default event handler
name_entry.bindtags((str(name_entry), "TEntry", "PostEvent", ".", "all"))
name_entry.bind_class("PostEvent", "<ButtonPress-1><Motion>", on_select)
root.mainloop()
A useful solution to Tkinter not having tons of built-in widgets (like, say, JavaFX does), is that you can easily make your own if you don't mind them being not-quite what you wanted :) Here's a rough-around-the-edges example of using a canvas to emulate an entry field that can't be selected. All I've given is the insert functionality (and delete too, I suppose, if you were clever about it), but you may want more. (Another plus is that, because it's a canvas item, you can do nifty formatting with it).
Am I right that by a not-selectable entry widget, you mean a canvas with text written on it? If you want to disable text-highlighting in ALL widgets, including the top-level frame, you could highjack the Button-1 event, deselect everything, and then consume the event whenever text is selected.
from tkinter import *
class NewEntry(Canvas):
def __init__( self, master, width, height ):
Canvas.__init__( self, master, width=width,height=height )
self.width = width
self.height = height
self.text = ''
def insert( self, index, text ):
self.text =''.join([self.text[i] for i in range(index)] + [text] + [self.text[i] for i in range(index,len(self.text))])
self.delete(ALL)
self.create_text(self.width//2,self.height//2,text=self.text)
root = Tk()
entry = NewEntry(root,100,100)
entry.insert(0,'hello world')
entry.insert(5,'world')
entry.pack()
root.mainloop()