Border disappearing on ItemsControl resize (using "stretchpanel") - c#

Yesterday I asked how to get an ItemsControl to have its children equally distributed across the available space; and after happily reading the answer, I wrote (well actually kind of copied and adjusted it) my own "stretchpanel", which does exactly what i want. However my children are supposed to have a border on their right side, which works, as long as the child is bigger than its content. As soon as it gets smaller the border disappears and i can't tell why (and thus, not fix it). Below you can find a simplified version of my view and of the overriden methods from my stretchpanel, using this sample as basis. Thanks a lot in advance.
<Grid Background="Red" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<ItemsControl x:Name="HorizontalListBox" Background="Blue" HorizontalAlignment="Left" HorizontalContentAlignment="Left" >
<ItemsControl.Items>
<System:String>Test</System:String>
<System:String>Test</System:String>
<System:String>This demonstrates the problem exceptionally well</System:String>
<System:String>Test</System:String>
<System:String>Test</System:String>
</ItemsControl.Items>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Controls:StretchPanel Orientation="Horizontal" HorizontalAlignment="Stretch"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Background="Yellow" HorizontalAlignment="Stretch">
<TextBlock Text="{Binding}" HorizontalAlignment="Stretch"/>
<Border BorderBrush="Black" BorderThickness="0,0,1,0"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
protected override Size MeasureOverride(Size availableSize)
{
var size = new Size();
SetSide(ref size, GetSide(availableSize));
foreach (UIElement child in Children)
{
child.Measure(availableSize);
SetOtherSide(ref size, Math.Max(GetOtherSide(size), GetOtherSide(child.DesiredSize)));
}
_measureSize = size;
return size;
}
protected override Size ArrangeOverride(Size finalSize)
{
double offset = 0;
var Side = GetSide(_measureSize) / Children.Count;
var Rest = GetSide(_measureSize) % Children.Count;
foreach (UIElement child in Children)
{
double side = Math.Floor(GetSide(_measureSize) / Children.Count);
if (Rest > 0)
{
side++;
Rest--;
}
var final = new Size();
SetSide(ref final, side);
SetOtherSide(ref final, GetOtherSide(finalSize));
child.Arrange(new Rect(GetOffsetPoint(offset), final));
offset += side;
}
return finalSize;
}

