Issue deleting a canvas object from class - python

I have a problem with deleting canvas object from class.
I created an object of type Rectangle called f. Then I need to delete this object. Python deletes f, but does not delete a canvas object, which is on Frame. I don't know where is the problem.
from tkinter import *
class Rectangle():
def __init__(self, coords, color):
self.coords = coords
self.color = color
def __del__(self):
print("In DELETE")
del self
print("Goodbye")
def draw(self, canvas):
"""Draw the rectangle on a Tk Canvas."""
print("In draw ")
print("Canvas = ",canvas)
print("self = ",self)
print("bild canvas = ",canvas.create_rectangle(*self.coords, fill=self.color))
root = Tk()
root.title('Basic Tkinter straight line')
w = Canvas(root, width=500, height=500)
f = []
f = Rectangle((0+30*10, 0+30*10, 100+30*10, 100+30*10), "yellow")
print("Draw object", f.draw(w), f)
f.__del__()
del f
w.pack()
mainloop()

Ok, the problem you are having is you started creating a Rectangle object for your own use, which seems reasonable, but you need to work on its implementation.
Anyways to accomplish what you want to do simply (without your object):
# draws a rectangle and returns a integer
rectangle_id = c.create_rectangle(*(0, 0, 30, 30), fill="yellow")
c.delete(rectangle_id) # removes it from the canvas
To accomplish what you want with your Rectangle object I suggest using an attribute to store the id when you drew it and implement a method that can delete it. It looks like you may want to use the __del__ method to remove it when there are no longer any references to your object. This can be done, but you should be aware of some caveats (outside of the scope of my answer... See: http://eli.thegreenplace.net/2009/06/12/safely-using-destructors-in-python/). I personally would opt for explicitly calling a method to delete the object representation from the view to avoid all that nonsense :).
There are many design decisions here I am ignoring, I suggest you put some thought into your use of OO here, or avoid it until you have better understanding of tkinter.

Related

Control location on screen where new windows open with graphics.py

I have a python program that deploys a windows via graphics.py. The initial window opened by the GraphWin class opens in the top left corner of the screen. Subsequent calls to GraphWin cascade from the upper left to the lower right.
I'd like to control the placement of each window. (Example: Have all the windows open in a grid-layout so I can create a dashboard.)
I think there is no such method in graphics.py right now.
Ref: The Book and webpage.
If you want to stick to using graphics.py, I suggest creating a dashboard by dividing a single window into different slots.
This option does exist in Tkinter library. Please refer to this answer for more information on that.
graphics.py doesn't provide a way for you to control the location of instances of its GraphWin class. However the fact that it's built on top of Python's Tk GUI toolkit module named tkinter means that sometimes you can work around its limitations by looking at its source code to see how things operate internally.
For example, here's a snippet of code from the module (version 5.0) showing the beginning of GraphWin class' definition from the graphics.py file:
class GraphWin(tk.Canvas):
"""A GraphWin is a toplevel window for displaying graphics."""
def __init__(self, title="Graphics Window",
width=200, height=200, autoflush=True):
assert type(title) == type(""), "Title must be a string"
master = tk.Toplevel(_root)
master.protocol("WM_DELETE_WINDOW", self.close)
tk.Canvas.__init__(self, master, width=width, height=height,
highlightthickness=0, bd=0)
self.master.title(title)
self.pack()
master.resizable(0,0)
self.foreground = "black"
self.items = []
self.mouseX = None
self.mouseY = None
self.bind("<Button-1>", self._onClick)
self.bind_all("<Key>", self._onKey)
self.height = int(height)
self.width = int(width)
self.autoflush = autoflush
self._mouseCallback = None
self.trans = None
self.closed = False
master.lift()
self.lastKey = ""
if autoflush: _root.update()
As you can see it's derived from a tkinter.Canvas widget which has an attribute named master which is a tkinter.Toplevel widget. It then initializes the Canvas base class and specifies the newly created Toplevel window as its parent.
The size and position of a Toplevel window can be controlled by calling its geometry() method as described in the linked documentation. This method expects to be passed a "geometry string" argument in a certain format ('wxh±x±y').
This mean you can take advantage of how this implementation detail in order to put it anywhere you want it and as well as resize if desired.
Here's an example of doing that:
from graphics import *
def main():
win = GraphWin("My Circle", 100, 100)
# Override size and position of the GraphWin.
w, h = 300, 300 # Width and height.
x, y = 500, 500 # Screen position.
win.master.geometry('%dx%d+%d+%d' % (w, h, x, y))
c = Circle(Point(50,50), 10)
c.draw(win)
win.getMouse() # pause for click in window
win.close()
if __name__ == '__main__':
main()
My desktop while script is running:

Passing a tkinter canvas between classes without calling the child from within the parent

I am somewhat of a beginner when it comes to Python, but i decided i want to write a basic 2-d physics playground. Unfortionetly i ran straigt into trouble when trying to setup the basic structure.
My plan is to create a GUI with a canvas in a parent function named mainWindow, then i figured i would create a child class (Hero) which creates a circle the user can manipulate on the canvas. This seems to work fairly well.
The problem occurs when i try to do anything with the Hero class, like call a function to delete the circle so i can redraw it in some direction. I can't seem to pass the canvas from the mainWindow to the Hero class. Any help would be greatly appreciated, including telling me that this is the wrong way to do things.
Im adding the two documents im working with since my rambling is probably hard to follow.
I run the program from the phesics.py document, resulting in the GUI poping up with my canvas and a red circle. When i close the window i get the following error:
classes.py", line 29, in moveHeroBody
canvas.delete(heroBody)
NameError: name 'canvas' is not defined
Unfortionetly i dont know how to get the "world" into the child
classes.py
from tkinter import *
class mainWindow():
def __init__(self):
#Setup the GUI
root = Tk()
root.geometry('800x600')
# Setup the canvas within the GUI (master)
world = Canvas(root, height = 600, width = 800, bg = "#FFFFFF")
world.place(relx = 0.5, rely = 0.5, anchor = CENTER)
Hero(world)
root.mainloop()
class Hero(mainWindow):
def __init__(self,world):
#Initial creation of hero at coordinates
x1 = 10
y1 = 10
x2 = 70
y2 = 70
heroBody = world.create_oval(x1,y1,x2,y2, fill = "#FF0000", outline = "#FF0000")
#Move the hero
def moveHeroBody():
print("moveHeroBody")
world.delete(heroBody)
phesics.py
from tkinter import *
from classes import *
mainWindow1 = mainWindow()
moveHero = Hero.moveHeroBody()
You're passing it ok, but you're throwing the value away. Also, Hero shouldn’t inherit from mainWindow.
You need to save world as an attribute so that you can reference it later.
class Hero():
def __init__(self,world):
self.world = world
...
Then, you can use self.world to reference the canvas:
def moveHeroBody():
print("moveHeroBody")
self.world.delete(heroBody)
Though, the above code will fail because heroBody is a variable local to the __init__ - you need to do the same with it:
class Hero():
def __init__(self,world):
self.world = world
...
self.heroBody = world.create_oval(...)
#Move the hero
def moveHeroBody():
print("moveHeroBody")
self.world.delete(self.heroBody)
I think you need to initialize the class Hero in your mainWindow class. The modifications needed to do in the code are:
classes.py
from tkinter import *
from time import sleep
class mainWindow():
def __init__(self):
#Setup the GUI
self.jump_gap = 25
root = Tk()
root.geometry('800x600')
# Setup the canvas within the GUI (master)
self.world = Canvas(root, height = 600, width = 800, bg = "#FFFFFF")
self.world.place(relx = 0.5, rely = 0.5, anchor = CENTER)
self.hero = Hero(self.world)
self.world.pack()
root.bind("<space>",self.jump) # -> [1] Binds the SPACE BAR Key to the function jump
root.mainloop()
def jump(self,event):
gaps = list(range(self.jump_gap))
for i in gaps:
self.world.after(1,self.hero.moveHeroJump(h=i)) # [2] -> Binds the moveHeroJump method with the window action to a queue of updates
self.world.update() #[2] updates the canvas
sleep(0.01*i) # Added some linear wait time to add some look to it
gaps.reverse()
for i in gaps:
self.world.after(1,self.hero.moveHeroJump(h=-i))
self.world.update()
sleep(0.01*i)
class Hero():
def __init__(self,world):
#Initial creation of hero at coordinates
self.world = world
self.x1 = 10
self.y1 = 410
self.x2 = 70
self.y2 = 470
self.heroBody = self.world.create_oval(self.x1,self.y1,self.x2,self.y2, fill = "#FF0000", outline = "#FF0000")
#Move the hero
def moveHeroJump(self,h):
print("moveHeroBody")
self.y1 -= h
self.y2 -= h
self.world.delete(self.heroBody)
self.heroBody = self.world.create_oval(self.x1,self.y1,self.x2,self.y2, fill = "#FF0000", outline = "#FF0000")
physics.py
from tkinter import *
from classes import *
mainWindow1 = mainWindow()
Edit
So this got me playing some minutes ago, and I researched some sources from stack in order to complete this question. Here are the sources (referenced in the code as well):
How to bind spacebar key to a certain method in tkinter python
Moving Tkinter Canvas
The solution edited above is capable to perform a simple animation of a ball jumping. self.jump_gap is a fixed quantity that tells the ball how much does it needs to jump. The jump parses a certain height h to the moveHeroJump method to make the ball change its position, after the change of position is queued into the Canvas an update is called to see the changes on the ball.

Trouble calling a function from a class to draw an oval in Python

Beginner here trying to make sense of classes. Below is the code for my class Cell:
import tkinter
import random
top = tkinter.Tk()
canvas = tkinter.Canvas(top, bg="grey", height=400, width=400)
canvas.pack()
class Cell:
def __init__(self, x, y, r):
self.x = random(x)
self.y = random(y)
self.r = 200
def show(self):
canvas.create_oval(self.x, self.y, self.r, self.r, fill = "blue")
top.mainloop()
I'm attempting to draw the cell in my main program by calling the function show from the class. Here is the code for my main window:
import tkinter
top = tkinter.Tk()
canvas = tkinter.Canvas(top, bg="grey", height=400, width=400)
canvas.pack()
from Cell import Cell
cell = Cell()
cell.show()
top.mainloop()
This is resulting in the canvas being drawn correctly, but the oval is nowhere to be found. I am not getting any errors either.
Any help would be appreciated. Thank you!
====================
Turns out, I misunderstood the arguments for create_oval. I found some code that converts the clunky create_oval function into a function which receives a set of coordinates for the center of the oval and a radius.
In addition to this, the help I received in understanding classes and other Python functionality helped significantly as well. Thanks to those who helped!
This is my revised code which works as intended.
import tkinter as tk
import random
top = tk.Tk()
canvas = tk.Canvas(top, width=400, height=400, bg="grey")
canvas.grid()
def _create_circle(self, x, y, r, **kwargs):
return self.create_oval(x-r, y-r, x+r, y+r, **kwargs)
tk.Canvas.create_circle = _create_circle
class Cell:
def __init__(self, canvas, x, y, r):
self.canvas = canvas
self.x = x
self.y = y
self.r = r
def show(self):
self.canvas.create_circle(self.x, self. y, self.r, fill = "blue")
cell = Cell(canvas, random.randrange(50, 350), random.randrange(50, 350), 25)
cell.show()
top.mainloop()
The problem here is that your main script and your Cell module are both creating a new Tk instance, adding a Canvas to it, and then calling its mainloop method.
If you trace through the order in which statements get executed, you'll find that the cell = Cell() and cell.show() don't happen until after the first top.mainloop() returns, and mainloop() doesn't return until you quit the program. (In fact, if your code did get that far, it would fail with a TypeError, which I'll get to below.)
But, more generally, you only want one Tk in your program, and everyone else should refer to that.
And, in this case, you want the same for the Canvas: just one of them, packed onto the one Tk main window.
So, how can Cell.show refer to the canvas global from another module?
The best solution is to not refer to it as a global at all, and instead pass it in to the initializer, the same way you do with x, y, and r:
class Cell:
def __init__(self, canvas, x, y, r):
self.canvas = canvas
self.x = random(x)
self.y = random(y)
self.r = 200
def show(self):
self.canvas.create_oval(self.x, self.y, self.r, self.r, fill = "blue")
And then in the main script:
cell = Cell(canvas, ?, ?. ?)
cell.show()
But notice those ?s I put there. Your Cell class definition demands x, y, and r values in its initializer, but your Cell() constructor call doesn't pass any. That will raise a TypeError complaining that you're missing required arguments.
What do you want to pass here? Since the canvas is 400x400, maybe you want to pass something like 400, 400, 200? If so:
cell = Cell(canvas, 400, 400, 200)
cell.show()
Going back to that initializer, you've got some other problems there:
self.x = random(x)
self.y = random(y)
That random is a module. You can't call a module. You probably wanted something like this:
self.x = random.randrange(x)
That calls a function from the random module, one which is defined to return a random number in range(0, x), which seems like what you want.
Also:
self.r = 200
Why take an r parameter, just to ignore it? You probably wanted this:
self.r = r
Or, maybe you didn't actually want x, y, and r as parameters? Maybe you want to hardcode randrange(400), randrange(400), and 200, or maybe you want to compute them from the width and height of the canvas parameter, or… you can do almost anything you want, you just have to think through what you want, and make sure the interface you declare in the def matches the way you call it in the Cell(…) later.
I think your execution path is never getting to the cell.show() line.
When you import Cell, you have code at the top level top.mainloop(). This enters the main loop and never exits, so you never get to the lines below it.
It's a good rule of thumb to avoid putting code at the base level. Leave that for defining classes and functions. If you want code to run when the file is called like a script, put it in a if __name__ == __main__: condition.
You also had some syntax issues using random and calling the Cell constructor.
The example below works as expected.
import tkinter
import random
top = tkinter.Tk()
canvas = tkinter.Canvas(top, bg="grey", height=400, width=400)
canvas.pack()
class Cell:
def __init__(self, x, y, r):
self.x = x
self.y = y
self.r = r
def show(self):
canvas.create_oval(self.x, self.y, self.r, self.r, fill = "blue")
if __name__ == "__main__":
cell = Cell(100, 50, 5)
cell.show()
top.mainloop()

