简体   繁体   中英

Tkinter save canvas and load

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:

  • improve file saving (give options to user/use filedialog )
  • improve object adding to screen (You will notice where the object appears relative to mouse when testing)
  • add the part that saves this as image

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.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM