Jpeg image lagging when zooming too much in FloatCanvas, NavCanvas Panning tool

Here you go. Not perfect, possibly needs work to adapt to your needs.

import wx

class ImageViewer(wx.Frame):

    def __init__(self, parent=None):

        wx.Frame.__init__(self, parent)

        self._scaleToFit = True
        self._panInProgress = False
        self._FirstPaint = True

        self._RawBitmap = wx.Bitmap("sample_image.jpeg", wx.BITMAP_TYPE_ANY)

        self.Bind(wx.EVT_PAINT, self.OnPaint)
        self.Bind(wx.EVT_MOUSEWHEEL, self.OnMouseWheel)
        self.Bind(wx.EVT_LEFT_DOWN, self.OnLeftDown)
        self.Bind(wx.EVT_LEFT_DCLICK, self.OnLeftDClick)
        self.Bind(wx.EVT_SIZE, self.OnSize)

        self.SetBackgroundStyle(wx.BG_STYLE_PAINT)

        self._ZoomFactor = 1.0
        self._Start_X_Pos = self._Start_Y_Pos = 0

        self._panVector = wx.Point2D(0, 0)
        self._inProgressPanStartPoint = wx.Point(0, 0)
        self._inProgressPanVector = wx.Point2D(0, 0)
        self._panInProgress = False

        self._BitmapWidth = self._RawBitmap.GetWidth()
        self._BitmapHeight = self._RawBitmap.GetHeight()

    def ScaleToFit(self):

        s = self.GetClientSize()

        win_aspect = s.x / s.y
        img_aspect = self._BitmapWidth / self._BitmapHeight

        # if the window is wider than the image, the height determines the scale factor
        if win_aspect > img_aspect:
            self._ZoomFactor = s.y / self._BitmapHeight
        else:
            self._ZoomFactor = s.x / self._BitmapWidth
        
        scaledImageWidth = self._BitmapWidth * self._ZoomFactor
        self._Start_X_Pos = (s.x - scaledImageWidth) / 2
        self._Start_X_Pos /= self._ZoomFactor
        
        scaledImageHeight = self._BitmapHeight * self._ZoomFactor
        self._Start_Y_Pos = (s.y - scaledImageHeight) / 2
        self._Start_Y_Pos /= self._ZoomFactor

        self.Refresh()

    def DoDrawCanvas(self, gc):

        gc.DrawBitmap(self._DrawBitmap, self._Start_X_Pos, self._Start_Y_Pos, self._BitmapWidth, self._BitmapHeight)
        self.SetTitle("x: %.2f, y: %.2f, zoom: %0.2f" % (self._Start_X_Pos, self._Start_Y_Pos, self._ZoomFactor))

    def OnSize(self, event):

        if self._scaleToFit:
            self.ScaleToFit()

        event.Skip()

    def OnPaint(self, event):

        dc = wx.AutoBufferedPaintDC(self)
        dc.Clear()

        # #direct2d renderer
        # d2dr = wx.GraphicsRenderer.GetDirect2DRenderer()
        # gc = d2dr.CreateContext(dc)

        gc = wx.GraphicsContext.Create(dc)

        if gc:
        
            totalPan = wx.Point2D(self._panVector.x + self._inProgressPanVector.x, self._panVector.y + self._inProgressPanVector.y)

            gc.Translate(-totalPan.x, -totalPan.y)
            gc.Scale(self._ZoomFactor, self._ZoomFactor)

            if self._FirstPaint:
            
                self._DrawBitmap = gc.CreateBitmap(self._RawBitmap)
                self._FirstPaint = False
            

            self.DoDrawCanvas(gc)
            del gc
        
    def OnMouseWheel(self, event):

        if self._panInProgress:
            self.FinishPan(False)
        
        rot = event.GetWheelRotation()
        delta = event.GetWheelDelta()

        oldZoom = self._ZoomFactor
        self._ZoomFactor += 0.25 * (rot / delta)

        if self._ZoomFactor < 0.1:
            self._ZoomFactor = 0.1
        
        # if self._ZoomFactor > 32.0:
            # self._ZoomFactor = 32
        
        a = oldZoom
        b = self._ZoomFactor

        # Set the panVector so that the point below the cursor in the new
        # scaled/panned cooresponds to the same point that is currently below it.
        uvPoint = event.GetPosition()
        newSTPoint = wx.Point2D((uvPoint.x + self._panVector.x) * b / a, (uvPoint.y + self._panVector.y) * b / a)
        self._panVector = wx.Point2D(newSTPoint.x - uvPoint.x, newSTPoint.y - uvPoint.y)

        self.Refresh()

    def ProcessPan(self, pt, refresh):

        self._inProgressPanVector = self._inProgressPanStartPoint - pt

        if refresh:
            self.Refresh()
    
    def FinishPan(self, refresh):

        if self._panInProgress:
        
            self.SetCursor(wx.NullCursor)

            if self.HasCapture():
                self.ReleaseMouse()
            
            self.Unbind(wx.EVT_LEFT_UP)
            self.Unbind(wx.EVT_MOTION)
            self.Unbind(wx.EVT_MOUSE_CAPTURE_LOST)

            self._panVector = wx.Point2D(self._panVector.x + self._inProgressPanVector.x, self._panVector.y + self._inProgressPanVector.y)
            self._inProgressPanVector = wx.Point2D(0, 0)
            self._panInProgress = False

            if refresh:
                self.Refresh()
     
    def OnLeftDClick(self, event):

        self._scaleToFit = not self._scaleToFit
        if self._scaleToFit:
            self.ScaleToFit()

    def OnLeftDown(self, event):

        cursor = wx.Cursor(wx.CURSOR_HAND)
        self.SetCursor(cursor)

        self._inProgressPanStartPoint = event.GetPosition()
        self._inProgressPanVector = wx.Point2D(0, 0)
        self._panInProgress = True

        self.Bind(wx.EVT_LEFT_UP, self.OnLeftUp)
        self.Bind(wx.EVT_MOTION, self.OnMotion)
        self.Bind(wx.EVT_MOUSE_CAPTURE_LOST, self.OnCaptureLost)

        self.CaptureMouse()

    def OnMotion(self, event):

        self.ProcessPan(event.GetPosition(), True)

    def OnLeftUp(self, event):

        self.ProcessPan(event.GetPosition(), False)
        self.FinishPan(True)

    def OnCaptureLost(self, event):

        self.FinishPan(True)


