4
votes

In order to make my WPF application high-DPI-aware, I am opting to create all of the internal graphics and icons using embedded XAML vector graphics.

However, I don't understand why my XAML vector graphics are blurry when they are rendered. Here is an example. I drew a simple floppy disk icon using XAML Path and Rectangle elements. The image below is the same icon rendered at two different sizes and with varying values of SnapsToDevicePixels and UseLayoutRounding (this is at my monitor's native resolution of 2048x1152 in Windows 7).

Overall view

Given that the icon is mostly horizontal and vertical lines, it looks strangely blurry. Here is a 600% zoom view of the larger icons:

enter image description here

And the smaller icons:

enter image description here

The edges are not crisp at all at either size. And with varying values of SnapsToDevicePixels, some edges are crisp but others still are not.

What is the proper way to draw icons like this such that they are not rendered blurrily?

Here is the XAML for this window, including the path data:

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="XAML Path Test" Height="175" Width="525">
    <Window.Resources>
        <ResourceDictionary>
            <Canvas x:Key="icon">
                <Path Width="84" Height="83" Canvas.Left="6.5" Canvas.Top="9.5" Stretch="Fill" StrokeThickness="5" StrokeMiterLimit="2.75" Stroke="#FF000000" Fill="#FFFFFFFF" Data="F1 M 9,12L 68.9914,12L 88,31.0086L 88,90L 9,90L 9,12 Z "/>
                <Rectangle Width="43" Height="41" Canvas.Left="23.5" Canvas.Top="9.5" Stretch="Fill" StrokeThickness="5" StrokeMiterLimit="2.75" Stroke="#FF000000" Fill="#FF000000"/>
                <Rectangle Width="23" Height="31" Canvas.Left="38.5" Canvas.Top="11.25" Stretch="Fill" StrokeThickness="5" StrokeMiterLimit="2.75" Stroke="#FF000000" Fill="#FFFFFFFF"/>
            </Canvas>
        </ResourceDictionary>
    </Window.Resources>
    <StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
            <Rectangle Width="50" Height="50" Margin="10">
                <Rectangle.Fill>
                    <VisualBrush Visual="{StaticResource icon}"/>
                </Rectangle.Fill>
            </Rectangle>

            <Rectangle Width="50" Height="50" Margin="10" SnapsToDevicePixels="True">
                <Rectangle.Fill>
                    <VisualBrush Visual="{StaticResource icon}"/>
                </Rectangle.Fill>
            </Rectangle>

            <Rectangle Width="50" Height="50" Margin="10" UseLayoutRounding="True" SnapsToDevicePixels="True">
                <Rectangle.Fill>
                    <VisualBrush Visual="{StaticResource icon}"/>
                </Rectangle.Fill>
            </Rectangle>
        </StackPanel>

        <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
            <Rectangle Width="24" Height="24" Margin="10">
                <Rectangle.Fill>
                    <VisualBrush Visual="{StaticResource icon}"/>
                </Rectangle.Fill>
            </Rectangle>

            <Rectangle Width="24" Height="24" Margin="10" SnapsToDevicePixels="True">
                <Rectangle.Fill>
                    <VisualBrush Visual="{StaticResource icon}"/>
                </Rectangle.Fill>
            </Rectangle>

            <Rectangle Width="24" Height="24" Margin="10" UseLayoutRounding="True" SnapsToDevicePixels="True">
                <Rectangle.Fill>
                    <VisualBrush Visual="{StaticResource icon}"/>
                </Rectangle.Fill>
            </Rectangle>
        </StackPanel>
    </StackPanel>
</Window>

Edit:

By wrapping the path+rectangles in a ControlTemplate as Chris W. suggested, and resizing using a Viewbox, the images are still blurry. From left to right, this is non-resized (the canvas size is 100x100), resized to 50, and resized to 24:

enter image description here

And the 600% zoom view:

enter image description here

Here is the ControlTemplate:

<ControlTemplate x:Key="icon">
    <Canvas Width="100" Height="100">
        <Path Width="84" Height="83" Canvas.Left="6.5" Canvas.Top="9.5" Stretch="Fill" StrokeThickness="5" StrokeMiterLimit="2.75" Stroke="#FF000000" Fill="#FFFFFFFF" Data="F1 M 9,12L 68.9914,12L 88,31.0086L 88,90L 9,90L 9,12 Z "/>
        <Rectangle Width="43" Height="41" Canvas.Left="23.5" Canvas.Top="9.5" Stretch="Fill" StrokeThickness="5" StrokeMiterLimit="2.75" Stroke="#FF000000" Fill="#FF000000"/>
        <Rectangle Width="23" Height="31" Canvas.Left="38.5" Canvas.Top="11.25" Stretch="Fill" StrokeThickness="5" StrokeMiterLimit="2.75" Stroke="#FF000000" Fill="#FFFFFFFF"/>
    </Canvas>
</ControlTemplate>

And the usages:

<Control Template="{StaticResource icon}"/>
<Viewbox Height="50">
    <Control Template="{StaticResource icon}"/>
</Viewbox>
<Viewbox Height="24">
    <Control Template="{StaticResource icon}"/>
