How to Save and Share Map Screenshots in Windows Store Apps (.NET)

A while ago I wrote a blog post on How to Share Maps Using the Search Charm in Windows Store Apps. In that blog post we made use of the Bing Maps REST Imagery service to generate a static image of the map that we could share in an email. This method has a couple of limitations. The first is that you can only render pushpins and routes on the static map image. The second is that the pushpin icon is limited to one that already exists in the service. This might be acceptable for simple applications but what if we want a true screenshot of our map? In this blog post we are going to take a look at how to take a screenshot of the map which we can then either share via the Share charm or save as a local file for use later in documents or presentations.

Full source code for this blog can be found in the MSDN Code Sample Gallery.

Before we dive in, its worth noting that if you just want a screenshot of your app there are three ways to go about this. The first is simply press the “Print Screen” button on your keyboard when your app is running to add a screenshot of your app to the clipboard. The second method applies to tablets and consists of pressing and holding down the Windows button on the tablet and the volume down button at the same time. The third method appears to be new in Windows 8.1 Update 1 and consists of pressing on the Share button and then clicking on the down arrow that appears. You will see at least two options; one to take a screenshot and the other to share the app in the Windows Store.

 

The Bing Maps Terms of Use allow you to make use of screenshots of the maps however there are some legal limitations to be aware that are documented under the Bing Maps Print Rights.

Creating the Base Project

To get started open Visual Studio and create a new project in C# or Visual Basic. Select the Blank App template, call the application BingMapsScreenshots, 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="BingMapsScreenshots.MainPage"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="using:BingMapsScreenshots"
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
    xmlns:m="using:Bing.Maps">

    <Grid Background="{ThemeResource ApplicationPageBackgroundThemeBrush}">       
        <m:Map Name="MyMap" Credentials="YOUR_BING_MAPS_KEY"/>

        <ScrollViewer Name="Instructions" MaxWidth="400" Margin="100" HorizontalAlignment="Left"/>
        
        <StackPanel Background="Black" HorizontalAlignment="Right" VerticalAlignment="Top" Margin="100">
            <Button Content="Take Screenshot of App" Tapped="TakeAppScreenshot_Tapped"/>
            <Button Content="Take Screenshot of Map" Tapped="TakeMapScreenshot_Tapped"/>
            <Button Content="Share Screenshot of Map" Tapped="ShareMapScreenshot_Tapped"/>
        </StackPanel>
    </Grid>
</Page>

This will add a map, a ScrollViewer for route instructions, and three buttons to the app. The buttons will be used to take screenshots and for sharing the map. Next open the MainPage.xaml.cs file and update it with the following code. This code contains the event handlers for the buttons without any logic in them yet. I’ve also added logic to display a route from Seattle to Miami on the map when the app loads so that we have something interesting to share.

C#

using System;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.ApplicationModel.DataTransfer;
using Windows.Graphics.Imaging;
using Windows.Storage.Streams;
using Windows.UI.Xaml;
using Windows.UI.Xaml.Controls;
using Windows.UI.Xaml.Input;

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

            MyMap.Loaded += MyMap_Loaded;
        }

        private async void MyMap_Loaded(object sender, RoutedEventArgs e)
        {
            var directionsManager = MyMap.DirectionsManager;
            directionsManager.Waypoints.Add(new Bing.Maps.Directions.Waypoint("Seattle, WA"));
            directionsManager.Waypoints.Add(new Bing.Maps.Directions.Waypoint("Miami, FL"));

            Instructions.Content = MyMap.DirectionsManager.RouteSummaryView;
            await directionsManager.CalculateDirectionsAsync();
        }

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

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

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

VB

Imports Windows.ApplicationModel.DataTransfer
Imports Windows.Storage.Streams
Imports Windows.Graphics.Imaging

