65.9K
CodeProject 正在变化。 阅读更多。
Home

Metro: Shuffle

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.83/5 (26投票s)

2012 年 7 月 4 日

CPOL

5分钟阅读

viewsIcon

60818

downloadIcon

2440

一个 Metro 磁贴拼图游戏。

 

引言

Shuffle 是一款数字拼图游戏,目标是将数字方块按数字顺序排列。由于这是一款 Metro 应用,因此非常注重触摸交互,以模拟真实的方块拼图游戏。用户需要滑动方块到位,移动一个方块可能会导致另一个方块也移动,例如在上图的截图中,向下移动方块 3 也会导致方块 2 向下移动。

Shuffle

Shuffle 包含顶部和底部应用栏,带有用于执行各种任务的按钮。

 

底部应用栏包含一个按钮,用户可以点击该按钮来打乱方块。

 

底部应用栏中的 Shuffle 按钮

顶部应用栏包含用于更改应用程序外观的按钮。

 

顶部应用栏中的主题按钮

顶部应用栏中的壁纸按钮

顶部应用栏左侧的彩色按钮可用于更改应用程序的主题,例如,点击绿色按钮会将应用程序主题更改为绿色。

顶部应用栏右侧的按钮可用于设置或删除应用程序的壁纸。点击“设置壁纸”按钮会显示文件选择器,让用户选择要用作应用壁纸的图像文件。

使用文件选择器选择图像

带壁纸的 Shuffle

设置壁纸不会覆盖应用的背景。当用户设置主题或壁纸时,下次打开应用时将会加载它们。

GameTile

每个方块都是一个 UserControl,包含一个只写属性,用于设置显示方块编号的 TextBlock 的 Text 属性。

Public WriteOnly Property TileNumber As Double
    Set(value As Double)
        NumberTextBlock.Text = value
    End Set
End Property
UserControl 还包含一个 Thumb 控件,其 DragDelta 事件处理程序负责处理方块的移动。
Private Sub GameTileThumb_DragDelta(ByVal sender As Object, ByVal e As DragDeltaEventArgs) _
    Handles GameTileThumb.DragDelta

    vChange = e.VerticalChange
    hChange = e.HorizontalChange

    absVChange = Math.Abs(e.VerticalChange)
    absHChange = Math.Abs(e.HorizontalChange)

    ' Vertical movement
    If (absVChange > absHChange) Then
        ' Down
        If (vChange > 0) Then
            drctn = Direction.Down
            MoveTileDownwards(e)
        Else ' Up
            drctn = Direction.Up
            MoveTileUpwards(e)
        End If
        ' Horizontal movement
    ElseIf (absHChange > absVChange) Then
        ' Right
        If (hChange > 0) Then
            drctn = Direction.Right
            MoveTileRight(e)
        Else
            ' Left
            drctn = Direction.Left
            MoveTileLeft(e)
        End If
    End If
    KeepInBounds()
    FullTileBlock()
    PartialTileBlock()
    PartialPlusFullTileBlock()
End Sub

MoveTileDownwards() 方法顾名思义,会向下移动方块。

Private Sub MoveTileDownwards(ByVal e As DragDeltaEventArgs)
    y = Canvas.GetTop(Me)
    x = Canvas.GetLeft(Me)

    If (x = LOWER_BOUND) OrElse (x = 130) OrElse (x = UPPER_BOUND) Then
        Canvas.SetTop(Me, (y + SHIFT))
        MoveTileBelow(e)
    End If
End Sub

MoveTileBelow() 会移动被移动方块正下方的任何方块。

Private Sub MoveTileBelow(ByVal e As DragDeltaEventArgs)
    For Each tile As GameTile In parentCanvas.Children
        tl_X = Canvas.GetLeft(tile)
        tl_Y = Canvas.GetTop(tile)

        If (tl_X = x) And (tl_Y = (y + TILE_HEIGHT)) Then
            tile.GameTileThumb_DragDelta(Nothing, e)
            Exit Sub
        End If
    Next
End Sub

