Tkinter Canvas Postscript not capturing Window elements outside of screen frame - python

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()

Related

How to wrap the text in a tkinter label dynamically?

The text in a label in tkinter can be wrapped into multiple lines when exceeding the limit given in the parameter wraplength.
However, this is a number of pixels, but instead, I'd like to use the full window width for this, and the wrap lenght should change whenever the user is changing the window size.
One approach could be to update the parameter manually with something like this:
def update_wraplength(id, root):
id.configure(wraplength=root.winfo_width())
root.after(10, lambda: update_wraplength(id,root))
Is there another way of doing this, maybe a parameter I do not know about?
You would have to update the wraplength every time the window size changes. You can detect when the window size changes with the "<Configure>" event.
my_label.bind('<Configure>', update_wraplength)
Remember it only works if you have the Label set up to expand to all available space.
Lets see if you can make sense of this code:
import Tkinter as tk
class WrappingLabel(tk.Label):
'''a type of Label that automatically adjusts the wrap to the size'''
def __init__(self, master=None, **kwargs):
tk.Label.__init__(self, master, **kwargs)
self.bind('<Configure>', lambda e: self.config(wraplength=self.winfo_width()))
def main():
root = tk.Tk()
root.geometry('200x200')
win = WrappingLabel(root, text="As in, you have a line of text in a Tkinter window, a Label. As the user drags the window narrower, the text remains unchanged until the window width means that the text gets cut off, at which point the text should wrap.")
win.pack(expand=True, fill=tk.X)
root.mainloop()
if __name__ == '__main__':
main()

With Tkinter, how to position a dialog box at initiation OR how to auto-resize a window after geometry is set?

Disclaimer: In all likelihood this could very well be an XY problem, I'd appreciate if you would point me to the correct direction.
Goal: I have a file that I'd like tkinter to read and then create widgets dynamically based on the file contents. I want the window to always open centered at my cursor.
For the sake of MCVE let's consider a plain text file that reads:
Madness?
This
IS
T K I N T E R
And tkinter should create 4 label widgets, with the main window resized to fit. That's very simple... until the file is encrypted. Before I can read the encrypted file, I need to askstring() for a password first (MCVE: let's also assume we're only asking for the key here).
My issues:
The askstring Dialog is always at a default position. I know it's supposed to be relative to the parent (root)...
But I can't set geometry() before I create the widgets or else it won't resize to the widgets...
And I can't pre-determine the size required for geometry() since I can't open the file without the password (key)...
Here's a MCVE sample of my most successful attempt:
import tkinter as tk
from tkinter.simpledialog import askstring
from cryptography.fernet import Fernet
class GUI(tk.Tk):
def __init__(self):
super().__init__()
# side note: If I don't withdraw() the root first, the dialog ends up behind the root
# and I couldn't find a way to get around this.
self.withdraw()
self.create_widgets()
# Attempt: I tried doing this instead of self.create_widgets():
# self.reposition()
# self.after(ms=1,func=self.create_widgets)
# The dialog is positioned correctly, but the window size doesn't resize
self.deiconify()
# I have to do the reposition AFTER mainloop or else the window size becomes 1x1
self.after(ms=1, func=self.reposition)
self.mainloop()
def get_key(self):
return askstring('Locked','Enter Key', show='*', parent=self)
def create_widgets(self):
self.lbls = []
with open('test2.txt', 'rb') as file:
encrypted = file.read()
key = self.get_key()
suite = Fernet(key)
self.data = suite.decrypt(encrypted).decode('utf-8')
for i in self.data.split('\n'):
self.lbls.append(tk.Label(self, text=i.strip()))
self.lbls[-1].pack()
def reposition(self):
width, height = self.winfo_width(), self.winfo_height()
self.geometry(f'{width}x{height}+{self.winfo_pointerx()-int(width/2)}+{self.winfo_pointery()-int(height/2)}')
gui = GUI()
Which achieves (that I can live with):
✓ Correct size based on file content
✓ Root positioned at center of cursor
⨯ Prompts for key at center of cursor
My questions are:
Is it possible to perform an auto-resize on the root again based on the widgets function similar to pack_propagate() after geometry() is set? It seems once geometry() is set the root won't propagate no more.
If not, how can I manually resize it in code? I tried retrieving the total heights of the widgets height = sum([i.winfo_reqheight() for i in self.lbls]) but height just becomes 0. But when I print(self.lbls[-1].winfo_reqheight()) in the self.create_widgets() it returns 26 each, and they actually print after my self.reposition() call, which is weird.
Failing that, is it possible to position the askstring() dialog prior to the the widgets being created?
I'm stumped. I feel like I'm going about this the wrong way but I'm not sure what is the correct way to handle this situation to break the cycle of dependency.
To help with reproducing here's the encrypted string as well as the key:
Data:
gAAAAABb2z3y-Kva7bdgMEbvnqGGRRJc9ZMrt8oc092_fuxSK1x4qlP72aVy13xR-jQV1vLD7NQdOTT6YBI17pGYpUZiFpIOQGih9jYsd-u1EPUeV2iqPLG7wYcNxYq-u4Z4phkHllvP
Key:
SecretKeyAnyhowGoWatchDareDevilS3ItsAmazing=
Edit: Based on #BryanOakley's answer here I can invoke self.geometry("") to reset, however it goes back to the native 200x200 size, and still doesn't propragate the widgets.
After a dozens more tries I think I got it working, here are the relevant parts I modified:
def __init__(self):
super().__init__()
self.withdraw()
# reposition the window first...
self.reposition()
# For some reason I can't seem to figure out,
# without the .after(ms=1) the dialog still goes to my top left corner
self.after(ms=1, func=self.do_stuff)
self.mainloop()
# wrap these functions to be called by .after()
def do_stuff(self):
self.create_widgets()
self.deiconify()
def reposition(self):
width, height = self.winfo_width(), self.winfo_height()
self.geometry(f'{width}x{height}+{self.winfo_pointerx()-int(width/2)}+{self.winfo_pointery()-int(height/2)}')
# reset the geometry per #BryanOakley's answer in the linked thread
# Seems it resets the width/height but still retains the x/y
self.geometry("")
It would appear #BryanOakley have answers for all things tkinter.
I'm still waiting to see if there will be others chiming in on the subject and will be happy to accept your answer to provide some clarity on the subject.

