I recently came across a question on StackOverflow where the user wanted to know how to drag images onto their image control in wxPython and have the dragged image resize into a thumbnail. This piqued my interest and I decided to figure out how to do it.
I knew that you could create a thumbnail in Python using the Pillow package. So if you’d like to follow along you will need to install Pillow and wxPython with pip:
pip install Pillow wxPython
Now that we have the latest versions of the packages we need, we can write some code. Let’s take a look:
Note: You will want to have wxPython 4 to ensure that this works correctly
import os import wx from PIL import Image from wx.lib.pubsub import pub PhotoMaxSize = 240 class DropTarget(wx.FileDropTarget): def __init__(self, widget): wx.FileDropTarget.__init__(self) self.widget = widget def OnDropFiles(self, x, y, filenames): image = Image.open(filenames) image.thumbnail((PhotoMaxSize, PhotoMaxSize)) image.save('thumbnail.png') pub.sendMessage('dnd', filepath='thumbnail.png') return True class PhotoCtrl(wx.App): def __init__(self, redirect=False, filename=None): wx.App.__init__(self, redirect, filename) self.frame = wx.Frame(None, title='Photo Control') self.panel = wx.Panel(self.frame) pub.subscribe(self.update_image_on_dnd, 'dnd') self.createWidgets() self.frame.Show() def createWidgets(self): instructions = 'Browse for an image or Drag and Drop' img = wx.Image(240,240) self.imageCtrl = wx.StaticBitmap(self.panel, wx.ID_ANY, wx.Bitmap(img)) filedroptarget = DropTarget(self) self.imageCtrl.SetDropTarget(filedroptarget) instructLbl = wx.StaticText(self.panel, label=instructions) self.photoTxt = wx.TextCtrl(self.panel, size=(200,-1)) browseBtn = wx.Button(self.panel, label='Browse') browseBtn.Bind(wx.EVT_BUTTON, self.on_browse) self.mainSizer = wx.BoxSizer(wx.VERTICAL) self.sizer = wx.BoxSizer(wx.HORIZONTAL) self.mainSizer.Add(wx.StaticLine(self.panel, wx.ID_ANY), 0, wx.ALL|wx.EXPAND, 5) self.mainSizer.Add(instructLbl, 0, wx.ALL, 5) self.mainSizer.Add(self.imageCtrl, 0, wx.ALL, 5) self.sizer.Add(self.photoTxt, 0, wx.ALL, 5) self.sizer.Add(browseBtn, 0, wx.ALL, 5) self.mainSizer.Add(self.sizer, 0, wx.ALL, 5) self.panel.SetSizer(self.mainSizer) self.mainSizer.Fit(self.frame) self.panel.Layout() def on_browse(self, event): """ Browse for file """ wildcard = "JPEG files (*.jpg)|*.jpg" dialog = wx.FileDialog(None, "Choose a file", wildcard=wildcard, style=wx.OPEN) if dialog.ShowModal() == wx.ID_OK: self.photoTxt.SetValue(dialog.GetPath()) dialog.Destroy() self.on_view() def update_image_on_dnd(self, filepath): self.on_view(filepath=filepath) def on_view(self, filepath=None): if not filepath: filepath = self.photoTxt.GetValue() img = wx.Image(filepath, wx.BITMAP_TYPE_ANY) # scale the image, preserving the aspect ratio W = img.GetWidth() H = img.GetHeight() if W > H: NewW = PhotoMaxSize NewH = PhotoMaxSize * H / W else: NewH = PhotoMaxSize NewW = PhotoMaxSize * W / H img = img.Scale(NewW,NewH) self.imageCtrl.SetBitmap(wx.Bitmap(img)) self.panel.Refresh() if __name__ == '__main__': app = PhotoCtrl() app.MainLoop()
When you run this code, you should see something like this:
The first class subclass’s wx.FileDropTarget and does the thumbnail creation magic. You will note that we override the OnDropFiles() method and create a thumbnail using the PhotoMaxSize variable. Then we use pubsub to tell our wxPython application to update itself. The other class is actually a subclass of wx.App and contains all the bits and pieces we need to display images. Frankly we don’t need to subclass wx.App. We could have just as easily sub-classed from wx.Frame for this example.
Regardless, all we did in this code is create a frame and a panel object, subscribed to a specific subscription with pubsub so it listens for a message from the DropTarget class and then updates the display. You will note that when the application object receives a message via pubsub, it will call the update_image_on_dnd() method which in turn will call the onView() method. This code basically just scales down the image passed to it so it will fit in our wx.StaticBitmap control. Of course, since we’re already passing in an image that should fit, we could probably add some logic to this method that skips the scaling in certain cases. But I left it in there for when the user opens an image using the browse button.
Anyway, once we have the image scaled the way we want, we call SetBitmap and refresh the panel. Give it a try and see what you can do to improve this example!