</Viewbox>
1
We're done something similar, but in our case one of my colleagues did the graphics using SVG and then converted it to XAML vector graphics. I don't know how he did it, he's the UI expert on our team. I just bring this up as a suggestion that you might be able to do the same thing. - Rod
@Rod he likely just made them in illustrator and used Mike Swanson's super handy dandy AI to XAML plugin, or I think Inkscape also exports to Path XAML. - Chris W.
@ChrisW. Right now I'm drawing them in Expression Design 4 and using the copy-to-XAML feature. In the future I'd like to draw them in SVG and convert to XAML from there. - tdenniston

1 Answers

3
votes

Your issue is that you're using VisualBrush wherein you're painting an area and you lose your actual vector status.

There's numerous other ways to accomplish this based on your actual needs and what you plan to do with it, but for your particular instance based just on what I see, I would just switch it to something like this instead using just ControlTemplate

<Window>
    <Window.Resources>      
        <ControlTemplate x:Key="icon">
            <Canvas>
               <Path Width="84" Height="83" Canvas.Left="6.5" Canvas.Top="9.5" Stretch="Fill" StrokeThickness="5" StrokeMiterLimit="2.75" Stroke="#FF000000" Fill="#FFFFFFFF" Data="F1 M 9,12L 68.9914,12L 88,31.0086L 88,90L 9,90L 9,12 Z "/>
                <Rectangle Width="43" Height="41" Canvas.Left="23.5" Canvas.Top="9.5" Stretch="Fill" StrokeThickness="5" StrokeMiterLimit="2.75" Stroke="#FF000000" Fill="#FF000000"/>
                <Rectangle Width="23" Height="31" Canvas.Left="38.5" Canvas.Top="11.25" Stretch="Fill" StrokeThickness="5" StrokeMiterLimit="2.75" Stroke="#FF000000" Fill="#FFFFFFFF"/>
             </Canvas>
        </ControlTemplate>
    </Window.Resources>

    <Grid>

        <Control Template="{StaticResource icon}"/>

    </Grid>
</Window>

Hope this helps and have a great weekend! :)

On and PS - For your resizing, keep ViewBox in mind or you'll need to re-tool your paths to not be hard set. If it were me, I would make a connected icon like that in one path (If you're using blend, select the Path and Rectangles and right-click -> Combine -> Unite) for one single line of path data and then turn that into a Style template instead and forego the ViewBox and other stuff.

ADDITIONAL INFO:

You could also template the Path like this which would be true vector;

<StackPanel>
    <StackPanel.Resources>

        <Style TargetType="Path" x:Key="DiskIcon">
            <Setter Property="Data" Value="F1 M 88.813,81.850 L 2.000,81.850 L 2.000,2.000 L 17.070,2.000 L 17.070,35.930 L 63.417,35.930 L 63.417,2.299 L 88.813,27.379 L 88.813,81.850 Z M 55.800,2.825 L 55.800,26.341 L 39.818,26.341 L 39.818,2.825 L 55.800,2.825 Z M 63.935,0.000 L 0.000,0.000 L 0.000,83.850 L 90.813,83.850 L 90.813,26.543 L 63.935,0.000 Z"/>
            <Setter Property="Fill" Value="Black"/>
            <Setter Property="Margin" Value="10"/>
            <Setter Property="Height" Value="100"/>
            <Setter Property="Width" Value="100"/>
        </Style>

    </StackPanel.Resources>

    <Path Style="{StaticResource DiskIcon}"/>

    <Path Style="{StaticResource DiskIcon}" Fill="Red"/>

    <Path Style="{StaticResource DiskIcon}" Fill="Blue"/>

</StackPanel>

Depending on the circumstances and need though I often build it into a ContentControl to host an embedded CLR ViewBox like mentioned in the "other ways" link I added in the beginning like;

<StackPanel>
        <StackPanel.Resources>
            <Style x:Key="TheAwesomeXAMLimage" TargetType="ContentControl">
                <Setter Property="Background" Value="Black"/>
                <Setter Property="Height" Value="90"/>
                <Setter Property="Width" Value="100"/>
                <Setter Property="Template">
                    <Setter.Value>
                        <ControlTemplate TargetType="ContentControl">

                            <Viewbox Stretch="Fill">

                                <Path Fill="{TemplateBinding Background}" 
                                          Data="F1 M 88.813,81.850 L 2.000,81.850 L 2.000,2.000 L 17.070,2.000 L 17.070,35.930 L 63.417,35.930 L 63.417,2.299 L 88.813,27.379 L 88.813,81.850 Z M 55.800,2.825 L 55.800,26.341 L 39.818,26.341 L 39.818,2.825 L 55.800,2.825 Z M 63.935,0.000 L 0.000,0.000 L 0.000,83.850 L 90.813,83.850 L 90.813,26.543 L 63.935,0.000 Z"/>

                            </Viewbox>

                        </ControlTemplate>
                    </Setter.Value>
                </Setter>
            </Style>
        </StackPanel.Resources>

        <ContentControl Style="{StaticResource TheAwesomeXAMLimage}"/>
        <ContentControl Style="{StaticResource TheAwesomeXAMLimage}"
                        Background="Red" Height="50" Width="60" Margin="10"/>

    </StackPanel>