Thanks to Kael Rowan, you can easily create a zoomable canvas in WPF. Sample project can be downloaded at the bottom of this blog entry.
Now, I need to modify this sample project to use its Viewbox feature. In MainWindow.xaml set the Viewbox, Stretch and ApplyTransform properties as follows:
<ZoomableCanvas ... Viewbox="0 0 400 400" Stretch="Fill" ApplyTransform="True" />
The problem
With this feature enabled, the zooming using the mouse wheel does not zoom about the mouse position. Instead, it seems to zoom around (0,0). This piece of code (in the mouse wheel handler) works without Viewbox:
// Adjust the offset to make the point under the mouse stay still.
var position = (Vector)e.GetPosition(MyListBox);
MyCanvas.Offset = (Point)((Vector)(MyCanvas.Offset + position) * x - position);
How can I get it working with a Viewbox? This is driving me nuts! Math/WPF gurus out there, please help!
Try this code
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
var position = e.GetPosition(this.MyCanvas);
var pre = this.MyCanvas.TransformToAncestor(this).Transform(position);
var x = Math.Pow(2, e.Delta / 3.0 / Mouse.MouseWheelDeltaForOneLine);
MyCanvas.Scale *= x;
var cur = this.MyCanvas.TransformToAncestor(this).Transform(position);
var offset = (cur - pre);
this.MyCanvas.Offset += offset;
}
Related
I've started using LightningChart in my real time monitoring application. In my app there are many y axis which use segmented layout (one y axis per segment):
mainChart.ViewXY.AxisLayout.YAxesLayout = YAxesLayout.Segmented;
My goal is that when you mouse click a segment, it gets larger compared to other segments (kinda like zoom effect) and the other segments gets smaller. When you click it again it goes back to normal.
I know I can change the size of the segments with:
mainChart.ViewXY.AxisLayout.Segments[segmentNumber].Height = someValue;
That takes care of the zooming effect.
Now the problem is that how can I solve which segment was actually clicked? I figured out that you get mouse position via MouseClick -event (e.MousePos) but that seem to give only the screen coordinates so i'm not sure that it helps.
I'm using the LightningChart version 8.4.2
You are correct that getting mouse position via MouseClick event is the key here. The screen coordinates you get via e.GetPosition (not e.MousePos) can be converted to chart axis values with CoordToValue() -method. Then you just compare the y-coordinate to each y-axis minimum/maximum value to find out what segment was clicked. Here is an example:
_chart.MouseClick += _chart_MouseClick;
private void _chart_MouseClick(object sender, MouseButtonEventArgs e)
{
var mousePos = e.GetPosition(_chart).Y;
double axisPos = 0;
bool isWithinYRange = false;
foreach (AxisY ay in _chart.ViewXY.YAxes)
{
ay.CoordToValue((float)mousePos, out axisPos, true);
if (axisPos >= ay.Minimum && axisPos <= ay.Maximum)
{
// Segment clicked, get the index via ay.SegmentIndex;
isWithinYRange = true;
}
}
if (!isWithinYRange)
{
// Not in any segment
}
}
After finding out the segment index, you can modify its height as you described:
_chart.ViewXY.AxisLayout.Segments[0].Height = 1.5;
Note Height means segment height compared to other segments.
Hope this is helpful.
I'm using HelixToolkit to see and interact with STL files. I need to draw or mark a point clicked by user on the window. I have the coordinates, I know where to draw that point, but I don't know how to draw it, can someone help me? I post some code to explain what I have right now:
private void vierport3d_MouseRightClick(object sender, MouseButtonEventArgs e)
{
Point mousePos = e.GetPosition(viewPort3d);
PointHitTestParameters hitParams = new PointHitTestParameters(mousePos);
VisualTreeHelper.HitTest(viewPort3d, null, ResultCallback, hitParams);
}
public HitTestResultBehavior ResultCallback(HitTestResult result)
{
RayHitTestResult rayResult = result as RayHitTestResult;
if (rayResult != null)
{
RayMeshGeometry3DHitTestResult rayMeshResult = rayResult as RayMeshGeometry3DHitTestResult;
//HERE I HAVE THE LOCATION TO DRAW
MessageBox.Show(rayMeshResult.PointHit.X + " " + rayMeshResult.PointHit.Y + " " + rayMeshResult.PointHit.Z);
if (rayMeshResult != null)
{
// I THINK I HAVE TO DRAW THE POINT HERE
}
}
return HitTestResultBehavior.Continue;
}
PD: I show the stl on a viewport3d.
We had same scenario in our project and used a sphere to visually indicate the point.
<ht:SphereVisual3D Radius="0.75" Fill="Red" Center="{Binding ContactPoint}" />
ContactPoint is a Point3D type.
This might help, but its probably not the most effecient.
Try the following:
This will create a 3D sphere that can be rendered at the given coordinates.
var sphereSize = 0.025;
/* keep these values low, the higher the values the more detailed the sphere which may impact your rendering perfomance.*/
var phi = 12;
var theta = 12;
MeshBuilder meshBuilder = new MeshBuilder();
Pass in your x,y,z to the first parameter. i.e. the click 3D location.
meshBuilder.AddSphere( new Point3D(x,y,z), sphereSize , theta, phi);
GeometryModel3D sphereModel = new GeometryModel3D(meshBuilder.ToMesh(),MaterialHelper.CreateMaterial(Brushes.Green,null,null,1,0));
Rendering the Point in your viewport
You will need a ModelVisual3D component as a child of the HelixViewport. ( This can be implemented in C# or in XAML) its up to you, ill show both ways.
C# version
NB: You need a reference to the helixviewport if its defined in xaml. Set the x:Name:"" to something appropriate. e.g x:Name="helixViewPort"
ModelVisual3D visualizer = new ModelVisual3D();
visualizer.Content = sphereModel;
helixViewPort.Children.Add(visualizer);
XAML version
I'll assume your xaml code has at least a helix view port so you'll have to add a ModelVisual3D child to the helix viewport if there's none.
<h:HelixViewport3D x:Name="HelixPlotViewPort" >
<h:DefaultLights/>
<ModelVisual3D x:Name="Visualizer">
</ModelVisual3D>
</h:HelixViewport3D>
//Then in C# add the following
Visualizer.Content = sphereModel;
That should do it, hope it helps, do inform us if you find a better solution. :)
I've created one helper app to demonstrate my problem.
I've a rectangle which is filled with an image brush, this brush can be transformed within the rectangle using gesture manipulations.
I'm determining the position of top left corner of the image from the top left corner of the rectangle. I'm getting correct values while (only) translating the image but getting wrong values while using pinch gestures. If you zoom in too much and translate the image, then brush moves in opposite direction.
Below is how you can reproduce my problem with the helper app attached below:
Run the app, get the top left corners of both image and rectangle together by just moving(without pinching) the image until you get the position value as (0,0).
Next Pinch and move the image and get back the top left corners together, now you can see that value is not (0,0).
Download here
Here is my Manipulation Delta Event:
public virtual void Brush_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
if (e.PinchManipulation != null)
{
// Rotate
_currentAngle = previousAngle + AngleOf(e.PinchManipulation.Original) - AngleOf(e.PinchManipulation.Current);
// Scale
_currentScale *= e.PinchManipulation.DeltaScale;
// Translate according to pinch center
double deltaX = (e.PinchManipulation.Current.SecondaryContact.X + e.PinchManipulation.Current.PrimaryContact.X) / 2 -
(e.PinchManipulation.Original.SecondaryContact.X + e.PinchManipulation.Original.PrimaryContact.X) / 2;
double deltaY = (e.PinchManipulation.Current.SecondaryContact.Y + e.PinchManipulation.Current.PrimaryContact.Y) / 2 -
(e.PinchManipulation.Original.SecondaryContact.Y + e.PinchManipulation.Original.PrimaryContact.Y) / 2;
_currentPos.X = previousPos.X + deltaX;
_currentPos.Y = previousPos.Y + deltaY;
}
else
{
// Translate
previousAngle = _currentAngle;
_currentPos.X += e.DeltaManipulation.Translation.X;
_currentPos.Y += e.DeltaManipulation.Translation.Y;
previousPos.X = _currentPos.X;
previousPos.Y = _currentPos.Y;
}
e.Handled = true;
ProcesstTransform();
}
void ProcesstTransform()
{
CompositeTransform gestureTransform = new CompositeTransform();
gestureTransform.CenterX = _currentPos.X;
gestureTransform.CenterY = _currentPos.Y;
gestureTransform.TranslateX = _currentPos.X - outputSize.Width / 2.0;
gestureTransform.TranslateY = _currentPos.Y - outputSize.Height / 2.0;
gestureTransform.Rotation = _currentAngle;
gestureTransform.ScaleX = gestureTransform.ScaleY = _currentScale;
brush.Transform = gestureTransform;
}
First, find the location of the initial upper left corner relative to the center of the transform. This is pretty much straight subtraction. These can be pre-calculated since the before-transformation frame won't change. You do NOT want to pre-scale _brushSize by multiplying in _scale. That will end up scaling the brush twice.
Point origCentre = new Point(ManipulationArea.ActualWidth / 2, ManipulationArea.ActualHeight / 2);
Point origCorner = new Point(origCentre.X - _brushSize.Width / 2, origCentre.Y - _brushSize.Height /2);
Then apply the gestureTransform to the corner point:
Point transCorner = gestureTransform.Transform(origCorner);
XValue.Text = transCorner.X.ToString();
YValue.Text = transCorner.Y.ToString();
This will get things pretty close to accurate, barring some rounding errors and some weirdness from the way the translation is tracked both by changing the position and then by applying the transform. Typically you would only do the latter. I'll leave tracking that down as an exercise for the reader :)
Rob Caplan from Microsoft helped me to solve this issue.
I'm writing a WPF application that displays a XAML object (it's basically a map drawn in XAML). As part of its features, it should zoom in/out and pan. The panning works fine, and the zoom zooms, but I can't quite understand how to zoom to a specific point, like my mouse cursor, for example.
This is my current code:
internal void PerformZoom(float ZoomFactor, Point ZoomCenterPoint)
{
m_originalTransform = m_canvas.RenderTransform;
float newZoomFactor = m_oldZoomFactor + ZoomFactor;
float scaleToApply = (newZoomFactor / m_oldZoomFactor);
m_totalZoom = newZoomFactor;
var st = new ScaleTransform(scaleToApply, scaleToApply);
TransformGroup tg = new TransformGroup();
tg.Children.Add(m_originalTransform);
tg.Children.Add(st);
m_canvas.RenderTransform = tg;
m_oldZoomFactor = newZoomFactor;
}
[edit] Found the solution - Just edited the CenterX / CenterY properties of the transformation and it worked like a charm.
Thanks for all your help!
[edit2] Here's a viable solution (considering the mouse position):
public partial class MainWindow
{
private float currentZoom = 1f;
private const float StepSize = .2f;
public MainWindow()
{
InitializeComponent();
}
private void MainGrid_OnMouseWheel(object sender, MouseWheelEventArgs e)
{
var pos = 1;
if (e.Delta < 0)
{
pos = -1;
}
var mousePosition = e.MouseDevice.GetPosition(MainGrid);
currentZoom += StepSize * pos;
var transform = new ScaleTransform(currentZoom, currentZoom, mousePosition.X, mousePosition.Y);
MainGrid.RenderTransform = transform;
}
}
You will have to compose your ScaleTransform with a TranslateTransform which translates your component while zooming.
The offset given by the TranslateTransform depends of the behavior you wanna have (i.e. center on mouse, center on screens center...)
I wrote in the past a behavior which you can attach to a component : It makes it zoomable (centered on mouse, reacting to mousewheel)
It's pretty dirty and not sure to be efficient (i no longer use it)... and comments are in french :-/
see the source
[edit] In fact, I remember it was to scroll and scale a Panels background. But it shouldnt be so hard to modify for applying it to any object as the transformations are the same for images and elements.
Everyone,
I have a WPF app that has a canvas that I have wrapped in a scroll Viewer. I have a slider in the status bar that allows the user to zoom in and out (just like Win 7's mspaint).
Here is some of the XAML:
<ScrollViewer Name="Map"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto">
<Canvas x:Name="WallsCanvas" Height="800" Width="1000" ClipToBounds="True">
<Canvas.LayoutTransform>
<ScaleTransform x:Name="WallsCanvasScale"
ScaleX="1" ScaleY="1" />
</Canvas.LayoutTransform>
</Canvas>
</ScrollViewer>
When I zoom in, and the scrollbars are visible, the scrollbars, no matter where they are set, jump to the middle.
It is exactly as if the value of the scrollbars stay the same but the max value increases.
What can I do to get them to ... say, if they were in the lower right corner, to stay in the lower right corner after a zoom in or out?
BTW, here is my Zoom in and out code:
private void SliderValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
var scales = new []{.125, .25, .5, 1, 2, 4, 8};
var scale = scales[(int)((Slider) sender).Value];
ScaleChanged(scale, WallsCanvasScale);
}
private static void ScaleChanged(double scale, ScaleTransform st)
{
st.ScaleX = scale;
st.ScaleY = scale;
}
So, no rocket science in my code but ...
Update idea: If I had access to the value and the max value of the scrollbars, could I get the percentage between the two and then after the zooming (scaling) I could re-apply the value of the scrollbar as a percentage of the max value????? But where is the value and the max value available?
Any help would be appreciated. I cannot think that I am the only one that has this problem since MSPaint (the Windows 7 version) works correctly and I assume it is a XAML app.
Here is a link (http://www.leesaunders.net/examples/zoomexample/zoomexample.zip) to a minimum working example project (VS 2010). When you run it, just move the scroll bars then zoom in a level, you will see the issue right away.
You just need to offset the shift which results from the scale because it scales from (0,0). It's a bit complicated but here's a sketch of what the method in your sample could look like:
private void SliderValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
var slider = (Slider)sender;
if (!slider.IsLoaded)
{
slider.Loaded += (s, le) => SliderValueChanged(sender, e);
return;
}
var scales = new[] { .125, .25, .5, 1, 2, 4, 8 };
var scale = scales[(int)((Slider)sender).Value];
// The "+20" are there to account for the scrollbars... i think. Not perfectly accurate.
var relativeMiddle = new Point((Map.ActualWidth + 20) / 2, (Map.ActualHeight + 20) / 2);
var oldLocation = CanvasScale.Transform(TemplateCanvas.PointFromScreen(relativeMiddle));
ScaleChanged(scale, CanvasScale);
var newLocation = CanvasScale.Transform(TemplateCanvas.PointFromScreen(relativeMiddle));
var shift = newLocation - oldLocation;
Map.ScrollToVerticalOffset(Map.VerticalOffset + shift.Y);
Map.ScrollToHorizontalOffset(Map.HorizontalOffset + shift.X);
lblScale.Content = scale.ToString("P1").Replace(".0", string.Empty);
}
Should be quite self-explanatory; the location of the center is measured before and after the scaling to calculate the shift of that point, which then is added to the current scroll-position.