WPF DataGrid Zoom to mouse position - wpf

I have a DataGrid that LayoutTransform is Binded to a Slider like that:
<DataGrid.LayoutTransform>
<ScaleTransform
ScaleX="{Binding ElementName=MySlider, Path=Value}"
ScaleY="{Binding ElementName=MySlider, Path=Value}" />
</DataGrid.LayoutTransform>
</DataGrid>
<Slider x:Name="MySlider"
Minimum="0.3"
Maximum="2.0"
SmallChange="0.1"
LargeChange="0.1"
Value="1.0"
IsSnapToTickEnabled="True"
TickFrequency="0.1"
TickPlacement="TopLeft"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
Width="200"
Margin="0,0,61,0" />
<TextBlock Name="Lstate"
Text="{Binding ElementName=MySlider, Path=Value, StringFormat={}{0:P0}}"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
Width="50" Height="20"
Margin="0,0,0,1" />
Now, in the Code I have the PreviewMouseWheel event with the following Code:
bool handle = (Keyboard.Modifiers & ModifierKeys.Control) > 0;
if (!handle)
return;
double value;
if (e.Delta > 0)
value = 0.1;
else
value = -0.1;
MySlider.Value += value;
And my question is: How to scroll to the actual Mouse Position like AutoCad or some other programs?
Thanks
Sorry for my bad english...

I have a very very good solution now:
VirtualizingStackPanel.IsVirtualizing="True"
VirtualizingStackPanel.VirtualizationMode="Standard"
EnableColumnVirtualization="False"
EnableRowVirtualization="True"
ScrollViewer.CanContentScroll="True"
private void Data_OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
// Scroll to Zoom
if (Keyboard.IsKeyDown(Key.LeftCtrl) || Keyboard.IsKeyDown(Key.RightCtrl))
{
// Prevent scroll
e.Handled = true;
var scrollview = FindVisualChild<ScrollViewer>(Data);
if (scrollview != null)
{
// The "+20" are there to account for the scrollbars... i think. Not perfectly accurate.
var relativeMiddle = new Point((Data.ActualWidth + 20) / 2 + (Mouse.GetPosition(Data).X - Data.ActualWidth / 2), (Data.ActualHeight + 20) / 2 + (Mouse.GetPosition(Data).Y - Data.ActualHeight / 2));
var oldLocation = Data.LayoutTransform.Transform(Data.PointFromScreen(relativeMiddle));
// Zoom
MySlider.Value += (e.Delta > 0) ? MySlider.TickFrequency : -MySlider.TickFrequency;
// Scroll
var newLocation = Data.LayoutTransform.Transform(Data.PointFromScreen(relativeMiddle));
// Calculate offset
var shift = newLocation - oldLocation;
if (scrollview.CanContentScroll)
{
// Scroll to the offset (Item)
scrollview.ScrollToVerticalOffset(scrollview.VerticalOffset + shift.Y / scrollview.ViewportHeight);
}
else
{
// Device independent Pixels
scrollview.ScrollToVerticalOffset(scrollview.VerticalOffset + shift.Y);
}
// Device independent Pixels
scrollview.ScrollToHorizontalOffset(scrollview.HorizontalOffset + shift.X);
}
}
}
It zooms to the Mouse Position on the Datagrid with and without virtualization.

Related

How can I refresh the position of a Slider with Scroll event?

