I'm new to pyglet, but have needed to learn it quickly in order to complete a school project. Basically, I have my first slide with the images menu and preview on it, and a second slide that has the end_screen image on it. The showing of the first slide works just fine, though the program wont let me transition into the second slide when I press a key. No Error message, It just doesnt do anything when I try to draw the second slide.
import pyglet
from pyglet.window import key
import time
slide = 1
preview_image = pyglet.image.load('untitled (3).jpg')
menu_image = pyglet.image.load('untitled (4).jpg')
end_screen_image = pyglet.image.load('untitled (5).jpg')
preview = pyglet.sprite.Sprite(preview_image, x=0, y=0)
menu = pyglet.sprite.Sprite(menu_image, x=540, y=0)
end_screen = pyglet.sprite.Sprite(end_screen_image, x=270, y=0)
window = pyglet.window.Window(1180, 630)
def update():
global menu
global window
global preview
global end_screen
global slide
if slide == 1:
window.clear()
preview.draw()
menu.draw()
elif slide == 2:
window.clear()
end_screen.draw()
#window.event
def on_key_press(symbol, modifiers):
global slide
slide = 2
update()
#window.event
def on_draw():
window.clear()
preview.draw()
menu.draw()
pyglet.app.run()
For some reason it just wont work. I've tried different kinds of update functioons but none of them worked either. Thanks in advance!
Main issue here is the concept of update and how you'd normally expect code to execute.
I'm guessing you're used to code being called as you write it, meaning that you think "If I press a button, I want update() to be called instead". And that's true, partially.
The problem you're experiencing is that Pyglet is trying to render things as fast as possible, and it does so by calling on_draw() as often as needed. So when you execute update() - a fraction of a second later on_draw() will be called again, and again.. and again. And the first line you have in on_draw is window.clear() - which clears the window and any changes made in update(). Hopefully that makes sense.
Instead, you probably want your "which thing am i showing"-logic in the on_draw every render. Altho this will be pretty slow, hopefully this convey the logic of how the automatic rendering process works and why your logic from school assignments doesn't necessarily translate well into projects where there's "background tasks" running.
import pyglet
from pyglet.window import key
import time
preview = pyglet.sprite.Sprite(pyglet.image.load('untitled (3).jpg'), x=0, y=0)
menu = pyglet.sprite.Sprite(pyglet.image.load('untitled (4).jpg'), x=540, y=0)
end_screen = pyglet.sprite.Sprite(pyglet.image.load('untitled (5).jpg'), x=270, y=0)
window = pyglet.window.Window(1180, 630)
#window.event
def on_key_press(symbol, modifiers):
global slide
slide = 2
#window.event
def on_draw():
window.clear()
if slide == 1:
window.clear()
preview.draw()
menu.draw()
elif slide == 2:
window.clear()
end_screen.draw()
pyglet.app.run()
Oh and btw, no need for global on variables in functions like your update() function. Unless you assign something to variables (menu = ...) they're going to be global by default (For instance, see in on_key_press where you update slide with slide = 2, in those cases you need to globalize first). Just a friendly reminder how global scope works in Python and it's functions - giving you some more wiggle room to write less code and achieve the same thing. So remember, = means you need to make them global, otherwise don't bother :)
I also made the code a bit "smaller", since you're not using preview_image i went ahead and passed the image-loading-result straight into Sprite() for preview for instance, saving 3 lines of code making it arguably slightly less code and thus more readable. It's a matter of taste tho, can revert it if you feel like it.
Related
I am writing a script to read my keystrokes and draw simple shapes in turtle.
To record keystrokes, I am using the keyboard module and I am using turtle for drawing.
I am getting struck due to the use of threading in the keyboard module.
What I am currently doing is-
I added hotkeys using keyboard.add_hotkey method.
If I am pressing a certain key, that letter is added to a list (named data) for later use.
When I press the combination of Ctrl+Shift+S, the save function is called. In the save function, a turtle window is instantiated, and the list data is popped one letter at a time. The shape is drawn according to the letter popped.
When the list gets empty, I save the drawing and close the turtle window.
The problem that I am facing is that once the save function is called, the program stops listening to other calls. It is perhaps due to the use of threads in keyboard module.
The code is attached here-
def start():
#turtle.mainloop()
s=turtle.Screen().setup( width = WIDTH, height = HEIGHT, startx = 0, starty = 0)
global t
t=turtle.Turtle()
turtle.ht()
t.ht()
def save():
start()
global t
global data
t.speed(0)
while data:
fun = data.pop()
if fun=='c':
draw_circle()
elif fun=='r':
draw_rectangle()
elif fun=='p':
draw_polygon()
elif fun=='h':
draw_hexagon()
elif fun=='t':
draw_triangle()
elif fun=='m':
draw_pentagon()
ts = turtle.getscreen()
ts.getcanvas().postscript(file="drawing.eps")
img = Image.open('drawing.eps')
img.save('drawing.png')
turtle.bye()
def push_fun(fun):
data.append(fun)
if __name__=='__main__':
keyboard.add_hotkey('ctrl+shift+s', save)
keyboard.add_hotkey('ctrl+shift+e', exit)
keyboard.add_hotkey('ctrl+shift+p', send_to_server)
# keyboard.add_hotkey('ctrl+shift+s', save, args=(data))
keyboard.add_hotkey('c', push_fun, args=('c',))
keyboard.add_hotkey('s', push_fun, args=('t',))
keyboard.add_hotkey('h', push_fun, args=('h',))
keyboard.add_hotkey('p', push_fun, args=('p',))
keyboard.add_hotkey('r', push_fun, args=('r',))
keyboard.wait()
After the save function is called, the program remains in the keyboard.wait() part but does not listen to any other key press.
you can end the wait by setting a key to press like:
keyboard.wait('space')
but keyboard.wait() without any keys given blocks all keystrokes forever
I'm not sure why you even put it there, I'm not even sure why it picks anything up at all, but I think you can do this:
if __name__=='__main__':
'''your keystrokes'''
while True: #or set a timer or something
pass
you didn't give me the whole code so I can't make sure it works
you also need to global the list data in the push_fun function
I hope it helps :)
The goal is to achieve different "screens" in TkInter and change between them. The easiest to imagine this is to think of a mobile app, where one clicks on the icon, for example "Add new", and new screen opens. The application has total 7 screens and it should be able to change screens according to user actions.
Setup is on Raspberry Pi with LCD+touchscreen attached. I am using tkinter in Python3. Canvas is used to show elements on the screen.
Since I am coming from embedded hardware world and have very little experience in Python, and generally high-level languages, I approached this with switch-case logic. In Python this is if-elif-elif...
I have tried various things:
Making global canvas object. Having a variable programState which determines which screen is currently shown. This obviously didn't work because it would just run once and get stuck at the mainloop below.
from tkinter import *
import time
root = Tk()
programState = 0
canvas = Canvas(width=320, height=480, bg='black')
canvas.pack(expand=YES, fill=BOTH)
if(programState == 0):
backgroundImage = PhotoImage(file="image.gif")
canvas.create_image(0,0, image=backgroundImage, anchor=NW);
time.sleep(2)
canvas.delete(ALL) #delete all objects from canvas
programState = 1
elif(programState == 1):
....
....
....
root.mainloop()
Using root.after function but this failed and wouldn't show anything on the screen, it would only create canvas. I probably didn't use it at the right place.
Trying making another thread for changing screens, just to test threading option. It gets stuck at first image and never moves to second one.
from tkinter import *
from threading import Thread
from time import sleep
def threadFun():
while True:
backgroundImage = PhotoImage(file="image1.gif")
backgroundImage2 = PhotoImage(file="image2.gif")
canvas.create_image(0,0,image=backgroundImage, anchor=NW)
sleep(2)
canvas.delete(ALL)
canvas.create_image(0,0,image=backgroundImage2, anchor=NW)
root = Tk()
canvas = Canvas(width=320, height=480, bg='black')
canvas.pack(expand=YES, fill=BOTH)
# daemon=True kills the thread when you close the GUI, otherwise it would continue to run and raise an error.
Thread(target=threadFun, daemon=True).start()
root.mainloop()
I expect this app could change screens using a special thread which would call a function which redraws elements on the canvas, but this has been failing so far. As much as I understand now, threads might be the best option. They are closest to my way of thinking with infinite loop (while True) and closest to my logic.
What are options here? How deleting whole screen and redrawing it (what I call making a new "screen") can be achieved?
Tkinter, like most GUI toolkits, is event driven. You simply need to create a function that deletes the old screen and creates the new, and then does this in response to an event (button click, timer, whatever).
Using your first canvas example
In your first example you want to automatically switch pages after two seconds. That can be done by using after to schedule a function to run after the timeout. Then it's just a matter of moving your redraw logic into a function.
For example:
def set_programState(new_state):
global programState
programState = new_state
refresh()
def refresh():
canvas.delete("all")
if(programState == 0):
backgroundImage = PhotoImage(file="image.gif")
canvas.create_image(0,0, image=backgroundImage, anchor=NW);
canvas.after(2000, set_programState, 1)
elif(programState == 1):
...
Using python objects
Arguably a better solution is to make each page be a class based off of a widget. Doing so makes it easy to add or remove everything at once by adding or removing that one widget (because destroying a widget also destroys all of its children)
Then it's just a matter of deleting the old object and instantiating the new. You can create a mapping of state number to class name if you like the state-driven concept, and use that mapping to determine which class to instantiate.
For example:
class ThisPage(tk.Frame):
def __init__(self):
<code to create everything for this page>
class ThatPage(tk.Frame):
def __init__(self):
<code to create everything for this page>
page_map = {0: ThisPage, 1: ThatPage}
current_page = None
...
def refresh():
global current_page
if current_page:
current_page.destroy()
new_page_class = page_map[programstate]
current_page = new_page_class()
current_page.pack(fill="both", expand=True)
The above code is somewhat ham-fisted, but hopefully it illustrates the basic technique.
Just like with the first example, you can call update() from any sort of event: a button click, a timer, or any other sort of event supported by tkinter. For example, to bind the escape key to always take you to the initial state you could do something like this:
def reset_state(event):
global programState
programState = 0
refresh()
root.bind("<Escape>", reset_state)
I am trying to learn pyglet and practice some python coding with a questionnaire thing, but I am unable to find a way to make the background picture be removed or drawn on top of or something for 10 seconds. I am new and am lacking in a lot of the knowledge I would need, thank you for helping!
import pyglet
from pyglet.window import Window
from pyglet.window import key
from pyglet import image
import time
card1 = False
cat_image = pyglet.image.load("cat.png")
dog_image = pyglet.image.load("dog.png")
image = pyglet.image.load("backg.png")
background_sprite = pyglet.sprite.Sprite(image)
cat = pyglet.sprite.Sprite(cat_image)
dog = pyglet.sprite.Sprite(dog_image)
window = pyglet.window.Window(638, 404, "Life")
mouse_pos_x = 0
mouse_pos_y = 0
catmeme = pyglet.image.load("catmeme.png")
sprite_catmeme = pyglet.sprite.Sprite(catmeme)
#window.event
def on_draw():
window.clear()
background_sprite.draw()
card_draw1(63, 192, 385, 192)
def card1():
while time.time() < (time.time() + 10):
window.clear()
sprite_catmeme.draw()
#window.event
def card_draw1(x1, y1, x2, y2):
cat.set_position(x1, y1)
dog.set_position(x2, y2)
cat.draw()
dog.draw()
def card_draw2():
pass
#window.event
def on_mouse_press(x, y, button, modifiers):
if x > cat.x and x < (cat.x + cat.width):
if y > cat.y and y < (cat.y + cat.height):
card1()
game = True
while game:
on_draw()
pyglet.app.run()
There's a few flaws in the order and in which you do things.
I will try my best to describe them and give you a piece of code that might work better for what your need is.
I also think your description of the problem is a bit of an XY Problem which is quite common when asking for help on complex matters where you think you're close to a solution, so you're asking for help on the solution you've come up with and not the problem.
I'm assuming you want to show a "Splash screen" for 10 seconds, which happens to be your background? And then present the cat.png and dog.png ontop of it, correct?
If that's the case, here's where you probably need to change things in order for it to work:
The draw() function
It doesn't really update the screen much, it simply adds things to the graphical memory. What updates the screen is you or something telling the graphics library that you're done adding things to the screen and it's time to update everything you've .draw()'n. So the last thing you need in the loop would be window.flip() in order for the things you've drawn to actually show.
Your things might show if you try to wiggle the window around, it should trigger a re-draw of the scene because of how the internal mechanics of pyglet work..
If you don't call .flip() - odds are probable that the redraw() call will never occur - which again, is a internal mechanism of Pyglet/GL that tells the graphics card that something has been updated, we're done updating and it's time to redraw the scene.
a scene
This is the word most commonly used for what the user is seeing.
I'll probably throw this around a lot in my text, so it's good to know that this is what the user is seeing, not what you've .draw()'n or what's been deleted, it's the last current rendering of the graphics card to the monitor.
But because of how graphical buffers work we've might have removed or added content to the memory without actually drawing it yet. Keep this in mind.
The pyglet.app.run() call
This is a never ending loop in itself, so having that in a while game: loop doesn't really make sense because .run() will "hang" your entire application, any code you want to execute needs to be in def on_draw or an event that is generated from within the graphical code itself.
To better understand this, have a look at my code, i've pasted it around a couple of times here on SO over the years and it's a basic model of two custom classes that inherits the behavior of Pyglet but lets you design your own classes to behave slightly differently.
And most of the functionality is under on_??? functions, which is almost always a function used to catch Events. Pyglet has a lot of these built in, and we're going to override them with our own (but the names must be the same)
import pyglet
from pyglet.gl import *
key = pyglet.window.key
class CustomSprite(pyglet.sprite.Sprite):
def __init__(self, texture_file, x=0, y=0):
## Must load the texture as a image resource before initializing class:Sprite()
self.texture = pyglet.image.load(texture_file)
super(CustomSprite, self).__init__(self.texture)
self.x = x
self.y = y
def _draw(self):
self.draw()
class MainScreen(pyglet.window.Window):
def __init__ (self):
super(MainScreen, self).__init__(800, 600, fullscreen = False)
self.x, self.y = 0, 0
self.bg = CustomSprite('bg.jpg')
self.sprites = {}
self.alive = 1
def on_draw(self):
self.render()
def on_close(self):
self.alive = 0
def on_key_press(self, symbol, modifiers):
if symbol == key.ESCAPE: # [ESC]
self.alive = 0
elif symbol == key.C:
print('Rendering cat')
self.sprites['cat'] = CustomSprite('cat.png', x=10, y=10)
elif symbol == key.D:
self.sprites['dog'] = CustomSprite('dog.png', x=100, y=100)
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()
Now, this code is kept simple on purpose, the full code I usually paste on SO can be found at Torxed/PygletGui, the gui.py is where most of this comes from and it's the main loop.
What I do here is simply replace the Decorators by using "actual" functions inside a class. The class itself inherits the functions from a traditional pyglet.window.Window, and as soon as you name the functions the same as the inherited onces, you replace the core functionality of Window() with whatever you decide.. In this case, i mimic the same functions but add a few of my own.
on_key_press
One such example is on_key_press(), which normally just contain a pass call and does nothing, here, we check if key.C is pressed, and if so - we add a item to self.sprites.. self.sprites just so happen to be in our render() loop, anything in there will be rendered ontop of a background.
Here's the pictures I used:
(named bg.jpg, cat.png, dog.png - note the different file endings)
class:CustomSprite
CustomSprite is a very simple class designed to make your life easier at this point, nothing else. It's very limited in functionality but the little it do is awesome.
It's soul purpose is to take a file name, load it as an image and you can treat the object like a traditional pyglet.sprite.Sprite, meaning you can move it around and manipulate it in many ways.
It saves a few lines of code having to load all the images you need and as you can see in gui_classes_generic.py you can add a heap of functions that's "invisible" and normally not readily availbale to a normal sprite class.
I use this a bunch! But the code gets complicated real fast so I kept this post simple on purpose.
the flip function
Even in my class, I still need to use flip() in order to update the contents of the screen. This is because .clear() clears the window as you would expect, that also triggers a redraw of the scene.
bg.draw() might in some cases trigger a redraw if the data is big enough or if something else happens, for instance you move the window.
but calling .flip() will tell the GL backend to force a redraw.
Further optimizations
There's a thing called batched rendering, basically the graphic card is designed to take enormous ammounts of data and render it in one go, so calling .draw() on several items will only clog the CPU before the GPU even gets a chance to shine. Read more about Batched rendering and graphics! It will save you a lot of frame rates.
Another thing is to keep as little functionality as possible in the render() loop and use the event triggers as your main source of coding style.
Pyglet does a good job of being fast, especially if you only do things on event driven tasks.
Try to avoid timers, but if you really do need to use time for things, such as removing cat.png after a certain ammount of time, use the clock/time event to call a function that removes the cat. Do not try to use your own t = time() style of code unless you know where you're putting it and why. There's a good timer, I rarely use it.. But you should if you're starting off.
This has been one hell of a wall of text, I hope it educated you some what in the life of graphics and stuff. Keep going, it's a hurdle to get into this kind of stuff but it's quite rewarding once you've mastered it (I still haven't) :)
Till now, I used to end my Tkinter programs with: tk.mainloop(), or nothing would show up! See example:
from Tkinter import *
import random
import time
tk = Tk()
tk.title = "Game"
tk.resizable(0,0)
tk.wm_attributes("-topmost", 1)
canvas = Canvas(tk, width=500, height=400, bd=0, highlightthickness=0)
canvas.pack()
class Ball:
def __init__(self, canvas, color):
self.canvas = canvas
self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
self.canvas.move(self.id, 245, 100)
def draw(self):
pass
ball = Ball(canvas, "red")
tk.mainloop()
However, when tried the next step in this program (making the ball move by time), the book am reading from, says to do the following. So I changed the draw function to:
def draw(self):
self.canvas.move(self.id, 0, -1)
and add the following code to my program:
while 1:
ball.draw()
tk.update_idletasks()
tk.update()
time.sleep(0.01)
But I noticed that adding this block of code, made the use of tk.mainloop() useless, since everything would show up even without it!!!
At this moment I should mention that my book never talks about tk.mainloop() (maybe because it uses Python 3) but I learned about it searching the web since my programs didn't work by copying book's code!
So I tried doing the following that would not work!!!
while 1:
ball.draw()
tk.mainloop()
time.sleep(0.01)
What's going on? What does tk.mainloop()? What does tk.update_idletasks() and tk.update() do and how that differs from tk.mainloop()? Should I use the above loop?tk.mainloop()? or both in my programs?
tk.mainloop() blocks. It means that execution of your Python commands halts there. You can see that by writing:
while 1:
ball.draw()
tk.mainloop()
print("hello") #NEW CODE
time.sleep(0.01)
You will never see the output from the print statement. Because there is no loop, the ball doesn't move.
On the other hand, the methods update_idletasks() and update() here:
while True:
ball.draw()
tk.update_idletasks()
tk.update()
...do not block; after those methods finish, execution will continue, so the while loop will execute over and over, which makes the ball move.
An infinite loop containing the method calls update_idletasks() and update() can act as a substitute for calling tk.mainloop(). Note that the whole while loop can be said to block just like tk.mainloop() because nothing after the while loop will execute.
However, tk.mainloop() is not a substitute for just the lines:
tk.update_idletasks()
tk.update()
Rather, tk.mainloop() is a substitute for the whole while loop:
while True:
tk.update_idletasks()
tk.update()
Response to comment:
Here is what the tcl docs say:
Update idletasks
This subcommand of update flushes all currently-scheduled idle events
from Tcl's event queue. Idle events are used to postpone processing
until “there is nothing else to do”, with the typical use case for
them being Tk's redrawing and geometry recalculations. By postponing
these until Tk is idle, expensive redraw operations are not done until
everything from a cluster of events (e.g., button release, change of
current window, etc.) are processed at the script level. This makes Tk
seem much faster, but if you're in the middle of doing some long
running processing, it can also mean that no idle events are processed
for a long time. By calling update idletasks, redraws due to internal
changes of state are processed immediately. (Redraws due to system
events, e.g., being deiconified by the user, need a full update to be
processed.)
APN As described in Update considered harmful, use of update to handle
redraws not handled by update idletasks has many issues. Joe English
in a comp.lang.tcl posting describes an alternative:
So update_idletasks() causes some subset of events to be processed that update() causes to be processed.
From the update docs:
update ?idletasks?
The update command is used to bring the application “up to date” by
entering the Tcl event loop repeatedly until all pending events
(including idle callbacks) have been processed.
If the idletasks keyword is specified as an argument to the command,
then no new events or errors are processed; only idle callbacks are
invoked. This causes operations that are normally deferred, such as
display updates and window layout calculations, to be performed
immediately.
KBK (12 February 2000) -- My personal opinion is that the [update]
command is not one of the best practices, and a programmer is well
advised to avoid it. I have seldom if ever seen a use of [update] that
could not be more effectively programmed by another means, generally
appropriate use of event callbacks. By the way, this caution applies
to all the Tcl commands (vwait and tkwait are the other common
culprits) that enter the event loop recursively, with the exception of
using a single [vwait] at global level to launch the event loop inside
a shell that doesn't launch it automatically.
The commonest purposes for which I've seen [update] recommended are:
Keeping the GUI alive while some long-running calculation is
executing. See Countdown program for an alternative. 2) Waiting for a window to be configured before doing things like
geometry management on it. The alternative is to bind on events such
as that notify the process of a window's geometry. See
Centering a window for an alternative.
What's wrong with update? There are several answers. First, it tends
to complicate the code of the surrounding GUI. If you work the
exercises in the Countdown program, you'll get a feel for how much
easier it can be when each event is processed on its own callback.
Second, it's a source of insidious bugs. The general problem is that
executing [update] has nearly unconstrained side effects; on return
from [update], a script can easily discover that the rug has been
pulled out from under it. There's further discussion of this
phenomenon over at Update considered harmful.
.....
Is there any chance I can make my program work without the while loop?
Yes, but things get a little tricky. You might think something like the following would work:
class Ball:
def __init__(self, canvas, color):
self.canvas = canvas
self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
self.canvas.move(self.id, 245, 100)
def draw(self):
while True:
self.canvas.move(self.id, 0, -1)
ball = Ball(canvas, "red")
ball.draw()
tk.mainloop()
The problem is that ball.draw() will cause execution to enter an infinite loop in the draw() method, so tk.mainloop() will never execute, and your widgets will never display. In gui programming, infinite loops have to be avoided at all costs in order to keep the widgets responsive to user input, e.g. mouse clicks.
So, the question is: how do you execute something over and over again without actually creating an infinite loop? Tkinter has an answer for that problem: a widget's after() method:
from Tkinter import *
import random
import time
tk = Tk()
tk.title = "Game"
tk.resizable(0,0)
tk.wm_attributes("-topmost", 1)
canvas = Canvas(tk, width=500, height=400, bd=0, highlightthickness=0)
canvas.pack()
class Ball:
def __init__(self, canvas, color):
self.canvas = canvas
self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
self.canvas.move(self.id, 245, 100)
def draw(self):
self.canvas.move(self.id, 0, -1)
self.canvas.after(1, self.draw) #(time_delay, method_to_execute)
ball = Ball(canvas, "red")
ball.draw() #Changed per Bryan Oakley's comment
tk.mainloop()
The after() method doesn't block (it actually creates another thread of execution), so execution continues on in your python program after after() is called, which means tk.mainloop() executes next, so your widgets get configured and displayed. The after() method also allows your widgets to remain responsive to other user input. Try running the following program, and then click your mouse on different spots on the canvas:
from Tkinter import *
import random
import time
root = Tk()
root.title = "Game"
root.resizable(0,0)
root.wm_attributes("-topmost", 1)
canvas = Canvas(root, width=500, height=400, bd=0, highlightthickness=0)
canvas.pack()
class Ball:
def __init__(self, canvas, color):
self.canvas = canvas
self.id = canvas.create_oval(10, 10, 25, 25, fill=color)
self.canvas.move(self.id, 245, 100)
self.canvas.bind("<Button-1>", self.canvas_onclick)
self.text_id = self.canvas.create_text(300, 200, anchor='se')
self.canvas.itemconfig(self.text_id, text='hello')
def canvas_onclick(self, event):
self.canvas.itemconfig(
self.text_id,
text="You clicked at ({}, {})".format(event.x, event.y)
)
def draw(self):
self.canvas.move(self.id, 0, -1)
self.canvas.after(50, self.draw)
ball = Ball(canvas, "red")
ball.draw() #Changed per Bryan Oakley's comment.
root.mainloop()
while 1:
root.update()
... is (very!) roughly similar to:
root.mainloop()
The difference is, mainloop is the correct way to code and the infinite loop is subtly incorrect. I suspect, though, that the vast majority of the time, either will work. It's just that mainloop is a much cleaner solution. After all, calling mainloop is essentially this under the covers:
while the_window_has_not_been_destroyed():
wait_until_the_event_queue_is_not_empty()
event = event_queue.pop()
event.handle()
... which, as you can see, isn't much different than your own while loop. So, why create your own infinite loop when tkinter already has one you can use?
Put in the simplest terms possible: always call mainloop as the last logical line of code in your program. That's how Tkinter was designed to be used.
I'm using an MVC / MVA design pattern, with multiple types of "views". One type is a "GuiView", which is a Tk window. I pass a view reference to my window object which does things like link buttons back to view functions (which the adapter / controller class also calls).
In order to do that, the view object constructor needed to be completed prior to creating the window object. After creating and displaying the window, I wanted to do some initial tasks with the view automatically. At first I tried doing them post mainloop(), but that didn't work because mainloop() blocked!
As such, I created the window object and used tk.update() to draw it. Then, I kicked off my initial tasks, and finally started the mainloop.
import Tkinter as tk
class Window(tk.Frame):
def __init__(self, master=None, view=None ):
tk.Frame.__init__( self, master )
self.view_ = view
""" Setup window linking it to the view... """
class GuiView( MyViewSuperClass ):
def open( self ):
self.tkRoot_ = tk.Tk()
self.window_ = Window( master=None, view=self )
self.window_.pack()
self.refresh()
self.onOpen()
self.tkRoot_.mainloop()
def onOpen( self ):
""" Do some initial tasks... """
def refresh( self ):
self.tkRoot_.update()
people
I have seen some tutorials showing how to create animations with PyGtk, the best one being this:
https://cairographics.org/cookbook/animationrotation/
This tutorial uses gobject.timeout_add() to set some clock to constantly refresh the animation.
What I wanted to do, instead, is to create FINITE animations, which I could trigger by clicking in some button or checkbox or anything.
For example, I could have a Window with a Button and a DrawingArea with a ball drawn.
When I click the button, the ball would go up and down, and stop. If I click the button again, the ball repeats the move.
In another scenario, the ball could be on the left of the screen. When I toggle a CheckBox, the ball goes (not instantly, but rather moves in a transition) to the right. If I uncheck, the ball comes back to its original position.
I am not planning to use anything else than pure Cairo/Gtk (no Clutter, no OpenGL, no PyGame), because I feel that it should be possible, and I want to go on studying Gtk to do some (future) GUI tricks with simple one-file scripts.
Since the question is about a broader problem, I think it is not needed to add code, but I could edit this question to put some code if anyone feels it would be better.
Thanks for reading and for any help! And Cairo/Gtk is great!
EDIT: after the precise explanation from Jeremy Flores, I came out with this code, which is useless (the ball goes to the right each time the button is clicked, untill falling off the window), but contains the elements upon which to build more creative and useful stuff. If anyone (including Jeremy) has anything to say about this code, specially about removing unnecessary parts, I would very gladly like to hear. Thanks!
#!/usr/bin/env python
import gtk, gobject
from math import pi
class Canvas(gtk.DrawingArea):
def __init__(self):
super(Canvas, self).__init__()
self.connect("expose_event", self.expose)
self.set_size_request(400,400)
self.x = 20
self.y = 20
self.counter = 0
def movealittle(self):
self.x += 1
self.counter += 1
self.queue_draw()
if self.counter <= 20:
return True
elif self.counter > 20:
self.counter = 0
return False
def expose(self, widget, event):
cr = widget.window.cairo_create()
rect = self.get_allocation()
w = rect.width
h = rect.height
cr.arc(self.x, self.y, 10, 0, 2*pi)
cr.fill()
def runanimation(widget):
gobject.timeout_add(5, canvas.movealittle)
print "runanimation call"
button = gtk.Button("Move")
button.connect("clicked", runanimation)
window = gtk.Window()
canvas = Canvas()
panel = gtk.VBox()
window.add(panel)
panel.pack_start(canvas)
panel.pack_start(button)
window.set_position(gtk.WIN_POS_CENTER)
window.show_all()
gtk.main()
gobject.timeout_add() can be used for finite animations. The callback you set up will keep being called until it returns False, at which point your method won't be called again.
For example, if you want a ball to be animated for 5 seconds, you would be responsible for determining how long the animation has been going, and then once it has passed 5 seconds, your method would return False to end the animation. When you want the animation to start again, simply re-register the callback with timeout_add and reset your time counter.
See: http://www.pygtk.org/pygtk2reference/gobject-functions.html#function-gobject--timeout-add for more information.