if __name__ == '__main__':

    app = wx.App(0)
    frame = ImageViewer(None)

    frame.SetSize(1200, 800)
    frame.Show()
    app.MainLoop()
    

Ah Thank you this seem to do the trick when zooming in this doesn’t seem to lag
though if at certain zoom anti aliasing could be disables i’d be great and as for the drag panning how I want it to behave approximatelly.
it’s just that now it lags when zooming out XD

I’ll get to working on it a bit and update!

I submitted a PR to fix this.
The basic idea is simple: if the image exceeds a certain threshold size (e.g., 20M pixels), it switches from DC to GraphicsContext to avoid rescaling the image. You can try the fix by applying the following monkey-patch in your code:

## Monkey-patch for ScaledBitmap zooming
if 1:
    from wx.lib.floatcanvas import FCObjects

    def _Draw(self, dc , WorldToPixel, ScaleWorldToPixel, HTdc=None):
        XY = WorldToPixel(self.XY)
        H = ScaleWorldToPixel(self.Height)[0]
        W = int(H * (self.bmpWidth / self.bmpHeight))
        XY = self.ShiftFun(XY[0], XY[1], W, H)
        if 0 < H * float(W) < 20e6:  # cf. FHD 1,920 x 1,080
            if (self.ScaledBitmap is None) or (H != self.ScaledHeight):
                Img = self.Image.Scale(W, H)
                self.ScaledHeight = H
                self.ScaledBitmap = wx.Bitmap(Img)
            dc.DrawBitmap(self.ScaledBitmap, XY, True)
        else:
            gc = wx.GraphicsContext.Create(dc)
            gc.DrawBitmap(self.ScaledBitmap, *XY, W, H)
        if HTdc and self.HitAble:
            HTdc.SetPen(self.HitPen)
            HTdc.SetBrush(self.HitBrush)
            HTdc.DrawRectangle(XY, (W, H))

    FCObjects.ScaledBitmap._Draw = _Draw

Any advice would be appreciated.