0

Here's what I want to achieve:

Under certain circumstances there shall be an overlay visible on top of all other elements in a window. This part is working.

The main container in the Windows is a TabControl and the overlay has a margin large enough to be displayed below the tab headers (where the content area is located).

The problem starts when the tab headers span two lines (or when the size of the font would change). Is there a way to place the overlay relative to the area where the content part of the TabControl would start?

Onur
  • 5,017
  • 5
  • 38
  • 54

3 Answers3

1

I suggest you look at inserting your overlay into the TabControl ControlTemplate, so it becomes part of the visual tree where the TabItem is drawn - and will therefore resize accordingly.

This is a extremely simplified example for clarity:

<ControlTemplate TargetType="{x:Type TabControl}">
    <StackPanel>
        <TabPanel />
        <Grid>

            <ContentPresenter x:Name="ContentTop" 
                        HorizontalAlignment="{TemplateBinding HorizontalAlignment}"
                        VerticalAlignment="{TemplateBinding VerticalAlignment}"
                        Margin="{TemplateBinding Padding}"
                        Cursor="{TemplateBinding Cursor}" />

            <YourOverlayControl x:Name="OverlayControl"></YourOverlayControl>

        </Grid>
    </StackPanel>
</ControlTemplate>

The main point is that your overlay control is presented on top of the ContentPresenter (within a grid control). The ContentPresenter in this template will be displaying your TabItem content.

If you haven't already defined a custom control template for the TabControl, check out this article on MSDN for a full featured example.

olitee
  • 1,683
  • 10
  • 12
  • Your solution works but it seems like a bit "too much" for me for this quit simple task. Especially since I couldn't find the default TabControl control template for .net 4.5. – Onur Jul 01 '15 at 08:18
  • The problem is there's no simple way to bind to the TabPanel within the TabControl, to ascertain it's actual height ... which is what you're really wanting to achieve? Another approach might be to create a custom implementation of the TabControl that exposes the TabPanel height. I'll add another answer. – olitee Jul 01 '15 at 10:50
1

Another approach might be to create a custom implementation of a TabControl that exposes a new dependency property: TabPanelActualHeight.

This assumes you're using the default control template, but should also work for any template that includes a TabPanel (TabPanel isn't a required part of the TabControl template).

public class CustomTabControl : TabControl
{
    private static readonly DependencyPropertyKey ReadOnlyPropPropertyKey
                            = DependencyProperty.RegisterReadOnly("ReadOnlyProp", typeof(double), typeof(CustomTabControl),
                                new FrameworkPropertyMetadata((double)0,
                                    FrameworkPropertyMetadataOptions.None));

    public static readonly DependencyProperty TabPanelActualHeightProperty = ReadOnlyPropPropertyKey.DependencyProperty;

    public double TabPanelActualHeight
    {
        get { return (double)GetValue(TabPanelActualHeightProperty); }
        protected set { SetValue(ReadOnlyPropPropertyKey, value); }
    }

    private TabPanel _tabPanel = null;

    public override void OnApplyTemplate()
    {
        base.OnApplyTemplate();

        _tabPanel = this.GetChildOfType<TabPanel>();

        if (_tabPanel != null)
        {
            TabPanelActualHeight = _tabPanel.ActualHeight;
            _tabPanel.SizeChanged += TabPanelOnSizeChanged;
        }

    }

    private void TabPanelOnSizeChanged(object sender, SizeChangedEventArgs sizeChangedEventArgs)
    {
        TabPanelActualHeight = _tabPanel.ActualHeight;
    }
}

NOTE: This example depends on the GetChildOfType<T> extension method from this SO post.

You can now simply bind to the TabPanelActualHeight property of the custom TabControl to help you offset/position the overlay.

Community
  • 1
  • 1
olitee
  • 1,683
  • 10
  • 12
  • Thanks for this answer. It inspired me to create an attached property that does roughly the same as your dependency property in the derived class. – Onur Jul 01 '15 at 15:14
  • Ah, yes an attached property is another way to go. Thanks for sharing your solution. – olitee Jul 01 '15 at 15:16
0

Inspired by this answer I created an attached property that allows me to extract the height of the tab panel. I use this value to create the margin I need for my overlay. I prefer this version because

  • I don't have to create a new style/modify the default style
  • Don't have to create a new control just for one small aspect

Here's the code for the attached property.

public static class TabPanelHeaderHeight
{
    private const double InitialValue = -1.0;

    private static readonly DependencyProperty HeaderHeightProperty =
        DependencyProperty.RegisterAttached(name: "HeaderHeight", propertyType: typeof(double), ownerType: typeof(TabPanelHeaderHeight),
        defaultMetadata: new FrameworkPropertyMetadata(defaultValue: InitialValue, flags: FrameworkPropertyMetadataOptions.Inherits | FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, propertyChangedCallback: OnHeaderHeightChanged));

    public static double GetHeaderHeight(UIElement element)
    {
        return (double)element.GetValue(HeaderHeightProperty);
    }
    public static void SetHeaderHeight(UIElement element, double value)
    {
        element.SetValue(HeaderHeightProperty,value);
    }

    private static void OnHeaderHeightChanged(DependencyObject obj, DependencyPropertyChangedEventArgs args)
    {
        var target = obj as TabControl;
        if (target == null)
            return;

        // we hijack the on value changed event to register our event handler to the value we're really interested in
        // but we want to do this only once, so we check if the value is the (invalid) initial value and change the value afterwards to register the event listener only once.
        if ((double)args.OldValue == InitialValue)
        {
            var tp = target.GetChildOfType<TabPanel>();

            tp.SizeChanged += (sender, eventArgs) => { TargetSizeChanged(target,tp); };
            TargetSizeChanged(target,tp);
        }
    }

    private static void TargetSizeChanged(TabControl target, TabPanel tp)
    {
        SetHeaderHeight(target, tp.ActualHeight);
    }

    public static T GetChildOfType<T>(this DependencyObject depObj) where T : DependencyObject
    {
        if (depObj == null) return null;

        for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
        {
            var child = VisualTreeHelper.GetChild(depObj, i);

            var result = (child as T) ?? GetChildOfType<T>(child);
            if (result != null) return result;
        }
        return null;
    }

}

the GetChildOfType helper is from this post

It could be used in xaml like this:

<TabControl  yourNs:TabPanelHeaderHeight.HeaderHeight="{Binding Path=HeaderHeight}" >
...

Either use a value converter to create the margin for the overlay or accomplish this in the view model.

Community
  • 1
  • 1
Onur
  • 5,017
  • 5
  • 38
  • 54