WPF Metro:Windows 8 开始屏幕“克隆”






4.81/5 (55投票s)
一个复制 Windows 8 开始屏幕的 WPF 应用程序
引言
在观看了一些 BUILD2011 视频后,我不禁想知道是否有可能在 WPF 中复制 Win 8 的开始屏幕。示例应用程序WPF Metro,显示磁贴,允许用户访问 Win XP 或 Win 7 开始菜单中可用的应用程序。 它是 Win 8 开始屏幕的一个简化版本。
要求
要查看源文件并运行演示,您需要
- 至少 15 英寸的屏幕空间
- Visual Studio 2010
WPF Metro
当您运行WPF Metro时,您将看到一系列磁贴。 双击一个磁贴以打开相关的应用程序。
要更改应用程序的皮肤,请将光标移动到应用程序的底部边缘,以激活“边缘 UI”,然后单击其中一个彩色按钮。
快速跳转
如果您不想通过滑动来平移到磁贴组,您可以按一个键将特定的磁贴组平移到视图中,例如,要将“M”磁贴组平移到视图中,请按 M 键。
设计和布局
我用 Expression Blend 设计了 WPF Metro。 主要的布局容器是 MainCanvas
和 MetroStackPanel
。
MetroStackPanel
是 MainCanvas
的子元素。 磁贴被添加到 WrapPanel
,然后添加到 MetroStackPanel
。 WPF Metro 中的一个磁贴是一个名为 Tile
的 UserControl
,它由一个 TextBlock
和一个 Image
控件组成,用于显示应用程序的图标。
代码
为了显示相关的磁贴,WPF Metro 提取 Start 菜单的 Programs 文件夹中的快捷方式链接到的 .exe 文件的图标。 然后通过将 Tile
控件添加到 WrapPanel
(最终添加到 MetroStackPanel
)中,按字母顺序显示这些磁贴。
Tile
UserControl
的代码如下所示
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
在 MainWindow
Initialized
事件处理程序中,我们执行以下操作:
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
在 MainWindow
Loaded
事件处理程序中,我们设置应用程序的皮肤并显示磁贴
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
包含 DisplayTiles()
方法的 Metrolizer
类的代码如下所示
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
类 IconsAndPaths
中的 GetIconsAndPaths()
方法查找 .lnk 文件链接到的 .exe 文件的位置,并提取它们的图标。 IconsAndPaths
类的代码如下所示
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
磁贴的滚动是通过使用 MainCanvas
的 PreviewMouseLeftButtonDown
和 PreviewMouseLeftButtonUp
事件来实现的。
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
DispatcherTimer
对象 timer
的 Tick
事件处理程序检查 StackPanel
是否超出视图,并将其恢复到合适的位置
' 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
如果您对我是如何进行应用程序换肤的感兴趣,请参阅我的 WPF 换肤文章 这里。
快速跳转
平移到特定的磁贴组是通过调用模块 QuickJumper
中定义的 ShiftStackPanel()
方法来完成的。
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
键和值被添加到 Metrolizer
类中定义的 WrapPanelLocation()
方法中的 WrapPanelDi
中。
结论
我希望您从本文中获得了一些有用的东西。 我最初不喜欢 Metro UI,但我认为它非常适合 Win8,考虑到由于运行操作系统的设备上的屏幕空间很大,截断的文本较少。 我认为 Win8,或者他们最终称它为什么,最终发布时将会非常成功。
历史
- 2011 年 9 月 20 日:首次发布
- 2011 年 9 月 24 日:更新代码以打开 Microsoft Office 应用程序
- 2011 年 9 月 29 日:添加快速跳转功能