Scrollbar for Message Widget in Tkinter - python

I am trying to convert a game I had previously written in command line into GUI (https://github.com/abhinavdhere/Share-Trader-PC/releases/tag/v1.0)
I have built a menu like structure of buttons in one frame, and then on clicking help, the previous frame f1 should disappear and help text should be displayed. I used a Message widget to display the text but it is long and needs scrollbar. I tried to add a vertical scrollbar but couldn't make it work. I referred python and tkinter: using scrollbars on a canvas and tried to do it that way but it still displays only the Message but no scrollbar. Here is the function for it:
def help(self):
self.f1.pack_forget()
f2=tk.Frame(self,bg='#FFCC00')
f2.grid(row=0,column=0)
helpMan=open("Game Rules.txt","r")
hText=helpMan.read()
c1=tk.Canvas(f2,width=640,height=480,scrollregion=(0,0,700,500))
c1.pack(side="left",expand=True,fill="both")
text1=tk.Message(f2,text=hText)
c1.create_window(0,0,anchor="nw",window=text1)
scrollY=tk.Scrollbar(f2,orient="vertical",command=c1.yview)
scrollY.pack(side="right",fill="y")
c1.config(yscrollcommand = scrollY.set)
P.S. Why is it such a hassle to make a simple scrollbar?

The message widget does not support scrolling. It is missing the commands yview and xview that are used for the scrolling protocol. It is really just a multiline label. It is also ugly and can't be themed.
You should replace the message widget with a text widget which also displays multiline text and can support scrolling and formatted text using tags to attach styling information if required.
To make the text widget look the same as the Message widget the following should work:
m = Message(root)
txt = Text(root, background=m.cget("background"), relief="flat",
borderwidth=0, font=m.cget("font"), state="disabled")
m.destroy()

Related

How to connect multiple text to one scrollbar with Tkinter

Im coding my personal text editor. But i have a problem with the 2 widget text and the scrollbar (connect one scrollbar to two text).
What is my idea and logic (at the beginning)?
I want to display 2 text, one for writing text entered by user, and one to display the number of the line. I pack both of them, in the root. Then i create a scrollbar, that will scroll on Y axes the 2 text, so what i want to do (mainly) is to connect 2 widget (text) to one scrollbar.
But it didn't work.
This system absolutely doesn't work, are there any suggest or fix to fix this first idea?
Other ideas that i found.
After the first attempt, i thought that i can pack the 2 texts into 1 container. I tried to create a frame (packed into root) that contains the 2 texts, i did this because i have to connect the scrollbar only to the frame. But it didn't work, moreover it didnt allow me to write the following snippet: command=frame.yview in the scrollbar option, it seems that i cant connect frame to scrollbar.
So:
I will ask u if my reasoning are good, and how to solve. If not what can i do?
Similar question found on Google: (but that i dont undestand)
How to scroll two parallel text widgets with one scrollbar?
Tkinter adding line number to text widget
from tkinter import *
root = Tk()
root.geometry("480x540+100+100")
root.config(cursor='')
line = Text(root, bg="light grey", font="Roman 24", width=4)
line.pack(side=LEFT, fill=BOTH)
text = Text(root, bg="grey", font="Roman 24")
text.pack(side=LEFT, fill=BOTH, expand=True)
scrollbar = Scrollbar(text, orient=VERTICAL, command=(line.yview, text.yview))
text.configure(yscrollcommand=scrollbar.set)
line.configure(yscrollcommand=scrollbar.set)
scrollbar.pack(side=RIGHT, fill=Y)
for n in range(50):
line.insert("{}.0".format(n+1), "{}\n".format(n+1))
text.insert("{}.0".format(n+1), "Line no. {}\n".format(n+1))
if __name__ == '__main__':
root.mainloop()
There's nothing special about a scrollbar - it just calls a function when you move it. The API for this function is well defined. While it normally should call the yview (or xview) method of a scrollable window, there's no requirement that it must.
If you want to control two widgets with a single scrollbar, create a function for your scrollbar that scrolls both windows.
def multiple_yview(*args):
line.yview(*args)
text.yview(*args)
scrollbar = Scrollbar(text, orient=VERTICAL, command=multiple_yview)
You will have a similar problem when you scroll the text widget while entering new lines or moving around with cursor keys. You'll need to configure the yscrollcommand attribute of the text widget to call a function so that it both updates the scrollbar and also scrolls the other window (and maybe also add additional line numbers)

how can i scroll text on a python tk/tinker Label?

I'm trying to add a simple log output to a tk window/frame.
So far i only found how to (easily) add a vertical scrollbar on canvas, entry lists and text (which is a fullblown text editor and has no textvariable linking support)
The Label can't be attached to a scrollbar (easily) because it lacks the yview attribute.
#my naive attempt:...
self.lbl_log = tk.Label(self, width=80, height=10, textvariable=self.string_log)
self.lbl_log.pack(side="top")
self.vsb = tk.Scrollbar(self, orient="vertical", command=self.lbl_log.yview)
self.lbl_log.configure(yscrollcommand=self.vsb.set)
AttributeError: 'Label' object has no attribute 'yview'
is there any easy and convenient way to scroll a label widget with several lines in python tk? I don't necessarily have to use a Label i just like it because it is simple and has the textvariable convenience) so i'm open to alternative widgets for this problem.
No, there isn't an easy way to scroll a label. If you need to scroll multiple lines, a label is the wrong choice of widget. If you need to scroll multiple lines of text, the text widget is the proper widget.

Python tkinter scrolling two TEXT widgets at the same time with arrow keys

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)

python tkinter - want to call a function when scrollbar clicked in ScrolledText widget

[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.

Validation on Tkinter.Text widget?

What are my options for getting validation with the Tkinter.Text widget? I don't require Text's advanced functionality, just its multiline attribute. Unfortunately, it lacks both textvariable and validate commands, so it seems that I cannot attach some kind of callback that checks things every time the text changes. I'd like to avoid having to bind to <KeyRelease>, as that looks to capture ALL keypresses, including the likes of Shift, Ctrl, etc, keys, and would appear to be a bit of a mess to work right.
I basically just need to check if the Text field is blank or not, and enable/disable an "Ok" button as appropriate (i.e., if no text, then the button is disabled).
In lieu of this, has anyone come across a decent subclass of Entry that adds multiline functionality that is written in Python? There is this, which adds textvariable to Text, but it is written in pure TCL, not Python, and seems like it would be difficult to integrate into my existing Python environment.
The binding to the <KeyRelease> button does not need to be messy, you don't have to check the value of the key pressed but fetch the content of the widget. Keep in mind that it always has a '\n' at the end, so when you retrive the contents don't forget to discard it:
content = text.get(1.0, "end-1c")
Then you just need to change the state of the button based on this value:
import Tkinter as tk
def configure_ok_button(event):
content = event.widget.get(1.0, "end-1c")
state = "active" if content else "disabled"
button.configure(state=state)
root = tk.Tk()
text = tk.Text(root)
button = tk.Button(root, text="Ok", state="disabled")
text.bind("<KeyRelease>", configure_ok_button)
text.pack()
button.pack()
root.mainloop()

Categories

Resources