Draw line: retrieve the covered pixels - pyqt

I want to draw a line on a widget:
from PyQt4 import QtGui, QtCore
class LineLabel(QtGui.QLabel):
def __init__(self,parent=None):
super(LineLabel,self).__init__(parent)
self.setMinimumSize(100,100)
self.setMaximumSize(100,100)
def paintEvent(self,e):
painter=QtGui.QPainter(self)
pen = QtGui.QPen()
pen.setWidth(5)
painter.setPen(pen)
painter.drawLine(10,10,90,90)
painter.end()
def test():
form = QtGui.QWidget()
label = LineLabel(form)
form.show()
return form
import sys
app = QtGui.QApplication(sys.argv)
window =test()
sys.exit(app.exec_())
What is the best way to get a list of the pixels that are covered by the line?
Update from the comments:
I don't need to know the pixels directly in between the start and end point but all those pixels that are changed to black (which are more pixels because the line has a certain width).
My overall goal is a fast way to know which pixels on the widget are black. Iterating over the pixels of the image and querying the color is much slower than reading the color value from a list in which the colors are stored: For me 1.9 seconds for an image with 1 million pixels to 0.23 seconds for a list with 1 million entries. Therefore I must update that list after every change of the image on the widget such as by drawing a line.
Answers that refer to a QGraphicsItem in a QGraphicsScene are also helpful.

You may use a linear equation to find the point you want in the line. I think that there is no reference to a draw line.
from PyQt4 import QtGui
from PyQt4.QtGui import QColor, QPaintEvent
m_nInitialX = 0.0
m_nInitialY = 0.0
# my line abstraction
class MyLine:
x1, y1, x2, y2 = .0, .0, .0, .0
width = .0
px, py = (.0, .0)
draw_point = False
def __init__(self, x1, y1, x2, y2, width):
self.x1, self.y1, self.x2, self.y2 = (x1, y1, x2, y2)
self.width = width
def is_in_line(self, x, y):
# mark a position in the line
m = (self.y2 - self.y1) / (self.x2 - self.x1)
print(m*(x-self.x1)-(y-self.y1))
if abs((m*(x-self.x1) - (y-self.y1))) <= self.width/2:
self.draw_point = True
return True
else:
return False
def add_red_point(self, x, y):
self.px, self.py = (x, y)
def draw(self, widget):
painter = QtGui.QPainter(widget)
pen = QtGui.QPen()
pen.setWidth(self.width)
painter.setPen(pen)
painter.drawLine(self.x1, self.y1, self.y2, self.y2)
if self.draw_point:
pen.setColor(QColor(255, 0, 0))
painter.setPen(pen)
painter.drawPoint(self.px, self.py)
painter.end()
line = MyLine(10, 10, 90, 90, width=10) # <-- my line abstraction
class LineLabel(QtGui.QLabel):
def __init__(self, parent=None):
super(LineLabel, self).__init__(parent)
self.setMinimumSize(100, 100)
self.setMaximumSize(100, 100)
# always redraw when needed
def paintEvent(self, e):
print("draw!")
line.draw(self)
def mousePressEvent(self, event):
# mark clicked position in line
m_nInitialX = event.pos().x()
m_nInitialY = event.pos().y()
if line.is_in_line(m_nInitialX, m_nInitialY):
line.add_red_point(m_nInitialX, m_nInitialY)
self.repaint()
def test():
form = QtGui.QWidget()
label = LineLabel(form)
form.show()
return form
import sys
app = QtGui.QApplication(sys.argv)
window = test()
sys.exit(app.exec_())

Related

Tile based lighting system 2d

