之前的博客中,介绍了如何基于C++和MFC类库实现计算机博弈比赛中常用的程序界面,本文介绍如何基于C#语言和.Net Framework对假想棋种-肆棋进行设计开发。规则如下:4*4的棋盘上有黑白双方共8枚棋子,每方有4个棋子放置在底线,默认黑方先行,交替行棋,每次走一个棋子,每个棋子只可以选择向前、向左上、向右上前进,遇到对方棋子可以吃掉,不可以连吃。双方轮流行棋至无棋可走为终局,棋子多者为胜方,棋子相同为和局。
一、C#与.Net简介
C#是微软推出的配合.NET平台的面向对象编程语言,它吸收了Java语言和Delphi语言的优点,可以看出其语法风格和Java比较类似,还采用了类似于Java的内存回收机制,用new操作符申请空间后,不需要考虑人工释放,由系统自动回收。尽管C#语言支持C原型的API进行内部操作,但C#编译器是将源代码转化为Microsoft中间语言(MSIL),然后将其和其它数据进行连接生成exe或dll文件的,这一机制也与Java的虚拟机机制具有相似之处。
C#本身只是一种语言,需要强大的库和框架的支持才能发挥作用,而.NET平台提供了丰富的界面控件,类似于VB,这些控件的属性可以通过IDE环境直接进行设置,大大简化了编码复杂程度,特别是更好地支持了网络相关的应用开发,例如XML已经成为网络中数据结构传递的标准,为了提高效率,C#语序直接将XML数据映射为结构,这样就可以有效地处理各种数据。对于肆棋的界面开发来说,采用桌面框架就足够了,因此对Visual Studio 2019的安装组件配置如图所示即可。
相关组件配置成功后,启动visual studio 2019选择新建项目,选择windows窗体应用(.Net Framework)
然后点击“下一步”,为了与前述的基于MFC结构的例程加以区别,这里命名为MyChess2。
创建后的界面显示如下图所示
我们将Form1属性中的Text属性修改为MyChess2,如下图所示,这样程序运行时标题栏显示将为MyChess2。
接下来,从解决方案资源管理器中选中Program.cs,并在右边显示的源代码区域加入chess类的定义,其中为了方便,将对CChess的定义进行了折叠。
CChess的完整声明如下所示:
public class CChess
{
// 0为空,1为白棋,-1为黑棋
protected int [ , ] m_Board;
protected int m_cur;
public CChess() { m_Board = new int[4, 4]; }
~CChess() { }
public bool ReadfromFile(string path)
{
FileStream fs;
try
{
fs = new FileStream(path, FileMode.Open, FileAccess.Read);
BinaryReader br = new BinaryReader(fs);
try
{
int col, row;
for (row = 0; row < 4; row++)
{
for (col = 0; col < 4; col++)
{
m_Board[row, col] = br.ReadInt32();
}
}
br.Close();
}
catch (EndOfStreamException fex)
{
MessageBox.Show(fex.Message);
}
}
catch (IOException ex)
{
MessageBox.Show(ex.Message);
}
return true;
}
public bool WritetoFile(string path)
{
FileStream fs;
try
{
fs = new FileStream(path, FileMode.Create);
BinaryWriter bw = new BinaryWriter(fs);
int col, row;
for (row=0; row<4; row++)
{
for(col=0; col<4; col++)
{
bw.Write(m_Board[row, col]);
}
}
bw.Close();
}
catch(IOException ex)
{
MessageBox.Show(ex.Message);
}
return true;
}
public void Begin(int borw)
{
int col;
for (col = 0; col < 4; col++)
{
//第一行为黑棋
m_Board[0, col] = -1;
//第二行为空
m_Board[1, col] = 0;
//第三行为空
m_Board[2, col] = 0;
//第四行为白棋
m_Board[3, col] = 1;
}
m_cur = borw;
}
//棋子从(startrow, startcol)移动到(stoprow, stopcol)
//先验证移动的合法性,然后再移动改变棋局数据
public void move(int startrow, int startcol, int stoprow, int stopcol)
{
if (startrow < 0 || startrow >= 4) { return; }
if (startcol < 0 || startcol >= 4) { return; }
if (stoprow < 0 || stoprow >= 4) { return; }
if (stopcol < 0 || stopcol >= 4) { return; }
//必须移动当前方的棋子
if (m_cur != m_Board[startrow, startcol])
return;
int drow, dcol;
drow = stoprow - startrow;
dcol = stopcol - startcol;
//左右移动只有三种方式,不满足为非法移动
if ((dcol != 1) && (dcol != 0) && (dcol != -1)) { return; }
//黑棋只能向下移动
if ((m_Board[startrow, startcol] == -1) && (drow != 1)) { return; }
//白棋只能向上移动
if ((m_Board[startrow, startcol] == 1) && (drow != -1)) { return; }
//如果是合法移动,执行该移动
m_Board[stoprow, stopcol] = m_Board[startrow, startcol];
//原位置置为空
m_Board[startrow, startcol] = 0;
//交换行棋方
m_cur = -1 * m_cur;
}
//如果黑白方有一方不能走棋,则棋局结束,win的值为1白方胜,win的值为-1
//黑方胜,win的值为0,双方和棋
public bool IsEnd(ref int win)
{
bool blackcango = false;
bool whitecango = false;
int blackcnt = 0;
int whitecnt = 0;
//检查是否能够走棋,同时统计黑白棋子数目
int row, col;
for (row = 0; row < 4; row++)
{
for (col = 0; col < 4; col++)
{
//黑棋
if (-1 == m_Board[row, col])
{
if (!blackcango)
{
if ((row + 1) < 4)
{
//不能吃己方棋子
if ((col - 1) > -1 && (m_Board[row + 1, col - 1] != -1))
blackcango = true;
if ((col + 1) < 4 && (m_Board[row + 1, col + 1] != -1))
blackcango = true;
if (m_Board[row + 1, col] != -1)
blackcango = true;
}
}
blackcnt++;
}
if (1 == m_Board[row, col])
{
if (!whitecango)
{
if ((row - 1) > -1)
{
//不能吃己方棋子
if ((col - 1) > -1 && (m_Board[row - 1, col - 1] != 1))
whitecango = true;
if ((col + 1) < 4 && (m_Board[row - 1, col + 1] != 1))
whitecango = true;
if (m_Board[row - 1, col] != 1)
whitecango = true;
}
}
whitecnt++;
}
}
}
if (blackcnt == whitecnt)
win = 0;
else if (blackcnt < whitecnt)
win = 1;
else
win = -1;
if (blackcango && whitecango)
return false;
else
return true;
}
public int GetPawn(int row, int col) { return m_Board[row, col]; }
};
加入CChess的声明后,编译运行,此时看到的是一个空空如也的窗体,但标题已经变为MyChess2。后面需要进一步加入其它控件使其完整。
二、添加菜单
添加完毕CChess的声明并编译测试完毕后,在Form1.cs上右键选择查看设计器,切换到显示Form1控件的窗口。
从右侧的工具箱中拖动MenuStrip到左边窗体上,这样就为主窗体添加了菜单。
加入菜单控件后,我们能看到主窗体上出现了可以编辑的菜单空项,为其添加内容,在添加时,其属性中的Name项内容会自动变为添加的中文名字加英文字母的组合,这种不利于后期基于Name属性分辨各菜单项内容。因此对其统一规范化命名。
添加完成后的菜单如下图所示
各菜单项对应name属性如下表所示,其中Name属性含有MENU的为下拉菜单项:
菜单项 |
Name属性 |
菜单项 |
Name属性 |
棋局 |
FILEMENU |
先手 |
OFFENMENU |
开局 |
FILENEW |
黑方 |
BLACKSIDE |
打开 |
FILEOPEN |
白方 |
WHITESIDE |
保存 |
FILESAVE |
帮助 |
HELPMENU |
另存为 |
FILESAVEAS |
使用说明 |
MANUAL |
添加完成后上述内容后,需要为各个菜单项添加响应,根据上面表格,共有七个菜单项需要添加相应的代码,以开局为例,如下为添加响应代码的过程。
选中“开局”菜单项,点击属性栏中的“事件”按钮,切换到事件列表显示
然后双击Click按钮,可以看到编程环境为我们生成了一个FILENEW_Click事件的响应,鼠标跳转到Form1.cs的代码设计界面,提示在响应事件中加入代码,其它菜单项的响应代码添加步骤与“开局”菜单项相同。注意我们希望菜单项“黑方”一开始就被选中,因此需要将其属性设置为“Checked”。
实际上我们注意到在响应FILEOPEN,FILESAVE的时候,我们还需要用到打开保存文件对话框,所以我们需要先为程序引入这两个控件,如图所示, 我们从工具箱拖动两个控件到主窗体。
我们期望保存的棋盘文件具有固定后缀,因此分别对openFileDialog1和saveFileDialog1进行属性设置,如下图所示:
设置完两个控件属性后,完成菜单项的响应代码如下所示,可以看到我们将CChess定义的变量m_chess定义为Form1的成员变量,这是因为我们所有操作都是在Form1类内执行的。ShellExcute语句用于完成调用系统当前关联程序打开说明文档“肆棋规则.rtf”,注意由于没有使用绝对路径,该文件应放置在和exe文件同一目录下。但编译时ShellExecute并不能被正确通过,因为它是一个win32的API,需要用import指令引入才能使用。
namespace MyChess2
{
public partial class Form1 : Form
{
CChess m_chess = new CChess();
int m_icurside;
string m_filepath;
public Form1()
{
InitializeComponent();
}
private void FILENEW_Click(object sender, EventArgs e)
{
m_icurside = -1;
m_filepath = "";
m_chess.Begin(m_icurside);
}
private void FILEOPEN_Click(object sender, EventArgs e)
{
if (openFileDialog1.ShowDialog() == DialogResult.OK)
{
m_filepath = openFileDialog1.FileName;
m_chess.ReadfromFile(m_filepath);
}
}
private void FILESAVE_Click(object sender, EventArgs e)
{
if (m_filepath == null)
return;
if (m_filepath == "")
{
FILESAVEAS_Click(sender, e);
return;
}
m_chess.WritetoFile(m_filepath);
}
private void FILESAVEAS_Click(object sender, EventArgs e)
{
if (saveFileDialog1.ShowDialog() == DialogResult.OK)
{
m_filepath = saveFileDialog1.FileName;
m_chess.WritetoFile(m_filepath);
}
}
private void BLACKSIDE_Click(object sender, EventArgs e)
{
m_icurside = -1;
BLACKSIDE.Checked = true;
WHITESIDE.Checked = false;
}
private void WHITESIDE_Click(object sender, EventArgs e)
{
m_icurside = 1;
BLACKSIDE.Checked = false;
WHITESIDE.Checked = true;
}
private void MANUAL_Click(object sender, EventArgs e)
{
ShellExecute(IntPtr.Zero, "open", @"肆棋规则.rtf", "", "", ShowCommands.SW_SHOWNORMAL);
}
}
}
为了使用ShellExecute,首先要加入using System.Runtime.InteropServices的语句
其次我们还需要在MANUAL_Click消息响应函数前加入一些相关定义,代码如下所示:
public enum ShowCommands : int
{
SW_HIDE = 0,
SW_SHOWNORMAL = 1,
SW_NORMAL = 1,
SW_SHOWMINIMIZED = 2,
SW_SHOWMAXIMIZED = 3,
SW_MAXIMIZE = 3,
SW_SHOWNOACTIVATE = 4,
SW_SHOW = 5,
SW_MINIMIZE = 6,
SW_SHOWMINNOACTIVE = 7,
SW_SHOWNA = 8,
SW_RESTORE = 9,
SW_SHOWDEFAULT = 10,
SW_FORCEMINIMIZE = 11,
SW_MAX = 11
}
[DllImport("shell32.dll")]
static extern IntPtr ShellExecute(
IntPtr hwnd,
string lpOperation,
string lpFile,
string lpParameters,
string lpDirectory,
ShowCommands nShowCmd);
private void MANUAL_Click(object sender, EventArgs e)
{
ShellExecute(IntPtr.Zero, "open", @"肆棋规则.rtf", "", "", ShowCommands.SW_SHOWNORMAL);
}
上述代码完成后,编译运行,可以看到程序已经具备了打开保存棋局功能,先手方也可以在“黑方”“白方”之前切换了,另外还可以查看规则说明,这些功能都已经实现了。
三、添加工具栏
可以想到工具栏也是通过引入控件的方式实现的,而且我们前述已经通过为菜单项添加代码实现了主要功能,因此在添加工具栏的过程中,我们只需要设置好控件属性,并将前面的响应代码绑定到工具栏按钮上。
首先,我们将工具栏控件拖到主窗体上。
由于我们要添加的功能用按钮就可以实现,因此为工具栏添加按钮控件,即选择第一项
选择Button后,在属性栏中设置Name属性,例如开局设置为TOOLFILENEW
图像属性也需要设置,但由于没有默认的图标资源,因此我们需要从外部导入
点击Image后面的...按钮,打开选择资源对话框
点击导入(M)按钮,找到合适的资源,这些图片资源可以从网上搜集或是其它程序中截图保存得到,导入的文件格式可以是gif,jpg,bmg,png,这些都是常见的图像格式。
导入后工具栏上图标就能够被显示出来,我们再为其绑定响应函数,这样工具栏按钮的添加就完成了。
实际运行,如果感觉图标过小,可以调整工具栏控件的ImageScalingSize属性,默认值为16,16,我们将其修改为了24,24,这样看起来更协调一些。
参照菜单项,选择好合适的图像,设置完工具栏的各项属性后,程序界面如下所示:
但是我们注意到由于工具栏中白方、黑方的Name属性设置为TOOLBLACKSIDE和TOOLWHITESIDE,和前面的菜单项不同,所以我们应该将前面的BLACKSIDE_Click和WHITESIDE_Click完善一下,具体如下:
private void BLACKSIDE_Click(object sender, EventArgs e)
{
m_icurside = -1;
BLACKSIDE.Checked = true;
WHITESIDE.Checked = false;
TOOLBLACKSIDE.Checked = true;
TOOLWHITESIDE.Checked = false;
}
private void WHITESIDE_Click(object sender, EventArgs e)
{
m_icurside = 1;
BLACKSIDE.Checked = false;
WHITESIDE.Checked = true;
TOOLBLACKSIDE.Checked = false;
TOOLWHITESIDE.Checked = true;
}
至此,工具栏按钮的功能和菜单项能够对应,工具栏的相关工作基本完成。
四、图形绘制
通过前面的工作,我们已经将基本的功能添加到了程序框架之中,但是此时棋盘还没有显示出来,要想实现棋盘的显示,需要使用C#支持的Graphics类来完成,由前述可见,在.net框架下,大部分功能都是通过引入控件来实现的,因此图形绘制也需要指定一个控件,在其上完成。具体过程如下,首先从工具箱中拖放一个PictureBox控件到主窗体。适当调整主窗体和picturebox的大小,我们注意到在不使用鼠标的情况下并没有合适的消息触发绘制,而在开局、打开等操作之后每次都调用绘制函数看起来似乎不是一个好的解决办法,为此我们使用定时器来触发消息,在该消息的响应函数中根据变量m_chess中的值对棋子进行绘制。因此还需要为主窗体添加一个定时器控件。
timer1的属性设置为50,将Enable标志位置为TRUE。即每隔50ms触发一次 然后按照之前添加消息响应函数的方式,为该定时器添加Timer1_Tick响应函数。
在代码方面,为了使用绘图函数,我们需要定义一些与绘图相关的成员变量以及和棋盘有关的参数,如下所示,我们需要加入一些变量的定义。
CChess m_chess = new CChess();
int m_icurside;
string m_filepath;
//棋盘参数
int m_deltax;
int m_deltay;
int m_len;
//绘图工具
SolidBrush brushWhite = new SolidBrush(Color.White);
SolidBrush brushBlack = new SolidBrush(Color.Black);
SolidBrush brushOrange = new SolidBrush(Color.FromArgb(252, 213, 181));
Pen penblack = new Pen(Color.Black, 3);
另外在定时器消息响应函数中加入相关代码:
private void Timer1_Tick(object sender, EventArgs e)
{
Bitmap image = new Bitmap(pictureBox1.ClientSize.Width, pictureBox1.ClientSize.Height);
// 初始化图形面板
Graphics g = Graphics.FromImage(image);
int w = pictureBox1.Width;
int h = pictureBox1.Height;
if(w>h)
{
m_deltax = (w - h)/2;
m_deltay = 0;
m_len = h / 4;
}
else
{
m_deltax = 0;
m_deltay = (h-w)/2;
m_len = w / 4;
}
//清空绘图区为白色
g.FillRectangle(brushWhite, 0, 0, w, h);
//在窗口绘图区中部绘制棋盘
int row, col;
for (row = 0; row < 4; row++)
{
for (col = 0; col < 4; col++)
{
int l = m_deltax + col * m_len;
int t = m_deltay + row * m_len;
//填充方格内部颜色
if ((row + col) % 2 == 0)
{
g.FillRectangle(brushOrange, l, t, m_len, m_len);
}
g.DrawRectangle(penblack, l, t, m_len, m_len);
int pawntype = m_chess.GetPawn(row, col);
if (pawntype != 0)
{
if (pawntype == -1)
g.FillEllipse(brushBlack, l + 5, t + 5, m_len - 10, m_len - 10);
else
g.FillEllipse(brushWhite, l + 5, t + 5, m_len - 10, m_len - 10);
g.DrawEllipse(penblack, l + 5, t + 5, m_len - 10, m_len - 10);
}
}
}
pictureBox1.CreateGraphics().DrawImage(image, 0, 0);
}
上述任务完成后运行测试程序,可以看到
如果点击开始按钮,可以看到棋子已经可以正确显示出来了。
五、鼠标交互
完成上述步骤后,最后要修改的就是引入鼠标事件的响应,我们期望能够在鼠标按键按下时拾起棋子,鼠标移动时棋子跟随移动,鼠标按键抬起时落子,如果落子合法那么执行相应的移动或是吃子操作,如果移动和操作非法,那么保持当前棋盘状态不变。要实现上述功能,首先需要添加多个成员变量如下:
//绘图工具
SolidBrush brushWhite = new SolidBrush(Color.White);
SolidBrush brushBlack = new SolidBrush(Color.Black);
SolidBrush brushOrange = new SolidBrush(Color.FromArgb(252, 213, 181));
Pen penblack = new Pen(Color.Black, 3);
//移动棋子
int m_sttrow = -1;
int m_sttcol = -1;
int m_endrow = -1;
int m_endcol = -1;
Point m_ptClick;
Point m_ptMove;
int m_sel = 0;
可以分别为三个鼠标响应事件添加相应代码,其中PointtoRowCol函数用于判断当前点点在了哪个棋盘格。
private bool PointtoRowCol(int ptx, int pty, ref int r, ref int c)
{
int rci;
for (rci = 0; rci < 16; rci++)
{
int left = m_deltax + rci % 4 * m_len;
int top = m_deltay + rci / 4 * m_len;
if (ptx>left && ptxtop && pty
最后还要添加绘制移动棋子的代码,注意绘制棋子的代码要添加在“ pictureBox1.CreateGraphics().DrawImage(image, 0, 0);”这条语句之前:
//绘制移动棋子
if (m_sel != 0)
{
int ml = m_deltax + m_sttcol * m_len + m_ptMove.X - m_ptClick.X;
int mt = m_deltay + m_sttrow * m_len + m_ptMove.Y - m_ptClick.Y;
//绘制棋子
int pawntype = m_chess.GetPawn(m_sttrow, m_sttcol);
if (pawntype != 0)
{
if (pawntype == -1)
g.FillEllipse(brushBlack, ml + 5, mt + 5, m_len - 10, m_len - 10);
else
g.FillEllipse(brushWhite, ml + 5, mt + 5, m_len - 10, m_len - 10);
g.DrawEllipse(penblack, ml + 5, mt + 5, m_len - 10, m_len - 10);
}
}
pictureBox1.CreateGraphics().DrawImage(image, 0, 0);
上述步骤完成后既可以测试程序是否能正常运行。