Zooming into a fixed point on a ScrollableControl

If I'd built subtitle support into the CMS that powers this website, then surely the subtitle would have been "or how I fixed that annoying zoom bug in the ImageBox control". And with that digression out of the way, onto the article, a nice and short one for a change!

I should probably point out that this article doesn't describe how to actually do any zooming (as that is dependant on what it is you are actually doing a zoom upon), but rather how to keep the viewport focused on a given point after zooming. To learn about zooming, please see previous articles that describe the ImageBox control in detail.

Users of the ImageBox control are probably aware of the zoom bug, where each time you use the mouse wheel to zoom in or out, the final image position is slightly offset, as shown by this short animation:

Fixing this bug is actually quite simple and I'm actually embarrassed at how long it took to fix and how I missed the solution for so long. The key to resolving this issue is finding out the document position under the mouse (by which I mean the position in the entire scroll area, not just the visible viewport) before applying the zoom, and then recalculating this position with the new zoom level, offset by the mouse position in the client control.

As it's probably easier just to show you the code rather than try and describe it, it results in this small function:

public virtual void ScrollTo(Point imageLocation, Point relativeDisplayPoint)
{
  int x;
  int y;

  x = (int)(imageLocation.X * this.ZoomFactor) - relativeDisplayPoint.X;
  y = (int)(imageLocation.Y * this.ZoomFactor) - relativeDisplayPoint.Y;

  this.AutoScrollPosition = new Point(x, y);
}

To use it, you add code similar to the following where you process mouse clicks, or mouse wheel, however you control zooming with the mouse:

Point cursorPosition;
Point currentPixel;
int currentZoom;

// TODO: Obtain cursor position from MouseEventArgs etc.

currentPixel = this.PointToImage(cursorPosition);
currentZoom = this.Zoom;

// TODO: Perform zoom here

if (this.Zoom != currentZoom)
  this.ScrollTo(currentPixel, cursorPosition);

So how does this work?

  1. Get the mouse cursor position, relative to the control
  2. Convert that position into the position of your virtual document - for the ImageBox control we use the PointToImage method
  3. Perform your zoom and recalculate the document scroll size etc.
  4. Call the ScrollTo method, passing in the document position and mouse cursor position

And now you end up with something similar to this:

There is one case where this does not work as expected - when you scroll in or out sufficiently to remove the scrollbars, or when moving from no-scrollbars to scrollbars. However, I think is fine given it works so well the rest of the time!

That's fine, but where's the ImageBox update?

Thanks to a generous donation from a visitor to the site, I recently sat down to work on the ImageBox control and resolve some of the issues - like the scrolling above. The next update has quite a lot of new functionality (better keyboard support, configurable zoom levels, flicker free scrolling and a handful of bug fixes to name a few of the changes) and will be posted presently. While the build is being finalized however, the above code will work fine in current builds of the ImageBox, if you adjust for the pixel offset the current PointToImage implementation uses.

About The Author

Gravatar

The founder of Cyotek, Richard enjoys creating new blog content for the site. Much more though, he likes to develop programs, and can often found writing reams of code. A long term gamer, he has aspirations in one day creating an epic video game. Until that time, he is mostly content with adding new bugs to WebCopy and the other Cyotek products.

Leave a Comment

While we appreciate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?

Styling with Markdown is supported