Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / WPF

WPF Metro: A Win8 Start Screen 'Clone'

4.81/5 (57 votes)
29 Sep 2011CPOL3 min read 158.8K   16.3K  
A WPF application that replicates the Windows 8 Start screen

WpfMetro/Screenshot_1.png

WpfMetro/Screenshot_2.png

WpfMetro/WPF_Metro_Blue.png

Introduction

After watching some BUILD2011 videos, I couldn't help wondering whether it was possible for me to replicate the Win 8 start screen in WPF. The sample app, WPF Metro, displays tiles that enable the user to access the applications available from the start menu in Win XP or Win 7. It is a more toned-down version of the Win 8 start screen.

Requirements

To view the source files and run the demo, you require:

  • At least 15" of screen real-estate
  • Visual Studio 2010

WPF Metro

When you run WPF Metro, you will be presented with a series of tiles. Double-click on a tile to open the related application.

To change the app's skin, move your cursor to the bottom edge of the application, to activate the 'Edge UI', and click on one of the colored buttons.

WpfMetro/Color_Buttons.png

Quick-Jump

If you don't want to pan to a tiles group by swiping, you can press a key to pan a particular tiles group into view, e.g., to pan the 'M' tiles group into view press the M key.

Design and Layout

I designed WPF Metro in Expression Blend. The primary layout containers are MainCanvas and MetroStackPanel.

WpfMetro/Layout.png

MetroStackPanel is a child element of MainCanvas. Tiles are added to a WrapPanel which is then added to MetroStackPanel. A tile in WPF Metro is a UserControl named Tile which is made up of a TextBlock and an Image control for displaying an application's icon.

WpfMetro/Tile.png

The Code

To display the relevant tiles, WPF Metro extracts the icons of the .exe files that the shortcuts in the Programs folder of the Start menu link to. The tiles are then displayed in alphabetical order by adding Tile controls to a WrapPanel which is eventually added to MetroStackPanel.

The code for the Tile UserControl looks like this:

VB.NET
Partial Public Class Tile

    Public Property ExecutablePath() As String

    Private Sub Tile_PreviewMouseDoubleClick(ByVal sender As Object, _
                                 ByVal e As System.Windows.Input.MouseButtonEventArgs) _
                                 Handles Me.PreviewMouseDoubleClick
        Try
            Process.Start(ExecutablePath)
        Catch ex As Win32Exception
            Exit Sub
        End Try
    End Sub
End Class

In the MainWindow Initialized event handler, we do the following;

VB.NET
Private Sub MainWindow_Initialized(ByVal sender As Object, _
                               ByVal e As System.EventArgs) Handles Me.Initialized
    timer = New DispatcherTimer
    anim = New DoubleAnimationUsingKeyFrames

    anim.Duration = TimeSpan.FromMilliseconds(1800)
    timer.Interval = New TimeSpan(0, 0, 0, 0, 1000)
    AddHandler timer.Tick, AddressOf timer_Tick
End Sub

In the MainWindow Loaded event handler, we set the skin for the application and display tiles:

VB.NET
Private Sub MainWindow_Loaded(ByVal sender As Object, _
                     ByVal e As System.Windows.RoutedEventArgs) Handles Me.Loaded
    Dim metro As New Metrolizer
    metro.DisplayTiles(MetroStackPanel)

    Dim path As String = My.Settings.SkinPath
    Dim newDictionary As New ResourceDictionary()
    newDictionary.Source = New Uri(path, UriKind.Relative)
    Me.Resources.MergedDictionaries.Clear()
    Me.Resources.MergedDictionaries.Add(newDictionary)
End Sub

The Metrolizer class containing the DisplayTiles() method looks as follows:

VB.NET
Imports System.Collections.Generic