It will work if you write your StretchPanel like this, which measure its child elements with zero width and height:
public class StretchPanel : Panel
{
protected override Size MeasureOverride(Size availableSize)
{
foreach (UIElement element in Children)
{
element.Measure(new Size());
}
return new Size();
}
protected override Size ArrangeOverride(Size finalSize)
{
for (int i = 0; i < Children.Count; i++)
{
int x1 = (int)(finalSize.Width / Children.Count * i);
int x2 = (int)(finalSize.Width / Children.Count * (i + 1));
Children[i].Arrange(new Rect(x1, 0, x2 - x1, finalSize.Height));
}
return finalSize;
}
}
and then set HorizontalAlignment="Stretch" (or don't set it at all, since that is the default value) on the ItemsControl.

Related

How do I get a Rectangle drawn on an adorner to scale with the Image element its bound to when the window size is changed?

I'm using a rectangle drawn on an adorner to mark a region of interest on an image. The issue is that if I resize the window, the rectangle doesn't change size.
I'm new to WPF, so I've done a bunch of research, googling what I can with multiple different search terms. I actually just learned adorners that way, and I've gotten this far on that, but I've hit a wall on how to finish this last piece. I know that my problem is based in the size of the rectangle, but I don't know what to capture/look for to adjust it, since wpf resizes the actual image object on window resize, so there's no scale factor to look at.
Here's the XAML for the application I'm testing things in.
<Window x:Class="TestingAdorners.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:TestingAdorners"
mc:Ignorable="d"
Title="MainWindow" Height="450" Width="800">
<Grid ClipToBounds="True">
<AdornerDecorator>
<Image Name="Btn" Source="nyan.png" Stretch="Uniform"/>
</AdornerDecorator>
</Grid>
</Window>
The adorner class:
class RoiAdorner : Adorner
{
public Rect rectangle = new Rect();
public RoiAdorner(UIElement adornedElement) : base(adornedElement)
{
rectangle.Height = 30;
rectangle.Width = 100;
IsHitTestVisible = false;
}
protected override void OnRender(DrawingContext drawingContext)
{
Pen pen = new Pen(Brushes.Green, 5);
drawingContext.DrawRectangle(null, pen, rectangle);
}
}
And the Xaml.cs
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
AdornerLayer.GetAdornerLayer(Btn).Add(new RoiAdorner(Btn));
}
private void Window_Loaded(object sender, RoutedEventArgs e)
{
}
}
The desired result is that the rectangle scales with the image object so that it always covers the same region of the image. The problem is I don't know how to capture a scale factor to scale it up and down as the window resizes.
Update: After thinking through Frenchy's suggestion I realized the answer is simply: "Normalize your coordinates"
you just adapt your render method like this:
class RoiAdorner : Adorner
{
public double factorX = 0d;
public double factorY = 0d;
public Rect rectangle = new Rect();
public RoiAdorner(UIElement adornedElement) : base(adornedElement)
{
rectangle.Height = 30;
rectangle.Width = 100;
IsHitTestVisible = false;
}
protected override void OnRender(DrawingContext drawingContext)
{
if (factorY == 0)
factorY = rectangle.Height / AdornedElement.DesiredSize.Height;
if (factorX == 0)
factorX = rectangle.Width / AdornedElement.DesiredSize.Width;
var r = new Rect(new Size(AdornedElement.DesiredSize.Width * factorX, AdornedElement.DesiredSize.Height * factorY));
//Rect adornedElementRect = new Rect(this.AdornedElement.DesiredSize);
drawingContext.DrawRectangle(null, new Pen(Brushes.Red, 5), r);
}
this.AdornedElement.DesiredSize gives you the size of image.
The approach I would use is to render the picture and rectangle to the same thing. Then that one thing is stretched, scaled or whatever.
One way to do this would be to use a DrawingImage. Drawing methods are extremely efficient if rather low level.
<Grid ClipToBounds="True">
<AdornerDecorator>
<Image Name="img" Stretch="Uniform">
<Image.Source>
<DrawingImage PresentationOptions:Freeze="True">
<DrawingImage.Drawing>
<DrawingGroup>
<ImageDrawing Rect="0,0,595,446" ImageSource="DSC00025.jpg"/>
<GeometryDrawing Brush="Green">
<GeometryDrawing.Geometry>
<RectangleGeometry Rect="0,0,100,30" />
</GeometryDrawing.Geometry>
</GeometryDrawing>
</DrawingGroup>
</DrawingImage.Drawing>
</DrawingImage>
</Image.Source>
</Image>
</AdornerDecorator>
</Grid>
Another is with a visualbrush. Controls inherit from visual - this is somewhat higher level coding.
<Grid ClipToBounds="True">
<AdornerDecorator>
<Rectangle Name="rec">
<Rectangle.Fill>
<VisualBrush Stretch="Uniform">
<VisualBrush.Visual>
<Grid Height="446" Width="595">
<Image Source="DSC00025.jpg" Stretch="Fill"/>
<Rectangle Height="30" Width="100" Fill="Green"
VerticalAlignment="Top" HorizontalAlignment="Left"/>
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Rectangle.Fill>
</Rectangle>
</AdornerDecorator>
</Grid>
Note that both of these are quick and dirty illustrations to give you the idea. The image I picked at random off my hard drive is sized 446 * 595. You could calculate sizes or bind or stretch as suits your requirement best.

Using Viewbox to scale Grid containing Labels and Textboxes

So I'm trying to build out a form that will automatically scale proportionally up and down based on the available width of the parent container, and the same column percentage ratios, like this:
There will be other surrounding content that needs to scale as well, like images and buttons (which will not be in the same grid), and from what I've read so far, using a Viewbox is the way to go.
However, when I wrap my grid in a Viewbox with Stretch="Uniform", the Textbox controls each collapse down to their minimum width, which looks like this:
If I increase the container width, everything scales as expected (good), but the textboxes are still collapsed to their minimum-possible width (bad):
If I type any content into the Textboxes, they will increase their width to contain the text:
...but I don't want that behavior - I want the Textbox element widths to be tied to the grid column widths, NOT to be dependent on the content.
Now, I've looked at a variety of SO questions, and this one comes closest to what I'm after:
How to automatically scale font size for a group of controls?
...but it still didn't really deal with the textbox width behavior specifically (when it interacts with the Viewbox beahvior), which seems to be the primary problem.
I've tried a variety of things - different HorizontalAlignment="Stretch" settings and so on, but nothing has worked so far. Here is my XAML:
<Window x:Class="WpfApp1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp1"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="5" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<StackPanel Grid.Column="0">
<StackPanel.Background>
<LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
<GradientStop Color="Silver" Offset="0"/>
<GradientStop Color="White" Offset="1"/>
</LinearGradientBrush>
</StackPanel.Background>
<Viewbox Stretch="Uniform" HorizontalAlignment="Stretch" VerticalAlignment="Stretch">
<Grid Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="1*" />
<ColumnDefinition Width="2*" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Label Content="Field A" Grid.Column="0" Grid.Row="0" />
<TextBox Grid.Column="1" Grid.Row="0" HorizontalAlignment="Stretch"></TextBox>
<Label Content="Field B" Grid.Column="2" Grid.Row="0" />
<TextBox Grid.Column="3" Grid.Row="0" HorizontalAlignment="Stretch"></TextBox>
</Grid>
</Viewbox>
<Label Content="Other Stuff"/>
</StackPanel>
<GridSplitter Grid.Column="1" HorizontalAlignment="Stretch" Height="100" Width="5"/>
<StackPanel Grid.Column="2">
<Label Content="Body"/>
</StackPanel>
</Grid>
</Window>
The reason for this behavior is that a Viewbox child is given infinite space to measure its desired size. Stretching the TextBoxes to infinite Width wouldn't make much sense, as that couldn't be rendered anyway, so their default size is returned.
You can use a converter to achieve the desired effect.
public class ToWidthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
double gridWidth = (double)value;
return gridWidth * 2/6;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
You can hook everything up adding these resources.
<Viewbox.Resources>
<local:ToWidthConverter x:Key="ToWidthConverter"/>
<Style TargetType="{x:Type TextBox}">
<Setter Property="Width"
Value="{Binding ActualWidth,
RelativeSource={RelativeSource AncestorType={x:Type Grid}},
Converter={StaticResource ToWidthConverter}}"/>
</Style>
</Viewbox.Resources>
UPDATE
I'm having trouble understanding the original problem of the infinite
grid width.
The infinite space approach is often used to determine the DesiredSize of a UIElement. In short, you give the control all the space it could possibly need (no constraints) and then measure it to retrieve its desired size.
Viewbox uses this approach to measure its child, but our Grid is dynamic in size (no Height or Width are set in code), so the Viewbox goes down another level at the grids children to see if it can determine a size by taking the sum of the components.
However, you can run in to problems when this sum of components exceeds the total available size, as shown below.
I replaced the textboxes with labels Foo and Bar and set their backgroundcolor to gray. Now we can see Bar is invading Body territory, this is clearly not something we meant to happen.
Again, the root of the problem comes from Viewbox not knowing how to divide infinity in to 6 equal shares (to map to columnwidths 1*, 2*, 1*,2*), so all we need to do is restore the link with the grids width. In ToWidthConverter the aim was to map the TextBox' Width to the Grids ColumnWidth of 2*, so I used gridWidth * 2/6. Now Viewbox is able to solve the equation again: each TextBox gets one third of gridwidth, and each Label one half of that (1* vs 2*).
Of course, when you scramble things up, by introducing new columns, you'll have to take care to keep the sum of the components in sync with the total available width. In other words, the equation needs to be solvable. Put in math, the sum of the desired sizes (of the controls you haven't constrained, labels in our example) and the converted sizes (as parts of gridWidth, textboxes in our example) needs to be less than or equal to the available size (gridWidth in our example).
I found the scaling to behave well if you use the converted sizes for TextBoxes, and let the star sized ColumnWidths handle most others. Keeping in mind to stay within the total available size.
One way to add some flexibility is to add a ConverterParameter to the mix.
public class PercentageToWidthConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
double gridWidth = (double)value;
double percentage = ParsePercentage(parameter);
return gridWidth * percentage;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
private double ParsePercentage(object parameter)
{
// I chose to let it fail if parameter isn't in right format
string[] s = ((string)parameter).Split('/');
double percentage = Double.Parse(s[0]) / Double.Parse(s[1]);
return percentage;
}
}
An example that divides gridWidth over 10 equal shares, and assigns these shares to the components accordingly.
<Viewbox Stretch="Uniform">
<Viewbox.Resources>
<local:PercentageToWidthConverter x:Key="PercentageToWidthConverter"/>
</Viewbox.Resources>
<Grid Background="White">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="3*" />
<ColumnDefinition Width="1*" />
</Grid.ColumnDefinitions>
<Label Content="Field A" Grid.Column="0" />
<TextBox Grid.Column="1"
Width="{Binding ActualWidth,
RelativeSource={RelativeSource AncestorType={x:Type Grid}},
Converter={StaticResource PercentageToWidthConverter},
ConverterParameter=2/10}" />
<Label Content="Field B" Grid.Column="2" />
<TextBox Grid.Column="3"
Width="{Binding ActualWidth,
RelativeSource={RelativeSource AncestorType={x:Type Grid}},
Converter={StaticResource PercentageToWidthConverter},
ConverterParameter=3/10}" />
<Button Content="Ok" Grid.Column="4"
Width="{Binding ActualWidth,
RelativeSource={RelativeSource AncestorType={x:Type Grid}},
Converter={StaticResource PercentageToWidthConverter},
ConverterParameter=1/10}" />
</Grid>
</Viewbox>
Note the shares for each control, grouped as 2 - 2 - 2 - 3 - 1 (with 1 for buttonwidth).
Finally, depending on the reusability you're after, some other ways to handle this:
Set fixed size(s) on your root Grid. Downsides:
Needs to be finetuned each time you change components (to achieve the
desired horizontal / vertical / fontsize ratio)
This ratio might break on different themes, Windows versions,...
Add a Behavior. As done in one of the answers in your linked FontSize post, but instead implemented to map the column widths to parts of gridWidth.
Create a custom panel, as proposed by #Grx70.
The problem with your approach is that Grid does not work exactly how we intuitively think it does. Namely, the star size works as expected only if these conditions are met:
The Grid has its horizontal alignment set to Stretch
The Grid is contained in a finite size container, i.e. its Measure method receives a constraint with finite Width (not double.PositiveInfinity)
This pertains to column sizing; row sizing is symmetrical. In your case the second condition is not met. I am not aware of any simple tricks to make Grid work as you expect, so my solution would be to create custom Panel that would do the job. That way you are in full control of how the controls are laid out. It's not really that hard to accomplish, although it requires some level of understanding how WPF layout system works.
Here's an example implementation that does your bidding. For the sake of brevity it only works horizontally, but it's not difficult to extend it to also work vertically.
public class ProportionalPanel : Panel
{
protected override Size MeasureOverride(Size constraint)
{
/* Here we measure all children and return minimal size this panel
* needs to be to show all children without clipping while maintaining
* the desired proportions between them. We should try, but are not
* obliged to, fit into the given constraint (available size) */
var desiredSize = new Size();
if (Children.Count > 0)
{
var children = Children.Cast<UIElement>().ToList();
var weights = children.Select(GetWeight).ToList();
var totalWeight = weights.Sum();
var unitWidth = 0d;
if (totalWeight == 0)
{
//We should handle the situation when all children have weights set
//to 0. One option is to measure them with 0 available space. To do
//so we simply set totalWeight to something other than 0 to avoid
//division by 0 later on.
totalWeight = children.Count;
//We could also assume they are to be arranged uniformly, so we
//simply coerce their weights to 1
for (var i = 0; i < weights.Count; i++)
weights[i] = 1;
}
for (var i = 0; i < children.Count; i++)
{
var child = children[i];
child.Measure(new Size
{
Width = constraint.Width * weights[i] / totalWeight,
Height = constraint.Height
});
desiredSize.Width += child.DesiredSize.Width;
desiredSize.Height =
Math.Max(desiredSize.Height, child.DesiredSize.Height);
if (weights[i] != 0)
unitWidth =
Math.Max(unitWidth, child.DesiredSize.Width / weights[i]);
}
if (double.IsPositiveInfinity(constraint.Width))
{
//If there's unlimited space (e.g. when the panel is nested in a Viewbox
//or a StackPanel) we need to adjust the desired width so that no child
//is given less than desired space while maintaining the desired
//proportions between them
desiredSize.Width = totalWeight * unitWidth;
}
}
return desiredSize;
}
protected override Size ArrangeOverride(Size constraint)
{
/* Here we arrange all children into their places and return the
* actual size this panel is. The constraint will never be smaller
* than the value of DesiredSize property, which is determined in
* the MeasureOverride method. If the desired size is larger than
* the size of parent element, the panel will simply be clipped
* or appear "outside" of the parent element */
var size = new Size();
if (Children.Count > 0)
{
var children = Children.Cast<UIElement>().ToList();
var weights = children.Select(GetWeight).ToList();
var totalWeight = weights.Sum();
if (totalWeight == 0)
{
//We perform same routine as in MeasureOverride
totalWeight = children.Count;
for (var i = 0; i < weights.Count; i++)
weights[i] = 1;
}
var offset = 0d;
for (var i = 0; i < children.Count; i++)
{
var width = constraint.Width * weights[i] / totalWeight;
children[i].Arrange(new Rect
{
X = offset,
Width = width,
Height = constraint.Height,
});
offset += width;
size.Width += children[i].RenderSize.Width;
size.Height = Math.Max(size.Height, children[i].RenderSize.Height);
}
}
return size;
}
public static readonly DependencyProperty WeightProperty =
DependencyProperty.RegisterAttached(
name: "Weight",
propertyType: typeof(double),
ownerType: typeof(ProportionalPanel),
defaultMetadata: new FrameworkPropertyMetadata
{
AffectsParentArrange = true, //because it's set on children and is used
//in parent panel's ArrageOverride method
AffectsParentMeasure = true, //because it's set on children and is used
//in parent panel's MeasuerOverride method
DefaultValue = 1d,
},
validateValueCallback: ValidateWeight);
private static bool ValidateWeight(object value)
{
//We want the value to be not less than 0 and finite
return value is double d
&& d >= 0 //this excludes double.NaN and double.NegativeInfinity
&& !double.IsPositiveInfinity(d);
}
public static double GetWeight(UIElement d)
=> (double)d.GetValue(WeightProperty);
public static void SetWeight(UIElement d, double value)
=> d.SetValue(WeightProperty, value);
}
And the usage looks like this:
<local:ProportionalPanel>
<Label Content="Field A" local:ProportionalPanel.Weight="1" />
<TextBox local:ProportionalPanel.Weight="2" />
<Label Content="Field B" local:ProportionalPanel.Weight="1" />
<TextBox local:ProportionalPanel.Weight="2" />
</local:ProportionalPanel>

