Udostępnij za pośrednictwem


Live Apps: Creating Custom Tile and Lock Screen Images

One of the really great ways that apps in Windows Phone can connect with the user is by supporting Live Tiles – tiles that are more than just a quick launcher, but instead get updated with timely, specific information to connect the user with the data that’s important to them.

As a developer, you can update Tiles in lots of different ways:

For most purposes, the Tile templates are plenty rich enough for the developer to be able to communicate new information to the user. For example, the Flip Tile template allows you to set all the properties shown here:

Windows Phone Flip Tile template properties

However, as you can see the areas for text are fairly limited, and sometimes the developer will want to communicate more than is possible using the available properties. The only way to do this is to create a custom image and draw the text (or additional graphics) onto the custom image, then use that to update the tile.

In Windows Phone 8, your app can also be the Lock Screen image provider. Here, all you can supply is the image, so if you wanted to communicate some up to date information, the only way you can do that is to write onto the image. But what a size of image! Plenty of room for writing additional text, icons or custom graphs for example!

Creating custom images using a User Control

The easiest way to layout a custom tile or lock screen image is to create a User Control of the required size, just as you would lay out a page. Of course, what you put onto the control is entirely up to you, but as an example, here’s the Xaml for a user control for a standard Wide Tile for the FlipTile template:

 <UserControl x:Class="RenderImageLibrary.WideTileControl"
    xmlns="https://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="https://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:d="https://schemas.microsoft.com/expression/blend/2008"
    xmlns:mc="https://schemas.openxmlformats.org/markup-compatibility/2006"
    mc:Ignorable="d"
    FontFamily="{StaticResource PhoneFontFamilyNormal}"
    FontSize="{StaticResource PhoneFontSizeNormal}"
    Foreground="{StaticResource PhoneForegroundBrush}"
    d:DesignHeight="336" d:DesignWidth="691">
    
    <Grid x:Name="LayoutRoot">
        <Image x:Name="BackgroundImage" Stretch="UniformToFill" />
        <Rectangle Stroke="Black">
            <Rectangle.Fill>
                <LinearGradientBrush EndPoint="0.5,1" StartPoint="0.5,0">
                    <GradientStop Color="#99000000" Offset="0.007"/>
                    <GradientStop Color="#19000000" Offset="1"/>
                </LinearGradientBrush>
            </Rectangle.Fill>
        </Rectangle>
        <TextBlock x:Name="TextOverlayTextBox" VerticalAlignment="Center" 
                   HorizontalAlignment="Center" 
                   Text="Text" Margin="31,29,31,77" TextWrapping="Wrap"
                   Style="{StaticResource PhoneTextLargeStyle}" Foreground="White"
                   Width="629" Height="230" />
    </Grid>
</UserControl>

As you can see, this is layed out in a Grid with an Image element first, and overlaying that is a black filled Rectangle which is semi transparent and overlays the image using a gradient fill. This is there so that the text which forms the top layer – in the TextBlock – and which is drawn in white, is clearly legible, even over a light coloured background image.

image

 

In the code for this control, there is a property to set the text overlay. The background image used is included as content in this sample so is not changeable at runtime, but this could easily be changed so that any new image could be plugged into it.

 using System;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.IO.IsolatedStorage;
using System.Windows.Media.Imaging;

namespace RenderImageLibrary
{
    public partial class WideTileControl : UserControl
    {
        public WideTileControl(string tileText)
        {
            InitializeComponent();

            Text = tileText;
        }

        public string Text { get; set; }

        public event EventHandler<SaveJpegCompleteEventArgs> SaveJpegComplete;

