get XY of object in wpf for this situation? - wpf

I have a Canvas with two or more objects.
Now, I put these objects in a new Canvas placed in the previous Canvas. Then, I rotate it.
Now, I want to know how to get the positions of the objects in the new Canvas, as though there were no new canvas.

You can use the following extension method to get the location of one UIElement with respect to another:
public static class ExtensionMethods
{
public static Point GetRelativePosition(this UIElement element, UIElement other)
{
return element.TransformToVisual(other)
.Transform(new Point(0, 0));
}
}

Related

Custom WPF canvas with multiple underlying visuals

Found interesting article describing how to create custom canvas control by exposing methods Add and Remove for underlying visuals of the Panel class. This way I could create universal canvas that can accommodate absolutely any way to draw on it, GDI+, Canvas, Drawings, etc. For example, the first layer would be Bitmap, second Canvas, third DrawingVisual, etc.
For simplicity, I'd like to extend existing Canvas, so I could have original behavior provided by default Canvas control + could create as many additional Visuals as I want to.
Here is what I have now.
public class VisualCanvas : Canvas
{
protected IList<Visual> _visuals = null;
protected override int VisualChildrenCount => _visuals.Count;
protected override Visual GetVisualChild(int index) => _visuals.ElementAtOrDefault(index);
public VisualCanvas()
{
_visuals = new List<Visual>();
_visuals.Add(new DrawingVisual());
//(_visuals[0] as DrawingVisual).RenderOpen();
}
public void AddVisual(Visual visual)
{
_visuals.Add(visual);
base.AddVisualChild(visual);
base.AddLogicalChild(visual);
}
public void DeleteVisual(Visual visual)
{
_visuals.Remove(visual);
base.RemoveVisualChild(visual);
base.RemoveLogicalChild(visual);
}
}
Unfortunately, DrawingVisual that I add in the constructor doesn't make this control to act like original Canvas because 2 methods that I overridden seem to expect different kind of Visual, not DrawingVisual.
How do I make this control work like original Canvas?
Found similar question on MSDN. Appears to be it's not enough to override only 2 methods to keep all children in one list of visuals. Default canvas collection InternalChildren also needs to be taken into account.
This implementation seems to keep default canvas behavior + new visuals, but may have unpredictable behavior because 2 class collections share the same index. Looks like it would be easier to extend this class with new properties for each visual type rather than trying to override existing ones. Feel free to post better ideas and implementations.
public class VisualCanvas : Canvas
{
protected IList<Visual> _visuals = new List<Visual>();
protected override int VisualChildrenCount => _visuals.Count + InternalChildren.Count;
protected override Visual GetVisualChild(int index) => _visuals.ElementAtOrDefault(index) ?? InternalChildren[index - _visuals.Count];
public void AddVisual(Visual visual)
{
_visuals.Add(visual);
base.AddVisualChild(visual);
base.AddLogicalChild(visual);
}
public void DeleteVisual(Visual visual)
{
_visuals.Remove(visual);
base.RemoveVisualChild(visual);
base.RemoveLogicalChild(visual);
}
}

WPF Control moves but its Adorner - Not :"/

