Rotating tkinter canvas by a particular angle - python

I am trying to use the code at Python Tkinter rotate image animation with the following change:
Instead of rotating the canvas endlessly, I want a rotation of "turn" degrees which is randomly decided using randint() function. However, after turning by this angle, the tkinter window disappears and an error is raised. How can I make the following code work.
From my intermediate level knowledge of Python, I can see that the "yield" statement is putting a generator to work.
You can use any image in place of "0.png" in my code. I am using Python 3.9.6. Thanks in advance. The following is the code I am trying to get to work.
from tkinter import *
from PIL import ImageTk, Image
from time import sleep
from random import randint
class SimpleApp(object):
def __init__(self, master, filename):
self.master = master
self.filename = filename
self.canvas = Canvas(master, bg="black", width=500, height=500)
self.canvas.pack()
self.update = self.draw().__next__
master.after(100, self.update)
def draw(self):
image = Image.open(self.filename)
angle = 0
turn = randint(30, 390)
for i in range(turn):
tkimage = ImageTk.PhotoImage(image.rotate(angle))
canvas_obj = self.canvas.create_image(250, 250, image=tkimage)
self.master.after_idle(self.update)
yield
self.canvas.delete(canvas_obj)
angle = (angle - 1) % 360
sleep(.01)
win = Tk()
app = SimpleApp(win, '0.png')
win.mainloop()

After last yield it exits function draw in normal way and then __next__() can't run it again and it raises StopIteration and this makes problem. Normally when it is used in for-loop then it catchs StopIteration. Or if you run it with next() then you can also catch StopIteration but in this example it is problem.
I would do it without yield. I would split it in two functions: draw() to set default values at start, and rotate() to update image.
import tkinter as tk
from PIL import ImageTk, Image
from time import sleep
from random import randint
class SimpleApp(object):
def __init__(self, master, filename):
self.master = master
self.filename = filename
self.canvas = tk.Canvas(master, bg="black", width=500, height=500)
self.canvas.pack()
self.draw()
def draw(self):
self.image = Image.open(self.filename)
self.angle = 0
self.turn = randint(30, 360)
self.canvas_obj = None
self.master.after(100, self.rotate)
def rotate(self):
# it will remove image after last move
#if self.canvas_obj:
# self.canvas.delete(self.canvas_obj)
if self.turn > 0:
# it will NOT remove image after last move
if self.canvas_obj:
self.canvas.delete(self.canvas_obj)
self.tkimage = ImageTk.PhotoImage(self.image.rotate(self.angle))
self.canvas_obj = self.canvas.create_image(250, 250, image=self.tkimage)
self.angle = (self.angle - 1) % 360
self.turn -= 1
self.master.after_idle(self.rotate)
win = tk.Tk()
app = SimpleApp(win, 'lenna.png')
win.mainloop()
lenna.png - (Wikipedia Lenna)

Related

How to save drawing on canvas as png file (linux)?

