WPF Custom Control/Control template

4k views Asked by At

I'm building a wpf application with a custom control and everything worked so far.
But now i encountered two problems:

  1. I want to assign a background color to my control but that overlays the rectangle inside the grid, so the rectangle becomes invisible.
  2. I tried to write a template for a ContentControl but the content does not render as expected, meaning only the display name does show up with the text of each progress bar.

The template for my custom control (if the code behind is of interest i'll add that as well):

<Style TargetType="{x:Type local:MetroProgressBar}">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="{x:Type local:MetroProgressBar}">
                <Grid Background="{TemplateBinding Background}">
                    <Rectangle Fill="{TemplateBinding ProgressBrush}" HorizontalAlignment="Left"
                               VerticalAlignment="Stretch" Width="{TemplateBinding ProgressBarWidth}"
                               Visibility="{TemplateBinding IsHorizontal, Converter={StaticResource BoolToVis}}"/>

                    <Rectangle Fill="{TemplateBinding ProgressBrush}" HorizontalAlignment="Stretch"
                               VerticalAlignment="Bottom" Height="{TemplateBinding ProgressBarHeight}"
                               Visibility="{TemplateBinding IsVertical, Converter={StaticResource BoolToVis}}"/>

                    <Border
                        Background="{TemplateBinding Background}"
                        BorderBrush="{TemplateBinding BorderBrush}"
                        BorderThickness="{TemplateBinding BorderThickness}"/>

                    <TextBlock VerticalAlignment="Center" HorizontalAlignment="Center"
                               Text="{TemplateBinding Text}"
                               FontSize="{TemplateBinding FontSize}" FontStyle="{TemplateBinding FontStyle}" FontWeight="{TemplateBinding FontWeight}"
                               FontFamily="{TemplateBinding FontFamily}" FontStretch="{TemplateBinding FontStretch}"
                               Foreground="{TemplateBinding Foreground}" TextWrapping="Wrap"/>

                    <Polygon Fill="{TemplateBinding BorderBrush}" Points="{TemplateBinding LeftBorderTriangle}"/>
                    <Polygon Fill="{TemplateBinding BorderBrush}" Points="{TemplateBinding RightBorderTriangle}"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>  

The template for the ContentControl:

<vm:RamViewModel x:Key="RamInformationSource"/>

<Style TargetType="ContentControl" x:Key="MemoryUsageTemplate">
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate>
                <Grid DataContext="{Binding Source={StaticResource RamInformationSource}}">
                    <Grid.ColumnDefinitions>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="*"/>
                        <ColumnDefinition Width="*"/>
                    </Grid.ColumnDefinitions>

                    <TextBlock HorizontalAlignment="Center" Text="{Binding DisplayName}" VerticalAlignment="Center"
                               FontSize="15"/>

                    <ctrl:MetroProgressBar Grid.Column="1" VerticalAlignment="Stretch" Width="55" Orientation="Vertical" HorizontalAlignment="Center"
                                           ExtenedBorderWidth="0.25" BorderBrush="Gray" Text="Available memory" Progress="{Binding AvailableMemory}"
                                           MaxValue="{Binding TotalMemory}"/>

                    <ctrl:MetroProgressBar Grid.Column="2" VerticalAlignment="Stretch" Width="60" Orientation="Vertical" HorizontalAlignment="Center"
                                           ExtenedBorderWidth="0.2" BorderBrush="Black" Text="Total memory" Progress="100"
                                           MaxValue="{Binding TotalMemory}"/>

                    <ctrl:MetroProgressBar Grid.Column="3" VerticalAlignment="Stretch" Width="60" Orientation="Vertical" HorizontalAlignment="Center"
                                           ExtenedBorderWidth="0.2" BorderBrush="DodgerBlue" Text="Used memory" Progress="{Binding UsedMemory}"
                                           MaxValue="{Binding TotalMemory}"/>
                </Grid>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

The xaml that displays the content:

...
<ContentControl Style="{StaticResource MemoryUsageTemplate}"/>

<ctrl:MetroProgressBar Grid.Row="1" BorderBrush="Black" Text="Test" HorizontalAlignment="Left" Background="Aquamarine"
                       Orientation="Horizontal" BorderThickness="2" Height="50" Width="200" Progress="46"/>

<ctrl:MetroProgressBar Grid.Row="1" BorderBrush="Black" Text="Test" HorizontalAlignment="Right"
                       Orientation="Horizontal" BorderThickness="2" Height="50" Width="200" Progress="46"/>
...  

Top: ContentControl with the template applied, left bottom: custom control with background set, right bottom: custom control with no background set

Top of the image shows the content control with the template applied. The bottom shows the two progress bars as defined in the last xaml (left with background, right without). That are all custom DPs that are defined for the control:

/// <summary>
/// Identifies the ExtenedBorderWidth property.
/// </summary>
public static readonly DependencyProperty ExtenedBorderWidthProperty =
    DependencyProperty.Register("ExtenedBorderWidth", typeof(double), typeof(MetroProgressBar),
                                    new PropertyMetadata(0.17, ExtendedBorderWidthValueChanged));

/// <summary>
/// Identifies the Text property.
/// </summary>
public static readonly DependencyProperty TextProperty =
    DependencyProperty.Register("Text", typeof(string), typeof(MetroProgressBar), new PropertyMetadata(""));

/// <summary>
/// Identifies the Orientation property.
/// </summary>
public static readonly DependencyProperty OrientationProperty =
    DependencyProperty.Register("Orientation", typeof(Orientation), typeof(MetroProgressBar), new PropertyMetadata(Orientation.Horizontal, OrientationValueChanged));

/// <summary>
/// Identifies the IsHorizontal property.
/// </summary>
public static readonly DependencyProperty IsHorizontalProperty =
    DependencyProperty.Register("IsHorizontal", typeof(bool), typeof(MetroProgressBar), new PropertyMetadata(true, OrientationChangedByProperty));

/// <summary>
/// Identifies the IsVertical property.
/// </summary>
public static readonly DependencyProperty IsVerticalProperty =
    DependencyProperty.Register("IsVertical", typeof(bool), typeof(MetroProgressBar), new PropertyMetadata(false, OrientationChangedByProperty));

/// <summary>
/// Identifies the ProgressBrush property.
/// </summary>
public static readonly DependencyProperty ProgressBrushProperty =
    DependencyProperty.Register("ProgressBrush", typeof(Brush), typeof(MetroProgressBar), new PropertyMetadata(new SolidColorBrush(Colors.LightGreen)));

/// <summary>
/// Identifies the LeftBorderTriangle property.
/// </summary>
public static readonly DependencyProperty LeftBorderTriangleProperty =
    DependencyProperty.Register("LeftBorderTriangle", typeof(PointCollection), typeof(MetroProgressBar), new PropertyMetadata(new PointCollection()));

/// <summary>
/// Identifies the RightBorderTriangle property.
/// </summary>
public static readonly DependencyProperty RightBorderTriangleProperty =
    DependencyProperty.Register("RightBorderTriangle", typeof(PointCollection), typeof(MetroProgressBar), new PropertyMetadata(new PointCollection()));

/// <summary>
/// Identifies the MaxValue property.
/// </summary>
public static readonly DependencyProperty MaxValueProperty =
    DependencyProperty.Register("MaxValue", typeof(ulong), typeof(MetroProgressBar), new PropertyMetadata(100UL, MaxValueChanged));

/// <summary>
/// Identifies the Progress property.
/// </summary>
public static readonly DependencyProperty ProgressProperty =
    DependencyProperty.Register("Progress", typeof(double), typeof(MetroProgressBar), new PropertyMetadata(0.0d, ProgressValueChanged));

/// <summary>
/// Identifies the ProgressBarWidth property.
/// </summary>
public static readonly DependencyProperty ProgressBarWidthProperty
    = DependencyProperty.Register("ProgressBarWidth", typeof(double), typeof(MetroProgressBar), new PropertyMetadata(0.0d));

/// <summary>
/// Identifies the ProgressBarHeight property.
/// </summary>
public static readonly DependencyProperty ProgressBarHeightProperty
    = DependencyProperty.Register("ProgressBarHeight", typeof(double), typeof(MetroProgressBar), new PropertyMetadata(0.0d));

The DP value changed callbacks and instance methods:

#region Static

/// <summary>
/// Changes the orientation based on the calling property.
/// </summary>
/// <param name="source">The source of the event.</param>
/// <param name="e">Event information.</param>
private static void OrientationChangedByProperty(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
    //lock (lockOrientationByProperty)
    {
        MetroProgressBar pb = source as MetroProgressBar;

        if (e.Property == IsVerticalProperty)
        {
            if ((bool)e.NewValue)
            {
                pb.IsHorizontal = false;
                pb.Orientation = Orientation.Vertical;
            }
            else
            {
                pb.IsHorizontal = true;
                pb.Orientation = Orientation.Horizontal;
            }
        }
        else
        {
            // IsVerticalProperty is property that changed
            if (!(bool)e.NewValue)
            {
                pb.IsHorizontal = false;
                pb.Orientation = Orientation.Vertical;
            }
            else
            {
                pb.IsHorizontal = true;
                pb.Orientation = Orientation.Horizontal;
            }
        }

        AdjustVisibleProgressRect(pb);
    }
}

/// <summary>
/// Sets the progress value to the new maximum value, if the new max value is less than
/// the current progress value.
/// </summary>
/// <param name="source">The source of the event.</param>
/// <param name="e">Event information.</param>
private static void MaxValueChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
    //lock (lockMaxValue)
    {
        MetroProgressBar pb = source as MetroProgressBar;

        ulong val = Convert.ToUInt64(e.NewValue);
        if (val < Convert.ToUInt64(pb.Progress))
        {
            pb.Progress = val;

            // Raise finished event
            pb.OnFinished(EventArgs.Empty);
        }
    }
}

