Where does the “soap” come from in WPF and how to deal with it



This guide is for WPF developers looking to get the sharpest picture possible in their applications. The WPF graphics system to the marrow of the bones is vectorial, but the end result of its work is still a raster. If you do not pay due attention to this fact, you may encounter various varieties of "soap" - parasitic artifacts of rasterization. In such a situation, it is important not to lose the presence of mind, the reasons for their occurrence are quite rational, and the methods of struggle are quite simple and effective.

Table of contents


Introduction
1. Scaling raster images
2. Coordinates that are not a multiple of the pixel size
3. Native resolution of raster images
4. Rasterization of vector images
5. Moving text vertically
6. Using the SnapsToDevicePixels property
7. Self-drawing controls
Conclusion
Links

Introduction


The insidiousness of rasterization artifacts is that they are not striking. Many developers simply do not notice defects in the size of one or two pixels. However, these little things affect the user’s experience with the application.

A small test of attentiveness:


Next, we will talk about the factors that distinguish the upper picture from the bottom, and how to eliminate them. If your applications do not have such problems, try turning on the mode for increasing text and interface elements in the Windows settings - most likely problems will appear. Holders of small screens with high resolution or just people with low vision often use this feature.


It is not at all necessary to read everything in its entirety, you may well limit yourself to viewing illustrations. You may remember them, and you will return to this guide when you really need to fight for the clarity of the graphic output of your application.

Each section of the manual is equipped with a demo application illustrating the problem in question and methods for solving it. You can download everything in a single archive (104 Kb) containing compiled modules and their source code (VS2010 project format).

So, where does WPF come from and how to deal with it?

1. Scaling raster images


When working with raster images, the most common cause of blurring is scaling during output. Give the Image element sizes that do not match the physical size of the image, and the result will no longer be similar to the source. Automatic adjustment of the image size to the size of the container often leads to a similar result. In this case, the reason for the appearance of artifacts is the need to transfer the image from one raster grid to another.



Counteraction

If the image was mistakenly in the scaling container, then you need to get it out of there. If the image sizes are incorrect, you need to adjust them. Everything is simple. But only as long as you test your application with standard settings. If you enable the mode of enlarged fonts and interface elements in Windows, then the resolution of the output of the WPF application will change, the virtual unit of measurement will become larger than the pixel size, and your bitmap image will be hopelessly corrupted by stretching.

If your plans do not include image scaling for users with larger fonts, you will have to make corrections on the fly. First you need to find out the current resolution of the output and its relation to the standard. For example, like this:

public static class Render
{
    static Render()
    {
        var flags = BindingFlags.NonPublic | BindingFlags.Static;
        var dpiProperty = typeof(SystemParameters).GetProperty("Dpi", flags);
        Dpi = (int)dpiProperty.GetValue(null, null);
        PixelSize = 96.0 / Dpi;
    }
    //Размер физического пикселя в виртуальных единицах
    public static double PixelSize { get; private set; }
    //Текущее разрешение
    public static int Dpi { get; private set; }
}

Next, you can independently set the dimensions of the picture in the successor of the standard Image, depending on the current resolution of the output. In XAML, such a picture should be placed without specifying specific sizes.

public class StaticImage : Image
{
    static StaticImage()
    {
        //Отслеживание смены исходной картинки
        Image.SourceProperty.OverrideMetadata(
            typeof(StaticImage), 
            new FrameworkPropertyMetadata(SourceChanged));
    }
    private static void SourceChanged(  DependencyObject obj, 
                                        DependencyPropertyChangedEventArgs e)
    {
        var image = obj as StaticImage;
        if (image == null) return;
        //Поправка размера картинки под текущее разрешение
        image.Width = image.Source.Width * Render.PixelSize;
        image.Height = image.Source.Height * Render.PixelSize;
    }
}



If scaling cannot be avoided, then all that remains for you is to fight to reduce the degree of distortion by choosing the output algorithm suitable for your case using the RenderOptions.BitmapScalingMode property .



2. Coordinates that are not a multiple of the pixel size


