Click here to Skip to main content
15,867,704 members
Articles / Desktop Programming / X11

Writing a XAML Minesweeper game for X11 and Windows

Rate me:
Please Sign up or sign in to vote.
5.00/5 (5 votes)
9 Nov 2015CPOL9 min read 16.4K   467   2   1
Currently none of the big Linux/Unix (X11) GUI application frameworks (GTK+, KDE) support XAML based application development. The Moonlight project (including XAML support) was abandoned on May 29, 2012. This article introduces a XAML based Minesweeper game with WPF.

Image 1 Download XamlMines_X11_32.zip Mono solution including full source and executable
Image 2 Download XamlMines_X11_64.zip Mono solution including full source and executable
Image 3 Download XamlMines_Win81.zip Visual Studio 2013 solution including full source and executable

Introduction

This article is a case study, how to write a MVVM (Model View ViewModel) design pattern based X11/Windows (cross platform) Minesweeper game (utilizing WPF UserControls) with XAML using the Roma Widget Set (Xrw). The Roma Widget Set is a zero dependency GUI application framework for X11 (it requires only assemblies of the free Mono standard installation and libraries of the free X11 distribution; it doesn't particularly require GNOME, KDE, cairo, pango or commercial libraries) and is implemented entirely in C#.

This article continues the works Writing a XAML dialog application for X11, Writing a XAML ribbon application for X11, Writing a XAML application for X11 with massive data binding and zero code, Writing a XAML calculator application for X11, Writing a XAML application with geometry objects (shapes) for X11, Writing a XAML application for X11 with UserControls and Writing a XAML 7 segment LCD display UserControl for X11 and Windows. As far as i know, this (utilizing the Xrw) is the first attempt to use XAML for X11 application development after the abandonment of Moonlight.

Neither the Roma Widget Set nor the XAML implementation are complete. This sample application is intended as yet another 'proof of concept' and checks out if and how it is possible to create MVVM design pattern based X11/Windows (cross platform) application with XAML.

Since this eighth attempt to use XAML for a X11 application development has been successful, further articles about XAML using the Roma Widget Set on X11 will follow certenly.

Background

The Motivation and the general Concept to use XAML for X11 application development are already explained in the Writing a XAML dialog application for X11 article.

Some bugs of the Xrw XAML wrapper have been fixed for this game after the release Xrw version 0.9.

Focus

Since the previous seven articles "Writing a XAML ..." already demonstrated several MVVM techniques, this article shall demonstrate that with XAML for X11

  • a windows compatible nice looking game can be created easily.

Using the code

The game was written with Mono Develop 2.4.1 for Mono 2.8.1 on OPEN SUSE 11.3 Linux 32 bit EN and GNOME desktop. Neither the port to any older nor to any newer version should be a problem. The sample application's solution is build with MONO/.NET 3.5. It consists of two projects (the complete sources are provided for download):

  • XamlMines contains the source code of the game and UserControls.
  • XamlPreprocessor contains the source code of the XAML preprocessor.

The game is also tested with Mono Develop 3.0.6 for Mono 3.0.4 on OPEN SUSE 12.3 Linux 64 bit DE and GNOME desktop, IceWM, TWM und Xfce.

The only difference between the 32 bit and the 64 bit solution is the definition of some X11 specific data types, as already described in the Programming Xlib with Mono develop -Part 1: Low level (proof of concept) article.

The Xlib/X11 window handling is based on the X11Wrapper assembly version 1.0 (a library version 1.0 early preview is included in this solution), that defines the function prototypes, structures and types for Xlib/X11 calls to the libX11.so. This assembly has been developed for the Programming Xlib with Mono Develop - Part 1: Low-level (proof of concept) project and has been advanced during the Programming the Roma Widget Set (C# X11) - a zero dependency GUI application framework - Basics project.

The GUI framework is based on the Xrw assembly version 1.0 (a library version 1.0 early preview is included in this solution), that defines the widgets/gadgets and its wrapper classes used within the XAML code (that should be as near to the Microsoft® original as reasonable). This assembly has been developed during the Programming the Roma Widget Set (C# X11) - a zero dependency GUI application framework - Basics project.

The game implements a UserControl BoardCell and utilizes the UserControl SimpleSevenSegment, already introduced by the article Writing a XAML 7 segment LCD display UserControl for X11 and Windows.

Advice: To use the class library documentation shortcut (F1) from MonoDevelop, the "mono-tools" package has to be installed.

The images of the sample application show the minesweeper game and the score list on several platforms.

The first image shows the game on OPEN SUSE 11.3 Linux 32 bit EN and GNOME desktop.

Image 4

The game shows a "game lost" state. Beside the chells that show how many mines are in the eight cells that surround the numbered one, one cell is marked to have a mine (gray mine) and one mine is exploded (black mine on red background).

The second image shows the game's score list on OPEN SUSE 11.3 Linux 32 bit EN and GNOME desktop.

Image 5

The list is updated after a game has finished successfully and limited to the 25 best scores.

The third image shows the game on OPEN SUSE 12.3 Linux 64 bit DE and Xfce.

Image 6

The game shows an "initial" state.

The fourth image shows the game's empty score list on OPEN SUSE 12.3 Linux 64 bit DE and Xfce.

Image 7

The fifth image shows the warning dialog on OPEN SUSE 12.3 Linux 64 bit DE and Xfce.

Image 8

This dialog pops up, if score list can not be written to "/usr/share/Mines/scores.xml".

The sixth image shows the game on Windows® 8.1 64 Bit Edition.

Image 9

The game shows an "initial" state.

The seventh image shows the game's empty score list on Windows® 8.1 64 Bit Edition.

Image 10

The application window utilizes the System.Windows.Controls.DockPanel as it's root layout manager. It also defines the images for smiley (SmilyGood and SmilyBad) and BoardCells (MarkedMine, ExplodedMine, Untouched, NoNeighbour, OneNeighbour, TwoNeighbour, ThreeNeighbour, FourNeighbour, FifeNeighbour, SixNeighbour, SevenNeighbour and EightNeighbour) as System.Windows.Media.DrawingImages.

The layout of the 7 segment displays for remaining uncovered mines and elapsed time as well as the smiley is realized by a System.Windows.Controls.Grid.

The layout of the game board is realized by a System.Windows.Controls.Viewbox (to enforce a quadratic geometry) and a nested System.Windows.Controls.UniformGrid.

There is no functional difference between the X11 and Windows 8.1 versions of the game, they share the same code entirely.

Walk through

Main view file context

The XAML (MainWindow.xaml)

Here the general structure of the XAML file (resources, menu, 7 segment displays, smiley and 63 board cells are omitted).

XML
<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>

All resources are System.Windows.Media.DrawingImages. Here are two examples, the elaborate ExplodedMine and the easy OneNeighbour.

XML
<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>

Compared to Writing a XAML 7 segment LCD display UserControl for X11 and Windows and previous articles, no enhancments or differences are to be discussed.

The code behind (MainWindow.xaml.cs)

The corresponding C# code file of the main view is MainWindow.xaml.cs and has more than 800 lines of code - to much to show them all. Instead i want to discuss some relevant methods:

C#
/// <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;
}

The method CountNeighbourMines() determines the column and row for the indicated board cell and checks the three (board cell is situated in one of the four corners), fife (board cell is situated at the board border) or eight neighbour board cells for mines. The constant value CELLS_PER_DIRECTION defines the number of board cells in x and y direction. The board is always quatratic - it has the same number of board cells in x and y direction.

This method is required to calculate whether a board cell has to show NoNeighbour, OneNeighbour, TwoNeighbour, ThreeNeighbour, FourNeighbour, FifeNeighbour, SixNeighbour, SevenNeighbour or EightNeighbour image.

C#
/// <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;
}

The method NextUnhidableNeighbour() determines the column and row for the indicated board cell and checks the three (board cell is situated in one of the four corners), fife (board cell is situated at the board border) or eight neighbour board cells whether they have a mine and - if they don't have a mine - includes the board cell into the return collection.

This method is required to calculate neighbour board cells, that will be switched from Untouched image to one of the NoNeighbour, OneNeighbour, TwoNeighbour, ThreeNeighbour, FourNeighbour, FifeNeighbour, SixNeighbour, SevenNeighbour or EightNeighbour images autamatically, if the player uncovers the indicated board cell.

C#
/// <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;
}

The method CheckGameSolved() determines whether all board cells, that don't have a mine, are uncovered.
This condition is met, if the number of uncovered board cells (no matter if board cell shows MarkedMine or Untouched image) matches the attribute _minesToDetect.

In this case the method calls UpdateAndShowScores() to add the new score to the list of scores.

C#
/// <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");
    }
}

The method UpdateAndShowScores() adds a new score to the list of scores (if argument newScoreItem is not null) and shows the Scores window. The list of scores is stored as the XML file "scores.xml" at System.Environment.SpecialFolder.CommonApplicationData or at System.Environment.SpecialFolder.LocalApplicationData as fallback, but System.Environment.SpecialFolder.LocalApplicationData can be accessed from the current user only and can store the scores of only one user.

To determine whether the target folder grants write access, the methods TryPrepareCommonApplicationDataPath() and TryPrepareLoacalApplicationDataPath() are called.

C#
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;
}

The methods TryPrepareCommonApplicationDataPath() and TryPrepareLoacalApplicationDataPath() (not listed here, but very similar) must distinguish between posix conformity and Microsoft Windows. Hence a compiler symbol UNIX is defined.

Image 11

Main view model file context

There is no ModelView for the main view, because no Model is required.

Main model file context

There is no Model for the main view, because no data are to process.