/// <summary>
/// Changes the width of the progress indication rectangle of the progress bar.
/// </summary>
/// <param name="source">The source of the event.</param>
/// <param name="e">Event information.</param>
private static void ProgressValueChanged(DependencyObject source, DependencyPropertyChangedEventArgs e)
{
    //lock (lockProgress)
    {
        MetroProgressBar pb = source as MetroProgressBar;
        AdjustVisibleProgressRect(pb, (double)e.NewValue);

        pb.OnProgressChanged(new ProgressChangedEventArgs((double)e.NewValue));

        // If new progress value equals or is greater than max value raise the finished event
        if (pb.MaxValue <= Convert.ToUInt64(e.NewValue))
            pb.OnFinished(EventArgs.Empty);
    }
}

/// <summary>
/// Changes the width of the progress indication rectangle of the progress bar.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">Event information.</param>
private static void OrientationValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    //lock (lockOrientation)
    {
        MetroProgressBar pb = sender as MetroProgressBar;
        pb.AdjustToOrientationChange();

        if (pb.Orientation == Orientation.Horizontal)
        {
            pb.IsVertical = false;
            pb.IsHorizontal = true;
        }
        else
        {
            pb.IsVertical = true;
            pb.IsHorizontal = false;
        }

        pb.OnOrientationChanged(new OrientationChangedEventArgs((Orientation)e.OldValue, (Orientation)e.NewValue));
    }
}

