After running into animation problems in Silverlight 3 and 4, I?m implementing my user interface using full blown WPF. This Pan and Zoom code works great, with a minor tweak and one issue: I need to control the allowable zoom ranges, and I don?t want the user to be able to pan the image ?right off of the map?, instead I need a hard border on all four sides. And these hard-stops need to pay attention to the current zoom level as well.
Minimum and Maximum Zoom
This part is easy ? I added two properties, MinimumZoom and MaximumZoom, default them to double.MinValue and double.MaxValue, and some trivial code in DoZoom to keep the scale values in line:
1: public PanAndZoomViewer() {
2: MaximumZoom = double.MaxValue;
3: MinimumZoom = double.MinValue;
4: }
5: public double MaximumZoom { get; set; }
6: public double MinimumZoom { get; set; }
7:
8: public void DoZoom(double deltaZoom,
9: Point mousePosition,
10: Point physicalPosition) {
11: // Keep Zoom within bounds declared by Minimum/MaximumZoom
12: double currentZoom = zoomTransform.ScaleX;
13: currentZoom *= deltaZoom;
14: if (currentZoom < MinimumZoom)
15: currentZoom = MinimumZoom;
16: else if (currentZoom > MaximumZoom)
17: currentZoom = MaximumZoom;
18: // do zoom animation stuff...
19: }
Limiting Pan Transform
This required slightly more thought, and some graph paper. Having a clue about how WPF deals with its coordinate systems would have been helpful too. I have a Canvas as the Content of my PanAndZoomViewer. No matter what panning & zooming happens, this Canvas will always have a fixed Height and Width. The Origin in our setup is always going to be the upper left corner of the Canvas, and remember that in the coordinate system used by Windows, ?up? and ?left? from that point are negative values. (The Y axis is the opposite of what you?d expect). Here?s a lame Paint.NET picture to help you understand:
The Red View area is the part of the map canvas you?re zoomed into and viewing. The blue area is the entire map. I don?t want you to be able to move the Red rectangle beyond the boundaries of the Blue Rectangle, because instead of viewing the map you?d be viewing the dining room table, or the Red Rectangle would fall off the table onto the floor, or something.
Handling the Left and Top boundaries of the Blue Rectangle are easy enough, at any zoom level. If you were to move the Red Rectangle to beyond either of those edges, then one or both of the values for (ToX, ToY) would be positive (past the Origin corner). So we reset positive values back to zero and then animate to keep things nice and smooth.
Pan Right, Bottom
The other two edges are slightly less intuitive, at least to me. Consider the Right edge of the Red Rectangle View: I don?t want this edge passing over the Blue Rectangle. This puts a ?maximum? value on the distance from the Left Edge of the Red Rectangle (at X = 0), and the Left Edge of the Blue Rectangle. Except that this value would be negative being to the left of the origin, so it?s really a minimum. (Trivial arithmetic, I know, and yet I still messed it up the first time.)
So, the minimum allowed ToX value is content.Width – (content.Width * scale). If you try to move the Red Rectangle farther away, we just snap it back to the edge. Same thing with the minimum Y values.
The Result
The code to handle this looks something like this:
1: private void control_MouseMove(object sender, MouseEventArgs e)
2: {
3: if (!IsMouseCaptured)
4: return; // don't care.
5:
6: // if the mouse is captured then move the content by changing the translate transform.
7: // use the Pan Animation to animate to the new location based on the delta between the
8: // starting point of the mouse and the current point.
9: Point physicalPoint = e.GetPosition(this);
10:
11: // where you'd like to move the top left corner of the view to
12: double toX = physicalPoint.X - ScreenStartPoint.X + startOffset.X;
13: double toY = physicalPoint.Y - ScreenStartPoint.Y + startOffset.Y;
14: Console.WriteLine("You're attempting to move to " + toX + "," + toY);
15:
16: double scaleValue = zoomTransform.ScaleX;
17: var content = (FrameworkElement)Content;
18:
19: // minimum values we can shift the origin to -
20: // maximum values is always 0 (we don't want the left side of the content
21: // ever being beyond the left part of the view
22: double minToX = content.Width - (content.Width * scaleValue);
23: double minToY = content.Height - (content.Height * scaleValue);
24:
25: // correct any invalid amounts:
26: if (toX > 0)
27: toX = 0;
28: else if (toX < minToX)
29: toX = minToX;
30:
31: if (toY > 0)
32: toY = 0;
33: else if (toY < minToY)
34: toY = minToY;
35:
36: translateTransform.BeginAnimation(TranslateTransform.XProperty, CreatePanAnimation(toX),
37: HandoffBehavior.Compose);
38: translateTransform.BeginAnimation(TranslateTransform.YProperty, CreatePanAnimation(toY),
39: HandoffBehavior.Compose);
40: }