Question on images and bitmaps

I want to cycle through all the images in a folder and .display the images sized to fit in a container, and centred, while maintaining the aspect ratio. I also want any unused areas of the container to be black.

I am having several issues.

While the wxPython docs show me the methods and parameters, the methods and properties don’t always do what I expect. In many cases the description just parrots the method name without explaining what it does. For example, I have a Static Bitmap that I do

self.bitmap.SetScaleMode(wx.StaticBitmap.Scale_AspectFit)

However, this doesn’t scale to fit. If an image is too large it is cropped. So I wrote my own scale method which works fine. Except that I would like the background to be black.

The first picture is being very small. I am currently displaying images only fullscreen do for now I have the onsize event disabled.

Can anyone offer suggestions? There is a lot of content in the wxPython docs regarding images and bitmaps, but very little on how to actually use them. Here is my code. Keep in mind it’s a work in progress.

import os
import sys
import wx
import wx.lib.mixins.inspection

ID_WHITE_BG   = wx.NewIdRef()
ID_BLACK_BG   = wx.NewIdRef()
ID_GREY_BG    = wx.NewIdRef()
ID_CHECK_BG   = wx.NewIdRef()
ID_NO_FRAME   = wx.NewIdRef()
ID_BOX_FRAME  = wx.NewIdRef()
ID_CROP_FRAME = wx.NewIdRef()


class MyApp(wx.App):

    def OnInit(self):
        self.frame = MyFrame(None, wx.ID_ANY, "")
        self.frame.SetSize(wx.DisplaySize())
        self.SetTopWindow(self.frame)
        self.frame.ShowFullScreen(True, wx.FULLSCREEN_ALL)
        return True


class MyFrame(wx.Frame):

    def __init__(self, *args, **kwds):
        # begin wxGlade: MyFrame.__init__
        kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)
        self.SetSize((400, 300))
        self.SetTitle("ImageView")

        panel = ImagePanel(self)


class ImagePanel(wx.Panel):
    """
    This control implements a basic image viewer. As the control is
    resized the image is resized (aspect preserved) to fill the panel.

    Methods:

        LoadImage(filename)
    """

    def __init__(self, parent, id=wx.ID_ANY,
                 pos=wx.DefaultPosition,
                 size=wx.DefaultSize,
                 style=wx.BORDER_SUNKEN
                 ):
        wx.Window.__init__(self, parent, id, pos, size, style=style)

        self.bitmap = wx.StaticBitmap(self, wx.ID_ANY)
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.bitmap, 1, wx.EXPAND, 0)
        self.SetSizer(sizer)
        self.image = None

        self.btnPrev = wx.Button(self, wx.ID_ANY, 'Prev')
        self.btnPrev.Visible = False
        self.Bind(wx.EVT_BUTTON, self.evt_prev, self.btnPrev)

        self.btnNext = wx.Button(self, wx.ID_ANY, 'Next')
        self.btnNext.Visible = False
        self.Bind(wx.EVT_BUTTON, self.evt_next, self.btnNext)

        self.btnExit = wx.Button(self, wx.ID_ANY, 'Exit')
        self.btnExit.Visible = False
        self.Bind(wx.EVT_BUTTON, self.evt_exit, self.btnExit)

        #self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Bind(wx.EVT_MOUSEWHEEL, self.on_scroll)

        #Define hotkeys
        hotkeys = [wx.AcceleratorEntry() for i in range(5)]
        hotkeys[0].Set(wx.ACCEL_NORMAL, wx.WXK_DOWN, self.btnNext.Id)
        hotkeys[1].Set(wx.ACCEL_NORMAL, wx.WXK_UP, self.btnPrev.Id)
        hotkeys[2].Set(wx.ACCEL_NORMAL, wx.WXK_RIGHT, self.btnNext.Id)
        hotkeys[3].Set(wx.ACCEL_NORMAL, wx.WXK_LEFT, self.btnPrev.Id)
        hotkeys[4].Set(wx.ACCEL_NORMAL, wx.WXK_ESCAPE, self.btnExit.Id)
        accel = wx.AcceleratorTable(hotkeys)
        self.SetAcceleratorTable(accel)

        # Get a list of all image files in the given folder
        self.files = self.GetFiles(os.getcwd())
        if not self.files:
            sys.exit()

        self.index = 0
        self.maxindex = len(self.files) - 1

        # Display the first image in the folder
        self.LoadImage(self.files[0])

    def evt_next(self, evt):
        """Display the next image in the list"""
        self.index = min(self.maxindex, self.index+1)
        self.LoadImage(self.files[self.index])
        if evt: evt.Skip()

    def evt_prev(self, evt):
        """Display the previous image in the list"""
        self.index = max(0, self.index-1)
        self.LoadImage(self.files[self.index])
        if evt: evt.Skip()

    def evt_exit(self, evt):
        sys.exit()

    def on_scroll(self, evt):
        """Display next or previous image depending on scroll direction"""
        if evt.WheelRotation < 0:
            self.evt_next(None)
        else:
            self.evt_prev(None)
        evt.Skip()

    def OnSize(self, event):
        """Resize image too fit container and redisplay"""
        self.image = self.ScaleToFit(self.image)
        self.DisplayImage(self.image)
        event.Skip()

    def LoadImage(self, file):
        """Load an image from a file, scale to fit, and display"""
        self.image = wx.Image(file, wx.BITMAP_TYPE_ANY)
        self.image = self.ScaleToFit(self.image)
        self.DisplayImage(self.image)

    def DisplayImage(self, image):
        self.bitmap.SetBitmap(self.image.ConvertToBitmap())

    def ScaleToFit(self, image):
        """Scale an image to fit its container"""

        # Get imsge size and container size
        cw, ch = self.Parent.GetSize()
        iw, ih = self.image.GetSize()

        aspect = ih / iw

        nw = cw
        nh = int(nw * aspect)

        if nh > ch:
            nh = ch
            nw = int(nh / aspect)

        return image.Scale(nw, nh)


    def GetFiles(self, folder):
        """Return a list of all image files in the given folder"""
        files = []
        images = ('.jpg','.jpeg','.gif','.png')

        for item in os.scandir(folder):
            #print(item.name)
            ext = os.path.splitext(item.name)[-1].lower()
            if ext in images:
                files.append(item.name)
                #print(item.name)

        return files

