0
votes

I'm trying to make a custom progress bar in WPF that has two values (the second is always equal to or higher than the first). The basic bar works ok like so:

<wpft:ClippingBorder BorderBrush="{StaticResource Border}"
                     Background="{StaticResource Background}"
                     BorderThickness="1" CornerRadius="4">
    <Grid Margin="-1" x:Name="Bars">
        <Border BorderBrush="{StaticResource Border}"
                Background="{Binding Value2Brush}"
                BorderThickness="1" CornerRadius="4"
                HorizontalAlignment="Left"
                Width="{Binding Value2Width}" />
        <Border BorderBrush="{StaticResource Border}"
                Background="{Binding Value1Brush}"
                BorderThickness="1" CornerRadius="4"
                HorizontalAlignment="Left"
                Width="{Binding Value1Width}" />
    </Grid>
</wpft:ClippingBorder>

(Where ClippingBorder is this. It's used to prevent glitching at the outer corners when the values are near 0.)

The net result is a nice rounded display: Progress image

Zoomed view, to more clearly show the rounded corners:

zoomed

In particular note that both of the inner bars share the same outer border and their right edge curves to the left, just like the outer border.

This works because it draws the longer bar first, then the shorter one on top of it. However, it only works reliably when the brushes are fully opaque -- in particular if Value1Brush is partially transparent then some of Value2Brush will show through it, which I don't want.

Ideally, I want the longer bar to only draw that portion of itself that extends beyond the shorter bar -- or equivalently, to set the clipping/opacity mask of the longer bar to be transparent in the area where the shorter bar is drawn.

But I'm not sure how to do that without losing the rounded corners.

2
What you want is to stop people using partially transparent colours for the top most progress bar? If not, then you would need another border underneath the top most one with background set to the background of the UC or window or whatever the parent is of this progress bar (nothing that binding can't solve), this way a different colour would bleed through, otherwise transparency will do what it is supposed to.XAMlMAX
No, I want transparent colours for the bars to work as expected. This means that the grey background should show through a translucent bar, but I don't want the longer bar colour showing through the shorter bar colour at all.Miral
In other words, I am looking for a way to do one of the following: 1. When drawing the longer bar, draw it as normal but clip out the region that will be covered by the shorter bar so that that part remains transparent (with the grey background). Either with Clip or with OpacityMask, but I don't know how to do a rounded rect with the former or an inverse mask with the latter. 2. Without using clipping/opacity, produce the same effect; ie. draw only the part of the longer bar that extends past the shorter bar -- but including filling in the "inverse corners" where the bars meet.Miral

2 Answers

0
votes

This is not a general solution, unfortunately, but it seems to work for this case. I had to give an x:Name to each of the internal borders and then put this in the code behind:

Constructor:

DependencyPropertyDescriptor.FromProperty(ActualWidthProperty, typeof(Border))
    .AddValueChanged(OuterBar, Child_ActualWidthChanged);
DependencyPropertyDescriptor.FromProperty(ActualWidthProperty, typeof(Border))
    .AddValueChanged(InnerBar, Child_ActualWidthChanged);

Handler:

private void Child_ActualWidthChanged(object sender, EventArgs e)
{
    var outerRect = new Rect(OuterBar.RenderSize);
    outerRect.Inflate(5, 5);
    var outer = new RectangleGeometry(outerRect);

    var corner = InnerBar.CornerRadius.TopLeft;
    var inner = new RectangleGeometry(new Rect(InnerBar.RenderSize), corner, corner);

    OuterBar.Clip = new GeometryGroup()
    {
        Children = { outer, inner }
    };
}

The basic idea is to start with a rectangle slightly larger than what the outer bar wants to draw, and then add a rectangle that exactly matches what the inner bar wants to draw -- this clips it out of the geometry. The whole is then used as a clip region for the outer bar so that it can't draw inside the inner bar's region.

I originally tried to do this in XAML with the following, but it didn't work (the converter was not called when the width changed); I'm not sure why, but just for posterity:

<Border.Clip>
    <GeometryGroup>
        <RectangleGeometry Rect="{Binding ElementName=OuterBar, Path=RenderSize,
            Converter={StaticResource BoundsConverter}, ConverterParameter=5.0}" />
        <RectangleGeometry Rect="{Binding ElementName=InnerBar, Path=RenderSize,
            Converter={StaticResource BoundsConverter}}" RadiusX="4" RadiusY="4" />
    </GeometryGroup>
</Border.Clip>

(Here the converter would take the RenderSize and make a Rect, with optional inflation, similar to the code above.)

-1
votes

I would try to use grid's columns. Putting two columns in the grid, first on with width="auto" second as "*".Put short border in the first column the other to the second column. When you change the width of your border column will resize accordingly to your border's width.