Drop GDI+ in WinForms and move to WPF

Tags: .NET, C#, WPF

Recently a friend of mine came to me asking for some advice on his first C#-project, in which he had to port an existing application to the .NET Framework. He had some minor issues with drawing figures on the screen and finding his way through GDI+. I was curious what he was porting to C#, since he’s always been for open source software. This is a small write-down of what he was doing and experienced problems with:

“I have a mediaplayer to play videos, but on top of the player I want to place a colored rectangle. Now ofcourse this rectangle has to support some alpha blending, or else you can’t see the movie. Preferably the rectangle should also have rounded corners and rescale with the player.”

Sounded to me like a case scenario from the self-paced training kit for … let’s say WPF. In my opinion it would be easier to do this with Windows Presentation Foundation instead of GDI+. And why stick with .NET 2.0, if you can use the power of WPF, considering 65% of Windows PCs have .NET 3.5 SP1 installed. To prove my point I just started on a small proof-of-concept, even though it’s been nearly 2 years since I played around with WPF (really sad, but true).

First of all I need some container to put my mediaplayer element and maybe some buttons to start and stop playing the video. For this I’ll be using a Grid with 2 rows, one for the MediaElement and one for the buttons, and a StackPanel to put my buttons in.

<Grid>
    <Grid.RowDefinitions>
        <RowDefinition/>
        <RowDefinition Height="auto"/>
    </Grid.RowDefinitions>
    <MediaElement Name="videoPlayer" Grid.Row="0" Source="Wildlife.wmv" LoadedBehavior="Manual" />
    <StackPanel Grid.Row="1" Orientation="Horizontal" HorizontalAlignment="Center">
        <Button Name="btnPlay" Click="PlayVideo">Play</Button>
        <Button Name="btnPause" Click="PauseVideo">Pause</Button>
        <Button Name="btnStop" Click="StopVideo">Stop</Button>
    </StackPanel>
</Grid>

With the MediaElement filling the first row of the grid, it’s auto-resizing together with the grid. The code behind these buttons can be kept quite simple for this POC. The only thing I’m doing here is hiding a button according to the state of the mediaplayer.

public void PlayVideo(object sender, RoutedEventArgs e)
{
    videoPlayer.Play();
    btnPlay.Visibility = Visibility.Collapsed;
    btnPause.Visibility = Visibility.Visible;
}

public void PauseVideo(object sender, RoutedEventArgs e)
{
    videoPlayer.Pause();
    btnPlay.Visibility = Visibility.Visible;
    btnPause.Visibility = Visibility.Collapsed;
}

The next part is to place a Rectangle, in semi-transparent red, with a rounded red Border on top of the MediaElement. A container that allows multiple elements to overlap each other and also gives the possibility to say which element should be on top is the Canvas. But moving the MediaElement object from the Grid’s row to the Canvas brings up a problem: the MediaElement doesn’t auto size anymore. To fix this I can bind the height and width properties to the same properties of the parent container. Notice that you have to use the ActualWidth and ActualHeight of the border instead of Width and Height to make this part work correctly.

<Border Name="border" Grid.Row="0" BorderThickness="1" BorderBrush="Red" CornerRadius="10"> 
    <Canvas Name="canvas" 
            Height="{Binding ElementName=border, Path=ActualHeight}" 
            Width="{Binding ElementName=border, Path=ActualWidth}"> 
        <MediaElement Name="videoPlayer" Source="Wildlife.wmv" LoadedBehavior="Manual" 
                      Height="{Binding ElementName=canvas, Path=Height}" 
                      Width="{Binding ElementName=canvas, Path=Width}"> 
        </MediaElement> 
        <Rectangle Name="rectangle" Panel.ZIndex="1" 
                      Height="{Binding ElementName=canvas, Path=Height}" 
                      Width="{Binding ElementName=canvas, Path=Width}"> 
            <Rectangle.Fill> 
                <SolidColorBrush Color="#B0FF0000" /> 
            </Rectangle.Fill> 
        </Rectangle> 
    </Canvas> 
</Border>

Now if you look at this in the designer or run the application, you’ll notice that the Border has rounded corners as wished, but the rectangle still goes over these rounded corners. To solve this I have to clip the Rectangle. But since I want the Rectangle to be resizable, the clipping geometry has to resize together with the Rectangle. Because I want to clip rounded corners on a Rectangle, it seems logic to me to use a RectangleGeometry for clipping.

The RectangleGeometry.Rect structure is a dependency property, but the Width and Height properties inside this structure are not, and so can’t be bound directly to the Width and Height and this might seem a problem at first. Luckily you can fix this problem in WPF by using multibinding and by writing a small converter class that converts your Width and Height parameters to a Rect object.

public class RectConvertor : MarkupExtension, IMultiValueConverter
{
    #region IMultiValueConverter Members
    /// <summary>
    /// Convert Width and Height to Rect
    /// </summary>
    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        Rect rect = new Rect(new Point(0, 0), new Size((double)values[0], (double)values[1]));
        return rect;
    }

    /// <summary>
    /// Convert back, is not needed in this example
    /// </summary>
    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
    #endregion

    public override object ProvideValue(IServiceProvider serviceProvider)
    {
        return this;
    }
}

Since I want to use the converter in XAML, I also have to derive from MarkupExtension. Once this is done, I can finish my layout by adding the clipping to the Rectangle.

<Rectangle.Clip>
    <GeometryGroup>
        <RectangleGeometry x:Name="clipRect" RadiusX="10" RadiusY="10">
            <RectangleGeometry.Rect>
                <MultiBinding Converter="{local:RectConvertor}">
                    <Binding ElementName="rectangle" Path="Width"/>
                    <Binding ElementName="rectangle" Path="Height"/>
                </MultiBinding>
            </RectangleGeometry.Rect>
        </RectangleGeometry>
    </GeometryGroup>
</Rectangle.Clip>

Although I didn’t work with WPF for quite some time, I’m pretty sure this didn’t take me as long as I would have needed to do the same without. And if you have other (and maybe better) ways to do it, feel free to share your ideas.

Comments

comments powered by Disqus