if __name__ == "__main__":

    os.chdir(r'd:\my pictures\.current')

    app = MyApp(0)
    app.MainLoop()

Rookie mistake. I inadvertently left in a SetSize in the Frame init. Once I removed it I no longer had the problem with the first image. That leaves me with two problems.

  1. I would like the background to be black, not white
  2. The first few images are centered with borders on the left and right. After a few of these I get an image that is left justified. Following that, everything is left justified, even images that were previously centered.
import os
import sys
import wx
import wx.lib.mixins.inspection

ID_WHITE_BG   = wx.NewIdRef()
ID_BLACK_BG   = wx.NewIdRef()
ID_GREY_BG    = wx.NewIdRef()
ID_CHECK_BG   = wx.NewIdRef()
ID_NO_FRAME   = wx.NewIdRef()
ID_BOX_FRAME  = wx.NewIdRef()
ID_CROP_FRAME = wx.NewIdRef()


class MyApp(wx.App):

    def OnInit(self):
        self.frame = MyFrame(None, wx.ID_ANY, "")
        self.SetTopWindow(self.frame)
        return True


class MyFrame(wx.Frame):

    def __init__(self, *args, **kwds):
        # begin wxGlade: MyFrame.__init__
        kwds["style"] = kwds.get("style", 0) | wx.DEFAULT_FRAME_STYLE
        wx.Frame.__init__(self, *args, **kwds)
        self.SetSize(wx.DisplaySize())
        self.SetTitle("ImageView")

        panel = ImagePanel(self)
        self.ShowFullScreen(True, wx.FULLSCREEN_ALL)


