Vastar
Vastar

Reputation: 89

DrawingContext non blurry lines

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 :

  1. changing the pen thickness, in relation to DPI
  2. adding a half pixel offset.

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>

enter image description here

(Border Top, Custom Border Bottom) (Zoomed in)

Upvotes: 0

Views: 58

Answers (0)

Related Questions