OpenGL error 1286 when window is minimised - python-3.x

Some information about what I'm using:
Ancient Potato integrated GPU (Intel(R) HD Graphics Family)
Windows 7
OpenGL <=3.1
Python 3.7.0
Error that I get instantly the moment I simply minimise the window:
$ python main.py
Traceback (most recent call last):
File "main.py", line 71, in <module>
main()
File "main.py", line 60, in main
renderer.render(mesh)
File "...\myproject\renderer.py", line 22, in render
glDrawElements(GL_TRIANGLES, mesh.indices, GL_UNSIGNED_INT, ctypes.c_void_p(0))
File "...\OpenGL\latebind.py", line 41, in __call__
return self._finalCall( *args, **named )
File "...\OpenGL\wrapper.py", line 854, in wrapperCall
raise err
File "...\OpenGL\wrapper.py", line 847, in wrapperCall
result = wrappedOperation( *cArguments )
File "...\OpenGL\error.py", line 232, in glCheckError
baseOperation = baseOperation,
OpenGL.error.GLError: GLError(
err = 1286,
baseOperation = glDrawElements,
pyArgs = (
GL_TRIANGLES,
6,
GL_UNSIGNED_INT,
c_void_p(None),
),
cArgs = (
GL_TRIANGLES,
6,
GL_UNSIGNED_INT,
c_void_p(None),
),
cArguments = (
GL_TRIANGLES,
6,
GL_UNSIGNED_INT,
c_void_p(None),
)
)
When I googled OpenGL errorcode 1286 I found that this happens in OpenGL context when something is wrong with Framebuffer. That really doesn't tell anything to me...
# renderer.py
class Renderer:
def __init__(self, colour=(0.0, 0.0, 0.0)):
self.colour = colour
#property
def colour(self):
return self._colour
#colour.setter
def colour(self, new_colour):
glClearColor(*new_colour, 1.0)
self._colour = new_colour
def render(self, mesh):
glBindVertexArray(mesh.vao_id)
glBindTexture(GL_TEXTURE_2D, mesh.texture)
glDrawElements(GL_TRIANGLES, mesh.indices, GL_UNSIGNED_INT, ctypes.c_void_p(0))
def clear(self):
glClear(GL_COLOR_BUFFER_BIT)
As I am using Framebuffers, I COULD have done something wrong, but I got everything to work the way I wanted it to (render to texture, then use the texture as source for rendering on quad and also as source for texture that will be rendered next frame, basically using GPU to manipulate grids of arbitrary data), I do it by swapping FBO's instead of swapping textures if it's unclear from the code:
# bufferedtexture.py (The place where I use Frame Buffer)
class BufferedTexture:
def __init__(self, width, height):
self._width = width
self._height = height
self._textures = glGenTextures(2)
self._buffers = glGenFramebuffers(2)
self._previous_buffer = 0
self._current_buffer = 1
self.init_buffer(0)
self.init_buffer(1)
#property
def width(self):
return self._width
#property
def height(self):
return self._height
#property
def buffer(self):
return self._buffers[self._current_buffer]
#property
def texture(self):
return self._textures[self._previous_buffer]
def init_buffer(self, index):
glBindFramebuffer(GL_FRAMEBUFFER, self._buffers[index])
glBindTexture(GL_TEXTURE_2D, self._textures[index])
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST)
glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, self.width, self.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, ctypes.c_void_p(0))
glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, self._textures[index], 0)
def set_texture_data(self, image_data):
glBindTexture(GL_TEXTURE_2D, self.texture)
glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, self.width, self.height, 0, GL_RGBA, GL_UNSIGNED_BYTE, image_data)
def swap_buffers(self):
self._previous_buffer = self._current_buffer
self._current_buffer = (self._current_buffer + 1) % 2
def enable(self):
glBindFramebuffer(GL_FRAMEBUFFER, self.buffer)
def disable(self):
glBindFramebuffer(GL_FRAMEBUFFER, 0)
def destroy(self):
glDeleteFramebuffers(self._buffers)
glDeleteTextures(self._textures)
And I use everything like this:
# main loop
while not window.should_close: # glfwWindowShouldClose(self.hwnd)
shader.enable() # glUseProgram(self._program)
buff.enable() # BufferedTexture object, source above
renderer.clear() # Renderer object, source above
renderer.render(mesh) # By the way, mesh is just a Quad, nothing fancy
buff.disable() # Tells driver that we will be drawing to screen again
buff.swap_buffers()
mesh.texture = buff.texture # give quad the texture that was rendered off-screen
renderer.clear()
renderer.render(mesh)
window.swap_buffers() # glfwSwapBuffers(self.hwnd)
window.poll_events() # glfwPollEvents()
I don't even know what could be wrong, again, this only happens when I minimise the window, otherwise I can leave it to run for hours and it's fine, but the moment I minimise it dies...
I even tried to add
assert(glCheckFramebufferStatus(GL_FRAMEBUFFER) == GL_FRAMEBUFFER_COMPLETE)
assert(glGetError() == GL_NO_ERROR)
at the end of BufferedTexture.init_buffer to quickly check whether it's a problem with FBO itself, but...
$ python main.py
<no assertion errors to be found>
<same error once I minimise>
TL;DR
Everything renders properly as intended;
haven't noticed any problems performance wise, no errors are being
thrown or swallowed while I initialize glfw and openGL stuff, I
raise RuntimeError myself when PyOpenGL would just be all fine with something going wrong (for some reason), without ever catching it;
Program crashes with OpenGL: 1286 when I minimise the window, losing focus does nothing, only when I minimise it...
Send help.
EDIT:
mesh = Mesh(indices, vertices, uvs)
buff = BufferedTexture(800, 600)
with Image.open("cat.jpg") as image:
w, h = image.size # the image is 800x600
img_data = np.asarray(image.convert("RGBA"), np.uint8)
buff.set_texture_data(img_data[::-1])
buff.swap_buffers()
buff.set_texture_data(img_data[::-1])
mesh.texture = buff.texture # this is just GL_TEXTURE_2D object ID
buff.disable()
while not window.should_close:
shader.enable()
#buff.enable()
#renderer.clear()
#renderer.render(mesh)
#buff.disable()
#buff.swap_buffers()
#mesh.texture = buff.texture
renderer.clear()
renderer.render(mesh)
window.swap_buffers()
window.poll_events()
Once I stop using buffers completely, it works as intended. So there's something wrong with my code, I hope.