/// <summary>
/// Causes the progress bar to reassign the extended border.
/// </summary>
/// <param name="sender">The source of the event.</param>
/// <param name="e">Event information.</param>
private static void ExtendedBorderWidthValueChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
{
    //lock (lockExtendedBorder)
    {
        MetroProgressBar pb = sender as MetroProgressBar;
        pb.SetUpBorderParts();
    }
}

/// <summary>
/// Adjusts the progress bars visible progress rectangles after progress or visible changes.
/// </summary>
/// <param name="pb">The progress bar that changed.</param>
/// <param name="newValue">The new progress value. Only has to be set if there has been a progress change.</param>
private static void AdjustVisibleProgressRect(MetroProgressBar pb, double newValue = -1)
{
    if (pb.Orientation == Orientation.Horizontal)
    {
        pb.ProgressBarWidth = (newValue == -1 ? pb.Progress : newValue) / pb.MaxValue * pb.Width;
    }
    else
    {
        pb.ProgressBarHeight = (newValue == -1 ? pb.Progress : newValue) / pb.MaxValue * pb.Height;
    }
}

#endregion

#region Non-Static

/// <summary>
/// Adjusts the border ornaments to the new orientation of the control.
/// </summary>
private void AdjustToOrientationChange()
{
    SetUpBorderParts();
}

