計算機博弈之程序界面開發-基於C#語言和.Net Framework

        之前的博客中,介紹瞭如何基於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 && ptx<left+m_len && pty>top && pty<top+m_len)
                {
                    r = rci / 4;
                    c = rci % 4;
                    return true;
                }
            }

            return false;
        }

        private void PictureBox1_MouseDown(object sender, MouseEventArgs e)
        {
            if (PointtoRowCol(e.X, e.Y, ref m_sttrow, ref m_sttcol))
            {
                if (m_chess.GetPawn(m_sttrow, m_sttcol) != 0)
                {
                    m_sel = m_chess.GetPawn(m_sttrow, m_sttcol);
                    m_ptClick.X = e.X;
                    m_ptClick.Y = e.Y;
                    m_ptMove = m_ptClick;
                    Invalidate();
                }
            }
        }

        private void PictureBox1_MouseMove(object sender, MouseEventArgs e)
        {
            if (m_sel != 0)
            {
                if (PointtoRowCol(e.X, e.Y, ref m_endrow, ref m_endcol))
                {
                    m_ptMove.X = e.X;
                    m_ptMove.Y = e.Y;
                    Invalidate();
                }
            }
        }

        private void PictureBox1_MouseUp(object sender, MouseEventArgs e)
        {
            if (m_sel != 0)
            {
                if (PointtoRowCol(e.X, e.Y, ref m_endrow, ref m_endcol))
                {
                    m_chess.move(m_sttrow, m_sttcol, m_endrow, m_endcol);
                }

                m_sel = 0;

                m_sttrow = -1;
                m_sttcol = -1;

                Invalidate();

                int borw=0;
                if (m_chess.IsEnd(ref borw))
                {
                    if (-1 == borw)
                    {
                        MessageBox.Show("黑方勝!");
                    }
                    else if (1 == borw)
                    {
                        MessageBox.Show("白方勝!");
                    }
                    else
                    {
                        MessageBox.Show("和棋!");
                    }
                }
            }
        }

         最後還要添加繪製移動棋子的代碼,注意繪製棋子的代碼要添加在“ 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);

          上述步驟完成後既可以測試程序是否能正常運行。

    

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章