J4N
J4N

Reputation: 20737

WPF: Resize item size to have all items visible

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: enter image description here

If I set a width and increase the size, I've this: enter image description here

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: enter image description here

Also, when the window is reduced, the images are cut: enter image description here

Upvotes: 3

Views: 2513

Answers (5)

Kylo Ren
Kylo Ren

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 why Height of Grid is bound to Width of Grid, we need not to calculate that.

OutPut:

1 2 3

Full Screen:

4

Upvotes: 0

Anthony Mason
Anthony Mason

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

Evk
Evk

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

bars222
bars222

Reputation: 1660

You can try this.

<ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
        <UniformGrid />
    </ItemsPanelTemplate>
</ItemsControl.ItemsPanel>

Upvotes: 0

CBreeze
CBreeze

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;

Difference Between * and auto

Upvotes: -1

Related Questions