Download XamlMines_X11_32.zip Mono solution including full source and executable
Download XamlMines_X11_64.zip Mono solution including full source and executable
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.
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.
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.
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.
The fifth image shows the warning dialog on OPEN SUSE 12.3 Linux 64 bit DE and Xfce.
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.
The game shows an "initial" state.
The seventh image shows the game's empty score list on Windows® 8.1 64 Bit Edition.
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.DrawingImage
s.
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).
<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.DrawingImage
s. Here are two examples, the elaborate ExplodedMine
and the easy 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>
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:
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.
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.
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.
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.
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.
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:
<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
.
private void HandleMouseLeave (object sender, System.Windows.Input.MouseEventArgs e)
{
this.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FFDDDDDD"));
}
private void HandleMouseEnter (object sender, System.Windows.Input.MouseEventArgs e)
{
this.BorderBrush = new SolidColorBrush((Color)ColorConverter.ConvertFromString("#FF888888"));
}
private void HandleMouseUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
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;
if (e.ChangedButton == System.Windows.Input.MouseButton.Left)
{
if (me.imageBoardCell.Source != (ImageSource)me.FindResource(MainWindow.Untouched) &&
me.imageBoardCell.Source != (ImageSource)me.FindResource(MainWindow.MarkedMine))
return;
if (HasMine)
{
me.imageBoardCell.Source = (ImageSource)me.FindResource(MainWindow.ExplodedMine);
w.GameStateGood = false;
me.InvalidateVisual();
}
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();
if (neighbours == 0)
{
List<object> nextUnhidableNeighbour = w.NextUnhidableNeighbour(me);
foreach (object o in nextUnhidableNeighbour)
HandleMouseUp(o, e);
}
}
}
else if (e.ChangedButton == System.Windows.Input.MouseButton.Right)
{
if (me.imageBoardCell.Source != (ImageSource)me.FindResource(MainWindow.Untouched) &&
me.imageBoardCell.Source != (ImageSource)me.FindResource(MainWindow.MarkedMine))
return;
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();
}
else
{
w.UndetectedMines += 1;
me.imageBoardCell.Source = (ImageSource)me.FindResource(MainWindow.Untouched);
me.InvalidateVisual();
}
}
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.