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);
        }
    }

Dani AI

Generated

This is almost always a timing/layout problem rather than a drawing bug in your paint code. Changing the scrolled area (AutoScrollMinSize) and then changing the scroll position (AutoScrollPosition) each cause layout/paint work — Windows/WinForms can do more than one paint pass and you see the old transform briefly, then the final one. is correct that the scrollbar size must reflect the zoom, and ’s experiment (setting the min size first) shows the same root cause.

Best-practice fix: batch the scroll-related property changes so only one layout/paint occurs. Suspend layout, set AutoScrollMinSize, set AutoScrollPosition, resume layout and then Invalidate once. Example pattern:

SuspendLayout();
AutoScrollMinSize = new Size((int)Math.Round(image.Width * zoom),
                             (int)Math.Round(image.Height * zoom));
// When setting AutoScrollPosition use positive coordinates; its getter returns negative values.
AutoScrollPosition = new Point((int)Math.Round(origin.X),
                               (int)Math.Round(origin.Y));
ResumeLayout(false);
Invalidate();

Extra tips that reduce visible flicker:

  • Keep DoubleBuffering and AllPaintingInWmPaint enabled (you already do).
  • Suppress background erasing to avoid an extra paint pass by ignoring WM_ERASEBKGND:
protected override void WndProc(ref Message m)
{
    const int WM_ERASEBKGND = 0x0014;
    if (m.Msg == WM_ERASEBKGND) return;
    base.WndProc(ref m);
}
  • If you still see a snap, avoid the built-in AutoScroll for tight animation: either call the protected SetDisplayRectLocation from your subclass (to move the virtual rect in one step) or use explicit HScrollBar/VScrollBar controls and draw using your transform — that gives you complete control over when paints happen.

Order matters: update the min-size first, then the position, and batch those updates. That removes the extra layout/paint pass that causes the “move then snap” effect.

Recommended Answers

All 2 Replies

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.

Be a part of the DaniWeb community

We're a friendly, industry-focused community of developers, IT pros, digital marketers, and technology enthusiasts meeting, networking, learning, and sharing knowledge.