I've updated the initial script with a modified version of Bryan-Oakley's answer. It now has 2 canvas, 1 with the draggable rectangle, and 1 with the plot. I would like the rectangle to be dragged along the x-axis on the plot if that is possible?
import tkinter as tk # python 3
# import Tkinter as tk # python 2
import numpy as np
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg, NavigationToolbar2Tk)
# Implement the default Matplotlib key bindings.
from matplotlib.backend_bases import key_press_handler
from matplotlib.figure import Figure
class Example(tk.Frame):
"""Illustrate how to drag items on a Tkinter canvas"""
def __init__(self, parent):
tk.Frame.__init__(self, parent)
fig = Figure(figsize=(5, 4), dpi=100)
t = np.arange(0, 3, .01)
fig.add_subplot(111).plot(t, 2 * np.sin(2 * np.pi * t))
#create a canvas
self.canvas = tk.Canvas(width=200, height=300)
self.canvas.pack(fill="both", expand=True)
canvas = FigureCanvasTkAgg(fig, master=root) # A tk.DrawingArea.
canvas.draw()
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
# this data is used to keep track of an
# item being dragged
self._drag_data = {"x": 0, "y": 0, "item": None}
# create a movable object
self.create_token(100, 150, "black")
# add bindings for clicking, dragging and releasing over
# any object with the "token" tag
self.canvas.tag_bind("token", "<ButtonPress-1>", self.drag_start)
self.canvas.tag_bind("token", "<ButtonRelease-1>", self.drag_stop)
self.canvas.tag_bind("token", "<B1-Motion>", self.drag)
def create_token(self, x, y, color):
"""Create a token at the given coordinate in the given color"""
self.canvas.create_rectangle(
x - 5,
y - 100,
x + 5,
y + 100,
outline=color,
fill=color,
tags=("token",),
)
def drag_start(self, event):
"""Begining drag of an object"""
# record the item and its location
self._drag_data["item"] = self.canvas.find_closest(event.x, event.y)[0]
self._drag_data["x"] = event.x
self._drag_data["y"] = event.y
def drag_stop(self, event):
"""End drag of an object"""
# reset the drag information
self._drag_data["item"] = None
self._drag_data["x"] = 0
self._drag_data["y"] = 0
def drag(self, event):
"""Handle dragging of an object"""
# compute how much the mouse has moved
delta_x = event.x - self._drag_data["x"]
delta_y = 0
# move the object the appropriate amount
self.canvas.move(self._drag_data["item"], delta_x, delta_y)
# record the new position
self._drag_data["x"] = event.x
self._drag_data["y"] = event.y
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
Any help is greatly appreciated.
The issue with your code is that you create two canvases, one for the matplotlib figure and one for the draggable rectangle while you want both on the same.
To solve this, I merged the current code of the question with the one before the edit, so the whole matplotlib figure is now embedded in the Tkinter window. The key modification I made to the DraggableLine class is that it now takes the canvas as an argument.
import tkinter as tk # python 3
# import Tkinter as tk # python 2
import numpy as np
import matplotlib.lines as lines
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg, NavigationToolbar2Tk)
from matplotlib.figure import Figure
class DraggableLine:
def __init__(self, ax, canvas, XorY):
self.ax = ax
self.c = canvas
self.XorY = XorY
x = [XorY, XorY]
y = [-2, 2]
self.line = lines.Line2D(x, y, color='red', picker=5)
self.ax.add_line(self.line)
self.c.draw_idle()
self.sid = self.c.mpl_connect('pick_event', self.clickonline)
def clickonline(self, event):
if event.artist == self.line:
self.follower = self.c.mpl_connect("motion_notify_event", self.followmouse)
self.releaser = self.c.mpl_connect("button_press_event", self.releaseonclick)
def followmouse(self, event):
self.line.set_xdata([event.xdata, event.xdata])
self.c.draw_idle()
def releaseonclick(self, event):
self.XorY = self.line.get_xdata()[0]
self.c.mpl_disconnect(self.releaser)
self.c.mpl_disconnect(self.follower)
class Example(tk.Frame):
"""Illustrate how to drag items on a Tkinter canvas"""
def __init__(self, parent):
tk.Frame.__init__(self, parent)
fig = Figure(figsize=(5, 4), dpi=100)
t = np.arange(0, 3, .01)
ax = fig.add_subplot(111)
ax.plot(t, 2 * np.sin(2 * np.pi * t))
# create the canvas
canvas = FigureCanvasTkAgg(fig, master=root) # A tk.DrawingArea.
canvas.draw()
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
self.line = DraggableLine(ax, canvas, 0.1)
if __name__ == "__main__":
root = tk.Tk()
Example(root).pack(fill="both", expand=True)
root.mainloop()
I'm not sure how to do it with tkinter or pyQt but I know how to make something like this with PyGame which is another GUI solution for python. I hope this example helps you:
import pygame
SCREEN_WIDTH = 430
SCREEN_HEIGHT = 410
WHITE = (255, 255, 255)
RED = (255, 0, 0)
FPS = 30
pygame.init()
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
rectangle = pygame.rect.Rect(176, 134, 17, 170)
rectangle_draging = False
clock = pygame.time.Clock()
running = True
while running:
for event in pygame.event.get():
if event.type == pygame.QUIT:
running = False
elif event.type == pygame.MOUSEBUTTONDOWN:
if event.button == 1:
if rectangle.collidepoint(event.pos):
rectangle_draging = True
mouse_x, mouse_y = event.pos
offset_x = rectangle.x - mouse_x
offset_y = rectangle.y - mouse_y
elif event.type == pygame.MOUSEBUTTONUP:
if event.button == 1:
rectangle_draging = False
print("Line is at: (", rectangle.x, ";", rectangle.y,")")
elif event.type == pygame.MOUSEMOTION:
if rectangle_draging:
mouse_x, mouse_y = event.pos
rectangle.x = mouse_x + offset_x
rectangle.y = mouse_y + offset_y
screen.fill(WHITE)
pygame.draw.rect(screen, RED, rectangle)
pygame.display.flip()
clock.tick(FPS)
pygame.quit()
And when you move the line around, the console prints the location that it is in.
Related
I have a solid-coloured ball moving in a tkinter canvas, using the move(), and update() method to refresh the canvas.
I want to keep track of the ball trajectory onscreen. i.e. - the ball moves to its new position but a line - series of points showing its trajectory - remains onscreen.
How do I do this? (two canvases, one of them transparent??)
Just as suggested, you can paint line that will start from the last position of the ball and end in the current new ball position.
I've made simple app doing so in tkinter, that you can modify to your needs.
It's using only one canvas and you can move ball just by clicking on the canvas. Right mouse click resets the canvas and ball position.
from tkinter import Tk, Canvas, Frame, BOTH
class MovingBall(Frame):
ball_r = 25
x, y = 0, 0 # ball last coords
def __init__(self):
super().__init__()
self.ball = None
self.initUI()
def initUI(self):
self.master.title("Moving Ball")
self.pack(fill=BOTH, expand=1)
self.canvas = Canvas(self)
self.ball = self.paint_ball(self.x, self.y)
self.canvas.pack(fill=BOTH, expand=1)
# Left click on canvas moves the ball
self.canvas.bind("<Button-1>", self.move)
# Right click on canvas reset ball position
self.canvas.bind("<Button-3>", self.reset)
def paint_ball(self, x, y):
return self.canvas.create_oval(x - self.ball_r, y - self.ball_r, x + self.ball_r, y + self.ball_r, fill="red",
outline="silver", width=1)
def paint_path(self, x, y):
return self.canvas.create_line(self.x, self.y, x, y, fill="silver", width=1)
def move(self, event):
# Remove last painted ball
self.canvas.delete(self.ball)
# Add new line to the path
self.paint_path(event.x, event.y)
# Paint new ball at new position
self.ball = self.paint_ball(event.x, event.y)
self.canvas.pack(fill=BOTH, expand=1)
# Store current ball coords
self.x, self.y = event.x, event.y
def reset(self, event):
"""Reset whole scene and put ball to 0,0"""
self.x, self.y = 0, 0
self.canvas.delete("all")
self.ball = self.paint_ball(self.x, self.y)
self.canvas.pack(fill=BOTH, expand=1)
def main():
root = Tk()
MovingBall()
root.geometry("1024x768")
root.mainloop()
if __name__ == '__main__':
main()
import time
from tkinter import *
import random
class SpaceField:
def __init__(self):
self.window = Tk()
self.window.title("Asteriods")
self.canvas = self.canvas_display() #creates canvas
self.asteriods = self.asteriod_creation_seperation() #creates asteroids
self.active = True
self.move_active() #Moves asteroids
self.canvas.update()
def asteriod_creation_seperation(self): #creation of multple asteriods
asteriod_spacingx = random.randint(1,800)
asteriod_spacingy = random.randint(1,800)
asteriod_list = list() # could list([])
for i in range(15):
asteriod = self.canvas.create_oval( 30, 50 , 80 , 100 , tags="asteriod", width=2, outline="white")
asteriod_list.append("asteriod")
self.canvas.move(asteriod, asteriod_spacingx, asteriod_spacingy)
asteriod_spacingx = random.randint(1,500)
asteriod_spacingy = random.randint(1,500)
print(asteriod_spacingy)
return asteriod_list
Asteroid Creation. Creates asteroids and gives them random positions.
def asteriod_update(self): #asteriods movement method #MAin problem
x12 = 1
self.canvas.move("asteriod", 3, x12)
pos = self.canvas.coords("asteriod")
print(pos)
if (pos)[2] > 500:
x12 *= 5
I think this is where I need to add the collision detection. I just have no idea how to combine the lists of the circles and the collisions.
def move_active(self): #Basically a true loop
if self.active:
self.asteriod_update()
self.window.after(40, self.move_active)
def canvas_display(self): #canvas
canvas = Canvas(self.window, width=500, height=400, background='black')
canvas.pack(expand=True, fill="both")
canvas.update()
return canvas
Canvas display nothing special
def run(self):
self.window.mainloop()
if __name__ == '__main__':
SpaceF = SpaceField()
SpaceF.run()
Asteroids is a classic game but there were a number of problems in your code. The main one was calling move_active during initialization. This prevented the code from completing its mainloop initialization.
The other problem was the asteroid_update method that basically didn't do anything, also using tags to control all asteroids didn't work either.
Everything else was OK, although you might consider using polygons.
Here is one way to produce a bouncing objects program. I've inserted remarks that describe the methods used.
Objects change the speed and direction when they hit the boundary so their trajectories are randomized.
from tkinter import *
from random import randint as rnd
class SpaceField:
def __init__(self):
self.window = Tk()
self.window.title("Asteriods")
# Define canvas size and active flag
self.wide, self.high, self.active = 500, 400, True
self.canvas_display()
self.asteriod_creation_seperation()
def asteriod_creation_seperation(self):
self.asteroids, self.speed = [], []
size, radius = 50, 25
for i in range(15):
spacex = rnd(size, self.wide - size)
spacey = rnd(size, self.high - size)
self.asteroids.append( # Store oval item id
self.canvas.create_oval(
spacex, spacey, spacex+size, spacey+size,
width=2, tags = "asteriod", outline = "white"))
self.speed.append((rnd(1,4),rnd(1,4))) # Store random speed x, y
def asteriod_update(self): # MAIN DRIVER: Work on ALL asteroids
for i, a in enumerate(self.asteroids):
xx, yy = self.speed[i] # get speed data
x, y, w, h = self.canvas.coords(a)
# check for boundary hit then change direction and speed
if x < 0 or w > self.wide:
xx = -xx * rnd(1, 4)
if y < 0 or h > self.high:
yy = -yy * rnd(1, 4)
# Limit max and min speed then store it
self.speed[i] = (max( -4, min( xx, 4)), max( -4, min( yy, 4 )))
self.canvas.move(a, xx, yy) # update asteroid position
def move_active(self):
if self.active:
self.asteriod_update()
self.window.after(40, self.move_active)
def canvas_display(self):
self.canvas = Canvas(
self.window, width = self.wide,
height = self.high, background = "black")
self.canvas.pack(expand = True, fill = "both")
def run(self): # Begin asteroids here so that mainloop is executed
self.window.after(200, self.move_active)
self.window.mainloop()
if __name__ == "__main__":
SpaceF = SpaceField()
SpaceF.run()
I'm tying to create a basic program for drawing to the screen by creating ovals every frame when the mouse is clicked. However, as the program runs for a bit, it starts becoming very choppy and the circles stop forming cohesive lines, due to the code not running fast enough to process precise mouse movement.
Here is my code -
import tkinter as tk
DRAW_HEIGHT = 560
DRAW_WIDTH = 560
PALETTE_HEIGHT = 40
def draw_palette(canvas):
canvas.create_rectangle(0, 0, DRAW_WIDTH, PALETTE_HEIGHT, fill = 'light grey', width= 0)
canvas.create_rectangle(DRAW_WIDTH/8, PALETTE_HEIGHT/5, 3*DRAW_WIDTH/8, 4*PALETTE_HEIGHT/5, fill = 'dark grey', width = 1)
canvas.create_rectangle(5*DRAW_WIDTH/8, PALETTE_HEIGHT/5, 7*DRAW_WIDTH/8, 4*PALETTE_HEIGHT/5, fill = 'dark grey',width = 1)
canvas.create_text(DRAW_WIDTH/4, PALETTE_HEIGHT/2, text = 'clear screen') #non-functional
class Brush():
def __init__(self,stroke_size,stroke_color):
self.size = stroke_size
self.color = stroke_color
self.mode = 'draw'
self.pos = (0,0)
self.clicked = False
def render(self,canvas):
if self.clicked:
canvas.create_oval( self.pos.x-self.size/2, self.pos.y-self.size/2,
self.pos.x+self.size/2, self.pos.y+self.size/2,
width = 0, fill = self.color )
def mouse_moved(self,event):
self.pos = event
def mouse_clicked(self,throwaway):
self.clicked = True
def mouse_released(self,throwaway):
self.clicked = False
#set up root window and canvas
root = tk.Tk()
root.geometry('{}x{}'.format(DRAW_WIDTH,DRAW_HEIGHT+PALETTE_HEIGHT))
c = tk.Canvas(root, width = DRAW_WIDTH, height = DRAW_HEIGHT + PALETTE_HEIGHT, bg = 'white')
c.pack()
b = Brush(40,'black')
#bind actions to functions
c.bind("<Button-1>",b.mouse_clicked)
c.bind("<ButtonRelease-1>",b.mouse_released)
c.bind("<Motion>",b.mouse_moved)
#main loop
while 1:
b.render(c)
draw_palette(c)
root.update()
I suppose I'm just asking if there's any way I can speed this up, but specifically I'm wondering if I can draw the shapes to the screen without using create_shape() every time.
For example,
oval = c.create_oval()
while 1:
canvas.draw(oval)
I know you can do something similar with canvas.move(), but I couldn't find anything that fit my situation.
I don't understand why you created loop while 1 and run render() and draw_palette() hundreds of times even if you don't need it.
I draw new circle in mouse_moved() and use root.mainloop() and it runs much better and create smoother line. Probably if I would draw line from previous place to current place or many ovals with some step then I would get even better line
EDIT: I changed little to draw first oval in mouse_click() - so I can see first oval even if I only click and don't move.
import tkinter as tk
# --- constanst ---
DRAW_HEIGHT = 560
DRAW_WIDTH = 560
PALETTE_HEIGHT = 40
# --- classes ---
class Brush():
def __init__(self,stroke_size,stroke_color):
self.size = stroke_size
self.color = stroke_color
self.mode = 'draw'
self.pos = (0,0)
self.clicked = False
def draw(self):
s = self.size/2
c.create_oval(
self.pos.x-s, self.pos.y-s,
self.pos.x+s, self.pos.y+s,
width=0, fill=self.color
)
def mouse_moved(self, event):
if self.clicked:
self.pos = event
self.draw()
def mouse_clicked(self, event):
self.clicked = True
self.pos = event
self.draw()
def mouse_released(self, event):
self.clicked = False
# --- functions ---
def draw_palette(canvas):
canvas.create_rectangle(0, 0, DRAW_WIDTH, PALETTE_HEIGHT, fill='light grey', width=0)
canvas.create_rectangle(DRAW_WIDTH/8, PALETTE_HEIGHT/5, 3*DRAW_WIDTH/8, 4*PALETTE_HEIGHT/5, fill='dark grey', width=1)
canvas.create_rectangle(5*DRAW_WIDTH/8, PALETTE_HEIGHT/5, 7*DRAW_WIDTH/8, 4*PALETTE_HEIGHT/5, fill='dark grey', width=1)
canvas.create_text(DRAW_WIDTH/4, PALETTE_HEIGHT/2, text='clear screen') #non-functional
# --- main ---
#set up root window and canvas
root = tk.Tk()
root.geometry('{}x{}'.format(DRAW_WIDTH, DRAW_HEIGHT+PALETTE_HEIGHT))
c = tk.Canvas(root, width=DRAW_WIDTH, height=DRAW_HEIGHT+PALETTE_HEIGHT, bg='white')
c.pack()
b = Brush(40, 'black')
#bind actions to functions
c.bind("<Button-1>", b.mouse_clicked)
c.bind("<ButtonRelease-1>", b.mouse_released)
c.bind("<Motion>", b.mouse_moved)
draw_palette(c)
root.mainloop()
EDIT:
I added function which adds ovals if distance between previous and current position is too big and there is gap. Now line is smooth even if mouse moves fast.
import tkinter as tk
# --- constanst ---
DRAW_HEIGHT = 560
DRAW_WIDTH = 560
PALETTE_HEIGHT = 40
# --- classes ---
class Brush():
def __init__(self,stroke_size,stroke_color):
self.size = stroke_size
self.color = stroke_color
self.mode = 'draw'
self.pos = None
self.prev = None
self.clicked = False
def draw_oval(self, x, y):
r = self.size/2 # radius
c.create_oval(x-r, y-r, x+r, y+r, width=0, fill=self.color)
def draw(self):
if self.pos:
self.draw_oval(self.pos.x, self.pos.y)
if self.prev:
# calculate distance between ovals
dx = self.pos.x - self.prev.x
dy = self.pos.y - self.prev.y
max_diff = max(abs(dx), abs(dy))
# add ovals if distance bigger then some size of oval (tested with //4, //8, //6, //5)
if max_diff > (self.size//6):
# how many ovals to add
parts = max_diff//(self.size//6)
# distance between ovals
step_x = dx/parts
step_y = dy/parts
# add ovals except first which is already on canvas
for i in range(1, parts):
x = self.pos.x - i*step_x
y = self.pos.y - i*step_y
self.draw_oval(x, y)
def mouse_moved(self, event):
if self.clicked:
self.prev = self.pos
self.pos = event
self.draw()
def mouse_clicked(self, event):
self.clicked = True
self.prev = None
self.pos = event
self.draw()
def mouse_released(self, event):
self.clicked = False
self.prev = None
self.pos = None
# --- functions ---
def draw_palette(canvas):
canvas.create_rectangle(0, 0, DRAW_WIDTH, PALETTE_HEIGHT, fill='light grey', width=0)
canvas.create_rectangle(DRAW_WIDTH/8, PALETTE_HEIGHT/5, 3*DRAW_WIDTH/8, 4*PALETTE_HEIGHT/5, fill='dark grey', width=1)
canvas.create_rectangle(5*DRAW_WIDTH/8, PALETTE_HEIGHT/5, 7*DRAW_WIDTH/8, 4*PALETTE_HEIGHT/5, fill='dark grey', width=1)
canvas.create_text(DRAW_WIDTH/4, PALETTE_HEIGHT/2, text='clear screen') #non-functional
# --- main ---
#set up root window and canvas
root = tk.Tk()
root.geometry('{}x{}'.format(DRAW_WIDTH, DRAW_HEIGHT+PALETTE_HEIGHT))
c = tk.Canvas(root, width=DRAW_WIDTH, height=DRAW_HEIGHT+PALETTE_HEIGHT, bg='white')
c.pack()
b = Brush(40, 'black')
#bind actions to functions
c.bind("<Button-1>", b.mouse_clicked)
c.bind("<ButtonRelease-1>", b.mouse_released)
c.bind("<Motion>", b.mouse_moved)
draw_palette(c)
root.mainloop()
I've got the following code that produces a plot that can interactively be modified. Clicking / holding the left mouse button sets the marker position, Holding the right button and moving the mouse moves the plotted data in direction x and using the mouse wheel zooms in/out. Additionally, resizing the window calls figure.tight_layout() so that the size of the axes is adapted to the window size.
# coding=utf-8
from __future__ import division
from Tkinter import *
import matplotlib
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from numpy import arange, sin, pi
matplotlib.use('TkAgg')
class PlotFrame(Frame):
def __init__(self, master, **ops):
Frame.__init__(self, master, **ops)
self.figure = Figure()
self.axes_main = self.figure.add_subplot(111)
for i in range(10):
t = arange(0, 300, 0.01)
s = sin(0.02 * pi * (t + 10 * i))
self.axes_main.plot(t, s)
self.plot = FigureCanvasTkAgg(self.figure, master=self)
self.plot.show()
self.plot.get_tk_widget().pack(fill=BOTH, expand=1)
self.dragging = False
self.dragging_button = None
self.mouse_pos = [0, 0]
self.marker = self.figure.axes[0].plot((0, 0), (-1, 1), 'black', linewidth=3)[0]
self.plot.mpl_connect('button_press_event', self.on_button_press)
self.plot.mpl_connect('button_release_event', self.on_button_release)
self.plot.mpl_connect('motion_notify_event', self.on_mouse_move)
self.plot.mpl_connect('scroll_event', self.on_mouse_scroll)
self.plot.mpl_connect("resize_event", self.on_resize)
def on_resize(self, _):
self.figure.tight_layout()
def axes_size(self):
pos = self.axes_main.get_position()
bbox = self.figure.get_window_extent().transformed(self.figure.dpi_scale_trans.inverted())
width, height = bbox.width * self.figure.dpi, bbox.height * self.figure.dpi
axis_size = [(pos.x1 - pos.x0) * width, (pos.y1 - pos.y0) * height]
return axis_size
def on_button_press(self, event):
# right mouse button clicked
if not self.dragging and event.button in (1, 3):
self.dragging = True
self.dragging_button = event.button
self.mouse_pos = [event.x, event.y]
# left mouse button clicked
if event.button == 1 and event.xdata is not None:
self.move_marker(event.xdata)
def on_button_release(self, event):
if self.dragging and self.dragging_button == event.button:
self.dragging = False
def on_mouse_move(self, event):
if self.dragging and self.dragging_button == 3:
dx = event.x - self.mouse_pos[0]
self.mouse_pos = [event.x, event.y]
x_min, x_max = self.figure.axes[0].get_xlim()
x_range = x_max - x_min
x_factor = x_range / self.axes_size()[0]
self.figure.axes[0].set_xlim([x_min - dx * x_factor, x_max - dx * x_factor])
self.plot.draw()
elif self.dragging and self.dragging_button == 1:
self.move_marker(event.xdata)
def on_mouse_scroll(self, event):
if event.xdata is None:
return
zoom_direction = -1 if event.button == 'up' else 1
zoom_factor = 1 + .4 * zoom_direction
x_min, x_max = self.figure.axes[0].get_xlim()
min = event.xdata + (x_min - event.xdata) * zoom_factor
max = event.xdata + (x_max - event.xdata) * zoom_factor
self.figure.axes[0].set_xlim([min, max])
self.plot.draw()
def move_marker(self, x_position):
y_min, y_max = self.figure.axes[0].get_ylim()
self.marker.set_data((x_position, x_position), (y_min, y_max))
self.plot.draw()
if __name__ == '__main__':
gui = Tk()
vf = PlotFrame(gui)
vf.pack(fill=BOTH, expand=1)
gui.mainloop()
The implementation works fine, but rendering is really slow when displaying a lot of lines. How can I make rendering faster? As you can see in the implementation above, the whole plot is drawn completely every time anything changes which shouldn't be necessary. My thoughts on this:
Resizing the window: draw everything
Zooming: draw everything
Moving the marker: just redraw the marker (one line) instead of drawing everything
Moving the plot in x direction: move the pixels currently displayed in the plot left/right and only draw pixels that are moved into the visible area
Drawing everything when resizing/zooming is fine for me, but I really need faster drawing of the latter two modifications. I already looked into matplotlib's animations, but as far as I understood, they won't help in my case. Any help is greatly appreciated, thanks!
The solution seems to be to cache elements that get redrawn as you said:
One major thing that gets redrawn is the background:
# cache the background
background = fig.canvas.copy_from_bbox(ax.bbox)
After caching restore it using restore region then just re-draw the points/line at every call you need
# restore background
fig.canvas.restore_region(background)
# redraw just the points
ax.draw_artist(points)
# fill in the axes rectangle
fig.canvas.blit(ax.bbox)
To optimize drawing blitting can be used. With it only given artists (those that were changed) will be rendered instead of the whole figure.
Motplotlib uses that technique internally in the animation module. You can use Animation class in it as a reference to implement the same behaviour in your code. Look at the _blit_draw() and several related functions after it in the sources.
I am trying to have a circle that, when clicked, moves somewhere else on the screen. However, when I click the circle, nothing happens.
#IMPORT STUFF
import pyglet as pg
from random import randint
mouse = pg.window.mouse
#VARS
window = pg.window.Window(width = 640, height = 480)
score = 0
circleImg = pg.image.load("circle.png")
circle = pg.sprite.Sprite(circleImg, randint(1, window.width), randint(1, window.height))
text = pg.text.Label("Click red!", font_name = "Times New Roman", font_size = 18, x = 260, y = 10)
#DETECT MOUSE PRESS ON CIRCLE
#window.event
def on_mouse_press(x, y, button, modifiers):
if x == circle.x and y == circle.y:
circle.x = randint(1, window.width)
circle.y = randint(1, window.height)
#window.event
def on_draw():
window.clear()
text.draw()
circle.draw()
pg.app.run()
import pyglet
from pyglet.gl import *
from random import randint
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
class Circle(pyglet.sprite.Sprite):
def __init__(self, radiance=5, x=0, y=0):
self.texture = pyglet.image.load('circle.png')
super(Circle, self).__init__(self.texture)
def click(self, x, y):
if x >= self.x and y >= self.y:
if x <= self.x + self.texture.width and y <= self.y + self.texture.height:
return self
mouse = pyglet.window.mouse
#VARS
window = pyglet.window.Window(width = 640, height = 480)
score = 0
#circleImg = pyglet.image.load("circle.png")
#circle = pyglet.sprite.Sprite(circleImg, randint(1, window.width), randint(1, window.height))
circle = Circle(x=50, y=50)
text = pyglet.text.Label("Click red!", font_name = "Times New Roman", font_size = 18, x = 260, y = 10)
#DETECT MOUSE PRESS ON CIRCLE
#window.event
def on_mouse_press(x, y, button, modifiers):
if circle.click(x, y):
print('Clicked in circle')
circle.x = randint(0, window.width - 10)
circle.y = randint(0, window.height - 10)
#window.event
def on_draw():
window.clear()
text.draw()
circle.draw()
pyglet.app.run()
A short description of what this does is it creates a custom class called Circle that inherits the Sprite class. It loads the circle.png as a texture with a alpha channel that gets blended by the GL library.
We add a custom function called click that checks if the lowest x,y coordinates are higher than the circles lowest x,y, then we check if the cursor is below x+width and same for y of the image region.
If that's the case, we return the circle sprite class as a True value in case we want to use the sprite.
Future enhancements:
You should draw the circle using gl functions, hence why I've defined radiance in the class definitions. However radiance here is never used, it's a placeholder for the future.
This is so you can use math to defined if you actually clicked within the circle, but this is beyond my scope of quick answers.. I would have to do a lot of debugging myself in order to get the math to add up (it's not my strong side).
What makes it work now is that we use the image width, height, x and y data to crudely check if we're within the image, aka "the circle".
trying to draw over sprite or change picture pyglet
As a bonus, I'll add this answer to the list of enhancements because it contains some stuff that might be useful. One would be to replace 90% of your code with a custom pyglet.window.Window class to replace global variables and decorators and stuff.
And it would look something like this:
import pyglet
from pyglet.gl import *
from random import randint
glEnable(GL_BLEND)
glBlendFunc(GL_SRC_ALPHA, GL_ONE_MINUS_SRC_ALPHA)
key = pyglet.window.key
class Circle(pyglet.sprite.Sprite):
def __init__(self, radiance=5, x=0, y=0):
self.texture = pyglet.image.load('circle.png')
super(Circle, self).__init__(self.texture)
def click(self, x, y):
if x >= self.x and y >= self.y:
if x <= self.x + self.texture.width and y <= self.y + self.texture.height:
return self
class MainScreen(pyglet.window.Window):
def __init__ (self):
super(MainScreen, self).__init__(800, 600, fullscreen = False)
self.x, self.y = 0, 0
self.bg = pyglet.sprite.Sprite(pyglet.image.load('background.jpg'))
self.sprites = {}
self.sprites['circle'] = Circle(x=50, y=50)
self.sprites['label'] = pyglet.text.Label("Click red!", font_name = "Times New Roman", font_size = 18, x = 260, y = 10)
self.alive = 1
def on_draw(self):
self.render()
def on_close(self):
self.alive = 0
def on_mouse_press(self, x, y, button, modifiers):
if self.sprites['circle'].click(x, y):
print('Clicked in circle')
self.sprites['circle'].x = randint(0, self.width - 10)
self.sprites['circle'].y = randint(0, self.height - 10)
def on_key_press(self, symbol, modifiers):
if symbol == key.ESCAPE: # [ESC]
self.alive = 0
def render(self):
self.clear()
self.bg.draw()
for sprite_name, sprite_obj in self.sprites.items():
sprite_obj.draw()
self.flip()
def run(self):
while self.alive == 1:
self.render()
# -----------> This is key <----------
# This is what replaces pyglet.app.run()
# but is required for the GUI to not freeze
#
event = self.dispatch_events()
x = MainScreen()
x.run()
I'm not familiar with pyglet, but I'm guessing the problem is that you're checking whether x == circle.x etc, which means it only moves when you click the single pixel at the exact centre of the circle. Try some kind of maximum distance from the centre (e.g. a hypotenuse math.sqrt( (x-circle.x)**2 + (y-circle.y)**2) < circle.radius