WrapPanel scrollbars

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>

Zooming into all content of a Canvas

I have this Xaml:
<Grid x:Name="DrawingGrid" Visibility="Collapsed">
<AppBarButton x:Name="ExitDrawingButton" Icon="Cancel" Click="ExitDrawingButton_Click"></AppBarButton>
<ScrollViewer x:Name="DScrollViewer" ManipulationMode="All" MaxZoomFactor="2.0" MinZoomFactor="1.0" HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden" DoubleTapped="DScrollViewer_DoubleTapped" Width="1140" Height="770">
<Canvas x:Name="inkCanvas" Background="Transparent" Width="1140" Height="770">
<StackPanel x:Name="DStackPanel" Orientation="Horizontal" Margin="0,0,0,0">
<Image x:Name="DImage0" HorizontalAlignment="Left" Source="{Binding nextImage}" Width="570" Canvas.ZIndex="0"/>
<Image x:Name="DImage1" HorizontalAlignment="Left" Source="{Binding nextImage}" Width="570" Canvas.ZIndex="0"/>
</StackPanel>
</Canvas>
</ScrollViewer>
</Grid>
I am drawing on Canvas using the CanvasManager.cs class and it is working fine.
Now I need to zoom on the Canvas: Zoom the Canvas (the ink) and Zoom what it contains (the StackPanel + the Images) together.
On doubleTapping the ScrollViewer containing the Canvas I have this method:
private async void DScrollViewer_DoubleTapped(object sender, DoubleTappedRoutedEventArgs e)
{
Point point = e.GetPosition(DScrollViewer);
if (DScrollViewer.ZoomFactor == 1)
{
await Task.Delay(300);
DScrollViewer.ChangeView(point.X, point.Y, 2.0F, false);
}
else
{
await Task.Delay(300);
DScrollViewer.ChangeView(point.X, point.Y, 1.0F, false);
}
}
The result is: only the Canvas (its Ink) is Zooming and the StackPanel and its Images are left at the place, same scale, intact!
What Am I doing wrong?
Your mistake is you are assign a constant value for the width of each image (Width="570") so that it will never zoomed in or out unless you have change it's width programmatically.
The best way to change the image's width is bind a variable value for it, you can create a converter that divide the width of canvas over two (canvas.width / 2) and bind the width of each image to this converter..
Then your zoom will work perfectly.

