I have a Canvas setup inside an ItemsControl, which I am populating with buttons. Each button has an X and Y position property which is used to position the button on the canvas.
What im trying to do is make the item positions relative to the size of the canvas. My problem is when I resize the canvas, the button ViewModels are not getting the NotifyPropertyChanged call, so their positions never update.
<ItemsControl ItemsSource="{Binding Buttons}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Top" Value="{Binding PosY, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
<Setter Property="Canvas.Left" Value="{Binding PosX, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"/>
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:ToggleButton}">
<local:ToggleButton DataContext="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
in the ToggleButtonViewModel:
public double PosY {
get {
return (_Button.PositionVertical * Size[1]);
}
}
public double PosX {
get { return (_Button.PositionHorizontal * Size[0]); }
}
Size is a double[] with the X and Y dimensions of the canvas. I've tried storing it in the CanvasViewModel, in which case I can keep it updated with a SizeChanged event on the canvas, but then I couldnt figure out how to update the PosX and PosY in the ToggleButtonViewModel when the size changes.
I also tried storing the size as a static parameter in all of the ToggleButtonViewModels, but i couldnt use a NotifyPropertyChanged with a static parameter.
Any ideas how I could get the PosX and PosY to update when the size of the canvas changes?
I think the way to go here, would be to implement a custom Panel which overrides your Arrange Logic, just like Clemens suggested, and add some attached Properties for the Positioning.
I made a really quick small solution for it and it seems to work like you want it to.
Its a simple Panel "RelativePositionCanvas", that supplies two attached Properties "RelativeX" and "RelativeY" which are double values which usually range from 0 to 1, and are used for setting the position of the top left corner, relative to the panel's size.
If you want to add more like aligning it to the right side, or making the size relative, you would just have to add some additional Attached Properties for those and use them in the ArrangeOverride method.
public class RelativePositionCanvas : Panel
{
#region Properties
#region RelativeX
public static readonly DependencyProperty RelativeXProperty =
DependencyProperty.RegisterAttached(
"RelativeX",
typeof(double),
typeof(RelativePositionCanvas),
new FrameworkPropertyMetadata(
0.0d,
new PropertyChangedCallback(OnPositioningChanged)));
public static double GetRelativeX(DependencyObject d)
{
return (double)d.GetValue(RelativeXProperty);
}
public static void SetRelativeX(DependencyObject d, double value)
{
d.SetValue(RelativeXProperty, value);
}
#endregion
#region RelativeY
public static readonly DependencyProperty RelativeYProperty =
DependencyProperty.RegisterAttached(
"RelativeY",
typeof(double),
typeof(RelativePositionCanvas),
new FrameworkPropertyMetadata(
0.0d,
new PropertyChangedCallback(OnPositioningChanged)));
public static double GetRelativeY(DependencyObject d)
{
return (double)d.GetValue(RelativeYProperty);
}
public static void SetRelativeY(DependencyObject d, double value)
{
d.SetValue(RelativeYProperty, value);
}
#endregion
private static void OnPositioningChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
UIElement uie = d as UIElement;
if (uie != null)
{
RelativePositionCanvas p = VisualTreeHelper.GetParent(uie) as RelativePositionCanvas;
if (p != null)
p.InvalidateArrange();
}
}
#endregion
protected override Size MeasureOverride(Size availableSize)
{
Size childConstraint = new Size(Double.PositiveInfinity, Double.PositiveInfinity);
foreach (UIElement child in InternalChildren)
{
if (child == null) { continue; }
child.Measure(childConstraint);
}
return new Size();
}
protected override Size ArrangeOverride(Size arrangeSize)
{
var children = this.Children;
foreach(UIElement child in children)
{
if (child == null) { continue; }
var childRelativeX = GetRelativeX(child);
var childRelativeY = GetRelativeY(child);
var posX = childRelativeX * arrangeSize.Width;
var posY = childRelativeY * arrangeSize.Height;
child.Arrange(new Rect(new Point(posX, posY), child.DesiredSize));
}
return arrangeSize;
}
}
To use it you would have to replace the ItemsPanelTemplate by the RelativePositionCanvas, and the Canvas.Top, and Canvas.Left properties by the RelativePositionCanvas.RelativeY and RelativePositionCanvas.RelativeX properties and should be fine to go
Option1) In the ToggleButtonViewModel, change your definitions of PosX and PosY to be "normal" WPF notifying properties (I assume you know what I mean) which notify when changed. When the values in Size change, set the properties on your ViewModel. For example:
_viewModel.PosY = _Button.PositionVertical * Size[1];
_viewModel.PosX = _Button.PositionHorizontal * Size[0];
This gives a nice separation of concerns; but may not be possible, depending on where you are holding what information.
Option2) Add a method to your ViewModel which you can call to indicate that Size has changed, within which you can:
RaiseNotifyPropertyChanged(nameof(PosX));
RaiseNotifyPropertyChanged(nameof(PosY));
(I can give you an implementation of RaiseNotifyPropertyChanged if you need, but it's pretty standard WPF boilerplate code that you're likely to already have in your ViewModel.)
Related
Simple xaml:
<WrapPanel Orientation="Vertical">
<Ellipse Width="100" Height="100" Fill="Red" />
<Ellipse Width="100" Height="100" Fill="Yellow" />
<Ellipse Width="100" Height="100" Fill="Green" />
</WrapPanel>
Resizing window:
How to show vertical and horizontal scrollbars when content doesn't fit?
Note: this should work for any content.
I tried to put it into ScrollViewer:
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<WrapPanel Orientation="Vertical">
...
</WrapPanel>
</ScrollViewer>
but then WrapPanel stops wrapping anything (always one column):
The problem here is that ScrollViewer gives (NaN, NaN) size to it child, so wrapping never occurs.
I tried to fix it by binding scroll viewer available height to max height of panel:
<ScrollViewer ...>
<WrapPanel MaxHeight="{Binding ViewportHeight, RelativeSource={RelativeSource AncestorType=ScrollViewer}}" ...>
This will limit panel height (not NaN anymore), so wrapping now occurs. But because this also adjust the height of panel - the vertical scrollbar will never appears:
How to add vertical scrollbar?
In my case WrapPanel is vertical, means it will fill columns as much as it can and then wrap to a new column from left to right. Scrollbars are needed when children doesn't fit either vertically (when available space is less than children height) or horizontally.
The idea thought can be used for a standard (horizontal) WrapPanel: from left to right, creating new rows when full. Absolutely same problem will arise (just tried it).
That sort of behavior is not possible with a WrapPanel without setting explicitly its Height/MinHeight for a Vertical orientation or Width/MinWidth for a Horizontal orientation. The ScrollViewer will only show the scrollbars when the FrameworkElement this scroll viewer wraps doesn't fit into the viewport.
You can create your own wrap panel that calculates its minimum size based on its children.
Alternatively, you can implement a Behavior<WrapPanel> or an attached property. This won't be as easy as just adding a couple of XAML tags, as you might expect.
We have solved this issue with an attached property. Let me give you an idea of what we did.
static class ScrollableWrapPanel
{
public static readonly DependencyProperty IsEnabledProperty =
DependencyProperty.RegisterAttached("IsEnabled", typeof(bool), typeof(ScrollableWrapPanel), new PropertyMetadata(false, IsEnabledChanged));
// DP Get/Set static methods omitted
static void IsEnabledChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var panel = (WrapPanel)d;
if (!panel.IsInitialized)
{
panel.Initialized += PanelInitialized;
}
// Add here the IsEnabled == false logic, if you wish
}
static void PanelInitialized(object sender, EventArgs e)
{
var panel = (WrapPanel)sender;
// Monitor the Orientation property.
// There is no OrientationChanged event, so we use the DP tools.
DependencyPropertyDescriptor.FromProperty(
WrapPanel.OrientationProperty,
typeof(WrapPanel))
.AddValueChanged(panel, OrientationChanged);
panel.Unloaded += PanelUnloaded;
// Sets up our custom behavior for the first time
OrientationChanged(panel, EventArgs.Empty);
}
static void OrientationChanged(object sender, EventArgs e)
{
var panel = (WrapPanel)sender;
if (panel.Orientation == Orientation.Vertical)
{
// We might have set it for the Horizontal orientation
BindingOperations.ClearBinding(panel, WrapPanel.MinWidthProperty);
// This multi-binding monitors the heights of the children
// and returns the maximum height.
var converter = new MaxValueConverter();
var minHeightBiding = new MultiBinding { Converter = converter };
foreach (var child in panel.Children.OfType<FrameworkElement>())
{
minHeightBiding.Bindings.Add(new Binding("ActualHeight") { Mode = BindingMode.OneWay, Source = child });
}
BindingOperations.SetBinding(panel, WrapPanel.MinHeightProperty, minHeightBiding);
// We might have set it for the Horizontal orientation
BindingOperations.ClearBinding(panel, WrapPanel.WidthProperty);
// We have to define the wrap panel's height for the vertical orientation
var binding = new Binding("ViewportHeight")
{
RelativeSource = new RelativeSource { Mode = RelativeSourceMode.FindAncestor, AncestorType = typeof(ScrollViewer)}
};
BindingOperations.SetBinding(panel, WrapPanel.HeightProperty, binding);
}
else
{
// The "transposed" case for the horizontal wrap panel
}
}
static void PanelUnloaded(object sender, RoutedEventArgs e)
{
var panel = (WrapPanel)sender;
panel.Unloaded -= PanelUnloaded;
// This is really important to prevent the memory leaks.
DependencyPropertyDescriptor.FromProperty(WrapPanel.OrientationProperty, typeof(WrapPanel))
.RemoveValueChanged(panel, OrientationChanged);
}
private class MaxValueConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
return values.Cast<double>().Max();
}
// ConvertBack omitted
}
}
It is maybe not the easiest way, and there are a little bit more lines that just a few XAML tags, but it works flawlessly.
You have to be careful with the error handling though. I've just omitted all the checks and exception handling in the sample code.
The usage is simple:
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<WrapPanel Orientation="Vertical" local:ScrollableWrapPanel.IsEnabled="True">
<!-- Content -->
</WrapPanel>
</ScrollViewer
It seems monitoring for children is one of important task to achieve wanted. So why not creating custom panel:
public class ColumnPanel : Panel
{
public double ViewportHeight
{
get { return (double)GetValue(ViewportHeightProperty); }
set { SetValue(ViewportHeightProperty, value); }
}
public static readonly DependencyProperty ViewportHeightProperty =
DependencyProperty.Register("ViewportHeight", typeof(double), typeof(ColumnPanel),
new FrameworkPropertyMetadata(double.PositiveInfinity, FrameworkPropertyMetadataOptions.AffectsMeasure | FrameworkPropertyMetadataOptions.AffectsArrange));
protected override Size MeasureOverride(Size constraint)
{
var location = new Point(0, 0);
var size = new Size(0, 0);
foreach (UIElement child in Children)
{
child.Measure(new Size(double.PositiveInfinity, double.PositiveInfinity));
if (location.Y != 0 && ViewportHeight < location.Y + child.DesiredSize.Height)
{
location.X = size.Width;
location.Y = 0;
}
if (size.Width < location.X + child.DesiredSize.Width)
size.Width = location.X + child.DesiredSize.Width;
if (size.Height < location.Y + child.DesiredSize.Height)
size.Height = location.Y + child.DesiredSize.Height;
location.Offset(0, child.DesiredSize.Height);
}
return size;
}
protected override Size ArrangeOverride(Size finalSize)
{
var location = new Point(0, 0);
var size = new Size(0, 0);
foreach (UIElement child in Children)
{
if (location.Y != 0 && ViewportHeight < location.Y + child.DesiredSize.Height)
{
location.X = size.Width;
location.Y = 0;
}
child.Arrange(new Rect(location, child.DesiredSize));
if (size.Width < location.X + child.DesiredSize.Width)
size.Width = location.X + child.DesiredSize.Width;
if (size.Height < location.Y + child.DesiredSize.Height)
size.Height = location.Y + child.DesiredSize.Height;
location.Offset(0, child.DesiredSize.Height);
}
return size;
}
}
The usage (instead of WrapPanel) is following:
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<local:ColumnPanel ViewportHeight="{Binding ViewportHeight, RelativeSource={RelativeSource AncestorType=ScrollViewer}}" ... >
...
</local:ColumnPanel>
</ScrollViewer>
The idea is to calculate layout manually, while MeasureOverride and ArrangeOverride will be automatically called whenever children are changed: added, deleted, resized, etc.
Measure logic is simple: start from (0,0) and measure next child size, if it fits into current column - add it, otherwise start and new column by offsetting location. During whole measurement cycle adjust the resulting size.
The only missing part of puzzle is to provide into measure/arrange cycles ViewportHeight from parent ScrollViewer. This is the role of ColumnPanel.ViewportHeight.
Here is the demo (button add purple circle):
You can do this by wrapping your wrappanel in a scrollviewer, but then binding the height and width of the inner panel to the Height and Width of the Viewport of the scrollviewer, so it stretches and contracts with the rest of the screen.
I've also added minimum Height and Width to my sample, which ensures that the scrollbars appear once the wrap panel is pushed smaller than it's min dimensions
<ScrollViewer x:Name="sv" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<WrapPanel MinWidth="200" Width="{Binding ElementName=sv, Path=ViewportWidth}" MinHeight="200" Height="{Binding ElementName=sv, Path=ViewportHeight}">
<Ellipse Fill="Red" Height="200" Width="200"/>
<Ellipse Fill="Yellow" Height="200" Width="200"/>
<Ellipse Fill="Green" Height="200" Width="200"/>
</WrapPanel>
</ScrollViewer>
I have a WPF .xaml file which contains an image framework element which is bound to a BitmapSource in my C# code. At the moment, I'm hard coding the image width and height of my bitmap (ie. 512px) and I've specified the framework element not to stretch the image. So, if my window is bigger than the bitmap, the image seems to float inside the bounds of the window. However, what I'd like is that the bitmap width and height is automatically bound to the width and height of the .xaml window. So, when I resize the window, the bitmap gets resized along with it. I don't simply want the image to be 'stretched'... rather, I want the bitmap width and height to match the window that contains it. I'm not entirely sure how to do this, but I'll post the setup that I have so far.
In my .xaml file, I have my Image framework element (Note: the render transform merely flips the image along the horizontal axis):
<Image Source="{Binding WpfPreview}" Name="VectorPreview" Stretch="None" Width="{Binding Xres}" Height="{Binding Yres}" RenderTransformOrigin="0.5,0.5">
<Image.RenderTransform>
<ScaleTransform ScaleY="-1"/>
</Image.RenderTransform>
</Image>
My C# code then has the following code in it:
protected BitmapSource wpfPreview;
public BitmapSource WpfPreview
{
get { return wpfPreview; }
set
{
wpfPreview = value;
NotifyPropertyChanged("WpfPreview");
}
}
protected static int xres = 512;
public int Xres
{
get { return xres; }
set
{
xres = value;
NotifyPropertyChanged("Xres");
UpdateBitmapPreview();
}
}
protected static int yres = 512;
public int Yres
{
get { return yres; }
set
{
yres = value;
NotifyPropertyChanged("Yres");
UpdateBitmapPreview();
}
}
protected GDI.Bitmap gdiPreviewImage = new GDI.Bitmap(xres, yres);
public void UpdateBitmapPreview()
{
if(gdiPreviewImage.Width != xres || gdiPreviewImage.Height != yres) gdiPreviewSurface = new GDI.Bitmap(xres, yres);
using (var graphics = GDI.Graphics.FromImage(gdiPreviewImage))
{
graphics.Clear(Color.White);
graphics.ResetTransform();
currentSlicer.DrawBitmapPreview(graphics, PreviewOptions);
}
WpfPreview = Imaging.CreateBitmapSourceFromHBitmap(
gdiPreviewImage.GetHbitmap(),
IntPtr.Zero,
Int32Rect.Empty,
BitmapSizeOptions.FromEmptyOptions()
);
}
You basically want to keep aspect ratio while resizing? I think these properties can fix this up. set ClipToBounds to true and Stretch to Uniform.
<Image Stretch="Uniform" ...
So I'm working with a system of switches and bulbs, which are attached together by wires. I'm supposed to make the switches draggable, in such a way that the wires also move. Since you cannot update the points directly in a PointCollection (I think they're a struct I think?), I've figured out a way, but this seems VERY verbose, and I figure there must be a better way to do this.
Basically I retrieve the points, do the manipulation, clear the collection, and add the new points:
Point firstPoint = new Point(firstPointCollection[0].X, firstPointCollection[0].Y + offset.Y);
Point secondPoint = new Point(firstPointCollection[1].X + offset.X, firstPointCollection[1].Y + offset.Y);
Point thirdPoint = new Point(firstPointCollection[2].X + offset.X, firstPointCollection[2].Y + offset.Y);
firstPointCollection.Clear();
firstPointCollection.Add(firstPoint);
firstPointCollection.Add(secondPoint);
firstPointCollection.Add(thirdPoint);
In a system with multiple wires, which all consist of multiple points, this very quickly gets very tedious to write. This must all be done in C#, but if there is a better way to do with this with Data Binding of some sort, please let me know.
well the whole thing could be done with data binding so as you update the location of a bulb it auto updates the UI, stay with me here the code example is a bit long..
the result of this demo is just some circles with lines joining them but you could template this up however you want.
The xaml window, this declares an items control that will display the bulbs (and the lines joining them)
<Window x:Class="points.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:points"
DataContext="{Binding RelativeSource={RelativeSource Self}}"
Title="MainWindow" Height="350" Width="525">
<ItemsControl ItemsSource="{Binding Bulbs}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<Canvas HorizontalAlignment="Left" VerticalAlignment="Top">
<Ellipse Fill="Blue" Width="10" Height="10">
<Ellipse.RenderTransform>
<TranslateTransform X="{Binding X}" Y="{Binding Y}"/>
</Ellipse.RenderTransform>
</Ellipse>
<Line X1="{Binding X}" Y1="{Binding Y}" X2="{Binding NextBulb.X}" Y2="{Binding NextBulb.Y}" Stroke="Red">
<Line.RenderTransform>
<TranslateTransform X="5" Y="5"/>
</Line.RenderTransform>
</Line>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Window>
and this is the cs, I've created a bulb structure that holds the X and Y location of a bulb and a reference to the next bulb in the chain, I've placed all bulbs in to a line and then just moved the Y location of element 5 a bit lower to show it updating.
using System.Collections.ObjectModel;
using System.Windows;
namespace points
{
public class Bulb : DependencyObject
{
public double X
{
get { return (double)GetValue(XProperty); }
set { SetValue(XProperty, value); }
}
public static readonly DependencyProperty XProperty =
DependencyProperty.Register("X", typeof(double), typeof(Bulb), new PropertyMetadata(0d));
public double Y
{
get { return (double)GetValue(YProperty); }
set { SetValue(YProperty, value); }
}
public static readonly DependencyProperty YProperty =
DependencyProperty.Register("Y", typeof(double), typeof(Bulb), new PropertyMetadata(0d));
public Bulb NextBulb
{
get { return (Bulb)GetValue(NextBulbProperty); }
set { SetValue(NextBulbProperty, value); }
}
public static readonly DependencyProperty NextBulbProperty =
DependencyProperty.Register("NextBulb", typeof(Bulb), typeof(Bulb), new PropertyMetadata(null));
}
public partial class MainWindow : Window
{
public ObservableCollection<Bulb> Bulbs
{
get { return (ObservableCollection<Bulb>)GetValue(BulbsProperty); }
set { SetValue(BulbsProperty, value); }
}
public static readonly DependencyProperty BulbsProperty =
DependencyProperty.Register("Bulbs", typeof(ObservableCollection<Bulb>), typeof(MainWindow), new PropertyMetadata(null));
public MainWindow()
{
Bulbs = new ObservableCollection<Bulb>();
InitializeComponent();
for (int i = 0; i < 10; i++)
{
double x = i * 50;
double y = 25;
Bulbs.Add(new Bulb()
{
X = x,
Y = y,
NextBulb = i > 0 ? Bulbs[i - 1] : null,
});
}
Bulbs[5].Y = 50;
}
}
}
The outcome of this is that altering the Bulbs structure in any way will update the bulb display on the UI without having to clear and recreate your collection every time, you can even run animations on the properties for some pretty cool effects.
I really like the accepted answer - an upvote well deserved - because it was well written and really works. But, I thought it would be worth noting: if you intend to use it for many quick changing values, updating a PointCollection will perform better. See How to draw a simple (line) graph?.
Regarding the code and it's verbosity, maybe this would have (7 years ago) helped:
firstPointCollection = new PointCollection(firstPointCollection.Select(p=>new Point(p.X + offset.X, p.Y + offset.Y)));
Cheers ;o)
First of all, the Offset() method
As Amnestic pointed out even though Point is a struct you can mutate it through the Offset method. However since he is using a PointCollection and not an array of Points when you use the indexer you will get a copy of the struct, not the original.
If the points were in a Point[] you could just use this:
firstPointCollection[0].Offset(x, y)
However because we are using a PointCollection you need to set the indexer as such:
firstPointCollection[0] = firstPointCollection[0].Offset(x, y)
The discussion over whether structs should be mutable or not is a whole other kettle of fish.
I am making my own custom panel, which is supposed to scroll vertically when the content does not fit the available space, so i put it in a ScrollViewer.
Right now i can't get the ScrollViewer to activate the scrollbar when the panel inside is bigger then the ScrollViewer itself.
The permille functions get attached properties telling how big the childs have to be compared to the available size (without scrolling), aka the ViewPort.
As the size passed in MeasureOverride passes infinite, i don't think i can use the permille functions there.
That is why i measure my children in ArrangeOverride (not best practice, i guess) but that way the scrollviewer doesn't scroll.
How do i get this to work?
My XAML code:
<ScrollViewer>
<controls:TilePanel x:Name="TilePanel" PreviewMouseLeftButtonDown="TilePanel_PreviewMouseLeftButtonDown" PreviewMouseLeftButtonUp="TilePanel_PreviewMouseLeftButtonUp"
PreviewMouseMove="TilePanel_PreviewMouseMove" DragEnter="TilePanel_DragEnter" Drop="TilePanel_Drop" AllowDrop="True" />
</ScrollViewer>
My Custom Panel Class:
/// <summary>
/// A Panel Showing Tiles
/// </summary>
public class TilePanel : PermillePanel
{
public TilePanel()
{
}
protected override Size MeasureOverride(Size constraint)
{
//here constraint width or height can be infinite.
//as tiles are a permille of that height, they too can be infinite after measuring
//this is unwanted behavior, so we measure in the ArrangeOverride method
if (constraint.Width == double.PositiveInfinity)
{
return new Size(0, constraint.Height);
}
else if (constraint.Height == double.PositiveInfinity)
{
return new Size(constraint.Width, 0);
}
else
{
return constraint;
}
}
protected override Size ArrangeOverride(Size arrangeSize)
{
//return base.ArrangeOverride(arrangeSize);
foreach (FrameworkElement child in InternalChildren)
{
Size availableSize = new Size();
//set the width and height for the child
availableSize.Width = arrangeSize.Width * TilePanel.GetHorizontalPermille(child) / 1000;
availableSize.Height = arrangeSize.Height * TilePanel.GetVerticalPermille(child) / 1000;
child.Measure(availableSize);
}
// arrange the children on the panel
// fill lines horizontally, when we reach the end of the current line, continue to the next line
Size newSize = new Size(arrangeSize.Width, arrangeSize.Height);
double xlocation = 0;
double ylocation = 0;
double ystep = 0;
double maxYvalue = 0;
foreach (FrameworkElement child in InternalChildren)
{
double endxlocation = xlocation + child.DesiredSize.Width;
double constrainedWidth = arrangeSize.Width * TilePanel.GetHorizontalPermille(child) / 1000;
double constrainedHeight = arrangeSize.Height * TilePanel.GetVerticalPermille(child) / 1000;
if (TilePanel.GetVerticalPermille(child) != 0 && TilePanel.GetHorizontalPermille(child) != 0)
{
//horizontal overflow -> next line
if (endxlocation >= this.DesiredSize.Width *1.01)
{
ylocation += ystep;
xlocation = 0;
}
}
Rect rect = new Rect(xlocation, ylocation, constrainedWidth, constrainedHeight);
child.Arrange(rect);
xlocation += constrainedWidth;
ystep = Math.Max(ystep, constrainedHeight);
maxYvalue = Math.Max(maxYvalue, ystep + constrainedHeight);
}
if (maxYvalue > newSize.Height)
{
newSize.Height = maxYvalue;
}
return newSize;
}
}
Calling Measure() from within ArrangeOverride() will cause problems. The framework detects this and forces a remeasure. Set a tracepoint in MeasureOverride(), and I'll bet you'll see that it keeps getting called over and over again, even though the layout hasn't changed1.
If you absolutely have to call Measure() from ArrangeOverride(), you will need to do so conditionally such that it only forces a remeasure when the available size actually changes since the last call to Measure(). Then, you'll effectively end up with two measure + arrange passes any time the layout is invalidated, as opposed to just one. However, such an approach is hacky, and I would advise sticking to the best practice of only measuring within MeasureOverride().
1Interestingly, your UI may still respond to input, despite this apparent "infinite loop" in the layout.
If you want to use a custom Panel inside a ScrollViewer then you must add the code that does the actual scrolling. You can do that by implementing the IScrollInfo Interface in your custom Panel.
You can find a tutorial that explains this interface and provides an example code imeplementation in the WPF Tutorial - Implementing IScrollInfo page on the Tech Pro website. It's a fairly simple procedure and looks a tiny bit like this:
public void LineDown() { SetVerticalOffset(VerticalOffset + LineSize); }
public void LineUp() { SetVerticalOffset(VerticalOffset - LineSize); }
public void MouseWheelDown() { SetVerticalOffset(VerticalOffset + WheelSize); }
public void MouseWheelUp() { SetVerticalOffset(VerticalOffset - WheelSize); }
public void PageDown() { SetVerticalOffset(VerticalOffset + ViewportHeight); }
public void PageUp() { SetVerticalOffset(VerticalOffset - ViewportHeight); }
...
I am new in WPF, I created a new UserControl MyUserControl.
Now I am surprised: the UserContol does not have a location.
How can I read (by code) myUserControl1.Location in the parent container?
I explain:
I have some Dots (UserControls) that the user can drag in a panel. Actually, I am not sure what kind of Panel this will be... Perhaps Grid.
Now, these dots should be linked with a Line.
Actually, I have a Dot.Head and Dot.Queue properties (also Dots). So, when a Head or Queue is added, I need to dinamically create a link (Line) between them [A]-----[B]. This for this Line I search the Start and End points to set.
Control XAML:
<UserControl x:Class="LinePlan.Stop"
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="21" d:DesignWidth="80">
<Canvas>
<Path Fill="LightBlue" Width="16" Height="16">
<Path.Data>
<EllipseGeometry x:Name="Dot" Center="8,8"
RadiusX="4" RadiusY="4"/>
</Path.Data>
</Path>
<TextBlock x:Name="StopText" Text="Eiffel Tower" Canvas.Left="16"/>
</Canvas>
</UserControl>
Code:
public partial class Stop : UserControl
{
private Stop head;
private Stop tail;
private LineGeometry headLine;
private LineGeometry queueLine;
public Stop()
{
InitializeComponent();
}
public Stop Head
{
get { return head; }
set
{
if (head != value)
{
head = value;
if (head == null)
{
if (headLine != null)
headLine = null;
}
else
{
headLine = new LineGeometry();
headLine.StartPoint = head.DotPosition;
headLine.EndPoint = this.DotPosition;
// ?? Add this line to the parent
}
}
}
}
public Stop Tail
{
get { return tail; }
set { tail = value; }
}
public Point DotPosition
{
get
{
double x = Canvas.GetLeft(this) + this.Dot.Center.X;
double y = Canvas.GetTop(this) + this.Dot.Center.Y;
return new Point(x, y);
}
set
{
Canvas.SetLeft(this, value.X - this.Dot.Center.X);
Canvas.SetTop(this, value.Y - this.Dot.Center.Y);
}
}
}
The WPF layout system doesn't use absolute positioning, unless you're placing your controls on a container that supports absolute positioning (typically a Canvas). If you're using a Canvas, you can get or set the position of the control using the Canvas.Left, Canvas.Right, Canvas.Top and Canvas.Bottom attached properties:
double x = Canvas.GetLeft(myControl);
double y = Canvas.GetTop(myControl);
Now, if you want the actual location of the control (relative to its parent), you can use the VisualTreeHelper.GetOffset method:
Vector offset = VisualTreeHelper.GetOffset(myControl);
double x = offset.X;
double y = offset.Y;
Elements (like user controls) are normally placed in panels in WPF. Depending on which panel you are using the panel may add some attached properties to the user control. If the user control is placed in a Canvas it will get the attached properties Left, Top, Right and Bottom. However, if the user control is placed in a Grid it will get the attached properties Row and Column (and some more). Other panels like StackPanel will not attach any properties. There is no such thing as a universal user control location.
Panels Overview
Attached Properties Overview
Assuming that you are using a Canvas as your panel you can access the attached Left and Top properties like this:
Double x = Canvas.GetLeft(myUserControl1);
Double y = Canvas.GetTop(myUserControl1);