#1,149 – Drawing an Arc in a Custom Shape

We can use a StreamGeometryContext to render some geometry in a custom Shape element that we can then use in XAML.  Below is an example that draws a simple arc from 0 degress to 90 degrees.  It uses a PolarPoint class to allow describing the arc start and finish as polar coordinates.  (A future post will allow a user to specify arc start and end).

    public class Arc : Shape
    {
        protected override Geometry DefiningGeometry
        {
            get
            {
                double maxWidth = RenderSize.Width;
                double maxHeight = RenderSize.Height;
                double maxRadius = Math.Min(maxWidth, maxHeight) / 2.0;

                PolarPoint arcStart = new PolarPoint(maxRadius, 0.0);
                PolarPoint arcFinish = new PolarPoint(maxRadius, 90.0);

                StreamGeometry geom = new StreamGeometry();
                using (StreamGeometryContext ctx = geom.Open())
                {
                    ctx.BeginFigure(
                        new Point((maxWidth / 2.0) + arcStart.X,
                                   (maxHeight / 2.0) - arcStart.Y),
                        false,
                        false);
                    ctx.ArcTo(
                        new Point((maxWidth / 2.0) + arcFinish.X,
                                  (maxHeight / 2.0) - arcFinish.Y),
                        new Size(maxRadius, maxRadius),
                        0.0,     // rotationAngle
                        false,   // greater than 180 deg?
                        SweepDirection.Counterclockwise,
                        true,    // isStroked
                        true);
                }

                return geom;
            }
        }
    }

Using the arc:

        <loc:Arc Stroke="Black" StrokeThickness="1"
                 Height="100" Width="100" Margin="5"
                 HorizontalAlignment="Center"/>

1149-001

#1,148 – Sample Code to Convert from Polar to Cartesian Coordinates

Below is some simple code (not productized) that can convert from two-dimensional polar to cartesian coordinates.

    public class PolarPoint
    {
        // Angle expressed in degrees
        public PolarPoint(double radius, double angleDeg)
        {
            if (radius < 0.0)
                throw new ArgumentException("Radius must be non-negative");
            if ((angleDeg < 0) || (angleDeg >= 360.0))
                throw new ArgumentException("Angle must be in range [0,360)");

            Radius = radius;
            AngleDeg = angleDeg;
        }

        // Polar coordinates
        public double Radius { get; set; }
        public double AngleDeg { get; set; }

        // Cartesian coordinates
        public double X
        {
            get { return Radius * Math.Cos(AngleDeg * Math.PI / 180.0); }
        }

        public double Y
        {
            get { return Radius * Math.Sin(AngleDeg * Math.PI / 180.0); }
        }

        public override string ToString()
        {
            return string.Format("({0},{1})", X, Y);
        }
    }

We can then use this class as follows.

            Console.WriteLine(new PolarPoint(0.0, 0.0));
            Console.WriteLine(new PolarPoint(1.0, 0.0));
            Console.WriteLine(new PolarPoint(1.0, 45.0));
            Console.WriteLine(new PolarPoint(1.0, 90.0));
            Console.WriteLine(new PolarPoint(1.0, 135.0));
            Console.WriteLine(new PolarPoint(1.0, 180.0));

1148-001

#1,147 – Converting from Polar Coordinates to Cartesian Coordinates

You can represent a two dimensional point as either a cartesian coordinate (X,Y) or a polar coordinate (r,theta).  Conversions between these two coordinate systems are shown below.

From polar coordinates to cartesian:

1147-001

To convert from cartesian coordinates to polar, the following formula works, as long as the X value is positive.

1147-002

If X is 0 or negative, then the calculation for theta becomes:

1147-003

#1,146 – Polar Coordinate System

In most cases when you’re working with graphical objects, you use a cartesian coordinate system, where each point is represented as an X and a Y value, indicating the point’s distance from an origin in two different dimensions.

You can also express points in a two-dimensional system using a polar coordinate system.  Each point in a polar coordinate system is represented with two values:

  • A radius value, describing how far the point is from an origin  (range is any non-negative number)
  • An angular coordinate, describing how many degrees around the circle the point is located, typically from a horizontal ray extending to the right of the origin  (range typically [0, 360) degrees or [0, 2*pi) radians)

