I have discovered that using tk.Scrollbar allows use of touch the scroll on a text widget. When I was overhauling the graphics of my application I could no longer use touch to scroll the text widget that used ttk.Scrollbar instead.
I am positive it is a difference between the widgets because I have toggled between the two when testing the problem.
That being said it is not a huge problem, since only a handful of devices this program will be installed on will have touch capabilities.
Is there any way to get touch on a ttk.scrollbar?
EDIT: Relevant Section of Code:
EDIT #2: Added the two functions at the bottom
self.text=tk.Text(self,font=("Consolas","11"),wrap="none",undo=True,bg="white",relief="flat")
self.numb=tk.Text(self,font=("Consolas","11"),wrap="none",width=4,relief="flat")
self.vsb=ttk.Scrollbar(self,command=self.scroller)
self.hsb=ttk.Scrollbar(self,command=self.text.xview,orient="horizontal")
self.text.configure(yscrollcommand=self.on_textscroll,xscrollcommand=self.hsb.set)
self.numb.config(bg="grey94",yscrollcommand=self.on_textscroll)
def scroller(self,*args):#Move me
self.text.yview(*args)
self.numb.yview(*args)
def on_textscroll(self, *args):#Move me
self.vsb.set(*args)
self.scroller('moveto', args[0])
(Not necessarily an answer, but at least code to experiment with and maybe build upon to make an answer.)
The following scrolls in all directions with middle mouse button held down with either tk (line 2 commented out) or ttk Scrollbar (line 2 uncommented).
from tkinter import Tk, Text, Scrollbar
from tkinter.ttk import Scrollbar
class Test(Tk):
def __init__(self):
super().__init__()
self.text = Text(self, wrap="none",undo=True,bg="white",relief="flat")
self.numb = Text(self, wrap="none",width=4,relief="flat")
self.vsb = Scrollbar(self,command=self.scroller)
self.hsb = Scrollbar(self,command=self.text.xview,orient="horizontal")
self.text.configure(yscrollcommand=self.on_textscroll,xscrollcommand=self.hsb.set)
self.numb.config(bg="grey94",yscrollcommand=self.on_textscroll)
self.numb.grid(row=0, column=0)
self.text.grid(row=0, column=1)
self.vsb.grid(row=0, column=2, sticky='ns')
self.hsb.grid(row=1, column=1, sticky='ew')
for i in range(100):
self.text.insert('insert',
('abcdefg%2d '%i)*10 + '\n')
def scroller(self,*args):#Move me
self.text.yview(*args)
self.numb.yview(*args)
def on_textscroll(self, *args):#Move me
self.vsb.set(*args)
self.scroller('moveto', args[0])
root=Test()
Adding the following at the end of init exposes middle mouse button clicks (it is botton 2, not 3), without disabling the scroll effect. Do fingers touches trigger this, with either scrollbar? If not, try to find out what event touches do generate.
def pr(event):
print(event)
self.text.bind('<Button-2>', pr)
self.text.bind('<ButtonRelease-2>', pr)
Related
The Problem
Using Python's tkinter, I'm trying to create custom buttons and other widgets by extending the Canvas widget. How can I change which custom canvas widgets get drawn on top as the user interacts with them?
lift() works for regular tkinter Buttons and other widgets, but raises an error when I try to use it to lift a Canvas, because Canvas has its own lift() method. Canvas's lift() is deprecated for Canvas in favor of tag_raise(). However, tag_raise() documentation says it "doesn’t work with window items", which fits my experience, and directs me to use lift() instead. My brain chased this seemingly circular documentation until it raised its own kind of StackOverflow exception, which brings me to ask you.
Code Illustration
Here's some basic code that runs and illustrates my problem. I've included button3, a regular button that can lift() as expected. If I click on custom_button1, however, the click_handler raises an exception.
from tkinter import Button, Canvas, Frame, Tk
from tkinter.constants import NW
class Example(Frame):
def __init__(self, root):
Frame.__init__(self, root)
self.canvas = Canvas(self, width=200, height=200, background="black")
self.canvas.grid(row=0, column=0, sticky="nsew")
self.button3 = Button(self.canvas, text="button3")
self.custom_button1 = MyCustomButton(self.canvas)
self.custom_button2 = MyCustomButton(self.canvas)
self.canvas.create_window(20, 20, anchor=NW, window=self.button3)
self.canvas.create_window(40, 40, anchor=NW, window=self.custom_button1)
self.canvas.create_window(34, 34, anchor=NW, window=self.custom_button2)
self.button3.bind("<Button-1>", self.click_handler)
self.custom_button1.bind("<Button-1>", self.click_handler)
self.custom_button2.bind("<Button-1>", self.click_handler)
def click_handler(self,event):
event.widget.lift() #raises exception if event.widget is a MyCustomButton
#note that Canvas.lift() is deprecated, but documentation
#says Canvas.tag_raise() doesn't work with window items
class MyCustomButton(Canvas):
def __init__(self, master):
super().__init__(master, width=40, height=25, background='blue')
if __name__ == "__main__":
root = Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
This works for as desired for button3, but for custom_button1, the exception that is raised is:
_tkinter.TclError: wrong # args: should be ".!example.!canvas.!mycustombutton2 raise tagOrId ?aboveThis?"
That exception makes sense in the context that Canvas.lift() and Canvas.tag_raise() are normally used to affect an item on the canvas by tag or id, not the canvas itself. I just don't know what to do about changing the stack order of the canvas itself so I can use it as a custom widget.
A Work-Around I've Considered
I could manage a bunch of custom widgets on a canvas by only having one canvas that handles all drawing and all the mouse events for all the widgets. I could still have classes for the widgets, but instead of inheriting from Canvas, they'd accept Canvas parameters. So adding would look something like the code below, and I'd have to write similar code for lifting, moving, determining if a click event applied to this button, changing active state, and so forth.
def add_to_canvas(self, canvas, offset_x=0, offset_y=0):
self.button_border = canvas.create_rectangle(
offset_x + 0, offset_y + 0,
offset_x + 40, offset_y + 25
)
#create additional button features
This work-around seems to go against established coding paradigms in tkinter, though. Furthermore, I believe this approach would prevent me from drawing these custom buttons above other window objects. (According to the create_window() documentation "You cannot draw other canvas items on top of a widget." In this work-around, all the custom buttons would be canvas items, and so if I'm reading this correctly, couldn't be drawn on top of other widgets.) Not to mention the extra code it would take to implement. That said, I don't currently have a better idea of how to implement this.
Thank you in advance for your help!
Unfortunately you've stumbled on a bug in the tkinter implementation. You can work around this in a couple of ways. You can create a method that does what the tkinter lift method does, or you can directly call the method in the tkinter Misc class.
Since you are creating your own class, you can override the lift method to use either of these methods.
Here's how you do it using the existing function. Be sure to import Misc from tkinter:
from tkinter import Misc
...
class MyCustomButton(Canvas):
...
def lift(self, aboveThis=None):
Misc.tkraise(self)
Here's how you directly call the underlying tk interpreter:
class MyCustomButton(Canvas):
...
def lift(self, aboveThis=None):
self.tk.call('raise', self._w, aboveThis)
With that, you can raise one button over the other by calling the lift method:
def click_handler(self,event):
event.widget.lift()
I'm designing a GUI application using Tkinter and for this project, I need buttons for the menu. While looking into the buttons I wasn't blown away by the customization options that come with the buttons, especially when I found out that you can bind click arguments to rectangles.
This allows me to customize the "button" in (almost) limitless ways, but to allow me to put text on the button I need to create a rectangle element and a text element and bind them together using Tkinter's tag_bind property.
One of the design properties of the button that I wanted was active fill when the user moused over the element. Right now I'm just using activefill="" which works, except the text element and the button element will only fill while the mouse is over that element. So, for example, when I mouse over the button the button excluding the text will highlight and vise versa when I mouse over the text.
Below is a simplified (for brevity) version of what I use to generate the buttons;
button = canvas.create_rectangle(button_width, button_height, 10, 10, fill="000", activefill="111", tags="test")
text = canvas.create_text((button_width/2), (button_height/2), activefill="111", tags="test")
canvas.tag_bind("test", "<Button-1>", "foo")
Is there a way to bind the active fill function to a tag rather than a specific element?
Another option is that I completely missed a bunch of information about customizing the buttons in Tkinter, and I would not be apposed to learning about that.
Option 1
I would personally not go for the presented solution. I do not know if you are using the button provided by tk or ttk. But, with the tkinter.tk, you could absolutely change the appearance of the button.
Following, I give you an example that produces a button with the following characteristics:
Blue foreground
Flat appearance
When hovered, the background is green
When pressed, the background is red
The code is as follows:
import tkinter as tk
root = tk.Tk()
# Function hovering
def on_enter(e):
btn['background'] = 'green'
def on_leave(e):
btn['background'] = 'white'
# Create the button
btn = tk.Button(root, background='white', activebackground='red', foreground='blue',relief='flat',text='Test',width=20)
btn.pack()
# Bindings
btn.bind("<Enter>", on_enter)
btn.bind("<Leave>", on_leave)
# Loop
root.mainloop()
Option 2
If even after having tried the tk.Button, you are not glad with the result, I would create a Frame containing a Label (you can do nearly anything with that combination). Then, you could change the background of the frame according to any user action, like:
import tkinter as tk
root = tk.Tk()
# Function hovering
def on_enter(e):
lab['background'] = 'green'
def on_leave(e):
lab['background'] = 'white'
# Click
def on_click(e):
print("hi")
# Create the frame with a label inside
fr = tk.Frame(root)
lab = tk.Label(fr, text="Test", width=20, background="white")
# Packing
fr.pack()
lab.pack()
# Bindings
fr.bind("<Enter>", on_enter)
fr.bind("<Leave>", on_leave)
lab.bind("<Button-1>", on_click)
# Loop
root.mainloop()
You could even create a class with the above combination.
I'm currently creating an adventure game and I want to bind alt+a to my callback. It doesn't do what I want, so I have two questions:
Is it possible to bind a function to a Label, too?
Why does this (simplyfied) code doesn't work?
Here is the code:
import tkinter as tk
dw = tk.Tk()
dw.title('Hearts')
def play(event):
print('This is the test.')
areal = tk.Frame(master=dw, width=1200, height=600, bg='blue')
areal.pack_propagate(0)
areal.pack(fill=tk.BOTH, expand=bool(dw))
areal.bind("<Alt-A>", play)
dw.mainloop()
It doesn't give me an error, but it doesn't do anything when I click the Frame and afterwards press alt+a. What is wrong here?
EDIT:
import tkinter as tk
def go_fwd(event):
areal.focus_set()
print(event.x, event.y)
dw = tk.Tk()
dw.title('Adventure')
areal = tk.Frame(master=dw, width=20000, height=600, bg='blue')
areal.pack_propagate(0)
areal.pack(fill=tk.BOTH, expand=bool(dw)-100)
areal.focus_set()
dw.bind("<Alt-A>", go_fwd)
enter = tk.Frame(master=dw, width=20000, height=100, bg='cyan')
enter.pack(fill=tk.X)
enterentry = tk.Text(master=enter, width=100, height=4, bg='white')
enterentry.pack()
enterbutton = tk.Button(master=enter, text='Senden', bg='red')
enterbutton.pack()
dw.mainloop()
Here is the complete code.
Is it possible to bind a function to a Label, too?
You can bind to any widget you want. However, if you bind key events, the bindings will only work if the widget has focus. By default, most widgets other than Entry and Text don't get focus unless you explicitly set the focus to them.
Note: only one widget can have keyboard focus at a time.
You can also set a binding to the root window, which will cause it to fire no matter what widget has focus.
For a more thorough explanation of how key bindings are processed, see this answer: https://stackoverflow.com/a/11542200/7432
Why does this (simplyfied) code doesn't work?
It doesn't work the way you expect because the binding is on a Frame widget, but that widget doesn't have the keyboard focus. You could give it focus with something like this:
areal.focus_set()
Or, you could only give it focus after you click on the frame, by creating a binding on a mouse click:
areal.bind("<1>", lambda event: areal.focus_set())
Note: you are binding to a capital "A", so make sure when you test that you're pressing control-alt-a
You need to bind to dw instead of your frame.
So, you can do dw.bind("<Alt-A>", play).
A minor note, Alt-A will bind to the uppercase A as expected, so you'd have to click Alt+Shift+A on your keyboard. Doing Alt+A on your keyboard won't work, you'd have to bind to Alt-a for this to work.
The main window has keyboard focus. Or, alternatively you can leave the bind on the frame and just do areal.focus_set() to set the focus to the frame.
Here's a python/tkinter program that puzzles me. The window displays a ttk.Entry that is readonly and a tkinter.Text that is disabled. It programmatically selects one character in the Entry box and never changes this selection. However the selection will change if I try to select text in the other box (the disabledText). This doesn't seem right.
Python 3.5.0 and tcl/tk 8.5.18 on OS X
When you run the program, you can see the "A" highlighted in the Entry (upper) box.
Push the "Write Data" button a few times; the print statement will display the "A" that's selected in the Entry box.
Sweep the mouse over some text in the Text (lower) box; it won't be highlighted, but the highlighting in the Entry will disappear.
Push the "Write Data" button; the print statement will display the characters you swept with the mouse.
Those characters came from selection_get() on the Entry! You can tell that it got them from the Text because the two boxes have no characters in common.
If somebody can explain this, I'd be most grateful.
import tkinter
from tkinter import ttk
class ButtonPanel(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.data = ttk.Entry(self, width=27, takefocus=False)
self.data.insert(0, "ABCDEFG")
self.data.select_range(0, 1) # select the "A"
self.data.state(["readonly"])
self.data.bind('<ButtonPress>', lambda e: 'break') # ignore mouse clicks
button = ttk.Button(self, text="Write Data", command=self.master.write)
self.data.grid(column=0, row=0, padx=10)
button.grid(column=1, row=0, padx=10)
def get(self):
return self.data.selection_get() # should always be the "A"
class App(ttk.Frame):
def __init__(self, parent):
super().__init__(parent)
self.bp = ButtonPanel(self)
self.display = tkinter.Text(self, width=50, height=10, wrap="char", takefocus="False")
self.display.insert('end', "HIJKLMNOPQRSTUV")
self.display.config(state="disabled")
self.bp.pack()
self.display.pack()
def write(self):
char = self.bp.get() # should always be the "A"
print("this should be just one character: ->{}<-".format(char))
if __name__ == "__main__":
root = tkinter.Tk()
root.title("What's up here?")
App(root).pack()
root.mainloop()
What you are observing is the default behavior. Both of those widgets (as well as the listbox) have an attribute named exportselection, with a default value of True. When True, the widget will export the selection to be the primary selection. On old unix systems (where tcl/tk and tkinter got its start), you could only have one "primary" selection at a time.
The simple solution is to set this option to False for the text widget. This will allow your application to have multiple items selected at once, but only the entry widget exports the selection to the clipboard (which is required for selection_get to work.
...
self.display = tkinter.Text(self, ..., exportselection=False)
...
The other issue is that on OSX the selection won't show for a disabled text widget. The text is being selected, you just can't see it. More accurately, the selection won't show except when the widget has focus, and by default, it is not given focus when you click on it.
I'm adding several widgets to a Frame which is located in a tix.NoteBook. When there are too much widgets to fit in the window, I want to use a scrollbar, so I put tix.ScrolledWindow inside that Frame and add my widgets to this ScrolledWindow instead.
The problem is that when using the grid() geometry manager, the scrollbar appears, but it is not working (The drag bar occupies the whole scroll bar).
from Tkinter import *
import Tix
class Window:
def __init__(self, root):
self.labelList = []
self.notebook = Tix.NoteBook(root, ipadx=3, ipady=3)
self.notebook.add('sheet_1', label="Sheet 1", underline=0)
self.notebook.add('sheet_2', label="Sheet 2", underline=0)
self.notebook.add('sheet_3', label="Sheet 3", underline=0)
self.notebook.pack()
#self.notebook.grid(row=0, column=0)
tab1=self.notebook.sheet_1
tab2=self.notebook.sheet_2
tab3=self.notebook.sheet_3
self.myMainContainer = Frame(tab1)
self.myMainContainer.pack()
#self.myMainContainer.grid(row=0, column=0)
scrwin = Tix.ScrolledWindow(self.myMainContainer, scrollbar='y')
scrwin.pack()
#scrwin.grid(row=0, column=0)
self.win = scrwin.window
for i in range (100):
self.labelList.append((Label(self.win)))
self.labelList[-1].config(text= "Bla", relief = SUNKEN)
self.labelList[-1].grid(row=i, column=0, sticky=W+E)
root = Tix.Tk()
myWindow = Window(root)
root.mainloop()
Whenever I change at least one of the geometry managers from pack() to grid(), the problem occurs. (Actually, I'd prefer using grid() for all containers.)
When I don't use the NoteBook widget, the problem does not occur either. The other examples here all seem to rely on pack().
Any ideas?
Many thanks,
Sano
I solved it without using ´tix.scrolledWindow´. Instead, I went for the autoscrollbar suggested by Fred Lundh here.
The main problem was the adaption to the NoteBook widget. First, I tried to put the scrollbar to the root, so that they would surround the whole window. Now, I wanted to change the hook for the scrollbar whenever I changed a tab, but the ´raisecmd´ of the Notebook did not work. Next, I thought of using the configure event on each tab - whenever a new tab is raised, its size changes and configure is called.
Well, after much trying without ever being satisfied I changed my approach and put the scrollbars inside of the tabs. The tabs and all subcontainers must get the ´grid_columnconfigure(0, weight=1)´ and ´grid_rowconfigure(0, weight=1)´ settings, or else they will not grow with the tabs.