class ImagePanel(wx.Panel):
    """
    This control implements a basic image viewer. As the control is
    resized the image is resized (aspect preserved) to fill the panel.

    Methods:

        LoadImage(filename)
    """

    def __init__(self, parent, id=wx.ID_ANY,
                 pos=wx.DefaultPosition,
                 size=wx.DefaultSize,
                 style=wx.BORDER_SUNKEN
                 ):
        wx.Window.__init__(self, parent, id, pos, size, style=style)

        self.bitmap = wx.StaticBitmap(self, wx.ID_ANY)
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.bitmap, 1, wx.EXPAND, 0)
        self.SetSizer(sizer)
        self.image = None

        self.btnPrev = wx.Button(self, wx.ID_ANY, 'Prev')
        self.btnPrev.Visible = False
        self.Bind(wx.EVT_BUTTON, self.evt_prev, self.btnPrev)

        self.btnNext = wx.Button(self, wx.ID_ANY, 'Next')
        self.btnNext.Visible = False
        self.Bind(wx.EVT_BUTTON, self.evt_next, self.btnNext)

        self.btnExit = wx.Button(self, wx.ID_ANY, 'Exit')
        self.btnExit.Visible = False
        self.Bind(wx.EVT_BUTTON, self.evt_exit, self.btnExit)

        #self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Bind(wx.EVT_MOUSEWHEEL, self.on_scroll)
        
        #Define hotkeys
        hotkeys = [wx.AcceleratorEntry() for i in range(5)]
        hotkeys[0].Set(wx.ACCEL_NORMAL, wx.WXK_DOWN, self.btnNext.Id)
        hotkeys[1].Set(wx.ACCEL_NORMAL, wx.WXK_UP, self.btnPrev.Id)
        hotkeys[2].Set(wx.ACCEL_NORMAL, wx.WXK_RIGHT, self.btnNext.Id)
        hotkeys[3].Set(wx.ACCEL_NORMAL, wx.WXK_LEFT, self.btnPrev.Id)
        hotkeys[4].Set(wx.ACCEL_NORMAL, wx.WXK_ESCAPE, self.btnExit.Id)
        accel = wx.AcceleratorTable(hotkeys)
        self.SetAcceleratorTable(accel)

        # Get a list of all image files in the given folder
        self.files = self.GetFiles(os.getcwd())
        if not self.files:
            sys.exit()

        self.index = 0
        self.maxindex = len(self.files) - 1

        # Display the first image in the folder
        self.LoadImage(self.files[0])
        
    def evt_next(self, evt):
        """Display the next image in the list"""
        self.index = min(self.maxindex, self.index+1)
        self.LoadImage(self.files[self.index])
        if evt: evt.Skip()

    def evt_prev(self, evt):
        """Display the previous image in the list"""
        self.index = max(0, self.index-1)
        self.LoadImage(self.files[self.index])
        if evt: evt.Skip()

    def evt_exit(self, evt):
        sys.exit()

    def on_scroll(self, evt):
        """Display next or previous image depending on scroll direction"""
        if evt.WheelRotation < 0:
            self.evt_next(None)
        else:
            self.evt_prev(None)
        evt.Skip()

    def OnSize(self, event):
        """Resize image too fit container and redisplay"""
        self.image = self.ScaleToFit(self.image)
        self.DisplayImage(self.image)
        event.Skip()

    def LoadImage(self, file):
        """Load an image from a file, scale to fit, and display"""
        self.image = wx.Image(file, wx.BITMAP_TYPE_ANY)
        self.image = self.ScaleToFit(self.image)
        self.DisplayImage(self.image)

    def DisplayImage(self, image):
        self.bitmap.SetBitmap(self.image.ConvertToBitmap())
        
    def ScaleToFit(self, image):
        """Scale an image to fit its container"""

        # Get imsge size and container size
        cw, ch = self.Parent.GetSize()
        iw, ih = self.image.GetSize()

        aspect = ih / iw
        
        nw = cw
        nh = int(nw * aspect)

        if nh > ch:
            nh = ch
            nw = int(nh / aspect)

        return image.Scale(nw, nh)
        

    def GetFiles(self, folder):
        """Return a list of all image files in the given folder"""
        files = []
        images = ('.jpg','.jpeg','.gif','.png')

        for item in os.scandir(folder):
            #print(item.name)
            ext = os.path.splitext(item.name)[-1].lower()
            if ext in images:
                files.append(item.name)
                #print(item.name)
       
        return files

if __name__ == "__main__":

    os.chdir(r'd:\my pictures\.current')
    
    app = MyApp(0)
    app.MainLoop()


