4

我只是无法在DPI 为 120 的笔记本电脑显示器上绘制单个像素宽的黑色(!)线。多年来我一直面临这个问题,我在 stackoverflow 上阅读了很多答案,但我根本无法OnRender得到drawingContext.DrawLine()工作代码。许多答案链接到这篇文章:wpftutorial.net: Draw On Physical Device Pixels。它建议使用指南并将它们偏移半个像素,即设置 y=10.5 而不是 96 DPI 监视器所需的位置 10。但它没有解释如何针对不同的 DPI 进行计算。

WPF 使用 1/96 英寸宽的逻辑单元 (LU),这意味着 drawingContext.DrawLine(Width: 1) 想要绘制一条 1/96 英寸宽的线。我的显示器的像素是 1/120 英寸宽。

文章只说 120 DPI 的线宽不应该是 1 LU,而是 0.8 LU,也就是 1/120 英寸,我的显示器一个像素的宽度。

因此偏移量应该是 0.4 而不是 0.5 吗?这使得整个坐标计算非常复杂。假设我想在 5 LU 上画一条线,即 0.0520 英寸处的 5/96 英寸。最近的监视器像素是 0.05 英寸的 6 号。这意味着校正需要为 0.25 LU。但如果我想在 6 LU 处画一条线,偏移量需要为 0.5。

我可以立即看到的一个问题是,我的代码无法确定坐标 0、0 是否精确地位于真实的监视器像素上。如果父控件将我的控件放在奇数个 LU 上,那么 0,0 将不会与监视器像素精确对齐,这意味着我没有机会计算特定 LU 的偏移量应该是多少。

所以我想,这到底是怎么回事,我只是尝试画 10 条线,每条线的 y 增加 0.1。结果(第二行)如下所示: 第一行:完美黑色 1 像素线,第二行:WPF 线

第一行显示了完美的 1 像素宽的黑线放大 8 倍后的样子。第二行显示了 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));
}

如您所知,它们中没有一个正好是 1 像素宽,因此它们都不是真正的黑色!

如果上面提到的文章是正确的,那么至少其中一行应该正确显示。原因:96 DPI和120 DPI相差1/5。这意味着每 5 个 LU 像素应该从与监视器像素完全相同的位置开始。偏移量应该是 1/2 LU,这就是为什么我做了 10 1/10 步。

有关使用该文章中推荐的指南的其他示例,请参见下文。正如那篇文章中所写,如果使用指南或使用偏移量并没有什么不同。

问题

请提供在120 DPI 显示器上真正绘制 1 像素宽的线的代码。我知道,stackoverflow 上已经有很多答案解释了理论上应该如何解决。另请注意,代码必须在OnRenderusingdrawingContext.DrawLine()而不是 using 中运行Visual,后者具有SnapToDevicePixels解决问题的属性。

致所有急于将问题标记为重复的人

我知道重复的问题对 stackoverflow 不利。但通常一个问题会被标记为重复,这实际上是不同的,这会阻止问题被讨论。因此,如果您认为已经有答案,请写下评论并给我一个检查的机会。然后我将运行该代码并放大它。只有这样才能判断它是否真的有效。

我已经尝试过的事情

我已经花了一周的时间尝试各种变化来获得线条。没有人给出黑色 1 监视器像素线。所有都是灰色的,宽度超过 1 个像素。

使用视觉选项

Clemens 建议使用RenderOptions.EdgeMode="Aliased"和/或SnapsToDevicePixels="True". 这是没有选项或任何选项组合的结果:

SnapsToDevicePixels = true;
RenderOptions.SetEdgeMode((DependencyObject)this, EdgeMode.Aliased);

使用视觉选项绘制的线条

它似乎SnapsToDevicePixels没有效果,但SetEdgeMode前 3 个破折号比后面的 7 个破折号高 1 个像素,这意味着锯齿似乎会按需要发生,但线条仍然是 2 甚至 3 个像素宽,并且不是正确的黑色。

使用指南

画线的各种方法

我画的第一条线paint.net作为参考,一条正确的线应该是什么样子。这是生成行的代码:

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

1 回答 1

0

我试图在 DPI 设置不是 96 的显示器上的符号应用程序(五线谱线、小节线、音符干等)中获得更好看的直线,并提出了以下代码。它真的对我有用。

Matrix m = PresentationSource.FromVisual(this).CompositionTarget.TransformToDevice;
double dpiFactor = 1/m.M11;
ScaleTransform scale = New ScaleTransform(dpiFactor, dpiFactor);
dc.PushTransform(scale);
...
...
dc.Pop();
于 2020-01-14T12:16:08.870 回答