当一个方块移动时,它会检查是否有方块挡住了它的路径。PartialTileBlock() 检查是否有方块部分挡住了它的路径,例如,在下图所示的例子中,方块 7 会阻止方块 3 向上移动。

Private Sub PartialTileBlock()
    Select Case drctn
        Case Direction.Up
            For Each tile As GameTile In parentCanvas.Children
                tX = Canvas.GetLeft(tile)
                tY = Canvas.GetTop(tile)
                If (tY < y) AndAlso (tY = (y - TILE_HEIGHT)) AndAlso (tX > x) AndAlso (tX < (x + TILE_WIDTH)) Then
                    Canvas.SetTop(Me, tY + TILE_HEIGHT)
                    Exit Sub
                ElseIf (tY < y) AndAlso (tY = (y - TILE_HEIGHT)) AndAlso (tX < x) AndAlso ((tX + TILE_WIDTH) > x) Then
                    Canvas.SetTop(Me, tY + TILE_HEIGHT)
                    Exit Sub
                End If
            Next
        Case Direction.Down
            For Each tile As GameTile In parentCanvas.Children
                tX = Canvas.GetLeft(tile)
                tY = Canvas.GetTop(tile)
                If (tY > y) AndAlso (tY = (y + TILE_HEIGHT)) AndAlso (tX > x) AndAlso (tX < (x + TILE_WIDTH)) Then
                    Canvas.SetTop(Me, tY - TILE_HEIGHT)
                    Exit Sub
                ElseIf (tY > y) AndAlso (tY = (y + TILE_HEIGHT)) AndAlso (tX < x) AndAlso ((tX + TILE_WIDTH) > x) Then
                    Canvas.SetTop(Me, tY - TILE_HEIGHT)
                    Exit Sub
                End If
            Next
        Case Direction.Left
            For Each tile As GameTile In parentCanvas.Children
                tX = Canvas.GetLeft(tile)
                tY = Canvas.GetTop(tile)
                If (tX < x) AndAlso (tX = (x - TILE_HEIGHT)) AndAlso (tY > y) AndAlso (tY < (y + TILE_WIDTH)) Then
                    Canvas.SetLeft(Me, tX + TILE_HEIGHT)
                    Exit Sub
                ElseIf (tX < x) AndAlso (tX = (x - TILE_HEIGHT)) AndAlso (tY < y) AndAlso ((tY + TILE_WIDTH) > y) Then
                    Canvas.SetLeft(Me, tX + TILE_HEIGHT)
                    Exit Sub
                End If
            Next
        Case Direction.Right
            For Each tile As GameTile In parentCanvas.Children
                tX = Canvas.GetLeft(tile)
                tY = Canvas.GetTop(tile)
                If (tX > x) AndAlso (tX = (x + TILE_HEIGHT)) AndAlso (tY > y) AndAlso (tY < (y + TILE_WIDTH)) Then
                    Canvas.SetLeft(Me, tX - TILE_HEIGHT)
                    Exit Sub
                ElseIf (tX > x) AndAlso (tX = (x + TILE_HEIGHT)) AndAlso (tY < y) AndAlso ((tY + TILE_WIDTH) > y) Then
                    Canvas.SetLeft(Me, tX - TILE_HEIGHT)
                    Exit Sub
                End If
            Next
    End Select
End Sub

MainPage

Metro 应用使用 Page,这相当于桌面应用程序中的 Window。Shuffle 只包含一个页面,即 MainPage。MainPage 包含一个二维数组,其中存储着 8 个方块显示的 9 个可能的初始坐标。

Private TileCoords()() As Double = New Double(8)() { _
    New Double() {0, 0}, New Double() {130, 0}, New Double() {260, 0}, _
    New Double() {0, 130}, New Double() {130, 130}, New Double() {260, 130}, _
    New Double() {0, 260}, New Double() {130, 260}, New Double() {260, 260}}

LoadTiles() 方法在 MainPage 加载时被调用,并将一个 Canvas 类型的布局容器填充 8 个方块。

