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);
Related
I have an image and i am panning it after zooming, on panning horizontally, image gets moved leaving white space at the left. I want to restrict when the image left bounds has reached. Same thing for top, right, bottom panning.
Panned image
Expected output
private void Grid1_MouseMove(object sender, MouseEventArgs e)
{
if (!image1.IsMouseCaptured) return;
Point p = e.MouseDevice.GetPosition(image1);
var rect2 = new Rect(image1.RenderSize);
Bounds = image1.TransformToAncestor(grid1).TransformBounds(rect2);
matrix = zoomMatrixTransform.Matrix;
Vector v = start - e.GetPosition(grid1);
matrix.OffsetX = origin.X - v.X;
matrix.OffsetY = origin.Y - v.Y;
zoomMatrixTransform.Matrix = matrix;
image1.RenderTransformOrigin = new Point(0.5, 0.5);
image1.LayoutTransform = zoomMatrixTransform;
}
I have attached the panned image and i want to restrict the red highlighted area. Also attached the expected output.
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);
}
I have a WPF app with two canvases which overlay each other . . .
<Canvas Name="GeometryCnv" Canvas.Top="0" Canvas.Left="0" Margin="10,21,315,251" />
<Canvas Name="ROIcnv" Background ="Transparent" Canvas.Top="0" Canvas.Left="0" Margin="10,21,315,251" MouseDown="ROIcnvMouseDown" MouseUp="ROIcnvMouseUp" MouseMove="ROIcnvMouseMove"/>
I draw some geometry on the first canvas and I draw a rectangle to denote a Region on Interest (ROI) on the second one, using the Mouse-down event to start the drawing, Mouse-move events while drawing (resizing or positioning) the rectangle, and the Mouse-up event to end the drawing.
Except that it's not handling the events reliably. It gets the initial Mouse-down event to start it. It gets Mouse-move events continuously - regardless of whether the mouse is moving - and it does not get the Mouse-up event at all, nor does it get any subsequent mouse down events, say if I double-click the mouse.
The event-handler code looks like this . . .
private void ROIcnvMouseDown(object sender, MouseButtonEventArgs e)
{
MouseLineBegin = Mouse.GetPosition(ROIcnv);
bMouseDown = true;
}
private void ROIcnvMouseUp(object sender, MouseButtonEventArgs e)
{
MouseLineEnd = Mouse.GetPosition(ROIcnv);
bMouseDown = false;
}
private void ROIcnvMouseMove(object sender, MouseEventArgs e)
{
iMM++; // counting mouse move events
ROIcnv.Children.Clear(); // clear the ROI canvas
if (bMouseDown) // if we're drawing now
{
MouseLineEnd = Mouse.GetPosition(ROIcnv);
// get the upper left and lower right = coords from the beginning and end points . . .
int ulx = 0;
int uly = 0;
int lrx = 0;
int lry = 0;
if (MouseLineEnd.X >= MouseLineBegin.X)
{
ulx = (int) MouseLineBegin.X;
lrx = (int) MouseLineEnd.X;
}
else
{
lrx = (int)MouseLineBegin.X;
ulx = (int)MouseLineEnd.X;
}
if (MouseLineEnd.Y >= MouseLineBegin.Y)
{
uly = (int)MouseLineBegin.Y;
lry = (int)MouseLineEnd.Y;
}
else
{
lry = (int)MouseLineBegin.Y;
uly = (int)MouseLineEnd.Y;
}
int h = Math.Abs(lry-uly);
int w = Math.Abs(lrx-ulx);
var rect = new Path
{
Data = new RectangleGeometry(new Rect(ulx, uly, w, h)),
Stroke = Brushes.Black,
StrokeThickness = 2
};
ROIcnv.Children.Add(rect);
}
}
... I've tried suspending the mouse in mid-air and resting it on towels to eliminate any vibrations that might cause spurious move events with no benefit, any anyway that wouldn't account for not getting subsequent up and down events.
Note: I tried this on another computer with exactly the same results.
You'll have much better responses if you provide a minimal, working example of your problem (specifically both your ROIcnvMouseDown and ROIcnvMouseUp methods are missing as are all of your property declarations). The problem is possibly due to your newly-created Path object interfering with the mouse messages, if so then it can be fixed by setting it's IsHitTestVisible property to false. Need a minimal example to determine this for sure though.
UPDATE: Sorry, my bad, I must have stuffed up the cut-n-paste into my test app. Try capturing the mouse in response to the mouse down event:
private void ROIcnvMouseDown(object sender, MouseButtonEventArgs e)
{
MouseLineBegin = Mouse.GetPosition(ROIcnv);
bMouseDown = true;
Mouse.Capture(sender as IInputElement);
}
And of course you need to release it in response to MouseUp:
private void ROIcnvMouseUp(object sender, MouseButtonEventArgs e)
{
MouseLineEnd = Mouse.GetPosition(ROIcnv);
bMouseDown = false;
Mouse.Capture(sender as IInputElement, CaptureMode.None);
ROIcnv.Children.Clear();
}
The other thing I've done is call ROIcnv.Children.Clear(); in response to MouseUp as I assume you no longer want the selection rectangle to be visible. On my machine this doesn't result in any spurious mouse move events.
Does that answer the question?
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".
I wrote a very simple newbie app with a 6-sided polyhedron (a "box") which rotates 180 degrees when I click a button. and then rotates back again on the next click. Every rotation grabs another 90MB and it doesn't let go until I close the app. The box is defined in the XAML. The Storyboard, DoubleAnimation and PropertyPath, etc, are all created ONCE, in the constructor. The button code looks like this:
private void button_Storyboard1_Click(object sender, RoutedEventArgs e)
{
GC.Collect();
if (_bFront)
{
_myDoubleAnimation.From = 0;
_myDoubleAnimation.To = 180;
_bFront = false;
}
else
{
_myDoubleAnimation.From = 180;
_myDoubleAnimation.To = 0;
_bFront = true;
}
_myDoubleAnimation.Duration = _Duration;
Storyboard.SetTargetName(_myDoubleAnimation, "rotate_me");
Storyboard.SetTargetProperty(_myDoubleAnimation, _PropP);
_sb.Children.Add(_myDoubleAnimation);
_sb.Begin(this.viewport3D1);
}
After a few rotations I'm out of memory! What's going on?
Could be totally wrong here, but aren't you adding _myDoubleAnimation to _sb.Children on each click, instead of just updating it?