I know that you can get the scrollbar position if I bind the frame the scrollbar is controlling to a function (onFrameConfigure) like this: self.calendar_frame.bind("<Configure>", self.onFrameConfigure), and from the event in the onFrameConfigure(self, event): I can get some x value of the scroll bar event.x. I thought the value was showing the location of the scrollbar in px so with that logic if I scroll to the maximum right side, I should get the same value (or similar value) as the width of the scrollbar but from the test below I got otherwise:
event.x: -640 | scrollbar.winfo_width(): 382
event.x: -415 | scrollbar.winfo_width(): 607
event.x: -245 | scrollbar.winfo_width(): 777
event.x: -713 | scrollbar.winfo_width(): 309
(the result above are only when the scrollbar is to the maximum right)
Don't see any way to use those value to determine whether the scrollbar is at the right end.
My question is: how can I detect when the scrollbar reached the end of one of the end-of-the-scrollbar?
Even better would be to detect when the scrollbar is near the end because for my purpose (read the purpose of my project here: here) I need something to trigger right before the scrollbar reach it maximum end.
There is a simple get method for tkinter scrollbar (more info) which can be used like this: scrollbar.get(). Whenever the scrollbar is moved by the user, I would just check with if scrollbar.get()[1] > 0.9:. With this method, I can execute my stuff when the scrollbar is closing in to the rightmost position.
If you're wanting to know when the scrolled widget is scrolled toward the edge, the simplest solution is to call a custom command rather than calling the set method of the associated scrollbar. This command can itself call the set method, and then do whatever else you want it to do.
This command will be passed two parameters which represent the range of the viewport into the scrolled widget. The numbers are floating point numbers between 0 and 1, though they are passed as strings rather than floats. The first number represents the first visible part of the widget, and the second number represents the last part of the visible widget.
For example, normally you would do something like this to wire a canvas and scrollbar together:
canvas = tk.Canvas(...)
scrollbar = tk.Scrollbar(..., command=canvas.xview)
canvas.configure(xscrollcommand=scrollbar.set)
Instead, create your own command which calls the scrollbar.set method and then does whatever else you want it to do. It would look something like this:
def handle_scroll(x0, x1):
hsb.set(x0, x1)
if float(x1) > .9:
...
canvas = tk.Canvas(...)
scrollbar = tk.Scrollbar(..., command=canvas.xview)
canvas.configure(xscrollcommand=handle_scroll)
Related
so I have created a vertical scrollview in tkinter in a frame in python, however I want that scroll view to always scroll down. Is there any way I can make that scroll view to scroll down, are there any commands and if there are how do I do it
scroll_x = Scrollbar(ShowDataFrame,orient=HORIZONTAL)
scroll_y = Scrollbar(ShowDataFrame,orient=VERTICAL)
chattable = Treeview(ShowDataFrame,columns=('name','chat'),yscrollcommand=scroll_y,xscrollcommand=scroll_x)
scroll_x.pack(side=BOTTOM,fill=X)
scroll_y.pack(side=RIGHT,fill=Y)
scroll_x.config(command=chattable.xview)
scroll_y.config(command=chattable.yview)
How do I make the scroll_y force scroll downwards?
If you want an item to be visible and you know the id, you can use the see method of the treeview widget.
For example, if you know that the last item has an id of "I064" you can scroll it into view like this:
chattable.see("I064")
If you don't have the id and simply want to scroll, you can scroll the widget by calling one of the yview methods (yview, yview_moveto, yview_scroll).
For example, to scroll to the end you can use yview_moveto with the value 1.0. The number represents what fraction of the contents should be scrolled off of the top. A value of .5 would scroll half-way through the data, a value of 0 scrolls to the top, and 1 scrolls to the bottom.
chattable.yview_moveto(1.0)
I am making a basic text editor and I am saving the scroll position in a file on closing the program. Then when opening the program it will read the scroll position from the file and update it so you can continue where you left off.
I can get the position fine from scrolledtext.yview() which returns a tuple with e.g. (0.42, 0.75)
But I cannot figure out how to change the scroll position. I have tried scrolledtext.vbar.set(0.42, 0.75) to try and update it but that doesn't work as in it doesn't do anything and gives no errors. I have also tried scrolledtext.yview(0.42, 0.75) but it says TclError: bad option "0.42": must be moveto or scroll so if anyone knows how to update it that would be greatly appreciated, cheers.
Edit(Code):
import tkinter as tk
root = tk.Tk()
Frame = frame(root)
Frame.pack()
textbox = ScrolledText(Frame)
textbox.pack()
textbox.yview() #this is saved to file, produces tuple of e.g. (0.42, 0.75)
textbox.vbar.set(0.3, 0.7) #this doesn't produce any errors but doesn't change the scroll position
textbox.yview(0.3, 0.7) #this is also something i have tried but produces the error _tkinter.TclError: bad option "0.4243827160493827": must be moveto or scroll
root.mainloop()
You can't expect the saved yview to work in all cases. If the file has been edited, the proportions could be all wrong.
The tuple you get from yview represents the fraction visible at the top and the fraction visible at the bottom. You can call yview_moveto to set the position at the top, and then let tkinter take care of the fraction at the bottom.
For example, if the yview you've saved is (0.42, 0.75), then you just need to call yview_moveto('0.42'). This will cause the view to be adjusted so that the given offset is at the top of the window.
In case of widgets update with change bbox sizes, i use a followed snippet to keep scroll position:
#before repaint store vsb position caclulated in pixels from top
bbox = canvas.bbox(ALL)
self.mem_vsb_pos = canvas.yview()[0] * (bbox[3] - bbox[1])
#after repaint (back calculation):
bbox = canvas.bbox(ALL)
canvas.yview_moveto(self.do_vsb_pos / (bbox[3]-bbox[1]))
#before repaint - if need repaint from top
self.mem_vsb_pos = 0.0
I'm building a GUI that have 2 text widgets. (I mean it has a bunch of things in it but the sake of this question lets leave it at 2 text widgets). What I want to do is that when I scroll the one text widget with the arrow key the other text widget also scrolls at the same time. I was able to accomplish this with the scrollbar (not shown in code) but, not with the arrow keys. I want the arrow key normal behaviour on both text areas at the same time. That is to say that when it gets to the bottom of the viewable text it scrolls down but if I scroll back up the text doesn't move just the arrow. You know, like any normal text editor. So the question is how do I accomplish this? Here is a snippet of my code.
#create Text widgets
descriptionTextField = Text(mainframe, width=40, height=10)
descriptionTextField.grid(column=2, row=5, sticky=(W))
descriptionTextField.bind("<Down>", OnEntryDown)
descriptionTextField.bind("<Up>", OnEntryUp)
pnTextField = Text(mainframe, width=40, height=10)
pnTextField.grid(column=3, row=5, sticky=(W))
pnTextField.bind("<Down>", OnEntryDown)
pnTextField.bind("<Up>", OnEntryUp)
#here are what I have for code that **DOESN'T** do what I want.
def OnEntryDown(event):
descriptionTextField.yview_scroll(1,"units")
pnTextField.yview_scroll(1,"units")
def OnEntryUp(event):
descriptionTextField.yview_scroll(-1,"units")
pnTextField.yview_scroll(-1,"units")
There has to be a way to find out when the next arrow key will be greater than the viewable area (in this case 10) and then scroll other wise just move the cursor.
NOTE: I can't get the code for up "< Up >" and down "< Down >" arrow to show up in my code above but believe me it is there.
Instead of trying to duplicate what the arrow key does, a different method would be to sync the two windows after the key has been processed (ie: set the yview of one to the yview of the other)? You can move the insertion cursor at the same time if you want. This technique will only work if the two widgets have the same number of lines.
While the right way would be to adjust the bindtags so that you create a binding after the class bindings, you can avoid that complication with the knowledge that tkinter processes the key press events. This means you can add bindings to key release events. It yields a tiny lag though.
It would look something like this:
descriptionTextField("<KeyRelease-Up>", OnArrow)
descriptionTextField("<KeyRelease-Down>", OnArrow)
pnTextField("<KeyRelease-Up>", OnArrow)
pnTextField("<KeyRelease-Down>", OnArrow)
...
def OnArrow(event):
widget = event.widget
other = pnTextField if widget == descriptionTextField else descriptionTextField
other.yview_moveto(widget.yview()[0])
other.mark_set("insert", widget.index("insert"))
Using bindtags eliminates the lag. You can set it up like this:
for widget in (descriptionTextField, pnTextField):
bindtags = list(widget.bindtags())
bindtags.insert(2, "custom")
widget.bindtags(tuple(bindtags))
widget.bind_class("custom", "<Up>", OnArrow)
widget.bind_class("custom", "<Down>", OnArrow)
[Edits noted:]
I want to hook into the ScrolledText widget so that when a user clicks anywhere in the scrollbar (or even scrolls it with the mouse wheel, hopefully) I can have it call a callback function where I can
[Add: "set a flag, and then return to let the ScrolledText widget do its thing."]
[Delete: " do something first (like turn off automatic scrolling) before the scrolling action takes place."]
Is this possible?
Thanks
Do you want to do something like turning off automatic scrolling, or is that actually what you want to do?
If you want to turn automatic scrolling on or off, just check the position of the text before inserting text. If it's at the end, add the text and autoscroll. If it's not at the end, add the text but don't scroll. This will work even if they scrolled by some other mechanism such as by using the page up / page down keys.
You can check a couple of different ways. I think the way I've always done it (not at my desktop right now to check, and it's been a few years...) is to call dlineinfo on the last character. If the last character is not visible, this command will return None. You can also use the yview command to see if the viewable range extends to the bottom. It returns two numbers that are a fraction between zero and one, for the first and last visible line.
While the user can't turn auto-scrolling on or off by clicking a button, this is arguably better because it will "just happen" when they scroll back to see something.
Not without reaching inside the ScrolledText to get at the Scrollbar and the Text and hook their bindings.
And, while you can do that, at that point, why even use ScrolledText? The whole point is that it's does the scroll bindings automagically without you having to understand them. If you don't want that, just use a Scrollbar and a Text directly. Tkinter Scrollbar Patterns explains how to do this in detail, but really, if you don't want to do anything unusual, it's just connecting a message from each one to a method on the other.
For example:
from Tkinter import *
def yscroll(*args):
print('yscroll: {}'.format(args))
scrollbar.set(*args)
def yview(*args):
print('view: {}'.format(args))
textbox.yview(*args)
root = Tk()
scrollbar = Scrollbar(root)
scrollbar.pack(side=RIGHT, fill=Y)
textbox = Text(root, yscrollcommand=yscroll)
for i in range(1000):
textbox.insert(END, '{}\n'.format(i))
textbox.pack(side=LEFT, fill=BOTH)
scrollbar.config(command=yview)
mainloop()
If you can't muddle out the details from the (sometimes confusing and incomplete) docs, play around with it. Basically, yview is called whenever the scrollbar is moved, and yscroll is called whenever the view is scrolled. The arguments to yscroll are obvious; those to yview less so, but the docs do explain them pretty well.
Note that, when you've set things up normally, dragging the scrollbar or swiping the trackpad or rolling the mousewheel over the scrollbar sends a yview, which makes our code call textbox.yview, which then sends a yscroll, and that does not cause a new yview (otherwise, there would be an infinite loop). So, you see both methods get called. On the other hand, swiping the trackpad or rolling the mousewheel over the text, or using the keyboard to move off the bottom, sends yscroll, which again does not cause a yview, so in this case you only see one of the two methods.
So, for example, if you change yview to not call textbox.yview, you can drag the scrollbar all you want, but the text view won't move. And if you change yscroll to not call scrollbar.set, you can swipe around the text all you want, but the scrollbar won't move.
If you want a horizontal scrollbar as well, everything is the same except with x in place of y. But ScrolledText doesn't do horizontal scrolling, so I assume you don't want it.
If you really do want to dig into ScrolledText, you can look at the source for your version, which is pretty trivial if you understand the example above. In fact, it's basically just an OO wrapper around the example above.
In at least 2.7 and 3.3, the ScrolledText is itself the Text, and its self.vbar is the Scrollbar. It sets yscrollcommand=self.vbar.set in its superclass initialization, and sets self.vbar['command'] = self.yview after vbar is constructed. And that's it.
So, just remove the explicit scrollbar creation, and access it as textbox.vbar, and the same hooking code as above works the same way:
from Tkinter import *
from ScrolledText import *
def yscroll(*args):
print('yscroll: {}'.format(args))
textbox.vbar.set(*args)
def yview(*args):
print('yview: {}'.format(args))
textbox.yview(*args)
root = Tk()
textbox = ScrolledText(root)
for i in range(1000):
textbox.insert(END, '{}\n'.format(i))
textbox.pack(side=LEFT, fill=BOTH)
textbox['yscrollcommand'] = yscroll
textbox.vbar.config(command=yview)
mainloop()
Just be aware that this (the fact that textbox is a normal Text, and textbox.vbar is its attached Scrollbar) isn't documented anywhere, so it could theoretically change one day.
I'm attempting to write a basic Tkinter GUI that has a Text widget at the top, then a Button widget left aligned under it, then another Text widget underneath the button. The problem I'm having is, after packing the Button widget to the left, when I then go to pack the second Text widget, it puts it next to the button on the right, rather than underneath the button. This happens regardless of what I set the side argument to for the second Text widget Here's a simple piece of code that demonstrates this behaviour:
from Tkinter import *
root = Tk()
w = Text(root)
w.pack()
x = Button(root, text="Hi there!")
x.pack(side=LEFT)
y = Text(root)
y.pack(side=BOTTOM)
root.mainloop()
So how would I go about setting up the second Text widget so that it appears below the button, rather than to the right of it?
There are generally two solutions to layout problems:
switch to using grid. It becomes real easy to do layouts like what you are trying to accomplish. Grid can solve probably 95% of all layout issues (it's amazing when you think about it -- Tk does with one manager what most toolkits need half a dozen to accomplish!)
use multiple frames. If some widgets need to be stacked top-to-bottom and some left-to-right you can't always get what you want packing everything in a single frame. Use one frame for the top-to-bottom parts of the layout and additional frames for the left-to-right content.
Also realize that widgets don't have to be children of the widget in which they are packed/gridded. You can use the "in" parameter to put widgets in some other container than their parent.
For example, in your specific example you can create three frames, top, middle, bottom. Pack these top-to-bottom in your toplevel window. Then you can pack the first text widget in the top, the button or buttons horizontally in the middle, and the other text widget in the bottom.
The advantage to such an approach is that it makes it much easier to change the layout in the future (which in my experience always happens at some point). You don't have to re-parent any of your widgets, just pack/place/grid them in some other container.
In your short example it doesn't make much difference, but for complex apps this strategy can be a life saver.
My best advice is this: layout isn't an afterthought. Do a little planning, maybe even spend five minutes drawing on some graph paper. First decide on the major regions of your app and use a frame or some other container for each (paned window, notebook, etc). Once you have those, do the same divide-and-conquer approach for each section. This lets you use different types of layout for different sections of your app. Toolbars get horizontal layout, forms might get vertical layout, etc.
I was initially misunderstanding how packing worked and didn't realise that the entire left side was being "claimed" when i did x.pack(side=LEFT). What I found after reading this and the answer by Alex here is that I was not really after having x packed to the left side at all, but rather having it anchored to the left, using anchor=W (W for West) instead of side=LEFT. My revised code snippet which does what I was after looks like this:
from tkinter import *
root = Tk()
w = Text(root)
w.pack()
x = Button(root, text="Hi there!")
x.pack(anchor=W)
y = Text(root)
y.pack(side=BOTTOM)
root.mainloop()
This way x is not "claiming" the left side anymore, it's just aligned to the left (or West) within its block of space.
Packing happens in the order the .pack methods are called, so once x has "claimed" the left side, that's it -- it will take up the left portion of its parent and everything else within its parent will be to its right. You need a Frame to "mediate", e.g....:
from Tkinter import *
root = Tk()
w = Button(root, text="Mysterious W")
w.pack()
f = Frame(root)
x = Button(f, text="Hi there!")
x.pack()
y = Button(f, text="I be Y")
y.pack(side=BOTTOM)
f.pack(side=LEFT)
root.mainloop()
(changed Texts to Buttons for more immediate visibility of layout only -- the Tkinter on this Mac doesn't show Texts clearly until they have focus, but Buttons are quite clear;-).
Do it the same way that WebView does using the Mosaic Canvas Widget Sets internals(which are very similar to Tk). The trick is that the second identical named Frame Object works as a Block Level Float(inline:block;) for everything placed after it and everything that calls "fr" already will automatically begin over inside of it.
You can have many doing this of TOP aligned widgets and simply add another identical named Frame where you want to break between side=LEFT's. Works after Bottom also.
fr=Frame(root)
fr.pack(fill=X, side=TOP)
block1=Label(fr)
block1.pack(side=LEFT)
block2=Label(fr)
block2.pack(side=LEFT)
block3=Button(fr)
block3.pack(side=LEFT)
# NAME IT THE SAME ID NAME AS THE FIRST MAIN FRAME...
fr=Frame(root)
fr.pack(fill=X, side=TOP)
# These NOW jump into the second Frame breaking the side=LEFT in new Frame
block4=Label(fr)
block4.pack(side=LEFT)
block5=Label(fr)
block5.pack(side=LEFT)
# AND THEY CONTINUE GOING side=LEFT AFTERWARDS.