Using GeometryDrawing in ItemsControl - c#

In my WPF application, I use an ObservableCollection of a class which contains PointCollection. This point collection is used to draw a polygon for each item in the observable collection. The code below show's how I use template binding to draw all the polys:
<Grid>
<ItemsControl ItemsSource="{Binding GeoLines}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type vm:GeoPointsViewModel}">
<Polygon Stroke="LightSkyBlue" StrokeThickness="0.5" Opacity="0.8">
<Polygon.Style>
<Style TargetType="{x:Type Polygon}">
<Setter Property="Points">
<Setter.Value>
<MultiBinding Converter="{StaticResource pointMultiConverter}">
<Binding Path="ActualWidth" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Canvas}}"/>
<Binding Path="ActualHeight" RelativeSource="{RelativeSource FindAncestor, AncestorType={x:Type Canvas}}"/>
<Binding Path="Points"/>
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Fill" Value="#FF0A0A10"/>
</Style>
</Polygon.Style>
</Polygon>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
Although it works perfectly, the performance isn't great since each polygon is a UIElement with all its bells and whistles. To improve performance, I'd like to use GeometryDrawing which has a lower footprint. Unfortunately, I've no idea how to do the data binding in that case. So, my question is how do I implement the code using GeometryDrawing (or whatever is more light weight than Shapes).

I'm not sure if using GeometryDrawing could considerably improve the performance but here's how you can implement it. The result may not look exactly what you want, that's because the default StartPoint of PathFigure:
<DataTemplate DataType="{x:Type vm:GeoPointsViewModel}">
<Border>
<Border.Background>
<DrawingBrush>
<DrawingBrush.Drawing>
<GeometryDrawing Brush="Red">
<GeometryDrawing.Geometry>
<PathGeometry>
<PathFigure IsClosed="True">
<PolyLineSegment Points="{Binding Points}"/>
</PathFigure>
</PathGeometry>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</Border.Background>
</Border>
</DataTemplate>
I used a simple Brush of Red for the polylines. You can also specify some Pen if you want. Note that the effect of polygon may not look exactly like what your original code produces. I guess you may have some more tweak to get it right.

I ended up using code behind to draw visuals directly. This bypassed a lot of UI layers and improved performance significantly. Any other suggestion to improve this code is still welcome.
private Brush geoBrush = (SolidColorBrush)(new BrushConverter().ConvertFrom("#FF0A0A10"));
private Pen geoPen = new Pen(Brushes.LightSkyBlue, 0.5);
private DropShadowEffect geoDropShadow = new DropShadowEffect
{
Color = Brushes.LightSteelBlue.Color,
BlurRadius = 8.0,
ShadowDepth = 0.0
};
private DrawingVisual GeoVisual = null;
private void UpdateGeoLines()
{
MapProjectionViewModel map = this.DataContext as MapProjectionViewModel;
if (map != null)
{
DrawingVisual visual = new DrawingVisual();
using (DrawingContext dc = visual.RenderOpen())
{
foreach (var item in map.GeoLines)
{
if (item.Points.Count > 1)
{
List<Point> points = new List<Point>();
foreach (var p in item.Points)
{
Point point = new Point(
p.X * canvas.ActualWidth,
p.Y * canvas.ActualHeight);
points.Add(point);
}
StreamGeometry geom = new StreamGeometry();
using (StreamGeometryContext gc = geom.Open())
{
Point p1 = points[0];
// Start new object, filled=true, closed=true
gc.BeginFigure(p1, true, true);
// isStroked=true, isSmoothJoin=true
gc.PolyLineTo(points, true, false);
}
geom.Freeze();
dc.DrawGeometry(geoBrush, geoPen, geom);
}
}
}
visual.Effect = geoDropShadow;
visual.Opacity = 0.8;
canvas.Visuals.Remove(GeoVisual);
canvas.Visuals.Add(visual);
GeoVisual = visual;
}
}

Related

How to create a view whose positions can be changed by accelerometer in Xamarin