I created an adorner on a WPF line element, because there was neet to add some text.
Now, when this line is moved, the adorner does not "follow" the line automatically. In fact, it does not refresh itsef:
here black curves is the Control drawing, and the red "120 m" is the adorner one.
Some code
void SegmentLine_Loaded(object sender, RoutedEventArgs e)
{
AdornerLayer aLayer = AdornerLayer.GetAdornerLayer(this);
if (aLayer != null)
{
aLayer.Add(new TextAdorner(this));
}
}
class TextAdorner : Adorner
{
public TextAdorner(UIElement adornedElement)
: base(adornedElement)
{
}
protected override void OnRender(DrawingContext drawingContext)
{
SegmentLine segment = (this.AdornedElement as SegmentLine);
if (segment != null)
{
Rect segmentBounds = new Rect(segment.DesiredSize);
var midPoint = new Point(
(segment.X1 + segment.X2) / 2.0,
(segment.Y1 + segment.Y2) / 2.0);
var lineFont = // get line font as Font
FormattedText ft = new FormattedText(
string.Format("{0} m", segment.Distance),
Thread.CurrentThread.CurrentCulture,
System.Windows.FlowDirection.LeftToRight,
new Typeface(lineFont.FontFamily.ToString()),
ligneFont.Size, Brushes.Red);
drawingContext.DrawText(ft, midPoint);
}
}
}
Why MeasureOverride, etc aren't being called
Your adorner's MeasureOverride, ArrangeOverride, and OnRender aren't being called because your SegmentLine control is never changing size or position:
Since your SegmentLine doesn't implement MeasureOverride, it always has the default size assigned by the layout engine.
Since your SegmentLine doesn't implement ArrangeOverride or manipulate any transforms, its position is always exactly the upper-left corner of the container.
The Adorner's MeasureOverride, ArrangeOverride and OnRender are only called by WPF under these conditions:
The AdornedElement changes size or position (this the most common case), or
One of the Adorner's properties chagnes and that property is marked AffectsMeasure, AffectsArrange, or AffectsRender, or
You call InvalidateMeasure(), InvalidateArrange(), or InvalidateVisuaul() on the adorner.
Because your SegmentLine never changes size or position, case 1 doesn't apply. Since you don't have any such properties on the Adorner and don't call InvalidateMeasure(), InvalidateArrange() or InvalidateVisual(), the other cases don't apply either.
Precise rules for Adorner re-measure
Here are the precise rules for when an adorned element change triggers a call to Adorner.MeasureOverride:
The adorned element must force a layout pass by invalidating its Measure or Arrange in response to some event. This could be triggered automatically by a change to a DependencyProperty with AffectsMeasure or AffectsArrange, or by a direct call to InvalidateMeasure(), InvalidateArrange() or InvalidateVisual().
The adorned element's Measure and Arrange methods must not be called directly from user code between the invalidation and the layout pass. In other words, you must wait for the layout manager to do the job.
The adorned element must make a non-trivial change to either its RenderSize or its Transform.
The combination of all transforms between the AdornerLayer and the adorned element must be affine. This will generally be the case as long as you are not using 3D.
Your SegmentLine is just drawing the line in a new place rather than updating its own dimensions, thereby omitting my requirement #3 above.
Recommendation
Normally I would recommend your adorner have AffectsRender DependencyProperties bound to the SegmentLine's properties, so any time X1, Y1, etc change in the SegmentLine they are also updated in the Adorner which causes the Adorner to re-render. This provides a very clean interface, since the adorner can be used on any control that has properties X1, Y1, etc, but it is less efficient than tightly coupling them.
In your case the adorner is clearly tightly bound to your SegmentLine, so I think it makes just as much sense to call InvalidateVisual() on the adorner from the SegmentLine's OnRender(), like this:
public class SegmentLine : Shape
{
TextAdorner adorner;
...
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
if(adorner==null)
{
var layer = AdornerLayer.GetAdornerLayer(this); if(layer==null) return;
adorner = new TextAdorner(this);
... set other adorner properties and events ...
layer.Add(adorner);
}
adorner.InvalidateVisual();
}
}
Note that this doesn't deal with the situation where the SegmentLine is removed from the visual tree and then added again later. Your original code doesn't deal with this either, so I avoided the complexity of dealing with that case. If you need that to work, do this instead:
public class SegmentLine : Shape
{
AdornerLayer lastLayer;
TextAdorner adorner;
...
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
var layer = AdornerLayer.GetAdornerLayer(this);
if(layer!=lastLayer)
{
if(adorner==null)
{
adorner = new TextAdorner(this);
... set other adorner properties and events ...
}
if(lastLayer!=null) lastLayer.Remove(adorner);
if(layer!=null) layer.Add(adorner);
lastLayer = layer;
}
adorner.InvalidateVisual();
}
}
How is the line being moved? Does the MeasureOverride or ArrangeOverride of the adorner get invoked after the move? OnRender will only get invoked if the visual is invalidated (e.g. invalidatevisual) so I'm guessing that the render isn't being invalidated.
May be you wanted to use segmentBounds to define midPoint? Otherwise what is it doing there? Looks like you are defining midPoint relative to not rerendered segment.
idiot fix, but it works
AdornerLayer aLayer;
void SegmentLine_Loaded(object sender, RoutedEventArgs e)
{
aLayer = AdornerLayer.GetAdornerLayer(this);
if (aLayer != null)
{
aLayer.Add(new TextAdorner(this));
}
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
if (aLayer != null)
{
aLayer.Update();
}
}
Now, the problem is that when I click on a the adorner the control itself does not recieve the hit...

problem with ContainerVisual.Transform

in my custom control i have a ContainerVisual object and a DrawingVisual under it.
I override ArrangeOverride and calculate the rectangle that i want to draw in based on the given size and the control's padding.
after that i set my ContainerVisual object's transform to the upper left corner of the rectangle so that the methods that render the drawing would not have to take account of the rectangle and assume that the drawing origin is at point 0,0.
this does not work, and the drawing is displaced. if instead i set transform of the DrawingVisual object it works and the rectangle is displayed the way it is supposed to be.
i thought that if i set transform on the container, it will automatically be applied to the visuals under it. is that so?
thanks for any help
EDIT: Updated the source code to show complete code.
class MyControl : Control
{
private readonly ContainerVisual container = new ContainerVisual();
private readonly DrawingVisual drawing = new DrawingVisual();
private Rect rect;
private void RenderDrawing()
{
using (var c = drawing.RenderOpen())
{
var p = new Pen(new SolidColorBrush(Colors.Black), 1);
c.DrawRectangle(null, p, new Rect(0, 0, rect.Width, rect.Height));
}
}
protected override Size ArrangeOverride(Size s)
{
var h = Math.Max(0, s.Height - Padding.Top - Padding.Bottom);
var w = Math.Max(0, s.Width - Padding.Left - Padding.Right);
var r = new Rect(Padding.Left, Padding.Top, w, h);
if (rect != r)
{
rect = r;
container.Clip = new RectangleGeometry(rect);
container.Transform = new TranslateTransform(rect.Left, rect.Top);
// replace the line above with the following line to make it work
// drawing.Transform = new TranslateTransform(rect.Left, rect.Top);
RenderDrawing();
}
return s;
}
protected override Visual GetVisualChild(int index)
{
return container;
}
protected override Size MeasureOverride(Size s)
{
return new Size();
}
protected override int VisualChildrenCount
{
get { return 1; }
}
public MyControl()
{
container.Children.Add(drawing);
AddVisualChild(container);
}
}
<Window x:Class="MyApp.MyWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:c="clr-namespace:MyApp"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid>
<c:MyControl Padding="20" />
</Grid>
</Window>
Explanation of strange clipping behavior
Now that you have posted your full source code I was finally able to see what you were seeing. Your problem isn't in the transform at all: It is in the clip!
If you comment out the container.Clip assignment statement, you get identical results no matter whether you put the transform on container or drawing
If you uncommented container.Clip assignment statement, the clipping region is perfectly centered on when the drawing is transformed, but when the container is transformed the clipping area is offset, so that only the lower and right lines of the rectangle were visible (and not all of those)
The reason this occurs is that the geometry specified for container.Clip is part of the container, so it is affected by container.Transform but not drawing.Transform:
This can be better understood by looking at the upper-left corners of the container, drawing, rectangle, and clip area relative to the upper-left corner of the window:
When you set the transform on the drawing:
Container is at (0,0) relative to window (null transform)
Clip area is at (20,20) relative to window (null transform + RectangleGeometry)
Drawing is at (20,20) relative to window (null transform + TranslateTransform)
Rectangle is at (20,20) relative to window (null transform + TranslateTransform + 0,0)
When you set the transform on the container:
Container is at (20,20) relative to window (TranslateTransform)
Clip area is at (40,40) relative to window (TranslateTransform + RectangleGeometry)
Drawing is at (20,20) relative to window (TranslateTransform + null transform)
Rectangle is at (20,20) relative to window (TranslateTransform + null transform + 0,0)
So your problem isn't that the transform isn't happening: It is that the transform is moving the clip area too, so the clip area no longer coincides with the rectangle and you can only see two sides of the rectangle.
Answer given for original code (retained because it has some useful explanation)
In fact, the code you posted never uses "container" so all you will see is a blank screen.
In your actual code you are using "container" incorrectly, preventing the events from occurring in the correct sequence to cause its Transform to be picked up and passed to the MIL layer.
Remember that when a Visual has a Transform set, it is not the visual itself but that Visual's visual parent that actually handles that transform. For example, if you render a page to XPS using ReachFramework or do hit testing, the Transform on the outermost Visual is ignored.
Your understanding is correct: If your visual tree is built following all the rules, it doesn't matter whether your transform is on your "container" or your "drawing".
Since you are using Control anyway, I'm curious why you don't just let the normal UIElement-based layout system handle your layout needs.
First update (retained for the same reason)
Thanks for the code correction. It is as I suspected: You are building your visual tree incorrectly. If you are using AddVisualChild you also must also override GetVisualChild and VisuaChildrenCount. This is because Visual does not store a list of children: It is up to the subclass (your class) to do this. What is happening is:
When you call AddVisualChild the container's transform is null so that is what is passed down to MILCore.
Later when you change the container's transform, it uses its parent pointer (that was set in AddVisualChild) to signal that its transform data must be refreshed. This update requires part of the visual tree to be scanned using GetVisualChild and VisualChildrenCount.
Since you didn't implement these methods this part of the update fails.
You say you are "new to WPF." Are you aware that you are playing with some of WPF's most low-level and esoteric features, ones that would never be used in a most ordinary WPF applications? It is equivalent to starting to learn programming using machine language. Normally you would use templates with Path, Rectangle, etc for this purpose. Sometimes you might go lower level and use a DrawingBrush with a DrawingGroup containing GeometryDrawings, etc. But you would almost never go all the way down to DrawingVisual and RenderOpen! The only time you would do that is when you have huge drawings consisting of millions of individual items and so you want to bypass all the layout and structure overhead of the higher layers for absolute maximum performance.
Manipulating the visual tree yourself (AddVisualChild, etc) is also an advanced feature. I always recommend people new to WPF stick with UIElement and above for the first few months, using Control with templates. I recommend they use Path and other shape subclasses for their drawings, and use VisualBrushes when advanced drawing effects are needed.
Hope this helps.
the problem is with the container.Clip. it should be
container.Clip = new RectangleGeometry(new Rect(0, 0, w, h));

