Bookmark and Share Share...    Subscribe to this feed Feed   About Christian Moser  


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




Last modified: 2008-10-24 02:15:02
Copyright (c) by Christian Moser, 2011.

 Comments on this article

Show all comments
Juan
Commented on 9.November 2008
I have a problem with WPF, maybe you could help me a little bit. I tried to draw on my window using your method with the OnRender method, but nothing appears on my screen.
Christian Moser
Commented on 11.November 2008
Hi Juan,
I have experienced the same problem. It seems that the OnRender() method does not work on Windows. Create a custom control that derives from Control and add it to your window. Override the OnRender() in your control. This will work.
Sam
Commented on 16.March 2009
I need to draw a grid which resembles a graph-sheet(along with minor grids), on that i need to plot line graph. I tried to the above technique, but some horizantal lines are still alaising. It does not alias on 1280x1024 resolution, but on all other resolutions it aliases. Also its takes very long to paint. Please help me on how can I do this so that this works for all resolutions and is paints very fast.
Thanks,
Sam
Cory Plotts
Commented on 11.September 2009
Great article! Very useful. One additional thought is that if you are considering scaling your stroke thicknesses to be physical units at the creation of the Pen ... you might want to also do that when setting up the guidelines. That is:
double halfPenWidth = pen.Thickness / 2 * dpiFactor;

And that implicitly brings up another point ... there is no reason that you have to do this for just 1 pixel stroke thicknesses ... although the benefits there are the most striking!
zafer
Commented on 15.December 2009
hi Christian Moser thanx alot for such explanation, though i have a question, does this also work when im SkewTransforming a control, cause the edges get very sharp and i want them to be smooth.
thanx alot,
Zafer
Yury
Commented on 26.March 2010
This seems like the optimal method to get rid of anti-aliasing. I think it's ironic to rely on owner drawn functions to achieve this when the goal of WPF is specifically to provide an alternative to custom drawing.
Mzithra D....
Commented on 24.April 2010
I have a problem with life. Can you help me out with that?
Zaggle...
Commented on 6.June 2010
You'd be better taking these stupid comments off each page on turning it into a forum, it makes your site look pretty stupid IMO
x
Commented on 16.June 2010
Take the time to add a signup system to keep idiots take away from what is a great site
HatemAmin
Commented on 28.June 2010
Hi Sam,
You can use the "DrawingBrush" to do such thing, please visit http://msdn.microsoft.com/en-us/library/aa480159.aspx
Johnny...
Commented on 30.August 2010
How can I draw lines & text with the crispiness using C++ or Delphi? I hate .Net and refuse to use WPF or any other crappy Microsoft technologies.
Igor...
Commented on 1.September 2010
How about performing this on Silverlight 4. Is it possible? How?
new_to_WPF
Commented on 25.May 2011
You can turn off anti-aliasing with RenderOptions.SetEdgeMode(). Use this to set the edge mode of a Canvas object to EdgeMode.Aliased. After that, anything you draw to the Canvas will not be blurry (except text).
Nebogipfel
Commented on 16.June 2011
I recently stumbled upon a strange phenomenon:

This Code in an Image.OnRender(dc) override

GuidelineSet gs = new GuidelineSet();
gs.GuidelinesX.Add(500.5); // Arbitrary Integer + 0.5 works
gs.GuidelinesY.Add(500.5); // Arbitrary Integer + 0.5 works
dc.PushGuidelineSet(gs);
Brush b = Brushes.Red;
for (int i = 1; i < 300; i++)
{
dc.DrawRectangle(null, new Pen(b, 1), new System.Windows.Rect(0, 0, i, i));
b = b == Brushes.Red ? Brushes.Blue : Brushes.Red;
}
dc.Pop();

produces perfectly pixel snapped Rectangles. It seems it takes only one Guideline in each X and Y to work. If one of the guideline gets removed, the corresponding axis gets blurred again whilst the other stays crisp.

Is here some blackbox magic going on? Or am I just stupid. Note that both options might apply ;)

Name
E-Mail (optional)
Comment