I have been writing small Python utility programs, a few lines of code using module 'glob' that go through a working folder and search for strings in text and code files. Also have one that can give you a slide show of any image files there. These programs are tiny, less than 1k, I tuck them away right in the folder of interest, then run them from IDLE or whatever.
Here is an example of a small utility program searching for a segment of code in its 'working folder'.

Recommended Answers

All 9 Replies

From the past, grep?
grep -rnw '/path/to/your/folder' -e 'your_search_string'

I remember folk quite scared of the command line.

I have dozens of python scripts I've developed over the years as well, a number of them that use wxPython for the gui elements. I've posted several of the larger ones on Daniweb as tutorials if you are interested. One, for example, embeds vlc media player in two side-by-side panels for comparing a video before and after processing. Typically I use that to see the results of "deshaking" a video with ffmpeg.

rproffitt, thank you! Unix 'grep' reminds me of Python module 're' I am still scared of that one after all those years! Admire the folks that use 're', since it is a language within a language! But then, I use 'html' code for PyQt QLabel and think it's charming.

BTW, the 'tiny utility' that goes through the folder it is in, and gives you a slide show of all the image files there is coded. I am test driving it to make sure it has no surprises. Looks mighty good so far!

I have located some images I have missed for years.

commented: When I write regular expressions, God and I know how it works. Now, only God knows. +0

Here is the 'tiny utility' (less than 2k code) that you can run in a folder to give you a slide show of all the major images in that folder. It employs PyQt6 so have that installed with your Python via 'python -m pip install pyqt6' Enjoy!

Here is a similar program I wrote a while back in wxPython. If you copy the following block to a .reg file, thyen run it, you will get "Zoompic" added to the context menu for folders. Zoompic implements pan and zoom.

Regedit file
REGEDIT4

; This entry adds the context menu "ZoomPic" for folders and drives

[HKEY_CLASSES_ROOT\Directory\shell\ZoomPic]

[HKEY_CLASSES_ROOT\Directory\Shell\ZoomPic]
@="ZoomPic"

[HKEY_CLASSES_ROOT\Directory\shell\ZoomPic\command]
;@="\"C:\\Windows\\pyw.exe\" \"D:\\apps\\ZoomPic\\ZoomPic.pyw\" \"%1\""
@="\"D:\\apps\\ZoomPic\\dist\\ZoomPic.exe\" \"%1\""

; this entry adds the context menu "ZoomPic" for a drive

[HKEY_CLASSES_ROOT\Drive\shell\ZoomPic]

[HKEY_CLASSES_ROOT\Directory\Shell\ZoomPic]
@="ZoomPic"

[HKEY_CLASSES_ROOT\Drive\shell\ZoomPic\command]
;@="\"C:\\Windows\\pyw.exe\" \"D:\\apps\\ZoomPic\\ZoomPic.pyw\" \"%1\""
@="\"D:\\apps\\ZoomPic\\dist\\ZoomPic.exe\" \"%1\""
Zoompic
"""
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 folder 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.

    Cycle through the images using the scroll wheel or arrow keys. Arrow
    left and up display the previous picture, arrow right and up the next.

    Left-clicking on an image will zoom in x2. Left-clicking and dragging
    will pan. The default zoom factor is 1.0. That does not mean the image
    is displayed actual size. The image is displayed to fit the container,
    be it the window ot the frame. The initial size is taken as 1.0. If
    you hold the shift key while left clicking you will double the click
    zoom factor (extra zoom).

    Pressing f will toggle between full screen and windowed.
    pressing m will toggle minimized to top left screen if in full screen
    Pressing esc will exit

Notes:

    Supports static images only.

    Change the value of FULL_SCREEN to False if you want to start windowed.
    Change the value of ZOOM_FACTOR if you want a larger or smaller zoom.

    You can convert to an executable by running the following command, but
    Windows Defender may flag it as a trojan. You can safely ignore this.

        pyinstaller --onefile Zoompic.pyw

    Many thanks go to Zig_Zag at discuss.wxpython.org/ for all his help
    resolving FullScreen and flicker problems. He saved me days of
    troubleshooting.

Audit:

    2025-10-23  rj  del sends current image to trashbin
    2024-06-04  rj  added image size to overlay and corrected % calc
    2023-12-30  rj  added note about pyinstaller
    2023-11-25  rj  added timer.Stop on exit and %zoom to display
    2022-08-15  rj  auto-hide cursor after one second
    2022-08-08  rj  added m to toggle minimize to top left screen
    2022-07-31  rj  added zoom more on SHIFT-LEFT-CLICK
    2022-07-31  rj  added hide cursor in zoom mode
    2022-07-29  rj  original code

"""

ZOOM_FACTOR = 2.0       # zoom factor for left-click
FULL_SCREEN = True      # set to False to start in windowed mode

import os
import sys
import shutil
import wx
import Message
import send2trash
import subprocess


class MyApp(wx.App):

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


class MyFrame(wx.Frame):

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

        self.SetSize(640,480)
        self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)

        self.normal_cursor = wx.Cursor(wx.CURSOR_ARROW)
        self.cross_cursor = wx.Cursor(wx.CURSOR_CROSS)

        # Get a list of all image files in the given folder.
        # Exit if no image files found.
        self.files = self.GetFiles(os.getcwd())
        if not self.files:
            Message.message('Zoompic', 'No image files in folder')
            self.Destroy()

        self.zoom  = 1.0
        self.minimized = False
        self.full_screen = True
        self.left_down = False    

        self.index = 0
        self.maxindex = len(self.files) - 1
        self.image = wx.Image(self.files[0], wx.BITMAP_TYPE_ANY)

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

        # Connect events to event handlers
        self.Bind(wx.EVT_SIZE, self.evt_size)
        self.Bind(wx.EVT_PAINT, self.evt_paint)
        self.Bind(wx.EVT_LEFT_DOWN, self.evt_left_down)
        self.Bind(wx.EVT_LEFT_UP, self.evt_left_up)
        self.Bind(wx.EVT_MOTION, self.evt_motion)
        self.Bind(wx.EVT_MOTION, self.evt_mouse_move)
        self.Bind(wx.EVT_RIGHT_UP, self.evt_mouse_right_up)

        # All non-mouse controls are via hotkeys
        # bound 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)

        # Toggle minimized
        self.btnMin = wx.Button(self, wx.ID_ANY, size=(0,0))
        self.btnMin.Visible = False
        self.Bind(wx.EVT_BUTTON, self.evt_min, self.btnMin)

        # Delete the current file
        self.btnDelete = wx.Button(self, wx.ID_ANY, size=(0,0))
        self.btnDelete.Visible = False
        self.Bind(wx.EVT_BUTTON, self.evt_delete, self.btnDelete)

        # 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.evt_scroll)

        #Define hotkeys
        hotkeys = [
            wx.AcceleratorEntry(
                wx.ACCEL_NORMAL, wx.WXK_DOWN, self.btnNext.Id),
                wx.AcceleratorEntry(wx.ACCEL_NORMAL, wx.WXK_RIGHT, self.btnNext.Id),
                wx.AcceleratorEntry(wx.ACCEL_NORMAL, wx.WXK_SPACE, self.btnNext.Id),
                wx.AcceleratorEntry(wx.ACCEL_NORMAL, wx.WXK_UP, self.btnPrev.Id),
                wx.AcceleratorEntry(wx.ACCEL_NORMAL, wx.WXK_LEFT, self.btnPrev.Id),
                wx.AcceleratorEntry(wx.ACCEL_NORMAL, wx.WXK_ESCAPE, self.btnExit.Id),
                wx.AcceleratorEntry(wx.ACCEL_NORMAL, wx.WXK_DELETE, self.btnDelete.Id),
                wx.AcceleratorEntry(wx.ACCEL_NORMAL, ord('f'), self.btnFull.Id),
                wx.AcceleratorEntry(wx.ACCEL_NORMAL, ord('m'), self.btnMin.Id),
        ]
        accel = wx.AcceleratorTable(hotkeys)
        self.SetAcceleratorTable(accel)

        # Set timer to auto-hide cursor after one second
        self.timeout = 1
        self.timer = wx.Timer()
        self.timer.Start(1000)

        self.timer.Bind(wx.EVT_TIMER, self.evt_timer)

        self.show_cursor()

    def evt_mouse_move(self, event):
        self.x, self.y = event.GetPosition()
        event.Skip()

    def evt_mouse_right_up(self, event):
        if wx.GetKeyState(wx.WXK_CONTROL) or (self.x > 1910 and self.y < 10):
            #Message.message('Zoompic', f'delete {self.files[self.index]}')
            self.evt_delete(None)
        event.Skip()

    def evt_timer(self, event):
        if self.timeout > 0:
            self.timeout -= 1
            if self.timeout == 0:
                self.hide_cursor()
        event.Skip()

    def evt_next(self, event):
        """Display the next image in the list"""        
        oldindex = self.index
        self.index = min(self.maxindex, self.index+1)
        if self.index != oldindex:
            self.image = wx.Image(self.files[self.index], wx.BITMAP_TYPE_ANY)
            self.Refresh()
        if event: event.Skip()

    def evt_prev(self, event):
        """Display the previous image in the list"""
        oldindex = self.index
        self.index = max(0, self.index-1)
        if self.index != oldindex:
            self.image = wx.Image(self.files[self.index], wx.BITMAP_TYPE_ANY)
            self.Refresh()
        if event: event.Skip()

    def evt_delete(self, event):
        """Delete the current file"""
        # Delete the current file item
        send2trash.send2trash(self.files[self.index])
        del self.files[self.index]
        self.maxindex -= 1

        if self.maxindex < 0:
            Message.message('Zoompic', 'No more images', 1)
            self.evt_exit(None)

        self.index = min(self.index, len(self.files) - 1)
        self.image = wx.Image(self.files[self.index], wx.BITMAP_TYPE_ANY)
        self.Refresh()

        if event: event.Skip()

    def evt_exit(self, event):
        """Exit app"""
        self.timer.Stop()
        self.Destroy()
        if event: event.Skip()

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

    def evt_motion(self, event):
        """Pan only if left mouse button is down"""
        if self.left_down:
            self.Refresh()
        else:
            self.timeout = 1
            self.show_cursor()
        event.Skip()

    def evt_size(self, event):
        """Redisplay image for new window size"""
        self.Refresh()
        event.Skip()

    def evt_left_down(self, event):
        """Enable zoom and redisplay image"""

        if wx.GetKeyState(wx.WXK_SHIFT):
            self.zoom = ZOOM_FACTOR * 2
        else:
            self.zoom = ZOOM_FACTOR

        self.left_down = True
        self.hide_cursor()
        self.Refresh()

        event.Skip()

    def evt_left_up(self, event):
        """Disable zoom and redisplay image"""
        self.zoom = 1.0
        self.left_down = False
        self.Refresh()
        event.Skip()

    def evt_full(self, event):
        """Toggle fullscreen and redisplay image"""

        self.full_screen = not self.full_screen

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

        self.Refresh()
        event.Skip()

    def evt_min(self, event):
        """Toggle minimized if full screen"""

        if self.full_screen:
            self.minimized = not self.minimized

            if self.minimized:
                self.lastsize = self.GetSize()
                self.SetSize(10,10)
            else:
                self.SetSize(self.lastsize)
                self.Refresh()

        event.Skip()

    def evt_paint(self, event):
        """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 in the display window.
        """

        # Scale the image to fit and convert to bitmap           
        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
        # mx,my - mouse position in display window
        # px,py - position of mouse as a value from 0.0 to 1.0
        # sx,sy - position of top left bitmap relative to window

        if self.left_down:
            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.AutoBufferedPaintDC(self)
        dc.Clear()
        dc.DrawBitmap(bitmap, sx, sy, True)

        # Draw the text overlay
        dc.SetTextForeground(wx.WHITE)
        dc.SetTextBackground(wx.BLACK)
        dc.SetBackgroundMode(wx.BRUSHSTYLE_SOLID)
        note = f'{self.files[self.index]}    {self.index+1} of {len(self.files)}'
        note += f'   {int(100*iw/self.image.Width)}%'
        note += f'   {self.image.Width}x{self.image.Height}'

        dc.DrawText(note, 0, 0)
        event.Skip()

    def ScaleToFit(self, image):
        """Scale an image to fit parent control"""

        # 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 GetFiles(self, folder):
        """Return a list of all image files in the given folder"""

        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')

    def hide_cursor(self):
        cursor = wx.Cursor(wx.CURSOR_BLANK)
        self.SetCursor(cursor)

    def show_cursor(self):
        cursor = wx.Cursor(wx.CURSOR_DEFAULT)
        self.SetCursor(cursor)