I think employing one of those versatile Buttons will make your (little) venture into higher mathematics obsolete: some of the turtles may already have done it :turtle: :turtle: :turtle: :turtle: :turtle: :turtle: :turtle:

Hello,
I found two old scripts for you
files_test.zip (26.1 KB)
or
look on Mike Driscoll’s blog :slight_smile:
https://www.blog.pythonlibrary.org/

You can also buy Cody Precord’s books.

Good luck!

Awesome. The test_viewer.py script is exactly what I’m looking for. A simple example of one concept, unmuddied by non-relevant code. Thanks.

Another example:
here is your modified script your_script.zip (26.0 KB)

Thanks for your continued interest. This is still a work in progress and need some optimization and refactoring but I want to get the last of the bugs out first. I have one interesting bug which is…

I’ve added a toggle (press ‘f’) between fullscreen and windowed. There is a line in init

self.full_screen = True

that sets the initial mode. Before running this app, set the initial value to False. Note that when you run it, you can toggle between windowed and fullscreen and everything works fine. Hoowever, if you set the initial mode to True (fullscreen), the first time you toggle to windowed mode the app goes bazoo. On my computer, the current image gets cropped, and clicking in the window mkes it disappear, although the app continues running.

isImage is a stripped down version for the posted code. My actual code is in an include file which determines if a file is an image based on the Windows registry.

import os
import sys
import wx


DEBUG = True

import inspect

def iam():
    """Returns the name of the function that called this function"""
    return inspect.getouterframes(inspect.currentframe())[1].function


class MyApp(wx.App):

    def OnInit(self):
        self.frame = MyFrame(None, -1)
        self.SetTopWindow(self.frame)
        self.frame.Show()        
        return True