Public Class Metrolizer

    Private wrapPanelX As Double = 0

    Public Sub DisplayTiles(ByRef metroStackPanel As StackPanel)
        Dim alphabet() As String = {"a", "b", "c", "d", "e", "f", "g", "h", "i", _
                                    "j", "k", "l", "m", "n", "o", "p", "q", "r", _
                                    "s", "t", "u", "v", "w", "x", "y", "z"}
        Dim numbers() As String = {"1", "2", "3", "4", "5", "6", "7", "8", "9", "0"}
        Dim di As Dictionary(Of String, String()) = New IconsAndPaths().GetIconsAndPaths()

        For Each s As String In alphabet
            Dim letter As String = s
            Dim coll = di.Where(Function(k) k.Key.StartsWith(letter, True, Nothing))
            If (coll.Count > 0) Then
                AddTiles(coll, metroStackPanel, letter)
            End If
        Next

        For Each s As String In numbers
            Dim letter As String = s
            Dim coll = di.Where(Function(k) k.Key.StartsWith(letter, True, Nothing))
            If (coll.Count > 0) Then
                AddTiles(coll, metroStackPanel, letter)
            End If
        Next
    End Sub

    Private Sub AddTiles(ByVal coll As IEnumerable_
			(Of KeyValuePair(Of String, String())), _
                         	ByRef metroStackPanel As StackPanel, ByVal letter As String)
        Dim tileWrapPanel As New WrapPanel
        tileWrapPanel.Orientation = Orientation.Vertical
        tileWrapPanel.Margin = New Thickness(0, 0, 20, 0)
        ' 3 tiles height-wise
        tileWrapPanel.Height = (110 * 3) + (6 * 3)

        For Each kvp As KeyValuePair(Of String, String()) In coll
            Dim newTile As New Tile
            newTile.ExecutablePath = kvp.Value(1)
            newTile.TileIcon.Source = New BitmapImage_
		(New Uri(kvp.Value(0), UriKind.Absolute))
            newTile.TileTxtBlck.Text = kvp.Key
            newTile.Margin = New Thickness(0, 0, 6, 6)
            tileWrapPanel.Children.Add(newTile)
        Next

        WrapPanelLocation(letter, tileWrapPanel)
        metroStackPanel.Children.Add(tileWrapPanel)
    End Sub

    ''' <summary>
    ''' Determines the probable location of a WrapPanel that is added
    ''' to MetroStackPanel (assuming that MetroStackPanel was
    ''' like a Canvas).
    ''' </summary>
    ''' <param name="letter">The alphabetical letter representing a WrapPanel group
    ''' in MetroStackPanel.</param>
    ''' <param name="tileWrapPanel">The WrapPanel that was added to MetroStackPanel.
    ''' </param>
    ''' <remarks></remarks>
    Private Sub WrapPanelLocation(ByVal letter As String, _
			ByVal tileWrapPanel As WrapPanel)
        If (WrapPanelDi.Count = 0) Then
            WrapPanelDi.Add(letter, 0)
        Else
            WrapPanelDi.Add(letter, wrapPanelX)
        End If

        ' Increase value of wrapPanelX as appropriate. 
        ' 6 is right margin of a Tile.
        If (tileWrapPanel.Children.Count <= 3) Then
            wrapPanelX += ((110 + 6) + 18)
        Else
            Dim numberOfColumns As Double = _
		Math.Ceiling(tileWrapPanel.Children.Count / 3)
            Dim x As Double = (numberOfColumns * 110) + (numberOfColumns * 6) + 18
            wrapPanelX += x
        End If
    End Sub
End Class

The GetIconsAndPaths() method in the class IconsAndPaths finds the location of .exe files that .lnk files are linked to and extracts their icons. The IconsAndPaths class looks like this:

VB.NET
Imports System.IO
Imports System.Drawing
Imports IWshRuntimeLibrary