Someone pointed out (but deleted their answers/comments for whatever reason), that window size becomes 0, 0 when minimized, and that was indeed the case, to prevent crashing and waste of resources when window is minimized, I did this:
Create and register a callback for window resize, this was very easy as I'm using only a single window and I already had it set up as a singleton, the callback just tells window whether it should sleep or not.
def callback(window, width, height):
Window.instance.sleeping = width == 0 or height == 0
(obviously) registered the callback for my own window:
glfwSetFramebufferSizeCallback(self.handle, callback)
I don't do anything besides polling events when window is "sleeping":
while not window.should_close:
window.poll_events()
if window.sleeping:
time.sleep(0.01)
continue
shader.enable()
buff.enable()
renderer.clear()
renderer.render(mesh)
buff.disable()
buff.swap_buffers()
mesh.texture = buff.texture
renderer.clear()
renderer.render(mesh)
window.swap_buffers()

Related

A QGraphicsPixmapItem with a transparency gradient

I would like to apply a transparency gradient on a QGraphicsPixmapItem but I don't know how to go about it, for now I can only apply the transparency on the whole QGraphicsPixmapItem (not the gradient). The item undergoes a mirror effect (like an element on a wall which is reflected on the ground), then I would like to set up this gradient of transparency (from top to bottom; quite opaque at the top ... and in several stages of transparencies, it ends up being completely transparent at the bottom of the item), then I add blur. This is the part of the transparency gradient that I can't seem to do... I don't understand how it can work. Everything I've tried doesn't work. Can you help me ?
Thanks in advance.
Here is a fairly short code:
for item in self.scene.selectedItems() :
# Width
item_height = item.boundingRect().height()
# mirroir
item.setTransform(QTransform(1, 0, 0, 0, -1, 0, 0, 0, 1))
#
item.setOpacity(0.7)
# Blur
blur = QGraphicsBlurEffect()
blur.setBlurRadius(8)
item.setGraphicsEffect(blur)
item.setPos(100, 100 + 2 * (int(item_height)))
##############################################
"""
alphaGradient = QLinearGradient(item.boundingRect().topLeft(), item.boundingRect().bottomLeft())
alphaGradient.setColorAt(0.0, Qt.transparent)
alphaGradient.setColorAt(0.5, Qt.black)
alphaGradient.setColorAt(1.0, Qt.transparent)
effect = QGraphicsOpacityEffect()
effect.setOpacityMask(alphaGradient)
item.setGraphicsEffect(effect) ########
"""
##############################################
"""
opacity = QGraphicsOpacityEffect()
lg = QLinearGradient()
lg.setStart(0, 0)
lg.setFinalStop(0, 100)
lg.setColorAt(0, Qt.transparent)
lg.setColorAt(0.5, Qt.transparent)
lg.setColorAt(1.0, Qt.transparent)
opacity.setOpacityMask(lg)
#opacity.setOpacity(1)
item.setGraphicsEffect(opacity)
"""
##############################################
Only one single QGraphicsEffect can be applied for each target item (or widget), so you have two options:
create a new image that applies the opacity gradient (using QPainter composition modes) and only set the blur effect;
set the opacity effect on a parent item;
In both cases, I'd suggest you to create the "mirror" item as a child of the actual QGraphicsPixmapItem, which will ensure that it will always follow what happens to the parent, including visibility and geometry changes, transformations, etc.
Generate the mirror image with a transparency gradient
For this, we use the composition features of QPainter, and specifically the CompositionMode_SourceIn which allows to use a gradient as source for the alpha channel of the image that is being drawn.
The procedure is the following:
create a new image based on the size of the mirrored one;
create a gradient that has full opacity at 0 and is transparent at 1 (or less);
draw that gradient on the image;
set the composition mode;
draw the mirrored image;
create a QGraphicsPixmapItem based on that image;
class MirrorPixmapItem(QtWidgets.QGraphicsPixmapItem):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.mirror = QtWidgets.QGraphicsPixmapItem(self)
self.blurEffect = QtWidgets.QGraphicsBlurEffect(blurRadius=8)
self.mirror.setGraphicsEffect(self.blurEffect)
if not self.pixmap().isNull():
self.createMirror()
def setPixmap(self, pixmap):
old = self.pixmap()
super().setPixmap(pixmap)
if old == pixmap:
return
self.createMirror()
def setOffset(self, *args):
super().setOffset(*args)
if not self.mirror.pixmap().isNull():
self.mirror.setOffset(self.offset())
def createMirror(self):
source = self.pixmap()
if source.isNull():
self.mirror.setPixmap(source)
return
scale = .5
height = int(source.height() * scale)
output = QtGui.QPixmap(source.width(), height)
output.fill(QtCore.Qt.transparent)
qp = QtGui.QPainter(output)
grad = QtGui.QLinearGradient(0, 0, 0, height)
grad.setColorAt(0, QtCore.Qt.black)
grad.setColorAt(1, QtCore.Qt.transparent)
qp.fillRect(output.rect(), grad)
qp.setCompositionMode(qp.CompositionMode_SourceIn)
qp.drawPixmap(0, 0,
source.transformed(QtGui.QTransform().scale(1, -scale)))
qp.end()
self.mirror.setPixmap(output)
self.mirror.setY(self.boundingRect().bottom())
self.mirror.setOffset(self.offset())
Set the opacity on the parent item
In this case, we still have the blur effect on the mirror item, but we apply a QGraphicsOpacityEffect on the parent (the original image) and use the opacityMask that includes both the original image and the mirror, will be at full opacity down to the height of the original, and then begins to fade out to full transparency for the mirror.
class MirrorPixmapItem2(QtWidgets.QGraphicsPixmapItem):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.opacity = QtWidgets.QGraphicsOpacityEffect(opacity=1)
self.setGraphicsEffect(self.opacity)
self.mirror = QtWidgets.QGraphicsPixmapItem(self)
self.blurEffect = QtWidgets.QGraphicsBlurEffect(blurRadius=8)
self.mirror.setGraphicsEffect(self.blurEffect)
if not self.pixmap().isNull():
self.createMirror()
# ... override setPixmap() and setOffset() as above
def createMirror(self):
source = self.pixmap()
if source.isNull():
self.mirror.setPixmap(source)
return
scale = .5
self.mirror.setPixmap(
source.transformed(QtGui.QTransform().scale(1, -scale)))
self.mirror.setPos(self.boundingRect().bottomLeft())
self.mirror.setOffset(self.offset())
height = source.height()
gradHeight = height + height * scale
grad = QtGui.QLinearGradient(0, 0, 0, gradHeight)
grad.setColorAt(height / gradHeight, QtCore.Qt.black)
grad.setColorAt(1, QtCore.Qt.transparent)
self.opacity.setOpacityMask(grad)
This is the result, as you can see there's no visible difference, which approach to use is a matter of choice, but the first one might be slightly better from the perspective of performance, as no additional effect is required when the items are drawn, which might be important if you are going to have many (as in hundreds or thousands) image items and/or at high resolutions.

