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'.
Dietrich_3 153 Junior Poster
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
''' search_file_code.py
Search text based files for a given string
If found, show line number and filename
tested using IDLE IDE on LinuxMint VegasEat 28aug2026
'''
import glob
import os
# code segment ...
search = ".after"
# use current working directory to look at all python code files
directory = os.path.join(os.getcwd(), "*.py")
path_list = []
for path in glob.glob(directory):
#print(path) # testing,,,
path_list.append(path)
# do the search
for path in sorted(path_list):
dirname, fname = os.path.split(path)
linenum = 0
with open(path, errors="ignore") as fin:
code = fin.read()
for line in code.split('\n'):
linenum += 1
if search in line:
print(f"'{search}' line {linenum} of {fname}") Recommended Answers
Jump to Post— rproffitt 3,266From the past, grep?
grep -rnw '/path/to/your/folder' -e 'your_search_string'I remember folk quite scared of the command line.
Jump to Post— Reverend Jim 6,665I 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 …
Jump to Post— Reverend Jim 6,665On reviewing I think I might replace the clumsy hidden button/accelerator keys with a more generic keyboard handler.
All 9 Replies
rproffitt 3,266 https://5calls.org Team Colleague
From the past, grep?
grep -rnw '/path/to/your/folder' -e 'your_search_string'
I remember folk quite scared of the command line.
Reverend Jim 6,665 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster
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.
Edited by Reverend Jim
Dietrich_3 153 Junior Poster
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.
Edited by Dietrich_3
rproffitt commented: When I write regular expressions, God and I know how it works. Now, only God knows. +0
Dietrich_3 153 Junior Poster
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!
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
''' slide_file_image.py
Find/display working folder image files
IDLE IDE on Linux VegasEat 31aug2026
'''
import glob
import os
from PyQt6.QtCore import *
from PyQt6.QtGui import *
from PyQt6.QtWidgets import *
class Slides(QWidget):
def __init__(self, image_files):
QWidget.__init__(self)
self.image_files = image_files
self.label = QLabel(" ", self)
self.label.setGeometry(10, 30, 640, 640)
self.timer = QBasicTimer()
self.step = 0
self.delay = 5000 # ms
self.btn_start = QPushButton("Start Slide Show",self)
self.btn_start.move(10, 5)
self.btn_start.clicked.connect(self.timerEvent)
self.btn_stop = QPushButton("Stop Slide Show",self)
self.btn_stop.move(170, 5)
self.btn_stop.clicked.connect(self.timer.stop)
sf = "Slides are shown {} seconds apart"
self.setWindowTitle(sf.format(self.delay/1000.0))
def timerEvent(self, e=None):
if self.step >= len(self.image_files):
self.timer.stop()
self.setWindowTitle("Slide Show Finished")
return
self.timer.start(self.delay, self)
file = self.image_files[self.step]
image = QPixmap(file)
self.label.setPixmap(image)
scaled_pixmap = image.scaled(640, 620,
Qt.AspectRatioMode.KeepAspectRatio)
self.label.setPixmap(scaled_pixmap)
self.setWindowTitle(f"{self.step} --> {file}")
self.step += 1
# image files in current working folder
dir_png = os.path.join(os.getcwd(), "*.png")
dir_jpg = os.path.join(os.getcwd(), "*.jpg")
dir_gif = os.path.join(os.getcwd(), "*.gif")
path_list = []
for path in glob.glob(dir_png) + glob.glob(dir_jpg) + glob.glob(dir_gif):
path_list.append(path)
app = QApplication([])
ss = Slides(sorted(path_list))
ss.show()
app.exec() Reverend Jim 6,665 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster
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() Edited by Reverend Jim
Reverend Jim 6,665 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster
On reviewing I think I might replace the clumsy hidden button/accelerator keys with a more generic keyboard handler.
Reverend Jim 6,665 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster
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() Edited by Reverend Jim
Reverend Jim 6,665 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster
If you are wondering why my initial docstring starts with r it is because Python complains about the back slashes inside the docstring.
rproffitt commented: Ahoy, ye scurvy dog! If ye want to talk like a real pirate, ye need a hearty "Arrr!" +16
Reverend Jim 6,665 Hi, I'm Jim, one of DaniWeb's moderators. Moderator Featured Poster
Ah ah ah. Seventeen days too early.
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.