I'm building the GUI for my Python app using tkinter, and I want the app to have a scrollbar that hides and shows when needed so that the window doesn't always have to be full size. I'm programming the code primarily on MacOS, and then Windows (because by the time I get something to work on MacOS getting it work on Windows is easy).
The scrollbar in tkinter, by default, doesn't automatically hide itself when it's not needed, but I found some code in this stackoverflow question that is supposed to do just that. I then made a test file so that I could fiddle around with the code without impacting my larger project. The code I pulled from stackoverflow wasn't quite what I needed, so I edited it in my test file. The code in the test file currently looks like this:
import tkinter as tk
class AutoScrollbar(tk.Scrollbar):
"""Create a scrollbar that hides iteself if it's not needed. Only
works if you use the pack geometry manager from tkinter.
"""
def set(self, lo, hi):
if float(lo) <= 0.0 and float(hi) >= 1.0:
self.pack_forget()
else:
if self.cget("orient") == tk.HORIZONTAL:
self.pack(fill=tk.X, side=tk.BOTTOM)
else:
self.pack(fill=tk.Y, side=tk.RIGHT)
tk.Scrollbar.set(self, lo, hi)
def grid(self, **kw):
raise tk.TclError("cannot use grid with this widget")
def place(self, **kw):
raise tk.TclError("cannot use place with this widget")
#Creating the root, canvas, and autoscrollbar
root = tk.Tk()
vscrollbar = AutoScrollbar(root)
canvas = tk.Canvas(root, yscrollcommand=vscrollbar.set)
canvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
vscrollbar.config(command=canvas.yview)
#Creating the frame its contents
frame = tk.Frame(canvas)
label = tk.Label(frame, text="text", font=("Arial", "512"))
label.pack()
#Stuff that I don't quite understand
canvas.create_window(0, 0, anchor=tk.NW, window=frame)
frame.update_idletasks()
canvas.config(scrollregion=canvas.bbox("all"))
root.mainloop()
However, whenever I run the program, the scrollbar doesn't display on the far right side of the screen like I want to. Instead, it shows up in the bottom right corner and seems to extend a white block across the bottom of the app container. Additionally, the white block seems to count as part of the contents of the frame, which causes the scrollbar to show up early.
Large enough window, no scrollbar
Slightly smaller window with scrollbar
The problem is even worse when I add the code to my application. The app container stays the same size, but the content is all scrunched into a much smaller box in the top left corner which expands when I pack in new widgets.
App homescreen before autoscroll implementation
App homescreen after autoscroll implementation
Expanded app homescreen after autoscroll implementation
I've been fiddling around for days. Any help is appreciated.
From your posted example, the problem comes from how you pack your canvas.
Change:
canvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
To:
canvas.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
Related
I am attempting to create fixed-size canvas widget with scroll bars. The canvas in question could be quite large, and will almost definitely be much larger than the frame containing it. I would like to keep the canvas at its fixed size, but be able to resize the window containing it. The issue I am having is I don't know how to bind the scroll bars to the edge of the window.
I have tried both .pack and .grid. The obvious issue with .grid is that it will simply place the scroll bars next to the canvas. Unfortunately, the canvas must have a fixed size that will always be larger than the window. Whenever I .pack, the canvas appears to resize with the window, even when I explicitly disable expand and set fill to None.
I have made set the background to black for the purpose of clearly seeing the canvas area.
import tkinter as tk
from tkinter import *
class DialogueCreation(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.xbar = tk.Scrollbar(parent, orient=HORIZONTAL)
self.xbar.pack(side=BOTTOM, fill=X)
self.ybar = tk.Scrollbar(parent)
self.ybar.pack(side=RIGHT, fill=Y)
self.item_canvas = tk.Canvas(parent, width=5000, height=5000, xscrollcommand=self.xbar.set, yscrollcommand=self.ybar.set)
self.item_canvas.pack(side=LEFT, expand=FALSE, fill=None)
self.item_canvas.configure(background='black')
if __name__ == '__main__':
root = tk.Tk()
DialogueCreation(root)
root.title("Editor")
root.mainloop()
The canvas is a massive 5000x5000, so I should definitely be able to scroll when the window is small. I want the scrollbars to remain flush with the edges of the window, without resizing my canvas. The scrollbars remain dormant no matter how large or small the window is. I'm assuming the canvas is resizing with the window, which is definitely not the desired result.
Eventually this canvas will have several images displayed on it, and the location of those images must not change on the canvas. I do not believe the issue is with how I bound the scrollbars (I checked several other posts on this website to make sure), but it would not be uncharacteristic if I missed something obvious like that.
When you say you want to create a fixed size canvas, I'm assuming that you mean you want the drawable area to be a fixed size, rather than have a fixed size for the viewable portion of the canvas.
To do that, you need to set the scrollregion attribute to the drawable area. You use the width and height attributes to set the size of the visible portion of the canvas.
Also, hooking up scrollbars is a two way street: you've got to configure the canvas to update the scrollbars, and configure the scrollbars to scroll the canvas.
Note: you made DialogCreation a Frame, yet you put all of the widgets directly in the parent. That's very unusual, and not the best way to do it. I recommend inheriting from Frame like you do, but then all of the widgets should go in self rather than parent.
When you do it this way, you need to make sure you call pack on the instance of DialogCreation, eg:
dr = DialogueCreation(root)
dr.pack(fill="both", expand=True)
Using pack
With pack, you should set the expand option to True if you want the visible portion to grow or shrink when the user resizes the window.
My personal experience is that code is easier to understand and easier to maintain if you separate widget creation from widget layout. The following code shows how I would rewrite your code using pack. Notice the additional lines for configuring xbar and ybar, as well as setting the scrollregion.
class DialogueCreation(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.xbar = tk.Scrollbar(self, orient=HORIZONTAL)
self.ybar = tk.Scrollbar(self)
self.item_canvas = tk.Canvas(self, width=400, height=400,
xscrollcommand=self.xbar.set,
yscrollcommand=self.ybar.set)
self.xbar.configure(command=self.item_canvas.xview)
self.ybar.configure(command=self.item_canvas.yview)
self.item_canvas.configure(scrollregion=(0,0,4999,4999))
self.item_canvas.configure(background='black')
self.xbar.pack(side=BOTTOM, fill=X)
self.ybar.pack(side=RIGHT, fill=Y)
self.item_canvas.pack(side=LEFT, expand=TRUE, fill=BOTH)
Using grid
The obvious issue with .grid is that it will simply place the scroll bars next to the canvas.
I don't see that as obvious at all. grid has no such restriction. You can put the scrollbar anywhere you want.
The important thing to remember with grid is that rows and columns do not automatically grow or shrink when the window as a whole changes size. You have to explicitly tell tkinter which rows and columns to grow and shrink.
To achieve the same effect as with using pack, you need to configure row zero, column zero to be given all extra space. You do that by giving them a weight that is greater than zero.
To use grid instead of pack, replace the last three lines of the above example with the following six lines:
self.item_canvas.grid(row=0, column=0, sticky="nsew")
self.xbar.grid(row=1, column=0, sticky="ew")
self.ybar.grid(row=0, column=1, sticky="ns")
self.grid_rowconfigure(0, weight=1)
self.grid_columnconfigure(0, weight=1)
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 have a tkinter window that I have given a background picture by creating a Label widget with a PhotoImage instance (referencing the image instance through Label attributing).
However when I run the script and move the main window below the start menu (am using Windows 10) or past the sides of the screens for even one moment, all the widgets packed onto the Label (w/ background pic) completely disappear.
They only come back (somewhat) upon hovering over them with the mouse it seems. Also the background picture remains and continues to fill the screen. Could it be that the background picture Label is being "lifted" and makes it seem like the widgets are disappearing? If so, how can I prevent this from happening?
The fix that I have found for now is to not use a Label with a PhotoImage as the parent "frame", but instead use a typical Frame widget with only a background color, but this is not ideal.
import tkinter as tk
root = tk.Tk()
root.geometry('600x350+600+300')
root.resizable(width=False, height=False)
boxBg = '#666'
frameBg = '#fff'
#problem method
backgroundImg = tk.PhotoImage(file='program_media/background.png')
bgFrame = tk.Label(root, image=backgroundImg)
bgFrame.image = backgroundImg
#less than ideal solution so far
#bgFrame = tk.Frame(root, bg='#fff')
bgFrame.pack(expand=1, fill=tk.BOTH)
mainFrame = tk.Frame(bgFrame)
mainFrame.pack(side=tk.TOP)
title = tk.Label(mainFrame, text='Test String')
title.pack(side=tk.TOP)
#widget creation code packed within mainFrame
#...
#... All these widgets (including mainFrame above) are disappearing
#...
#end of widget creation code
root.mainloop()
See what I mean in this screenshot of BEFORE and AFTER moving the main window below the start menu.
I'm creating a json editor in python using tkinter.
I've added a scrollbar by creating a Canvas, and putting a Frame inside it.
Then I set the Scrollbar command to canvas.yview.
Theres two things that are messing up, and I have no idea why.
When I press the scroll buttons (up and down arrows) the canvas is not scrolling
I am packing the scrollbar onto the window (root) right now instead of the frame, because whenever i pack it onto the frame, the tkinter application does not open, and my computer fan starts turning on... Anyone know what is going on here? (Therefore the scrollbar is tiny if you try to run the code)
Here is my code:
EDIT> Code shortened
import Tkinter as tk
import webbrowser
import os
import bjson as bj
class App:
def __init__(self, master):
self.window = master
self.window.geometry("800x450")
self.canvas = tk.Canvas(self.window, width=800, height=400)
self.master = tk.Frame(self.canvas, width=800, height=400)
self.canvas.pack()
self.master.place(x=0, y=0)
scrollbar = tk.Scrollbar(self.window)
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
scrollbar.config(command=self.canvas.yview)
def init(self):
master = self.master
self.frames = {
"Home": HomeFrame(master)
}
self.openFrame = None
self.loadFrame("Home")
def loadFrame(self, frame):
self.openFrame = self.frames[frame]
self.openFrame.display()
def setTitle(self, t):
self.window.title(t)
class Frame:
def __init__(self, master):
self.master = master
self.frame = tk.Frame(master)
self.frame.grid(row=0, column=0, sticky='news')
self.init()
self.frame_create()
def display(self):
self.frame.tkraise() #raises frame to top
self.frame_load() #initializes the frame
def clear(self):
for widget in self.frame.winfo_children():
widget.destroy()
def init(self): pass
def frame_load(self): pass
def frame_create(self): pass
class HomeFrame(Frame):
def frame_create(self):
p = self.frame
for i in range(20):
tk.Label(p, text="This is content... " + str(i)).pack()
for j in range(2):
LineBreak(p)
def LineBreak(p):
tk.Label(p, text="").pack()
root = tk.Tk()
glob = {}
app = App(root)
app.init()
root.mainloop()
It is a bit long, and a bit messy, but you should see how I'm adding the scrollbar in the __init__ of App
Anyone have any idea what's going on, and how to fix it?
Thanks in advance!
There are many things wrong with your code. However, the problem with the scrollbar not working properly has to do with two things you are neglecting to do:
First, scrollbars and widgets require two way communication. The canvas needs to be told about the scrollbar, and the scrollbar needs to be told about the canvas. You are doing one but not the other:
self.canvas.configure(yscrollcommand=scrollbar.set)
scrollbar.configure(command=self.canvas.yview)
Second, you need to configure the scrollregion attribute of the canvas. This tells tkinter what part of the larger virtual canvas you want to be viewable. Typically this is done in a binding on the <Configure> method of the canvas, and usually you will want to set it to the bounding box of everything in the canvas. For the latter you can pass the string "all" to the bbox method:
self.canvas.configure(scrollregion=self.canvas.bbox("all"))
If you know the exact size of the area you want to be scrollable, you can simply set it to that value (eg: scrollregion=(0,0,1000,1000) to scroll around in a region that is 1000x1000 pixels).
The reason for point #2 is that you can't use both pack and grid for widgets that share the same parent. When you do, you'll get the behavior you describe. That is because grid will try to layout all of the widgets. This may result in some widgets changing size. pack will notice the change in the size of one or more widgets and try to re-layout all of the widgets. This may result in some widgets changing size. grid will notice the change in the size of one or more widgets and try to re-layout all of the widgets. And so on.
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.