Reputation: 89
I have been trying to create my own Decorator
class, something like the Border
class, but with custom line drawing for the corners. I want some corners to be round and others to be straight.
One thing I am struggling with is trying to get lines that are not blurry. Having looked at the source code for the Border
class, they are :
Having applied this logic (by copying over their RoundLayoutValue
method), I am now getting good results, and my lines are sharp at all DPI scaling values I have tested (100%, 125%, 150%, 175%, 200% and 225%)
My question is, can anyone explain why this works? why does adding a half pixel offset fix the blurriness? also, when should you apply the half pixel offset. For the code below, I was mostly just adding it until it fixed the blurriness, but am not sure of the exact rule?
I believe the pen scaling is just to snap to pixel boundaries?
NOTE : UseLayoutRounding is set on the main window
internal class CustomBorder : Decorator
{
private Brush _brush = Brushes.PaleGreen;
public double StrokeThickness { get; set; } = 1;
public double Corner { get; set; } = 12;
public CustomBorder()
{
}
protected override void OnRender(DrawingContext drawingContext)
{
base.OnRender(drawingContext);
// assume x and y are the same
double dpi = PresentationSource.FromVisual(this).CompositionTarget.TransformToDevice.M11;
double penThickness = DoubleUtil.RoundLayoutValue(StrokeThickness, dpi);
var pen = (Pen)new Pen(Brushes.Black, penThickness).GetAsFrozen();
double halfThickness = penThickness * 0.5;
var rect = new Rect(0, 0, ActualWidth, ActualHeight);
var geometry = new StreamGeometry();
using (var context = geometry.Open())
{
context.BeginFigure(rect.TopLeft + new Vector(halfThickness, Corner), true, true);
context.ArcTo(rect.TopLeft + new Vector(Corner, halfThickness),
new Size(Corner, Corner), 90, false, SweepDirection.Clockwise, true, true);
context.LineTo(rect.TopRight + new Vector(halfThickness, halfThickness), true, true);
context.LineTo(rect.BottomRight + new Vector(halfThickness, -Corner), true, true);
context.LineTo(rect.BottomRight + new Vector(-Corner, halfThickness), true, true);
context.LineTo(rect.BottomLeft + new Vector(Corner, halfThickness), true, true);
context.LineTo(rect.BottomLeft + new Vector(halfThickness, -Corner), true, true);
context.Close();
}
drawingContext.DrawGeometry(_brush, pen, geometry);
}
}
internal static class DoubleUtil
{
// Const values come from sdk\inc\crt\float.h
internal const double DBL_EPSILON = 2.2204460492503131e-016; /* smallest such that 1.0+DBL_EPSILON != 1.0 */
internal const float FLT_MIN = 1.175494351e-38F; /* Number close to zero, where float.MinValue is -float.MaxValue */
/// <summary>
/// AreClose - Returns whether or not two doubles are "close". That is, whether or
/// not they are within epsilon of each other. Note that this epsilon is proportional
/// to the numbers themselves to that AreClose survives scalar multiplication.
/// There are plenty of ways for this to return false even for numbers which
/// are theoretically identical, so no code calling this should fail to work if this
/// returns false. This is important enough to repeat:
/// NB: NO CODE CALLING THIS FUNCTION SHOULD DEPEND ON ACCURATE RESULTS - this should be
/// used for optimizations *only*.
/// </summary>
/// <returns>
/// bool - the result of the AreClose comparision.
/// </returns>
/// <param name="value1"> The first double to compare. </param>
/// <param name="value2"> The second double to compare. </param>
public static bool AreClose(double value1, double value2)
{
//in case they are Infinities (then epsilon check does not work)
if (value1 == value2) return true;
// This computes (|value1-value2| / (|value1| + |value2| + 10.0)) < DBL_EPSILON
double eps = (Math.Abs(value1) + Math.Abs(value2) + 10.0) * DBL_EPSILON;
double delta = value1 - value2;
return (-eps < delta) && (eps > delta);
}
internal static double RoundLayoutValue(double value, double dpiScale)
{
double newValue;
// If DPI == 1, don't use DPI-aware rounding.
if (!AreClose(dpiScale, 1.0))
{
newValue = Math.Round(value * dpiScale) / dpiScale;
// If rounding produces a value unacceptable to layout (NaN, Infinity or MaxValue), use the original value.
if (IsNaN(newValue) || double.IsInfinity(newValue) || AreClose(newValue, double.MaxValue))
{
newValue = value;
}
}
else
{
newValue = Math.Round(value);
}
return newValue;
}
#if !PBTCOMPILER
[StructLayout(LayoutKind.Explicit)]
private struct NanUnion
{
[FieldOffset(0)] internal double DoubleValue;
[FieldOffset(0)] internal ulong UintValue;
}
// The standard CLR double.IsNaN() function is approximately 100 times slower than our own wrapper,
// so please make sure to use DoubleUtil.IsNaN() in performance sensitive code.
// PS item that tracks the CLR improvement is DevDiv Schedule : 26916.
// IEEE 754 : If the argument is any value in the range 0x7ff0000000000001L through 0x7fffffffffffffffL
// or in the range 0xfff0000000000001L through 0xffffffffffffffffL, the result will be NaN.
public static bool IsNaN(double value)
{
NanUnion t = new NanUnion();
t.DoubleValue = value;
ulong exp = t.UintValue & 0xfff0000000000000;
ulong man = t.UintValue & 0x000fffffffffffff;
return (exp == 0x7ff0000000000000 || exp == 0xfff0000000000000) && (man != 0);
}
#endif
}
<Window x:Class="WpfApp26.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:WpfApp26"
mc:Ignorable="d"
UseLayoutRounding="True"
Title="MainWindow"
Height="450"
Width="800">
<StackPanel VerticalAlignment="Center">
<Border HorizontalAlignment="Center"
VerticalAlignment="Center"
Background="PaleGreen"
BorderThickness="1"
BorderBrush="Black"
CornerRadius="12,0,0,0"
Margin="5">
<TextBlock Text="Hello world!"
Margin="25" />
</Border>
<local:CustomBorder HorizontalAlignment="Center"
VerticalAlignment="Center"
Margin="5">
<TextBlock Text="Hello world!"
Margin="25" />
</local:CustomBorder>
</StackPanel>
</Window>
(Border Top, Custom Border Bottom) (Zoomed in)
Upvotes: 0
Views: 58