How to set panning limits on WPF Canvas based on zoom level? - wpf

I'm trying to set zooming and panning limits on a control I found here:
https://wpfextensions.codeplex.com
I managed to set zooming limits, but now I'm having trouble setting the panning limits so that you can't pan the object inside the canvas, outside the view.
I succeeded in setting the limits, but only when the zoom level is 1 (Zoom == 1, so no zoom), but the moment you increase the zoom (by rotating the mouse wheel) things start to go wrong: the limits are set, but they are not set correctly.
In order to set them correctly I have to take into consideration the deltaZoom (the amount zoom changed compared to the previous zoom value).
Small demo project
I have created a simple, standalone project where I can reproduce the issue:
https://github.com/igorpopovio/CanvasZoomPan
The project shows a desktop window with the ZoomControl (canvas with ScaleTransform, TranslateTransform and a bunch of dependency properties to make it easier to work with the transforms). The ZoomControl contains a red square and the window contains the ZoomControl and a debug list of properties so I can see live how they change based on left click drag and mouse wheel zoom.
Expected vs actual behaviour
Expected behaviour: object/red square edge cannot get out of the current view.
Actual behaviour: object/red square edge gets out of the current view (still has limits, but aren't correctly set).
Code explanations
All the action happens in this file and the important bits are:
the panning limits: MinTranslateX, MaxTranslateX; MinTranslateY, MaxTranslateY
the current panning: TranslateX, TranslateY
the current zoom: Zoom
the amount zoom changed: deltaZoom (local variable)
the Zoom_PropertyChanged method
the LimitZoomingAndPanning method
What I tried
In the LimitZoomingAndPanning method I set the translation/panning limits which are working for Zoom == 1 (deltaZoom == 1), but are giving incorrect limits for any other Zoom values:
MinTranslateX = box.BottomLeft.X * deltaZoom;
MinTranslateY = box.BottomLeft.Y * deltaZoom;
MaxTranslateX = ActualWidth - box.Size.Width * deltaZoom;
MaxTranslateY = ActualHeight - box.Size.Height * deltaZoom;
The box variable is actually the bounding box of the object inside the canvas. ActualWidth and ActualHeight are the size of the canvas on which the object is rendered.
Logically, all the translation/panning limits should depend on deltaZoom.
Maybe I'm missing something?

I originally tried doing the same thing with the ZoomAndPanControl but wasn't able to achieve what I wanted so I ended up writing my own control to provide a constrained zoom and pan control.
I have packaged this control up on nuget but it can be found on my gist and on my github with a demo project loading an image into the viewport control.
PM > Install-Package Han.Wpf.ViewportControl
Usage:
<controls:Viewport MinZoom="1" MaxZoom="50" ZoomSpeed="1.1">
<Grid width="1200" height="1200">
<Button />
</Grid>
</controls:Viewport
and add the theme to the app.xaml:
<Application.Resources>
<ResourceDictionary Source="pack://application:,,,/Han.Wpf.ViewportControl;component/Themes/Generic.xaml" />
</Application.Resources>
I know your not using the MatrixTransform but to constrain the control to the bounds of the parent is calculated like below:
private void OnMouseWheel(object sender, MouseWheelEventArgs e)
{
if (IsEnabled)
{
var scale = e.Delta > 0 ? ZoomSpeed : 1 / ZoomSpeed;
var position = e.GetPosition(_content);
var x = Constrain(scale, MinZoom / _matrix.M11, MaxZoom / _matrix.M11);
var y = Constrain(scale, MinZoom / _matrix.M22, MaxZoom / _matrix.M22);
_matrix.ScaleAtPrepend(x, y, position.X, position.Y);
ZoomX = _matrix.M11;
ZoomY = _matrix.M22;
Invalidate();
}
}
private void OnMouseMove(object sender, MouseEventArgs e)
{
if (IsEnabled && _capture)
{
var position = e.GetPosition(this);
var point = new Point
{
X = position.X - _origin.X,
Y = position.Y - _origin.Y
};
var delta = point;
_origin = position;
_matrix.Translate(delta.X, delta.Y);
Invalidate();
}
}
In the Invalidate call Constrain()
private double Constrain(double value, double min, double max)
{
if (min > max)
{
min = max;
}
if (value <= min)
{
return min;
}
if (value >= max)
{
return max;
}
return value;
}
private void Constrain()
{
var x = Constrain(_matrix.OffsetX, _content.ActualWidth - _content.ActualWidth * _matrix.M11, 0);
var y = Constrain(_matrix.OffsetY, _content.ActualHeight - _content.ActualHeight * _matrix.M22, 0);
_matrix = new Matrix(_matrix.M11, 0d, 0d, _matrix.M22, x, y);
}

Related

Generalizing the ScaleTransform (WPF) when image is not aligned to x-y axes

My linear algebra is weak. WPF is a great system for Rendering different transformations upon an image. However, the standard ScaleTransform will only scale the image's along the x-y axes. When the edges have first been rotated, the result of applying the ScaleTransform will result in a skewed transformation (as shown below) since the edges are no longer aligned.
So, if I have an image that has undergone several different transforms with the result being shown by the WPF rendering system, how do I calculate the correct matrix transform to take the (final rotated image) and scale it along the axes of the rendered image?
Any help or suggestions will be most appreciated.
TIA
(For the complete code, please see my previous question.)
Edit #1: To see the above effect:
Drop image onto Inkcavas. -- no skewing seen.
Rotate image counterclockwise (to about 45deg) -- no skewing seen.
Make the image larger (about twice its prescaled size -- no skewing seen.
Rotate the image clockwise (about back to where it started) -- skewing is
immediately seen during and after the rotation.
If step 3 is skipped, simple rotation -- no matter how many times done -- will not cause the skewing effect. Actually, this makes sense. The ScaleTransform preserves the distance from center from the edges of the image. If the image is at an angle, the x-y distance from the edges of the transform are no longer constant through the width and length of the rendered image. So the edges get appropriately scaled, but the angles are changed.
Here is the most relevant code:
private ImageResizing(Image image)
{
if (image == null)
throw new ArgumentNullException("image");
_image = image;
TransformGroup tg = new TransformGroup();
image.RenderTransformOrigin = new Point(0.5, 0.5); // All transforms will be based on the center of the rendered element.
tg.Children.Add(image.RenderTransform); // Keeps whatever transforms have already been applied.
image.RenderTransform = tg;
_adorner = new MyImageAdorner(image); // Create the adorner.
InstallAdorner(); // Get the Adorner Layer and add the Adorner.
}
Note: The image.RenderTransformOrigin = new Point(0.5, 0.5) is set to the center
of the rendered image. All transforms will be based on the center of the image at the time it is seem by the transform.
public MyImageAdorner(UIElement adornedElement)
: base(adornedElement)
{
visualChildren = new VisualCollection(this);
// Initialize the Movement and Rotation thumbs.
BuildAdornerRotate(ref moveHandle, Cursors.SizeAll);
BuildAdornerRotate(ref rotateHandle, Cursors.Hand);
// Add handlers for move and rotate.
moveHandle.DragDelta += new DragDeltaEventHandler(moveHandle_DragDelta);
moveHandle.DragCompleted += new DragCompletedEventHandler(moveHandle_DragCompleted);
rotateHandle.DragDelta += new DragDeltaEventHandler(rotateHandle_DragDelta);
rotateHandle.DragCompleted += new DragCompletedEventHandler(rotateHandle_DragCompleted);
// Initialize the Resizing (i.e., corner) thumbs with specialized cursors.
BuildAdornerCorner(ref topLeft, Cursors.SizeNWSE);
// Add handlers for resizing.
topLeft.DragDelta += new DragDeltaEventHandler(TopLeft_DragDelta);
topLeft.DragCompleted += TopLeft_DragCompleted;
// Put the outline border arround the image. The outline will be moved by the DragDelta's
BorderTheImage();
}
#region [Rotate]
/// <summary>
/// Rotate the Adorner Outline about its center point. The Outline rotation will be applied to the image
/// in the DragCompleted event.
/// </summary>
/// <param name="sender"></param>
/// <param name="e"></param>
void rotateHandle_DragDelta(object sender, DragDeltaEventArgs e)
{
// Get the position of the mouse relative to the Thumb. (All cooridnates in Render Space)
Point pos = Mouse.GetPosition(this);
// Render origin is set at center of the adorned element. (all coordinates are in rendering space).
double CenterX = AdornedElement.RenderSize.Width / 2;
double CenterY = AdornedElement.RenderSize.Height / 2;
double deltaX = pos.X - CenterX;
double deltaY = pos.Y - CenterY;
double angle;
if (deltaY.Equals(0))
{
if (!deltaX.Equals(0))
angle = 90;
else
return;
}
else
{
double tan = deltaX / deltaY;
angle = Math.Atan(tan); angle = angle * 180 / Math.PI;
}
// If the mouse crosses the vertical center,
// find the complementary angle.
if (deltaY > 0)
angle = 180 - Math.Abs(angle);
// Rotate left if the mouse moves left and right
// if the mouse moves right.
if (deltaX < 0)
angle = -Math.Abs(angle);
else
angle = Math.Abs(angle);
if (double.IsNaN(angle))
return;
// Apply the rotation to the outline. All Transforms are set to Render Center.
rotation.Angle = angle;
rotation.CenterX = CenterX;
rotation.CenterY = CenterY;
outline.RenderTransform = rotation;
}
/// Rotates image to the same angle as outline arround the render origin.
void rotateHandle_DragCompleted(object sender, DragCompletedEventArgs e)
{
// Get Rotation Angle from outline. All element rendering is set to rendering center.
RotateTransform _rt = outline.RenderTransform as RotateTransform;
// Add RotateTransform to the adorned element.
TransformGroup gT = AdornedElement.RenderTransform as TransformGroup;
RotateTransform rT = new RotateTransform(_rt.Angle);
gT.Children.Insert(0, rT);
AdornedElement.RenderTransform = gT;
outline.RenderTransform = Transform.Identity; // clear transform from outline.
}
#endregion //Rotate
#region [TopLeft Corner
// Top Left Corner is being dragged. Anchor is Bottom Right.
void TopLeft_DragDelta(object sender, DragDeltaEventArgs e)
{
ScaleTransform sT = new ScaleTransform(1 - e.HorizontalChange / outline.ActualWidth, 1 - e.VerticalChange / outline.ActualHeight,
outline.ActualWidth, outline.ActualHeight);
outline.RenderTransform = sT; // This will immediately show the new outline without changing the Image.
}
/// The resizing outline for the TopLeft is based on the bottom right-corner. The resizing transform for the
/// element, however, is based on the render origin being in the center. Therefore, the Scale transform
/// received from the outling must be recalculated to have the same effect--only from the rendering center.
///
/// TopLeft_DragCompleted resize the element rendering.
private void TopLeft_DragCompleted(object sender, DragCompletedEventArgs e)
{
// Get new scaling from the Outline.
ScaleTransform _sT = outline.RenderTransform as ScaleTransform;
scale.ScaleX *= _sT.ScaleX; scale.ScaleY *= _sT.ScaleY;
Point Center = new Point(AdornedElement.RenderSize.Width/2, AdornedElement.RenderSize.Height/2);
TransformGroup gT = AdornedElement.RenderTransform as TransformGroup;
ScaleTransform sT = new ScaleTransform( _sT.ScaleX, _sT.ScaleY, Center.X, Center.Y);
gT.Children.Insert(0, sT);
AdornedElement.RenderTransform = gT;
outline.RenderTransform = Transform.Identity; // Clear outline transforms. (Same as null).
}
#endregion
Note: I am adding each new transform to the first of the children list. This makes calculations on the image easier.
I could not find with Google or in text all the elements needed to answer this question completely. So, for all other newbies like my self, I will post this (very long) answer. (Editors and Gurus please feel free to correct).
A word toward setup. I have an inkcanvas onto which an image is dropped and added as a child of the inkcanvas. At the time of the drop, an adorner containing a Thumb on each corner for resizing, a Top-Middle thumb for rotating, and a middle thumb for translation is added for final positioning of the image. Along with a "outline" designed as a path element, the Thumbs and outline complete the Adorner and create a kind of wire frame around the adorned element.
There are multiple key points:
WPF first uses a layout pass to position elements within their parent container, followed by a rendering pass to arrange the element. Transforms can be applied to either or both the layout and rendering passes. However, it needs to be noted that the layout pass uses an x-y coordinate system with the origin on the top left of the parent, where as the rendering system inherently references the top left of the child element. If the layout position of the dropped element is not specifically defined, it will by default be added to the "origin" of the parent container.
The RenderTransform is by default a MatrixTransform but can be replaced by a TransformGroup. Using either or both of these allows for Matrices (in the MatrixTransform) or Transforms (in the TransformGroup) to be applied in any order. My preference was to use the MatrixTransforms to better see the relationship between scaling, rotation, and translation.
The rendering of the adorner follows the element it adorns. That is, the element's rendering will also be applied to the Adorner. This behavior can be overriden by use of
public override GeneralTransform GetDesiredTransform(GeneralTransform transform)
As noted in the initial question, I had avoided using SetTop() and SetLeft() as they messed up my other matrices. In hindsight, the reason my matrices failed was because SetTop() and SetLeft() apparently work during the layout phase--so all my coordinates for rendering were off. (I was using a TransalateTransform to position the image upon drag-drop.) However, using SetTop() and SetLeft() apparently act during the Layout Phase. Using this greatly simplified the calculations needed for the Rendering Phase since all coordinates could refer to the image without regard to the position on the canvas.
private void IC_Drop(object sender, DragEventArgs e)
{
InkCanvas ic = sender as InkCanvas;
// Setting InkCanvasEditingMode.None is necessary to capture DrawingLayer_MouseDown.
ic.EditingMode = InkCanvasEditingMode.None;
ImageInfo image_Info = e.Data.GetData(typeof(ImageInfo)) as ImageInfo;
if (image_Info != null)
{
// Display enlarged image on ImageLayer
// This is the expected format for the Uri:
// ImageLayer.Source = new BitmapImage(new Uri("/Images/Female - Front.png", UriKind.Relative));
// Source = new BitmapImage(image_Info.Uri);
Image image = new Image();
image.Width = image_Info.Width * 4;
// Stretch.Uniform keeps the Aspect Ratio but totally screws up resizing the image.
// Stretch.Fill allows for resizing the Image without keeping the Aspect Ratio.
image.Stretch = Stretch.Fill;
image.Source = new BitmapImage(image_Info.Uri);
// Position the drop. Note that SetLeft and SetTop are active during the Layout phase of the image drop and will
// be applied before the Image hits its Rendering stage.
Point position = e.GetPosition(ic);
InkCanvas.SetLeft(image, position.X);
InkCanvas.SetTop(image, position.Y);
ic.Children.Add(image);
ImageResizing imgResize = ImageResizing.Create(image);
}
}
Since I want to be able to resize the image from any direction, the image is set with Stretch.Fill. When Stretch.Uniform was used, the image appeared to first be resized then jump back to its initial size.
Since I am using MatrixTransform, the order of the Matrices is important. So when applying the Matrices, for my use
// Make new render transform. The Matrix order of multiplication is extremely important.
// Scaling should be done first, followed by (skewing), rotation and translation -- in
// that order.
MatrixTransform gT = new MatrixTransform
{
Matrix = sM * rM * tM
};
ele.RenderTransform = gT;
Scaling (sM), is performed before Rotation (rM). Translation is applied last. (C# does matrix multiplication from left to right).
In review of the matrices, it is apparent that the Rotation Matrix also involves skewing elements. (Which makes sense since apparently the RotationTransform is intended to keep the angles at the edges constant). Thus, the Rotation Matrix depends on the size of the image.
In my case, the reason scaling after rotation was causing skewing is because the Scaling transform multiplies the distance between points of the image and the x-y axes. So if the edge of the image is not of constant distance to the x-y axes, scaling will distort (i.e., skew) the image.
Putting this together, results in the following method to resize the image:
Action<Matrix, Vector> DragCompleted = (growthMatrix, v) =>
{
var ele = AdornedElement;
// Get the change vector. Transform (i.e, Rotate) change vector into x-y axes.
// The Horizontal and Vertical changes give the distance between the the current cursor position
// and the Thumb.
Matrix m = new Matrix();
m.Rotate(-AngleDeg);
Vector v1 = v * m;
// Calculate Growth Vector.
var gv = v1 * growthMatrix;
// Apply new scaling along the x-y axes to obtain the rendered size.
// Use the current Image size as the reference to calculate the new scaling factors.
var scaleX = sM.M11; var scaleY = sM.M22;
var W = ele.RenderSize.Width * scaleX; var H = ele.RenderSize.Height * scaleY;
var sx = 1 + gv.X/ W; var sy = 1 + gv.Y / H;
// Change ScalingTransform by applying the new scaling factors to the existing scaling transform.
// Do not add offsets to the scaling transform matrix as they will be included in future scalings.
// With RenderTransformOrigin set to the image center (0.5, 0.5), scalling occurs from the center out.
// Move the new center of the new resized image to its correct position such that the image's thumb stays
// underneath the cursor.
sM.Scale(sx, sy);
tM.Translate(v.X / 2, v.Y / 2);
// New render transform. The order of the transform's is extremely important.
MatrixTransform gT = new MatrixTransform
{
Matrix = sM * rM * tM
};
ele.RenderTransform = gT;
outline.RenderTransform = Transform.Identity; // clear this transform from the outline.
};
Just to be clear, my "Growth Matrix" is defined in such a manner as to result in "Positive" growth as the cursor is moved away from the center of the image. For Example, the TopLeft corner will "grow" the image when moved to the left and up. Hence
growth matrix = new Matrix(-1, 0, 0, -1, 0, 0) for top-left corner.
The last problem is that of correctly calculating the rotation center (i.e., I want to spin, not orbit). This becomes greatly simplified by using
// All transforms will be based on the center of the rendered element.
AdornedElement.RenderTransformOrigin = new Point(0.5, 0.5);
Lastly, since I am scaling from a corner, the center of the image needs to be translated to keep the corner underneath the cursor.
Sorry for the length of this answer, but there is much to cover (and learn :) ). Hope this helps somebody.

How to set the location of WPF window to the bottom right corner of desktop?

I want to show my window on top of the TaskBar's clock when the windows starts.
How can I find the bottom right corner location of my desktop?
I use this code that works well in windows forms app but does not work correctly in WPF:
var desktopWorkingArea = System.Windows.Forms.Screen.PrimaryScreen.WorkingArea;
this.Left = desktopWorkingArea.Right - this.Width;
this.Top = desktopWorkingArea.Bottom - this.Height;
This code works for me in WPF both with Display 100% and 125%
private void Window_Loaded(object sender, RoutedEventArgs e)
{
var desktopWorkingArea = System.Windows.SystemParameters.WorkArea;
this.Left = desktopWorkingArea.Right - this.Width;
this.Top = desktopWorkingArea.Bottom - this.Height;
}
In brief I use
System.Windows.SystemParameters.WorkArea
instead of
System.Windows.Forms.Screen.PrimaryScreen.WorkingArea
To access the desktop rectangle, you could use the Screen class - Screen.PrimaryScreen.WorkingArea property is the rectangle of your desktop.
Your WPF window has Top and Left properties as well as Width and Height, so you could set those properties relative to the desktop location.
You can use the window's SizeChanged event instead of Loaded if you want the window to stay in the corner when its size changes. This is especially handy if the window has Window.SizeToContent set to some value other than SizeToContent.Manual; in this case it will adjust to fit the content while staying in the corner.
public MyWindow()
{
SizeChanged += (o, e) =>
{
var r = SystemParameters.WorkArea;
Left = r.Right - ActualWidth;
Top = r.Bottom - ActualHeight;
};
InitializeComponent();
}
Note also that you should subtract ActualWidth and ActualHeight (instead of Width and Height as shown in some other replies) to handle more possible situations, for example switching between SizeToContent modes at runtime.
My code:
MainWindow.WindowStartupLocation = WindowStartupLocation.Manual;
MainWindow.Loaded += (s, a) =>
{
MainWindow.Height = SystemParameters.WorkArea.Height;
MainWindow.Width = SystemParameters.WorkArea.Width;
MainWindow.SetLeft(SystemParameters.WorkArea.Location.X);
MainWindow.SetTop(SystemParameters.WorkArea.Location.Y);
};
I solved this problem with a new window containing a label named MessageDisplay. The code accompanying the window was as follows:
public partial class StatusWindow : Window
{
static StatusWindow display;
public StatusWindow()
{
InitializeComponent();
}
static public void DisplayMessage( Window parent, string message )
{
if ( display != null )
ClearMessage();
display = new StatusWindow();
display.Top = parent.Top + 100;
display.Left = parent.Left + 10;
display.MessageDisplay.Content = message;
display.Show();
}
static public void ClearMessage()
{
display.Close();
display = null;
}
}
For my application, the setting of top and left puts this window below the menu on the main window (passed to DisplayMessage in the first parameter);
This above solutions did not entirely work for my window - it was too low and the bottom part of the window was beneath the taskbar and below the desktop workspace. I needed to set the position after the window content had been rendered:
private void Window_ContentRendered(object sender, EventArgs e)
{
var desktopWorkingArea = System.Windows.SystemParameters.WorkArea;
this.Left = desktopWorkingArea.Right - this.Width - 5;
this.Top = desktopWorkingArea.Bottom - this.Height - 5;
}
Also, part of the frame was out of view, so I had to adjust by 5. Not sure why this is needed in my situation.
#Klaus78 's answer is correct. But since this is first thing google pops up and if working in environments where screen resolution can change often such that your app runs on virtual desktops or virtual servers and you still need it to update its placement when the screen resolution changes I have found linking to the SystemEvents.DisplaySettingsChanged event to be beneficial. Here is an example using rx and you can put this in your constructor for your view.
Observable
.FromEventPattern<EventHandler, EventArgs>(_ => SystemEvents.DisplaySettingsChanged += _, _ => SystemEvents.DisplaySettingsChanged -= _)
.Select(_ => SystemParameters.WorkArea)
.Do(_ =>
{
Left = _.Right - Width;
Top = _.Bottom - Height;
})
.Subscribe();

How can I make WPF ScrollViewer middle-click-scroll?

Clicking the middle mouse button (aka: mouse wheel) and then moving the mouse down slightly lets users scroll in IE, and most Windows apps. This behavior appears to be missing in WPF controls by default? Is there a setting, a workaround, or something obvious that I'm missing?
I have found how to achieve this using 3 mouse events (MouseDown, MouseUp, MouseMove). Their handlers are attached to the ScrollViewer element in the xaml below:
<Grid>
<ScrollViewer MouseDown="ScrollViewer_MouseDown" MouseUp="ScrollViewer_MouseUp" MouseMove="ScrollViewer_MouseMove">
<StackPanel x:Name="dynamicLongStackPanel">
</StackPanel>
</ScrollViewer>
<Canvas x:Name="topLayer" IsHitTestVisible="False" />
</Grid>
It would be better to write a behaviour instead of events in code-behind, but not everyone has the necessary library, and also I don't know how to connect it with the Canvas.
The event handlers:
private bool isMoving = false; //False - ignore mouse movements and don't scroll
private bool isDeferredMovingStarted = false; //True - Mouse down -> Mouse up without moving -> Move; False - Mouse down -> Move
private Point? startPosition = null;
private double slowdown = 200; //The number 200 is found from experiments, it should be corrected
private void ScrollViewer_MouseDown(object sender, MouseButtonEventArgs e)
{
if (this.isMoving == true) //Moving with a released wheel and pressing a button
this.CancelScrolling();
else if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Pressed)
{
if (this.isMoving == false) //Pressing a wheel the first time
{
this.isMoving = true;
this.startPosition = e.GetPosition(sender as IInputElement);
this.isDeferredMovingStarted = true; //the default value is true until the opposite value is set
this.AddScrollSign(e.GetPosition(this.topLayer).X, e.GetPosition(this.topLayer).Y);
}
}
}
private void ScrollViewer_MouseUp(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Released && this.isDeferredMovingStarted != true)
this.CancelScrolling();
}
private void CancelScrolling()
{
this.isMoving = false;
this.startPosition = null;
this.isDeferredMovingStarted = false;
this.RemoveScrollSign();
}
private void ScrollViewer_MouseMove(object sender, MouseEventArgs e)
{
var sv = sender as ScrollViewer;
if (this.isMoving && sv != null)
{
this.isDeferredMovingStarted = false; //standard scrolling (Mouse down -> Move)
var currentPosition = e.GetPosition(sv);
var offset = currentPosition - startPosition.Value;
offset.Y /= slowdown;
offset.X /= slowdown;
//if(Math.Abs(offset.Y) > 25.0/slowdown) //Some kind of a dead space, uncomment if it is neccessary
sv.ScrollToVerticalOffset(sv.VerticalOffset + offset.Y);
sv.ScrollToHorizontalOffset(sv.HorizontalOffset + offset.X);
}
}
If to remove the method calls AddScrollSign and RemoveScrollSign this example will work. But I have extended it with 2 methods which set scroll icon:
private void AddScrollSign(double x, double y)
{
int size = 50;
var img = new BitmapImage(new Uri(#"d:\middle_button_scroll.png"));
var adorner = new Image() { Source = img, Width = size, Height = size };
//var adorner = new Ellipse { Stroke = Brushes.Red, StrokeThickness = 2.0, Width = 20, Height = 20 };
this.topLayer.Children.Add(adorner);
Canvas.SetLeft(adorner, x - size / 2);
Canvas.SetTop(adorner, y - size / 2);
}
private void RemoveScrollSign()
{
this.topLayer.Children.Clear();
}
Example of icons:
And one last remark: there are some problems with the way Press -> Immediately Release -> Move. It is supposed to cancel scrolling if a user clicks the mouse left button, or any key of keyboard, or the application looses focus. There are many events and I don't have time to handle them all.
But standard way Press -> Move -> Release works without problems.
vorrtex posted a nice solution, please upvote him!
I do have some suggestions for his solution though, that are too lengthy to fit them all in comments, that's why I post a separate answer and direct it to him!
You mention problems with Press->Release->Move. You should use MouseCapturing to get the MouseEvents even when the Mouse is not over the ScrollViewer anymore. I have not tested it, but I guess your solution also fails in Press->Move->Move outside of ScrollViewer->Release, Mousecapturing will take care of that too.
Also you mention using a Behavior. I'd rather suggest an attached behavior that doesn't need extra dependencies.
You should definately not use an extra Canvas but do this in an Adorner.
The ScrollViewer itsself hosts a ScrollContentPresenter that defines an AdornerLayer. You should insert the Adorner there. This removes the need for any further dependency and also keeps the attached behavior as simple as IsMiddleScrollable="true".

ScrollViewer with Canvas, auto-resize canvas to content including negative space

I have a canvas in a scrollviewer.
To be able to use the scrollviewer, I've overridden the Canvas's MeasureOverride method to return the size of all children.
This works fine, except that the canvas can only contain items in positive space for the scrollviewer to work correctly.
I'd like the user to be able to drag elements around without the restriction of them having to be on the positive side of the canvas's origin.
So basicly I want to be able to position the elements anywhere, like at (-200, 10) or (500, -20) , and be able to scroll from the most left element (-200) to the most right(500), and from top to bottom.
To complicate matters the canvas can be scaled using the LayoutTransform, and I'd like to include the view-area in the scrollbars, so not only the min/max bounds of the children are taken into account by the scrollbars, but also the min/max of the current view-area.
Does anybody know how to make this work ?
Today I worked on this problem only :)
Happy to share the code which works:
public void RepositionAllObjects(Canvas canvas)
{
adjustNodesHorizontally(canvas);
adjustNodesVertically(canvas);
}
private void adjustNodesVertically(Canvas canvas)
{
double minLeft = Canvas.GetLeft(canvas.Children[0]);
foreach (UIElement child in canvas.Children)
{
double left = Canvas.GetLeft(child);
if (left < minLeft)
minLeft = left;
}
if (minLeft < 0)
{
minLeft = -minLeft;
foreach (UIElement child in canvas.Children)
Canvas.SetLeft(child, Canvas.GetLeft(child) + minLeft);
}
}
private void adjustNodesHorizontally(Canvas canvas)
{
double minTop = Canvas.GetTop(canvas.Children[0]);
foreach (UIElement child in canvas.Children)
{
double top = Canvas.GetTop(child);
if (top < minTop)
minTop = top;
}
if (minTop < 0)
{
minTop = -minTop;
foreach (UIElement child in canvas.Children)
Canvas.SetTop(child, Canvas.GetTop(child) + minTop);
}
}
Now call RepositionAllObjects method to realign the objects as and when required.
Of course, you need to have your own canvas derived from Canvas. And this method is required (you can tweak it if you like):
public static Rect GetDimension(UIElement element)
{
Rect box = new Rect();
box.X = Canvas.GetLeft(element);
box.Y = Canvas.GetTop(element);
box.Width = element.DesiredSize.Width;
box.Height = element.DesiredSize.Height;
return box;
}
protected override Size MeasureOverride(Size constraint)
{
Size availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity);
double minX = 900000; //some dummy high number
double minY = 900000; //some dummy high number
double maxX = 0;
double maxY = 0;
foreach (UIElement element in this.Children)
{
element.Measure(availableSize);
Rect box = GetDimension(element);
if (minX > box.X) minX = box.X;
if (minY > box.Y) minY = box.Y;
if (maxX < box.X + box.Width) maxX = box.X + box.Width;
if (maxY < box.Y + box.Height) maxY = box.Y + box.Height;
}
if (minX == 900000) minX = 0;
if (minY == 900000) minY = 0;
return new Size { Width = maxX - minX, Height = maxY - minY };
}
Now, all you need to do is wrap this canvas inside a scrollviewer.
Hope it helps!
I think the ScrollViewer assumes that the origin will be at (0, 0) -- after all, there's no way to tell it otherwise; Canvas is the only panel that knows about specified-and-nonzero origins.
A simple option might be to forget about a ScrollViewer, and implement your own panning functionality -- perhaps with dragging, or perhaps with a big "scroll left" button on the left side of the view, a "scroll right" button on the right, etc. -- and implement it in terms of a transform on the content.
But if you need to stick with the classic scrollbar metaphor, I think you could make your own panel (or override ArrangeOverride too, which amounts to the same thing) and offset all the positions -- make them zero-based. So if you have an element at (20, -20) and another at (0, 5), you would need to offset everything downward by 20 to make it fit in a zero-based space; and when you lay out your children, the first would go at (20, 0) and the second at (0, 25).
But now scrolling is weird. If the user is scrolled all the way to the bottom, and drags something off the bottom edge, the view stays put; same with the right. But if they're scrolled all the way to the top, and drag something off the top edge, suddenly everything jumps downward, because the ScrollViewer's VerticalOffset was zero before and is still zero, but zero means something different because of your layout offset.
You might be able to get around the scrolling weirdness by binding the HorizontalOffset and VerticalOffset. If your ViewModel automatically fixed up those properties (and fired PropertyChanged) whenever a control moves "negative", then you might be able to keep the view scrolled to the same logical content, even though you're now telling WPF's layout engine that everything is somewhere else. I.e., your ViewModel would track the logical scroll position (zero, before you drag the element negative), and would report the zero-based scroll position to the ScrollViewer (so after the drag, you would tell the ScrollViewer that its VerticalOffset is now 20).