I'm now tryin to scaling a image with mouse-wheel cmd.
and have some problems with refresh the position of slider position.
MainWindow.xaml
<Grid Grid.Row="1">
<Slider x:Name="slider_value" Value="{Binding Scale, Mode=TwoWay}" Maximum="3" Minimum="1"/>
</Grid>
<Other Grid>
<target obj want to scale>
<Grid.LayoutTransform>
<ScaleTransform ScaleX="{Binding ElementName=slider_value, Path=Value}"
ScaleY="{Binding ElementName=slider_value, Path=Value}"/>
</Grid.LayoutTransform>
</Other Grid>
MainViewModel.cs
private void ExecuteZoom(MouseWheelEventArgs e) {
if(e.Delta > 0) {
if(Scale>=3)
return;
Scale += 0.1;
}
if(e.Delta < 0) {
if(Scale<=1)
return;
Scale -= 0.1;
}
//((MainWindow)Application.Current.MainWindow).UpdateLayout();
//i thought this can be solved by above code but don't work
Debug.Print($"{Scale}");
}
I checked
the value of var Scale varies successfully with mouseWheel
the value of var Scale varies successfully with moving slider
the scale of target varies with slider moved by "Manually"
the value of var Scale successfully is changed
but the position of slider isn't affected, and also ScaleTransForm doesnt work.
thanks!
enter image description here
enter image description here

Scaling InkCanvas strokes to fit specific dimensions to crop out unused space

