문제

Background:

I have a basic Clock CustomControl that has a DateTime property that updates every second. Its default ControlTemplate is an AnalogClockFace CustomControl, but I also have a DigitalClockFace CustomControl that I can set as the Template value to have a digital display.

Recently, I thought I'd get adventurous and try making a FlipDownClockFace CustomControl, something like the old fashioned clocks where the numbers were split vertically half way up and displayed on rolls that would turn and flip down the next number as time progressed. Unfortunately I pretty much fell at the first, well second hurdle.

The problem:

To simplify this problem somewhat, I need to display two TextBlocks, with each displaying opposite halves of a Text value. By that I mean that one should display the top half of a TextBlock.Text value and the other one should display the bottom half of a TextBlock.Text value. Actually, I can clip (show) the top half of the TextBlock ok, it's displaying the bottom half that's the problem.

To be clear, I need the clip area to automatically resize to fit the TextBlock regardless of the FontSize value, so I cant just hard code a Height value. This question just relates to the clipping of the TextBlock and I will sort out all the Transform Animations required for my FlipDownClockFace control later.

What I tried so far:

I managed to find out that I could use the RenderedGeometry property of a Rectangle element as the TextBlock.Clip value and so I thought that I could just pop a Rectangle in each half of a Grid to clip the two (overlayed) TextBlocks. This example shows a reconstruction of where it went wrong (the DoubleDivisorConverter just divides the data bound value (the Height) by the value of the ConverterParameter (in this case, 2.0)):

<Grid TextElement.FontSize="72">
    <Grid.RowDefinitions>
        <RowDefinition />
        <RowDefinition />
    </Grid.RowDefinitions>
    <TextBlock Grid.Row="0" Grid.RowSpan="2" Name="FirstValueBottomTextBox" Text="23" 
Clip="{Binding RenderedGeometry, ElementName=FirstValueBottomClipRectangle}" 
VerticalAlignment="Center" HorizontalAlignment="Center" Background="LightBlue" />
    <Rectangle Name="FirstValueBottomClipRectangle" Grid.Row="1" Height="{Binding 
ActualHeight, ElementName=FirstValueBottomTextBox, Converter={StaticResource 
DoubleDivisorConverter}, ConverterParameter=2.0}" VerticalAlignment="Top" />
</Grid>

Basically I worked out that the problem is that RenderedGeometry just uses the Geometry of the Rectangle and not the position, so this only ever clips (shows) the top half of the text, regardless of the actual position of the Rectangle. I couldn't find any way to move the clip area to the bottom to just show the bottom half of the text.

Before you suggest it, I can't do this either, as the Rect.Height property is unfortunately not a DependencyProperty:

<TextBlock Grid.Row="0" Grid.RowSpan="2" Name="FirstValueBottomTextBox" Text="23"
VerticalAlignment="Center" HorizontalAlignment="Center" Background="LightBlue">
    <TextBlock.Clip>
        <RectangleGeometry>
            <RectangleGeometry.Rect>
                <Rect Height="{Binding ActualHeight, ElementName=
FirstValueBottomTextBox, Converter={StaticResource DoubleDivisorConverter}, 
ConverterParameter=2.0}" />
            </RectangleGeometry.Rect>
        </RectangleGeometry>
    </TextBlock.Clip>
</TextBlock>

So if anyone has any clues on how I can achieve this or if you need any more information, please let me know.

도움이 되었습니까?

해결책

Is it required to use the Clip functionality? Or is OpacityMask an option? The following code comes close to such a clock:

<Grid VerticalAlignment="Center" HorizontalAlignment="Center" TextElement.FontSize="36">
    <Grid.Resources>
        <Style x:Key="UpperHalfStyle" TargetType="{x:Type TextBlock}">
            <Setter Property="Margin" Value="0"/>
            <Setter Property="OpacityMask">
                <Setter.Value>
                    <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
                        <GradientStop Offset="0.5" Color="White"/>
                        <GradientStop Offset="0.5" Color="Transparent"/>
                    </LinearGradientBrush>
                </Setter.Value>
            </Setter>
        </Style>
        <Style x:Key="LowerHalfStyle" TargetType="{x:Type TextBlock}">
            <Setter Property="Margin" Value="0,2,0,0"/>
            <Setter Property="OpacityMask">
                <Setter.Value>
                    <LinearGradientBrush StartPoint="0.5,0" EndPoint="0.5,1">
                        <GradientStop Offset="0.5" Color="Transparent"/>
                        <GradientStop Offset="0.5" Color="White"/>
                    </LinearGradientBrush>
                </Setter.Value>
            </Setter>
        </Style>
    </Grid.Resources>

    <StackPanel Orientation="Horizontal">
        <Grid>
            <TextBlock Text="1" Style="{StaticResource UpperHalfStyle}"/>
            <TextBlock Text="1" Style="{StaticResource LowerHalfStyle}"/>
        </Grid>
        <Grid>
            <TextBlock Text="0" Style="{StaticResource UpperHalfStyle}"/>
            <TextBlock Text="0" Style="{StaticResource LowerHalfStyle}"/>
        </Grid>
        <TextBlock Text=":"/>
        <Grid>
            <TextBlock Text="3" Style="{StaticResource UpperHalfStyle}"/>
            <TextBlock Text="3" Style="{StaticResource LowerHalfStyle}"/>
        </Grid>
        <Grid>
            <TextBlock Text="7" Style="{StaticResource UpperHalfStyle}"/>
            <TextBlock Text="7" Style="{StaticResource LowerHalfStyle}"/>
        </Grid>
    </StackPanel>