A fairly simple way to get an unplanned blur is to center the Image in the container (also works for Rectangle and other Shape descendants ). In half the cases, the width of the container is not completely divided in half.

However, you can do without centering. The Grid container , for example, allows you to divide yourself into parts in arbitrary proportions. Here is a typical case when everything is fine in the upper left cell, the fog in the lower right, and something average in the rest:



No less effectively helps to smear the picture of the task in Canvas indentation or positions that are not a multiple of the size of the pixel.

In all of the above cases, you will get a “soap”, even if no scaling occurs during the output. The boundaries of the pixels of the image cease to fall within the boundaries of the pixels of the screen, and they are smeared during rasterization.


Counteraction

With containers, everything is simple, setting the UseLayoutRounding property to True forces the container ( including Window ) to automatically round the position of its children to the nearest integer pixel value. In other cases, you will have to, one way or another, explicitly bind the coordinates to the borders of the pixels.

Note that the expression “integer coordinates” means “coordinates divisible by the pixel size entirely” only in the standard resolution of 96 dpi, in all other Math.Round will not help you. In the general case, you can round the coordinate to the nearest pixel border like this:

static public double SnapToPixels(double value)
{
    value += PixelSize / 2;
    //На нестандартных DPI размер пикселя в WPF-единицах дробный.
    //Перемножение на 1000 нужно из-за потерь точности
    //при представлении дробных чисел в double
    //2.4 / 0.4 = 5.9999999999999991
    //240.0 / 40.0 = 6.0
    var div = (value * 1000) / (PixelSize * 1000);
    return (int)div * PixelSize;
}


3. Native resolution of raster images


The first time you encounter the problem of scaling bitmap images, you will most likely try to google if Image has a magic output mode without scaling. There is such a mode, it is Stretch = "None" , but you should rely on it and remove the explicit task of output sizes, how do you find yourself at risk. Raster images have their own resolution, it is indicated in the metadata, and WPF takes it into account when forming the dimensions of the image. If you do not know what it is, then, under a successful set of circumstances, you can believe in black magic: some of the images you have will be drawn as expected, and completely analogous to them will swell or shrink under the same conditions.

Download these three images for experiments (each of them has a different resolution):

      

If the resolution of the picture does not match the resolution of the output, then when downloading its dimensions are multiplied by the ratio of resolutions, and when rendering in the “original” size, you suddenly get a zoom. By the way, the StaticImage class in the first method is also not protected from these distortions, as it relies on the Source.Width and Source.Height properties .

None of the graphic editors or watchmen will wave the image resolution controls in front of you. Moreover, some of these programs do not show at all and do not allow changing this parameter. However, it is important.





Counteraction

In order for the dimensions of the image in virtual units to correspond to its size in pixels, you need to set the resolution to 96 dpi in the graphics editor. Nothing will happen to the image itself when changing the resolution, only metadata will change.

Standard Paint is not suitable for this, you will need something more serious. In the popular free IrfanView viewer, you can set the resolution in the image properties display dialog (hotkey I):


In the equally free Paint.NET editor, the same effect can be achieved by going to the "Image" menu, then "Canvas Size ..." (hotkey Ctrl + Shit + R).


If you do not want or cannot work with resolution in a graphical editor (for example, your application works with downloadable user images), you can change the resolution of the downloaded image programmatically. Here is an example of the download function for 32-bit images:

//Загрузка Image.Source с принудительной установкой 96 dpi
BitmapSource ConvertBitmapTo96DPI(string path)
{
    var uri = new Uri(path);
    var bitmapImage = new BitmapImage(uri);
    int width = bitmapImage.PixelWidth;
    int height = bitmapImage.PixelHeight;
    int stride = width * 4; // 4 байта на пиксель
    var pixelData = new byte[stride * height];
    bitmapImage.CopyPixels(pixelData, stride, 0);
    return BitmapSource.Create( width, height, 96, 96, 
                                PixelFormats.Bgra32,
                                bitmapImage.Palette, 
                                pixelData, stride);
}


4. Rasterization of vector images