class MyFrame(wx.Frame):

    def __init__(self, parent, id):
        wx.Frame.__init__(self, parent, wx.ID_ANY)
        self.SetSize(640,480)

        # Get a list of all image files in the given folder
        self.files = self.GetFiles(os.getcwd())
        if not self.files:
            sys.exit()

        self.scale = 1.0
        self.index = 0
        self.maxindex = len(self.files) - 1
        self.full_screen = True

        self.SetTitle("ZoomPic")
        self.SetBackgroundColour(wx.BLACK)

        self.Bind(wx.EVT_SIZE, self.on_size)
        self.Bind(wx.EVT_PAINT, self.on_paint)
        self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
        self.Bind(wx.EVT_LEFT_UP, self.on_left_up)

        self.ShowFullScreen(self.full_screen, wx.FULLSCREEN_ALL)

        # All controls are via hotkeys linked to zero sized buttons

        # Display previous picture
        self.btnPrev = wx.Button(self, wx.ID_ANY, size=(0,0))
        self.btnPrev.Visible = False
        self.Bind(wx.EVT_BUTTON, self.evt_prev, self.btnPrev)

        # Display next picture
        self.btnNext = wx.Button(self, wx.ID_ANY, size=(0,0))
        self.btnNext.Visible = False
        self.Bind(wx.EVT_BUTTON, self.evt_next, self.btnNext)

        # Toggle fullscreen
        self.btnFull = wx.Button(self, wx.ID_ANY, size=(0,0))
        self.btnFull.Visible = False
        self.Bind(wx.EVT_BUTTON, self.evt_full, self.btnFull)

        # Exit app
        self.btnExit = wx.Button(self, wx.ID_ANY, size=(0,0))
        self.btnExit.Visible = False
        self.Bind(wx.EVT_BUTTON, self.evt_exit, self.btnExit)

        # Display previous or next picture depending on scroll direction
        self.Bind(wx.EVT_MOUSEWHEEL, self.on_scroll)

        #Define hotkeys
        hotkeys = [wx.AcceleratorEntry() for i in range(6)]
        hotkeys[0].Set(wx.ACCEL_NORMAL, wx.WXK_DOWN, self.btnNext.Id)
        hotkeys[1].Set(wx.ACCEL_NORMAL, wx.WXK_UP, self.btnPrev.Id)
        hotkeys[2].Set(wx.ACCEL_NORMAL, wx.WXK_RIGHT, self.btnNext.Id)
        hotkeys[3].Set(wx.ACCEL_NORMAL, wx.WXK_LEFT, self.btnPrev.Id)
        hotkeys[4].Set(wx.ACCEL_NORMAL, wx.WXK_ESCAPE, self.btnExit.Id)
        hotkeys[5].Set(wx.ACCEL_NORMAL, ord('f'), self.btnFull.Id)
        accel = wx.AcceleratorTable(hotkeys)
        self.SetAcceleratorTable(accel)

    def evt_next(self, evt):
        """Display the next image in the list"""
        if DEBUG: print('enter',iam())
        oldindex = self.index
        self.index = min(self.maxindex, self.index+1)
        if self.index != oldindex:
            self.Refresh()
        if evt: evt.Skip()

    def evt_prev(self, evt):
        """Display the previous image in the list"""
        if DEBUG: print('enter',iam())
        oldindex = self.index
        self.index = max(0, self.index-1)
        if self.index != oldindex:
            self.Refresh()
        if evt: evt.Skip()

    def evt_exit(self, evt):
        """Exit app"""
        if DEBUG: print('enter',iam())
        sys.exit()

    def on_scroll(self, evt):
        """Display next or previous image depending on scroll direction"""
        if DEBUG: print('enter',iam())
        if evt.WheelRotation < 0:
            self.evt_next(None)
        else:
            self.evt_prev(None)
        evt.Skip()

    def ScaleToFit(self, image):
        """Scale an image to fit parent control"""
        if DEBUG: print('enter',iam())

        # Get image size and container size
        cw, ch = self.Size
        iw, ih = image.GetSize()

        aspect = ih / iw

        # Determine new size with aspect and adjust if any cropping
        nw = cw
        nh = int(nw * aspect)

        if nh > ch:
            nh = ch
            nw = int(nh / aspect)

        return image.Scale(int(nw*self.scale), int(nh*self.scale))

    def on_size(self, evt):
        if DEBUG: print('enter',iam())
        self.Refresh()
        evt.Skip()
        if DEBUG: print('leave',iam())

    def on_left_down(self, evt):
        if DEBUG: print('enter',iam())
        self.scale = 2.0
        self.Refresh()
        evt.Skip()

    def on_left_up(self, evt):
        if DEBUG: print('enter',iam())
        self.scale = 1.0
        self.Refresh()
        evt.Skip()

    def evt_full(self, evt):
        """Toggle fullscreen"""
        if DEBUG: print('enter',iam())

        self.full_screen = not self.full_screen

        if self.full_screen:
            self.ShowFullScreen(True, wx.FULLSCREEN_ALL)
        else:
            self.ShowFullScreen(False, 0)

        self.Refresh()
        evt.Skip()
        if DEBUG: print('leave',iam())

    def on_paint(self, evt):
        # Draw the current image file into the frame
        if DEBUG: print('enter',iam())

        # Get current image, scale to screen, then convert to bitmap
        image  = wx.Image(self.files[self.index], wx.BITMAP_TYPE_ANY)
        image  = self.ScaleToFit(image)
        bitmap = image.ConvertToBitmap()

        # Determine upper left corner to centre image on screen
        iw, ih = bitmap.GetSize()
        sw, sh = self.Size
        sx, sy = int((sw - iw) / 2), int((sh - ih) / 2)

        # Draw the image
        dc = wx.PaintDC(self)
        dc.DrawBitmap(bitmap, sx, sy, True)
        print(f'{self.Position=}')
        evt.Skip()

    def GetFiles(self, folder):
        """Return a list of all image files in the given folder"""
        if DEBUG: print('enter',iam())
        files = []

        for item in os.scandir(folder):
            if self.isImage(item.name):
                files.append(item.name)

        return files

    def isImage(self, file):
        ext = os.path.splitext(file)[-1].lower()
        return ext in ('.jpg','.jpeg','.gif','.png')

if __name__ == "__main__":

    # If a folder was given and it exists, start display
    if len(sys.argv) > 0:
        folder = sys.argv[1]
        if os.path.isdir(folder):
            os.chdir(folder)    
            app = MyApp(0)
            app.MainLoop()

I’ve also added a simple zoom (left click-hold) which I hope to expand to a zoom & pan.

In case anyone will find it useful…

"""                                                                                 
    Name:                                                                           
                                                                                    
        perceivedType.py                                                            
                                                                                    
    Description:                                                                    
                                                                                    
        This is a set of methods that use the Windows registry to return a string   
        describing how Windows interprets the given file. The current methods will  
        return a string description as provided by Windows, or "unknown" if Windows 
        does not have an associated file type. The auxiliary functions return True  
        or False for tests for specific file types.                                 
                                                                                    
     Auxiliary Functions:                                                           
                                                                                    
          isVideo(file)   -   returns True if PerceivedType = "video"               
          isAudio(file)   -   returns True if PerceivedType = "audio"               
          isImage(file)   -   returns True if PerceivedType = "image"               
          isText (file)   -   returns True if PerceivedType = "text"                
                                                                                    
    Parameters:                                                                     
                                                                                    
        file:str    a file name                                                     
        degug:bool  print debug info if True (default=False)                        
                                                                                    
    Audit:                                                                          
                                                                                    
        2021-07-17  rj  original code                                               
                                                                                    
"""

import os
import winreg


def perceivedType(file: str, debug: bool = False) -> str:
    """Returns the windows registry perceived type string for the given file"""

    if debug:
        print(f'\nchecking {file=}')

    try:
        key = winreg.OpenKey(winreg.HKEY_CLASSES_ROOT, os.path.splitext(file)[-1])
        inf = winreg.QueryInfoKey(key)

        for i in range(0, inf[1]):
            res = winreg.EnumValue(key, i)
            if debug:
                print(f'    {res=}')
            if res[0] == 'PerceivedType':
                return res[1].lower()
    except:
        pass

    return "unknown"

def isVideo(file: str) -> str: return perceivedType(file) == 'video'
def isAudio(file: str) -> str: return perceivedType(file) == 'audio'
def isImage(file: str) -> str: return perceivedType(file) == 'image'
def isText(file: str) -> str: return perceivedType(file) == 'text'


if __name__ == '__main__':
    for file in ('file.avi', 'file.mov', 'file.txt', 'file.jpg', 'file.mp3', 'file.pdf', 'file.xyz'):
        print('Perceived type of "%s" is %s' % (file, perceivedType(file, debug=True)))

Hello ,
Is your problem solved with this?
test.zip (25.7 KB)

No. With that version if you start in full screen, then press f to switch to windowed, the app still hangs.

Latest stab.

I can now left-click anywhere on the image to zoom x 2 centered on the mouse. Left-clicking and dragging will pan across the zoomed image.

But now I have two anomaliies.

If you start in windowed mode, zoom/pan works with only a mild flicker. If you then go fullscreen and try zoom/pan there is serious flickering. In fact, on my laptop I see more black than image.

If I start in fullscreen and zoom/pan I get the mild flickering like I got when starting in windowed mode. Switching to windowed mode crashes that app like before. Again, when starting windowed I can switch between modes with no prooblem.

"""
Name:

    ZoomPic.pyw

Description:

    Shell extension to add a quick image browser function to explorer.

Install:

    Just requires a simple addition to the registry. Double click on
    ZoomPic.reg to add the shell extension. Note that before you do this
    you should ensure that the full path to pythonw.exe and zoompic.pyw
    are modified to match your system.

Usage:

    Once installed, just right click on a foolder containing image files
    and select ZoomPic. If you don't want to install the shell extension
    then just start it in the folder you want to view.

    Left-clicking on an image will zoom in x2. Left-clicking and dragging
    will pan.

    Pressing f will toggle between full screen and windowed.

    Pressing esc will exit

Note:

    Supports static images only. If you want to show the pictures in a
    resizable window rather than full screen, change the value of
    FULL_SCREEN to False.

"""

import os
import sys
import wx

DEBUG = False

import inspect

def iam():
    """Returns the name of the function that called this function"""
    return inspect.getouterframes(inspect.currentframe())[1].function


class MyApp(wx.App):

    def OnInit(self):
        self.frame = MyFrame(None, -1)
        self.SetTopWindow(self.frame)
        self.frame.Show()        
        return True


