我只是无法在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 像素宽的黑线放大 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 上已经有很多答案解释了理论上应该如何解决。另请注意,代码必须在OnRender
usingdrawingContext.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);
}
}
}