locked
Make DrawingContext paint into pixels, not between RRS feed

  • Question

  • When using WPF layout for normal controls, you don't need to worry too much about the "half pixel problem". Everything is well aligned to screen pixels. But when using DrawingContext and DrawingVisuals, it starts to get a mess again.

    My rectangle starts at 0/0 and has a size of 80/80. Calling DrawingContext.DrawRectangle with this data and a single-pixel-wide pen, I get blurred lines all around that cover two pixel rows with half opacity. The first pixel row to the top and left is actually outside my control and the other row is the first visible inside my control bounds.

    This is actually very inconvenient. I understand that GDI+ invented this to get "device-independent rendering" where the coordinates would not address entire pixels but instead the values in between (like you do on a function graph in the maths lesson). The problem is that it's entirely pointless. You always have a device, and that device will need to display colour spots somehow. For those spots to be visible, they need to have some size. Enter the pixel.

    Why does WPF (or GDI+ for that matter) always have to spill those spots in between the plates and not on them?

    I could offset all my coordinates by +0.5/+0.5 to work against that stupid design. But not only is that cumbersome, it blurs other parts of the design. Just consider mouse events. They usually come with the coordinates of the mouse cursor. AFAIK, these are always on a pixel, not between them, and they're also integer numbers, not x.5. How should I compare mouse locations if they don't match rendering locations? And if I intentionally offset all my layout locations, how would I need to undo that when interacting with the mouse? Screen locations (of windows for example) are also on-pixel.

    I've tried applying a Margin "0.5,0.5,0,0" or "0.5,0.5,-0.5,-0.5" to my host control, or applying a TranslateTransform with X and Y of 0.5, but none of these had any effect. The lines remained just as blurry. There's nothing like SnapsToDevicePixels activated here.

    What else could I try to fix this? I need 1px-wide lines to show as exactly 1 pixel, not two half pixels. I want sharp rendering at normal scaling at least. And I'd very much prefer the first visible pixel inside my control bounds to be 0/0 and the last Width-1/Height-1, just as it used to be some decades ago when pixels were the accepted layout unit.


    • Edited by LonelyPixel Tuesday, August 12, 2014 3:22 PM
    Tuesday, August 12, 2014 3:21 PM