</Grid>

I just overlay two TextBlock elements. The first one is only opaque in the upper half, while the second one is only opaque in the lower half. I also added a Margin to the lower half, so that it becomes clear that they are two different elements. I do not know the rest of your design, but I assume you will want to add borders around the numbers. For this, you might have to restrict the height of the elements somehow. Otherwise the borders will surround the whole TextBlock and not just the visible part.

다른 팁

Okay, so I tried @gehho's suggestion and it worked fine for the most part. I built my FlipDownClockFace control and it all worked beautifully. The problem is that I'm a perfectionist and I wanted to make it look more realistic. I wanted to take a notch out of the corners of the Border elements that contain the TextBlocks but found that I couldn't do that using the OpacityMask.

So I went back to drawing board to investigate the Clip possibilities. In the end, I found a solution that enabled me to bind to the Points of a PathGeometry used as the Clip shape using a custom Converter. The Converter takes in the Width and Height of the Border using a MultiBinding and converts them into a Point around its edge dependant on the ConverterParameter. It's pretty verbose (lots of code) but it works. For those interested, here is a trimmed down version:

<Border CornerRadius="{Binding BorderCornerRadius, RelativeSource={RelativeSource AncestorType={x:Type Controls:FlipDownControl}}}" VerticalAlignment="Center" HorizontalAlignment="Center" Background="{TemplateBinding Background}" BorderThickness="{TemplateBinding BorderThickness}" BorderBrush="{TemplateBinding BorderBrush}">
    <Border.Clip>
        <PathGeometry>
            <PathFigure StartPoint="0,0">
                <LineSegment>
                    <LineSegment.Point>
                        <MultiBinding Converter="{StaticResource WidthAndHeightToPointConverter}" ConverterParameter="MiddleLeft">
                            <Binding Path="ActualWidth" RelativeSource="{RelativeSource AncestorType={x:Type Border}}" Mode="OneWay" />
                            <Binding Path="ActualHeight" RelativeSource="{RelativeSource AncestorType={x:Type Border}}" Mode="OneWay" />
                        </MultiBinding>
                    </LineSegment.Point>
                </LineSegment>
                <LineSegment>
                    <LineSegment.Point>
                        <MultiBinding Converter="{StaticResource WidthAndHeightToPointConverter}" ConverterParameter="MiddleRight">
                            <Binding Path="ActualWidth" RelativeSource="{RelativeSource AncestorType={x:Type Border}}" Mode="OneWay" />
                            <Binding Path="ActualHeight" RelativeSource="{RelativeSource AncestorType={x:Type Border}}" Mode="OneWay" />
                        </MultiBinding>
                    </LineSegment.Point>
                </LineSegment>
                <LineSegment>
                    <LineSegment.Point>
                        <MultiBinding Converter="{StaticResource WidthAndHeightToPointConverter}" ConverterParameter="BottomRight">
                            <Binding Path="ActualWidth" RelativeSource="{RelativeSource AncestorType={x:Type Border}}" Mode="OneWay" />
                            <Binding Path="ActualHeight" RelativeSource="{RelativeSource AncestorType={x:Type Border}}" Mode="OneWay" />
                        </MultiBinding>
                    </LineSegment.Point>
                </LineSegment>
                <LineSegment>
                    <LineSegment.Point>
                        <MultiBinding Converter="{StaticResource WidthAndHeightToPointConverter}" ConverterParameter="BottomLeft">
                            <Binding Path="ActualWidth" RelativeSource="{RelativeSource AncestorType={x:Type Border}}" Mode="OneWay" />
                            <Binding Path="ActualHeight" RelativeSource="{RelativeSource AncestorType={x:Type Border}}" Mode="OneWay" />
                        </MultiBinding>
                    </LineSegment.Point>
                </LineSegment>
            </PathFigure>
        </PathGeometry>
    </Border.Clip>
    <TextBlock Text="{Binding FrontValue, RelativeSource={RelativeSource AncestorType={x:Type Controls:FlipDownControl}}}" TextAlignment="Center" Padding="{TemplateBinding Padding}" />
</Border>

Although this is the solution that I actually went with in the end, I'll leave the accepted answer with @gehho.

라이센스 : CC-BY-SA ~와 함께 속성
제휴하지 않습니다 StackOverflow
scroll top