Is noLoop stopping execution of draw?

This is my first post here, so I apologize if I'm making any mistakes.
I recently started to study Processing in Python mode and I'm trying to develop a code that, after selecting an image from your computer, reads the colors and inserts them in a list. The final idea is to calculate the percentage of certain colors in the image. For this I am using the following code:
img = None
tam=5
cores_img = []
def setup():
size (500, 500)
selectInput(u"Escolha a ilustração para leitura de cores", "adicionar_imagens")
noLoop()
def adicionar_imagens(selection):
global img
if selection == None:
print(u"Seleção cancelada")
else:
print(u"Você selecionou " + selection.getAbsolutePath())
img = loadImage(selection.getAbsolutePath())
def draw():
if img is not None:
image (img, 0, 0)
for xg in range(0, img.width, tam):
x = map(xg, 0, img.width, 0, img.width)
for yg in range(0, img.height, tam):
y = map(yg, 0, img.height, 0, img.height)
cor = img.get(int(x), int(y))
cores_img.append(cor)
print (cores_img)
I'm using noLoop() so that the colors are added only once to the list. However, it seems that the draw is not running. It performs the setup actions, but when the image is selected, nothing happens. There is also no error message.
I'm completely lost about what might be happening. If anyone has any ideas and can help, I really appreciate it!
Calling noLoop() indeed stops the draw() loop from running, which means by the time you've selected and image nothing yould happen.
You can however manually call draw() (or redraw()) once the image is loaded:
img = None
tam=5
cores_img = []
def setup():
size (500, 500)
selectInput(u"Escolha a ilustração para leitura de cores", "adicionar_imagens")
noLoop()
def adicionar_imagens(selection):
global img
if selection == None:
print(u"Seleção cancelada")
else:
print(u"Você selecionou " + selection.getAbsolutePath())
img = loadImage(selection.getAbsolutePath())
redraw()
def draw():
if img is not None:
image (img, 0, 0)
for xg in range(0, img.width, tam):
x = map(xg, 0, img.width, 0, img.width)
for yg in range(0, img.height, tam):
y = map(yg, 0, img.height, 0, img.height)
cor = img.get(int(x), int(y))
cores_img.append(cor)
print (cores_img)
You should pay attention to a few details:
As the reference mentions, calling get() is slow: pixels[x + y * width] is faster (just remember to call loadPixels() if the array doesn't look right)
PImage already has a pixels array: calling img.resize(img.width / tam, img .height / tam) should downsample the image so you can read the same list
x = map(xg, 0, img.width, 0, img.width) (and similarly y) maps from one range to the same range which has no effect
e.g.
img = None
tam=5
cores_img = None
def setup():
size (500, 500)
selectInput(u"Escolha a ilustração para leitura de cores", "adicionar_imagens")
noLoop()
def adicionar_imagens(selection):
global img, cores_img
if selection == None:
print(u"Seleção cancelada")
else:
print(u"Você selecionou " + selection.getAbsolutePath())
img = loadImage(selection.getAbsolutePath())
print("total pixels",len(img.pixels))
img.resize(img.width / tam, img.height / tam);
cores_img = list(img.pixels)
print("resized pixels",len(img.pixels))
print(cores_img)
def draw():
pass
Update
I thought that calling noLoop on setup would make draw run once. Still
it won't print the image... I'm calling 'image (img, 0, 0)' at the end
of 'else', on 'def adicionar_imagens (selection)'. Should I call it
somewhere else?
think of adicionar_imagens time-wise, running separate to setup() and draw()
you are right, draw() should be called once (because of noLoop()), however it's called as soon as setup() completes but not later (as navigating the file system, selecting a file and confirming takes time)
draw() would need to be forced to run again after the image was loaded
Here's an updated snippet:
img = None
# optional: potentially useful for debugging
img_resized = None
tam=5
cores_img = None
def setup():
size (500, 500)
selectInput(u"Escolha a ilustração para leitura de cores", "adicionar_imagens")
noLoop()
def adicionar_imagens(selection):
global img, img_resized, cores_img
if selection == None:
print(u"Seleção cancelada")
else:
print(u"Você selecionou " + selection.getAbsolutePath())
img = loadImage(selection.getAbsolutePath())
# make a copy of the original image (to keep it intact)
img_resized = img.get()
# resize
img_resized.resize(img.width / tam, img.height / tam)
# convert pixels array to python list
cores_img = list(img.pixels)
# force redraw
redraw()
# print data
print("total pixels",len(img.pixels))
print("resized pixels",len(img.pixels))
# print(cores_img)
def draw():
print("draw called " + str(millis()) + "ms after sketch started")
# if an img was selected and loaded, display it
if(img != None):
image(img, 0, 0)
# optionally display resized image
if(img_resized != None):
image(img_resized, 0, 0)
Here are a couple of notes that may be helpful:
each pixel in the list is a 24 bit ARGB colour (e.g. all channels are stored in a single value). if you need individual colour channels remember you have functions like red(), green(), blue() available. (Also if that gets slow notice the example include faster versions using bit shifting and masking)
the Histogram example could be helpful. You would need to port from Java to Python syntax and use 3 histograms (one for each colour channel), however the principle of counting intensities is nicely illustrated

Animating a gif W/O using PIL

I am currently trying to make life easier by having the code pull all the frames into my code and execute the animation. My current code is:
import time
from tkinter import *
import os
root = Tk()
imagelist = []
for file in os.listdir("MY-DIRECTORY"):
if file.endswith(".gif"):
imagelist.append(PhotoImage(file=str(os.path.join("MY-DIRECTORY", file))))
# extract width and height info
photo = PhotoImage(file=imagelist[0])
width = photo.width()
height = photo.height()
canvas = Canvas(width=width, height=height)
canvas.pack()
# create a list of image objects
giflist = []
for imagefile in imagelist:
photo = PhotoImage(file=imagefile)
giflist.append(photo)
# loop through the gif image objects for a while
for k in range(0, 1000):
for gif in giflist:
canvas.create_image(width / 2.0, height / 2.0, image=gif)
canvas.update()
time.sleep(0.1)
root.mainloop()
When I try to execute the file it gives me this error which I cannot make sense of.
Traceback (most recent call last):
File "C:/Users/Profile/Desktop/folder (2.22.2019)/animation2.py", line 21, in <module>
photo = PhotoImage(file=imagelist[0])
File "C:\Users\Profile\AppData\Local\Programs\Python\Python37-32\lib\tkinter\__init__.py", line 3545, in __init__
Image.__init__(self, 'photo', name, cnf, master, **kw)
File "C:\Users\Profile\AppData\Local\Programs\Python\Python37-32\lib\tkinter\__init__.py", line 3501, in __init__
self.tk.call(('image', 'create', imgtype, name,) + options)
_tkinter.TclError: couldn't open "pyimage1": no such file or directory
Note: I am adapting this code to fit my needs I did not write it.
I ran some print statements to verify the pyimage was being uploaded to the array but I cannot figure out why its saying there is no such file or directory if its already uploaded to the array. Can you all please shine some light.
I found that I was creating an unnecessary array of objects lower in the code. giflist[]. I ultimately resolved the issue by removing it and having the loop use the array that was created earlier in the code imagelist. The following code works now.
import time
from tkinter import *
import os
root = Tk()
imagelist = []
for file in os.listdir("My-Directory"):
if file.endswith(".gif"):
imagelist.append(PhotoImage(file=str(os.path.join("My-Directory", file))))
# Extract width and height info
photo = PhotoImage(file="My-Directory")
width = photo.width()
height = photo.height()
canvas = Canvas(width=width, height=height)
canvas.pack()
# Loop through the gif image objects for a while
for k in range(0, len(imagelist)):
for gif in imagelist:
canvas.create_image(width / 2.0, height / 2.0, image=gif)
canvas.update()
time.sleep(0.1)
root.mainloop()

Cryptic error Closing Toplevel window Python 3 Canvas

No idea what the comments are on this. But, never mind, I fixed it myself.
Took a while to figure out that Python is really sloppy and leaves timers running when you destroy the process completely. Then a while later, you get a blow up when it comes out of the timer and has nowhere to go. That's a very basic thing. You clean up after yourself in a destroy operation. Python does not.
I am able to open and close my toplevel window while keeping the main window going but I get a large, cryptic error message when I close the child window. This happens both when I close it via the X or the defined button in the child window. If I comment out the timer event (self.ater), the error messages do not come up. But I have to have that to update the form.
The error message is coming up on the next timer pop. It is not destroyed and pops but there is nowhere to go any longer.
I can't make the error message show up exactly right. Did the best I could.
Here is the error message block, then the code to create and close the child window.
Exception in Tkinter callback
Traceback (most recent call last):
File "/usr/lib/python3.4/tkinter/init.py", line 1536, in call
return self.func(*args)
File "/usr/lib/python3.4/tkinter/init.py", line 585, in callit
func(*args)
File "/home/pi/IltiShares/#Mikes Files/#Python3 Source/GPS Data Show 11-02-2016.py", line 630, in UpdateSky
t_canvas.itemconfig(zz, text=str(self.counter))
File "/usr/lib/python3.4/tkinter/init.py", line 2419, in itemconfigure
return self._configure(('itemconfigure', tagOrId), cnf, kw)
File "/usr/lib/python3.4/tkinter/init.py", line 1313, in _configure
self.tk.call(_flatten((self._w, cmd)) + self._options(cnf))
_tkinter.TclError: invalid command name ".1959370864.1959118928.1959118192.1959118800"
def ShowSky(self):
global t_canvas
global zz
fontsize = 8
labelwidth = 15
textwidth = 17
self.counter += 1
t = Toplevel(self)
t.wm_title("Sky Map or SV's")
frame2 = Frame(t)
frame2.config(height = 400, width = 400) # These are pixels
frame2.config(relief = RIDGE)
frame2.pack(side = LEFT, anchor= 'ne')
t_canvas = Canvas(frame2, height = 300, width = 300, borderwidth = 2,
bg= '#b3ffff', relief = GROOVE)
t_canvas.pack()
self.waiting = StringVar()
self.waiting.set('.')
zz = t_canvas.create_text(50, 50, text = '01')
Button(frame2, text='Close', width = 10, bg = '#FFc0c0', command=t.destroy).pack()
self.after(1000, self.UpdateSky)
pass
def UpdateSky(self):
global t_canvas
global zz
self.counter += 1
# Test movement and text change.
t_canvas.itemconfig(zz, text=str(self.counter))
t_canvas.coords(zz,self.counter,60)
self.after(8000, self.UpdateSky)
pass

_tkinter.TclError:unknown option "23" Code works with few object, but not with many

To start off I am super new at using Tkinter,
The problem I am having is that my code works if I have only one of the object type. It will interact correctly if it is the only one of that tag type. So if I have one 'boat' and 100 'shells' each time it executes, it does so correctly.
The code detects if there is collision between two objects and then changes the currently selected item's color to a random. So as long as there is only one tag type currently it will work correctly. So if I click and drag the 'boat' into a 'shell' it will switch it's color. Then if I take 1 of the 100 'shell's and do the same I get this error.
I do not understand why it works correctly when there is only one object of a given type and to interacts a infinite amount of other objects but when there is more than one of a tag type it fails.
It correctly selects the id number for the selected object so I am just lost right now and appreciate any help.
Follows is the error I receive and the code I am using. It is just the vital parts needed to preform the needed task. The collision code is the same as in the code though.
Exception in Tkinter callback
Traceback (most recent call last):
File "C:\Python34\lib\tkinter\__init__.py", line 1487, in __call__
return self.func(*args)
File "H:/Charles Engen/PycharmProjects/Battleship/BattleShipGUI.py", line 112, in on_token_motion
self.collision_detection(event)
File "H:/Charles Engen/PycharmProjects/Battleship/BattleShipGUI.py", line 85, in collision_detection
self.canvas.itemconfig(current_token, outline=_random_color())
File "C:\Python34\lib\tkinter\__init__.py", line 2385, in itemconfigure
return self._configure(('itemconfigure', tagOrId), cnf, kw)
File "C:\Python34\lib\tkinter\__init__.py", line 1259, in _configure
self.tk.call(_flatten((self._w, cmd)) + self._options(cnf))
_tkinter.TclError: unknown option "22"
import tkinter as tk
from random import randint
HEIGHT = 400
WIDTH = 680
def _random_color():
'''Creates a random color sequence when called'''
random_color = ("#"+("%06x" % randint(0, 16777215)))
return random_color
class Board(tk.Tk):
'''Creates a Board Class'''
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.menu_item = tk.Menu(self)
self.file_menu = tk.Menu(self.menu_item, tearoff=0)
self.file_menu.add_command(label='New', command=self.new_game)
self.file_menu.add_command(label='Exit', command=self.quit)
self.config(menu=self.file_menu)
self.canvas = tk.Canvas(width=WIDTH, height=HEIGHT)
self.canvas.pack(fill='both', expand=True)
# adds variable that keeps track of location
self._token_location_data = {"x": 0, "y": 0, "item": None}
def _create_object(self, coord, fcolor, color, token_name):
'''Creates an object with a tag, each object is assigned the ability to be clicked and dragged'''
(x, y) = coord
if token_name == 'boat':
points = [10+x, 10+y, 20+x, 20+y, 110+x, 20+y, 120+x, 10+y, 80+x,
10+y, 80+x, 0+y, 60+x, 0+y, 60+x, 10+y]
self.canvas.create_polygon(points, outline=fcolor, fill=color, width=3, tag=token_name)
elif token_name == 'shell':
self.canvas.create_oval(0+x, 0+y, 10+x, 10+y, outline=fcolor, fill=color, width=3, tag=token_name)
self.canvas.tag_bind(token_name, '<ButtonPress-1>', self.on_token_button_press)
self.canvas.tag_bind(token_name, '<ButtonRelease-1>', self.on_token_button_press)
self.canvas.tag_bind(token_name, '<B1-Motion>', self.on_token_motion)
def collision_detection(self, event):
'''This function tracks any collision between the boat and shell objects'''
# I will upgrade this to take any object collision
token = self.canvas.gettags('current')[0]
current_token = self.canvas.find_withtag(token)
x1, y1, x2, y2 = self.canvas.bbox(token)
overlap = self.canvas.find_overlapping(x1, y1, x2, y2)
for item in current_token:
for over in overlap:
if over != item:
# Changes the color of the object that is colliding.
self.canvas.itemconfig(current_token, outline=_random_color())
# The following three functions are required to just move the tokens
def on_token_button_press(self, event):
'''Adds ability to pick up tokens'''
# Stores token item's location data
self._token_location_data['item'] = self.canvas.find_closest(event.x, event.y)[0]
self._token_location_data['x'] = event.x
self._token_location_data['y'] = event.y
def on_token_button_release(self, event):
'''Adds ability to drop token'''
# Resets the drag
self._token_location_data['item'] = self.canvas.find_closest(event.x, event.y)
self._token_location_data['x'] = event.x
self._token_location_data['y'] = event.y
def on_token_motion(self, event):
'''Adds ability to keep track of location of tokens as you drag them'''
# Computes how much the object has moved
delta_x = event.x - self._token_location_data['x']
delta_y = event.y - self._token_location_data['y']
# move the object the appropriate amount
self.canvas.move(self._token_location_data['item'], delta_x, delta_y)
# record the new position
self._token_location_data['x'] = event.x
self._token_location_data['y'] = event.y
# Detects collision between objects
self.collision_detection(event)
def new_game(self):
'''Creates new game by deleting the current canvas and creating new objects'''
# Deletes current canvas
self.canvas.delete('all')
# runs the create board mechanism
self._generate_board()
# adds code to create a shell and boat on the screen from the appropriate function
for i in range(1):
self._create_object((410, 15*i), _random_color(), _random_color(), 'boat')
for i in range(4):
for j in range(10):
self._create_object((590+(i*10), 15*j), _random_color(), _random_color(), 'shell')
def main():
app = Board()
app.mainloop()
if __name__ == '__main__':
main()
To run the code, I removed the call to the non-existent ._generate_board. After this, I get the same error but with unknown option '3'.
The exception is caused by passing to self.canvas.itemconfig a tuple of ids, which you misleadingly call current_token, instead of a tag or id. A singleton is tolerated because of the _flatten call, but anything more becomes an error. I am rather sure that it is not a coincidence that '3' is the second member of the shell tuple. Passing token instead stops the exception. Also, there should be a break after itemconfig is called the first time.
With this, however, the shells are treated as a group, and the bounding box incloses all shells and overlap includes all shells. This is why moving a single shell away from the others is seen as a collision. At this point, all shells are randomized to a single new color if one is moved. To fix this, token should be set to the single item set in on_token_button_press, instead of a tag group. This implements your todo note. Here is the result.
def collision_detection(self, event):
'''Detect collision between selected object and others.'''
token = self._token_location_data['item']
x1, y1, x2, y2 = self.canvas.bbox(token)
overlap = self.canvas.find_overlapping(x1, y1, x2, y2)
for over in overlap:
if over != token:
# Changes the color of the object that is colliding.
self.canvas.itemconfig(token, outline=_random_color())
break
A minor problem is that you do the tag binds for 'shell' for each shell (in _create_object. Instead, put the tag binds in new so they are done once.
for tag in 'boat', 'shell':
self.canvas.tag_bind(tag, '<ButtonPress-1>', self.on_token_button_press)
self.canvas.tag_bind(tag, '<ButtonRelease-1>', self.on_token_button_press)
self.canvas.tag_bind(tag, '<B1-Motion>', self.on_token_motion)

Resources