C# WPF 自制扫雷小游戏
By
jerryxjr1220
at 2023-10-08 • 0人收藏 • 566人看过

用C# WPF开发的练手小项目
<hc:GlowWindow x:Class="WPFMine.Views.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:vm="clr-namespace:WPFMine.ViewModels"
xmlns:hc="https://handyorg.github.io/handycontrol"
d:DataContext="{d:DesignInstance Type=vm:MainViewModel}"
mc:Ignorable="d"
Title="Mine Game" Height="490" Width="410" ResizeMode="NoResize"
FontFamily="JetBrains Mono"
FontSize="{StaticResource TextFontSize}">
<Window.Resources>
</Window.Resources>
<hc:SimplePanel>
<ScrollViewer x:Name="scrollViewer" VerticalScrollBarVisibility="Auto">
<StackPanel>
<WrapPanel HorizontalAlignment="Center" Margin="0,10, 0, 2">
<TextBox Text="{Binding TimeUsed, StringFormat={}{0:mm:ss}}" FontSize="20" Foreground="DarkRed"
Margin="10,0" />
<Button Style="{StaticResource ButtonIcon}"
hc:IconElement.Geometry="{StaticResource ClockGeometry}" Foreground="DarkBlue" />
<TextBox Text="{Binding RemainingMines}" FontSize="20" Foreground="DarkRed" Margin="10,0" />
</WrapPanel>
<Grid>
<ItemsControl x:Name="itemscontrol" Width="400" Height="400" Background="LightGray"
ItemsSource="{Binding GameObjects}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding XPosActual}" />
<Setter Property="Canvas.Top" Value="{Binding YPosActual}" />
</Style>
</ItemsControl.ItemContainerStyle>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border BorderBrush="DarkGray" BorderThickness="1" Width="40" Height="40"
HorizontalAlignment="Center" VerticalAlignment="Center">
<Border x:Name="OuterBorder" Background="WhiteSmoke" BorderThickness="1"
BorderBrush="WhiteSmoke" Width="35" Height="35">
<Border.RenderTransform>
<TranslateTransform X="-2" Y="-1" />
</Border.RenderTransform>
<ToggleButton x:Name="MineCanvas" Style="{StaticResource ToggleButtonDefault}"
FontSize="12" BorderThickness="0"
Width="35" Height="35" IsChecked="False"
Click="MineCanvas_OnClick"
MouseWheel="MineCanvas_OnMouseWheel" />
</Border>
</Border>
<DataTemplate.Triggers>
<DataTrigger Binding="{Binding GameObjectType}" Value="0">
<Setter TargetName="MineCanvas" Property="Background" Value="LightGray" />
</DataTrigger>
<DataTrigger Binding="{Binding GameObjectType}" Value="1">
<Setter TargetName="MineCanvas" Property="Background" Value="LightGray" />
<Setter TargetName="MineCanvas" Property="Foreground" Value="Blue" />
<Setter TargetName="MineCanvas" Property="Content" Value="1" />
</DataTrigger>
<DataTrigger Binding="{Binding GameObjectType}" Value="2">
<Setter TargetName="MineCanvas" Property="Background" Value="LightGray" />
<Setter TargetName="MineCanvas" Property="Foreground" Value="Green" />
<Setter TargetName="MineCanvas" Property="Content" Value="2" />
</DataTrigger>
<DataTrigger Binding="{Binding GameObjectType}" Value="3">
<Setter TargetName="MineCanvas" Property="Background" Value="LightGray" />
<Setter TargetName="MineCanvas" Property="Foreground" Value="Chocolate" />
<Setter TargetName="MineCanvas" Property="Content" Value="3" />
</DataTrigger>
<DataTrigger Binding="{Binding GameObjectType}" Value="4">
<Setter TargetName="MineCanvas" Property="Background" Value="LightGray" />
<Setter TargetName="MineCanvas" Property="Foreground" Value="Red" />
<Setter TargetName="MineCanvas" Property="Content" Value="4" />
</DataTrigger>
<DataTrigger Binding="{Binding GameObjectType}" Value="5">
<Setter TargetName="MineCanvas" Property="Background" Value="LightGray" />
<Setter TargetName="MineCanvas" Property="Foreground" Value="Cyan" />
<Setter TargetName="MineCanvas" Property="Content" Value="5" />
</DataTrigger>
<DataTrigger Binding="{Binding GameObjectType}" Value="6">
<Setter TargetName="MineCanvas" Property="Background" Value="LightGray" />
<Setter TargetName="MineCanvas" Property="Foreground" Value="Brown" />
<Setter TargetName="MineCanvas" Property="Content" Value="6" />
</DataTrigger>
<DataTrigger Binding="{Binding GameObjectType}" Value="7">
<Setter TargetName="MineCanvas" Property="Background" Value="LightGray" />
<Setter TargetName="MineCanvas" Property="Foreground" Value="NavajoWhite" />
<Setter TargetName="MineCanvas" Property="Content" Value="7" />
</DataTrigger>
<DataTrigger Binding="{Binding GameObjectType}" Value="8">
<Setter TargetName="MineCanvas" Property="Background" Value="LightGray" />
<Setter TargetName="MineCanvas" Property="Foreground" Value="Black" />
<Setter TargetName="MineCanvas" Property="Content" Value="8" />
</DataTrigger>
<DataTrigger Binding="{Binding GameObjectType}" Value="9">
<Setter TargetName="MineCanvas" Property="Background" Value="LightGray" />
<Setter TargetName="MineCanvas" Property="Foreground" Value="OrangeRed" />
<Setter TargetName="MineCanvas" Property="Content" Value="X" />
</DataTrigger>
<DataTrigger Binding="{Binding GameObjectType}" Value="10">
<Setter TargetName="MineCanvas" Property="Background" Value="LightGray" />
<Setter TargetName="MineCanvas" Property="Foreground" Value="GreenYellow" />
<Setter TargetName="MineCanvas" Property="Content" Value="V" />
</DataTrigger>
<DataTrigger Binding="{Binding IsClicked}" Value="false">
<Setter TargetName="MineCanvas" Property="Opacity" Value="0" />
<Setter TargetName="MineCanvas" Property="BorderThickness" Value="0" />
</DataTrigger>
<DataTrigger Binding="{Binding IsClicked}" Value="true">
<Setter TargetName="MineCanvas" Property="Opacity" Value="1" />
<Setter TargetName="MineCanvas" Property="BorderThickness" Value="0" />
<Setter TargetName="MineCanvas" Property="Background" Value="FloralWhite" />
<Setter TargetName="OuterBorder" Property="BorderThickness" Value="0" />
<Setter TargetName="OuterBorder" Property="Background" Value="FloralWhite" />
</DataTrigger>
</DataTemplate.Triggers>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
</StackPanel>
</ScrollViewer>
</hc:SimplePanel>
</hc:GlowWindow>using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
namespace WPFMine.Models;
public partial class GameObject : ObservableObject
{
public enum ObjectType
{
Blank,
N1,
N2,
N3,
N4,
N5,
N6,
N7,
N8,
Mine,
Flag
}
[ObservableProperty] private int _blockSize;
[ObservableProperty] private ObjectType _gameObjectType;
[ObservableProperty] private bool _isClicked;
[ObservableProperty] private int _xPos;
[ObservableProperty] private int _xPosActual;
[ObservableProperty] private int _yPos;
[ObservableProperty] private int _yPosActual;
public GameObject()
{
}
public GameObject(string _type, int _x, int _y, int _blockSize = 40)
{
switch (_type)
{
case "B":
GameObjectType = ObjectType.Blank;
break;
case "1":
GameObjectType = ObjectType.N1;
break;
case "2":
GameObjectType = ObjectType.N2;
break;
case "3":
GameObjectType = ObjectType.N3;
break;
case "4":
GameObjectType = ObjectType.N4;
break;
case "5":
GameObjectType = ObjectType.N5;
break;
case "6":
GameObjectType = ObjectType.N6;
break;
case "7":
GameObjectType = ObjectType.N7;
break;
case "8":
GameObjectType = ObjectType.N8;
break;
case "M":
GameObjectType = ObjectType.Mine;
break;
case "F":
GameObjectType = ObjectType.Flag;
break;
}
XPos = _x;
YPos = _y;
BlockSize = _blockSize;
XPosActual = XPos * BlockSize;
YPosActual = YPos * BlockSize;
IsClicked = false;
}
protected override void OnPropertyChanged(PropertyChangedEventArgs e)
{
base.OnPropertyChanged(e);
if (e.PropertyName == "XPos") XPosActual = XPos * BlockSize;
if (e.PropertyName == "YPos") YPosActual = YPos * BlockSize;
}
}using System;
using System.ComponentModel;
using CommunityToolkit.Mvvm.ComponentModel;
using WPFMine.Models;
namespace WPFMine.ViewModels;
public partial class MainViewModel : ObservableObject
{
[ObservableProperty] private int _blockSize = 40;
[ObservableProperty] private BindingList<GameObject> _gameObjects;
[ObservableProperty] private int _remainingMines;
[ObservableProperty] private DateTime _timeUsed;
public int[,] MineCountMap;
public string[,] MineMap;
public MainViewModel()
{
MineMap = new string[10, 10];
MineCountMap = new int[10, 10];
GameObjects = new BindingList<GameObject>();
GameInit();
//发送消息
//WeakReferenceMessenger.Default.Send(new CustomizedMessage(new MessageModel()));
}
//游戏初始化
public void GameInit()
{
MineMap = new string[10, 10];
MineCountMap = new int[10, 10];
for (var x = 0; x < 10; x++)
for (var y = 0; y < 10; y++)
{
MineMap[x, y] = "B";
MineCountMap[x, y] = 0;
}
//设置地雷数
RemainingMines = 10;
for (var i = 0; i < RemainingMines; i++)
{
(var x, var y) = GenerateMine();
MineMap[x, y] = "M";
if (x > 0 && y > 0) MineCountMap[x - 1, y - 1]++;
if (x > 0 && y < 9) MineCountMap[x - 1, y + 1]++;
if (x < 9 && y > 0) MineCountMap[x + 1, y - 1]++;
if (x < 9 && y < 9) MineCountMap[x + 1, y + 1]++;
if (x > 0) MineCountMap[x - 1, y]++;
if (x < 9) MineCountMap[x + 1, y]++;
if (y > 0) MineCountMap[x, y - 1]++;
if (y < 9) MineCountMap[x, y + 1]++;
}
TimeUsed = new DateTime(3000, 1, 1, 0, 0, 0);
GameObjects = new BindingList<GameObject>();
for (var x = 0; x < 10; x++)
for (var y = 0; y < 10; y++)
if (MineMap[x, y] == "M")
GameObjects.Add(new GameObject("M", x, y, BlockSize));
else if (MineMap[x, y] == "B" && MineCountMap[x, y] > 0)
GameObjects.Add(new GameObject(MineCountMap[x, y].ToString(), x, y, BlockSize));
else
GameObjects.Add(new GameObject("B", x, y, BlockSize));
}
//生成随机地雷
public (int, int) GenerateMine()
{
var random = new Random(DateTime.Now.Microsecond);
var x = random.Next(0, 10);
var y = random.Next(0, 10);
return (x, y);
}
//扫雷并判断是否触雷
public bool MakeTransparent(int x, int y)
{
var isover = false;
foreach (var go in GameObjects)
{
if (go.XPos == x && go.YPos == y && go.GameObjectType == GameObject.ObjectType.Mine)
{
go.IsClicked = true;
isover = true;
return isover;
}
if (go.XPos == x && go.YPos == y) go.IsClicked = true;
}
return isover;
}
}
//定义消息
// public class CustomizedMessage : ValueChangedMessage<MessageModel> // PropertyChangedMessage // RequestMessage ( Async + ..)
// {
// public CustomizedMessage(MessageModel value) : base(value)
// {
// }
// }using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Threading;
using HandyControl.Controls;
using WPFMine.Models;
using WPFMine.ViewModels;
using MessageBox = HandyControl.Controls.MessageBox;
namespace WPFMine.Views;
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : GlowWindow
{
private readonly MainViewModel mainViewModel;
public DispatcherTimer timer;
public MainWindow()
{
InitializeComponent();
#region 注册并接受消息
//WeakReferenceMessenger.Default.Register<CustomizedMessage>(this, (o, m) => { MessageBox.Show("Received!"); });
#endregion
mainViewModel = new MainViewModel();
DataContext = mainViewModel;
//计时器设置
timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromSeconds(1);
timer.Tick += (sender, args) => { mainViewModel.TimeUsed += TimeSpan.FromSeconds(1); };
timer.Start();
}
//点击扫雷
private void MineCanvas_OnClick(object sender, RoutedEventArgs e)
{
var point = Mouse.GetPosition(itemscontrol);
var xpos = (int)(point.X / mainViewModel.BlockSize);
var ypos = (int)(point.Y / mainViewModel.BlockSize);
foreach (var go in mainViewModel.GameObjects)
//如果已经扫过,则跳过
if (go.XPos == xpos && go.YPos == ypos && go.IsClicked)
return;
//扫雷,并判断是否游戏结束
var isover = mainViewModel.MakeTransparent(xpos, ypos);
if (isover)
{
timer.Stop();
MessageBox.Show(
$"You lose!\r\nTime used: {mainViewModel.TimeUsed.Minute} min {mainViewModel.TimeUsed.Second} sec",
"Game Over");
mainViewModel.GameInit();
timer.Start();
}
}
//标记地雷
private void MineCanvas_OnMouseWheel(object sender, MouseWheelEventArgs e)
{
var point = Mouse.GetPosition(itemscontrol);
var xpos = (int)(point.X / mainViewModel.BlockSize);
var ypos = (int)(point.Y / mainViewModel.BlockSize);
foreach (var go in mainViewModel.GameObjects)
{
//如果是地雷,且未被扫过,则标记,地雷数-1
if (go.XPos == xpos && go.YPos == ypos && go.GameObjectType == GameObject.ObjectType.Mine && !go.IsClicked)
{
go.GameObjectType = GameObject.ObjectType.Flag;
go.IsClicked = true;
mainViewModel.RemainingMines--;
break;
}
//如果不是地雷,则游戏结束
if (go.XPos == xpos && go.YPos == ypos && go.GameObjectType != GameObject.ObjectType.Mine && !go.IsClicked)
{
timer.Stop();
MessageBox.Show(
$"You lose!\r\nTime used: {mainViewModel.TimeUsed.Minute} min {mainViewModel.TimeUsed.Second} sec",
"Game Over");
mainViewModel.GameInit();
timer.Start();
}
}
//剩余地雷数为0,游戏胜利
if (mainViewModel.RemainingMines == 0)
{
timer.Stop();
MessageBox.Show(
$"You win!\r\nTime used: {mainViewModel.TimeUsed.Minute} min {mainViewModel.TimeUsed.Second} sec",
"Game Over");
mainViewModel.GameInit();
}
}
}
5 个回复 | 最后更新于 2023-10-08
2023-10-08
#3
回复#1 @admin :
修改一个MakeTransparent函数,增加一个ContinueTransparent函数
实现了一开一大片功能。