I am looking for a tile based lighting system for my tile based game. I have not tried anything because I can't think of an effective way to do this. I have searched stack overflow and I found this but its not what I want. I am making a 2d version of Minecraft with pygame.
here is my tile class
class tile():
def __init__(self, block_category, block_type, x, y, world, win):
self.x, self.y, self.width, self.height = (x*64), (y*64), 64, 64
self.block_type = block_type
self.light_level = 1 # i want light level to range from 0-1
self._image = None
self.world = world
self.win = win
self.posx, self.posy = x, y
try:
self._image = self.world.block_textures[block_category][block_type]
except:
self._image = self.world.block_textures["missing"]["missing_texture"]
self.image = self._image
def draw(self):
#draw code here self.posx, self.win, self.world and self.posy are used here if you are wondering
def change_block(self, block_category, block_type):
try:
self._image = self.world.block_textures[block_category][block_type]
except:
self._image = self.world.block_textures["missing"]["missing_texture"]
self.image = self._image
and my world data looks like this
def generate_world(self):
for x in range(0, self.width):
self.tiles[x] = {}
for y in range(0, self.height):
self.tiles[x][y] = tile("terrain", "air", x, y, self, self.win)
for x in range(0, self.width):
for y in range(0, self.height):
if y == 0:
self.tiles[x][y].change_block("terrain", "bedrock")
elif y == 38:
self.tiles[x][y].change_block("terrain", "grass_block")
elif y < 38 and y > 34:
self.tiles[x][y].change_block("terrain", "dirt")
elif y < 35 and y > 0:
self.tiles[x][y].change_block("terrain", "stone")
if x == 0 or x == self.height - 1:
self.tiles[x][y].change_block("terrain", "bedrock")
return self.tiles
my game looks like this
For 2D games like you're making, how we could apply lighting - more like, shadowing - could go into 2 options:
Change screen color to shadow color & set transparency to objects, as OP suggested
Sandwich entire thing between screen and light layer
Let's start with problem of 1st option:
Problem of setting transparency
Here's demo code based on your idea:
"""
Demonstration of color overlapping
"""
import pygame as pg
class Player(pg.sprite.Sprite):
def __init__(self):
super(Player, self).__init__()
self.image = pg.Surface((50, 50))
self.image.fill((255, 255, 255))
self.rect = self.image.get_rect()
# setting alpha on player
self.image.set_alpha(125)
def update(self, *args, **kwargs):
x, y = pg.mouse.get_pos()
c_x, c_y = self.rect.center
self.rect.move_ip(x - c_x, y - c_y)
def mainloop():
player = Player()
screen = pg.display.set_mode((500, 500))
circle_colors = (255, 0, 0), (0, 255, 0), (0, 0, 255)
circle_coords = (150, 250), (250, 250), (350, 250)
# make surface, set alpha then draw circle
bg_surfaces = []
for (color, center) in zip(circle_colors, circle_coords):
surface = pg.Surface((500, 500), pg.SRCALPHA, 32)
surface.convert_alpha()
surface.set_alpha(125)
pg.draw.circle(surface, color, center, 75)
bg_surfaces.append(surface)
running = True
while running:
screen.fill((0, 0, 0))
# draw background
for surface in bg_surfaces:
screen.blit(surface, surface.get_rect())
for event in pg.event.get():
if event.type == pg.QUIT:
running = False
player.update()
screen.blit(player.image, player.rect)
pg.display.flip()
if __name__ == '__main__':
pg.init()
mainloop()
pg.quit()
As you see, now the player (White square)'s color is Mixed with background circles.
It's basically just like what the drawing program does with layers.
Set layer transparency 50% and stack - everything mixes, producing undesirable effect which is far from lighting effect you wanted.
Unless you want Creeper or Steve to blend with the background and become a ghosty figure, it's better to go for sandwiched layout.
Sandwiched Layout
Following is demo code which uses mouse position as light source position.
Rendering order is Ground > Player > light overlay(shadow)
Demo code:
"""
Code demonstration for https://stackoverflow.com/q/72610504/10909029
Written on Python 3.10 (Using Match on input / event dispatching)
"""
import math
import random
import itertools
from typing import Dict, Tuple, Sequence
import pygame as pg
class Position:
"""Namespace for size and positions"""
tile_x = 20
tile_size = tile_x, tile_x
class SpriteGroup:
"""Namespace for sprite groups, with chain iterator keeping the order"""
ground = pg.sprite.Group()
entities = pg.sprite.Group()
light_overlay = pg.sprite.Group()
#classmethod
def all_sprites(cls):
return itertools.chain(cls.ground, cls.entities, cls.light_overlay)
class Player(pg.sprite.Sprite):
"""Player class, which is merely a rect following pointer in this example."""
def __init__(self):
super(Player, self).__init__()
self.image = pg.Surface((50, 50))
self.image.fill((255, 255, 255))
self.rect = self.image.get_rect()
SpriteGroup.entities.add(self)
self.rect.move_ip(225, 225)
def update(self, *args, **kwargs):
pass
# Intentionally disabling mouse following code
# x, y = pg.mouse.get_pos()
# c_x, c_y = self.rect.center
# self.rect.move_ip(x - c_x, y - c_y)
class TileLightOverlay(pg.sprite.Sprite):
"""
Light overlay tile. Using separate sprites, so we don't have to blit on
every object above ground that requires lighting.
"""
# light lowest boundary
lighting_lo = 255
# light effect radius
light_radius = Position.tile_x * 8
def __init__(self, x, y):
super(TileLightOverlay, self).__init__()
self.image = pg.Surface(Position.tile_size)
self.image.fill((0, 0, 0))
self.rect = self.image.get_rect()
self.rect.move_ip(x * Position.tile_x, y * Position.tile_x)
SpriteGroup.light_overlay.add(self)
def update(self, *args, **kwargs):
self.image.set_alpha(self.brightness)
#property
def brightness(self):
"""Calculate distance between mouse & apply light falloff accordingly"""
distance = math.dist(self.rect.center, pg.mouse.get_pos())
if distance > self.light_radius:
return self.lighting_lo
return (distance / self.light_radius) * self.lighting_lo
class TileGround(pg.sprite.Sprite):
"""Ground tile representation. Not much is going on here."""
def __init__(self, x, y, tile_color: Sequence[float]):
super(TileGround, self).__init__()
self.image = pg.Surface(Position.tile_size)
self.image.fill(tile_color)
self.rect = self.image.get_rect()
self.rect.move_ip(x * Position.tile_x, y * Position.tile_x)
SpriteGroup.ground.add(self)
# create and keep its pair light overlay tile.
self.light_tile = TileLightOverlay(x, y)
class World:
"""World storing ground tile data."""
# tile type storing color etc. for this example only have color.
tile_type: Dict[int, Tuple[float, float, float]] = {
0: (56, 135, 93),
1: (36, 135, 38),
2: (135, 128, 56)
}
def __init__(self):
# coord system : +x → / +y ↓
# generating random tile data
self.tile_data = [
[random.randint(0, 2) for _ in range(25)]
for _ in range(25)
]
# generated tiles
self.tiles = []
def generate(self):
"""Generate world tiles"""
for x, row in enumerate(self.tile_data):
tiles_row = [TileGround(x, y, self.tile_type[col]) for y, col in enumerate(row)]
self.tiles.append(tiles_row)
def process_input(event: pg.event.Event):
"""Process input, in case you need it"""
match event.key:
case pg.K_ESCAPE:
pg.event.post(pg.event.Event(pg.QUIT))
case pg.K_UP:
pass
# etc..
def display_fps_closure(screen: pg.Surface, clock: pg.time.Clock):
"""FPS display"""
font_name = pg.font.get_default_font()
font = pg.font.Font(font_name, 10)
color = (0, 255, 0)
def inner():
text = font.render(f"{int(clock.get_fps())} fps", True, color)
screen.blit(text, text.get_rect())
return inner
def mainloop():
# keeping reference of method/functions to reduce access overhead
fetch_events = pg.event.get
display = pg.display
# local variable setup
screen = display.set_mode((500, 500))
player = Player()
world = World()
world.generate()
clock = pg.time.Clock()
display_fps = display_fps_closure(screen, clock)
running = True
# main loop
while running:
screen.fill((0, 0, 0))
# process event
for event in fetch_events():
# event dispatch
match event.type:
case pg.QUIT:
running = False
case pg.KEYDOWN:
process_input(event)
# draw in ground > entities > light overlay order
for sprite in SpriteGroup.all_sprites():
sprite.update()
screen.blit(sprite.image, sprite.rect)
# draw fps - not related to question, was lazy to remove & looks fancy
clock.tick()
display_fps()
display.flip()
if __name__ == '__main__':
pg.init()
pg.font.init()
mainloop()
pg.quit()
You'll see it's blending properly with shadow without mixing color with ground tiles.
There could be much better approach or ways to implement this - as I never used pygame before, there would be bunch of good/better stuffs I didn't read on document.
But one thing for sure - always approach your goal with mindset that everything is related to your problem until you reach the goal! Comment you thought it wasn't going to be helpful gave me idea for this design.
One option is a black background, then I use set_alpha() to set how light or dark the tile is (how much the black background is seen through the tile) and no overlay is needed. Thanks to #jupiterbjy's original answer for inspiration.