Removing the render transform of a child element

Ok,
So I have a situation, where an border is being scaled (sometimes by a large amount) and translated. Inside the border is a grid, and inside the grid are two images, one is a photo and is stretched to the size of the border, and the other, I intend on being an icon, which needs to be a fixed size in the bottom left hand corner.
The problem is, that I want to remove the effect scaling is having on the icon. This is because I've given the icon a fixed size and would like it to remain that size, but unfortunately the scaling from the border is propagating down the the children of the border and effecting them also.
So I've tried using an attached property, similar to this pixel snapping artical (http://blogs.msdn.com/b/devdave/archive/2008/06/22/using-an-attached-dependencyproperty-to-implement-pixel-snapping-as-an-attached-behavior.aspx), but it doesn't seem to make a difference. When steped through, the elements which are being modified in LayoutUpdate always seem to have the identity matrix for the render transform anyway, before I've set it.
I guess I'm miss-interperating how render transforms are applied to children maybe?
Anyway, this is what I have (Also, I know this (if it worked) would remove translation too, which isn't what I want!):
public static readonly DependencyProperty IsConstantSizeProperty =
DependencyProperty.RegisterAttached(
"ConstantWidth",
typeof(bool),
typeof(ItemsControlEX),
new PropertyMetadata(new PropertyChangedCallback(IsConstantSizeChanged)));
private static List<FrameworkElement> m_constSizeObjects = new List<FrameworkElement>();
private static void IsConstantSizeChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
{
bool isConstantWidth = (bool)args.NewValue;
if (isConstantWidth)
{
FrameworkElement el = (FrameworkElement)obj;
m_constSizeObjects.Add(el);
el.LayoutUpdated += new EventHandler(el_LayoutUpdated);
el.Unloaded += new RoutedEventHandler(el_Unloaded);
}
}
static void el_Unloaded(object sender, RoutedEventArgs e)
{
FrameworkElement el = (FrameworkElement)sender;
el.Unloaded -= new RoutedEventHandler(el_Unloaded);
el.LayoutUpdated -= new EventHandler(el_LayoutUpdated);
m_constSizeObjects.Remove(el);
}
static void el_LayoutUpdated(object sender, EventArgs e)
{
foreach (FrameworkElement el in m_constSizeObjects)
{
MatrixTransform trans = new MatrixTransform();
trans.Matrix = Matrix.Identity;
el.RenderTransform = trans;
}
}
public static void SetIsConstantWidth(UIElement element, Boolean value)
{
element.SetValue(IsConstantSizeProperty, value);
}
public static Boolean GetIsConstantWidth(UIElement element)
{
return (Boolean)element.GetValue(IsConstantSizeProperty);
}
I'm thinking I'm probably thinking about this in completely the wrong way maybe. I guess the sensible solution would be to refactor to remove the need for scaling, but I guess I was just after a quicker solution that I can use until I have time.
Any help is appreciated! :)
Thanks!
Andy.
If you are only scaling (I assume fixed aspect ratio) that seems overly complicated, why not place the photo in a ViewBox container? Place the ViewBox (containing the photo) and the icon (in that order) in a parent grid.
Make the icon relative to the bottom
left using alignment and margin
settings
Resize the viewbox to scale your image.
The grid will shrink to fit the viewbox size. The icon will remain relative to the grid bottom-left.
Your pixel snapping behaviour should work on a ViewBox.
If you need a specific example, please provide some of your Xaml to work from.

