如何使用OnRender中的drawingContext在120 DPI分辨率下绘制精确的1个监视器像素线?

时间:2019-04-05 19:52:23

标签: c# wpf drawingcontext onrender

我只是无法在DPI为120的笔记本电脑显示器上使用OnRenderdrawingContext.DrawLine()中画一条像素宽的黑色(!)线。多年来,我一直遇到这个问题,并且阅读这里有许多关于stackoverflow的答案,但我根本无法获得有效的代码。许多答案链接到本文: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上画一条线,即5/96英寸在0.0520英寸处。最近的监视器像素将是6英寸(0.05英寸)。意味着校正将需要为0.25 LU。但是,如果我想在6 LU处画一条线,则偏移量必须为0.5。

我可以立即看到的一个问题是我的代码无法确定坐标0、0恰好位于实际的显示器像素上。如果父控件将我的控件放置在不均匀的LU上,则0,0不会与监视器像素精确对齐,这意味着我没有机会计算特定LU的偏移量。

所以我想,我到底想画10条线,每条线将y增加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步。

有关该文章中建议的使用指南的其他示例,请参见下文。如该文章所述,如果使用准则或使用偏移量没有什么区别。

问题

请提供代码,该代码可在120 DPI显示器上真正画出1像素宽的线。我知道,关于stackoverflow的答案已经很多,它们解释了理论上应该如何解决。另请注意,该代码必须使用OnRenderdrawingContext.DrawLine()中运行,而不是使用Visual来运行,SnapToDevicePixels具有诸如RenderOptions.EdgeMode="Aliased"之类的属性来解决该问题。

对于所有急于将问题标记为重复的人

我知道重复的问题对stackoverflow不利。但是通常一个问题被标记为重复,这实际上是不同的,因而阻止了该问题的讨论。因此,如果您认为已经有了答案,请发表评论,并给我机会进行检查。然后,我将运行该代码并将其放大。只有这样才能知道它是否真的有效。

我已经尝试过的东西

我已经花了一周的时间尝试各种变体来获得线条。没有人给出黑色的1个监视器像素线。全部为灰色,且宽度超过1像素。

使用视觉选项

克莱门斯建议使用SnapsToDevicePixels="True"和/或SnapsToDevicePixels = true; RenderOptions.SetEdgeMode((DependencyObject)this, EdgeMode.Aliased); 。这是没有选项或它们的任何组合的结果:

SnapsToDevicePixels

lines drawn using Visual options

似乎SetEdgeMode没有任何作用,但是对于paint.net,前3个破折号比后面的7个破折号高1个像素,这意味着出现了混叠现象,但是行仍然是2或甚至3像素宽,而且黑色也不正确。

使用准则

various ways to draw a line

我在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); } } } 中绘制的第一行是作为参考,以显示适当的行应为什么样。这是生成行的代码:

@PostMapping(path = "/test", consumes = "application/json")
public String test(@RequestBody User user) {
  return user.toString();
}

@PostMapping(path = "/test", consumes = "application/x-www-form-urlencoded")
public String test(User user) {
  return user.toString();
}

1 个答案:

答案 0 :(得分:0)

我试图在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();