#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 пиксель, увеличенная в 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();