From the description of the first three reasons for the appearance of “soap,” you may have a well-founded opinion that there are some problems with bitmaps in WPF. Indeed, for working in a vector environment it is much more natural to use vector images. At the first experiments, the conversion of SVG to XAML seems to be a panacea, allowing you to no longer think about sizes and pixels. Alas, this is not so. At exactly midnight, the carriage turns into a pumpkin, and the vector image is rasterized for display.

The fewer pixels there are in the output, the more artifacts. On images with a size of 48 pixels or less (this is almost 80% of all graphics in desktop applications), the situation degenerates into the following: a vector image is correctly rasterized in only one resolution, which is optimized for the rest, insofar as. Display the vector icon in the wrong size for which it was prepared, and inexorable anti-aliasing will not keep you waiting.



Counteraction

In some cases, you can do with a simple increase in image size. For example, for buttons on the toolbar, use images 32x32 pixels in size, and for icons in the context menu 25x25. However, if it really matters to you how the vector icon will be rasterized, then you need to optimize it for a specific resolution - the necessary details of the image must coincide with the pixel borders of the output raster.

5. Move text vertically


When displaying text, WPF uses some sharpening technique. While the text is static, it looks as clear as possible for its position and the selected rasterization mode (.NET Framework 4.0 and higher). When moving vertically, at certain shear values, the “sharpilka” sharply turns off and then smoothly turns back on.

Here is an example of a spurious blur effect on a button with text animation when pressed:



The blurring conditions in this example are rather mysterious. Apparently, the combination of the lack of a top border, nesting of text in the StackPanel (in the real situation, the button also had a picture) and a sharp shift of the text down by one or two pixels seems to work .

Counteraction

In the .NET Framework 4.0 and higher, using the TextOptions attribute , you can choose from two text rasterization modes: Ideal and Display . This will slightly reduce the unpleasant blur effect. In previous versions of the framework, the rasterization mode corresponds to the Ideal mode - as the letter fits on the pixel grid, it is also rasterized. In Display mode , intermediate processing is used: the horizontal text is always clearly attached to the pixels, and the same letters are rasterized the same. You can read more about text output modes here and here .

The blur effect of abrupt text movement is easy to reproduce in the laboratory. It is enough to move it vertically to positions that are not multiple of the size of the pixel. The following is an example of parallel movement of three blocks of text. The top two blocks with the same output mode are blurred differently. Obviously, it’s not the shift value that matters, but the position of the text relative to the raster grid.


The mere fact of moving the text does not necessarily lead to “dynamic blur”. If this effect occurs, it affects the entire line. Alas, the developer is not provided with means to control this effect. In some cases, its occurrence can be avoided by aligning the coordinates of the block with the boundaries of the pixels or by choosing the amount of shift empirically.

6. Using the SnapsToDevicePixels Property


If you use such basic visual elements as Rectangle , Ellipse , Line , Path , Border , etc., then when displayed in coordinates that are not a multiple of the pixel size, they will certainly show you blurring of vertical and horizontal lines. Here is an example of an image constructed using the designated elements:



In the illustrations below, the control with the demo image moves in 0.2 pixel steps. First, he does this separately along each of the axes, then along an arc of a circle. Depending on the phase of movement, the local coordinate grid of the control is differently superimposed on the physical raster grid.



Sometimes the outer borders of the TV are clear, sometimes the borders of its screen, but this never happens at the same time. The buttons are clear either horizontally or vertically, and more often blurry on both axes. The antenna in any combination is a little smoothed and for this reason does not complex.

Counteraction

You can enable the control to snap to pixels by setting the SnapsToDevicePixels property to True (for this it is enough to set this attribute on the root grid). The result will be more stable:



Nevertheless, the picture is neither unchanged during movements, nor ideally attached to pixels. The TV screen shakes within one pixel on both axes, and its buttons are always blurry.

