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

为 X11 和 Windows 编写 XAML 扫雷游戏

starIconstarIconstarIconstarIconstarIcon

5.00/5 (5投票s)

2015 年 11 月 10 日

CPOL

9分钟阅读

viewsIcon

17975

downloadIcon

522

目前,没有任何主流的 Linux/Unix (X11) GUI 应用程序框架(如 GTK+、KDE)支持基于 XAML 的应用程序开发。Moonlight 项目(包括 XAML 支持)已于 2012 年 5 月 29 日被弃用。本文介绍了一个基于 XAML 和 WPF 的扫雷游戏。

 下载 XamlMines_X11_32.zip Mono 解决方案,包含完整的源代码和可执行文件
 下载 XamlMines_X11_64.zip Mono 解决方案,包含完整的源代码和可执行文件
 下载 XamlMines_Win81.zip Visual Studio 2013 解决方案,包含完整的源代码和可执行文件

引言

本文是一项案例研究,介绍如何使用 **Roma Widget Set** (Xrw) 编写一个基于 MVVM (Model View ViewModel) 设计模式的 X11/Windows(跨平台)扫雷游戏(利用 WPF UserControls)并使用 XAML。Roma Widget Set 是一个零依赖的 GUI 应用程序框架,适用于 X11(仅需要免费 Mono 标准安装的程序集和免费 X11 发行版的库;它不特别需要 GNOME、KDE、cairo、pango 或商业库),并且完全用 C# 实现。

本文是以下系列文章的延续:为 X11 编写 XAML 对话框应用程序为 X11 编写 XAML 功能区应用程序为 X11 编写具有海量数据绑定和零代码的 XAML 应用程序为 X11 编写 XAML 计算器应用程序为 X11 编写具有几何对象(形状)的 XAML 应用程序为 X11 编写带 UserControls 的 XAML 应用程序为 X11 和 Windows 编写 XAML 7 段 LCD 显示屏 UserControl。据我所知,这是 Moonlight 被弃用后,首次尝试使用 Xrw 进行 XAML for X11 应用程序开发。

Roma Widget Set 和 XAML 实现都未完成。此示例应用程序旨在作为又一个“概念验证”,并检查是否以及如何能够使用 XAML 为 X11/Windows(跨平台)应用程序创建基于 MVVM 设计模式的应用程序。

由于这是第八次尝试使用 XAML 进行 X11 应用程序开发,并且已成功,因此后续肯定会有更多关于使用 Roma Widget Set 在 X11 上进行 XAML 开发的文章。

背景

使用 XAML 进行 X11 应用程序开发的 **动机** 和总体 **概念** 已在 为 X11 编写 XAML 对话框应用程序 文章中进行了介绍。

Xrw XAML 包装器的一些 bug 在 Xrw 版本 0.9 发布后已在此游戏中修复。

焦点

由于之前的七篇“编写 XAML…”文章已经演示了多种 MVVM 技术,本文将演示使用 XAML for X11

  • 可以轻松创建与 Windows 兼容且外观精美的游戏。

使用代码

游戏是使用 Mono Develop 2.4.1 和 Mono 2.8.1 在 OPEN SUSE 11.3 Linux 32 位 EN 和 GNOME 桌面环境下编写的。移植到任何旧版本或新版本都不应是问题。示例应用程序的解决方案是使用 MONO/.NET 3.5 构建的。它包含两个项目(提供完整的源代码供下载)。

  • XamlMines 包含游戏的源代码和 UserControls。
  • XamlPreprocessor 包含 XAML 预处理器的源代码。

该游戏也在 OPEN SUSE 12.3 Linux 64 位 DE 和 GNOME 桌面、IceWM、TWM 和 Xfce 环境下,使用 Mono Develop 3.0.6 和 Mono 3.0.4 进行了测试。

32 位和 64 位解决方案之间的唯一区别是某些 X11 特定数据类型的定义,这已在 使用 Mono Develop 编程 Xlib - 第一部分:低级(概念验证) 文章中进行了描述。