Private Sub LoadTiles()
    Dim rnd As New Random
    Dim n As Integer = 1

    Do
        num = rnd.Next(0, 9)

        If (rndList.Contains(num) <> True) Then
            rndList.Add(num)
        End If
    Loop Until rndList.Count = 8

    For Each i As Integer In rndList
        x = TileCoords(i)(0)
        y = TileCoords(i)(1)

        gameTile = New GameTile
        gameTile.TileNumber = n

        Canvas.SetLeft(gameTile, x)
        Canvas.SetTop(gameTile, y)
        GameCanvas.Children.Add(gameTile)
           
        n += 1
    Next
End Sub

AppBar

AppBar 是一个用于显示应用程序特定命令和工具的工具栏。默认情况下它是隐藏的,当用户从屏幕边缘滑动时会显示或隐藏。应用栏可以出现在页面顶部或底部,或两者都有。它被分配给 Page 的 TopAppBar 或 BottomAppBar 属性。

Shuffle 的 AppBar 在 MainPage 的 XAML 标记中定义如下:

<Page.TopAppBar>
    <AppBar x:Name="tpAppBar" Padding="10,0,10,0" Height="76">
        <Grid>
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Left">
                <Button x:Name="GreenButton" Background="#FF03A018" Margin="20,0,20,0" Width="80" Height="40"/>
                <Button x:Name="RedButton" Background="#FFBF0303" Margin="0,0,20,0" Width="80" Height="40"/>
                <Button x:Name="BlueButton" Background="#FF0C84E8" Margin="0,0,20,0" Width="80" Height="40"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
                <Button x:Name="SetWallpaperButton" Content="Set Wallpaper" Margin="0,0,20,0"/>
                <Button x:Name="RemoveWallpaperButton" Content="Remove Wallpaper" Margin="0,0,15,0"/>                                 
            </StackPanel>
        </Grid>
    </AppBar>
</Page.TopAppBar>
...    
<Page.BottomAppBar>
    <AppBar x:Name="btmAppBar" Padding="10,0,10,0" Height="76">
        <Grid>
            <Button x:Name="ShuffleButton" Content="Shuffle" HorizontalAlignment="Center"/>
        </Grid>
    </AppBar>
</Page.BottomAppBar>

洗牌

点击 Shuffle 按钮会调用 ShuffleTiles() 方法。

Private Sub ShuffleTiles()
    rndList.Clear()
    GameCanvas.Children.Clear()
    LoadTiles()
End Sub

设置主题

不幸的是,在 Metro 中无法引用 DynamicResource。这给 Metro 应用的皮肤/主题化带来了一个棘手的局面。在 Shuffle 中,我更改了构成应用背景的 Image 的 Source 属性值,并使用了一些自定义 Storyboard 来更改两个 Path 的 Fill 属性。

<Grid>       
    <Image x:Name="BackgroundImage" Source="Images/BackgroundBlue.png" Stretch="UniformToFill" Margin="0"/>
    <Image x:Name="WallpaperImage" Stretch="UniformToFill" Margin="0"/>
    <Grid Name="GameGrid">
        <Path x:Name="OuterEdge" Fill="{StaticResource OuterEdgeBrush}" 
              Stretch="Fill" Width="396" Height="396" Data="..."/>                
        <Path x:Name="InnerSurface" Fill="#FF005170" 
              Stretch="Fill" StrokeLineJoin="Round" Stroke="{x:Null}" Data="..." 
              Height="390" Width="390"/>            
        ...            
    </Grid>
</Grid>

用于更改主题的 Storyboard 在 Page 的 Resources 部分中定义。

<Page.Resources>
    ...
    <!-- Storyboards for themes -->
    <Storyboard x:Name="BlueThemeStoryboard">
        <ColorAnimation Storyboard.TargetName="OuterEdge" 
                    Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)"
                    To="#FF001F2E" Duration="00:00:00.1" EnableDependentAnimation="True"/>
        <ColorAnimation Storyboard.TargetName="OuterEdge" 
                    Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)"
                    To="#FF003953" Duration="00:00:00.1" EnableDependentAnimation="True"/>
        <ColorAnimation Storyboard.TargetName="OuterEdge" 
                    Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)"
                    To="#FF0075AC" Duration="00:00:00.1" EnableDependentAnimation="True"/>

        <ColorAnimation Storyboard.TargetName="InnerSurface" 
                    Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
                    To="#FF005170" Duration="00:00:00.1"/>
    </Storyboard>

    <Storyboard x:Name="GreenThemeStoryboard">
        <ColorAnimation Storyboard.TargetName="OuterEdge" 
                    Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)"
                    To="#FF042E00" Duration="00:00:00.1" EnableDependentAnimation="True"/>
        <ColorAnimation Storyboard.TargetName="OuterEdge" 
                    Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)"
                    To="#FF085300" Duration="00:00:00.1" EnableDependentAnimation="True"/>
        <ColorAnimation Storyboard.TargetName="OuterEdge" 
                    Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)"
                    To="#FF10AC00" Duration="00:00:00.1" EnableDependentAnimation="True"/>

        <ColorAnimation Storyboard.TargetName="InnerSurface" 
                    Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
                    To="#FF0A7000" Duration="00:00:00.1"/>
    </Storyboard>

    <Storyboard x:Name="RedThemeStoryboard">
        <ColorAnimation Storyboard.TargetName="OuterEdge" 
                    Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[0].(GradientStop.Color)"
                    To="#FF2E0000" Duration="00:00:00.1" EnableDependentAnimation="True"/>
        <ColorAnimation Storyboard.TargetName="OuterEdge" 
                    Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[2].(GradientStop.Color)"
                    To="#FF530000" Duration="00:00:00.1" EnableDependentAnimation="True"/>
        <ColorAnimation Storyboard.TargetName="OuterEdge" 
                    Storyboard.TargetProperty="(Shape.Fill).(GradientBrush.GradientStops)[1].(GradientStop.Color)"
                    To="#FFAC0000" Duration="00:00:00.1" EnableDependentAnimation="True"/>

        <ColorAnimation Storyboard.TargetName="InnerSurface" 
                    Storyboard.TargetProperty="(Shape.Fill).(SolidColorBrush.Color)"
                    To="#FF700000" Duration="00:00:00.1"/>
    </Storyboard>
</Page.Resources>

在 Metro 中,并非所有自定义动画都会默认运行。可能影响性能的动画必须在运行前启用。这些类型的动画被称为“依赖动画”。要启用它们,必须将其 EnableDependentAnimation 属性设置为 True。请注意,上面 XAML 标记中定义的一些动画对象具有此属性设置为 True,特别是那些针对带有 GradientBrush 的 Path 的动画。

现在可以通过调用 SetTheme() 方法来设置主题。

''' <summary>
''' Sets the user's preferred theme and writes the user's preference to
''' the app's settings.
''' </summary>
''' <param name="theme">User's preferred theme</param>
Private Sub SetTheme(ByVal theme As String)
    Dim localSettings As ApplicationDataContainer = ApplicationData.Current.LocalSettings
    localSettings.CreateContainer("ThemeContainer", ApplicationDataCreateDisposition.Always)

    Dim bmp As New BitmapImage

    Select Case theme
        Case "Blue"
            bmp.UriSource = New Uri(Me.BaseUri, "/Images/BackgroundBlue.png")
            BlueThemeStoryboard.Begin()
        Case "Green"
            bmp.UriSource = New Uri(Me.BaseUri, "/Images/BackgroundGreen.png")
            GreenThemeStoryboard.Begin()
        Case "Red"
            bmp.UriSource = New Uri(Me.BaseUri, "/Images/BackgroundRed.png")
            RedThemeStoryboard.Begin()
    End Select
    ' Write setting
    localSettings.Containers("ThemeContainer").Values("ThemeSetting") = theme
    BackgroundImage.Source = bmp
End Sub

请注意,在上面的方法中,我创建了一个名为 ThemeSetting 的应用程序设置,它存储在一个名为 ThemeContainer 的容器中。一旦写入了这个设置,它将在下次启动应用程序时用于加载用户首选的主题。

''' <summary>
''' Checks whether the user has set a preferred theme and sets
''' it when the app is loaded.
''' </summary>
Private Sub LoadTheme()
    Dim localSettings As ApplicationDataContainer = ApplicationData.Current.LocalSettings

    If (localSettings.Containers.ContainsKey("ThemeContainer")) Then
        ' Read setting
        Dim theme As String = CStr(localSettings.Containers("ThemeContainer").Values("ThemeSetting"))
        SetTheme(theme)
    End If
End Sub

设置壁纸

设置壁纸是通过调用 SetWallpaper() 方法完成的。

''' <summary>
''' Sets the app's wallpaper when the user selects an image file.
''' </summary>
Private Async Sub SetWallpaper()
    Dim openPicker As New FileOpenPicker
    With openPicker
        .ViewMode = PickerViewMode.Thumbnail
        .SuggestedStartLocation = PickerLocationId.PicturesLibrary
        .FileTypeFilter.Add(".png")
        .FileTypeFilter.Add(".jpg")
        .FileTypeFilter.Add(".jpeg")
    End With

    Dim file As StorageFile = Await openPicker.PickSingleFileAsync()

    If (file IsNot Nothing) Then
        Dim stream As IRandomAccessStream = Await file.OpenAsync(FileAccessMode.Read)
        Dim bmp As New BitmapImage
        bmp.SetSource(stream)
        WallpaperImage.Source = bmp

        ' Store the file
        StorageApplicationPermissions.FutureAccessList.Clear()
        Dim token As String = StorageApplicationPermissions.FutureAccessList.Add(file)
    End If
End Sub

上面的方法启动了文件选择器。文件选择器是一个界面,允许用户选择一个或多个文件供应用程序打开。由于这里使用文件选择器来显示用户可以设置为壁纸的图片,因此 FileOpenPicker 对象的 ViewMode 被设置为 PickerViewMode.Thumbnail。SuggestedStartLocation 属性指定了 Pictures 库是文件选择器第一次启动时应该检查图片的第一个位置。(在后续用户启动文件选择器时,它将从用户上次检查的目录开始)。我还通过将其添加到 FileTypeFilter 属性返回的列表中来指定文件选择器应处理的文件类型。

当所有必要的文件选择器属性都设置好后,通过调用 Await FileOpenPicker.PickSingleFileAsync() 来显示文件选择器。

一旦文件被打开,我需要跟踪该文件,以便下次启动应用时可以使用它来显示壁纸。要做到这一点,我将文件添加到 FutureAccessList。将文件添加到 FutureAccessList 会返回一个 token,这是一个唯一标识列表中文件的字符串值。要访问下一个应用启动时的文件,我可以将 token 存储在应用的设置中,但还有另一种选择,我将在下一节中解释。

加载壁纸

应用程序加载时会检查用户是否指定了壁纸。这是通过调用 LoadWallpaper() 方法完成的。

''' <summary>
''' Sets the app's wallpaper when the application is loaded.
''' </summary>
Private Async Sub LoadWallpaper()
    Dim n As Integer = StorageApplicationPermissions.FutureAccessList.Entries.Count

    If (n > 0) Then
        Dim firstToken As String = StorageApplicationPermissions.FutureAccessList.Entries.First.Token
        Dim retrievedFile As StorageFile = Await StorageApplicationPermissions.FutureAccessList.GetFileAsync(firstToken)
        Dim stream As IRandomAccessStream = Await retrievedFile.OpenAsync(FileAccessMode.Read)
        Dim bmp As New BitmapImage
        bmp.SetSource(stream)
        WallpaperImage.Source = bmp
    End If
End Sub

要设置壁纸,我检索已添加到 FutureAccessList 的文件。这是通过使用列表中第一个条目的 token 来完成的。如果文件被添加到 FutureAccessList,那么该应用程序的 FutureAccessList 将只有一个条目。

结论

在此应用程序中,我使用了微软定义的几项 Metro 功能。以下列表包含与一些已使用功能相关的指南链接;

历史

  • 2012 年 7 月 4 日:首次发布
  • 2012 年 7 月 9 日:添加了主题和壁纸功能
© . All rights reserved.