0

For example, implement a TwoColumnStackPanel. As the name suggests, the general StackPanel can only stack elements in one column, while my TwoColumnStackPanel can stack elements in two columns.

The TwoColumnStackPanel should distribute elements evenly in the two columns. If 4 elements, the left 2 and the right 2; 5 elements, the left 2 and right 3.

I think the TwoColumnStackPanel is actually two side-by-side StackPanels and can it be implemented using the existing StackPanel?

class TwoColumnStackPanel : Panel
{
    private readonly StackPanel leftPanel;
    private readonly StackPanel rightPanel;

    public TwoColumnStackPanel()
    {
        leftPanel = new StackPanel();
        rightPanel = new StackPanel();
    }

    protected override Size MeasureOverride(Size availableSize)
    {
        int size = InternalChildren.Count;
        int leftCount = size / 2;
        int rightCount = size - leftCount;
        //Load elements to left stackpanel.
        int index = 0;
        leftPanel.Children.Clear();
        for (int s = 0; s < leftCount; s++)
        {
            leftPanel.Children.Add(InternalChildren[index + s]);
        }
        //Load elements to right stackpanel.
        index += leftCount;
        rightPanel.Children.Clear();
        for (int s = 0; s < rightCount; s++)
        {
            rightPanel.Children.Add(InternalChildren[index + s]);//error
        }

        //Measure the two stackpanel and the sum is my desired size.
        double columnWidth = availableSize.Width / 2;

        leftPanel.Measure(new Size(columnWidth, availableSize.Height));
        rightPanel.Measure(new Size(columnWidth, availableSize.Height));

        return new Size(leftPanel.DesiredSize.Width + rightPanel.DesiredSize.Width, Math.Max(leftPanel.DesiredSize.Height, rightPanel.DesiredSize.Height));
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        leftPanel.Arrange(new Rect(0,0,leftPanel.DesiredSize.Width,leftPanel.DesiredSize.Height));
        rightPanel.Arrange(new Rect(leftPanel.DesiredSize.Width,0,rightPanel.DesiredSize.Width,rightPanel.DesiredSize.Height));

        return finalSize;
    }
}

The above code throws exception at the label line. How to fix it? Am I implementing it in the right way?