class MyFrame(wx.Frame):

    def __init__(self, parent, id):
        wx.Frame.__init__(self, parent, wx.ID_ANY)
        self.SetSize(640,480)

        # Get a list of all image files in the given folder
        self.files = self.GetFiles(os.getcwd())
        if not self.files:
            sys.exit()

        self.zoom = 1.0
        self.index = 0
        self.maxindex = len(self.files) - 1
        self.full_screen = True
        self.left_down = False


        self.SetTitle("ZoomPic")
        self.SetBackgroundColour(wx.BLACK)

        self.Bind(wx.EVT_SIZE, self.on_size)
        self.Bind(wx.EVT_PAINT, self.on_paint)
        self.Bind(wx.EVT_LEFT_DOWN, self.on_left_down)
        self.Bind(wx.EVT_LEFT_UP, self.on_left_up)
        self.Bind(wx.EVT_MOTION, self.on_motion)

        self.SetDoubleBuffered(True)
        self.ShowFullScreen(self.full_screen, wx.FULLSCREEN_ALL)

        # All controls are via hotkeys linked to zero sized buttons

        # Display previous picture
        self.btnPrev = wx.Button(self, wx.ID_ANY, size=(0,0))
        self.btnPrev.Visible = False
        self.Bind(wx.EVT_BUTTON, self.evt_prev, self.btnPrev)

        # Display next picture
        self.btnNext = wx.Button(self, wx.ID_ANY, size=(0,0))
        self.btnNext.Visible = False
        self.Bind(wx.EVT_BUTTON, self.evt_next, self.btnNext)

        # Toggle fullscreen
        self.btnFull = wx.Button(self, wx.ID_ANY, size=(0,0))
        self.btnFull.Visible = False
        self.Bind(wx.EVT_BUTTON, self.evt_full, self.btnFull)

        # Exit app
        self.btnExit = wx.Button(self, wx.ID_ANY, size=(0,0))
        self.btnExit.Visible = False
        self.Bind(wx.EVT_BUTTON, self.evt_exit, self.btnExit)

        # Display previous or next picture depending on scroll direction
        self.Bind(wx.EVT_MOUSEWHEEL, self.on_scroll)

        #Define hotkeys
        hotkeys = [wx.AcceleratorEntry() for i in range(6)]
        hotkeys[0].Set(wx.ACCEL_NORMAL, wx.WXK_DOWN, self.btnNext.Id)
        hotkeys[1].Set(wx.ACCEL_NORMAL, wx.WXK_UP, self.btnPrev.Id)
        hotkeys[2].Set(wx.ACCEL_NORMAL, wx.WXK_RIGHT, self.btnNext.Id)
        hotkeys[3].Set(wx.ACCEL_NORMAL, wx.WXK_LEFT, self.btnPrev.Id)
        hotkeys[4].Set(wx.ACCEL_NORMAL, wx.WXK_ESCAPE, self.btnExit.Id)
        hotkeys[5].Set(wx.ACCEL_NORMAL, ord('f'), self.btnFull.Id)
        accel = wx.AcceleratorTable(hotkeys)
        self.SetAcceleratorTable(accel)

    def evt_next(self, evt):
        """Display the next image in the list"""        
        if DEBUG: print('enter',iam())
        oldindex = self.index
        self.index = min(self.maxindex, self.index+1)
        if self.index != oldindex:
            self.Refresh()
        if evt: evt.Skip()

    def evt_prev(self, evt):
        """Display the previous image in the list"""
        if DEBUG: print('enter',iam())
        oldindex = self.index
        self.index = max(0, self.index-1)
        if self.index != oldindex:
            self.Refresh()
        if evt: evt.Skip()

    def evt_exit(self, evt):
        """Exit app"""
        if DEBUG: print('enter',iam())
        sys.exit()

    def on_scroll(self, evt):
        """Display next or previous image depending on scroll direction"""
        if DEBUG: print('enter',iam())
        if evt.WheelRotation < 0:
            self.evt_next(None)
        else:
            self.evt_prev(None)
        evt.Skip()

    def on_motion(self, evt):
        """Pan only if left mouse button is down"""
        if self.left_down:
            self.Refresh()
        evt.Skip()

    def ScaleToFit(self, image):
        """Scale an image to fit parent control"""
        if DEBUG: print('enter',iam())

        # Get image size, container size, and calculate aspect ratio
        cw, ch = self.Size
        iw, ih = image.GetSize()
        aspect = ih / iw

        # Determine new size with aspect and adjust if any cropping
        nw = cw
        nh = int(nw * aspect)

        if nh > ch:
            nh = ch
            nw = int(nh / aspect)

        # Return the newly scaled image
        return image.Scale(int(nw*self.zoom), int(nh*self.zoom))

    def on_size(self, evt):
        """Redisplay image for new window size"""
        if DEBUG: print('enter',iam())
        self.Refresh()
        evt.Skip()
        if DEBUG: print('leave',iam())

    def on_left_down(self, evt):
        """Enable zoom and redisplay image"""
        if DEBUG: print('enter',iam())
        self.zoom = 2.0
        self.left_down = True
        self.Refresh()
        evt.Skip()

    def on_left_up(self, evt):
        """Disable zoom and redisplay image"""
        if DEBUG: print('enter',iam())
        self.zoom = 1.0
        self.left_down = False
        self.Refresh()
        evt.Skip()

    def evt_full(self, evt):
        """Toggle fullscreen and redisplay image"""
        if DEBUG: print('enter',iam())

        self.full_screen = not self.full_screen

        if self.full_screen:
            self.ShowFullScreen(True, wx.FULLSCREEN_ALL)
        else:
            self.ShowFullScreen(False, 0)

        self.Refresh()
        evt.Skip()
        if DEBUG: print('leave',iam())

    def on_paint(self, evt):
        """Draw the current image

        If the left mouse button is down then we are zooming and
        maybe panning so use the last zoomed bitmap instead of
        reading from the file and creating a new bitmap. The part
        of the zoomed image that is displayed is determined based on
        the location of the mouse on the screen.
        """

        if DEBUG: print('enter',iam())

        # If the left button is down then we are zoomed in. In that
        # case we have already read the image from the file.
        if not self.left_down:
            self.image = wx.Image(self.files[self.index], wx.BITMAP_TYPE_ANY)
        image = self.ScaleToFit(self.image)
        bitmap = image.ConvertToBitmap()

        # Determine upper left corner to centre image on screen
        iw, ih = bitmap.GetSize()
        sw, sh = self.Size

        # If we are zoomed in then determine start position
        # based on mouse position, otherwise centre

        if self.left_down:
            # Get mouse position relative to frame, not screen
            mx, my = self.ScreenToClient(wx.GetMousePosition())
            px, py = (mx / sw), (my / sh)
            sx, sy = int((sw-iw)*px), int((sh-ih)*py)             
        else:
            sx, sy = int((sw - iw) / 2), int((sh - ih) / 2)

        # Draw the image
        dc = wx.PaintDC(self)
        dc.DrawBitmap(bitmap, sx, sy, True)
        dc.SetTextForeground(wx.WHITE)
        dc.SetTextBackground(wx.BLACK)
        dc.DrawText(self.files[self.index], 5, 5)
        evt.Skip()

    def GetFiles(self, folder):
        """Return a list of all image files in the given folder"""
        if DEBUG: print('enter',iam())
        files = []

        for item in os.scandir(folder):
            if self.isImage(item.name):
                files.append(item.name)

        return files

    def isImage(self, file):
        ext = os.path.splitext(file)[-1].lower()
        return ext in ('.jpg','.jpeg','.gif','.png')


if __name__ == "__main__":

    # If a folder was given and it exists, start display
    if len(sys.argv) > 0:
        folder = sys.argv[1]
        if os.path.isdir(folder):
            os.chdir(folder)    
            app = MyApp(0)
            app.MainLoop()

Here is a solution to your previous problem. : test_v3.zip (25.7 KB)

Here is the without flickering version for me: test_v4.zip (26.7 KB)

Thank you so much. I’m still reading up on DC and double buffering. There’s a lot to take in. As for the part about the app crashing, I am at a loss as to why it makes a difference setting FullScreen in MyApp rather than MyFrame. It’s just going to be one of those things I’ll have to remember.

In any case, my app is now fully functional. I’ll add a “thanks to zig_zag at wxpython.org for all the help” to my audit trail in the header.