Scaled static bitmap is being cropped on load

Because several of my apps must display imagers I wanted to create an image control that I could reuse. Initially all I wanted it to do was display the image file in the control, scaled to fit (show the entire image), and to be resizeable. Simple task which appeared to work except I noticed one small glitch.

When the image is initially loaded, part of the image is cropped. If I resized the window, even by as little as one pixel, the entire image was redisplayed with no cropping. Sizing it back down to the original app size properly scaled the image with no cropping. After several hours of dicking around with it I still cannot get the image to display with no cropping on load. Any suggestions?

FYI the mouse handler is a place holder for now. I eventually hope to add zoom/pan features.

I’ll try to post two screen caps. The first should show the display on first load. The second shows the display after I very slightly resize up, then down by one pixel.

import wx

class ImagePanel(wx.Panel):
    
    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.bmpImage = wx.StaticBitmap(self, wx.ID_ANY)
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.bmpImage, 1, wx.EXPAND, 0)
        self.SetSizer(sizer)
        self.Layout()

        self.bitmap = None      #loaded image in bitmap format
        self.image  = None      #loaded image in image format 
        
        self.blank = wx.Bitmap(1,1)

        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)
        
    def OnSize(self, event):
        """When panel is resized, scale the image to fit"""
        self.ScaleToFit()
        event.Skip()

    def OnMouseWheel(self, event):
        print('')
        obj = event.GetEventObject()
        m = wx.GetMouseState()
        print(f'{m.ControlDown()=}')
        print(f'{m.ShiftDown()=}')
        print(f'{m.LeftIsDown()=}')
        print(f'{m.RightIsDown()=}')
        print(f'{m.GetPosition()=}')
        print(f'{m.GetX()=}')
        print(f'{m.GetY()=}')
        mx,my = m.GetPosition()
        px,py = obj.Parent.GetPosition()
        iw,ih = obj.GetSize()
        print(f'{mx-px=} {my-py=} {iw=} {ih=}')

        event.Skip()

    def Load(self, file:str) -> None:
        """Load the image file into the control for display"""
        self.bitmap = wx.Bitmap(file, wx.BITMAP_TYPE_ANY)
        self.image  = wx.Bitmap.ConvertToImage(self.bitmap)
        self.bmpImage.SetBitmap(self.bitmap)
        self.ScaleToFit()

    def Clear(self):
        """Set the displayed image to blank"""
        self.bmpImage.SetBitmap(self.blank)

    def ScaleToFit(self) -> None:
        """
        Scale the image to fit in the container while maintaining
        the original aspect ratio.
        """
        if self.bitmap:
            
            #get container (c) and image (i) dimensions
            cw,ch = self.Parent.GetSize()
            iw,ih = self.image.GetSize()

            aspect = ih/iw

            #calculate width to fit, and height from aspect ratio
            nw = cw
            nh = int(nw * aspect)

            #if new height is too large then fit height,
            #and calculate width from aspect ratio
            if nh > ch:
                nh = ch
                nw = int(nh / aspect)

            print(f'{cw=} {ch=} {nw=} {nh=}')                
  
            #scale the image to new dimensions and display
            image = self.image.Scale(nw,nh)
            self.bmpImage.SetBitmap(image.ConvertToBitmap())
            
            self.Refresh()


if __name__ == "__main__":
    app = wx.App()
    frame = wx.Frame(None)
    frame.SetSize((786,486))
    panel = ImagePanel(frame)
    frame.Show()
    panel.Load('D:\\test.jpg')
    app.MainLoop()

Hi ReverendJim,
I think you’ll need a layout there :thinking:

...    
    panel.Load('D:\\test.jpg')
    panel.Layout()
    app.MainLoop()

or optionally you could load the image before you show the frame.

    ...
    panel = ImagePanel(frame)
    panel.Load('D:\\test.jpg')
    frame.Show()
    ...

What’s happening with the resize is that it’s calling Layout in the background - unless you specifically disable it.

One tiny mistake. Thanks. Adding the zoom in/out was easy after that.

import wx
import wx.lib.mixins.inspection

class ImagePanel(wx.Panel):
    
    def __init__(self, parent, id=wx.ID_ANY,
                 pos   = wx.DefaultPosition,
                 size  = wx.DefaultSize,
                 style = wx.BORDER_SUNKEN
                 ):
                 
        super().__init__(parent, id, pos, size, style=style)

        self.bmpImage = wx.StaticBitmap(self, wx.ID_ANY)
        sizer = wx.BoxSizer(wx.HORIZONTAL)
        sizer.Add(self.bmpImage, 1, wx.EXPAND, 0)
        self.SetSizer(sizer)
        
        self.bitmap = None      #loaded image in bitmap format
        self.image  = None      #loaded image in image format 
        self.aspect = None      #loaded image aspect ratio
        self.zoom   = 1.0
        
        self.blank  = wx.Bitmap(1,1)

        self.Bind(wx.EVT_SIZE, self.OnSize)
        self.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)

        wx.lib.inspection.InspectionTool().Show()

        self.e = None

    def OnSize(self, event):
        """When panel is resized, scale the image to fit"""
        self.ScaleToFit()
        event.Skip()

    def OnMouseWheel(self, event):
        """zoom in/out on CTRL+scroll"""
        m = wx.GetMouseState()

        if m.ControlDown():
            delta = 0.1 * event.GetWheelRotation() / event.GetWheelDelta()
            self.zoom = max(1, self.zoom + delta)
            self.ScaleToFit()

        event.Skip()

    def Load(self, file:str) -> None:
        """Load the image file into the control for display"""
        self.bitmap = wx.Bitmap(file, wx.BITMAP_TYPE_ANY)
        self.image  = wx.Bitmap.ConvertToImage(self.bitmap)
        self.aspect = self.image.GetSize()[1] / self.image.GetSize()[0]
        self.zoom   = 1.0
        self.bmpImage.SetBitmap(self.bitmap)

        self.ScaleToFit()

    def Clear(self):
        """Set the displayed image to blank"""
        self.bmpImage.SetBitmap(self.blank)

    def ScaleToFit(self) -> None:
        """
        Scale the image to fit in the container while maintaining
        the original aspect ratio.
        """
        
        if self.image:
            
            #get container (c) dimensions
            cw,ch = self.GetSize()

            #calculate new (n) dimensions with same aspect ratio
            nw = cw
            nh = int(nw * self.aspect)

            #if new height is too large then recalculate sizes to fit
            if nh > ch:
                nh = ch
                nw = int(nh / self.aspect)

            #Apply zoom
            nh = int(nh * self.zoom)
            nw = int(nw * self.zoom)
  
            #scale the image to new dimensions and display
            image = self.image.Scale(nw,nh)
            self.bmpImage.SetBitmap(image.ConvertToBitmap())
            self.Layout()


if __name__ == "__main__":
    app = wx.App()
    frame = wx.Frame(None)
    frame.SetSize((800,625))
    panel = ImagePanel(frame)
    frame.Show()
    panel.Load('D:\\test.jpg')
    app.MainLoop()