I wrote a code which creates canvas and enables you to load picture. You can also add logo and text to this picture and drag. (Something like Photo Watermark Application) After all, you can save all work as PNG. format. What I want to do is, I want to save all data as canvas object, and later I can be able to load this canvas object and continue working. Maybe I would like to change text or logo coordinates on loaded object/canvas.
I don't know saving it as canvas object right thing. I would be very appreciated if you help me on this topic.
Here is my code.
from PIL import Image, ImageTk, ImageGrab
import tkinter as tk
from tkinter.filedialog import askopenfilename, asksaveasfilename, asksaveasfile
def buttonpress(function, *args):
value = function(*args)
print(type(value))
def open_file():
img_path = askopenfilename(title="Select A File", filetype=(("jpeg files", "*.jpg"), ("all files", "*.*")))
if img_path:
canvas.imageList = [] # To get rid of garbage collector, empty list is added
img = Image.open(img_path)
img = img.resize((750, 500), Image.ANTIALIAS)
image = ImageTk.PhotoImage(img)
created_image_id = canvas.create_image(450, 250, image=image)
canvas.imageList.append(image)
# Change button displays
browse_button.grid_forget()
logo_button.grid(column=1, row=5)
text_button.grid(column=4, row=5)
save_button.grid(column=7, row=5)
return created_image_id
def add_logo():
logo_path = askopenfilename(title="Select A File", filetype=(("jpeg files", "*.jpg"), ("all files", "*.*")))
if logo_path:
canvas.logoList = [] # To get rid of garbage collector, empty list is added
logo = Image.open(logo_path)
logo = logo.resize((200, 200), Image.ANTIALIAS)
logo = ImageTk.PhotoImage(logo)
canvas.create_image(450, 250, image=logo)
canvas.logoList.append(logo)
# Change button displays
logo_button.grid_forget()
text_button.grid(column=3, row=5)
save_button.grid(column=6, row=5)
def on_drag(event):
if canvas.selected != 1:
# Calculate distance moved from last position
dx, dy = event.x - canvas.startxy[0], event.y - canvas.startxy[1]
# Move the selected item
canvas.move(canvas.selected, dx, dy)
# Update last position
canvas.startxy = (event.x, event.y)
def on_click(event):
selected = canvas.find_overlapping(event.x - 10, event.y - 10, event.x + 10,
event.y + 10) # List of selected items with mouse
canvas.startxy = (event.x, event.y) # Define "startxy" variable in canvas class
if selected:
canvas.selected = selected[-1] # Select the top-most item #define "selected" variable in canvas class
else:
canvas.selected = None
def add_text():
canvas.create_text(100, 10, fill="darkblue", font="Times 20 italic bold",
text="Click the bubbles that are multiples of two.")
def save_pic(widget):
file = asksaveasfilename(filetypes=[('Portable Network Graphics', '*.png')])
x = root.winfo_rootx() + 75
y = root.winfo_rooty()
x1 = x + widget.winfo_width() - 175
y1 = y + widget.winfo_height()
ImageGrab.grab().crop((x, y, x1, y1)).save(file + ".png")
root = tk.Tk()
root.geometry("900x600")
root.resizable(0, 0)
root.title("Photo Watermark Application")
canvas = tk.Canvas(root, width=900, height=500)
canvas.grid(column=0, row=0, columnspan=9, rowspan=5)
# Browse button
browse_text = tk.StringVar()
browse_button = tk.Button(root, command=lambda: buttonpress(open_file), textvariable=browse_text, font="Ariel",
bg="black",
fg="white",
height=2, width=10)
browse_text.set("Browse")
browse_button.grid(column=4, row=5)
# Add logo button
add_logo_text = tk.StringVar()
logo_button = tk.Button(root, command=add_logo, textvariable=add_logo_text, font="Ariel", bg="black", fg="white",
height=2, width=10)
add_logo_text.set("Add Logo")
# Add text button
add_text_text = tk.StringVar()
text_button = tk.Button(root, command=add_text, textvariable=add_text_text, font="Ariel", bg="black", fg="white",
height=2, width=10)
add_text_text.set("Add Text")
# Add save picture button
save_text_text = tk.StringVar()
save_button = tk.Button(root, command=lambda: save_pic(canvas), textvariable=save_text_text, font="Ariel", bg="black",
fg="white",
height=2, width=10)
save_text_text.set("Save Picture")
root.bind("<B1-Motion>", on_drag) # B1-MOTION = Dragging items using mouse
root.bind("<Button-1>", on_click) # BUTTON-1 = Left click with mouse
root.mainloop()
Ok, so I made a little example of how that might be done (will step through the code a bit):
import json
from tkinter import Tk, Canvas
class Circle:
def __init__(self, parent: "Canvas", x, y):
self.parent = parent
self.x = x
self.y = y
self.width = 50
self.height = 50
self.type = 'circle'
self.allow_move = False
self.move_offset_x = 0
self.move_offset_y = 0
self.circle = self.parent.create_oval(self.x, self.y, self.x + self.width, self.y + self.height, fill='black')
def move(self, x, y):
x, y = x - self.move_offset_x, y - self.move_offset_y
self.x, self.y = x, y
self.parent.coords(self.circle, x, y, x + self.width, y + self.height)
def check_coords(event=None):
for obj in obj_lst:
if obj.x < event.x < obj.x + obj.width and obj.y < event.y < obj.y + obj.width:
obj.allow_move = True
obj.move_offset_x = event.x - obj.x
obj.move_offset_y = event.y - obj.y
break
def move(event=None):
for obj in obj_lst:
if obj.allow_move:
obj.move(event.x, event.y)
def set_moving_false(event=None):
for obj in obj_lst:
if obj.allow_move:
obj.allow_move = False
def create_obj(event=None):
obj_lst.append(Circle(canvas, event.x, event.y))
def save(event=None):
with open('untitled.canvas', 'w') as file:
obj_dict = {f'{obj.type} {id}': (obj.x, obj.y) for id, obj in enumerate(obj_lst)}
json.dump(obj_dict, file)
root = Tk()
canvas = Canvas(root, width=500, height=400)
canvas.pack()
obj_type_dict = {'circle': Circle}
try:
with open('untitled.canvas') as file:
obj_dict = json.load(file)
obj_lst = []
for obj_type, attributes in obj_dict.items():
obj = obj_type_dict[obj_type.split()[0]](canvas, attributes[0], attributes[1])
obj_lst.append(obj)
except FileNotFoundError:
with open('untitled.canvas', 'w') as file:
pass
obj_lst = []
canvas.bind('<Button-1>', check_coords)
canvas.bind('<B1-Motion>', move)
canvas.bind('<ButtonRelease-1>', set_moving_false)
canvas.bind('<Button-3>', create_obj)
root.bind('<Control-s>', save)
root.mainloop()
First start with imports, I chose to use json
because it was possible and pickle
has some security issues so why not use json
. And stuff from tkinter
.
Then I define a class that will account for circle objects (simple objects that come in one size and nothing fancy).
Then the most gets handled using binds and some functions (just some moving stuff).
The important parts are reading and creating files.
So bound to Control + s
is a function that saves the file ( save()
). First it reads from the list of the objects that are on the screen. (they get appended there by create_obj()
function) it stores their type and coordinates in a dictionary which then gets dumped into the file.
Reading from file is also interesting ( try/except
is there in case the file does not exist). When it reads it loads the dictionary and in a loop adds items to the list by using the obj_type_dict
dictionary to determine the type of the object. And while the objects are created they are also drawn.
Controls:
Ctrl + s
- save Click and drag with mouse button 1
- move the object around Click mouse button 3 (right)
- create an object Suggestions:
filedialog
)Anyways this is just an example of how that might be done, also for images You would have to add an attribute that saves the image path if You want to load them, and make other adjustments because json
cannot serialize an image (I think).
Since you didn't seem to understand how to use my suggestion in a comment, here's a runnable proof-of-concept to demonstrate its feasibility of doing what I suggested — namely about defining a Canvas
subclass that recorded information about each object drawn on it and was able to save and load that into a file. For simplicity I decided to save the data in JSON format because the file is human-readable (and Python provides support for it in its standard library, but as I said, the choice is yours. (As @Matiiss also suggested in his answer, pickle
would be another viable alternative to consider and would be more compact).
Since this is only a demo, I've only implemented support for Canvas
lines and rectangles — but that should enough to give you an overall idea of how to implement the rest of the Canvas
widget, like ovals, polygons, and whatnot.Note that it might not be possible to do them all, but I don't think that will be a problem — because you're probably not going to need or even be using them.
A notable except to that would be images because the pattern shown below for saving the information wouldn't work because of their image=
option argument. To handle them you'll need to either save the image file's path or the data in it — however the latter option would involve copying a lot of data into the save file and require encoding and decoding it into and out of an JSON-serializable form (if you were to using that format).
Updated
To clarify what was I saying about images being a special case (and because that might be something would want to be able to handle, I've modified the code to show how one way that could be done — namely by embedding the image info in the saved canvas file, which including the path to the image file). this seem the cleanest way to do it in conjunction with JSON format. This means the saved canvases don't actually contain the image data itself.
Here's the 8-ball-tbgr.png image file used in the code.
import json
from PIL import Image, ImageTk
import tkinter as tk
from tkinter.constants import *
class MemoCanvas(tk.Canvas):
''' Canvas subclass that remembers the items drawn on it. '''
def __init__(self, parent, *args, **kwargs):
super().__init__(*args, **kwargs)
self.memo = {} # Database of Canvas item information.
# Canvas object constructors.
def create_line(self, *args, **kwargs):
id = super().create_line(*args, **kwargs)
self.memo[id] = dict(type='line', args=args, kwargs=kwargs)
return id
def create_rectangle(self, *args, **kwargs):
id = super().create_rectangle(*args, **kwargs)
self.memo[id] = dict(type='rectangle', args=args, kwargs=kwargs)
return id
def create_image(self, *args, imageinfo=None, **kwargs):
id = super().create_image(*args, **kwargs)
if not imageinfo:
raise RuntimeError('imageinfo dictionary must be supplied')
del kwargs['image'] # Don't store in memo.
self.memo[id] = dict(type='image', args=args, imageinfo=imageinfo, kwargs=kwargs)
return id
# General methods on Canvas items (not fully implemented - some don't update memo).
def move(self, *args):
super().move(*args)
def itemconfigure(self, *args, **kwargs):
super().itemconfigure(*args, **kwargs)
def delete(self, tag_or_id):
super().delete(tag_or_id)
if isinstance(tag_or_id, str) and tag_or_id.lower() != 'all':
if self.memo[tag_or_id]['type'] == 'image':
del self.imagerefs[tag_or_id]
del self.memo[tag_or_id]
else:
try:
self.memo.clear()
del self.imagerefs
except AttributeError:
pass
# General purpose utility.
def setdefaultattr(obj, name, value):
''' If obj has attribute, return it, otherwise create and assign the value
value to it first. '''
try:
return getattr(obj, name)
except AttributeError:
setattr(obj, name, value)
return value
# Demo app functions.
def draw_objects(canvas):
canvas.create_line(100, 200, 300, 400, fill='red')
canvas.create_rectangle(100, 200, 300, 400, outline='green')
# Add an image to canvas.
imageinfo = dict(imagepath=IMAGEPATH, hsize=100, vsize=100, resample=Image.ANTIALIAS)
imagepath, hsize, vsize, resample = imageinfo.values()
image = Image.open(imagepath).resize((hsize, vsize), resample)
image = ImageTk.PhotoImage(image)
id = canvas.create_image(450, 150, image=image, imageinfo=imageinfo)
imagerefs = setdefaultattr(canvas, 'imagerefs', {})
imagerefs[id] = image # Save reference to image created.
def clear_objects(canvas):
canvas.delete('all')
canvas.memo.clear()
try:
del canvas.imagerefs
except AttributeError:
pass
def save_objects(canvas, filename):
with open(filename, 'w') as file:
json.dump(canvas.memo, file, indent=4)
def load_objects(canvas, filename):
clear_objects(canvas) # Remove current contents.
# Recreate each saved canvas item.
with open(filename, 'r') as file:
items = json.load(file)
for item in items.values():
if item['type'] == 'line':
canvas.create_line(*item['args'], **item['kwargs'])
elif item['type'] == 'rectangle':
canvas.create_rectangle(*item['args'], **item['kwargs'])
elif item['type'] == 'image':
# Recreate image item from imageinfo.
imageinfo = item['imageinfo']
imagepath, hsize, vsize, resample = imageinfo.values()
image = Image.open(imagepath).resize((hsize, vsize), resample)
image = ImageTk.PhotoImage(image)
item['kwargs']['image'] = image
id = canvas.create_image(*item['args'], imageinfo=imageinfo, **item['kwargs'])
imagerefs = setdefaultattr(canvas, 'imagerefs', {})
imagerefs[id] = image # Save reference to recreated image.
else:
raise TypeError(f'Unknown canvas item type: {type!r}')
# Main
WIDTH, HEIGHT = 640, 480
FILEPATH = 'saved_canvas.json'
IMAGEPATH = '8-ball-tbgr.png'
CMDS = dict(Draw=lambda: draw_objects(canvas),
Save=lambda: save_objects(canvas, FILEPATH),
Clear=lambda: clear_objects(canvas),
Load=lambda: load_objects(canvas, FILEPATH),
Quit=lambda: root.quit())
root = tk.Tk()
root.title('Save and Load Canvas Demo')
root.geometry(f'{WIDTH}x{HEIGHT}')
canvas = MemoCanvas(root)
canvas.pack(fill=BOTH, expand=YES)
btn_frame = tk.Frame(root)
btn_frame.pack()
for text, command in CMDS.items():
btn = tk.Button(btn_frame, text=text, command=command)
btn.pack(side=LEFT)
root.mainloop()
Here's a screenshot of it running:
And here's the truncated contents of the JSON format file it creates (couldn't post the complete contents because it's too big due the image data being in it now).
{
"1": {
"type": "line",
"args": [
100,
200,
300,
400
],
"kwargs": {
"fill": "red"
}
},
"2": {
"type": "rectangle",
"args": [
100,
200,
300,
400
],
"kwargs": {
"outline": "green"
}
},
"3": {
"type": "image",
"args": [
450,
150
],
"imageinfo": {
"imagepath": "8-ball-tbgr.png",
"hsize": 100,
"vsize": 100,
"resample": 1
},
"kwargs": {}
}
}
The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.