Gqqnbig
  • 5,845
  • 10
  • 45
  • 86
  • you can't move "children" like that , why not to implement the panel you want ? – ZSH Dec 18 '12 at 07:16
  • What about using a [UniformGrid](http://msdn.microsoft.com/en-us/library/system.windows.controls.primitives.uniformgrid_properties.aspx) with two [Columns](http://msdn.microsoft.com/en-us/library/system.windows.controls.primitives.uniformgrid.columns.aspx)? – Clemens Dec 18 '12 at 09:58
  • @Clemens, it is just a demo. What about implement a panel that has 10 children. 1st at top, 2nd at left, 3th at bottom, 4th at right(like DockPanel), and the remaining stacks(like StackPanel) at center? – Gqqnbig Dec 18 '12 at 10:06
  • @LoveRight If you really need to implement something like this, you should create a specialized Panel as ZSH has shown. – Clemens Dec 18 '12 at 10:46

2 Answers2

1

you should not use panels to implement panel
the better way(not perfect but it will give you the idea):

class TwoColumnStackPanel : Panel
{

    protected override Size MeasureOverride(Size availableSize)
    {   //split the size
        Size halfPanelSize = new Size(availableSize.Width / 2, availableSize.Height / 2);
        Size secondHalfPanelSize = new Size(availableSize.Width - halfPanelSize.Width, availableSize.Height - halfPanelSize.Height);
        int firstHalf = InternalChildren.Count / 2;

        for (int i = 0; i < firstHalf; i++) //measure the first column
        {
            InternalChildren[i].Measure(halfPanelSize);
            Debug.WriteLine(InternalChildren[i].DesiredSize);
        }

        for (int i = firstHalf; i < InternalChildren.Count; i++)//measure the second column
        {
            InternalChildren[i].Measure(secondHalfPanelSize);
            Debug.WriteLine(InternalChildren[i].DesiredSize);
        }

        return availableSize;
    }

    protected override Size ArrangeOverride(Size finalSize)
    {
        Size halfPanelSize = new Size(finalSize.Width / 2, finalSize.Height / 2);
        Size secondHalfPanelSize = new Size(finalSize.Width - halfPanelSize.Width, finalSize.Height - halfPanelSize.Height);
        int firstHalf = InternalChildren.Count / 2;
        Point location = new Point();


        for (int i = 0; i < firstHalf; i++) 
        {// arrange from (0,0) and add the height
            InternalChildren[i].Arrange(new Rect(location.X, location.Y, halfPanelSize.Width, InternalChildren[i].DesiredSize.Height));
            location.Y += InternalChildren[i].DesiredSize.Height;

        }

        location.X = halfPanelSize.Width; // move to the next column
        location.Y = 0;

        for (int i = firstHalf; i < InternalChildren.Count ; i++)
        {// arrange from (firts column width,0) and add the height
            InternalChildren[i].Arrange(new Rect(location.X, location.Y, secondHalfPanelSize.Width, InternalChildren[i].DesiredSize.Height));
            location.Y += InternalChildren[i].DesiredSize.Height;
        }

        return finalSize;
    }
}
ZSH
  • 905
  • 5
  • 15
  • Thank you very much for your complete code. I really do not want to re-invent the algorithms that DockPanel and StackPanel use. My actual project needs to use a panel with the behavior of DockPanel and StackPanel.(See my previous comment) If I have such panel, I can put it into ListBox, and arrange and select items easily. – Gqqnbig Dec 18 '12 at 10:12
  • no problem , this is the only way i see to create new panel , and its easier to use – ZSH Dec 18 '12 at 16:20
1

I know this is old, but there's actually a much easier way to implement this where you can use the existing functionality of two (or more) existing panels to get the behavior you want.

First, subclass UniformGrid, not a plain Panel. You only need to subclass a panel when you want to do the arranging. You don't. You want your internal stack panels to do that. You're just distributing them to the internal panels. (You also mentioned a dock panel but that would mean you'd have to also specify docked attached properties, but either way, this code is exactly the same.

*Note: Feel free to use any panel as the root. I just picked UniformGrid so the StackPanels would be side-by-side. But you can nest any panels within any other panels. Completely up to you.

Next, in the constructor for your new subclass, you add the two internal StackPanels to the UniformGrid, one on the left, one on the right. Note that this means your 'Children' collection will actually return the two StackPanels, not the controls you're adding. This is ok because we're no longer going to use the Children property. You're going to create your own property to use for your provided children.

Do that now. Create a new property called StackPanelChildren of type ObservableCollection, then on your class, add the attribute [ContentProperty("StackPanelChildren")] which tells the XAML processor anything between the opening and closing tags of your control are inserted into that property, not the normal Children property. Next, in the constructor of the class add a Collection Changed handler so you know when children have been added or removed. Then simply, in that handler you add or remove items from the two internal StackPanels as needed.

To be really complete, you may want to override CreateUIElementCollection (which backs the actual Children property) so you can return a read-only version of it so people can't mess with your internal StackPanels. I'll leave it up to you to figure out that part since it isn't actually needed.

Here's some pseudo-code (typed from my head so it may not compile, but you get the idea...

[ContentProperty("StackPanelChildren")]
public class TwoColumnStackPanel : UniformGrid
{
    private readonly StackPanel leftStackPanel = new StackPanel();
    private readonly StackPanel rightStackPanel = new StackPanel();

    public TwoColumnStackPanel()
    {
        this.Rows = 1;
        this.Columns = 2;
        this.Children.Add(leftStackPanel);
        this.Children.Add(rightStackPanel);

        StackPanelChildren.CollectionChanged += StackPanelChildren_CollectionChanged;
    }

    // Note: You should make this a read-only Dependency property
    // I'm just doing it this way for brevity in typing
    private readonly ObservableCollection<UIElement> _stackPanelChildren = new ObservableCollection<UIElement>(); 
    public ObservableCollection<UIElement> StackPanelChildren
    {
        get{ return _stackPanelChildren; }
    }

    private void StackPanelChildren_CollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
    {
        // Add your child item distribution logic here. Make sure to account for
        // all new items, all removed items, and the Reset action which
        // clears all items but doesn't actually provide them on the argument
        // since you just assume 'everything must go!'
    } 

}

Additional Info:

You can add additional properties similar to the new StackPanelChildren property. However only one can be used as the default content. Still, you can specify any of them in XAML if you explicitly call them out. For instance, say for some reason you want a second collection of items, perhaps they only go in the left panel. That property is called AdditionalLeftPanelChildren. You'd just define it the exact same way in code (an ObservableCollection) and access like this...

<MyCustomPanel>

    <TextBlock Text="I'm in the normal content for the StackPanels." />
    <TextBlock Text="So am I!" />

    <MyCustomPanel.AdditionalLeftPanelChildren>

        <TextBlock Text="I go to the other property! Woot!" />

    </MyCustomPanel.AdditionalLeftPanelChildren>

</MyCustomPanel>
Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
  • It's an amazing idea! Thanks so much. – Gqqnbig Apr 28 '15 at 07:06
  • Thanks! I've come to really love that ContentProperty attribute. Makes creating XAML-friendly controls really easy. Oh... and if you do like this, mind voting it up, and/or marking it as the accepted solution then? Much appreciated! :) – Mark A. Donohoe Apr 30 '15 at 07:49