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

img_sample_1.jpg

   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

source.zip


Additional Information

Link :

- - - - -

https://wiki.wxpython.org/TitleIndex

https://docs.wxpython.org/

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..

Active bitmaps or quasi-animation (Phoenix) (last edited 2023-11-28 09:24:54 by Ecco)

NOTE: To edit pages in this wiki you must be a member of the TrustedEditorsGroup.