编写基于Windows窗体的应用软件

编译:刘彦青 人气:4170

尽管一般的客户机应用软件仍然很普及,但简单的、基于浏览器与网络交互(通常是互联网)的应用软件将日渐成为主流,这就意味着对编程的要求将由编写独立的程序向编写面向对象、模块化的程序发展。

.NET平台提倡这种变化,尤其是.NET的被称作Windows窗体的用户接口模型。软件开发人员可以使用C#或Visual Basic 7.0来创建Windows窗体,在这篇文章中我将介绍如何使用C#创建.NET平台上基于Windows窗体的用户界面。

我们首先来看看Windows窗体是怎么回事,然后再讨论如何用C#编写基于Windows窗体的应用程序。在这种方式下,所有的Windows应用程序的工作方式都类似,Windows都需要维护许多Windows类。在早期的Windows程序开发中,软件开发人员最大的任务是保证一个80多行的模板文件能够正确地运行,然后可以逐步地添加事件处理程序。使用MFC,开发人员无需自己编写WinMain()和WndProc()函数,Windows窗体则发展了这一趋势,使得软件开发人员无需在编写枯燥的代码上花费太多的时间。

用SDK和MFC开发软件,仍然与Windows API非常类似。如果要对开发的软件进行严格的控制,使用C和SDK是最好的选择;如果需要相当的灵活性,使用SDK或MFC仍然可以进行有效地开发,但如果简单、直观的开发环境比控制性或灵活性更为重要时,使用Windows窗体开发基于窗体的应用程序就比较合适了。

编写客户端代码

使用Windows窗体,可以编写.NET平台上的客户端程序。如果你曾经使用过VB,可能会对基于窗体的应用程序比较熟悉,就会发现Windows窗体与VB中的窗体有点类似。使用SDK或SFC编程需要直接与Windows API打交道,即使使用象MFC这样的构架,也只是比使用Windows API有了一点小小的进步。Windows窗体则隐藏了传统的Windows编程方式中的模板文件的许多细节,而以一种带有菜单和标题的窗口的方式出现,它可以对鼠标运动、菜单选择等一般的计算机的事件作出反应,还可以在客户机程序区中显示各种对象,实现这些功能的代码将比采用SDK甚至MFC时所需的代码抽象得多。

在应用程序中我们可以将Windows窗体显示为标准的窗口、MDI窗口、对话框、图形程序界面。与使用VB编程一样,定义Windows窗体用户界面也是在窗体的客户机程序区中放置各种控制,但Windows窗体可以更好的显示在界面上定义的各种对象。

除显示各种对象和管理标准控制外,Windows窗体还可以通过属性定义它们的外表。例如,如果要通过编程的方法在屏幕上移动Windows窗体,就可以通过设置它的X属性来实现。Windows窗体通过方法来实现各种操作,它还对各种事件作出反应,实现与用户之间的交互。

Windows窗体是在.NET构架或通用语言运行库(CLR)中运行的类的实例。编写一个基于Windows窗体的应用程序通常也就是对WinForm类的一个实例进行初始化、设置其属性,并建立相关的事件处理程序。由于Windows窗体是一个普通的基于CLR的类,完全支持对象的继承,因此可以在编程中使用标准的、面向对象的方法实现对基于Windows窗体的类的继承。

从Tic-Tac-Toe游戏学起

一个基于Windows窗体的应用软件在开始处都需要通过一系列的using语句导入必要的描述(程序所要求的类型定义):

namespace CSharpTicTacToe {
    using System;
    using System.Drawing;
    using System.Drawing.Drawing2D;
    using System.WinForms;
};

第一个namespace关健字是可选的,但对定义函数的作用域非常有用;后面的每个using关健字都通知C#编译器程序正在使用哪能个系统工具。因为tic-tac-toe游戏是一个Windows窗体,源代码文件使用了System中的WinForms名字空间;由于游戏还使用了图形,源代码文件还使用了URT工具。

在调用名字空间后,就通过继承系统提供的Form类得到了一个C#语言中的Windows窗体:

public class CSharpTicTacToe : Form {
    // 这里是Windows窗体的代码,包括数据、构造器和一些事件处理程序
}

在定义变量类型时,C#提倡使用枚举方式。tic-tac-toe游戏使用了三个枚举类型的变量:玩家类型、分数类型和位置名字类型,下面是这些枚举数据类型的定义:

public enum Player {
    XPlayer,
    OPlayer
}

public enum Mark {
    XMark,
    OMark,
    Blank
}

public enum Positions {
    TopLeft,
    TopCenter,
    TopRight,
    MiddleLeft,
    MiddleCenter,
    MiddleRight,
    BottomLeft,
    BottomCenter,
    BottomRight,
    Unknown
}

定义窗体时,它还需要一些数据、一个构造器和一些事件处理程序,我们将依次讨论这些问题。首先来看看窗体所需要的数据,tic-tac-toe游戏所需要的数据包括一个描述游戏中棋盘所需要的3X3的数组。(如图1所示)这个游戏使用的是一个简单的棋盘,每一个方格都需要维护它在屏幕上的位置以及是否已经有玩家占领了它,此外,根据是哪个玩家占领了它,还要在方格中显示“X”或“0”。

public struct BoardSpace {
    public BoardSpace(Mark mark, int left, int top, int right, int bottom) {
    }

    public void SetMark(Player player) {
        // 如果方格是空的,在其中填上玩家的代号 
    }