Plot text in 3d-plot that does not scale or move

Hello Pyqtgraph community,
I want to be able to create a "fixed" text window in a 3D interactive plot generated in PyQtGraph.
This text window will contain simulation-related information and should be visible at all times, regardless if you zoom in/out or pan to the left or right; and the location of the window should not change.
So far all the solutions I have found, create a text object that moves as the scaling of the axes changes. For example, the code below prints text on 3D axis, but once you zoom in/out the text moves all over the place. Any ideas would be greatly appreciated.
Thanks in advance
from pyqtgraph.Qt import QtCore, QtGui
import pyqtgraph.opengl as gl
from pyqtgraph.opengl.GLGraphicsItem import GLGraphicsItem
class GLTextItem(GLGraphicsItem):
"""
Class for plotting text on a GLWidget
"""
def __init__(self, X=None, Y=None, Z=None, text=None):
GLGraphicsItem.__init__(self)
self.setGLOptions('translucent')
self.text = text
self.X = X
self.Y = Y
self.Z = Z
def setGLViewWidget(self, GLViewWidget):
self.GLViewWidget = GLViewWidget
def setText(self, text):
self.text = text
self.update()
def setX(self, X):
self.X = X
self.update()
def setY(self, Y):
self.Y = Y
self.update()
def setZ(self, Z):
self.Z = Z
self.update()
def paint(self):
self.GLViewWidget.qglColor(QtCore.Qt.white)
self.GLViewWidget.renderText(self.X, self.Y, self.Z, self.text)
if __name__ == '__main__':
# Create app
app = QtGui.QApplication([])
w1 = gl.GLViewWidget()
w1.resize(800, 800)
w1.show()
w1.setWindowTitle('Earth 3D')
gl_txt = GLTextItem(10, 10, 10, 'Sample test')
gl_txt.setGLViewWidget(w1)
w1.addItem(gl_txt)
while w1.isVisible():
app.processEvents()
So I was finally able to find a solution. What needs to be done is the following:
Subclass the GLViewWidget
From the derived class, overload the paintGL() so that it uses the member function renderText() to render text on the screen every time the paingGL() is called.
renderText() is overloaded to support both absolute screen coordinates, as well as axis-based coordinates:
i) renderText(int x, int y, const QString &str, const QFont &font = QFont()): plot based on (x, y) window coordinates
ii) renderText(double x, double y, double z, const QString &str, const QFont &font = QFont()): plot on (x, y, z) scene coordinates
You might want to use the QtGui.QFontMetrics() class to get the dimensions of the rendered text so you can place it in a location that makes sense for your application, as indicated in the code below.
from pyqtgraph.opengl import GLViewWidget
import pyqtgraph.opengl as gl
from PyQt5.QtGui import QColor
from pyqtgraph.Qt import QtCore, QtGui
class GLView(GLViewWidget):
"""
I have implemented my own GLViewWidget
"""
def __init__(self, parent=None):
super().__init__(parent)
def paintGL(self, *args, **kwds):
# Call parent's paintGL()
GLViewWidget.paintGL(self, *args, **kwds)
# select font
font = QtGui.QFont()
font.setFamily("Tahoma")
font.setPixelSize(21)
font.setBold(True)
title_str = 'Screen Coordinates'
metrics = QtGui.QFontMetrics(font)
m = metrics.boundingRect(title_str)
width = m.width()
height = m.height()
# Get window dimensions to center text
scrn_sz_width = self.size().width()
scrn_sz_height = self.size().height()
# Render text with screen based coordinates
self.qglColor(QColor(255,255,0,255))
self.renderText((scrn_sz_width-width)/2, height+5, title_str, font)
# Render text using Axis-based coordinates
self.qglColor(QColor(255, 0, 0, 255))
self.renderText(0, 0, 0, 'Axis-Based Coordinates')
if __name__ == '__main__':
# Create app
app = QtGui.QApplication([])
w = GLView()
w.resize(800, 800)
w.show()
w.setWindowTitle('Earth 3D')
w.setCameraPosition(distance=20)
g = gl.GLGridItem()
w.addItem(g)
while w.isVisible():
app.processEvents()

