繁体   English   中英

Tkinter 保存 canvas 并加载

[英]Tkinter save canvas and load

我编写了一个代码,它创建了 canvas 并使您能够加载图片。 您还可以在此图片中添加徽标和文本并拖动。 (类似于照片水印应用程序)毕竟,您可以将所有工作保存为 PNG。 格式。 我想要做的是,我想将所有数据保存为 canvas object,然后我可以加载这个 canvas ZA8CFDE63311BD59EB66666AC9 继续工作。 也许我想更改加载的对象/画布上的文本或徽标坐标。

我不知道把它保存为 canvas object 是对的。 如果您在这个主题上帮助我,我将不胜感激。

这是我的代码。

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

好的,所以我举了一个小例子来说明如何做到这一点(将逐步介绍代码):

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

首先从导入开始,我选择使用json因为它是可能的并且pickle有一些安全问题所以为什么不使用json 还有来自tkinter的东西。

然后我定义了一个 class 来解释圆形对象(简单的对象,只有一种尺寸,没有什么花哨的)。

然后使用绑定和一些函数(只是一些移动的东西)来处理大多数。

重要的部分是读取和创建文件。

所以绑定到Control + s是一个 function 保存文件( save() )。 首先,它从屏幕上的对象列表中读取。 (它们通过create_obj()函数附加在那里)它将它们的类型和坐标存储在字典中,然后将其转储到文件中。

从文件中读取也很有趣(如果文件不存在,则存在try/except )。 当它读取它时,它会加载字典并在循环中通过使用obj_type_dict字典将项目添加到列表中,以确定 object 的类型。 在创建对象的同时,它们也被绘制。

控制:

  • Ctrl + s - 保存
  • Click and drag with mouse button 1 - 移动 object
  • Click mouse button 3 (right) - 创建 object

建议:

  • 改进文件保存(为用户/使用filedialog提供选项)
  • 改进 object 添加到屏幕(您会注意到 object 在测试时相对于鼠标出现的位置)
  • 添加将其保存为图像的部分

无论如何,这只是如何完成的一个示例,也适用于图像如果要加载它们,则必须添加一个保存图像路径的属性,并进行其他调整,因为json无法序列化图像(我认为)。

由于您似乎不明白如何在评论中使用我的建议,这里有一个可运行的概念验证,以证明其执行我建议的可行性 - 即关于定义一个Canvas子类,该子类记录有关每个 object 的信息并且能够保存并将其加载到文件中。 为简单起见,我决定将数据保存为 JSON 格式,因为该文件是人类可读的(并且 Python 在其标准库中提供了对它的支持,但正如我所说,选择是你的。(正如@Matiiss 在他的回答中所建议的那样, pickle将是另一个值得考虑的可行替代方案,并且会更紧凑)。

由于这只是一个演示,我只实现了对Canvas线条和矩形的支持 - 但这足以让您全面了解如何实现Canvas多边形和椭圆形小部件、椭圆形等请注意,可能无法全部完成,但我认为这不会成为问题——因为您可能不需要甚至不会使用它们。

一个值得注意的例外是图像,因为下面显示的用于保存信息的模式由于它们的image=选项参数而不起作用。 要处理它们,您需要保存图像文件的路径或其中的数据——但是后一种选择将涉及将大量数据复制到保存文件中,并且需要对其进出 JSON 序列化形式进行编码和解码(如果您要使用该格式)。

更新

为了澄清我所说的图像是一种特殊情况(并且因为这可能是想要能够处理的事情,我已经修改了代码以显示可以完成的一种方式 - 即通过将图像信息嵌入保存的 canvas 文件,其中包括图像文件的路径)。 这似乎是与 JSON 格式结合使用的最干净的方法。 这意味着保存的画布实际上并不包含图像数据本身。

这是代码中使用的8-ball-tbgr.png图像文件。

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

这是它运行的屏幕截图:

运行脚本的屏幕截图

这是它创建的 JSON 格式文件的截断内容(无法发布完整的内容,因为它现在太大了,因为其中包含图像数据)。

{
    "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": {}
    }
}

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

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