Xlib/X11 窗口处理基于 **X11Wrapper** 程序集版本 1.0(此解决方案中包含一个 1.0 版本早期预览版),它定义了 Xlib/X11 调用 libX11.so. 的函数原型、结构和类型。此程序集是为 使用 Mono Develop 编程 Xlib - 第一部分:低级(概念验证) 项目开发的,并在 编程 Roma Widget Set (C# X11) - 一个零依赖的 GUI 应用程序框架 - 基础 项目中得到了改进。

GUI 框架基于 **Xrw** 程序集版本 1.0(此解决方案中包含一个 1.0 版本早期预览版),它定义了 XAML 代码中使用的控件/小部件及其包装类(应尽可能接近 Microsoft® 原版)。此程序集是在 编程 Roma Widget Set (C# X11) - 一个零依赖的 GUI 应用程序框架 - 基础 项目中开发的。

该游戏实现了一个 UserControl **BoardCell**,并利用了 为 X11 和 Windows 编写 XAML 7 段 LCD 显示屏 UserControl 文章中介绍的 UserControl **SimpleSevenSegment**。

建议:要使用 MonoDevelop 的类库文档快捷键 (F1),必须安装“mono-tools”软件包。

示例应用程序的图像展示了游戏在多个平台上的扫雷游戏和分数列表。

第一张图片展示了游戏在 OPEN SUSE 11.3 Linux 32 位 EN 和 GNOME 桌面上的运行情况。

游戏中显示了一个“游戏失败”的状态。除了显示周围八个单元格中有多少地雷的数字单元格外,一个单元格被标记为有地雷(灰色地雷),还有一个地雷被炸开(红色背景上的黑色地雷)。

第二张图片展示了游戏在 OPEN SUSE 11.3 Linux 32 位 EN 和 GNOME 桌面上的分数列表。

列表在游戏成功结束后更新,并且限制为前 25 名得分。

第三张图片展示了游戏在 OPEN SUSE 12.3 Linux 64 位 DE 和 Xfce 环境下的运行情况。

游戏中显示了一个“初始”状态。

第四张图片展示了游戏在 OPEN SUSE 12.3 Linux 64 位 DE 和 Xfce 环境下的空分数列表。

第五张图片展示了游戏在 OPEN SUSE 12.3 Linux 64 位 DE 和 Xfce 环境下的警告对话框。

当分数列表无法写入 "/usr/share/Mines/scores.xml" 时,会弹出此对话框。

第六张图片展示了游戏在 Windows® 8.1 64 位版本上的运行情况。

游戏中显示了一个“初始”状态。

第七张图片展示了游戏在 Windows® 8.1 64 位版本下的空分数列表。

应用程序窗口使用 System.Windows.Controls.DockPanel 作为根布局管理器。它还定义了笑脸(SmilyGood 和 SmilyBad)以及 **BoardCell**(MarkedMine, ExplodedMine, Untouched, NoNeighbour, OneNeighbour, TwoNeighbour, ThreeNeighbour, FourNeighbour, FifeNeighbour, SixNeighbour, SevenNeighbourEightNeighbour)的图像,作为 System.Windows.Media.DrawingImage

用于显示剩余未覆盖地雷和经过时间的 7 段显示屏以及笑脸的布局是通过 System.Windows.Controls.Grid 实现的。

游戏棋盘的布局是通过 System.Windows.Controls.Viewbox(强制实现二次几何形状)和嵌套的 System.Windows.Controls.UniformGrid 实现的。

X11 版本和 Windows 8.1 版本的游戏之间没有功能差异,它们完全共享相同的代码。

逐步演示

主视图文件上下文

XAML (MainWindow.xaml)

这是 XAML 文件的一般结构(省略了资源、菜单、7 段显示屏、笑脸和 63 个棋盘单元格)。

<Window         x:Class="Mines.MainWindow"
                xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
                xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
                xmlns:src="clr-namespace:Mines"
                xmlns:ext="clr-namespace:System.Windows.ExtControls"
                xmlns:my="clr-namespace:Mines"
                Name="Mines" Title="Mines for WPF"
                Width="500" Height="650" Icon="XrwIcon16.bmp"
                Background="#FFDDDDDD">
    <Window.Resources>
        <ResourceDictionary>
... // The DrawingImage resources.
        </ResourceDictionary>
    </Window.Resources>
    <DockPanel Name="dockpanelMain" DataContext="{StaticResource MainViewModel}">
        <Menu      Name="MainMenu"         DockPanel.Dock="Top" >
... // The menu.
        </Menu>
        
        <Grid Name="gridMain">
            <Grid.RowDefinitions>
                <RowDefinition Height="8"></RowDefinition>
                <RowDefinition Height="80"></RowDefinition>
                <RowDefinition Height="8"></RowDefinition>
                <RowDefinition Height="*"></RowDefinition>
                <RowDefinition Height="8"></RowDefinition>
            </Grid.RowDefinitions>
            <Grid.ColumnDefinitions>
                <ColumnDefinition Width="8"></ColumnDefinition>
                <ColumnDefinition Width="*"></ColumnDefinition>
                <ColumnDefinition Width="8"></ColumnDefinition>
            </Grid.ColumnDefinitions>
                
            <Grid Name="gridPoints" Grid.Column="1" Grid.Row="1" Background="#FFDDDDDD">
... // The  7 segment displays for remaining uncovered mines and elapsed time, the smiley.
            </Grid>
                
            <Viewbox Name="viewboxBoard" Grid.Column="1" Grid.Row="3" >
                <UniformGrid Name="uniformgridBoard" Background="#DDDDDD" Rows="8" Columns="8">
                   <my:BoardCell x:Name="boardcell0A01"/>
... // The other 63 board cells.
               </UniformGrid>
            </Viewbox>

        </Grid>
    </DockPanel>
</Window>

所有资源都是 System.Windows.Media.DrawingImage。以下是两个示例:详细的 ExplodedMine 和简单的 OneNeighbour

<DrawingImage x:Key="ExplodedMine">
    <DrawingImage.Drawing>
        <DrawingGroup>
            <DrawingGroup.Children>
                <GeometryDrawing Brush="#FFFF0000" Geometry="M 0,0 L 100,0 L 100,100 L 0,100 Z" />
                <GeometryDrawing Brush="#FF000000" Geometry="M 50,15 C 68,15 85,32 85,50 C 85,69 69,85 50,85 C 32,85 15,69 15,50 C 15,32 32,15 50,15 Z">
                    <GeometryDrawing.Pen>
                        <Pen Thickness="1" LineJoin="Round" Brush="#FF666666"/>
                    </GeometryDrawing.Pen>
                </GeometryDrawing>
                <GeometryDrawing Brush="#FF888888" Geometry="M 40,25 C 49,25 55,31 55,40 C 55,49 49,55 40,55 C 31,55 25,49 25,40 C 25,31 31,25 40,25 Z">
                    <GeometryDrawing.Pen>
                        <Pen Thickness="1" LineJoin="Round" Brush="#FF666666"/>
                    </GeometryDrawing.Pen>
                </GeometryDrawing>
                <GeometryDrawing Brush="#FFEEEEEE" Geometry="M 38,30 C 40.8,30 44,33.2 44,37 C 44,40.8 40.8,44 37,44 C 33.2,44 30,40.8 30,37 C 30,33.2 33.2,30 37,30 Z">
                    <GeometryDrawing.Pen>
                        <Pen Thickness="1" LineJoin="Round" Brush="#FFAAAAAA"/>
                    </GeometryDrawing.Pen>
                </GeometryDrawing>
                <GeometryDrawing Brush="#FF000000" Geometry="M 46,17 L 50,2 L 54,17 Z" />
                <GeometryDrawing Brush="#FF000000" Geometry="M 83,46 L 98,50 L 83,54 Z" />
                <GeometryDrawing Brush="#FF000000" Geometry="M 46,83 L 50,98 L 54,83 Z" />
                <GeometryDrawing Brush="#FF000000" Geometry="M 17,46 L 2,50 L 17,54 Z" />
                <GeometryDrawing Brush="#FF000000" Geometry="M 30,24 L 17,17 L 24,30 Z" />
                <GeometryDrawing Brush="#FF000000" Geometry="M 70,24 L 83,17 L 76,30 Z" />
                <GeometryDrawing Brush="#FF000000" Geometry="M 70,76 L 83,83 L 76,70 Z" />
                <GeometryDrawing Brush="#FF000000" Geometry="M 30,76 L 17,83 L 24,70 Z" />
            </DrawingGroup.Children>
        </DrawingGroup>
    </DrawingImage.Drawing>
</DrawingImage>

<DrawingImage x:Key="OneNeighbour">
    <DrawingImage.Drawing>
        <DrawingGroup>
            <DrawingGroup.Children>
                <GeometryDrawing Brush="#FFCCCCCC" Geometry="M 0,0 L 100,0 L 100,100 L 0,100 Z" />
                <GeometryDrawing Brush="#FF0000AA" Geometry="M 48,25 L 55,25 L 55,75 L 50,75 L 50,30 L 40,40 L 33,40 Z" />
            </DrawingGroup.Children>
        </DrawingGroup>
    </DrawingImage.Drawing>
</DrawingImage>

为 X11 和 Windows 编写 XAML 7 段 LCD 显示屏 UserControl 和之前的文章相比,没有需要讨论的增强或区别。

后端代码 (MainWindow.xaml.cs)

主视图对应的 C# 代码文件是 MainWindow.xaml.cs,有 800 多行代码,无法全部展示。我将讨论一些相关方法。

/// <summary>Counts the neighbour mines for indicated board cell.</summary>
/// <param name="currentBoardCell">The board cell to count the neighbour mines for.</param>
/// <returns>The number of neighbour mines.</returns>
public int CountNeighbourMines(UIElement currentBoardCell)
{
    System.Windows.Controls.UIElementCollection children = this.uniformgridBoard.Children;
    int currentIndex = children.IndexOf(currentBoardCell);
    int col = currentIndex % CELLS_PER_DIRECTION;
    int row = (int)currentIndex / CELLS_PER_DIRECTION;

    int nNeighbours = 0;

    if (row > 0)
    {
        if (col > 0)
          nNeighbours += ((children[(row-1)*CELLS_PER_DIRECTION+col-1] as BoardCell).HasMine ? 1 : 0);

        nNeighbours += ((children[(row-1)*CELLS_PER_DIRECTION+col] as BoardCell).HasMine ? 1 : 0);

        if (col + 1 < CELLS_PER_DIRECTION)
          nNeighbours += ((children[(row-1)*CELLS_PER_DIRECTION+col+1] as BoardCell).HasMine ? 1 : 0);
    }
    {
        if (col > 0)
          nNeighbours += ((children[row*CELLS_PER_DIRECTION+col-1] as BoardCell).HasMine ? 1 : 0);

        if (col + 1 < CELLS_PER_DIRECTION)
          nNeighbours += ((children[row*CELLS_PER_DIRECTION+col+1] as BoardCell).HasMine ? 1 : 0);
    }
    if (row + 1 < CELLS_PER_DIRECTION)
    {
        if (col > 0)
          nNeighbours += ((children[(row+1)*CELLS_PER_DIRECTION+col-1] as BoardCell).HasMine ? 1 : 0);

        nNeighbours += ((children[(row+1)*CELLS_PER_DIRECTION+col] as BoardCell).HasMine ? 1 : 0);

        if (col + 1 < CELLS_PER_DIRECTION)
          nNeighbours += ((children[(row+1)*CELLS_PER_DIRECTION+col+1] as BoardCell).HasMine ? 1 : 0);
    }

    return nNeighbours;
}

CountNeighbourMines() 方法确定指示的棋盘单元格的列和行,并检查三个(位于四个角之一的棋盘单元格)、五个(位于棋盘边缘的棋盘单元格)或八个相邻棋盘单元格中是否有地雷。常量 CELLS_PER_DIRECTION 定义了 x 和 y 方向上的棋盘单元格数量。棋盘始终是正方形的,在 x 和 y 方向上具有相同数量的棋盘单元格。

此方法用于计算棋盘单元格是否应显示 NoNeighbourOneNeighbourTwoNeighbourThreeNeighbourFourNeighbourFifeNeighbourSixNeighbourSevenNeighbourEightNeighbour 图像。

/// <summary>Calculate the next unhidable neighbour cell.</summary>
/// <param name="currentBoardCell">The cell to calculate the next unhidable neighbour cell
/// for.<see cref="IElement"/></param>
/// <returns>The list of next unhidable neighbour cell on success, or an empty list
/// otherwise.<see cref="System.Object"/></returns>
public List<object> NextUnhidableNeighbour(UIElement currentBoardCell)
{
    System.Windows.Controls.UIElementCollection children = this.uniformgridBoard.Children;
    int currentIndex = children.IndexOf(currentBoardCell);
    int col = currentIndex % CELLS_PER_DIRECTION;
    int row = (int)currentIndex / CELLS_PER_DIRECTION;

    List<object> result = new List<object>();

    if (row > 0)
    {
        if (col > 0)
        {
            BoardCell c = (children[(row - 1) * CELLS_PER_DIRECTION + col - 1] as BoardCell);
            if (!c.HasMine)
                if (c != currentBoardCell)
                    result.Add(c);
        }

        {
            BoardCell c = (children[(row - 1) * CELLS_PER_DIRECTION + col] as BoardCell);
            if (!c.HasMine)
                if (c != currentBoardCell)
                    result.Add(c);
        }

        if (col + 1 < CELLS_PER_DIRECTION)
        {
            BoardCell c = (children[(row - 1) * CELLS_PER_DIRECTION + col + 1] as BoardCell);
            if (!c.HasMine)
                if (c != currentBoardCell)
                    result.Add(c);
        }
    }
    {
        if (col > 0)
        {
            BoardCell c = (children[row * CELLS_PER_DIRECTION + col - 1] as BoardCell);
            if (!c.HasMine)
                if (c != currentBoardCell)
                    result.Add(c);
        }

        if (col + 1 < CELLS_PER_DIRECTION)
        {
            BoardCell c = (children[row * CELLS_PER_DIRECTION + col + 1] as BoardCell);
            if (!c.HasMine)
                if (c != currentBoardCell)
                    result.Add(c);
        }
    }
    if (row + 1 < CELLS_PER_DIRECTION)
    {
        if (col > 0)
        {
            BoardCell c = (children[(row + 1) * CELLS_PER_DIRECTION + col - 1] as BoardCell);
            if (!c.HasMine)
                if (c != currentBoardCell)
                    result.Add(c);
        }

        {
            BoardCell c = (children[(row + 1) * CELLS_PER_DIRECTION + col] as BoardCell);
            if (!c.HasMine)
                if (c != currentBoardCell)
                    result.Add(c);
        }

        if (col + 1 < CELLS_PER_DIRECTION)
        {
            BoardCell c = (children[(row + 1) * CELLS_PER_DIRECTION + col + 1] as BoardCell);
            if (!c.HasMine)
                if (c != currentBoardCell)
                    result.Add(c);
        }
    }

    return result;
}

NextUnhidableNeighbour() 方法确定指示的棋盘单元格的列和行,并检查三个(位于四个角之一的棋盘单元格)、五个(位于棋盘边缘的棋盘单元格)或八个相邻棋盘单元格是否有地雷,并且——如果它们没有地雷——则将棋盘单元格包含在返回的集合中。

此方法用于计算相邻的棋盘单元格,当玩家揭开指示的棋盘单元格时,这些单元格将自动从 Untouched 图像切换到 NoNeighbourOneNeighbourTwoNeighbourThreeNeighbourFourNeighbourFifeNeighbourSixNeighbourSevenNeighbourEightNeighbour 图像之一。

/// <summary>Check whether game is solved.</summary>
/// <returns>True if game is solved, or false otherwise.<see cref="System.Boolean"/></returns>
public bool CheckGameSolved ()
{
    if (_gameSolved == true)
        return true;
    
    int countUncovered = 0;
    System.Windows.Controls.UIElementCollection children = this.uniformgridBoard.Children;
    for (int index = 0; index < children.Count; index++)
    {
        string imageResourceName = (children[index] as MainWindow).GetImageResourceName();
        if (imageResourceName == MainWindow.EightNeighbour)
            countUncovered++;
        else if (imageResourceName == MainWindow.SevenNeighbour)
            countUncovered++;
        else if (imageResourceName == MainWindow.SevenNeighbour)
            countUncovered++;
        else if (imageResourceName == MainWindow.SixNeighbour)
            countUncovered++;
        else if (imageResourceName == MainWindow.FifeNeighbour)
            countUncovered++;
        else if (imageResourceName == MainWindow.FourNeighbour)
            countUncovered++;
        else if (imageResourceName == MainWindow.ThreeNeighbour)
            countUncovered++;
        else if (imageResourceName == MainWindow.TwoNeighbour)
            countUncovered++;
        else if (imageResourceName == MainWindow.OneNeighbour)
            countUncovered++;
        else if (imageResourceName == MainWindow.NoNeighbour)
            countUncovered++;
    }
    
    if (children.Count - countUncovered == _minesToDetect)
    {
        _gameSolved = true;
        
        DateTime dt        = DateTime.Now;
        ulong timeStamp    = (ulong)(dt.Second + dt.Minute * 60 + dt.Hour * 3600) +
                             (ulong)dt.Day * (ulong)86400;
        string user = System.Environment.UserName;
        
        UpdateAndShowScores (new ScoreItem (1, _minesToDetect, (int)(timeStamp - _timeStamp),
                             dt.ToShortDateString () + " " + dt.ToLongTimeString (), user));
        return true;
    }
    else
        return false;
}

CheckGameSolved() 方法确定是否所有未被揭开的棋盘单元格(没有地雷的)都已被揭开。
当未被揭开的棋盘单元格数量(无论棋盘单元格显示 MarkedMine 还是 Untouched 图像)等于属性 _minesToDetect 时,此条件满足。

在这种情况下,该方法调用 UpdateAndShowScores() 将新分数添加到分数列表中。

/// <summary>Access the score list (XML file) and show the scores dialog. Add a new score,
/// if parameter is not null.</summary>
/// <param name="newScoreItem">A new score to add to the score list, if  not
/// null.<see cref="ScoreItem"/></param>
private void UpdateAndShowScores (ScoreItem newScoreItem)
{
    string commonAppDataPath;
    string localAppDataPath;
    bool commonPathOK = TryPrepareCommonApplicationDataPath(out commonAppDataPath);
    bool localPathOK  = TryPrepareLoacalApplicationDataPath(out localAppDataPath);
    
    if (commonPathOK == true && !string.IsNullOrEmpty (commonAppDataPath))
    {
        commonAppDataPath = Path.Combine (commonAppDataPath, "scores.xml");
        System.Xml.XmlDocument appData = new System.Xml.XmlDocument ();
        
        XmlNode rootNode   = null;
        XmlNode scoresNode = null;
        if (!File.Exists(commonAppDataPath))
        {
            appData.CreateXmlDeclaration ("1.0", "UTF-8", "");
            rootNode = appData.CreateNode(XmlNodeType.Element, "Mines", "");
            appData.AppendChild (rootNode);
            scoresNode = appData.CreateNode(XmlNodeType.Element, "Scores", "");
            rootNode.AppendChild (scoresNode);
        }
        else
        {
            appData.Load (localAppDataPath);
            rootNode = appData.ChildNodes[0];
            if (rootNode != null)
            {
                scoresNode = rootNode.FirstChildOfTypeName ("Scores");
            }
            else
            {
                MessageBox.Show ("Unable to read common application data from '" +
                                 commonAppDataPath + "'. File content corrupt.");
                return;
            }
        }

        UpdateAndShowScores(newScoreItem, scoresNode);
        
        appData.Save (commonAppDataPath);
    }
    else if (localPathOK == true && !string.IsNullOrEmpty (localAppDataPath))
    {
        localAppDataPath = Path.Combine (localAppDataPath, "AppData.xml");
        System.Xml.XmlDocument appData = new System.Xml.XmlDocument ();
        
        XmlNode rootNode   = null;
        XmlNode scoresNode = null;
        if (!File.Exists(localAppDataPath))
        {
            appData.CreateXmlDeclaration ("1.0", "UTF8", "");
            rootNode = appData.CreateNode(XmlNodeType.Element, "Mines", "");
            appData.AppendChild (rootNode);
            scoresNode = appData.CreateNode(XmlNodeType.Element, "Scores", "");
            rootNode.AppendChild (scoresNode);
        }
        else
        {
            appData.Load (localAppDataPath);
            rootNode = appData.ChildNodes[0];
            if (rootNode != null)
            {
                scoresNode = rootNode.FirstChildOfTypeName ("Scores");
            }
            else
            {
                MessageBox.Show ("Unable to read local application data from '" +
                                  localAppDataPath + "'. File content corrupt.");
                return;
            }
        }

        UpdateAndShowScores(newScoreItem, scoresNode);
        
        XmlNode settingsNode = rootNode.FirstChildOfTypeName ("Settings");
        if (settingsNode == null)
        {
            settingsNode = appData.CreateNode(XmlNodeType.Element, "Settings", "");
            rootNode.AppendChild (settingsNode);
        }
        
        XmlNode suppressNoCommonAppDataWarningNode =
           settingsNode.FirstChildOfTypeName ("SuppressNoCommonAppDataWarning");
        if (suppressNoCommonAppDataWarningNode == null)
        {
            suppressNoCommonAppDataWarningNode =
                appData.CreateNode(XmlNodeType.Element, "SuppressNoCommonAppDataWarning", "");
            settingsNode.AppendChild (suppressNoCommonAppDataWarningNode);
        }
        
        if (suppressNoCommonAppDataWarningNode.Attributes.Count < 1 ||
            suppressNoCommonAppDataWarningNode.Attributes["value"] == null ||
            suppressNoCommonAppDataWarningNode.Attributes["value"].Value == "false")
        {
            MessageBoxResult result =
                MessageBox.Show ("There is no access go the common application data.\n" +
                                 "Use local application data instead.\n\nShow this message again?",
                                 "WARNING", MessageBoxButton.YesNoCancel);
            XmlAttribute suppressVallue;
            
            if (suppressNoCommonAppDataWarningNode.Attributes.Count < 1 ||
                suppressNoCommonAppDataWarningNode.Attributes["value"] == null)
            {
                suppressVallue = appData.CreateAttribute ("value");
                suppressNoCommonAppDataWarningNode.Attributes.Append (suppressVallue);
            }
            else
                suppressVallue = suppressNoCommonAppDataWarningNode.Attributes["value"];
            
            if (result == MessageBoxResult.No)
                suppressVallue.Value = "true";
            else
                suppressVallue.Value = "false";
        }
        
        appData.Save (localAppDataPath);
    }
    else
    {
        MessageBox.Show ("Failed to open scores.", "Error");
    }
}

UpdateAndShowScores() 方法将新分数添加到分数列表(如果参数 newScoreItem 不为 null),并显示 Scores 窗口。分数列表存储为 XML 文件 "scores.xml",位于 System.Environment.SpecialFolder.CommonApplicationData 或作为备用选项位于 System.Environment.SpecialFolder.LocalApplicationData。但是,System.Environment.SpecialFolder.LocalApplicationData 只能从当前用户访问,并且只能存储一个用户的分数。

为了确定目标文件夹是否授予写入权限,将调用 TryPrepareCommonApplicationDataPath()TryPrepareLoacalApplicationDataPath() 方法。

private bool TryPrepareCommonApplicationDataPath (out string appDataPath)
{
    appDataPath = string.Empty;
    string appName = System.Reflection.Assembly.GetExecutingAssembly ().GetName ().Name;
        
    try
    {
        appDataPath = System.Environment.GetFolderPath(
            System.Environment.SpecialFolder.CommonApplicationData);
        bool accessGranted = false;
#if !UNIX
        System.Security.AccessControl.DirectorySecurity directorySec;
        directorySec = Directory.GetAccessControl(appDataPath);
        appDataPath = Path.Combine(appDataPath, appName);
        if (!Directory.Exists(appDataPath))
        {
            Directory.CreateDirectory(appDataPath);
            directorySec = Directory.GetAccessControl(appDataPath);

            DirectoryInfo dInfo = new DirectoryInfo(appDataPath);
            System.Security.AccessControl.DirectorySecurity dSecurity = dInfo.GetAccessControl();
            System.Security.Principal.SecurityIdentifier dSid =
                new System.Security.Principal.SecurityIdentifier(
                    System.Security.Principal.WellKnownSidType.WorldSid, null);
            dSecurity.AddAccessRule(new System.Security.AccessControl.FileSystemAccessRule (dSid,
                System.Security.AccessControl.FileSystemRights.FullControl,
                System.Security.AccessControl.InheritanceFlags.ObjectInherit |
                System.Security.AccessControl.InheritanceFlags.ContainerInherit,
                System.Security.AccessControl.PropagationFlags.NoPropagateInherit,
                System.Security.AccessControl.AccessControlType.Allow));
            dInfo.SetAccessControl(dSecurity);
        }
        accessGranted = true;
#else
        Mono.Unix.Native.Stat stat;
        int result = Mono.Unix.Native.Syscall.stat (appDataPath, out stat);
        if (result == 0 &&
            (stat.st_mode & Mono.Unix.Native.FilePermissions.S_IWOTH) ==
            Mono.Unix.Native.FilePermissions.S_IWOTH)
            accessGranted = true;
        
        appDataPath = Path.Combine (appDataPath, appName);
        if (accessGranted == true && !Directory.Exists (appDataPath))
        {
            result = Mono.Unix.Native.Syscall.mkdir (appDataPath,
                                                     Mono.Unix.Native.FilePermissions.S_IRWXG |
                                                     Mono.Unix.Native.FilePermissions.S_IRWXU |
                                                     Mono.Unix.Native.FilePermissions.S_IRWXO);
            if (result != 0)
                accessGranted = false;
        }
#endif
        return accessGranted;
    }
    catch (Exception ex)
    {
        Console.WriteLine ("Unable to access common application data path: " + ex.Message);
    }
    return false;
}

TryPrepareCommonApplicationDataPath()TryPrepareLoacalApplicationDataPath() 方法(此处未列出,但非常相似)必须区分 POSIX 标准和 Microsoft Windows。因此,定义了一个编译器符号 UNIX

主视图模型文件上下文

主视图没有 ModelView,因为不需要 Model。

主模型文件上下文

主视图没有 Model,因为没有数据需要处理。

SimpleSevenSegment UserControl

有关详细信息,请参阅文章 为 X11 和 Windows 编写 XAML 7 段 LCD 显示屏 UserControl

BoardCell UserControl

XAML (BoardCell.xaml)

UserControl 的 XAML 文件是

<UserControl x:Class="Mines.BoardCell"
             xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
             xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
             xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
             xmlns:my="clr-namespace:Mines"
             BorderBrush="#FFDDDDDD" BorderThickness="1">
    <Image Name="imageBoardCell" Source="{DynamicResource Untouched}" MouseEnter="HandleMouseEnter"
     MouseLeave="HandleMouseLeave" MouseUp="HandleMouseUp"/>
</UserControl>

UserControl 只包含一个 System.Windows.Controls.Image

后台代码 (BoardCell.xaml.cs)

UserControl 对应的 C# 代码文件是 BoardCell.xaml.cs。其中包含 System.Windows.Controls.Image 的事件处理程序等。

/// <summary>Handle the MouseLeave event.</summary>
/// <param name="sender">The sender of the event.</param>
/// <param name="e">The <see cref="System.Windows.Input.MouseEventArgs"/> instance
/// containing the event data.</param>
private void HandleMouseLeave (object sender, System.Windows.Input.MouseEventArgs e)
{
    this.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FFDDDDDD"));
}

/// <summary>Handle the MouseEnter event.</summary>
/// <param name="sender">The sender of the event.</param>
/// <param name="e">The <see cref="System.Windows.Input.MouseEventArgs"/> instance
/// containing the event data.</param>
private void HandleMouseEnter (object sender, System.Windows.Input.MouseEventArgs e)
{
    this.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF888888"));
}

/// <summary>Handle the MouseUp event.</summary>
/// <param name="sender">The sender of the event.</param>
/// <param name="e">The <see cref="System.Windows.Input.MouseButtonEventArgs"/> instance
/// containing the event data.</param>
/// <remarks>The 'sender' can eiter be a <see cref="System.Windows.Controls.Image"/>
/// (if invoked by a mouse click) or a <see cref="BoardCell"/> (if invoked by code).</remarks>
private void HandleMouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
    // Check prerequisits.
    Mines.MainWindow w = Application.Current.MainWindow as Mines.MainWindow;
    if (!w.GameStateGood)
        return;

    BoardCell me = (sender is Image ? this : sender as BoardCell);
    if (me == null)
        return;

    // Try to uncover.
    if (e.ChangedButton == System.Windows.Input.MouseButton.Left)
    {
        // Check whether board cell can be uncovered.
        if (me.imageBoardCell.Source != (ImageSource)me.FindResource(MainWindow.Untouched) &&
            me.imageBoardCell.Source != (ImageSource)me.FindResource(MainWindow.MarkedMine))
            return;
        
        // Uncover!
        if (HasMine)
        {
            me.imageBoardCell.Source = (ImageSource)me.FindResource(MainWindow.ExplodedMine);
            w.GameStateGood = false;

            me.InvalidateVisual();
        }
        // Uncover neighbours!
        else
        {
            int neighbours = w.CountNeighbourMines(me);

            if (neighbours == 8)
                me.imageBoardCell.Source = (ImageSource)me.FindResource(MainWindow.EightNeighbour);
            else if (neighbours == 7)
                me.imageBoardCell.Source = (ImageSource)me.FindResource(MainWindow.SevenNeighbour);
            else if (neighbours == 6)
                me.imageBoardCell.Source = (ImageSource)me.FindResource(MainWindow.SixNeighbour);
            else if (neighbours == 5)
                me.imageBoardCell.Source = (ImageSource)me.FindResource(MainWindow.FifeNeighbour);
            else if (neighbours == 4)
                me.imageBoardCell.Source = (ImageSource)me.FindResource(MainWindow.FourNeighbour);
            else if (neighbours == 3)
                me.imageBoardCell.Source = (ImageSource)me.FindResource(MainWindow.ThreeNeighbour);
            else if (neighbours == 2)
                me.imageBoardCell.Source = (ImageSource)me.FindResource(MainWindow.TwoNeighbour);
            else if (neighbours == 1)
                me.imageBoardCell.Source = (ImageSource)me.FindResource(MainWindow.OneNeighbour);
            else
                me.imageBoardCell.Source = (ImageSource)me.FindResource(MainWindow.NoNeighbour);

            me.InvalidateVisual();

            // Uncover neighbours of neighbours !
            if (neighbours == 0)
            {
                List<object> nextUnhidableNeighbour = w.NextUnhidableNeighbour(me);
                foreach (object o in nextUnhidableNeighbour)
                    HandleMouseUp(o, e);
            }
        }
    }
    // Try to mark.
    else if (e.ChangedButton == System.Windows.Input.MouseButton.Right)
    {
        // Check whether board cell can be marked.
        if (me.imageBoardCell.Source != (ImageSource)me.FindResource(MainWindow.Untouched) &&
            me.imageBoardCell.Source != (ImageSource)me.FindResource(MainWindow.MarkedMine))
            return;

        // Mark!
        if (me.imageBoardCell.Source == (ImageSource)me.FindResource(MainWindow.Untouched))
        {
            if (w.UndetectedMines == 0)
                return;
            w.UndetectedMines -= 1;
            me.imageBoardCell.Source = (ImageSource)me.FindResource(MainWindow.MarkedMine);
            
            me.InvalidateVisual();
        }
        // Unmark!
        else // (me.imageBoardCell.Source == (ImageSource)me.FindResource(MainWindow.MarkedMine))
        {
            w.UndetectedMines += 1;
            me.imageBoardCell.Source = (ImageSource)me.FindResource(MainWindow.Untouched);
            
            me.InvalidateVisual();
        }
    }
    
    // Update game state.
    w.CheckGameSolved ();
}

事件处理程序 HandleMouseUp() 通过使用 MainWindow 的一些方法和属性(如 GameStateGoodUndetectedMinesCountNeighbourMines()NextUnhidableNeighbour()CheckGameSolved())来实现完整的游戏棋盘用户交互。

后续步骤

目前棋盘大小固定为 CELLS_PER_DIRECTION = 8,地雷数量固定为 _minesToDetect = CELLS_PER_DIRECTION * 2。两者都可以动态化,以实现不同的游戏级别。

关注点

这是另一个适用于 X11 的 XAML 应用程序,完全兼容 Microsoft®,展示了这种方法的主要优点(与使用 GTK+ 或 KDE 的实现相比):100% 跨平台兼容的 GUI 定义以及创建 GUI 所节省的代码行数。

用户控件的使用可以将复杂的 GUI 分解为更简单、更易于维护的部分,节省代码重复,并且可以应用于 X11 和 Windows(跨平台)。

历史

本文初稿日期为 2015 年 11 月 10 日。

© . All rights reserved.