        public void BeginSaveJpeg()
        {
            // Load the background image directly - important thing is to delay 
            // rendering the image to the WriteableBitmap until the image has
            // finished loading
            BitmapImage backgroundImage = 
                new BitmapImage(new Uri("DefaultWideTile.jpg", UriKind.Relative));
            backgroundImage.CreateOptions = BitmapCreateOptions.None;

            backgroundImage.ImageOpened += (s, args) =>
                {
                    try
                    {
                        // Set the loaded image
                        BackgroundImage.Source = backgroundImage;

                        // Set the text
                        TextOverlayTextBox.Text = this.Text;

                        // Explicitly size the control - for use in a background agent
                        this.UpdateLayout();
                        this.Measure(new Size(691, 336));
                        this.UpdateLayout();
                        this.Arrange(new Rect(0, 0, 691, 336));

                        var wb = new WriteableBitmap(691, 336);
                        wb.Render(LayoutRoot, null);
                        wb.Invalidate();

                        // Create a filename for JPEG file in isolated storage
                        // Tile images *must* be in shared/shellcontent.
                        String fileName = "Tile_" + Guid.NewGuid().ToString() + ".jpg";

                        var myStore = IsolatedStorageFile.GetUserStoreForApplication();
                        if (!myStore.DirectoryExists("shared/shellcontent"))
                        {
                            myStore.CreateDirectory("shared/shellcontent");
                        }

                        using (IsolatedStorageFileStream myFileStream = 
                            myStore.CreateFile("shared/shellcontent/" + fileName))
                        {
                            // Encode WriteableBitmap object to a JPEG stream.
                            wb.SaveJpeg(myFileStream, wb.PixelWidth, wb.PixelHeight, 0, 75);
                            myFileStream.Close();
                        }

                        // Delete images from earlier execution
                        var filesTodelete = 
                             from f in myStore.GetFileNames("shared/shellcontent/Tile_*")
                                .AsQueryable()
                             where !f.EndsWith(fileName)
                             select f;
                        foreach (var file in filesTodelete)
                        {
                            myStore.DeleteFile("shared/shellcontent/" + file);
                        }

                        // Fire completion event
                        if (SaveJpegComplete != null)
                        {
                            SaveJpegComplete(this, new SaveJpegCompleteEventArgs(true, 
                                                           "shared/shellcontent/" + fileName));
                        }
                    }
                    catch (Exception ex)
                    {
                        // Log it
                        System.Diagnostics.Debug.WriteLine(
                                "Exception in SaveJpeg: " + ex.ToString());

                        if (SaveJpegComplete != null)
                        {
                            var args1 = new SaveJpegCompleteEventArgs(false, "");
                            args1.Exception = ex;
                            SaveJpegComplete(this, args1);
                        }
                    }
                };

            return;
        }
    }
}
 

The most important part of this code is the BeginSaveJpeg method which ensures the control is sized and layed out correctly, renders the control onto a WriteableBitmap which is used to write out the image to a jpg, and then fires the SaveJpegComplete event to inform callers that the work is done. Note that as this is a tile image, the method writes it to the /shared/ShellContent folder which is required for images used for tile updates.

 

We can now make use of this in – for example – a background agent. Here’s the code from the attached sample that creates a custom image in a background agent:

 

 namespace BackgroundUpdate
{
    public class ScheduledAgent : ScheduledTaskAgent
    {
        ...
 
        /// <summary>
        /// Agent that runs a scheduled task
        /// </summary>
        /// <param name="task">
        /// The invoked task
        /// </param>
        /// <remarks>
        /// This method is called when a periodic or resource intensive task is invoked
        /// </remarks>
        protected override void OnInvoke(ScheduledTask task)
        {
            // Render a new image for the lock screen
            System.Threading.ManualResetEvent mre = new System.Threading.ManualResetEvent(false);

            Deployment.Current.Dispatcher.BeginInvoke(() =>
            {
                if (LockScreenManager.IsProvidedByCurrentApplication)
                {
                    LockScreenImageControl cont = new LockScreenImageControl();
                    cont.SaveJpegComplete += (s, args) =>
                        {
                            if (args.Success)
                            {
                                // Set the lock screen image URI - App URI syntax is required!  
                                Uri lockScreenImageUri = new Uri("ms-appdata:///Local/" 
                                                        + args.ImageFileName, UriKind.Absolute);
                                Debug.WriteLine(lockScreenImageUri.ToString());

                                LockScreen.SetImageUri(lockScreenImageUri);
                            }
                            mre.Set(); 
                        };
                    cont.BeginSaveJpeg();
                }
            });

            // Wait for Lock Screen image to complete
            mre.WaitOne();
            // Then reset for the Tile Image operation
            mre.Reset();

            Deployment.Current.Dispatcher.BeginInvoke(() =>
            {
                // Render the new tile image
                RenderImageLibrary.WideTileControl wtc = new RenderImageLibrary.WideTileControl(
                    "This text is written onto the image - allows you to specify more text than " 
      + "you can fit onto a standard tile using the available properties in the tile templates");

                wtc.SaveJpegComplete += async (s, args) =>
                {
                    try
                    {
                        if (args.Success)
                        {
                            // Set the tile image URI - "isostore:/" is important! 
                            // Note that the control already
                            // puts the image into /Shared/ShellContent which is where tile 
                            // images in the local folder must be
                            Uri tileImageUri = new Uri("isostore:/" 
                                              + args.ImageFileName, UriKind.RelativeOrAbsolute);
                            Debug.WriteLine(tileImageUri.ToString());
                            


                            // Set the tile image
                            FlipTileData ftd = new FlipTileData();
                            ftd.WideBackgroundImage = tileImageUri;
                            ftd.Title = "Set by BG agent";

                            ShellTile.ActiveTiles.First().Update(ftd);
                        }
                        else
                        {
                            Debug.WriteLine(args.Exception.ToString());
                        }
                    }
                    catch (Exception ex)
                    {
                        Debug.WriteLine(ex.ToString());
                    }
                    finally
                    {
                        mre.Set();
                    }
                };

                wtc.BeginSaveJpeg();
            }
    );
            // Wait for tile operation to complete
            mre.WaitOne();

            NotifyComplete();

        }
    }
}

The result is a pretty custom tile image which you can see after you tap and hold on the sample app where it is listed in the Programs list, and then tap ‘Pin to Start’:

wp_ss_20130328_0001

 

The sample code gives examples for both a custom tile and a custom Lock Screen Image. If you study the code, you can see that the Windows Phone APIs show a shocking disregard (!) for consistency over how the URI of the image must be supplied. For the LockScreen API, the image can be anywhere in local storage, but you must use an ‘Application URI’ format, for example:

Uri lockScreenImageUri = new Uri("ms-appdata:///Local/myNewLockScreenImage.jpg" UriKind.Absolute);

LockScreen.SetImageUri(lockScreenImageUri);

The ShellTile API is different. Its images must be in /Shared/ShellContent or a subfolder of it, and the URI must be specified as an absolute URI, using the isostore: protocol:

Uri tileImageUri = new Uri("isostore:/Shared/ShellContent/myNewTileImage.jpg" , UriKind.RelativeOrAbsolute);
// Set the tile image

FlipTileData ftd = new FlipTileData();

ftd.WideBackgroundImage = tileImageUri;

ShellTile.ActiveTiles.First().Update(ftd);

That minor grumble apart, the results are great!

Download the sample solution here

Comments

  • Anonymous
    December 04, 2013
    This is what i searched for two weeks now and found it. But i have an question. I have an weather app wich downloads data and it properly displayed in the app, but how can i set these values in the widetile control so they update in the background agent and make an new jpg for the live tile. I have lets say for the front tile the temperature and maybe an icon wich is in the local storage of the app but it depends on the downloaded data which one will be displayed. And for the back tile i had an idea about the next week temperatures, i have all this in my app but how to update it in backgroundagent amd make this jpg?!
  • Anonymous
    April 10, 2014
    How to use the above for the Medium and Small Tile control. I mean how to apply it in the MainPage and backround task?
  • Anonymous
    July 02, 2014
    I can't get the BitmapImage.ImageOpened to fire. Tried using different BitmapCreateOptions, but nothing causes the BitmapImage.ImageOpened to start :(
  • Anonymous
    December 30, 2014
    This cannot woprk in the background dino i guess because this is using ui to create it. you will have to download the data as xml or json then place it then create it