3
votes

I am writing a program that displays a map, and on top of it another layer where position of cameras and their viewing direction is shown. The map itself can be zoomed and panned. The problem is that the map files are of significant size and zooming does not go smoothly.

I created class ZoomablePictureBox : PictureBox to add zooming and panning ability. I tried different methods, from this and other forums, for zooming and panning and ended up with the following, firing on the OnPaint event of ZoomablePictureBox:

  private void DrawImgZoomed(PaintEventArgs e)
    {
        e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;

            if (imgZoomed != null)
                e.Graphics.DrawImage(imgZoomed, new Rectangle(-ShiftX, -ShiftY, imgZoomed.Width, imgZoomed.Height), 0, 0, imgZoomed.Width, imgZoomed.Height, GraphicsUnit.Pixel);

    }

Where ShiftX and ShiftY provide proper map panning (calculation irrelevant for this problem).

imgZoomed is zoomed version of original map calculated in BackgroundWorker everytime the zoom changes:

private void bgWorker_DoWork(object sender, DoWorkEventArgs e)
    {

        Bitmap workerImage = e.Argument as Bitmap;
        Bitmap result;

        result = new Bitmap(workerImage, new Size((int)(workerImage.Width * Zoom), (int)(workerImage.Height * Zoom)));

        e.Result = result;
    }

So current approach is, that everytime user scrolls mousewheel, the new imgZoomed is calculated based on current Zoom. With map size of ~30 MB this can take up to 0,5 second which is annoying, but panning runs smoothly.

I realize that this may not be the best idea. In previous approach i did not create zoomed image copy everytime mouse is scrolled but did this instead:

e.Graphics.DrawImage(Image, new Rectangle(-ShiftX, -ShiftY, (int)(this._image.Width * Zoom), (int)(this._image.Height * Zoom)), 0, 0, Image.Width, Image.Height, GraphicsUnit.Pixel);

Zoom was much smoother, because from what i understand, it just stretched original image. On the other hand panning was skipping heavily.

I wast thinking of:

  • creating copies of orignial map for each zoom in memory/on hard drive - it will take up too much memory/hdd space
  • creating copies of orignial map for next/actual/previous zooms so i have more time to calculate next step - it will not help if user scrolls more than one step at a time

I also tried matrix transformations - no real performance gain, and calculating pan was a real pain in the arse.

I'm running in circles here and don't know how to do it. If i open the map in default Windows picture viewer zooming and panning is smooth. How do they do that?

In what way do I achieve smooth zooming and panning at the same time?

1
I've recently implemented something similar and after many tests the smoothest performance I got by far was achieved using StretchBlt with cached Graphics and HBitmap objects. This helped me achieve super smooth display even on 7000X7000 images.Rotem

1 Answers

2
votes

Sorry this is so long, I left in only the essential parts.

Most of the p/invoke stuff is taken from pinvoke.net

My solution for smooth panning across large images involves using the StretchBlt gdi method instead of Graphics.DrawImage. I've created a static class that groups all the native blitting operations.

Another thing that helps greatly is caching the HBitmap and Graphics objects of the Bitmap.

public class ZoomPanWindow
{
    private Bitmap map;
    private Graphics bmpGfx;
    private IntPtr hBitmap;

    public Bitmap Map
    {
        get { return map; }
        set 
        {
            if (map != value)
            {
                map = value;
                //dispose/delete any previous caches
                if (bmpGfx != null) bmpGfx.Dispose();
                if (hBitmap != null) StretchBltHelper.DeleteObject(hBitmap);
                if (value == null) return;
                //cache the new HBitmap and Graphics.
                bmpGfx = Graphics.FromImage(map);
                hBitmap = map.GetHbitmap();
             }
        }
    }

    protected override void OnPaint(PaintEventArgs e)
    {
        if (map == null) return;
        //finally, the actual painting!
        Rectangle mapRect = //whatever zoom/pan logic you implemented.
        Rectangle thisRect = new Rectangle(0, 0, this.Width, this.Height);
        StretchBltHelper.DrawStretch(
            hBitmap,
            bmpGfx,
            e.Graphics,
            mapRect,
            thisRect);
    }
}

public static class StretchBltHelper
{
    public static void DrawStretch(IntPtr hBitmap, Graphics srcGfx, Graphics destGfx,
        Rectangle srcRect, Rectangle destRect)
    {
        IntPtr pTarget = destGfx.GetHdc();
        IntPtr pSource = CreateCompatibleDC(pTarget);
        IntPtr pOrig = SelectObject(pSource, hBitmap);
        if (!StretchBlt(pTarget, destRect.X, destRect.Y, destRect.Width, destRect.Height,
            pSource, srcRect.X, srcRect.Y, srcRect.Width, srcRect.Height,
            TernaryRasterOperations.SRCCOPY))
        throw new Win32Exception(Marshal.GetLastWin32Error());

        IntPtr pNew = SelectObject(pSource, pOrig);
        DeleteDC(pSource);
        destGfx.ReleaseHdc(pTarget);
    }

    [DllImport("gdi32.dll", EntryPoint = "SelectObject")]
    public static extern System.IntPtr SelectObject(
        [In()] System.IntPtr hdc,
        [In()] System.IntPtr h);

    [DllImport("gdi32.dll", ExactSpelling = true, SetLastError = true)]
    static extern bool DeleteDC(IntPtr hdc);

    [DllImport("gdi32.dll", EntryPoint = "DeleteObject")]
    [return: MarshalAs(UnmanagedType.Bool)]
    public static extern bool DeleteObject(
        [In()] System.IntPtr ho);

    [DllImport("gdi32.dll", ExactSpelling = true, SetLastError = true)]
    static extern IntPtr CreateCompatibleDC(IntPtr hdc);

    [DllImport("gdi32.dll")]
    static extern bool StretchBlt(IntPtr hdcDest, int nXOriginDest, int nYOriginDest,
        int nWidthDest, int nHeightDest,
        IntPtr hdcSrc, int nXOriginSrc, int nYOriginSrc, int nWidthSrc, int nHeightSrc,
        TernaryRasterOperations dwRop);

    public enum TernaryRasterOperations : uint
    {
        SRCCOPY = 0x00CC0020
        //there are many others but we don't need them for this purpose, omitted for brevity
    }
}