Create an image in a canvas inside a class [duplicate] - python

This question already has answers here:
Why does Tkinter image not show up if created in a function?
(5 answers)
Closed 1 year ago.
I wanted to create a chess program using OOP. So I made a superclass Pieces, a subclass Bishop, and a UI class GameUI. I created a canvas in the class GameUI. I wanted, that when I instantiate an object bishop in the class GameUI, it shows an Image from a bishop, on the canvas.
The problem is, when I instantiate the Bishop, I don't see any image. So I tried to do the same with a text : instead of using the method create_image from the class Canvas, I used the method create_text, and it worked : I saw a text on the canvas. That means, the problem comes from the method create_image, and I don't understand it.
If I create an Image directly in the class GameUi, it works! but that's not what I want...
So I don't have any error message. I see the canvas (with a blue background), but no image on it.
Here's the code :
from tkinter import PhotoImage, Tk, Canvas
class Pieces:
def __init__(self, can, color, x_position, y_position):
self.color = color
self.x_position = x_position
self.y_position = y_position
class Bishop(Pieces):
def __init__(self, can, color, x_position, y_position):
super().__init__(can, color, x_position, y_position)
if color == "black":
icon_path = 'black_bishop.png'
elif color == "white":
icon_path = 'white_bishop.png'
icon = PhotoImage(file=icon_path) # doesn't see the image
can.create_image(x, y, image=icon)
class GameUI:
def __init__(self):
self.windows = Tk()
self.windows.title("My chess game")
self.windows.geometry("1080x720")
self.windows.minsize(300, 420)
self.can = Canvas(self.windows, width=1000, height=600, bg='skyblue')
icon = PhotoImage(file=icon_path) # here I create the image in this class, and
can.create_image(x, y, image=icon) # we can see it very well
self.bishop = Bishop(self.can, "black", 50, 50)
self.can.pack()
self.windows.mainloop()
app = GameUI()

To make your code work, I decided to sort of rewrite it based on this answer. It works now, but really the only thing that you needed to add was self.icon instead of icon. icon gets garbage collected since there is no further reference to it, while self.icon remains. Also, it's not entirely the same as yours was, so it probably needs a bit of rewriting too.
from tkinter import *
from random import randint
class Piece:
def __init__(self, canvas, x1, y1):
self.x1 = x1
self.y1 = y1
self.canvas = canvas
class Bishop(Piece):
def __init__(self, canvas, x1, y1, color):
super().__init__(canvas, x1, y1)
if color == "black":
icon_path = 'black_bishop.png'
elif color == "white":
icon_path = 'white_bishop.png'
self.icon = PhotoImage(file=icon_path)
self.ball = canvas.create_image(self.x1, self.y1, image=self.icon)
def move_piece(self):
deltax = randint(0,5)
deltay = randint(0,5)
self.canvas.move(self.ball, deltax, deltay)
self.canvas.after(50, self.move_piece)
class GameUI:
def __init__(self):
# initialize root Window and canvas
root = Tk()
root.title("Chess")
root.resizable(False,False)
canvas = Canvas(root, width = 300, height = 300)
canvas.pack()
# create two ball objects and animate them
bishop1 = Bishop(canvas, 10, 10, 'white')
bishop2 = Bishop(canvas, 60, 60, 'black')
bishop1.move_piece()
bishop2.move_piece()
root.mainloop()
app = GameUI()

Related

Resizing Canvas on Tkinter first run issue

