Как нарисовать точную линию в 1 пиксель монитора, используя DrawingContext в OnRender для разрешения 120 точек на дюйм?

#c# #wpf #drawingcontext #onrender

#c# #wpf #drawingcontext #onrender

Вопрос:

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

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

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

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

Вопрос

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

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

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

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

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

Использование визуальных опций

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

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

линии, нарисованные с использованием визуальных опций

Кажется, что это SnapsToDevicePixels не имеет никакого эффекта, но SetEdgeMode первые 3 штриха на 1 пиксель выше, чем следующие 7 штрихов, что означает, что сглаживание, похоже, происходит по желанию, но линии по-прежнему имеют ширину 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);
    }
  }
}
  

Комментарии:

1. Помогает ли это установить RenderOptions.EdgeMode="Aliased" и / или SnapsToDevicePixels="True" в окне?

2. @Clemens: Я пытался SnapsToDevicePixels раньше, но безуспешно. Я предполагал, что такого рода параметры работают только с Visual s, но я рисую непосредственно в OnRender . Однако, основываясь на вашем предложении, я попробовал еще раз (см. Отредактированную часть в моем вопросе), и, похоже, EdgeMode это улучшение.

Ответ №1:

Я пытался добиться лучшего вида прямых линий в приложении для обозначения (линии посоха, штриховые линии, основы для заметок и т.д. …) На дисплеях с настройками 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();