Scrollbar not working on canvas

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.

Displaying only one frame at a time in tkinter

I've been struggling with this for a while. I think I'm missing some simple piece of information and I hope you guys can help clear this up for me.
I'm trying to get tkinter to display different frames which I will eventually place widgets inside of. Here's what I did:
I've made a class that is supposed to initialize the window and make all the different frames the program will run.
I've made a separate class for each frame(I'm intending to have variables associated with the different classes when the program is done), and assigned a variable that will start that class up and make it run it's init function
I ended the StartUp class by telling it to tkraise() the frame I want displayed, and that's where things stop working correctly.
I set each frame to a different color, so when you run this program you will see that they split the screen space up instead of one being raised to the top. What am I missing?
One last point, I am purposely trying to spell everything out in my program, I learn better that way. I left it so I have to type tkinter.blah-blah-blah in front of each tkinter command so I can recognize them easily, and I decided not to have my classes inherit Frame or Tk or anything. I'm trying to understand what I'm doing.
import tkinter
class StartUp:
def __init__(self):
self.root = tkinter.Tk()
self.root.geometry('300x300')
self.container = tkinter.Frame(master=self.root, bg='blue')
self.container.pack(side='top', fill='both', expand=True)
self.page1 = Page1(self)
self.page2 = Page2(self)
self.page1.main_frame.tkraise()
class Page1():
def __init__(self, parent):
self.main_frame = tkinter.Frame(master=parent.container, bg='green')
self.main_frame.pack(side='top', fill='both', expand=True)
class Page2():
def __init__(self, parent):
self.main_frame = tkinter.Frame(master=parent.container, bg='yellow')
self.main_frame.pack(side='top', fill='both', expand=True)
boot_up = StartUp()
boot_up.root.mainloop()
When you do pack(side='top', ...), top doesn't refer to the top of the containing widget, it refers to the top of any empty space in the containing widget. Page initially takes up all of the space, and then when you pack Page2, it goes below Page1 rather than being layered on top of it.
If you are using the strategy of raising one window above another, you need to either use grid or place to layer the widgets on top of each other. The layering is something pack simply can't do.
Your other choice is to call pack_forget on the current window before calling pack on the new windowl

Horizontal Scrollbar not working in Python TKinter Frame

I have a canvas (for ttk.Frame not having xview,yview methods) holding frames inside of it, which I want to browse horizontally, as they are in the same row. Found some tutorials, and while they were dealing with different combination of widgets, I followed them, and tried to apply them to my problem, but with no success. I do see a scrollbar, but it doesn't do or respond to anything.
class App:
def __init__(self,db):
self.db = db
self.root = tkinter.Tk()
self.masterframe = ttk.Frame(self.root)
self.masterframe.grid()
self.mastercanvas = tkinter.Canvas(self.masterframe)
self.mastercanvas.grid()
self.scrollbar = ttk.Scrollbar(self.masterframe,orient="horizontal",
command = self.mastercanvas.xview)
self.scrollbar.grid()
self.mastercanvas.configure(xscrollcommand=self.scrollbar.set)
for i,e in enumerate(self.db.elements):
xf = XFrame(self,e)
xf.grid(row=0,column=i,sticky="n")
Edit:
class XFrame:
def __init__(self,app,x):
self.app = app
self.x = x
self.frame = ttk.Frame(self.app.mastercanvas)
self.set_up() # this sets frame's padding and populates it with widgets
Now, where ever I paste two lines of code here suggested* - at the end of the first init definition, or at the end of the second init definition - nothing new happens. I see my frames appearing as I intended them to appear - 3 of them. Part of the 4th. And an unfunctional scrollbar.
*
self.update_idletasks() # using self.root in first, self.app.root in second variant
self.mastercanvas.configure(scrollregion=self.mastercanvas.bbox("all"))
# in second variant reffered to as self.app.mastercanvas ...
You've got to configure the scrollregion attribute of the canvas so that it knows how much of the virtual canvas you want to scroll. This should do it:
# create everything that will be inside the canvas
...
# next, cause the window to be drawn, so the frame will auto-size
self.update_idletasks()
# now, tell the canvas to scroll everything that is inside it
self.mastercanvas.configure(scrollregion=self.mastercanvas.bbox("all"))
That being said, there's nothing in your canvas to be scrolled, and I'm not entirely sure what you were expecting to be in the canvas. If you are wanting to scroll through the instances of XFrame, they need to be children of some other frame, and that other frame needs to be an object on the canvas.

Categories