trouble creating oval widgets in python tkinter

I am having issues creating oval shapes on a canvas widget, so i have declared a canvas widget on a frame in the constructor.
class Map(Frame):
def __init__(self, master = none):
Frame.__init__(self, master)
....... ##lines of code
c = Canvas(master, width = 500,height = 500,relief = "groove")
c.pack(side = "right")
def operation(self):
.............. ##lines of code
self.createoval() ##call create oval method after loop code
def createoval(self):
x = 0
y = 0
c1 = canvas.create_oval(x-5, y-5, x+10, y+10, fill = "red")
##error map object has no create_oval.
c1.coords(c1, x, y)
c1.move(c1, 500, 500)
return c1
Ideally, what i would like is to create oval shapes after the loop has finished, so i would call the method that handles this, however i am having two issues.
1st. First issue i am having is creating oval on the canvas that has been established in the constructor. When i try to do this, i get message "canvas not defined" or when i use self.canvas.createoval, i get the message "map object has no attribute canvas"
So my question is how can i create a method that can create ovals on a canvas establsihed in the constructor?
2nd issue:
c1 = canvas.create_oval(x-5, y-5, x+10, y+10, fill = "red")
I get error that x is not defined, even though it is defined and i have used the coords() method but this has not fixed the problem.
You've got a number of issues with this code. Below is code that works for me in creating an oval on your canvas (although given your coordinates, it is partly off screen).
from tkinter import *
class Map(Frame):
def __init__(self, master = None):
Frame.__init__(self, master)
self.c = Canvas(master, width = 500,height = 500,relief = "groove")
self.c.pack(side = "right")
def operation(self):
self.createoval() ##call create oval method after loop code
def createoval(self):
x = 0
y = 0
c1 = self.c.create_oval(x-5, y-5, x+10, y+10, fill = "red")
return c1
m = Map(Tk())
m.createoval()
To address some of your problems:
You used none when you should have used None. It is case sensitive!
When you create an object, if you want a variable to be known to that instance of the object, you need to make it part of self. That means when you create your canvas with c = Canvas(... you need to actually have self.c = Canvas(.... Otherwise c will only exist within the scope of __init__ and will not exist anywhere else.
You try to create your oval using the canvas variable but where did this variable come from? It was never defined previously. You specifically called your canvas variable c in the __init__ method so you need to call it the same thing in the createoval method. Note you still also need to use self.c.
You used the line c1.coords(c1,x,y) presumably to set the position of the oval. However the creation of the oval will have already set the position, so if that was your intent, this is a redundant line. It is also not being used correctly. The variable c1 that was created is a simple integer which is an ID for the oval on the canvas. You need to call the coords method with the canvas object using self.c.coords(c1,x,y). This will reset the coordinates for the c1 oval on the canvas self.c to the position (x,y), however since you never changed x or y after creating the oval, it will set it to the same position and you won't notice any change.
You'll see the same problem with your call to move as you do for coords.

Python; tkinter; Canvas objects and events

I have a class with some mouse events I made :
class graphic_object(object):
def mouse_click(self,event):
#do something
def mouse_move(self,event):
#do something
def mouse_unpressed(self,event):
#do something
Instances of this class aren't literally graphic objects on the screen, but they have their graphic representation, which is circle-shaped, and as I said, they listen to the mouse events. Both, graphic representation and event handling are managed by tkinter.Canvas object, which is their visual container.
When I make one istance of this class:
graphic1 = graphic_object(a,b,c,d) # init method takes coordinates of the circle as arguments; a,b,c,d - numbers
Everything works as it should, object responds on the mouse events in desired way. But when I make two instances:
graphic1 = graphic_object(a,b,c,d)
graphic2 = graphic_object(e,f,g,h)
only the last created object responds on the mouse events.
This is the condition where I check if the mouse is over the circle:
if d < self.radius:
where d is distance between mouse position, and the center of the circle, and radius is radius of the circle.
In the debugger I see that self.center is always the center of the last created object, so condition is always on
the second circle. So, how can I make that both objects respond to the mouse events?
Events handling:
C = Canvas()
C.bind("<Button-1>" ,self.mouse_click)
C.bind("<B1-Motion>",self.mouse_move)
C.bind("<ButtonRelease-1>",self.mouse_unpressed)
It appears that in your mouse binding you are relying on a pre-computed global variable (d). This is not how you should implement such bindings. The first thing you should do in the binding is get the current mouse coordinates, and then calculate d.
Your other choice is to put the binding on each canvas object using the tag_bind method of the canvas. See this question for an example: How do I attach event bindings to items on a canvas using Tkinter?
You wrote in a comment to this answer that you are only sometimes getting mouse clicks. There is not enough detail in your code to know what you're doing, but I can assure you that the canvas doesn't normally fail in such a manner.
I can't debug your code since you are only showing bits and pieces, but here's a working example that tries to illustrate the use of tag_bind. I took some liberties with your code. For example, I added a name parameter so I can print out which circle you clicked on. When I test this, every click seems to register on the proper circle.
import Tkinter as tk
class Example(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.canvas = tk.Canvas(self, width=400, height=400,
background="bisque")
self.canvas.pack(fill="both", expand=True)
graphic1 = GraphicObject(10,10,100,100, name="graphic1")
graphic2 = GraphicObject(110,110,200,200, name="graphic2")
graphic1.draw(self.canvas)
graphic2.draw(self.canvas)
class GraphicObject(object):
def __init__(self, x0,y0,x1,y1, name=None):
self.coords = (x0,y0,x1,y1)
self.name = name
def draw(self, canvas, outline="black", fill="white"):
item = canvas.create_oval(self.coords, outline=outline, fill=fill)
canvas.tag_bind(item, "<1>", self.mouse_click)
def mouse_click(self, event):
print "I got a mouse click (%s)" % self.name
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()

Categories