Public Class IconsAndPaths
    Public IconsPathsDi As New Dictionary(Of String, String())

    Public Function GetIconsAndpaths() As Dictionary(Of String, String())
        Dim path As String = Environment.GetFolderPath_
		(Environment.SpecialFolder.CommonStartMenu) & _
                            "\Programs"
        Dim startMenuProgDir As New DirectoryInfo(path)

        If (startMenuProgDir.Exists <> True) Then
            Dim dirPath As String = Environment.GetFolderPath_
				(Environment.SpecialFolder.StartMenu) & _
                                    "\Programs"
            startMenuProgDir = New DirectoryInfo(dirPath)
        End If
        Dim shell As New WshShell

        CreateIconsDirectory()

        For Each fi As FileInfo In startMenuProgDir.GetFiles
            If (fi.Extension = ".lnk") Then
                ' The length of the file's name alone minus .lnk
                Dim nameLength As Integer = fi.Name.Length - 4
                ' Name to display in UserControl
                Dim displayName As String = fi.Name.Substring(0, nameLength)
                ' Copy of shortcut
                Dim link As IWshShortcut = CType(shell.CreateShortcut(fi.FullName),  _
                                                IWshShortcut)

                Dim potentialExePath As String = link.TargetPath
                Dim potentialExe As New FileInfo(potentialExePath)

                If (potentialExe.Extension = ".exe") Then
                    Dim tileIconPath As String = Environment.CurrentDirectory & _
                                                "\WPF Metro Icons\" & _
                                                displayName & ".png"
                    Try
                        Dim ico As System.Drawing.Icon = _
                            System.Drawing.Icon.ExtractAssociatedIcon(potentialExePath)
                        ico.ToBitmap().Save(tileIconPath, Imaging.ImageFormat.Png)

                        AddToDictionary(displayName, tileIconPath, potentialExePath)
                    Catch ex As FileNotFoundException
                        Exit Try
                    End Try

                End If
            End If
        Next

        ' Get icons for .lnk in ...Start Menu\Programs\...
        For Each di As DirectoryInfo In startMenuProgDir.GetDirectories()
            For Each fi As FileInfo In di.GetFiles()
                If (fi.Extension = ".lnk") Then
                    ' The length of the file's name alone minus .lnk
                    Dim nameLength As Integer = fi.Name.Length - 4
                    ' Name to display in UserControl
                    Dim displayName As String = fi.Name.Substring(0, nameLength)
                    ' Avoid install and uninstall files
                    If (displayName.Contains("install") <> True) Then
                        Dim link As IWshShortcut = CType(shell.CreateShortcut_
						(fi.FullName),  _
                                                        IWshShortcut)
                        Dim potentialExePath As String = link.TargetPath

                        If (potentialExePath.Contains(".exe")) Then
                            Dim tileIconPath As String = _
				Environment.CurrentDirectory & _
                                                "\WPF Metro Icons\" & _
                                                displayName & ".png"
                            Try
                                Dim ico As Icon = _
				Icon.ExtractAssociatedIcon(potentialExePath)
                                ico.ToBitmap().Save(tileIconPath, _
				Imaging.ImageFormat.Png)

                                CheckMSOfficeApps(displayName, tileIconPath)

                                AddToDictionary(displayName, tileIconPath, _
						potentialExePath)
                                
                            Catch ex As FileNotFoundException
                                Exit Try
                            Catch ex As ArgumentException
                                Exit Try
                            End Try

                        End If
                    End If
                End If
            Next
        Next

        Return IconsPathsDi
    End Function

    Private Sub AddToDictionary(ByVal displayName As String, _
				ByVal tileIconPath As String, _
                               ByVal exePath As String)
        If Not IconsPathsDi.ContainsKey(displayName) Then
            IconsPathsDi.Add(displayName, New String() {tileIconPath, exePath})
        End If
    End Sub

    Private Sub CheckMSOfficeApps(ByVal app As String, ByVal tileIconPath As String)
        If (app.Contains("Microsoft Office Access")) Then
            AddToDictionary(app, tileIconPath, "MSACCESS.EXE")
        ElseIf (app.Contains("Microsoft Office Excel")) Then
            AddToDictionary(app, tileIconPath, "EXCEL.EXE")
        ElseIf (app.Contains("Microsoft Office InfoPath")) Then
            AddToDictionary(app, tileIconPath, "INFOPATH.EXE")
        ElseIf (app.Contains("Microsoft Office OneNote")) Then
            AddToDictionary(app, tileIconPath, "ONENOTEM.EXE")
        ElseIf (app.Contains("Microsoft Office Outlook")) Then
            AddToDictionary(app, tileIconPath, "OUTLOOK.EXE")
        ElseIf (app.Contains("Microsoft Office PowerPoint")) Then
            AddToDictionary(app, tileIconPath, "POWERPNT.EXE")
        ElseIf (app.Contains("Microsoft Office Publisher")) Then
            AddToDictionary(app, tileIconPath, "MSPUB.EXE")
        ElseIf (app.Contains("Microsoft Office Word")) Then
            AddToDictionary(app, tileIconPath, "WINWORD.EXE")
        Else
            Exit Sub
        End If
    End Sub

    Private Sub CreateIconsDirectory()
        Dim dir As String = Environment.CurrentDirectory & "\WPF Metro Icons"
        If (Directory.Exists(dir)) Then
            Dim di As New DirectoryInfo(dir)
            For Each fi As FileInfo In di.GetFiles
                fi.Delete()
            Next
        Else
            Directory.CreateDirectory(dir)
        End If
    End Sub
End Class

Scrolling of the tiles is done through the use of the PreviewMouseLeftButtonDown and PreviewMouseLeftButtonUp events of MainCanvas.

VB.NET
Private Sub MainCanvas_PreviewMouseLeftButtonDown(ByVal sender As Object, _
                                ByVal e As System.Windows.Input.MouseButtonEventArgs) _
                                Handles MainCanvas.PreviewMouseLeftButtonDown
    initMouseX = e.GetPosition(MainCanvas).X
    x = Canvas.GetLeft(MetroStackPanel)
End Sub