Rotating a border doesn't change window size

I try to rotate a Border and have the MainWindow change his size based on the new space taken by the Border rotation.
I've set SizeToContent="WidthAndHeight" but window size does't take change when I rotated the border.
Do I need to programmatically set the Width and Height for the MainWindow or this can be achieved changing the xaml code in some other way?
My xaml code:
<Window x:Class="MyClass.MainWindow"
WindowStyle="None" AllowsTransparency='True'
Topmost='False' Background="Transparent" ShowInTaskbar='False'
SizeToContent="WidthAndHeight" WindowStartupLocation="Manual">
<Border Name="MyBorder"
BorderBrush="Transparent"
Background="Transparent"
HorizontalAlignment="Left"
VerticalAlignment="Top"
RenderTransformOrigin="0.5,0.5">
</Border>
</Windows>
My c# code on Window_KeyDown:
# RotateTransform rt = new RotateTransform() is declared at class level.
if (e.Key == Key.I)
{
if (rt.Angle + 1 < 360)
{
rt.Angle += 1;
}
else
{
rt.Angle = 0;
}
MyBorder.RenderTransform = rt;
}
Use LayoutTransform instead of RenderTransform
From MSDN: Transforms Overview
LayoutTransform – A transform that is applied before the layout pass. After the transform is applied, the layout system processes the
transformed size and position of the element.
RenderTransform – A transform that modifies the appearance of the element but is applied after the layout pass is complete. By using the
RenderTransform property instead of the LayoutTransform property, you
can obtain performance benefits.
Example
<Border Name="MyBorder"
BorderBrush="Transparent"
Background="Transparent"
HorizontalAlignment="Left"
VerticalAlignment="Top"
RenderTransformOrigin="0.5,0.5">
<Border.LayoutTransform>
<RotateTransform Angle="90"/>
</Border.LayoutTransform>
</Border>
So in your case
RotateTransform rt = new RotateTransform(0.0, 0.5, 0.5);
private void Window_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.I)
{
if (rt.Angle + 1 < 360)
{
rt.Angle += 1;
}
else
{
rt.Angle = 0;
}
MyBorder.LayoutTransform = rt;
}
}}

Categories