How to use matplotlib blitting to add matplot.patches to an matplotlib plot in wxPython?

I am making a plot using the matplotlib library and showing it in my wxPython GUI. I am plotting a massive amount of data points from a LIDAR instrument. The thing is, I would like to draw rectangles in this plot to indicate interesting areas. But when I draw a rectangle on the same axes as the plot, the whole plot gets replotted which takes lots of time. This is because of the self.canvas.draw(), a function which replots everything.
The code gets displayed as follows in the GUI:
Printscreen of GUI
Here is a minimal working example of the problem. U can draw rectangles by holding the right mouse button. Once you plot the NetCDF data using the button on the left, the drawing of rectangles gets really slow. I tried some things with blitting using the examples provided by ImportanceOfBeingErnest but after a lot of tries, I still have not managed to get it to work.
To make the minimal working example work, you will have to specify the path to the NetCDF file under the plot_Data() function. I provided the NetCDF file which to download here:
Download NetCDF file
How can I blit the self.square to the self.canvas in the onselect function?
import netCDF4 as nc
import matplotlib
matplotlib.use('WXAgg')
from matplotlib.figure import Figure
from matplotlib.backends.backend_wxagg import FigureCanvasWxAgg as FigureCanvas
import matplotlib.pyplot as plt
import matplotlib.colors as colors
import matplotlib.widgets
import time
import wx
class rightPanel(wx.Panel):
def __init__(self, parent):
wx.Panel.__init__(self, parent, style=wx.SUNKEN_BORDER)
self.initiate_Matplotlib_Plot_Canvas()
self.add_Matplotlib_Widgets()
def initiate_Matplotlib_Plot_Canvas(self):
self.figure = Figure()
self.axes = self.figure.add_subplot(111)
self.colorbar = None
self.canvas = FigureCanvas(self, -1, self.figure)
self.sizer = wx.BoxSizer(wx.VERTICAL)
self.sizer.Add(self.canvas, proportion=1, flag=wx.ALL | wx.GROW)
self.SetSizer(self.sizer)
self.Fit()
self.canvas.draw()
def add_Matplotlib_Widgets(self):
self.rectangleSelector = matplotlib.widgets.RectangleSelector(self.axes, self.onselect,
drawtype="box", useblit=True,
button=[3], interactive=False
)
def onselect(self, eclick, erelease):
tstart = time.time()
x1, y1 = eclick.xdata, eclick.ydata
x2, y2 = erelease.xdata, erelease.ydata
height = y2-y1
width = x2-x1
self.square = matplotlib.patches.Rectangle((x1,y1), width,
height, angle=0.0, edgecolor='red',
fill=False
#blit=True gives Unknown property blit
)
self.axes.add_patch(self.square)
self.canvas.draw()
# =============================================================================
# self.background = self.canvas.copy_from_bbox(self.axes.bbox)
#
#
# self.canvas.restore_region(self.background)
#
# self.axes.draw_artist(self.square)
#
# self.canvas.blit(self.axes.bbox)
# =============================================================================
tend = time.time()
print("Took " + str(tend-tstart) + " sec")
def plot_Data(self):
"""This function gets called by the leftPanel onUpdatePlot. This updates
the plot to the set variables from the widgets"""
path = "C:\\Users\\TEST_DATA\\cesar_uvlidar_backscatter_la1_t30s_v1.0_20100501.nc"
nc_data = self.NetCDF_READ(path)
print("plotting......")
vmin_value = 10**2
vmax_value = 10**-5
combo_value = nc_data['perp_beta']
self.axes.clear()
plot_object = self.axes.pcolormesh(combo_value.T, cmap='rainbow',
norm=colors.LogNorm(vmin=vmin_value, vmax=vmax_value))
self.axes.set_title("Insert title here")
if self.colorbar is None:
self.colorbar = self.figure.colorbar(plot_object)
else:
self.colorbar.update_normal(plot_object)
self.colorbar.update_normal(plot_object)
print('canvas draw..............')
self.canvas.draw()
print("plotting succesfull")
###############################################################################
###############################################################################
"""BELOW HERE IS JUST DATA MANAGEMENT AND FRAME/PANEL INIT"""
###############################################################################
###############################################################################
def NetCDF_READ(self, path):
in_nc = nc.Dataset(path)
list_of_keys = in_nc.variables.keys()
nc_data = {} #Create an empty dictionary to store NetCDF variables
for item in list_of_keys:
variable_shape = in_nc.variables[item].shape
variable_dimensions = len(variable_shape)
if variable_dimensions > 1:
nc_data[item] = in_nc.variables[item][...] #Adding netCDF variables to dictonary
return nc_data
class leftPanel(wx.Panel):
def __init__(self, parent, mainPanel):
wx.Panel.__init__(self, parent)
button = wx.Button(self, -1, label="PRESS TO PLOT")
button.Bind(wx.EVT_BUTTON, self.onButton)
self.mainPanel = mainPanel
def onButton(self, event):
self.mainPanel.rightPanel.plot_Data()
class MainPanel(wx.Panel):
def __init__(self, parent):
"""Initializing the mainPanel. This class is called by the frame."""
wx.Panel.__init__(self, parent)
self.SetBackgroundColour('red')
"""Acquire the width and height of the monitor"""
width, height = wx.GetDisplaySize()
"""Split mainpanel into two sections"""
self.vSplitter = wx.SplitterWindow(self, size=(width,(height-100)))
self.leftPanel = leftPanel(self.vSplitter, self)
self.rightPanel = rightPanel(self.vSplitter)
self.vSplitter.SplitVertically(self.leftPanel, self.rightPanel,102)
class UV_Lidar(wx.Frame):
"""Uppermost class. This class contains everything and calls everything.
It is the container around the mainClass, which on its turn is the container around
the leftPanel class and the rightPanel class. This class generates the menubar, menu items,
toolbar and toolbar items"""
def __init__(self, parent, id):
print("UV-lidar> Initializing GUI...")
wx.Frame.__init__(self, parent, id, 'UV-lidar application')
self.Bind(wx.EVT_CLOSE, self.OnCloseWindow)
self.mainPanel = MainPanel(self)
def OnCloseWindow(self, event):
self.Destroy()
if __name__ == '__main__':
app = wx.App()
frame = UV_Lidar(parent=None, id=-1)
frame.Show()
print("UV-lidar> ")
print("UV-lidar> Initializing GUI OK")
app.MainLoop()
I have found the solution myself:
In order to blit a matplotlib patch, you will have to first add the patch to the axes. Then draw the patch on the axes and then you can blit the patch to the canvas.
square = matplotlib.patches.Rectangle((x1,y1), width,
height, angle=0.0, edgecolor='red',
fill=False)
self.axes.add_patch(square)
self.axes.draw_artist(square)
self.canvas.blit(self.axes.bbox)
If you do not want to use self.canvas.draw but still use matplotlib widgets which have useblit=True, you can save the plot as a background image: self.background = self.canvas.copy_from_bbox(self.axes.bbox) and restore it later by using: self.canvas.restore_region(self.background). This is a lot faster than drawing everything over!
When using the matplotlib's RectangleSelector widget with useblit=True, it will create another background instance variable, which interferes with your own background instance variable. To fix this problem, you will have to set the background instance variable of the RectangleSelector widget to be equal to your own background instance variable. However, this should only be done after the RectangleSelector widget is no longer active. Otherwise it will save some of the drawing animation to the background. So once the RectangleSelector has become inactive, you can update its background using: self.rectangleSelector.background = self.background
The code that had to be edited is given below. wx.CallLater(0, lambda: self.tbd(square)) is used so that the background instance variable of the RectangleSelector widget is updated only when it has become inactive.
def add_Matplotlib_Widgets(self):
"""Calling these instances creates another self.background in memory. Because the widget classes
restores their self-made background after the widget closes it interferes with the restoring of
our leftPanel self.background. In order to compesate for this problem, all background instances
should be equal to eachother. They are made equal in the update_All_Background_Instances(self)
function"""
"""Creating a widget that serves as the selector to draw a square on the plot"""
self.rectangleSelector = matplotlib.widgets.RectangleSelector(self.axes, self.onselect,
drawtype="box", useblit=True,
button=[3], interactive=False
)
def onselect(self, eclick, erelease):
self.tstart = time.time()
x1, y1 = eclick.xdata, eclick.ydata
x2, y2 = erelease.xdata, erelease.ydata
height = y2-y1
width = x2-x1
square = matplotlib.patches.Rectangle((x1,y1), width,
height, angle=0.0, edgecolor='red',
fill=False
#blit=True gives Unknown property blit
)
"""In order to keep the right background and not save any rectangle drawing animations
on the background, the RectangleSelector widget has to be closed first before saving
or restoring the background"""
wx.CallLater(0, lambda: self.tbd(square))
def tbd(self, square):
"""leftPanel background is restored"""
self.canvas.restore_region(self.background)
self.axes.add_patch(square)
self.axes.draw_artist(square)
self.canvas.blit(self.axes.bbox)
"""leftPanel background is updated"""
self.background = self.canvas.copy_from_bbox(self.axes.bbox)
"""Setting all backgrounds equal to the leftPanel self.background"""
self.update_All_Background_Instances()
print('Took '+ str(time.time()-self.tstart) + ' s')
def update_All_Background_Instances(self):
"""This function sets all of the background instance variables equal
to the lefPanel self.background instance variable"""
self.rectangleSelector.background = self.background