I am attempting to scale an InkCanvas's contents (Strokes) to fit a fixed page size for printing. I want to essentially crop out all of the surrounding whitespace from the InkCanvas, and scale up the Strokes to fit the page while maintaining aspect ratio.
In the handler below the markup, you can see that I am changing the dimensions of the grid that starts at 800x300, and I'm making it 425x550, half of the size of a printable page.
markup:
<Grid>
<Button Height="100" Width="100" HorizontalAlignment="Left" PreviewMouseLeftButtonDown="Button_PreviewMouseLeftButtonDown_1" />
<Grid Width="1200" Height="1400" Background="Aquamarine" HorizontalAlignment="Right" VerticalAlignment="Top">
<Grid Background="Red" x:Name="grid" Width="800" Height="300">
<Viewbox x:Name="vb" Width="800" Height="300" Stretch="Fill" StretchDirection="Both">
<InkCanvas Width="500" Height="500" Background="Transparent" IsEnabled="True"/>
</Viewbox>
</Grid >
</Grid>
</Grid>
codebehind file:
bool b = true;
private void Button_PreviewMouseLeftButtonDown_1(object sender, MouseButtonEventArgs e)
{
if (b)
{
//I toyed with using Uniform, UniformToFill, and Fill
vb.Stretch = Stretch.Fill;
grid.Width = 425;
grid.Height = 550;
//scale viewbox down until it fits horizontally
var scaleX = grid.Width / vb.Width;
vb.Width *= scaleX;
vb.Height *= scaleX;
//if constraining it to the width made it larger than it needed to be vertically, scale it down
if (vb.Height > grid.Height)
{
var scaleY = grid.Height / vb.Height;
vb.Width *= scaleY;
vb.Height *= scaleY;
}
b = false;
}
else
{
//reset it back to what it was
vb.Stretch = Stretch.Fill;
grid.Width = 800;
grid.Height = 300;
vb.Width = grid.Width;
vb.Height = grid.Height;
b = true;
}

Area Calculation of CombinedGeometry parts

I need to calculate some areas based on Geometry intersection.
In my example I have the following Geometries:
Left RectangleGeometry
Right RectangleGeometry
EllipseGeometry
The Ellipse is in the middle of the Rectangles and I want two get the following data:
Area of the intersection between the Ellipse and left rectangle
Area of the intersection between the Ellipse and the right rectangle
Total Area of the Ellipse.
The issue is that the total area of the ellipse, EllipseGeometry.GetArea(), and the "LeftEllipseGeometry".GetArea() + "RightEllipseGeometry".GetArea() are different.
The sum of intersections areas have to be the same as the ellipe Area.
I made an example where you can test and see the problem.
MainWindow.xaml.cs
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
//LEFT
rectLeft = new RectangleGeometry();
rectLeft.Rect = new Rect(new Point(75, 100), new Point(700, 600));
Path pathRectLeft = new Path();
pathRectLeft.Stroke = Brushes.Red;
pathRectLeft.Data = rectLeft;
grdMain.Children.Add(pathRectLeft);
//RIGHT
rectRight = new RectangleGeometry();
rectRight.Rect = new Rect(new Point(700, 100), new Point(1300, 600));
Path pathRectRight = new Path();
pathRectRight.Stroke = Brushes.Green;
pathRectRight.Data = rectRight;
grdMain.Children.Add(pathRectRight);
//ELLIPSE
ellipseGeo = new EllipseGeometry();
ellipseGeo.RadiusX = 200;
ellipseGeo.RadiusY = 200;
ellipseGeo.Center = new Point(700, 350);
Path ellipsePath = new Path();
ellipsePath.Stroke = Brushes.Blue;
ellipsePath.Data = ellipseGeo;
grdMain.Children.Add(ellipsePath);
lblEllipseArea.Content = String.Concat("Area Ellipse = ", ellipseGeo.GetArea());
}
private void Button_Click(object sender, RoutedEventArgs e)
{
CombinedGeometry cgLeft = new CombinedGeometry();
cgLeft.Geometry1 = rectLeft;
cgLeft.Geometry2 = ellipseGeo;
cgLeft.GeometryCombineMode = GeometryCombineMode.Intersect;
Path cgLeftPath = new Path();
cgLeftPath.Stroke = Brushes.Yellow;
cgLeftPath.Data = cgLeft;
grdMain.Children.Add(cgLeftPath);
lblEllipseAreaLeft.Content = String.Concat("Area Left Ellipse = ", cgLeft.GetArea());
CombinedGeometry cgRight = new CombinedGeometry();
cgRight.Geometry1 = rectRight;
cgRight.Geometry2 = ellipseGeo;
cgRight.GeometryCombineMode = GeometryCombineMode.Intersect;
Path cgRightPath = new Path();
cgRightPath.Stroke = Brushes.White;
cgRightPath.Data = cgRight;
grdMain.Children.Add(cgRightPath);
lblEllipseAreaRight.Content = String.Concat("Area Right Ellipse = ", cgRight.GetArea());
lblEllipseTotal.Content = String.Concat("Area Ellipse Total = ", cgLeft.GetArea() + cgRight.GetArea());
}
MainWindow.xaml
<Grid>
<StackPanel Orientation="Vertical" Background="Black">
<Grid Background="Black" Height="700" Name="grdMain">
</Grid>
<Grid Background="Black" Height="150">
<StackPanel Orientation="Vertical">
<Button Height="30" Width="70" Click="Button_Click">Click Me!!!</Button>
<StackPanel Orientation="Horizontal">
<Label Foreground="White" Name="lblEllipseArea"></Label>
<Label Foreground="White" Name="lblEllipseArea2" Margin="20 0 0 0"></Label>
<Label Foreground="White" Name="lblEllipseAreaRight" Margin="20 0 0 0"></Label>
<Label Foreground="White" Name="lblEllipseAreaRight2" Margin="20 0 0 0"></Label>
<Label Foreground="White" Name="lblEllipseAreaLeft" Margin="20 0 0 0"></Label>
<Label Foreground="White" Name="lblEllipseAreaLeft2" Margin="20 0 0 0"></Label>
</StackPanel>
<StackPanel Orientation="Horizontal">
<Label Foreground="White" Name="lblEllipseTotal" Margin="20 0 0 0"></Label>
<Label Foreground="White" Name="lblEllipseTotal2" Margin="20 0 0 0"></Label>
</StackPanel>
</StackPanel>
</Grid>
</StackPanel>
</Grid>
I think you might never get the "exact" areas from the CombinedGeometry. As expected, WPF does not use the "ideal" method to calculate this values. From MSDN: "Some Geometry methods (such as GetArea) produce or use a polygonal approximation of the geometry".
Check MSDN

Event bubbling with CaptureMouse() in Silverlight