//扫雷并判断是否触雷
public bool MakeTransparent(int x, int y)
{
var isover = false;
foreach (var go in GameObjects)
{
if (go.XPos == x && go.YPos == y && go.GameObjectType == GameObject.ObjectType.Mine)
{
go.IsClicked = true;
isover = true;
return isover;
}
if (go.XPos == x && go.YPos == y && go.GameObjectType == GameObject.ObjectType.Blank && !go.IsClicked)
{
go.IsClicked = true;
ContinueTransparent(x, y);
}
if (go.XPos == x && go.YPos == y) go.IsClicked = true;
}
return isover;
}
public void ContinueTransparent(int x, int y)
{
foreach (var go in GameObjects)
{
if (Math.Abs(go.XPos - x) <= 1 && Math.Abs(go.YPos - y) <= 1 && go.GameObjectType == GameObject.ObjectType.Blank && !go.IsClicked)
{
go.IsClicked = true;
ContinueTransparent(go.XPos, go.YPos);
}
}
}
2023-10-08
#4
一开一大片连同旁边的数字一起开
//判断相邻格是否为空白,连续开启空白格及数字格
public void ContinueTransparent(int x, int y)
{
foreach (var go in GameObjects)
{
if (Math.Abs(go.XPos - x) <= 1 && Math.Abs(go.YPos - y) <= 1 && go.GameObjectType == GameObject.ObjectType.Blank && !go.IsClicked)
{
go.IsClicked = true;
ContinueTransparent(go.XPos, go.YPos);
}
if (Math.Abs(go.XPos - x) <= 1 && Math.Abs(go.YPos - y) <= 1 && go.GameObjectType != GameObject.ObjectType.Blank
&& go.GameObjectType != GameObject.ObjectType.Mine && go.GameObjectType != GameObject.ObjectType.Flag && !go.IsClicked)
{
go.IsClicked = true;
}
}
}登录后方可回帖

你这没一开一大片的,