Как нарисовать точную линию в 1 пиксель монитора, используя drawContext в OnRender для разрешения 120 DPI? - PullRequest
2 голосов
/ 05 апреля 2019

Мне просто не удается нарисовать черную (!) Линию шириной в один пиксель в OnRender, используя drawingContext.DrawLine() на мониторе моего ноутбука, который имеет DPI 120. Я сталкиваюсь с этой проблемой много лет назад и читаю много ответов здесь на stackoverflow, но я просто не могу получить рабочий код. Многие ответы ссылаются на эту статью: wpftutorial.net: Draw On Physical Device Pixels . Он рекомендует использовать Руководящие принципы и смещать их на половину пикселя, то есть установить y = 10,5 вместо желаемой позиции 10 для монитора 96 DPI. Но это не объясняет, как сделать расчет для разных точек на дюйм.

WPF использует логические единицы (LU), которые имеют ширину 1/96 дюйма, что означает, что drawContext.DrawLine (Width: 1) хочет нарисовать линию шириной 1/96 дюйма. Пиксели моего монитора имеют ширину 1/120 дюйма.

В статье только говорится, что ширина линии для 120 DPI должна составлять не 1 LU, а 0,8 LU, что составляет 1/120 дюйма, ширину пикселя от моего монитора.

Соответственно смещение должно быть 0,4 вместо 0,5? Это делает весь расчет координат очень сложным. Допустим, я хочу нарисовать линию на 5 LU, то есть на 5/96 дюйма, что составляет 0,0520 дюйма. Ближайший пиксель монитора будет номер 6, который находится на 0,05 дюйма. Значение коррекции должно быть 0,25 LU. Но если я хочу нарисовать линию в 6 LU, смещение должно быть 0,5.

Одна проблема, которую я сразу вижу, состоит в том, что мой код не может быть уверен, что координаты 0, 0 находятся точно на реальном пикселе монитора. Если родительский элемент управления помещает мой элемент управления в неравное число LU, то значение 0,0 не будет точно совпадать с пикселем монитора, что означает, что у меня нет шансов рассчитать для конкретного LU, каким должно быть его смещение.

Так что я подумал, какого черта, я просто пытаюсь нарисовать 10 линий, для каждой из которых увеличивается у на 0,1. Результат (второй ряд) выглядит так: First row: perfect black 1 pixel line, second row: WPF lines

В первой строке показано, как должна выглядеть идеальная черная линия шириной в 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 шагов.

См. Ниже другие примеры с использованием рекомендаций, рекомендованных в этой статье. Как написано в этой статье, не имеет значения, если кто-то использует руководящие принципы или работает со смещениями.

Вопрос

Пожалуйста, предоставьте код , который действительно рисует линию шириной 1 пиксель на мониторе с разрешением 120 точек на дюйм. Я знаю, уже есть много ответов на stackoverflow, которые объясняют, как это должно быть теоретически решено. Также обратите внимание, что код должен запускаться в OnRender с использованием drawingContext.DrawLine(), а не Visual, который имеет свойства, такие как SnapToDevicePixels, для решения проблемы.

Всем, кто слишком хочет пометить вопросы как дублирующие

Я понимаю, что повторяющиеся вопросы вредны для stackoverflow. Но часто вопрос помечается как дубликат, который на самом деле отличается, и это затем мешает обсуждению вопроса. Поэтому, если вы считаете, что ответ уже есть, напишите комментарий и дайте мне возможность проверить его. Затем я запущу этот код и увеличу его. Только тогда можно сказать, действительно ли это работает.

То, что я уже пробовал

Я потратил уже неделю, пробуя всевозможные варианты, чтобы получить строки. Никто не дал черную 1 пиксельную линию монитора. Все они сероватые и имеют ширину более 1 пикселя.

Использование визуальных параметров

Клеменс предложил использовать RenderOptions.EdgeMode="Aliased" и / или SnapsToDevicePixels="True". Вот результат без какой-либо комбинации параметров:

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

lines drawn using Visual options

Кажется, SnapsToDevicePixels не имеет никакого эффекта, но с SetEdgeMode первые 3 черты на 1 пиксель выше, чем следующие 7 черточек, что означает, что сглаживание кажется желательным, но линии по-прежнему имеют ширину 2 или даже 3 пикселя. и не совсем черный.

Использование руководящих принципов

various ways to draw a line

Самая первая строка, которую я нарисовал в 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);
    }
  }
}
...