Get clicked chess piece from an SVG chessboard

I am developing a chess GUI in Python 3.6.3 using PyQt5 5.9.1 (GUI framework) and python-chess 0.21.1 (chess library) on Windows 10. I want to get the value of a piece that was clicked on an SVG chessboard (provided by python-chess) so that I can then move that piece to another square.
After the first left mouse click and getting the piece, I want to get the second left mouse click from the user and get the square that the user clicked on. Then my chess GUI must move the piece from originating square to the target square.
So, here's my complete working code so far. Any hints or actual code additions are very welcome.
import chess
import chess.svg
from PyQt5.QtSvg import QSvgWidget
from PyQt5.QtCore import pyqtSlot, Qt
from PyQt5.QtWidgets import QApplication, QWidget
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Chess Titan")
self.setGeometry(300, 300, 800, 800)
self.widgetSvg = QSvgWidget(parent=self)
self.widgetSvg.setGeometry(10, 10, 600, 600)
self.chessboard = chess.Board()
#pyqtSlot(QWidget)
def mousePressEvent(self, event):
if event.buttons() == Qt.LeftButton:
## How to get the clicked SVG chess piece?
# Envoke the paint event.
self.update()
#pyqtSlot(QWidget)
def paintEvent(self, event):
self.chessboardSvg = chess.svg.board(self.chessboard).encode("UTF-8")
self.widgetSvg.load(self.chessboardSvg)
if __name__ == "__main__":
chessTitan = QApplication([])
window = MainWindow()
window.show()
chessTitan.exec()
If size of chessboard is known, you can find the coordinates of the mouseclick from event.pos() resp.event.x(), event.y() depending on marginwidth and squaresize, see chess.svg.py line 129 ff.
edit Nov 25: event.pos() is in this example in MainWindow coordinates, to find the coordinates on chessboard all must be calculated from top left corner represented by self.svgX and self.svgY:
import chess
import chess.svg
from PyQt5.QtCore import pyqtSlot, Qt
from PyQt5.QtSvg import QSvgWidget
from PyQt5.QtWidgets import QApplication, QWidget
class MainWindow(QWidget):
def __init__(self):
super().__init__()
self.setWindowTitle("Chess Titan")
self.setGeometry(300, 300, 800, 800)
self.widgetSvg = QSvgWidget(parent=self)
self.svgX = 50 # top left x-pos of chessboard
self.svgY = 50 # top left y-pos of chessboard
self.cbSize = 600 # size of chessboard
self.widgetSvg.setGeometry(self.svgX,self.svgY, self.cbSize, self.cbSize)
self.coordinates = True
# see chess.svg.py line 129
self.margin = 0.05*self.cbSize if self.coordinates == True else 0
self.squareSize = (self.cbSize - 2 * self.margin) / 8.0
self.chessboard = chess.Board()
self.pieceToMove = [None, None]
#pyqtSlot(QWidget)
def mousePressEvent(self, event):
if self.svgX < event.x() <= self.svgX + self.cbSize and self.svgY < event.y() <= self.svgY + self.cbSize: # mouse on chessboard
if event.buttons() == Qt.LeftButton:
# if the click is on chessBoard only
if self.svgX + self.margin < event.x() < self.svgX + self.cbSize - self.margin and self.svgY + self.margin < event.y() < self.svgY + self.cbSize - self.margin:
file = int((event.x() - (self.svgX + self.margin))/self.squareSize)
rank = 7 - int((event.y() - (self.svgY + self.margin))/self.squareSize)
square = chess.square(file, rank) # chess.sqare.mirror() if white is on top
piece = self.chessboard.piece_at(square)
coordinates = '{}{}'.format(chr(file + 97), str(rank +1))
if self.pieceToMove[0] is not None:
move = chess.Move.from_uci('{}{}'.format(self.pieceToMove[1], coordinates))
self.chessboard.push(move)
print(self.chessboard.fen())
piece = None
coordinates= None
self.pieceToMove = [piece, coordinates]
else:
print('coordinates clicked')
# Envoke the paint event.
self.update()
else:
QWidget.mousePressEvent(self, event)
#pyqtSlot(QWidget)
def paintEvent(self, event):
self.chessboardSvg = chess.svg.board(self.chessboard, size = self.cbSize, coordinates = self.coordinates).encode("UTF-8")
self.widgetSvg.load(self.chessboardSvg)
if __name__ == "__main__":
chessTitan = QApplication([])
window = MainWindow()
window.show()
chessTitan.exec()
move white and black pieces alternating, they change the color if the same color is moved twice.
Below is the Python, PyQt5 and python-chess code for a fully functional chess GUI that has legal move detection built in, so chess piece movement behaves according to the rules of chess.
#! /usr/bin/env python
"""
This module is the execution point of the chess GUI application.
"""
import sys
import chess
from PyQt5.QtCore import pyqtSlot, Qt
from PyQt5.QtSvg import QSvgWidget
from PyQt5.QtWidgets import QApplication, QWidget
class MainWindow(QWidget):
"""
Create a surface for the chessboard.
"""
def __init__(self):
"""
Initialize the chessboard.
"""
super().__init__()
self.setWindowTitle("Chess GUI")
self.setGeometry(300, 300, 800, 800)
self.widgetSvg = QSvgWidget(parent=self)
self.widgetSvg.setGeometry(10, 10, 600, 600)
self.boardSize = min(self.widgetSvg.width(),
self.widgetSvg.height())
self.coordinates = True
self.margin = 0.05 * self.boardSize if self.coordinates else 0
self.squareSize = (self.boardSize - 2 * self.margin) / 8.0
self.pieceToMove = [None, None]
self.board = chess.Board()
self.drawBoard()
#pyqtSlot(QWidget)
def mousePressEvent(self, event):
"""
Handle left mouse clicks and enable moving chess pieces by
clicking on a chess piece and then the target square.
Moves must be made according to the rules of chess because
illegal moves are suppressed.
"""
if event.x() <= self.boardSize and event.y() <= self.boardSize:
if event.buttons() == Qt.LeftButton:
if self.margin < event.x() < self.boardSize - self.margin and self.margin < event.y() < self.boardSize - self.margin:
file = int((event.x() - self.margin) / self.squareSize)
rank = 7 - int((event.y() - self.margin) / self.squareSize)
square = chess.square(file, rank)
piece = self.board.piece_at(square)
coordinates = "{}{}".format(chr(file + 97), str(rank + 1))
if self.pieceToMove[0] is not None:
move = chess.Move.from_uci("{}{}".format(self.pieceToMove[1], coordinates))
if move in self.board.legal_moves:
self.board.push(move)
piece = None
coordinates = None
self.pieceToMove = [piece, coordinates]
self.drawBoard()
def drawBoard(self):
"""
Draw a chessboard with the starting position and then redraw
it for every new move.
"""
self.boardSvg = self.board._repr_svg_().encode("UTF-8")
self.drawBoardSvg = self.widgetSvg.load(self.boardSvg)
return self.drawBoardSvg
if __name__ == "__main__":
chessGui = QApplication(sys.argv)
window = MainWindow()
window.show()
sys.exit(chessGui.exec_())
a_manthey_67 and Boštjan Mejak, I've combined features from both of your solutions:
https://github.com/vtad4f/chess-ui/blob/master/board.py
The full version integrates AI player(s) with your board UI:
Run make to build https://github.com/vtad4f/chess-ai/
Run main.py to play a game https://github.com/vtad4f/chess-ui/

Tkinter - How to move image from canvas in slow motion

guys. I am trying to create my own version of a card game. I got the following problem trying to move my cards to the center of the canvas on click event. Here is an example of my code
import tkinter as tk
class gui(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.canvas = tk.Canvas(parent, bg="blue", highlightthickness=0)
self.canvas.pack(fill="both", expand=True)
self.img = PhotoImage(file="card.gif")
self.card = self.canvas.create_image(10, 10, image=self.img)
self.canvas.tag_bind(self.card, '<Button-1>', self.onObjectClick1)
def onObjectClick1(self, event):
if self.canvas.find_withtag("current"):
x = 400
y = 400
self.canvas.coords("current", x, y)
self.canvas.tag_raise("current")
if __name__ == "__main__":
root = tk.Tk()
w, h = root.winfo_screenwidth(), root.winfo_screenheight()
root.geometry("%dx%d+0+0" % (w, h))
gui(root)
root.mainloop()
What I want is to move my card but not just move from one coordinate to another but giving it in slow motion effect.
The basic idea is to write a function that moves an object a small amount, and then schedules itself to be called again after a short delay. It does this until it reaches its destination.
Here is a very simple example that moves a couple items independently. You can adjust the speed by changing the speed parameter, or by changing the values of delta_x and delta_y.
This is a very simplistic algorithm that just increases the x and y coordinates by a fixed amount. You could instead calculate equally spaced points along a curve or straight line. Regardless, the animation technique remains the same.
import Tkinter as tk
def move_object(canvas, object_id, destination, speed=50):
dest_x, dest_y = destination
coords = canvas.coords(object_id)
current_x = coords[0]
current_y = coords[1]
new_x, new_y = current_x, current_y
delta_x = delta_y = 0
if current_x < dest_x:
delta_x = 1
elif current_x > dest_x:
delta_x = -1
if current_y < dest_y:
delta_y = 1
elif current_y > dest_y:
delta_y = -1
if (delta_x, delta_y) != (0, 0):
canvas.move(object_id, delta_x, delta_y)
if (new_x, new_y) != (dest_x, dest_y):
canvas.after(speed, move_object, canvas, object_id, destination, speed)
root = tk.Tk()
canvas = tk.Canvas(root, width=400, height=400)
canvas.pack()
item1 = canvas.create_rectangle(10, 10, 30, 30, fill="red")
item2 = canvas.create_rectangle(360, 10, 380, 30, fill="green")
move_object(canvas, item1, (200, 180), 25)
move_object(canvas, item2, (200, 220), 50)
root.mainloop()
In order to 'animate' your cards moving, a system of breaking down the total distance to be moved, and then moving/updating by smaller distances over a time-period would work.
For example, if you wish to move a card 400 units in x & y, something like this could work:
total_time = 500 #Time in milliseconds
period = 8
dx = 400/period
dy = 400/period
for i in range(period):
self.canvas.move(chosen_card, dx, dy)
root.after(total_time/period) #Pause for time, creating animation effect
root.update() #Update position of card on canvas
This could be a basic premise for an animation. Of course you would need to edit the total_time and period variables in my example to create what you feel is right.
This code below (ready for copy/paste and run as it is) gives a nice smooth motion on my box:
import tkinter as tk
import time
class gui(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.canvas = tk.Canvas(parent, bg="blue", highlightthickness=0)
self.canvas.pack(fill="both", expand=True)
self.img = tk.PhotoImage(file="card.gif")
self.card = self.canvas.create_image(10, 10, image=self.img)
self.canvas.tag_bind(self.card, '<Button-1>', self.onObjectClick1)
def onObjectClick1(self, event):
if self.canvas.find_withtag("current"):
x = 400
y = 400
self.canvas.coords("current", x, y)
self.canvas.tag_raise("current")
total_time = 500 #Time in milliseconds
period = 400
dx = 400/period
dy = 400/period
for i in range(period):
self.canvas.move(self.card, dx, dy) # chosen_card
time.sleep(0.01)
# root.after(total_time/period) #Pause for time, creating animation effect
root.update() #Update position of card on canvas
if __name__ == "__main__":
root = tk.Tk()
w, h = root.winfo_screenwidth(), root.winfo_screenheight()
root.geometry("%dx%d+0+0" % (w, h))
gui(root)
root.mainloop()

Resources