I am creating a painting application and I want to save my drawing on canvas widget as png file on my computer. This is my code:
from tkinter import *
from tkinter.filedialog import *
from functools import partial
from tkinter import Menu
from tkinter import filedialog,messagebox
from PIL import Image
from tkinter.colorchooser import askcolor
import pyscreenshot as ImageGrab
import pyautogui
class PaintingApp:
x=y=None
def __init__(self,window):
self.window = window
self.upper_frame = Frame(window)
self.upper_frame.grid(row=0,column=0, padx=10, pady=5,sticky="ew")
self.lower_frame = Frame(window)
self.lower_frame.grid(row=2, column=0, padx=10, pady=5,sticky="ew")
self.canvas= Canvas(self.lower_frame,width=500,height=530,bg="white")
self.canvas.grid()
self.objects = [] #objects on canvas
self.pen_size = 2
self.pcolor = "black"
self.pen = Button(self.upper_frame,text="Pen",command=partial(self.pen_draw,thickness=self.pen_size))
self.pen.grid(row=0,column=3,padx=(10,160))
self.bg = Button(self.upper_frame,text="Background",command= self.bgcolor) #change bg color
self.bg.grid(row=2,column=1,padx=(100,10))
self.upper_menu()
self.canvas.bind("<Button-1>", self.get_x_and_y)
self.canvas.bind("<B1-Motion>", lambda event, b=self.pen_size: self.pen_draw(b,event))
self.im = None
def save_pic(self,event=None):
file = asksaveasfilename(defaultextension=".png")
x = self.canvas.winfo_rootx() + self.canvas.winfo_x()
y = self.canvas.winfo_rooty() + self.canvas.winfo_y()
x1 = x + self.canvas.winfo_width()
y1 = y + self.canvas.winfo_height()
self.im=ImageGrab.grab(bbox=(x,y,x1,y1))
self.im.save(file[19:])
def pen_color(self,color):
self.pcolor= color
def get_x_and_y(self,event):
global x,y
x, y = event.x, event.y
def pen_draw(self,thickness,event=None):
global x,y
self.canvas.bind("<Button-1>", self.get_x_and_y) # Bind to pen_draw function
self.canvas.bind("<B1-Motion>", lambda event, b=self.pen_size: self.pen_draw(b,event))
if event != None:
self.objects.append(self.canvas.create_line((x, y, event.x, event.y), fill=self.pcolor,width=self.pen_size,capstyle=ROUND,smooth=True))
x, y = event.x, event.y
def upper_menu(self):
self.menubar = Menu(window)
self.menu1 = Menu(self.menubar, tearoff=0)
self.menu1.add_command(label="Save pic", command=self.save_pic)
self.menu1.add_separator()
self.menu1.add_command(label="Exit", command=window.destroy)
self.menubar.add_cascade(label="Settings", menu=self.menu1)
self.menu2 = Menu(self.menubar, tearoff=0)
self.menu2.add_command(label="Open pic")
self.menubar.add_cascade(label="Image", menu=self.menu2)
self.window.config(menu=self.menubar)
def bgcolor(self):
chosen_color = askcolor(color=self.canvas["bg"])[1]
self.canvas.configure(bg=chosen_color)
window = Tk()
window.geometry("500x450")
p = PaintingApp(window)
window.mainloop()
Now I have tried many many codes but it won't work. The code I provided above saves an all black picture which does not make any sense. I have tried using the module pyautogui as well but I still get the same result.
def save_pic(self,event=None):
file = asksaveasfilename(defaultextension=".png")
x = self.canvas.winfo_rootx() + self.canvas.winfo_x()
y = self.canvas.winfo_rooty() + self.canvas.winfo_y()
x1 = x + self.canvas.winfo_width()
y1 = y + self.canvas.winfo_height()
self.im=pyautogui.screenshot(region=(x,y,x1,y1))
self.im.save(file[19:])
If you use a screen shot library, you should wait until the backend UI framework(X11 in your case) finishes the drawing. Also you can use the PIL.ImageGrab.grab() in Pillow instead of pyscreenshot.
So do like this.(I fixed several errors in the original example, such as an incorrect path.)
...
from PIL import ImageGrab
...
class PaintingApp:
...
def save_pic(self,event=None):
file = asksaveasfilename(defaultextension=".png")
def grab_and_save():
x = self.canvas.winfo_rootx()
y = self.canvas.winfo_rooty()
x1 = x + self.canvas.winfo_width()
y1 = y + self.canvas.winfo_height()
self.im = ImageGrab.grab(bbox=(x,y,x1,y1))
self.im.save(file)
self.window.update()
self.window.after(1000, grab_and_save) # This waits 1000ms.
...
...
A better method will be to install Tkimg and export the bitmap on the canvas directly, but it will need some hard work. You can start with an unmaintained project, python-tkimg.

Refresh canvas after deleting - Tkinter