SimpleSevenSegment UserControl

See article Writing a XAML 7 segment LCD display UserControl for X11 and Windows for details.

BoardCell UserControl

The XAML (BoardCell.xaml)

The XAML file of the UserControl is:

XML
<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>

The UserControl consists of one System.Windows.Controls.Image only.

The code behind (BoardCell.xaml.cs)

The corresponding C# code file of the UserControl is BoardCell.xaml.cs. It contains, among other things, the event handler of the System.Windows.Controls.Image.

C#
/// <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 ();
}

The event handler HandleMouseUp() realizes the complete user interaction of the game board utilizing some of the MainWindow methods and properties, like GameStateGood, UndetectedMines, CountNeighbourMines(), NextUnhidableNeighbour() and CheckGameSolved().

Next steps

Currently the board size is fixed to CELLS_PER_DIRECTION = 8 and the number of mines is fixed to _minesToDetect = CELLS_PER_DIRECTION * 2. Both can be made dynamic to implement several game levels.

Points of Interest

This is another XAML application for X11, fully compatible with Microsoft®, showing the main advantages of this approach (compared to an implementation with GTK+ or KDE): The 100% cross platform compatible GUI definition and the savings of code lines to create the GUI.

The use of user controls can divide complex GUIs into more simple and maintainable peaces, saves code repitition and can be applied for X11 and Windows (cross platform).

History

The first version of this article is from 10. November 2015.

License

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


Written By
Team Leader Celonis SA
Germany Germany
I am currently the CEO of Symbioworld GmbH and as such responsible for personnel management, information security, data protection and certifications. Furthermore, as a senior programmer, I am responsible for the automatic layout engine, the simulation (Activity Based Costing), the automatic creation of Word/RTF reports and the data transformation in complex migration projects.

The main focus of my work as a programmer is the development of Microsoft Azure Services using C# and Visual Studio.

Privately, I am interested in C++ and Linux in addition to C#. I like the approach of open source software and like to support OSS with own contributions.

Comments and Discussions

 
Praisewholy mogoly! Pin
John Torjo10-Nov-15 0:50
professionalJohn Torjo10-Nov-15 0:50 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.