Reputation: 20737
I've the following code:
<ItemsControl ItemsSource="{Binding SubItems}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"></WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="*"></RowDefinition>
<RowDefinition Height="Auto"></RowDefinition>
</Grid.RowDefinitions>
<Image Source="{Binding Image}" ></Image>
<TextBlock Text="{Binding Name}" Grid.Row="1" HorizontalAlignment="Center"/>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
Currently if I run this code, every item(grid) tries to take the full space available and I've only 1-2 items visible over the 20+ I've in my SubItems
collections.
If I set a MaxWidth
to my Grid, I see all of them, but when I maximize the window, I've a lot of free space.
If I don't set any width, I've this:
If I set a width and increase the size, I've this:
The goal is to have something like the second case, but without having to set a width, and having it scale if I increase the window size.
Edit2
I tried with UniformGrid, but two issues. With two elements, it seems it absolutely wants to have 4 column and 3 rows. Even if would be better with 3 column 4 rows:
Also, when the window is reduced, the images are cut:
Upvotes: 3
Views: 2513
Reputation: 8823
Create Your DataTemplate
like this:
<DataTemplate>
<Grid Height="{Binding RelativeSource={RelativeSource Self},Path=ActualWidth,Mode=OneWay}">
<Grid.Width>
<MultiBinding Converter="{StaticResource Converter}">
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="ActualWidth" Mode="OneWay" />
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="ActualHeight" Mode="OneWay" />
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="DataContext.SubItems.Count" />
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="ActualWidth" />
</MultiBinding>
</Grid.Width>
<Grid.RowDefinitions>
Converter:
public class Converter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
double TotalWidth = System.Convert.ToDouble(values[0]), TotalHeight = System.Convert.ToDouble(values[1]);
int TotalItems = System.Convert.ToInt32(values[2]);
var TotalArea = TotalWidth * TotalHeight;
var AreasOfAnItem = TotalArea / TotalItems;
var SideOfitem = Math.Sqrt(AreasOfAnItem);
var ItemsInCurrentWidth = Math.Floor(TotalWidth / SideOfitem);
var ItemsInCurrentHeight = Math.Floor(TotalHeight / SideOfitem);
while (ItemsInCurrentWidth * ItemsInCurrentHeight < TotalItems)
{
SideOfitem -= 1;//Keep decreasing the side of item unless every item is fit in current shape of window
ItemsInCurrentWidth = Math.Floor(TotalWidth / SideOfitem);
ItemsInCurrentHeight = Math.Floor(TotalHeight / SideOfitem);
}
return SideOfitem;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
{
return null;
}
}
Explanation of Logic: The approach is very simple. Calculate the area of the ItemsControl
and divide the area equal in all items. That is also the best scenario possible visually. So if we have 20 items in your list and area is 2000 unit(square shape) then each item gets 100 unit of area to render.
Now the tricky part is area of ItemsControl
can't be in square shape always but items will be in square shape always. So if we want to display all items without any area getting trimmed by overflow we need to reduce area of every items till its fit in current shape. The while
loop in Converter
does that by calculating if all items are fully visible
or not. If all items are not fully visible it knows the size needs to be reduced.
NOTE: Every item will be of same
Height
&Width
(square area). That's whyHeight
ofGrid
is bound toWidth
ofGrid
, we need not to calculate that.
OutPut:
Full Screen:
Upvotes: 0
Reputation: 175
This is rather ambiguous but have you tried using Blend for Visual Studio? It is very good at assisting in, not only debugging, but also designing the UI for a WPF application. In the long run, it may be best as you don't have to maintain any custom controls/bindings.
Upvotes: 0
Reputation: 101493
If nothing else will help, consider writing your own panel. I don't have time now for a complete solution, but consider this.
First, tiling rectangle with squares the way you want is not quite trivial. This is known as packing problem and solutions are often hard to find (depends on the concrete problem). I have taken algorithm to find approximate tile size from this question: Max square size for unknown number inside rectangle.
When you have square size for given width and height of your panel, the rest is easier:
public class AdjustableWrapPanel : Panel {
protected override Size MeasureOverride(Size availableSize) {
// get tile size
var tileSize = GetTileSize((int) availableSize.Width, (int) availableSize.Height, this.InternalChildren.Count);
foreach (UIElement child in this.InternalChildren) {
// measure each child with a square it should occupy
child.Measure(new Size(tileSize, tileSize));
}
return availableSize;
}
protected override Size ArrangeOverride(Size finalSize) {
var tileSize = GetTileSize((int)finalSize.Width, (int)finalSize.Height, this.InternalChildren.Count);
int x = 0, y = 0;
foreach (UIElement child in this.InternalChildren)
{
// arrange in square
child.Arrange(new Rect(new Point(x,y), new Size(tileSize, tileSize)));
x += tileSize;
if (x + tileSize >= finalSize.Width) {
// if need to move on next row - do that
x = 0;
y += tileSize;
}
}
return finalSize;
}
int GetTileSize(int width, int height, int tileCount)
{
if (width*height < tileCount) {
return 0;
}
// come up with an initial guess
double aspect = (double)height / width;
double xf = Math.Sqrt(tileCount / aspect);
double yf = xf * aspect;
int x = (int)Math.Max(1.0, Math.Floor(xf));
int y = (int)Math.Max(1.0, Math.Floor(yf));
int x_size = (int)Math.Floor((double)width / x);
int y_size = (int)Math.Floor((double)height / y);
int tileSize = Math.Min(x_size, y_size);
// test our guess:
x = (int)Math.Floor((double)width / tileSize);
y = (int)Math.Floor((double)height / tileSize);
if (x * y < tileCount) // we guessed too high
{
if (((x + 1) * y < tileCount) && (x * (y + 1) < tileCount))
{
// case 2: the upper bound is correct
// compute the tileSize that will
// result in (x+1)*(y+1) tiles
x_size = (int)Math.Floor((double)width / (x + 1));
y_size = (int)Math.Floor((double)height / (y + 1));
tileSize = Math.Min(x_size, y_size);
}
else
{
// case 3: solve an equation to determine
// the final x and y dimensions
// and then compute the tileSize
// that results in those dimensions
int test_x = (int)Math.Ceiling((double)tileCount / y);
int test_y = (int)Math.Ceiling((double)tileCount / x);
x_size = (int)Math.Min(Math.Floor((double)width / test_x), Math.Floor((double)height / y));
y_size = (int)Math.Min(Math.Floor((double)width / x), Math.Floor((double)height / test_y));
tileSize = Math.Max(x_size, y_size);
}
}
return tileSize;
}
}
Upvotes: 4
Reputation: 1660
You can try this.
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
Upvotes: 0
Reputation: 2975
You need to change your RowDefinition
to look something more like this;
<RowDefinition Height="*"/>
One of your rows is set to Auto
, this will attempt to fill only the space it needs. The other is set to *
, this will automatically stretch to fill all the space it can.
Notice there is also no need to type </RowDefinition>
, you can simply end in />
. This link might be of particular use to you;
Upvotes: -1