Fixed deep zoom

Is there any way to perform a fixed zoom on a multiscaleimage in deep zoom? i.e. click once to zoom about point X,Y to 2x, click again to restore to the original position and zoom level?
I have written the code to zoom in and out but calling zoomaboutlogicalpoint midway through the zoom process results in zooming out too far (I guess due to the 1/2 factor in the mouse up event - can I obtain the zoom level?). Also I'd like the zoomed out image to be central (I guess i change the point to zoom to that midway in the image but this doesnt seem to work, perhaps I need to factor in the ViewPort position?)
e.g.
private void msi_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Point p = e.GetPosition(msi);
Zoom(2, p);
}
private void msi_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
Point p = e.GetPosition(msi);
Zoom(1/2, p);
}
public void Zoom(double zoom, Point pointToZoom)
{
bool zoomingIn = zoom > 1;
bool zoomingOut = zoom < 1;
double minViewportWidth = 0.05;
double maxViewportWidth = 1;
if (msi.ViewportWidth < minViewportWidth && zoomingIn)
{
return;
}
if (msi.ViewportWidth > maxViewportWidth && zoomingOut)
{
return;
}
Point logicalPoint = this.msi.ElementToLogicalPoint(pointToZoom);
this.msi.ZoomAboutLogicalPoint(zoom, logicalPoint.X, logicalPoint.Y);
}
Thanks.
Resetting the transform origin before zooming out seems to have done the trick:
msi.RenderTransformOrigin = new Point(msi.Height / 2, msi.Width / 2);

Resources