Microsoft Dot Net Master

Microsoft Dot Net Master
Microsoft Dot Net Master

Tuesday, July 31, 2012

Draw lines excactly on physical device pixels

Why do my lines appear so blurry?

When you draw a line in WPF you will experience that they often appear blurry. The reason for this is the antialiasing system that spreads the line over multiple pixels if it doesn't align with physical device pixels.
The following example shows a usercontrol that overrides the OnRender method for custom drawing a rectange to the drawingContext. Even if all points are integer values and my screen has a resolution of 96dpi the lines appear blurry. Why?
 
protected override void OnRender(DrawingContext drawingContext)
{
    Pen pen = new Pen(Brushes.Black, 1);
    Rect rect = new Rect(20,20, 50, 60);
 
    drawingContext.DrawRectangle(null, pen, rect);
}
 
 

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 );

No comments:

Post a Comment