Virtual Developer Workshop: Containerized Development with Docker
Today we'll find out how we can change the wallpaper of a Windows 8 Store app. Hold on tight, we have a bumpy ride ahead. Let's get started.
Windows 8 Store Apps
As we know by now, and if you have followed my most recent articles (the last two months or so), Windows 8 Store apps differ quite a bit from desktop apps. While doing today's app, you will see most of the differences concern timers, and API's as well as serialization capabilities. If you are new to Windows 8 Store apps, I suggest you read this article.
What Can We Work With?
- XAML features, including XAML controls - I quote Homer Simpson: "doh!". Obviously we well need these as this is a Windows 8 Store app.
- Background tasks - This we will use instead of Timer controls, which aren't available here.
- SystemParametersInfo API - Yes, it still does work.
- Asynchronous programming - We will use Async programming methods to accomplish our tasks asynchronously. Have a look at this article I wrote recently.
- Basic serialization - The reason why I included this point is: It is logical to assume we will need some sort of serialization here, as we will need to keep track of which paper was set and so on. I haven't gone into much detail here, honestly; perhaps in an updated version of this article (or a future article) I will.
We aren't able to work with the following features:
- Timer controls - these controls do not exist in the Windows 8 Store framework
- Registry - Windows 8 Store apps cannot access the regsitry. We can however use the following two APIs:
Welcome to Windows 8 Store programming!
Our project's aim is to set change the wallpaper at different intervals. Because of the fact that there is no timer control present, we will have to create a Background task. This is not as simple as double clicking on a control. We will have to create a separate project for our background task, register it, and make it work with our main project. This means that we will have two projects in one solution.
Another caveat is that we have limited resources at our disposal. We will only be allowed to access certain folders, which are known to the app, and read their contents asynchronously. Lastly, we do not have access to the registry, so saving info might be hard.
Let us create our first project. Give it a name of Wallpaper Genie 2013, and design it to look like Figure 1.
Figure 1 - Our Main form's design
Add another Page to your project through the Project menu, and design it to resemble Figure 2
Figure 2 - Settings page
Add the following Imports to your MainPage:
Imports Windows.Storage Imports Windows.Storage.Streams Imports Windows.UI.Xaml.Media.Imaging Imports System.Runtime.InteropServices Imports Windows.ApplicationModel.Background Imports Windows.UI.Core Imports Windows.UI.Core.CoreWindow Imports System.Xml Imports System.Runtime.Serialization Imports System.IO
Here, you can have a detailed look at the available namespaces in Windows 8 Store apps and their uses.
Add the following code to your MainPage class:
Public arrFolderNames() As String Public arrSubFolderNames() As String Public arrFileNames() As String Private PaperDuration As Integer Private Async Sub btSelect_Click(sender As Object, e As RoutedEventArgs) Handles btSelect.Click lstFolders.Items.Clear() Dim RootFolder As StorageFolder = KnownFolders.PicturesLibrary Dim FolderList As IReadOnlyList(Of IStorageItem) = Await RootFolder.GetItemsAsync Dim Counter As Integer For Each folder In FolderList If TypeOf folder Is StorageFolder Then lstFolders.Items.Add(folder.Name) ReDim Preserve arrFolderNames(Counter) arrFolderNames(Counter) = folder.Path Counter += 1 End If Next End Sub Private Async Sub lstFolders_SelectionChanged(sender As Object, e As SelectionChangedEventArgs) Handles lstFolders.SelectionChanged lstSubFolders.Items.Clear() Dim SubFolder As StorageFolder = Await StorageFolder.GetFolderFromPathAsync(arrFolderNames(lstFolders.SelectedIndex)) Dim SubFolderList As IReadOnlyList(Of IStorageItem) = Await SubFolder.GetItemsAsync Dim Counter As Integer For Each Folder In SubFolderList If TypeOf Folder Is StorageFolder Then lstSubFolders.Items.Add(Folder.Name) ReDim Preserve arrSubFolderNames(Counter) arrSubFolderNames(Counter) = Folder.Path Counter += 1 End If Next End Sub Private Async Sub lstSubFolders_SelectionChanged(sender As Object, e As SelectionChangedEventArgs) Handles lstSubFolders.SelectionChanged lstFiles.Items.Clear() Dim SubFolder As StorageFolder = Await StorageFolder.GetFolderFromPathAsync(arrSubFolderNames(lstSubFolders.SelectedIndex)) Dim SubFolderList As IReadOnlyList(Of IStorageItem) = Await SubFolder.GetItemsAsync Dim Counter As Integer For Each File In SubFolderList If TypeOf File Is StorageFile Then lstFiles.Items.Add(File.Name) ReDim Preserve arrFileNames(Counter) arrFileNames(Counter) = File.Path Counter += 1 End If Next End Sub Private Async Sub lstFiles_SelectionChanged(sender As Object, e As SelectionChangedEventArgs) Handles lstFiles.SelectionChanged Dim strPicLoc As String = arrFileNames(lstFiles.SelectedIndex) Dim File2 As StorageFile = Await StorageFile.GetFileFromPathAsync(strPicLoc) Dim src As New BitmapImage() src.SetSource(Await File2.OpenAsync(FileAccessMode.Read)) imgWall.Source = src imgWall.Stretch = Stretch.Fill End Sub
Here, we first obtain a list of all folders inside the PicturesLibrary. Next, we obtain a list of subfolders within a selected folder inside the Pictures library. Lastly, we again, asynchronously, get a list of files inside the selected sub folder inside the selected folder in pictures Library. What a mouthful! Keeping it simple. The last sub, obtains the selected file and displays the picture inside an image control. This has also changed (if you're a newbie to Windows 8 store programming). It is not as simple as just supplying a destination source of the picture (as in desktop apps). You have to open the picture and read its contents into the image control.
If you were to run your project now, you will be able to select folders, sub folders, and files and it would look similar to figure 3.
Figure 3 - Our program in action - isn't Alizee Jacotey just beautiful!!?
This is where things get interesting, and I will continue with the Mainpage code a bit later. We have to put each part of the puzzle first, before we can have a completed puzzle.
Add the next event to the MainPage:
Private Sub btSettings_Click(sender As Object, e As RoutedEventArgs) Handles btSettings.Click Dim spSettings As New Frame() spSettings.Navigate(GetType(BasicPage1)) Window.Current.Content = spSettings Window.Current.Activate() End Sub
The Settings button takes us to the Settings page (or Frame) and activates it.
Let's now have a look at the settings page.
Not much work here, as this page will solely be used to determine when we want the wallpaper to change. Let us add its code:
Private wpDuration As String Private Sub btBack_Click(sender As Object, e As RoutedEventArgs) Handles btBack.Click Me.Frame.Navigate(GetType(MainPage), wpDuration) End Sub Private Sub rdOneMin_Checked(sender As Object, e As RoutedEventArgs) Handles rdOneMin.Checked wpDuration = "1M" End Sub Private Sub rdTenMin_Checked(sender As Object, e As RoutedEventArgs) Handles rdTenMin.Checked wpDuration = "10" End Sub Private Sub rdOneWeek_Checked(sender As Object, e As RoutedEventArgs) Handles rdOneWeek.Checked wpDuration = "1W" End Sub Private Sub rdOneDay_Checked(sender As Object, e As RoutedEventArgs) Handles rdOneDay.Checked wpDuration = "1D" End Sub End Class
There is not much happening here. All we have done here is to store whichever option was selected into a variable called wpDuration. We need to now transfer this variable back to our MainPage. We do this by overriding the MainPage's OnNavigatedTo event:
Protected Overrides Sub OnNavigatedTo(e As Navigation.NavigationEventArgs) MyBase.OnNavigatedTo(e) Dim pDur As String = TryCast(e.Parameter, String) Select Case pDur Case "1M" PaperDuration = 60 Case "10" PaperDuration = 600 Case "1D" PaperDuration = 86400 Case "1W" PaperDuration = 604800 End Select End Sub End Class
Once we are back at the MainPage, we determine which option buttons were selected by investigating the variable's contents. How did it know which variable to use? If you look at the settings page's Back click event, you will notice that we passed the wpDuration variable as a parameter. Inside the MainPage's OnNavigatedTo event we cast the parameter to a string, and voila - the two pages can now communicate with each other!
What is stored inside the PaperDuration variable is the amount of seconds for each option. 60 for one minute. 600 for 10 minutes. 86400 for once a day, and 604800 for once a week. As this program might run continuously (especially on a Windows 8 Phone) this works quite nicely. Now, you may recall that we do not have any timers at our disposal. What we should do now is make use of a Background task, and run it at each of these increments, depending of course on which button was selected.
Adding a Background Task
Background on Background Tasks
Have a proper read through these articles:
- Being productive when your app is offscreen
- Being productive in the background – background tasks
- Guidelines for background tasks (Windows Store apps)
Sadly, all the code samples are in C# only (as always...).
Now that we have decent understanding of the need for Background tasks, we have to know how to create one.
Add a new project to your existing solution by clicking File, Add, Project. Select the Class Library project and give it a nice name. I have named mine GenieBackTask. Rename the default class name (Class1) to something more descriptive such as clsBack.vb. Your Solution Explorer should look like Figure 4.
Figure 4 - Solution Explorer
Add the following code to clsBack:
Imports Windows.ApplicationModel.Background Imports Windows.Storage Imports Windows.UI.Core Imports Windows.UI.Core.CoreWindow Imports System.Runtime.InteropServices Imports System.Xml Imports System.Runtime.Serialization Namespace GenieBackTask Public NotInheritable Class clsBack Implements IBackgroundTask <DllImport("user32", setlasterror:=True)> _ Private Shared Function SystemParametersInfo( _ ByVal intAction As Integer, _ ByVal intParam As Integer, _ ByVal strParam As String, _ ByVal intWinIniFlag As Integer) As Integer ' returns non-zero value if function succeeds End Function Const SPIF_UPDATEINIFILE = &H1 Const SPI_SETDESKWALLPAPER = 20 Const SPIF_SENDWININICHANGE = &H2 Public Async Sub Run(taskInstance As IBackgroundTaskInstance) Implements IBackgroundTask.Run Dim Deferral As BackgroundTaskDeferral = taskInstance.GetDeferral() Await UpdateUI() Deferral.Complete() End Sub Private Async Function UpdateUI() As Task Dim MyDispatcher = GetForCurrentThread().Dispatcher Await MyDispatcher.RunAsync(CoreDispatcherPriority.Normal, Async Function() Dim picker As New Windows.Storage.Pickers.FileOpenPicker picker.SuggestedStartLocation = Pickers.PickerLocationId.PicturesLibrary Dim strPicFileName = Await picker.PickSingleFileAsync Dim x As Integer x = SystemParametersInfo(SPI_SETDESKWALLPAPER, 0&, strPicFileName.DisplayName, SPIF_UPDATEINIFILE Or SPIF_SENDWININICHANGE) End Function) End Function End Class End Namespace
Let us break this code down, piece by piece. Obviously, the first couple of lines are the namespaces that we need.
Namespace GenieBackTask Public NotInheritable Class clsBack Implements IBackgroundTask
Creates a namespace (for the project) and creates the class. You will notice that this class implements the IBackgroundTask interface. This interface provides a method to perform the work of a background task.
The next section:
<DllImport("user32", setlasterror:=True)> _ Private Shared Function SystemParametersInfo( _ ByVal intAction As Integer, _ ByVal intParam As Integer, _ ByVal strParam As String, _ ByVal intWinIniFlag As Integer) As Integer ' returns non-zero value if function succeeds End Function Const SPIF_UPDATEINIFILE = &H1 Const SPI_SETDESKWALLPAPER = 20 Const SPIF_SENDWININICHANGE = &H2
Is the declaration of our API that will change the wallpapers.
Now things get tricky! The Run sub (which has to be included inside any Background task) is what will run when it has been triggered. The trigger we will set inside our MainPage class when we register the background task. The Run sub looks like:
Public Async Sub Run(taskInstance As IBackgroundTaskInstance) Implements IBackgroundTask.Run Dim Deferral As BackgroundTaskDeferral = taskInstance.GetDeferral() Await UpdateUI() Deferral.Complete() End Sub
What happens here is the following:
- We again implement the IBackgroundTask interface, but this time its Run method.
- We obtain a Deferral object as the task will run Async.
- We call our method that will run.
- We complete the deferral.
Now the fun part! This...
Private Async Function UpdateUI() As Task Dim MyDispatcher = GetForCurrentThread().Dispatcher Await MyDispatcher.RunAsync(CoreDispatcherPriority.Normal, Async Function() Dim picker As New Windows.Storage.Pickers.FileOpenPicker picker.SuggestedStartLocation = Pickers.PickerLocationId.PicturesLibrary Dim strPicFileName = Await picker.PickSingleFileAsync Dim x As Integer x = SystemParametersInfo(SPI_SETDESKWALLPAPER, 0&, strPicFileName.DisplayName, SPIF_UPDATEINIFILE Or SPIF_SENDWININICHANGE) End Function) End Function
...runs another Async function inside of it, and dispatches it to the current thread. We created a FileOpenPicker, which allows the user to select a file inside the PicturesLibrary. Obviously you can customize this further (as this is just an example) to locate the specific picture you need.
Finally, we apply the selected picture as a Wallpaper. Build this project now.
This is all we need for the Background task. We now need to connect it to our main project, and then register and start it from our main project. Connect this task to the main project now by opening the Package.Manifest file and navigating to the Declarations tab and entering GenieBackTask.clsBack in the Entry Point field, as displayed in Figure 5.
Figure 5 - Page Manifest
Now our main output project knows about the task. All that is left now is to register the task in code from our MainPage, and then we're done. Add the following code to the Mainpage class:
Private Sub btnSet_Click(sender As Object, e As RoutedEventArgs) Handles btnSet.Click Dim x = RegisterBackgroundTask("GenieBackTask.clsBack", "GenieBackTask", New TimeTrigger(PaperDuration, False)) End Sub Public Shared Function RegisterBackgroundTask(TaskEntryPoint As String, TaskName As String, Trigger As IBackgroundTrigger) As BackgroundTaskRegistration For Each cur In BackgroundTaskRegistration.AllTasks If cur.Value.Name = TaskName Then Return DirectCast(cur.Value, BackgroundTaskRegistration) End If Next Dim Builder As New BackgroundTaskBuilder Builder.Name = TaskName Builder.TaskEntryPoint = TaskEntryPoint Builder.SetTrigger(Trigger) Dim task As BackgroundTaskRegistration = Builder.Register() Return task End Function Public Shared Sub UnregisterBackgroundTasks(TaskName As String) For Each cur In BackgroundTaskRegistration.AllTasks If cur.Value.Name = TaskName Then cur.Value.Unregister(True) End If Next End Sub
In btnSet we register the background task by supplying the same EntryPoint (as we did in the Mainfest), giving it a name, and when this task should run (PaperDuration).
Lastly, we Unregister the task. This is needed so that it doesn't take up unnecessary resources when the app is closed.
This was tough, agreed. But things fall into place the more you work with a certain technology. My aim is just to guide you in the right direction, and I try to make your transition from normal desktop apps to Windows Store apps easier. I hope you have learned a thing or two today. Until next time, cheers!