    public void Render(Graphics g) {
        Pen pen = new Pen(Color.FromARGB(170, Color.Black), 3);

        switch (m_mark) {
            case Mark.XMark:
                g.DrawLine(pen, m_left, m_top, m_right,
                m_bottom);
                g.DrawLine(pen, m_left, m_bottom, m_right,
                m_top);
                break;
            case Mark.OMark:
                int cx = m_right - m_left;
                int cy = m_bottom - m_top;
                g.DrawEllipse(pen, m_left, m_top, cx, cy);
                break;
            default:
                break;
        }
    }

    public Mark m_mark;
    public int m_top, m_left, m_right, m_bottom;
};

tic-tac-toe游戏需要管理3X3的方格数组(如图2所示),同时还管理一个3X3的BoardSpace对象组,刷新划分tic-tac-toe方格的线条,并要求每个方格刷新自己。由于这个游戏的逻辑主要与棋盘有关,因此这个游戏主要就是随着数据、鼠标的活动创建这个棋盘的一种形式。图3中包括了基于Windows窗体的程序所需要的初始化代码。需要注意的是,游戏初始化了棋盘、创建了一个Reset按键和事件处理程序,而且还可以截获MouseDown和Paint事件。

public struct TicTacToeBoard {
    BoardSpace[,] m_BoardSpaces;

    public void Initialize() {
        m_BoardSpaces = new BoardSpace[3, 3];
        // 初始化每个方格在屏幕上的位置,并把方格的状态初始化为空  
        // 第一个方格: 
        m_BoardSpaces[0, 0] = new BoardSpace(Mark.Blank, 1,
        1, 50, 50);
        // 其余的以此类推 
    }

    public void ClearBoard() {
        // 清空所有的方格 
    }

    public Player EvaluateGame() {
        // 检查邻近的方格并检查是哪个玩家占着它  
    }

    public Positions HitTest(int x, int y, Player player) {
        // 检查得到的坐标,并用玩家的代码对正确的方格进行标识 
    }

    public void Render(Graphics g) {
        Pen pen = new Pen(Color.FromARGB(170, Color.Black), 5);
        g.DrawLine(pen, 1, 50, 150, 50);
        g.DrawLine(pen, 50, 1, 50, 150);
        g.DrawLine(pen, 1, 100, 150, 100);
        g.DrawLine(pen, 100, 1, 100, 150);
        for (int i = 0; i < 3; i++) {
            for (int j = 0; j < 3; j++) {
                m_BoardSpaces[i, j].Render(g);
            }
        }
    }
}

public class CSharpTicTacToe : Form {
    public Player m_Player = Player.XPlayer;
    TicTacToeBoard m_board = new TicTacToeBoard();

    public CSharpTicTacToe() {
        SetStyle(ControlStyles.Opaque, true);
        Size = new Size(500, 500);
        Text = "CSharp Tic Tac Toe";
        m_board.Initialize();
        //Finally add a button so that we can render to a bitmap 
        Button buttonRestart = new Button();
        buttonRestart.Size = new Size(100, 50);
        buttonRestart.Location = new Point(300, 100);
        buttonRestart.Text = "Restart";
        buttonRestart.AddOnClick(new EventHandler(Restart));
        this.Controls.Add(buttonRestart);
    }

    private void Restart(object sender, EventArgs e) {
        m_Player = Player.XPlayer;
        m_board.ClearBoard();
        this.Invalidate();
    }

    protected override void OnMouseDown(MouseEventArgs e) {
        base.OnMouseDown(e);
        Positions position = m_board.HitTest(e.X, e.Y, m_Player);
        if (position == Positions.Unknown) {
            return;
        }

        if (m_Player == Player.XPlayer) {
            m_Player = Player.OPlayer;
        }
        else {
            m_Player = Player.XPlayer;
        }
        this.Invalidate();
    }

    protected override void OnPaint(PaintEventArgs e) {
        Graphics g = e.Graphics;
        e.Graphics.SmoothingMode = SmoothingMode.AntiAlias;
        g.FillRectangle(new SolidBrush(Color.FromARGB(250, Color.White)), ClientRectangle);
        m_board.Render(g);
    }

    public static void Main() {
        Application.Run(new CSharpTicTacToe());
    }
}

在大多数时间,对事件作出反应就是覆盖相应的函数。例如,该游戏对MouseDown和Paint事件作出反应,在系统产生事件后,就会自动地调用相应的函数。我们需要人工在系统中挂上nonsystem以及按下鼠标键等事件的处理函数,此外它还创建了一个清除棋盘的函数的按钮。

Windows窗体的编程是基于UI的,要求对屏幕进行刷新。Windows窗体可以很好地处理WM_PAINT消息。Form类中包括一个可以覆盖的名字为OnPaint()的函数,通过覆盖这个函数,就可以截获绘图事件,在屏幕上显示相应的内容。仔细看一下例子中的源代码,就会发现Paint事件处理程序的参数中包括一个图像对象,图像对象中包括绘制线条和图形、填充区域和你希望的在屏幕上显示图形的功能。

tic-tac-toe游戏通过要求棋盘重新进行刷新作为对Paint事件的响应。如果仔细地研究一下例子程序中的TicTacToeBoard类和BoardSpace类,就会发现它们都有一个使用Graphics对象的DrawLine()和DrawEllipse()方法在屏幕上显示图形的Render()函数。Windows窗体和C#最为方便之处是开发人员无需关心GDI类的资源,这些工作都由.NET Framework来完成。

Windows窗体还具有许多别的功能,包括在窗体上添加菜单和图标,显示对话框,截取Paint和MouseDown之外的其他事件,在以后的文章中,我们将逐步地介绍这方面的知识。

Contributors: FHL