Setting the SnapsToDevicePixels property to True encourages the visual element to fall within the boundaries of the pixels of the screen when rendering. Each control will strive to do this with different zeal and in different ways. For example, Image , Label, and TextBlockrelate to this attribute completely indifferently. Line will only hit pixels if the original geometry is successful. Rectangle , on the contrary, will jump out of the pants and always hit the pixels.

For a more stable pixel binding, you need to adjust the original image:
  1. add 0.5 to all Y-coordinates of the lines representing the buttons so that their edges coincide with the pixel grid in the control space;
  2. indent the TV screen integer, for example 2, so that it does not shake the binding to the nearest pixel borders.



By the way, these actions will help to get a clear and stable output only in the standard resolution of 96 dpi, in the rest there will be confusion and reeling. To get into pixels in any resolution, you should refer to the recommendations of the section "Self-drawing controls" (the sizes of controls will have to be adjusted on the go, based on the physical size of the pixel).

7. Self-drawing controls


If you override OnRender in your visual element and draw it yourself using the DrawingContext , then you have exactly the same rasterization problems as the Shape heirs from the previous method, but you must implement the SnapsToDevicePixels functionality yourself. If, of course, you want. You can not bother and do something like this:

public class Washer : FrameworkElement
{
    public Washer()
    {
        _brush = new SolidColorBrush(Color.FromRgb(247, 148, 29));
        _brush.Freeze();
        _pen = new Pen(Brushes.Black, 1);
        _pen.Freeze();
    }
    protected override void OnRender(DrawingContext dc)
    {
        //Ножки
        dc.DrawLine(_pen, new Point(1, 21), new Point(4, 21));
        dc.DrawLine(_pen, new Point(12, 21), new Point(15, 21));
        //Корпус
        var rect = new Rect(0, 0, 16, 21);
        dc.DrawRectangle(_brush, null, rect);
        //Кнопки
        dc.DrawLine(_pen, new Point(12, 1), new Point(12, 4));
        dc.DrawLine(_pen, new Point(14, 1), new Point(14, 4));
        //Окошко
        dc.DrawEllipse(Brushes.White, _pen, new Point(8, 11), 5, 5);
        //Защелка на окошке
        rect = new Rect(10, 10, 4, 2);
        dc.DrawRectangle(Brushes.White, _pen, rect);
    }
    private Pen _pen;
    private Brush _brush;
}

Despite the fact that all the given coordinates are integer, superposition on the pixel grid in the control space will occur as follows:


The width of the pen is counted in both directions from the line specified by the coordinates. If half the pen is not completely divided by the width of the pixel, then the edges of the line do not fall within the boundaries of the pixels, even if the specified coordinates fit exactly in them. If the image is complex, it may turn out that as an image do not move, but to achieve that all its components are clear does not work. The illustration shows the movement of the image in increments of 0.2 pixels.



Counteraction

WPF provides special tools for snapping to pixels - guidelines . At the stage of forming a chain of actions for rendering control (this is what the OnRender method does ), you can specify the vertical and horizontal coordinates in the control space, which, when output, should exactly fit the pixel borders.


In code, it looks like this ( OnRender method only ):

protected override void OnRender(DrawingContext dc)
{
    double halfPen = _pen.Thickness / 2;
    //Ножки
    var snapX = new double[] { 1, 12 };
    var snapY = new double[] { 21 + halfPen };
    dc.PushGuidelineSet(new GuidelineSet(snapX, snapY));        
    dc.DrawLine(_pen, new Point(1, 21), new Point(4, 21));
    dc.DrawLine(_pen, new Point(12, 21), new Point(15, 21));        
    dc.Pop();
    //Корпус
    snapX = new double[] { 0, };
    snapY = new double[] { 21 };
    dc.PushGuidelineSet(new GuidelineSet(snapX, snapY));
    var rect = new Rect(0, 0, 16, 21);
    dc.DrawRectangle(_brush, null, rect);
    dc.Pop();
    //Кнопки
    snapX = new double[] { 12 - halfPen };
    snapY = new double[] { 1 };
    dc.PushGuidelineSet(new GuidelineSet(snapX, snapY));
    dc.DrawLine(_pen, new Point(12, 1), new Point(12, 4));
    dc.DrawLine(_pen, new Point(14, 1), new Point(14, 4));
    dc.Pop();
    //Окошко
    snapX = new double[] { 3 - halfPen };
    snapY = new double[] { 6 - halfPen };
    dc.PushGuidelineSet(new GuidelineSet(snapX, snapY));
    dc.DrawEllipse(Brushes.White, _pen, new Point(8, 11), 5, 5);
    dc.Pop();
    //Защелка на окошке
    snapX = new double[] { 10 - halfPen };
    snapY = new double[] { 10 - halfPen };
    dc.PushGuidelineSet(new GuidelineSet(snapX, snapY));
    rect = new Rect(10, 10, 4, 2);
    dc.DrawRectangle(Brushes.White, _pen, rect);
    dc.Pop();
}