Below is a picture showing two sample points expressed in polar coordinates.

  • (2.0, 60) – Radius = 2, Angle = 60 degrees (counterclockwise) from horizontal
  • (1.0, 180) – Radius = 1, Angle = 180 degrees (counterclockwise) from horizontal

1146-001

 

#1,145 – Using RenderSize in Custom Shape

When drawing a geometry in a custom Shape element, you could draw using hard-coded coordinates.  It’s more common, however, to use the RenderSize property of the UIElement to render the object so that the geometry scales based on the size of the control.

Below, we create a custom shape that draws a diagonal line from the upper left corner of the control to the lower right.

    public class MyShape : Shape
    {
        protected override Geometry DefiningGeometry
        {
            get
            {
                double maxWidth = RenderSize.Width;
                double maxHeight = RenderSize.Height;

                StreamGeometry geom = new StreamGeometry();
                using (StreamGeometryContext ctx = geom.Open())
                {
                    ctx.BeginFigure(
                        new Point(0.0, 0.0),
                        false,
                        false);
                    ctx.LineTo(
                        new Point(maxWidth, maxHeight),
                        true,
                        false);
                }

                return geom;
            }
        }
    }

We can use the shape in XAML as follows:

    <StackPanel>
        <loc:MyShape Stroke="Black" StrokeThickness="1"
                     Height="50" Width="50"
                     HorizontalAlignment="Center"/>
    </StackPanel>

Now when we change the size of the underlying control, the geometry adjusts as well.

1145-001

1145-002

1145-003

#1,144 – Geometry in Custom Shape Doesn’t Automatically Scale

If you define a custom Shape by creating some Geometry, the resulting geometry will not automatically scale when shape’s size is changed.

Suppose that we have the following custom shape.

    public class MyShape : Shape
    {
        protected override Geometry DefiningGeometry
        {
            get
            {
                StreamGeometry geom = new StreamGeometry();
                using (StreamGeometryContext ctx = geom.Open())
                {
                    ctx.BeginFigure(
                        new Point(0.0, 0.0),
                        false,
                        false);
                    ctx.LineTo(
                        new Point(50.0, 50.0),
                        true,
                        false);
                }

                return geom;
            }
        }
    }

Placing this control in a StackPanel, it’s size is just large enough to accommodate the geometry.

1144-001

If we explicitly make the shape larger, the underlying geometry stays the same size.

1144-002

#1,143 – Coordinate System for StreamGeometry

You can use a StreamGeometry object, along with the StreamGeometryContext returned by its Open method, to draw simple geometric shapes.

When using the various methods of a StreamGeometryContext instance, you work with X and Y values.  The coordinate system used has the upper left corner of the drawing region at (0,0), with X values increasing from left to right and Y values increasing from top to bottom.

1143-001

Below, we have a custom shape that draws a line segment from (0,0) to (50,50) and then another line segment to (75,25).

    public class MyShape : Shape
    {
        protected override Geometry DefiningGeometry
        {
            get
            {
                return GetMyShapeGeometry();
            }
        }

        private Geometry GetMyShapeGeometry()
        {
            StreamGeometry geom = new StreamGeometry();
            using (StreamGeometryContext ctx = geom.Open())
            {
                ctx.BeginFigure(
                    new Point(0.0, 0.0),
                    false,    // is NOT filled
                    false);   // is NOT closed
                ctx.LineTo(
                    new Point(50.0, 50.0),
                    true,     // is stroked (line visible)
                    false);   // is not smoothly joined w/other segments
                ctx.LineTo(
                    new Point(75.0, 25.0),
                    true,     // is stroked (line visible)
                    false);   // is not smoothly joined w/other segments
            }

            return geom;
        }
    }

We can then use this shape from XAML.

    <Canvas>
        <loc:MyShape Canvas.Top="0" Canvas.Left="0"
                     Stroke="Black" />
    </Canvas>

1143-002

#1,142 – Setting Attached Property Value from Code

You can change the value of an attached property for a given control from code by using the SetValue or SetCurrentValue methods.  You call these methods on the control that the property is attached to, passing in a reference to the property and the new value.  (SetCurrentValue is preferred, to avoid overwriting a local value).

