Reputation: 4996
Heres example:
<Window x:Class="ListViewItemSpacing.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:c="clr-namespace:ListViewItemSpacing.Controls"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ListViewItemSpacing"
mc:Ignorable="d"
Title="MainWindow" Height="350" Width="525">
<Grid>
<ListView FlowDirection="LeftToRight" Background="#222">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Width="{Binding (FrameworkElement.ActualWidth), RelativeSource={RelativeSource AncestorType=ScrollContentPresenter}}" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.Items>
<Rectangle Fill="#27f" Width="100" Height="100"/>
<Rectangle Fill="#27f" Width="100" Height="100"/>
<Rectangle Fill="#27f" Width="100" Height="100"/>
<Rectangle Fill="#27f" Width="100" Height="100"/>
<Rectangle Fill="#27f" Width="100" Height="100"/>
<Rectangle Fill="#27f" Width="100" Height="100"/>
</ListView.Items>
</ListView>
</Grid>
</Window>
This XAML
produces 6 blue squares, 4 in the first row, 2 in the next row. After 4 first blue squares there is some space on the right.
In other words - it looks like left aligned text.
I want it to look like justified text, so I want the horizontal spacing between the squares adjusted to align ListView
to left and right, space evenly distributed between elements, not on the right side.
Although it looks trivial to do, I don't know how to do it. I don't know where to even start. For example - I can see some spacing added by default. Ugly by default, because horizontal spacing is much greater than vertical, but it's not big enough to align the items to both left and right side. But where does the spacing come from? How to change it even manually? I don't want to mess with the elements themselves. Elements in real world app would come from different module and those modules should be well separated.
Most obvious thought is to handle SizeChanged
event of the ListView
and adjust horizontal spacing manually, however - WHERE is the spacing? I mean how can I access items spacing in code behind?
To clarify: the desired rendering should still contain 6 identical blue squares, but first 4 aligned to left and right control edge, the remaining 2 aligned to the left and to previous row squares.
Default look is like this:
----------------
|[] [] [] [] |
|[] [] |
----------------
Desired look is like this:
----------------
|[] [] [] []|
|[] [] |
----------------
Upvotes: 1
Views: 1606
Reputation: 4996
Thanks to Rachel and DTig I finally made it. It seems like there's no panel capable of aligning elements like WrapPanel
but with customizable HorizontalContentAlignment
. And obviously - HorizontalContentAlignment = HorizontalAlignment.Stretch
is what I needed here.
Following Rachel advice I tried this, but it didn't support HorizontalAlignment.Stretch
value.
So I added the support, which works provided element widths are equal to each other:
/// <summary>
/// <see cref="Panel"/> like <see cref="WrapPanel"/> which supports <see cref="HorizontalContentAlignment"/> property.
/// </summary>
public class AlignableWrapPanel : Panel {
/// <summary>
/// <see cref="HorizontalAlignment"/> property definition.
/// </summary>
public static readonly DependencyProperty HorizontalContentAlignmentProperty =
DependencyProperty.Register(
"HorizontalContentAlignment",
typeof(HorizontalAlignment),
typeof(AlignableWrapPanel),
new FrameworkPropertyMetadata(HorizontalAlignment.Left, FrameworkPropertyMetadataOptions.AffectsArrange)
);
/// <summary>
/// Gets or sets the horizontal alignment of the control's content.
/// </summary>
[BindableAttribute(true)]
public HorizontalAlignment HorizontalContentAlignment {
get { return (HorizontalAlignment)GetValue(HorizontalContentAlignmentProperty); }
set { SetValue(HorizontalContentAlignmentProperty, value); }
}
/// <summary>
/// Measures the size in layout required for child elements and determines a size for the <see cref="AlignableWrapPanel"/>.
/// </summary>
/// <param name="constraint">The available size that this element can give to child elements. Infinity can be specified as a value to indicate that the element will size to whatever content is available.</param>
/// <returns>The size that this element determines it needs during layout, based on its calculations of child element sizes.</returns>
protected override Size MeasureOverride(Size constraint) {
var curLineSize = new Size();
var panelSize = new Size();
var children = base.InternalChildren;
for (var i = 0; i < children.Count; i++) {
var child = children[i] as UIElement;
// Flow passes its own constraint to children
child.Measure(constraint);
var sz = child.DesiredSize;
if (curLineSize.Width + sz.Width > constraint.Width) { //need to switch to another line
panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width);
panelSize.Height += curLineSize.Height;
curLineSize = sz;
if (sz.Width > constraint.Width) { // if the element is wider then the constraint - give it a separate line
panelSize.Width = Math.Max(sz.Width, panelSize.Width);
panelSize.Height += sz.Height;
curLineSize = new Size();
}
}
else { //continue to accumulate a line
curLineSize.Width += sz.Width;
curLineSize.Height = Math.Max(sz.Height, curLineSize.Height);
}
}
// the last line size, if any need to be added
panelSize.Width = Math.Max(curLineSize.Width, panelSize.Width);
panelSize.Height += curLineSize.Height;
return panelSize;
}
/// <summary>
/// Positions child elements and determines a size for a <see cref="AlignableWrapPanel"/>.
/// </summary>
/// <param name="arrangeBounds">The final area within the parent that this element should use to arrange itself and its children.</param>
/// <returns>The actual size used.</returns>
protected override Size ArrangeOverride(Size arrangeBounds) {
var firstInLine = 0;
var curLineSize = new Size();
var accumulatedHeight = 0.0;
var children = InternalChildren;
for (var i = 0; i < children.Count; i++) {
var sz = children[i].DesiredSize;
if (curLineSize.Width + sz.Width > arrangeBounds.Width) { //need to switch to another line
ArrangeLine(accumulatedHeight, curLineSize, arrangeBounds.Width, firstInLine, i);
accumulatedHeight += curLineSize.Height;
curLineSize = sz;
if (sz.Width > arrangeBounds.Width) { //the element is wider then the constraint - give it a separate line
ArrangeLine(accumulatedHeight, sz, arrangeBounds.Width, i, ++i);
accumulatedHeight += sz.Height;
curLineSize = new Size();
}
firstInLine = i;
}
else { //continue to accumulate a line
curLineSize.Width += sz.Width;
curLineSize.Height = Math.Max(sz.Height, curLineSize.Height);
}
}
if (firstInLine < children.Count)
ArrangeLine(accumulatedHeight, curLineSize, arrangeBounds.Width, firstInLine, children.Count);
return arrangeBounds;
}
/// <summary>
/// Arranges elements within a line.
/// </summary>
/// <param name="y">Line vertical coordinate.</param>
/// <param name="lineSize">Size of the items line.</param>
/// <param name="boundsWidth">Width of the panel bounds.</param>
/// <param name="start">Index of the first child which belongs to the line.</param>
/// <param name="end">Index of the last child which belongs to the line.</param>
private void ArrangeLine(double y, Size lineSize, double boundsWidth, int start, int end) {
var children = InternalChildren;
var x = 0.0;
var stretchOffset = 0.0;
if (HorizontalContentAlignment == HorizontalAlignment.Center) x = (boundsWidth - lineSize.Width) / 2;
else if (HorizontalContentAlignment == HorizontalAlignment.Right) x = (boundsWidth - lineSize.Width);
else if (HorizontalAlignment == HorizontalAlignment.Stretch) {
var childWidth = children[start].DesiredSize.Width; // warning, this works only when all children have equal widths
int n = (int)boundsWidth / (int)childWidth;
if (children.Count > n) {
var takenWidth = n * childWidth;
var spaceWidth = boundsWidth - takenWidth;
stretchOffset = spaceWidth / (n - 1);
}
}
for (var i = start; i < end; i++) {
var child = children[i];
child.Arrange(new Rect(x, y, child.DesiredSize.Width, lineSize.Height));
x += child.DesiredSize.Width + stretchOffset;
}
}
}
This is basically Tig's solution with Stretch
alignment added.
Here's test XAML for this:
<Window
x:Class="ListViewItemSpacing.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:c="clr-namespace:CustomControls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:ListViewItemSpacing"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
Title="MainWindow"
Width="525"
Height="250"
mc:Ignorable="d">
<Grid>
<ListView Background="#222">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<c:AlignableWrapPanel Width="{Binding (FrameworkElement.ActualWidth), RelativeSource={RelativeSource AncestorType=ScrollContentPresenter}}" HorizontalContentAlignment="Stretch" />
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.Items>
<Rectangle
Width="100"
Height="100"
Fill="#27f" />
<Rectangle
Width="100"
Height="100"
Fill="#27f" />
<Rectangle
Width="100"
Height="100"
Fill="#27f" />
<Rectangle
Width="100"
Height="100"
Fill="#27f" />
<Rectangle
Width="100"
Height="100"
Fill="#27f" />
<Rectangle
Width="100"
Height="100"
Fill="#27f" />
</ListView.Items>
</ListView>
</Grid>
</Window>
It's not perfect, but it does the specific job just perfectly. It won't work properly if the sizes of child elements would be different - in such case takenWidth
should be calculated as sum of children with indices from start
to end
. You should also specify a different condition for the last line.
Thanks again, kind strangers :)
Upvotes: 3
Reputation: 3760
Let say we don't know the number of columns. The complete code for the windows xaml and code behind (Fix the class name to match your own). Try this:
Xaml:
<Window x:Class="SO39640127.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:so39640127="clr-namespace:SO39640127"
mc:Ignorable="d"
Title="MainWindow"
Height="{Binding LayoutHeight, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Width="{Binding LayoutWidth,Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
d:DataContext="{d:DesignInstance so39640127:MainWindow}">
<Grid>
<ListView Background="#222" VerticalContentAlignment="Top">
<ListView.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid HorizontalAlignment="Center" VerticalAlignment="Top"
Columns="{Binding LayoutColumns, UpdateSourceTrigger=PropertyChanged}"
Width="{Binding (FrameworkElement.ActualWidth), RelativeSource={RelativeSource AncestorType=ScrollContentPresenter}}"></UniformGrid>
</ItemsPanelTemplate>
</ListView.ItemsPanel>
<ListView.Items>
<Rectangle Fill="#27f" Width="{Binding ItemWidth}" Height="{Binding ItemHeight}" Margin="{Binding VerticalSpacing}"/>
<Rectangle Fill="#27f" Width="{Binding ItemWidth}" Height="{Binding ItemHeight}" Margin="{Binding VerticalSpacing}"/>
<Rectangle Fill="#27f" Width="{Binding ItemWidth}" Height="{Binding ItemHeight}" Margin="{Binding VerticalSpacing}"/>
<Rectangle Fill="#27f" Width="{Binding ItemWidth}" Height="{Binding ItemHeight}" Margin="{Binding VerticalSpacing}"/>
<Rectangle Fill="#27f" Width="{Binding ItemWidth}" Height="{Binding ItemHeight}" Margin="{Binding VerticalSpacing}"/>
<Rectangle Fill="#27f" Width="{Binding ItemWidth}" Height="{Binding ItemHeight}" Margin="{Binding VerticalSpacing}"/>
</ListView.Items>
</ListView>
</Grid>
</Window>
Codebehind:
public partial class MainWindow : Window, INotifyPropertyChanged
{
private double itemHeight;
private double itemWidth;
private int layoutColumns;
private int layoutHeight;
private int layoutWidth;
private Thickness verticalSpacing;
public MainWindow()
{
InitializeComponent();
DataContext = this;
InitializeDesign();
}
public double ItemWidth
{
get { return itemWidth; }
set
{
if (value.Equals(itemWidth))
return;
itemWidth = value;
OnPropertyChanged();
}
}
public double ItemHeight
{
get { return itemHeight; }
set
{
if (value.Equals(itemHeight))
return;
itemHeight = value;
OnPropertyChanged();
}
}
public int LayoutColumns
{
get { return layoutColumns; }
set
{
if (value == layoutColumns)
return;
layoutColumns = value;
OnPropertyChanged();
}
}
public int LayoutWidth
{
get { return layoutWidth; }
set
{
if (value == layoutWidth)
return;
layoutWidth = value;
OnPropertyChanged();
UpdateCalculations();
}
}
public int LayoutHeight
{
get { return layoutHeight; }
set
{
if (value == layoutHeight)
return;
layoutHeight = value;
OnPropertyChanged();
UpdateCalculations();
}
}
public Thickness VerticalSpacing
{
get { return verticalSpacing; }
set
{
if (value.Equals(verticalSpacing))
return;
verticalSpacing = value;
OnPropertyChanged();
}
}
public event PropertyChangedEventHandler PropertyChanged;
private void UpdateCalculations()
{
//Calculate the # of elements that fit on the list view
var totalItems = (int) (LayoutWidth/ItemWidth);
LayoutColumns = totalItems;
}
private void InitializeDesign()
{
LayoutWidth = 525;
LayoutHeight = 350;
ItemWidth = 100;
ItemHeight = 100;
VerticalSpacing = new Thickness(3);
}
protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
{
var handler = PropertyChanged;
if (handler != null)
handler(this, new PropertyChangedEventArgs(propertyName));
}
}
I'm achieving vertical spacing through margin in the items. You can try resizing in order to see how the columns adjust correctly depending on the dimensions of the window. Also, I'm using a little bit of MVVM to do the bindings for demo purposes. You should of course adapt it accordingly.
Hope this helps!
Upvotes: 0