To work with guides you do not need to know the current output resolution. It is enough to arrange them appropriately in the local coordinates of the control. The above example only works correctly in the standard resolution of 96 dpi, in which the pen width matches the pixel size. To achieve clear line boundaries in other resolutions, you will have to assign a guide to each of its sides.



An important nuance is that with a DrawingContextguides interact through the stack and at the same time affect the entire current output, and not just the figures to the borders of which they fall. That is why in the example, to align two parallel lines, only one guide is used for each axis. If you collect all the guides used and push them onto the stack at once, the result will be disastrous. Because of the conflict, only some of them will work, the rest will be ignored.

Alignment to the pixel borders along the guides is carried out in both directions, therefore, in different situations, different parts of the image can be moved in different directions. When moving the picture, the parts of the washing machine are wound relative to each other, and in some situations, her legs disappear. The image can be changed by optimizing for stable output in a specific resolution, as was done in the previous section, but it will not work to achieve stable output in any resolution. Below we consider an alternative way to snap to pixels, devoid of this drawback.

Alternative counteraction

It is not necessary to use guides to snap to pixels when self-drawing controls. Cycling enthusiasts can fall within the boundaries of pixels by manually adjusting the rendered primitives. It is not as difficult as it may seem. The following conditions will be required:

  1. the coordinates of the source data must fall within the boundaries of the pixels. For 96 dpi, you can use Math.Round , for the general case you have to round to a specific pixel size;
  2. the width of the feathers used must be a multiple of the pixel size;
  3. in cases where the pen width contains an odd number of pixels, the coordinates of the displayed primitive should be shifted by half the pixel width;
  4. when displaying the control, it is necessary to correct for the shift of its coordinates relative to the raster grid and restart OnRender at any of its movements.

The first two points can be implemented using such a static class (its fragments were given in the first two sections):

//Информация о текущем разрешении и функции по попаданию в границы пикселей
public static class Render
{
    static Render()
    {
        var flags = BindingFlags.NonPublic | BindingFlags.Static;
       var dpiProperty = typeof(SystemParameters).GetProperty("Dpi", flags);
       Dpi = (int)dpiProperty.GetValue(null, null);
        PixelSize = 96.0 / Dpi;
        HalfPixelSize = PixelSize / 2;
    }
    //Размер физического пикселя в виртуальных единицах
    public static double PixelSize { get; private set; }
    //Текущее разрешение
    public static int Dpi { get; private set; }
    //Округление до границ пикселей
    static public double SnapToPixels(double value)
    {
        value += HalfPixelSize;
        //На нестандартных DPI размер пикселя в WPF-единицах дробный.
        //Перемножение на 1000 нужно из-за потерь точности
        //при представлении дробных чисел в double
        //2.4 / 0.4 = 5.9999999999999991
        //2400.0 / 400.0 = 6.0
        var div = (value * 1000) / (PixelSize * 1000);
        return (int)div * PixelSize;
    }
    private static readonly double HalfPixelSize;
}

If any value (for example, pen width or screen coordinate) needs to be firmly tied to the pixel size, then just set it as Render.PixelSize * n . If you need to round it to a multiple of the pixel size, you must use the Render.SnapToPixels method .

