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.
Related
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)
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 fairly new to python and try to build a simple GUI following an object oriented approach. Therefor I let my widget classes inherit from tk.Frame and create an application controller to build the GUI.
My application contains the following two files:
mainModule.py
# coding: utf8
from testPackage import myGUI
import Tkinter as tk
root = tk.Tk() # Main window
my_gui = myGUI.MainApplication(root)
root.mainloop() # Hold window open until we close it
myGUI.py
# coding: utf8
import Tkinter as tk
# Application initializer
class MainApplication(tk.Frame):
def __init__(self, master):
self.master = master
tk.Frame.__init__(self, master)
self.configure_gui()
self.pack() # <--- CODE IN QUESTION
self.create_widgets()
def configure_gui(self):
self.master.title("Any title")
self.master.geometry('800x600')
self.master.minsize(600, 100)
def create_widgets(self):
self.main_window = MainWindow(self)
self.main_window.pack()
# Main Data Window
class MainWindow(tk.Frame):
def __init__(self, master):
self.master = master
tk.Frame.__init__(self, master)
self.main_window = tk.Label(self, text="This is a test")
self.main_window.pack(side="top", fill="x")
Initially running my code without self.pack() (marked as #Code in question) in the MainApplication class definition gave me only the root basic window without the Label created from MainWindow class.
As simple as the answer may be but why?
I used a few sources to get into the topic inbefore and some of them didn't use any geometry manager on the root window (or i'm to inexpierienced to see it. Source below for examples).
Source:
https://www.begueradj.com/tkinter-best-practices.html
http://python-textbok.readthedocs.io/en/latest/Introduction_to_GUI_Programming.html#putting-it-all-together
Tkinter example code for multiple windows, why won't buttons load correctly?
Only after reading following answers regarding widget creation I came aware of the missing part:
Best way to structure a tkinter application
creating a custom widget in tkinter
Figuring that pack() on initialization would be the same as
MainApplication(root).pack(side="top", fill="both", expand=True)
or
Example(root).place(x=0, y=0, relwidth=1, relheight=1)
for a grid based layout.
I guess it's a very simple or obvious element i don't see relating to inheritance but i'm not entirely sure why have to pack() the MainApplication Frame and furthermore why it seems to work for example without this step.
Because MainWindow inherits from Frame, it is itself a frame. If you never call pack, place, or grid on it, it will be invisible. This is no different than if you created a button or scrollbar or any other widget and then don't call one of those methods on it.
Since all of the other widgets are a children of this frame, they will be invisible since their parent is invisible.
Somewhat unrelated to the question being asked, self.pack() is a code smell. Generally speaking, a class should never call pack, place or grid on itself. This tightly couples itself to the caller (meaning, this widget has to know that the caller is using one of those methods).
In other words, if you decide that MainApplication wants to switch from pack to grid for all of its children, you can't just update MainApplication, you also have to update MainWindow. In this case, the root window has only one child so the problem is fairly small, but by doing it this way you are starting a bad practice that will eventually cause you problems.
The rule of thumb is that the function that creates a widget should be responsible for adding it to the screen. That means that it would be better to do it like this:
root = tk.Tk()
my_gui = myGUI.MainApplication(root)
my_gui.pack(fill="both", expand=True)
You would then need to remove self.pack() from MainApplication.__init__.
You also have some very misleading code that might be contributing to the confusion. Take a look at this code:
# Main Data Window
class MainWindow(tk.Frame):
def __init__(self, master):
...
self.main_window = tk.Label(self, text="This is a test")
self.main_window.pack(side="top", fill="x")
Notice how you have a class named MainWindow. Within that you have a label that is named self.main_window, but self.main_window is not a MainWindow. This is especially confusing since the function that creates MainWindow also creates an instance variable named self.main_window.
You might want to consider renaming this label to be something else (eg: self.label, self.greeting, etc).
As the title says, when attempting to save the Canvas using Postscript, it works fine with all non-window elements (rects, ovals etc..) and it works perfectly in capturing window elements that are CURRENTLY on screen when I push the button. But none of the window elements outside of the screen at the time.
This issue seems so arbitrary I gotta wonder if there even is a solution, hopefully someone out there has figured something out.
Here is some example code, where I simplify to present the exact issue:
#!/usr/bin/python3
#
# This file is intended as a simplified example for Stack Overflow.
# The original program is far greater and is a writing tool for branching dialogue, much like Twine.
from tkinter import Tk, Canvas, Frame, Text, Label
class Canv(Canvas):
def __init__(self, parent):
"""Simple Canvas class."""
Canvas.__init__(self, parent)
self.parent = parent
self.config(background="white", width=960, height=640)
self.num = 1
self.pack()
self.bindings()
def bindings(self):
"""All the button bindings."""
self.bind("<Button-1>", self.add_window)
self.bind("<ButtonPress-2>", self.mark)
self.bind("<ButtonRelease-2>", self.drag)
self.bind("<Button-3>", self.take_ps)
def add_window(self, e):
"""Here I add the Label as a Canvas window.
And include an Oval to mark its location.
"""
text = "Textwindow {}".format(self.num)
self.num += 1
window = TextWindow(self, text)
pos = (self.canvasx(e.x), self.canvasy(e.y))
self.create_window(pos, window=window)
bbox = (pos[0]-50, pos[1]-50, pos[0]+50, pos[1]+50)
self.create_oval(bbox, width=3, outline="green")
def mark(self, e):
"""Simple Mark to drag method."""
self.scan_mark(e.x, e.y)
def drag(self, e):
"""This drags, using the middle mouse button, the canvas to move around."""
self.scan_dragto(e.x, e.y, 5)
def take_ps(self, e):
"""Here I take a .ps file of the Canvas.
Bear in mind the Canvas is virtually infinite, so I need to set the size of the .ps file
to the bounding box of every current element on the Canvas.
"""
x1, y1, x2, y2 = self.bbox("all")
self.postscript(file="outfile.ps", colormode="color", x=x1, y=y1, width=x2, height=y2)
print("Writing file outfile.ps...")
class TextWindow(Frame):
def __init__(self, parent, text):
"""Very simple label class.
Might have been overkill, I originally intended there to be more to this class,
but it proved unnecesary for this example.
"""
Frame.__init__(self, parent)
self.pack()
self.label = Label(self, text=text)
self.label.pack()
if __name__ == "__main__": #<---Boilerplate code to run tkinter.
root = Tk()
app = Canv(root)
root.mainloop()
This is an example .jpg based on the postscript.
As you can see from the image, all the green circles on the right have the window label intact. Well ALL the green circles are supposed to have them, and in the program they work fine, the are just not showing in the postscript. And yes, my screen was over the right circles when I clicked the take_ps button.
As for alternatives, I need the canvas to be draggable, I need it to expand, potentially vast distances in both direction. And I cannot put text directly on the canvas, as it would take too much space. It is intended to have text fields, not just a label in the windows on the canvas (became too much code for this example), and the reason I need text in a window, and not directly on the screen, is the text might easily take more space then it should. I need the canvas to show the RELATION between the text fields, and the text windows to contain the text for editing, not necessarily full display. As it says, I'm making a branching dialogue tool for a game, much like Twine.
I ran into this issue also. I was able to configure the canvas temporarily to match the size of the output image. Then I configured it back to the original size after I was done creating the postscript file.
height_0 = canvas.winfo_height()
width_0 = canvas.winfo_width()
canvas.config(width= max_width, height= max_height)
root.update()
canvas.postscript(file='filename.ps',colormode='color')
canvas.config(width= width_0, height= height_0)
root.update()
The canvas dosen't work for me with tkinter, i got menu working, also toplevel windows but not canvas. Here is my example:
class Interface(Frame):
def __init__(self, master=None):
self.__loadSettings()
Frame.__init__(self,master)
self.m=Menu(self)
menu = Menu(self.m, tearoff=0)
self.m.add_cascade(label="File", menu=menu)
menu.add_command(label="New", command=self.__newGame)
menu = Menu(self.m, tearoff=0)
self.m.add_cascade(label="Edit", menu=menu)
menu.add_command(label="Settings", command=self.__settings)
self.master.config(menu=self.m)
self.canvas= Canvas(self,height=500, width=500)
self.canvas.create_rectangle(100,100,400,400, fill="blue")
root = Tk()
ui = Interface(root)
ui.mainloop()
The windows and menu works, but not the canvas.
The first problem is that you put the canvas in a frame (an instance of Interface) but you never make this frame visible. Since this frame is designed to be the whole UI (I'm assuming), you can do this:
root = Tk()
ui = Interface(root)
ui.pack(side="top", fill="both", expand=True)
ui.mainloop()
Notice that I call pack on the ui object.
That only solves half of the problem. The second problem is that you aren't making the canvas visible in its parent. You can use pack, grid or place for that. Here I use pack:
self.canvas.pack(side="top", fill="both", expand=True)
You seem to have a third problem as well -- you're creating a menu but you aren't causing it to be visible, either. In the case of a menubar, you usually give it as the value of the menu attribute of a root window. In your case you might want to do something like this:
self.master.configure(menu=self.m)