My problem is that when I consult an image from D:/Folder/my_drawing.jpg after "cleaning" the canvas, the canvas is dirty with the previous drawn images. The canvas is visually clear, but accumulates the former drawn image and the new one. The goal is make a Paint like program, that allows save draws and with a button that clean all the canvas.
The behaviour is as follow:
First I draw the curved line, after I clean the canvas, and after that, I draw the line, and when I consult the image, opening the file, the image is composed as shown below:
This is the code, in Python:
import os
from tkinter import *
from PIL import Image, ImageDraw
class Paint(object):
def __init__(self):
self.root = Tk()
self.pen_button = self.use_pen
self.save_button = Button(self.root, text='Save', command=self.Save)
self.save_button.grid(row=0, column=3)
self.eraser_button = Button(self.root, text='Clean canvas', command=self.use_eraser)
self.eraser_button.grid(row=0, column=1)
self.c = Canvas(self.root, bg='white', width=600, height=600)
self.c.grid(row=1, columnspan=5)
self.setup()
self.root.mainloop()
def activate_button(self, some_button):
self.active_button = some_button
def use_pen(self):
self.activate_button(self.pen_button)
def setup(self):
self.path=''
self.old_x = None
self.old_y = None
self.image1 = Image.new("RGB",(600,600),'white')
self.draw = ImageDraw.Draw(self.image1)
self.active_button = self.pen_button
self.c.bind('<B1-Motion>', self.paint)
self.c.bind('<ButtonRelease-1>', self.reset)
def use_eraser(self):
self.c.delete(ALL)
def Save(self):
self.c.postscript(file="my_drawing.jpg", colormode='color')
filename = "my_drawing.jpg"
self.image1.save(filename)
def paint(self, event):
self.line_width = 2.0
paint_color = 'black'
if self.old_x and self.old_y:
self.c.create_line(self.old_x, self.old_y, event.x, event.y,
width=self.line_width, fill=paint_color, dash=(),capstyle=ROUND, smooth=TRUE, splinesteps=36)
self.draw.line([self.old_x, self.old_y, event.x, event.y], fill="black", width=5)
self.old_x = event.x
self.old_y = event.y
def reset(self, event):
self.old_x, self.old_y = None, None
if __name__ == '__main__':
Paint()
Yes, well..., you never actually display the image on the canvas.
In the function paint() you first draw a line segmant on the canvas, then you draw it in the image.
The function use_eraser() clears the canvas but does nothing with the image self.image1.
So, don't draw anything on the canvas but only on the image, and then display the image on the canvas. I have not worked much with PIL but I should think the image display will update automatically as you draw on it.
I've solved it with this changes. I hope it serves to another with the same problem.
import io
def Save(self):
ps=self.c.postscript(colormode='color')
img = Image.open(io.BytesIO(ps.encode('utf-8')))
img.save('D:/Folder/my_drawing.jpg')
def paint(self, event):
self.line_width = 2.0
paint_color = self.color
if self.old_x and self.old_y:
self.c.create_line(self.old_x, self.old_y, event.x, event.y,
width=self.line_width, fill=paint_color, dash=(),
capstyle=ROUND, smooth=TRUE, splinesteps=36)
self.old_x = event.x
self.old_y = event.y

Tkinter canvas PhotoImage is not appearing

I have a problem with my code. I am creating a small video game called Lumanite. I have created the homepage and have started the graphics generation, but I have run into a bug. I am using Python 3.3 and am on a Win 10 laptop. I run the program through a run file, which accesses the main_game file that uses the classes outlined in a separate file, spritesclasses. I am trying to make a sprite appear. Here is the code for the main_game file and the spritesclasses file. (They import the canvas and root from a MENU file)
#SPRITES
from tkinter import *
from GUI_FILE import canvas, root
from OPENING_FILE import show, hide
class Sprite():
def __init__(self, photoplace):
self.orgin = photoplace
self.photo = PhotoImage(file=photoplace)
self.w = self.photo.width()
self.h = self.photo.height()
def draw(self):
self.sprite = canvas.create_image(self.h, self.w, image=self.photo)
And the MAIN_GAME file:
#Main Game File:
from tkinter import *
from OPENING_FILE import show, hide
from GUI_FILE import root, canvas
from spritesclasses import *
def start_game():
genterrain()
def genterrain():
test = Sprite("logo.gif")
test.draw()
And the sprites are not appearing. No error or anything. Please help me. I will supply you with information at a further notice.
This is a known but tricky issue. You can read about it in Why do my Tkinter images not appear? I've implemented one possible solution below:
from tkinter import *
class Sprite():
def __init__(self, photoplace):
self.photo = PhotoImage(file=photoplace)
self.w = self.photo.width()
self.h = self.photo.height()
self.sprite = None
def draw(self):
canvas = Canvas(root, width=self.w, height=self.h)
canvas.pack()
self.sprite = canvas.create_image(0, 0, anchor=NW, image=self.photo)
def start_game():
genterrain()
def genterrain():
sprite = Sprite("logo.gif")
sprite.draw()
sprites.append(sprite) # keep a reference!
root = Tk()
sprites = []
start_game()
root.mainloop()
The assignment self.photo = PhotoImage(file=photoplace) isn't a sufficient reference as the object test goes out of scope when genterrain() returns and is garbage collected, along with your image. You can test this by commenting out the line sprites.append(sprite) and see your image disappear again.
Also, it wasn't clear why you were positioning the image at it's own width and height -- the first to arguments to create_image() are the X and Y position. I moved canvas creation into draw() so I could size the canvas to the image but that's not a requirement of the visibility fix.