I'm writing a window program in Silverlight, meaning that the top bar of a popup has a draggable area, and within that draggable area, an "X" that closes the window. My drag function has a capturemouse() event which, when combined with the event bubbling that occurs, prevents the close function from being called. Here's the code:
private void close_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e){
pop.IsOpen = false;
hasFocus = true;
}
private void topBar_MouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
Border item = sender as Border;
mouseY = e.GetPosition(null).Y;
mouseX = e.GetPosition(null).X;
draggable = true;
item.CaptureMouse();
}
private void topBar_MouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
if(draggable){
double changeY = e.GetPosition(null).Y - mouseY;
double changeX = e.GetPosition(null).X - mouseX;
double top = changeY + (double)pop.GetValue(Canvas.TopProperty);
double left = changeX + (double)pop.GetValue(Canvas.LeftProperty);
if(top<0){
top = 0;
}
if(left<0){
left = 0;
}
if(left>670){
left = 670;
}
if(top>450){
top = 450;
}
pop.SetValue(Canvas.TopProperty, top);
pop.SetValue(Canvas.LeftProperty, left);
mouseY = e.GetPosition(null).Y;
mouseX = e.GetPosition(null).X;
}
}
private void topBar_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
Border item = sender as Border;
draggable = false;
mouseY = -1;
mouseX = -1;
item.ReleaseMouseCapture();
}
EDIT: here is the XAML for the entire popup:
<Popup x:Name="pop" Height="200" Width="200" VerticalAlignment="Center" HorizontalAlignment="Center">
<Border CornerRadius="5" Width="200" Height="200" Background="#FFFAFCFF" BorderThickness="1">
<Border.BorderBrush>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="#99666666" Offset="0" />
<GradientStop Color="#99F5F5F5" Offset="0.5"/>
<GradientStop Color="#99666666" Offset="1"/>
</LinearGradientBrush>
</Border.BorderBrush>
<StackPanel>
<Border x:Name="topBar" CornerRadius="4,4,0,0" BorderBrush="Silver" BorderThickness="0,0,0,1" Background="Crimson" Width="198" Height="20" MouseLeftButtonDown="topBar_MouseLeftButtonDown" MouseMove="topBar_MouseMove" MouseLeftButtonUp="topBar_MouseLeftButtonUp">
<Image x:Name="close" Source="X.png" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="0,0,7,0" Height="11" Width="11" MouseLeftButtonUp="close_MouseLeftButtonUp" />
</Border>
<StackPanel Margin="10">
<TextBlock Text="Printer info goes here..." />
</StackPanel>
</StackPanel>
</Border>
</Popup>
The problem occurs because of your MouseCapture calls. When you set the mousecapture, the border is the only control that will be allowed to initiate mouse events. This means the image, while the mouse button is down, no longer triggers mouseevents.
Without the mousecapture it should work fine. Just for my curiosity, why do you set and release it?
I hope this helps.
Edit:
You can get the position of the mouseEvent and see if it falls in the image:
var x = e.GetPosition(close).X;
var y = e.GetPosition(close).Y;
if (0 <= x && x <= 11 && 0 <= y && y <= 11)
{
//do the close call
}
I haven't tested this code but it should be close to what you want to do.

Converting one value to multiple properties

I'm using the following data template:
<DataTemplate>
<Grid Width="40" Height="40">
<Ellipse Width="30" Height="30" x:Name="ellipse" />
<TextBlock Text="{Binding Robot.Id}" HorizontalAlignment="Center" VerticalAlignment="Center" />
<Line X1="20" X2="40" X2="20" X2="30" x:Name="line" />
</Grid>
</DataTemplate>
I want to apply the following code to the line when a property of the DataContext changes:
void UpdateHeading(double angle)
{
var center = grid.Width/2;
var radius = ellipse.Width/2;
line.X1 = center + (radius+5)*Math.Sin(angle);
line.Y1 = center + (radius+5)*Math.Cos(angle);
line.X2 = center + (radius-5)*Math.Sin(angle);
line.Y2 = center + (radius-5)*Math.Cos(angle);
}
Note that the code needs access to the size of two other elements
What is the best way to add the code? using a value converter doesn't seem right here, since I need to convert one property to four
There is also an IMultiValueConverter
EDIT:
You should have a ViewModel with a angle property and can then bind as the following (i have only demonstrated for x1:
<DataTemplate>
<Grid Width="40" Height="40" x:Name="grid">
<Ellipse Width="30" Height="30" x:Name="ellipse" />
<TextBlock Text="{Binding Robot.Id}" HorizontalAlignment="Center" VerticalAlignment="Center" />
<Line X2="40" X2="20" X2="30" x:Name="line">
<Line.X1>
<MultiBinding Converter="{StaticResource yourConverter}" ConverterParameter="{yourns:Enum">
<Binding ElementName=grid Path=Width />
<Binding ElementName=ellipse Path=Width />
<Binding Path=Angle />
</MultiBinding>
</Line.X1>
</Line>
</Grid>
</DataTemplate>
Implement INotifyPropertyChanged in you class that is providing the DataContext and expose properties for all the values that need to be updated. Then just bind to those properties. The properties can calculate whatever values they need and the UI will get updated.
For Example:
public class Heading : INotifyPropertyChanged
{
private string name = "";
public string Name { get { return name; } set { name = value; SendPropertyChanged("Name"); } }
public int Radius { get { return GridWidth/2; } }
public double X1 { get { return Center + (Radius + 5) * Math.Sin(Angle); } }
public double X2 { get { return Center + (Radius + 5) * Math.Cos(Angle); } }
public double Y1 { get { return Center + (Radius - 5) * Math.Sin(Angle); } }
public double Y2 { get { return Center + (Radius - 5) * Math.Cos(Angle); } }
public int Center { get { return GridWidth/2; } }
private int gridWidth = 50;
public int GridWidth { get { return gridWidth; } set { gridWidth = value; } }
private double angle;
public double Angle { get { return angle; } set { angle = value; SendPropertyChanged(""); } } //Empty string to notify of all properties
public event PropertyChangedEventHandler PropertyChanged;
void SendPropertyChanged(string propertyName)
{
if (PropertyChanged != null)
{
PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
}
}
}
Then just set the binding in you template as so:
<DataTemplate>
<Grid >
<Ellipse Width="{Binding GridWidth}" Height="40" x:Name="ellipse" Fill="Green" />
<TextBlock Text="{Binding Name}" HorizontalAlignment="Center" VerticalAlignment="Center" />
<Line X1="{Binding X1}" X2="{Binding X2}" Y1="{Binding Y1}" Y2="{Binding Y2}" x:Name="line" Stroke="Black" />
</Grid>
</DataTemplate>
Hope this helps.
I decided to create my own shape.
XAML usage:
<Grid Width="40" Height="40">
<Ellipse x:Name="ellipse" Width="30" Height="30" />
<TextBlock Text="{Binding Robot.Id}" HorizontalAlignment="Center" VerticalAlignment="Center" />
<Controls:HeadingLine BoundingSize="40" ShapeSize="30" Length="10" Angle="{Binding Heading}" Stroke="Black" StrokeThickness="1" />
</Grid>
And code:
public sealed class HeadingLine : Shape
{
// Properties definitions
....
// Code based on http://www.codeproject.com/KB/WPF/wpfarrow.aspx
protected override Geometry DefiningGeometry
{
get
{
var geometry = new StreamGeometry();
using (var context = geometry.Open())
{
InternalDrawArrowGeometry(context);
}
geometry.Freeze();
return geometry;
}
}
private void InternalDrawArrowGeometry(StreamGeometryContext context)
{
var center = BoundingSize / 2;
var radius = ShapeSize / 2;
var offset = Length / 2;
var angle = Math.PI - Angle;
var x1 = center + (radius + offset) * Math.Sin(angle);
var y1 = center + (radius + offset) * Math.Cos(angle);
var x2 = center + (radius - offset) * Math.Sin(angle);
var y2 = center + (radius - offset) * Math.Cos(angle);
context.BeginFigure(new Point(x1, y1), false, false);
context.LineTo(new Point(x2, y2), true, true);
}
}

Resources