Compartilhar via


How to Create Custom Shapes in Windows Store Apps (C#)

In the Bing Maps SDK for Windows Store Apps there are five main types of data that you can add to the map: Pushpin, MapPolyline, MapPolygon, MapTileLayer, and UIElements. UIElements can be added to the map just like pushpins and are a great way to create custom shapes to the map. The main shapes we will focus on are the MapPolyline and MapPolygon classes. In the Bing Maps SDK for Windows Store apps these classes are pretty basic when it comes to customizations. The MapPolyline class lets you set the width and color of the line, the MapPolygon class only lets you set the fill color. If you have used any of the other Bing Maps SDK’s you may remember that there were a lot more options in other versions, such as being able to create dashed lines, and apply brushes to shapes instead of just colors. In this blog post we will see how we can make use of the ability to add a UIElement to the map to create custom shapes that have a lot more customization options.

Full source code for this blog post can be downloaded from the MSDN Code Samples here.

Creating the Base Project

To get started open Visual Studio and create a new project in C#. Select the Blank App template, call the application CustomShapes, and then press OK.

Next, add a reference to Bing Maps SDK. Right click on the References folder and press Add Reference. Select Windows → Extensions, and then select Bing Maps for C#, C++ and Visual Basic. If you do not see this option, be sure to verify that you have installed the Bing Maps SDK for Windows Store apps. While you are here, also add a reference to the Microsoft Visual C++ Runtime Package, as this is required by the Bing Maps SDK when developing using C# or Visual Basic. Press OK.

In Solution Explorer, set the Active solution platform in Visual Studio by right clicking on the Solution folder and selecting Properties. Select Configuration Properties → Configuration. Find your project and under the Platform column, set the target platform to x86, and press OK.

Now open the MainPage.xaml file and update the XAML to the following.

 <Page
    x:Class="CustomShapes.MainPage"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:CustomShapes"
    xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:m="using:Bing.Maps">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">
        <m:Map Name="MyMap" Credentials="YOUR_BING_MAPS_KEY"/>
        
        <StackPanel HorizontalAlignment="Right" VerticalAlignment="Top" Margin="100" Background="Black">
            <Button Content="Draw Polyline" Tapped="DrawPolyline_Tapped"/>
            <Button Content="Draw Polygon" Tapped="DrawPolygon_Tapped"/>
        </StackPanel>
    </Grid>
</Page>

This will add a map and two buttons to the app for drawing a custom polyline or polygon. Next open the MainPage.xaml.cs file and update it with the following code. This code contains the events handlers for the buttons without any logic in them yet.

 using Bing.Maps;
using Windows.UI;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;
using Windows.UI.Xaml.Media;

namespace CustomShapes
{
    public sealed partial class MainPage : Page
    {
        public MainPage()
        {
            this.InitializeComponent();
        }

        private void DrawPolyline_Tapped(object sender, TappedRoutedEventArgs e)
        {
        }

        private void DrawPolygon_Tapped(object sender, TappedRoutedEventArgs e)
        {
        }
    }
}
Creating Custom Shape Classes

Earlier we learnt that we can add UIElement’s to the map. If you set the location of the UIElement, using the MapLayer.SetPosition method, it will be bounded to that location. However, if we don’t set the location of the UIElement it will postion itself to the top left corner of the map and will not move as the map moves. With this in mind we can add a panel to the map and add our custom shapes to it. This will allow us to view custom shapes on the map, but these shapes won’t have any spatial relevance. To give these shapes spatial relevance we will need to use the map to convert the location coordinates into pixel coordinates so as to accurately position and draw the shapes over top of the map. This can be done using the TryLocationsToPixels method on the map. In addition to this we will need to update the position of the shape every time the map is moved or resized. This can easily be done using the respective map events.

To create our custom shape classes it might make sense to derive from the base Polyline or Polygon classes in the Windows.UI.Xaml.Shapesnamespace however these classes are sealed. To get around this we will derive our custom shape classes from the Panel class and expose properties that wrap the base Polyline and Polygon classes. For performance, rather than recreating the shape every time the map moves we will instead create a single shape and update the position as the map moves.

Create a new class file called CustomMapPolyline.cs. We will add four public properties this class; Location, Stroke, StrokeDashArray, and StrokeThickness. The Location property is a LocationCollection used to position the polyline. Whenever this property is set or the map moves, an update method will be called that converts the location data into pixel coordinates for the polyline. The Stroke property is the Brush used to draw the polyline. The StorkeDashArray property allows you to create dashed polylines. The StrokeThickness property allows you to specify the width of the polyline. Update the CustomMapPolyline.cs file with the following code.

 using Bing.Maps;
using System.Collections.Generic;
using Windows.Foundation;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;

namespace CustomShapes
{
    public class CustomMapPolyline : Panel
    {
        private Map _map;
        private LocationCollection _locations;
        private Polyline _basePolyline;

        public CustomMapPolyline(Map map)
        {
            _map = map;

            this.Width = _map.ActualWidth;
            this.Height = _map.ActualHeight;

            // Create a base polyline object to add to the map.
            _basePolyline = new Polyline();
            _basePolyline.StrokeDashCap = PenLineCap.Round;
            this.Children.Add(_basePolyline);

            _locations = new LocationCollection();

            // Update position when map view or size changes.
            _map.ViewChanged += (s, e) =>
            {
                UpdatePosition();
            };

            _map.SizeChanged += (s, e) =>
            {
                this.Width = _map.ActualWidth;
                this.Height = _map.ActualHeight;

                UpdatePosition();
            };
        }

        public LocationCollection Locations
        {
            get { return _locations; }
            set
            {
                _locations = value;
                UpdatePosition();
            }
        }

        public Brush Stroke
        {
            get { return _basePolyline.Stroke; }
            set { _basePolyline.Stroke = value; }
        }

        public DoubleCollection StrokeDashArray
        {
            get { return _basePolyline.StrokeDashArray; }
            set { _basePolyline.StrokeDashArray = value; }
        }

        public double StrokeThickness
        {
            get { return _basePolyline.StrokeThickness; }
            set { _basePolyline.StrokeThickness = value; }
        }

        private void UpdatePosition()
        {
            IList<Point> pixels = new List<Point>();

            // Convert Locations into pixels.
            if (_map.TryLocationsToPixels(_locations, pixels))
            {
                var points = new PointCollection();

                foreach (var p in pixels)
                {
                    points.Add(p);
                }

                _basePolyline.Points = points;
            }
        }
    }
}

The class for creating custom polygons won’t be all that different. The main differences are that the base shape will be a Polygon and we will add one additional property called Fill. The Fill property will allow us to set the brush to use to fill the polygon. Create a new class file called CustomMapPolygon.cs and update it with the following code.

 using Bing.Maps;
using System.Collections.Generic;
using Windows.Foundation;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Media;
using Windows.UI.Xaml.Shapes;

namespace CustomShapes
{
    public class CustomMapPolygon : Panel
    {
        private Map _map;
        private LocationCollection _locations;
        private Polygon _basePolygon;

        public CustomMapPolygon(Map map)
        {
            _map = map;

            this.Width = _map.ActualWidth;
            this.Height = _map.ActualHeight;

            // Create a base polygon object to add to the map.
            _basePolygon = new Polygon();
            _basePolygon.StrokeDashCap = PenLineCap.Round;
            this.Children.Add(_basePolygon);

            _locations = new LocationCollection();

            // Update position when map view or size changes.
            _map.ViewChanged += (s, e) =>
            {
                UpdatePosition();
            };

            _map.SizeChanged += (s, e) =>
            {
                this.Width = _map.ActualWidth;
                this.Height = _map.ActualHeight;

                UpdatePosition();
            };
        }

        public LocationCollection Locations
        {
            get { return _locations; }
            set
            {
                _locations = value;
                UpdatePosition();
            }
        }

        public Brush Fill
        {
            get { return _basePolygon.Fill; }
            set { _basePolygon.Fill = value; }
        }

        public Brush Stroke
        {
            get { return _basePolygon.Stroke; }
            set { _basePolygon.Stroke = value; }
        }

        public DoubleCollection StrokeDashArray
        {
            get { return _basePolygon.StrokeDashArray; }
            set { _basePolygon.StrokeDashArray = value; }
        }

        public double StrokeThickness
        {
            get { return _basePolygon.StrokeThickness; }
            set { _basePolygon.StrokeThickness = value; }
        }

        private void UpdatePosition()
        {
            IList<Point> pixels = new List<Point>();

            // Convert Locations into pixels.
            if (_map.TryLocationsToPixels(_locations, pixels))
            {
                var points = new PointCollection();

                foreach (var p in pixels)
                {
                    points.Add(p);
                }

                _basePolygon.Points = points;
            }
        }
    }
}
Implementing the Custom Shapes

Implementing the custom shapes is fairly easy. When the user taps the button to draw the polyline the DrawPolyline_Tapped event handler will be fired. In this event handler the map will be cleared and a new CustomMapPolyline object will be created. We will pass in a collection of locations and add a dash array to create a dashed line. We will then add the polyline to the map. Update this event handler with the following code:

 private void DrawPolyline_Tapped(object sender, TappedRoutedEventArgs e)
{
    MyMap.Children.Clear();
            
    var locations = new LocationCollection()
    {
        new Location(15,30),
        new Location(30,30),
        new Location(30,15),
    };

    var cp = new CustomMapPolyline(MyMap)
    {
        Locations = locations,
        Stroke = new SolidColorBrush(Color.FromArgb(150, 0, 255, 0)),
        StrokeThickness = 5,
        StrokeDashArray = new DoubleCollection { 2, 4 }
    };

    MyMap.Children.Add(cp);
}

When the user taps the button to draw the polygon the DrawPolygon_Tapped event handler will be fired. In this event handler the map will be cleared and a new CustomMapPolygon object will be created. We will pass in a collection of locations, a dash array to create a dashed line around the edge of the polyon, and a use a LinearGradientBrush to fill it. We will then add the polygon to the map. Update this event handler with the following code:

 private void DrawPolygon_Tapped(object sender, TappedRoutedEventArgs e)
{
    MyMap.Children.Clear();

    var locations = new LocationCollection()
    {
        new Location(15,30),
        new Location(30,30),
        new Location(30,15),
    };

    / /Create gradient brush.
    var fillBrush = new LinearGradientBrush();
    fillBrush.GradientStops.Add(new GradientStop() { Color = Color.FromArgb(150, 0, 0, 255), Offset = 0 });
    fillBrush.GradientStops.Add(new GradientStop() { Color = Color.FromArgb(150, 0, 0, 0), Offset = 1 });

    var cp = new CustomMapPolygon(MyMap)
    {
        Locations = locations,
        Fill = fillBrush,
        Stroke = new SolidColorBrush(Color.FromArgb(150, 0, 255, 0)),
        StrokeThickness = 5,
        StrokeDashArray = new DoubleCollection { 2, 4 }
    };

    MyMap.Children.Add(cp);
}

At this point the application is complete. Run it by pressing F5 or by pressing the Debug button. When you press the button to draw the polyline a green dashed polyline will appear on the map. When you press the button to draw a polygon a triangle will be drawn with a linear gradient from blue to black and a green dashed outline will be displayed on the map and like in the screenshot below.

Note that these custom shapes also work in Birdseye mode as well.

Taking things further

In this blog post we saw how to create custom shapes that can be accurately positioned over top the map. This method can be used to create other custom shapes such as complex polygons with holes, circles of geodesic lines or any other shape you could think of. The key process is to create a base shape to work with and update the pixel coordinates as the map moves or any position related properties are changed. In addition to this you can use Brushes rather than just colors to style your shapes. This opens up the possibility for a number of interesting brushes that you can use in Windows Store Apps as documented here. Some of the more interesting brushes include the ImageBrush, LinearGradientBrush, and WebViewBrush.

If you wanted to create spatially accurate circles you could expose two properties; Radius and Center location, instead of a Locations property. When these properties change or the map move you could then calculate a bunch of locations that create a circle and then use these to render a polygon that represents a circle. If the circles are going to be small and you aren’t concerned about making them spatially accurate, you can calculate an approximate pixel radius based on the zoom level of the map and the latitude coordinate of the center property. You can then create a circle on the map using the Ellipse class. I did this in a previous blog post to draw the accuracy circle of a GPS location.

If you are looking for some other great resources on Bing Maps for Windows Store apps, look through this blog or check out all the Bing Maps MSDN code samples. Also, if you are building a Windows Store app then download my free eBook on creating Location Intelligent Windows Store apps here.

Related Blog Posts

Comments