It is convenient to implement the third and fourth conditions (correction for subpixel shifts of controls and odd sizes of feathers) in the form of a base class for independently rendered controls:

public class SelfDrawingControlBase : FrameworkElement
{
    public SelfDrawingControlBase()
    {
        Snap = 0.5 * Render.PixelSize;
        SubpixelOffset = new Point(0, 0);
        LayoutUpdated += OnLayoutUpdated;
    }
    protected void OnLayoutUpdated(object sender, EventArgs e)
    {
        FixSubpixelOffset();
        InvalidateVisual();
    }
    //Подгонка координат линии для точного попадания в границы пикселей
    protected void SnapLine(Pen pen, ref Point begin, ref Point end)
    {
        var snapX = -SubpixelOffset.X;
        var snapY = -SubpixelOffset.Y;
        if (IsOdd(pen.Thickness))
        {
            if (begin.X == end.X)
                snapX += Snap;
            if (begin.Y == end.Y)
                snapY += Snap;
        }
        begin.X += snapX;
        begin.Y += snapY;
        end.X += snapX;
        end.Y += snapY;
    }
    //Подгонка координат прямоугольника для точного попадания в границы пикселей
    protected void SnapRectangle(Pen pen, ref Rect rect)
    {
        var snapX = -SubpixelOffset.X;
        var snapY = -SubpixelOffset.Y;
        if (pen != null && IsOdd(pen.Thickness))
        {
            snapX += Snap;
            snapY += Snap;
        }
        rect.Location = new Point(rect.Left + snapX, rect.Top + snapY);
    }
    //Подгонка координат эллипса для точного попадания в границы пикселей
    protected void SnapEllipse(Pen pen, ref Point center)
    {
        var snapX = -SubpixelOffset.X;
        var snapY = -SubpixelOffset.Y;
        if (pen != null && IsOdd(pen.Thickness))
        {
            snapX += Snap;
            snapY += Snap;
        }
        center.X += snapX;
        center.Y += snapY;
    }
    //Половинка пикселя для привязки к пиксельной сетке
    protected double Snap { get; private set; }
    //Общий сдвиг контрола относительно границ пикселей
    protected Point SubpixelOffset { get; private set; }
    //Выяснение сдвига контрола относительно границ пикселей
    //для учёта его в дальнейшей привязки к пикселям
    private void FixSubpixelOffset()
    {
        var offset = TranslatePoint(new Point(0, 0),
                                    Application.Current.MainWindow);
        SubpixelOffset = new Point( ModByPixel(offset.X),
                                    ModByPixel(offset.Y));
    }
    //Проверка на нечётное количество пикселей
    private static bool IsOdd(double value)
    {
        //На нестандартных DPI размер пикселя в WPF-единицах дробный.
        //Перемножение на 1000 нужно из-за потерь точности
        //при представлении дробных чисел в double
        //1.0 % 0.1 = 0.09999999999999995
        //1000.0 % 100.0 = 0.0
        return (value * 1000) % (Render.PixelSize * 2 * 1000) != 0;
    }
    //Остаток от деления на ширину пиксела
    private static double ModByPixel(double value)
    {
        return ((value * 1000) % (Render.PixelSize * 1000)) / 1000;
    }
}

The main functionality of this class is to correct the coordinates of graphic primitives before displaying them. SnapXXX methods modify the original data so that the rendering result falls exactly within the boundaries of the pixels.

Rectangles and ellipses are enough to shift entirely by half a pixel with feathers of odd width. For horizontal lines, you need to adjust the Y coordinate and do not touch the X coordinate, for vertical lines - vice versa. During coordinate corrections, the shift of the control relative to the pixel grid is also taken into account.

Snap to pixels in the example with a washing machine:

public class Washer : SelfDrawingControlBase
{
    public Washer()
    {
        _brush = new SolidColorBrush(Color.FromRgb(247, 148, 29));
        _brush.Freeze();
        _pen = new Pen(Brushes.Black, 1);
        _pen.Freeze();
    }
    protected override void OnRender(DrawingContext dc)
    {
        //Ножки
        Point start = new Point(1, 21);
        Point end = new Point(4, 21);
        SnapLine(_pen, ref start, ref end);
        dc.DrawLine(_pen, start, end);
        start = new Point(12, 21);
        end = new Point(15, 21);
        SnapLine(_pen, ref start, ref end);
        dc.DrawLine(_pen, start, end);
        //Корпус
        var rect = new Rect(0, 0,16, 21);
        SnapRectangle(null, ref rect);
        dc.DrawRectangle(_brush, null, rect);
        //Кнопки
        start = new Point(12, 1);
        end = new Point(12, 4);
        SnapLine(_pen, ref start, ref end);
        dc.DrawLine(_pen, start, end);
        start = new Point(14, 1);
        end = new Point(14, 4);
        SnapLine(_pen, ref start, ref end);
        dc.DrawLine(_pen, start, end);
        //Окошко
        var center = new Point(8, 11);
        SnapEllipse(_pen, ref center);
        dc.DrawEllipse(    Brushes.White, _pen,
                        center, 5, 5);
        //Защелка на окошке
        rect = new Rect(10, 10, 4, 2);
        SnapRectangle(_pen, ref rect);
        dc.DrawRectangle(Brushes.White, _pen, rect);
    }
    private Pen _pen;
    private Brush _brush;
}

The result is obtained in a similar way with guides, but at the same time stable with respect to the movements of the control. This is due to the fact that the correction of coordinates is carried out only in one direction.



The given solution of the example works only in the standard resolution of 96 dpi - pen sizes and primitive coordinates are inserted into the code with specific numbers for clarity. If you need the image to be correctly snapped to pixels in any resolutions, then before transferring data to SnapXXX methods , you need to round them to pixel borders using the Render.SnapToPixels method .
Here, for example, is the control drawing a rectangle that scales adequately when changing the resolution and falls within the boundaries of the pixels:

public class CrossDpiBrick : SelfDrawingControlBase
{
    public CrossDpiBrick()
    {
        _brush = new SolidColorBrush(Color.FromRgb(247, 148, 29));
        _brush.Freeze();
        _pen = new Pen(Brushes.Black, Render.SnapToPixels(7));
        _pen.Freeze();
    }
    protected override void OnRender(DrawingContext dc)
    {
        var rect = new Rect(Render.SnapToPixels(10),
                            Render.SnapToPixels(10),
                            Render.SnapToPixels(120),
                            Render.SnapToPixels(40));
        SnapRectangle(_pen, ref rect);
        dc.DrawRoundedRectangle(_brush, _pen, rect,
                                Render.SnapToPixels(10),
                                Render.SnapToPixels(10));
    }
    private Pen _pen;
    private Brush _brush;
}



Manual snap to pixels requires a bit more intervention in the rendering process than when working with the built-in WPF tools, but allows you to more flexibly control this process and achieve stable output at any output resolution.

Conclusion


WPF provides the developer with all the necessary tools for binding to screen pixels. In the standard library of controls, these tools are used by default, and in most cases you can not bother with subpixel shifts. However, in order to achieve a clear conclusion when working with vector and raster graphics, as well as when rendering controls independently, the developer is forced to use the means of pixel binding explicitly.

For this, first of all, it is necessary to notice the emerging problems of rasterization. It is not so easy. Even Microsoft products do not always have the perfect picture. Here, for example, are elements of the integrated vector graphics editor in Microsoft Word 2010:

Familiar artifacts? If now for you the elimination of such detected problems is a technical matter, the purpose of this guide has been achieved. Thanks for attention!

References


MSDN - Pixel Snapping in WPF Applications
MSDN - UIElement.UseLayoutRounding Property
Pete Brown - Choose your Fonts and Text Rendering Options Wisely
MSDN Blogs - WPF 4.0 Text Stack Improvements
MSDN - How to: Apply a GuidelineSet to a Drawing
MSDN - UIElement.SnapsToDevicePixels Property

Original demo code: download (104 Kb)

Thanks for the illustrations:

romson

melkopuz

sevendot

Also popular now: