wxPython Image Viewer Shell Extension

Introduction

I recently bought a new laptop. Since I haven't used VB.Net in years (nothing but Python now) I did not install Visual Studio. That meant rewriting all my VB apps in Python/wxPython. One of my most often used apps was a shell extension I wrote to add a folder right-click option to launch a small image viewer. This short tutorial will explain how the pieces work. I will not get into how to write an app in wxPython. You will find other tutorials on that here on Daniweb.

I'll post the entire project as three files

  1. ZoomPic.pyw (main code)
  2. ZoomPic.reg (Windows registry mod - need to be edited)
  3. perCeivedType.py (Utility method for file screening)

I'll check this thread periodically for questions. I likely glossed over some things that may need further explanation. There's a reason I don't get paid to do this.

Drawing the display

The images are displayed in one of two modes, windowed, or full-screen. Normally, if you wanted to display an image you would use a wx.StaticBitmap control. The process would be

  • Read the data from the file into a wx.Image object
  • Scale the image to fit the size of the display control
  • Convert the image to a bitmap and copy to the wx.StaticBitmap

There are a few problems with this approach.

  • The image will be distorted unless the container has the same aspect ratio as the image
  • You will usually get an annoying flicker when changing files

We'll handle the first problem by writing a rescale method to ensure that the image will fit in the container with no distortion. We'll handle the second by using a method called double buffering to do the redrawing of the display.

Here is the rescale method.

cw,ch   the height and width of the container
iw,ih   the height and width of the image
aspect  the aspect ratio h/w of the original image
nw,nh   the new width and height adjusting for the container

After we calculate the new width and height based on the width of the container we then check to see if the new height is bigger than the container height. If so we recalculate the new width and height based on the container height.

Note that this only creates a scaled image. It does not display it.

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

Now comes the part I still find confusing - direct draw using double buffering. Normally you let wxPython handle refreshing the display but in our case we are going to force a redraw on several events. We'll get to those events in a minute. For now, let's see how to do a direct draw.

We are going to create something called a device context. Consider it like a printer driver, but for the screen. You can only create a device context within the event handler for an EVT_PAINT event which we will trigger manually by calling the Refresh() method. If it sounds confusing, it is. But it will make more sense in a minute. We are going to use double buffering.

I'm going to use an analogy to explain what double buffering is. I'm either going to insult your intelligence, or expose my ignorance (based on my limited understanding). You've all seen the flip-style animation where you draw a separate picture on the sheets of a book. When you flip the pages you see an animated scene. Imagine that every time you flipped to a new page you had to wait for the artist to draw the image. That would introduce an annoying delay. Now imagine that while you are looking at one image the artist is busy rendering the next (he's really fast) so that when you flip to the next page it is already rendered.

That is a simple-minded way of looking at double buffering. The new image is not displayed until it is ready. Here's how we do it. First we create the device context. Then we clear it and draw the bitmap into the device at the given starting x and y coordinates. Then, as a bonus, we add some text to the display.

dc = wx.AutoBufferedPaintDC(self)
dc.Clear()
dc.DrawBitmap(bitmap, sx, sy, True)
dc.SetTextForeground(wx.WHITE)
dc.SetTextBackground(wx.BLACK)
dc.DrawText(self.files[self.index], 5, 5)

Note that for this to work we must have set

self.SetBackgroundStyle(wx.BG_STYLE_CUSTOM)

in the MyFrame initialization. Why this is necessary was explained to me by "you just have to". I eventually found this in the wxPython official docs.

There are a lot of methods for the device context object for drawing all sorts of shapes, icons, etc. For the first iteration of our drawing method we are going to assume an image has already been loaded from the file and that we are not concerned about zoom/pan. Here is the first iteration of the on_paint method

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

    # 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
    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)
    dc.SetTextForeground(wx.WHITE)
    dc.SetTextBackground(wx.BLACK)
    dc.DrawText(self.files[self.index], 5, 5)
    evt.Skip()

Now we want to add a few controls to the interface. I like to use hotkeys. For one, navigating with things like arrow keys is easier (for me) than constantly moving the mouse and clicking, plus, putting buttons or menus on the display takes away from the picture. The hotkeys I've defined are

arrow left/up      prev image
arrow right/down   next image
f                  toggle full-screen/windowed (later)
esc                quit  

Mouse controls are

wheel              prev/next image
left-click         zoom
left-click/drag    zoom/pan

Hotkeys (or accelerators) can be linked to menu items and buttons. Because we want the buttons to be invisible we will make them zero-sized. Then we bind them to our custom event handlers. Each event handler ends with a call to Skip() to allow wxPython to do whatever default actions it does. Our button setup looks like

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

Linking the controls to hotkeys is done by

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

And the mouse setup looks like

# Connect Mouse events to event handlers
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.Bind(wx.EVT_MOUSEWHEEL, self.on_scroll)

The EVT_MOTION event handler will be used when zooming/panning.

