Silverlight Button Event Not firing - silverlight

I know this type of question has been asked before, But I have tried the suggestions to no avail, so hopefully some fresh eyes can help me on this.
I have a Custom Control, which is basically a Border with a grid inside which contains 2 textboxes and a button.
The control also has a octagon that is added in codebehind.
The button's click event does not fire. I gather it has probably got something to do with the controls above it getting the click event instead of the Button, but I don't know how to solve it. Or perhaps because the octagon is drawn afterwards in the code behind.
I have tried so many different things, moving the button, adding another grid, setting the background to Transparent. This is driving me nuts.
Below is the code.
XAML
<UserControl x:Class="HSCGym.UserControls.CustomErrorControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignHeight="800" d:DesignWidth="1000">
<Border x:Name="ErrorBorder" BorderBrush="Black" BorderThickness="2" CornerRadius="20" Background="White"
Width="900" Height="700"
VerticalAlignment="Stretch" HorizontalAlignment="Stretch"
Grid.Row="0" Grid.RowSpan="2">
<Border.Effect>
<DropShadowEffect BlurRadius="7" Direction="300" ShadowDepth="6" Color="Black" />
</Border.Effect>
<Grid x:Name="LayoutRoot" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Background="Transparent" >
<Grid.RowDefinitions>
<RowDefinition Height="55" />
<RowDefinition Height="55" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<TextBlock Text="{Binding ErrorText}" HorizontalAlignment="Center" FontSize="40" Grid.Row="0"/>
<TextBlock Text="{Binding ErrorText2}" HorizontalAlignment="Center" FontSize="40" Grid.Row="1"/>
<Button x:Name="btnExit" VerticalAlignment="Bottom" HorizontalAlignment="Left" Margin="38,0,0,20"
Content="Exit" Height="50" Width="200" FontSize="20" Click="btnExit_Click"
Grid.Row="2"/>
</Grid>
</Border>
</UserControl>
And the Code Behind:
public partial class CustomErrorControl
{
public string ErrorText { get; set; }
public string ErrorText2 { get; set; }
public string StopText { get; set; }
private readonly int x;
private readonly int y;
private readonly int r;
public CustomErrorControl(int x, int y, int radius, string stopError)
{
InitializeComponent();
this.x = x;
this.y = y;
r = radius;
StopText = stopError;
DrawOctagon(this.x, this.y, r);
}
public void DrawOctagon(int x, int y, int R)
{
int r2 = (int)(R/Math.Sqrt(2));
// OuterOctagon
Point[] outerOctagon = new Point[8];
outerOctagon[0].X = x;
outerOctagon[0].Y = y - R;
outerOctagon[1].X = x + r2;
outerOctagon[1].Y = y - r2;
outerOctagon[2].X = x + R;
outerOctagon[2].Y = y;
outerOctagon[3].X = x + r2;
outerOctagon[3].Y = y + r2;
outerOctagon[4].X = x;
outerOctagon[4].Y = y + R;
outerOctagon[5].X = x - r2;
outerOctagon[5].Y = y + r2;
outerOctagon[6].X = x - R;
outerOctagon[6].Y = y;
outerOctagon[7].X = x - r2;
outerOctagon[7].Y = y - r2;
HexColor stop1Colour = new HexColor("#FFA30D0D");
HexColor stop2Colour = new HexColor("#FFCA0C0C");
HexColor stop3Colour = new HexColor("#FFF71212");
GradientStop gs1 = new GradientStop { Color = stop1Colour, Offset = 0.98 };
GradientStop gs2 = new GradientStop { Color = stop2Colour, Offset = 0.5 };
GradientStop gs3 = new GradientStop { Color = stop3Colour, Offset = 0.04 };
LinearGradientBrush gb = new LinearGradientBrush
{
StartPoint = new Point(0.77, 0.85),
EndPoint = new Point(0.13, 0.15),
GradientStops = new GradientStopCollection {gs1, gs2, gs3}
};
DropShadowEffect dse = new DropShadowEffect
{
BlurRadius = 8,
Color = Colors.Black,
Direction = 320,
ShadowDepth = 10.0,
Opacity = 0.5
};
Polygon octagonOuter = new Polygon
{
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center,
Fill = gb,
Stroke = new SolidColorBrush(Colors.Black),
StrokeThickness = 5,
StrokeDashArray = new DoubleCollection { 10, 0 },
Effect = dse,
Points = new PointCollection
{
new Point(outerOctagon[0].X, outerOctagon[0].Y),
new Point(outerOctagon[1].X, outerOctagon[1].Y),
new Point(outerOctagon[2].X, outerOctagon[2].Y),
new Point(outerOctagon[3].X, outerOctagon[3].Y),
new Point(outerOctagon[4].X, outerOctagon[4].Y),
new Point(outerOctagon[5].X, outerOctagon[5].Y),
new Point(outerOctagon[6].X, outerOctagon[6].Y),
new Point(outerOctagon[7].X, outerOctagon[7].Y),
}
};
double outerOctCenterY = octagonOuter.Points[6].Y - octagonOuter.Points[2].Y;
double outerOctCenterX = octagonOuter.Points[4].X - octagonOuter.Points[0].X;
var rotate = new RotateTransform { Angle = 22.8, CenterX = outerOctCenterX, CenterY = outerOctCenterY };
octagonOuter.RenderTransform = rotate;
Grid.SetRow(octagonOuter, 2);
TextBlock tbStopError = new TextBlock
{
Text = StopText,
Foreground = new SolidColorBrush(Colors.White),
FontFamily = new FontFamily("Courier New"),
FontSize = 80,
FontWeight = FontWeights.Bold,
VerticalAlignment = VerticalAlignment.Center,
HorizontalAlignment = HorizontalAlignment.Center
};
Grid.SetRow(tbStopError, 2);
LayoutRoot.Children.Add(octagonOuter);
LayoutRoot.Children.Add(tbStopError);
}
private void btnExit_Click(object sender, RoutedEventArgs e)
{
MessageBox.Show("Clicked");
}
}
Any ideas?
Many thanks
Neill
Update:
I have tested quickly putting MouseLeftButtonUp events on the grid and the border.
When I click the border, first the grid event fires, then the border event fires, but if I click the button, nothing fires. Does this mean the Button is above the grid and border so should be firing the event?
I will try some of the other suggestions as well in the meant time.
Neill
Update 2:
It gets stranger. I moved the button to the top and then click event fires. What I see is that from a certain Y point and below, absolutely no mouseleftbuttonup or click events fire, nada. This makes no sense. If I go from the bottom and click moving slowly up, then at a certain point, all events start firing.
Neill
Update 3
Hi all,
After removing controls one by one to see what is causing the issue, I now know it is the Border control. If I remove it, all works fine, when it is back, same problem as before. Any ideas?
Thanks
Neill
Update 4
Hi all,
OK so I finally sorted out the problem, so in case anybody finds themselves in a similar situation. It had nothing to do with that page at all. I am loading the page in a frame from a different page and what it seems is that I had to many row definitions set in the grid on the main page. After I corrected that, everything works fine.
Thanks

First try fill button by Transparent color (00000000).
Also you can send solution or part of it to me and I shall try to help.

Related

Binding between a template-generated object and its parent's property

The title of this question might be wrong, I am not sure how to phrase it. I am trying to implement a very simple dashboard in which users can drag controls around inside a Canvas control. I wrote a MoveThumb class that inherits Thumb to achieve this. It is working well enough. Now, I want to make sure that the user cannot move the draggable control outside the Canvas. It is simple enough to write the logic itself to limit the drag boundaries inside this MoveThumb class:
Public Class MoveThumb
Inherits Thumb
Public Sub New()
AddHandler DragDelta, New DragDeltaEventHandler(AddressOf Me.MoveThumb_DragDelta)
End Sub
Private Sub MoveThumb_DragDelta(ByVal sender As Object, ByVal e As DragDeltaEventArgs)
Dim item As Control = TryCast(Me.DataContext, Control)
If item IsNot Nothing Then
Dim left As Double = Canvas.GetLeft(item)
Dim top As Double = Canvas.GetTop(item)
Dim right As Double = left + item.ActualWidth
Dim bottom As Double = top + item.ActualHeight
Dim canvasWidth = 450
Dim canvasHeight = 800
If left + e.HorizontalChange > 0 Then
If top + e.VerticalChange > 0 Then
If right + e.HorizontalChange < canvasWidth Then
If bottom + e.VerticalChange > canvasHeight Then
Canvas.SetLeft(item, left + e.HorizontalChange)
Canvas.SetTop(item, top + e.VerticalChange)
End If
End If
End If
End If
End If
End Sub
End Class
And the XML:
<Window x:Class="MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:diagramDesigner"
xmlns:s="clr-namespace:diagramDesigner"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid>
<Canvas x:Name="Canvas1">
<Canvas.Resources>
<ControlTemplate x:Key="MoveThumbTemplate" TargetType="{x:Type s:MoveThumb}">
<Rectangle Fill="Transparent"/>
</ControlTemplate>
<ControlTemplate x:Key="DesignerItemTemplate" TargetType="ContentControl">
<Grid DataContext="{Binding RelativeSource={RelativeSource TemplatedParent}}">
<s:MoveThumb Template="{StaticResource MoveThumbTemplate}" Cursor="SizeAll"/>
<ContentPresenter Content="{TemplateBinding ContentControl.Content}"/>
</Grid>
</ControlTemplate>
</Canvas.Resources>
<ContentControl Name="DesignerItem"
Width="100"
Height="100"
Canvas.Top="100"
Canvas.Left="100"
Template="{StaticResource DesignerItemTemplate}">
<Ellipse Fill="Blue" IsHitTestVisible="False"/>
</ContentControl>
</Canvas>
</Grid>
</Window>
Problem is, I am explicitly stating the width and height of the canvas inside that MoveThumb class, which means that if my window changes size, and the Canvas changes size, the drag boundaries will remain the same. Ideally, I want to bind canvasWidth and canvasHeight to the actualWidth and actualHeight of the Canvas.
I am not sure what is the best way to achieve it. Acquiring the actualWidth and actualHeight values inside the MoveThumb_DragDelta function with something like actualWidth = mainWindow.Canvas1.ActualWidth would be quick and simple, but very bad coding practice.
Ideally, I imagine it would be best to pass the limits as arguments to the constructor of MoveThumb and stored as global field/property, but I don't see a way to do it, since this class is used as a template in the XML code, and not generated from code-behind. I am not exactly sure if that would work at all, because the MoveThumb might be instantized only once (during the creation of the control), so it won't work when the Canvas changes it's size afterwards.
So I probably should do some kind of one-way binding between the actualWidth of Canvas1 and canvasWidth (declared as global property) of MoveThumb. But again, I have no idea how to access it, since MoveThumb is used as a TargetType of ControlTemplate inside Canvas.Resources.
I am still pretty new to WPF, and it feels like there should be some very simple way to achieve this, but I'm not seeing it. Can anyone help?
Example using Dependency Properties:
public partial class MoveThumb : Thumb
{
private double privateCanvasWidth = double.NaN, privateCanvasHeight = double.NaN;
private static readonly Binding bindingActualWidth = new Binding()
{
Path = new PropertyPath(ActualWidthProperty),
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(Canvas), 1)
};
private static readonly Binding bindingActualHeight = new Binding()
{
Path = new PropertyPath(ActualHeightProperty),
RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(Canvas), 1)
};
public MoveThumb()
{
DragDelta += MoveThumb_DragDelta;
SetBinding(CanvasWidthProperty, bindingActualWidth);
SetBinding(CanvasHeightProperty, bindingActualHeight);
}
static MoveThumb()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(MoveThumb), new FrameworkPropertyMetadata(typeof(MoveThumb)));
}
private static void MoveThumb_DragDelta(object sender, DragDeltaEventArgs e)
{
MoveThumb thumb = (MoveThumb)sender;
//FrameworkElement item = thumb.MovableContent;
//if (item == null)
//{
// return;
//}
double left = Canvas.GetLeft(thumb);
double top = Canvas.GetTop(thumb);
double right = left + thumb.ActualWidth;
double bottom = top + thumb.ActualHeight;
double canvasWidth = thumb.privateCanvasWidth;
if (double.IsNaN(canvasWidth))
canvasWidth = 450;
double canvasHeight = thumb.privateCanvasHeight;
if (double.IsNaN(canvasHeight))
canvasWidth = 800;
left += e.HorizontalChange;
top += e.VerticalChange;
right += e.HorizontalChange;
bottom += e.VerticalChange;
if (left > 0 &&
top > 0 &&
right < canvasWidth &&
bottom < canvasHeight)
{
Canvas.SetLeft(thumb, left);
Canvas.SetTop(thumb, top);
}
}
}
// DependecyProperties
[ContentProperty(nameof(MovableContent))]
public partial class MoveThumb
{
/// <summary>Canvas Width.</summary>
public double CanvasWidth
{
get => (double)GetValue(CanvasWidthProperty);
set => SetValue(CanvasWidthProperty, value);
}
/// <summary><see cref="DependencyProperty"/> for property <see cref="CanvasWidth"/>.</summary>
public static readonly DependencyProperty CanvasWidthProperty =
DependencyProperty.Register(nameof(CanvasWidth), typeof(double), typeof(MoveThumb), new PropertyMetadata(double.NaN, CanvasSizeChanged));
/// <summary>Canvas Height.</summary>
public double CanvasHeight
{
get => (double)GetValue(CanvasHeightProperty);
set => SetValue(CanvasHeightProperty, value);
}
/// <summary><see cref="DependencyProperty"/> for property <see cref="CanvasHeight"/>.</summary>
public static readonly DependencyProperty CanvasHeightProperty =
DependencyProperty.Register(nameof(CanvasHeight), typeof(double), typeof(MoveThumb), new PropertyMetadata(double.NaN, CanvasSizeChanged));
// Property change handler.
// The code is shown as an example.
private static void CanvasSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
MoveThumb thumb = (MoveThumb)d;
if (e.Property == CanvasWidthProperty)
{
thumb.privateCanvasWidth = (double)e.NewValue;
}
else if (e.Property == CanvasHeightProperty)
{
thumb.privateCanvasHeight = (double)e.NewValue;
}
else
{
throw new Exception("God knows what happened!");
}
MoveThumb_DragDelta(thumb, new DragDeltaEventArgs(0, 0));
}
/// <summary>Movable content.</summary>
public FrameworkElement MovableContent
{
get => (FrameworkElement)GetValue(MovableContentProperty);
set => SetValue(MovableContentProperty, value);
}
/// <summary><see cref="DependencyProperty"/> for property <see cref="MovableContent"/>.</summary>
public static readonly DependencyProperty MovableContentProperty =
DependencyProperty.Register(nameof(MovableContent), typeof(FrameworkElement), typeof(MoveThumb), new PropertyMetadata(null));
}
In the Project add the theme "Themes\Generic.xaml":
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:customcontrols="clr-namespace:EmbedContent.CustomControls"
xmlns:s="clr-namespace:Febr20y">
<Style TargetType="{x:Type s:MoveThumb}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type s:MoveThumb}">
<ContentPresenter Content="{TemplateBinding MovableContent}"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
<Grid>
<Canvas x:Name="Canvas1">
<s:MoveThumb x:Name="DesignerItem"
Width="100"
Height="100"
Canvas.Top="100"
Canvas.Left="100">
<Ellipse Fill="Blue"/>
</s:MoveThumb>
</Canvas>
</Grid>
Video YouTube
Source Code

WPF 2D Game: Making a Camera that follows an object on a Canvas

I am trying to make a simple 2D Game in WPF, and I've come across a problem I can't solve.
Let's say I have a player on a 700x700 Canvas, but my MainWindow's Width and Height are set to 400.
I want to be able to have a camera like feature, that follows a rectangle object on the canvas (this object symbolises the player) and shows the corresponding portion of the canvas whenever the player moves.
In theory how could I implement a feature like this?
Here's a rough sample of how to do that with a ScrollViewer. Use the arrow keys to move the player around while keeping it in the view of the "camera". The canvas' background is set to a radial-brush, to see the camera moving.
MainWindow.xaml
<Window x:Class="WpfApp6.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d"
Title="MainWindow"
Background="#222"
Loaded="Window_Loaded"
SizeToContent="WidthAndHeight"
PreviewKeyDown="Window_PreviewKeyDown">
<Grid>
<ScrollViewer x:Name="CanvasViewer"
HorizontalScrollBarVisibility="Hidden"
VerticalScrollBarVisibility="Hidden">
<Canvas x:Name="Canvas"
IsHitTestVisible="False">
<Canvas.Background>
<RadialGradientBrush>
<GradientStop Offset="0"
Color="Orange" />
<GradientStop Offset="1"
Color="Blue" />
</RadialGradientBrush>
</Canvas.Background>
</Canvas>
</ScrollViewer>
</Grid>
</Window>
MainWindow.xaml.cs
public partial class MainWindow : Window
{
double _playerSize;
Rectangle _playerRect;
Vector _playerPosition;
public MainWindow()
{
InitializeComponent();
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
InitializeSizes();
InitializePlayerRect();
}
#region initialize
private void InitializeSizes()
{
_playerSize = 50;
Canvas.Width = 700;
Canvas.Height = 700;
CanvasViewer.Width = 400;
CanvasViewer.Height = 400;
}
private void InitializePlayerRect()
{
_playerRect = new Rectangle
{
Fill = Brushes.Lime,
Width = _playerSize,
Height = _playerSize,
HorizontalAlignment = HorizontalAlignment.Left,
VerticalAlignment = VerticalAlignment.Top
};
Canvas.Children.Add(_playerRect);
}
#endregion
#region move player
private void Window_PreviewKeyDown(object sender, KeyEventArgs e)
{
switch (e.Key)
{
case Key.Left: MovePlayerLeft(); break;
case Key.Up: MovePlayerUp(); break;
case Key.Right: MovePlayerRight(); break;
case Key.Down: MovePlayerDown(); break;
}
}
private void MovePlayerLeft()
{
var newX = _playerPosition.X - _playerSize;
_playerPosition.X = Math.Max(0, newX);
UpdatePlayerPositionAndCamera();
}
private void MovePlayerUp()
{
var newY = _playerPosition.Y - _playerSize;
_playerPosition.Y = Math.Max(0, newY);
UpdatePlayerPositionAndCamera();
}
private void MovePlayerRight()
{
var newX = _playerPosition.X + _playerSize;
_playerPosition.X = Math.Min(Canvas.Width - _playerSize, newX);
UpdatePlayerPositionAndCamera();
}
private void MovePlayerDown()
{
var newY = _playerPosition.Y + _playerSize;
_playerPosition.Y = Math.Min(Canvas.Height - _playerSize, newY);
UpdatePlayerPositionAndCamera();
}
#endregion
#region update player and camera
private void UpdatePlayerPositionAndCamera()
{
UpdatePlayerPosition();
UpdateCamera();
}
private void UpdatePlayerPosition()
{
// move the playerRect to it's new position
_playerRect.Margin = new Thickness(_playerPosition.X, _playerPosition.Y, 0, 0);
}
private void UpdateCamera()
{
// calculate offset of scrollViewer, relative to actual position of the player
var offsetX = _playerPosition.X / 2;
var offsetY = _playerPosition.Y / 2;
// move the "camera"
CanvasViewer.ScrollToHorizontalOffset(offsetX);
CanvasViewer.ScrollToVerticalOffset(offsetY);
}
#endregion
}
WPF isn't really the right technology for games, you're much better off using something like MonoGame.
To answer your question, though, you can wrap your canvas in a ScrollViewer:
<ScrollViewer x:Name="theScrollView" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" CanContentScroll="True">
<Canvas x:Name="theCanvas" Width="5000" Height="5000" />
</ScrollViewer>
Then in code you scroll the view to whatever position your camera is at:
theScrollView.ScrollToHorizontalOffset(100);

Wpf Live-Charts display tooltip based on mouse cursor move without mouse hover over Line chart point

I am using WPF Live-Charts (https://lvcharts.net)
I want the tooltip to display the point value according to the mouse cursor movement, as in the image link below.
I tried, but I haven't found a way to display the tooltip without hovering the mouse cursor over the point in Live-Charts.
Examples:
If anyone has done this, can you give some advice?
The solution is relatively simple. The problem with LiveCharts is, that it not well documented. It gets you easily started by providing some examples that target general requirements. But for advanced scenarios, the default controls doesn't offer enough flexibility to customize the behavior or layout. There is no documentation about the details on how things work or what the classes of the library are intended for.
Once I checked the implementation details, I found the controls to be really horrible authored or designed.
Anyway, this simple feature you are requesting is a good example for the shortcomings of the library - extensibility is really bad. Even customization is bad. I wish the authors would have allowed templates, as this would make customization a lot easier. It should be simple to to extend the existing behavior, but apparently its not, unless you know about undocumented implementation details.
The library doesn't come in like a true WPF library. I don't know the history, maybe it's a WinForms port by WinForms devs.
But it's free and open source. And that's a big plus.
The following example draws a cursor on the plotting area which snaps to the nearest chart point and higlights it, while the mouse is moving.
A custom ToolTip follows the mouse pointer to show info about the currently selected chart point:
ViewModel.cs
public class ViewModel : INotifyPropertyChanged
{
public ViewModel()
{
var chartValues = new ChartValues<Point>();
// Create a sine
for (double x = 0; x < 361; x++)
{
var point = new Point() {X = x, Y = Math.Sin(x * Math.PI / 180)};
chartValues.Add(point);
}
SeriesCollection = new SeriesCollection
{
new LineSeries
{
Configuration = new CartesianMapper<Point>()
.X(point => point.X)
.Y(point => point.Y),
Title = "Series X",
Values = chartValues,
Fill = Brushes.DarkRed
}
};
}
private ChartPoint selectedChartPoint;
public ChartPoint SelectedChartPoint
{
get => this.selectedChartPoint;
set
{
this.selectedChartPoint = value;
OnPropertyChanged();
}
}
private double cursorScreenPosition;
public double CursorScreenPosition
{
get => this.cursorScreenPosition;
set
{
this.cursorScreenPosition = value;
OnPropertyChanged();
}
}
public SeriesCollection SeriesCollection { get; set; }
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
MainWindow.xaml.cs
partial class MainWindow : Window
{
private void MoveChartCursorAndToolTip_OnMouseMove(object sender, MouseEventArgs e)
{
var chart = sender as CartesianChart;
if (!TryFindVisualChildElement(chart, out Canvas outerCanvas) ||
!TryFindVisualChildElement(outerCanvas, out Canvas graphPlottingArea))
{
return;
}
var viewModel = this.DataContext as ViewModel;
Point chartMousePosition = e.GetPosition(chart);
// Remove visual hover feedback for previous point
viewModel.SelectedChartPoint?.View.OnHoverLeave(viewModel.SelectedChartPoint);
// Find current selected chart point for the first x-axis
Point chartPoint = chart.ConvertToChartValues(chartMousePosition);
viewModel.SelectedChartPoint = chart.Series[0].ClosestPointTo(chartPoint.X, AxisOrientation.X);
// Show visual hover feedback for previous point
viewModel.SelectedChartPoint.View.OnHover(viewModel.SelectedChartPoint);
// Add the cursor for the x-axis.
// Since Chart internally reverses the screen coordinates
// to match chart's coordinate system
// and this coordinate system orientation applies also to Chart.VisualElements,
// the UIElements like Popup and Line are added directly to the plotting canvas.
if (chart.TryFindResource("CursorX") is Line cursorX
&& !graphPlottingArea.Children.Contains(cursorX))
{
graphPlottingArea.Children.Add(cursorX);
}
if (!(chart.TryFindResource("CursorXToolTip") is FrameworkElement cursorXToolTip))
{
return;
}
// Add the cursor for the x-axis.
// Since Chart internally reverses the screen coordinates
// to match chart's coordinate system
// and this coordinate system orientation applies also to Chart.VisualElements,
// the UIElements like Popup and Line are added directly to the plotting canvas.
if (!graphPlottingArea.Children.Contains(cursorXToolTip))
{
graphPlottingArea.Children.Add(cursorXToolTip);
}
// Position the ToolTip
Point canvasMousePosition = e.GetPosition(graphPlottingArea);
Canvas.SetLeft(cursorXToolTip, canvasMousePosition.X - cursorXToolTip.ActualWidth);
Canvas.SetTop(cursorXToolTip, canvasMousePosition.Y);
}
// Helper method to traverse the visual tree of an element
private bool TryFindVisualChildElement<TChild>(DependencyObject parent, out TChild resultElement)
where TChild : DependencyObject
{
resultElement = null;
for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
{
DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);
if (childElement is Popup popup)
{
childElement = popup.Child;
}
if (childElement is TChild)
{
resultElement = childElement as TChild;
return true;
}
if (TryFindVisualChildElement(childElement, out resultElement))
{
return true;
}
}
return false;
}
}
MainWindow.xaml
<Window>
<Window.DataComtext>
<ViewModel />
</Window.DataContext>
<CartesianChart MouseMove="MoveChartCursorAndToolTip_OnMouseMove"
Series="{Binding SeriesCollection}"
Zoom="X"
Height="600">
<CartesianChart.Resources>
<!-- The cursor for the x-axis that snaps to the nearest chart point -->
<Line x:Key="CursorX"
Canvas.ZIndex="2"
Canvas.Left="{Binding SelectedChartPoint.ChartLocation.X}"
Y1="0"
Y2="{Binding ElementName=CartesianChart, Path=ActualHeight}"
Stroke="Gray"
StrokeThickness="1" />
<!-- The ToolTip that follows the mouse pointer-->
<Border x:Key="CursorXToolTip"
Canvas.ZIndex="3"
Background="LightGray"
Padding="8"
CornerRadius="8">
<StackPanel Background="LightGray">
<StackPanel Orientation="Horizontal">
<Path Height="20" Width="20"
Stretch="UniformToFill"
Data="{Binding SelectedChartPoint.SeriesView.(Series.PointGeometry)}"
Fill="{Binding SelectedChartPoint.SeriesView.(Series.Fill)}"
Stroke="{Binding SelectedChartPoint.SeriesView.(Series.Stroke)}"
StrokeThickness="{Binding SelectedChartPoint.SeriesView.(Series.StrokeThickness)}" />
<TextBlock Text="{Binding SelectedChartPoint.SeriesView.(Series.Title)}"
VerticalAlignment="Center" />
</StackPanel>
<TextBlock Text="{Binding SelectedChartPoint.X, StringFormat=X:{0}}" />
<TextBlock Text="{Binding SelectedChartPoint.Y, StringFormat=Y:{0}}" />
</StackPanel>
</Border>
</CartesianChart.Resources>
<CartesianChart.AxisY>
<Axis Title="Y" />
</CartesianChart.AxisY>
<CartesianChart.AxisX>
<Axis Title="X" />
</CartesianChart.AxisX>
</CartesianChart>
<Window>

WPF Image Pan, Zoom and Scroll with layers on a canvas

I'm hoping someone can help me out here. I'm building a WPF imaging application that takes live images from a camera allowing users to view the image, and subsequently highlight regions of interest (ROI) on that image. Information about the ROIs (width, height, location relative to a point on the image, etc) is then sent back to the camera, in effect telling/training the camera firmware where to look for things like barcodes, text, liquid levels, turns on a screw, etc. on the image). A desired feature is the ability to pan and zoom the image and it's ROIs, as well as scroll when the image is zoomed larger than the viewing area. The StrokeThickness and FontSize of the ROI's need to keep there original scale, but the width and height of the shapes within an ROI need to scale with the image (this is critical to capture exact pixel locations to transmit to the camera). I've got most of this worked out with the exception of scrolling and a few other issues. My two areas of concern are:
When I introduce a ScrollViewer I don't get any scroll behavior. As I understand it I need to introduce a LayoutTransform to get the correct ScrollViewer behavior. However when I do that other areas start to break down (e.g. ROIs don't hold their correct position over the image, or the mouse pointer begins to creep away from the selected point on the image when panning, or the left corner of my image bounces to the current mouse position on MouseDown .)
I can't quite get the scaling of my ROI's the way I need them. I have this working, but it is not ideal. What I have doesn't retain the exact stroke thickness, and I haven't looked into ignoring scale on the textblocks. Hopefully you'll see what I'm doing in the code samples.
I'm sure my issue has something to do with my lack of understanding of Transforms and their relationship to the WPF layout system. Hopefully a rendition of the code that exhibits what I've accomplished so far will help (see below).
FYI, if Adorners are the suggestion, that may not work in my scenario because I could end up with more adorners than are supported (rumor 144 adorners is when things start breaking down).
First off, below is a screenshot showing an image with to ROI's (text and a shape). The rectangle, ellipse and text need to follow the area on the image in scale and rotation, but not they shouldn't scale in thickness or fontsize.
Here's the XAML that is showing the above image, along with a Slider for zooming (mousewheel zoom will come later)
<Window x:Class="PanZoomStackOverflow.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
Title="MainWindow" Height="768" Width="1024">
<DockPanel>
<Slider x:Name="_ImageZoomSlider" DockPanel.Dock="Bottom"
Value="2"
HorizontalAlignment="Center" Margin="6,0,0,0"
Width="143" Minimum=".5" Maximum="20" SmallChange=".1"
LargeChange=".2" TickFrequency="2"
TickPlacement="BottomRight" Padding="0" Height="23"/>
<!-- This resides in a user control in my solution -->
<Grid x:Name="LayoutRoot">
<ScrollViewer Name="border" HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<Grid x:Name="_ImageDisplayGrid">
<Image x:Name="_DisplayImage" Margin="2" Stretch="None"
Source="Untitled.bmp"
RenderTransformOrigin ="0.5,0.5"
RenderOptions.BitmapScalingMode="NearestNeighbor"
MouseLeftButtonDown="ImageScrollArea_MouseLeftButtonDown"
MouseLeftButtonUp="ImageScrollArea_MouseLeftButtonUp"
MouseMove="ImageScrollArea_MouseMove">
<Image.LayoutTransform>
<TransformGroup>
<ScaleTransform />
<TranslateTransform />
</TransformGroup>
</Image.LayoutTransform>
</Image>
<AdornerDecorator> <!-- Using this Adorner Decorator for Move, Resize and Rotation and feedback adornernments -->
<Canvas x:Name="_ROICollectionCanvas"
Width="{Binding ElementName=_DisplayImage, Path=ActualWidth, Mode=OneWay}"
Height="{Binding ElementName=_DisplayImage, Path=ActualHeight, Mode=OneWay}"
Margin="{Binding ElementName=_DisplayImage, Path=Margin, Mode=OneWay}">
<!-- This is a user control in my solution -->
<Grid IsHitTestVisible="False" Canvas.Left="138" Canvas.Top="58" Height="25" Width="186">
<TextBlock Text="Rectangle ROI" HorizontalAlignment="Center" VerticalAlignment="Top"
Foreground="Orange" FontWeight="Bold" Margin="0,-15,0,0"/>
<Rectangle StrokeThickness="2" Stroke="Orange"/>
</Grid>
<!-- This is a user control in my solution -->
<Grid IsHitTestVisible="False" Canvas.Left="176" Canvas.Top="154" Height="65" Width="69">
<TextBlock Text="Ellipse ROI" HorizontalAlignment="Center" VerticalAlignment="Top"
Foreground="Orange" FontWeight="Bold" Margin="0,-15,0,0"/>
<Ellipse StrokeThickness="2" Stroke="Orange"/>
</Grid>
</Canvas>
</AdornerDecorator>
</Grid>
</ScrollViewer>
</Grid>
</DockPanel>
Here's the C# that manages pan and zoom.
public partial class MainWindow : Window
{
private Point origin;
private Point start;
private Slider _slider;
public MainWindow()
{
this.InitializeComponent();
//Setup a transform group that we'll use to manage panning of the image area
TransformGroup group = new TransformGroup();
ScaleTransform st = new ScaleTransform();
group.Children.Add(st);
TranslateTransform tt = new TranslateTransform();
group.Children.Add(tt);
//Wire up the slider to the image for zooming
_slider = _ImageZoomSlider;
_slider.ValueChanged += _ImageZoomSlider_ValueChanged;
st.ScaleX = _slider.Value;
st.ScaleY = _slider.Value;
//_ImageScrollArea.RenderTransformOrigin = new Point(0.5, 0.5);
//_ImageScrollArea.LayoutTransform = group;
_DisplayImage.RenderTransformOrigin = new Point(0.5, 0.5);
_DisplayImage.RenderTransform = group;
_ROICollectionCanvas.RenderTransformOrigin = new Point(0.5, 0.5);
_ROICollectionCanvas.RenderTransform = group;
}
//Captures the mouse to prepare for panning the scrollable image area
private void ImageScrollArea_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
_DisplayImage.ReleaseMouseCapture();
}
//Moves/Pans the scrollable image area assuming mouse is captured.
private void ImageScrollArea_MouseMove(object sender, MouseEventArgs e)
{
if (!_DisplayImage.IsMouseCaptured) return;
var tt = (TranslateTransform)((TransformGroup)_DisplayImage.RenderTransform).Children.First(tr => tr is TranslateTransform);
Vector v = start - e.GetPosition(border);
tt.X = origin.X - v.X;
tt.Y = origin.Y - v.Y;
}
//Cleanup for Move/Pan when mouse is released
private void ImageScrollArea_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
_DisplayImage.CaptureMouse();
var tt = (TranslateTransform)((TransformGroup)_DisplayImage.RenderTransform).Children.First(tr => tr is TranslateTransform);
start = e.GetPosition(border);
origin = new Point(tt.X, tt.Y);
}
//Zoom according to the slider changes
private void _ImageZoomSlider_ValueChanged(object sender, RoutedPropertyChangedEventArgs<double> e)
{
//Panel panel = _ImageScrollArea;
Image panel = _DisplayImage;
//Set the scale coordinates on the ScaleTransform from the slider
ScaleTransform transform = (ScaleTransform)((TransformGroup)panel.RenderTransform).Children.First(tr => tr is ScaleTransform);
transform.ScaleX = _slider.Value;
transform.ScaleY = _slider.Value;
//Set the zoom (this will affect rotate too) origin to the center of the panel
panel.RenderTransformOrigin = new Point(0.5, 0.5);
foreach (UIElement child in _ROICollectionCanvas.Children)
{
//Assume all shapes are contained in a panel
Panel childPanel = child as Panel;
var x = childPanel.Children;
//Shape width and heigh should scale, but not StrokeThickness
foreach (var shape in childPanel.Children.OfType<Shape>())
{
if (shape.Tag == null)
{
//Hack: This is be a property on a usercontrol in my solution
shape.Tag = shape.StrokeThickness;
}
double orignalStrokeThickness = (double)shape.Tag;
//Attempt to keep the underlying shape border/stroke from thickening as well
double newThickness = shape.StrokeThickness - (orignalStrokeThickness / transform.ScaleX);
shape.StrokeThickness -= newThickness;
}
}
}
}
The code should work in a .NET 4.0 or 4.5 project and solution, assuming no cut/paste errors.
Any thoughts? Suggestions are welcome.
Ok. This is my take on what you described.
It looks like this:
Since I'm not applying any RenderTransforms, I get the desired Scrollbar / ScrollViewer functionality.
MVVM, which is THE way to go in WPF. UI and data are independent thus the DataItems only have double and int properties for X,Y, Width,Height, etc that you can use for whatever purposes or even store them in a Database.
I added the whole stuff inside a Thumb to handle the panning. You will still need to do something about the Panning that occurs when you are dragging / resizing a ROI via the ResizerControl. I guess you can check for Mouse.DirectlyOver or something.
I actually used a ListBox to handle the ROIs so that you may have 1 selected ROI at any given time. This toggles the Resizing Functionality. So that if you click on a ROI, you will get the resizer visible.
The Scaling is handled at the ViewModel level, thus eliminating the need for custom Panels or stuff like that (though #Clemens' solution is nice as well)
I'm using an Enum and some DataTriggers to define the Shapes. See the DataTemplate DataType={x:Type local:ROI} part.
WPF Rocks. Just Copy and paste my code in a File -> New Project -> WPF Application and see the results for yourself.
<Window x:Class="MiscSamples.PanZoomStackOverflow_MVVM"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:MiscSamples"
Title="PanZoomStackOverflow_MVVM" Height="300" Width="300">
<Window.Resources>
<DataTemplate DataType="{x:Type local:ROI}">
<Grid Background="#01FFFFFF">
<Path x:Name="Path" StrokeThickness="2" Stroke="Black"
Stretch="Fill"/>
<local:ResizerControl Visibility="Collapsed" Background="#30FFFFFF"
X="{Binding X}" Y="{Binding Y}"
ItemWidth="{Binding Width}"
ItemHeight="{Binding Height}"
x:Name="Resizer"/>
</Grid>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding IsSelected, RelativeSource={RelativeSource AncestorType=ListBoxItem}}" Value="True">
<Setter TargetName="Resizer" Property="Visibility" Value="Visible"/>
</DataTrigger>
<DataTrigger Binding="{Binding Shape}" Value="{x:Static local:Shapes.Square}">
<Setter TargetName="Path" Property="Data">
<Setter.Value>
<RectangleGeometry Rect="0,0,10,10"/>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Shape}" Value="{x:Static local:Shapes.Round}">
<Setter TargetName="Path" Property="Data">
<Setter.Value>
<EllipseGeometry RadiusX="10" RadiusY="10"/>
</Setter.Value>
</Setter>
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
<Style TargetType="ListBox" x:Key="ROIListBoxStyle">
<Setter Property="ItemsPanel">
<Setter.Value>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</Setter.Value>
</Setter>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<ItemsPresenter/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="ListBoxItem" x:Key="ROIItemStyle">
<Setter Property="Canvas.Left" Value="{Binding ActualX}"/>
<Setter Property="Canvas.Top" Value="{Binding ActualY}"/>
<Setter Property="Height" Value="{Binding ActualHeight}"/>
<Setter Property="Width" Value="{Binding ActualWidth}"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="ListBoxItem">
<ContentPresenter ContentSource="Content"/>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</Window.Resources>
<DockPanel>
<Slider VerticalAlignment="Center"
Maximum="2" Minimum="0" Value="{Binding ScaleFactor}" SmallChange=".1"
DockPanel.Dock="Bottom"/>
<ScrollViewer VerticalScrollBarVisibility="Visible"
HorizontalScrollBarVisibility="Visible" x:Name="scr"
ScrollChanged="ScrollChanged">
<Thumb DragDelta="Thumb_DragDelta">
<Thumb.Template>
<ControlTemplate>
<Grid>
<Image Source="/Images/Homer.jpg" Stretch="None" x:Name="Img"
VerticalAlignment="Top" HorizontalAlignment="Left">
<Image.LayoutTransform>
<TransformGroup>
<ScaleTransform ScaleX="{Binding ScaleFactor}" ScaleY="{Binding ScaleFactor}"/>
</TransformGroup>
</Image.LayoutTransform>
</Image>
<ListBox ItemsSource="{Binding ROIs}"
Width="{Binding ActualWidth, ElementName=Img}"
Height="{Binding ActualHeight,ElementName=Img}"
VerticalAlignment="Top" HorizontalAlignment="Left"
Style="{StaticResource ROIListBoxStyle}"
ItemContainerStyle="{StaticResource ROIItemStyle}"/>
</Grid>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</ScrollViewer>
</DockPanel>
Code Behind:
public partial class PanZoomStackOverflow_MVVM : Window
{
public PanZoomViewModel ViewModel { get; set; }
public PanZoomStackOverflow_MVVM()
{
InitializeComponent();
DataContext = ViewModel = new PanZoomViewModel();
ViewModel.ROIs.Add(new ROI() {ScaleFactor = ViewModel.ScaleFactor, X = 150, Y = 150, Height = 200, Width = 200, Shape = Shapes.Square});
ViewModel.ROIs.Add(new ROI() { ScaleFactor = ViewModel.ScaleFactor, X = 50, Y = 230, Height = 102, Width = 300, Shape = Shapes.Round });
}
private void Thumb_DragDelta(object sender, DragDeltaEventArgs e)
{
//TODO: Detect whether a ROI is being resized / dragged and prevent Panning if so.
IsPanning = true;
ViewModel.OffsetX = (ViewModel.OffsetX + (((e.HorizontalChange/10) * -1) * ViewModel.ScaleFactor));
ViewModel.OffsetY = (ViewModel.OffsetY + (((e.VerticalChange/10) * -1) * ViewModel.ScaleFactor));
scr.ScrollToVerticalOffset(ViewModel.OffsetY);
scr.ScrollToHorizontalOffset(ViewModel.OffsetX);
IsPanning = false;
}
private bool IsPanning { get; set; }
private void ScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (!IsPanning)
{
ViewModel.OffsetX = e.HorizontalOffset;
ViewModel.OffsetY = e.VerticalOffset;
}
}
}
Main ViewModel:
public class PanZoomViewModel:PropertyChangedBase
{
private double _offsetX;
public double OffsetX
{
get { return _offsetX; }
set
{
_offsetX = value;
OnPropertyChanged("OffsetX");
}
}
private double _offsetY;
public double OffsetY
{
get { return _offsetY; }
set
{
_offsetY = value;
OnPropertyChanged("OffsetY");
}
}
private double _scaleFactor = 1;
public double ScaleFactor
{
get { return _scaleFactor; }
set
{
_scaleFactor = value;
OnPropertyChanged("ScaleFactor");
ROIs.ToList().ForEach(x => x.ScaleFactor = value);
}
}
private ObservableCollection<ROI> _rois;
public ObservableCollection<ROI> ROIs
{
get { return _rois ?? (_rois = new ObservableCollection<ROI>()); }
}
}
ROI ViewModel:
public class ROI:PropertyChangedBase
{
private Shapes _shape;
public Shapes Shape
{
get { return _shape; }
set
{
_shape = value;
OnPropertyChanged("Shape");
}
}
private double _scaleFactor;
public double ScaleFactor
{
get { return _scaleFactor; }
set
{
_scaleFactor = value;
OnPropertyChanged("ScaleFactor");
OnPropertyChanged("ActualX");
OnPropertyChanged("ActualY");
OnPropertyChanged("ActualHeight");
OnPropertyChanged("ActualWidth");
}
}
private double _x;
public double X
{
get { return _x; }
set
{
_x = value;
OnPropertyChanged("X");
OnPropertyChanged("ActualX");
}
}
private double _y;
public double Y
{
get { return _y; }
set
{
_y = value;
OnPropertyChanged("Y");
OnPropertyChanged("ActualY");
}
}
private double _height;
public double Height
{
get { return _height; }
set
{
_height = value;
OnPropertyChanged("Height");
OnPropertyChanged("ActualHeight");
}
}
private double _width;
public double Width
{
get { return _width; }
set
{
_width = value;
OnPropertyChanged("Width");
OnPropertyChanged("ActualWidth");
}
}
public double ActualX { get { return X*ScaleFactor; }}
public double ActualY { get { return Y*ScaleFactor; }}
public double ActualWidth { get { return Width*ScaleFactor; }}
public double ActualHeight { get { return Height * ScaleFactor; } }
}
Shapes Enum:
public enum Shapes
{
Round = 1,
Square = 2,
AnyOther
}
PropertyChangedBase (MVVM Helper class):
public class PropertyChangedBase:INotifyPropertyChanged
{
public event PropertyChangedEventHandler PropertyChanged;
protected virtual void OnPropertyChanged(string propertyName)
{
Application.Current.Dispatcher.BeginInvoke((Action) (() =>
{
PropertyChangedEventHandler handler = PropertyChanged;
if (handler != null) handler(this, new PropertyChangedEventArgs(propertyName));
}));
}
}
Resizer Control:
<UserControl x:Class="MiscSamples.ResizerControl"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<Grid>
<Thumb DragDelta="Center_DragDelta" Height="10" Width="10"
VerticalAlignment="Center" HorizontalAlignment="Center"/>
<Thumb DragDelta="UpperLeft_DragDelta" Height="10" Width="10"
VerticalAlignment="Top" HorizontalAlignment="Left"/>
<Thumb DragDelta="UpperRight_DragDelta" Height="10" Width="10"
VerticalAlignment="Top" HorizontalAlignment="Right"/>
<Thumb DragDelta="LowerLeft_DragDelta" Height="10" Width="10"
VerticalAlignment="Bottom" HorizontalAlignment="Left"/>
<Thumb DragDelta="LowerRight_DragDelta" Height="10" Width="10"
VerticalAlignment="Bottom" HorizontalAlignment="Right"/>
</Grid>
</UserControl>
Code Behind:
public partial class ResizerControl : UserControl
{
public static readonly DependencyProperty XProperty = DependencyProperty.Register("X", typeof(double), typeof(ResizerControl), new FrameworkPropertyMetadata(0d,FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty YProperty = DependencyProperty.Register("Y", typeof(double), typeof(ResizerControl), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty ItemWidthProperty = DependencyProperty.Register("ItemWidth", typeof(double), typeof(ResizerControl), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public static readonly DependencyProperty ItemHeightProperty = DependencyProperty.Register("ItemHeight", typeof(double), typeof(ResizerControl), new FrameworkPropertyMetadata(0d, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault));
public double X
{
get { return (double) GetValue(XProperty); }
set { SetValue(XProperty, value); }
}
public double Y
{
get { return (double)GetValue(YProperty); }
set { SetValue(YProperty, value); }
}
public double ItemHeight
{
get { return (double) GetValue(ItemHeightProperty); }
set { SetValue(ItemHeightProperty, value); }
}
public double ItemWidth
{
get { return (double) GetValue(ItemWidthProperty); }
set { SetValue(ItemWidthProperty, value); }
}
public ResizerControl()
{
InitializeComponent();
}
private void UpperLeft_DragDelta(object sender, DragDeltaEventArgs e)
{
X = X + e.HorizontalChange;
Y = Y + e.VerticalChange;
ItemHeight = ItemHeight + e.VerticalChange * -1;
ItemWidth = ItemWidth + e.HorizontalChange * -1;
}
private void UpperRight_DragDelta(object sender, DragDeltaEventArgs e)
{
Y = Y + e.VerticalChange;
ItemHeight = ItemHeight + e.VerticalChange * -1;
ItemWidth = ItemWidth + e.HorizontalChange;
}
private void LowerLeft_DragDelta(object sender, DragDeltaEventArgs e)
{
X = X + e.HorizontalChange;
ItemHeight = ItemHeight + e.VerticalChange;
ItemWidth = ItemWidth + e.HorizontalChange * -1;
}
private void LowerRight_DragDelta(object sender, DragDeltaEventArgs e)
{
ItemHeight = ItemHeight + e.VerticalChange;
ItemWidth = ItemWidth + e.HorizontalChange;
}
private void Center_DragDelta(object sender, DragDeltaEventArgs e)
{
X = X + e.HorizontalChange;
Y = Y + e.VerticalChange;
}
}
In order to transform shapes without changing their stroke thickness, you may use Path objects with transformed geometries.
The following XAML puts an Image and two Paths on a Canvas. The Image is scaled and translated by a RenderTransform. The same transform is also used for the Transform property of the geometries of the two Paths.
<Canvas>
<Image Source="C:\Users\Public\Pictures\Sample Pictures\Desert.jpg">
<Image.RenderTransform>
<TransformGroup x:Name="transform">
<ScaleTransform ScaleX="0.5" ScaleY="0.5"/>
<TranslateTransform X="100" Y="50"/>
</TransformGroup>
</Image.RenderTransform>
</Image>
<Path Stroke="Orange" StrokeThickness="2">
<Path.Data>
<RectangleGeometry Rect="50,100,100,50"
Transform="{Binding ElementName=transform}"/>
</Path.Data>
</Path>
<Path Stroke="Orange" StrokeThickness="2">
<Path.Data>
<EllipseGeometry Center="250,100" RadiusX="50" RadiusY="50"
Transform="{Binding ElementName=transform}"/>
</Path.Data>
</Path>
</Canvas>
Your application may now simply change the transform object in response to input events like MouseMove or MouseWheel.
Things get a little bit trickier when it comes to also transforming TextBlocks or other element that should not be scaled, but only be moved to a proper location.
You may create a specialized Panel which is able to apply this kind of transform to its child elements. Such a Panel would define an attached property that controls the position of a child element, and would apply the transform to this position instead of the RenderTransform or LayoutTransform of the child.
This may give you an idea of how such a Panel could be implemented:
public class TransformPanel : Panel
{
public static readonly DependencyProperty TransformProperty =
DependencyProperty.Register(
"Transform", typeof(Transform), typeof(TransformPanel),
new FrameworkPropertyMetadata(Transform.Identity,
FrameworkPropertyMetadataOptions.AffectsArrange));
public static readonly DependencyProperty PositionProperty =
DependencyProperty.RegisterAttached(
"Position", typeof(Point?), typeof(TransformPanel),
new PropertyMetadata(PositionPropertyChanged));
public Transform Transform
{
get { return (Transform)GetValue(TransformProperty); }
set { SetValue(TransformProperty, value); }
}
public static Point? GetPosition(UIElement element)
{
return (Point?)element.GetValue(PositionProperty);
}
public static void SetPosition(UIElement element, Point? value)
{
element.SetValue(PositionProperty, value);
}
protected override Size MeasureOverride(Size availableSize)
{
var infiniteSize = new Size(double.PositiveInfinity,
double.PositiveInfinity);
foreach (UIElement element in InternalChildren)
{
element.Measure(infiniteSize);
}
return new Size();
}
protected override Size ArrangeOverride(Size finalSize)
{
foreach (UIElement element in InternalChildren)
{
ArrangeElement(element, GetPosition(element));
}
return finalSize;
}
private void ArrangeElement(UIElement element, Point? position)
{
var arrangeRect = new Rect(element.DesiredSize);
if (position.HasValue && Transform != null)
{
arrangeRect.Location = Transform.Transform(position.Value);
}
element.Arrange(arrangeRect);
}
private static void PositionPropertyChanged(
DependencyObject obj, DependencyPropertyChangedEventArgs e)
{
var element = (UIElement)obj;
var panel = VisualTreeHelper.GetParent(element) as TransformPanel;
if (panel != null)
{
panel.ArrangeElement(element, (Point?)e.NewValue);
}
}
}
It would be used in XAML like this:
<local:TransformPanel>
<local:TransformPanel.Transform>
<TransformGroup>
<ScaleTransform ScaleX="0.5" ScaleY="0.5" x:Name="scale"/>
<TranslateTransform X="100"/>
</TransformGroup>
</local:TransformPanel.Transform>
<Image Source="C:\Users\Public\Pictures\Sample Pictures\Desert.jpg"
RenderTransform="{Binding Transform, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:TransformPanel}}"/>
<Path Stroke="Orange" StrokeThickness="2">
<Path.Data>
<RectangleGeometry Rect="50,100,100,50"
Transform="{Binding Transform, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:TransformPanel}}"/>
</Path.Data>
</Path>
<Path Stroke="Orange" StrokeThickness="2">
<Path.Data>
<EllipseGeometry Center="250,100" RadiusX="50" RadiusY="50"
Transform="{Binding Transform, RelativeSource={RelativeSource Mode=FindAncestor, AncestorType=local:TransformPanel}}"/>
</Path.Data>
</Path>
<TextBlock Text="Rectangle" local:TransformPanel.Position="50,150"/>
<TextBlock Text="Ellipse" local:TransformPanel.Position="200,150"/>
</local:TransformPanel>
Well this answer doesn't really help OP with his more specified issue but in general, camera panning, zooming in and out, and looking around (using the mouse) is quite difficult and so I just wanted to give some insight on how I implemented camera movement into my viewport scene (like Blender or Unity etc.)
This is the class called CameraPan, which contains some variables you can customize to edit the zooming in and out distance/speed, pan speed and camera look sensitivity. At the bottom of the class there is some hashed out code that represents the basic implementation into any scene. You first need to create a viewport and assign it to a 'Border' (which is a UI element that can handle mouse events since Viewport can't) and also create a camera alongside a couple of other public variables that are accessed from the CameraPan Class:
public partial class CameraPan
{
Point TemporaryMousePosition;
Point3D PreviousCameraPosition;
Quaternion QuatX;
Quaternion PreviousQuatX;
Quaternion QuatY;
Quaternion PreviousQuatY;
private readonly float PanSpeed = 4f;
private readonly float LookSensitivity = 100f;
private readonly float ZoomInOutDistance = 1f;
private readonly MainWindow mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
public Vector3D LookDirection(PerspectiveCamera camera, Point3D pointToLookAt) // Calculates vector direction between two points (LookAt() method)
{
Point3D CameraPosition = camera.Position;
Vector3D VectorDirection = new Vector3D
(pointToLookAt.X - CameraPosition.X,
pointToLookAt.Y - CameraPosition.Y,
pointToLookAt.Z - CameraPosition.Z);
return VectorDirection;
}
public void PanLookAroundViewport_MouseMove(object sender, MouseEventArgs e) // Panning the viewport using the camera
{
if (e.MiddleButton == MouseButtonState.Pressed)
{
Point mousePos = e.GetPosition(sender as Border); // Gets the current mouse pos
Point3D newCamPos = new Point3D(
((-mousePos.X + TemporaryMousePosition.X) / mainWindow.Width * PanSpeed) + PreviousCameraPosition.X,
((mousePos.Y - TemporaryMousePosition.Y) / mainWindow.Height * PanSpeed) + PreviousCameraPosition.Y,
mainWindow.MainCamera.Position.Z); // Calculates the proportional distance to move the camera,
//can be increased by changing the variable 'PanSpeed'
if (Keyboard.IsKeyDown(Key.LeftCtrl)) // Pan viewport
{
mainWindow.MainCamera.Position = newCamPos;
}
else // Look around viewport
{
double RotY = (e.GetPosition(sender as Label).X - TemporaryMousePosition.X) / mainWindow.Width * LookSensitivity; // MousePosX is the Y axis of a rotation
double RotX = (e.GetPosition(sender as Label).Y - TemporaryMousePosition.Y) / mainWindow.Height * LookSensitivity; // MousePosY is the X axis of a rotation
QuatX = Quaternion.Multiply(new Quaternion(new Vector3D(1, 0, 0), -RotX), PreviousQuatX);
QuatY = Quaternion.Multiply(new Quaternion(new Vector3D(0, 1, 0), -RotY), PreviousQuatY);
Quaternion QuaternionRotation = Quaternion.Multiply(QuatX, QuatY); // Composite Quaternion between the x rotation and the y rotation
mainWindow.camRotateTransform.Rotation = new QuaternionRotation3D(QuaternionRotation); // MainCamera.Transform = RotateTransform3D 'camRotateTransform'
}
}
}
public void MiddleMouseButton_MouseDown(object sender, MouseEventArgs e) // Declares some constants when mouse button 3 is first held down
{
if (e.MiddleButton == MouseButtonState.Pressed)
{
var mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
TemporaryMousePosition = e.GetPosition(sender as Label);
PreviousCameraPosition = mainWindow.MainCamera.Position;
PreviousQuatX = QuatX;
PreviousQuatY = QuatY;
}
}
public void MouseUp(object sender, MouseEventArgs e)
{
mainWindow.CameraCenter = new Point3D(
mainWindow.CameraCenter.X + mainWindow.MainCamera.Position.X - mainWindow.OriginalCamPosition.X,
mainWindow.CameraCenter.Y + mainWindow.MainCamera.Position.Y - mainWindow.OriginalCamPosition.Y,
mainWindow.CameraCenter.Z + mainWindow.MainCamera.Position.Z - mainWindow.OriginalCamPosition.Z);
// Sets the center of rotation of cam to current mouse position
} // Declares some constants when mouse button 3 is first let go
public void ZoomInOutViewport_MouseScroll(object sender, MouseWheelEventArgs e)
{
var cam = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault().MainCamera;
if (e.Delta > 0) // Wheel scrolled forwards - Zoom In
{
cam.Position = new Point3D(cam.Position.X, cam.Position.Y, cam.Position.Z - ZoomInOutDistance);
}
else // Wheel scrolled forwards - Zoom Out
{
cam.Position = new Point3D(cam.Position.X, cam.Position.Y, cam.Position.Z + ZoomInOutDistance);
}
}
// -----CODE IN 'public MainWindow()' STRUCT-----
/*
public PerspectiveCamera MainCamera = new PerspectiveCamera();
public AxisAngleRotation3D MainCamAngle;
public RotateTransform3D camRotateTransform;
public Point3D CameraCenter = new Point3D(0, 0, 0);
public Point3D OriginalCamPosition;
public MainWindow()
{
Viewport3D Viewport = new Viewport3D();
CameraPan cameraPan = new CameraPan(); // Initialises CameraPan class
MainCamera.Position = new Point3D(0, 2, 10);
MainCamera.FieldOfView = 60;
MainCamera.LookDirection = cameraPan.LookDirection(MainCamera, new Point3D(0, 0, 0));
// Some custom camera settings
OriginalCamPosition = MainCamera.Position;
// Saves the MainCamera's first position
camRotateTransform = new RotateTransform3D() // Rotation of camera
{
CenterX = CameraCenter.X,
CenterY = CameraCenter.Y,
CenterZ = CameraCenter.Z,
};
MainCamAngle = new AxisAngleRotation3D() // Rotation value of camRotateTransform
{
Axis = new Vector3D(1, 0, 0),
Angle = 0
};
camRotateTransform.Rotation = MainCamAngle;
MainCamera.Transform = camRotateTransform;
Border viewportHitBG = new Border() { Width = Width, Height = Height, Background = new SolidColorBrush(Colors.White) };
// UI Element to detect mouse click events
viewportHitBG.MouseMove += cameraPan.PanLookAroundViewport_MouseMove;
viewportHitBG.MouseDown += cameraPan.MiddleMouseButton_MouseDown;
viewportHitBG.MouseWheel += cameraPan.ZoomInOutViewport_MouseScroll;
viewportHitBG.MouseUp += cameraPan.MouseUp;
// Mouse Event handlers
// Assign the camera to the viewport
Viewport.Camera = MainCamera;
// Assign Viewport as the child of the UI Element that detects mouse events
viewportHitBG.Child = Viewport;
}
*/
}
The mouse event handlers run the specified camera pan functions depending on mouse and key events. The setup is similar to Unity Viewport controls (middle mouse to look around, middle mouse + CTRL to pan around, scroll wheel to zoom).
Here is my full implementation of the camera pan if you want it. It includes a scene that draws a red cube and lets you pan around the scene using the camera:
public partial class MainWindow : Window
{
private readonly TranslateTransform3D Position;
private readonly RotateTransform3D Rotation;
private readonly AxisAngleRotation3D Transform_Rotation;
private readonly ScaleTransform3D Scale;
public PerspectiveCamera MainCamera = new PerspectiveCamera();
public AxisAngleRotation3D MainCamAngle;
public RotateTransform3D camRotateTransform;
public Point3D CameraCenter = new Point3D(0, 0, 0);
public Point3D OriginalCamPosition;
public MainWindow()
{
InitializeComponent();
Height = SystemParameters.PrimaryScreenHeight;
Width = SystemParameters.PrimaryScreenWidth;
WindowState = WindowState.Maximized;
#region Initialising 3D Scene Objects
// Declare scene objects.
Viewport3D Viewport = new Viewport3D();
Model3DGroup ModelGroup = new Model3DGroup();
GeometryModel3D Cube = new GeometryModel3D();
ModelVisual3D CubeModel = new ModelVisual3D();
#endregion
#region UI Grid Objects
Grid grid = new Grid();
Slider AngleSlider = new Slider()
{
Height = 50,
VerticalAlignment = VerticalAlignment.Top,
};
AngleSlider.ValueChanged += AngleSlider_MouseMove;
grid.Children.Add(AngleSlider);
#endregion
#region Camera Stuff
CameraPan cameraPan = new CameraPan();
MainCamera.Position = new Point3D(0, 2, 10);
MainCamera.FieldOfView = 60;
MainCamera.LookDirection = cameraPan.LookDirection(MainCamera, new Point3D(0, 0, 0));
OriginalCamPosition = MainCamera.Position;
camRotateTransform = new RotateTransform3D()
{
CenterX = CameraCenter.X,
CenterY = CameraCenter.Y,
CenterZ = CameraCenter.Z,
};
MainCamAngle = new AxisAngleRotation3D()
{
Axis = new Vector3D(1, 0, 0),
Angle = 0
};
camRotateTransform.Rotation = MainCamAngle;
MainCamera.Transform = camRotateTransform;
Border viewportHitBG = new Border() { Width = Width, Height = Height, Background = new SolidColorBrush(Colors.White) };
viewportHitBG.MouseMove += cameraPan.PanLookAroundViewport_MouseMove;
viewportHitBG.MouseDown += cameraPan.MiddleMouseButton_MouseDown;
viewportHitBG.MouseWheel += cameraPan.ZoomInOutViewport_MouseScroll;
viewportHitBG.MouseUp += cameraPan.MouseUp;
// Asign the camera to the viewport
Viewport.Camera = MainCamera;
#endregion
#region Directional Lighting
// Define the lights cast in the scene. Without light, the 3D object cannot
// be seen. Note: to illuminate an object from additional directions, create
// additional lights.
AmbientLight ambientLight = new AmbientLight
{
Color = Colors.WhiteSmoke,
};
ModelGroup.Children.Add(ambientLight);
#endregion
#region Mesh Of Object
Vector3DCollection Normals = new Vector3DCollection
{
new Vector3D(0, 0, 1),
new Vector3D(0, 0, 1),
new Vector3D(0, 0, 1),
new Vector3D(0, 0, 1),
new Vector3D(0, 0, 1),
new Vector3D(0, 0, 1)
};
PointCollection TextureCoordinates = new PointCollection
{
new Point(0, 0),
new Point(1, 0),
new Point(1, 1),
new Point(0, 1),
};
Point3DCollection Positions = new Point3DCollection
{
new Point3D(-0.5, -0.5, 0.5), // BL FRONT 0
new Point3D(0.5, -0.5, 0.5), // BR FRONT 1
new Point3D(0.5, 0.5, 0.5), // TR FRONT 2
new Point3D(-0.5, 0.5, 0.5), // TL FRONT 3
new Point3D(-0.5, -0.5, -0.5), // BL BACK 4
new Point3D(0.5, -0.5, -0.5), // BR BACK 5
new Point3D(0.5, 0.5, -0.5), // TR BACK 6
new Point3D(-0.5, 0.5, -0.5) // TL BACK 7
};
MeshGeometry3D Faces = new MeshGeometry3D()
{
Normals = Normals,
Positions = Positions,
TextureCoordinates = TextureCoordinates,
TriangleIndices = new Int32Collection
{
0, 1, 2, 2, 3, 0,
6, 5, 4, 4, 7, 6,
4, 0, 3, 3, 7, 4,
2, 1, 5, 5, 6, 2,
7, 3, 2, 2, 6, 7,
1, 0, 4, 4, 5, 1
},
};
// Apply the mesh to the geometry model.
Cube.Geometry = Faces;
#endregion
#region Material Of Object
// The material specifies the material applied to the 3D object.
// Define material and apply to the mesh geometries.
Material myMaterial = new DiffuseMaterial(new SolidColorBrush(Color.FromScRgb(255, 255, 0, 0)));
Cube.Material = myMaterial;
#endregion
#region Transform Of Object
// Apply a transform to the object. In this sample, a rotation transform is applied, rendering the 3D object rotated.
Transform_Rotation = new AxisAngleRotation3D()
{
Angle = 0,
Axis = new Vector3D(0, 0, 0)
};
Position = new TranslateTransform3D
{
OffsetX = 0,
OffsetY = 0,
OffsetZ = 0
};
Scale = new ScaleTransform3D
{
ScaleX = 1,
ScaleY = 1,
ScaleZ = 1
};
Rotation = new RotateTransform3D
{
Rotation = Transform_Rotation
};
Transform3DGroup transformGroup = new Transform3DGroup();
transformGroup.Children.Add(Rotation);
transformGroup.Children.Add(Scale);
transformGroup.Children.Add(Position);
Cube.Transform = transformGroup;
#endregion
#region Adding Children To Groups And Parents
// Add the geometry model to the model group.
ModelGroup.Children.Add(Cube);
CubeModel.Content = ModelGroup;
Viewport.Children.Add(CubeModel);
viewportHitBG.Child = Viewport;
grid.Children.Add(viewportHitBG);
#endregion
Content = grid;
}
private void AngleSlider_MouseMove(object sender, RoutedEventArgs e)
{
Slider slider = (Slider)sender;
Transform_Rotation.Angle = slider.Value * 36;
Transform_Rotation.Axis = new Vector3D(0, 1, 0);
Scale.ScaleX = slider.Value / 5; Scale.ScaleY = slider.Value / 5; Scale.ScaleZ = slider.Value / 5;
Position.OffsetX = slider.Value / 5; Position.OffsetY = slider.Value / 5; Position.OffsetZ = slider.Value / 5;
}
}
public partial class CameraPan
{
Point TemporaryMousePosition;
Point3D PreviousCameraPosition;
Quaternion QuatX;
Quaternion PreviousQuatX;
Quaternion QuatY;
Quaternion PreviousQuatY;
private readonly float PanSpeed = 4f;
private readonly float LookSensitivity = 100f;
private readonly float ZoomInOutDistance = 1f;
private readonly MainWindow mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
public Vector3D LookDirection(PerspectiveCamera camera, Point3D pointToLookAt) // Calculates vector direction between two points (LookAt() method)
{
Point3D CameraPosition = camera.Position;
Vector3D VectorDirection = new Vector3D
(pointToLookAt.X - CameraPosition.X,
pointToLookAt.Y - CameraPosition.Y,
pointToLookAt.Z - CameraPosition.Z);
return VectorDirection;
}
public void PanLookAroundViewport_MouseMove(object sender, MouseEventArgs e) // Panning the viewport using the camera
{
if (e.MiddleButton == MouseButtonState.Pressed)
{
Point mousePos = e.GetPosition(sender as Border); // Gets the current mouse pos
Point3D newCamPos = new Point3D(
((-mousePos.X + TemporaryMousePosition.X) / mainWindow.Width * PanSpeed) + PreviousCameraPosition.X,
((mousePos.Y - TemporaryMousePosition.Y) / mainWindow.Height * PanSpeed) + PreviousCameraPosition.Y,
mainWindow.MainCamera.Position.Z); // Calculates the proportional distance to move the camera,
//can be increased by changing the variable 'PanSpeed'
if (Keyboard.IsKeyDown(Key.LeftCtrl)) // Pan viewport
{
mainWindow.MainCamera.Position = newCamPos;
}
else // Look around viewport
{
double RotY = (e.GetPosition(sender as Label).X - TemporaryMousePosition.X) / mainWindow.Width * LookSensitivity; // MousePosX is the Y axis of a rotation
double RotX = (e.GetPosition(sender as Label).Y - TemporaryMousePosition.Y) / mainWindow.Height * LookSensitivity; // MousePosY is the X axis of a rotation
QuatX = Quaternion.Multiply(new Quaternion(new Vector3D(1, 0, 0), -RotX), PreviousQuatX);
QuatY = Quaternion.Multiply(new Quaternion(new Vector3D(0, 1, 0), -RotY), PreviousQuatY);
Quaternion QuaternionRotation = Quaternion.Multiply(QuatX, QuatY); // Composite Quaternion between the x rotation and the y rotation
mainWindow.camRotateTransform.Rotation = new QuaternionRotation3D(QuaternionRotation); // MainCamera.Transform = RotateTransform3D 'camRotateTransform'
}
}
}
public void MiddleMouseButton_MouseDown(object sender, MouseEventArgs e) // Declares some constants when mouse button 3 is first held down
{
if (e.MiddleButton == MouseButtonState.Pressed)
{
var mainWindow = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault();
TemporaryMousePosition = e.GetPosition(sender as Label);
PreviousCameraPosition = mainWindow.MainCamera.Position;
PreviousQuatX = QuatX;
PreviousQuatY = QuatY;
}
}
public void MouseUp(object sender, MouseEventArgs e)
{
mainWindow.CameraCenter = new Point3D(
mainWindow.CameraCenter.X + mainWindow.MainCamera.Position.X - mainWindow.OriginalCamPosition.X,
mainWindow.CameraCenter.Y + mainWindow.MainCamera.Position.Y - mainWindow.OriginalCamPosition.Y,
mainWindow.CameraCenter.Z + mainWindow.MainCamera.Position.Z - mainWindow.OriginalCamPosition.Z);
// Sets the center of rotation of cam to current mouse position
} // Declares some constants when mouse button 3 is first let go
public void ZoomInOutViewport_MouseScroll(object sender, MouseWheelEventArgs e)
{
var cam = Application.Current.Windows.OfType<MainWindow>().FirstOrDefault().MainCamera;
if (e.Delta > 0) // Wheel scrolled forwards - Zoom In
{
cam.Position = new Point3D(cam.Position.X, cam.Position.Y, cam.Position.Z - ZoomInOutDistance);
}
else // Wheel scrolled forwards - Zoom Out
{
cam.Position = new Point3D(cam.Position.X, cam.Position.Y, cam.Position.Z + ZoomInOutDistance);
}
}
// -----CODE IN 'public MainWindow()' STRUCT-----
/*
public PerspectiveCamera MainCamera = new PerspectiveCamera();
public AxisAngleRotation3D MainCamAngle;
public RotateTransform3D camRotateTransform;
public Point3D CameraCenter = new Point3D(0, 0, 0);
public Point3D OriginalCamPosition;
public MainWindow()
{
Viewport3D Viewport = new Viewport3D();
CameraPan cameraPan = new CameraPan(); // Initialises CameraPan class
MainCamera.Position = new Point3D(0, 2, 10);
MainCamera.FieldOfView = 60;
MainCamera.LookDirection = cameraPan.LookDirection(MainCamera, new Point3D(0, 0, 0));
// Some custom camera settings
OriginalCamPosition = MainCamera.Position;
// Saves the MainCamera's first position
camRotateTransform = new RotateTransform3D() // Rotation of camera
{
CenterX = CameraCenter.X,
CenterY = CameraCenter.Y,
CenterZ = CameraCenter.Z,
};
MainCamAngle = new AxisAngleRotation3D() // Rotation value of camRotateTransform
{
Axis = new Vector3D(1, 0, 0),
Angle = 0
};
camRotateTransform.Rotation = MainCamAngle;
MainCamera.Transform = camRotateTransform;
Border viewportHitBG = new Border() { Width = Width, Height = Height, Background = new SolidColorBrush(Colors.White) };
// UI Element to detect mouse click events
viewportHitBG.MouseMove += cameraPan.PanLookAroundViewport_MouseMove;
viewportHitBG.MouseDown += cameraPan.MiddleMouseButton_MouseDown;
viewportHitBG.MouseWheel += cameraPan.ZoomInOutViewport_MouseScroll;
viewportHitBG.MouseUp += cameraPan.MouseUp;
// Mouse Event handlers
// Assign the camera to the viewport
Viewport.Camera = MainCamera;
// Assign Viewport as the child of the UI Element that detects mouse events
viewportHitBG.Child = Viewport;
}
*/
}
I hope it helps someone in the future!

WPF GridSplitter visiblity

I have a problem regarding GridSplitter visiblity.
In this, whatever I am hosting a Winform DataGridView. The GridSplitter, when dragged is properly visible on other controls. But not on this grid. In fact, whatever I host instead of Datagridview, becomes the topmost control, which makes the GridSplitter hide behind it.
<Grid>
<Grid.RowDefinitions>
<RowDefinition Name="rowForButton"/>
<RowDefinition Name="rowForGridSplitter" Height="Auto" MinHeight="81" />
</Grid.RowDefinitions>
<Button Grid.Row="0" Height="50" Width="110" Content="Button in First Row"/>
<my:WindowsFormsHost Panel.ZIndex="0" Grid.Row="1" Margin="30,11,138,0" x:Name="winHost" Height="58" VerticalAlignment="Top" OpacityMask="Transparent">
<win:DataGridView x:Name="dataGridView"></win:DataGridView>
</my:WindowsFormsHost>
<GridSplitter BorderThickness="1" Panel.ZIndex="1" Grid.Row="1" HorizontalAlignment="Stretch" Height="5" ShowsPreview="True" VerticalAlignment="Top">
</GridSplitter>
</Grid>
Usually you should either put a GridSplitter into its own grid cell or ensure via margins that no control can overlap it. But I don't know whether that exactly applies to you here. See also here.
I encountered this problem also, there is my solution:
var splitter = new GridSplitter()
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
FocusVisualStyle = null,
ShowsPreview = true,
Background = new SolidColorBrush(new Color() { R = 1, G = 1, B = 1, A = 1 }),
};
// non-style / essential window which will display over your WinForm control
var PopupWindowForSplitter = new PopupWindow()
{
Background = new SolidColorBrush(new Color() { R = 1, G = 1, B = 1, A = 1 }),
Visibility = Visibility.Collapsed
};
PopupWindowForSplitter.Show();
...
Point _ptForSplitterDrag = new Point(0,0);
splitter.DragStarted += (o, e) =>
{
var pt = splitter.PointToScreen(new Point());
_ptForSplitterDrag = splitter.PointToScreen(Mouse.GetPosition(splitter));
PopupWindowForSplitter.Left = pt.X;
PopupWindowForSplitter.Top = pt.Y;
PopupWindowForSplitter.Height = splitter.ActualHeight;
PopupWindowForSplitter.Width = splitter.ActualWidth;
PopupWindowForSplitter.Activate();
PopupWindowForSplitter.Visibility = Visibility.Visible;
};
splitter.DragDelta += (o, e) =>
{
var pt = splitter.PointToScreen(Mouse.GetPosition(splitter)) - _ptForSplitterDrag
+ splitter.PointToScreen(new Point());
if (splitter.ResizeDirection == GridResizeDirection.Rows)
{
PopupWindowForSplitter.Top = pt.Y;
}
else
{
PopupWindowForSplitter.Left = pt.X;
}
};
splitter.DragCompleted += (o, e) =>
{
var initializeData = typeof(GridSplitter).GetMethod("InitializeData", BindingFlags.NonPublic | BindingFlags.Instance);
var moveSplitter = typeof(GridSplitter).GetMethod("MoveSplitter", BindingFlags.NonPublic | BindingFlags.Instance);
if (moveSplitter != null && initializeData != null)
{
initializeData.Invoke(splitter, new object[] { true });
var pt = splitter.PointToScreen(Mouse.GetPosition(splitter)) - _ptForSplitterDrag;
if (splitter.ResizeDirection == GridResizeDirection.Rows)
{
moveSplitter.Invoke(splitter, new object[] { 0, pt.Y });
}
else
{
moveSplitter.Invoke(splitter, new object[] { pt.X, 0 });
}
}
PopupWindowForSplitter.Visibility = Visibility.Collapsed;
};
Maybe there are some issues in my description because of my poor english, but I think the code is enough to explain it.
Windows Forms controls are always rendered seperately from your WPF controls, and as a result will always appear over your WPF application.
See Hosting a Microsoft Win32 Window in WPF (subheading Notable Differences in Output Behavior) for more info.
Try using a WPF-native DataGrid control. There are a couple of commercial third-party controls you can buy, or you could take a look at one provided by Microsoft (currently still in CTP):
Xceed DataGrid
Telerik RadGridView
Microsoft DataGrid CTP
In your situation, the quickest fix would be to move the GirdSplitter to the Row with the Button:
<Grid>
<Grid.RowDefinitions>
<RowDefinition Name="rowForButton"/>
<RowDefinition Name="rowForGridSplitter" Height="Auto" MinHeight="81" />
</Grid.RowDefinitions>
<Button Grid.Row="0" Height="50" Width="110" Content="Button in First Row"/>
<my:WindowsFormsHost Panel.ZIndex="0" Grid.Row="1" Margin="30,11,138,0" x:Name="winHost" Height="58" VerticalAlignment="Top" OpacityMask="Transparent">
<win:DataGridView x:Name="dataGridView"></win:DataGridView>
</my:WindowsFormsHost>
<GridSplitter BorderThickness="1" Panel.ZIndex="1" Grid.Row="0" HorizontalAlignment="Stretch" Height="5" ShowsPreview="True" VerticalAlignment="Bottom">
</GridSplitter>
</Grid>
Now just adjust the margins to make sure there is some space between the button and the grid splitter.
The solution would be to add a 'Windows Form' label inside the grid splitter, and to do so programmatically after the addition of the DataGridView so it appears on top of it, as follows:
void AddLabelToSplitter()
{
string template =
#" <ControlTemplate
xmlns='http://schemas.microsoft.com/winfx/2006/xaml/presentation'
xmlns:mn='clr-namespace:MyNameSpace;assembly=MyAssembly'
xmlns:wf='clr-namespace:System.Windows.Forms;assembly=System.Windows.Forms'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml' TargetType='{x:Type GridSplitter}'>
<mn:ExtendedWindowsFormsHost x:Name='Grid_Splitter_WindowsFormsHost' HorizontalAlignment='Stretch' VerticalAlignment='Stretch'>
<wf:Label Dock='Fill' BackColor='DarkGray'></wf:Label>
</mn:ExtendedWindowsFormsHost>
</ControlTemplate>";
Grid_Splitter.Template = (ControlTemplate)XamlReader.Parse(template);
}
Using a regular windows form host would not work, as it wouldn't pass down the mouse events to the splitter, so use the ExtendedWindowsFormsHost instead from below link:
Keep Mouse Events bubbling from WindowsFormsHost on

Resources