Below, we set a value for MyAttProps.Important in XAML, but also wire up a Click event to allow changing the value from code.

        <Label x:Name="lblHi" Content="Hi there"
               loc:MyAttProps.Important="True"
               Background="AliceBlue"/>
        <Button Content="Change Content"
                Click="Button_Click"/>

In the code-behind, we use SetCurrentValue to change the value.

        private void Button_Click(object sender, RoutedEventArgs e)
        {
            bool impValue = (bool)lblHi.GetValue(MyAttProps.ImportantProperty);

            lblHi.SetCurrentValue(MyAttProps.ImportantProperty, !impValue);
        }

#1,141 – Attached Properties Allow Customization of Existing Controls

An attached property is a dependency property defined in one class and then attached (used) on an instance of some other class.

You can define your own attached properties as a mechanism for extending the appearance or behavior of a control.

Below, we register a dependency property of type bool, named Important.  When Important is set to true, we set the foreground brush of the control to red.

    public class MyAttProps
    {
        // Surround property definition
        static PropertyMetadata ImportantMetadata =
            new PropertyMetadata(
                false,                // Default value
                OnImportantChanged,   // Changed callback
                null);                // Coerce value callback

        public static readonly DependencyProperty ImportantProperty =
            DependencyProperty.RegisterAttached(
                "Important",          // Property name
                typeof(bool),         // Property type
                typeof(MyAttProps),   // Defining class type
                ImportantMetadata);   // Metadata described above

        // Allow setting value from XAML
        public static void SetImportant(DependencyObject depObj, bool value)
        {
            depObj.SetValue(ImportantProperty, value);
        }

        public static bool GetImportant(DependencyObject depObj)
        {
            return (bool)depObj.GetValue(ImportantProperty);
        }

        // Important property has changed
        public static void OnImportantChanged(
            DependencyObject d,
            DependencyPropertyChangedEventArgs e)
        {
            Control ctrl = d as Control;
            bool important = (bool)e.NewValue;
            if ((ctrl != null) && important)
                ctrl.SetValue(Control.ForegroundProperty, Brushes.Red);
            else
                ctrl.ClearValue(Control.ForegroundProperty);
        }
    }

We can now use this new property from XAML:

        <Label x:Name="lblHi" Content="Hi there" 
               loc:MyAttProps.Important="True"
               Background="AliceBlue"/>

1141-001

#1,140 – Using a Value Converter in a Template

You can use a value converter anywhere in XAML where you are using data binding.

Below is an example of using a value converter within a data template.  The Visibility property is bound to the underlying Actor object that is the data context for the item template.  The value converter then derives a value for Visibility from several properties within the Actor object.  (Assume that we have an ActorList property that is a collection of Actor instances).

The XAML includes:

    <Window.Resources>
        <loc:DeadFredConverter x:Key="deadFredConverter"/>
    </Window.Resources>

    <StackPanel>
        <ListBox Margin="15" Width="270" Height="320"
             ItemsSource="{Binding ActorList}">
            <ListBox.ItemTemplate>
                <DataTemplate>
                    <StackPanel Orientation="Horizontal">
                        <Image Source="{Binding Image}" Height="80"/>
                        <StackPanel Margin="5">
                            <TextBlock Text="{Binding FullName}" FontSize="12" FontWeight="Bold"/>
                            <TextBlock Text="{Binding Dates}"/>
                            <TextBlock Text="{Binding KnownFor}" Margin="0,5,0,0" FontStyle="Italic"/>
                        </StackPanel>
                        <Label Content="Dead Fred !" Foreground="Red"
                               FontWeight="Bold"
                               Visibility="{Binding Converter={StaticResource deadFredConverter}}"/>
                    </StackPanel>
                </DataTemplate>
            </ListBox.ItemTemplate>
        </ListBox>
    </StackPanel>

The body of the value converter is:

    class DeadFredConverter : IValueConverter
    {
        // Convert to Visibility, deriving from properties on Actor object
        public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            Actor a = (Actor)value;

            Visibility vis = Visibility.Hidden;

            if ((a.FirstName == "Fred") &&
                a.DeathYear.HasValue &&
                (a.DeathYear <= DateTime.Today.Year))
                vis = Visibility.Visible;

            return vis;
        }

        public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
        {
            throw new NotImplementedException();
        }
    }

1140-001