Finally, we bind our handlers for painting and resizing

# Connect events to event handlers
self.Bind(wx.EVT_SIZE, self.on_size)
self.Bind(wx.EVT_PAINT, self.on_paint)
How it works

When the application starts, it scans the current working directory for image files. As a simple test, we can look for all files with an extension matching a list of known image files. That's what I use in this example in the isImage method. I'll include here a more general version which checks the Windows Registry to see what files Windows recognizes as images.

The file names are saved in a list and the first file is read into self.image. Since this is done before we bind the event handlers, when on_paint is called the first time the Window is displayed it will already have access to the first image. After that, any time we want to display a new image, all we have to do is read it into self.image then call Refresh to trigger on_paint.

By maintaining a flag (self.left_down) that keeps track of when the left mouse button is pressed, we can determine whether to display the image full size (relative to the window) or zoomed in.

One thing in particular I had trouble with was switching between windowed and full-screen. If I started the application in windowed mode I was able to toggle between modes with no problem. However, If I started in full-screen, toggling to windowed mode caused the app to hang. User Zig_Zag at the wxPython forum showed me how to fix it, but I still don't see why the fix works. Originally I had

self.ShowFullScreen(True)

in the initialization of MyFrame. When moved to OnInit for MyApp as

self.frame.ShowFullScreen(True)

the problem went away.

Adding as a shell extension

You can run ZoomPic from the command line by giving it a folder name as a parameter. More useful is having it available in the context menu for a folder. On my computer, the registry location for a folder context menu is under HKEY_CLASSES_ROOT at

Directory
    Shell
        ZoomPic
            command
                (Default) REG_SZ "C:\Users\rjdeg\AppData\Local\Programs\Python\Python310\pythonw.exe" "D:\apps\ZoomPic\ZoomPic.pyw" "%1"

There are likely other entries under Directory at the same level as Shell so we just add ZoomPic as a new one. This will not cause anything else to stop working. Note that the value of the REG_SZ value will change from system to system. You'll have to put in the full path for your pythonw.exe and zoompic.pyw. To make it easier, just take the attached file, ZoomPic.reg, modify it for your system, then run it (or double click on it) to add it to your registry.