if __name__ == "__main__":

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

On reviewing I think I might replace the clumsy hidden button/accelerator keys with a more generic keyboard handler.

I cleaned up zoompic.

r"""
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 folder 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.

    Cycle through the images using the scroll wheel or arrow keys. Arrow
    left and up display the previous picture, arrow right and up the next.

    Left-clicking on an image will zoom in x2. Left-clicking and dragging
    will pan. The default zoom factor is 1.0. That does not mean the image
    is displayed actual size. The image is displayed to fit the container,
    be it the window ot the frame. The initial size is taken as 1.0. If
    you hold the shift key while left clicking you will double the click
    zoom factor (extra zoom).

    Pressing f will toggle between full screen and windowed.
    pressing m will toggle minimized to top left screen if in full screen
    Pressing esc will exit

Note:

    Supports static images only.

    I occasionally want to do minimal editing on an image. In my case I use
    FastStone portable which is defined in EXT_VUEWER. Pressing "v" will
    spawn that viewer. Change EXT_VIEWER for whatever app you use.

    Change the value of FULL_SCREEN to False if you want to start windowed.
    Change the value of ZOOM_FACTOR if you want a larger or smaller zoom.

    You can convert to an executable by running the following command, but
    Windows Defender may flag it as a trojan. You can safely ignore this.

        pyinstaller --onefile Zoompic.pyw

    Many thanks go to Zig_Zag at discuss.wxpython.org/ for all his help
    resolving FullScreen and flicker problems. He saved me days of
    troubleshooting.

    You may want to add custom Actions for
    C:\Users\Jim\AppData\Local\PowerToys\WinUI3Apps\PowerToysPeek.UI.exe

TODO:

    Add support for CTRL-WHEEL and touchpad pinch zoom in/out

Audit:

    2026-09-02  rj  removed invisible buttons
    2025-10-24  rj  add quick copy
    2025-10-23  rj  del sends current image to trashbin
    2024-06-04  rj  added image size to overlay and corrected % calc
    2023-12-30  rj  added note about pyinstaller
    2023-11-25  rj  added timer.Stop on exit and %zoom to display
    2022-08-15  rj  auto-hide cursor after one second
    2022-08-08  rj  added m to toggle minimize to top left screen
    2022-07-31  rj  added zoom more on SHIFT-LEFT-CLICK
    2022-07-31  rj  added hide cursor in zoom mode
    2022-07-29  rj  original code

"""