/// <summary>
/// Sets up the triangles that are placed on the left and right side of the progress bar.
/// </summary>
private void SetUpBorderParts()
{
    PointCollection leftBorder = new PointCollection();
    PointCollection rightBorder = new PointCollection();

    double borderWidth = ExtenedBorderWidth;

    if (Orientation == Orientation.Horizontal)
    {
        // Left triangle
        leftBorder.Add(new Point(0, 0));
        leftBorder.Add(new Point(0, Height));
        leftBorder.Add(new Point(Width * borderWidth, 0));

        // Right triangle
        rightBorder.Add(new Point(Width, 0));
        rightBorder.Add(new Point(Width, Height));
        rightBorder.Add(new Point(Width - (Width * borderWidth), Height));
    }
    else
    {
        // Top border
        leftBorder.Add(new Point(0, 0));
        leftBorder.Add(new Point(Width, 0));
        leftBorder.Add(new Point(0, Height * borderWidth));

        // Bottom border
        rightBorder.Add(new Point(0, Height));
        rightBorder.Add(new Point(Width, Height));
        rightBorder.Add(new Point(Width, Height - (Height * borderWidth)));
    }

    LeftBorderTriangle = leftBorder;
    RightBorderTriangle = rightBorder;
}

/// <summary>
/// Raises the Fnished event.
/// </summary>
/// <param name="e">Information on the event.</param>
protected virtual void OnFinished(EventArgs e)
{
    EventHandler handler = finished;

    if (handler != null)
    {
        handler(this, e);
    }
}

/// <summary>
/// Raises the ProgressChanged event.
/// </summary>
/// <param name="e">Information on the event.</param>
protected virtual void OnProgressChanged(ProgressChangedEventArgs e)
{
    EventHandler<ProgressChangedEventArgs> handler = progressChanged;

    if (handler != null)
    {
        handler(this, e);
    }
}

/// <summary>
/// Raises the OrientationChanged event.
/// </summary>
/// <param name="e">Information on the event.</param>
protected virtual void OnOrientationChanged(OrientationChangedEventArgs e)
{
    EventHandler<OrientationChangedEventArgs> handler = orientationChanged;

    if (handler != null)
    {
        handler(this, e);
    }
}

/// <summary>
/// Raises the RenderSizeChanged event and sets up the border parts.
/// </summary>
/// <param name="sizeInfo">Info on the size change.</param>
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
    base.OnRenderSizeChanged(sizeInfo);
    SetUpBorderParts();
    AdjustVisibleProgressRect(this);
}

#endregion
1

There are 1 answers

0
Streamline On BEST ANSWER

I found the answer to my first problem... Basically the Border element of the progress bar had its Background-property bound to the Control-background and since it is after the Rectangle in the visual tree it overlayed both of them.

The second problem occurred because i used Height and Width instead of ActualHeight and ActualWidth in the code of the user control. So when working with e.g. HorizontalAlignment.Stretch the Width/Height properties are not set and therefore all calculations based on them do not work.