3

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

J4N
  • 19,480
  • 39
  • 187
  • 340

5 Answers5

4

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;
    }
}
Community
  • 1
  • 1
Evk
  • 98,527
  • 8
  • 141
  • 191
  • I've to give it a try, but I must admit I've never done my own panel. Also your algorithm seems to be interessant, but work only for square(in my case they are kind of rectangle. If I find a way to retrieve the "ideal" ratio "width/height" of my controls, that would be nice. I found a little bit weird that there is no component already doing this. – J4N Apr 13 '16 at 05:54
  • Does panel I provided satisfy your needs if controls are squares (did you test it I mean)? If yes, we can figure out a way for non square controls. – Evk Apr 13 '16 at 06:57
  • Unfortunately I'm not at home and I cannot check for now, I will give it a try this evening :) – J4N Apr 13 '16 at 11:01
  • I think I've found an algorithm that suits me well to compute the Height/Width, so I'm starting from your example. Can you tell me where your "InternalChildren" comes from? – J4N Apr 18 '16 at 17:40
  • Note that my panel is inherited from wpf Panel class, and InternalChildren is one of its properties. – Evk Apr 18 '16 at 17:42
  • If you want to render children non-square just do child.Arrange(new Rect(new Point(x,y), child.DesiredSize)); in arrange (or use your calculated width and height instead of just tileSize) – Evk Apr 18 '16 at 17:56
  • In fact I had to use `Children`(I moved my code to a UWP app finally). The "last" issue that I'm having is that the "Desired size" == (0, 0), which doesn't help me to find the correct width/height ratio. Is there a way to get their MinWidth/MinHeight? – J4N Apr 18 '16 at 18:04
  • After you called Measure on child control, like on code above, child should set its DesiredSize. – Evk Apr 18 '16 at 18:07
  • I finally made it work :) I'm quite happy with the result, thank you very much. I was just wondering if you can think of an easy way to "animate" the transition between two layouts? – J4N Apr 18 '16 at 18:29
  • Yes, look at animated panels. There are several, some are not free (Telerik has such), some are free. For example: http://www.codeproject.com/Articles/153554/Animated-WPF-Panels (there is C# code attached, though in article they use VB). There are also blend behaviors that might help: https://blogs.msdn.microsoft.com/mcsuksoldev/2011/04/07/animated-panel-layout-transitions-in-wpf-and-silverlight-using-blend-behaviors/ – Evk Apr 18 '16 at 20:07
  • Well I just did a custom panel ^^, I've the feeling that either I will have to drop my implementation either to animated them? – J4N Apr 18 '16 at 20:10
  • Look at Extending Existing Panels block in the article I mentioned above. – Evk Apr 18 '16 at 20:12
  • @Evk I tested this logic, and there are some sizes of panel for which items are getting overflown and are not fully visible. I think this logic needs some more work. – Kylo Ren Apr 20 '16 at 05:33
  • @KyloRen maybe your elements has margins\paddings - algorithm above does not take this into account. But anyway author already found algorithm that suits his needs - I wanted to just show how you can write custom panel to solve layout problems. – Evk Apr 20 '16 at 06:38
  • @Evk No that's not the case, in some sizes it actually cause to overflow a very large part of subelements so it can be margin etc – Kylo Ren Apr 20 '16 at 06:45
0

You can try this.

<ItemsControl.ItemsPanel>
    <ItemsPanelTemplate>
        <UniformGrid />
    </ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
bars222
  • 1,652
  • 3
  • 13
  • 14
0

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

