I've got a working app that draws shapes and images on a zoomable canvas. I'm wondering about the effects of changing my current approach from using a global, transient settings object to using inherited attached properties.
To explain: A few aspects of the drawing (e.g. line widths, font sizes, etc) are user-configurable but are also affected by zoom level. I quickly found that merely binding, say, the StrokeThickness of my shapes to a configured value caused problems. If the user zoomed the canvas way in or out, the thickness changed. I wanted it to stay constant.
So I chose up with a solution that instead bound my shapes to a global, transient set of "Live" settings derived from the configured settings and the current zoom scale. The code-behind changes these live settings as the user zooms my canvas in or out.
private void UpdateScaledSizesAfterZoom()
{
// Get the scale from the canvas' scale transform.
if (!(Scene.LayoutTransform is ScaleTransform st))
return;
var div = st.ScaleX > 0 ? st.ScaleX : 1;
// Update all live settings with the new scale.
LiveSettings.LineWidth = Settings.LineWidth/ div;
LiveSettings.FontSize = Settings.FontSize / div;
}
Binding:
<Path StrokeThickness="{Binding Source={x:Static LiveSettings.Default}, Path=LineWidth}" Data=... blah blah blah .../>
This all works well enough but something about tying all my objects to a global object just plain bothers me. I can stay with it if I must but I wouldn't mind something cleaner.
So I wondered about an approach that used WPF Property Inheritance instead; I could, alternately register properties like this as inherited attached properties on my canvas ("ShapeCanvas"). Then my shapes could bind to "ShapeCanvas.LineWidth" and not need to rely on the existence of some global settings object.
However I might have many, many shapes. So I am wondering about how this might affect performance. How expensive is it for WPF to propagate a property like this through containment inheritance?
I've already debugged some of how attached properties work and it appears that when an attached inherited property changes, literally every item in the inheritance context gets notified about it. So it seems like this could be quite expensive indeed. I wouldn't want to make my zooming laggy or anything.
Does anyone have any experience with such issues? Is this something to be concerned about.
Here is a very simple example of how to transform the Geometry of a Path instead of the Path element itself, which avoids the need to re-scale its StrokeThickness:
<Window.Resources>
<MatrixTransform x:Key="GeometryTransform"/>
</Window.Resources>
<Canvas Background="Transparent" MouseWheel="Canvas_MouseWheel">
<Path Fill="Yellow" Stroke="Blue" StrokeThickness="3">
<Path.Data>
<PathGeometry Figures="M100,50 L150,100 100,150 50,100Z"
Transform="{StaticResource GeometryTransform}"/>
</Path.Data>
</Path>
</Canvas>
with this MouseWheel handler:
private void Canvas_MouseWheel(object sender, MouseWheelEventArgs e)
{
var transform = (MatrixTransform)Resources["GeometryTransform"];
var matrix = transform.Matrix;
var scale = e.Delta > 0 ? 1.1 : 1 / 1.1;
var pos = e.GetPosition((IInputElement)sender);
matrix.ScaleAt(scale, scale, pos.X, pos.Y);
transform.Matrix = matrix;
}
Related
The common question about positioning an element inside a Canvas is "How to position the center of element (instead of the top left corner)".
WPF: Resizing a circle, keeping the center point instead of TopLeft?
WPF Center Ellipse at X, Y
WPF element positioning on a Canvas
Several solutions are presented, but they all have drawbacks.
The easiest solution is to accommodate the element size during while setting the Canvas.Left and Canvas.Top properties programmatically. This works, but only once. This solution doesn't support bindings and it will break when the element size is changed. You also cannot set the Canvas.Left or Canvas.Top using
Another set of solutions involve translate transformations utilizing either RenderTransform or Margin. These solutions require binding some property to the -0.5 * Width or -0.5 * Height. Such binding requires creating a custom ValueConverter and is impossible to create using only XAML.
So, is there a simple way to position an element inside canvas so that its Canvas.Left and Canvas.Top correspond to the element's center and both size and position properties can be bound to some other properties?
XAML and bindings seem very powerful, but sometimes there are simple problems that require very complex solutions. In my bindings library creating such binding would be as easy as writing element.Center = position or element.TopLeft = position - element.Size / 2, but don't let me get carried away.
I've found a very simple solution which uses only XAML and supports binding both size and position properties of the element. It seems that when the WPF control with alignment set too Stretch or Center is placed inside the canvas, the element "gravitates" towards centering as the (Canvas.Left, Canvas.Top) point (the state that we desire), but is stopped by the "angle plate" placed at the same (Canvas.Left, Canvas.Top) point. How do I know about this "gravitation"? It's evident when you ease the block by setting the Margin of the element to a negative value. Setting the negative margin allows the element to move towards its center goal. The element moves until the Margin reaches (-Height / 2, -Width / 2) so that the element becomes perfectly centered at the (Canvas.Left, Canvas.Top) point. Further changes don't cause any movement since the element is already perfectly positioned.
Solution: set Margin="-1000000".
So in the following code the ellipses are both centered at the (200, 200) point. The first ellipse has Left and Top properties corresponding to the ellipse center allowing to easily bind them with some other objects' properties.
<Canvas>
<Ellipse Width="100" Height="100" Canvas.Left="200" Canvas.Top="200" Opacity="0.5" Fill="Red" Margin="-100000" />
<Ellipse Width="100" Height="100" Canvas.Left="150" Canvas.Top="150" Opacity="0.5" Fill="Blue" />
</Canvas>
The bad thing is this solution only work in WPF. Silverlight and WinRT don't have the described behavior.
Even simpler, at least for Shapes, would be to use a Path with an appropriate Geometry:
<Canvas>
<Path Canvas.Left="200" Canvas.Top="200" Fill="Red" Opacity="0.5">
<Path.Data>
<EllipseGeometry RadiusX="50" RadiusY="50"/>
</Path.Data>
</Path>
</Canvas>
I solved this by putting the canvas into the bottom-right cell of a 2x2 grid. That makes the 0,0 coordinate the center of the grid, and then you can do all your drawing relative to that.
This solution worked for me Center text at a given point on a WPF Canvas. He uses a MultiBinding and a custom Converter to adjust the margin on his element. Brilliant!
I am doing an application where I draw using PathFigure, LineSegment, ... stored as DrawingVisuals in a FrameworkElement.
I need to show the same drawing in 2 different UI elements (Grids or Panels, whatever...)
One will be used by the user to draw and the second will just allow to visualize the same drawing, zoom and scroll without affecting the 1st UI element viewport.
I will have more than 2000 DrawingVisuals, it would be stupid to duplicate them...
I am currently scratching my head to figure out the best way to do it.
What is, in your opinion, the right solution to achieve this?
More information:
I have tried the obvious way, with a simple XAML
<Grid>
<StackPanel>
<Border Name="B1" Background="Bisque" Width="400" Height="200"/>
<Border Name="B2" Background="Beige" Width="400" Height="200"/>
</StackPanel>
</Grid>
Then a simple code
var map = new VdMap();
B1.Child = map;
var elem = new ElementVisual(map);
elem.StartElement(20, 20);
elem.AddSegment(80, 60);
elem.AddSegment(10, 80);
elem.EndElement();
elem.Draw();
B2.Child = map;
VdMap is a FrameworkElement
ElementVisual, StartElement, AddElement are my internal functions. The important thing are:
B1.Child = map; //I attach my Map to the border
B2.Child = map; //I try to attach the same FrameworkElement to the second border.
And I get the run time error "Specified element is already the logical child of another element. Disconnect it first."
Well looks like it is going to be harder than I thought.
Ok,
I probably found the right way to do it: The VisualBrush and a Rectangle.
var mapClone = new VisualBrush { Visual = map };
rectangle = new Rectangle {
Width = 300, Height = 300,
Stroke = Brushes.Black, HorizontalAlignment = HorizontalAlignment.Left,
Fill = mapClone };
B2.Children.Add(rectangle);
I have now in B2 a visual copy of my map that I can scale and transform separately.
Any better idea is welcomed.
In the <ImageBrush/> element, there are AlignmentX and AlignmentY attributes with values Left/Center/Right and Top/Center/Bottom, respectively.
What I'm wanting to do is set my own value in, for example, AlignmentX either as a value or as another enumeration like AlignmentX="HalfCenter" where HalfLeft equals my own value (halfway between Center and Left). For example, if I have this:
<Rectangle Canvas.Left="0" Stroke="LimeGreen" StrokeThickness="16" Canvas.Top="0"
Width="400" Height="400" >
<Rectangle.Fill>
<ImageBrush ImageSource="newone.jpg"
Stretch="None" AlignmentX="HalfLeft" AlignmentY="Top" />
</Rectangle.Fill>
</Rectangle>
I don't know if this is a Dependency Property, Attached Property or otherwise (don't yet know how to create those). In the helpfile, it says in TileBrush.AlignmentXProperty field: Public Shared ReadOnly AlignmentXProperty As DependencyProperty. Does the ReadOnly word here mean that I can't set this property to a custom property?
If this can't be an override of that property, how can I create my own? I think this is an Attached Property and it could be called something different, like OffsetX and OffsetY that set an ImageBrush to a location inside its parent Shape. I'm getting very confused by the SL documentation on how I would do this though (almost no examples in VB.NET - but even the C# ones aren't all that revealing).
If it is possible, how would I get started on this?
Save yourself the pain and just use a value convertor and even that is going to be a little tricky, since you are going to have to apply a rendertransform or something to react to your enums.
You also could write your own panel which is probably a better idea.
You have a few different problems here to confront, creating the attached property, validating the enum, having the enum do what you want it to do when it is set.
Your also going to have to learn about MeasureOverride and ArrangeOverride
If you just can't help yourself ... Look Here
I'm creating a graph control. what i'm doing for adding x and y axis tally labels is i'm
adding a text block to each tally mark and show the value related to that tally mark.
but when i need to load data form the database and redraw the textbolcks again and refresh the graph area i can't remove the older textblocks they are still on the graph pane.
to overcome this problem i thought to put the text blocks in side a group box and when graph pane is redrawn to delete the group box elements and put them again..
is this approach correct?
please tell me how to put elements to groupbox in code behind class?
and please tell me if their is any other solution to my problem.
regards,
rangana.
In WPF there are many solutions to most problems. I will discuss three possible solutions to your problem - the one you describe and two others. You can decide which will work best for you.
Solution 1: Using TextBlock objects to disply the labels
It sounds like you have a Canvas and you're adding a TextBlock to it for each tick mark. This is a viable solution if performance isn't too critical and you can't use data binding.
There are two ways to remove the TextBlocks in this case:
You can keep a List<TextBlock> containing all the Textblocks list of the TextBlocks you created the last time you created the labels. Whenever you recreate the labels, run through this list and remove each TextBlock on the list from the containing panel (the Canvas)
You can create a new Canvas and put the TextBlocks on it, then delete the whole Canvas when you relabel.
Here is an example of the second technique, since it is slightly more efficient:
class MyGraphBuilder
{
Canvas _labelCanvas;
...
void AddLabels()
{
// Remove old label canvas, if any
if(_labelCanvas!=null)
_graphCanvas.Children.Remove(_labelCanvas);
// Add new label canvas
_labelCanvas = new Canvas();
_graphCanvas.Children.Add(_labelCanvas);
// Create labels
foreach(...)
{
...
_labelCanvas.Add(new TextBlock ...
}
...
}
}
Solution 2: Using data binding
In WPF you can create many graphs without writing a single line of code! WPF's built in data binding is sufficient to create relatively complex bar charts, etc.
Here is an example of using data binding to create a simple bar chart:
<ItemsControl ItemsSource="{Binding myData}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<DockPanel>
<TextBlock Width="50" Text="{Binding Label}"/>
<Rectangle VerticalAlignment="{Stretch}" Width="{Binding Value}">
<Rectangle.LayoutTransform>
<ScaleTransform ScaleX="10" /> <!-- Scale factor here, can be binding too -->
</Rectangle.LayoutTransform>
</Rectangle>
<TextBlock Text="{Binding Value}" FontSize="8"/>
</DockPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Numeric labels can be added to the horizontal axis by using a second ItemsControl laid out horizontally, and with its data template a fixed width and showing numbers and tick marks.
Solution 3: Using low level Drawing classes
Build your graph by constructing a DrawingGroup object and adding GeometryDrawing and GlyphRunDrawing objects to it, then putting the DrawingGroup inside DrawingVisual and add that to your main Panel.
You should use one GeometryDrawing or GlyphRunDrawing for each set of items sharing a given brush and pen. For example if your axes and tick marks are all the same color and width, create a single GeometryDrawing for all of them, but if each tick mark is a differnet color, create multiple GeometryDrawing objects.
You will create a Geometry object for each GeometryDrawing. For the best efficiency it should be a StreamGeometry, but the other Geometry classes also work well, may be easier to use, and may be initialized in XAML. Creating a PathGeometry or EllipseGeometry is probably already familar to you so I'll focus on creating a StreamGeometry. You do this by calling the Open method in a using() statement, then writing to the returned context. Here is an example:
Geometry BuildAxesAndTicksGeometry()
{
// First create geometry
var geometry = new StreamGeometry();
using(var context = geometry.Open())
{
// Horizontal axis
context.BeginFigure(new Point(0,0), false, false);
context.LineTo(new Point(_width, 0), true, false);
// Vertical axis
context.BeginFigure(new Point(0,0), false, false);
context.LineTo(new Point(0, _height), true, false);
// Horizontal ticks
for(int i=0; i<_nTicksHorizontal; i++)
{
context.BeginFiture(new Point(i * _tickSpacing, -10), false, false);
context.LineTo(new Point(i * _tickSpacing, 10), true, false);
}
// Do same for vertical ticks
}
// Now add it to a drawing
return new GeometryDrawing { Geometry = geometry, Stroke = _axisPen };
}
Drawing BuildDrawing()
{
var mainDrawing = new DrawingGroup();
mainDrawing.Add(BuildAxesAndTicksGeometry());
... // Add other drawings, including one or more for the data
return mainDrawing;
}
void UpdateDrawing()
{
myDrawingVisual.Drawing = BuildDrawing(); // where myDrawingVisual is defined in the XAML
}
Comparison of solutions
For most cases I would recommend solution 2 or 3, for these reasons:
If the graph is simple enough to use data binding it will save you a lot of time. Go with solution 2.
If the graph cannot be done with data binding, using Drawing objects is approximately as simple as any other technique, and can perform better. Go with solution 3.
In your case if you've already invested significant work into your Solution 1, you may want to stick with it even though it probably isn't the best.
Anyone know if it's possible to databind the ScaleX and ScaleY of a render transform in Silverlight 2 Beta 2? Binding transforms is possible in WPF - But I'm getting an error when setting up my binding in Silverlight through XAML. Perhaps it's possible to do it through code?
<Image Height="60" HorizontalAlignment="Right"
Margin="0,122,11,0" VerticalAlignment="Top" Width="60"
Source="Images/Fish128x128.png" Stretch="Fill"
RenderTransformOrigin="0.5,0.5" x:Name="fishImage">
<Image.RenderTransform>
<TransformGroup>
<ScaleTransform ScaleX="1" ScaleY="1"/>
<SkewTransform/>
<RotateTransform/>
<TranslateTransform/>
</TransformGroup>
</Image.RenderTransform>
</Image>
I want to bind the ScaleX and ScaleY of the ScaleTransform element.
I'm getting a runtime error when I try to bind against a double property on my data context:
Message="AG_E_PARSER_BAD_PROPERTY_VALUE [Line: 1570 Position: 108]"
My binding looks like this:
<ScaleTransform ScaleX="{Binding Path=SelectedDive.Visibility}"
ScaleY="{Binding Path=SelectedDive.Visibility}"/>
I have triple verified that the binding path is correct - I'm binding a slidebar against the same value and that works just fine...
Visibility is of type double and is a number between 0.0 and 30.0. I have a value converter that scales that number down to 0.5 and 1 - I want to scale the size of the fish depending on the clarity of the water. So I don't think it's a problem with the type I'm binding against...
ScaleTransform doesn't have a data context so most likely the binding is looking for SelectedDive.Visibility off it's self and not finding it. There is much in Silverlight xaml and databinding that is different from WPF...
Anyway to solve this you will want to set up the binding in code**, or manually listen for the PropertyChanged event of your data object and set the Scale in code behind.
I would choose the latter if you wanted to do an animation/storyboard for the scale change.
** i need to check but you may not be able to bind to it. as i recall if the RenderTransform is not part of an animation it gets turned into a matrix transform and all bets are off.
Is it a runtime error or compile-time, Jonas? Looking at the documentation, ScaleX and ScaleY are dependency properties, so you should be able to write
<ScaleTransform ScaleX="{Binding Foo}" ScaleY="{Binding Bar}" />
... where Foo and Bar are of the appropriate type.
Edit: Of course, that's the WPF documentation. I suppose it's possible that they've changed ScaleX and ScaleY to be standard properties rather than dependency properties in Silverlight. I'd love to hear more about the error you're seeing.
Ah I think I see your problem. You're attempting to bind a property of type Visibility (SelectedDive.Visibility) to a property of type Double (ScaleTransform.ScaleX). WPF/Silverlight can't convert between those two types.
What are you trying to accomplish? Maybe I can help you with the XAML. What is "SelectedDive" and what do you want to happen when its Visibility changes?
Sorry - was looking for the answer count to go up so I didn't realise you'd edited the question with more information.
OK, so Visibility is of type Double, so the binding should work in that regard.
As a workaround, could you try binding your ScaleX and ScaleY values directly to the slider control that SelectedDive.Visibility is bound to? Something like:
<ScaleTransform ScaleX="{Binding ElementName=slider1,Path=Value}" ... />
If that works then it'll at least get you going.
Edit: Ah, I just remembered that I read once that Silverlight doesn't support the ElementName syntax in bindings, so that might not work.
Yeah maybe the embedded render transforms aren't inheriting the DataContext from the object they apply to. Can you force the DataContext into them? For example, give the transform a name:
<ScaleTransform x:Name="myScaler" ... />
... and then in your code-behind:
myScaler.DataContext = fishImage.DataContext;
... so that the scaler definitely shares its DataContext with the Image.
Ok, is the Image itself picking up the DataContext properly?
Try adding this:
<Image Tooltip="{Binding SelectedDive.Visibility}" ... />
If that compiles and runs, hover over the image and see if it displays the right value.
I was hoping to solve this through XAML, but turns out Brian's suggestion was the way to go. I used Matt's suggestion to give the scale transform a name, so that I can access it from code. Then I hooked the value changed event of the slider, and manually updates the ScaleX and ScaleY property. I kept my value converter to convert from the visibility range (0-30m) to scale (0.5 to 1). The code looks like this:
private ScaleConverter converter;
public DiveLog()
{
InitializeComponent();
converter = new ScaleConverter();
visibilitySlider.ValueChanged += new
RoutedPropertyChangedEventHandler<double>(visibilitySlider_ValueChanged);
}
private void visibilitySlider_ValueChanged(object sender,
RoutedPropertyChangedEventArgs<double> e)
{
fishScale.ScaleX = (double)converter.Convert(e.NewValue,
typeof(double), null, CultureInfo.CurrentCulture);
fishScale.ScaleY = fishScale.ScaleX;
}