简体   繁体   中英

How to draw an exact 1 monitor pixel line using drawingContext in OnRender for a 120 DPI resolution?

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, ie 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, ie 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: 第一行:完美黑色1像素线,第二行:WPF线

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

I was trying to achieve better looking straight lines in a notation application (staff lines, bar lines, note stems etc...) on displays with DPI settings other than 96, and came up with the following code. It really worked for me.

Matrix m = PresentationSource.FromVisual(this).CompositionTarget.TransformToDevice;
double dpiFactor = 1/m.M11;
ScaleTransform scale = New ScaleTransform(dpiFactor, dpiFactor);
dc.PushTransform(scale);
...
...
dc.Pop();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM