I just cannot manage to draw a single pixel wide black (!) line in OnRender
using drawingContext.DrawLine()
on my laptop monitor which has a DPI of 120. I face this problems since years and I read many answers here on stackoverflow, but I can simply not get a working code. Many answers link to this article: wpftutorial.net: Draw On Physical Device Pixels. It recommends to use Guidelines and to offset them by half a pixel, i.e. setting y=10.5 instead of the desired position 10 for a 96 DPI monitor. But it doesn't explain how to do the calculation for different DPIs.
WPF uses logical units (LU) which are 1/96 inch wide, meaning drawingContext.DrawLine(Width: 1) wants to draw a line 1/96 inch wide. The pixels of my monitor are 1/120 inch wide.
The article only says that the line width for 120 DPI should not be 1 LU, but 0.8 LU, which is 1/120 inch, the width of a pixel from my monitor.
Should accordingly the offset be 0.4 instead of 0.5 ? This makes the whole coordinate calculation very complicated. Let's say I want to draw a line on 5 LU, i.e. 5/96 inch which is at 0.0520 inch. The nearest monitor pixel would be number 6 which is at 0.05 inch. Meaning the correction would need to be 0.25 LU. But if I want to draw a line at 6 LU, the offset would need to be would be 0.5.
One problem I can see right away is that my code cannot be sure that the coordinates 0, 0 are precisely on a real monitor pixel. If the parent control puts my control on an uneven number of LU, then 0,0 would not precisely align with a monitor pixel, meaning I have no chance to calculate for a particular LU what its offset should be.
So I thought, what the heck, I just try to draw 10 lines, for each increasing its y by 0.1. The result (second row) looks like that:
The first row shows how a perfect 1 pixel wide, black line should look like, enlarged 8 times. The second row shows the line drawn by WPF:
Pen penB08 = new Pen(Brushes.Black, 0.8);
for (int i = 0; i < 10; i++) {
drawingContext.DrawLine(penB08, new Point(i * 9, i*0.1), new Point(i * 9 + 5, i*0.1));
}
As you can tell, not one of them is precisely 1 pixel wide and therfore none of them is really black !
If the above mentioned article is right, at least one of the lines should display properly. Reason: the difference between 96 DPI and 120 DPI is 1/5. Meaning every 5th LU pixel should start at exactly the same position like a monitor pixel. The offset should be 1/2 LU, that's why I made 10 1/10 steps.
See below for other examples using guidelines as recommended in that article. As written in that article, it doesn't make a difference if one uses guidelines or works with offsets.
Question
Please provide the code which truly draws a 1 pixel wide line on a 120 DPI monitor. I know, there are already many answers on stackoverflow which explain how it should be theoretically solved. Please also note that the code must run in OnRender
using drawingContext.DrawLine()
as opposed to using Visual
, which has the properties like SnapToDevicePixels
to address the problem.
To all who are too eager to mark questions as dupplicate
I understand that duplicate questions are bad for stackoverflow. But often a question gets marked as duplicate, which is actually different and that then prevents the question being discussed. So if you think there is already an answer, please write a comment and give me a chance to check it. I will then run that code and enlarge it. Only then one can tell, if it really works.
Things I tried already
I spent already a week trying all kind of variations to get the lines. None gave a black 1 monitor pixel line. All are grayish and more than 1 pixel wide.
Using Visual Options
Clemens suggested using RenderOptions.EdgeMode="Aliased"
and/or SnapsToDevicePixels="True"
. Here is the result without or any combination of the options:
SnapsToDevicePixels = true;
RenderOptions.SetEdgeMode((DependencyObject)this, EdgeMode.Aliased);
It seems SnapsToDevicePixels
has no effect, but with SetEdgeMode
the first 3 dashes are 1 pixel higher than the following 7 dashes, meaning aliasing seems to happen as desired, but the lines are still 2 or even 3 pixels wide and not properly black.
Using Guidelines
The very first line I painted in paint.net
as a reference how a proper line should look like. Here is the code to generate the lines:
using System;
using System.Windows;
using System.Windows.Media;
namespace Sample {
public partial class MainWindow: Window {
GlyphDrawer glyphDrawerNormal;
GlyphDrawer glyphDrawerBold;
public MainWindow() {
InitializeComponent();
Background = Brushes.Transparent;
var dpi = VisualTreeHelper.GetDpi(this);
glyphDrawerNormal = new GlyphDrawer(FontFamily, FontStyle, FontWeight, FontStretch, dpi.PixelsPerDip);
glyphDrawerBold = new GlyphDrawer(FontFamily, FontStyle, FontWeights.Bold, FontStretch, dpi.PixelsPerDip);
}
protected override void OnRender(DrawingContext drawingContext) {
drawingContext.DrawRectangle(Brushes.White, null, new Rect(0, 0, Width, Height));
drawSampleLines(drawingContext);
}
Pen penB1 = new Pen(Brushes.Black, 1);
Pen penB08 = new Pen(Brushes.Black, 0.8);
Pen penB05 = new Pen(Brushes.Black, 0.5);
const double x0 = 10.0;
const double x1 = 300.0; //line start
const double x2 = 305.0;
const int ySpacing = 18;
const int lineOffset = -7;
private void drawSampleLines(DrawingContext drawingContext) {
var y = 30.0;
glyphDrawerNormal.Write(drawingContext, new Point(x0, y), "Perfect, 1 pix y step", FontSize, Brushes.Black);
y += 2*ySpacing;
glyphDrawerBold.Write(drawingContext, new Point(x0, y), "Line samples with 1 logical unit width pen", FontSize, Brushes.Black);
y += ySpacing;
drawLineSet(drawingContext, ref y, penB1);
y += 2*ySpacing;
glyphDrawerBold.Write(drawingContext, new Point(x0, y), "Line samples with 0.8 logical unit width pen", FontSize, Brushes.Black);
y += ySpacing;
drawLineSet(drawingContext, ref y, penB08);
y += 2*ySpacing;
glyphDrawerBold.Write(drawingContext, new Point(x0, y), "Line samples with 0.5 logical unit width pen", FontSize, Brushes.Black);
y += ySpacing;
drawLineSet(drawingContext, ref y, penB05);
}
private void drawLineSet(DrawingContext drawingContext, ref double y, Pen pen) {
var yL = y + lineOffset;
glyphDrawerNormal.Write(drawingContext, new Point(x0, y), "Plain, 1 pix y step", FontSize, Brushes.Black);
for (int i = 0; i < 10; i++) {
drawingContext.DrawLine(pen, new Point(x1 + i * 9, yL+i), new Point(x2 + i * 9, yL+i));
}
y += ySpacing; yL += ySpacing;
glyphDrawerNormal.Write(drawingContext, new Point(x0, y), "Plain, 1 pix y step, 0.5 pix x offset", FontSize, Brushes.Black);
for (int i = 0; i < 10; i++) {
drawingContext.DrawLine(pen, new Point(x1 + i * 9 + .5, yL+i + 0.5), new Point(x2 + i * 9, yL+i));
}
y += ySpacing; yL += ySpacing;
glyphDrawerNormal.Write(drawingContext, new Point(x0, y), "Plain, 0.5 pix y step", FontSize, Brushes.Black);
for (int i = 0; i < 10; i++) {
drawingContext.DrawLine(pen, new Point(x1 + i * 9, yL+i/2.0), new Point(x2 + i * 9, yL+i/2.0));
}
y += ySpacing; yL += ySpacing;
glyphDrawerNormal.Write(drawingContext, new Point(x0, y), "with Guidelines, 1 pix y step", FontSize, Brushes.Black);
for (int i = 0; i < 10; i++) {
var xLeft = x1 + i * 9;
var xRight = x2 + i * 9;
var yLine = yL + i;
GuidelineSet guidelines = new GuidelineSet();
guidelines.GuidelinesX.Add(xLeft);
guidelines.GuidelinesX.Add(xRight);
guidelines.GuidelinesY.Add(yLine);
drawingContext.PushGuidelineSet(guidelines);
drawingContext.DrawLine(pen, new Point(xLeft, yLine), new Point(xRight, yLine));
drawingContext.Pop();
}
y += ySpacing; yL += ySpacing;
glyphDrawerNormal.Write(drawingContext, new Point(x0, y), "with Guidelines and 0.5 pix y offset, 1 pix y step", FontSize, Brushes.Black);
for (int i = 0; i < 10; i++) {
var xLeft = x1 + i * 9;
var xRight = x2 + i * 9;
var yLine = yL + i;
GuidelineSet guidelines = new GuidelineSet();
guidelines.GuidelinesX.Add(xLeft);
guidelines.GuidelinesX.Add(xRight);
guidelines.GuidelinesY.Add(yLine + 0.5);
drawingContext.PushGuidelineSet(guidelines);
drawingContext.DrawLine(pen, new Point(xLeft, yLine), new Point(xRight, yLine));
drawingContext.Pop();
}
y += ySpacing; yL += ySpacing;
glyphDrawerNormal.Write(drawingContext, new Point(x0, y), "with Guidelines and 0.5 pix x offset, 1 pix y step", FontSize, Brushes.Black);
for (int i = 0; i < 10; i++) {
var xLeft = x1 + i * 9;
var xRight = x2 + i * 9;
var yLine = yL + i;
GuidelineSet guidelines = new GuidelineSet();
guidelines.GuidelinesX.Add(xLeft + 0.5);
guidelines.GuidelinesX.Add(xRight + 0.5);
guidelines.GuidelinesY.Add(yLine);
drawingContext.PushGuidelineSet(guidelines);
drawingContext.DrawLine(pen, new Point(xLeft, yLine), new Point(xRight, yLine));
drawingContext.Pop();
}
}
}
/// <summary>
/// Draws glyphs to a DrawingContext. From the font information in the constructor, GlyphDrawer creates and stores the GlyphTypeface, which
/// is used everytime for the drawing of the string.
/// </summary>
public class GlyphDrawer {
Typeface typeface;
public GlyphTypeface GlyphTypeface {
get { return glyphTypeface; }
}
GlyphTypeface glyphTypeface;
public float PixelsPerDip { get; }
public GlyphDrawer(FontFamily fontFamily, FontStyle fontStyle, FontWeight fontWeight, FontStretch fontStretch, double pixelsPerDip) {
typeface = new Typeface(fontFamily, fontStyle, fontWeight, fontStretch);
if (!typeface.TryGetGlyphTypeface(out glyphTypeface))
throw new InvalidOperationException("No glyphtypeface found");
PixelsPerDip = (float)pixelsPerDip;
}
/// <summary>
/// Writes a string to a DrawingContext, using the GlyphTypeface stored in the GlyphDrawer.
/// </summary>
/// <param name="drawingContext"></param>
/// <param name="origin"></param>
/// <param name="text"></param>
/// <param name="size">same unit like FontSize: (em)</param>
/// <param name="brush"></param>
public void Write(DrawingContext drawingContext, Point origin, string text, double size, Brush brush) {
if (string.IsNullOrEmpty(text)) return;
ushort[] glyphIndexes = new ushort[text.Length];
double[] advanceWidths = new double[text.Length];
double totalWidth = 0;
for (int charIndex = 0; charIndex<text.Length; charIndex++) {
ushort glyphIndex = glyphTypeface.CharacterToGlyphMap[text[charIndex]];
glyphIndexes[charIndex] = glyphIndex;
double width = glyphTypeface.AdvanceWidths[glyphIndex] * size;
advanceWidths[charIndex] = width;
totalWidth += width;
}
GlyphRun glyphRun = new GlyphRun(glyphTypeface, 0, false, size, PixelsPerDip, glyphIndexes, origin, advanceWidths, null, null, null, null, null, null);
drawingContext.DrawGlyphRun(brush, glyphRun);
}
}
}