Displaying TkInter and OpenCV windows on the same time

I am working to extend this solution given to me previously.
The user can draw randomly using the mouse on a TkInter Canvas widget, after that, the same curves with the same pixels coordinates are drawn on the OpenCV window.
I want to modify that solution so that the TkInter Canvas and the OpenCV window must be shown in the same time: each time the suer finishes drawing one curve on TkInter it is immediately redrawn on the OpenCV window.
Is there a way to fulfill this goal ?
Thank you in advance for any help.
This is arguably even easier to do than to draw all lines in OpenCV later.
I'd make the MaClasse class only make a blank image, open a window and show it on initiation, and give it a method to draw a single line and show the image again.Then you can create a MaClasse object in Test and draw a line in OpenCV each time you draw a line in Tkinter. Then you won't even need to save all lines you've drawn (you can completely remove self.liste).
from Tkinter import *
import numpy as np
import cv2
class Test:
def __init__(self):
self.b1="up"
self.xold=None
self.yold=None
self.liste=[]
self.maclasse = MaClasse()
def test(self,obj):
self.drawingArea=Canvas(obj)
self.drawingArea.pack()
self.drawingArea.bind("<Motion>",self.motion)
self.drawingArea.bind("<ButtonPress-1>",self.b1down)
self.drawingArea.bind("<ButtonRelease-1>",self.b1up)
def b1down(self,event):
self.b1="down"
def b1up(self,event):
self.b1="up"
self.xold=None
self.yold=None
self.liste.append((self.xold,self.yold))
def motion(self,event):
if self.b1=="down":
if self.xold is not None and self.yold is not None:
event.widget.create_line(self.xold,self.yold,event.x,event.y,fill="red",width=3,smooth=TRUE)
self.maclasse.dessiner_ligne(self.xold,self.yold,event.x,event.y)
self.xold=event.x
self.yold=event.y
self.liste.append((self.xold,self.yold))
class MaClasse:
def __init__(self):
self.s=600,600,3
self.ma=np.zeros(self.s,dtype=np.uint8)
cv2.namedWindow("OpenCV",cv2.WINDOW_AUTOSIZE)
cv2.imshow("OpenCV",self.ma)
def dessiner_ligne(self, xold, yold, x, y):
cv2.line(self.ma,(xold, yold),(x,y),[255,255,255],2)
cv2.imshow("OpenCV",self.ma)
if __name__=="__main__":
root = Tk()
root.wm_title("Test")
v = Test()
v.test(root)
root.mainloop()
Since the above code using a Tkinter window and a OpenCV window does not work for you, you could also show the OpenCV image in a Tkinter Toplevel window.
from Tkinter import *
import numpy as np
import cv2
import Image, ImageTk
class Test:
def __init__(self, parent):
self.parent = parent
self.b1="up"
self.xold=None
self.yold=None
self.liste=[]
self.maclasse = MaClasse(self.parent)
def test(self):
self.drawingArea=Canvas(self.parent)
self.drawingArea.pack()
self.drawingArea.bind("<Motion>",self.motion)
self.drawingArea.bind("<ButtonPress-1>",self.b1down)
self.drawingArea.bind("<ButtonRelease-1>",self.b1up)
def b1down(self,event):
self.b1="down"
def b1up(self,event):
self.b1="up"
self.xold=None
self.yold=None
self.liste.append((self.xold,self.yold))
def motion(self,event):
if self.b1=="down":
if self.xold is not None and self.yold is not None:
event.widget.create_line(self.xold,self.yold,event.x,event.y,fill="red",width=3,smooth=TRUE)
self.maclasse.dessiner_ligne(self.xold,self.yold,event.x,event.y)
self.xold=event.x
self.yold=event.y
self.liste.append((self.xold,self.yold))
class MaClasse:
def __init__(self, parent):
self.s=600,600,3
self.ma=np.zeros(self.s,dtype=np.uint8)
self.top = Toplevel(parent)
self.top.wm_title("OpenCV Image")
self.label = Label(self.top)
self.label.pack()
self.show_image()
def dessiner_ligne(self, xold, yold, x, y):
cv2.line(self.ma,(xold, yold),(x,y),[255,255,255],2)
self.show_image()
def show_image(self):
self.im = Image.fromarray(self.ma)
self.imgtk = ImageTk.PhotoImage(image=self.im)
self.label.config(image=self.imgtk)
if __name__=="__main__":
root = Tk()
root.wm_title("Test")
v = Test(root)
v.test()
root.mainloop()

