Active bitmaps or quasi-animation (Phoenix)
Keywords : Active bitmaps, Animation.
Demonstrating :
Tested Python 3.8.10, wxPython 4.2.1 gtk3 (phoenix), wxWidgets 3.2.2.1, Pillow versions 9.1.0 and 9.5.0, Linux.
Tested py3.x, wx4.x and Win10.
Are you ready to use some samples ?
Test, modify, correct, complete, improve and share your discoveries !
As the source is too heavy for wxpywiki, please use the link to download all useful files :
Latest version here : https://discuss.wxpython.org/t/activebitmaps-or-quasi-animation/36491
1 '''
2 ActiveBitmap.py
3
4 A function based on wx.StaticBitmap, capable of accepting multiple bitmaps that can be cycled through using a Timer
5 a quasi-animation if you will.
6 It can utilise existing Gif files, either broken down into a list of the component parts, or a valid Gif filename,
7 with the caveat that they should not have been optimised for size ( see below )
8
9 ActiveBitmap will utilise PIL - Pillow (Fork of the Python Imaging Library), if available, for speed purposes,
10 when processing Gif files or animated WEBP files.
11
12 Based originally on the requirement to be able to flash a bitmap to denote that something relevant or of note had occurred.
13 or an alternative for a pulse progress dialog to denote something is happening
14
15 ActiveBitmap will accept between 1 and many bitmaps, which once activated, will display one after the other,
16 using a predefined time lapse between each image.
17 Upon reaching the last bitmap or defined end of range, it cycles around to the beginning of the series or start of range
18 and starts again.
19 This continues until stopped, or a given time period or a given number of repetitions.
20
21 (This means GIF files can be converted or partially converted to a list (series) using wx.Image, or pass a Gif file
22 and allow ActiveBitmap to convert it - See below)
23
24 Bitmaps can be inserted into the series at a given point and removed.
25 A sub-set of the images to be used can be defined
26 The animation can be restricted by time or number of repetitions
27
28 The series may also be extended by one or more new bitmaps.
29
30 Cycling through the series can be stopped and started at will.
31 Activate(True) starts the image cycling.
32 Activate(True, delay=n) starts the image cycling at delay Seconds in the future
33 Activate(False) immediately stop cycling at the current image.
34 Activate(False, delay=n) stop cycling at delay Seconds in the future and Reset
35
36 Reset() immediately stop cycling and set the image to the first supplied image.
37 StopAt(index) stop cycling when the image reaches the index.
38
39
40 The time period for cycling can be set to whatever you define in milliseconds and is changeable during operation.
41
42 All supplied bitmaps are scaled to the same size as:
43 the size parameter if supplied;
44 the size of the smallest bitmap in the series.
45 Any subsequent Additions or Extensions to the series are scaled to the same size.
46
47 wx.NullBitmap can used as a valid placeholder or visual gap in the series.
48
49 The current image index can be queried or Set
50
51 Responds to Mouse events like a normal StaticBitmap
52
53
54 Author: J Healey
55 Created: 19/04/2023
56 Copyright: J Healey - 2023
57 License: GPL 3 or any later version
58 Version: 1.2.2
59 Email: <rolfofsaxony@gmx.com>
60 Written & tested: Linux, Python 3.8.10, wxPython 4.2.1 gtk3 (phoenix) wxWidgets 3.2.2.1, Pillow versions 9.1.0 and 9.5.0
61
62 Changelog:
63 1.2.2 Bug fix
64 When stopping an ActiveBitmap, if a delayed start or a delayed stop was still progress, the timers
65 were not stopped.
66 This could lead to a coredump if the programmer had stopped the ActiveBitmap and then destroyed it.
67 Function Stop(), now stops all timers.
68
69 1.2.1 Add ability to process an animated png (x.apng) file as if it were a gif file.
70 imghdr cannot recognise an animated png so this version of ActiveBitmap drops the import of imghdr and
71 identifies webp, gif and apng files by the presence of 'loop' in the image's PIL info dictionary.
72 The loading of an animated png file relies on the PIL image library, as wx.Image does not differentiate
73 between a .apng file and a normal .png file, thus rendering it as a single image.
74
75 1.2.0 Add the PIL image library as it processes images up to 50 times quicker than wx.Image,
76 depending on image size and the number of images to process
77 The PIL library also seems to cope with 'optimised' Gif files and animated .Webp files
78
79 Include thread for rescaling the images, as this allows us to start displaying the images,
80 as soon as the first one is ready.
81 (ActiveBitmap will cycle through available images, adding to the animation as more images become available)
82
83 The PIL library remains optional. If it is not available ActiveBitmap reverts to wx.Image.
84
85 The test for a single image at Activate() checks to ensure that the Scaling thread isn't running,
86 which would signify that more images are currently be produced.
87
88 Include File Error bitmaps in case something goes wrong during Gif conversion.
89
90 1.1.0 Ensure that illegal values cannot be set in the SetRange function
91 Test for missing bitmap_list, allowing easier definition of a dynamic resizing bitmap,
92 rather than an animation
93 Don't start the period timer is there is only a single bitmap.
94
95 Default class constructor.
96
97 @param parent: Parent window. Must not be None.
98 @param id: identifier. A value of -1 indicates a default value.
99 @param bitmap: Primary bitmap to use
100 @param pos: Position. If the position (-1, -1) is specified
101 then a default position is chosen.
102 @param size: If the default size (-1, -1) is specified then the size is is based on the size of
103 the primary bitmap.
104 @param style: wx.Border style default wx.TRANSPARENT_WINDOW|wx.BORDER_NONE
105 @param bitmap_list:
106 A list containing zero or more wx.Bitmap entries that will by cycled through when activated
107 wx.NullBitmap is a valid entry
108 A single Gif filename is a valid entry
109 @param name: Widget name.
110
111 Functions:
112
113 Activate(boolean, delay=n, repetitions=-1)
114 True Start image cycling
115 False Stop image cycling
116 delay will Start or Stop at delay Seconds in the future depending on the boolean
117 repetitions, the number times to play the sequence, -1 equals forever
118 Defaults: True , -1, -1
119
120 SetTimerPeriod(period=n) Wait Period in milliseconds before cycling to next image
121 If this is used whilst the timer is currently running, it is
122 applied instantly allowing you to speed up or slow down the
123 current rate of cycling.
124 Default: 1000
125
126 GetTimerPeriod() Get current Wait Period in milliseconds
127
128 Reset() Stop cycling and revert image to primary image
129
130 StopAt(index=0) Stop cycling when image next reaches this index
131
132 Stop() Alternate to Activate(False)
133
134 Start() Alternate to Activate(True)
135
136 GetIndex() Return current image index
137
138 SetIndex(index=0) Set current image index
139
140 Extend(extension_list=[]) Extend current list of images with a list of extra wx.Bitmaps
141
142 InsertBitmap(index=0, bitmap=wx.NullBitmap)
143 Insert a wx.Bitmap at index
144
145 DeleteBitmap(index=0) Delete bitmap at position index
146
147 GetCount() Return the number of images currently in use
148
149 SetRange(start=0, end=999) Set a sub-set of images to cycle through
150
151 GetRange() Returns the start and end of the current cycle range
152
153 GetFrameBitmap(index) Return indexed image as a wx.Bitmap
154
155 GetFrameImage(index) Return indexed image as a wx.Image
156
157 IsRunning() Return True or False if the control is Active
158
159 Converting a GIF file to an ActiveBitmap
160 ----------------------------------------
161 The speed of the animation is governed by period timer, irrespective of the original speed of the gif file.
162
163 Method 1
164 --------
165 Allow ActiveBitmap to extract the images from the Gif file
166 When creating the control, specify the bitmap to be a wx.NullBitmap and then give the Gif filename as the only entry
167 in the bitmap_list
168 e.g.
169 Abitmap = ActiveBitmap(panel, -1, bitmap=wx.NullBitmap, bitmap_list = ['led1s.gif',])
170
171 Method 2 extract the Gif file images manually, then pass them as a list, for PIL see the demonstration code
172 --------
173 gif_list = []
174 f = 'led1s.gif'
175 cnt = wx.Image.GetImageCount(f)
176 im = wx.Image()
177 for i in range(cnt):
178 im.LoadFile(f, index=i)
179 bmp = im.ConvertToBitmap(depth=32)
180 im.Clear()
181 bmp.UseAlpha()
182 gif_list.append(bmp)
183 if not gif_list: # ensure at least 1 image if there's a problem
184 gif_list.append(wx.NullBitmap)
185
186 The list 'gif_list' can now be set as the bitmap_list and the initial bitmap set to gif_list[0]
187 Speed of loading can be an issue if there are many images in the gif.
188 Modifying the options in range() you could convert just every 3rd image i.e. for i in range(0, count, 3)
189 or a group of images in the middle e.g. for i in range(20, 50)
190 or reverse the images e.g. for i in range(60, 20, -1)
191
192 When creating the control, specify the bitmap to be the first entry in the gif_list and then specify the gif_list
193 as the bitmap_list
194 e.g. Abitmap = ActiveBitmap(panel, -1, bitmap=gif_list[0], bitmap_list = gif_list)
195
196 NOTE:
197 Gif files can be optimised i.e. they aren't strictly a batch of images of the same size.
198 Optimisation is often acheived by creating frame images that contain only the differences in pixels from the previous frame.
199 Obviously this can reduce the file size immensely but to play the animation, you need a more sophisticated tool
200 than ActiveBitmap e.g. wx.adv.Animation and wx.adv.AnimationCtrl
201 imagemagick's 'identify' command will reveal this, as the individual image sizes will differ or be the same.
202 If they differ don't use ActiveBitmap OR grab a copy of a program called gifsicle, which has an unoptimise option
203 gifsicle --unoptimize -i optimised.gif > unoptimised.gif
204 and use the unoptimised output.
205
206 Using PIL instead of relying om wx.Image will also handle optimised gif files seamlessly for you
207 ActiveBitmap will attempt to load PIL for you, if it is on your machine
208
209 Example program
210
211 import wx
212 from ActiveBitmap import ActiveBitmap
213 class Frame(wx.Frame):
214 def __init__(self, parent):
215 wx.Frame.__init__(self, parent, -1, "ActiveBitmap Demonstration", size=(320, 450))
216 panel = wx.Panel(self)
217 box = wx.StaticBox(panel, wx.ID_ANY, "Animations", pos=(10, 10), size=(300, 400))
218 bitmap1 = ActiveBitmap(box, -1, pos=(10, 10), bitmap=wx.NullBitmap, bitmap_list=['cube_s.gif',])
219 bitmap1.SetTimerPeriod(50)
220
221 bitmap2 = ActiveBitmap(box, -1, pos=(10, 150), bitmap=wx.Bitmap('amber.png'), \
222 bitmap_list=[wx.Bitmap('red.png'), wx.Bitmap('green.png'),])
223 bitmap2.SetTimerPeriod(250)
224
225 bitmap3 = ActiveBitmap(box, -1, pos=(10, 200), size=(200, -1), bitmap=wx.NullBitmap, bitmap_list=['led1s.gif',])
226 bitmap3.SetTimerPeriod(50)
227
228 self.Show()
229
230 bitmap1.Activate()
231 bitmap2.Activate()
232 bitmap3.Activate()
233
234 app = wx.App()
235 frame = Frame(None)
236 app.MainLoop()
237
238
239 '''
240 import wx
241 import threading
242 import os
243 try:
244 from PIL import Image, ImageSequence
245 pil_loaded = True
246 except Exception:
247 pil_loaded = False
248
249 #-------------------------------------------------------------------------------
250
251 class GradientPanel(wx.Panel):
252 def __init__(self, parent, initialColour="#ffffff", destColour="#7f7f7f", singleGradient=False, \
253 direction=wx.SOUTH, inherit=False):
254 wx.Panel.__init__(self, parent)
255
256 self.Bind(wx.EVT_ERASE_BACKGROUND, self.OnEraseBackground)
257 self.Bind(wx.EVT_SIZE, self.OnResize)
258
259 if inherit:
260 self.initial = parent.GetBackgroundColour()
261 self.dest = wx.Colour(self.initial).ChangeLightness(170)
262 else:
263 try:
264 self.initial = wx.Colour(initialColour)
265 except AssertionError:
266 self.initial = wx.Colour("#ffffff")
267 try:
268 self.dest = wx.Colour(destColour)
269 except AssertionError:
270 self.dest = wx.Colour("#7f7f7f")
271
272 if singleGradient: # test initial colour to decide gradient lighter or darker
273 if self.initial.RGB > wx.Colour("#7f7f7f").RGB:
274 chg = 60
275 else:
276 chg = 170
277 self.dest = wx.Colour(self.initial).ChangeLightness(chg)
278
279 self.dir = direction
280 #------------
281
282 # Simplified init method.
283 self.SetProperties()
284
285 #---------------------------------------------------------------------------
286
287 def SetProperties(self):
288 if wx.Platform == "__WXMSW__":
289 self.SetDoubleBuffered(True) # No flicker
290
291 def OnResize(self, event):
292 self.Refresh()
293 event.Skip()
294
295 def OnEraseBackground(self, event):
296 dc = event.GetDC()
297 dc.GradientFillLinear(self.GetClientRect(),
298 self.initial, self.dest,
299 self.dir)
300 #event.Skip() # don't use
301
302 #-------------------------------------------------------------------------------
303
304 class ActiveBitmap(wx.StaticBitmap):
305 def __init__(self, parent, id=wx.ID_ANY, bitmap=wx.NullBitmap, pos=wx.DefaultPosition,
306 size=wx.DefaultSize, style=wx.BORDER_NONE|wx.TRANSPARENT_WINDOW, bitmap_list=None, name=''):
307 wx.StaticBitmap.__init__(self, parent, id, bitmap, pos, size, style, name)
308
309 self.bitmaps = []
310 self.bitmap_index = 0
311 if not bitmap_list:
312 self.bitmap_list = []
313 else:
314 self.bitmap_list = bitmap_list
315 self._size = size
316 self._stop_at = -1
317 self.range_start = 0
318 self.range_end = 999
319 self.repetitions = -1
320 self.timer_period = 1000 # default timer period = 1 second
321 self.name = name
322
323 bsize = self.SizeBitmap(bitmap)
324
325 # perform scaling via thread allows for starting showing bitmaps as soon as the first one is ready
326 self.thread = ScaleThread(self, bsize)
327
328 while not self.bitmaps:
329 wx.MilliSleep(100)
330 self.bitmap = self.bitmaps[0]
331 wx.StaticBitmap.SetBitmap(self, self.bitmap)
332
333 self.timer = wx.Timer(self)
334 self.delay_start_timer = wx.Timer(self)
335 self.delay_stop_timer = wx.Timer(self)
336
337 self.Bind(wx.EVT_TIMER, self.OnNext, self.timer)
338 self.Bind(wx.EVT_TIMER, lambda event: self.OnDelay(event, True), self.delay_start_timer)
339 self.Bind(wx.EVT_TIMER, lambda event: self.OnDelay(event, False), self.delay_stop_timer)
340
341 def SizeBitmap(self, initial_bitmap):
342 '''
343 In case a wx.NullBitmap is passed as the initial bitmap, hunt for the first valid bitmap to ascertain size
344 Also validate bitmap_list in case of wx.NullBitmap or rebuild bitmap list if it's a Gif file
345 '''
346 valid_bmp = wx.Bitmap.FromRGBA(1, 1, red=255, green=255, blue=255, alpha=0)
347 fb = ErrorBitmap()
348 failed_bmps = [fb.failed1_bmp, fb.failed2_bmp]
349 w, h = self._size
350 sizes = [(w, h),]
351
352 # is the bitmap list a gif file if so generate a new bitmap_list from it.
353 if self.bitmap_list:
354 try:
355 f = self.bitmap_list[0]
356 except Exception:
357 f = None
358 else:
359 f = None
360 try:
361 im = Image.open(f)
362 except FileNotFoundError:
363 im = None
364 self.bitmap_list = list(failed_bmps)
365 w = 100
366 h = 30
367 except Exception as e:
368 im = None
369
370 if im and 'loop' in im.info:
371 self.bitmap_list = []
372 # ŧest for the PIL library as it's up to 50 times quicker in comparison and handles optimised Gif files
373 if pil_loaded:
374 pil_scaled = False
375 if w >=0 and h >=0:
376 pil_scaled = True
377 im = Image.open(f)
378 seq = ImageSequence.all_frames(im)
379 for frame in seq:
380 if pil_scaled:
381 frame = frame.resize((w, h))
382 bmp = wx.Bitmap.FromBufferRGBA(frame.width, frame.height, frame.convert("RGBA").tobytes())
383 if bmp.HasAlpha():
384 bmp.UseAlpha()
385 self.bitmap_list.append(bmp)
386 else:
387 im = wx.Image()
388 im.SetOption(wx.IMAGE_OPTION_GIF_TRANSPARENCY, wx.IMAGE_OPTION_GIF_TRANSPARENCY_UNCHANGED)
389 cnt = wx.Image.GetImageCount(f)
390 for i in range(cnt):
391 im.LoadFile(f, type=wx.BITMAP_TYPE_ANY, index=i)
392 bmp = im.ConvertToBitmap(depth=32)
393 im.Clear()
394 if bmp.HasAlpha():
395 bmp.UseAlpha()
396 self.bitmap_list.append(bmp)
397 else:
398 # insert the initial bitmap at the front of the list for sizing
399 if initial_bitmap != wx.NullBitmap:
400 self.bitmap_list.insert(0, initial_bitmap)
401
402 if not self.bitmap_list: # ensure at least 1 image if there's a problem
403 self.bitmap_list.append(wx.Bitmap(fb.failed1_bmp))
404
405 bsize = (-1, -1)
406 # validate bitmap list
407 for i, b in enumerate(self.bitmap_list):
408 try:
409 bsize = b.GetSize()
410 sizes.append((bsize.x, bsize.y))
411 except AssertionError:
412 self.bitmap_list[i] = valid_bmp
413
414 # find smallest tuple where <=1 is absent
415 try:
416 bsize = min(y for y in sizes if y[0] > 1 and y[1] > 1)
417 except Exception:
418 bsize = (1, 1)
419
420 # Calculate the dimension ratio in case the supplied size parameter is missing a dimension e.g. (30, -1)
421 if self._size != wx.DefaultSize:
422 if w <=0 or h <=0:
423 ratio = bsize[1] / bsize[0]
424 if h < 0 and w >= 0:
425 h = int(w * ratio)
426 if w < 0 and h >= 0:
427 w = int(h / ratio)
428
429 if w >=0 and h >=0:
430 bsize = (w, h)
431 self.SetMinSize(bsize)
432 self.SetSize(bsize)
433 return bsize
434
435 def Activate(self, arg=True, delay=-1, repetitions=-1):
436 if len(self.bitmaps) < 2 and not self.thread.is_alive(): # single image it's a staticbitmap don't cycle
437 if self.timer.IsRunning():
438 self.timer.Stop()
439 return
440 if delay > 0: # Delayed activation
441 if arg:
442 self.delay_start_timer.StartOnce(delay * 1000)
443 else:
444 self.delay_stop_timer.StartOnce(delay * 1000)
445 return
446
447 if arg is True:
448 self.bitmap_index = self.range_start
449 self.timer.Start(self.timer_period)
450 else:
451 if self.timer.IsRunning():
452 self.timer.Stop()
453 self.repetitions = repetitions
454
455 def OnDelay(self, event, arg):
456 if arg:
457 self.Activate(arg)
458 else:
459 self.Reset()
460
461 def OnNext(self, event=None):
462 self.bitmap = self.bitmaps[self.bitmap_index]
463 wx.StaticBitmap.SetBitmap(self, self.bitmap)
464 if self._stop_at > -1 and self._stop_at == self.bitmap_index:
465 self._stop_at = -1
466 self.timer.Stop()
467 self.Refresh()
468 self.bitmap_index += 1
469 if self.bitmap_index >= len(self.bitmaps) and self.repetitions == 0:
470 self.Reset()
471 if self.bitmap_index >= len(self.bitmaps) or self.bitmap_index >= self.range_end:
472 self.repetitions -= 1
473 self.bitmap_index = self.range_start
474
475 def SetTimerPeriod(self, period=1000):
476 self.timer_period = period
477 if self.timer.IsRunning():
478 self.timer.Start(self.timer_period)
479
480 def GetTimerPeriod(self):
481 return self.timer_period
482
483 def Reset(self):
484 if self.timer.IsRunning():
485 self.timer.Stop()
486 wx.StaticBitmap.SetBitmap(self, self.bitmaps[0])
487 self.bitmap_index = len(self.bitmaps) - 1
488 self.Refresh()
489
490 def StopAt(self, index=0):
491 self._stop_at = index
492
493 def Stop(self):
494 if self.timer.IsRunning():
495 self.timer.Stop()
496 self.delay_start_timer.Stop()
497 self.delay_stop_timer.Stop()
498
499 def Start(self):
500 self.bitmap_index = self.range_start
501 self.timer.Start(self.timer_period)
502
503 def GetIndex(self):
504 return self.bitmap_index
505
506 def GetCount(self):
507 return len(self.bitmaps)
508
509 def GetFrameBitmap(self, index=-1):
510 if index >= 0 and index < len(self.bitmaps) - 1:
511 return self.bitmaps[index]
512
513 def GetFrameImage(self, index=-1):
514 if index >= 0 and index < len(self.bitmaps) - 1:
515 return self.bitmaps[index].ConvertToImage()
516
517 def SetRange(self, start=0, end=999):
518 if start >= 0 and start <= len(self.bitmaps) - 1:
519 self.range_start = start
520 else:
521 self.range_start = 0
522 if end >= self.range_start and end <= len(self.bitmaps) - 1:
523 self.range_end = end
524 else:
525 self.range_end = len(self.bitmaps) - 1
526
527 def GetRange(self):
528 return self.range_start, self.range_end
529
530 def SetIndex(self, index=0):
531 self.bitmap_index = index
532 if self.bitmap_index >= len(self.bitmaps) or self.bitmap_index >= self.range_end:
533 self.bitmap_index = self.range_start
534
535 def Extend(self, extension_list = []):
536 if len(self.bitmaps): # Check if at least one image is available
537 bsize = self.bitmaps[0].GetSize()
538 else:
539 bsize = self.GetMinSize()
540
541 for b in extension_list:
542 img = b.ConvertToImage()
543 img.Rescale(bsize[0], bsize[1], quality=wx.IMAGE_QUALITY_HIGH)
544 bmp = img.ConvertToBitmap(depth=32)
545 bmp.UseAlpha()
546 self.bitmaps.append(bmp)
547
548 def InsertBitmap(self, index=0, bitmap=wx.NullBitmap):
549 bsize = self.bitmaps[0].GetSize()
550 img = bitmap.ConvertToImage()
551 img.Rescale(bsize[0], bsize[1], quality=wx.IMAGE_QUALITY_HIGH)
552 bmp = img.ConvertToBitmap(depth=32)
553 bmp.UseAlpha()
554 self.bitmaps.insert(index, bmp)
555
556 def DeleteBitmap(self, index=0):
557 if index <= len(self.bitmaps) -1:
558 x = self.bitmaps.pop(index)
559
560 def IsRunning(self):
561 return self.timer.IsRunning()
562
563 def DoGetBestSize(self):
564 return self.bitmap.GetSize()
565
566 def Enable(self, arg=True):
567 wx.StaticBitmap.Enable(self, arg)
568 self.Refresh()
569
570 def Disable(self, arg=False):
571 wx.StaticBitmap.Enable(self, arg)
572 self.Refresh()
573
574 def OnEraseBackground(self,event):
575 pass
576
577 def OnSize(self, event):
578 event.Skip()
579 self.Refresh()
580
581 def SetWindowStyle(self, style):
582 self._style = style
583 self.Refresh()
584
585 class ScaleThread(threading.Thread):
586 '''
587 Scale bitmaps via a thread for speed up
588 '''
589 def __init__(self,parent_target, bsize):
590 threading.Thread.__init__(self)
591 self.parent = parent_target
592 self.bitmaps = self.parent.bitmaps
593 self.bitmap_list = self.parent.bitmap_list
594 self.bsize = bsize
595 self.start() # start the thread
596
597 def run(self):
598 # Resize bitmaps to one standard size
599 for b in self.bitmap_list:
600 img = b.ConvertToImage()
601 img.Rescale(self.bsize[0], self.bsize[1], quality=wx.IMAGE_QUALITY_HIGH)
602 bmp = img.ConvertToBitmap(depth=32)
603 if bmp.HasAlpha():
604 bmp.UseAlpha()
605 self.bitmaps.append(bmp)
606 return
607
608 class ErrorBitmap():
609 '''
610 Create a bitmap of text for use if there's a file error
611 '''
612 def __init__(self):
613 self.failed1_bmp = wx.Bitmap(100, 30)
614 dc = wx.MemoryDC()
615 dc.SelectObject(self.failed1_bmp)
616 dc.Clear()
617 text = "File Error"
618 tw, th = dc.GetTextExtent(text)
619 dc.DrawText(text, int((100-tw)/2), int((30-th)/2))
620 dc.SelectObject(wx.NullBitmap)
621 self.failed2_bmp = wx.Bitmap(100, 30)
622 dc = wx.MemoryDC()
623 dc.SelectObject(self.failed2_bmp)
624 dc.Clear()
625 dc.DrawText(text, int((100-tw)/2)+2, int((30-th)/2))
626 dc.SelectObject(wx.NullBitmap)
627
628 class Demonstration(wx.Frame):
629 import wx.lib.scrolledpanel as scrolled
630 def __init__(self):
631 wx.Frame.__init__(self, None, -1, title='Active Bitmap/Animation Demonstration', size=(500, 300))
632
633 fb = ErrorBitmap()
634 failed_bmps = [fb.failed1_bmp, fb.failed2_bmp]
635
636 # gif preload example
637 # loading the gif images can be slow so if possible arrange to do this before hand
638 # or dispense with this and let ActiveBitmap extract the images from the gif for you
639
640 gif_list = []
641 #f = 'cube_s.gif' # pre-extract this Gif file for bitmap1 below
642 #f = 'led1s.gif' # pre-extract this Gif file for bitmap1 below
643 f = None # Don't pre-extract a Gif file for bitmap1 below
644 # Either it will be normal image files or ActiveBitmap will extract
645 # the Gif file for you depending on the definition of bitmap1 below
646
647 # Optional Pre-extraction routine with PIL as it's 50 times quicker in comparison and handles optimised Gif files
648 if pil_loaded and f:
649 try:
650 im = Image.open(f)
651 except FileNotFoundError:
652 im = None
653 else:
654 im = None
655
656 if im and 'loop' in im.info:
657 print("Pre-loading", f)
658 im = Image.open(f)
659 seq = ImageSequence.all_frames(im)
660 for frame in seq:
661 bmp = wx.Bitmap.FromBufferRGBA(frame.width, frame.height, frame.convert("RGBA").tobytes())
662 if bmp.HasAlpha():
663 bmp.UseAlpha()
664 gif_list.append(bmp)
665
666 if not gif_list: # ensure at least 1 image if there's a problem
667 gif_list.extend(failed_bmps)
668
669 # Pre-extraction routine ends
670
671 panel = Demonstration.scrolled.ScrolledPanel(self, -1, style=wx.TAB_TRAVERSAL)
672 panel.SetupScrolling()
673 panel.SetBackgroundColour("#e6e6fa") # lavender
674 #panel = GradientPanel(self, initialColour="#30f030", destColour="#ff3030", \
675 # inherit=False, singleGradient=False, direction=wx.SOUTH)
676
677 self.text = wx.StaticText(panel, -1, label="My animated bitmaps")
678 self.text.SetForegroundColour(wx.BLUE)
679
680 # standard NON Gif example a list of image files converted to wx.Bitmaps
681 #self.bitmap1 = ActiveBitmap(panel, -1, bitmap=wx.Bitmap('1.png'), size=(30, 30), name="Bitmap 1", \
682 # bitmap_list = [wx.Bitmap('2.png'), wx.Bitmap('3.png'), wx.Bitmap('4.png'),])
683
684 # gif example of a pre-extracted Gif ( see above )
685 #self.bitmap1 = ActiveBitmap(panel, -1, bitmap=gif_list[0], bitmap_list = gif_list, name="Gif_preloaded")
686
687 # straight Gif example passes the Gif filename as the lone argument to bitmap_list
688 self.bitmap1 = ActiveBitmap(panel, -1, bitmap=wx.NullBitmap, bitmap_list = ['hourglass.gif',], size=(90, 100), name="Hourglass")
689
690 self.bitmap2 = ActiveBitmap(panel, -1, bitmap=wx.Bitmap('amber.png'), name="Bitmaps", \
691 bitmap_list = [wx.Bitmap('red.png'), wx.Bitmap('green.png'),])
692
693 self.bitmap3 = ActiveBitmap(panel, -1, bitmap=wx.NullBitmap, bitmap_list=['load1.gif',], name="Load")
694 self.bitmap3.SetTimerPeriod(200)
695
696 buttonReset = wx.Button(panel, -1, label="Reset")
697 buttonActivate = wx.Button(panel, -1, label="Activate")
698 buttonStop = wx.Button(panel, -1, label="Stop")
699
700 sizer = wx.BoxSizer(wx.HORIZONTAL)
701 sizer.Add(self.text, 0, wx.ALIGN_CENTER_VERTICAL|wx.ALL, 20)
702 sizer.Add(self.bitmap1, 0, wx.ALIGN_CENTER_VERTICAL)
703
704 sizer2 = wx.BoxSizer(wx.HORIZONTAL)
705 sizer2.Add(self.bitmap2, 0, wx.ALIGN_CENTER_VERTICAL)
706
707 sizer3 = wx.BoxSizer(wx.HORIZONTAL)
708 sizer3.Add(self.bitmap3, 0, wx.ALIGN_CENTER_VERTICAL)
709
710 buttonsizer = wx.BoxSizer(wx.HORIZONTAL)
711
712 buttonsizer.Add(buttonReset)
713 buttonsizer.Add(buttonActivate)
714 buttonsizer.Add(buttonStop)
715
716 mainsizer = wx.BoxSizer(wx.VERTICAL)
717 mainsizer.Add(sizer, 0, wx.ALIGN_CENTER|wx.ALL, 10)
718 mainsizer.Add(sizer2, 0, wx.ALIGN_CENTER|wx.ALL, 10)
719 mainsizer.Add(sizer3, 0, wx.ALIGN_CENTER|wx.ALL, 10)
720 mainsizer.Add(buttonsizer, 0, wx.ALIGN_CENTER|wx.ALL, 10)
721
722 buttonReset.Bind(wx.EVT_BUTTON, self.OnReset)
723 buttonActivate.Bind(wx.EVT_BUTTON, self.OnActivate)
724 buttonStop.Bind(wx.EVT_BUTTON, self.OnStop)
725
726 # ActiveBitmaps are clickable
727 self.bitmap1.Bind(wx.EVT_LEFT_DOWN, self.OnClick)
728 self.bitmap2.Bind(wx.EVT_LEFT_DOWN, self.OnClick)
729 self.bitmap3.Bind(wx.EVT_LEFT_DOWN, self.OnClick)
730
731 panel.SetSizer(mainsizer)
732
733 # timer spèed if playing the gif file
734 self.bitmap1.SetTimerPeriod(200)
735 # standard timer speed
736 #self.bitmap1.SetTimerPeriod(500)
737
738 #self.bitmap1.SetRange(50, 80) # Animate a sub-set of the loaded images from image 50 to image 80
739
740 self.bitmap1.Activate(True, delay=0, repetitions=-1)
741
742 #self.bitmap1.Activate(True, repetitions=3) # Start animation and Stop after 3 repetitions
743 #self.bitmap1.Activate(True, delay=10) # Start animation after 10 seconds
744 #self.bitmap1.Activate(False, delay=10) # Stop animation after 10 seconds
745 #self.bitmap1.StopAt(30) # Stop animation at image 30
746
747 self.bitmap2.SetTimerPeriod(500)
748 self.bitmap2.Activate(True) # Start immediately
749 self.bitmap2.Activate(False, delay=20) # Stop animation after 20 seconds
750 self.bitmap2.SetToolTip("Cycles Amber, Red and Green for 20 seconds")
751
752 self.bitmap3.Activate()
753
754 self.Show()
755
756 def OnReset(self, event):
757 self.bitmap1.Reset()
758 self.text.SetLabel("Reset and Stopped")
759 # Various Testing options
760 #for i in range(0, self.bitmap1.GetCount()):
761 # self.bitmap1.DeleteBitmap(index=0)
762 #self.bitmap1.Extend([wx.Bitmap('./cube1.jpg'), wx.Bitmap('./cube2.jpg'), wx.Bitmap('./cube3.jpg'),])
763 #self.bitmap1.SetTimerPeriod(int(self.bitmap1.GetTimerPeriod()/2)) # double the speed of the animation
764 #self.bitmap1.Extend([wx.Bitmap('5.png'),])
765 #self.bitmap1.DeleteBitmap(index=1)
766 #self.bitmap1.InsertBitmap(index=1, bitmap=wx.Bitmap('5.png'))
767
768 def OnActivate(self, event):
769 self.bitmap1.Activate(True)
770 self.bitmap2.Activate(True) # Start immediately
771 self.bitmap2.Activate(False, delay=20) # Stop animation after 20 seconds
772 self.text.SetLabel("My animated bitmaps")
773
774 def OnStop(self, event):
775 self.bitmap1.Activate(False)
776 self.text.SetLabel("Stopped")
777
778 def OnClick(self, event):
779 obj = event.GetEventObject()
780 print(event.GetClassName(), obj.name, "Mouse Button", event.GetButton(), "Clicked")
781 print("Running", obj.IsRunning(), "at frame", obj.GetIndex(), "of", obj.GetCount(), "\n")
782
783
784 if __name__ == '__main__':
785 app = wx.App()
786 frame = Demonstration()
787 app.MainLoop()
Download source
Additional Information
Link :
- - - - -
https://wiki.wxpython.org/TitleIndex
As the source is too heavy for wxpywiki, please use the link to download all useful files.
https://discuss.wxpython.org/t/activebitmaps-or-quasi-animation/36491
Thanks to
J. Healey, the wxPython community...
About this page
Date(d/m/y) Person (bot) Comments :
26/11/23 - Ecco (Created page for wxPython Phoenix).
Comments
- blah, blah, blah..