Мне просто не удается нарисовать черную (!) Линию шириной в один пиксель в 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. Результат (второй ряд) выглядит так:
В первой строке показано, как должна выглядеть идеальная черная линия шириной в 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);
}
}
}