Private Sub MainCanvas_PreviewMouseLeftButtonUp(ByVal sender As Object, _
                                  ByVal e As System.Windows.Input.MouseButtonEventArgs) _
                                  Handles MainCanvas.PreviewMouseLeftButtonUp
    finalMouseX = e.GetPosition(MainCanvas).X
    Dim diff As Double = Math.Abs(finalMouseX - initMouseX)

    ' Make sure the diff is substantial so that tiles 
    ' don't scroll on double-click.
    If (diff > 5) Then
        If (finalMouseX < initMouseX) Then
            newX = x - (diff * 2)
        ElseIf (finalMouseX > initMouseX) Then
            newX = x + (diff * 2)
        End If

        anim.KeyFrames.Add(New SplineDoubleKeyFrame(newX, _
                                    KeyTime.FromTimeSpan(TimeSpan.FromSeconds(1)), _
                                    New KeySpline(0.161, 0.079, 0.008, 1)))
        anim.FillBehavior = FillBehavior.HoldEnd
        MetroStackPanel.BeginAnimation(Canvas.LeftProperty, anim)
        anim.KeyFrames.Clear()
        timer.Start()
    End If
End Sub

The Tick event handler of the DispatcherTimer object, timer, checks whether the StackPanel is out of view and restores it to a suitable position:

VB.NET
' Check whether the StackPanel is no longer in view and
' return it to a suitable position.
Private Sub timer_Tick(ByVal sender As Object, ByVal e As EventArgs)
    Dim mspWidth As Double = MetroStackPanel.ActualWidth

    If (newX > 200) Then
        anim.KeyFrames.Add(New SplineDoubleKeyFrame(45, _
                           KeyTime.FromTimeSpan(TimeSpan.FromSeconds(1)), _
                           New KeySpline(0.161, 0.079, 0.008, 1)))
        anim.FillBehavior = FillBehavior.HoldEnd
        MetroStackPanel.BeginAnimation(Canvas.LeftProperty, anim)
        anim.KeyFrames.Clear()
    ElseIf ((newX + mspWidth) < 500) Then
        Dim widthX As Double = 500 - (newX + mspWidth)
        Dim shiftX As Double = newX + widthX
        anim.KeyFrames.Add(New SplineDoubleKeyFrame(shiftX, _
                           KeyTime.FromTimeSpan(TimeSpan.FromSeconds(1)), _
                           New KeySpline(0.161, 0.079, 0.008, 1)))
        anim.FillBehavior = FillBehavior.HoldEnd
        MetroStackPanel.BeginAnimation(Canvas.LeftProperty, anim)
        anim.KeyFrames.Clear()
    End If
    timer.Stop()
End Sub

If you are interested in how I went about skinning the app, refer to my WPF skinning article here.

Quick-Jump

Panning to a particular tiles group is done by calling the ShiftStackPanel() method defined in Module QuickJumper.

VB.NET
Imports System.Windows.Media.Animation

Module QuickJumper

    Public WrapPanelDi As New Dictionary(Of String, Double)

    ' Depending on which key was pressed moves MetroStackPanel so that
    ' the WrapPanel containing required tiles is in view.
    Public Sub ShiftStackPanel(ByRef letter As String, _
			ByRef metroStackPanel As StackPanel)
        If WrapPanelDi.ContainsKey(letter.ToLower()) Then
            Dim doubleAnim As New DoubleAnimationUsingKeyFrames()
            Dim newX As Double = WrapPanelDi(letter.ToLower())
            doubleAnim.Duration = TimeSpan.FromMilliseconds(1800)

            doubleAnim.KeyFrames.Add(New SplineDoubleKeyFrame(-newX, _
                                     KeyTime.FromTimeSpan(TimeSpan.FromSeconds(1)), _
                                     New KeySpline(0.161, 0.079, 0.008, 1)))
            doubleAnim.FillBehavior = FillBehavior.HoldEnd
            metroStackPanel.BeginAnimation(Canvas.LeftProperty, doubleAnim)
            doubleAnim.KeyFrames.Clear()
        End If
    End Sub

End Module

Keys and values are added to WrapPanelDi in the WrapPanelLocation() method defined in class Metrolizer.

Conclusion

I hope that you picked up something useful from this article. I initially did not like the Metro UI, but I think it's a great fit on Win8, considering that cut-off text is less numerous because of the generous screen real-estate on devices running the OS. I think that Win8, or whatever they'll finally call it, will be a great success when it finally launches.

History

  • 20th Sep 2011: Initial post
  • 24th Sep 2011: Updated code to open Microsoft Office apps
  • 29th Sep 2011: Added quick-jump feature

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)