I want to make a program that begins as a small window, then when given a path to an image, it maximises the screen and places the image in the centre.
If you run the code below you will see that the window maximises, the image is loaded into memory, the code runs with no errors and self.open_image calls self.draw_image(self.pimg) which runs without error, however the image is not present on the canvas.
If I click the button "Fix" and call self.fix it calls self.draw_image(self.pimg) which runs without error and correctly draws the image.
How can you call the same function twice with the same arguments and get different results. What is different.
I get the feeling this is happening because something has taken place in the main loop that hasn't taken place at the end of self.__init__, so that when i call self.draw_image the second time self.cv.create_image is able to interact with something in the resizable canvas.
In this example I am happy to assume the program will always begin as a small window and become a maximised window untill it is closed, never being resized again, however in my real program I would like to make it more dynamic where the window handles resizing sensibly, this is just a minimum reproducible example. It is for this reason that I would like to use the ResizingCanvas class (or one like it) even though I feel that it is likely the cause of the issue I am experiencing.
I have tried using breakpoints and stepping through the code watching the variables get created but I cant see the difference between the self.cv the first time around and self.cv after I click the button.
I read about a similar issue here on this question and he suggests binding "<Configure>" To the canvas and passing the coords from the event to the canvas. However this has already been implemented in ResizingCanvas
from tkinter import *
from PIL import Image, ImageTk
class ResizingCanvas(Canvas):
# https://stackoverflow.com/a/22837522/992644
def __init__(self,parent,**kwargs):
Canvas.__init__(self,parent,**kwargs)
self.bind("<Configure>", self.on_resize)
self.height = self.winfo_reqheight()
self.width = self.winfo_reqwidth()
def on_resize(self,event):
""" determine the ratio of old width/height to new width/height"""
wscale = float(event.width)/self.width
hscale = float(event.height)/self.height
self.width = event.width
self.height = event.height
# resize the canvas
self.config(width=self.width, height=self.height)
# rescale all the objects tagged with the "all" tag
self.scale("all",0,0,wscale,hscale)
class main():
def __init__(self, name = None):
self.root = Tk()
self.name = name # Filename
myframe = Frame(self.root)
myframe.pack(fill=BOTH, expand=YES)
self.cv = ResizingCanvas(myframe, width=850, height=400, bg="dark grey", highlightthickness=0)
self.cv.pack(fill=BOTH, expand=YES)
self.b = Button(self.cv, text = 'Fix', command = self.fix).grid(row=1,column=1)
self.open_img()
def draw_image(self, img, x = None, y = None):
""" Handles the drawing of the main image"""
self.img = ImageTk.PhotoImage(img)
self.cv.create_image(self.root.winfo_screenwidth()/2,
self.root.winfo_screenheight()/2, image=self.img, tags=('all'))
def open_img(self, event=''):
self.pimg = Image.open(self.name)
self.root.state("zoomed")
self.draw_image(self.pimg)
def fix(self, event=''):
self.draw_image(self.pimg)
def run(self):
self.root.mainloop()
if __name__ == "__main__":
path = 'example.png'
app = main(path)
app.run()
What should happen in the video:
I click run and the image is displayed immediately, without having to click the fix button.
What does happen in the video:
I click run and the image is not displayed until I click the fix button, afterwhich it works.
Changing
self.root.state("zoomed") to self.root.state("normal")
in your code (I am working on Python3) I can only get:
[
the image above, played a little bit starting from How to get tkinter canvas to dynamically resize to window width?
and now the code seems to work with me:
from time import sleep
from tkinter import *
from PIL import Image, ImageTk
class ResizingCanvas(Canvas):
# https://stackoverflow.com/a/22837522/992644
def __init__(self,parent, **kwargs):
# Canvas.__init__(self,parent,**kwargs)
print(kwargs)
Canvas.__init__(self,parent,**kwargs)
self.bind("<Configure>", self.on_resize)
# self.height = self.winfo_reqheight()
# self.width = self.winfo_reqwidth()
self.height = self.winfo_height()
self.width = self.winfo_width()
# self.height = height
# self.width = width
# self.__dict__.update(kwargs)
def on_resize(self,event):
""" determine the ratio of old width/height to new width/height"""
wscale = (event.width)//self.width
hscale = (event.height)//self.height
self.width = event.width
self.height = event.height
# resize the canvas
self.config(width=self.width, height=self.height)
# rescale all the objects tagged with the "all" tag
self.scale("all",0,0,wscale,hscale)
class main():
def __init__(self, name = None):
self.pippo = Tk()
self.name = name # Filename
self.myframe = Frame(self.pippo)
self.myframe.pack(side = BOTTOM, expand=YES)
# myframe.pack(fill=BOTH, expand='TRUE')
self.cv = ResizingCanvas(self.myframe, width=850, height=400, bg="dark grey", highlightthickness=0)
self.cv.pack(fill=BOTH, expand=YES)
# sleep(2)
self.b = Button(self.myframe, text = 'Fix', command = self.fix)#.grid(row=1,column=1)
self.b.pack(side=TOP)
self.open_img()
# self.pippo.mainloop() ## use it if you eliminate def run
def draw_image(self, img, x = None, y = None):
""" Handles the drawing of the main image"""
self.img = ImageTk.PhotoImage(img)
# self.cv.create_image(self.pippo.winfo_screenwidth()/2,
# self.pippo.winfo_screenheight()/2, image=self.img, tags=('all'))
self.cv.create_image(self.pippo.winfo_width()/2,
self.pippo.winfo_reqheight()/2, image=self.img, tags=('all'))
def open_img(self, event=''):
self.pimg = Image.open(self.name)
self.pippo.state("normal")
self.draw_image(self.pimg)
def fix(self, event=''):
self.draw_image(self.pimg)
def run(self):
self.pippo.mainloop()
if __name__ == "__main__":
path = 'example.png'
app = main(path)
app.run()
don't know about your question though, but wanted to be sure your starting example works right. Let me know if it could be related to python/pillow/tkinter version or something else
Here my window image results before ad after pressing fix button :
At the end found out that your code does work as long as you use
self.root.attributes('-zoomed', True) instead of `self.root.state("zoomed")`
The problem is here. self.root.winfo_screenwidth()
Change it to self.cv.width. I don't know why.
def draw_image(self, img, x = None, y = None):
""" Handles the drawing of the main image"""
self.img = ImageTk.PhotoImage(img)
self.cv.create_image(self.root.winfo_screenwidth()/2,
self.root.winfo_screenheight()/2, image=self.img, tags=('all'))
Change the last line to
self.cv.create_image(self.cv.width/2,
self.cv.height/2, image=self.img, tags=('all'))
Fixes the issue.
Tk.winfo_screenwidth() according to https://tkdocs.com/shipman/universal.html returns the width of the screen, indepedant of the size of the window, so even if you have a small window on a 1920x1080 display, this function will return 1920.
self.cv.width returns the width of the canvas object.

How would make it so I can apply a button to any window I want?

I had trouble wording the question so let me explain. I am learning to use Object Oriented Programming as part of a project, so I decided to start off by creating classes for creating windows and buttons to place on the windows (Labels ect. will come later too). In other words, I want to be able to place button on a "Window1" without specifying "Window1" in the class (and instead pass it through with the CreateButton(values). Here is my code so far, and help is appreaciated. PS. It functioned how it should before I tried the windowname parameter, and I set the window manually to home_window, but I cant seem to get the windowname paramater to pass through properly.
import tkinter as tk
from tkinter import ttk
home_window = tk.Tk()
home_window.title("Rota System")
home_window.geometry("600x600")
def thankyou():
print ("Thank you very much")
class CreateWindow():
def __init__(self, name, title, geometry):
self.name = name
self.title = title
self.geometry = geometry
name = tk.Tk()
name.title(title)
name.geometry(geometry)
class CreateButton():
def __init__(self, name, width, height, x, y, font, size, text, command, windowname):
#I want to be able to place the button on any window I want, not just home_window
self.name = name
self.width = width
self.height = height
self.x = x
self.y = y
self.font = font
self.size = size
self.text = text
self.command = command
self.windowname = windowname
name = tk.Button(windowname, text=text, command=command)
name.config(font=(font, size))
name.place(x=x, y=y, height=height, width=width)
CreateWindow("Test", "Test", "900x600")
CreateButton("Test","200","200","0","0","Courier",10,"Test",thankyou, "Test")
First: tkinter needs object's ID returned by Tk(), Button(), etc. - not names (strings) which you created.
With string names you would have to keep global dictionary {name: object_id} to convert name to object_id.
BTW:
Tk() should be used only to create main window. For other windows you should use Toplevel()
Your code in classes (and they names) rather fit to functions.
Function creates object and use return to return this object so it can be used in other function.
import tkinter as tk
from tkinter import ttk
# --- functions ---
def thank_you():
print ("Thank you very much")
def create_window(title, geometry):
window = tk.Tk()
window.title(title)
window.geometry(geometry)
return window
def create_button(parent, width, height, x, y, font, size, text, command):
button = tk.Button(parent, text=text, command=command)
button.config(font=(font, size))
button.place(x=x, y=y, height=height, width=width)
return button
# --- main ---
window = create_window("Test", "900x600")
button = create_button(window, 200, 200, 0, 0, "Courier", 10, "Test", thank_you)
window.mainloop()
Using classes I would inherit it. So I can assing instance to variable and use in other class as parent.
And I would use nouns as names for classes.
import tkinter as tk
from tkinter import ttk
# --- classes ---
class MainWindow(tk.Tk):
def __init__(self, name, title, geometry):
super().__init__()
self.name = name
self.title(title)
self.geometry(geometry)
#self._title = title
#seff._geometry = geometry
class MyButton(tk.Button):
def __init__(self, parent, name, width, height, x, y, font, size, text, command):
super().__init__(parent, text=text, command=command)
self.name = name
self.config(font=(font, size))
self.place(x=x, y=y, height=height, width=width)
#self._width = width
#self._height = height
#self._x = x
#self._y = y
#self._font = font
#self._size = size
#self._text = text
#self._command = command
# --- functions ---
def thank_you():
print("Thank you very much")
# --- main ---
window = MainWindow("WindowName", "Test", "900x600")
button = MyButton(window, "ButtonName", 200, 200, 0, 0, "Courier", 10, "Test", thank_you)
window.mainloop()
See: PEP 8 -- Style Guide for Python Code -
I don't know if there is about nouns for class names and verbs for functions (or maybe it was in book Clean Code, Robert C. Martin) but usually people use this rule.

To draw rectangles inside label using Qpainter in python

I am trying to draw rectangles according to a percentage value inside my labels. So it is important for me to know the exact coordinates of my labels. Since I am using multiple windows in cascade, the global positions of the current window are not retrieved accurately. Is there an easy workaround to this problem without changing my entire coding layout? form2 is my current view which is displayed as part of form1.
It could be helpful if I can map the coordinates of my label by using mapTo some widget inside my current window. My code seems to work except for the position of my rectangle. I have tried maptToGlobal() and mapToParent() and
self.label_pos = self.mapTo(self.frame, self.label_1.geometry().bottomLeft()) I cannot seem to access the widgets written inside my parent class using self. Or is there something wrong with my syntax?
import Ui_ImageCrop # design of my window using Qt Designer
import math
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMainWindow, QLabel
from PyQt5.QtGui import QPainter, QPen
class ImageCrop(Ui_ImageCrop.Ui_MainWindow, QMainWindow):
def __init__(self, parent=None):
super(ImageCrop,self ).__init__()
self.setupUi(self)
def paintEvent(self,e):
painter = QPainter(self)
self.square1 = Show_Square()
if self.lineEdit_1.text() == "":
self.square1.percent = 0
else:
self.square1.percent = float(self.lineEdit_1.text())
self.label_pos = self.mapTo(self, self.label_1.geometry().bottomLeft())
self.square1.x = self.label_pos.x()
self.square1.y = self.label_pos.y()
self.square1.w = self.label_1.width()
self.square1.h = self.label_1.height()
self.square1.drawRectangles(painter)
self.update()
class Show_Square(QLabel):
def __init__(self):
super(Show_Square,self).__init__()
self.percent = 50
self.x = 0
self.y = 0
self.w = 10
self.h = 10
def drawRectangles(self, painter):
center_x = float(self.x+self.w/2)
center_y = float(self.y+self.h/2)
rect_crop = float(self.percent*self.w*self.h/100)
k = float(self.h/self.w)
rect_w = int(math.sqrt(rect_crop/k))
rect_h = int(k*rect_w)
rect_x = int(center_x-rect_h/2)
rect_y = int(center_y-rect_w/2)
painter.setPen(QPen(Qt.black, 1, Qt.SolidLine))
painter.drawRect(rect_x,rect_y,rect_w,rect_h)
This is a solution that I could come out with. Total three frames were used in my layout for each label. So each label positions were found after adding the enclosing frame geometries.
frame1 = self.frame_1.geometry().topLeft()
frame2 = self.frame_2.geometry().topLeft()
frame3 = self.frame_3.geometry().topLeft()
label1_topLeft = frame1 + frame2 + frame3 + self.label_1.geometry().topLeft()
label1_bottomRight = frame1 + frame2 + frame3 + self.label_1.geometry().bottomRight()
Also, instead of placing the rectangles by drawing over the pixmap each time in paintEvent, it seems to work faster when the rectangles are drawn inside the labels. One could leave the images partly transparent.

Python Tkinter coordinate canvas error

I am a total beginner here. I would like to know the coordinates of x1 while it is moving, so it will be keep updating.
Here is my code.
from tkinter import *
import tkinter as tk
import time
import random
class Example(tk.Frame):
def __init__(self,parent):
tk.Frame.__init__(self)`
#create a canvas
self.canvas = tk.Canvas(width=600, height=250)
self.canvas.pack()
self.road()
self.crossing()
def road(self):
Line1 = self.canvas.create_line(50, 50, 450, 50)
Line2 = self.canvas.create_line(50, 100, 450, 100)
def crossing(self):
CLine1 = self.canvas.create_line(350, 50, 350, 100)
CLine2 = self.canvas.create_line(375, 50, 375, 100)
class Car:
def __init__(self,x1,y1,x2,y2,vx,vy,color,Example):
self.x1 = x1
self.y1 = y1
self.x2 = x2
self.y2 = y2
self.vx = vx
self.vy = vy
self.color=color
self.Example = Example
def drawit(self):
self.Example.canvas.create_rectangle(x1,y1,x2,y2,color)
def moveit(self,vx,vy):
self.Example.canvas.move(vx,vy)
if __name__ == "__main__":
root = tk.Tk()
my_canvas = Example(root).pack(fill="both", expand=True)
mycar = Car(60, 60, 125, 90,3,0,"red",Example)
mycar.drawit()
mycar.moveit()
print (mycar.x1)
root.mainloop()
Here is the error message:
AttributeError: type object 'Example' has no attribute 'canvas'
Process finished with exit code 1
Any help would be much appreciated.
You have some basic misunderstandings of how classes and objects work. Instead of doing this:
my_canvas = Example(root)
my_canvas.pack(fill="both", expand=True)
mycar = Car(60, 60, 125, 90,3,0,"red",Example)
(note: you also need to call pack on a separate line from where the widget is created and assigned to a variable. See https://stackoverflow.com/a/1101765/7432)
You need to do this:
my_canvas = Example(root).pack(fill="both", expand=True)
mycar = Car(60, 60, 125, 90,3,0,"red", my_canvas)
You need to pass in the instance of Example (eg: my_canvas), not the class (eg: Example). Also, Car needs to use the example like this:
class Car:
def __init__(self,x1,y1,x2,y2,vx,vy,color,example):
...
self.example=example
def drawit(self):
self.example.canvas.create_rectangle(x1,y1,x2,y2,color)
def moveit(self,vx,vy):
self.example.canvas.move(vx,vy)
Your Car doesn't have access to the Example object. I would pass my Example to my Car on init, so that it can access its context. e.g.
class Car:
def __init__(self, x, y, example):
...
self.example=example
def do_stuff(self):
self.example.canvas.draw(whatever)
example = Example(args)
car = Car(3,4,example)
car.do_stuff()
In this example, Car objects have access to the canvas that you create in your Example class, so they can draw themselves, etc.

Why tkinter canvas doesn't update when used in a class

I am writing a small program, where I want to draw something on canvas. This code works for me;
import tkinter as tk
from PIL import Image, ImageTk
from l_systems import Lindenmayer
if __name__ == "__main__":
root = tk.Tk()
root.title("Draw Shapes with L-Equations")
cv = tk.Canvas(width=600, height=600, bg='white')
cv.pack()
image1 = Image.new("RGB", (600, 600), (255,255,255))
koch = Lindenmayer(image1)
koch.init(
iterations = 6,
angle = 25,
axiom = "---X",
rules = {"X":"2F-[1[X]+3X]4+F[3+FX]-X", "F":"FF"},
constants = "X") # This creates a drawing on PIL image
# Canvas.create_image expects a PhotoImage
photo = ImageTk.PhotoImage(image1)
cv.create_image((300,300), image=photo)
root.mainloop()
However, I want to organize my tkinter application as a class, therefore I have tried this code,
class main(tk.Frame):
w = 600
h = 600
def __init__(self,parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
self.cv = tk.Canvas(width=self.w, height=self.h, bg='white')
self.cv.pack()
self.render_image()
def render_image(self):
image1 = Image.new("RGB", (self.w, self.h), (255,255,255))
koch = Lindenmayer(image1)
koch.init(
iterations = 6,
angle = 25,
axiom = "---X",
rules = {"X":"2F-[1[X]+3X]4+F[3+FX]-X", "F":"FF"},
constants = "X"
)
photo = ImageTk.PhotoImage(image1)
self.cv.create_image((self.w/2,self.h/2), image=photo)
if __name__ == "__main__":
root = tk.Tk()
root.title("Draw Shapes with L-Equations")
app = main(root).pack()
root.mainloop()
In this second case, I don't see any drawing on canvas. It is just a white background. How can I fix this?
PhotoImage can have problem in classes and functions. Garbage collector can remove it from memory.
EDIT:
I could check this (because I have to Lindenmayer module)
but your class could look this:
Almost everything is in class.
Class names should normally use the CapWords convention. - see PEP 8 -- Style Guide for Python Code. Event SO use that rule to recognize classes in code and use light blue color.
import tkinter as tk
from PIL import Image, ImageTk
from l_systems import Lindenmayer
class Main(tk.Frame):
def __init__(self,parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.w = 600
self.h = 600
self.parent = parent
self.parent.title("Draw Shapes with L-Equations")
self.cv = tk.Canvas(width=self.w, height=self.h, bg='white')
self.cv.pack()
self.render_image()
self.parent.pack()
def render_image(self):
image1 = Image.new("RGB", (self.w, self.h), (255,255,255))
koch = Lindenmayer(image1)
koch.init(
iterations = 6,
angle = 25,
axiom = "---X",
rules = {"X":"2F-[1[X]+3X]4+F[3+FX]-X", "F":"FF"},
constants = "X"
)
self.photo = ImageTk.PhotoImage(image1)
self.cv.create_image((self.w/2,self.h/2), image=self.photo)
def run(self):
self.parent.mainloop()
if __name__ == "__main__":
Main(tk.Tk()).run()

Categories

Resources