ZOOM_FACTOR = 2.0       # zoom factor for left-click
FULL_SCREEN = True      # set to False to start in windowed mode
EXT_VIEWER  = r'D:\apps\fsportable.5.1\fsviewer.exe'      
COPYPATH    = r'd:\archive\work\trash'

import os
import sys
import shutil
import wx
import Message
import send2trash
import subprocess


class MyApp(wx.App):

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


class MyFrame(wx.Frame):

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

        self.SetSize(640,480)
        self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)

        self.normal_cursor = wx.Cursor(wx.CURSOR_ARROW)
        self.cross_cursor = wx.Cursor(wx.CURSOR_CROSS)

        # Get a list of all image files in the given folder.
        # Exit if no image files found.
        self.files = self.GetFiles(os.getcwd())
        if not self.files:
            self.Destroy()

        # Set copy folder

        if not os.path.isdir(COPYPATH):
            os.mkdir(COPYPATH)
        self.copypath = COPYPATH

        self.zoom  = 1.0
        self.minimized = False
        self.full_screen = True
        self.left_down = False    

        self.index = 0
        self.maxindex = len(self.files) - 1
        self.image = wx.Image(self.files[0], wx.BITMAP_TYPE_ANY)

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

        # Connect events to event handlers
        self.Bind(wx.EVT_SIZE, self.evt_size)
        self.Bind(wx.EVT_PAINT, self.evt_paint)
        self.Bind(wx.EVT_LEFT_DOWN, self.evt_left_down)
        self.Bind(wx.EVT_LEFT_UP, self.evt_left_up)
        self.Bind(wx.EVT_MOTION, self.evt_motion)
        self.Bind(wx.EVT_MOTION, self.evt_mouse_move)

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

        # Define hotkeys and create accelerator table

        self.prev_id = wx.NewIdRef()    # display previous image
        self.next_id = wx.NewIdRef()    # display next image
        self.full_id = wx.NewIdRef()    # toggle full screen
        self.min_id  = wx.NewIdRef()    # minimize to button
        self.del_id  = wx.NewIdRef()    # delete current image
        self.copy_id = wx.NewIdRef()    # copy current image to defined folder
        self.view_id = wx.NewIdRef()    # open current image in defined viewer
        self.exit_id = wx.NewIdRef()    # close app

        self.Bind(wx.EVT_MENU, self.on_prev, id=self.prev_id)
        self.Bind(wx.EVT_MENU, self.on_next, id=self.next_id)
        self.Bind(wx.EVT_MENU, self.on_full, id=self.full_id)
        self.Bind(wx.EVT_MENU, self.on_min , id=self.min_id)
        self.Bind(wx.EVT_MENU, self.on_del , id=self.del_id)
        self.Bind(wx.EVT_MENU, self.on_copy, id=self.copy_id)
        self.Bind(wx.EVT_MENU, self.on_view, id=self.view_id)
        self.Bind(wx.EVT_MENU, self.on_exit, id=self.exit_id)

        hotkeys = [
            (wx.ACCEL_NORMAL, wx.WXK_RIGHT, self.next_id),
            (wx.ACCEL_NORMAL, wx.WXK_SPACE, self.next_id),
            (wx.ACCEL_NORMAL, wx.WXK_DOWN, self.next_id),
            (wx.ACCEL_NORMAL, wx.WXK_UP, self.prev_id),
            (wx.ACCEL_NORMAL, wx.WXK_LEFT, self.prev_id),
            (wx.ACCEL_NORMAL, wx.WXK_ESCAPE, self.exit_id),
            (wx.ACCEL_NORMAL, wx.WXK_DELETE, self.del_id),
            (wx.ACCEL_NORMAL, ord('f'), self.full_id),
            (wx.ACCEL_NORMAL, ord('m'), self.min_id),
            (wx.ACCEL_NORMAL, ord('c'), self.copy_id),
            (wx.ACCEL_NORMAL, ord('v'), self.view_id),
        ]
        accel = wx.AcceleratorTable(hotkeys)
        self.SetAcceleratorTable(accel)

        # Set timer to auto-hide cursor after one second
        self.timeout = 1
        self.timer = wx.Timer()
        self.timer.Start(1000)

        self.timer.Bind(wx.EVT_TIMER, self.evt_timer)

        self.show_cursor()

    def evt_mouse_move(self, event):
        """
        Keeps track of the current mouse position
        """
        self.x, self.y = event.GetPosition()
        event.Skip()

    def evt_timer(self, event):
        """
        Hide the cursor after a one second delay
        """
        if self.timeout > 0:
            self.timeout -= 1
            if self.timeout == 0:
                self.hide_cursor()
        event.Skip()

    def on_next(self, event):
        """
        Display the next image in the list
        """
        oldindex = self.index
        self.index = min(self.maxindex, self.index+1)
        if self.index != oldindex:
            self.image = wx.Image(self.files[self.index], wx.BITMAP_TYPE_ANY)
            self.Refresh()

    def on_prev(self, event):
        """
        Display the previous image in the list
        """
        oldindex = self.index
        self.index = max(0, self.index-1)
        if self.index != oldindex:
            self.image = wx.Image(self.files[self.index], wx.BITMAP_TYPE_ANY)
            self.Refresh()

    def on_view(self, event):
        """
        Open the current file in the defined viewer
        """
        subprocess.Popen([EXT_VIEWER, self.files[self.index]])

    def on_copy(self, event):
        """
        Copy the current file to COPYPATH if possible
        """

        if not self.copypath:
            return

        folder = self.copypath
        file   = self.files[self.index]
        try:
            if os.path.isdir(folder):
                Message.message('Zoompic', f'copy "{file}" to {folder}', 1)
                shutil.copy(self.files[self.index], folder)
            else:
                Message.message('Zoompic', f'folder {folder} not available', 1)
        except:
            Message.message('Zoompic', 'Could not copy file')

    def on_del(self, event):
        """
        Delete the current file to the recycle bin
        """

        send2trash.send2trash(self.files[self.index])
        del self.files[self.index]
        self.maxindex -= 1

        if self.maxindex < 0:
            Message.message('Zoompic', 'No more images', 1)
            self.evt_exit(None)

        self.index = min(self.index, len(self.files) - 1)
        self.image = wx.Image(self.files[self.index], wx.BITMAP_TYPE_ANY)
        self.Refresh()

    def on_exit(self, event):
        """
        Exit app
        """
        self.timer.Stop()
        self.Destroy()
        if event: event.Skip()

    def evt_scroll(self, event):
        """
        Display next or previous image depending on scroll direction
        """
        if event.WheelRotation < 0:
            self.on_next(None)
        else:
            self.on_prev(None)
        event.Skip()

    def evt_motion(self, event):
        """
        Pan only if left mouse button is down
        """
        if self.left_down:
            self.Refresh()
        else:
            self.timeout = 1
            self.show_cursor()
        event.Skip()

    def evt_size(self, event):
        """
        Redisplay image for new window size
        """
        self.Refresh()
        event.Skip()

    def evt_left_down(self, event):
        """
        Enable zoom and redisplay image
        """

        if wx.GetKeyState(wx.WXK_SHIFT):
            self.zoom = ZOOM_FACTOR * 2
        else:
            self.zoom = ZOOM_FACTOR

        self.left_down = True
        self.hide_cursor()
        self.Refresh()

        event.Skip()

    def evt_left_up(self, event):
        """
        Disable zoom and redisplay image
        """
        self.zoom = 1.0
        self.left_down = False
        self.Refresh()
        event.Skip()

    def on_full(self, event):
        """
        Toggle fullscreen and redisplay image
        """

        self.full_screen = not self.full_screen

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

        self.Refresh()

    def on_min(self, event):
        """
        Toggle minimized if full screen
        """

        if self.full_screen:
            self.minimized = not self.minimized

            if self.minimized:
                self.lastsize = self.GetSize()
                self.SetSize(10,10)
            else:
                self.SetSize(self.lastsize)
                self.Refresh()

    def evt_paint(self, event):
        """
        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 in the display window.
        """

        # Scale the image to fit and convert to bitmap           
        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
        # mx,my - mouse position in display window
        # px,py - position of mouse as a value from 0.0 to 1.0
        # sx,sy - position of top left bitmap relative to window

        if self.left_down:
            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.AutoBufferedPaintDC(self)
        dc.Clear()
        dc.DrawBitmap(bitmap, sx, sy, True)

        # Draw the text overlay
        dc.SetTextForeground(wx.WHITE)
        dc.SetTextBackground(wx.BLACK)
        dc.SetBackgroundMode(wx.BRUSHSTYLE_SOLID)
        note =  f'{self.files[self.index]}    {self.index+1} of {len(self.files)}'
        note += f'   {int(100*iw/self.image.Width)}%'
        note += f'   {self.image.Width}x{self.image.Height}'
        #print(f'{sw=} {sh=} {iw=} {ih=} {self.image.Width=} {self.image.Height=}')

        dc.DrawText(note, 0, 0)
        event.Skip()

    def ScaleToFit(self, image):
        """
        Scale an image to fit parent control
        """

        # 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 GetFiles(self, folder):
        """
        Return a list of all image files in the given folder
        """

        files = []

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

        return files

    def isImage(self, file):
        """
        Return True if file extension is in the given image list,
        else False
        """
        ext = os.path.splitext(file)[-1].lower()
        return ext in ('.jpg','.jpeg','.gif','.png')

    def hide_cursor(self):
        cursor = wx.Cursor(wx.CURSOR_BLANK)
        self.SetCursor(cursor)

    def show_cursor(self):
        cursor = wx.Cursor(wx.CURSOR_DEFAULT)
        self.SetCursor(cursor)

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()

If you are wondering why my initial docstring starts with r it is because Python complains about the back slashes inside the docstring.

commented: Ahoy, ye scurvy dog! If ye want to talk like a real pirate, ye need a hearty "Arrr!" +16
Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.