Public NotInheritable Class MainPage
    Inherits Page

    Public Sub New()
        Me.InitializeComponent()

        AddHandler MyMap.Loaded, AddressOf MyMap_Loaded
    End Sub

    Private Async Sub MyMap_Loaded(sender As Object, e As RoutedEventArgs)
        Dim directionsManager = MyMap.DirectionsManager

        directionsManager.Waypoints.Add(New Bing.Maps.Directions.Waypoint("Seattle, WA"))
        directionsManager.Waypoints.Add(New Bing.Maps.Directions.Waypoint("Miami, FL"))

        Instructions.Content = MyMap.DirectionsManager.RouteSummaryView
        Await directionsManager.CalculateDirectionsAsync()
    End Sub

    Private Sub TakeAppScreenshot_Tapped(sender As Object, e As TappedRoutedEventArgs)
    End Sub

    Private Sub TakeMapScreenshot_Tapped(sender As Object, e As TappedRoutedEventArgs)
    End Sub

    Private Sub ShareMapScreenshot_Tapped(sender As Object, e As TappedRoutedEventArgs)
    End Sub
End Class

At this point if we run the app and give it a moment to load the route it will look like this. Pressing any of the buttons won’t do anything as we haven’t added the logic for them yet.

Saving a Screenshot

We can create a screenshot of a FrameworkElement by using a RenderTargetBitmap. To keep things clean, we will create a couple of helper Task methods. The first method will take in a FrameworkElement to take a screenshot of and an IRandomAccessStream to save the screenshot to. This method will encode the screenshot as a PNG image file. The second helper method will only take in a FrameworkElement and will return a RandomAccessStreamReference. Notice that these methods take in a FrameworkElement and not a Map object. This allows us to pass in just about anything that is displayed in our app that we want to take a screenshot of. Add the following code to the MainPage.xaml.cs file.

C#

private async Task ScreenshotToStreamAsync(FrameworkElement element, IRandomAccessStream stream)
{
    var renderTargetBitmap = new Windows.UI.Xaml.Media.Imaging.RenderTargetBitmap();
    await renderTargetBitmap.RenderAsync(element);

    var pixelBuffer = await renderTargetBitmap.GetPixelsAsync();

    var dpi = Windows.Graphics.Display.DisplayInformation.GetForCurrentView().LogicalDpi;

    var encoder = await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream);
    encoder.SetPixelData(
        BitmapPixelFormat.Bgra8,
        BitmapAlphaMode.Ignore,
        (uint)renderTargetBitmap.PixelWidth,
        (uint)renderTargetBitmap.PixelHeight,
        dpi,
        dpi,
        pixelBuffer.ToArray());

    await encoder.FlushAsync();
}

private async Task<RandomAccessStreamReference> ScreenshotToStreamReferenceAsync(FrameworkElement element)
{
    var ms = new InMemoryRandomAccessStream();
    await ScreenshotToStreamAsync(element, ms);
    ms.Seek(0);
    return RandomAccessStreamReference.CreateFromStream(ms);
}

VB

Private Async Function ScreenshotToStreamAsync(element As FrameworkElement, stream As IRandomAccessStream) As Task
    Dim renderTargetBitmap = New Windows.UI.Xaml.Media.Imaging.RenderTargetBitmap()
    Await renderTargetBitmap.RenderAsync(element)

    Dim pixelBuffer = Await renderTargetBitmap.GetPixelsAsync()

    Dim dpi = Windows.Graphics.Display.DisplayInformation.GetForCurrentView().LogicalDpi

    Dim encoder = Await BitmapEncoder.CreateAsync(BitmapEncoder.PngEncoderId, stream)
    encoder.SetPixelData(
        BitmapPixelFormat.Bgra8,
        BitmapAlphaMode.Ignore,
        renderTargetBitmap.PixelWidth,
        renderTargetBitmap.PixelHeight,
        dpi,
        dpi,
        pixelBuffer.ToArray())

    Await encoder.FlushAsync()
End Function

Private Async Function ScreenshotToStreamReferenceAsync(element As FrameworkElement) As Task(Of RandomAccessStreamReference)
    Dim ms = New InMemoryRandomAccessStream()
    Await ScreenshotToStreamAsync(element, ms)
    ms.Seek(0)
    Return RandomAccessStreamReference.CreateFromStream(ms)
End Function

Next we will create a method that creates a screenshot of a FrameworkElement and then saves it to a file using the FileSavePicker. Add the following code to the MainPage.xaml.cs file.

C#

private async void SaveScreenshot(FrameworkElement captureSource, string suggestedName)
{
    //Create a FileSavePicker.
    var savePicker = new Windows.Storage.Pickers.FileSavePicker()
    {
        DefaultFileExtension = ".png",
        SuggestedFileName = suggestedName,
        SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.PicturesLibrary
    };

    savePicker.FileTypeChoices.Add(".png", new System.Collections.Generic.List<string> { ".png" });

    //Prompt the user to select a file.
    var saveFile = await savePicker.PickSaveFileAsync();

    //Verify the user selected a file.
    if (saveFile != null)
    {                
        using (var fileStream = await saveFile.OpenAsync(Windows.Storage.FileAccessMode.ReadWrite))
        {
            //Capture the screenshot and save it to the file stream.
            await ScreenshotToStreamAsync(captureSource, fileStream);
        }
    }
}

VB

Private Async Sub SaveScreenshot(captureSource As FrameworkElement, suggestedName As String)
    'Create a FileSavePicker.
    Dim savePicker = New Windows.Storage.Pickers.FileSavePicker()
    savePicker.DefaultFileExtension = ".png"
    savePicker.SuggestedFileName = suggestedName
    savePicker.SuggestedStartLocation = Windows.Storage.Pickers.PickerLocationId.PicturesLibrary

    savePicker.FileTypeChoices.Add(".png", New System.Collections.Generic.List(Of String)(New String() {".png"}))

    'Prompt the user to select a file.
    Dim saveFile = Await savePicker.PickSaveFileAsync()

    'Verify the user selected a file.
    If saveFile IsNot Nothing Then
        Using fileStream = Await saveFile.OpenAsync(Windows.Storage.FileAccessMode.ReadWrite)
            'Capture the screenshot and save it to the file stream.
            Await ScreenshotToStreamAsync(captureSource, fileStream)
        End Using
    End If
End Sub

We can now update the button handlers for capturing screenshots to make use of this method. The first handler creates a screenshot of the whole app, and the second handler will create a screenshot of just the map.

C#

private void TakeAppScreenshot_Tapped(object sender, TappedRoutedEventArgs e)
{
    SaveScreenshot(this, "AppScreenshot.png");
}

private void TakeMapScreenshot_Tapped(object sender, TappedRoutedEventArgs e)
{
    SaveScreenshot(MyMap, "MapScreenshot.png");
}

VB

Private Sub TakeAppScreenshot_Tapped(sender As Object, e As TappedRoutedEventArgs)
    SaveScreenshot(Me, "AppScreenshot.png")
End Sub

Private Sub TakeMapScreenshot_Tapped(sender As Object, e As TappedRoutedEventArgs)
    SaveScreenshot(MyMap, "MapScreenshot.png")
End Sub

If you run the application and press the button to take a screenshot of just the map you will be prompted to select a file to save the screenshot to. Once this is done, if you view the screenshot it should look something like the following image. Notice that our buttons for taking screenshots and the route instructions are not in the image as they are not a part of the map.

Sharing a Screenshot

In order to share our map we will need to make use of the Share charm. To do this we will need to attach a DataRequested event to the DataTransferManager when the user navigates to the app. We will also remove this event handler when the user navigates away from the app. When this event is triggered we will generate two screenshots, one of the map and the other of the route instructions. Notice that for the route instructions we are capturing a screenshot of the Map.DirectionsManager.RouteSummaryView element rather than the Instructions ScrollViewer element. The main reason for this is that the ScrollViewer will clip the route instructions to only those that are visible on the screen whereas using the RouteSummaryView captures the full set of instructions. We will then share the images by setting the HTML format of the share charm in a similar manner to how we did it in the previous blog post on sharing maps. Add the following code to the MainPage.xaml.cs file.

C#

protected override void OnNavigatedTo(Windows.UI.Xaml.Navigation.NavigationEventArgs e)
{
    base.OnNavigatedTo(e);

    //Register Share Charm handler
    DataTransferManager.GetForCurrentView().DataRequested += ShareScreenshot;
}

protected override void OnNavigatedFrom(Windows.UI.Xaml.Navigation.NavigationEventArgs e)
{
    base.OnNavigatedFrom(e);

    //Unregister Share Charm handler
    DataTransferManager.GetForCurrentView().DataRequested -= ShareScreenshot;
}

private async void ShareScreenshot(DataTransferManager sender, DataRequestedEventArgs args)
{
    //A temporary image URL that is used for referencing the created map image. 
    string localImage = "ms-appx:///images/map.png";
    string instructionsImage = "ms-appx:///images/instructions.png";
                        
    //Handle the Share Charm request and insert HTML content. 
    var request = args.Request;
    request.Data.Properties.Title = "My Map Screenshoot";
    request.Data.Properties.Description = "Share a screenshot of the map.";

    request.Data.SetHtmlFormat(HtmlFormatHelper.CreateHtmlFormat(
        string.Format("Here is a screenshot of the map and directions.<br/><br/><img src='{0}'/><br/><img src='{1}' style='height:{2}px;width:{3}px;'/>", 
            localImage, instructionsImage,
            MyMap.DirectionsManager.RouteSummaryView.ActualHeight,
            MyMap.DirectionsManager.RouteSummaryView.ActualWidth)));

    //Create memory stream with screenshot of map.
    request.Data.ResourceMap[localImage] = await ScreenshotToStreamReferenceAsync(MyMap);

    //Create memory stream with screenshot of route instructions.
    request.Data.ResourceMap[instructionsImage] = await ScreenshotToStreamReferenceAsync(MyMap.DirectionsManager.RouteSummaryView); 
}

VB

Protected Overrides Sub OnNavigatedTo(e As NavigationEventArgs)
    MyBase.OnNavigatedTo(e)

    'Register Share Charm handler
    AddHandler DataTransferManager.GetForCurrentView().DataRequested, AddressOf ShareScreenshot
End Sub

Protected Overrides Sub OnNavigatedFrom(e As NavigationEventArgs)
    MyBase.OnNavigatedFrom(e)

    'Unregister Share Charm handler
    RemoveHandler DataTransferManager.GetForCurrentView().DataRequested, AddressOf ShareScreenshot
End Sub

Private Async Sub ShareScreenshot(sender As DataTransferManager, args As DataRequestedEventArgs)
    'A temporary image URL that is used for referencing the created map image. 
    Dim localImage = "ms-appx:///images/map.png"
    Dim instructionsImage = "ms-appx:///images/instructions.png"

    'Handle the Share Charm request and insert HTML content. 
    Dim request = args.Request
    request.Data.Properties.Title = "My Map Screenshoot"
    request.Data.Properties.Description = "Share a screenshot of the map."

    request.Data.SetHtmlFormat(HtmlFormatHelper.CreateHtmlFormat(
        String.Format("Here is a screenshot of the map and directions.<br/><br/><img src='{0}'/><br/><img src='{1}' style='height:{2}px;width:{3}px;'/>",
            localImage, instructionsImage,
            MyMap.DirectionsManager.RouteSummaryView.ActualHeight,
            MyMap.DirectionsManager.RouteSummaryView.ActualWidth)))

    'Create memory stream with screenshot of map.
    request.Data.ResourceMap(localImage) = Await ScreenshotToStreamReferenceAsync(MyMap)

    'Create memory stream with screenshot of route instructions.
    request.Data.ResourceMap(instructionsImage) = Await ScreenshotToStreamReferenceAsync(MyMap.DirectionsManager.RouteSummaryView)
End Sub

The final step is to add the logic for our custom share button. Update the ShareMapScreenshot_Tapped button handler with the following code.

C#

private void ShareMapScreenshot_Tapped(object sender, TappedRoutedEventArgs e)
{
    //Show the charm bar with Share option opened 
    DataTransferManager.ShowShareUI(); 
}

VB

Private Sub ShareMapScreenshot_Tapped(sender As Object, e As TappedRoutedEventArgs)
    'Show the charm bar with Share option opened 
    DataTransferManager.ShowShareUI()
End Sub

If you run the application, wait for the route to load, and then either press our custom share button or use the standard Share charm button, you will be able to share the map and the route instructions. If you select the option to share using the Mail app you should see the map and instructions in the mail app flyout like in the image below.

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.

- Ricky Brundritt, EMEA Bing Maps TSP