Kylo Ren
  • 8,551
  • 6
  • 41
  • 66
  • I will check your solution this evening, but I cannot know for sure the ratio of my sub elements. I've another solution in progress which takes this in account – J4N Apr 18 '16 at 05:03
  • @J4N If you don't know the size ratio of your sub elements then you can use same converter for Height of Grid and remove the Height Binding. But looking at the pictures provided in question they all look of same size essentially of same Height & Width to me. But I think regardless of this all sub elements needs to be re sized in same ratio when parent Width/height change. That's what my logic is based on. Am I getting your problem right or do you mean something else by saying, "ratio of my sub elements"? – Kylo Ren Apr 18 '16 at 05:16
  • @J4N Also if large images cause problem of overflow then instead of Grid Height/Width binding , bind Height/Width of Image. – Kylo Ren Apr 18 '16 at 05:21
  • I don't know the size ratio either(it's a pane that will be reused with different item template. The idea is to use the ratio between their Min/Max size as the "ideal" ratio. You're correct, every element will have exactly the same width/height, but like you see, you have an image, but also some text, which makes the element higher than wide. – J4N Apr 18 '16 at 09:10
  • @J4N ok. but even the images has Text with them, some will have less some will have large, but regardless the no of objects for each sub item they all are encapsulated in a single item(ItemsControl Item). So problem again becomes to arrange n no of rectangle shapes(or n no of images). which i think above logic will handle. Am I missing something? – Kylo Ren Apr 18 '16 at 09:22
  • Well, your case doesn't use the whole space(if extra space is available on the right, we need to increase a little bit each element). Also if the element is twice as high as wide, it will not work. – J4N Apr 18 '16 at 12:47
  • @J4N actually it's cause I consider the sub elements in square size. So if I try to increase the width I have to deal with increased height too. As you can see in the logic, elements covers all space available first any then start reducing their size to fit all elements. But your sub elements are of square size you yourself said so, so there is no way you will be able to cover all space. – Kylo Ren Apr 18 '16 at 12:57
  • @J4N mathematically, if your elements have 8 unit width and 8 unit height. So every element will take 64 unit square area. so any area which is multiple of 64 can be divided into n elements, but not other nos. as a very basic any natural no that is no divisible by any number less then it, can't be divided into equal pieces. hope ablove make sense. – Kylo Ren Apr 18 '16 at 13:04
  • No, I didn't, even in the other answers I state that my elements are not square, sorry :( And the fact it's not a square modify consequently the layout(I can have some layout that require to have 6x2 layouts because the item has twice the height than the width. – J4N Apr 18 '16 at 16:42
  • @J4N in one the comment above you said "every element will have exactly the same width/height". But if not I'll update the answer in the evening for your requirement – Kylo Ren Apr 19 '16 at 05:05
  • Yes, every element will have the same width/height, but the width is not the same as the height. I just wanted to say that every element will have the same dimensions. – J4N Apr 19 '16 at 09:38
  • @J4N you know if Height can't be same as Width then the requirement is absurd. I mean I can fit 100 elements in 100 width and 500 height rectangle in one row. There must be some ratio in between Height & Width. And you already marked the answer even that gives the same solution of square subitems? And to be candid that will give you wrong layout in some sizes.... – Kylo Ren Apr 20 '16 at 04:32
  • Yes, the accepted answer gave me a good direction with a custom panel, in which I was able to take in account, so I used his custom panel idea with the width/height algo I found – J4N Apr 20 '16 at 06:14
  • Also, I'm basing myself on the ratio of the MinWidth and MinHeight to know the "expected" width/height ratio :) – J4N Apr 20 '16 at 07:17
  • @J4N actually the marked algo is creating problem in some layout sizes, so it needs to be looked into. and for size i think square size is best for over all look&fell and performance of resizing also. – Kylo Ren Apr 20 '16 at 07:21
  • I will put tonight the final solution I ended with, and some screenshot on how it renders – J4N Apr 20 '16 at 08:04
  • @J4N did you find the complete solution? – Kylo Ren Apr 27 '16 at 04:05
  • I just put it as a part of a small project of mine here: https://github.com/jgrossrieder/AstroApp/blob/master/AstroApp/UserControls/WrapGridPanel.cs (sorry, the project is not compiling for now, I was in a rush to push it – J4N Apr 27 '16 at 20:18
  • @J4N thanks, solution works very smooth... But for prime number it gives very weird sizes..like really long rectangles... – Kylo Ren Apr 28 '16 at 09:40
  • Do you have an example? With which min/max? – J4N Apr 30 '16 at 04:41
0

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.

Anthony Mason
  • 165
  • 1
  • 12
-1

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

CBreeze
  • 2,925
  • 4
  • 38
  • 93
  • Well, my image is very big, I don't want that it use the whole space, only the space that allows to have all elements fitting in it. I think you didn't understood me. I need to have each grid sized to have all elements of the ItemsControl fitting in the window. – J4N Dec 08 '15 at 11:52
  • So have you tried instead to use `` for both rows? This way it will designate the space required for all the elements. If the image is too large then maybe it might be a case of resizing the image? – CBreeze Dec 08 '15 at 11:54
  • Yes I did, But since my image is very big(to be able to have its full resolution when the application it's full screen), having it to Auto try to fit the full image – J4N Dec 08 '15 at 12:00
  • I added some screenshot to illustrate. The screenshot has been taken with the Height="auto" – J4N Dec 08 '15 at 12:06