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 propertySnapToDevicePixels
.
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 aGuidelineSet
. 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(); }
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 pixelsAdjust 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