WPF 2010 Static map

I am creating a WPF application in .net 4.0.
Basically I want to have a map of the world (2d) where by I can place images on, depending on their country location. This map can be an image however, it would be hard to determine the co-ordinates for the location right?
Can someone provide me with the simplest way of doing this?
I would do this by placing the image/shape object in Canvas Panel (panel with coordinates system) also I would add a wpf toolkit zoomviewer to get nice panning and zooming of the map.
Then I would make a Dictionary where key is a name of the country and value is a Point where pictures should be placed.
There are many solutions to this, all depending on your design (and tastes).
In all cases, the thing to remember is that the placement of the items on the map will always be relative to the size of the map, and will need to be recalculated whenever the size of the background map is changed. This calculation will be depend on the map itself (i.e. is it a rectangle or round, is the latitude/longitude a fixed and equal grid, the layout of the map, etc.).
One possible way to set up the main window is something like this:
This assumes that there is a folder called "Resources" that contains an image called "world-map.jpg", with its Build Action set to Resource.
Then, in your code behind, you would have to have something that actually adds a new image to the map in the appropriate place.
Personally, I would probably create a class (perhaps a custom or user control) to hold the image information, including the latitude/longitude, an Image object, etc. The main window (or ViewModel) would have a collection of these objects, which would make it easier to put them in the right places when a resize occurred. For example:
public partial class Window1 : Window
{
ObservableCollection<MyMapImageClass> mapImages = new ObservableCollection<MyMapImageClass>();
public Window1()
{
InitializeComponent();
layout.SizeChanged += new SizeChangedEventHandler(layout_SizeChanged);
}
void layout_SizeChanged(object sender, SizeChangedEventArgs e)
{
foreach (MyMapImageClass mapImage in mapImages)
{
Point point = CalculateImagePosition(mapImage.latitude, mapImage.longitude);
mapImage.Image.SetValue(Canvas.LeftProperty, point.X);
mapImage.Image.SetValue(Canvas.TopProperty, point.Y);
}
}
Point CalculateImagePlacement(double latitude, double longitude)
{
Point point = new Point();
// Do the calculations appropriate to your map
point.X = LongCalculation(longitude);
point.Y = LatCalculation(latitude);
}
void PlaceImage(double latitude, double longitude)
{
Image img = new Image();
ImageSourceConverter converter = new ImageSourceConverter();
string path = "pack://application:,,,/Resources/SomeImage.png";
ImageSource source = (ImageSource)converter.ConvertFromString(path);
img.Source = source;
img.Width = 10d;
Point point = CalculateImagePlacement(latitude, longitude);
img.SetValue(Canvas.LeftProperty, point.X);
img.SetValue(Canvas.TopProperty, point.Y);
layout.Children.Add(img);
MyMapImageClass mapImage = new MyMapImageClass();
mapImage.Latitude = latitude;
mapImage.Longitude = longitude;
mapImage.Image = img;
mapImages.Add(mapImage);
}
}

Resources