How to execute a subprocess with multithreading

This program works and displays server latency on a small canvas, but because it takes the program time to ping the server and display the ping def display():, it is not possible to drag the window class WindowDraggable():, until the subprocess has finished, and thus there is lag when dragging the window. Can this lag be resolved with mutil-threading so the window can be dragged smoothly?
from tkinter import *
from PIL import ImageTk, Image
import subprocess
import _thread
host = "141.101.115.212" #host IP address
root = Tk()
root.overrideredirect(1)
im = Image.open("image.png")
width, height = im.size
canvas = Canvas(root, width=width, height=height)
canvas.pack()
image = ImageTk.PhotoImage(file="image.png")
canvas.create_image(0, 0, image=image, anchor=NW)
text = canvas.create_text(125, 75, anchor=CENTER)
def display():
global text
#Launches 'command' windowless and waits until finished; finds ping
suinfo = subprocess.STARTUPINFO()
suinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
x = subprocess.Popen(["ping.exe", "141.101.115.212"], stdout=subprocess.PIPE, startupinfo=suinfo)
#find latency with regex
x = str(x.communicate()[0])
lhs, rhs = x.split("Average = ")
lhs, rhs = rhs.split("\\", 1)
lhs, rhs = lhs.split("m")
if int(lhs) > 999:
lhs = "999" + "ms"
latency = lhs
canvas.itemconfig(text, text=latency, width=width)
canvas.itemconfig(text, font=("courier", 25, "bold"))
canvas.itemconfig(text, fill="white")
root.after(1000, display)
class WindowDraggable():
def __init__(self, label):
self.label = label
label.bind('<ButtonPress-1>', self.StartMove)
label.bind('<ButtonRelease-1>', self.StopMove)
label.bind('<B1-Motion>', self.OnMotion)
def StartMove(self, event):
self.x = event.x
self.y = event.y
def StopMove(self, event):
self.x = None
self.y = None
def OnMotion(self,event):
x = (event.x_root - self.x - self.label.winfo_rootx() + self.label.winfo_rootx())
y = (event.y_root - self.y - self.label.winfo_rooty() + self.label.winfo_rooty())
root.geometry("+%s+%s" % (x, y))
label = Label(root, text='drag me')
WindowDraggable(label)
label.pack()
#_thread.start_new_thread( print_time, ("Thread-2", 4, ) )
root.after(0, display())
root.mainloop()
Rather than try to fight Tkinter's builtin loop/threading, use it:
def wait_for_it(proc):
proc.poll()
if proc.returncode is None: # subprocess hasn't finished yet
root.after(100, lambda: wait_for_it(proc)) # register a callback for 100ms
else:
display(proc.communicate()[0])
def display(x):
lhs, rhs = x.split("Average = ")
# the rest of your code goes here...
# instead of root.after(0, display)
wait_for_it(subprocess.Popen(['ping', 'google.com']))
As a slight aside, I highly recommend pasting your code on the Code Review Stack Exchange to get some style pointers and help simplifying it.

Categories