Hello,

I am attempting to develop a custom control that allows the user to
zoom to a specific point on an image, typically a map. The problem
that I am having is when I attempt to adjust the scrollbar position,
the update of the image is very jerky. For example, in the code below,
when the user selects a point on the image, the image is zoomed in on
the point and the scroll bar position is updated to the new origin of
the viewport. When the image is updated, it appears to be drawn at its
original position and scale, then the control's OnPaint method is
called and the image is drawn correctly. This gives the appearance of
the image moving left and up before snapping to the zoomed point. The
problem seems to be exacerbated when double buffering is enabled. Has
anyone else experienced this?

Here's the example code:

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows.Forms;
using System.Drawing.Imaging;
using System.Drawing.Drawing2D;
using System.Drawing;

namespace Zoom
{
    public class ZoomPoint : ScrollableControl
    {
        enum ZoomDirection
        {
            In,
            Out
        }
        #region Private Data
        private float _zoom = 1.0f;
        private PointF _origin = new PointF(0, 0);
        private Image _image = null;
        #endregion

        public ZoomPoint() {
            SetStyle(ControlStyles.UserPaint, true);
            SetStyle(ControlStyles.AllPaintingInWmPaint, true);
            SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
            SetStyle(ControlStyles.ResizeRedraw, true);
            this.AutoScroll = true;
            UpdateScroll();
        }

        public Image Image {
            get {
                return _image;
            }
            set {
                _image = value;
                _origin = PointF.Empty;
                _zoom = 1.0F;
                UpdateScroll();
                Invalidate();
            }
        }

        protected override void OnPaintBackground(PaintEventArgs e) {
            // don't allow the background to be painted
        }

        protected override void OnPaint(PaintEventArgs e) {

            Graphics g = e.Graphics;

            ClearBackground(g);

            float dx = -_origin.X;
            float dy = -_origin.Y;

            g.Transform = new Matrix(_zoom, 0, 0, _zoom, dx, dy);
            g.FillRectangle(Brushes.Blue, 100, 100, 5, 5);
            DrawImage(g);
        }

        private void ClearBackground(Graphics g) {
            g.Clear(SystemColors.Window);
        }

        protected override void OnScroll(ScrollEventArgs se) {
            base.OnScroll(se);

            if (se.ScrollOrientation ==
ScrollOrientation.HorizontalScroll) {
                _origin.X += se.NewValue - se.OldValue;
            }
            else {
                _origin.Y += se.NewValue - se.OldValue;
            }
            Invalidate();
        }

        protected override void OnMouseWheel(MouseEventArgs e) {
            if (e.Delta > 0) {
                ZoomToPoint(e.Location, ZoomDirection.In);
            }
            else {
                ZoomToPoint(e.Location, ZoomDirection.Out);
            }
            Invalidate();
        }

        protected override void OnMouseClick(MouseEventArgs e) {
            ZoomToPoint(e.Location, ZoomDirection.In);
            Invalidate();
        }

        private void UpdateScroll() {

            if (_image != null) {

                Size scrollSize = new Size(
                    (int)Math.Round(_image.Width * _zoom),
                    (int)Math.Round(_image.Height * _zoom));

                Point position = new Point(
                    (int)Math.Round(_origin.X),
                    (int)Math.Round(_origin.Y));

                this.AutoScrollMinSize = scrollSize;
                this.AutoScrollPosition = position;
            }
            else {
                this.AutoScrollMargin = this.Size;
            }

        }

        private void ZoomToPoint(Point viewPoint, ZoomDirection
direction) {
            // get the model point
            PointF modelPoint = ToModelPoint(viewPoint);

            if (direction == ZoomDirection.In) {
                // Increase the zoom
                _zoom *= 1.25F;
            }
            else {
                // decrease the zoom
                _zoom *= .75F;
            }

            // calculate the new origin
            _origin.X = (modelPoint.X * _zoom) - viewPoint.X;
            _origin.Y = (modelPoint.Y * _zoom) - viewPoint.Y;

            UpdateScroll();
        }

        private PointF ToModelPoint(Point viewPoint) {
            PointF modelPoint = new PointF();

            modelPoint.X = (_origin.X + viewPoint.X) / _zoom;
            modelPoint.Y = (_origin.Y + viewPoint.Y) / _zoom;

            return modelPoint;
        }

        private void DrawImage(Graphics g) {
            if (null != _image) {
                // set the transparency color for the image
                ImageAttributes attr = new ImageAttributes();
                attr.SetColorKey(Color.White, Color.White);

                Rectangle destRect = new Rectangle(0, 0, _image.Width,
_image.Height);
                g.DrawImage(_image, destRect, 0, 0, _image.Width,
_image.Height, GraphicsUnit.Pixel, attr);
            }
        }

        protected override void Dispose(bool disposing) {
            if (disposing) {
                if (null != _image) {
                    _image.Dispose();
                    _image = null;
                }
            }
            base.Dispose(disposing);
        }
    }

Well
Rather than the code, you need to take the zoom and work out the new size of the scrollbar.. as what is now subject to the scrollbar is no longer the same size.

I don't think I see what you mean. The size of the srollbar, or the AutoScrollMinSize, is calculated as the zoomed image size. That part seems to work fine. The scroll position is then set to the origin of the viewport, which seems to be exactly where I want it. But when I do set the position, there seems to be a major flicker.

After some experiments, I found that If I perform the following steps, I don't get the same flicker.

1. Call ZoomToPoint
2. Set the AutoScrollMinSize property based on the scaled image size
3. Call Invalidate
4. Set the AutoScrollPosition property to the origin.

This article has been dead for over six months. Start a new discussion instead.