PysimpleGUI - simple animation - python

Thank you for reading this.
I'm working on a simple animation that is based on one of two examples from the PysimpleGUI cookbook. The attached code, of course, it is not doing anything. I've looked through many examples trying to figure out how to update the canvas but without success.
My first attempt was based on the sine wave plot example. I have an endless while loop and a display function. The display on the graph area shows the first iteration through the loop but is never updated after that.
The display function contains:
graph.DrawCircle((i,j), 5, line_color='black', etc
A second related question, should I be using the canvas or the graph method (as in the sine wave plot example), or doesn't it matter?
I don't want to overwhelm the reader with too much code. If I can get the following to work then I may have a good chance with the real code.
import PySimpleGUI as sg
import time
layout = [
[sg.Canvas(size=(100, 100), background_color='red', key= 'canvas')],
[sg.T('Change circle color to:'), sg.Button('Red'), sg.Button('Blue')]
]
window = sg.Window('Canvas test')
window.Layout(layout)
window.Finalize()
canvas = window.FindElement('canvas')
cir = canvas.TKCanvas.create_oval(50, 50, 100, 100)
while True:
event, values = window.Read()
'''
if event is None:
break
if event == 'Blue':
canvas.TKCanvas.itemconfig(cir, fill="Blue")
elif event == 'Red':
canvas.TKCanvas.itemconfig(cir, fill="Red")
'''
# this is the part that I need to sort out
for i in range(10):
if i % 2 == 0:
canvas.TKCanvas.itemconfig(cir, fill="Blue")
else:
canvas.TKCanvas.itemconfig(cir, fill="Red")
time.sleep(1)

I discovered the answer and that is, window.Read(timeout = 0).

In order for changes to show up in a window after making changes, you much either call Read or Refresh. I think all you need to do is down in your bottom loop, add the line:
window.Refresh()
From the docs at http://www.PySimpleGUI.org:
Refresh()
Cause changes to the window to be displayed on the screen.
Normally not needed unless the changes are immediately required or if
it's going to be a while before another call to Read.

Related

Tkinter Canvas items stored cleared and recalled

I am a beginner messing around in python 3.6 with tkinter canvas and have built a function that generates a solar system at random using elipses and the random method.
I would like to be able to save these items and all their attributes like their tags and bindings so that I can clear the canvas and draw a new random system. I would then like to go back to the first generated system if I need to.
I do not want to save the canvas as an image as I am binding the items to functions. Any ideas on how to achieve this?
As far as I know, you only get the items "id" as a handle to that item, and while you can delete an item with a given ID, you can not recreate it just with that ID.
What you could do is to given all those elements a common tag and use tag_lower and tag_raise to hide and show the items below and above the "background" pane. The objects are still on the canvas, but can not be seen and do not react to e.g. mouse events.
import tkinter, random
root = tkinter.Tk()
canvas = tkinter.Canvas(root)
canvas.pack()
space = canvas.create_rectangle(0, 0, 200, 200, fill="#000000")
for _ in range(10):
x, y = random.randint(0, 200), random.randint(0, 200)
s = canvas.create_oval(x, y, x+10, y+10, fill="#ffff00", tags="star")
canvas.tag_bind(s, "<Button>", lambda e: print("twinkle"))
root.after(3000, lambda: canvas.tag_lower("star", space))
root.after(6000, lambda: canvas.tag_raise("star", space))
root.mainloop()
Update: As suggested by OP in comments, one can also set the items' state to hidden, being probably the clearer option and not needing some obscure (-ing) background item.
root.after(3000, lambda: canvas.itemconfig("star", state="hidden"))
root.after(6000, lambda: canvas.itemconfig("star", state="normal"))

how to set the orientation of the stimulus for each trial in psychopy

I am fairly new to the python language and psychopy. I am practicing it by creating dummy experiments. Here, I am trying to create an experiment about bayesian brain. Non-vertical lines will be presented to the participant while no respond is expected from the participants, just exposure. Then for the last trial (it stays on the monitor for longer period of time to be responded), it is expected from the participant to judge whether the last line trial is vertical or not? (after exposing to non-vertical lines, I am expecting to see a change in perception of verticality).
However, there are so many things that I couldn't learn from the web. I am pretty sure you guys can help me easily.
My primary problem is; how to set up the orientation of the line? I found out the stim.ori but not sure how to use it on 'line' stimuli. Below I've attached the codes that I made so far. Also, I have added some extra questions with #.
I tried to be clear as much as I can. Sorry for my bad english.
Thank you!
from psychopy import visual, core, event #import some libraries from PsychoPy
import random
#create a window
mywin = visual.Window([800,600],monitor="testMonitor", units="deg")
#stimuli
lineo = visual.Line(mywin, start=(-5, -1), end=(-5, 1))
fixation = visual.GratingStim(mywin, size=0.2, pos=[0,0], color = 'black')
#draw the stimuli and update the window
n = 5 # trial number
i = 0
while i < n:
#fixation
fixation.draw()
mywin.flip()
presses = event.waitKeys(1)
# stimulus
orientationlist = [20,30,40,50,60] # I want to draw the orientation info from this list
x = random.choice(orientationlist)
lineo.ori((x)) #
lineo.draw()
mywin.flip()
presses= event.waitKeys(2)
i +=1
if i == 5: # how do I change the number into the length of the trial; len(int(n) didnt work.
lineo.draw()
mywin.flip()
presses = event.waitKeys(4)
#quiting
# I dont know how to command psychopy for quiting the
# experiment when 'escape' is pressed.
#cleanup
mywin.close()
core.quit()
There's a few things that you would want to do differently. I've updated your code and marked changes with the comment "CHANGE". Changing the orientation of a stimulus is pretty consistent in psychopy, so it's no different for Line than any other visual stimulus type.
from psychopy import visual, core, event #import some libraries from PsychoPy
import random
#create a window
mywin = visual.Window([800,600],monitor="testMonitor", units="deg")
#stimuli
lineo = visual.Line(mywin, start=(-5, -1), end=(-5, 1))
fixation = visual.GratingStim(mywin, size=0.2, pos=[0,0], color = 'black')
orientationlist = [20,30,40,50,60] # CHANGED. No need to redefine on every iteration of the while-loop.
#draw the stimuli and update the window
n = 5 # trial number
for i in range(n): # CHANGED. This is much neater in your case than a while loop. No need to "manually" increment i.
#fixation
fixation.draw()
mywin.flip()
event.waitKeys(1) # CHANGED. No need to assign output to anything if it isn't used.
# stimulus
lineo.ori = random.choice(orientationlist) # CHANGED. Alternative: lineo.setOri(random.choice(orientationlist)).
lineo.draw()
mywin.flip()
event.waitKeys(2)
# At this point, all the stimuli have been shown. So no need to do an if-statement within the loop. The following code will run at the appropriate time
lineo.draw()
mywin.flip()
event.waitKeys(keyList=['escape']) # CHANGED. Listen for escape, do not assign to variable
# CHANGED. No need to call core.quit() or myWin.close() here since python automatically cleans everything up on script end.

How to use multiprocessing or threading with tkinter

I am trying to make a simple space invaders game and a problem I have run into is getting things t happen at the same time. I have binded the shooting action to the canvas of the game so that when you click a function is called. I would like it so that this function can be called multiple times at once so that multiple "lasers/bullets" can be seen on the screen at any one time. At the minute when you click and a "laser/bullet" is already on screen, the previous one disappears and a new one appears. CODE:
class Game1():
def __init__(self, xcoord1=380, ycoord1=550, xcoord2=400, ycoord2=570):
self.Master = Master
self.Master.geometry("800x600+300+150")
Game1Canvas = Canvas(self.Master, bg="black", height=600, width=800)
Game1Canvas.place(x=0, y=0)
self.Canvas = Game1Canvas
self.Canvas.bind("<Button-1>", self.Shoot)
self.Ship = self.Canvas.create_rectangle(self.xcoord1, self.ycoord1, self.xcoord2, self.ycoord2, fill = "red")
def Shoot(self):
self.LaserLocation = 0
for self.LaserLocation in range(0 , 112):
Master.after(1, self.Canvas.create_rectangle(self.xcoord1, self.ycoord1 - (self.LaserLocation * 5), self.xcoord2 - 5, self.ycoord2 - (self.LaserLocation * 5), fill = "pink", tag=str(CurrentTag)))
Master.update()
self.Canvas.delete(str(CurrentTag))
This is a much more "dumbed" down version of the code at the minute because I've been trying a bunch of different ways to get this working and it's a mess. I am aware of the multiprocessing and threading imports and I have tried them both but am unable to get them working for my code. If someone could reply back with a solution I would be very grateful. Cheers.
You don't need to use multithreading or multiprocessing. You also don't need (nor want) to be drawing new rectangles every millisecond, or multiple times per millisecond.
The solution is to have your Shoot function merely create a single rectangle, and add it to a list. Then, using a simple animation mechanism, iterate over the list and move each bullet up one or two pixels. You do this by creating a function that calls itself every 20-30 ms.
The solution looks something like this:
def Shoot(self):
laser = self.Canvas.create_rectangle(...)
self.lasers.append(laser)
def do_animation(self):
# make a copy of the list of lasers to iterate
# over, so we can remove items from the original
# list when they go off screen
lasers = self.lasers[:]
for laser in lasers:
# get current coordinates of this laser
(x0,y0,x1,y1) = self.canvas.bbox(laser)
if x1 < self.canvas.winfo_height():
# if it is not off screen, move it up
self.canvas.move(laser, 0, -10)
else:
# if it IS off screen, delete it
self.canvas.delete(laser)
self.lasers.remove(laser)
self.after(30, self.do_animation)
The above will move the lasers every 30 milliseconds (about 33 fps).

Python tkinter widget image animation function with after in a loop misbehaving

I am slowly learning my way through Python and tkinter. :)
In a game I'm making there are animations of images displayed within widgets (namely buttons).
Animating frame-by-frame is mundane, so I came up with a function to help me automate a 10-frame animation.
In a loop of range(10) (as I have 10 frames) the function calls the after() method which has a function callback, each displaying next frame of animation.
Since time within the after method is larger for each consecutive iteration of the loop, each new frame should be displayed nicely after given time (here it's 34ms).
That's all fine in theory, however when I run the code and appropriate functions are called, the button does not animate properly. Only the last frame seems to pop out.
The way I see it, after some reading on how tkinter works, is that each after in a loop should set independent callback in tkinter's "timeline" to be called after some time. Thus in my opinion this code should work.
What do you make of it? What've I got wrong, is my logic about after() in a loop off?
#Python 3.4.3
def animateMine(object):
global firstAnimateMineCall
for frame in range(10):
frame += 1
time = 34 * frame
root.after(time, lambda: mineAnimationFrame(object, frame))
if firstAnimateMineCall and frame == 10:
root. after(500 , lambda: animateAllMines(object))
firstAnimateMineCall = False
In the doubtful event this'd be useful:
def mineAnimationFrame(object, frame):
tempDir = "Resources/Mine/saperx_mine_%s.png" % (frame)
tempImage = PhotoImage(file=tempDir)
object.configure(image=tempImage)
object.image = tempImage
object.disabled = True
A simplistic, good-looking and easy to implement solution to this problem I came up with (thanks to CurlyJoe's advice).
*A major pro of this design is that it's easy to adjust it to your frames quantity... you got 5? Just change 1 value and it's good to go! Got 900? Still easy. 6,02*10^23 frames? Still just 1 change ;]*
To adjust to your frame size, just change the list comprehension range(10) to whatever quantity you wish, the code will take care of the rest.
from tkinter import Tk, PhotoImage, Button, FLAT
root = Tk()
mineImagesList = [PhotoImage(file="Resources/Mine/saperx_mine_%s.png" % (frame)) for frame in range(1, 11)]
button = Button(root, bd=0, relief=FLAT, command= lambda: func(button))
def func (object, frame=0):
object.configure(image=mineImagesList[frame])
object.image = mineImagesList[frame]
print("Object image:", object.image)
if frame+1 < len(mineImagesList):
frame += 1
root.after(34, lambda frame=frame, object=object: func(object=object, frame=frame))
button.pack()
root.mainloop()

Single mouse click within While loop (PsychoPy)

I am using PsychoPy and I would like to print the position of my mouse when it has been clicked.
The actual printing of the position needs to be placed inside a while loop. Using the code below, when I click I get more than one output lines, which print the same positions. I would like to have only one output printing for each click.
This is the code I am using:
#!/usr/bin/env python2
from psychopy import visual, core, event
from pyglet.gl import *
width = 600
height = 600
myWin = visual.Window([width,height], color='white',units='pix',monitor='testMonitor')
#This will set the windows units (pixels) to GL units
glMatrixMode(GL_PROJECTION)
glLoadIdentity()
glOrtho(0, width, 0, height, -1, 1)
glMatrixMode(GL_MODELVIEW)
glLoadIdentity()
glEnable(GL_BLEND)
glBlendFunc(GL_ZERO, GL_SRC_COLOR)
myMouse = event.Mouse() # will use myWin by default
while True:
#Triangle left
glColor3f(1.0, 0.0, 0.0)
glBegin(GL_TRIANGLES)
glVertex3f(150, 550, 1)
glVertex3f(50, 350, 1)
glVertex3f(250, 350, -1)
glEnd()
if myMouse.getPressed()[0]:
myMouse.clickReset()
print myMouse.getPos()
myWin.flip()
core.quit()
Is there something wrong I am doing? Should the 'frame rate' of the while loop be changed?
I've not used the module, but it seems like mouse events are thrown for mouse raises as well.
You'll need to
Store the mouse state for next time
Each iteration, test whether the mouse state for button 0 has gone up → down
The mouse state is returned by myMouse.getPressed.
So something like:
oldMouseIsDown = False
while True:
...
mouseIsDown = myMouse.getPressed()[0]
myMouse.clickReset()
if mouseIsDown and not oldMouseIsDown:
print myMouse.getPos()
oldMouseIsDown = mouseIsDown
Veedrac's answer is correct. Your code is using the typical PsychoPy pattern of checking for events once every time the window is redrawn. This will typically be happening at least at 60 Hz. So unless you manage to press the mouse button for less than 16.7 ms (or less for a faster screen), you will detect it multiple times as being pressed, as each time you check on successive window redraws, the mouse button remains down. Even though it was pushed only once, the duration of the push is not instantaneous.
As Veedrac suggests, you therefore need to maintain the previous state of the mouse button in a variable so that you can choose to only print the position once. Mouseup events are not relevant here: you are purely testing for whether the button is currently pressed.
I had a very similar problem and fixed it in a slightly different way from the accepted answer. The advantage of this one is that you can set how long you want to 'desensitize' the mouse after a click (i.e. to prevent longer clicks from triggering your if loop multiple times). Depending on your mouse or the user's click release speed you can change minFramesAfterClick:
minFramesAfterClick = 10 # to prevent re-entering the if loop too early
myMouse.clickReset()
timeAfterClick = 0
while True:
timeAfterClick += 1
if myMouse.getPressed()[0] and timeAfterClick >= minFramesAfterClick:
print myMouse.getPos()
myMouse.clickReset()
timeAfterClick = 0
myWin.flip()
By the way, the reason why the OP couldn't get Veedrac's answer work is because line oldMouseIsDown = mouseIsDown should be placed it inside the if loop rather than after it (not enough reputation to comment there).

Categories

Resources