More general file types
"""                                                                                    Name:                                                                                                                                                             
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)))
Recommended Reading

Two excellent books on developing applications in wxPython are

wxPython 2.8 Application Development Cookbook by Cody Precord.

Creating GUI Applications With wxPython by Michael Driscoll

The second book is more recent and uses wxPython features that may not have been available in earlier books.

Another excellent resource is wxPython Forum. The experts there were very helpful when I got stuck.

Noindex RSS feeds?

Has anyone ever come across any authoritative statement from Google on their official position of whether RSS feeds should be noindexed or not?

Open API and Omnichannel with Apache Kafka in Healthcare

IT modernization and innovative new technologies change the healthcare industry significantly. This blog series explores how data streaming with Apache Kafka enables real-time data processing and business process automation. Real-world examples show how traditional enterprises and startups increase efficiency, reduce cost, and improve the human experience across the healthcare value chain, including pharma, insurance, providers, retail, and manufacturing. This is part five: Open API and Omnichannel. Examples include Care.com and Invitae.

Blog Series - Kafka in Healthcare

Many healthcare companies leverage Kafka today. Use cases exist in every domain across the healthcare value chain. Most companies deploy data streaming in different business domains. Use cases often overlap. I tried to categorize a few real-world deployments into different technical scenarios and added a few real-world examples:

Inspirational Websites Roundup #40

Time for a fresh new roundup of wonderful website designs that will boost your creative inspiration! Sans serif is super trendy right now and dark color themes prevail with a touch of saturated goodness. It’s exciting to see that we entered an era of information underload with focus on what matters.

I really hope you enjoy our 40th roundup edition and get inspired to the max!

OBLIO

Bao Society

Metagolden

Pop In The City

Camille Mormal

Forner

GOALS

AI model

Future Green Steps

Fix Studio

[ Ledger ] Market

NODO x MAX

Weltio

E-bike Store

Elektra Virtual Museum

SAPPORO NISHIYAMA EUROPE GmbH

Les Mecs au Camion

Nick Herasimenka

KIKK Festival 2022

Studio HEED

SIXMOREVODKA

BiSH iS OVER!

PRESS BUTTER SAND

Oxide

Bakken & Bรฆck

Bravo musique

ORE

Tilton

Gentilhomme

Gleec

Amirali

Flecto

Deriving Ideal Indexes: A Guide

Indexes are there to simplify our work when searching for data: they speed up SELECT queries at the expense of slowing down other kinds of queries like DELETEs, UPDATEs, and INSERT s instead. However, as awesome as indexes might be, they also need a lot of work to get right โ€” in this blog, we will tell you how you should go about deriving ideal indexes for your database. The majority of the examples in this article will focus on MySQL: however, the concept is the same for all major database management systems available on the market today.

What Are Indexes?

If you are familiar with database structures, great โ€” because that's essentially what indexes are! Indexes are database structures that can be used to quickly find rows having specific column values. At the expense of taking up disk space and time if your tables are big and you find yourself adding indexes on top of them, indexes allow databases to skip reading through entire tables and instead, only scan relevant rows which means that databases have less data to scan through.

Securing Container Base Images Using Kyverno Policies

Before jumping into the details, itโ€™s worth explaining what the โ€œBase Imageโ€ term refers to. Words matter, there is some misunderstanding between the terms โ€œParentโ€ and โ€œBaseโ€ image. As defined by the Docker documentation, a parent of an image is the image used to start the build of the current image, typically the image identified in the FROM directive in the Dockerfile. If the parent image is SCRATCH, then the image is considered a base image.

However, the terms โ€œbase imageโ€ and โ€œparent imageโ€ are often used interchangeably in the container community.

Upgrading Kubernetes Clusters With Cluster API on Oracle Cloud

In this post, Iโ€™ll cover an awesome feature of Cluster API: the ability to do a rolling upgrade of your Kubernetes cluster. Cluster API makes it simple, and repeatable.

Iโ€™ll be totally honest, Iโ€™ve manually upgraded a Kubernetes cluster, it wasnโ€™t the end of the world, but Iโ€™m a lazy hacker so why do it manually when I can automate it and have the safety of repeatability?

Differences Between Windows and SQL Server Authentication

Authentication is a crucial aspect of any security strategy. It is a process or act of proving that a specific personโ€™s identity is true, valid, or genuine. To put it simply, it is an act of determining whether someone or something is what it actually claims to be.

When it comes to Microsoft SQL Server, there are two different modes of authentication, namely Windows mode and mixed mode. These two modes determine how the system authenticates or identifies a particular user

Common Mistakes to Avoid When Migrating

For enterprises, being able to migrate thousands of applications is an inevitable part of staying competitive. Figuring out how to achieve a successful migration is scary so letโ€™s dive into the pitfalls to avoid.

COVID-19 has created both a technical talent shortage combined with an increase in demand for accelerated technical timelines. Many companies are starting to face a โ€œRed Queenโ€ effect, where companies are having to re-define how and where they are competing in the market to ensure they remain relevant. Today businesses that remain complacent are rewarded with a biting disadvantage and a perpetual catchup cycle. Migrations from incumbent technologies to more responsive and cost-effective solutions are a significant part of any enterpriseโ€™s metamorphosis; however, it can be fraught with pitfalls for the unprepared.

How Do You Know If a Graph Database Solves the Problem?

One of the greatest questions to consistently badger a developer is "what technology should I use?". Days of thought and analysis determines which option(s) (from an increasingly growing number) best suits the need, manages volume and demand, plans for long-term strategy, simplifies/reduces support, and gets approved by colleagues and management.

Those steps may even seem easy compared to real life. The decision's complexity can get compounded by how much buy-in is needed, and the current constraints of existing technology plus developer knowledge. For instance, investing in an unknown or newer solution means allocation for learning costs.

Supabase and Nuxt 3 Quickstart Guide

This example provides the steps to build a simple user management app (from scratch!) using Supabase and Svelte. It includes:

  • Supabase Database: a Postgres database for storing your user data.
  • Supabase Auth: users can sign in with magic links (no passwords, only email).
  • Supabase Storage: users can upload a photo.
  • Row Level Security: data is protected so that individuals can only access their own data.
  • Instant APIs: APIs will be automatically generated when you create your database tables.

By the end of this guide you'll have an app that allows users to log in and update some basic profile details:

External Debugging Tools 3: JMXTerm

When tracking a bug we need to take a two-pronged approach. Similar to tongs that wrap the buggy module from both sides and squeezes to find the problematic part. Up until now, we discussed tools that are very low level. Some can be used to debug system-level services. Today weโ€™ll discuss the other side of the stack but still a very advanced management tool. To understand this you need to understand the field weโ€™re in.

As developers, we deal with code and applications. Deployment is for OPS/DevOps and the tooling they use is often alien to us. Itโ€™s not that they have bad tools. On the contrary, they have amazing tools. But theyโ€™re usually designed for massive scale. When you need to manage thousands of servers you need a way to control things in all of them. For that, we need a different set of tools.

Cloud Tagging Strategic Practices

The hype around cloud cost optimization is soaring high along with cloud adoption. According to NASSCOM, enterprises are expected to increase their cloud budget by nearly 5-15% CAGR till FY 2025. 

โ€œThe wider the cloud adoption, the more complex is the cloud cost managementโ€

Best Jira Add-ons for the Cloud

Do you know how they say thereโ€™s an app for everything nowadays? 

Well, I just went through the catalog on the Atlassian Marketplace. While not completely sold on the previous statement, I do find myself believing that thereโ€™s definitely an add-on for anything youโ€™d want Jira to do. And for the things you never knew you wanted.