I'm just curious how I would go about updating the image opened with the Zoom function WITHOUT calling the Zoom function again. Also if you know of any easier way to do what I'm doing, which is simply just trying to make a image "Zoom-able" with mouse wheel, then my ears are open lol. Any other comments on the code are appreciated, Thank You!
Specifically this part right here:
pictureVar = StringVar()
pictureVar.set("a.png")
Zoom(root, path=pictureVar.get())
pictureVar.set("b.png")
The picture then gets opened using Image.open() here:
class Zoom(Frame):
''' Simple zoom with mouse wheel '''
def __init__(self, mainframe, path):
''' Initialize the main Frame '''
Frame.__init__(self, master=mainframe)
# Vertical and horizontal scrollbars for canvas
vbar = AutoScrollbar(self.master, orient='vertical')
hbar = AutoScrollbar(self.master, orient='horizontal')
vbar.grid(row=0, column=1, sticky='ns')
hbar.grid(row=1, column=0, sticky='we')
# Open image
self.image = Image.open(path) #<------------------------!!!!!!!!!!!
Here is the entire code though:
import csv
import os
from tkinter import *
from PIL import ImageTk, Image
root = Tk()
class AutoScrollbar(Scrollbar):
''' A scrollbar that hides itself if it's not needed.
Works only if you use the grid geometry manager '''
def set(self, lo, hi):
if float(lo) <= 0.0 and float(hi) >= 1.0:
self.grid_remove()
else:
self.grid()
Scrollbar.set(self, lo, hi)
class Zoom(Frame):
''' Simple zoom with mouse wheel '''
def __init__(self, mainframe, path):
''' Initialize the main Frame '''
Frame.__init__(self, master=mainframe)
# Vertical and horizontal scrollbars for canvas
vbar = AutoScrollbar(self.master, orient='vertical')
hbar = AutoScrollbar(self.master, orient='horizontal')
vbar.grid(row=0, column=1, sticky='ns')
hbar.grid(row=1, column=0, sticky='we')
# Open image
self.image = Image.open(path)
# Create canvas and put image on it
self.canvas = Canvas(self.master, highlightthickness=0,
xscrollcommand=hbar.set, yscrollcommand=vbar.set)
self.canvas.grid(row=0, column=0, sticky='nswe')
vbar.configure(command=self.canvas.yview) # bind scrollbars to the canvas
hbar.configure(command=self.canvas.xview)
# Make the canvas expandable
self.master.rowconfigure(0, weight=1)
self.master.columnconfigure(0, weight=1)
# Bind events to the Canvas
self.canvas.bind('<ButtonPress-1>', self.move_from)
self.canvas.bind('<B1-Motion>', self.move_to)
self.canvas.bind('<MouseWheel>', self.wheel)
self.imscale = 0.25
self.imageid = None
self.delta = 0.75
# Text is used to set proper coordinates to the image. You can make it invisible.
self.text = self.canvas.create_text(0, 0, anchor='nw', text='')
self.show_image()
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
def move_from(self, event):
''' Remember previous coordinates for scrolling with the mouse '''
self.canvas.scan_mark(event.x, event.y)
def move_to(self, event):
''' Drag (move) canvas to the new position '''
self.canvas.scan_dragto(event.x, event.y, gain=1)
def wheel(self, event):
''' Zoom with mouse wheel '''
scale = 1.0
# Respond to Linux (event.num) or Windows (event.delta) wheel event
if event.num == 5 or event.delta == -120:
scale *= self.delta
self.imscale *= self.delta
if event.num == 5 or event.delta == 120:
scale /= self.delta
self.imscale /= self.delta
# Rescale all canvas objects
x = self.canvas.canvasx(event.x)
y = self.canvas.canvasy(event.y)
self.canvas.scale('all', x, y, scale, scale)
self.show_image()
self.canvas.configure(scrollregion=self.canvas.bbox('all'))
def show_image(self):
''' Show image on the Canvas '''
if self.imageid:
self.canvas.delete(self.imageid)
self.imageid = None
self.canvas.imagetk = None # delete previous image from the canvas
width, height = self.image.size
new_size = int(self.imscale * width), int(self.imscale * height)
imagetk = ImageTk.PhotoImage(self.image.resize(new_size))
# Use self.text object to set proper coordinates
self.imageid = self.canvas.create_image(self.canvas.coords(self.text),
anchor='nw', image=imagetk)
self.canvas.lower(self.imageid) # set it into background
self.canvas.imagetk = imagetk # keep an extra reference to prevent garbage-collection
pictureVar = StringVar()
pictureVar.set("a.png")
Zoom(root, path=pictureVar.get())
pictureVar.set("b.png")
mainloop()
Related
When the following code is run, a text box appear with scrollable. However, I need to make the textbox resizable with the cursor. How can I it?
import tkinter as tk
import sys
class Redirect():
def __init__(self, widget, autoscroll=True):
self.widget = widget
self.autoscroll = autoscroll
self.output = widget
def write(self, text):
self.widget.insert('end', text)
if self.autoscroll:
self.widget.see("end")
self.output.update_idletasks()
root = tk.Tk()
root.geometry("1200x620+1+1")
frame = tk.Frame(root)
frame.place(x=650, y=310, anchor="c", width=850, height=400 )
text=tk.Text(frame,width=25, height=8, wrap='word')
text.pack(side='left', fill='both', expand=True )
scrollbar = tk.Scrollbar(frame)
scrollbar.pack(side='right', fill='y')
text['yscrollcommand'] = scrollbar.set
scrollbar['command'] = text.yview
old_stdout = sys.stdout
sys.stdout = Redirect(text)
root.mainloop()
sys.stdout = old_stdout
Tkinter doesn't directly support this. However, it includes all of the basic building blocks to accomplish this.
All you need to do is add a resize grip that the user can click on, and then bindings to resize the widget when you click and drag that grip.
Here's a simple example. It uses the ttk Sizegrip widget. That widget has built-in bindings to resize the window, but we can override them with custom bindings to resize the widget instead. This example requires that you use place, since that's what your original example uses.
import tkinter as tk
from tkinter import ttk
class ResizableText(tk.Frame):
def __init__(self, parent, **kwargs):
super().__init__(parent)
self.text = tk.Text(self, **kwargs)
self.scrollbar = tk.Scrollbar(self, command=self.text.yview)
self.scrollbar.pack(side="right", fill="y")
self.text.pack(side="left", fill="both", expand=True)
sizegrip = ttk.Sizegrip(self.text)
sizegrip.place(relx=1.0, rely=1.0, anchor="se")
sizegrip.configure(cursor="sizing")
sizegrip.bind("<1>", self._resize_start)
sizegrip.bind("<B1-Motion>", self._resize)
def _resize_start(self, event):
self._x = event.x
self._y = event.y
return "break"
def _resize(self, event):
delta_x = event.x - self._x
delta_y = event.y - self._y
self.place_configure(
width = self.winfo_width() + delta_x,
height = self.winfo_height() + delta_y
)
return "break"
root = tk.Tk()
root.geometry("1200x620+1+1")
rt = ResizableText(root, width=25, height=8, wrap="word")
rt.place(x=650, y=310, anchor="c", width=850, height=400 )
root.mainloop()
I am making a python progrem using Tkint GUI and the canvas element. I attached a scrollbar to the canvas element so that the user can scroll to unseen regions of the canvas. I created a dotted grid that would allow the user to hover over the dots and if clicked the program draws a circle over the dot. Also as the mouse enters and leave each dot, and dashed circle is drawn and erased.
I also have a print procedures that shows debugging information of the actions performed:
Everything works on the initially visible portion of the canvas. However, when I scroll down, I noticed that the bounded events of click on hover do work, but the canvas graphics are not being triggered/drawn and nothing appears.
I can't understand why initially the graphics were successfully drawn, the event bounded and also working, but the canvas graphics just won't work. Is there some issue between the scroll bar and canvas? Here is the code for the initialization of the canvas and scroll bars:
def __init__(self, parent, model, settings, *args, **kwargs):
super().__init__(parent, *args, **kwargs)
self.width = 625
self.height = 500
self.canvas = tk.Canvas(self, width=self.width, height=self.height, background='white', cursor='arrow')
self.canvas.grid(row=0, column=0)
self.focusDotImage = -1
print("self.focusDotImage: ", self.focusDotImage)
# configure the scroll region
self.canvas.configure(scrollregion=(0, 0, self.width * 2, self.height * 2))
# create scrollbars and connect to canvas
xScroll = tk.Scrollbar(
self,
command=self.canvas.xview,
orient=tk.HORIZONTAL
)
xScroll.grid(row=1, column=0, sticky='new')
yScroll = tk.Scrollbar(self, command=self.canvas.yview)
yScroll.grid(row=0, column=1, sticky='nsw')
self.canvas.configure(yscrollcommand=yScroll.set)
self.canvas.configure(xscrollcommand=xScroll.set)
# Draw Dotted Grid width
self.space = 25
self.dotRadius= 1
self.nodeRadius = 5
for i in range(0, self.width):
for j in range(0, self.height):
dotItem = self.canvas.create_rectangle(i * self.space-self.dotRadius, j * self.space-self.dotRadius, i * self.space + self.dotRadius,
j * self.space + self.dotRadius, fill='lightgrey')
self.canvas.tag_bind(dotItem, '<Enter>', self._on_dot_enter)
self.canvas.tag_bind(dotItem, '<Leave>', self._on_dot_leave)
self.canvas.bind('<Button-1>', self._on_click)
Per comment request, I built a small runnable demo to showcase the weird effects of the canavas/scrolling issue:
import tkinter as tk
def _on_click(event):
print("On click imagine item...", )
global image_item3
canvas.delete(image_item3)
def _on_click_canvas(e):
print("coord {}, {}".format(e.x, e.y))
canvas.create_oval(e.x, e.y, (e.x+10), (e.y+10), fill='white')
# Create root and canvas
root = tk.Tk()
width = 1024
height = 768
canvas = tk.Canvas(
root, background='black',
width=width, height=height,
)
canvas.grid(row=0, column=0)
image_item = canvas.create_oval((200, 200), (300, 300), fill='white')
image_item2 = canvas.create_oval((300, 300), (400, 400), fill='white')
global image_item3
image_item3 = canvas.create_oval((200, 900), (300, 1000), fill='white')
canvas.tag_bind(image_item3, '<Button-1>', _on_click)
canvas.bind('<Button-1>', _on_click_canvas)
# configure the scroll region
canvas.configure(scrollregion=(0, 0, width * 2, height * 2))
# create scrollbars and connect to canvas
xscroll = tk.Scrollbar(
root,
command=canvas.xview,
orient=tk.HORIZONTAL
)
xscroll.grid(row=1, column=0, sticky='new')
yscroll = tk.Scrollbar(root, command=canvas.yview)
yscroll.grid(row=0, column=1, sticky='nsw')
canvas.configure(yscrollcommand=yscroll.set)
canvas.configure(xscrollcommand=xscroll.set)
root.mainloop()
If the canvas is scrolled to other region, the coordinates of the top-left corner of the canvas is not (0, 0) anymore. However (event.x, event.y) is still relative to the top-left corner of the canvas, so (event.x, event.y) will not be the actual coordinates in the canvas.
To get the correct coordinates, you can use self.canvas.canvasx() and self.canvas.canvasy() to convert the event coordinates to actual canvas coordinates.
I am coding a game in Python with tkinter. The basic functionality is that an image is displayed but it's covered with a grid of boxes that can be destroyed. I've written a linear version of the code that is as simplified as I can get it:
from tkinter import *
from PIL import ImageTk, Image
class Box:
def __init__(self, parent, row, column, width, height):
self.row = row
self.column = column
self.canvas = Canvas(parent, bg='black', width=width, height=height)
self.canvas.grid(row=row, column=column, sticky=NSEW)
self.canvas.bind('<Button-1>', self.destroy)
def destroy(self, event):
print('Destroyed box at', self.row, self.column)
self.canvas.destroy()
root = Tk()
root.title('Image Reveal')
root.state('zoomed')
image_raw = None
# Create a frame for the image
image_frame = Frame(root, highlightbackground="blue", highlightthickness=5)
image_frame.pack(expand=True, anchor=CENTER, fill=BOTH, padx=0, pady=0)
# Load image and put it in a label
image_raw = Image.open('image.png')
image = ImageTk.PhotoImage(image_raw)
image_label = Label(image_frame, image=image, borderwidth=5, relief='ridge')
image_label.pack(anchor=CENTER, expand=True)
# Calculate the size of the boxes
image_label.update()
width = image.width()
height = image.height()
column_amount = 10
row_amount = 10
box_width = width / column_amount
box_height = height / row_amount
# Create boxes that fill the grid
for row in range(row_amount):
for column in range(column_amount):
Box(image_label, row, column, box_width, box_height)
root.mainloop()
You can click on the box to destroy it and it also logs the coordinates of the box destroyed.
The desired behavior: You can click on the boxes to destroy them, gradually revealing the image. The other boxes should stay in the same place (the same cells of the grid), so the grid shape doesn't change.
The unexpected behavior: Destroying one box makes the rest of the boxes disappear somehow. Yet you can still click them (it still logs the destruction message). Overall this is the closest I've got to my desired behavior but I can't figure out why this is happening or how to approach this functionality better.
WORKING CODE:
Lukas Krahbichler helped achieve most of the desired behavior. Here is the updated code.
Destroying the boxes however led to the grid resizing when a full row or a column of boxes gets destroyed. In order to fix that, I instead hide the box by lowering it behind the image. At first that didn't work, but then I found this -
Turns out to lower the WHOLE canvas, there is no direct method for that. The final code:
from tkinter import *
from PIL import ImageTk, Image
class Box:
def __init__(self, root, image_frame, image_label, row, column):
self.root = root
self.row = row
self.column = column
self.image_label = image_label
self.image_frame = image_frame
self.visible = True
self.canvas = Canvas(image_frame, bg='black')
self.canvas.grid(row=row, column=column, sticky=NSEW, padx=5, pady=5)
self.canvas.bind('<Button-1>', self.hide)
def hide(self, event):
# Lower the box under the image to hide it
self.canvas.tk.call('lower', self.canvas._w, None)
self.visible = False
print('Hid box at', self.row, self.column)
root = Tk()
root.title('Image Reveal')
root.state('zoomed')
image_raw = None
column_amount = 10
row_amount = 10
boxes = []
# Create a frame for the image
image_frame = Frame(root, highlightbackground="blue", highlightthickness=0)
image_frame.pack(anchor=CENTER, fill=None, padx=0, pady=0)
# Load image and put it in a label
image_raw = Image.open('image.png')
image = ImageTk.PhotoImage(image_raw)
width = image.width()
height = image.height()
# Create a label for the image
image_label = Label(image_frame, image=image, borderwidth=0, relief='ridge')
# Configure the image frame so it doesn't resize <----- UPDATED
image_frame.config(width=width, height=height)
image_frame.grid_propagate(False)
image_label.grid_propagate(False)
# Place the image label in the frame
image_label.grid(rowspan=row_amount, columnspan=column_amount, sticky="NSEW")
# Configure the grid weights <----- UPDATED
for row in range(10):
image_frame.rowconfigure(row, weight=1)
for column in range(10):
image_frame.columnconfigure(column, weight=1)
# Calculate the size of the boxes
image_label.update()
# Create boxes that fill the grid
for row in range(row_amount):
for column in range(column_amount):
box = Box(root, image_frame, image_label, row, column)
boxes.append(box)
root.mainloop()
In tkinter you are not really supposed to place other widgets (for a example a Canvas) in a Label. If you grid everything in the "image_frame" and give the "image_label" a column and a rowspan it should work.
from tkinter import *
from PIL import ImageTk, Image
class Box:
def __init__(self, parent, row, column, width, height):
self.row = row
self.column = column
self.canvas = Canvas(parent, bg='black', width=width, height=height)
self.canvas.grid(row=row, column=column, sticky=NSEW)
self.canvas.bind('<Button-1>', self.destroy)
def destroy(self, event):
print('Destroyed box at', self.row, self.column)
self.canvas.destroy()
root = Tk()
root.title('Image Reveal')
root.state('zoomed')
image_raw = None
# Create a frame for the image
image_frame = Frame(root, highlightbackground="blue", highlightthickness=5)
image_frame.pack(expand=True, anchor=CENTER, fill=BOTH, padx=0, pady=0)
# Define row and column amount
column_amount = 10
row_amount = 10
# Load image and put it in a label
image_raw = Image.open('image.png')
image = ImageTk.PhotoImage(image_raw)
image_label = Label(image_frame, image=image, borderwidth=5, relief='ridge')
image_label.grid(rowspan=row_amount, columnspan=column_amount, sticky="NSEW")
# Calculate the size of the boxes
image_label.update()
width = image.width()
height = image.height()
box_width = width / column_amount
box_height = height / row_amount
# Create boxes that fill the grid
for row in range(row_amount):
for column in range(column_amount):
Box(image_frame, row, column, box_width, box_height)
root.mainloop()
here is my problem. I'm trying to convert my canvas drew into an image. First of all, I create the postscript of my canvas drew, then I convert it into png. The problem is that I obtain white images w/o changes.
So,there is my class:
class Paint(Frame):
def __init__(self, parent):
Frame.__init__(self, parent)
self.parent = parent
self.color = "white"
self.brush_size = 5
self.setUI()
def draw(self, event):
self.canv.create_rectangle(event.x - 7, event.y - 7, event.x + 7, event.y + 7,
fill=self.color, outline=self.color)
def setUI(self):
self.pack(fill=BOTH, expand=1)
self.canv = Canvas(self, bg="black")
self.columnconfigure(0, weight=1)
self.rowconfigure(0, weight=1)
self.canv.grid(padx=5, pady=5, sticky=E + W + S + N)
self.canv.bind("<B1-Motion>", self.draw)
def clear(self):
self.canv.delete(ALL)
def save(self):
self.canv.update()
self.canv.postscript(file="D:\\Новая папка\\test.xps", colormode="color")
im2 = Image.open("D:\\Новая папка\\test.xps")
im2.save("D:\\Новая папка\\test.png", 'png')
My main:
root = Tk()
frame=Canvas(root,height=200,width=300)
root.geometry("500x600")
app = Paint(frame)
frame.create_rectangle(10,10,50,50)
frame.pack()
b3= Button(
text="Apply!",
width=35,
height=1,
bg="white",
fg="black",
command=lambda :[which_button("Activated"),b3_clicked(),app.save()],
font=25
)
b3.pack(side=TOP)
root.mainloop()
When you save the canvas drawings to postscript file, the background color of the canvas is ignored. Since you have used white color as the paint color, so the output drawings are white on white which looks like nothing is drawn.
If you want to have a black background in the output postscript file, use .create_rectangle(..., fill="black") to fill the canvas with black color.
class Paint(Frame):
...
def setUI(self):
# better call pack() outside Paint class
#self.pack(fill=BOTH, expand=1)
self.canv = Canvas(self)
# fill the canvas with black color
# make sure the rectangle is big enough to cover the visible area
self.canv.create_rectangle(0, 0, 1000, 1000, fill="black")
...
Also below is how you should create the Paint class:
root = Tk()
root.geometry("500x600")
app = Paint(root)
app.pack(fill=BOTH, expand=1)
...
Background
I am creating a cartesian graph GUI where each node on the graph is a Frame() object placed on the grid() of my Tkinter object. This is the interface for a pathfinding algorithm.
Goal
I want to change the color of whatever node is clicked on. This would create an "obstacle."
Problem
The mouse coordinates are relative to the individual frames within the graph, instead of the entire window.
For example, a node at (9,9) in a 10x10 graph should output pixel coordinates of the location in the window. However, the coordinates are relative to the position inside the node. So the top left is 0,0 when it should be approximately 200,200.
Should I try and map the coordinates to the grid? or is that just an object that structures the window? Is there a way to overwrite the mouse position to only the parent window?
TLTR
The coordinates of my mouse are relative to the Frame() in the grid instead of the parent window of the Tkinter object.
import tkinter as tk
class Node:
""" Constructor and other methods """
def draw(self, x, y):
if self.nodeType == 'open':
frame = tk.Frame(
master=window,
bg="black",
borderwidth=1,
width = 40,
height = 40
)
frame.grid(row=x,column=y)
label = tk.Label(master = frame, text=" ")
label.pack()
""" Other conditions drawing start and end nodes which are identical """
class Graph:
def __init__(self,master,size,startX,startY,endX,endY):
""" Miscelaneous constructor stuff for A* Algorithm """
window.bind("<Button-1>", self.handleMouseClick)
def _eventCoords(self, event):
row = int(event.y)
col = int(event.x)
print(row),
print(col)
return row, col
def handleMouseClick(self, event):
print(event.x, event.y)
row, column = self._eventCoords(event)
if(self.graph[row][column].nodeType != 'open'):
return
self.graph[row][column].nodeType = "closed"
self.drawObsticle(row, column)
def drawObsticle(self, row, col):
frame = tk.Frame(
master=window,
bg="black",
borderwidth=1,
width = 40,
height = 40
)
frame.grid(row=row,column=col)
label = tk.Label(master = frame, bg = "gray", text=" ")
label.pack()
def displayGraph(self):
""" Trivail 2D for loop """
def getAdj(self,node):
""" Path helper function """
def inRange(self,x,y):
""" Path helper function """
def findShortestPath(self, event):
""" Path Algorithm """
size = int(sys.argv[1])
startX = int(sys.argv[2])
startY = int(sys.argv[3])
endX = int(sys.argv[4])
endY = int(sys.argv[5])
window = tk.Tk()
w = Graph(window, size, startX, startY, endX, endY)
window.title('Path Finder')
window.mainloop()
Rather than calculate back what Frame was clicked based on the coordinates, you can use event.widget to get the clicked Frame directly. However, it would be much easier to bind the Button click to the Frame itself, so that each Frame handles it's own click. Here's an example you can run:
import tkinter as tk
class Node(tk.Frame):
BLOCK_COLOR = "black"
CLEAR_COLOR = "light gray"
def __init__(self,master=None, **kwargs):
super().__init__(master, **kwargs)
self.config(borderwidth=1, bg=self.CLEAR_COLOR, relief=tk.RAISED)
self.bind('<1>', self.on_click)
def on_click(self, event=None):
if self['bg'] == self.CLEAR_COLOR:
self['bg'] = self.BLOCK_COLOR
else:
self['bg'] = self.CLEAR_COLOR
class Graph(tk.Frame):
def __init__(self,master=None, rows=5, columns=5, **kwargs):
super().__init__(master, **kwargs)
for row_num in range(rows):
for col_num in range(columns):
f = Node(self)
f.grid(row=row_num, column=col_num, sticky='nsew')
self.rowconfigure(list(range(rows)), weight=1, uniform='cell', minsize=40)
self.columnconfigure(list(range(columns)), weight=1, uniform='cell', minsize=40)
window = tk.Tk()
w = Graph(window)
w.pack(expand=True, fill=tk.BOTH)
window.title('Path Finder')
window.mainloop()