0

I have a requirement where I need to align some buttons within a fixed area of the screen. The chosen layout behaves/looks as follows;

alignment requirements

Specifically, I need a 3 button layout where the buttons reach the edges, a 2 button layout that has equals spacing between buttons and to the edge of the container, and a single button layout that's just simply centred.

I've tried/considered;

  • Negative margin on container, positive margin on buttons. (Problem is that the "2 button" layout does not have equal spacing)
  • Manually calculating the margins for each control based on the number of elements in the collection using IValueConverters (Problem is that it seems overly complicated for what I'm trying to achieve, lots of maths..)
  • Creating 3 separate layouts and choosing the correct one based on collection size. (Problem because it feels so, so, so very hacky)

Key Points:

  • For layout 2, the gaps between the buttons should be equal to the size of the gap that reaches to the edge of the container (see the second layout in image).
  • There is a logic swap between layout 2 and 3, layout 3 doesn't have "outside gaps", it only has gaps between each button.
  • There is no 4 layout, no requirement for it.

So my question is: What's the "correct" way to achieve this spacing requirement? I'm looking for a simple and fairly standard way to do it.

Conclusion:

I thought it might be interesting to note that I ended up not being able to do this. The mathsy solution and the "custom control" solutions would have worked if I'd tried harder, but I ended up taking the easy route and just "centralised a listview" and had done with it.

Dan Rayson
  • 1,315
  • 1
  • 14
  • 37
  • 1
    Maybe you could change [UniformGrid Source Code](https://referencesource.microsoft.com/#PresentationFramework/src/Framework/System/Windows/Controls/Primitives/UniformGrid.cs,fd8042c6510b4f30) to behave as you want. I think you can't run from math in this case. – Avestura Jul 27 '18 at 13:32
  • @0xaryan Thanks for the suggestion - I've also provided that same advise to people in the past, to modifying WPF source code. Unfortunately that's not possible in my scenario. – Dan Rayson Jul 27 '18 at 13:47
  • Couldn't that be achieved using `Grid` and defining dynamic spacing (using `*` notation) ? – scharette Jul 27 '18 at 13:48
  • @scharette Afraid not - the content of the grid columns needs to be included in the calculation of the gaps, it's not based on cell size, it's based on item size, if that makes sense. – Dan Rayson Jul 27 '18 at 13:56

3 Answers3

2

First create a new Panel named DanRaysonPanel.

Edited:

class DanRaysonPanel : ItemsControl
{
    /// <summary>
    /// Holds a Grid as ItemsPanel
    /// </summary>
    public Grid GridContainer { get; set; }

    public void Refresh() => OnItemsChanged(null);

    protected override void OnItemsChanged(NotifyCollectionChangedEventArgs e)
    {

        var grid = GridContainer;

        if (grid == null) return;

        grid.ColumnDefinitions.Clear();

        if (Items.Count == 1) return;

        if ((Items.Count & 1) == 0) // Element count is even
        {
            for (int i = 0; i < Items.Count * 2 + 1; i++)
            {
                if ((i & 1) == 1)
                {
                    grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = GridLength.Auto });
                    Grid.SetColumn(Items[i / 2] as UIElement, i);
                }
                else grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) });
            }
        }
        else
        {

            for (int i = 0; i < Items.Count * 2 - 1; i++)
            {
                if ((i & 1) == 0)
                {
                    grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = GridLength.Auto });
                    Grid.SetColumn(Items[i/2] as UIElement, i);
                }
                else grid.ColumnDefinitions.Add(new ColumnDefinition() { Width = new GridLength(1, GridUnitType.Star) });
            }
        }

    }

}

Now add an instance of this panel to your view:

  <local:DanRaysonPanel x:Name="RaysonPanel">
    <local:DanRaysonPanel.ItemsPanel>
        <ItemsPanelTemplate>
            <Grid HorizontalAlignment="Stretch" Loaded="Grid_Loaded">
                <Grid.RowDefinitions>
                    <RowDefinition />
                </Grid.RowDefinitions>
            </Grid>
        </ItemsPanelTemplate>
    </local:DanRaysonPanel.ItemsPanel>

    <Button Width="100" Height="50" />

</local:DanRaysonPanel>

That ItemsPanel should declare it's ItemsPanel property explicitly. But you can edit the items as you want (Manual or Programmatically).

For the final step, you need to handle Grid_Loaded event.

  private void Grid_Loaded(object sender, RoutedEventArgs e)
    {
        RaysonPanel.GridContainer = sender as Grid;
        RaysonPanel.Refresh();
    }

P.S. The only that made this code too long is that ItemsControl.ItemsPanel doesn't give you the declared Grid in code-behind. If you found a way for that, you can make this code much simpler.

Avestura
  • 1,438
  • 14
  • 24
  • Having tried this one, it's extremely close to being the right answer. There's one anomaly, when it's the 1-button layout it'll left align iirc. I did have to debug it a little as the `Items[i / 2] as UIElement` was failing with the way I was databinding it (I was databinding to a list of strings). – Dan Rayson Jul 31 '18 at 09:07
  • @DanRayson Anomaly can fix easily, just add `if (Items.Count == 1) return;` above the main `if` in `OnItemsChanged` event handler. Code already modified to fix this. – Avestura Jul 31 '18 at 11:13
  • @DanRayson `Grid.SetColumn` requires a `UIElement` as parameter, not a `string`. You might need to wrap your string into to a `TextBlock` before binding it. – Avestura Jul 31 '18 at 11:16
0

This will work:

<ItemsControl ItemsSource="{Binding List}" Margin="-40,0">
    <ItemsControl.ItemsPanel>
        <ItemsPanelTemplate>
            <UniformGrid 
                  Columns="{Binding ListCount, Converter={StaticResource PlusOne}}" 
                  Rows="1"/>
        </ItemsPanelTemplate>
    </ItemsControl.ItemsPanel>

    <ItemsControl.ItemContainerStyle>
        <Style TargetType="ContentPresenter">
            <Setter Property="HorizontalAlignment" Value="Right"/>
            <Setter Property="Margin" Value="0,0,-40,0"/>
        </Style>
    </ItemsControl.ItemContainerStyle>

    <ItemsControl.ItemTemplate>
        <DataTemplate>
            <Button Width="80" Height="55" Text="{Binding ItemName}"/>
        </DataTemplate>
    </ItemsControl.ItemTemplate>
</ItemsControl>

PlusOne is a converter that returns its input + 1

Note that, ItemContainerStyle is used to move each item of ItemsControl to its proper position. Left and Right margins of ItemsControl, as well as Right field of the margin, should be equal to negative of half the width of each item defined in ItemTemplate.

Another note that, you can also use a Grid as the ItemsPanelTemplate and follow the instructions in this answer to have a bindable ColumnDefinitions. However, it takes WAY more effort than the provided code

Bizhan
  • 16,157
  • 9
  • 63
  • 101
  • It's close, but not quite. The solution you've shown doesn't scale the gaps correctly depending on container size.. I've made a test app with your suggestion, and I can see the layout isn't quite as I asked for, close, but not quite. – Dan Rayson Jul 27 '18 at 15:02
  • As an example: https://imgur.com/mebdv4m You can see the two outer buttons aren't placed at the sides of the container, they're floating further inward. – Dan Rayson Jul 27 '18 at 15:05
  • @DanRayson yes I understand. That can be fixed using a `Margin` on `ItemsControl`. I've updated my answer. – Bizhan Jul 27 '18 at 15:06
  • Unfortunately not... There's a lot of strange things happen when you start resizing the container... – Dan Rayson Jul 27 '18 at 15:27
  • @DanRayson I found the problem. set the margin of `ItemsControl` to `-40,0` instead of `40,0` and it should work now. Oh wait. you need to replace `List.Count()` with a bindable property as well. – Bizhan Jul 27 '18 at 15:57
0

I would put the buttons in a StackPanel and have the left margin of the Buttons define the spacing, but make the first button 0. Center the StackPanel with HoriztonalAlignment.

kenny
  • 21,522
  • 8
  • 49
  • 87
  • How would you calculate/arrive at the correct widths for the margins? – Dan Rayson Jul 31 '18 at 12:54
  • the way I read the drawing is the margins are "fixed", maybe that's not true. Certainly can calculate it as the other method, but that's not how I interpreted the requirements. – kenny Jul 31 '18 at 13:26
  • I've updated the question in the hope it's more explanatory. Thanks. – Dan Rayson Jul 31 '18 at 13:51