简体   繁体   English

如何将 tkinter 画布保存为图像

[英]How to save a tkinter canvas as an image

I would like to save my drawing on a tkinter canvas as an image so I can open it for later use.我想将我的绘图保存在 tkinter 画布上作为图像,以便我可以打开它以供以后使用。 I currently use this save system from this post however this is not a good way for me.我目前使用这篇文章中的这个保存系统,但这对我来说不是一个好方法。 First I would need to add an offset and second if i set the application so only some part of the canvas is actually visible, the part where the canvas is not visible appears black when saving the image.首先,我需要添加一个偏移量,其次,如果我设置应用程序,那么实际上只有画布的某些部分是可见的,而在保存图像时,画布不可见的部分会显示为黑色。

你好

only part of the canvas is actually visible.实际上只有画布的一部分是可见的。 If I open the saved image this is what it looks like如果我打开保存的图像,这就是它的样子在此处输入图像描述 only what was visible is actually there(the entire image was yellow before saving it).只有可见的东西实际上是存在的(整个图像在保存之前是黄色的)。

The code of saving the image.保存图像的代码。

def save(widget(canvas), filelocation):
    x=root.winfo_rootx()+widget.winfo_x() + 74
    y=root.winfo_rooty()+widget.winfo_y() + 109
    x1=x+widget.winfo_width()
    y1=y+widget.winfo_height()
    ImageGrab.grab().crop((x,y,x1,y1)).save(filelocation)

Idea主意