Answers

  • I think this one can answer your question:

    http://wpftutorial.net/DrawOnPhysicalDevicePixels.html

    Quote:

    Resolution independence

    WPF is resoultion independent. This means you specify the size of an user interface element in inches, not in pixels. A logical unit in WPF is 1/96 of an inch. This scale is chosen, because most screens have a resolution of 96dpi. So in most cases 1 logical unit maches to 1 physical pixel. But if the screen resolution changes, this rule is no longer valid.

    Align the edges not the center points

    The reason why the lines appear blurry, is that our points are center points of the lines not edges. With a pen width of 1 the edges are drawn excactly between two pixels.


    A first approach is to round each point to an integer value (snap to a logical pixel) an give it an offset of half the pen width. This ensures, that the edges of the line align with logical pixels. But this assumes, that logical and physical device pixels are the same. This is only true if the screen resolution is 96dpi, no scale transform is applied and our origin lays on a logical pixel.

    Using SnapToDevicePixels for controls

    All WPF controls provide a property SnapToDevicePixels. If set to true, the control ensures the all edges are drawn excactly on physical device pixels. But unfortunately this feature is only available on control level.

    Using GuidelineSets for custom drawing

    Our first approach to snap all points to logical pixels is easy but it has a lot of assumptions that must be true to get the expected result. Fortunately the developers of the milcore (MIL stands for media integration layer, that's WPFs rendering engine) give us a way to guide the rendering engine to align a logical coordinate excatly on a physical device pixels. To achieve this, we need to create a GuidelineSet. The GuidelineSet contains a list of logical X and Y coordinates that we want the engine to align them to physical device pixels.
    If we look at the implementation of SnapToDevicePixels we see that it does excatly the same.

    protected override void OnRender(DrawingContext drawingContext)
    {
        Pen pen = new Pen(Brushes.Black, 1);
        Rect rect = new Rect(20,20, 50, 60);
     
        double halfPenWidth = pen.Thickness / 2;
     
        // Create a guidelines set
        GuidelineSet guidelines = new GuidelineSet();
        guidelines.GuidelinesX.Add(rect.Left + halfPenWidth);
        guidelines.GuidelinesX.Add(rect.Right + halfPenWidth);
        guidelines.GuidelinesY.Add(rect.Top + halfPenWidth);
        guidelines.GuidelinesY.Add(rect.Bottom + halfPenWidth);
     
        drawingContext.PushGuidelineSet(guidelines);
        drawingContext.DrawRectangle(null, pen, rect);
        drawingContext.Pop();
    }
     
    The example above is the same as at the beginning of the article. But now we create a GuidelinesSet. To the set we add a horizontal or vertical guidelines for each logical coordinate that we want to have aligned with physical pixels. And that is not the center point, but the edge of our lines. Therefore we add half the penwidth to each point.
    Before we draw the rectange on the DrawingContext we push the guidelines to the stack. The result are lines that perfecly match to our physical device pixels


    Adjust the penwidth to the screen resolution

    The last thing we need to consider is that the width of the pen is still defined in logical units. If we want to keep the pen width to one pixel (think a moment if you really want to have this) you can scale the pen width with the ration between your screen resolution and WPF's logical units which is 1/96. The following sample shows you how to do this.

    Matrix m = PresentationSource.FromVisual(this)
                    .CompositionTarget.TransformToDevice;
    double dpiFactor = 1/m.M11;
     
    Pen scaledPen = new Pen( Brushes.Black, 1 * dpiFactor );

    Best Regards,
    Please remember to mark the replies as answers if they help

    • Edited by IssueKiller Wednesday, August 13, 2014 6:45 AM
    • Marked as answer by LonelyPixel Wednesday, August 13, 2014 12:25 PM
    Wednesday, August 13, 2014 6:45 AM

All replies

  • I think this one can answer your question:

    http://wpftutorial.net/DrawOnPhysicalDevicePixels.html

    Quote:

    Resolution independence

    WPF is resoultion independent. This means you specify the size of an user interface element in inches, not in pixels. A logical unit in WPF is 1/96 of an inch. This scale is chosen, because most screens have a resolution of 96dpi. So in most cases 1 logical unit maches to 1 physical pixel. But if the screen resolution changes, this rule is no longer valid.

    Align the edges not the center points

    The reason why the lines appear blurry, is that our points are center points of the lines not edges. With a pen width of 1 the edges are drawn excactly between two pixels.


    A first approach is to round each point to an integer value (snap to a logical pixel) an give it an offset of half the pen width. This ensures, that the edges of the line align with logical pixels. But this assumes, that logical and physical device pixels are the same. This is only true if the screen resolution is 96dpi, no scale transform is applied and our origin lays on a logical pixel.

    Using SnapToDevicePixels for controls

    All WPF controls provide a property SnapToDevicePixels. If set to true, the control ensures the all edges are drawn excactly on physical device pixels. But unfortunately this feature is only available on control level.

    Using GuidelineSets for custom drawing

    Our first approach to snap all points to logical pixels is easy but it has a lot of assumptions that must be true to get the expected result. Fortunately the developers of the milcore (MIL stands for media integration layer, that's WPFs rendering engine) give us a way to guide the rendering engine to align a logical coordinate excatly on a physical device pixels. To achieve this, we need to create a GuidelineSet. The GuidelineSet contains a list of logical X and Y coordinates that we want the engine to align them to physical device pixels.
    If we look at the implementation of SnapToDevicePixels we see that it does excatly the same.

    protected override void OnRender(DrawingContext drawingContext)
    {
        Pen pen = new Pen(Brushes.Black, 1);
        Rect rect = new Rect(20,20, 50, 60);
     
        double halfPenWidth = pen.Thickness / 2;
     
        // Create a guidelines set
        GuidelineSet guidelines = new GuidelineSet();
        guidelines.GuidelinesX.Add(rect.Left + halfPenWidth);
        guidelines.GuidelinesX.Add(rect.Right + halfPenWidth);
        guidelines.GuidelinesY.Add(rect.Top + halfPenWidth);
        guidelines.GuidelinesY.Add(rect.Bottom + halfPenWidth);
     
        drawingContext.PushGuidelineSet(guidelines);
        drawingContext.DrawRectangle(null, pen, rect);
        drawingContext.Pop();
    }
     
    The example above is the same as at the beginning of the article. But now we create a GuidelinesSet. To the set we add a horizontal or vertical guidelines for each logical coordinate that we want to have aligned with physical pixels. And that is not the center point, but the edge of our lines. Therefore we add half the penwidth to each point.
    Before we draw the rectange on the DrawingContext we push the guidelines to the stack. The result are lines that perfecly match to our physical device pixels


    Adjust the penwidth to the screen resolution

    The last thing we need to consider is that the width of the pen is still defined in logical units. If we want to keep the pen width to one pixel (think a moment if you really want to have this) you can scale the pen width with the ration between your screen resolution and WPF's logical units which is 1/96. The following sample shows you how to do this.

    Matrix m = PresentationSource.FromVisual(this)
                    .CompositionTarget.TransformToDevice;
    double dpiFactor = 1/m.M11;
     
    Pen scaledPen = new Pen( Brushes.Black, 1 * dpiFactor );

    Best Regards,
    Please remember to mark the replies as answers if they help

    • Edited by IssueKiller Wednesday, August 13, 2014 6:45 AM
    • Marked as answer by LonelyPixel Wednesday, August 13, 2014 12:25 PM
    Wednesday, August 13, 2014 6:45 AM
  • And this article is also helpful: http://daniel-albuschat.blogspot.com/2011/04/avoiding-anti-aliasing-when-drawing.html


    Best Regards,
    Please remember to mark the replies as answers if they help

    Wednesday, August 13, 2014 6:47 AM
  • Thank you, I didn't know those guidelines. It makes drawing more complicated, too, and I don't know how exactly the aligning is applied (there's no documentation about it - why do I have to add the half pixel, not subtract it or leave it away for instance), but at least it works as expected.

    Another issue that remains (and that I had to deal with in GDI+ already) is that a rectangle is painted on the top and left but outside the bottom and right side of the given locations. I still need to decrease the size before drawing it. Drawing a 7 by 7 rectangle produces two lines with 7 pixels in between (i. e. an 8 by 8 rectangle). This should only affect outlines, not fills (from GDI+). So I need to distinguish again, what I will use a rectangle for - outline or fill. Different corrections need to be applied then to get a consistent result. How stupid. I need to verify every single drawing by each pixel, you cannot rely on the API. (Violated the principle of least astonishment.)

    Wednesday, August 13, 2014 12:32 PM