I have tried the following method by testing out the data binding first before introducing in accelerometer:
Path.RenderTransform TransformGroup data binding is not working. As I click on the button, the label will change but nothing changes in the path. The path still changes shape if I give it an arbitrary number.
<Button Text="Increase Y Position" Command="{Binding IncreaseCommand}" />
<AbsoluteLayout>
<Path Fill="Blue"
Stroke="Red">
<Path.Data>
<EllipseGeometry
Center="{Binding CircleLocation}"
RadiusX="10"
RadiusY="10"/>
</Path.Data>
</Path>
</AbsoluteLayout>
<Path Stroke="Black"
Aspect="Uniform"
HorizontalOptions="Center"
HeightRequest="100"
WidthRequest="100"
Data="omitted for brievity">
<Path.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleY="{Binding YPosition}"/>
</TransformGroup>
</Path.RenderTransform>
</Path>
in view model
public AnimationViewModel()
{
IncreaseCommand = new Command(IncreaseCount);
}
public System.Windows.Input.ICommand IncreaseCommand { get; }
void IncreaseCount()
{
yPosition++ ;
OnPropertyChanged(nameof(DisplayCount));
OnPropertyChanged(nameof(YPosition));
}
public double DisplayCount => yPosition;
public double YPosition
{
get => yPosition;
set
{
if (yPosition == value)
return;
yPosition = value;
OnPropertyChanged(nameof(YPosition));
// OnPropertyChanged(nameof(RectParameter));
}
}
using TranslateTransform causes arbitrary number not working, either, even in the official sample. Binding did not work, either. Binding to a slider source work though, but that is not comptaible with accelerometer.
<TranslateTransform X="50"
Y="50" />
I had a similar issue. I was trying to bind a RotateTransform to a property in my ViewModel but nothing worked. I started with the following XAML. I created temporary properties/bindings for StrokeThickness and StrokeColor to confirm that binding to the ViewModel was working and hardcoding the angle in the XAML also worked.
<Path StrokeThickness="{Binding StrokeThickness}"
Stroke="{Binding StrokeColor}"
StrokeLineCap="Round"
Data="M194.5,500 V 280" >
<Path.RenderTransform>
<RotateTransform CenterX="194.5" CenterY="280" Angle="{Binding Direction}" />
</Path.RenderTransform>
</Path>
I tried a number of variations but I couldn't get the Angle to bind. Finally I created a new Transform property and created the transform in my ViewModel which worked successfully:
public Transform RotateTransform
{
get
{
return new RotateTransform(45, 194.5, 280);
}
}
With updated XAML:
<Path StrokeThickness="{Binding StrokeThickness}"
Stroke="{Binding StrokeColor}"
StrokeLineCap="Round"
Data="M194.5,500 V 280"
RenderTransform="{Binding RotateTransform}" />

How to apply Render Transform to mouse position

I'm making a ploting application. Because I want same behaviour as math plots I apply following transformation to canvas with data points:
<UserControl.Resources>
<TransformGroup x:Key="CanvasTransform">
<TranslateTransform X="30" Y="30"/>
<ScaleTransform ScaleX="1" ScaleY="-1" CenterX=".5" CenterY=".5" />
</TransformGroup>
</UserControl.Resources>
Here is the transformation used:
<ListBox>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<Canvas>
<Canvas.RenderTransformOrigin>
<Point X="0.5" Y="0.5"/>
</Canvas.RenderTransformOrigin>
<Canvas.RenderTransform>
<Binding Source="{StaticResource CanvasTransform}"/>
</Canvas.RenderTransform>
</Canvas>
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
So far so good. The problem is with adding points to plot. Because the mouse click event is returning position in window coordinates it's no use. The point is added at wrong location, because it's transformed after adding.
e.g. Canvas is 400 units high. I click in top left corner mouse location is [X=10, Y=10] this point is added to the plot and rendered. The render transform then use the [10,10] point and calculates it's new position: [X=40,Y=360] (window coordinates).
It means that I click in top corner and the point appears in bottom corner. Which is in fact correct behavior.
My question is, how to apply the render transform manually before storing the point, so the point will appear under mouse.
So far I have tried following:
var trans = Resources["CanvasTransform"] as TransformGroup;
var mouse = e.GetPosition(this); // mouse position relative to canvas
var newPoint = trans.Transform(mouse);
But after this transformation newPoint has following coordinates [40,-39]. Again I know why is the result as it is. The origin of the transform is [0,0] and the translation is 29 probably due to rounding error.
Now I can take this new point and manualy change the values - subtract 30 from X coord and then add Canvas.ActualHeight to Y coordinate, which will fix the position.
But then what's the point?
My question is: is it possible to apply the RenderTransform in same fashion as the rendere is doing it, to avoid fiddling with coordinates manualy?
CenterX=".5" CenterY=".5" in ScaleTransform is unnecessary. All it does is adds a tiny translate transform (half-pixel).
To get source position from transformed position, you need to use inverse transformation (Inverse property of Transform). This is where X-30 error comes from.
To change transformation origin, you need to, first, subtract half canvas size, then transform, then add half canvas size.
var origin = new Point(lstItems.ActualWidth / 2, lstItems.ActualHeight / 2);
var transform = ((TransformGroup)Resources["CanvasTransform"]).Clone();
transform.Children.Insert(0, new TranslateTransform(-origin.X, -origin.Y));
transform.Children.Add(new TranslateTransform(origin.X, origin.Y));
_transform = transform.Inverse;
Complete sample:
MainWindow.xaml
<Window x:Class="So21501609WpfMouseRenderTransform.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" SizeToContent="WidthAndHeight">
<Control.Resources>
<TransformGroup x:Key="CanvasTransform">
<TranslateTransform X="30" Y="30"/>
<ScaleTransform ScaleX="1" ScaleY="-1" CenterX=".5" CenterY=".5"/>
</TransformGroup>
<Style TargetType="TextBlock">
<Setter Property="Background" Value="SkyBlue"/>
</Style>
</Control.Resources>
<ItemsControl x:Name="lstItems" MouseDown="LstItems_OnMouseDown" Width="400" Height="400" Background="Transparent">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas>
<Canvas.RenderTransformOrigin>
<Point X="0.5" Y="0.5"/>
</Canvas.RenderTransformOrigin>
<Canvas.RenderTransform>
<Binding Source="{StaticResource CanvasTransform}"/>
</Canvas.RenderTransform>
</Canvas>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.Items>
<TextBlock Canvas.Left="10" Canvas.Top="10" Text="10 10"/>
<TextBlock Canvas.Left="10" Canvas.Top="300" Text="10 300"/>
<TextBlock Canvas.Left="300" Canvas.Top="300" Text="300 300"/>
<TextBlock Canvas.Left="300" Canvas.Top="10" Text="300 10"/>
</ItemsControl.Items>
</ItemsControl>
</Window>
MainWindow.xaml.cs
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace So21501609WpfMouseRenderTransform
{
public partial class MainWindow
{
private GeneralTransform _transform;
public MainWindow ()
{
InitializeComponent();
Loaded += OnLoaded;
}
private void OnLoaded (object sender, RoutedEventArgs routedEventArgs)
{
var origin = new Point(lstItems.ActualWidth / 2, lstItems.ActualHeight / 2);
var transform = ((TransformGroup)Resources["CanvasTransform"]).Clone();
transform.Children.Insert(0, new TranslateTransform(-origin.X, -origin.Y));
transform.Children.Add(new TranslateTransform(origin.X, origin.Y));
_transform = transform.Inverse;
}
private void LstItems_OnMouseDown (object sender, MouseButtonEventArgs e)
{
Point pos = _transform.Transform(e.GetPosition(lstItems));
var item = new TextBlock { Text = pos.ToString() };
Canvas.SetLeft(item, pos.X);
Canvas.SetTop(item, pos.Y);
lstItems.Items.Add(item);
}
}
}

Radial Sweeping of a Brush

I'm looking for a way to fill something using a radial sweep of a defined brush in WPF. I'm going to break down what I want in a series of images in an attempt to make it clear.
Let's say I define a brush in WPF that looks like so:
I then want to use a slice of that brush like so:
And tile it across the radius of a circle as shown:
Finally, in order to fill the shape, I would like to sweep the brush across all angles of the circle, providing me with a result similar to this:
In this particular case I'm attempting to make concentric circles. I know I could achieve this using RadialGradientBrush but this is an obnoxious solution as in order to precisely control the width of my concentric circles I would need change the number of radial stops based on the size of the circle. To make it worse, if the circle size changes, the radial stops will not change unless I use some kind of converter based on the circle width/height.
I was hoping for a clean solution to do this maybe with paths or something but any other suggestions for making circle slices of controlled size is welcome.
How about this to draw a few concentric circles by use of a Brush?
<Rectangle>
<Rectangle.Fill>
<DrawingBrush Stretch="Uniform">
<DrawingBrush.Drawing>
<GeometryDrawing>
<GeometryDrawing.Pen>
<Pen Brush="Black" Thickness="1"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Geometry>
<GeometryGroup>
<EllipseGeometry RadiusX="1" RadiusY="1"/>
<EllipseGeometry RadiusX="3" RadiusY="3"/>
<EllipseGeometry RadiusX="5" RadiusY="5"/>
<EllipseGeometry RadiusX="7" RadiusY="7"/>
<EllipseGeometry RadiusX="9" RadiusY="9"/>
<EllipseGeometry RadiusX="11" RadiusY="11"/>
</GeometryGroup>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</Rectangle.Fill>
</Rectangle>
Regarding the concentric circle case, combining Clemens's solution with a converter allows for precisely sized circles whose width can be changed dynamically and the amount of circles is set to as many will fit in the allowed area.
class SizeSpacingToCircleGroupConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
if (values == null) return values;
var input = values.OfType<double>().ToArray();
if (input.Length != 3) return values;
var width = input[0];
var height = input[1];
var spacing = input[2];
var halfSpacing = spacing / 2;
var diameter = width > height ? height : width;
var lineCount = (int)Math.Floor((diameter / (2 * spacing)) - 1);
if (lineCount <= 0) return values;
var circles = Enumerable.Range(0, lineCount).Select(i =>
{
var radius = halfSpacing + (i * spacing);
return new EllipseGeometry() { RadiusX = radius, RadiusY = radius };
}).ToArray();
var group = new GeometryGroup();
foreach (var circle in circles) group.Children.Add(circle);
return group;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotImplementedException();
}
}
And the XAML:
<Rectangle Height="{StaticResource Diameter}" Width="{StaticResource Diameter}">
<Rectangle.Fill>
<DrawingBrush Stretch="None">
<DrawingBrush.Drawing>
<GeometryDrawing>
<GeometryDrawing.Pen>
<Pen Brush="{StaticResource ForegroundBrush}" Thickness="{StaticResource SpacingDiv2}"/>
</GeometryDrawing.Pen>
<GeometryDrawing.Geometry>
<MultiBinding Converter="{StaticResource SizeSpacingToCircleGroupConverter}">
<Binding Source="{StaticResource Diameter}" />
<Binding Source="{StaticResource Diameter}" />
<Binding Source="{StaticResource Spacing}" />
</MultiBinding>
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingBrush.Drawing>
</DrawingBrush>
</Rectangle.Fill>
</Rectangle>
In my case I'm just using doubles defined in my resource dictionary but I could easily use a binding from a view model.
I still won't mark anything as the accepted answer however because the question was about a tiled radial sweep which could be useful for other reasons.

How to rotate an image within a StackPanel or Grid in Silverlight

I have an image control sits inside a Grid control. I already have a button to enable zoom-in to this image. After zoom-in, the Horizontal/vertical scroll bars are displayed. And then I rotate the image contained grid, the image and the grid scroll bar are messed up. How should I incorporate both zoom-in and rotate for the image control? The following are the code that I am using in my project.
The image control zoom-in code I used (x is the image control):
if ((x as Image) != null) { x.Height = x.Height * 1.3; x.Width = x.Width * 1.3; }
The rotation code I used (x is the image control):
if ((x as Image) != null)
{
RotateTransform rotate = new RotateTransform(); rotate.Angle = rotateAngle;
rotate.CenterX = x.Width / 2;
rotate.CenterY = x.Height / 2;
x.RenderTransform = rotate;
};
The XAML is:
<ScrollViewer x:Name="scrollViewer" Height="480" Width="615"
VerticalScrollBarVisibility="Auto"
HorizontalScrollBarVisibility="Auto">
<ScrollViewer.Content>
<Grid x:Name="ImageGrid">
<StackPanel x:Name="ImageStackPanel">
<Image Source="..." VerticalAlignment="Center" Width="220" Height="170" ></Image>
</StackPanel>
</Grid>
</ScrollViewer.Content>
</ScrollViewer>
Does anybody have any existing code snippet that I can borrow to resolve this trick?
I think you need to use TransformGroup to use more than one transform at the time:
ScaleTransform myScaleTransform = new ScaleTransform();
myScaleTransform.ScaleY = 3;
RotateTransform myRotateTransform = new RotateTransform();
myRotateTransform.Angle = 45;
// Create a TransformGroup to contain the transforms
// and add the transforms to it.
TransformGroup myTransformGroup = new TransformGroup();
myTransformGroup.Children.Add(myScaleTransform);
myTransformGroup.Children.Add(myRotateTransform);
// Associate the transforms to the image.
x.RenderTransform = myTransformGroup;
This may work for your needs:
<Image x:Name="image" Source="myImageSource" Stretch="Uniform"
HorizontalAlignment="Center" VerticalAlignment="Center"
RenderTransformOrigin="0.5, 0.5">
<Image.RenderTransform>
<TransformGroup>
<RotateTransform x:Name="Rotate"/>
<ScaleTransform x:Name="Scale" />
</TransformGroup>
</Image.RenderTransform>
</Image>
code behind:
Rotate.Angle = 45;
Scale = 0.25;
You may be missing the LayoutTransformer from the Silverlight Toolkit, and the AnimationMediator from one of the Toolkit developers.
With the LayoutTransformer you can set its content to anything, not just images, and apply any transformation with it, and as opposed to the usual RenderTransform, it will affect layout and actual sizes.
I have a similar scenario and I use it like this:
<Grid>
<fs:AnimationMediator x:Name="RotateMediator" LayoutTransformer="{Binding ElementName=LayoutTransformer}" AnimationValue="{Binding Angle, ElementName=RotateTransform, Mode=TwoWay}" />
<fs:AnimationMediator x:Name="ScaleXMediator" LayoutTransformer="{Binding ElementName=LayoutTransformer}" AnimationValue="{Binding ScaleX, ElementName=ScaleTransform, Mode=TwoWay}" />
<fs:AnimationMediator x:Name="ScaleYMediator" LayoutTransformer="{Binding ElementName=LayoutTransformer}" AnimationValue="{Binding ScaleY, ElementName=ScaleTransform, Mode=TwoWay}" />
<tkt:LayoutTransformer x:Name="LayoutTransformer" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<tkt:LayoutTransformer.LayoutTransform>
<TransformGroup>
<RotateTransform x:Name="RotateTransform" />
<ScaleTransform x:Name="ScaleTransform" />
</TransformGroup>
</tkt:LayoutTransformer.LayoutTransform>
<Image x:Name="MyImage" Source="mysource.png" Width="600" Height="800" />
</tkt:LayoutTransformer>
</Grid>
Because of the lack of MultiBinding you'd probably additionally have to manually handle the input value (from Slider controls etc) changed events and then set the AnimationValues of RotateMediator etc accordingly.

Rotate image (control) in Silverlight with Transform

I'm trying to rotate an image in Silverlight and can't seem to get it right. I've tried a few different ways so far and can't find the answer.
<Image Opacity=".5" x:Name="compassImg" Source="compass.png">
<Image.RenderTransform>
<RotateTransform x:Name="compassRotator"></RotateTransform>
</Image.RenderTransform>
</Image>
+
void compass_CurrentValueChanged(object sender, SensorReadingEventArgs<CompassReading> e)
{
Dispatcher.BeginInvoke(() =>
{
compassRotator.Angle = e.SensorReading.TrueHeading;
});
}
and
<Image Opacity=".5" x:Name="compassImg" Source="compass.png"></Image>
+
void compass_CurrentValueChanged(object sender, SensorReadingEventArgs<CompassReading> e)
{
Dispatcher.BeginInvoke(() =>
{
compassImg.RenderTransform = new CompositeTransform()
{
CenterX = 0.5,
CenterY = 0.5,
Rotation = e.SensorReading.TrueHeading
};
//OR (variations with 0.5 and width / 2 for both composite and rotate
compassImg.RenderTransform = new RotateTransform()
{
CenterX = compassImg.Width / 2,
CenterY = compassImg.Height / 2,
Angle = e.SensorReading.TrueHeading
};
});
}
It rotates, but it always rotates around 0/0. What am I doing wrong?
I looked up MSDN, and the second form is correct. http://msdn.microsoft.com/en-us/library/system.windows.media.rotatetransform.centerx.aspx (It is the coordinates, not fraction).
However, if you put a breakpoint where you apply the transform, you may find that Width is NaN. This is because width wasn't set. What you want is the ActualWidth.
One good way for exploration of transforms is to paste the following snippet into your XAML and experiment away.
<StackPanel HorizontalAlignment="Left">
<TextBlock>Center X</TextBlock>
<Slider
Name="RTX" Minimum="0.0" Maximum="116" />
<TextBlock>Center Y</TextBlock>
<Slider
Name="RTY" Minimum="0.0" Maximum="800"/>
<TextBlock>Angle</TextBlock>
<Slider
Name="Angle" Minimum="0.0" Maximum="360" />
</StackPanel>
<Image Source="{Binding ImagePath}" Name="image1">
<Image.RenderTransform>
<RotateTransform Angle="{Binding ElementName=Angle,Path=Value}"
CenterX="{Binding ElementName=RTX, Path=Value}"
CenterY="{Binding ElementName=RTY, Path=Value}"/>
</Image.RenderTransform>
</Image>
You need to set the RenderTransformOrigin Property to "0.5, 0.5", this will rotate the element around its centre.

Categories

Resources