Is it possible to style the contents of a DockPane

2020-04-08 13:16发布

问题:

Consider this XAML snippet...

<DockPanel x:Name="TestDockPanel">

    <Button x:Name="RightButton" Content="Right" DockPanel.Dock="Right" />
    <Button x:Name="FillButton" Content="Fill" />

</DockPanel>

As written, the DockPanel will layout 'RightButton' to the right, then fill the rest of the area with 'FillButton' like this...

We're trying to find a way to style it so that when 'FillButton' has its visibility changed to 'Collapsed', 'RightButton' should now fill the area, like this...

The only way we know how to do this is to physically remove 'FillButton' from the children of 'TestDockPanel' but that requires code-behind, which we're trying to avoid.

Update

Below in an answer, I've posed a solution based on a subclass. However, I'm leaving this open since I'd like something that can be used with any DockPanel (or other subclass) and preferrably applied via a style or an attached behavior. Also, to be clear, a requirement of the solution is that it must be based on a DockPanel, not a grid or other panel.

回答1:

I created a solution in the form of a DockPanel subclass, but I'm not marking this as the accepted answer because I'm hoping to still find a way to do this via styles or attached behaviors so it can be used with any DockPanel (or other subclass), not just this.

Still, for others, this may be helpful so I'm posting it here.

The code for the full class is below. The meat of the work is in the ArrangeOverride method, which was based on the original logic from DockPanel as extracted from Reflector.

The way the existing logic worked was inside ArrangeOverride, if LastChildFill was set it stored the index of the last child (i.e. the index of the item to be filled) in a variable. If LastChildFill wasn't set, it instead stored 'count' in that variable.

Then when looping through the children performing the actual arrange, if the element being arranged had an index less than the previously stored index, it performed the 'docking' logic, otherwise it performed 'fill' logic.

That meant when LastChildFill was false, every element ran the 'docking' logic since they all had an index below that stored index (which again equals 'count', or highest index + 1). However, when LastChildFill was true, the last element didn't have an index less than the stored index (it was actually equal to it), so that one element ran the 'fill' logic while everything else ran the 'docking' logic.

The change I made was if LastChildFill is set, as above, the stored index starts out pointing to the last child, but I then check the visibility of that element and if it's invisible, I lower the index by one and check again, continuing until I either find a visible element, or I run out of children to check (i.e. if they were all invisible.) That's also why I named the variable 'firstFilledIndex' since technically that, and all elements afterwards use the 'Fill' logic, even though all the elements after it are invisible.

I finally added a new LastVisibleChildFill property to enable or disable my new behavior. As a help to consumers, if you set that to true, it implicitly also set LastChildFill to true for you.

Here's the full code.

public class DockPanelEx : DockPanel
{
    public static readonly DependencyProperty LastVisibleChildFillProperty = DependencyProperty.Register(
        "LastVisibleChildFill",
        typeof(bool),
        typeof(DockPanelEx),
        new UIPropertyMetadata(true, (s,e) => {

            var dockPanelEx = (DockPanelEx)s;
            var newValue = (bool)e.NewValue;

            if(newValue)
                dockPanelEx.LastChildFill = true; // Implicitly enable LastChildFill
            // Note: For completeness, we may consider putting in code to set
            // LastVisibileChildFill to false if LastChildFill is set to false

        }));

    /// <summary>
    /// Indicates that LastChildFill should fill the last visible child
    /// Note: When set to true, automatically also sets LastChildFill to true as well.
    /// </summary>
    public bool LastVisibleChildFill
    {
        get { return (bool)GetValue(LastVisibleChildFillProperty); }
        set { SetValue(LastVisibleChildFillProperty, value); }
    }

    protected override Size ArrangeOverride(Size totalAvailableSize)
    {
        UIElementCollection internalChildren = base.InternalChildren;
        int count = internalChildren.Count;

        int firstFilledIndex = count;

        if(LastChildFill)
        {
            for(firstFilledIndex = count - 1; firstFilledIndex >= 0; firstFilledIndex--)
            {
                if(!LastVisibleChildFill || internalChildren[firstFilledIndex].IsVisible)   
                    break;
            }
        }

        double usedLeftEdge   = 0.0;
        double usedTopEdge    = 0.0;
        double usedRightEdge  = 0.0;
        double usedBottomEdge = 0.0;

        for (int i = 0; i < count; i++)
        {
            UIElement element = internalChildren[i];
            if (element != null)
            {
                Size desiredSize = element.DesiredSize;

                var finalRect = new Rect(
                    usedLeftEdge,
                    usedTopEdge,
                    Math.Max(0.0, (totalAvailableSize.Width  - (usedLeftEdge + usedRightEdge))),
                    Math.Max(0.0, (totalAvailableSize.Height - (usedTopEdge  + usedBottomEdge))));

                if (i < firstFilledIndex)
                {
                    switch (GetDock(element))
                    {
                        case Dock.Left:
                            usedLeftEdge += desiredSize.Width;
                            finalRect.Width = desiredSize.Width;
                            break;

                        case Dock.Top:
                            usedTopEdge += desiredSize.Height;
                            finalRect.Height = desiredSize.Height;
                            break;

                        case Dock.Right:
                            usedRightEdge += desiredSize.Width;
                            finalRect.X = Math.Max((double) 0.0, (double) (totalAvailableSize.Width - usedRightEdge));
                            finalRect.Width = desiredSize.Width;
                            break;

                        case Dock.Bottom:
                            usedBottomEdge += desiredSize.Height;
                            finalRect.Y = Math.Max((double) 0.0, (double) (totalAvailableSize.Height - usedBottomEdge));
                            finalRect.Height = desiredSize.Height;
                            break;
                    }
                }
                element.Arrange(finalRect);
            }
        }
        return totalAvailableSize;
    }
}