After reading from this post it explains i could recreate all the stuff i drew on the canvas.从这篇文章阅读后,它解释说我可以重新创建我在画布上绘制的所有东西。 So my idea is to put all the stuff i drew on the canvas such as lines i created on an invisible layer and paste it on the image.所以我的想法是把我画的所有东西都放在画布上,比如我在一个不可见的图层上创建的线条,然后将它粘贴到图像上。 However i dont know if this is possible(may be possible with PIL , numpy or cv2 )但是我不知道这是否可能(可能使用PILnumpycv2

Code(Is minimal reproducable)代码(最小可重现)

import tkinter as tk
from tkinter import colorchooser, Canvas, N
from tkinter.ttk import *
from PIL import Image, ImageTk, ImageGrab
import keyboard

def save(widget, filelocation):
    x=root.winfo_rootx()+widget.winfo_x()
    y=root.winfo_rooty()+widget.winfo_y()
    x1=x+widget.winfo_width()
    y1=y+widget.winfo_height()
    ImageGrab.grab().crop((x,y,x1,y1)).save(filelocation)

def type_of(color):
    type_pen = 'marker'
    if type_pen == 'marker':
        pencil_motion_marker(color = color)

#pixel pen
def pencil_motion_marker(color):
    stage.bind('<Button-1>', get_pos_marker)
    stage.bind('<B1-Motion>', lambda event, color = color: pencil_draw_marker(event, color))

def get_pos_marker(event):
    global lastx, lasty
    
    lastx, lasty = event.x, event.y

def pencil_draw_marker(event, color):
    stage.create_line((lastx, lasty, event.x, event.y), width = width.get(), fill = color, capstyle = 'round')
    get_pos_marker(event)

def choose_pen_color():
    pencilcolor = colorchooser.askcolor(title = 'Pencil Color')
    type_of(pencilcolor[1])

##
        
def pencil_click():
    global width, opacity

    Whitepencolb = Button(optionsframe, text = 'Whitepencolimg', style = 'COLBG.TButton', command = lambda m = 'White': type_of(m))
    Whitepencolb.grid(row = 0, column = 0, padx = 10, pady = 1)
    
    Redpencolb = Button(optionsframe, text = 'Redpencolimg', style = 'COLBG.TButton', command = lambda m = 'Red': type_of(m))
    Redpencolb.grid(row = 1, column = 0, padx = 10, pady = 1)
    
    Magentapencolb = Button(optionsframe, text = 'Magentapencolimg', style = 'COLBG.TButton', command = lambda m = 'Magenta': type_of(m))
    Magentapencolb.grid(row = 0, column = 1, padx = 10, pady = 1)

    Limegreenpencolb = Button(optionsframe, text = 'Limegreenpencolimg', style = 'COLBG.TButton', command = lambda m = 'Lime': type_of(m))
    Limegreenpencolb.grid(row = 1, column = 1, padx = 10, pady = 1)
    
    Greenpencolb = Button(optionsframe, text = 'Greenpencolimg', style = 'COLBG.TButton', command = lambda m = 'Green': type_of(m))
    Greenpencolb.grid(row = 0, column = 2, padx = 10, pady = 1)
    
    Bluepencolb = Button(optionsframe, text = 'Bluepencolimg', style = 'COLBG.TButton', command = lambda m = 'Blue': type_of(m))
    Bluepencolb.grid(row = 1, column = 2, padx = 10, pady = 1)
    
    Cyanpencolb = Button(optionsframe, text = 'Cyanpencolimg', style = 'COLBG.TButton', command = lambda m = 'Cyan': type_of(m))
    Cyanpencolb.grid(row = 0, column = 3, padx = 10, pady = 1)
    
    Yellowpencolb = Button(optionsframe, text = 'Yellowpencolimg', style = 'COLBG.TButton', command = lambda m = 'Yellow': type_of(m))
    Yellowpencolb.grid(row = 1, column = 3, padx = 10, pady = 1)

    Orangepencolb = Button(optionsframe, text = 'Orangepencolimg', style = 'COLBG.TButton', command = lambda m = 'Orange': type_of(m))
    Orangepencolb.grid(row = 0, column = 4, padx = 10, pady = 1)

    Graypencolb = Button(optionsframe, text = 'Graypencolimg', style = 'COLBG.TButton', command = lambda m = 'Gray': type_of(m))
    Graypencolb.grid(row = 1, column = 4, padx = 10, pady = 1)

    Blackpencolb = Button(optionsframe, text = 'Blackpencolimg', style = 'COLBG.TButton', command = lambda m = 'Black': type_of(m))
    Blackpencolb.grid(row = 0, column = 5, padx = 10, pady = 1)

    Createnewpencolb = Button(optionsframe, text = 'Createnewpencolimg', style = 'COLBG.TButton', command = choose_pen_color)
    Createnewpencolb.grid(row = 1, column = 5, padx = 10, pady = 1)

    widthlabel = Label(optionsframe, text = 'Width: ', style = 'LABELBG.TLabel')
    width = Scale(optionsframe, from_ = 1, to = 20, style = 'SCALEBG.Horizontal.TScale')
    widthlabel.grid(row = 0, column = 6)
    width.grid(row = 0, column = 7)
    width.set(20)

    opacitylabel = Label(optionsframe, text = 'Opacity: ', style = 'LABELBG.TLabel')
    opacity = Scale(optionsframe, from_ = 0, to = 1.0, style = 'SCALEBG.Horizontal.TScale')
    opacitylabel.grid(row = 1, column = 6)
    opacity.grid(row = 1, column = 7)
    opacity.set(1.0)

def setup(filelocation):
    global stage, img_id, optionsframe, draw
    
    for widgets in root.winfo_children():
        widgets.destroy()

    root.config(bg = '#454545')
    iconsframewidth = int(screen_width / 20)
    
    frames = Style()
    frames.configure('FRAMES.TFrame', background = '#2a2a2a')
    sep = Style()
    sep.configure('SEP.TFrame', background = '#1a1a1a')
    style = Style()
    style.configure('STAGE.TFrame', background = '#454545')
    icon = Style()
    icon.configure('ICON.TButton', background = '#2a2a2a', foreground = '#2a2a2a')
    
    iconsframe = Frame(root, width = iconsframewidth, style = 'FRAMES.TFrame')
    iconsframe.pack(side = 'left', expand = False, fill = 'y')
    iconsframe.pack_propagate(0)
    sep1frame = Frame(root, style = 'SEP.TFrame', width = 5)
    sep1frame.pack(side = 'left', expand = False, fill = 'y')
    optionsframe = Frame(root, style = 'FRAMES.TFrame', height = 100)
    optionsframe.pack(side = 'top', expand = False, fill = 'x')
    optionsframe.pack_propagate(0)
    sep2frame = Frame(root, style = 'SEP.TFrame', height = 5)
    sep2frame.pack(side = 'top', expand = False, fill = 'x')
    propertyframe = Frame(root, style = 'FRAMES.TFrame', width = 150)
    propertyframe.pack(side = 'right', expand = False, fill = 'y')
    propertyframe.pack_propagate(0)
    sep3frame = Frame(root, style = 'SEP.TFrame', width = 5)
    sep3frame.pack(side = 'right', expand = False, fill = 'y')
    stageframe = Frame(root, style = 'STAGE.TFrame')
    stageframe.pack(side = 'top', expand = True, fill = 'both')
    stageframe.pack_propagate(0)

    image = Image.open(filelocation)
    width, height = image.size

    stage = Canvas(stageframe, width = width, height = height)
    stage.pack(side="top", anchor = 'c', expand=True)

    root.update()

    keyboard.add_hotkey("ctrl+s", lambda widget = stage, filelocation = filelocation: save(widget, filelocation))

    pencilbutton = Button(iconsframe, text = 'pencilimg', command = pencil_click, style = 'ICON.TButton')
    pencilbutton.pack(anchor = N, pady = 10)

    imgtk = ImageTk.PhotoImage(Image.open(filelocation)) 
    img_id = stage.create_image(stage.winfo_width() / 2, stage.winfo_height() / 2, image = imgtk)
    stage.image = imgtk

root = tk.Tk()
root.title('App')

screen_width = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()

w = 1150
h = 600
x = (screen_width / 2) - (w / 2)
y = (screen_height / 2) - (h / 2)

root.geometry('%dx%d+%d+%d' % (w, h, x, y))
root.minsize(1150, 600)

setup('Test.png')
root.mainloop()

Image图片

在此处输入图像描述

Small Problem小问题

Replying to @Claudio : I am using the screenshot technique for saving Canvas as an image to a file right now.回复@Claudio:我现在正在使用屏幕截图技术将 Canvas 作为图像保存到文件中。 I noticed that the saved canvas image looks like this at the corner我注意到保存的画布图像在角落看起来像这样在此处输入图像描述 and after saving and reopening the image it looks like this保存并重新打开图像后,它看起来像这样在此处输入图像描述 ( the border of the canvas increases the size of the canvas image ). (画布的边框增加了画布图像的大小)。

Update 2. June 2022: Small Problem Solved by the updated code provided in the accepted answer.更新 2. 2022 年 6 月:已接受答案中提供的更新代码解决了小问题

How to save a tkinter Canvas graphics as an image?如何将 tkinter Canvas 图形保存为图像?

It seems to be a fact that tkinter doesn't provide a direct method allowing to get an image of Canvas graphics for saving it to an image file. tkinter 似乎没有提供直接的方法来获取 Canvas 图形的图像以将其保存到图像文件中,这似乎是一个事实。 There are two ways around this problem requiring solely an import of the Python PIL module (Pillow).有两种方法可以解决这个问题,只需要导入 Python PIL 模块(Pillow)。

One of this ways is to perform a screenshot of painting on the Canvas area which can be done using PIL.ImageGrab.grab() or any other of the various methods for performing (cropped) screenshots and saving them to an image file ( see eg Fast screenshot of a small part of the screen in Python for a Python screenshot module fast enough to allow to make a video of the progressing painting on the Canvas ).其中一种方法是在 Canvas 区域执行绘画的屏幕截图,这可以使用PIL.ImageGrab.grab()或任何其他执行(裁剪)屏幕截图并将它们保存到图像文件的各种方法来完成(参见例如Python 屏幕一小部分的快速屏幕截图,用于 Python 屏幕截图模块,速度足够快,可以在 Canvas 上制作进度绘画的视频)。

Another way is to paint on a Python PIL image updating the tkinter Canvas with the modified PIL image saving it then to a file using the .save() method available for saving PIL image objects.另一种方法是在 Python PIL 图像上绘画,使用修改后的 PIL 图像更新 tkinter Canvas,然后使用可用于保存 PIL 图像对象的.save()方法将其保存到文件中。

The code provided in the question works generally as expected if save() uses both the Frame ( stageframe ) and the Canvas ( stage ) widgets required for getting the right x,y values for cropping of the screenshot in case the Canvas is placed within a Frame AND if the bounding box for cropping the screenshot takes into account that tkinter Canvas widget size includes a Canvas border and a Canvas highlight-border.如果save()同时使用 Frame ( stageframe ) 和 Canvas ( stage ) 小部件来获取正确的 x,y 值以裁剪屏幕截图,则问题中提供的代码通常按预期工作,以防 Canvas 放置在如果用于裁剪屏幕截图的边界框考虑到 tkinter Canvas 小部件大小包括 Canvas 边框和 Canvas 高亮边框,则框架和。

The code below is the in the question provided code with some added comments and appropriate modifications.下面的代码是问题中提供的代码,带有一些添加的注释和适当的修改。 It doesn't require the keyboard module and saves the by painting modified Canvas as image file by clicking on the most-left upper pencilbutton handled by the pencil_click() function.它不需要键盘模块,并通过单击由pencilbutton pencil_click()函数处理的最左上角铅笔按钮将修改后的Canvas 保存为图像文件。 It provides both methods for saving the graphics of the tkinter Canvas to an image file.它提供了将 tkinter Canvas 的图形保存到图像文件的两种方法。 Select one of them by assigning appropriate value to the global method variable ( method = 'screenshot' or method = 'imagepaint' ):通过为全局method变量( method = 'screenshot'method = 'imagepaint' )分配适当的值来选择其中之一:

# https://stackoverflow.com/questions/72459847/how-to-save-a-tkinter-canvas-as-an-image
from tkinter     import Tk, colorchooser, Canvas, N, PhotoImage
from tkinter.ttk import Style, Frame, Button, Label, Scale
from PIL import Image, ImageTk   # required to load images in tkinter
# method = 'screenshot' or 'imagepaint'
method = 'screenshot'
borderthickness_bd = 2
highlightthickness = 1
if method == 'imagepaint': 
    from PIL import ImageDraw  # required to draw on the image 
if method == 'screenshot': 
    from PIL import ImageGrab  # required for the screenshot
filelocation = 'Test.png'
savelocation = 'Test_.png'
def save(stageframe, stage, savelocation):
    if method == 'imagepaint': 
        global image
        image.save(savelocation)
    if method == 'screenshot':
        global borderthickness_bd,  highlightthickness
        brdt = borderthickness_bd + highlightthickness
        # +1 and -2 because of thicknesses of Canvas borders (bd-border and highlight-border):
        x=root.winfo_rootx()+stageframe.winfo_x()+stage.winfo_x() +1*brdt
        y=root.winfo_rooty()+stageframe.winfo_y()+stage.winfo_y() +1*brdt
        x1=x+stage.winfo_width() -2*brdt
        y1=y+stage.winfo_height()-2*brdt
        ImageGrab.grab().crop((x,y,x1,y1)).save(savelocation)

def type_of(color):
    type_pen     = 'marker'
    if type_pen == 'marker':
        pencil_motion_marker(color = color)

#pixel pen
def pencil_motion_marker(color):
    stage.bind('<Button-1>' , get_pos_marker)
    stage.bind('<B1-Motion>', lambda event, color = color: pencil_draw_marker(event, color))

def get_pos_marker(event):
    global lastx, lasty
    lastx, lasty = event.x, event.y

def pencil_draw_marker(event, color):
    global method, lastx, lasty, draw, image, img_id
    # print( (lastx, lasty, event.x, event.y), color, int(width.get()) )
    if method == 'screenshot': 
        stage.create_line((lastx, lasty, event.x, event.y), width = width.get(), fill = color, capstyle = 'round')
        get_pos_marker(event)
    if method == 'imagepaint':
        w12 = int(width.get()/2)
        draw.ellipse( (event.x-w12, event.y-w12, event.x+w12, event.y+w12), fill=color )
        imgtk  = ImageTk.PhotoImage(image)
        stage.itemconfig(img_id, image=imgtk)
        stage.image = imgtk

def choose_pen_color():
    pencilcolor = colorchooser.askcolor(title = 'Pencil Color')
    type_of(pencilcolor[1])

##
        
def pencil_click():
    
    global width, opacity, stageframe, stage, savelocation

    # imgToSave = stage.image                                      # gives a PhotoImage object
    # imgToSave._PhotoImage__photo.write("Test.gif", format='gif') # which can be saved, but ...
    #                                ^--- ... with no painting done on Canvas - only the image.

    save(stageframe, stage, savelocation)
    
    Whitepencolb = Button(optionsframe, text = 'Whitepencolimg', style = 'COLBG.TButton', command = lambda m = 'White': type_of(m))
    Whitepencolb.grid(row = 0, column = 0, padx = 10, pady = 1)
    
    Redpencolb = Button(optionsframe, text = 'Redpencolimg', style = 'COLBG.TButton', command = lambda m = 'Red': type_of(m))
    Redpencolb.grid(row = 1, column = 0, padx = 10, pady = 1)
    
    Magentapencolb = Button(optionsframe, text = 'Magentapencolimg', style = 'COLBG.TButton', command = lambda m = 'Magenta': type_of(m))
    Magentapencolb.grid(row = 0, column = 1, padx = 10, pady = 1)

    Limegreenpencolb = Button(optionsframe, text = 'Limegreenpencolimg', style = 'COLBG.TButton', command = lambda m = 'Lime': type_of(m))
    Limegreenpencolb.grid(row = 1, column = 1, padx = 10, pady = 1)
    
    Greenpencolb = Button(optionsframe, text = 'Greenpencolimg', style = 'COLBG.TButton', command = lambda m = 'Green': type_of(m))
    Greenpencolb.grid(row = 0, column = 2, padx = 10, pady = 1)
    
    Bluepencolb = Button(optionsframe, text = 'Bluepencolimg', style = 'COLBG.TButton', command = lambda m = 'Blue': type_of(m))
    Bluepencolb.grid(row = 1, column = 2, padx = 10, pady = 1)
    
    Cyanpencolb = Button(optionsframe, text = 'Cyanpencolimg', style = 'COLBG.TButton', command = lambda m = 'Cyan': type_of(m))
    Cyanpencolb.grid(row = 0, column = 3, padx = 10, pady = 1)
    
    Yellowpencolb = Button(optionsframe, text = 'Yellowpencolimg', style = 'COLBG.TButton', command = lambda m = 'Yellow': type_of(m))
    Yellowpencolb.grid(row = 1, column = 3, padx = 10, pady = 1)

    Orangepencolb = Button(optionsframe, text = 'Orangepencolimg', style = 'COLBG.TButton', command = lambda m = 'Orange': type_of(m))
    Orangepencolb.grid(row = 0, column = 4, padx = 10, pady = 1)

    Graypencolb = Button(optionsframe, text = 'Graypencolimg', style = 'COLBG.TButton', command = lambda m = 'Gray': type_of(m))
    Graypencolb.grid(row = 1, column = 4, padx = 10, pady = 1)

    Blackpencolb = Button(optionsframe, text = 'Blackpencolimg', style = 'COLBG.TButton', command = lambda m = 'Black': type_of(m))
    Blackpencolb.grid(row = 0, column = 5, padx = 10, pady = 1)

    Createnewpencolb = Button(optionsframe, text = 'Createnewpencolimg', style = 'COLBG.TButton', command = choose_pen_color)
    Createnewpencolb.grid(row = 1, column = 5, padx = 10, pady = 1)

    widthlabel = Label(optionsframe, text = 'Width: ', style = 'LABELBG.TLabel')
    width = Scale(optionsframe, from_ = 1, to = 100, style = 'SCALEBG.Horizontal.TScale')
    widthlabel.grid(row = 0, column = 6)
    width.grid(row = 0, column = 7)
    width.set(20)

    opacitylabel = Label(optionsframe, text = 'Opacity: ', style = 'LABELBG.TLabel')
    opacity = Scale(optionsframe, from_ = 0, to = 1.0, style = 'SCALEBG.Horizontal.TScale')
    opacitylabel.grid(row = 1, column = 6)
    opacity.grid(row = 1, column = 7)
    opacity.set(1.0)

def setup(filelocation):
    global stage, stageframe, img_id, optionsframe, draw, image, img_id, method
    global borderthickness_bd, highlightthickness
    
    for widgets in root.winfo_children():
        widgets.destroy()

    root.config(bg = '#454545')
    iconsframewidth = int(screen_width / 20)
    
    frames = Style()
    frames.configure('FRAMES.TFrame', background = '#2a2a2a')
    sep = Style()
    sep.configure('SEP.TFrame', background = '#1a1a1a')
    style = Style()
    style.configure('STAGE.TFrame', background = '#454545')
    icon = Style()
    icon.configure('ICON.TButton', background = '#2a2a2a', foreground = '#2a2a2a')
    
    iconsframe = Frame(root, width = iconsframewidth, style = 'FRAMES.TFrame')
    iconsframe.pack(side = 'left', expand = False, fill = 'y')
    iconsframe.pack_propagate(0)
    sep1frame = Frame(root, style = 'SEP.TFrame', width = 5)
    sep1frame.pack(side = 'left', expand = False, fill = 'y')
    optionsframe = Frame(root, style = 'FRAMES.TFrame', height = 100)
    optionsframe.pack(side = 'top', expand = False, fill = 'x')
    optionsframe.pack_propagate(0)
    sep2frame = Frame(root, style = 'SEP.TFrame', height = 5)
    sep2frame.pack(side = 'top', expand = False, fill = 'x')
    propertyframe = Frame(root, style = 'FRAMES.TFrame', width = 150)
    propertyframe.pack(side = 'right', expand = False, fill = 'y')
    propertyframe.pack_propagate(0)
    sep3frame = Frame(root, style = 'SEP.TFrame', width = 5)
    sep3frame.pack(side = 'right', expand = False, fill = 'y')
    stageframe = Frame(root, style = 'STAGE.TFrame')
    stageframe.pack(side = 'top', expand = True, fill = 'both')
    stageframe.pack_propagate(0)

    image = Image.open(filelocation)
    width, height = image.size
    if method == 'imagepaint': 
        draw = ImageDraw.Draw(image)

    imgtk  = ImageTk.PhotoImage(image)
    # width, height = imgtk._PhotoImage__size
    
    # imgtk  = PhotoImage(filelocation)
    #   ^--- no width, hight information ???
    
    stage = Canvas(stageframe, width = width, height = height, bd=borderthickness_bd, highlightthickness=highlightthickness) # default: bd=2, highlightthickness=1
    stage.pack(side="top", anchor = 'c', expand=True)

    root.update()

    # keyboard.add_hotkey("ctrl+s", lambda widget = stageframe, filelocation = filelocation: save(widget, filelocation))

    pencilbutton = Button(iconsframe, text = 'pencilimg', command = pencil_click, style = 'ICON.TButton')
    pencilbutton.pack(anchor = N, pady = 10)

    img_id = stage.create_image(stage.winfo_width() / 2, stage.winfo_height() / 2, image = imgtk)
    stage.image = imgtk

root = Tk()
root.title('App')

screen_width  = root.winfo_screenwidth()
screen_height = root.winfo_screenheight()

w = 1150
h =  600
x = (screen_width  / 2) - (w / 2)
y = (screen_height / 2) - (h / 2)

root.geometry('%dx%d+%d+%d' % (w, h, x, y))
root.minsize(1150, 600)

setup(filelocation)
root.mainloop()

Cropping a screenshot as a way of saving the graphics of tkinter Canvas is to be preferred over painting on a PIL image updating the tkinter Canvas because the latter has the side effect of slowing graphics down so painting smoothness suffer.裁剪屏幕截图作为保存 tkinter Canvas 图形的一种方式比在更新 tkinter Canvas 的 PIL 图像上绘画更可取,因为后者具有减慢图形速度的副作用,因此绘画平滑度会受到影响。

To see how to change the look of a button in tkinter (to change after the first click the pencilbutton to a savebutton ), check out Python tkinter: error _tkinter.TclError: bad window path name ".!button2" for how it can be done.要查看如何更改savebutton pencilbutton ,请查看Python tkinter: error _tkinter.TclError: bad window path name ".!button2"以了解它是如何实现的完毕。

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

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