封面

Cover

第1篇 基础知识

第1篇 基础知识

 第1章 初识C#及其开发环境

 第2章 开始C#之旅

 第3章 变量与常量

 第4章 表达式与运算符

 第5章 字符与字符串

 第6章 流程控制语句

 第7章 数组和集合

 第8章 属性和方法

 第9章 结构和类

本篇通过对C#及其开发环境、变量与常量、表达式与运算符、字符与字符串、流程控制语句、数组和集合、属性和方法、结构和类等内容的介绍,并结合大量的图示、举例、视频等使读者快速掌握C#语言,为以后编程奠定坚实的基础。

第1章 初识C#及其开发环境

第1章
初识C#及其开发环境

视频讲解:63分钟

C#是微软公司推出的一种语法简洁、类型安全的面向对象的编程语言,开发人员可以通过它编写在.NET Framework上运行的各种安全可靠的应用程序。本书中涉及的程序都是通过Visual Studio 2015开发环境编译的,Visual Studio 2015开发环境是开发C#应用程序最好的工具。本章将详细地介绍C#语言的相关内容,并且通过图文并茂的形式介绍安装与卸载Visual Studio 2015开发环境的全过程。

通过阅读本章,您可以:

 了解C#语言的特点

 了解C#与.NET框架的关系

 掌握如何安装与卸载Visual Studio 2015开发环境

 掌握如何创建项目

 熟悉Visual Studio 2015开发环境的常用菜单栏、工具栏和面板

1.1 C#概述

视频讲解:光盘\TM\lx\1\C#概述.mp4

C#是一种面向对象的编程语言,主要用于开发可以运行在.NET平台上的应用程序。C#的语言体系都构建在.NET框架上,近几年C#呈现上升趋势,这也正说明了C#语言的简单、现代、面向对象和类型安全等特点正在被更多人所认同,而在TIOBE编程语言排行榜上,C#语言也常年排行前列。本节将详细介绍C#语言的特点以及C#与.NET的关系。

1.1.1 C#语言及其特点

C#是微软公司设计的一种编程语言,是从C和C++派生来的一种简单、现代、面向对象和类型安全的编程语言,并且能够与.NET框架完美结合。C#具有以下突出的特点:

(1)语法简洁,不允许直接操作内存,去掉了指针操作。

(2)彻底的面向对象设计,C#具有面向对象语言所应有的一切特性:封装、继承和多态。

(3)与Web紧密结合,C#支持绝大多数的Web标准,例如HTML、XML、SOAP等。

(4)强大的安全性机制,可以消除软件开发中常见的错误(如语法错误),.NET提供的垃圾回收器能够帮助开发者有效地管理内存资源。

(5)兼容性,因为C#遵循.NET的公共语言规范(CLS),从而保证能够与其他语言开发的组件兼容。

(6)灵活的版本处理技术,因为C#语言本身内置了版本控制功能,使开发人员能更加容易地开发和维护。

(7)完善的错误、异常处理机制,C#提供了完善的错误和异常处理机制,使程序在交付应用时能够更加健壮。

1.1.2 认识.NET Framework

.NET Framework是微软公司推出的完全面向对象的软件开发与运行平台。.NET Framework具有两个主要组件:公共语言运行时(Common Language Runtime, CLR)和类库。

 公共语言运行时:公共语言运行时(CLR)负责管理和执行由.NET编译器编译产生的中间语言代码(.NET程序执行原理如图1.1所示)。由于公共语言运行库的存在,解决了很多传统编译语言的一些致命缺点,如垃圾内存回收、安全性检查等。

 类库:类库我们比较好理解,就好比一个大仓库里装满了工具。类库里有很多现成的类,可以拿来直接使用。例如,文件操作时,可以直接使用类库里的IO类。

图1.1 .NET程序执行原理

1.1.3 C#与.NET框架

.NET框架是微软公司推出的一个全新的编程平台,目前的版本是4.6。C#是专门为与微软公司的.NET Framework一起使用而设计的(.NET Framework是一个功能非常丰富的平台,可开发、部署和执行分布式应用程序)。C#就其本身而言只是一种语言,尽管它是用于生成面向.NET环境的代码,但它本身不是.NET的一部分。.NET支持的一些特性,C#并不支持。而C#语言支持的另一些特性,.NET却不支持(例如运算符重载)。在安装Visual Studio 2015的同时,.NET Framework 4.6也被安装到本地计算机中。

1.1.4 C#的应用领域

在当前的主流开发语言中,C/C++一般用在底层和桌面程序;PHP等一般只是用在Web开发上;而只有C#,它几乎可用于所有领域,如在嵌入式、便携式计算机、电视、电话、手机和其他大量设备上运行。C#的用途数不胜数,它拥有无可比拟的能力。C#应用领域主要包括:

 游戏软件开发。

 桌面应用系统开发。

 交互式系统开发。

 智能手机程序开发。

 多媒体系统开发。

 网络系统开发。

 RIA应用程序(Silverlight)开发。

 操作系统平台开发。

 Web应用开发。

C#无处不在,它可应用于任何地方、任何领域,如果仔细观察,就会发现,C#就在我们身边,例如我们经常使用的飞信软件、PPTV播放器、KFC官方网站、当当网等项目都是使用C#编写的,它们的运行效果分别如图1.2~图1.5所示。

图1.2 飞信软件

图1.3 PPTV播放器

图1.4 KFC官方网站

图1.5 当当网首页

互动练习:尝试使用C#制作一个模拟双色球选号的程序。

1.2 安装与卸载Visual Studio 2015

视频讲解:光盘\TM\lx\1\02 Visual Studio 2015开发环境的安装.mp4

Visual Studio 2015是微软为了配合.NET战略推出的IDE开发环境,同时也是目前开发C#程序最新的工具,本节将对Visual Studio 2015的安装与卸载进行详细讲解。

1.2.1 安装Visual Studio 2015系统的必备条件

安装Visual Studio 2015之前,首先要了解安装Visual Studio 2015所需的必备条件,检查计算机的软硬件配置是否满足Visual Studio 2015开发环境的安装要求,具体要求如表1.1所示。

表1.1 安装Visual Studio 2015所需的必备条件

1.2.2 安装Visual Studio 2015

Visual Studio 2015(后面简称VS 2015)是微软为了配合.NET战略而推出的IDE开发环境,同时也是目前开发C#程序最新的工具,本节以VS 2015社区版的安装为例讲解具体的安装步骤。

说明

VS 2015社区版是完全免费的,其下载地址为:http://www.visualstudio.com/zh-cn/downloads/visual-studio-2015-downloads-vs。

安装VS 2015社区版的步骤如下:

(1)使用虚拟光驱软件加载下载的vs2015.rc_com_chs.iso文件,然后双击vs_community.exe文件开始安装。

(2)应用程序会自动跳转到如图1.6所示的VS 2015安装程序界面,在该界面中,单击“…”按钮可以设置VS 2015的安装路径,一般使用默认设置即可,产品默认路径为C:\Program Files\Microsoft Visual Studio 14.0,这里根据本地计算机的实际情况,将安装路径设置成了E:\Program Files\Microsoft Visual Studio 14.0;然后选择安装类型,采用“典型”即可,单击“安装”按钮,即可进入到VS 2015的安装进度界面,如图1.7所示。

图1.6 VS 2015安装程序界面

图1.7 VS 2015安装进度界面

(3)安装完成后,进入VS 2015的安装完成页,如图1.8所示。

图1.8 VS 2015的安装完成页

1.2.3 启动Visual Studio 2015

第一次启动Visual Studio 2015开发环境的步骤如下:

(1)在Visual Studio 2015的安装完成页中单击“启动”按钮,即可启动VS 2015开发环境,如图1.9所示。

说明

在安装完成界面可能会出现一个Android SDK相关的警告信息,这些警告信息不影响VS 2015开发环境的正常使用,忽略即可。

(2)在第一次启动VS 2015开发环境时,会提示使用微软的Outlook账号进行登录,也可以不进行登录,直接单击“以后再说”链接,打开VS 2015的启动界面,如图1.10所示。

图1.9 启动VS 2015

图1.10 VS 2015启动界面

(3)在图1.10中,用户可以根据自己的实际情况,选择适合自己的开发语言,这里选择的是“Visual C#开发”选项,然后单击“启动Visual Studio”按钮,即可进入VS 2015的主界面,如图1.11所示。

图1.11 VS 2015主界面

1.2.4 卸载Visual Studio 2015

如果要卸载Visual Studio 2015开发环境,可以按以下步骤进行:

(1)在Windows 7操作系统中,选择“控制面板”/“程序”/“程序和功能”命令,在打开的窗口中选择Microsoft Visual Studio Community 2015,如图1.12所示。

图1.12 添加或删除程序

(2)单击“更改”按钮,进入Microsoft Visual Studio Community 2015的维护页面,如图1.13所示,然后单击“卸载”按钮,即可卸载Visual Studio 2015。

图1.13 Microsoft Visual Studio Community 2015的维护页面

1.3 熟悉Visual Studio 2015开发环境

视频讲解:光盘\TM\lx\1\03 熟悉Visual Studio 2015开发环境.mp4

本节对Visual Studio 2015开发环境中的菜单栏、工具栏、“工具箱”窗口、“属性”窗口、“错误列表”窗口、“输出”窗口等进行介绍。

1.3.1 创建项目

初期学习C#语法和面向对象编程主要在Windows控制台应用程序环境下完成,下面将按步骤介绍控制台应用程序的创建过程。

创建控制台应用程序的步骤如下:

(1)选择“开始”/“所有程序”/Microsoft Visual Studio 2015/Visual Studio 2015命令,进入Visual Studio 2015开发环境起始页,如图1.14所示。

图1.14 Visual Studio 2015起始页

(2)启动Visual Studio 2015开发环境之后,可以通过两种方法创建项目:一种是在菜单栏中选择“文件”/“新建”/“项目”命令,如图1.15所示;另一种是在“起始页”中选择“新建项目”命令,如图1.16所示。

图1.15 在菜单栏中选择“文件”/“新建”/“项目”命令

图1.16 选择“新建项目”命令

选择以上其中一种方法创建项目,将弹出如图1.17所示的“新建项目”对话框。

图1.17 “新建项目”对话框

说明

在图1.17中选择“Windows窗体应用程序”,即可创建Windows的窗体程序。

(3)选择要使用的.NET框架和“控制台应用程序”后,用户可对所要创建的控制台应用程序进行命名、选择存放位置、是否创建解决方案目录等设定(在命名时可以使用用户自定义的名称,也可使用默认名ConsoleApplication1;用户可以单击“浏览”按钮来设置项目存放的位置;需要注意的是,解决方案名称与项目名称一定要统一),然后单击“确定”按钮,完成控制台应用程序的创建。

互动练习:尝试使用C#制作一个验证身份证号码的工具,具体要求为:当用户输入身份证号码后,单击一个按钮,即可显示该身份证号对应的所属地区、生日、性别以及该身份证号码是否合法等信息。

1.3.2 菜单栏

菜单栏显示了所有可用的Visual Studio 2015命令,除了“文件”、“编辑”、“视图”、“窗口”和“帮助”菜单之外,还提供编程专用的功能菜单,如“项目”、“生成”、“调试”、“工具”和“测试”等,如图1.18所示。

每个菜单项中都包含若干个菜单命令,分别执行不同的操作,例如,“调试”菜单包括调试程序的各种命令,如“启动调试”、“开始执行”和“新建断点”等,如图1.19所示。

图1.18 Visual Studio 2015菜单栏

图1.19 “调试”菜单

1.3.3 工具栏

为了操作更方便、快捷,菜单项中常用的命令按功能分组分别放入相应的工具栏中。通过工具栏可以快速访问常用的菜单命令。常用的工具栏有标准工具栏和调试工具栏,下面分别进行介绍。

(1)标准工具栏包括大多数常用的命令按钮,如新建项目、打开文件、启动调试、保存、全部保存等。标准工具栏如图1.20所示。

(2)调试工具栏包括对应用程序进行调试的快捷按钮,如图1.21所示。

图1.20 Visual Studio 2015标准工具栏

图1.21 Visual Studio 2015调试工具栏

说明

在调试程序或运行程序的过程中,通常可用以下4种快捷键来操作:

(1)按下F5快捷键实现调试运行程序。

(2)按下Ctrl+F5快捷键实现不调试运行程序。

(3)按下F11快捷键实现逐语句调试程序。

(4)按下F10快捷键实现逐过程调试程序。

1.3.4 解决方案资源管理器

解决方案资源管理器(如图1.22所示)提供了项目及文件的视图,并且提供对项目和文件相关命令的便捷访问。与此窗口关联的工具栏提供了适用于列表中突出显示项的常用命令。若要访问解决方案资源管理器,可以选择“视图”/“解决方案资源管理器”命令。

图1.22 解决方案资源管理器

1.3.5 “工具箱”窗口

工具箱是Visual Studio 2015的重要工具,每一个开发人员都必须对这个工具非常熟悉。工具箱提供了进行C#程序开发所必需的控件。通过工具箱,开发人员可以方便地进行可视化的窗体设计,简化了程序设计的工作量,提高了工作效率。根据控件功能的不同,将工具箱划分为11个栏目,如图1.23所示。

单击其中某个栏目,将显示该栏目下的所有控件,如图1.24所示。当需要某个控件时,可以通过双击所需要的控件直接将控件加载到Windows窗体中,也可以先单击选择需要的控件,再将其拖动到Windows窗体上。“工具箱”窗口中的控件可以通过工具箱右键菜单(如图1.25所示)来控制,例如,实现控件的排序、删除、选择显示方式等。

图1.23 “工具箱”窗口

图1.24 展开后的“工具箱”窗口

图1.25 工具箱右键菜单

1.3.6 “属性”窗口

“属性”窗口是Visual Studio 2015中另一个重要的工具,该窗口中为C#程序的开发提供了简单的属性修改方式。Windows窗体中的各个控件属性都可以由“属性”窗口设置完成。“属性”窗口不仅提供了属性的设置及修改功能,还提供了事件的管理功能。“属性”窗口可以管理控件的事件,方便编程时对事件的处理。

另外,“属性”窗口采用了两种方式管理属性和方法,分别为按分类方式和按字母顺序方式。读者可以根据自己的习惯采用不同的方式。该窗口的下方还有简单的帮助,方便开发人员对控件的属性进行操作和修改,“属性”窗口的左侧是属性名称,相对应的右侧是属性值。“属性”窗口如图1.26所示。

图1.26 “属性”窗口

1.3.7 “错误列表”窗口

“错误列表”窗口为代码中的错误提供了即时的提示和可能的解决方法。例如,当某句代码结束时忘记了输入分号时,错误列表中会显示如图1.27所示的错误。错误列表就好像是一个错误提示器,它可以将程序中的错误代码及时显示给开发人员,并通过提示信息找到相应的错误代码。

图1.27 “错误列表”窗口

说明

双击错误列表中的某项,Visual Studio 2015开发平台会自动定位到发生错误的语句。

1.3.8 “输出”窗口

“输出”窗口用于提示项目的生成情况,在实际编程操作中,开发人员会无数次地看到这个窗口,其外观如图1.28所示。“输出”窗口相当于一个记事器,它将程序运行的整个过程以数据的形式进行显示,这样可以让开发者清楚地看到程序各部分的加载与编译过程。

图1.28 “输出”窗口

1.4 小结

本章首先对.NET及C#语言进行了简单的介绍,然后通过图文并茂的方式讲解了Visual Studio 2015集成开发环境的安装与卸载,并且对Visual Studio 2015的菜单栏、工具栏及常用窗口进行了详细的介绍。

第2章 开始C#之旅

第2章
开始C#之旅

视频讲解:68分钟

本章将详细介绍如何编写一个C#程序,以及C#程序的基本结构,另外,还对C#程序的常用编写规范进行了介绍。

通过阅读本章,您可以:

 掌握命名空间的概念以及如何引用命名空间

 了解类的声明方法

 了解Main方法的使用

 掌握一些常用的标识符和关键字

 了解什么是C#语句

 掌握如何为C#程序添加注释

 掌握编写C#程序时需要遵循的书写规则和命名规范

2.1 编写第一个C#程序

视频讲解:光盘\TM\lx\2\01 编写第一个C#程序.mp4

在大多数书籍中,编写的第一个小程序通常是输出“Hello World!”,这里使用Visual Studio 2015和C#语言来编写这个程序,即在控制台上显示字符串“Hello World!”。具体操作步骤如下:

(1)在Windows 8.1操作系统的开始界面中选择“程序”/Visual Studio 2015命令,打开Visual Studio 2015。

(2)选择Visual Studio 2015工具栏中的“文件”/“新建”/“项目”命令,打开“新建项目”对话框,如图2.1所示。

图2.1 “新建项目”对话框

(3)选择“控制台应用程序”选项,命名为Hello_World,选择保存在“E:\Test01\”文件夹下,然后单击“确定”按钮创建一个控制台应用程序。

(4)在Main方法中输入代码。

【例2.1】 创建一个控制台应用程序,使用WriteLine方法输出“Hello World!”字符串,代码如下。(实例位置:光盘\TM\sl\2\1)

    static void Main(string[] args)		        	//Main 方法,在此方法下编写代码输出数据
    {
        Console.WriteLine("Hello World!");		    	//输出 “Hello World!”
        Console.ReadLine();
    }

程序的运行结果如图2.2所示。

图2.2 输出字符串“Hello World!”

互动练习:尝试使用C#仿微信的全民飞机大战游戏制作一个打飞机游戏,具体要求为:通过键盘上的上、下、左、右按键移动飞机,并击落随机降落的敌机。

2.2 初识C#程序结构

C#程序结构大体可以分为命名空间、类、Main方法、标识符、关键字、语句和注释等。下面将对C#程序的结构进行详细讲解。

2.2.1 命名空间

视频讲解:光盘\TM\lx\2\02 命名空间.mp4

C#程序是利用命名空间组织起来的。命名空间既用作程序的“内部”组织系统,也用作向“外部”公开的组织系统(即一种向其他程序公开自己拥有的程序元素的方法)。如果要调用某个命名空间中的类或者方法,首先需要使用using指令引入命名空间,using指令将命名空间名所标识的命名空间内的类型成员导入当前编译单元中,从而可以直接使用每个被导入的类型的标识符,而不必加上它们的完全限定名。

C#中的各命名空间就好像是一个存储了不同类型的仓库,而using指令就好比是一把钥匙,命名空间的名称就好比仓库的名称,可以通过钥匙打开指定名称的仓库,从而在仓库中获取所需的物品。

using指令的基本形式为:

    using  命名空间名;

【例2.2】 创建一个控制台应用程序,建立一个命名空间N1,在命名空间N1中有一个类A,在项目中使用using指令引入命名空间N1,然后在命名空间Test02中即可实例化命名空间N1中的类,然后调用此类中的Myls方法,代码如下。(实例位置:光盘\TM\sl\2\2)

    using N1;							//使用 using 指令引入命名空间N1
    namespace Test02
    {
       class Program
       {
           static void Main(string[] args)
           {
                A oa = new A();            		        //实例化 N1 中的类 A
                oa.Myls();			        	//调用类 A 中的 Myls 方法
           }
       }
    }
    namespace N1      			                         //建立命名空间 N1
    {
        class A                                   		//实例化命名空间 N1 中的类 A
        {
           public void Myls()
           {
                Console.WriteLine("用一生下载你");		//输出字符串
                Console.ReadLine();
           }
        }
    }

程序的运行结果为“用一生下载你”。

2.2.2 类

视频讲解:光盘\TM\lx\2\03 类.mp4

类是一种数据结构,它可以封装数据成员、函数成员和其他的类。类是创建对象的模板。C#中所有的语句都必须位于类内。因此,类是C#语言的核心和基本构成模块。C#支持自定义类,使用C#编程就是编写自己的类来描述实际需要解决的问题。

类就好比医院的各个部门,如内科、骨科、泌尿科、眼科等,在各科室中都有自己的工作方法,相当于在类中定义的变量、方法等。如果要救治车祸重伤的病人,光是一个部门是不行的,可能要内科、骨科、脑科等多个部门一起治疗才行,这时可以让这几个部门临时组成一个小组,对病人进行治疗,这个小组就相当于类的继承,也就是该小组可以动用这几个部门中的所有资源和设备。

使用任何新的类之前都必须声明它,一个类一旦被声明,就可以当作一种新的类型来使用,在C#中通过使用class关键字来声明类,声明形式如下。

    [类修饰符] class [类名]  [基类或接口]
    {
         [类体]
    }

在C#中,类名是一种标识符,必须符合标识符的命名规则。类名要能够体现类的含义和用途。类名一般采用第一个字母大写的名词,也可以采用多个词构成的组合词。

【例2.3】 声明一个最简单的类,此类没有任何意义,只演示如何声明一个类,代码如下。

    class MyClass
    {
    }

闯关训练:在C#程序中定义一个名称为“Car”的表示汽车的类。

2.2.3 Main方法

视频讲解:光盘\TM\lx\2\04 Main方法.mp4

Main方法是程序的入口点,C#程序中必须包含一个Main方法,在该方法中可以创建对象和调用其他方法,一个C#程序中只能有一个Main方法,并且在C#中所有的Main方法都必须是静态的。C#是一种面向对象的编程语言,即使是程序的启动入口点,它也是一个类的成员。由于程序启动时还没有创建类的对象,因此,必须将入口点Main方法定义为静态方法,使它可以不依赖于类的实例对象而执行。

Main方法就相当于汽车的电瓶,在生产汽车时,将各个零件进行组装,相当于程序的编写。当汽车组装完成后,就要检测汽车是否可用,如果想启动汽车,就必须通过电瓶来启动汽车的各个部件,如发动机、车灯等,电瓶就相当于启动汽车的入口点。

可以用3个修饰符修饰Main方法,分别是public、static和void。

 public:说明Main方法是共有的,在类的外面也可以调用整个方法。

 static:说明方法是一个静态方法,即这个方法属于类的本身而不是这个类的特定对象。调用静态方法不能使用类的实例化对象,必须直接使用类名来调用。

 void:此修饰符说明方法无返回值。

2.2.4 标识符及关键字

视频讲解:光盘\TM\lx\2\05 标识符及关键字.mp4

1.标识符

标识符可以简单地理解为一个名字,用来标识类名、变量名、方法名、数组名、文件名的有效字符序列。

C#语言规定标识符由任意顺序的字母、下划线(_)和数字组成,并且第一个字符不能是数字。标识符不能是C#中的保留关键字。

下面是合法标识符:

    _ID
    name
    user_age

下面是非法标识符:

    4word
    string

在C#语言中,标识符中的字母是严格区分大小写的,如good和Good是两个不同的标识符。

2.关键字

关键字是C#语言中已经被赋予特定意义的一些单词,不可以把这些关键字作为标识符来使用。大家经常看到的class、static和void等都是关键字。C#语言中的常用关键字如表2.1所示。

表2.1 C#常用关键字

2.2.5 C#语句

视频讲解:光盘\TM\lx\2\06 C#语句.mp4

语句是构造所有C#程序的基本单位。语句可以声明局部变量或常数、调用方法、创建对象或将值赋给变量、属性或字段,语句通常以分号终止。

【例2.4】 下面的代码就是一条语句。

    Console.WriteLine("Hello World!");

此语句便是调用Console类中的WriteLine方法,输出指定的字符串“Hello World!”。

互动练习:尝试使用C#根据经纬度模仿船体的航行轨迹,具体要求为:将GPS获取到的经纬度信息保存到文本文件中,然后从该文本文件中读取信息,并根据读取到的信息,使用GDI+绘制航行轨迹图。

2.2.6 注释

视频讲解:光盘\TM\lx\2\07 注释.mp4

编译器编译程序时不执行注释的代码或文字,其主要功能是对某行或某段代码进行说明,方便对代码的理解与维护,这一过程就好像是超市中各商品的下面都附有价格标签,对商品的价格进行说明。注释可以分为行注释和块注释两种,行注释都以“//”开头。

【例2.5】 在“Hello World!”程序中使用行注释,代码如下。

    static void Main(string[] args)         					//程序的 Main 方法
    {
         Console.WriteLine("Hello World!");					//输出 “Hello World!”
         Console.ReadLine();
    }

如果注释的行数较少,一般使用行注释。对于连续多行的大段注释,则使用块注释,块注释通常以“/*”开始,以“*/”结束,注释的内容放在它们之间。

【例2.6】 在“Hello World!”程序中使用块注释,代码如下。

注意

注释可以出现在代码的任意位置,但是不能分隔关键字和标识符。例如,下面的代码注释是错误的:

    static void  //错误的注释 Main(string[] args)

闯关训练:创建一个控制台应用程序,为代码添加著作权信息的注释,包括开发日期、作者、代码的作用等。

2.3 程序编写规范

视频讲解:光盘\TM\lx\2\08 程序编写规范.mp4

本节将详细介绍代码的书写规则以及命名规范,使用代码书写规则和命名规范可以使程序代码更加规范化,对代码的理解与维护起到至关重要的作用。

2.3.1 代码书写规则

代码书写规则通常对应用程序的功能没有影响,但它们能改善对源代码的理解。养成良好的习惯对于软件的开发和维护都是很有益的,下面介绍一些代码书写规则。

 尽量使用接口,然后使用类实现接口,以提高程序的灵活性。

 尽量不要手工更改计算机生成的代码,若必须更改,一定要改成和计算机生成的代码风格一样。

 关键的语句(包括声明关键的变量)必须要写注释。

 建议局部变量在最接近使用它的地方声明。

 不要使用goto系列语句,除非是用在跳出深层循环时。

 避免写超过5个参数的方法。如果要传递多个参数,则使用结构。

 避免书写代码量过大的try-catch模块。

 避免在同一个文件中放置多个类。

 生成和构建一个长的字符串时,一定要使用StringBuilder类型,而不用string类型。

 switch语句一定要有default语句来处理意外情况。

 对于if语句,应该使用一对“{ }”把语句块包含起来。

 尽量不使用this关键字引用。

2.3.2 命名规范

命名规范在编写代码中起到很重要的作用,虽然不遵循命名规范,程序也可以运行,但是使用命名规范可以很直观地了解代码所代表的含义。下面列出一些命名规范,供读者参考。

(1)用Pascal规则来命名方法和类型,Pascal的命名规则是第一个字母必须大写,并且后面的连接词的第一个字母均为大写。

【例2.7】 定义一个公共类,并在此类中创建一个公共方法,代码如下。

    public class User            //创建一个公共类
    {
        public void GetInfo()    //在公共类中创建一个公共方法
        {
        }
    }

(2)用Camel规则来命名局部变量和方法的参数,Camel规则是指名称中第一个单词的第一个字母小写。

【例2.8】 声明一个字符串变量和创建一个公共方法,代码如下。

    string strUserName;                                        //声明一个字符串变量 strUserName
    public void addUser(string strUserId, byte[] byPassword);  //创建一个具有两个参数的公共方法

(3)所有的成员变量前加前缀“_”。

【例2.9】 在公共类DataBase中声明一个私有成员变量_connectionString,代码如下。

    Public class DataBase                    //创建一个公共类
    {
           private string_connectionString;  //声明一个私有成员变量
    }

(4)接口的名称加前缀“I”。

【例2.10】 创建一个公共接口Iconvertible,代码如下。

    public interface Iconvertible                  //创建一个公共接口 Iconvertible
    {
           byte ToByte();                           //声明一个 byte 类型的方法
    }

(5)方法的命名,一般将其命名为动宾短语。

【例2.11】 在公共类File中创建CreateFile方法和GetPath方法,代码如下。

    public class File                                        //创建一个公共类
    {
         public void CreateFile(string filePath)             //创建一个 CreateFile 方法
         {
         }
         public void GetPath(string path)                    //创建一个 GetPath 方法
         {
         }
    }

(6)所有的成员变量声明在类的顶端,用一个换行把它和方法分开。

【例2.12】 在类的顶端声明两个私有变量_productId和_productName,代码如下。

    public class Product                                         //创建一个公共类
    {
        private string_productId;                                //在类的顶端声明变量
        private string_productName;                              //在类的顶端声明变量
        //创建一个公共方法
        public void AddProduct(string productId,string productName)
        {
        }
    }

说明

在类中定义私有变量和私有方法,变量和方法只能在该类中使用,不能对类进行实例化,对其进行调用。

(7)用有意义的名字命名空间namespace,如公司名、产品名。

【例2.13】 利用公司名和产品名命名空间namespace,代码如下。

    namespace Zivsoft             //公司命名
    {
    }
    namespace ERP                 //产品命名
    {
    }

(8)使用某个控件的值时,尽量命名局部变量。

【例2.14】 创建一个方法,在方法中声明一个字符串变量title,使其等于Label控件的Text值,代码如下。

    public string GetTitle()                             //创建一个公共方法
    {
      string title=lbl_Title.Text;                       //定义一个局部变量
      return title;                                      //使用这个局部变量
    }

说明

在定义有返回值的方法时,必须在设置方法时,定义方法的类型,并在方法体结束后用return返回值。

2.4 小结

本章主要介绍了C#程序的结构、代码书写规范和命名规则。在C#程序的结构中,读者需要重点掌握命名空间、类以及C#语句。命名空间在C#程序中占有重要的地位,通过引入命名空间,可以将空间下的成员引入到当前的编译单元中。而类是C#语言的核心和基本构成模块,程序员可以通过编写类来描述实际开发需要解决的问题。在编写程序代码时,读者要养成一种良好的编写习惯,本章列出一些代码书写规则和命名规范,希望能对读者有所帮助。

2.5 动手纠错

(1)运行“光盘\TM\排错练习\02\01”文件夹下的程序,出现“当前上下文中不存在名称Name”的错误提示,请根据注释改正程序。

(2)运行“光盘\TM\排错练习\02\02”文件夹下的程序,出现“应输入标识符;int是关键字”的错误提示,请根据注释改正程序。

(3)运行“光盘\TM\排错练习\02\03”文件夹下的程序,出现“无法使用实例引用访问成员Test03.Program.i;请改用类型名称对其加以限定”的错误提示,请根据注释改正程序。

(4)运行“光盘\TM\排错练习\02\04”文件夹下的程序,出现“使用了未赋值的局部变量price”的错误提示,请根据注释改正程序。

(5)运行“光盘\TM\排错练习\02\05”文件夹下的程序,出现“Test05.Test.i不可访问,因为它受保护级别限制”的错误提示,请根据注释改正程序。

第3章 变量与常量

第3章
变量与常量

视频讲解:148分钟

应用程序的开发离不开变量与常量的应用。变量本身被用来存储特定类型的数据,而常量则用来存储不变的数据值。本章详细地介绍变量的类型和基本操作,同时对常量也做了详细的讲解,讲解过程中为了便于读者理解结合了大量的举例。

通过阅读本章,您可以:

 了解变量的基本概念

 掌握变量的声明及赋值

 熟悉变量的作用域

 掌握值类型的概念及用法

 掌握引用类型的概念及用法

 熟悉枚举类型的概念及用法

 掌握常用的类型转换方式

 了解常量的概念及用法

3.1 变量的基本概念

视频讲解:光盘\TM\lx\3\01 变量的基本概念.mp4

变量本身被用来存储特定类型的数据,可以根据需要随时改变变量中所存储的数据值。变量具有名称、类型和值。变量名是变量在程序源代码中的标识。变量类型确定它所代表的内存的大小和类型,变量值是指它所代表的内存块中的数据。在程序的执行过程中,变量的值可以发生变化。使用变量之前必须先声明变量,即指定变量的类型和名称。

3.2 变量的声明及赋值

视频讲解:光盘\TM\lx\3\02 变量的声明及赋值.mp4

变量在使用之前,必须进行声明并赋值,本节将对变量的声明及赋值,以及变量的作用域进行详细讲解。

3.2.1 声明变量

变量的使用是程序设计中一个十分重要的环节。为什么要定义变量呢?简单地说,就是要告诉编译器(Compiler)这个变量是属于哪一种数据类型,这样编译器才知道需要配置多少空间给它,以及它能存放什么样的数据。在程序运行过程中,空间内的值是变化的,这个内存空间就称为变量。声明变量就是指定变量的名称和类型,变量的声明非常重要,未经声明的变量本身并不合法,也因此没有办法在程序当中使用。在C#中,声明一个变量是由一个类型和跟在后面的一个或多个变量名组成,多个变量之间用逗号分开,声明变量以分号结束。

【例3.1】 声明一个整型变量LS,然后再同时声明3个字符型变量Str1、Str2和Str3,代码如下。

    int LS;                             //声明一个整型变量
    string Str1, Str2, Str3;            //同时声明 3 个字符型变量

在第1行代码中,声明了一个名称为LS的整型变量。在第二行代码中,声明了3个字符串型的变量,分别为Str1、Str2和Str3。

声明变量时,还可以初始化变量,即在每个变量名后面加上给变量赋初始值的指令。

【例3.2】 声明一个整型变量a,并且赋值为927。然后,再同时声明3个字符型变量,并初始化,代码如下。

    int a = 927;                                                //初始化整型变量 a
    string x = "用一生下载你", y = "芸烨湘枫", z = "一生所爱";  //初始化字符型变量 x、y 和 z

在声明变量时,要注意变量名的命名规则。C#的变量名是一种标识符,应该符合标识符的命名规则。变量名是区分大小写的,下面列出变量的命名规则。

 变量名只能由数字、字母和下划线组成。

 变量名的第一个符号只能是字母和下划线,不能是数字。

 不能使用关键字作为变量名。

 一旦在一个语句块中定义了一个变量名,那么在变量的作用域内都不能再定义同名的变量。

说明

在C#语言中允许使用汉字或其他语言文字作为变量名,如“int年龄= 21”,在程序运行时并不出现什么错误,但建议读者尽量不要使用这些语言文字作为变量名。

3.2.2 变量的赋值

在C#中,使用赋值运算符“=”(等号)来给变量赋值,将等号右边的值赋给左边的变量。【例3.3】 声明一个变量,并给变量赋值,代码如下。

    int sum;                   //声明一个变量
    sum = 2008;                //使用赋值运算符 “=” 给变量赋值

在3.2.1节介绍的初始化变量,其实是一种特殊的赋值方式,它在声明变量的同时给变量赋值。在给变量赋值时,等号右边也可以是一个已经被赋值的变量。

【例3.4】 首先声明两个变量sum和num,然后将变量sum赋值为927,最后将变量sum赋值给变量num,代码如下。

    int sum,num;              //声明两个变量
    sum = 927;                //给变量 sum 赋值为 927
    num = sum;                //将变量 sum 赋值给变量 num

注意

在对多个同类型的变量赋同一个值时,为了节省代码的行数,可以同时对多个变量进行初始化,如“int a, b, c, d, e; a = b = c = d = e = 0;”。

3.2.3 变量的作用域

由于变量被定义出来后只是暂存在内存中,等到程序执行到某一个点后,该变量会被释放掉,也就是说变量有它的生命周期。因此,变量的作用域是指程序代码能够访问该变量的区域,若超出该区域,则在编译时会出现错误。在程序中,一般会根据变量的“有效范围”将变量分为“成员变量”和“局部变量”。

1.成员变量

在类体中定义的变量被称为成员变量,成员变量在整个类中都有效。类的成员变量又可分为两种,即静态变量和实例变量。

【例3.5】 声明静态变量和实例变量,实例代码如下。

    class Test
    {
       int x = 45;
       static int y = 90;
    }

其中,x为实例变量,y为静态变量(也称类变量)。如果在成员变量的类型前面加上关键字static,这样的成员变量称为静态变量。静态变量的有效范围可以跨类,甚至可达到整个应用程序之内。对于静态变量,除了能在定义它的类内存取,还能直接以“类名.静态变量”的方式在其他类内使用。

2.局部变量

在类的方法体中定义的变量(方法内部定义,“{”与“}”之间的代码中声明的变量)称为局部变量。局部变量只在当前代码块中有效。

图3.1 变量的有效范围

在类的方法中声明的变量,包括方法的参数,都属于局部变量。局部变量只有在当前定义的方法内有效,不能用于类的其他方法中。局部变量的生命周期取决于方法,当方法被调用时,C#编译器为方法中的局部变量分配内存空间,当该方法的调用结束后,则会释放方法中局部变量占用的内存空间,局部变量也将会销毁。

变量的有效范围如图3.1所示。

【例3.6】 创建一个控制台应用程序,使用for循环将0~20之间的数字显示出来。然后在for语句中声明变量i,此时i就是局部变量,其作用域只限于for循环体内,代码如下。(实例位置:光盘\TM\sl\3\1)

    static void Main(string[] args)
    {
	        //调用 for 语句循环输出数字
          for (int i = 0; i <= 20; i++)                      //for 循环内的局部变量i
          {
                Console.WriteLine(i.ToString());             //输出 0~20 的数字
          }
          Console.ReadLine();
    }

程序的运行结果为0~20之间的数字。

互动练习:某著名的在线通信软件公司出过这么一道面试题,说“经理有3个女儿,她们的年龄和是13岁,年龄的乘积等于经理的年龄。有位员工知道经理年龄,但是不能确定他3个女儿都是多大,这时,经理跟他说,我只有一个女儿超过5岁,于是那位员工就知道了经理3个女儿的年龄,那么经理的3个女儿都分别是多大?”使用C#推算经理3个女儿的年龄。

3.3 数据类型

C#中的变量类型根据其定义可以分为两种:一种是值类型,另一种是引用类型。这两种类型的差异在于数据的存储方式,值类型的变量本身直接存储数据。而引用类型则存储实际数据的引用,程序通过此引用找到真正的数据,在以下内容中将会对这些类型进行详细讲解。

3.3.1 值类型

视频讲解:光盘\TM\lx\3\03 值类型.mp4

值类型变量直接存储其数据值,主要包含整数类型、浮点类型以及布尔类型等。值类型变量在栈中进行分配,因此效率很高,使用值类型主要目的是为了提高性能。值类型具有如下特性:

 值类型变量都存储在栈中。

 访问值类型变量时,一般都是直接访问其实例。

 每个值类型变量都有自己的数据副本,因此对一个值类型变量的操作不会影响其他变量。

 复制值类型变量时,复制的是变量的值,而不是变量的地址。

 值类型变量不能为null,必须具有一个确定的值。

值类型是从System.ValueType类继承而来的类型,下面详细介绍值类型中包含的几种数据类型。

1.整数类型

整数类型用来存储整数数值,即没有小数部分的数值。可以是正数,也可以是负数。整型数据在C#程序中有3种表示形式,分别为十进制、八进制和十六进制。

 十进制:十进制的表现形式大家都很熟悉,如120、0、-127。

注意

不能以0作为十进制数的开头(0除外)。

 八进制:如0123(转换成十进制数为83)、-0123(转换成十进制数为-83)。

注意

八进制必须以0开头。

 十六进制:如0x25(转换成十进制数为37)、0Xb01e(转换成十进制数为45086)。

注意

十六进制必须以0X或0x开头。

在C#中内置的整数类型如表3.1所示。

表3.1 C#内置的整数类型

byte类型以及short类型是范围比较小的整数,如果正整数的范围没有超过65535,声明为ushort类型即可,当然更小的数值直接以byte类型作处理即可。只是使用这种类型时必须特别注意数值的大小,否则可能会导致运算溢出的错误。

【例3.7】 创建一个控制台应用程序,在其中声明一个int类型的变量ls并初始化为927、一个byte类型的变量shj并初始化为255,最后输出,代码如下。(实例位置:光盘\TM\sl\3\2)

    static void Main(string[] args)
    {  
         int ls = 927;                             //声明一个 int 类型的变量 ls
         byte shj = 255;                           //声明一个 byte 类型的变量 shj
         Console.WriteLine("ls={0}", ls);          //输出 int 类型变量 ls
         Console.WriteLine("shj={0}", shj);        //输出 byte 类型变量 shj
         Console.ReadLine();
    }

程序的运行结果为:

    ls=927
    shj=255

此时,如果将byte类型的变量shj赋值为266,重新编译程序,就会出现错误提示。主要原因是byte类型的变量是8位无符号整数,它的范围在0~255之间,266已经超出了byte类型的范围,所以编译程序会出现错误提示。

注意

在定义局部变量时,要对其进行初始化。

2.浮点类型

浮点类型变量主要用于处理含有小数的数值数据,浮点类型主要包含float和double两种数值类型。表3.2列出了这两种数值类型的描述信息。

表3.2 浮点类型及描述

如果不做任何设置,包含小数点的数值都被认为是double类型,例如9.27,没有特别指定的情况下,这个数值是double类型。如果要将数值以float类型来处理,就应该通过强制使用f或F将其指定为float类型。

【例3.8】 下面的代码就是将数值强制指定为float类型。

    float theMySum = 9.27f;                              //使用 f 强制指定为 float 类型
    float theMuSums = 1.12F;                             //使用 F 强制指定为 float 类型

如果要将数值强制指定为double类型,则应该使用d或D进行设置,但加不加“d”或“D”没有硬性规定,可以加也可以不加。

【例3.9】 下面的代码就是将数值强制指定为double类型。

    double myDou = 927d;                                  //使用 d 强制指定为 double 类型
    double mudou = 112D;                                  //使用 D 强制指定为 double 类型

注意

如果需要使用float类型变量时,必须在数值的后面跟随f或F,否则编译器会直接将其作为double类型处理。也可以在double类型的值前面加上(float),对其进行强制转换。

3.布尔类型

布尔类型主要用来表示true/false值,一个布尔类型的变量,其值只能是true或者false,不能将其他的值指定给布尔类型变量,布尔类型变量不能与其他类型之间进行转换。布尔类型通常被用在流程控制中作为判断条件。

【例3.10】 将927赋值给布尔类型变量x,代码如下。

    bool x=927;

这样赋值显然是错误的,编译器会返回错误提示“常量值927无法转换为bool”。布尔类型变量大多数被应用到流程控制语句当中,例如,循环语句或者if语句等。

说明

在定义全局变量时,如果没有特定的要求不用对其进行初始化,整数类型和浮点类型的默认初始化为0,布尔类型的初始化为false。

闯关训练:开发财务系统时,通过值类型创建存储流动资金金额的临时性变量。

3.3.2 引用类型

视频讲解:光盘\TM\lx\3\04 引用类型.mp4

引用类型是构建C#应用程序的主要对象类型数据。在应用程序执行的过程中,预先定义的对象类型以new创建对象实例,并且存储在堆中。堆是一种由系统弹性配置的内存空间,没有特定大小及存活时间,因此可以被弹性地运用于对象的访问。引用类型就类似于生活中的代理商,代理商没有自己的产品,而是代理厂家的产品,使其就好像是自己的产品一样。

引用类型具有如下特征。

 必须在托管堆中为引用类型变量分配内存。

 使用new关键字来创建引用类型变量。

 在托管堆中分配的每个对象都有与之相关联的附加成员,这些成员必须被初始化。

 引用类型变量是由垃圾回收机制来管理的。

 多个引用类型变量可以引用同一对象,这种情形下,对一个变量的操作会影响另一个变量所引用的同一对象。

 引用类型被赋值前的值都是null。

所有被称为“类”的都是引用类型,主要包括类、接口、数组和委托。下面通过一个实例来演示如何使用引用类型。

【例3.11】 创建一个控制台应用程序,在其中创建一个类C,在此类中建立一个字段Value,并初始化为0,然后在程序的其他位置通过new创建对此类的引用类型变量,最后输出,代码如下。(实例位置:光盘\TM\sl\3\3)

程序的运行结果为:

    Values:0,927
    Refs:112,112

3.3.3 值类型与引用类型的区别

视频讲解:光盘\TM\lx\3\05 值类型与引用类型的区别.mp4

从概念上看,值类型直接存储其值,而引用类型存储对其值的引用。这两种类型存储在内存的不同地方。在C#中,必须在设计类型时就决定类型实例的行为。如果在编写代码时不能理解引用类型和值类型的区别,那么将会给代码带来不必要的异常。

从内存空间上看,值类型是在栈中操作,而引用类型则在堆中分配存储单元。栈在编译时就分配好内存空间,在代码中有栈的明确定义,而堆是程序运行中动态分配的内存空间,可以根据程序的运行情况动态地分配内存的大小。因此,值类型总是在内存中占用一个预定义的字节数。而引用类型的变量则在堆中分配一个内存空间,这个内存空间包含的是对另一个内存位置的引用,这个位置是托管堆中的一个地址,即存放此变量实际值的地方。

也就是说值类型相当于现金,要用就直接用,而引用类型相当于存折,要用得先去银行取。

说明

C#的所有值类型均隐式派生自System.ValueType,而System.ValueType直接派生于System.Object。即System.ValueType本身是一个类类型,而不是值类型。其关键在于ValueType重写了Equals()方法,从而对值类型按照实例的值来比较,而不是引用地址来比较。

下面以一段代码来详细讲解值类型与引用类型的区别,代码如下。

运行结果如图3.2所示。

图3.2 值类型与引用类型

从图3.2中可以看出,当改变了Stamp_1.Age的值时,age没跟着变,而在改变了Stamp_2.Name的值后,guru.Name却跟着变了,这就是值类型和引用类型的区别。在声明age值类型变量时,将Stamp_1.Age的值赋给它,这时,编译器在栈上分配了一块空间,然后把Stamp_1.Age的值填进去,二者没有任何关联,就像在计算机中复制文件一样,只是把Stamp_1.Age的值复制给age了。而引用类型则不同,在声明guru时把Stamp_2赋给它。前面说过,引用类型包含的只是堆上数据区域地址的引用,其实就是把Stamp_2的引用也赋给guru,因此它们指向了同一块内存区域。既然是指向同一块区域,不管修改谁,另一个的值都会跟着改变。就像信用卡跟亲情卡一样,用亲情卡取了钱,与之关联的信用卡账上也会跟着发生变化。

3.3.4 枚举类型

视频讲解:光盘\TM\lx\3\06 枚举类型.mp4

枚举类型是一种独特的值类型,它用于声明一组具有相同性质的常量,编写与日期相关的应用程序时,经常需要使用年、月、日、星期等日期数据,可以将这些数据组织成多个不同名称的枚举类型。使用枚举可以增加程序的可读性和可维护性。同时,枚举类型可以避免类型错误。

说明

在定义枚举类型时,如果不对其进行赋值,默认情况下,第一个枚举数的值为0,后面每个枚举数的值依次递增1。

在C#中使用关键字enum类声明枚举,其形式如下。

    enum 枚举名
    {
      list1=value1,
      list2=value2,
      list3=value3,
      …
      listN=valueN,
    }

其中,大括号{}中的内容为枚举值列表,每个枚举值均对应一个枚举值名称,value1~valueN为整数数据类型,list1~listN则为枚举值的标识名称。下面通过一个实例来演示如何使用枚举类型。

【例3.12】 创建一个控制台应用程序,通过使用枚举来判断当前系统日期是星期几,代码如下。(实例位置:光盘\TM\sl\3\4)

    class Program
    {
        enum MyDate                                             //使用 enum 创建枚举
     {
           Sun = 0,                                             //设置枚举值名称 Sun,枚举值为 0
           Mon = 1,                                             //设置枚举值名称 Mon,枚举值为 1
           Tue = 2,                                             //设置枚举值名称 Tue,枚举值为 2
           Wed = 3,                                             //设置枚举值名称 Wed,枚举值为 3
           Thi = 4,                                             //设置枚举值名称 Thi,枚举值为 4
           Fri = 5,                                             //设置枚举值名称 Fri,枚举值为 5
           Sat = 6                                              //设置枚举值名称 Sat,枚举值为 6
        }
        static void Main(string[] args)
        {
           int k = (int)DateTime.Now.DayOfWeek; 		//获取代表星期几的返回值
           switch (k)
           {
			            //如果 k 等于枚举变量 MyDate 中的 Sun 的枚举值,则输出今天是星期日
                case (int)MyDate.Sun: Console.WriteLine("今天是星期日"); break;
			            //如果 k 等于枚举变量 MyDate 中的 Mon 的枚举值,则输出今天是星期一
                case (int)MyDate.Mon: Console.WriteLine("今天是星期一"); break;
			            //如果 k 等于枚举变量 MyDate 中的 Tue 的枚举值,则输出今天是星期二
                case (int)MyDate.Tue: Console.WriteLine("今天是星期二"); break;
			            //如果 k 等于枚举变量 MyDate 中的 Wed 的枚举值,则输出今天是星期三
                case (int)MyDate.Wed: Console.WriteLine("今天是星期三"); break;
			            //如果 k 等于枚举变量 MyDate 中的 Thi 的枚举值,则输出今天是星期四
                case (int)MyDate.Thi: Console.WriteLine("今天是星期四"); break;
			            //如果 k 等于枚举变量 MyDate 中的 Fri 的枚举值,则输出今天是星期五
                case (int)MyDate.Fri: Console.WriteLine("今天是星期五"); break;
			            //如果 k 等于枚举变量 MyDate 中的 Sat 的枚举值,则输出今天是星期六
                case (int)MyDate.Sat: Console.WriteLine("今天是星期六"); break;
            }
            Console.ReadLine();
        }
    }

程序运行的结果为“今天是星期三”。

查看程序运行的结果,因为当前日期是2014年4月16日星期三,所以输出的结果显示当天是星期三。程序首先通过enum关键字建立一个枚举,枚举值名称分别代表一周的七天,如果枚举值名称是Sun,说明其代表的是一周中的星期日,其枚举值为0,依此类推。然后,声明一个int类型的变量k,用于获取当前表示的日期是星期几。最后,调用swith语句,输出当天是星期几。

3.3.5 类型转换

视频讲解:光盘\TM\lx\3\07 类型转换.mp4

类型转换就是将一种类型转换成另一种类型,转换可以是隐式转换或者显式转换,本节将详细介绍这两种转换方式,并讲解有关装箱和拆箱的内容。

说明

对于类型转换,读者可以这么想象,大脑前面是一片内存,源和目标分别是两个大小不同的内存块(由变量及数据的类型来决定),将源数据赋值给目标内存的过程,就是用目标内存块去套取源内存中的数据,能套多少算多少。

1.隐式转换

所谓隐式转换就是不需要声明就能进行的转换。进行隐式转换时,编译器不需要进行检查就能自动进行转换。表3.3列出了可以进行隐式转换的数据类型。

表3.3 隐式类型转换表

从int、uint、long或ulong到float,以及从long或ulong到double的转换可能导致精度损失,但是不会影响其数量级。其他的隐式转换不会丢失任何信息。

说明

当一种类型的值转换为大小相等或更大的另一类型时,则发生扩大转换。当一种类型的值转换为较小的另一种类型时,则发生收缩转换。

【例3.13】 将int类型的值隐式转换成long类型,代码如下。

    int i = 927;	                     //声明一个整型变量 i 并初始化为 927
    long j = i;                              //隐式转换成 long 类型
2.显式转换

显式转换也可以称为强制转换,需要在代码中明确地声明要转换的类型。如果要把高精度的变量的值赋给低精度的变量,就需要使用显式转换。表3.4列出了需要进行显式转换的数据类型。

表3.4 显式类型转换表

由于显式转换包括所有隐式转换和显式转换,因此总是可以使用强制转换表达式将任何数值类型转换为任何其他的数值类型。

【例3.14】 创建一个控制台应用程序,将double类型的x进行显式类型转换,代码如下。(实例位置:光盘\TM\sl\3\5)

    static void Main(string[] args)
    {
         double x = 19810927.0112;            //建立 double 类型变量 x
         int y = (int)x;                      //显示转换成整型变量 y
         Console.WriteLine(y);                 //输出整型变量 y
         Console.ReadLine();
    }

程序的运行结果为19810927。

显示转换也可以通过Convert关键字进行显式类型转换,上述例子还可以通过下面的代码实现。

【例3.15】 创建一个控制台应用程序,通过Convert关键字进行显式类型转换,代码如下。

    double x = 19810927.0112;              //建立 double 类型变量 x
    int y = Convert.ToInt32(x);            //通过 Convert 关键字转换
    Console.WriteLine(y);                  //输出整型变量 y
    Console.ReadLine();
3.装箱和拆箱

将值类型转换为引用类型的过程叫做装箱,相反,将引用类型转换为值类型的过程叫做拆箱,下面将通过例子详细介绍装箱与拆箱的过程。

(1)装箱

装箱允许将值类型隐式转换成引用类型,下面通过一个实例演示如何进行装箱操作。

【例3.16】 创建一个控制台应用程序,声明一个整型变量i并赋值为2008,然后将其复制到装箱对象obj中,最后再改变变量i的值,代码如下。(实例位置:光盘\TM\sl\3\6)

    static void Main(string[] args)
    {
         int i = 2008;                     //声明一个 int 类型变量 i,并初始化为 2008
         object obj = i;                   //声明一个 object 类型 obj,其初始化值为 i
         Console.WriteLine("1、i 的值为{0},装箱之后的对象为{1}", i, obj);
         i = 927;                          //重新将 I 赋值为927
         Console.WriteLine("2、i 的值为{0},装箱之后的对象为{1}", i, obj);
         Console.ReadLine();
    }

程序的运行结果为:

    1.i 的值为 2008,装箱之后的对象为 2008
    2.i 的值为 927,装箱之后的对象为 2008

从程序运行结果可以看出,值类型变量的值复制到装箱得到的对象中,装箱后改变值类型变量的值,并不会影响装箱对象的值。

(2)拆箱

拆箱允许将引用类型显式转换为值类型,下面通过一个示例演示拆箱的过程。

【例3.17】 创建一个控制台应用程序,声明一个整型变量i并赋值为112,然后将其复制到装箱对象obj中,最后,进行拆箱操作将装箱对象obj赋值给整型变量j,代码如下。(实例位置:光盘\TM\sl\3\7)

    static void Main(string[] args)
    {
         int i = 112;                           //声明一个 int 类型的变量 i,并初始化为 112
         object obj = i;                        //执行装箱操作
         Console.WriteLine("装箱操作:值为{0},装箱之后对象为{1}", i, obj);
         int j = (int)obj;                      //执行拆箱操作
         Console.WriteLine("拆箱操作:装箱对象为{0},值为{1}", obj, j);
         Console.ReadLine();
    }

程序的运行结果为:

    装箱操作:值为 112,装箱之后对象为 112
    拆箱操作:装箱对象为 112,值为 112

查看程序运行结果,不难看出,拆箱后得到的值类型数据的值与装箱对象相等。需要读者注意的是,在执行拆箱操作时,要符合类型一致的原则,否则会出现异常。

说明

装箱是将一个值类型转换为一个对象类型(object),而拆箱则是将一个对象类型显式转换为一个值类型。对于装箱而言,它是将被装箱的值类型复制一个副本来转换,而对于拆箱而言,需要注意类型的兼容性,例如,不能将一个值为“string”的object类型转换为int类型。

互动练习:尝试使用C#制作一个可以在窗体上画桃花的游戏,具体要求为在窗体的左侧显示桃花的3种状态,即花骨朵、花蕾、开花,然后用鼠标单击某一种状态,即可在右侧显示的桃枝上绘制桃花的相应状态。

3.4 常量

视频讲解:光盘\TM\lx\3\08 常量.mp4

常量就是其值固定不变的量,而且常量的值在编译时就已经确定了。常量的类型只能为下列类型之一:sbyte、byte、short、ushort、int、uint、long、ulong、char、float、double、decimal、bool、string等。C#中使用关键字const定义常量,并且在创建常量时必须设置它的初始值。常量就相当于每个公民的身份证号,一旦设置就不允许修改。

【例3.18】 声明一个正确的常量,同时再声明一个错误的常量,以便读者对比参考,代码如下。

    const double PI = 3.1415926;	               //正确的声明方法
    const int MyInt;	                               //错误:定义常量时没有初始化

与变量不同,常量在整个程序中只能被赋值一次。在为所有的对象共享值时,常量是非常有用的。下面通过一个例子演示常量与变量的差异。

【例3.19】 创建一个控制台应用程序,声明一个变量MyInt并且赋值为927,然后再声明一个常量MyWInt并赋值为112,最后,将变量MyInt赋值为1039,关键代码如下。(实例位置:光盘\TM\sl\3\8)

    static void Main(string[] args)
    {
         int MyInt = 927;                                //声明一个整型变量
         const int MyWInt = 112;                         //声明一个整型常量
         Console.WriteLine("变量 MyInt={0}",MyInt);      //输出
         Console.WriteLine("常量 MyWInt={0}",MyWInt);    //输出
         MyInt = 1039;                                   //重新将变量赋值为 1039
         Console.WriteLine("变量 MyInt={0}", MyInt);     //输出
         Console.ReadLine();
    }

执行程序,输出的结果为:

    变量 MyInt=927
    常量 MyWInt=112
    变量 MyInt=1039

变量MyInt的初始化值为927,而常量MyWInt的值等于112,由于变量的值是可以修改的,所以变量MyInt可以重新被赋值为1039后输出。通过查看输出结果,可以看到变量MyInt的值已经被修改,如果尝试修改常量MyWInt的值,编译器会出现错误信息,阻止进行这样的操作。

3.5 小结

本章重点讲解了变量和常量,通过大量的举例说明,使读者更好地理解所学知识的用法。在阅读本章时,要重点掌握值类型、引用类型和枚举类型的概念及用法,并且要了解如何进行类型转换。了解变量的基本知识后,要掌握如何对变量进行操作,了解变量的作用域以及如何为变量赋值。本章最后对常量进行了详细的叙述,包括常量的概念及常量的基本类型。

3.6 实践与练习

(1)尝试开发一个程序,在该程序中建立一个静态方法,在静态方法中声明一个局部变量,并对其赋值,然后输出。(答案位置:光盘\TM\sl\3\9)

(2)尝试开发一个程序,要求声明一个常量,然后试着更改这个常量的值,看会引发什么错误。(答案位置:光盘\TM\sl\3\10)

3.7 动手纠错

(1)运行“光盘\TM\排错练习\03\01”文件夹下的程序,出现“常量值300无法转换为byte”的错误提示,请根据注释改正程序。

(2)运行“光盘\TM\排错练习\03\02”文件夹下的程序,出现“意外的字符;”的错误提示,请根据注释改正程序。

(3)运行“光盘\TM\排错练习\03\03”文件夹下的程序,出现“不能隐式地将double类型转换为float类型;请使用F后缀创建此类型”的错误提示,请根据注释改正程序。

(4)运行“光盘\TM\排错练习\03\04”文件夹下的程序,出现“无法将类型double隐式转换为int。存在一个显式转换(是否缺少强制转换?)”的错误提示,请根据注释改正程序。

(5)运行“光盘\TM\排错练习\03\05”文件夹下的程序,出现“未处理InvalidCastException指定的转换无效”的错误提示,请根据注释改正程序。

(6)运行“光盘\TM\排错练习\03\06”文件夹下的程序,出现“赋值号左边必须是变量、属性或索引器”的错误提示,请根据注释改正程序。

(7)运行“光盘\TM\排错练习\03\07”文件夹下的程序,出现“无法使用实例引用访问成员Test07.Test.PI;请改用类型名称对其加以限定”的错误提示,请根据注释改正程序。

第4章 表达式与运算符

第4章
表达式与运算符

视频讲解:98分钟

表达式在C#程序中应用广泛,尤其是在计算功能中,往往需要大量的表达式。而大多数表达式都使用运算符,运算符结合一个或一个以上的操作数,便形成了表达式,并且返回运算结果。本章将对C#中的表达式与运算符进行详细讲解。

通过阅读本章,您可以:

 了解什么是表达式

 熟悉算术运算符的使用

 掌握赋值运算符

 掌握如何使用关系运算符

 掌握逻辑运算符的使用方法

 掌握如何对变量进行位操作

 熟悉运算符的优先级顺序

4.1 表达式

视频讲解:光盘\TM\lx\4\01 表达式.mp4

表达式是由运算符和操作数组成的。运算符设置对操作数进行什么样的运算。例如,+、-、*和/都是运算符,操作数包括文本、常量、变量和表达式等。

例如,下面几行代码就是简单的表达式。

    int i = 927;                        //声明一个 int 类型的变量 i 并初始化为 927
    i = i * i + 112;                    //改变变量 i 的值
    int j = 2012;                       //声明一个 int 类型的变量 j 并初始化为 2012
    j = j / 2;                          //改变变量 j 的值

在C#中,如果表达式最终的计算结果为所需的类型值,表达式就可以出现在需要值或对象的任意位置。

【例4.1】 创建一个控制台应用程序,声明两个int类型的变量i和j,并将其分别初始化为927和112,然后输出i*i+j*j的正弦值,代码如下。(实例位置:光盘\TM\sl\4\1)

    int i = 927;                                  //声明一个 int 类型的变量 i 并初始化为 927
    int j = 112;                                  //声明一个 int 类型的变量 j 并初始化为 112
    Console.WriteLine(Math.Sin(i*i+j*j));	  //表达式作为参数输出
    Console.ReadLine();

程序的运行结果为-0.599423085852245。

在上面的代码中,表达式i*i+j*j作为方法Math.Sin的参数来使用,同时,表达式Math.Sin(i*i+j*j)还是方法Console.WriteLine的参数。

4.2 运算符

运算符是一些特殊的符号,主要用于数学函数、一些类型的赋值语句和逻辑比较方面。C#中提供了丰富的运算符,如算术运算符、赋值运算符、比较运算符等。本节将向读者介绍这些运算符。

4.2.1 算术运算符

视频讲解:光盘\TM\lx\4\02 算术运算符.mp4

+、-、*、/和%运算符都称为算术运算符,分别用于进行加、减、乘、除和模(求余数)运算。C#中算术运算符的功能及使用方式如表4.1所示。

表4.1 C#算术运算符

其中,“+”和“-”运算符还可以作为数据的正负符号,如+5、-7。

1.加法运算符

加法运算符(+)通过两个数相加来执行标准的加法运算。

【例4.2】 创建一个控制台应用程序,声明两个整型变量M1和M2,并将M1赋值为927,然后使M2的值为M1与M1相加之后的值,代码如下。(实例位置:光盘\TM\sl\4\2)

    static void Main(string[] args)
    {
         int M1 = 927;                            //声明整型变量 M1,并赋值为 927
         int M2;                                  //声明整型变量 M2
         M2 = M1 + M1;                           //M2 的值为 M1 与 M1 相加之后的值
         Console.WriteLine(M2.ToString());
         Console.Read();
    }

程序的运行结果为1854。

说明

如果想要对整型变量M进行加1操作,可以用“M=M+1;”来实现,也可以用增量运算符(++)实现,如M++或++M。++M是前缀增量操作,该操作的结果是操作数加1之后的值;M++是后缀增量操作,该运算的结果是操作数增加之前的值。

2.减法运算符

减法运算符(-)通过从一个表达式中减去另外一个表达式的值来执行标准的减法运算。

【例4.3】 创建一个控制台应用程序,声明两个decimal类型变量R1和R2,并分别赋值为1112.82和9270.81,然后再声明一个decimal类型变量R3,使其值为R2减去R1之后得到的值,代码如下。(实例位置:光盘\TM\sl\4\3)

    static void Main(string[] args)
    {
         decimal R1 = (decimal)1112.82;        //声明整型变量 R1,并赋值为 1112.82
         decimal R2 = (decimal)9270.81;        //声明整型变量 R2,并赋值为 9270.81
         decimal R3;                           //声明整型变量 R3
         R3 = R2 - R1;                         //R3 的值为 R2 减去 R1 得到的值
         Console.WriteLine(R3.ToString());
         Console.Read();
     }

程序的运行结果为8157.99。

说明

如果想要对整型变量R进行减1操作,可以用“M=M-1;”来实现,也可以用减量运算符(--)实现,如R--或--R。--R是前缀减量操作,该操作的结果是操作数减1之后的值;R--是后缀减量操作,该运算的结果是操作数减少之前的值。

3.乘法运算符

乘法运算符(*)将两个表达式进行乘法运算并返回它们的乘积。

【例4.4】 创建一个控制台应用程序,声明两个整型变量ls1和ls2,并分别赋值为10和20。再声明一个整型变量sum,使其值为ls1和ls2的乘积,代码如下。(实例位置:光盘\TM\sl\4\4)

    static void Main(string[] args)
    {
         int ls1 = 10;                          //声明整型变量 ls1,并赋值为 10
         int ls2 = 20;                          //声明整型变量 ls2,并赋值为 20
         int sum;                               //声明整型变量 sum
         sum = ls1 * ls2;                       //使 sum 的值为 ls1 和 ls2 的乘积
         Console.WriteLine(sum.ToString());
         Console.Read();
    }

程序的运行结果为200。

4.除法运算符

除法运算符(/)执行算术除运算,它用被除数表达式除以除数表达式而得到商。

【例4.5】 创建一个控制台应用程序,声明两个整型变量shj1和shj2,并分别赋值为45和5。再声明一个整型变量ls,使其值为shj1除以shj2得到的值,代码如下。(实例位置:光盘\TM\sl\4\5)

    static void Main(string[] args)
    {
         int shj1 = 45;                          //声明整型变量 shj1,并赋值为 45
         int shj2 = 5;                           //声明整型变量 shj2,并赋值为 5
         int ls;                                 //声明整型变量 ls
         ls = shj1 / shj2;                       //使 ls 的值为 shj1 除以 shj2 得到的值
         Console.WriteLine(ls.ToString());
         Console.Read();
    }

程序的运行结果为9。

注意

在用算术运算符(+、-、*、/)运算时,产生的结果可能会超出所涉及数值类型的值的范围,这样,会导制运行结果不正确。

5.求余运算符

求余运算符(%)返回除数与被除数相除之后的余数,通常用这个运算符来创建余数在特定范围内的等式。

【例4.6】 创建一个控制台应用程序,声明两个整型变量I1和I2,并分别赋值为55和10。再声明一个整型变量I3,使其值为I1与I2求余运算之后的值,代码如下。(实例位置:光盘\TM\sl\4\6)

    static void Main(string[] args)
    {
         int I1 = 55;                     //声明整型变量 I1,并赋值为 55
         int I2 = 10;                     //声明整型变量 I1,并赋值为 10
         int I3;                          //声明整型变量 I3
         I3 = I1 % I2;                    //使 I3 等于 I1 与 I2 求余运算之后的值
         Console.WriteLine(I3.ToString());
         Console.Read();
    }

程序的运行结果为5。

说明

在获取两个数相除的余数时,也可以用Math类的DivRem方法来实现,如上述代码中的I3 = I1%I2可以写成Math.DivRem(I1,I2,out I3),I3中存储了I1和I2的余数。

闯关训练:根据第4.2.1节所学知识,制作一个简易的计算器程序,可以实现两个数的加、减、乘、除运算。

4.2.2 赋值运算符

视频讲解:光盘\TM\lx\4\03 赋值运算符.mp4

赋值运算符为变量、属性、事件等元素赋新值。赋值运算符主要有=、+=、-=、*=、/=、%=、&=、|=、^=、<<=和>>=运算符。赋值运算符的左操作数必须是变量、属性访问、索引器访问或事件访问类型的表达式,如果赋值运算符两边的操作数的类型不一致,就需要首先进行类型转换,然后再赋值。

在使用赋值运算符时,右操作数表达式所属的类型必须可隐式转换为左操作数所属的类型,运算将右操作数的值赋给左操作数指定的变量、属性或索引器元素。C#中的赋值运算符及其运算规则如表4.2所示。

表4.2 赋值运算符

下面以加赋值(+=)运算符为例,举例说明赋值运算符的用法。

【例4.7】 创建一个控制台应用程序,声明一个int类型的变量i,并初始化为927,然后通过加赋值运算符改变i的值,使其在原有的基础上增加112,代码如下。(实例位置:光盘\TM\sl\4\7)

    static void Main(string[] args)
    {
         int i = 927;                              //声明一个 int 类型的变量 i 并初始化为 927
         i += 112;                                 //使用加赋值运算符
         Console.WriteLine("最后i的值为:{0}",i);  //输出最后变量 i 的值
         Console.ReadLine();
    }

程序的运行结果为:

最后i的值为:1039

说明

在C#中可以把赋值运算符连在一起使用。如:

x = y = z = 5;

在这个语句中,变量x、y、z都得到同样的值5,但在程序开发中不建议使用这种赋值语法。

互动练习:尝试使用C#制作一个张家口冬奥会的倒计时程序,具体要求为:使用当前日期和张家口冬奥会的开幕日期相比较,得出天数,然后以图片的形式显示倒计时天数。

4.2.3 关系运算符

视频讲解:光盘\TM\lx\4\04 关系运算符.mp4

关系运算符属于二元运算符,用于程序中的变量之间、变量和自变量之间以及其他类型的信息之间的比较,它返回一个代表运算结果的布尔值。当运算符对应的关系成立时,运算结果为true,否则为false。所有关系运算符通常用在条件语句中来作为判断的依据。C#中的关系运算符共有6个,如表4.3所示。

关系运算符就好像对两个铁球进行比较,看看这两个铁球哪个大,重量是否相等,并给出一个“真”或“假”的值。

表4.3 关系运算符

下面对这几种关系运算符进行详细讲解。

1.相等运算符

要查看两个表达式是否相等,可以使用相等运算符(==)。相等运算符对整型、浮点型和枚举类型数据的操作是一样的。它只简单地比较两个表达式,并返回一个布尔结果。

【例4.8】 创建一个控制台应用程序,声明两个decimal类型变量L1和L2,并分别赋值为1981.00m和1982.00m,然后再声明一个bool类型变量result,使其值等于L1和L2进行相等运算后的返回值,代码如下。(实例位置:光盘\TM\sl\4\8)

    decimal L1 = 1981.00m;                //声明 decimal 类型变量 L1
    decimal L2 = 1982.00m;                //声明 decimal 类型变量 L2
    bool result;                          //声明 bool 类型变量 result
    //使result等于L1和L2进行相等运算后的返回值
    result = L1 == L2;
    Console.WriteLine(result);             //输出运行结果
    Console.ReadLine();

程序的运行结果为false。

2.不等运算符

不等运算符(!=)是与相等运算符相反的运算符,有两种格式的不等运算符可以应用到表达式,一种是普通的不等运算符(!=),另外一种是相等运算符的否定!(a==b)。通常,这两种格式可以计算出相同的值。

【例4.9】 创建一个控制台应用程序,声明两个decimal类型变量S1和S2,并分别赋值为1981.00m和1982.00m,然后再声明两个bool类型变量result和result1,使它们的值分别等于两种不等运算返回的值,代码如下。(实例位置:光盘\TM\sl\4\9)

    decimal S1 = 1981.00m;                //声明 decimal 类型变量 S1
    decimal S2 = 1982.00m;                //声明 decimal 类型变量 S2
    bool result;                          //声明 bool 类型变量 result
    bool result1;                         //声明 bool 类型变量 result1
    result = S1 != S2;                   //获取不等运算返回值第一种方法
    result1 = !(S1 == S2);               //获取不等运算返回值第二种方法
    Console.WriteLine(result);            //输出结果
    Console.WriteLine(result1);           //输出结果
    Console.ReadLine();

程序的运行结果为:

    true
    true
3.小于运算符

如果要比较一个值是否小于另外一个值,可以使用小于运算符(<)。当左边的表达式的值小于右边表达式的值时,结果是真;否则,结果是假。

【例4.10】 创建一个控制台应用程序,声明两个整型变量U1和U2,并分别赋值为1112和927,再声明一个bool类型变量result,使其值等于U1和U2进行小于运算的返回值,代码如下。(实例位置:光盘\TM\sl\4\10)

    int U1 = 1112;                           //声明整型变量 U1
    int U2 = 927;                            //声明整型变量 U2
    bool result;                             //声明 bool 型变量 result
    //使 result 等于 U1 和 U2 进行小于运算的返回值
    result = U1 < U2;
    Console.WriteLine(result);               //输出结果
    Console.ReadLine();

程序的运行结果为false。

说明

在用小于或大于运算符对值进行判断时,如果把判断符左右两边的值进行调换,其判断的结果也会随之改变。

4.大于运算符

如果比较一个值是否大于另外一个值,可以使用大于运算符(>)。当左边的表达式的值大于右边的表达式的值时,结果是真;否则,结果是假。

【例4.11】 创建一个控制台应用程序,声明两个整型变量F1和F2,并分别赋值为18和8,再声明一个bool类型变量result,使其值等于F1和F2进行大于运算的返回值,代码如下。(实例位置:光盘\TM\sl\4\11)

    int F1 = 18;                           //声明整型变量 F1
    int F2 = 8;                            //声明整型变量 F2
    bool result;                           //声明 bool 型变量 result
    //使 result 等于 F1 和 F2 进行大于运算的返回值
    result = F1 >F2;
    Console.WriteLine(result);              //输出结果
    Console.ReadLine();   

程序的运行结果为true。

5.小于等于运算符

如果要比较一个值是否小于或等于另外一个值,可以使用小于等于运算符(<=)。当左边表达式的值小于或等于右边表达式的值时,结果是真;否则,结果是假。

【例4.12】 创建一个控制台应用程序,声明两个整型变量X1和X2,并分别赋值为12和9,然后再声明一个bool类型变量result,使其值等于X1和X2进行小于等于运算的返回值,代码如下。(实例位置:光盘\TM\sl\4\12)

    int X1 = 12;                           //声明整型变量 X1
    int X2 = 9;                            //声明整型变量 X2
    bool result;                           //声明 bool 型变量 result
    //使 result 等于 X1 和 X2 进行小于或等于运算的返回值
    result = X2 <=X1;
    Console.WriteLine(result);             //输出结果
    Console.ReadLine();

程序的运行结果为true。

6.大于等于运算符

大于等于运算符(>=)用于查看某个值是否大于或等于另外一个值。当运算符左边表达式的值大于或等于右边表达式的值时,结果是真;否则,结果是假。

【例4.13】 创建一个控制台应用程序,声明两个整型变量T1和T2,并分别赋值为1112和927,再声明一个bool类型变量result,使其值等于T1和T2进行大于等于运算的返回值,代码如下。(实例位置:光盘\TM\sl\4\13)

    int T1 = 1112;                //声明整型变量 T1
    int T2 = 927;                 //声明整型变量 T2
    bool result;                  //声明 bool 型变量 result
    //使 result 等于 T1 和 T2 进行大于或等于运算的返回值
    result = T2 >=T1;
    Console.WriteLine(result);    //输出结果
    Console.ReadLine();

程序的运行结果为false。

说明

关系运算符一般常用于判断或循环语句中。

4.2.4 逻辑运算符

视频讲解:光盘\TM\lx\4\05 逻辑运算符.mp4

返回类型为布尔值的表达式,如关系运算符,可以被组合在一起构成一个更复杂的表达式,这是通过逻辑运算符来实现的。C#中的逻辑运算符主要包括&(&&)(逻辑与)、||(逻辑或)、!(逻辑非)。逻辑运算符的操作元必须是bool型数据。在逻辑运算符中,除了“!”是一元运算符之外,其他都是二元运算符。表4.4给出了逻辑运算符的用法和含义。

表4.4 逻辑运算符

结果为bool型的变量或表达式可以通过逻辑运算符组合为逻辑表达式。

用逻辑运算符进行逻辑运算时,结果如表4.5所示。

表4.5 使用逻辑运算符进行逻辑运算

逻辑运算符“&&”与“&”都表示“逻辑与”,那么它们之间的区别在哪里呢?从表4.5可以看出,当两个表达式都为true时,逻辑与的结果才会是true。使用逻辑运算符“&”会判断两个表达式;而逻辑运算符“&&”则是针对bool类型的类进行判断,当第一个表达式为false时则不去判断第二个表达式,直接输出结果从而节省计算机判断的次数。通常将这种在逻辑表达式中从左端的表达式可推断出整个表达式的值称为“短路”,而那些始终执行逻辑运算符两边的表达式称为“非短路”。“&&”属于“短路”运算符,而“&”则属于“非短路”运算符。

【例4.14】 创建一个控制台应用程序,在Main方法中创建整型变量,使用逻辑运算符对变量进行运算,并将运算结果输出。(实例位置:光盘\TM\sl\4\14)

    class Program
    {
        static void Main(string[] args)
        {
            int a = 2;                           //声明 int 型变量 a
            int b = 5;                           //声明 int 型变量 b
            //声明 boolean 型变量,用于保存应用逻辑运算符“&&”后的返回值
            bool result = ((a > b) && (a != b));
            //声明 boolean 型变量,用于保存应用逻辑运算符“||”后的返回值
            bool result2 = ((a > b) || (a != b));
            Console.WriteLine(result);            //将变量 result 输出
            Console.WriteLine(result2);           //将变量 result2 输出
        }
    }

程序的运行结果为:

    false
    true

4.2.5 位运算符

视频讲解:光盘\TM\lx\4\06 位运算符.mp4

位运算符除按位与、按位或运算符外,其他只能用于处理整数的操作数。位运算是完全针对位方面的操作。整型数据在内存中以二进制的形式表示,如int型变量7的二进制表示是00000000 00000000 00000000 00000111。

左边最高位是符号位,最高位若是0则表示正数,若为1则表示负数。负数采用补码表示,如-8的二进制表示为11111111 11111111 11111111 11111000,这样就可以对整型数据进行按位运算。

1.“按位与”运算

“按位与”运算的运算符为“&”,“按位与”运算的运算法则是:如果两个整型数据a、b对应位都是1,则结果位才是1,否则为0。如果两个操作数的精度不同,则结果的精度与精度高的操作数相同,如图4.1所示。

2.“按位或”运算

“按位或”运算的运算符为“|”,“按位或”运算的运算法则是:如果两个操作数对应位都是0,则结果位才是0,否则为1。如果两个操作数的精度不同,则结果的精度与精度高的操作数相同,如图4.2所示。

图4.1 5&4的运算过程

图4.2 3|6的运算过程

3.“按位取反”运算

“按位取反”运算也称“按位非”运算,运算符为“~”,为单目运算符。“按位取反”就是将操作数二进制中的1修改为0,0修改为1,如图4.3所示。

4.“按位异或”运算

“按位异或”运算的运算符是“^”,“按位异或”运算的运算法则是:当两个操作数的二进制表示相同(同时为0或同时为1)时,结果为0,否则为1。若两个操作数的精度不同,则结果数的精度与精度高的操作数相同,如图4.4所示。

5.移位操作

除了上述位运算符之外,还可以对数据按二进制位进行移位操作。C#中的移位运算符有以下两种。

图4.3 ~7的运算过程

图4.4 10^3的运算过程

 <<:左移。

 >>:右移。

图4.5 右移

对于X<<N或X>>N形式的运算,含义是将X向左或向右移动N位,得到的结果的类型与X相同。在此处,X的类型只能是int、uint、long或ulong, N的类型只能是int,或者显式转换为这些类型之一,否则编译程序时会出现错误。具体执行时,左移就是将左边的操作数在内存中的二进制数据左移右边操作数指定的位数,右边移空的部分补0。右移则复杂一些。当使用“>>”符号时,如果最高位是0,左移空的位就输入0;如果最高位是1,左移空的位就输入1,如图4.5所示。

技巧

移位可以实现整数除以或乘以2的n次方的效果。例如,y<<2与y*4的结果相同;y>>1的结果与y/2的结果相同。总之,一个数左移n位,就是将这个数乘以2的n次方;一个数右移n位,就是将这个数除以2的n次方。

【例4.15】 创建一个控制台应用程序,使变量intmax向左移位8次,并输出结果,代码如下。(实例位置:光盘\TM\sl\4\15)

    uint intmax = 4294967295;                 //声明 uint 类型变量 intmax
    uint bytemask;                            //声明 uint 类型变量 bytemask
    bytemask = intmax << 8;                  //使 intmax 左移 8 次
    Console.WriteLine(bytemask);              //输出结果
    Console.ReadLine();

程序的运行结果为4294967040。

闯关训练:使用“按位取反”运算符对纯数字的密码进行加密,例如,有一个密码为“123456”,则其对应的二进制数为“11110001001000000”,对其进行按位取反运算之后为“00001110110111111”,该二进制数对应的十进制数为“7615”,表示密码“123456”加密之后为“7615”。

4.2.6 其他特殊运算符

视频讲解:光盘\TM\lx\4\07 其他特殊运算符.mp4

C#还有一些运算符不能简单地归到某个类型中,下面对这些特殊的运算符进行详细讲解。

1.is运算符

is运算符用于检查变量是否为指定的类型。如果是,返回真;否则,返回假。

【例4.16】 创建一个控制台应用程序,判断整型变量i是否为整型,可以通过下面的代码进行判断,代码如下。(实例位置:光盘\TM\sl\4\16)

    int i = 0;                              //声明整型变量 i
    bool result = i is int;                //判断 i 是否为整型
    Console.WriteLine(result);              //输出结果
    Console.ReadLine();

因为i是整型,所以运行程序返回值为true。

注意

不能重载is运算符。is运算符只考虑引用转换、装箱转换和取消装箱转换。不考虑其他转换,如用户定义的转换。

2.条件运算符

条件运算符(?:)根据布尔型表达式的值返回两个值中的一个。如果条件为true,则计算第一个表达式并以它的计算结果为准;如果为false,则计算第二个表达式并以它的计算结果为准。使用格式为:

    条件式?值1:值2

【例4.17】 创建一个控制台应用程序,判断用户输入的年份是不是闰年,代码如下。(实例位置:光盘\TM\sl\4\17)

    static void Main(string[] args)
    {
         Console.Write("请输入一个年份:");                //屏幕输入提示字符串
         string str = Console.ReadLine();                  //获取用户输入的年份
    int year = Int32.Parse(str);                           //将输入的年份转换成 int 类型
    //计算输入的年份是否为闰年
    bool isleapyear=((year%400)==0)||(((year%4)==0)&&((year%100)!=0));
    //利用条件运算符输入“是”或者“不是”
         string yesno = isleapyear ? "是" : "不是";
         Console.WriteLine("{0}年{1}闰年",year,yesno);      //输出结果
         Console.ReadLine();
    }
3.new运算符

new运算符用于创建一个新的类型实例,它有以下3种形式。

 对象创建表达式,用于创建一个类类型或值类型的实例。

 数组创建表达式,用于创建一个数组类型实例。

 代表创建表达式,用于创建一个新的代表类型实例。

【例4.18】 创建一个控制台应用程序,使用new运算符创建一个数组,向数组中添加项目,然后输出数组中的项,代码如下。(实例位置:光盘\TM\sl\4\18)

    static void Main(string[] args)
    {
         string[] ls = new string[5];            //创建具有 5 个项目的 string 类型数组
         ls[0] = "ls1";                           //为数组第一项赋值
         ls[1] = "ls2";                           //为数组第二项赋值
         ls[2] = "ls3";                           //为数组第三项赋值
         ls[3] = "ls4";                           //为数组第四项赋值
         ls[4] = "ls5";                           //为数组第五项赋值
         Console.WriteLine(ls[0]);                //输出数组第一项
         Console.WriteLine(ls[1]);                //输出数组第二项
         Console.WriteLine(ls[2]);                //输出数组第三项
         Console.WriteLine(ls[3]);                //输出数组第四项
         Console.WriteLine(ls[4]);                //输出数组第五项
         Console.ReadLine();
    }

程序的运行结果如图4.6所示。

4.typeof运算符

typeof运算符用于获得系统原型对象的类型,也就是Type对象。Type类包含关于值类型和引用类型的信息。typeof运算符可以在C#语言中各种位置使用,以找出关于引用类型和值类型的信息。

【例4.19】 创建一个控制台应用程序,利用typeof运算符获取引用整型的信息,并输出结果,代码如下。(实例位置:光盘\TM\sl\4\19)

    static void Main(string[] args)
    {
         Type mytype = typeof(int);                      //获取 int 类型的原型对象
         Console.WriteLine("类型:{0}", mytype);         //输出结果
         Console.ReadLine();
    }

程序的运行结果如图4.7所示。

图4.6 输出结果

图4.7 运行结果

4.3 运算符优先级

视频讲解:光盘\TM\lx\4\08 运算符优先级.mp4

C#中的表达式是使用运算符连接起来的符合C#规范的式子,运算符的优先级决定了表达式中运算执行的先后顺序。运算符优先级其实就相当于进销存的业务流程,如进货、入库、销售、出库,只能按这个步骤进行操作。运算符的优先级也是这样的,它是按照一定的级别进行计算的,通常优先级由高到低的顺序依次是:

 增量和减量运算。

 算术运算。

 关系运算。

 逻辑运算。

 赋值运算。

如果两个运算符有相同的优先级,那么左边的表达式要比右边的表达式先被处理。在表达式中,可以通过括号()来调整运算符的运算顺序,将想要优先运算的运算符放置在括号()中。当程序开始执行时,括号()内的运算符会被优先执行。表4.6列出了所有运算符从高到低的优先级顺序。

表4.6 运算符的优先级顺序

技巧

在编写程序时尽量使用括号( )运算符来限定运算次序,以免产生错误的运算顺序。

互动练习:尝试使用C#制作一个根据出生日期显示其对应星座信息的程序,具体要求为:选择出生日期,然后显示该日期所对应的星座,以及该星座的幸运数字、星座特征、幸运日期、幸运颜色、幸运地点、个性特征、优点及缺点等信息。

4.4 小结

本章主要的内容是表达式与运算符,几种常用的运算符需要读者重点掌握。在应用程序开发中,运算符被频繁地使用,可见其重要性。最常见的运算符有算术运算符、赋值运算符、关系运算符、逻辑运算符、位运算符以及其他一些特殊的运算符。如果表达式中需要同时存在几个运算符,那么就必须考虑运算符的优先级顺序,优先级顺序高的要比优先级顺序低的先被执行。

4.5 实践与练习

(1)尝试开发一个程序,要求用+运算符进行加法和串联字符串“15”。(答案位置:光盘\TM\sl\4\20)

(2)尝试开发一个程序,要求使用?:运算符对变量进行赋值。(答案位置:光盘\TM\sl\4\21)

(3)尝试开发一个程序,分别通过++x和x++运算符求和。(答案位置:光盘\TM\sl\4\22)

4.6 动手纠错

(1)运行“光盘\TM\排错练习\04\01”文件夹下的程序,出现“未处理DivideByZeroException尝试除以零”的错误提示,请根据注释改正程序。

(2)运行“光盘\TM\排错练习\04\02”文件夹下的程序,对比程序中将“--”自减运算符放在操作数前后的运算结果。

(3)运行“光盘\TM\排错练习\04\03”文件夹下的程序,出现“无法将类型double隐式转换为int。存在一个显式转换(是否缺少强制转换?)”的错误提示,请根据注释改正程序。

(4)运行“光盘\TM\排错练习\04\04”文件夹下的程序,出现“无法将类型int隐式转换为bool”的错误提示,请根据注释改正程序。

(5)运行“光盘\TM\排错练习\04\05”文件夹下的程序,出现“System.Console是类型,但此处被当作变量来使用”的错误提示,请根据注释改正程序。

(6)运行“光盘\TM\排错练习\04\06”文件夹下的程序,出现“只有assignment、call、increment、decrement、await和new对象表达式可用作语句”的错误提示,请根据注释改正程序。

(7)运行“光盘\TM\排错练习\04\07”文件夹下的程序,出现“意外的字符“”的错误提示,请根据注释改正程序。

(8)运行“光盘\TM\排错练习\04\08”文件夹下的程序,出现“未将对象引用设置到对象的实例”的错误提示,请根据注释改正程序。

第5章 字符与字符串

第5章
字符与字符串

视频讲解:127分钟

本章主要介绍字符和字符串,C#中用Char、String等类来表示它们。C#中对于文字的处理大多是通过对字符和字符串的操作来实现的。本章详细地介绍字符与字符串的相关内容,讲解过程中为了便于读者理解结合了大量的举例。

通过阅读本章,您可以:

 了解什么是字符类

 掌握如何定义及使用字符类

 了解什么是字符串类

 掌握常见的几种字符串的处理方法

 了解字符类与字符串类的区别

 了解什么是可变字符串类

 掌握可变字符串类的定义及使用

5.1 字符类Char的使用

视频讲解:光盘\TM\lx\5\01 字符类Char的使用.mp4

5.1.1 Char类概述

Char类主要用来存储单个字符,占用16位(两个字节)的内存空间。在定义字符型变量时,要以单引号表示,如's'表示一个字符。而"s"则表示一个字符串,虽然其只有一个字符,但由于使用双引号,所以它仍然表示字符串,而不是字符。Char的定义非常简单,可以通过下面的代码定义字符。

    Char ch1='L';
    char ch2='1';

注意

Char只定义一个Unicode字符。Unicode字符是目前计算机中通用的字符编码,它为针对不同语言中的每个字符设定了统一的二进制编码,用于满足跨语言、跨平台的文本转换、处理的要求。

5.1.2 Char类的使用

Char类为开发人员提供了许多的方法,可以通过这些方法灵活地操作字符。Char类的常用方法及说明如表5.1所示。

表5.1 Char的常用方法及说明

可以看到Char提供了非常多的实用方法,其中以Is和To开头的比较重要。以Is开头的方法大多是判断Unicode字符是否为某个类别,以To开头的方法主要是转换为其他Unicode字符。

【例5.1】 创建一个控制台应用程序,演示如何使用Char类提供的常见方法,代码如下。(实例位置:光盘\TM\sl\5\1)

    static void Main(string[] args)
    {
         char a = 'a';                                                 //声明字符 a
         char b = '8';                                                 //声明字符 b
         char c = 'L';                                                 //声明字符 c
         char d = '.';                                                 //声明字符 d
         char e = '|';                                                 //声明字符 e
         char f = ' ';                                                 //声明字符 f
         //使用 IsLetter 方法判断 a 是否为字母
         Console.WriteLine("IsLetter 方法判断 a 是否为字母:{0}", Char.IsLetter(a));
         //使用 IsDigit 方法判断 b 是否为数字
         Console.WriteLine("IsDigit 方法判断 b 是否为数字:{0}", Char.IsDigit(b));
         //使用 IsLetterOrDigit 方法判断 c 是否为字母或数字
         Console.WriteLine("IsLetterOrDigit 方法判断 c 是否为字母或数字:{0}", Char.IsLetterOrDigit(c));
         //使用 IsLower 方法判断 a 是否为小写字母
         Console.WriteLine("IsLower 方法判断 a 是否为小写字母:{0}", Char.IsLower(a));
         //使用 IsUpper 方法判断 c 是否为大写字母
         Console.WriteLine("IsUpper 方法判断 c 是否为大写字母:{0}", Char.IsUpper(c));
         //使用 IsPunctuation 方法判断 d 是否为标点符号
         Console.WriteLine("IsPunctuation 方法判断 d 是否为标点符号:{0}", Char.IsPunctuation(d));
         //使用 IsSeparator 方法判断 e 是否为分隔符
         Console.WriteLine("IsSeparator 方法判断 e 是否为分隔符:{0}", Char.IsSeparator(e));
         //使用 IsWhiteSpace 方法判断 f 是否为空白
         Console.WriteLine("IsWhiteSpace 方法判断 f 是否为空白:{0}", Char.IsWhiteSpace(f));
         Console.ReadLine();
    }

程序的运行结果如图5.1所示。

图5.1 Char类常用方法的应用

5.1.3 转义字符

转义字符是一种特殊的字符变量,其以反斜线“\”开头,后跟一个或多个字符。转义字符具有特定的含义,不同于字符原有的意义,故称“转义”。例如,定义一个字符,而这个字符是单引号,如果不使用转义字符,则会产生错误。

转义字符就相当于一个电源变换器,电源变换器就是通过一定的手段获得所需的电源形式,例如交流变成直流、高电压变为低电压、低频变为高频等。转义字符也是,它是将字符转换成另一种操作形式,或是将无法一起使用的字符进行组合。

注意

转义符\(单个反斜杠)只针对后面紧跟着的单个字符进行操作。

【例5.2】 不使用转义字符定义字符,字符的值为单引号,产生错误,代码如下。

    static void Main(string[] args)               //Main 方法
    {
         char M=''';                              //声明一个字符变量,值为单引号
    }

程序的运行结果如图5.2所示。

图5.2 错误提示

【例5.3】 为了避免此错误,应该使用转义字符,代码如下。

    static void Main(string[] args)               //Main 方法
    {
         char a='\'';                             //使用转义字符定义字符的值为单引号
    }

此外还有其他转义字符,如表5.2所示。

表5.2 转义字符

说明

大多数重要的正则表达式语言运算符都是非转义的单个字符。转义符\(单个反斜杠)通知正则表达式分析器反斜杠后面的字符不是运算符。例如,分析器将r视为字符,而将后跟r的反斜杠(\r)视为回车功能。

【例5.4】 创建一个控制台应用程序,然后通过转义字符使Console.Write与Console.WriteLine有相同的效果,代码如下。(实例位置:光盘\TM\sl\5\2)

    static void Main(string[] args)                             //Main 方法
    {
         Console.WriteLine("用一生下载你");                     //通过 Console.WriteLine 输出字符串
         Console.Write("用一生下载你\n");                       //通过使用转义字符输出字符串
         Console.Write("用一生下载你\n 芸烨湘枫");              //通过使用转义字符输出字符串
         Console.ReadLine();
    }

程序的运行结果如图5.3所示。

图5.3 转义字符的使用

闯关训练:尝试使用Char字符实现字母与ASCII的相互转换,提示:在将字母转换为ASCII时,需要用到Encoding编码类;而在将ASCII转换为字母时,直接使用Char强制类型转换即可。

5.2 字符串类String的使用

前面的章节中介绍了Char类型,它只能表示单个字符,不能表示由多个字符连接而成的字符串。在C#语言中字符串作为对象来处理,可以通过String类来创建字符串对象。

5.2.1 字符串的声明及赋值

视频讲解:光盘\TM\lx\5\02 字符串的声明及赋值.mp4

在C#语言中,字符串必须包含在一对""(双引号)之内。例如:

    "23.23"、"ABCDE"、"你好"

这些都是字符串常量,字符串常量是系统能够显示的任何文字信息,甚至是单个字符。

注意

在C#中由" "号包围的都是字符串,不能作为其他数据类型来使用,如"1+2"的输出结果永远也不会是3。

可以通过以下语法格式来声明字符串变量:

    String str=[null]

 String:指定该变量为字符串类型。

 str:任意有效的标识符,表示字符串变量的名称。

 null:如果省略null,表示str变量是未初始化的状态,否则表示声明的字符串的值就等于null。

【例5.5】 声明字符串变量s,实例代码如下。

    String s;

说明

声明字符串变量必须经过初始化才能使用,否则编译器会报出“使用了未赋值的变量”。

声明字符串之后,需要对其进行赋值,字符串的赋值使用“=”,例如,声明一个字符串变量str,并为其赋值“C#编程词典”,代码如下:

    string str="C#编程词典";

5.2.2 连接多个字符串

视频讲解:光盘\TM\lx\5\03 连接多个字符串.mp4

使用“+”运算符可完成对多个字符串连接的功能。“+”运算符可以连接多个字符串并产生一个String对象。

【例5.6】 定义两个字符串,使用“+”运算符连接,代码如下。

    String s1 = "hello";              //声明 String 对象 s1
    String s2 = "world";              //声明 String 对象 s2
    String s = s1 + " " + s2;         //将对象 s1 和 s2 连接后的结果赋值给 s

技巧

C#中一句相连的字符串不能分开在两行中写。例如:

     Console.WriteLine("I like
     C#")

这种写法是错误的,如果一个字符串太长,为了便于阅读,可以将这个字符串分在两行上书写。此时就可以使用“+”将两个字符串连起来,之后在加号处换行。因此,上面的语句可以修改为:

     Console.WriteLine ("I like"+
     "C#");

5.2.3 比较字符串

视频讲解:光盘\TM\lx\5\04 比较字符串.mp4

对字符串值进行比较时,可以使用前面学过的比较运算符“==”实现。

【例5.7】 使用比较运算符比较两个字符串的值是否相等,实例代码如下。

    string str1 = "mingrikeji";
    string str2 = "mingrikeji";
    Console.WriteLine((str1 == str2));

此时,输出结果为true。

除了使用比较运算符“==”,在C#中最常见的比较字符串的方法还有Compare、CompareTo和Equals方法等,这些方法都归属于String类。下面对这3种方法进行详细的介绍。

1.Compare方法

Compare方法用来比较两个字符串是否相等,它有很多个重载方法,其中最常用的两种方法如下。

    Int compare(string strA,string strB)
    Int Compare(string strA,string strB,bool ignorCase)

 strA和strB:代表要比较的两个字符串。

 ignorCase:是一个布尔类型的参数,如果这个参数的值是true,那么在比较字符串时就忽略大小写的差别。Compare方法是一个静态方法,所以在使用时,可以直接引用。

【例5.8】 创建一个控制台应用程序,声明两个字符串变量,然后使用Compare方法比较两个字符串是否相等,代码如下。(实例位置:光盘\TM\sl\5\3)

    static void Main(string[] args)
    {
         string Str1 = "芸烨湘枫";                            //声明一个字符串 Str1
         string Str2 = "用一生下载你";                        //声明一个字符串 Str2
         Console.WriteLine(String.Compare(Str1, Str2));       //输出字符串 Str1 与 Str2 比较后的返回值
         Console.WriteLine(String.Compare(Str1, Str1));       //输出字符串 Str1 与 Str1 比较后的返回值
         Console.WriteLine(String.Compare(Str2, Str1));       //输出字符串 Str2 与 Str1 比较后的返回值
         Console.ReadLine();
    }

程序的运行结果为:

    1
    0
    -1

注意

比较字符串并非比较字符串长度的大小,而是比较字符串在英文字典中的位置。比较字符串按照字典排序的规则,判断两个字符串的大小。在英文字典中,前面的单词小于在后面的单词。

2.CompareTo方法

CompareTo方法与Compare方法相似,都可以比较两个字符串是否相等,不同的是CompareTo方法以实例对象本身与指定的字符串作比较,其语法如下。

    public int CompareTo(string strB)

【例5.9】 对字符串stra和字符串strb进行比较,代码如下。

    stra.CompareTo(strb)

如果stra的值与strb的值相等则返回0;如果stra的值大于strb的值,则返回1;否则返回-1。

【例5.10】 创建一个控制台应用程序,使用CompareTo方法比较两个字符串,代码如下。(实例位置:光盘\TM\sl\5\4)

    static void Main(string[] args)
    {
         string Str1 = "芸烨湘枫";                        //声明一个字符串 Str1
         string Str2 = "用一生下载你";                    //声明一个字符串 Str2
         Console.WriteLine(Str1.CompareTo(Str2));         //输出 Str1 与 Str2 比较后的返回值
         Console.ReadLine();
    }

由于字符串Str1在字典中的位置比字符串Str2的位置靠前,所以运行结果为1。

3.Equals方法

Equals方法主要用于比较两个字符串是否相同,如果相同返回值是true,否则为false,其常用的两种方式的语法如下。

    public bool Equals(string value)
    public static bool Equals(string a,string b)

 value:是与实例比较的字符串。

 a和b:是要进行比较的两个字符串。

【例5.11】 创建一个控制台应用程序,声明两个字符串变量,然后使用Equals方法比较两个字符串是否相同,代码如下。(实例位置:光盘\TM\sl\5\5)

    static void Main(string[] args)
    {
         string Str1 = "芸烨湘枫";                              //声明一个字符串 Str1
         string Str2 = "用一生下载你";                          //声明一个字符串 Str2
         Console.WriteLine(Str1.Equals(Str2));                  //用 Equals 方法比较字符串 Str1 和 Str2
         Console.WriteLine(String.Equals(Str1, Str2));          //用 Equals 方法比较字符串 Str1 和 Str2
         Console.ReadLine();
    }

程序的运行结果为:

    false
    false

说明

Equals方法执行顺序(区分大小写和区域性)比较。

5.2.4 格式化字符串

视频讲解:光盘\TM\lx\5\05 格式化字符串.mp4

在C#中,String类提供了一个静态的Format方法,用于将字符串数据格式化成指定的格式,其语法格式如下。

    Public static string Format(string format,object obj);

 format:用来指定字符串所要格式化的形式。

 obj:要被格式化的对象。

说明

format参数由零或多个文本序列与零或多个索引占位符混合组成,其中索引占位符称为格式项,它们与此方法的参数列表中的对象相对应。格式设置过程将每个格式项替换为相应对象值的文本表示形式。格式项的语法是{索引[,对齐方式][:格式字符串]},它指定了一个强制索引、格式化文本的可选长度和对齐方式,以及格式说明符字符的可选字符串,其中格式说明符字符用于控制如何设置相应对象的值的格式。

【例5.12】 创建一个控制台应用程序,声明两个string类型的变量strA和strB,然后使用Format方法格式化这两个string类型变量,最后输出格式化后的字符串,代码如下。(实例位置:光盘\TM\sl\5\6)

    static void Main(string[] args)
    {
         string StrA = "用一生下载你";                                     //声明字符串 StrA
         string StrB = "永不放弃";                                         //声明字符串 StrB
         string newstr = String.Format("{0},{1}!!!",StrA,StrB);            //格式化字符串
         Console.WriteLine(newstr);                                        //输出结果
         Console.ReadLine();
    }

程序的运行结果为“用一生下载你,永不放弃!!!”。

如果希望日期时间按照某种格式输出,那么可以使用Format方法将日期时间格式化成指定的格式。在C#中,已经提供了一些用于日期时间的格式规范,具体描述如表5.3所示。

表5.3 用于日期时间的格式规范

下面通过一个实例,演示如何使用日期时间的格式规范,以格式规范D为例。

【例5.13】 创建一个控制台应用程序,声明一个DateTime类型的变量dt,用于获取系统的当前日期时间,然后通过使用格式规范D将日期时间格式化为“YYYY年MM月dd日”的格式,代码如下。(实例位置:光盘\TM\sl\5\7)

    static void Main(string[] args)
    {
         DateTime dt = DateTime.Now;                     //获取系统当前日期
         string strB = String.Format("{0:D}", dt);       //格式化成短日期格式
         Console.WriteLine(strB);                        //输出日期
         Console.ReadLine();
    }

程序的运行结果为“2012年2月22日”。

互动练习:尝试使用C#制作一个商品金额大小写转换的程序,具体要求为:在一个文本框中输入小写的金额,单击“转换”按钮,自动转换为大写的金额表示方法。

5.2.5 截取字符串

视频讲解:光盘\TM\lx\5\06 截取字符串.mp4

String类提供了一个Substring方法,该方法可以截取字符串中指定位置和指定长度的子字符串,其语法格式如下。

    public string Substring(int startIndex,int length)

 startIndex:子字符串的起始位置的索引。

 length:子字符串中的字符数。

【例5.14】 创建一个控制台应用程序,声明两个string类型的变量StrA和StrB,并将StrA初始化为“用一生下载你”,然后使用Substring方法从索引1开始截取4个字符,赋值给StrB,并输出StrB,代码如下。(实例位置:光盘\TM\sl\5\8)

    static void Main(string[] args)
    {
         string StrA = "用一生下载你";         //声明字符串 StrA
         string StrB = "";                     //声明字符串 StrB
         StrB = StrA.Substring(1, 4);          //截取字符串
         Console.WriteLine(StrB);              //输出截取后的字符串
         Console.ReadLine();
    }

程序的运行结果为“一生下载”。

说明

在用Substring方法截取字符串时,如果length参数的长度大于截取字符串的长度,将从起始位置的索引处截取之后的所有字符。

闯关训练:从一个全路径的字符串中分离出文件路径、文件名及扩展名等信息。

5.2.6 分割字符串

视频讲解:光盘\TM\lx\5\07 分割字符串.mp4

String类提供了一个Split方法,用于分割字符串,此方法的返回值是包含所有分割子字符串的数组对象,可以通过数组取得所有分割的子字符串,其语法格式如下。

    public string[ ] split(params char[ ] separator);

separator:是一个数组,包含分隔符。

【例5.15】 创建一个控制台应用程序,声明一个string类型变量StrA,初始化为“用^一生#下载,你”,然后通过Split方法分割变量StrA,并输出分割后的字符串,代码如下。(实例位置:光盘\TM\sl\5\9)

    static void Main(string[] args)
    {
         string StrA = "用^一生#下载,你";              //声明字符串 StrA
         char[] separator ={ '^','#',',' };            //声明分割字符的数组
         String[] splitstrings = new String[100];      //声明一个字符串数组
         splitstrings = StrA.Split(separator);         //分割字符串
         for(int i = 0; i < splitstrings.Length; i++)
    {
         Console.WriteLine("item{0}:{1}", i, splitstrings[i]);
    }
    Console.ReadLine();
}

程序的运行结果为:

    item0:用
    item1:一生
    item2:下载
    item3:你

5.2.7 插入和填充字符串

视频讲解:光盘\TM\lx\5\08 插入字符串.mp4

1.插入字符串

String类提供了一个Insert方法,用于向字符串的任意位置插入新元素,其语法格式如下。

    public string Insert(int startIndex, string value);

 startIndex:用于指定所要插入的位置,索引从0开始。

 value:指定所要插入的字符串。

【例5.16】 创建一个控制台应用程序,声明3个string类型的变量str1、str2和str3。将变量str1初始化为“下载”,然后使用Insert方法在字符串str1的索引0处插入字符串“用一生”,并赋给字符串str2,最后在字符串str2的索引5处插入字符串“你”,赋给字符串str3,并输出str3,代码如下。(实例位置:光盘\TM\sl\5\10)

    static void Main(string[] args) //Main 方法
    {
         string str1 = "下载";                              //声明字符串变量 str1 并赋值为“下载”
         string str2;                                       //声明字符串变量 str2
         str2 = str1.Insert(0,"用一生");                    //使用 Insert 方法向字符串 str1 中插入字符串
         string str3 = str2.Insert(5,"你");                 //使用 Insert 方法向字符串 str2 中插入字符串
         Console.WriteLine(str3);                           //输出字符串变量 str3
         Console.ReadLine();
    }

程序的运行结果为“用一生下载你”。

说明

如果想要在字符串的尾部插入字符串,可以用字符串变量的Length属性来设置插入的起始位置。

2.填充字符串

String类提供了PadLeft/PadRight方法用于填充字符串,PadLeft方法在字符串的左侧进行字符填充,而PadRight方法在字符串的右侧进行字符填充。PadLeft方法的语法格式如下。

    public string PadLeft(int totalWidth,char paddingChar)

PadRight方法的语法格式如下。

    public string PadRight(int totalWidth,char paddingChar)

 totalWidth:指定填充后的字符串长度。

 paddingChar:指定所要填充的字符,如果省略,则填充空格符号。

【例5.17】 创建一个控制台应用程序,声明3个string类型的变量str1、str2和str3。将str1初始化为“*^__^*”,然后使用PadLeft方法在str1的左侧填充字符“(”,并赋给字符串str2。最后使用Pad Right方法在字符串str2的右侧填充字符“)”,最后得到字符串“(*^__^*)”,赋给字符串str3,并输出字符串str3,代码如下。(实例位置:光盘\TM\sl\5\11)

    static void Main(string[] args)
    {
         string str1 = "*^__^*";                         //声明一个字符串变量 str1
         //声明一个字符串变量 str2,并使用 PadLeft 方法在 str1 的左侧填充字符“(”
         string str2 = str1.PadLeft(7, '(');
         //声明一个字符串变量 str3,并使用 PadRight 方法在 str2 的右侧填充字符“)”
         string str3 = str2.PadRight(8, ')');
         Console.WriteLine("补充字符串之前:"+str1);    //输出字符串 str1
         Console.WriteLine("补充字符串之后:"+str3);    //输出字符串 str2
         Console.ReadLine();
    }

程序的运行结果为:

    补充字符串之前:*^__^*
    补充字符串之后:(*^__^*)

5.2.8 删除字符串

视频讲解:光盘\TM\lx\5\09 删除字符串.mp4

String类提供了一个Remove方法,用于从一个字符串的指定位置开始,删除指定数量的字符,其语法格式如下。

    Public String Remove( int startIndex);
    Public String Remove( int startIndex, int count);

 startIndex:用于指定开始删除的位置,索引从0开始。

 count:指定删除的字符数量。

注意

参数count的值不能为0或是负数(startIndex参数也不能为负数),如果为负数,将会引发ArgumentOutOfRangeException异常(当参数值超出调用的方法所定义的允许取值范围时引发的异常);如果为0,则删除无意义,也就是没有进行删除。

此方法有两种语法格式,第一种格式删除字符串中从指定位置到最后位置的所有字符。第二种格式从字符串中指定位置开始删除指定数目的字符。

【例5.18】 创建一个控制台应用程序,声明一个string类型的变量str1,并初始化为“用一生下载你”,然后使用Remove方法的第一种语法格式删除从索引3后面的所有字符,代码如下。(实例位置:光盘\TM\sl\5\12)

    static void Main(string[] args)           //Main 方法
    {
         string str1 = "用一生下载你";         //声明一个字符串变量 str1
         //声明一个字符串变量 str2,并使用 Remove 方法从字符串 str1 的索引 3 处开始删除
         string str2 = str1.Remove(3);
         Console.WriteLine(str2);              //输出字符串 str2
         Console.ReadLine();
    }

程序的运行结果为“用一生”。

下面再通过实例演示如何使用Remove方法的第二种语法格式。

【例5.19】 创建一个控制台应用程序,声明一个string类型变量str1,并初始化为“芸烨湘枫”,然后使用Remove方法的第二种语法格式从索引位置1开始,删除两个字符,代码如下。(实例位置:光盘\TM\sl\5\13)

    static void Main(string[] args)          //Main 方法
    {
         string str1 = "芸烨湘枫";            //声明一个字符串变量 str1,并初始化
         //声明一个字符串变量 str2,并使用 Remove 方法从字符串 str1 的索引 1 处开始删除两个字符
         string str2 = str1.Remove(1,2);
         Console.WriteLine(str2);             //输出字符串 str2
         Console.ReadLine();
    }

程序的运行结果为“芸枫”。

5.2.9 复制字符串

视频讲解:光盘\TM\lx\5\10 复制字符串.mp4

String类提供了Copy和CopyTo方法,用于将字符串或子字符串复制到另一个字符串或Char类型的数组中。

1.Copy方法

创建一个与指定的字符串具有相同值的字符串的新实例,其语法格式如下。

    public static string Copy(string str)

 str:是要复制的字符串。

 返回值:与str具有相同值的字符串。

【例5.20】 创建一个控制台应用程序,声明一个string类型的变量stra,并初始化为“芸烨湘枫”,然后使用Copy方法复制字符串stra,并赋给字符串strb,代码如下。(实例位置:光盘\TM\sl\5\14)

    string stra = "芸烨湘枫";             //声明一个字符串变量 stra 并初始化
    string strb;                          //声明一个字符串变量 strb
    //使用 String 类的 Copy 方法,复制字符串 stra 并赋值给 strb
    strb = String.Copy(stra);
    Console.WriteLine(strb);              //输出字符串 strb
    Console.ReadLine();

程序的运行结果为“芸烨湘枫”。

2.CopyTo方法

CopyTo方法的功能与Copy方法基本相同,但是CopyTo方法可以将字符串的某一部分复制到另一个数组中,其语法格式如下。

    Public void CopyTo(int sourceIndex,char[ ]destination,int destinationIndex,int count);

参数说明如表5.4所示。

表5.4 CopyTo方法的参数及说明

注意

当参数sourceIndex、destinationIndex或count为负数,或者参数count大于从startIndex到此实例末尾的子字符串的长度,或者参数count大于从destinationIndex到destination末尾的子数组的长度时,则引发ArgumentOutOfRangeException异常(当参数值超出调用的方法所定义的允许取值范围时引发的异常)。

下面通过实例演示如何使用CopyTo方法。

【例5.21】 创建一个控制台应用程序,声明一个string类型变量str1,并初始化为“用一生下载你”。然后声明一个Char类型的数组str,使用CopyTo方法将“一生下载”复制到数组str中,代码如下。(实例位置:光盘\TM\sl\5\15)

    static void Main(string[] args)
    {
         string str1 = "用一生下载你";          //声明一个字符串变量 str1 并初始化
         char[] str=new char[100];              //声明一个字符数组 str
         //将字符串 str 从索引 1 开始的 4 个字符串复制到字符数组 str 中
         str1.CopyTo(1,str,0,4);
         Console.WriteLine(str);                //输出字符数组中的内容
         Console.ReadLine();
    }

程序的运行结果为“一生下载”。

5.2.10 替换字符串

视频讲解:光盘\TM\lx\5\11 删除字符串.mp4

String类提供了一个Replace方法,用于将字符串中的某个字符或字符串替换成其他的字符或字符串,其语法格式如下。

    public string Replace(char OChar,char NChar)
    public string Replace(string OValue,string NValue)

参数说明如表5.5所示。

表5.5 Replace方法的参数及说明

第一种语法格式主要用于替换字符串中指定的字符,第二种语法格式主要用于替换字符串中指定的字符串,下面通过实例演示这两种语法格式的用法。

【例5.22】 创建一个控制台应用程序,声明一个string类型变量a,并初始化为“one world,one dream”。然后使用Replace方法的第一种语法格式将字符串中的“,”替换成“*”,最后使用Replace方法的第二种语法格式将字符串中的“one world”替换成“One World”,代码如下。(实例位置:光盘\TM\sl\5\16)

    static void Main(string[] args)
    {
         string a = "one world,one dream";            //声明一个字符串变量 a 并初始化
         //使用 Replace 方法将字符串 a 中的“,”替换为“*”,并赋值给字符串变量 b
         string b = a.Replace(',','*');
         Console.WriteLine(b);                        //输出字符串变量 b
         //使用 Replace 方法将字符串 a 中的“one world”替换为“One World”
         string c = a.Replace("one world", "One World");
         Console.WriteLine(c);                        //输出字符串变量 c
         Console.ReadLine();
    }

程序的运行结果为:

    one world*one dream
    One World,one dream

互动练习:尝试使用C#制作一个对字符串进行加密与解密的程序,主要调用已有类库中的ToEncrypt方法和ToDecrypt方法实现。

5.3 可变字符串类

视频讲解:光盘\TM\lx\5\12 可变字符串类.mp4

对于创建成功的字符串对象,它的长度是固定的,内容不能被改变和编译。虽然使用“+”可以达到附加新字符或字符串的目的,但“+”会产生一个新的String实例,会在内存中创建新的字符串对象。如果重复地对字符串进行修改,将极大地增加系统开销。而C#中提供了一个可变的字符序列StringBuilder类,大大提高了频繁增加字符串的效率。本节将对其使用进行讲解。

5.3.1 StringBuilder类的定义

StringBuilder类有6种不同的构造方法,本节只介绍最常用的一种,其语法格式如下。

    public StringBuilder(string value,int cap)

 value:StringBuilder对象引用的字符串。

 cap:设定StringBuilder对象的初始大小。

【例5.23】 创建一个StringBuilder对象,其初始引用的字符串为“Hello World!”,代码如下。

    StringBuilder MyStringBuilder = new StringBuilder("Hello World!");

说明

类表示值为可变字符序列的类似字符串的对象。之所以说值是可变的,是因为在通过追加、移除、替换或插入字符而创建它后可以对它进行修改。

5.3.2 StringBuilder类的使用

StringBuilder类存在于System.Text命名空间中,如果要创建StringBuilder对象,首先必须引用此命名空间。StringBuilder类中常用的几个操作字符串的方法如表5.6所示。

表5.6 StringBuilder类中的常用方法及说明

下面通过实例来演示如何使用StringBuilder类中的这5种方法。

【例5.24】 创建一个控制台应用程序,声明一个int类型的变量Num,并初始化为1000,然后创建一个StringBuilder对象LS,其初始值为“用一生下载你”,初始大小为100。最后使用StringBuilder类的Append、AppendFormat、Insert、Remove和Replace方法操作StringBuilder对象,代码如下。(实例位置:光盘\TM\sl\5\17)

    static void Main(string[] args)
    {
         int Num = 1000;                        //声明一个 int 类型变量 Num 并初始化为 1000
         //实例化一个 StringBuilder 类,并初始化为“用一生下载你”
         StringBuilder LS = new StringBuilder("用一生下载你",100);
         LS.Append("VS 芸烨湘枫");              //使用 Append 方法将字符串追加到 LS 的末尾
         Console.WriteLine(LS);                 //输出 LS
         //使用 AppendFormat 方法将字符串按照指定的格式追加到 LS 的末尾
         LS.AppendFormat("{0:C}",Num);
         Console.WriteLine(LS);                 //输出 LS
         LS.Insert(0,"名称:");                 //使用 Insert 方法将“名称:”追加到 LS 的开头
         Console.WriteLine(LS);                 //输出 LS
         //使用 Remove 方法从 LS 中删除索引 15 以后的字符串
         LS.Remove(15,LS.Length-15);
         Console.WriteLine(LS);                 //输出 LS
         //使用 Replace 方法将“名称:”替换成“一生所爱”
         LS.Replace("名称", "一生所爱");
         Console.WriteLine(LS);                 //输出 LS
         Console.ReadLine();
    }

程序的运行结果如图5.4所示。

图5.4 StringBuilder类中几种方法的应用

5.3.3 StringBuilder类与String类的区别

String对象是不可改变的,每次使用String类中的方法时,都要在内存中创建一个新的字符串对象,这就需要为该新对象分配新的空间。在需要对字符串执行重复修改的情况下,与创建新的String对象相关的系统开销可能会非常昂贵。如果要修改字符串而不创建新的对象,则可以使用StringBuilder类。例如,当在一个循环中将许多字符串连接在一起时,使用StringBuilder类可以提升性能。

【例5.25】 创建一个控制台应用程序,在主方法中编写如下代码,验证字符串操作和可变字符串操作的效率。(实例位置:光盘\TM\sl\5\18)

    static void Main(string[] args)
    {
         String str = "";                                         //创建空字符串
         //定义对字符串执行操作的起始时间
         long starTime = DateTime.Now.Millisecond;
         for (int i = 0; i < 10000; i++)
         {                                                        //利用 for 循环执行 10000 次操作
               str = str + i;                                     //循环追加字符串
         }
         long endTime = DateTime.Now.Millisecond;                 //定义对字符串操作后的时间
         long time = endTime - starTime;                          //计算对字符串执行操作的时间
         Console.WriteLine("String 消耗时间:" + time);           //将执行的时间输出
         StringBuilder builder = new StringBuilder("");           //创建字符串生成器
         starTime = DateTime.Now.Millisecond;                     //定义操作执行前的时间
         for (int j = 0; j < 10000; j++)
         {                                                        //利用 for 循环进行操作
               builder.Append(j);                                 //循环追加字符
          }
         endTime = System.DateTime.Now.Millisecond;               //定义操作后的时间
         time = endTime - starTime;                               //追加操作执行的时间
         Console.WriteLine("StringBuilder 消耗时间:" + time);    //将操作时间输出
         Console.ReadLine();
    }

运行结果如图5.5所示。

图5.5 验证字符串操作和可变字符串操作的效率

通过这一实例可以看出,两者执行的时间差距很大。如果在程序中频繁附加字符串,建议使用StringBuilder。

闯关训练:制作一个程序,要求根据标点符号对字符串进行分行操作。

5.4 小结

本章介绍了用于文本处理的Char、String和StringBuilder类。在介绍这3个类时,结合了大量的实例进行讲解,使读者能够从实例中掌握每种类的用法。在学习本章时,读者要重点掌握String类中处理字符串的一些方法,这些方法在开发程序时会经常用到。StringBuilder类允许使用同一个字符串对象进行字符串的维护操作,这样,可以在操作字符串数据的过程中提高效率,尤其是处理大量文字数据时。

5.5 实践与练习

(1)尝试开发一个程序,要求将字符串中的每个字符颠倒输出。(答案位置:光盘\TM\sl\5\19)

(2)尝试开发一个程序,要求去掉字符串中的所有空格。(答案位置:光盘\TM\sl\5\20)

(3)尝试开发一个程序,主要实现从字符串中分离文件路径、文件名及扩展名的功能。(答案位置:光盘\TM\sl\5\21)

5.6 动手纠错

(1)运行“光盘\TM\排错练习\05\01”文件夹下的程序,出现“无法将类型string隐式转换为char”的错误提示,请根据注释改正程序。

(2)运行“光盘\TM\排错练习\05\02”文件夹下的程序,出现“与Test.Program.ShowInfo(string)最匹配的重载方法具有一些无效参数”的错误提示,请根据注释改正程序。

(3)运行“光盘\TM\排错练习\05\03”文件夹下的程序,出现“字符文本中的字符太多”的错误提示,请根据注释改正程序。

(4)运行“光盘\TM\排错练习\05\04”文件夹下的程序,出现“未处理ArgumentOutOfRangeException索引和长度必须引用该字符串内的位置。”的错误提示,请根据注释改正程序。

(5)运行“光盘\TM\排错练习\05\05”文件夹下的程序,出现“无法识别的转义序列”的错误提示,请根据注释改正程序。

(6)运行“光盘\TM\排错练习\05\06”文件夹下的程序,出现“未处理NullReferenceException未将对象引用设置到对象的实例。”的错误提示,请根据注释改正程序。

(7)运行“光盘\TM\排错练习\05\07”文件夹下的程序,出现“与string.PadLeft(int,char)最匹配的重载方法具有一些无效参数”的错误提示,请根据注释改正程序。

第6章 流程控制语句

第6章
流程控制语句

视频讲解:109分钟

流程控制对于任何一门编程语言来说都是至关重要的,它提供了控制程序步骤的基本手段。如果没有流程控制语句,整个程序将按照线性的顺序来执行,不能根据用户的输入决定执行的序列。本章将对C#中的流程控制语句进行详细讲解。

通过阅读本章,您可以:

 掌握if条件语句的使用方法

 了解if语句与switch语句的区别

 掌握while循环语句的使用方法

 掌握do…while循环语句的使用方法

 了解while语句与do…while语句的区别

 掌握for语句的使用方法

 熟悉foreach语句

 了解常用的几种跳转语句的使用方法

6.1 条件判断语句

条件判断语句用于根据某个表达式的值从若干条给定语句中选择一个来执行。条件判断语句包括if语句和switch语句两种,下面对这两种条件判断语句进行详细讲解。

条件判断语句执行过程就好像在商场买东西时,是拿现金还是刷卡。如果刷卡,是用信用卡,还是银行卡,它是对事物的一个选择过程。

6.1.1 if语句

视频讲解:光盘\TM\lx\6\01 if语句.mp4

使用if条件语句,可选择是否要执行紧跟在条件之后的那个语句。关键字if之后是作为条件的“布尔表达式”,如果该表达式返回的结果为true,则执行其后的语句;若为false,则不执行if条件之后的语句。if条件语句可分为简单的if条件语句、if…else语句和if…else if多分支语句。

1.简单的if条件语句

语法如下:

    if(布尔表达式)
    {
         语句序列
    }

 布尔表达式:必要参数,表示它最后返回的结果必须是一个布尔值。它可以是一个单纯的布尔变量或常量,也可以是使用关系或布尔运算符的表达式。

 语句序列:可选参数。可以是一条或多条语句,当表达式的值为true时执行这些语句。若语句块中仅有一条语句,则可以省略条件语句中的“{}”。

【例6.1】 使用if语句判断变量i是否大于927,如果返回值为true,则输出字符串,代码如下。

    int i = 928;                     //声明一个 int 类型变量 i
    if (i > 927)                     //调用 if 语句判断 i 是否大于 927
    {
          Console.WriteLine("i 大于 927");
    }

说明

虽然if后面的复合语句块只有一条语句,省略“{}”并无语法错误,但为了增强程序的可读性最好不要省略。

简单的if条件语句的执行过程如图6.1所示。

2.if...else语句

if...else语句是条件语句中最常用的一种形式,它会针对某种条件有选择地作出处理。通常表现为“如果满足某种条件,就进行某种处理,否则就进行另一种处理”。

语法如下:

    if(布尔表达式)
    {
         语句序列
    }
    else
    {
         语句序列
    }

if后面()内的表达式的值必须是bool型的。如果表达式的值为true,则执行紧跟if语句的复合语句;如果表达式的值为false,则执行else后面的复合语句。if...else语句的执行过程如图6.2所示。

图6.1 if条件语句的执行过程

图6.2 if...else语句的执行过程

技巧

对于if…else语句可以使用三元条件运算符对语句进行简化,如下面的代码:

      if(a > 0)
         b = a;
      else
         b = -a;

可以简写成:

      b = a > 0?a:-a;

上段代码为求绝对值的语句,如果a > 0,就把a的值赋值给变量b,否则将-a赋值给变量b。也就是问号“?”前面的表达式为真,则将问号与冒号之间的表达式的计算结果赋值给变量b,否则将冒号后面的表达式的计算结果赋值给变量b。使用三元运算符的好处是可以使代码简洁,并且有一个返回值。

【例6.2】 创建一个控制台应用程序,声明一个int类型的变量i,将其初始化为927。然后通过if…else语句判断这个i值是否大于927,如果大于则输出“i大于927”,否则执行else子句,输出“i不大于927”,代码如下。(实例位置:光盘\TM\sl\6\1)

   static void Main(string[] args)
   {
        int i = 927;                           //声明一个 int 类型变量 i
        if (i > 927)                           //调用 if 语句判断 i 是否大于 927
        {
            Console.WriteLine("i 大于 927");    //如果大于 927 则输出“i 大于 927”
        }
        else                                    //否则
        {
            Console.WriteLine("i 不大于 927");  //输出“i 不大于 927”
        }
        Console.ReadLine();
  }

程序的运行结果为“i不大于927”。

闯关训练:使用if...else语句实现一个报销业务花销的程序,具体实现时,判断支出是否为业务花销,如果是业务花销,则输出“正常报销!”;否则,如果不是业务花销,则输出“不符合规定报销”。

3.if...else if多分支语句

if...else if多分支语句用于针对某一事件的多种情况进行处理。通常表现为“如果满足某种条件,就进行某种处理,否则如果满足另一种条件则执行另一种处理”。

语法如下:

   if(条件表达式 1){
   语句序列 1
   }
   else if(条件表达式 2){
     语句序列 2
   }
   …
   else if(条件表达式 n){
         语句序列 n
   }

 条件表达式1~条件表达式n:必要参数。可以由多个表达式组成,但最后返回的结果一定要为bool类型。

 语句序列:可以是一条或多条语句,当条件表达式1的值为true时,执行语句序列1;当条件表达式2的值为true时,执行语句序列2,依此类推。当省略任意一组语句序列时,可以保留其外面的“{}”,也可以将“{}”替换为“;”。

if...else if多分支语句的执行过程如图6.3所示。

图6.3 if...else if多分支语句执行过程

【例6.3】 创建一个控制台应用程序,使用if...else if多分支语句实现根据用户输入的年龄,输出相应的字符串,代码如下。(实例位置:光盘\TM\sl\6\2)

程序的运行结果如图6.4所示。

图6.4 if...else if多分支语句的使用

说明

在if语句中可以使用return语句,用于退出if语句所在的类的方法。

6.1.2 switch多分支语句

视频讲解:光盘\TM\lx\6\02 switch多分支语句.mp4

switch语句是多分支条件判断语句,它根据表达式的值使程序从多个分支中选择一个用于执行的分支。switch语句的基本格式如下。

    switch(【表达式】)
    {
         case 【常量表达式】:【语句块】
         break;
         case 【常量表达式】:【语句块】
         break;
         …
         case 【常量表达式】:【语句块】
         default:【语句块】
         break;
    }

switch关键字后面的括号()中是条件表达式,大括号{}中的程序代码是由多个case子句组成的。每个case关键字后面都有语句块,这些语句块都是switch语句可能执行的语句块。如果符合条件值,则case下的语句块就会被执行,语句块执行完毕后,紧接着会执行break语句,使程序跳出switch语句。在switch语句中,【表达式】的类型必须是sbyte、byte、short、ushort、int、uint、long、ulong、char、string或者枚举类型中的一种。【常量表达式】的值必须是与【表达式】的类型兼容的常量,并且在一个switch语句中,不同case关键字后面的【常量表达式】必须不同。如果指定了相同的【常量表达式】,会导致编译时出错。一个switch语句中只能有一个default标签。在switch语句中,在case子句的语句块后经常使用break语句,其主要作用是跳出switch语句。下面通过实例演示如何使用break语句跳出switch语句。

说明

switch语句可以包括任意数目的case子句,但是任何两个case语句都不能具有相同的值。

switch语句的执行过程如图6.5所示。

图6.5 switch语句的执行过程

【例6.4】 创建一个控制台应用程序,声明一个string类型变量MyStr,并初始化为“用一生下载你”,然后switch语句根据这个变量选择要执行的语句,最后使用break语句跳出switch语句,代码如下。(实例位置:光盘\TM\sl\6\3)

    static void Main(string[] args)
    {
         string MyStr = "用一生下载你";                       //声明一个字符串变量 MyStr 并初始化
         switch (MyStr)                                       //调用 switch 语句
                           {
                               //如果 MyStr 的值是“用一生下载你”,执行分支 1
                              case "用一生下载你": Console.WriteLine("用一生下载你"); break;
                              //如果 MyStr 的值是“一生所爱”,执行分支 2
                              case"一生所爱": Console.WriteLine("一生所爱"); break;
                              //如果 MyStr 的值是“芸烨湘枫”,执行分支 3
                              case "芸烨湘枫": Console.WriteLine("芸烨湘枫"); break;
                              //如果 MyStr 的值都不符合以上分支的内容,则执行 default 语句
                              default: Console.WriteLine("无字符"); break;
        }
        Console.ReadLine();
    }

程序的运行结果为“用一生下载你”。

注意

在switch语句中,case语句后常量表达式的值可以为整数,但绝不可以是其他实数。例如,下面的代码就是不合法的:

      case 1.1;

在许多情况下,switch语句可以简化if…else语句,而且执行效率更高。

【例6.5】 创建一个控制台应用程序,如果使用if…else语句判断用户输入的月份所在的季节,是非常繁琐的。而使用switch语句进行判断,相对就变得非常简单明了,代码如下。(实例位置:光盘\TM\sl\6\4)

程序的运行结果如图6.6所示。

图6.6 根据输入的月份输出相应的季节

互动练习:尝试使用C#制作判断用户登录身份的程序,用户的登录身份包括管理员、员工、普通用户3种,如果用户名为admin,则为管理员;如果用户名为mr,则为员工,如果用户名不是admin或者mr,则为普通用户。

6.2 循环语句

循环语句主要用于重复执行嵌入语句,在C#中,常见的循环语句有while语句、do…while语句、for语句和foreach语句。下面将对这几种循环语句做详细讲解。

6.2.1 while语句

视频讲解:光盘\TM\lx\6\03 while语句.mp4

while语句用于根据条件值执行一条语句零次或多次,当每次while语句中的代码执行完毕时,将重新查看是否符合条件值,若符合则再次执行相同的程序代码,否则跳出while语句,执行其他程序代码。

1.基本的while循环

while语句的基本格式如下。

    while(【布尔表达式】)
    {
         【语句块】
    }

while语句的执行顺序如下:

图6.7 while语句的执行过程

 计算【布尔表达式】的值。

 如果【布尔表达式】的值为true,程序执行【语句块】。执行完毕重新计算【布尔表达式】的值是否为true。

 如果【布尔表达式】的值为false,则控制将转移到while语句的结尾。

while语句在现实生活中就相当于公园中的木马,当按下“启动”按钮时(也就是布尔表达式设置为true),木马将不停地转动。如果按下“停止”按钮(也就是布尔表达式设为false),木马将停止转动。while循环语句的执行过程如图6.7所示。

下面通过实例演示如何使用while语句。

【例6.6】 创建一个控制台应用程序,声明一个string类型的数组,并初始化数组,然后通过while语句循环输出数组中的所有成员,代码如下。(实例位置:光盘\TM\sl\6\5)

程序的运行结果为:

    2014 年 NBA 总冠军马刺队主力队员:
    蒂姆·邓肯
    马努·吉诺比利
    托尼·帕克
    卡哇伊·莱昂纳德
    伯瑞斯·迪奥
    丹尼·格林
    帕蒂·米尔斯

注意

初学者经常犯的一个错误就是在while表达式的括号后加“;”。如:

      while(x = = 5); 
      Console.WriteLine ("x 的值为 5");

这时,程序会认为要执行一条空语句,从而进入无限循环,C#编译器又不会报错,这可能会浪费很多时间去调试,读者应注意这个问题。

2.跳出或执行下一次循环

在while语句的嵌入语句块中,break语句用于将控制转到while语句的结束点,而continue语句可用于将控制直接转到下一次循环。

说明

在循环语句中,可以通过goto、return或throw语句退出。

【例6.7】 创建一个控制台应用程序,声明两个int类型的变量s和num,分别初始化为0和80。然后通过while语句循环输出,当s大于40时,使用break语句终止循环。当s是偶数时,使用continue语句将程序转到下一次循环,从而实现输出40以内的所有奇数,代码如下。(实例位置:光盘\TM\sl\6\6)

    static void Main(string[] args)
    {
         int s = 0, num = 80;                   //声明两个 int 类型的变量并初始化
         while (s < num)                        //调用 while 语句,当 s 小于 num 时执行
         {
             s++; //s 自增 1
             if (s > 40)                        //使用 if 语句判断 s 是否大于 40
             {
                 break;                         //使用 break 语句终止循环
             }
             if ((s % 2) == 0)                  //调用 if 语句判断 s 是否为偶数
             {
                 continue;                      //使用 continue 语句将程序转到下一次循环
             }
             Console.WriteLine(s);              //输出 s
         }
         Console.ReadLine();
    }

程序的运行结果为“1~39的所有奇数”。

闯关训练:有一个经典的面试题,有一组数1、1、2、3、5、8、13、21、34…要求用递归算法算出这组数的第30个数是多少?请用C#编程实现。

6.2.2 do…while语句

视频讲解:光盘\TM\lx\6\04 do...while语句.mp4

do...while循环语句与while循环语句类似,它们之间的区别是:while语句为先判断条件是否成立再执行循环体,而do...while循环语句则先执行一次循环后,再判断条件是否成立。也就是说do...while循环语句中“{}”中的程序段至少要被执行一次。do...while循环语句基本形式如下。

    do
    {
          【语句块】
    }while(【布尔表达式】);

do…while语句的执行顺序如下。

图6.8 do…while循环语句的执行过程

 程序首先执行【语句块】。

 当程序到达【语句块】的结束点时,计算【布尔表达式】的值。如果【布尔表达式】的值是true,程序转到do…while语句的开头;否则,结束循环。

与while语句的一个明显区别是do...while语句在结尾处多了一个分号(;)。根据do...while循环语句的语法特点总结出do...while循环语句的执行过程,如图6.8所示。

【例6.8】 创建一个控制台应用程序,定义一个string类型的数组,并初始化数组,然后使用do…while语句循环输出数组中的值,代码如下。(实例位置:光盘\TM\sl\6\7)

    static void Main(string[] args)
    {
         string[] myArray = new string[3] { "世界杯", "欧洲杯", "欧冠" }; //声明一个 string 数组并初始化
         int i = 0;
         do                                                               //调用 do...while 语句
         {
                 Console.WriteLine(myArray[i]);                           //输出数组中数据
                 i++;
         } while (i < myArray.Length);                                    //设置 do...while 语句的条件
         Console.ReadLine();
    }

程序的运行结果为:

    世界杯
    欧洲杯
    欧冠

6.2.3 for语句

视频讲解:光盘\TM\lx\6\05 for语句.mp4

for语句是C#程序设计中最有用的循环语句之一。for语句用于计算一个初始化序列,然后当某个条件为真时,重复执行嵌套语句并计算一个迭代表达式序列。如果为假,则终止循环,退出for循环。for语句的基本形式如下。

    for(【初始化表达式】;【条件表达式】;【迭代表达式】)
    {
         【语句块】
    }

【初始化表达式】由一个局部变量声明或者由一个逗号分隔的表达式列表组成。用【初始化表达式】声明的局部变量的作用域从变量的声明开始,一直到嵌入语句的结尾。【条件表达式】必须是一个布尔表达式。【迭代表达式】必须包含一个用逗号分隔的表达式列表。

for语句的执行原理就好像是复印机复印纸张一样,可以在复印机上设置要复印的张数,也就是设置循环条件,然后开始复印。当复印的张数等于设置的张数时,也就是循环条件为假时,将停止复印。

for语句执行的顺序如下。

 如果有【初始化表达式】,则按变量初始值设定项或语句表达式的书写顺序指定它们,此步骤只执行一次。

 如果存在【条件表达式】,则计算它。

 如果不存在【条件表达式】,则程序将转移到嵌入语句。如果程序到达了嵌入语句的结束点,按顺序计算for循环表达式,然后从上一个步骤中for条件的计算开始,执行另一次循环。

for循环语句的执行过程如图6.9所示。

说明

在应用for循环体时,循环体中的3个条件不能为空,若为空,如for( ; ; ),for语句将出现死循环。

【例6.9】 创建一个控制台应用程序,首先声明一个int类型的数组,然后向数组中添加10个值,最后使用for循环语句遍历数组,并将数组中的值输出,代码如下。(实例位置:光盘\TM\sl\6\8)

    static void Main(string[] args)
    {
         int[] myint = new int[10];             //声明一个具有 10 个元素的整型数组
         myint[0] = 0;                          //向数组中添加第一个元素
         myint[1] = 1;                          //向数组中添加第二个元素
         myint[2] = 2;                          //向数组中添加第三个元素
         myint[3] = 3;                          //向数组中添加第四个元素
         myint[4] = 4;                          //向数组中添加第五个元素
         myint[5] = 5;                          //向数组中添加第六个元素
         myint[6] = 6;                          //向数组中添加第七个元素
         myint[7] = 7;                          //向数组中添加第八个元素
         myint[8] = 8;                          //向数组中添加第九个元素
         myint[9] = 9;                          //向数组中添加第十个元素
         for (int i = 0; i < myint.Length; i++) //调用 for 循环语句   
         {
               Console.WriteLine("myint[{0}]的值是:{1}", i, myint[i]); //输出结果
         }
         Console.ReadLine();
    }

程序的运行结果如图6.10所示。

图6.9 for循环语句执行过程

图6.10 输出数组中的值

互动练习:尝试使用C#制作一个数字猜猜看小游戏,具体要求为:使用随机对象产生一个1至100间的整数,当用户单击“开始”按钮时,动态生成100个按钮,并开始计时,如果用户单击的数字小于随机数,那么被单击的按钮变为红色,并显示字符串“小”,如果用户单击的数字大于随机数,那么被单击的按钮变为红色,并显示字符串“大”,如果单击的数字等于随机数就会弹出消息框,提示已经猜对了数字,并显示用时及猜测次数。

6.2.4 foreach语句

视频讲解:光盘\TM\lx\6\06 foreach语句.mp4

foreach语句是for语句的特殊简化版本,但是foreach语句并不能完全取代for语句,然而,任何foreach语句都可以改写为for语句版本。foreach语句用于枚举一个集合的元素,并对该集合中的每个元素执行一次嵌入语句。但是,foreach语句不应用于更改集合内容,以避免产生不可预知的错误。foreach语句的基本形式如下。

    foreach(【类型】 【迭代变量名】 in 【集合类型表达式】)
    {
          【语句块】
    }

其中,【类型】和【迭代变量名】用于声明迭代变量,迭代变量相当于一个范围覆盖整个语句块的局部变量。在foreach语句执行期间,迭代变量表示当前正在为其执行迭代的集合元素。

【集合类型表达式】必须有一个从该集合的元素类型到迭代变量的类型的显示转换,如果【集合类型表达式】的值为null,则会出现异常。

说明

foreach语句也可以用于循环访问数组中的元素。

【例6.10】 创建一个控制台应用程序,实例化一个ArrayList数组,向数组中添加值,然后通过使用foreach语句遍历整个数组,并输出数组中的值,代码如下。(实例位置:光盘\TM\sl\6\9)

    static void Main(string[] args)
    {
         ArrayList alt = new ArrayList();               //实例化 ArrayList 类
         alt.Add("用一生下载你");                       //使用 Add 方法向对象中添加数据
         alt.Add("芸烨湘枫");                           //使用 Add 方法向对象中添加数据
         alt.Add("一生所爱");                           //使用 Add 方法向对象中添加数据
         alt.Add("情茧");                               //使用 Add 方法向对象中添加数据
         alt.Add("痞子 CAI");                           //使用 Add 方法向对象中添加数据
         Console.WriteLine("您收藏的网名有:");         //输出提示
         foreach (string InternetName in alt)           //使用 foreach 语句输出数据
         {
             Console.WriteLine(InternetName);           //输出 ArrayList 对象中的所有数据
         }
         Console.ReadLine();
    }

程序的运行结果如图6.11所示。

图6.11 输出数组值

6.3 跳转语句

视频讲解:光盘\TM\lx\6\07 跳转语句.mp4

跳转语句主要用于无条件地转移控制,跳转语句会将控制转到某个位置,这个位置就成为跳转语句的目标。如果跳转语句出现在一个语句块内,而跳转语句的目标却在该语句块之外,则称该跳转语句退出该语句块。跳转语句主要包括break语句、continue语句、goto语句和return语句,本节将对这几种跳转语句分别进行介绍。

6.3.1 break语句

break语句只能应用在switch、while、do…while、for或foreach语句中,当多个switch、while、do…while、for或foreach语句互相嵌套时,break语句只应用于最里层的语句。如果要穿越多个嵌套层,则必须使用goto语句。

下面主要举例说明break语句在switch语句和for语句中的使用。

1.break语句在switch语句中的应用

【例6.11】 创建一个控制台应用程序,声明一个int类型的变量i用于获取当前日期的返回值,然后通过使用switch语句根据变量i输出当前日期是星期几,代码如下。(实例位置:光盘\TM\sl\6\10)

    static void Main(string[] args)
    {
         int i = Convert.ToInt32(DateTime.Today.DayOfWeek);           //获取当前日期的数值
         switch (i)                                                   //调用 switch 语句
         {
             case 1: Console.WriteLine("今天是星期一"); break; //如果 i 是 1,则输出今天是星期一
             case 2: Console.WriteLine("今天是星期二"); break; //如果 i 是 2,则输出今天是星期二
             case 3: Console.WriteLine("今天是星期三"); break; //如果 i 是 3,则输出今天是星期三
             case 4: Console.WriteLine("今天是星期四"); break; //如果 i 是 4,则输出今天是星期四
             case 5: Console.WriteLine("今天是星期五"); break; //如果 i 是 5,则输出今天是星期五
             case 6: Console.WriteLine("今天是星期六"); break; //如果 i 是 6,则输出今天是星期六
             case 0: Console.WriteLine("今天是星期日"); break; //如果 i 是 7,则输出今天是星期日
         }
         Console.ReadLine();
    }

程序的运行结果为“今天是星期三”。

2.break语句在for语句中的应用

【例6.12】 创建一个控制台应用程序,使用两个for语句做嵌套循环,在内层的for语句中,使用break语句,实现当int类型变量j等于12时,跳出内循环,代码如下。(实例位置:光盘\TM\sl\6\11)

程序的运行结果为:

    第 0 次循环:0 1 2 3 4 5 6 7 8 9 10 11
    第 1 次循环:0 1 2 3 4 5 6 7 8 9 10 11
    第 2 次循环:0 1 2 3 4 5 6 7 8 9 10 11
    第 3 次循环:0 1 2 3 4 5 6 7 8 9 10 11

从程序的运行结果中可以看出,使用break语句只终止了内层循环,并没有影响到外部的循环,所以程序依然经历了4次循环。

6.3.2 continue语句

continue语句只能应用于while、do…while、for或foreach语句中,用来忽略循环语句块内位于其后面的代码而直接开始一次新的循环。当多个while、do…while、for或foreach语句互相嵌套时,continue语句只能使直接包含它的循环语句开始一次新的循环。

说明

在循环体中,不要在同一个语句块中使用多个跳转语句。

【例6.13】 创建一个控制台应用程序,使用两个for语句做嵌套循环,在内层的for语句中,使用continue语句,实现当int类型变量j为偶数时,不输出,重新开始下一次内层的for循环,只输出0~20内的所有奇数,代码如下。(实例位置:光盘\TM\sl\6\12)

程序的运行结果为:

    第 0 次循环:1 3 5 7 9 11 13 15 17 19
    第 1 次循环:1 3 5 7 9 11 13 15 17 19
    第 2 次循环:1 3 5 7 9 11 13 15 17 19
    第 3 次循环:1 3 5 7 9 11 13 15 17 19

从程序的运行结果可以看出,当int类型的变量j为偶数时,使用continue语句,忽略它后面的代码,而重新执行内层的for循环,输出0~20以内的奇数,这期间,并没有影响外部的for循环,程序依然执行了4次循环。

6.3.3 goto语句

goto语句用于将控制转移到由标签标记的语句。goto语句可以被应用在switch语句中的case标签和default标签,以及标记语句所声明的标签。goto语句的3种形式如下。

    goto 【标签】
    goto case 【参数表达式】
    goto default

goto【标签】语句的目标是具有给定标签的标记语句,goto case语句的目标是它所在的switch语句中的某个语句列表,此列表包含一个具有给定常数值的case标签,goto default语句的目标是它所在的switch语句中的default标签。

说明

goto的一个通常用法是将控制传递给特定的switch-case标签或switch语句中的默认标签。goto语句还用于跳出深嵌套循环。

【例6.14】 创建一个控制台应用程序,通过goto语句实现程序跳转到指定语句,代码如下。(实例位置:光盘\TM\sl\6\13)

程序的运行结果如图6.12所示。

图6.12 查找指定字符串

注意

虽然goto语句有一定的使用价值,但是目前对它的使用存在争议。有人建议避免使用它,有人建议把它用来作为排除错误的基本工具,各种观点截然不同。虽然许多人不用goto语句也能够编程,但是仍然有人使用它。所以要小心使用,同时一定要确保程序是可维护的。

6.3.4 return语句

return语句表示返回,当把return语句用在普通的程序代码中时,它表示返回,并且不再执行return之后的代码。当把return语句用在类中的方法时,它就是控制返回方法的调用者,如果方法有返回类型,return语句必须返回这个类型的值;而如果方法没有返回类型,则应该使用没有表达式的return语句。

【例6.15】 创建一个控制台应用程序,定义一个返回类型为string类型的方法,利用return语句,返回一个string类型的值,然后在Main方法中调用这个自定义的方法,并输出这个方法的返回值,代码如下。(实例位置:光盘\TM\sl\6\14)

程序的运行结果如图6.13所示。

图6.13 return语句的应用

闯关训练:通过使用goto跳转语句,实现在数组中搜索指定图书的功能,具体实现时,首先在窗体的左侧显示一个图书列表,然后在窗体右侧添加一个文本框控件和一个按钮,当用户输入要查找的书名时,单击按钮,即可在左侧图书列表中查找是否存在该图书。

6.4 小结

本章详细介绍了条件判断语句、循环语句和跳转语句的概念及用法。在程序中,语句是程序完成一次操作的基本单位,而流程语句控制语句执行的顺序。在讲解流程语句过程中,通过实例演示了每种语句的用法。在学习本章内容时,读者要重点掌握if语句、switch语句、for语句和while语句的用法,因为这几种语句在程序开发中会经常用到,希望通过对本章的学习,读者能够熟练掌握C#中流程控制语句的使用,并能够应用于实际的开发中。

6.5 实践与练习

(1)尝试开发一个程序,要求在判断变量I大于等于6时,输出I的值。(答案位置:光盘\TM\sl\6\15)

(2)尝试开发一个程序,要求使用goto跳转语句跳转到程序中的指定位置。(答案位置:光盘\TM\sl\6\16)

(3)尝试开发一个程序,要求使用switch…case语句判断指定的整数范围。(答案位置:光盘\TM\sl\6\17)

6.6 动手纠错

(1)运行“光盘\TM\排错练习\06\01”文件夹下的程序,出现“无法将类型int隐式转换为bool”的错误提示,请根据注释改正程序。

(2)运行“光盘\TM\排错练习\06\02”文件夹下的程序,出现如图6.14所示的错误提示,请根据注释改正程序。

图6.14 错误提示

(3)运行“光盘\TM\排错练习\06\03”文件夹下的程序,出现“控制不能从一个case标签(case "ICBC":)贯穿到另一个case标签”的错误提示,请根据注释改正程序。

(4)运行“光盘\TM\排错练习\06\04”文件夹下的程序,发现程序会无限制输出,显然是出现了死循环,请根据注释改正程序。

(5)运行“光盘\TM\排错练习\06\05”文件夹下的程序,出现“应输入;”的错误提示,请根据注释改正程序。

(6)运行“光盘\TM\排错练习\06\06”文件夹下的程序,出现“空语句可能有错误”的警告信息,请根据注释改正程序。

(7)运行“光盘\TM\排错练习\06\07”文件夹下的程序,本意是想输出1~100数字自身的和,结果却没有任何输出,请根据注释改正程序。

(8)运行“光盘\TM\排错练习\06\08”文件夹下的程序,出现“没有要中断或继续的封闭循环”的错误提示,请根据注释改正程序。

(9)运行“光盘\TM\排错练习\06\09”文件夹下的程序,出现“Test.Program.Div(int,int):并非所有的代码路径都返回值”的错误提示,请根据注释改正程序。

第7章 数组和集合

第7章
数组和集合

视频讲解:107分钟

数组是最为常见的一种数据结构,是相同类型的、用一个标识符封装到一起的基本类型数据序列或对象序列。可以用一个统一的数组名和下标来唯一确定数组中的元素。实质上数组是一个简单的线性序列,因此数组访问起来很快。而集合可以看成是一种特殊的数组,它也可以存储多个数据,C#中常用的集合包括ArrayList集合和Hashtable(哈希表)。

通过阅读本章,您可以:

 了解数组的基本概念

 掌握一维数组和二维数组的使用

 熟练掌握数组的常用操作

 掌握常用的数组排序算法

 掌握ArrayList集合类的使用及操作

 熟悉Hashtable(哈希表)的使用及操作

7.1 数组概述

视频讲解:光盘\TM\lx\7\01 数组概述.mp4

数组是大部分编程语言中都支持的一种数据类型,无论是C语言、C++、C#还是Java,都支持数组的概念。

数组是具有相同数据类型的一组数据的集合。例如,球类的集合—足球、篮球、羽毛球等;电器集合—电视机、洗衣机、电风扇等。在程序设计中,可以将这些集合称为数组。数组中的每一个变量称为数组的元素,数组能够容纳元素的数量称为数组的长度。数组中的每个元素都具有唯一的索引与其相对应,数组的索引从零开始。

数组是通过指定数组的元素类型、数组的秩(维数)及数组每个维度的上限和下限来定义的,即一个数组的定义需要包含以下几个要素。

 元素类型。

 数组的维数。

 每个维数的上下限。

在程序设计中引入数组可以更有效地管理和处理数据。可根据数组的维数将数组分为一维数组、二维数组……

7.2 一维数组的创建和使用

视频讲解:光盘\TM\lx\7\02 一维数组的使用.mp4

一维数组实质上是一组相同类型数据的线性集合,当在程序中需要处理一组数据时,或者传递一组数据时,可以应用这种类型的数组。一维数组就好比一个大型的零件生产公司,而公司中的各个车间(如车间1、车间2、车间3等,这些名称相当于数组中的索引号)就相当于一维数组中的各元素,这些车间既可以单独使用,也可以一起使用。本节将介绍一维数组的创建及使用。

7.2.1 一维数组的创建

数组作为对象允许使用new关键字进行内存分配。在使用数组之前,必须首先定义数组变量所属的类型。一维数组的创建有两种形式。

1.先声明,再用new运算符进行内存分配

声明一维数组使用以下形式:

    数组元素类型[] 数组名字;

数组元素类型决定了数组的数据类型。它可以是C#中任意的数据类型,包括简单类型和组合类型。数组名字为一个合法的标识符,符号“[]”指明该变量是一个数组类型变量。单个“[]”表示要创建的数组是一个一维数组。

【例7.1】 声明一维数组,实例代码如下。

    int[] arr;     //声明 int 型数组,数组中的每个元素都是 int 型数值
    string[] str;  //声明 string 数组,数组中的每个元素都是 string 型数值

声明数组后,还不能访问它的任何元素,因为声明数组只是给出了数组名字和元素的数据类型,要想真正使用数组,还要为它分配内存空间。在为数组分配内存空间时必须指明数组的长度。为数组分配内存空间的语法格式如下:

    数组名字 = new 数组元素类型[数组元素的个数];

 数组名字:被连接到数组变量的名称。

 数组元素个数:指定数组中变量的个数,即数组的长度。

通过上面的语法可知,使用new关键字分配数组时,必须指定数组元素的类型和数组元素的个数,即数组的长度。

【例7.2】 为数组分配内存,实例代码如下。

    arr = new int[5];

图7.1 一维数组的内存模式

以上代码表示要创建一个有5个元素的整型数组,并且将创建的数组对象赋给引用变量arr,即引用变量arr引用这个数组,如图7.1所示。

在图7.1中arr为数组名称,方括号“[]”中的值为数组的下标。数组通过下标来区分数组中不同的元素。数组的下标是从0开始的。由于创建的数组arr中有5个元素,因此数组中元素的下标为0~4。

说明

使用new关键字为数组分配内存时,整型数组中各个元素的初始值都为0。

2.声明的同时为数组分配内存

这种创建数组的方法是将数组的声明和内存的分配合在一起执行。

语法如下:

    数组元素类型[] 数组名 = new 数组元素类型[数组元素的个数];

【例7.3】 声明并为数组分配内存,实例代码如下。

    int[ ] month = new int[12]

上面的代码创建数组month,并指定了数组长度为12。这种创建数组的方法也是C#程序编写过程中普遍的做法。

7.2.2 一维数组的初始化

数组可以与基本数据类型一样进行初始化操作。数组的初始化可分别初始化数组中的每个元素。数组的初始化有以下两种形式:

    int[] arr = new int[]{1,2,3,5,25}; //第一种初始化方式
    int []arr2 = {34,23,12,6};         //第二种初始化方式

从中可以看出,数组的初始化就是包括在大括号之内用逗号分开的表达式列表。用逗号(,)分割数组中的各个元素,系统自动为数组分配一定的空间。用第一种初始化方式,将创建5个元素的数组,依次为1、2、3、5、25。第二种初始化方式,会创建4个元素的数组,依次为34、23、12、6。

7.2.3 一维数组的使用

一维数组是常见的一种数据结构,下面的实例是使用一维数组将1~12月份各月的天数输出。

【例7.4】 创建一个控制台应用程序,其中定义了一个int类型的一维数组,实现将各月的天数输出,程序代码如下。(实例位置:光盘\TM\sl\7\1)

    static void Main(string[] args)
    {
         //创建并初始化一维数组
         int[] day = new int[] { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 };
         for (int i = 0; i < 12; i++)                             //利用循环将信息输出
         {
             Console.WriteLine((i + 1) + "月有" + day[i] + "天"); //输出的信息
         }
         Console.ReadLine();
    }

按Ctrl+F5键查看运行结果,如图7.2所示。

图7.2 输出1~12月份各月的天数

7.3 二维数组的创建和使用

视频讲解:光盘\TM\lx\7\03 二维数组的使用.mp4

二维数组常用于表示表,表中的信息以行和列的形式组织,第一个下标代表元素所在的行,第二个下标代表元素所在的列。

7.3.1 二维数组的创建

二维数组可以看作是特殊的一维数组,声明二维数组的语法如下。

    数组元素类型[,] 数组名字;

【例7.5】 声明二维数组,实例代码如下。

    int[,] myarr;

同一维数组一样,二维数组在声明时也没有分配内存空间,同样要使用关键字new来分配内存,然后才可以访问每个元素。

对于高维数组,有两种为数组分配内存的方式:

(1)直接为每一维分配内存空间

【例7.6】 为每一维数组分配内存,实例代码如下。

    int[,] a=new int[2,4];

上述代码创建了二维数组a,二维数组a中包括两个长度为4的一维数组,内存分配如图7.3所示。

图7.3 二维数组内存分配(第一种方式)

(2)分别为每一维分配内存空间

【例7.7】 分别为每一维分配内存,实例代码如下。

    int[][] a = new int[2][];
    a[0] = new int[2];
    a[1] = new int[3];

通过第二种方式为二维数组分配内存,如图7.4所示。

图7.4 二维数组内存分配(第二种方式)

7.3.2 二维数组初始化

二维数组的初始化同一维数组初始化类似,同样可以使用大括号完成。

语法如下:

    type[,] arrayname = {value1,value2…valuen};

 type:数组数据类型。

 arrayname:数组名称,一个合法的标识符。

 value:数组中各元素的值。

【例7.8】 初始化二维数组,实例代码如下。

    int[,] myarr1 = new int[,]{ { 12, 0 }, { 45, 10 } };
    int[,] myarr2 = {{12,0},{45,10}};

初始化二维数组后,要明确数组的下标都是从0开始。例如,上面的代码中myarr[1,1]的值为10。

int型二维数组是以int[,]myarr1来定义的,所以可以直接给myarr1[x,y]赋值。例如,给myarr1[1]的第2个元素赋值的语句如下:

    myarr1[1,1] = 20;

闯关训练:使用C#实现将二维数组中的行列互调显示出来。如:

7.3.3 二维数组的使用

需要存储表格的数据时,可以使用二维数组。如图7.5所示举例说明了4行3列的二维数组的存储结构。

【例7.9】 创建一个控制台应用程序,其中定义了一个静态的二维数组,并使用数组的GetLength方法获取数组的行数和列数,然后通过遍历数组输出其元素值。程序代码如下。(实例位置:光盘\TM\sl\7\2)

按Ctrl+F5键查看运行结果,如图7.6所示。

图7.5 二维数组的存储结构

图7.6 二维数组实例运行结果

互动练习:假设客车的座位数是9行4列,使用二维数组在控制台应用程序中实现简单客车售票系统。具体要求为:使用一个二维数组记录客车售票系统中的所有座位号,并在每个座位号上都显示“【有票】”,然后用户输入一个坐标位置,按Enter键,即可将该座位号显示为“【已售】”。

7.4 数组的基本操作

视频讲解:光盘\TM\lx\7\04 数组的基本操作.mp4

C#中的数组是由System.Array类派生而来的引用对象,因此可以使用Array类中的各种方法对数组进行各种操作。本节将对数组的常用操作进行详细讲解。

7.4.1 遍历数组

遍历数组就是获取数组中的每个元素,C#中,可以使用foreach语句实现数组的遍历功能。

【例7.10】 下面创建一个int类型的一维数组,该数组中包含10个元素,然后使用foreach语句遍历该数组中的元素,代码如下。

    int[] arr = new int[10] { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 };
    //采用 foreach 语句对 arr 数组进行遍历
    foreach (int number in arr)
        Console.WriteLine(number);
    Console.ReadLine();

程序运行结果:

    10
    20
    30
    40
    50
    60
    70
    80
    90
    100

7.4.2 添加/删除数组元素

1.添加数组元素

添加数组元素有两种情况:一是在数组中添加一个元素,二是在数组中添加一个数组,下面通过实例分别对这两种情况进行介绍。

【例7.11】 创建一个控制台应用程序,首先自定义一个AddArray方法,用来在指定索引号的后面添加元素,并返回新得到的数组;然后在Main方法中调用该自定义方法,向指定的一维数组中添加元素。代码如下。(实例位置:光盘\TM\sl\7\3)

程序运行结果如图7.7所示。

图7.7 向数组中添加元素

【例7.12】 创建一个控制台应用程序,首先自定义一个AddArray方法,用来在指定索引号的后面添加一个数组,并返回新得到的数组;然后在Main方法中调用该自定义方法,向指定的一维数组中添加一个数组。代码如下。(实例位置:光盘\TM\sl\7\4)

程序运行结果如图7.8所示。

2.删除数组元素

图7.8 向数组中添加数组

删除数组元素主要有两种情况:一是在不改变数组元素总数的情况下删除指定元素(也就是用删除元素后面的元素覆盖要删除的元素);二是删除指定元素后,根据删除元素的个数n,使删除后的数组长度减n。下面通过实例分别对这两种情况进行介绍。

【例7.13】 创建一个控制台应用程序,首先自定义一个DeleteArray方法,用来在不改变长度的情况下从数组的指定索引处删除指定长度的元素;然后在Main方法中调用该自定义方法,从指定的数组中删除一个元素。代码如下。(实例位置:光盘\TM\sl\7\5)

程序运行结果如图7.9所示。

图7.9 不改变长度删除数组中的元素

【例7.14】 创建一个控制台应用程序,自定义方法ret_DeleteArray在删除元素或指定区域的元素后,改变数组的长度。代码如下。(实例位置:光盘\TM\sl\7\6)

程序运行结果如图7.10所示。

7.4.3 对数组进行排序

图7.10 改变长度删除数组中的元素

C#中提供了用于对数组进行排序的方法Array.Sort和Array.Reverse,其中,Array.Sort方法用于对一维Array数组中的元素进行排序,Array.Reverse方法用于反转一维Array数组或部分Array数组中元素的顺序。

【例7.15】 下面使用Array.Sort方法对数组中的元素进行从小到大的排序,代码如下。

    iint[] arr = new int[] { 3, 9, 27, 6, 18, 12, 21, 15 };
    Array.Sort(arr);                               //对数组元素排序

注意

在Sort方法中所用到的数组不能为空,也不能是多维数组,它只对一维数组进行排序。

【例7.16】 下面使用Array.Reverse方法对数组的元素进行反向排序,代码如下。

    int[] arr = new int[] { 3, 9, 27, 6, 18, 12, 21, 15 };
    Array. Reverse(arr);                          //对数组元素反向排序

7.4.4 数组的合并与拆分

数组的合并与拆分在很多情况下都会被应用,在对数组进行合并或拆分时,数组与数组之间的类型应一致。下面对数组的合并及拆分进行详细讲解。

1.数组的合并

数组的合并实际上就是将多个一维数组合并成一个一维数组,或者将多个一维数组合并成一个二维数组或多维数组。

注意

在合并数组时,如果是将两个一维数组合并成一个一维数组,那么新生成数组的元素个数必须为要合并的两个一维数组元素个数的和。

【例7.17】 创建一个控制台应用程序,首先将两个一维数组合并成一个新的一维数组,然后再将定义的两个一维数组合并为一个新的二维数组。程序代码如下。(实例位置:光盘\TM\sl\7\7)

按Ctrl+F5键查看运行结果,如图7.11所示。

2.数组的拆分

数组的拆分实际上就是将一个一维数组拆分成多个一维数组,或是将多维数组拆分成多个一维数组或多个多维数组。

【例7.18】 创建一个控制台应用程序,将一个int类型的二维数组拆分成两个int类型的一维数组。程序代码如下。(实例位置:光盘\TM\sl\7\8)

按Ctrl+F5键查看运行结果,如图7.12所示。

图7.11 数组合并实例运行结果

图7.12 数组拆分实例运行结果

7.5 数组排序算法

视频讲解:光盘\TM\lx\7\05 数组排序算法.mp4

数组有很多常用的算法,本节将介绍常用的排序算法,包括冒泡排序法、直接插入排序法和选择排序法。

7.5.1 冒泡排序

冒泡排序方法以简洁的思想与实现方法而备受青睐,是广大初学者最先接触的一个排序算法。这种方法排序数组元素的过程总是小数往前放,大数往后放,类似水中气泡往上升的动作,所以称作冒泡排序。

1.基本思想

冒泡排序的基本思想是对比相邻的元素值,如果满足条件就交换元素值,把较小的元素移动到数组前面,把大的元素移动到数组后面(也就是交换两个元素的位置),这样较小的元素就像气泡一样从底部上升到顶部。

2.算法示例

冒泡算法由双层循环实现,其中外层循环用于控制排序轮数,一般是要排序的数组长度减1次,因为最后一次循环只剩下一个数组元素,不需要对比,同时数组已经完成排序了。而内层循环主要用于对比数组中每个临近元素的大小,以确定是否交换位置,对比和交换次数以排序轮数而减少。例如,一个拥有6个元素的数组,在排序过程中每一次循环的排序过程和结果如图7.13所示。

图7.13 6个元素数组的排序过程

比较次数: 1 2 3 4 5 1 2 3 4 1 2 3 1 2 1

第一轮外层循环时把最大的元素值63移动到了最后面(相应的比63小的元素向前移动,类似气泡上升),第二轮外层循环不再对比最后一个元素值63,因为它已经确认为最大(不需要上升),应该放在最后,需要对比和移动的是其他剩余元素,这次将元素24移动到了63的前一个位置。其他循环将依此类推,继续完成排序任务。

3.算法实现

下面来介绍一下冒泡排序的具体用法。

【例7.19】 创建一个控制台应用程序,使用冒泡法对数组中的元素从小到大进行排序。程序代码如下。(实例位置:光盘\TM\sl\7\9)

按Ctrl+F5键查看运行结果,如图7.14所示。

图7.14 冒泡法排序实例运行结果

注意

在对数组进行遍历时,要用当前数组的Length属性来获取数组的元素个数。

从实例的运行结果来看,数组中的元素已经按从小到大的顺序排序好了。冒泡排序的主要思想就是:把相邻两个元素进行比较,如满足一定条件则进行交换(如判断大小或日期前后等),每次循环就将最大(或最小)的元素排在最后,下一次循环是对数组中其他的元素进行类似操作。

7.5.2 直接插入排序

直接排序方法是一种常用的数组排序算法,是初学者应该掌握的。

1.基本思想

直接插入排序是一种最简单的排序方法,其基本操作是将一个记录插入到已排好序的有序表中,从而得到一个新的、记录数增1的有序表,然后再从剩下的关键字中选取下一个插入对象,反复执行直到整个序列有序。

2.算法示例

插入排序法实现时,将n个有序数存放在数组a中,要插入的数为x,首先确定x插在数组中的位置p,数组中p之后的元素都向后移一个位置,空出a(p),将x放入a(p)。这样即可实现插入后数列仍然有序。

举例:

    初始数组资源 【63 4 24 1 3 15】
    第一趟排序后 【4 63】 24 1 3 15
    第二趟排序后 【4 24 63】 1 3 15
    第三趟排序后 【1 4 24 63】 3 15
    第四趟排序后 【1 3 4 24 63】 15
    第五趟排序后 【1 3 4 15 24 63】
3.算法实现

下面来介绍一下直接插入排序的具体用法。

【例7.20】 创建一个控制台应用程序,使用直接插入法对数组中的元素从小到大进行排序。程序代码如下。(实例位置:光盘\TM\sl\7\10)

按Ctrl+F5键查看运行结果,如图7.15所示。

图7.15 直接插入法排序实例运行结果

闯关训练:使用希尔排序算法对一维数组“63,4,24,1,3,15”进行排序。希尔排序又称“缩小增量排序”,其基本思想是,先将整个待排序的一组序列分割成为若干子序列,然后分别进行直接插入排序,待整个序列中的数“基本有序”时再对全体记录进行一次直接插入排序。

7.5.3 选择排序法

选择排序方法的排序速度要比冒泡排序快一些,也是常用的排序算法,是初学者应该掌握的。

1.基本思想

选择排序的基本思想是将指定排序位置与其他数组元素分别对比,如果满足条件就交换元素值,注意这里有别于冒泡排序,不是交换相邻元素,而是把满足条件的元素与指定的排序位置交换(如从第一个元素开始排序),这样排序好的位置逐渐扩大,最后整个数组都成为已排序好的格式。

这就好比有一个小学生,从包含数字1~10的乱序的数字堆中分别选择合适的数字,组成一个从1~10的排序,而这个学生首先从数字堆中选出1,放在第一位,然后选出2(注意这时数字堆中已经没有1了),放在第二位,依次类推,直到其找到数字9,放到8的后面,最后剩下10,就不用选择了,直接放到最后就可以了。

与冒泡排序相比,选择排序的交换次数要少很多,所以速度会快些。

2.算法示例

每一趟在n个记录中选取关键字最小的记录作为有序序列的第I个记录,并且令I为1~n-1,进行n-1趟选择操作。

例如:

    初始数组资源 【63 4 24 1 3 15】
第一趟排序后【1】 4 24 63 3 15 第二趟排序后【1 3】 24 63 4 15 第三趟排序后【1 3 4】 63 24 15 第四趟排序后【1 3 4 15】 24 63 第五趟排序后【1 3 4 15 24】 63
3.算法实现

下面来介绍一下选择排序法的具体用法。

【例7.21】 创建一个控制台应用程序,使用选择排序法对数组中的元素从小到大进行排序,程序代码如下。(实例位置:光盘\TM\sl\7\11)

按Ctrl+F5键查看运行结果,如图7.16所示。

图7.16 选择排序法实例运行结果

7.6 ArrayList类

视频讲解:光盘\TM\lx\7\06 ArrayList类.mp4

ArrayList类相当于一种高级的动态数组,它是Array类的升级版本,本节将对该类进行详细介绍。

7.6.1 ArrayList类概述

ArrayList类位于System.Collections命名空间下,它可以动态地添加和删除元素。可以将ArrayList类看作扩充了功能的数组,但它并不等同于数组。

与数组相比,ArrayList类为开发人员提供了以下功能。

 数组的容量是固定的,而ArrayList的容量可以根据需要自动扩充。

 ArrayList提供添加、删除和插入某一范围元素的方法,但在数组中,只能一次获取或设置一个元素的值。

 ArrayList提供将只读和固定大小包装返回到集合的方法,而数组不提供。

 ArrayList只能是一维形式,而数组可以是多维的。

说明

Array的长度是它可包含的元素总数。Array的秩是Array中的维数。Array中维度的下限是Array中该维度的起始索引,多维Array的各个维度可以有不同的界限。

ArrayList提供了3个构造器,通过这3个构造器可以有3种声明方式,下面分别介绍。

(1)默认的构造器,将会以默认的大小(16位)来初始化内部的数组。构造器格式如下。

    public ArrayList();

通过以上构造器声明ArrayList的语法格式如下。

    ArrayList List = new ArrayList();

List:ArrayList对象名。

【例7.22】 声明一个ArrayList对象,并给其添加10个int类型的元素值,代码如下。

    ArrayList List = new ArrayList();
    for (int i = 0; i < 10; i++) //给 ArrayList 对象添加 10 个 int 元素
         List.Add(i);

(2)用一个ICollection对象来构造,并将该集合的元素添加到ArrayList中。构造器格式如下。

    public ArrayList(ICollection);

通过以上构造器声明ArrayList的语法格式如下。

    ArrayList List = new ArrayList(arryName);

 List:ArrayList对象名。

 arryName:要添加集合的数组名。

【例7.23】 声明一个int类型的一维数组,然后声明一个ArrayList对象,同时将已经声明的一维数组中的元素添加到该对象中,代码如下。

    int[] arr = new int[] { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
    ArrayList List = new ArrayList(arr);

(3)用指定的大小初始化内部的数组。构造器格式如下。

    public ArrayList(int);

通过以上构造器声明ArrayList的语法格式如下。

    ArrayList List = new ArrayList(n);

 List:ArrayList对象名。

 n:ArrayList对象的空间大小。

【例7.24】 声明一个具有10个元素的ArrayList对象,并为其赋初始值,代码如下。

    ArrayList List = new ArrayList(10);
    for (int i = 0; i < List.Count; i++)      //给 ArrayList 对象添加 10 个 int 元素
          List.Add(i);

ArrayList常用属性及说明如表7.1所示。

表7.1 ArrayList常用属性及说明

7.6.2 ArrayList元素的添加

向ArrayList集合中添加元素时,可以使用ArrayList类提供的Add方法和Insert方法,下面对这两个方法进行详细介绍。

1.Add方法

Add方法用来将对象添加到ArrayList集合的结尾处,其语法格式如下。

    public virtual int Add(Object value)

 value:要添加到ArrayList的末尾处的Object,该值可以为空引用。

 返回值:ArrayList索引,已在此处添加了value。

说明

ArrayList允许null值作为有效值,并且允许重复的元素。

【例7.25】 声明一个包含6个元素的一维数组,并使用该数组实例化一个ArrayList对象,然后使用Add方法为该ArrayList对象添加元素,代码如下。

    int[] arr = new int[] { 1, 2, 3, 4, 5, 6 };
    ArrayList List = new ArrayList(arr);   //使用声明的一维数组实例化一个 ArrayList 对象
    List.Add(7);                           //为 ArrayList 对象添加元素
2.Insert方法

Insert方法用来将元素插入ArrayList集合的指定索引处,其语法格式如下。

    public virtual void Insert (
         int index,
         Object value)

 index:从零开始的索引,应在该位置插入value。

 value:要插入的Object,该值可以为空引用。

说明

如果ArrayList实际存储元素数已经等于ArrayList可存储的元素数,则会通过自动重新分配内部数组增加ArrayList的容量,并在添加新元素之前将现有元素复制到新数组中。

【例7.26】 声明一个包含6个元素的一维数组,并使用该数组实例化一个ArrayList对象,然后使用Insert方法在该ArrayList对象的指定索引处添加一个元素,代码如下。

    nt[] arr = new int[] { 1, 2, 3, 4, 5, 6 };
    ArrayList List = new ArrayList(arr);           //使用声明的一维数组实例化一个 ArrayList 对象
    List.Insert(3, 7);                      //在 ArrayList 集合的指定位置添加一个元素

【例7.27】 创建一个控制台应用程序,其中定义了一个int类型的一维数组,并使用该数组实例化一个ArrayList对象,然后分别使用ArrayList对象的Add方法和Insert方法向ArrayList集合的结尾处和指定索引处添加元素。程序代码如下。(实例位置:光盘\TM\sl\7\12)

按Ctrl+F5键查看运行结果,如图7.17所示。

图7.17 ArrayList元素的添加实例运行结果

说明

在ArrayList集合中插入一个数组时,只需要在上面代码中重新声明一个一维数组,并将List.Insert(6,6)语句改为List.InsertRange(6,一维数组名)即可。

注意

程序中使用ArrayList类时,需要在命名空间区域添加using System.Collections;,下面将不再提示。

闯关训练:使用ArrayList集合记录学生的姓名、性别及出生年月等信息。如:

    小王 男 1980-03-08
    小刘 女 1981-06-21
    小赵 男 1990-07-01
    小李 男 1995-09-25

7.6.3 ArrayList元素的删除

在ArrayList集合中删除元素时,可以使用ArrayList类提供的Clear方法、Remove方法、RemoveAt方法和RemoveRange方法。下面对这4个方法进行详细介绍。

1.Clear方法

Clear方法用来从ArrayList中移除所有元素,其语法格式如下。

    public virtual void Clear()

【例7.28】 声明一个包含6个元素的一维数组,并使用该数组实例化一个ArrayList对象,然后使用Clear方法清除ArrayList中的所有元素,代码如下。

    int[] arr = new int[] { 1, 2, 3, 4, 5, 6 };
    ArrayList List = new ArrayList(arr);
    List.Clear();
2.Remove方法

Remove方法用来从ArrayList中移除特定对象的第一个匹配项,其语法格式如下。

    public virtual void Remove(Object obj)

obj:要从ArrayList移除的Object,该值可以为空引用。

说明

在删除ArrayList中的元素时,如果不包含指定对象,则ArrayList将保持不变。

【例7.29】 声明一个包含6个元素的一维数组,并使用该数组实例化一个ArrayList对象,然后使用Remove方法从声明的ArrayList对象中移除与3匹配的元素,代码如下。

    int[] arr = new int[] { 1, 2, 3, 4, 5, 6 };
    ArrayList List = new ArrayList(arr);
    List.Remove(3);
3.RemoveAt方法

RemoveAt方法用来移除ArrayList的指定索引处的元素,其语法格式如下。

    public virtual void RemoveAt(int index)

index:要移除的元素的从零开始的索引。

【例7.30】 声明一个包含6个元素的一维数组,并使用该数组实例化一个ArrayList对象,然后使用RemoveAt方法从声明的ArrayList对象中移除索引为3的元素,代码如下。

    int[] arr = new int[] { 1, 2, 3, 4, 5, 6 };
    ArrayList List = new ArrayList(arr);
    List.RemoveAt(3);
4.RemoveRange方法

RemoveRange方法用来从ArrayList中移除一定范围的元素,其语法格式如下。

    public virtual void RemoveRange(
         int index,
         int count)

 index:要移除的元素的范围从零开始的起始索引。

 count:要移除的元素数。

注意

在RemoveRange方法中参数count的长度不能超出数组的总长度减去参数index的值。

【例7.31】 声明一个包含6个元素的一维数组,并使用该数组实例化一个ArrayList对象,然后在该ArrayList对象中使用RemoveRange方法从索引3处删除两个元素,代码如下。

    int[] arr = new int[] { 1, 2, 3, 4, 5, 6 };
    ArrayList List = new ArrayList(arr);
    List.RemoveRange(3,2);

【例7.32】 创建一个控制台应用程序,使用RemoveRange方法删除ArrayList集合中的一批元素。程序代码如下。(实例位置:光盘\TM\sl\7\13)

按Ctrl+F5键查看运行结果,如图7.18所示。

7.6.4 ArrayList的遍历

图7.18 ArrayList元素的删除实例运行结果

ArrayList集合的遍历与数组类似,都可以使用foreach语句,下面通过一个实例说明如何遍历ArrayList集合中的元素。

【例7.33】 创建一个控制台应用程序,其中实例化了一个ArrayList对象,并使用Add方法向ArrayList集合中添加了两个元素,然后使用foreach语句遍历ArrayList集合中的各个元素并输出。程序代码如下。(实例位置:光盘\TM\sl\7\14)

按Ctrl+F5键查看运行结果,如图7.19所示。

图7.19 ArrayList的遍历实例运行结果

7.6.5 ArrayList元素的查找

查找ArrayList集合中的元素时,可以使用ArrayList类提供的Contains方法、IndexOf方法和LastIndexOf方法。IndexOf方法和LastIndexOf方法的用法与string字符串类的同名方法的用法基本相同,下面主要对Contains方法进行详细介绍。

Contains方法用来确定某元素是否在ArrayList集合中,其语法格式如下。

    public virtual bool Contains(Object item)

 item:要在ArrayList中查找的Object,该值可以为空引用。

 返回值:如果在ArrayList中找到item,则为true;否则为false。

【例7.34】 声明一个包含6个元素的一维数组,并使用该数组实例化一个ArrayList对象,然后使用Contains方法判断数字2是否在ArrayList集合中,代码如下。

    int[] arr = new int[] { 1, 2, 3, 4, 5, 6 };
    ArrayList List = new ArrayList(arr);
    Console.Write(List.Contains(2));             //判断 ArrayList 集合中是否包含指定的元素

程序的运行结果为true。

7.7 Hashtable(哈希表)

视频讲解:光盘\TM\lx\7\07 Hashtable(哈希表).mp4

Hashtable(哈希表)是一种重要的集合类型,本节将对Hashtable的概念及使用方法进行详细介绍。

7.7.1 Hashtable概述

Hashtable通常称为哈希表,它表示键/值对的集合,这些键/值对根据键的哈希代码进行组织。它的每个元素都是一个存储在DictionaryEntry对象中的键/值对。键不能为空引用,但值可以。

Hashtable的构造函数有多种,这里介绍两种最常用的。

(1)使用默认的初始容量、加载因子、哈希代码提供程序和比较器来初始化Hashtable类的新的空实例,语法如下。

   public Hashtable()

(2)使用指定的初始容量、默认加载因子、默认哈希代码提供程序和默认比较器来初始化Hashtable类的新的空实例,语法如下。

    public Hashtable(int capacity)

capacity:Hashtable对象最初可包含的元素的近似数目。

Hashtable常用属性及说明如表7.2所示。

表7.2 Hashtable常用属性及说明

7.7.2 Hashtable元素的添加

向Hashtable中添加元素时,可以使用Hashtable类提供的Add方法。下面对该方法进行详细介绍。

Add方法用来将带有指定键和值的元素添加到Hashtable中,其语法格式如下。

    public virtual void Add(Object key,Object value)

 key:要添加的元素的键。

 value:要添加的元素的值,该值可以为空引用。

说明

如果指定了Hashtable的初始容量,则不用限定向Hashtable对象中添加因子的个数。容量会根据加载的因子自动增加。

【例7.35】 创建一个控制台应用程序,其中实例化一个Hashtable对象,然后使用Add方法为该Hashtable对象添加3个元素,代码如下。(实例位置:光盘\TM\sl\7\15)

    Hashtable hashtable = new Hashtable();  //实例化 Hashtable 对象
    hashtable.Add("id", "BH0001");          //向 Hashtable 中添加元素
    hashtable.Add("name", "TM");
    hashtable.Add("sex", "男");
    Console.WriteLine(hashtable.Count);     //获得 Hashtable 中的元素个数

程序的运行结果为3。

7.7.3 Hashtable元素的删除

在Hashtable中删除元素时,可以使用Hashtable类提供的Clear方法和Remove方法。下面对这两个方法进行详细介绍。

1.Clear方法

Clear方法用来从Hashtable中移除所有元素,其语法格式如下。

    public virtual void Clear()

【例7.36】 创建一个控制台应用程序,其中实例化一个Hashtable对象,同时使用Add方法为该Hashtable对象添加3个元素,然后使用Clear方法移除Hashtable中的所有元素,代码如下。(实例位置:光盘\TM\sl\7\16)

    Hashtable hashtable = new Hashtable();  //实例化 Hashtable 对象
    hashtable.Add("id", "BH0001");          //向 Hashtable 中添加元素
    hashtable.Add("name", "TM");
    hashtable.Add("sex", "男");
    hashtable.Clear();                      //移除 Hashtable 中的元素
    Console.WriteLine(hashtable.Count);

程序的运行结果为0。

2.Remove方法

Remove方法用来从Hashtable中移除带有指定键的元素,其语法格式如下。

    public virtual void Remove(Object key)

key:要移除的元素的键。

【例7.37】 创建一个控制台应用程序,其中实例化一个Hashtable对象,同时使用Add方法为该Hashtable对象添加3个元素,然后使用Remove方法移除Hashtable中键为sex的元素,代码如下。(实例位置:光盘\TM\sl\7\17)

    Hashtable hashtable = new Hashtable();     //实例化 Hashtable 对象
    hashtable.Add("id", "BH0001");             //向 Hashtable 中添加元素
    hashtable.Add("name", "TM");
    hashtable.Add("sex", "男");
    hashtable.Remove("sex");                   //移除 Hashtable 中的指定元素
    Console.WriteLine(hashtable.Count);

程序的运行结果为2。

7.7.4 Hashtable的遍历

Hashtable的遍历与数组类似,都可以使用foreach语句。这里需要注意的是,由于Hashtable中的元素是一个键/值对,因此需要使用DictionaryEntry结构来进行遍历。DictionaryEntry结构表示一个键/值对的集合。下面通过一个实例说明如何遍历Hashtable中的元素。

【例7.38】 创建一个控制台应用程序,其中实例化了一个Hashtable对象,并使用Add方法向Hashtable中添加了3个元素,然后使用foreach语句遍历Hashtable中的各个键/值对并输出。程序代码如下。(实例位置:光盘\TM\sl\7\18)

    static void Main(string[] args)
    {
         Hashtable hashtable = new Hashtable();                               //实例化 Hashtable 对象
         hashtable.Add("id", "BH0001");                                       //向 Hashtable 中添加元素
         hashtable.Add("name", "TM");
         hashtable.Add("sex", "男");
         Console.WriteLine("\t 键\t 值");
         foreach (DictionaryEntry dicEntry in hashtable) //遍历 Hashtable 中的元素并输出其键/值对
         {
             Console.WriteLine("\t " + dicEntry.Key + "\t " + dicEntry.Value);
         }
         Console.WriteLine();
     }

按Ctrl+F5键查看运行结果,如图7.20所示。

7.7.5 Hashtable元素的查找

图7.20 Hashtable的遍历实例运行结果

在Hashtable中查找元素时,可以使用Hashtable类提供的Contains方法、ContainsKey方法和ContainsValue方法。下面主要对这3个方法进行详细介绍。

1.Contains方法

Contains方法用来确定Hashtable中是否包含特定键,其语法格式如下。

    public virtual bool Contains(Object key)

 key:要在Hashtable中定位的键。

 返回值:如果Hashtable包含具有指定键的元素,则为true;否则为false。

【例7.39】 创建一个控制台应用程序,其中实例化一个Hashtable对象,同时使用Add方法为该Hashtable对象添加3个元素,然后使用Contains方法判断键id是否在Hashtable中,代码如下。(实例位置:光盘\TM\sl\7\19)程序的运行结果为true。

    Hashtable hashtable = new Hashtable();              //实例化 Hashtable 对象
    hashtable.Add("id", "BH0001");                      //向 Hashtable 中添加元素
    hashtable.Add("name", "TM");
    hashtable.Add("sex", "男");
    Console.WriteLine(hashtable.Contains("id"));        //判断 Hashtable 中是否包含指定的键

说明

ContainsKey方法和Contains方法实现的功能、语法都相同,这里不再详细说明。

2.ContainsValue方法

ContainsValue方法用来确定Hashtable中是否包含特定值,其语法格式如下。

    public virtual bool ContainsValue(Object value)

 value:要在Hashtable中定位的值,该值可以为空引用。

 返回值:如果Hashtable包含带有指定的value的元素,则为true;否则为false。

【例7.40】 创建一个控制台应用程序,其中实例化一个Hashtable对象,同时使用Add方法为该Hashtable对象添加3个元素,然后使用ContainsValue方法判断值id是否在Hashtable中,代码如下。(实例位置:光盘\TM\sl\7\20)

    Hashtable hashtable = new Hashtable();                    //实例化 Hashtable 对象
    hashtable.Add("id", "BH0001");                            //向 Hashtable 中添加元素
    hashtable.Add("name", "TM");
    hashtable.Add("sex", "男");
    Console.WriteLine(hashtable.ContainsValue("id"));         //判断 Hashtable 中是否包含指定的键值

程序的运行结果为false。

互动练习:尝试使用哈希表记录网络电台程序中的电台名称及电台网址,具体要求为:在“电台地址”下拉列表中选择或输入电台地址,如果该电台地址已经存在于本程序的数据文件中,则会在“电台名称”下拉列表中自动显示其对应的电台名称;否则,用户可以在“电台名称”下拉列表中手动输入电台名称。

7.8 小结

本章首先对数组进行了详细讲解,然后介绍了两种常用的集合ArrayList和Hashtable。讲解数组时,主要将数组分为一维数组和二维数组,然后重点对数组的各种操作,例如遍历、添加、删除、排序、合并和拆分等进行了详细讲解。此外,又通过概述元素的添加、删除、遍历、查找的方式,介绍了ArrayList集合和Hashtable的使用。通过本章的学习,读者应该能够熟练掌握数组及其各种常用的操作,熟悉ArrayList集合及Hashtable的使用,并能将其应用于实际开发中。

7.9 实践与练习

(1)尝试开发一个程序,要求使用希尔排序法对定义的一维数组进行排序。(答案位置:光盘\ TM\sl\7\21)

(2)尝试开发一个程序,当用户输入一个字符串之后,判断该字符串中包含几个汉字(提示:可以使用ArrayList集合实现)。(答案位置:光盘\TM\sl\7\22)

7.10 动手纠错

(1)运行“光盘\TM\排错练习\07\01”文件夹下的程序,出现“应输入长度为5的数组初始值设定项”的错误提示,请根据注释改正程序。

(2)运行“光盘\TM\排错练习\07\02”文件夹下的程序,出现“未处理IndexOutOfRangeException索引超出了数组界限”的错误提示,请根据注释改正程序。

(3)运行“光盘\TM\排错练习\07\03”文件夹下的程序,出现“无法将类型int隐式转换为string”的错误提示,请根据注释改正程序。

(4)运行“光盘\TM\排错练习\07\04”文件夹下的程序,出现“无效的秩说明符:应为","或"]"”的错误提示,请根据注释改正程序。

(5)运行“光盘\TM\排错练习\07\05”文件夹下的程序,程序可以正常运行,但是获取的二维数组的行数并不正确,请根据注释改正程序。

(6)运行“光盘\TM\排错练习\07\06”文件夹下的程序,出现“未处理InvalidCastException无法将类型为System.Int32的对象强制转换为类型System.String”的错误信息,请根据注释改正程序。

第8章 属性和方法

第8章
属性和方法

视频讲解:43分钟

属性和方法是C#程序中的两个重要组成部分,其中,属性提供灵活的机制来读取、编写或计算私有字段的值,而方法则以一部分代码构成代码块的形式存在,用来实现特定的功能。本章将对属性和方法进行详细讲解。

通过阅读本章,您可以:

 了解属性的基本概念

 掌握属性的定义及使用

 了解方法的基本概念

 掌握方法的声明及使用

 掌握常用的几种方法参数类型

 掌握重载方法的使用

 熟悉Main方法的用途及注意事项

8.1 属性

视频讲解:光盘\TM\lx\8\01 属性.mp4

属性提供功能强大的方法以将声明信息与C#代码(类型、方法、属性等)相关联,一旦属性与程序实体关联,即可使用名为反射的技术对属性进行查询。本节将对属性进行详细讲解。

8.1.1 属性概述

属性是一种用于访问对象或类的特性的成员,它可以表示字体的大小、窗体的标题和客户的名称等内容。

属性有访问器,这些访问器指定在它们的值被读取或写入时需要执行的语句。因此属性提供了一种机制,它把读取和写入对象的某些特性与一些操作关联起来。可以像使用公共数据成员一样使用属性,但实际上它们是称为“访问器”的一种特殊方法,这使得数据在被轻松访问的同时,仍能提供方法的安全性和灵活性。

对于属性的理解其实并不难,相信大家都玩过游戏吧,其实在游戏中也能找到属性,例如人物属性,常见的有攻击、防御、速度、智力、敏捷、力量、生命值、魔法值等;而物品属性是用来加强人物属性的,常见的有加攻击力、加防御力、加生命、加魔法、加抗性等。

注意

属性不能作为ref参数或out参数传递。

属性具有以下特点:

 属性可向程序中添加元数据。元数据是嵌入程序中的信息,如编译器指令或数据描述。

 程序可以使用反射检查自己的元数据。

 通常使用属性与COM交互。

属性以两种形式存在:一种是在公共语言运行库的基类库中定义的属性,另一种是自己创建、可以向代码中添加附加信息的自定义属性。

【例8.1】 下面代码用来将System.Reflection.TypeAttributes.Serializable属性用于自定义类,以便使该类中的成员可以序列化,代码如下。

    [System.Serializable]
    public class MyClass
    {}

上面代码中的Serialzable为.NET Framework类库中定义的属性。

自定义属性在类中是通过以下方式声明的:指定属性的访问级别,后面是属性的类型,接下来是属性的名称,然后是声明get访问器和(或)set访问器的代码模块,其他语法格式如下:

    访问修饰符 数据类型 属性名
    {
        get
        {
            return 变量名;
        }
        set
        {
            变量名 = value;
        }
    }

访问修饰符用来确定属性的可用范围,下面介绍常用的几个访问修饰符。

 public:不限制对该属性的访问。

 protected:只能从其所在类和所在类的子类(派生类)进行访问。

 internal:只有其所在类才能访问。

 private:私有访问修饰符,只能在其声明类中使用。

【例8.2】 下面代码自定义了一个Date类,该类中有一个属性day,因为该属性提供了get和set访问器,因此它是可读可写属性,代码如下。

说明

get访问器与方法体相似,它必须返回属性类型的值;而set访问器类似于返回类型为void的方法,它使用称为value的隐式参数,此参数的类型是属性的类型。

8.1.2 属性的使用

程序中调用属性的语法格式如下。

    对象名.属性名

注意

①如果要在其他类中调用自定义属性,必须将自定义属性的访问级别设置为public。

②如果属性为只读属性,不能在调用时为其赋值,否则产生异常。

【例8.3】 创建一个控制台应用程序,其中定义了一个MyClass类,并在该类中定义了两个string类型的变量,分别用来记录用户的编号和姓名,然后在该类中自定义两个属性,用来表示用户编号和姓名。定义完成后,在Program主程序类中实例化自定义类MyClass的一个对象,并分别对其中定义的用户编号和用户姓名属性赋值,最后调用Console类的WriteLine方法将赋值后的用户编号和用户姓名输出。程序代码如下。(实例位置:光盘\TM\sl\8\1)

按Ctrl+F5键查看运行结果,如图8.1所示。

图8.1 属性的使用实例运行结果

8.2 方法

视频讲解:光盘\TM\lx\8\02 方法.mp4

方法是一种用于实现可以由对象或类执行的计算或操作的成员。类的方法主要是和类相关联的动作,它是类的外部界面。对于那些私有的字段来说,外部界面实现对它们的操作一般只能通过方法来实现。

方法就是为达到某种目的而采取的途径、步骤、手段等,例如以一个职员来进一步说明方法。方法先生:男,现任C#部门工程师;工作分配:用C#语言编写公司指定的项目,并对其进行测试。以上的方法先生相当于方法的名称,而工作分配相当于方法要实现的目的。

本节将对方法进行详细讲解。

8.2.1 方法的声明

方法是包含一系列语句的代码块,在C#中,每个执行指令都是在方法的上下文中完成的。

方法在类或结构中声明,声明时需要指定访问级别、返回值、方法名称及方法参数,方法参数放在括号中,并用逗号隔开。括号中没有内容表示声明的方法没有参数。

方法声明可以包含一组特性和private、public、protected、internal4个访问修饰符的任何一个有效组合,还可以包含new、static、virtual、override、sealed、abstract以及extern等修饰符。

如果以下所有条件都为真,则表明所声明的方法具有一个有效的修饰符组合。

 该声明包含一个有效的访问修饰符组合。

 该声明中所包含的修饰符彼此各不相同。

 该声明最多包含下列修饰符中的一个:static、virtual和override。

 该声明最多包含下列修饰符中的一个:new和override。

 如果该声明包含abstract修饰符,则该声明不包含下列任何修饰符:static、virtual、sealed或extern。

 如果该声明包含private修饰符,则该声明不包含下列任何修饰符:virtual、override或abstract。

 如果该声明包含sealed修饰符,则该声明还包含override修饰符。

方法声明的返回类型指定了由该方法计算和返回值的类型,如果该方法并不返回值,则其返回类型为void。

一个方法的名称和形参列表定义了该方法的签名,具体地讲,一个方法的签名由它的名称以及它的形参的个数、修饰符和类型组成。返回类型不是方法签名的组成部分,形参的名称也不是方法签名的组成部分。

注意

一个方法的返回类型和它的形参列表中所引用的各个类型,必须具有至少与该方法本身相同的可访问性。

对于abstract和extern方法,方法主体只包含一个分号。对于所有其他方法,方法主体由一个块组成,该块指定了在调用方法时要执行的语句。

方法的名称必须与在同一个类中声明的所有其他非方法成员的名称都不相同。此外,一个方法的签名必须与在同一个类中声明的所有其他方法的签名都不相同,并且在同一类中声明的两个方法的签名不能只有ref和out不同。

【例8.4】 声明一个public类型的无返回值方法method,代码如下。

    public void method()
    {
         Console.Write("方法声明");
    }

互动练习:计算器是编程初学者最常见的一个例子,很多高校通常都使用编写计算器代码来考查学生的编程能力,这里通过封装方法实现一个简单的计算器,要求在控制台中输入两个数及数学符号,通过调用方法计算相应的结果。

8.2.2 方法的参数类型

调用方法时可以给该方法传递一个或多个值,传给方法的值叫作实参,在方法内部,接收实参的变量叫作形参,形参在紧跟着方法名的括号中声明,形参的声明语法与变量的声明语法一样。形参只在括号内部有效。声明方法参数时,可以通过关键字params、ref和out实现,下面分别对这3种参数类型进行讲解。

1.params参数

params参数用来指定在参数数目可变时采用的方法参数,params参数必须是一维数组。

【例8.5】 本实例声明一个静态方法UseParams,接收一个string[]类型的参数,然后利用for循环输出数组的元素。代码如下。(实例位置:光盘\TM\sl\8\2)

    static void UseParams(params string[] list)
    {
         for (int i = 0; i < list.Length; i++)
         {
               Console.WriteLine(list[i]);
    }
         }
    static void Main()
    {
         string[] strName = new string[5] { "我", "是", "中", "国", "人" };
         UseParams(strName);
         Console.Read();
     }

按Ctrl+F5键查看运行结果,如图8.2所示。

2.ref参数

ref参数使方法参数按引用传递,其效果是:当控制权传递回调用方法时,在方法中对参数所做的任何更改都将反映在该变量中。如果要使用ref参数,则方法声明和调用方法都必须显式使用ref关键字。

【例8.6】 本实例声明一个静态方法Method,并接收一个int型的ref参数。代码如下。(实例位置:光盘\TM\sl\8\3)

    public static void Method(ref int i)
    {
         i = 44;
    }
    public static void Main()
    {
         int val = 0;
         Method(ref val);
         Console.WriteLine(val);
         Console.Read();
    }

按Ctrl+F5键查看运行结果,如图8.3所示。

图8.2 params参数的使用

图8.3 ref参数的使用

3.out参数

out关键字用来定义输出参数,它会导致参数通过引用来传递,这与ref关键字类似,不同之处在于ref要求变量必须在传递之前进行初始化,而使用out关键字定义的参数,不用进行初始化即可使用。如果要使用out参数,则方法声明和调用方法都必须显式使用out关键字。

【例8.7】 本实例声明一个静态方法Method,并接收一个out类型的参数。代码如下。(实例位置:光盘\TM\sl\8\4)

    public static void Method(out int i)
    {
         i = 44;
    }
    public static void Main()
    {
         int value;
         Method(out value);
         Console.WriteLine("输出参数:"+value);
         Console.Read();
    }

按Ctrl+F5键查看运行结果,如图8.4所示。

图8.4 out参数的使用

8.2.3 方法的分类

方法分为静态方法和非静态方法,如果一个方法声明中含有static修饰符,则称该方法为静态方法。如果没有static修饰符,则称该方法为非静态方法。下面分别对静态方法和非静态方法进行介绍。

1.静态方法

静态方法不对特定实例进行操作,调用时,需要直接使用类名进行调用。

【例8.8】 创建一个控制台应用程序,其中定义了一个静态的方法Add,该方法有两个参数,其返回类型为int,它主要用来实现两个整数相加的功能,然后在主函数Main中使用类名直接调用自定义的静态方法,并传递两个参数。程序代码如下。(实例位置:光盘\TM\sl\8\5)

    public static int Add(int x, int y)                    //定义一个静态方法
    {
         return x + y;
    }
         static void Main(string[] args)
    {
         Console.WriteLine("结果为:" + Program.Add(3, 5));  //使用类名调用静态方法
    }

按Ctrl+F5键查看运行结果,如图8.5所示。

2.非静态方法

非静态方法是对类的某个给定的实例进行操作,调用时,需要使用类的实例(对象)进行调用。

【例8.9】 创建一个控制台应用程序,其中定义了一个非静态的方法Add。该方法有两个参数,其返回类型为int,它主要用来实现两个整数相加的功能,然后在主函数Main中实例化Program类的一个对象,并使用该对象名调用自定义的非静态方法,并传递两个参数。程序代码如下。(实例位置:光盘\TM\sl\8\6)

    public int Add(int x, int y)                              //定义一个非静态方法
    {
         return x + y;
    }
    static void Main(string[] args)
    {
         Program program = new Program();                       //实例化类对象
         Console.WriteLine("结果为:" + program.Add(3, 5));     //使用类对象调用定义的非静态方法
    }

说明

调用非静态方法时,也可以使用this关键字。

按Ctrl+F5键查看运行结果,如图8.6所示。

图8.5 静态方法的使用实例运行结果

图8.6 非静态方法的使用实例运行结果

8.2.4 方法的重载

方法重载是指方法名相同,但参数的数据类型、个数或顺序不同的方法。只要类中有两个以上的同名方法,但是使用的参数类型、个数或顺序不同,调用时,编译器即可判断在哪种情况下调用哪种方法。

【例8.10】 创建一个控制台应用程序,其中定义了一个重载方法Add,并在Main方法中分别调用其各种重载形式对传入的参数进行计算。程序代码如下。(实例位置:光盘\TM\sl\8\7)

    public static int Add(int x, int y)              //定义一个静态方法 Add,返回值为 int 类型,有两个 int 类型的参数
    {
         return x + y;
    }
    public double Add(int x, double y)               //重新定义方法 Add,它与第一个的返回值类型及参数类型不同
    {
         return x + y;
    }
    public int Add(int x, int y,int z)               //重新定义方法 Add,它与第一个的参数个数不同
    {
         return x + y + z;
    }
    static void Main(string[] args)
    {
         Program program = new Program();            //实例化类对象
         int x = 3;
         int y = 5;
         int z = 7;
         double y2 = 5.5;
         //根据传入的参数类型及参数个数的不同调用不同的 Add 重载方法
         Console.WriteLine(x + "+" + y + "=" + Program.Add(x, y));
         Console.WriteLine(x + "+" + y2 + "=" + program.Add(x, y2));
         Console.WriteLine(x + "+" + y + "+" + z + "=" + program.Add(x, y, z));
    }

按Ctrl+F5键查看运行结果,如图8.7所示。

图8.7 方法的重载实例运行结果

8.2.5 Main方法

Main方法是程序的入口点,程序将在此处创建对象和调用其他方法,一个C#程序中只能有一个入口点,每新建一个项目,程序都会自动生成一个Main方法,默认的Main方法代码如下。

    static void Main(string[] args){}

说明

Main方法默认访问级别为private。

Main方法是一个特别重要的方法,使用时需要注意以下几点:

 Main方法是程序的入口点,程序控制在该方法中开始和结束。

 该方法在类或结构的内部声明,它必须为静态方法,而且不应该为公共方法。

 它可以具有void或int返回类型。

 声明Main方法时既可以使用参数,也可以不使用参数。

 参数可以作为从零开始索引的命令行参数来读取。

8.3 小结

本章中首先对属性及其使用进行了介绍,然后分别通过声明、参数类型、分类和重载等对方法进行了详细讲解,最后对C#中的入口点方法Main进行了简单介绍。通过本章的学习,读者应该能够熟练使用属性和方法进行实际项目开发,以方便代码后期的维护。

8.4 实践与练习

(1)尝试开发一个程序,要求在自定义类中定义一个星期的属性,并且最大值不能超过7天。(答案位置:光盘\TM\sl\8\8)

(2)尝试开发一个程序,要求在自定义类中定义两个方法,然后在Program主程序类中定义一个方法,通过两个类中方法的互相调用,输出“这是一个方法的示例!”字样。(答案位置:光盘\TM\sl\8\9)

8.5 动手纠错

(1)运行“光盘\TM\排错练习\08\01”文件夹下的程序,出现“Test.MyClass.ID不可访问,因为它受保护级别限制”的错误提示,请根据注释改正程序。

(2)运行“光盘\TM\排错练习\08\02”文件夹下的程序,出现“无法对属性或索引器Test.MyClass. ID赋值--它是只读的”的错误提示,请根据注释改正程序。

(3)运行“光盘\TM\排错练习\08\03”文件夹下的程序,出现“Test.Program.Add(int,int,int):并非所有的代码路径都返回值”的错误提示,请根据注释改正程序。

(4)运行“光盘\TM\排错练习\08\04”文件夹下的程序,出现“Add方法没有任何重载采用2个参数”的错误提示,请根据注释改正程序。

(5)运行“光盘\TM\排错练习\08\05”文件夹下的程序,出现“类型Test.Program已定义了一个名为Add的具有相同参数类型的成员”的错误提示,请根据注释改正程序。

(6)运行“光盘\TM\排错练习\08\06”文件夹下的程序,出现“非静态字段、方法或属性Test.Program.Add(int,int)要求对象引用”的错误提示,请根据注释改正程序。

(7)运行“光盘\TM\排错练习\08\07”文件夹下的程序,出现“与Test.Program.ShowInfo(string)最匹配的重载方法具有一些无效参数”及“参数1:无法从int转换为string”的错误提示,请根据注释改正程序。

第9章 结构和类

第9章
结构和类

视频讲解:64分钟

本章将介绍C#中两个重要的概念:结构和类。结构是从过程化程序设计中保留下来的一种数据类型,而类则是面向对象程序设计中的最基本、也是最重要的一个概念。本章将对面向对象技术、结构和类进行详细讲解。

通过阅读本章,您可以:

 了解结构的基本概念

 掌握结构的用途及使用方法

 了解面向对象技术的基本概念

 了解类的基本概念

 掌握类及其构造函数、析构函数的使用

 掌握对象的声明及实例化

 掌握类的封装、继承和多态

9.1 结构

视频讲解:光盘\TM\lx\9\结构.exe

结构就是几个数据组成的数据结构,它与类共享几乎所有相同的语法,但结构比类受到的限制更多。本节将对结构进行详细讲解。

9.1.1 结构概述

结构是一种值类型,通常用来封装一组相关的变量,结构中可以包括构造函数、常量、字段、方法、属性、运算符、事件和嵌套类型等。但如果要同时包括上述几种成员,则应该考虑使用类。

结构实际是将多个相关的变量包装成为一个整体使用。在结构体中的变量,可以是相同、部分相同,或完全不同的数据类型。例如,将公司里的职员看作一个结构体,可以将个人信息放入结构体中,主要包含姓名、年龄、出生年月、性别、籍贯、婚否、职务。

结构具有以下特点:

 结构是值的类型。

 向方法传递结构时,结构是通过传值方式传递的,而不是作为引用传递的。

 结构的实例化可以不使用new运算符。

 结构可以声明构造函数,但它们必须带参数。

 一个结构不能从另一个结构或类继承。所有结构都直接继承自System.ValueType,后者继承自System.Object。

 结构可以实现接口。

 在结构中初始化实例字段是错误的。

说明

在结构声明中,除非字段被声明为const或static,否则无法初始化。

C#中使用struct关键字来声明结构,语法如下。

    结构修饰符 struct 结构名
    {
    }

【例9.1】 下面声明一个矩形结构,该结构中定义了矩形的宽和高,并自定义了一个Area方法,用来计算矩形的面积,代码如下。

    public struct Rect                   //定义一个矩形结构
    {
         public double width;            //矩形的宽
         public double height;           //矩形的高
         public double Area()            //矩形面积{
         {
             return width * height;
         }
    }

9.1.2 结构的使用

下面通过一个实例说明如何在程序中使用结构。

【例9.2】 创建一个控制台应用程序,其中声明一个矩形结构,该结构中定义了矩形的宽和高。在该结构中定义一个构造函数,该构造函数中有两个参数,用来初始化矩形的宽和高,接着自定义一个Area方法,用来计算矩形的面积。然后在Main方法中实例化矩形结构的一个对象,并通过调用结构中的自定义方法计算矩形的面积,最后使用矩形结构的构造函数再次实例化矩形结构的一个对象,并再次调用结构中的自定义方法计算矩形的面积。程序代码如下。(实例位置:光盘\TM\sl\9\1)

按Ctrl+F5键查看运行结果,如图9.1所示。

图9.1 结构的使用实例运行结果

9.2 面向对象概述

在程序开发初期人们使用结构化开发语言,但随着软件的规模越来越庞大,结构化语言的弊端也逐渐暴露出来,开发周期被无休止地拖延,产品的质量也不尽如人意,结构化语言已经不再适合当前的软件开发。这时人们开始将另一种开发思想引入程序中,即面向对象的开发思想。面向对象思想是人类最自然的一种思考方式,它将所有预处理的问题抽象为对象,同时了解这些对象具有哪些相应的属性以及展示这些对象的行为,以解决这些对象面临的一些实际问题,这样就在程序开发中引入了面向对象设计的概念,面向对象设计实质上就是对现实世界的对象进行建模操作。

9.2.1 对象

在面向对象中,算法与数据结构被看作一个整体,称为对象。现实世界中任何类的对象都具有一定的属性和操作,也总能用数据结构与算法两者合而为一来描述,所以可以用下面的等式来定义对象和程序。

对象=(算法+数据结构),程序=(对象+对象+……)。

从上面的等式可以看出,程序就是许多对象在计算机中相继表现自己,而对象则是一个个程序实体。

现实世界中,随处可见的一种事物就是对象,对象是事物存在的实体,如人类、书桌、计算机、高楼大厦等。人类解决问题的方式总是将复杂的事物简单化,于是就会思考这些对象都是由哪些部分组成的。通常都会将对象划分为两个部分,即动态部分与静态部分。静态部分,顾名思义就是不能动的部分,这个部分被称为“属性”,任何对象都会具备其自身属性,如一个人,包括高矮、胖瘦、性别、年龄等属性。然而具有这些属性的人会执行哪些动作也是一个值得探讨的部分,如可以哭泣、微笑、说话、行走等,这些是这个人具备的行为(动态部分),人类通过探讨对象的属性和观察对象的行为了解对象。

在计算机的世界中,面向对象程序设计的思想要以对象来思考问题,首先要将现实世界的实体抽象为对象,然后考虑这个对象具备的属性和行为。例如,现在面临一只大雁要从北方飞往南方这样一个实际问题,试着以面向对象的思想来解决这一实际问题。步骤如下:

图9.2 识别对象的属性

(1)首先可以从这一问题中抽象出对象,这里抽象出的对象为大雁。

(2)然后识别这个对象的属性。对象具备的属性都是静态属性,如大雁有一对翅膀、黑色的羽毛等。这些属性如图9.2所示。

(3)接着识别这个对象的动态行为,即这只大雁可以进行的动作,如飞行、觅食等,这些行为都是因为这个对象基于其属性而具有的动作。这些行为如图9.3所示。

(4)识别出这些对象的属性和行为后,这个对象就被定义完成,然后可以根据这只大雁具有的特性制定这只大雁要从北方飞向南方的具体方案以解决问题。

实际上,究其本质,所有的大雁都具有以上的属性和行为,可以将这些属性和行为封装起来以描述大雁这类动物。由此可见,类实质上就是封装对象属性和行为的载体,而对象则是类抽象出来的一个实例,两者之间的关系如图9.4所示。

图9.3 识别对象具有的行为

图9.4 描述对象与类之间的关系

9.2.2 类

不能将所谓的一个事物描述成一类事物,如一只鸟不能称为鸟类,如果需要对同一类事物统称,就不得不说明类这个概念。

图9.5 鸟类结构

类就是同一类事物的统称,如果将现实世界中的一个事物抽象成对象,类就是这类对象的统称,如鸟类、家禽类、人类等。类是构造对象时所依赖的规范,如一只鸟具有一对翅膀,它可以通过这对翅膀飞行,而基本上所有的鸟都具有翅膀这个特性和飞行的技能,这样的具有相同特性和行为的一类事物就称为类,类的思想就是这样产生的。在图9.4中已经描述过类与对象之间的关系,对象就是符合某个类定义所产生出来的实例。更为恰当的描述是:类是世间事物的抽象称呼,而对象则是这个事物相对应的实体。如果面临实际问题,通常需要实例化类对象来解决。例如,解决大雁南飞的问题,这里只能拿这只大雁来处理这个问题,不能拿大雁类或是鸟类来解决。

类是封装对象的属性和行为的载体,反过来说具有相同属性和行为的一类实体被称为类。例如,一个鸟类,鸟类封装了所有鸟的共同属性和应具有的行为,其结构如图9.5所示。

定义完成鸟类之后,可以根据这个类抽象出一个实体对象,最后通过实体对象来解决相关的实际问题。

在C#语言中,类中对象的行为是以方法的形式定义的,对象的属性是以成员变量的形式定义的,而类包括对象的属性和方法,有关类的具体实现会在后续章节中进行介绍。

9.2.3 封装

面向对象程序设计具有以下特点:

 封装性。

 继承性。

 多态性。

封装是面向对象编程的核心思想,将对象的属性和行为封装起来,而将对象的属性和行为封装起来的载体就是类,类通常对客户隐藏其实现细节,这就是封装的思想。例如,用户使用计算机,只需要使用手指敲击键盘就可以实现一些功能,用户无须知道计算机内部是如何工作的,即使用户可能知道计算机的工作原理,但在使用计算机时并不完全依赖于计算机工作原理这些细节。

采用封装的思想保证了类内部数据结构的完整性,应用该类的用户不能轻易直接操作此数据结构,而只能执行类允许公开的数据。这样就避免了外部对内部数据的影响,提高了程序的可维护性。

使用类实现封装特性如图9.6所示。

图9.6 封装特性示意图

9.2.4 继承

类与类之间同样具有关系,如一个百货公司类与销售员类相联系,类之间的这种关系被称为关联。关联是描述两个类之间的一般二元关系,例如,一个百货公司类与销售员类就是一个关联,学生类与教师类也是一个关联。两个类之间的关系有很多种,继承是关联中的一种。

当处理一个问题时,可以将一些有用的类保留下来,当遇到同样问题时拿来复用。假如这时需要解决信鸽送信的问题,我们很自然就会想到图9.5所示的鸟类。由于鸽子属于鸟类,鸽子具有与鸟类相同的属性和行为。便可以在创建信鸽类时将鸟类拿来复用,并且保留鸟类具有的属性和行为。不过,并不是所有的鸟都有送信的习惯,因此还需要再添加一些信鸽具有的独特属性以及行为。鸽子类保留了鸟类的属性和行为,这样就节省了定义鸟和鸽子共同具有的属性和行为的时间,这就是继承的基本思想。可见软件的代码使用继承思想可以缩短软件开发的时间,复用那些已经定义好的类可以提高系统性能,减少系统在使用过程中出现错误的几率。

继承性主要利用特定对象之间的共有属性。例如,平行四边形是四边形(正方形、矩形也都是四边形),平行四边形与四边形具有共同特性,就是拥有4个边,可以将平行四边形类看作四边形的延伸,平行四边形复用了四边形的属性和行为,同时添加了平行四边形独有的属性和行为,如平行四边形的对边平行且相等。这里可以将平行四边形类看作是从四边形类中继承的。在C#语言中将类似于平行四边形的类称为子类,将类似于四边形的类称为父类。值得注意的是,可以说平行四边形是特殊的四边形,但不能说四边形是平行四边形,也就是说子类的实例都是父类的实例,但不能说父类的实例是子类的实例。图9.7阐明了图形类之间的继承关系。

图9.7 图形类层次结构示意图

从图9.7中可以看出,继承关系可以使用树形关系来表示,父类与子类存在一种层次关系。一个类处于继承体系中,它既可以是其他类的父类,为其他类提供属性和行为,也可以是其他类的子类,继承父类的属性和方法,如三角形既是图形类的子类同时也是等边三角形的父类。

9.2.5 多态

在9.2.4节中介绍了继承,了解了父类和子类,其实将父类对象应用于子类的特征就是多态。依然以图形类来说明多态,每个图形都拥有绘制自己的能力,这个能力可以看作是该类具有的行为,如果将子类的对象统一看作是父类的实例对象,这样当绘制任何图形时,可以简单地调用父类也就是图形类绘制图形的方法即可绘制任何图形,这就是多态最基本的思想。

多态性允许以统一的风格编写程序,以处理种类繁多的已存在的类以及相关类。该统一风格可以由父类来实现,根据父类统一风格的处理,就可以实例化子类的对象。由于整个事件的处理都只依赖于父类的方法,所以日后只要维护和调整父类的方法即可,这样就降低了维护的难度,节省了时间。

在提到多态的同时,不得不提到抽象类和接口,因为多态的实现并不依赖具体类,而是依赖于抽象类和接口。

再回到绘制图形的实例上来。作为所有图形的父类图形类,它具有绘制图形的能力,这个方法可以称为“绘制图形”,但如果要执行这个“绘制图形”的命令,没有人知道应该画什么样的图形,并且如果要在图形类中抽象出一个图形对象,没有人能说清这个图形究竟是什么图形,所以使用“抽象”这个词汇来描述图形类比较恰当。在C#语言中称这样的类为抽象类,抽象类不能实例化对象。在多态的机制中,父类通常会被定义为抽象类,在抽象类中给出一个方法的标准,而不给出实现的具体流程。实质上这个方法也是抽象的,如图形类中的“绘制图形”方法只提供一个可以绘制图形的标准,并没有提供具体绘制图形的流程,因为没有人知道究竟需要绘制什么形状的图形。

在多态的机制中,比抽象类更为方便的方式是将抽象类定义为接口。由抽象方法组成的集合就是接口。接口的概念在现实中也极为常见,如从不同的五金商店买来螺丝帽和螺丝钉,螺丝帽很轻松地就可以拧在螺丝钉上,可能螺丝帽和螺丝钉的厂家不同,但这两个物品可以很轻易地组合在一起,这是因为生产螺丝帽和螺丝钉的厂家都遵循着一个标准,这个标准在C#中就是接口。依然拿“绘制图形”来说明,可以将“绘制图形”作为一个接口的抽象方法,然后使图形类实现这个接口,同时实现“绘制图形”这个抽象方法,当三角形类需要绘制时,就可以继承图形类,重写其中的“绘制图形”方法,并改写这个方法为“绘制三角形”,这样就可以通过这个标准绘制不同的图形。

9.3 类

视频讲解:光盘\TM\lx\9\类.exe

类是一种数据结构,它可以包含数据成员(常量和域)、函数成员(方法、属性、事件、索引器、运算符、构造函数和析构函数)和嵌套类型。类支持继承,继承是一种使子类(派生类)可以对基类进行扩展和专用化的机制。

类(Class)实际上是对某种类型的对象定义变量和方法的原型,它表示对现实生活中一类具有共同特征的事物的抽象,是面向对象编程的基础。

本节将对类进行详细讲解。

9.3.1 类的概念

类是对象概念在面向对象编程语言中的反映,是相同对象的集合。类描述了一系列在概念上有相同含义的对象,并为这些对象统一定义了编程语言上的属性和方法。例如水果就可以看作一个类,苹果、梨、葡萄都是该类的子类(派生类),苹果的生产地、名称(如富士苹果)、价格、运输途径相当于该类的属性,苹果的种植方法相当于类方法。果汁也可以看作一个类,包含苹果汁、葡萄汁、草莓汁等。如果想要知道苹果汁是用什么地方的苹果制作而成的,可以查看水果类中关于苹果的相关属性。这时就用到了类的继承,也就是说果汁类是水果类的继承类。简而言之,类是C#中功能最为强大的数据类型,像结构一样,类也定义了数据类型的数据和行为。然后,程序开发人员可以创建作为此类的实例的对象。与结构不同,类支持继承,而继承是面向对象编程的基础部分。

9.3.2 类的声明

C#中,类是使用class关键字来声明的,语法如下。

    类修饰符 class 类名
    {
    }

【例9.3】 下面以汽车为例声明一个类,代码如下。

    public class Car
    {
         public int number;      //编号
         public string color;    //颜色
         private string brand;   //厂家
    }

public是类的修饰符,下面介绍常用的几个类修饰符。

 new:仅允许在嵌套类声明时使用,表明类中隐藏了由基类中继承而来的、与基类中同名的成员。

 public:不限制对该类的访问。

 protected:只能从其所在类和所在类的子类(派生类)进行访问。

 internal:只有其所在类才能访问。

 private:只有.NET中的应用程序或库才能访问。

 abstract:抽象类,不允许建立类的实例。

 sealed:密封类,不允许被继承。

说明

类定义可在不同的源文件之间进行拆分。

9.3.3 构造函数和析构函数

构造函数和析构函数是类中比较特殊的两种成员函数,主要用来对对象进行初始化和回收对象资源。一般来说,对象的生命周期从构造函数开始,以析构函数结束。如果一个类含有构造函数,在实例化该类的对象时就会调用,如果含有析构函数,则会在销毁对象时调用。构造函数的名字和类名相同,析构函数和构造函数的名字相同,但析构函数要在名字前加一个波浪号(~)。当退出含有该对象的成员时,析构函数将自动释放这个对象所占用的内存空间。本节将详细介绍如何在程序中使用构造函数和析构函数。

1.构造函数的概念及使用

构造函数是在创建给定类型的对象时执行的类方法。构造函数具有与类相同的名称,它通常初始化新对象的数据成员。

【例9.4】 创建一个控制台应用程序,在Program类中定义了3个int类型的变量,分别用来表示加数、被加数和加法的和,然后声明Program类的一个构造函数,并在该构造函数中为加法的和赋值,最后在Main方法中实例化Program类的对象,并输出加法的和。程序代码如下。(实例位置:光盘\TM\sl\9\2)

按Ctrl+F5键查看运行结果,如图9.8所示。

图9.8 构造函数的使用实例运行结果

注意

不带参数的构造函数称为“默认构造函数”。无论何时,只要使用new运算符实例化对象,并且不为new提供任何参数,就会调用默认构造函数。

2.析构函数的概念及使用

析构函数是以类名加~来命名的。.NET Framework类库有垃圾回收功能,当某个类的实例被认为是不再有效,并符合析构条件时,.NET Framework类库的垃圾回收功能就会调用该类的析构函数实现垃圾回收。

【例9.5】 创建一个控制台应用程序,其中在Program类中声明了其析构函数,并在该析构函数中输出了一个字符串。这时在Main方法中实例化Program类的对象,运行程序时,自动调用析构函数,并实现析构函数中的功能。程序代码如下。(实例位置:光盘\TM\sl\9\3)

    class Program
    {
        ~Program()                                //析构函数
        {
            Console.WriteLine("析构函数自动调用"); //输出一个字符串
        }
        static void Main(string[] args)
        {
            Program program = new Program();       //实例化 Program 对象
        }
    }

程序的运行结果为“析构函数自动调用”。

注意

一个类中只能有一个析构函数,并且无法调用析构函数,它是被自动调用的。

9.3.4 对象的创建及使用

C#是一门面向对象的程序设计语言,对象是由类抽象出来的,所有的问题都是通过对象来处理,对象可以操作类的属性和方法解决相应的问题,所以了解对象的产生、操作和消亡对学习C#是十分必要的。本节就来讲解对象在C#语言中的应用。

1.对象的创建

对象可以认为是在一类事物中抽象出某一个特例,通过这个特例来处理这类事物出现的问题。在C#语言中通过new操作符来创建对象。前面在讲解构造函数时介绍过,每实例化一个对象就会自动调用一次构造函数,实质上这个过程就是创建对象的过程。准确地说,可以在C#语言中使用new操作符调用构造函数创建对象。

语法如下:

    Test test=new Test();
    Test test=new Test("a");

其参数说明如表9.1所示。

表9.1 创建对象语法中的参数说明

test对象被创建出来时,test对象就是一个对象的引用,这个引用在内存中为对象分配了存储空间;另外,可以在构造函数中初始化成员变量,当创建对象时,自动调用构造函数,也就是说在C#语言中初始化与创建是被捆绑在一起的。

每个对象都是相互独立的,在内存中占据独立的内存地址,并且每个对象都具有自己的生命周期,当一个对象的生命周期结束时,对象变成了垃圾,由.NET自带的垃圾回收机制处理。

注意

在C#语言中对象和实例事实上可以通用。

下面来看一个创建对象的示例。

【例9.6】 在项目中创建CreateObject类,在该类中创建对象并在主方法中创建对象。

    public class CreateObject
    {
         public CreateObject()                   //构造函数
         {
              Console.WriteLine("创建对象");
         }
         public static void main(String args[])  //主方法
         {
              new CreateObject();                //创建对象
         }
    }

在上述实例的主方法中使用new操作符创建对象,在创建对象的同时,将自动调用构造函数中的代码。

2.访问对象的属性和行为

当用户使用new操作符创建一个对象后,可以使用“对象.类成员”来获取对象的属性和行为。前文已经提到过,对象的属性和行为在类中是通过类成员变量和成员方法的形式来表示的,所以当对象获取类成员时,也就相应地获取了对象的属性和行为。

【例9.7】 在项目中创建Program类,在该类中说明对象是如何调用类成员的。(实例位置:光盘\TM\sl\9\4)

运行程序,结果如图9.9所示。

在上述代码的主方法中首先实例化一个对象,然后使用“.”操作符调用类的成员变量和成员方法。但是在运行结果中可以看到,虽然使用两个对象调用同一个成员变量,结果却不相同,因为在打印这个成员变量的值之前将该值重新赋值为60,但在赋值时使用的是第二个对象t2调用成员变量,所以在第一个对象t1调用成员变量打印该值时仍然是成员变量的初始值,由此可见,两个对象的产生是相互独立的,改变了t2的i值,不会影响到t1的i值。在内存中这两个对象的布局如图9.10所示。

图9.9 使用对象调用类成员运行结果

图9.10 内存中t1、t2两个对象的布局

3.对象的引用

在C#语言中尽管一切都可以看作对象,但真正的操作标识符实质上是一个引用,那么引用究竟在C#中是如何体现的呢?来看下面的语法。

语法如下:

    类名 对象引用名称

如一个Book类的引用可以使用以下代码:

    Book book;

通常一个引用不一定需要有一个对象相关联。引用与对象相关联的语法如下:

    Book book=new Book();

 Book:类名。

 book:对象。

 new:创建对象操作符。

注意

引用只是存放一个对象的内存地址,并非存放一个对象,严格地说引用和对象是不同的,但是可以将这种区别忽略,如可以简单地说book是Book类的一个对象,而事实上应该是book包含Book对象的一个引用。

4.对象的销毁

每个对象都有生命周期,当对象的生命周期结束时,分配给该对象的内存地址将会被回收。在其他语言中需要手动回收废弃的对象,但是C#拥有一套完整的垃圾回收机制,用户不必担心废弃的对象占用内存,垃圾回收器将回收无用的但占用内存的资源。

在谈到垃圾回收机制之前,首先需要了解何种对象会被.NET垃圾回收器视为垃圾。主要包括以下两种情况:

 对象引用超过其作用范围,则这个对象将被视为垃圾,如图9.11所示。

 将对象赋值为null,如图9.12所示。

图9.11 对象超过作用范围将消亡

图9.12 对象被置为null值时将消亡

9.3.5 this关键字

【例9.8】 在项目中创建一个类文件,该类中定义了setName(),并将方法的参数值赋予类中的成员变量。

    private void setName(String name)     //定义一个 setName()方法
    {
         this.name=name;                  //将参数值赋予类中的成员变量
    }

在上述代码中可以看到,成员变量与在setName()方法中的形式参数的名称相同,都为name,那么该如何在类中区分使用的是哪一个变量呢?在C#语言中规定使用this关键字来代表本类对象的引用,this关键字被隐式地用于引用对象的成员变量和方法,如在上述代码中,this.name指的就是Book类中的name成员变量,而this.name=name语句中的第二个name则指的是形参name。实质上setName()方法实现的功能就是将形参name的值赋予成员变量name。

在这里,读者明白了this可以调用成员变量和成员方法,但C#语言中最常规的调用方式是使用“对象.成员变量”或“对象.成员方法”进行调用。

既然this关键字和对象都可以调用成员变量和成员方法,那么this关键字与对象之间具有怎样的关系呢?

事实上,this引用的就是本类的一个对象,在局部变量或方法参数覆盖了成员变量时,如上面代码的情况,就要添加this关键字明确引用的是类成员还是局部变量或方法参数。

如果省略this关键字,直接写成name=name,那只是把参数name赋值给参数变量本身而已,成员变量name的值没有改变,因为参数name在方法的作用域中覆盖了成员变量name。

其实,this除了可以调用成员变量或成员方法之外,还可以作为方法的返回值。

【例9.9】 在项目中创建一个类文件,在该类中定义Book类型的方法,并通过this关键字进行返回。

    public Book getBook()
    {
         return this;        //返回 Book 类引用
    }

在getBook()方法中,方法的返回值为Book类,所以方法体中使用return this这种形式将Book类的对象进行返回。

9.3.6 类与对象的关系

类是一种抽象的数据类型,但是其抽象的程度可能不同,而对象就是一个类的实例,例如,将农民设计为一个类,张三和李四就可以各为一个对象。

从这里可以看出,张三和李四有很多共同点,他们都在某个农村生活,早上都要出门务农,晚上都会回家。对于这样相似的对象就可以将其抽象出一个数据类型,此处抽象为农民。这样,只要将农民这个数据类型编写好,程序中就可以方便地创建张三和李四这样的对象。在代码需要更改时,只需要对农民类型进行修改即可。

综上所述,可以看出类与对象的区别:类是具有相同或相似结构、操作和约束规则的对象组成的集合,而对象是某一类的具体化实例,每一个类都是具有某些共同特征的对象的抽象。

9.4 类的面向对象特性

视频讲解:光盘\TM\lx\9\类的面向对象特性.exe

9.4.1 类的封装

C#中可使用类来达到数据封装的效果,这样就可以使数据与方法封装成单一元素,以便于通过方法存取数据。除此之外,还可以控制数据的存取方式。

在面向对象编程中,大多数都是以类作为数据封装的基本单位。类将数据和操作数据的方法结合成一个单位。设计类时,不希望直接存取类中的数据,而是希望通过方法来存取数据,这样就可以达到封装数据的目的,方便以后的维护升级,也可以在操作数据时多一层判断。

此外,封装还可以解决数据存取的权限问题,可以使用封装将数据隐藏起来,形成一个封闭的空间,然后可以设置哪些数据只能在这个空间中使用,哪些数据可以在空间外部使用。一个类中包含敏感数据,有些人可以访问,有些人不能访问,如果不对这些数据的访问加以限制,后果将会非常严重。所以在编写程序时,要对类的成员使用不同的访问修饰符,从而定义它们的访问级别。

封装的目的是增强安全性和简化编程,使用者不必了解具体的实现细节,而只是要通过外部接口这一特定的访问权限来使用类的成员。例如充电器,它是将220V的电源经过降压整流滤波后,用导线与电池相连,然后进行充电。而降压整流滤波这一过程就相当于类的封装。

【例9.10】 创建一个控制台应用程序,其中自定义了一个MyClass类,该类用来封装加数和被加数属性;然后自定义一个Add方法,该方法用来返回该类中两个int属性的和;Program主程序类中,实例化自定义类的对象,并分别为MyClass类中的两个属性赋值,最后调用MyClass类中的自定义方法Add返回两个属性的和。程序代码如下。(实例位置:光盘\TM\sl\9\5)

程序的运行结果为8。

9.4.2 类的继承

继承是面向对象编程最重要的特性之一。任何类都可以从另外一个类继承,这就是说,这个类拥有它继承的类的所有成员。在面向对象编程中,被继承的类称为父类或基类。C#中提供了类的继承机制,但只支持单继承,而不支持多重继承,即在C#中一次只允许继承一个类,不能同时继承多个类。

利用类的继承机制,用户可以通过增加、修改或替换类中的方法对这个类进行扩充,以适应不同的应用要求。利用继承,程序开发人员可以在已有类的基础上构造新类,这一性质使类支持分类的概念。在日常生活中很多东西都很有条理,那是因为它们有着很好的层次分类。如果不用层次分类,则需要对每个对象都定义其所有的性质。使用继承后,每个对象就可以只定义自己的特殊性质,每一层的对象只需定义本身的性质,其他性质可以从上一层继承下来。

继承的基本思想是基于某个父类的扩展,制定出一个新的子类,子类可以继承父类原有的属性和方法,也可以增加原来父类所不具备的属性和方法,或者直接重写父类中的某些方法。例如,平行四边形是特殊的四边形,可以说平行四边形类继承了四边形类,这时平行四边形类将所有四边形具有的属性和方法都保留下来,并基于四边形类扩展了一些新的平行四边形类特有的属性和方法。

下面演示一下继承性。创建一个新类Test,同时创建另一个新类Test2继承Test类,其中包括重写的父类成员方法以及新增成员方法等。在图9.13中描述了类Test与Test2的结构以及两者之间的关系。

图9.13 Test与Test2类之间的继承关系

在C#中使用“:”来标识两个类的继承关系。继承一个类时,类成员的可访问性是一个重要的问题。子类(派生类)不能访问基类的私有成员,但是可以访问其公共成员。这就是说,只要使用public声明类成员,就可以让一个类成员被基类和子类(派生类)同时访问,同时也可以被外部的代码访问。

为了解决基类成员访问问题,C#还提供了另外一种可访问性:protected,只有子类(派生类)才能访问protected成员,基类和外部代码都不能访问protected成员。

说明

继承类时,需要使用冒号加类名。当对一个类应用sealed修饰符时,此修饰符会阻止其他类从该类继承。

【例9.11】 创建一个控制台应用程序,其中自定义了一个MyClass1类。然后自定义一个MyClass2类,该类继承于MyClass1类。这时,MyClass2类就拥有MyClass1类中的所有公有成员,并且可以扩展其成员。Program主程序类中,可以通过MyClass2类的对象调用MyClass1类中的方法。程序代码如下。(实例位置:光盘\TM\sl\9\6)

程序的运行结果为:

    8
    8
    15

继承并不只是扩展父类的功能,还可以重写父类的成员方法。重写(还可以称为覆盖)就是在子类中将父类的成员方法的名称保留,重写成员方法的实现内容,更改成员方法的存储权限,或是修改成员方法的返回值类型。

在继承中还有一种特殊的重写方式,子类与父类的成员方法返回值、方法名称、参数类型及个数完全相同,唯一不同的是方法实现内容,这种特殊重写方式被称为重构。

注意

当重写父类方法时,修改方法的修饰权限只能从小的范围到大的范围改变,例如,父类中的doit()方法的修饰权限为protected,继承后子类中的方法doit()的修饰权限只能修改为public,不能修改为private。如图9.14所示的重写关系就是错误的。

图9.14 重写时不能降低方法的修饰权限范围

9.4.3 类的多态

多态使子类(派生类)的实例可以直接赋予基类的变量(这里不需要进行强制类型转换),然后直接就可以通过这个变量调用子类(派生类)的方法。

利用多态可以使程序具有良好的扩展性,并可以对所有类对象进行通用的处理。假如,现在需要绘制一个平行四边形,这时可以在平行四边形类中定义一个draw()方法,具体实现代码如例9.12所示。

【例9.12】 定义一个平行四边形的类Parallelogram,在类中定义一个draw()方法。

    public class Parallelogram
    {
         //实例化保存平行四边形对象的数组对象
         public void draw(Parallelogram p) //定义 draw()方法,参数为本类对象
         {
             …//绘图语句
         }
    }

如果需要定义一个绘制正方形的方法,应该如何处理?如果定义一个正方形类来处理正方形对象,这样处理只会带来代码冗余的缺点。

如果定义一个正方形和平行四边形的综合类,分别处理正方形和平行四边形对象,这样处理也没有多大意义。

如果定义一个四边形类,让它处理所有继承该类的对象,根据“可以把子类对象赋值给父类类型的变量”原则,可以使每个继承四边形类的对象作为draw()方法的参数,然后在draw()方法中作一些限定就可以根据不同图形类对象绘制相应的图形,从而以变更为通用的四边形类来取代具体的正方形类和平行四边形类。这样处理能够很好地解决代码冗余问题,同时也带来易于维护的优点,因为可以加入任何继承父类的子类对象,而父类方法也无须修改。

创建四边形类的具体实现代码如例9.13所示。

【例9.13】 创建Program类,再分别创建两个类Square和Parallelogramgle,它们都继承了Program类。在Program类中编写draw()方法,该方法接收Program类的对象作为参数,即使用Square和Parallelogramgle这两个类的父类作为方法参数。在主方法中分别以Square和Parallelogramgle这两个类的实例对象作为参数执行draw()方法。代码如下。(实例位置:光盘\TM\sl\9\7)

图9.15 多态的实现

运行程序,结果如图9.15所示。

从本实例的运行结果中可以看出,以不同类对象为参数调用draw()方法可以处理不同图形的问题。使用多态节省了开发和维护时间,因为程序员无须在所有的子类中定义执行相同功能的方法,避免了大量重复代码的开发,同时只要实例化一个继承父类的子类对象即可调用相应的方法,这里只要维护父类中的这个方法即可。

注意

在派生于同一个类的不同对象上执行任务时,多态是一种极为有效的技巧,使用的代码最少。可以把一组对象放到一个数组中然后调用它们的方法,在这种情况下多态的作用就体现出来了,这些对象不必是相同类型的对象。当然如果它们都继承自某个类,可以把这些子类(派生类)都放到一个数组中。如果这些对象都有同名方法,就可以调用每个对象的同名方法。

9.5 小结

本章主要介绍了面向对象技术的两个最重要的概念:结构和类。其中,结构不支持继承,它自动派生于System.ValueType类,而类支持单继承。本章中首先对结构及其使用进行了详细介绍,然后对面向对象基础进行了介绍,最后着重讲解了类的声明、构造函数和析构函数,同时对类的封装、继承和多态性进行了重点讲解。学习完本章,读者应该熟练掌握结构和类的使用,并能够在实际应用中充分使用类的封装、继承和多态性。

9.6 实践与练习

(1)尝试开发一个程序,要求自定义一个结构,该结构中包含圆的半径及求圆面积的方法,然后在Main方法中调用自定义的结构求指定半径的圆面积。(答案位置:光盘\TM\sl\9\8)

(2)尝试开发一个程序,要求自定义一个类,该类中封装矩形的长和宽,然后再定义一个类,继承自已经定义的类,在继承类中根据基类中封装的矩形的长和宽求矩形的面积。(答案位置:光盘\TM\sl\9\9)

第2篇 核心技术

第2篇 核心技术

 第10章 Windows窗体

 第11章 Windows应用程序常用控件

 第12章 Windows应用程序高级控件

 第13章 数据访问技术

 第14章 DataGridView数据控件

 第15章 LINQ数据访问技术

 第16章 程序调试与异常处理

本篇介绍Windows窗体、Windows应用程序常用控件、Windows应用程序高级控件、数据访问技术、DataGridView数据控件、LINQ数据访问技术以及程序调试与异常处理等。读者学习完这一部分,应该能够开发一些小型应用程序。

第10章 Windows窗体

第10章
Windows窗体

视频讲解:61分钟

Windows环境中主流的应用程序都是窗体应用程序,Windows窗体应用程序比命令行应用程序要复杂得多,理解它的结构的基础是理解窗体。所以深刻认识Windows窗体变得尤为重要。本章将详细地介绍Windows窗体的相关知识,讲解过程中为了便于读者理解结合了大量的举例。

通过阅读本章,您可以:

 了解什么是Form窗体

 掌握如何添加新窗体

 掌握如何设置启动窗体

 掌握窗体的基本属性和事件

 了解什么是MDI窗体

 掌握如何设置父窗体和子窗体

 掌握如何对子窗体进行排列

 掌握如何创建继承窗体

10.1 Form窗体

视频讲解:光盘\TM\lx\10\Form窗体及基本操作.exe

Form窗体也称为窗口,是.NET框架的智能客户端技术,使用窗体可以显示信息、请求用户输入以及通过网络与远程计算机通信,使用Visual Studio 2015可以轻松地创建Form窗体。下面将对Form窗体的相关内容进行详细介绍。

10.1.1 Form窗体的概念

在Windows中,窗体是向用户显示信息的可视图面,窗体是Windows应用程序的基本单元。窗体都具有自己的特征,可以通过编程来设置。窗体也是对象,窗体类定义了生成窗体的模板,每实例化一个窗体类,就产生一个窗体。.NET框架类库的System.Windows.Forms命名控件中定义的Form类是所有窗体类的基类。编写窗体应用程序时,首先需要设计窗体的外观和在窗体中添加控件或组件。虽然可以通过编写代码来实现,但是却不直观,也不方便,而且很难精确地控制界面。如果要编写窗体应用程序,推荐使用Visual Studio 2015。Visual Studio 2015提供了一个图形化的可视化窗体设计器,可以实现所见即所得的设计效果,快速开发窗体应用程序。

10.1.2 添加和删除窗体

添加或删除窗体,首先要创建一个Windows应用程序,在第1章已经介绍过如何创建Windows应用程序,此处不再赘述。如图10.1所示为一个新创建的Windows应用程序。

图10.1 Windows应用程序

如果要向项目中添加一个新窗体,可以在项目名称NewForm上单击鼠标右键,在弹出的快捷菜单中选择“添加”/“Windows窗体”命令或者“添加”/“新建项”命令,如图10.2所示。

图10.2 添加新窗体的右键菜单

选择“新建项”或者“Windows窗体”命令后,都会打开“添加新项”对话框,如图10.3所示。

图10.3 “添加新项”对话框

选择“Windows窗体”选项,输入窗体名称后,单击“添加”按钮,即可向项目中添加一个新的窗体。

说明

在设置窗体的名称时,不要用关键字进行设置。

删除窗体的方法非常简单,只需在要删除的窗体名称上单击鼠标右键,在弹出的快捷菜单中选择“删除”命令,即可将窗体删除,如图10.4所示。

图10.4 删除窗体

10.1.3 多窗体的使用

一个完整的Windows应用程序是由多个窗体组成,此时,就需要对多窗体设计有所了解。多窗体即是向项目中添加多个窗体,在这些窗体中实现不同的功能。下面对多窗体的建立以及如何设置启动窗体进行讲解。

1.多窗体的建立

多窗体的建立是向某个项目中添加多个窗体,还是以10.1.2节中的项目为例,演示如何建立多窗体应用程序,添加多窗体后的项目如图10.5所示。

图10.5 向项目中添加多个窗体

具体添加窗体的方法在10.1.2节中已经做了详细的介绍,此处不再赘述。以向项目中添加3个窗体为例演示多窗体的建立,实际项目中可以添加任意多个窗体。

说明

在添加多个窗体时,其名称不能重名。

2.设置启动窗体

向项目中添加了多个窗体以后,如果要调试程序,必须要设置先运行的窗体。这样就需要设置项目的启动窗体。项目的启动窗体是在Program.cs文件中设置的,在Program.cs文件中改变Run方法的参数,即可实现设置启动窗体。

Run方法用于在当前线程上开始运行标准应用程序,并使指定窗体可见。

语法如下:

    public static void Run(Form mainForm)

mainForm:代表要设为启动窗体的窗体。

【例10.1】 要将Form1窗体设置为项目的启动窗体,就可以通过下面的代码实现。

    Application.Run(new Form1());

10.1.4 窗体的属性

窗体都包含一些基本的组成要素,包括图标、标题、位置和背景等,这些要素可以通过窗体的“属性”面板进行设置,也可以通过代码实现。但是为了快速开发窗体应用程序,通常都是通过“属性”面板进行设置。下面详细介绍窗体的常见属性设置。

1.更换窗体的图标

添加一个新的窗体后,窗体的图标是系统默认的图标。如果想更换窗体的图标,可以在“属性”面板中设置窗体的Icon属性,窗体的默认图标和更换后的图标如图10.6所示。更换窗体图标的过程非常简单,具体操作如下:

(1)选中窗体,然后在窗体的“属性”面板中选中Icon属性,会出现按钮,如图10.7所示。

图10.6 窗体的默认图标与更换后的图标

图10.7 窗体的Icon属性

注意

在添加窗体图标时,其图片格式只能是ico。

(2)单击按钮,打开选择图标文件的窗体,如图10.8所示。

图10.8 选择图标文件的窗体

(3)选择新的窗体图标文件之后,单击“打开”按钮,完成窗体图标的更换。

2.隐藏窗体的标题栏

在某种情况下需要隐藏窗体的标题栏,例如,软件的加载窗体,大多数都采用无标题栏的窗体。通过设置窗体的FormBorderStyle属性的属性值,即可隐藏窗体的标题栏。FormBorderStyle属性有7个属性值,其属性值及说明如表10.1所示。

表10.1 FormBorderStyle属性的属性值及说明

隐藏窗体的标题栏,只需将FormBorderStyle属性设置为None即可。

3.控制窗体的显示位置

可以通过窗体的StartPosition属性,设置加载窗体时窗体在显示器中的位置。StartPosition属性有5个属性值,其属性值及说明如表10.2所示。

表10.2 StartPosition属性的属性值及说明

在设置窗体的显示位置时,只需根据不同的需要选择属性值即可。

4.修改窗体的大小

在窗体的属性中,通过Size属性设置窗体的大小。双击窗体属性面板中的Size属性,可以看到其下拉菜单中有Width和Height两个属性,分别用于设置窗体的宽和高。修改窗体的大小,只需更改Width和Height属性的值即可。

说明

在设置窗体的大小时,其值是Int32类型的,不要用单精度和双精度进行设置。

5.设置图像背景的窗体

为使窗体设计更加美观,通常会设置窗体的背景。可以设置窗体的背景颜色,也可以设置窗体的背景图片。通过设置窗体的BackgroundImage属性,可以设置窗体的背景图片。具体操作如下:

(1)选中窗体“属性”面板中的BackgroundImage属性,会出现按钮,如图10.9所示。

(2)单击按钮,打开“选择资源”对话框,如图10.10所示。

图10.9 BackgroundImage属性

图10.10 “选择资源”对话框

在“选择资源”对话框中,有两个单选按钮。一个是“本地资源”,另一个是“项目资源文件”。其区别是选中“本地资源”单选按钮后,直接选择图片,保存的是图片的路径;而选中“项目资源文件”单选按钮后,会将选择的图片保存到项目资源文件Resources.resx中。无论选择哪种方式,都需要单击“导入”按钮选择背景图片,单击“确定”按钮完成窗体背景图片的设置。Form1窗体背景图片设置前后对比如图10.11所示。

图10.11 设置窗体背景图片前后对比

10.1.5 窗体的显示与隐藏

1.窗体的显示

如果要在一个窗体中通过按钮打开另一个窗体,就必须通过调用Show方法显示窗体。

语法如下:

    public void Show()

【例10.2】 在Form1窗体中添加一个Button按钮,在按钮的Click事件中调用Show方法,打开Form2窗体,代码如下。

    Form2 frm2 = new Form2();     //实例化 Form2
    frm2.Show();                  //调用 Show 方法显示 Form2 窗体

程序的运行结果如图10.12所示。

图10.12 使用Show方法显示窗体

2.窗体的隐藏

通过调用Hide方法隐藏窗体。

语法如下:

    public void Hide()

【例10.3】 通过登录窗口登录系统,输出用户名和密码后,单击“登录”按钮,隐藏登录窗口,显示主窗体,关键代码如下。

    this.Hide();                     //调用 Hide 方法隐藏当前窗体
    frmMain frm = new frmMain();     //实例化 frmMain
    frm.Show();                      //调用 Show 方法打开窗体

10.1.6 窗体的事件

Windows是事件驱动的操作系统,对Form类的任何交互都是基于事件来实现的。Form类提供了大量的事件用于响应对窗体执行的各种操作。下面详细介绍窗体的Click、Load和FormClosing事件。

1.Click(单击)事件

当单击窗体时,将会触发窗体的Click事件。

语法如下:

    public event EventHandler Click

【例10.4】 在窗体的Click事件中编写代码,实现当单击窗体时,弹出提示框,代码如下。

    private void Form1_Click(object sender, EventArgs e)   //窗体的 Click 事件
    {
         MessageBox.Show("已经单击了窗体!");              //弹出提示框
    }

程序的运行结果如图10.13所示。

图10.13 单击窗体触发Click事件

2.Load(加载)事件

窗体加载时,将触发窗体的Load事件。

说明

可以在Load事件中分配窗体的使用资源。

语法如下:

    public event EventHandler Load

【例10.5】 当窗体加载时,弹出提示框,询问是否查看窗体,单击“是”按钮,查看窗体,代码如下。

    private void Form1_Load(object sender, EventArgs e)                //窗体的 Load 事件
    {
         //使用 if 语句判断是否单击了“是”按钮
         if(MessageBox.Show("是否查看窗体!", "",MessageBoxButtons.YesNo, MessageBoxIcon.Information) ==
    DialogResult.Yes)
         {
         }
    }

程序的运行结果如图10.14所示。

3.FormClosing(关闭)事件

窗体关闭时,触发窗体的FormClosing事件。

说明

可以使用此事件执行一些任务,如释放窗体使用的资源,还可使用此事件保存窗体中的信息或更新其父窗体。

语法如下:

    public event FormClosingEventHandler FormClosing

【例10.6】 创建一个Windows应用程序,用于实现当关闭窗体之前弹出提示框,询问是否关闭当前窗体,单击“是”按钮,则关闭窗体,代码如下。(实例位置:光盘\TM\sl\10\1)

程序的运行结果如图10.15所示。

图10.14 弹出提示框

图10.15 运行结果

说明

如果要防止窗体的关闭,应使用FormClosing事件,并将传递给事件处理程序的CancelEventArgs的Cancel属性设置为true。

10.2 MDI窗体

视频讲解:光盘\TM\lx\10\MDI窗体的使用.exe

窗体是所有界面的基础,这就意味着为了打开多个文档,需要具有能够同时处理多个窗体的应用程序。为了适应这个需求,产生了MDI窗体,即多文档界面。

大家都玩过游戏吧,例如《超级玛丽》,该游戏只能对一个人物进行控制,除非用两个游戏手柄才能分别对主副人物进行操作,这就相当于Form窗体。而《红色警戒》游戏则可以用鼠标选中多个人物,并同时对其进行操作,这相当于MDI窗体。

下面将详细介绍MDI窗体的相关内容。

10.2.1 MDI窗体的概念

图10.16 多文档窗体界面

多文档界面(Multiple-Document Interface, MDI)窗体用于同时显示多个文档,每个文档显示在各自的窗口中。MDI窗体中通常有包含子菜单的窗口菜单,用于在窗口或文档之间进行切换。MDI窗体十分常见。如图10.16所示为一个MDI窗体界面。

MDI窗体的应用非常广泛,例如,如果某公司的库存系统需要实现自动化,则需要使用窗体来输入客户和货物的数据、发出订单以及跟踪订单。这些窗体必须链接或者从属于一个界面,并且必须能够同时处理多个文件。这样,就需要建立MDI窗体以解决这些需求。

10.2.2 如何设置MDI窗体

图10.17 设置父窗体

在MDI窗体中,起到容器作用的窗体被称为“父窗体”,可放在父窗体中的其他窗体被称为“子窗体”,也称为“MDI子窗体”。当MDI应用程序启动时,首先会显示父窗体。所有的子窗体都在父窗体中打开,在父窗体中可以随时打开多个子窗体。每个应用程序只能有一个父窗体,其他子窗体不能移出父窗体的框架区域。下面介绍如何将窗体设置成父窗体或子窗体。

1.设置父窗体

如果要将某个窗体设置为父窗体,只要在窗体的属性面板中,将IsMdiContainer属性设置为true即可,如图10.17所示。

说明

在设置MDI窗体的主窗体时,要尽可能用项目的启动窗体进行设置。

2.设置子窗体

设置完父窗体,即可通过设置某个窗体的MdiParent属性来确定子窗体。

语法如下:

    public Form MdiParent { get; set; }

属性值:MDI父窗体。

【例10.7】 将Form2、Form3、Form4、Form5这4个窗体设置成子窗体,并且在父窗体中打开这4个子窗体,代码如下。

    Form2 frm2 = new Form2();          //实例化 Form2
    frm2.Show();                       //使用 Show 方法打开窗体
    frm2. MdiParent = this;            //设置 MdiParent 属性,将当前窗体作为父窗体
    Form3 frm3 = new Form3();          //实例化 Form3
    frm3.Show();                       //使用 Show 方法打开窗体
    frm3. MdiParent = this;            //设置 MdiParent 属性,将当前窗体作为父窗体
    Form4 frm4 = new Form4();          //实例化 Form4
    frm4.Show();                       //使用 Show 方法打开窗体
    frm4. MdiParent = this;            //设置 MdiParent 属性,将当前窗体作为父窗体
    Form5 frm5 = new Form5();          //实例化 Form5
    frm5.Show();                       //使用 Show 方法打开窗体
    frm5.MdiParent = this;             //设置 MdiParent 属性,将当前窗体作为父窗体

程序的运行结果参见图10.16。

10.2.3 排列MDI子窗体

如果一个MDI窗体中有多个子窗体同时打开,假如不对其排列顺序进行调整,那么界面会非常混乱,而且不容易浏览。那么如何解决这个问题呢?可以通过使用带有MdiLayout枚举的LayoutMdi方法来排列多文档界面父窗体中的子窗体。

语法如下:

    public void LayoutMdi(MdiLayout value)

value:是MdiLayout枚举值之一,用来定义MDI子窗体的布局。

MdiLayout枚举用于指定MDI父窗体中子窗体的布局。

语法如下:

    public enum MdiLayout

MdiLayout的枚举成员及说明如表10.3所示。

表10.3 MdiLayout的枚举成员

下面通过一个实例演示如何使用带有MdiLayout枚举的LayoutMdi方法来排列多文档界面父窗体中的子窗体。

【例10.8】 创建一个Windows应用程序,向项目中添加4个窗体,然后使用LayoutMdi方法以及MdiLayout枚举设置窗体的排列,开发步骤如下。(实例位置:光盘\TM\sl\10\2)

(1)新建一个Windows应用程序,命名为Test02,默认窗体为Form1.cs。

(2)将窗体Form1的IsMdiContainer属性设置为true,以用作MDI父窗体,然后再添加3个Windows窗体,用作MDI子窗体。

(3)在Form1窗体中,添加一个MenuStrip控件,用作该父窗体的菜单项。

(4)通过MenuStrip控件建立4个菜单项,分别为“加载子窗体”、“水平平铺”、“垂直平铺”和“层叠排列”。运行程序时,单击“加载子窗体”菜单后,可以加载所有的子窗体,代码如下。

    private void 加载子窗体 ToolStripMenuItem_Click(object sender, EventArgs e)
    {
         Form2 frm2 = new Form2();                //实例化 Form2
         frm2.MdiParent = this;                   //设置 MdiParent 属性,将当前窗体作为父窗体
         frm2.Show();                             //使用 Show 方法打开窗体
         Form3 frm3 = new Form3();                //实例化 Form3
         frm3.MdiParent = this;                   //设置 MdiParent 属性,将当前窗体作为父窗体
         frm3.Show();                             //使用 Show 方法打开窗体
         Form4 frm4 = new Form4();                //实例化 Form4
         frm4.MdiParent = this;                   //设置 MdiParent 属性,将当前窗体作为父窗体
         frm4.Show();                             //使用 Show 方法打开窗体
    }

程序的运行结果如图10.18所示。

(5)加载所有的子窗体之后,单击“水平平铺”菜单,使窗体中所有的子窗体水平排列,代码如下。

    private void 水平平铺 ToolStripMenuItem_Click(object sender, EventArgs e)
    {
         LayoutMdi(MdiLayout.TileHorizontal);                   //使用 MdiLayout 枚举实现窗体的水平平铺
    }

程序的运行结果如图10.19所示。

图10.18 加载所有子窗体

图10.19 水平平铺子窗体

(6)单击“垂直平铺”菜单,使窗体中所有的子窗体垂直排列,代码如下。

    private void 垂直平铺 ToolStripMenuItem_Click(object sender, EventArgs e)
    {
         LayoutMdi(MdiLayout.TileVertical);                      //使用 MdiLayout 枚举实现窗体的垂直平铺
    }

程序的运行结果如图10.20所示。

(7)单击“层叠排列”菜单,使窗体中所有的子窗体层叠排列,代码如下。

    private void 层叠排列 ToolStripMenuItem_Click(object sender, EventArgs e)
    {
         LayoutMdi(MdiLayout.Cascade);                         //使用 MdiLayout 枚举实现窗体的层叠排列
    }

程序的运行结果如图10.21所示。

图10.20 垂直平铺子窗体

图10.21 层叠排列子窗体

10.3 继承窗体

视频讲解:光盘\TM\lx\10\继承窗体的使用.exe

10.3.1 继承窗体的概念

继承窗体就是根据现有窗体的结构创建一个与其一样的新窗体,这种从现有窗体继承的过程称为可视化继承。在某种情况下,项目可能需要一个与在以前的项目中创建的窗体类似的窗体,或者希望创建一个基本窗体,其中含有随后将在项目中再次使用的控件布局之类的设置,每次重复使用时,都会对该原始窗体模板进行修改。这时,就需要创建继承窗体。通过从基窗体继承,来创建新Windows窗体是重复最佳工作成果的快捷方法,而不必每次需要窗体时都重新创建一个。为了从一个窗体继承,包含该窗体的文件或命名空间必须已编译成可执行文件或DLL。

继承窗体就好像是游戏的升级版本,升级版本不但有原版本的一切功能,还添加了一些新功能,或是对原版本进行了一些改动。

10.3.2 创建继承窗体

了解什么是继承窗体之后,接下来介绍如何创建继承窗体。创建继承窗体的方法有两种:一种是通过编程方式创建继承窗体,另一种是使用继承选择器创建继承窗体。下面对这两种方法分别进行介绍。

1.以编程方式创建继承窗体

以编程方式创建继承窗体时,主要是在类定义中将引用添加到要从其继承的窗体。引用应包含该窗体的命名空间,后面跟一个句点,然后是基窗体本身的名称。

【例10.9】 创建一个Windows应用程序,以Form1为基窗体,将Form2设置为继承窗体,开发步骤如下。(实例位置:光盘\TM\sl\10\3)

(1)创建一个Windows应用程序,默认窗体为Form1.cs。

(2)在Form1窗体上添加一个TextBox控件、一个Button控件和一个Label控件。在Button控件的Click事件中添加代码,实现Label控件显示TextBox控件中输入的内容。

(3)向项目中添加一个新的Windows窗体,命名为Form2。

(4)修改Form2窗体代码文件中Form2类所继承的基类。

Form2窗体的原始代码如下。

    namespace Test03                                    //项目名称
    {
       public partial class Form2 : Form                //表示当前窗体继承于 Form 类
       {
       }
    }

修改后的代码如下。

    namespace Test03                                                //项目名称
    {
       public partial class Form2 : Test03.Form1                    //使 Form2 窗体继承项目 Test03 的 Form1 窗体
       {
       }
    }

运行Form2窗体,其修改前后对比如图10.22和图10.23所示。

图10.22 Form2的起始运行窗体

图10.23 成为Form1窗体的继承窗体

2.使用继承选择器创建继承窗体

继承窗体或其他对象的最简便方法是使用“继承选择器”对话框。通过该对话框,就可利用已经在其他解决方案中创建的代码或用户界面。为了使用“继承选择器”对话框从某个窗体继承,包含该窗体的项目必须已生成为可执行文件或DLL。若要生成项目,可以选择“生成”菜单中的“生成解决方案”命令。下面介绍如何使用继承选择器创建继承窗体。

(1)在“解决方案资源管理器”面板的项目名称上单击鼠标右键,在弹出的快捷菜单中选择“添加”/“新建项”命令,打开“添加新项”对话框。

(2)从“添加新项”对话框中选择继承的窗体后,单击“添加”按钮,打开“继承选择器”对话框。

(3)从“继承选择器”对话框中选择添加的继承窗体的基窗体后,单击“确定”按钮,完成继承窗体的添加。

10.3.3 在继承窗体中修改继承的控件属性

在向窗体中添加控件时,其Modifiers属性默认为Private。因此,如果继承这样的窗体,在继承窗体中,控件的属性全部为不可编辑状态。如果希望在继承窗体中编辑各个控件的属性,首先要将基窗体中控件的Modifiers属性全部设置为Public。

【例10.10】 创建一个Windows应用程序,在继承窗体中修改基窗体中控件的属性,具体步骤如下。(实例位置:光盘\TM\sl\10\4)

(1)创建一个Windows应用程序,默认窗体为Form1.cs。

(2)在Form1窗体中添加一个Button控件,设置其Text属性为“用一生下载你”,并将其Modifiers属性设置为Public,如图10.24所示。

(3)添加一个继承窗体,命名为Form2。

(4)继承窗体Form2的运行结果与基窗体Form1相同,但是由于将基窗体中Button控件的Modifiers属性设置为Public,所以在继承窗体Form2中可以修改Button控件的属性。

(5)将Button控件的Text属性重新设置为“芸烨湘枫”,其前后对比如图10.25和图10.26所示。

图10.24 设置Modifiers属性

图10.25 修改之前继承Form1窗体

图10.26 修改之后的Button控件

10.4 小结

本章主要介绍了Form窗体、MDI窗体和继承窗体,Form窗体是开发窗体应用程序的基本单位,熟练掌握Form窗体的应用,将为快速开发C#窗体应用程序打下坚实的基础。读者要了解窗体的属性、事件的使用,掌握如何对窗体进行基本的设置。多文档界面被称为MDI窗体,大多数的窗体应用程序都使用MDI窗体开发,所以要重点掌握MDI窗体的设置以及如何排列子窗体。而使用继承窗体可以将基窗体的内容继承下来,从而减少了重复创建与基窗体类似的窗体的工作。

10.5 实践与练习

(1)尝试制作一个窗体,该窗体要求为半透明渐显窗体。(答案位置:光盘\TM\sl\10\5)

(2)尝试制作一个窗体,要求该窗体的标题栏文字右对齐。(答案位置:光盘\TM\sl\10\6)

第11章 Windows应用程序常用控件

第11章
Windows应用程序常用控件

视频讲解:93分钟

控件是窗体设计的基本组成单位,通过使用控件可以高效地开发Windows应用程序。所以,熟练掌握控件是合理、有效地进行程序开发的重要前提。本章详细介绍了Windows应用程序常用控件,讲解过程中为了便于读者理解,结合了大量的举例。

通过阅读本章,您可以:

 了解控件的分类及作用

 了解控件的命名规范

 掌握控件的相关操作

 了解什么是文本类控件

 了解选择类控件的分类及用法

 掌握分组控件的使用方法

 了解菜单、工具栏和状态栏控件的使用方法

11.1 控件概述

视频讲解:光盘\TM\lx\11\控件概述.exe

窗口是由控件有机构成的,所以熟悉控件是进行合理、有效的程序开发的重要前提。Windows应用程序中的控件分为常用控件和高级控件,在以下的内容中,将对常用控件进行讲解,Windows应用程序的高级控件将会在第12章进行介绍。

控件是用户可以输入或操作数据的对象,相当于汽车中的方向盘、油门、刹车、离合器等,它们都是对汽车进行操作的控件。

11.1.1 控件的分类及作用

在Visual Studio 2015开发环境中,常用控件可以分为文本类控件、选择类控件、分组控件、菜单控件、工具栏控件以及状态栏控件。Windows应用程序控件的基类是位于System.Windows.Forms命名空间的Control类。Control类定义了控件类的共同属性、方法和事件,其他的控件类都直接或间接地派生自这个基类。几种常用控件的作用如表11.1所示。

表11.1 常用控件的作用

11.1.2 控件命名规范

在使用控件的过程中,可以通过控件默认的名称调用。如果自定义控件名称,就要遵循控件的命名规范。控件的常用命名规范如表11.2所示。

表11.2 控件的命名规范

11.2 控件的相关操作

视频讲解:光盘\TM\lx\11\控件的相关操作.exe

对控件的相关操作包括添加控件、对齐控件、锁定控件和删除控件等,在以下内容中将会对这几种操作进行讲解。通过对本节的学习,完全可以掌握如何操作Windows控件。

11.2.1 添加控件

对控件的操作,可以通过“在窗体上绘制控件”、“将控件拖曳到窗体上”和“以编程方式向窗体添加控件”这3种方法添加控件。

1.在窗体上绘制控件

在工具箱中单击要添加到窗体的控件,然后在该窗体上单击希望控件左上角所处的位置,然后拖动到希望该控件右下角所处位置,控件即按指定的位置和大小添加到窗体中。

2.将控件拖曳到窗体上

在工具箱中单击所需的控件并将其拖到窗体上,控件以其默认大小添加到窗体上的指定位置。

3.以编程方式向窗体添加控件

通过new关键字实例化要添加控件所在的类,然后将实例化的控件添加到窗体中。

【例11.1】 通过Button按钮的Click事件添加一个TextBox控件,代码如下。

    private void button1_Click(object sender, System.EventArgs e)    //Button 按钮的 Click 事件
    {
        TextBox myText = new TextBox();                              //实例化 TextBox 类
        myText.Location = new Point(25,25);                          //设置对象的 Location 属性
        this.Controls.Add (myText);                                  //将控件添加到当前窗体中
    }

11.2.2 对齐控件

选定一组控件,这些控件需要对齐。在执行对齐之前,首先选定主导控件(首先被选定的控件就是主导控件)。控件组的最终位置取决于主导控件的位置,再选择菜单栏中的“格式”/“对齐”命令,然后选择对齐方式。

 左对齐:将选定控件沿它们的左边对齐。

 居中对齐:将选定控件沿它们的中心点水平对齐。

 右对齐:将选定控件沿它们的右边对齐。

 顶端对齐:将选定控件沿它们的顶边对齐。

 中间对齐:将选定控件沿它们的中心点垂直对齐。

 底部对齐:将选定控件沿它们的底边对齐。

11.2.3 锁定控件

在控件的“属性”窗口中,单击Locked属性并选择true即可锁定控件。此外,还可以右击控件,在弹出的快捷菜单中选择“锁定控件”命令。如果要锁定窗体上的所有控件,可以选择菜单栏中的“格式”/“锁定控件”命令。

说明

完成窗体设置后,为了避免误操作而改变窗体的控件设置,可以用控件锁定的方法对控件进行定位。

11.2.4 删除控件

删除控件的方法非常简单,可以在控件上单击鼠标右键,在弹出的快捷菜单中选择“删除”命令进行删除。或者选中控件,然后按下Delete键。

11.3 文本类控件

视频讲解:光盘\TM\lx\11\文本类控件的使用.exe

文本类控件主要包括标签控件(Label控件)、按钮控件(Button控件)、文本框控件(TextBox控件)和有格式文本控件(RichTextBox控件)。通过对本节的学习,可以掌握文本类控件的基本用法。

11.3.1 标签控件(Label控件)

标签控件(Label控件)主要用于显示用户不能编辑的文本,标识窗体上的对象(例如,给文本框、列表框等添加描述信息)。也可以通过编写代码来设置要显示的文本信息。如果添加一个标签控件,系统会自动创建标签控件的一个对象,如图11.1所示为Label控件。

下面详细介绍标签控件(Label控件)的一些基本设置。

1.设置标签文本

设置标签控件(Label控件)显示的文本有两种方法:第一种是直接在标签控件(Label控件)的属性面板中设置Text属性,第二种是通过代码设置Text属性。

【例11.2】 向窗体中拖曳一个Label控件,然后将其显示文本设置为“用一生下载你”,如图11.2所示。

也可以通过代码设置Text属性,代码如下。

    label1.Text = "用一生下载你";                   //设置 Label 控件的 Text 属性
2.显示/隐藏控件

通过设置Visible属性可以用来设置显示/隐藏标签控件(Label控件),如果Visible属性的值为true,则显示控件。如果Visible属性的值为false,则隐藏控件。

【例11.3】 显示标签控件(Label控件),将Visible属性设置为true,如图11.3所示。

图11.1 Label控件

图11.2 设置Text属性

图11.3 设置Visible属性

如果要显示标签控件(Label控件),可通过代码将其Visible属性设置为true,代码如下。

    label1.Visible = true;             //设置 Label 控件的 Visible 属性

11.3.2 按钮控件(Button控件)

按钮控件(Button控件)允许用户通过单击来执行操作。按钮控件(Button控件)既可以显示文本,也可以显示图像。当该Button控件被单击时,先被按下,然后被释放。如图11.4所示为Button控件。

下面详细介绍按钮控件(Button控件)的一些常用设置。

1.响应按钮的单击事件

单击按钮控件(Button控件)时将引发Click事件,执行Click事件中的代码。

【例11.4】 创建一个Windows应用程序,单击按钮控件(Button控件),引发Click事件,弹出提示框,代码如下。(实例位置:光盘\TM\sl\11\1)

    private void button1_Click(object sender, EventArgs e)         //按钮的 Click 事件
    {
         MessageBox.Show("单击了按钮,引发了 Click 事件");         //弹出提示框
    }

程序的运行结果如图11.5所示。

图11.4 Button控件

图11.5 引发Click事件

2.将按钮设置为窗体的“接受”按钮

通过设置窗体的AcceptButton属性,可以设置窗体的“接受”按钮。如果设置了此按钮,则用户每次按下Enter键都相当于单击该按钮。

【例11.5】 创建一个Windows应用程序,将Button1按钮设置为Form1窗体的“接受”按钮,运行程序。当按下Enter键时,就会激发Button1按钮的Click事件,与单击Button1按钮的结果是一样的,代码如下。(实例位置:光盘\TM\sl\11\2)

    private void Form1_Load(object sender, EventArgs e)     //窗体的 Load 事件
    {
         this.AcceptButton = button1;                       //将 button1 按钮设置为窗体的“接受”按钮
    }
    private void button1_Click(object sender, EventArgs e)  //按钮的 Click 事件
    {
         MessageBox.Show("引发了接受按钮");                 //弹出提示框
    }

运行程序,按下Enter键,激发Button1按钮,如图11.6所示。

3.将按钮设置为窗体的“取消”按钮

通过设置窗体的CancelButton属性,可以设置窗体的“取消”按钮。如果设置该属性,则每次用户按下Esc键都相当于单击了该按钮。

【例11.6】 创建一个Windows应用程序,将Button2按钮设置为Form1窗体的“取消”按钮,运行程序。当按下Esc键时,就会激发Button2按钮,代码如下。(实例位置:光盘\TM\sl\11\3)

    private void Form1_Load(object sender, EventArgs e)          //窗体的 Load 事件
    {
         this.CancelButton = button1;                           //将 Button2 按钮设置为窗体的“取消”按钮
    }
    private void button1_Click(object sender, EventArgs e)       //按钮的 Click 事件
    {
         MessageBox.Show("单击了取消按钮");                      //弹出提示框
    }

运行程序,按下Esc键,激发Button2按钮,如图11.7所示。

图11.6 将按钮设置为窗体的“接受”按钮

图11.7 将按钮设置为窗体的“取消”按钮

说明

如果想实现鼠标移入和移出按钮时,改变按钮的样式或字体样式,可以用OnMouseEnter(在鼠标指针进入控件时发生)和OnMouseLeave(在鼠标离开控件的可见部分时发生)事件来实现。

11.3.3 文本框控件(TextBox控件)

文本框控件(TextBox控件)用于获取用户输入的数据或者显示文本。文本框控件(TextBox控件)通常用于可编辑文本,也可以使其成为只读控件。文本框可以显示多个行,对文本换行使其符合控件的大小。如图11.8所示为TextBox控件。

下面详细介绍文本框控件(TextBox控件)的一些设置。

1.创建只读文本框

通过设置文本框控件(TextBox控件)的ReadOnly属性,可以设置文本框是否为只读。如果ReadOnly属性为true,那么不能编辑文本框,而只能通过文本框显示数据。

【例11.7】 创建一个Windows应用程序,将文本框设置为只读,并且使文本框显示“用一生下载你”,代码如下。(实例位置:光盘\TM\sl\11\4)

    private void Form1_Load(object sender, EventArgs e)     //窗体的 Load 事件
    {
         textBox1.ReadOnly = true;                          //将文本框设置为只读
         textBox1.Text = "用一生下载你";                    //设置其 Text 属性
    }

程序的运行结果如图11.9所示。

2.创建密码文本框

通过设置文本框的PasswordChar属性或者UseSystemPasswordChar属性可以将文本框设置成密码文本框,使用PasswordChar属性可以设置输入密码时,文本框中显示的字符(例如,将密码显示成“*”或“#”等)。而如果将UseSystemPasswordChar属性设置为true,则输入密码时,文本框中将密码显示成为“*”。

【例11.8】 创建一个Windows应用程序,使用PasswordChar属性将密码文本框中的字符自定义显示为“@”,也可以将UseSystemPasswordChar属性设置为true,使密码文本框中的字符显示为“*”,代码如下。(实例位置:光盘\TM\sl\11\5)

    private void Form1_Load(object sender, EventArgs e)           //窗体的 Load 事件
    {
         textBox1.PasswordChar = '@';                             //设置文本框的 PasswordChar 属性为字符@
         //设置文本框的 UseSystemPasswordChar 属性为 true
         textBox2.UseSystemPasswordChar = true;
    }

程序的运行结果如图11.10所示。

图11.8 TextBox控件

图11.9 设置文本框为只读

图11.10 设置PasswordChar属性

说明

在TextBox控件中可以通过BorderStyle属性获取或设置文本框的边框类型,其值为None(无边框)、FixedSingle(单行边框)、Fixed3D(三维边框)。

3.创建多行文本框

默认情况下,文本框控件(TextBox控件)只允许输入单行数据,如果将其Multiline属性设置为true,文本框控件(TextBox控件)就可以输入多行数据。

【例11.9】 创建一个Windows应用程序,将文本框的Multiline属性设置为true,使其能够输入多行数据,代码如下。(实例位置:光盘\TM\sl\11\6)

    private void Form1_Load(object sender, EventArgs e)  //窗体的 Load 事件
    {
         textBox1.Multiline = true;                      //设置文本框的 Multiline 属性使其多行显示
         //设置文本框的 Text 属性
         textBox1.Text = "昨夜星辰昨夜风,画楼西畔桂堂东。身无彩凤双飞翼,心有灵犀一点通。";
         textBox1.Height = 100;                          //设置文本框的高
    }

图11.11 Multiline属性设置为true

程序的运行结果如图11.11所示。

4.突出显示文本框中的文本

在文本框控件(TextBox控件)中,可以通过编程方式选择文本。可以通过SelectionStart属性和SelectionLength属性设置突出显示的文本,SelectionStart属性用于设置选择的起始位置,SelectionLength属性用于设置选择文本的长度。

说明

如果用SelectionStart属性设置的起始位置在回车符和换行符之间,选择长度将自动增加1,以使所获得的选择跨越整个行尾标记。该属性值不能为负数。

【例11.10】 创建一个Windows应用程序,从字符串索引为5的位置开始选择,选择文本的长度为5,将选择的文本突出显示,代码如下。(实例位置:光盘\TM\sl\11\7)

    private void Form1_Load(object sender, EventArgs e)         //窗体的 Load 事件
    {
         textBox1.Multiline = true;                             //设置文本框的 Multiline 属性使其多行显示
         textBox1.Text = "昨夜星辰昨夜风,画楼西畔桂堂东。身无彩凤双飞翼,心有灵犀一点通。";
         textBox1.Height = 100;                                 //设置文本框的高
         textBox1.SelectionStart = 5;                           //从文本框中索引为 5 的位置开始选择
         textBox1.SelectionLength = 5;                          //选择长度是 5 个字符
    }

程序的运行结果如图11.12所示。

5.响应文本框的文本更改事件

当文本框中的文本发生更改时,将会引发文本框的TextChanged事件。

【例11.11】 创建一个Windows应用程序,在文本框的TextChanged事件中编写代码。实现当文本框中的文本更改时,Label控件显示更改后的文本,代码如下。(实例位置:光盘\TM\sl\11\8)

    //文本框的 TextChanged 事件
    private void textBox1_TextChanged(object sender, EventArgs e)
    {
         label1.Text = textBox1.Text;                    //Label 控件显示的文字随文本框中的数据而改变
    }

程序的运行结果如图11.13所示。

图11.12 突出显示文本框中指定的文本

图11.13 显示更改后的文本

11.3.4 有格式文本控件(RichTextBox控件)

图11.14 RichTextBox控件

有格式文本控件(RichTextBox控件)用于显示、输入和操作带有格式的文本。RichTextBox控件除了执行TextBox控件的所有功能之外,还可以显示字体、颜色和链接,从文件加载文本和嵌入的图像,撤销和重复编辑操作以及查找指定的字符。如图11.14所示为RichTextBox控件。

下面详细介绍RichTextBox控件的几种基本使用方法。

1.在RichTextBox控件中显示滚动条

通过设置RichTextBox控件的Multiline属性,可以控制控件中是否显示滚动条。Multiline属性设为true,则显示滚动条;设为false则不显示滚动条。默认情况下,此属性被设置为true。滚动条分为水平滚动条和垂直滚动条,通过ScrollBars属性可以设置如何显示滚动条。ScrollBars属性的属性值及说明如表11.3所示。

表11.3 ScrollBars属性的属性值及说明

注意

当WordWrap(指示多行文本框控件在必要时是否自动换行到下一行的开始)属性为true时,则不论ScrollBars属性的值是什么,都不会显示水平滚动条。

【例11.12】 创建一个Windows应用程序,使RichTextBox控件只显示垂直滚动条,首先将Multiline属性设为true,然后设置ScrollBars属性的值为Vertical,代码如下。(实例位置:光盘\TM\sl\11\9)

    private void Form1_Load(object sender, EventArgs e)
    {
         richTextBox1.Multiline = true;            //将 Multiline 属性设为 true 实现多行显示
         //设置 ScrollBars 属性实现只显示垂直滚动条
         richTextBox1.ScrollBars = RichTextBoxScrollBars.Vertical;
    }

图11.15 显示垂直滚动条

运行程序,向控件中输入数据,如图11.15所示。

2.在RichTextBox控件中设置字体属性

通过SelectionFont属性设置RichTextBox控件中字体的字体系列、大小和字样。通过SelectionColor属性设置字体的颜色。

【例11.13】 创建一个Windows应用程序,将RichTextBox控件中文本的字体设置为楷体,字体大小为12,字样是粗体,文本的颜色为蓝色,代码如下。(实例位置:光盘\TM\sl\11\10)

    private void Form1_Load(object sender, EventArgs e)       //窗体的 Load 事件
    {
         richTextBox1.Multiline = true;                       //将 Multiline 属性设为 true 实现多行显示
         //设置 ScrollBars 属性实现只显示垂直滚动条
         richTextBox1.ScrollBars = RichTextBoxScrollBars.Vertical;
         //设置 SelectionFont 属性实现控件中的文本为楷体,大小为 12,字样是粗体
         richTextBox1.SelectionFont = new Font("楷体", 12, FontStyle.Bold);
         //设置 SelectionColor 实现控件中的文本颜色为蓝色
         richTextBox1.SelectionColor = System.Drawing.Color.Blue;
    }

程序的运行结果如图11.16所示。

3.将RichTextBox控件显示为超链接样式

RichTextBox控件可以将Web链接显示为彩色或下划线形式。可以编写代码,在单击链接时打开浏览器窗口,该窗口中显示链接文本中指定的网站。通过Text属性,设置控件中含有超链接的文本。然后在控件的LinkClicked事件中编写事件处理程序,将所需的文本发送到浏览器。

【例11.14】 创建一个Windows应用程序,在控件的文本内容中含有超链接地址,超链接地址显示为彩色并且带有下划线,单击这个超链接地址后,会打开相应的网站,代码如下。(实例位置:光盘\TM\sl\11\11)

    private void Form1_Load(object sender, EventArgs e)
    {
         richTextBox1.Multiline = true;                    //将 Multiline 属性设为 true 实现多行显示
         //设置 ScrollBars 属性实现只显示垂直滚动条
         richTextBox1.ScrollBars = RichTextBoxScrollBars.Vertical;
         //设置控件的 Text 属性
         richTextBox1.Text = "欢迎登录 http://www.cccxy.com 编程体验网";
    }
    private void richTextBox1_LinkClicked(object sender, LinkClickedEventArgs e)
    {
         //在控件的 LinkClicked 事件中编写如下代码实现内容中的网址带下划线
         System.Diagnostics.Process.Start(e.LinkText);
    }

程序的运行结果如图11.17所示。

图11.16 设置控件中文本的字体属性

图11.17 文本中含有超链接地址

注意

在RichTextBox控件的文本中设置超链接时,必须用“http://”开头,且http的前面不能用数字和字母,只能用空格或是汉字,否则将无法实现超链接操作。

4.在RichTextBox控件中设置段落格式

RichTextBox控件具有多个用于设置所显示的文本格式的选项。可以通过设置SelectionBullet属性将选定的段落设置为项目符号列表的格式。也可以使用SelectionIndent和SelectionHangingIndent属性设置段落相对于控件的左右边缘进行缩进。

【例11.15】 创建一个Windows应用程序,将控件的SelectionBullet属性设为true,使控件中的内容以项目符号列表的格式排列,代码如下。(实例位置:光盘\TM\sl\11\12)

    private void Form1_Load(object sender, EventArgs e)
    {
         richTextBox1.Multiline = true;               //将 Multiline 属性设为 true 实现多行显示
         //设置 ScrollBars 属性实现只显示垂直滚动条
         richTextBox1.ScrollBars = RichTextBoxScrollBars.Vertical;
         //控件的 SelectionBullet 属性设为 true,使控件中的内容以项目符号列表的格式排列
         richTextBox1.SelectionBullet = true;
    }

运行程序,向控件中输入数据,结果如图11.18所示。

通过SelectionIndent属性设置一个整数,该整数表示控件的左边缘和文本的左边缘之间的距离(以像素为单位)。通过SelectionRightIndent属性设置一个整数,该整数表示控件的右边缘与文本的右边缘之间的距离(以像素为单位)。

【例11.16】 创建一个Windows应用程序,设置控件SelectionIndent属性的值为8,使控件中数据的左边缘与控件的左边缘之间的距离为8像素。设置SelectionRightIndent属性为12,使控件中文本的右边缘与控件右边缘的距离为12像素,代码如下。(实例位置:光盘\TM\sl\11\13)

    private void Form1_Load(object sender, EventArgs e)
    {
         richTextBox1.Multiline = true;              //将 Multiline 属性设为 true 实现多行显示
         //设置 ScrollBars 属性实现只显示垂直滚动条
         richTextBox1.ScrollBars = RichTextBoxScrollBars.Vertical;
         //设置 SelectionIndent 属性的值为 8,使控件中数据的左边缘与控件的左边缘之间的距离为 8 像素
         richTextBox1.SelectionIndent = 8;
         //设置 SelectionRightIndent 属性为 12,使控件中文本的右边缘与控件右边缘的距离为 12 像素
         richTextBox1.SelectionRightIndent = 12;
    }

运行程序,向控件中输入数据,结果如图11.19所示。

图11.18 将控件中的内容设置为项目符号列表

图11.19 设置文本的段落格式

11.4 选择类控件

视频讲解:光盘\TM\lx\11\选择类控件的使用.exe

选择类控件主要包括下拉组合框控件(ComboBox控件)、复选框控件(CheckBox控件)、单选按钮控件(RadioButton控件)、数值选择控件(NumericUpDown控件)和列表控件(ListBox控件),本节将对这些控件进行详细讲解。

11.4.1 下拉组合框控件(ComboBox控件)

图11.20 ComboBox控件

下拉组合框控件(ComboBox控件)用于在下拉组合框中显示数据。下拉组合框控件(ComboBox控件)主要由两部分组成:第一部分是一个允许用户输入列表项的文本框;第二部分是一个列表框,它显示一个选项列表,用户可从中选择一项。如图11.20所示为ComboBox控件。

下面详细介绍ComboBox控件的一些常见用法。

说明

ComboBox显示与一个ListBox组合的文本框编辑字段,使用户可以从列表中选择项,也可以输入新文本。

1.创建只可以选择的下拉框

通过设置控件的DropDownStyle属性,可以将ComboBox控件设置为可以选择的下拉框。DropDownStyle属性有3个属性值,这3个属性值对应不同的样式。

 Simple:使ComboBox控件的列表部分总是可见的。

 DropDown:DropDownStyle属性的默认值,使用户可以编辑ComboBox控件的文本框部分,只有单击右侧的箭头才能显示列表部分。

 DropDownList:用户不能编辑ComboBox控件的文本框部分,呈现下拉框的样式。

将控件的DropDownStyle属性设置为DropDownList,控件就只能是可以选择的下拉框,不能编辑文本框部分的内容。

【例11.17】 创建一个Windows应用程序,将ComboBox控件的DropDownStyle属性设置为DropDownList,并且向控件中添加3项,使其只可以进行选择操作的下拉框,代码如下。(实例位置:光盘\TM\sl\11\14)

    private void Form1_Load(object sender, EventArgs e) //窗体的 Load 事件
    {
         //设置 DropDownStyle 属性,使控件呈现下拉列表的样式
         comboBox1.DropDownStyle = ComboBoxStyle.DropDownList;
         comboBox1.Items.Add("用一生下载你");           //向控件中添加数据
         comboBox1.Items.Add("芸烨湘枫");               //向控件中添加数据
         comboBox1.Items.Add("一生所爱");               //向控件中添加数据
    }

图11.21 只可以选择的下拉列表样式

程序的运行结果如图11.21所示。

2.选中下拉组合框中可编辑部分的所有文本

通过控件的SelectAll方法,可以选择ComboBox控件的可编辑部分的所有文本。

语法如下:

    public void SelectAll()

使用SelectAll方法之前,要将控件的DropDownStyle属性设置为DropDown,这样才能在文本框部分对选择项进行编辑。

【例11.18】 创建一个Windows应用程序,将控件的DropDownStyle属性设置为DropDown,然后向控件中添加3项。选择下拉列表中的某项,然后单击“选择”按钮调用控件的SelectAll方法。当再次查看下拉列表时,可以看到可编辑文本框中的内容已经被选中,代码如下。(实例位置:光盘\TM\sl\11\15)

    private void Form1_Load(object sender, EventArgs e)
    {
         //设置 DropDownStyle 属性,使控件呈现下拉列表的样式
         comboBox1.DropDownStyle = ComboBoxStyle.DropDown;
         //向控件中添加项目
         comboBox1.Items.Add("用一生下载你");
         comboBox1.Items.Add("芸烨湘枫");
         comboBox1.Items.Add("一生所爱");
    }
    private void button1_Click(object sender, EventArgs e)
    {
    //使用 SelectAll 方法。当再次查看下拉列表时,可以看到可编辑文本框中的内容已经被选中
         comboBox1.SelectAll();
    }

程序的运行结果如图11.22所示。

3.响应下拉组合框的选项值更改事件

当下拉列表的选择项发生改变时,将会引发控件的SelectedValueChanged事件。

说明

当SelectedValue属性更改时才触发SelectedValueChanged事件。

【例11.19】 创建一个Windows应用程序,当下拉列表的选择项发生改变时,引发了控件的SelectedValueChanged事件。在控件的SelectedValueChanged事件中,使Label控件的Text属性等于控件的选择项,代码如下。(实例位置:光盘\TM\sl\11\16)

    private void Form1_Load(object sender, EventArgs e)
    {
         //设置 DropDownStyle 属性,使控件呈现下拉列表的样式
         comboBox1.DropDownStyle = ComboBoxStyle.DropDown;
         //向控件中添加项目
         comboBox1.Items.Add("用一生下载你");
         comboBox1.Items.Add("芸烨湘枫");
         comboBox1.Items.Add("一生所爱");
    }
    private void comboBox1_SelectedValueChanged(object sender, EventArgs e)
    {
         //在控件的 SelectedValueChanged 事件中,使 Label 控件的 Text 属性等于控件的选择项
         label1.Text = comboBox1.Text;
    }

程序的运行结果如图11.23所示。

图11.22 SelectAll方法的使用

图11.23 获取控件改变后的值

11.4.2 复选框控件(CheckBox控件)

复选框控件(CheckBox控件)用来表示是否选取了某个选项条件,常用于为用户提供具有是/否或真/假值的选项。如图11.24所示为CheckBox控件。

下面详细介绍复选框控件(CheckBox控件)的一些常见用法。

1.判断复选框是否选中

通过在控件的Click事件中判断控件的CheckState属性,来判断复选框是否被选中。CheckState属性的返回值是Checked或Unchecked,返回值Checked表示控件处在选中状态,而返回值Unchecked表示控件已经取消选中状态。

说明

CheckBox控件指示某个特定条件是处于打开状态还是处于关闭状态。它常用于为用户提供是/否或真/假选项。可以成组使用复选框(CheckBox)控件以显示多重选项,用户可以从中选择一项或多项。

【例11.20】 创建一个Windows应用程序,在控件的Click事件中判断控件是否被选中,并弹出相应的提示,代码如下。(实例位置:光盘\TM\sl\11\17)

    private void checkBox1_Click(object sender, EventArgs e)
    {
         //使用 if 语句判断控件是否被选中
         if (checkBox1.CheckState == CheckState.Checked)
         {
               MessageBox.Show("CheckBox 控件被选中");   //如果被选中弹出相应提示
         }
         else                                            //否则
         {
               //提示该控件的选择被取消
               MessageBox.Show("CheckBox 控件选择被取消");
         }
    }

程序的运行结果如图11.25和图11.26所示。

图11.24 CheckBox控件

图11.25 控件被选中

图11.26 控件取消选择

2.响应复选框的选中状态更改事件

当控件的选择状态发生改变时,将会引发控件的CheckStateChanged事件。

【例11.21】 创建一个Windows应用程序,在控件的CheckStateChanged事件中编写代码,实现当控件的选择状态发生改变时,弹出提示框,代码如下。(实例位置:光盘\TM\sl\11\18)

    private void checkBox1_CheckStateChanged(object sender, EventArgs e)
    {
         //在 CheckStateChanged 事件中编写代码,实现当控件的选择状态发生改变时,弹出提示框
         MessageBox.Show("控件的选择状态发生改变");
    }

程序的运行结果如图11.27所示。

图11.27 选中状态更改引发CheckStateChanged事件

注意

可以在该事件中通过Checked(判断复选框是否为选中状态)属性判断复选框是否被选中。

11.4.3 单选按钮控件(RadioButton控件)

单选按钮控件(RadioButton控件)为用户提供由两个或多个互斥选项组成的选项集。当用户选中某单选按钮时,同一组中的其他单选按钮不能同时选定。如图11.28所示为RadioButton控件。

图11.28 RadioButton控件

说明

单选按钮必须在同一组中才能实现单选效果。

下面详细介绍单选按钮控件(RadioButton控件)的一些常见用途。

1.判断单选按钮是否选中

通过在控件的Click事件中判断控件的Checked属性的返回值是否为true,即可判断单选按钮是否被选中,如果返回值是true,则控件被选中;返回值为false,则控件选中状态被取消。

【例11.22】 创建一个Windows应用程序,在窗体中添加两个RadioButton控件,分别在两个控件的Click事件中通过if语句判断控件的Checked属性的返回值是否为true,代码如下。(实例位置:光盘\TM\sl\11\19)

    private void Form1_Load(object sender, EventArgs e)
    {
         //设置两个单选按钮的 Checked 属性为 false
         radioButton1.Checked = false;
         radioButton2.Checked = false;
    }
    private void radioButton2_Click(object sender, EventArgs e)
    {
         //控件的 Click 事件中通过 if 语句判断控件的 Checked 属性的返回值是否为 true
         if (radioButton2.Checked == true)
         {
               MessageBox.Show("RadioButton2 控件被选中");
         }
    }
    private void radioButton1_Click(object sender, EventArgs e)
    {
         //控件的 Click 事件中通过 if 语句判断控件的 Checked 属性的返回值是否为 true
         if (radioButton1.Checked == true)
         {
               MessageBox.Show("RadioButton1 控件被选中");
         }
    }

程序的运行结果如图11.29和图11.30所示。

图11.29 RadioButton1控件被选中

图11.30 RadioButton2控件被选中

2.响应单选按钮选中状态更改事件

当控件的选中状态发生更改时,会引发控件的CheckedChanged事件。

【例11.23】 创建一个Windows应用程序,在窗体中添加一个RadioButton控件和两个Button控件,单击Button1按钮,选中RadioButton控件;单击Button2按钮,取消RadioButton控件的选中状态,代码如下。(实例位置:光盘\TM\sl\11\20)

    private void radioButton1_CheckedChanged(object sender, EventArgs e)
    {
         //在控件的 CheckedChanged 事件中编写代码,实现当控件的选择状态改变时弹出提示
         MessageBox.Show("RadioButton1 控件的选中状态被更改");
    }
    private void button1_Click(object sender, EventArgs e)
    {
         radioButton1.Checked = true;           //选中单选按钮
    }
    private void button2_Click(object sender, EventArgs e)
    {
         radioButton1.Checked = false;          //取消单选按钮的选中状态
    }
    private void Form1_Load(object sender, EventArgs e)
    {
         radioButton1.Checked = false;          //设置单选按钮的 Checked 属性为 false
    }

程序的运行结果如图11.31和图11.32所示。

图11.31 选中RadioButton控件

图11.32 取消选中RadioButton控件

11.4.4 数值选择控件(NumericUpDown控件)

数值选择控件(NumericUpDown控件)是一个显示和输入数值的控件。该控件提供一对上下箭头,用户可以单击上下箭头选择数值,也可以直接输入。该控件的Maximum属性可以设置数值的最大值,如果输入的数值大于这个属性的值,则自动把数值改为设置的最大值。该控件的Minimum属性可以设置数值的最小值,如果输入的数值小于这个属性的值,则自动把数值改为设置的最小值。如图11.33所示为NumericUpDown控件。

下面详细介绍数值选择控件(NumericUpDown控件)的常见用途。

1.获取NumericUpDown控件中显示的数值

通过控件的Value属性,可以获取NumericUpDown控件中显示的数值。

语法如下:

    public decimal Value { get; set; }

属性值:NumericUpDown控件的数值。

【例11.24】 创建一个Windows应用程序,向窗体中添加一个NumericUpDown控件和一个Label控件,在窗体的Load事件中,首先设置控件的Maximum属性为20,Minimum属性为1。当控件的值发生改变时,通过Label控件显示更改后的控件中的数值,代码如下。(实例位置:光盘\TM\sl\11\21)

    private void Form1_Load(object sender, EventArgs e)
    {
         numericUpDown1.Maximum = 20;         //设置控件的最大值为 20
         numericUpDown1.Minimum = 1;          //设置控件的最小值为 1
    }
    private void numericUpDown1_ValueChanged(object sender, EventArgs e)
    {
         //实现当控件的值改变时,显示当前的值
         label1.Text = "当前控件中显示的数值:" + numericUpDown1.Value;
    }

程序的运行结果如图11.34所示。

图11.33 NumericUpDown控件

图11.34 获取控件中显示的数值

说明

当UserEdit属性(指示用户是否已输入值)设置为true,则在验证或更新该值之前,将调用ParseEditText方法(将数字显示框中显示的文本转换为数值)。然后,验证该值是否在Minimum(最小值)和Maximum(最大值)两个值之间,并调用UpdateEditText方法(以适当的格式显示数字显示框中的当前值)。

2.设置NumericUpDown控件中数值的显示方式

NumericUpDown控件的DecimalPlaces属性用于确定在小数点后显示几位数,默认值为0。ThousandsSeparator属性用于确定是否每隔3个十进制数字位就插入一个分隔符,默认情况下为false。如果将Hexadecimal属性设置为true,则该控件可以用十六进制(而不是十进制格式)显示值,默认情况下为false。

说明

DecimalPlaces属性的值不能小于0,或大于99,否则会出现ArgumentOutOfRangeException异常(当参数值超出调用的方法所定义的允许取值范围时引发的异常)。

【例11.25】 创建一个Windows应用程序,通过设置NumericUpDown控件的DecimalPlaces属性为2,可以使控件中数值的小数点后显示两位数,代码如下。(实例位置:光盘\TM\sl\11\22)

    private void Form1_Load(object sender, EventArgs e)
    {
         numericUpDown1.Maximum = 20;         //设置控件的最大值为 20
         numericUpDown1.Minimum = 1;          //设置控件的最小值为 1
         //设置控件的 DecimalPlaces 属性,使控件中的数值的小数点后显示两位数
         numericUpDown1.DecimalPlaces = 2;
    }

程序的运行结果如图11.35所示。

图11.35 DecimalPlaces属性设置为2

11.4.5 列表控件(ListBox控件)

列表控件(ListBox控件)用于显示一个列表,用户可以从中选择一项或多项。如果选项总数超出可以显示的项数,则控件会自动添加滚动条。如图11.36所示为ListBox控件。

图11.36 ListBox控件

下面详细介绍ListBox控件的几种常见用法。

1.在ListBox控件中添加和移除项

通过ListBox控件的Items属性的Add方法,可以向ListBox控件中添加项目。通过ListBox控件的Items属性的Remove方法,可以将ListBox控件中选中的项目移除。

【例11.26】 创建一个Windows应用程序,通过ListBox控件的Items属性的Add方法和Remove方法,实现向控件中添加项目以及移除选中项目,代码如下。(实例位置:光盘\TM\sl\11\23)

程序的运行结果如图11.37所示。

2.创建总显示滚动条的列表控件

通过设置控件的HorizontalScrollbar属性和ScrollAlwaysVisible属性可以使控件总显示滚动条。如果将HorizontalScrollbar属性设置为true,则显示水平滚动条。如果将ScrollAlwaysVisible属性设置为true,则始终显示垂直滚动条。

【例11.27】 创建一个Windows应用程序,向窗体中添加一个ListBox控件、一个TextBox控件和一个Button控件,将ListBox控件的HorizontalScrollbar属性和ScrollAlwaysVisible属性都设置为true,使其能显示水平和垂直方向的滚动条,代码如下。(实例位置:光盘\TM\sl\11\24)

程序的运行结果如图11.38所示。

图11.37 向ListBox控件中添加和移除项

图11.38 控件总显示滚动条

说明

在ListBox控件中可使用MultiColumn属性指示该控件是否支持多列,如果将其设置为true,则支持多列显示。

3.在ListBox控件中选择多项

通过设置SelectionMode属性的值可以实现在ListBox控件中选择多项。SelectionMode属性的属性值是SelectionMode枚举值之一,默认为SelectionMode.One。SelectionMode枚举成员及说明如表11.4所示。

表11.4 SelectionMode枚举成员及说明

下面以MultiExtended为例介绍如何使用枚举成员。

【例11.28】 创建一个Windows应用程序,通过设置控件的SelectionMode属性值为SelectionMode枚举成员MultiExtended,实现在控件中可以选择多项,并且用户可使用Shift键、Ctrl键和箭头键来进行选择,代码如下。(实例位置:光盘\TM\sl\11\25)

程序的运行结果如图11.39所示。

图11.39 ListBox控件中选择多项

11.5 分组类控件

视频讲解:光盘\TM\lx\11\分组类控件的使用.exe

分组类控件主要包括容器控件(Panel控件)、分组框控件(GroupBox控件)和选项卡控件(TabControl控件),下面将对这些控件进行详细的讲解。通过对本节的学习,读者应该可以掌握分组类控件的使用方法。

11.5.1 容器控件(Panel控件)

图11.40 Panel控件

容器控件(Panel控件)用于为其他控件提供可识别的分组。容器控件(Panel控件)可以使窗体的分类更详细,便于用户理解。容器控件(Panel控件)可以有滚动条。如图11.40所示为Panel控件。

容器控件就好像是商场的各个楼层,如1楼是化妆品层、2楼是男装层、3楼是女装层等。当然,也可以在各层中继续划分,也就是可以在容器控件中嵌套放置多个容器控件。

使用Panel控件的Show方法可以显示控件。

语法如下:

    public void Show()

【例11.29】 创建一个Windows应用程序,如果文本框中输入的文本是“芸烨湘枫”,则调用Show方法显示Panel控件。Panel控件中有一个RichTextBox控件,用于显示“芸烨湘枫”的相关信息,代码如下。(实例位置:光盘\TM\sl\11\26)

程序的运行结果如图11.41所示。

图11.41 显示Panel控件

说明

如果将Panel控件的Enabled属性(设置控件是否可以对用户交互作出响应)设置为false,那么在该容器中的所有控件将设为不可用状态。

11.5.2 分组框控件(GroupBox控件)

分组框控件(GroupBox控件)主要为其他控件提供分组,按照控件的分组来细分窗体的功能。其在所包含的控件集周围总是显示边框,并且可以显示标题,但是分组框控件(GroupBox控件)没有滚动条。如图11.42所示为GroupBox控件。

可以通过控件的Text属性来设置控件要显示的标题。

语法如下:

    public override string Text { get; set;}

【例11.30】 创建一个Windows应用程序,通过设置GroupBox控件的Text属性,使GroupBox控件的标题为“诗词”,代码如下。(实例位置:光盘\TM\sl\11\27)

    private void Form1_Load(object sender, EventArgs e)
    {
         groupBox1.Text = "诗词";                 //设置控件的 Text 属性,使其显示“诗词”
    }

程序的运行结果如图11.43所示。

图11.42 GroupBox控件

图11.43 设置控件的标题

11.5.3 选项卡控件(TabControl控件)

图11.44 TabControl控件

选项卡控件(TabControl控件)可以添加多个选项卡,然后在选项卡上添加子控件。这样就可以把窗体设计成多页,使窗体的功能划分为多个部分。选项卡中可包含图片或其他控件。选项卡控件还可以用来创建用于设置一组相关属性的属性页。如图11.44所示为TabControl控件。

TabControl控件包含选项卡页,TabPage控件表示选项卡,TabControl控件的TabPages属性表示其中的所有TabPage控件的集合。TabPages集合中TabPage选项卡的顺序反映了TabControl控件中选项卡的顺序。下面详细介绍TabControl控件的一些常用设置。

1.改变选项卡的显示样式

通过使用TabControl控件和组成控件上各选项卡的TabPage对象的属性,可以更改Windows窗体中选项卡的外观。通过设置这些属性,可使用编程方式在选项卡上显示图像,以垂直方式而非水平方式显示选项卡,显示多行选项卡,以及启用或禁用选项卡。

(1)在选项卡的标签部位显示图标

【例11.31】 创建一个Windows应用程序,向窗体中添加一个ImageList控件,然后将图像添加到ImageList控件的图像列表中。将TabControl控件的ImageList属性设置为ImageList控件,将TabPage的ImageIndex属性设置为列表中的相应图像的索引,代码如下。(实例位置:光盘\TM\sl\11\28)

    private void Form1_Load(object sender, EventArgs e)
    {
         tabControl1.ImageList = imageList1;          //设置控件的 ImageList 属性为 imageList1
         //第一个选项卡的图标是 imageList1 中索引为 0 的图标
         tabPage1.ImageIndex = 0;
         tabPage1.Text = "选项卡 1";                  //设置控件第一个选项卡的 Text 属性
         //第二个选项卡的图标是 imageList1 中索引为 0 的图标
         tabPage2.ImageIndex = 0;
         tabPage2.Text = "选项卡 2";                  //设置控件第二个选项卡的 Text 属性
    }

程序的运行结果如图11.45所示。

说明

为了使用户更容易了解选项卡的作用,可以在鼠标移入选项卡时,弹出一个提示信息,对当前选项卡的作用或操作步骤进行详细说明。其设置步骤如下:将tabPage属性中的ShowToolTips属性设置为true,然后在tabPage属性的ToolTipText属性中输入相关的说明文字。

(2)将选项卡显示为按钮

将TabControl控件的Appearance属性设置为Buttons或FlatButtons,即可将选项卡显示为按钮样式。如果设置为Buttons,则选项卡具有三维按钮的外观。如果设置为FlatButtons,则选项卡具有平面按钮的外观。

【例11.32】 创建一个Windows应用程序,将控件的Appearance属性设置为Buttons,使选项卡具有三维按钮的外观,代码如下。(实例位置:光盘\TM\sl\11\29)

    private void Form1_Load(object sender, EventArgs e)
    {
         tabControl1.ImageList = imageList1;           //设置控件的 ImageList 属性为 imageList1
         //第一个选项卡的图标是 imageList1 中索引为 0 的图标
         tabPage1.ImageIndex = 0;
         //第二个选项卡的图标是 imageList1 中索引为 0 的图标
         tabPage2.ImageIndex = 0;
         //将控件的 Appearance 属性设置为 Buttons,使选项卡具有三维按钮的外观
         tabControl1.Appearance = TabAppearance.Buttons;
    }

程序的运行结果如图11.46所示。

图11.45 标签部位显示图标

图11.46 将选项卡显示为按钮

2.在选项卡中添加控件

如果要在选项卡中添加控件,可以通过TabPage的Controls属性的Add方法实现。

Add方法主要用于将指定的控件添加到控件集合中。

语法如下:

    public virtual void Add(Control value)

value:要添加到控件集合的控件。

【例11.33】 创建一个Windows应用程序,通过TabPage的Controls属性的Add方法,向tabPage1中添加一个按钮控件,代码如下。(实例位置:光盘\TM\sl\11\30)

    private void Form1_Load(object sender, EventArgs e)
    {
         tabControl1.ImageList = imageList1;          //设置控件的 ImageList 属性为 imageList1
         //第一个选项卡的图标是 imageList1 中索引为 0 的图标
         tabPage1.ImageIndex = 0;
         //第二个选项卡的图标是 imageList1 中索引为 0 的图标
         tabPage2.ImageIndex = 0;
         Button btn1 = new Button();                  //实例化一个 Button 类,动态生成一个按钮
         btn1.Text = "新增按钮";                      //设置按钮的 Text 属性
         tabPage1.Controls.Add(btn1);                 //使用 Add 方法,将这个按钮添加到选项卡 1 中
    }

程序的运行结果如图11.47所示。

图11.47 向tabPage1中添加按钮

注意

在设置TabControl控件中的控件位置时,是以TabControl控件为参照物的。

3.添加和移除选项卡

(1)以编程方式添加选项卡

控件默认情况下,TabControl控件包含两个TabPage控件,可以使用TabPages属性的Add方法添加新的选项卡。

Add方法主要用于将TabPage添加到集合。

说明

在TabPages集合中所添加的选项卡页的顺序,是选项卡在控件中出现的顺序。

集合中的选项卡页的顺序反映了选项卡在控件中出现的顺序。

语法如下:

    public void Add(TabPage value)

value:要添加的TabPage。

【例11.34】 创建一个Windows应用程序,使用TabControl控件的TabPages属性的Add方法,向控件中添加新的选项卡,代码如下。(实例位置:光盘\TM\sl\11\31)

    private void Form1_Load(object sender, EventArgs e)
    {
         tabControl1.ImageList = imageList1;          //设置控件的 ImageList 属性为 imageList1
         //第一个选项卡的图标是 imageList1 中索引为 0 的图标
         tabPage1.ImageIndex = 0;
         //第二个选项卡的图标是 imageList1 中索引为 0 的图标
         tabPage2.ImageIndex = 0;
         //声明一个字符串变量,用于生成新增选项卡的名称
         string Title = "新增选项卡 " + (tabControl1.TabCount + 1).ToString();
         TabPage MyTabPage = new TabPage(Title);      //实例化 TabPage
         //使用 TabControl 控件的 TabPages 属性的 Add 方法添加新的选项卡
         tabControl1.TabPages.Add(MyTabPage);
    }

图11.48 添加选项卡

程序的运行结果如图11.48所示。

(2)以编程方式移除选项卡

如果要移除控件中的某个选项卡,可以使用TabPages属性的Remove方法。

Remove方法的功能是从集合中移除TabPage。

语法如下:

    public void Remove(TabPage value)

value:要移除的TabPage。

说明

在用TabPages属性的Remove(value)方法删除选项卡时,如果参数value的值为空则触发异常。

【例11.35】 创建一个Windows应用程序,通过使用TabPages属性的Remove方法,删除指定的选项卡,代码如下。(实例位置:光盘\TM\sl\11\32)

    private void button1_Click(object sender, EventArgs e)
    {
         //声明一个字符串变量,用于生成新增选项卡的名称
         string Title = "新增选项卡 " + (tabControl1.TabCount + 1).ToString();
         TabPage MyTabPage = new TabPage(Title);        //实例化 TabPage
         //使用 TabControl 控件的 TabPages 属性的 Add 方法添加新的选项卡
         tabControl1.TabPages.Add(MyTabPage);
    }
    private void button2_Click(object sender, EventArgs e)
    {
         if (tabControl1.SelectedIndex == 0)            //判断是否选择了要删除的选项卡
         {
               MessageBox.Show("请选择要删除的选项卡"); //如果没有选择,弹出提示
         }
         else
         {
               //使用 TabControl 控件的 TabPages 属性的 Remove 方法删除指定的选项卡
               tabControl1.TabPages.Remove(tabControl1.SelectedTab);
         }
    }

程序的运行结果如图11.49所示。

如果要删除所有的选项卡,可以使用TabPages属性的Clear方法。

图11.49 删除指定的选项卡

Clear方法主要用于从集合中移除所有的选项卡页。

语法如下:

    public virtual void Clear()

【例11.36】 删除控件中所有的选项卡,代码如下。

    tabControl1.TabPages.Clear();           //使用 Clear 方法删除所有的选项卡

11.6 菜单、工具栏和状态栏控件

视频讲解:光盘\TM\lx\11\菜单、工具栏和状态栏控件的使用.exe

菜单是窗体应用程序主要的用户界面要素,工具栏为应用程序提供了操作系统的界面,状态栏显示系统的一些状态信息。在以下内容中,将会对菜单控件(MenuStrip控件)、工具栏控件(ToolStrip控件)和状态栏控件(StatusStrip控件)进行详细讲解。

11.6.1 菜单控件(MenuStrip控件)

菜单控件(MenuStrip控件)是程序的主菜单。MenuStrip控件取代了先前版本的MainMenu控件。MenuStrip控件支持多文档界面、菜单合并、工具提示和溢出。可以通过添加访问键、快捷键、选中标记、图像和分隔条,来增强菜单的可用性和可读性。如图11.50所示为MenuStrip控件。

为使读者更加直观地了解MenuStrip控件的用法,下面通过一个实例介绍如何创建菜单栏。

【例11.37】 创建一个Windows应用程序,演示如何通过MenuStrip控件创建一个类似Word的“文件”菜单,具体步骤如下。(实例位置:光盘\TM\sl\11\33)

(1)创建一个Windows应用程序,从工具箱中将MenuStrip控件拖曳到窗体中,如图11.51所示。

(2)在输入菜单名称时,系统会自动产生输入下一个菜单名称的提示,如图11.52所示。

(3)在如图11.52所示的文本框中输入“文件(&F)”后,就会产生“文件(F)”。在此处,“&”被识别为确认快捷键的字符。例如,“文件(F)”菜单就可以通过键盘上的Alt+F键打开。同样,在“文件(F)”菜单下创建“新建(N)”“打开(O)”“关闭(C)”“保存(S)”子菜单。当单击“文件”菜单后,可以在弹出的菜单中单击鼠标右键,添加其他的内容,如图11.53所示。

图11.50 MenuStrip控件

图11.51 将MenuStrip控件拖曳到窗体中

(4)添加完毕,最后效果如图11.54所示。

图11.52 输入菜单名称

图11.53 添加菜单内容

图11.54 菜单示意图

说明

在使用菜单中的快捷键时,首先要选择主菜单,在弹出下拉列表后,才可以在键盘中单击子菜单所对应的快捷键。

说明

在设置菜单项时,也可以在MenuStrip控件中的Items属性所调用的“项集合编辑器”对话框中添加所需的菜单项。

11.6.2 工具栏控件(ToolStrip控件)

工具栏控件(ToolStrip控件)是.NET框架3.5增加的新控件,它替换了早期版本的ToolBar控件、ToolStrip及其关联的类,可以创建具有Windows XP、Office、Internet Explorer或自定义的外观和行为的工具栏及其他用户界面元素。这些元素支持溢出及运行时重新排序。如图11.55所示为ToolStrip控件。

创建工具栏的过程比较简单,下面通过实例演示如何创建工具栏。

工具栏其实就相当于工厂每个工人的工具箱,每个工人都有自己常用的工具(可以是工厂发的,也可以是自己做的),为了方便工作,将这些常用工具放入个人工具箱中。

【例11.38】 创建一个Windows应用程序,演示如何通过ToolStrip控件创建一个工具栏,具体步骤如下。(实例位置:光盘\TM\sl\11\34)

(1)创建一个Windows应用程序,从工具箱中将ToolStrip控件拖曳到窗体中,如图11.56所示。

(2)单击工具栏上向下箭头的提示图标,如图11.57所示。

图11.55 ToolStrip控件

图11.56 将ToolStrip控件拖曳到窗体中

图11.57 添加工具栏项目

从图11.57中可以看到,当单击工具栏中向下的箭头,添加工具栏项目时,在下拉菜单中有8种不同的类型,下面分别进行介绍。

 Button:包含文本和图像中可让用户选择的项。

 Label:包含文本和图像的项,不可以让用户选择,可以显示超链接。

 SplitButton:在Button的基础上增加了一个下拉菜单。

 DropDownButton:用于下拉菜单选择项。

 Separator:分隔符。

 ComboBox:显示一个ComboBox的项。

 TextBox:显示一个TextBox的项。

 ProgressBar:显示一个ProgressBar的项。

(3)添加相应的工具栏按钮后,可以设置按钮显示的图像,如图11.58所示。

(4)运行程序,结果如图11.59所示。

图11.58 设置按钮图像

图11.59 程序运行结果

11.6.3 状态栏控件(StatusStrip控件)

状态栏控件(StatusStrip控件)通常处于窗体的最底部,用于显示窗体上的对象的相关信息,或者可以显示应用程序的信息。通常,StatusStrip控件由ToolStripStatusLabel对象组成,每个这样的对象都可以显示文本、图标或同时显示这两者。StatusStrip还可以包含ToolStripDropDownButton、ToolStripSplitButton和ToolStripProgressBar控件。如图11.60所示为StatusStrip控件。

下面通过一个实例演示如何使用StatusStrip控件。

【例11.39】 创建一个Windows应用程序,使用StatusStrip控件制作状态栏,在状态栏中显示当前日期,以及ToolStripProgressBar控件,单击“加载”按钮后,加载进度条,代码如下。(实例位置:光盘\TM\sl\11\35)

    private void Form1_Load(object sender, EventArgs e)
    {
         //在任务栏上显示系统的当前日期
         this.toolStripStatusLabel2.Text = DateTime.Now.ToShortDateString();
    }
    private void button1_Click(object sender, EventArgs e)
    {
         this.toolStripProgressBar1.Minimum = 0;           //进度条的起始数值
         this.toolStripProgressBar1.Maximum = 5000;        //进度条的最大值
         this.toolStripProgressBar1.Step = 2;              //进度条的增值
         for (int i = 0; i <= 4999; i++)                   //使用 for 循环读取数据
         {  
               this.toolStripProgressBar1.PerformStep();   //按照 Step 属性的数量增加进度栏的当前位置
         }
    }

程序的运行结果如图11.61所示。

图11.60 StatusStrip控件

图11.61 状态栏的应用

注意

ToolStripProgressBar控件只能以水平方向显示。

11.7 小结

本章主要介绍了Windows应用程序的常用控件,在讲解的过程中,通过大量的实例演示控件的用法。Windows应用程序中,常用控件大体分为文本类控件、选择类控件、分组控件和菜单、工具栏以及状态栏控件。在介绍这些常用控件的同时,还介绍了控件的一些常用设置,供读者在编写程序时参考。

11.8 实践与练习

(1)尝试开发一个程序,要求在录入商品数量时,验证录入的数据必须是数字。(答案位置:光盘\TM\sl\11\36)

(2)尝试开发一个程序,要求在Button按钮中显示更加人性化的图标。(答案位置:光盘\TM\sl\11\37)

(3)尝试开发一个程序,要求根据CheckBox复选框的选中状态判断用户的操作权限。(答案位置:光盘\TM\sl\11\38)

第12章 Windows应用程序高级控件

第12章
Windows应用程序高级控件

视频讲解:71分钟

第11章已经讲解了Windows应用程序的常用控件,除此之外,Windows应用程序还有一些高级控件。熟练地掌握这些高级控件,在开发应用程序过程中可以快速地实现一些复杂的功能。本章将详细介绍Windows应用程序的高级控件,讲解过程中为了便于读者理解结合了大量的举例。

通过阅读本章,您可以:

 了解什么是存储图像控件并掌握其用法

 了解什么是列表视图控件

 掌握树控件的常见用法

 了解日期控件的常用设置

 掌握月历控件的用法

 了解日期控件和月历控件的区别

 掌握其他高级控件的用法

12.1 ImageList控件(存储图像控件)

视频讲解:光盘\TM\lx\12\ImageList控件的使用.exe

图12.1 ImageList控件

ImageList控件(存储图像控件)用于存储图像资源,然后在控件上显示出来,这样就简化了对图像的管理。ImageList控件的主要属性是Images,它包含关联控件将要使用的图片。每个单独的图像可通过其索引值或其键值来访问。所有图像都将以同样的大小显示,该大小由ImageSize属性设置,较大的图像将缩小至适当的尺寸。如图12.1所示为ImageList控件(存储图像控件)。

ImageList控件实际上就相当于一个图片集,也就是将多个图片存储到图片集中,当想要对某一图片进行操作时,只需根据其图片的编号,就可以找出该图片,并对其进行操作。

12.1.1 在ImageList控件中添加图像

使用ImageList控件的Images属性的Add方法,可以通过编程的方式向ImageList控件中添加图像。下面对Add方法进行详细介绍。

Add方法的功能是将指定图像添加到ImageList中。

语法如下:

    public void Add(Image value)

value:要添加到列表中的图像。

【例12.1】 创建一个Windows应用程序,首先获取图像的路径,然后通过ImageList控件的Images属性的Add方法向控件中添加图像,代码如下。(实例位置:光盘\TM\sl\12\1)

    private void Form1_Load(object sender, EventArgs e)
    {
         //设置要加载的第一张图片的路径
         string Path = Application.StartupPath.Substring(0,Application.StartupPath.Substring(0,Application.
         StartupPath. LastIndexOf("\\")).LastIndexOf("\\"));
         Path += @"\01.jpg";
         //设置要加载的第二张图片的路径
         string Path2 = Application.StartupPath.Substring(0, Application.StartupPath.Substring(0, Application.
         StartupPath. LastIndexOf("\\")).LastIndexOf("\\"));
         Path2 += @"\02.jpg";
         Image Mimg=Image.FromFile(Path,true);         //创建一个 Image 对象
         imageList1.Images.Add(Mimg);                  //使用 Images 属性的 Add 方法向控件中添加图像
         Image Mimg2 = Image.FromFile(Path2, true);    //创建一个 Image 对象
         imageList1.Images.Add(Mimg2);                 //使用 Images 属性的 Add 方法向控件中添加图像
         imageList1.ImageSize = new Size(200,165);     //设置显示图片的大小
         pictureBox1.Width = 200;                      //设置 pictureBox1 控件的宽      
         pictureBox1.Height = 165;                     //设置 pictureBox1 控件的高
    }
    private void button1_Click(object sender, EventArgs e)
    {
         //设置 pictureBox1 的图像索引是 imageList1、控件索引为 0 的图片
         pictureBox1.Image = imageList1.Images[0];
    }
    private void button2_Click(object sender, EventArgs e)
    {
         //设置 pictureBox1 的图像索引是 imageList1、控件索引为 1 的图片
         pictureBox1.Image = imageList1.Images[1];
    }       

程序的运行结果如图12.2所示。

图12.2 显示添加后的图像

说明

在向ImageList组件中存储图片时,可以通过该组件的ImageSize属性设置图片的尺寸,其默认尺寸是16×16,最大尺寸是256×256。

12.1.2 在ImageList控件中移除图像

在ImageList控件中可以使用RemoveAt方法移除单个图像或使用Clear方法清除图像列表中的所有图像。下面详细介绍RemoveAt方法和Clear方法。

RemoveAt方法用于从列表中移除图像。

语法如下:

    public void RemoveAt(int index)

index:要移除的图像的索引。

说明

在用RemoveAt方法删除图片时,如果索引无效(超出范围),则发生运行异常。

Clear方法主要用于从ImageList中移除所有图像。

语法如下:

    public void Clear()

【例12.2】 创建一个Windows应用程序,设置在控件上显示的图像,使用Images属性的Add方法将其添加到控件中。然后运行程序,单击“加载图像”按钮显示图像,再单击“移除图像”按钮移除图像,然后重新单击“加载图像”按钮,将弹出“没有图像”的提示,代码如下。(实例位置:光盘\TM\sl\12\2)

程序的运行结果如图12.3所示。

图12.3 移除控件中的图像

还可以使用Clear方法从ImageList中移除所有图像,代码如下。

    imageList1.Images.Clear();               //使用 Clear 方法移除所有图像

12.2 ListView控件(列表视图控件)

视频讲解:光盘\TM\lx\12\ListView控件的使用.exe

图12.4 ListView控件

ListView控件(列表视图控件)显示带图标的项的列表,可以显示大图标、小图标和数据。使用ListView控件可以创建类似Windows资源管理器右窗口的用户界面。如图12.4所示为ListView控件。

ListView控件可以通过View属性设置项在控件中显示的方式,View属性的值及说明如表12.1所示。

表12.1 View属性的值及说明

12.2.1 在ListView控件中添加移除项

1.添加项

可以使用ListView控件的Items属性的Add方法向控件中添加项,下面介绍Add方法。

Add方法用于将项添加至项的集合中。

语法如下:

    public virtual ListViewItem Add(string text,int imageIndex)

 text:项的文本。

 imageIndex:要为该项显示的图像的索引。

 返回值:已添加到集合中的ListViewItem。

【例12.3】 创建一个Windows应用程序,通过使用ListView控件的Items属性的Add方法向控件中添加项,代码如下。(实例位置:光盘\TM\sl\12\3)

    private void button1_Click(object sender, EventArgs e)
    {
         if (textBox1.Text == "")                    //判断文本框中是否输入数据
         {
               MessageBox.Show("项目不能为空");      //如果没有输入数据则弹出提示
         }
         else                                        //否则
    {
               //使用 ListView 控件的 Items 属性的 Add 方法向控件中添加项
               listView1.Items.Add(textBox1.Text.Trim());
         }
    }

程序的运行结果如图12.5所示。

图12.5 添加项目

说明

在ListView控件中添加完项目后,可以用CheckBoxes属性显示复选框,以便用户可以选中要对其执行操作的项。

2.移除项

通过使用控件的Items属性的RemoveAt或Clear方法可以移除控件中的项。RemoveAt方法移除指定的项,而Clear方法移除列表中的所有项。下面介绍RemoveAt和Clear方法。

RemoveAt方法用于移除集合中指定索引处的项。

语法如下:

    public virtual void RemoveAt(int index)

index:从零开始的索引(属于要移除的项)。

Clear方法用于从集合中移除所有项。

语法如下:

    public virtual void Clear()

【例12.4】 创建一个Windows应用程序,向控件中添加3个项目,然后选择要移除的项,单击“移除项”按钮,即可通过控件的Items属性的RemoveAt方法移除指定的项,单击“清空”按钮可以调用Clear方法清空所有的项,代码如下。(实例位置:光盘\TM\sl\12\4)

程序的运行结果如图12.6和图12.7所示。

图12.6 移除项目之前

图12.7 移除项目之后

说明

在删除ListView控件中的项目前,必须对项目的个数进行判断。如果为0,则不进行项目的删除,否则会触发异常。

12.2.2 选择ListView控件中的项

通过控件的Selected属性可以设置控件中的选择项,下面介绍Selected属性。

Selected属性用于获取或设置一个值,该值指示是否选定此项。

语法如下:

    public bool Selected { get; set; }

属性值:如果选定此项,则为true;否则为false。

【例12.5】 创建一个Windows应用程序,向ListView控件中添加3项,然后设置控件中的第3项的Selected属性为true,即设置为选择项,代码如下。(实例位置:光盘\TM\sl\12\5)

    private void Form1_Load(object sender, EventArgs e)
    {
         listView1.Items.Add("用一生下载你");         //使用 Add 方法向控件中添加项目
         listView1.Items.Add("芸烨湘枫");             //使用 Add 方法向控件中添加项目
         listView1.Items.Add("一生所爱");             //使用 Add 方法向控件中添加项目
         listView1.Items[2].Selected = true;          //使用 Selected 方法选中第 3 项
    }

程序的运行结果如图12.8所示。

图12.8 设置控件选择项

说明

如果用按钮控件选择ListView控件中的项,除了用Selected属性选择指定的项外,还通过Focus方法使ListView控件获得焦点。

12.2.3 为ListView控件中的项添加图标

如果为ListView控件中的项添加图标,则需要与ImageList控件相结合。使用ImageList控件设置ListView控件中项的图标,ListView控件可显示3个图像列表中的图标。List视图、Details视图和SmallIcon视图显示SmallImageList属性中指定的图像列表中的图像。LargeIcon视图显示LargeImageList属性中指定的图像列表中的图像。列表视图还可以在大图标或小图标旁显示StateImageList属性中设置的一组附加图标。实现的步骤如下。

(1)将相应的属性(SmallImageList、LargeImageList或StateImageList)设置为想要使用的现有ImageList组件。

(2)为每个具有关联图标的列表项设置ImageIndex或StateImageIndex属性。这些属性可在代码中设置,或在“ListViewItem集合编辑器”中进行设置。若要打开“ListViewItem集合编辑器”,可在“属性”窗口中单击Items属性旁的省略号按钮,这些属性可在设计器中使用“属性”窗口进行设置,也可在代码中设置。

【例12.6】 创建一个Windows应用程序,设置ListView控件的LargeImageList和SmallImageList属性为控件imageList1。然后用代码向ImageList控件中添加图像,并且向ListView控件中添加两项,设置这两项的ImageIndex属性分别为0和1,代码如下。(实例位置:光盘\TM\sl\12\6)

    private void Form1_Load(object sender, EventArgs e)
    {
         listView1.LargeImageList = imageList1;            //设置控件的 LargeImageList 属性
         imageList1.ImageSize = new Size(37,36);           //设置 imageList 控件图标的大小
         //向 imageList1 中添加两个图标
         imageList1.Images.Add(Image.FromFile("01.png"));
         imageList1.Images.Add(Image.FromFile("02.png"));
         listView1.SmallImageList = imageList1;            //设置控件的 SmallImageList 属性
         //向控件中添加两项
         listView1.Items.Add("用一生下载你");
         listView1.Items.Add("芸烨湘枫");
         listView1.Items[0].ImageIndex = 0;                //控件中第一项的图标索引为 0
         listView1.Items[1].ImageIndex = 1;                //控件中第二项的图标索引为 1
    }

程序的运行结果如图12.9所示。

图12.9 为控件中的项添加图标

说明

如果想要将ImageList组件中的大图标显示在ListView控件中的各项时,一般使用LargeImageList属性。如果要设置大图标,一般使用StateImageList属性进行设置。

12.2.4 在ListView控件中启用平铺视图

启用ListView控件的平铺视图功能,可以在图形信息和文本信息之间提供一种视觉平衡。为平铺视图中的某项显示的文本信息与为详细信息视图定义的列信息相同。在ListView控件中,平铺视图与分组功能或插入标记功能一起结合使用。如果要启用平铺视图,需要将View属性设置为Tile,可以通过设置TileSize属性来调整平铺的大小。关于View属性的值及说明参见表12.1。

【例12.7】 创建一个Windows应用程序,将控件View属性设置为Tile,启用平铺视图。为ImageList控件添加两个图片作为ListView控件中项的图标,向ListView控件中添加5项,设置各项的图标,通过控件的TileSize属性设置平铺的宽、高分别为100和50,代码如下。(实例位置:光盘\TM\sl\12\7)

    private void Form1_Load(object sender, EventArgs e)
    {
         listView1.View = View.Tile;                //设置 listView1 控件的 View 属性
         //设置控件的 LargeImageList 属性,其大图标在 imageList1 控件中选择
         listView1.LargeImageList = imageList1;
         //向 imageList1 控件中添加两张图片
         imageList1.Images.Add(Image.FromFile("1.bmp"));
         imageList1.Images.Add(Image.FromFile("2.bmp"));
         //向控件中添加项目
         listView1.Items.Add("用一生下载你");
         listView1.Items.Add("一生所爱");
         listView1.Items.Add("用一生下载你");
         listView1.Items.Add("一生所爱");
         listView1.Items.Add("用一生下载你");
         //设置控件中项目的图标
         listView1.Items[0].ImageIndex = 0;
         listView1.Items[1].ImageIndex = 1;
         listView1.Items[2].ImageIndex = 0;
         listView1.Items[3].ImageIndex = 1;
         listView1.Items[4].ImageIndex = 0;
         listView1.TileSize = new Size(100,50);      //设置 listView1 控件的 TileSize 属性
    }

程序的运行结果如图12.10所示。

图12.10 启用平铺视图

说明

在对ListView控件中的GridLines(行和列之间是否显示网格线)和FullRowSelect(单击某项是否选择其所有子项)属性进行操作时,必须将View属性设置为View.Details。

12.2.5 为ListView控件中的项分组

使用ListView控件的分组功能可以用分组形式显示相关项组。在屏幕上,这些组由包含组标题的水平组标头分隔。可以使用ListView组按字母顺序、日期或任何其他逻辑组合对项进行分组,从而简化大型列表的导航。若要启用分组,首先必须在设计器中或以编程方式创建一个或多个组。定义组后,可向组分配ListView项。此外,可以用编程方式将一个组中的项移至另外一个组中。下面介绍为ListView控件中的项分组的方法。

1.添加组

使用Groups集合的Add方法可以向控件中添加组,Add方法用于将指定的ListViewGroup添加到集合。

语法如下:

    public int Add(ListViewGroup group)

 group:要添加到集合中的ListViewGroup。

 返回值:该组在集合中的索引。或者,如果集合中已存在该组,则为-1。

【例12.8】 使用Groups集合的Add方法向控件listView1中添加一个分组,标题为“测试”,排列方式为左对齐,代码如下。

    listView1.Groups.Add(new ListViewGroup("测试",_HorizontalAlignment.Left));
2.移除组

使用Groups集合的RemoveAt或Clear方法,可以移除指定的组或者移除所有的组。

RemoveAt方法移除集合中指定索引位置的组。

语法如下:

    public void RemoveAt(int index)

index:要移除的ListViewGroup的集合中的索引。

Clear方法用于从集合中移除所有组。

语法如下:

    public void Clear()

【例12.9】 使用Groups集合的RemoveAt方法移除索引为1的组,使用Clear方法移除所有的组,代码如下。

    listView1.Groups.RemoveAt(1); //移除索引为 1 的组
    listView1.Groups.Clear();     //使用 Clear 方法移除所有的组
3.向组分配项或在组之间移动项

设置各个项的System.Windows.Forms.ListViewItem.Group属性,可以向组分配项或在组之间移动项。

【例12.10】 将ListView控件的第一项分配到第一个组中,代码如下。

    listView1.Items[0].Group = listView1.Groups[0];    //将 ListView 控件的第一项分配到第一个组中

【例12.11】 创建一个Windows应用程序,将ListView控件的View属性设置为SmallIcon。使用Groups集合的Add方法创建两个分组,标题分别为“名称”和“年龄”,排列方式为左对齐。向ListView控件中添加6项,然后设置每项的Group属性,将控件中的项进行分组,代码如下。(实例位置:光盘\TM\sl\12\8)

    private void Form1_Load(object sender, EventArgs e)
    {
         //设置 listView1 控件的 View 属性,设置样式
         listView1.View = View.SmallIcon;
         //为 listView1 建立两个组
         listView1.Groups.Add(new ListViewGroup("名称",HorizontalAlignment.Left));
         listView1.Groups.Add(new ListViewGroup("年龄", HorizontalAlignment.Left));
         //向控件中添加项目
         listView1.Items.Add("用一生下载你");
         listView1.Items.Add("芸烨湘枫");
         listView1.Items.Add("一生所爱");
         listView1.Items.Add("28");
         listView1.Items.Add("27");
         listView1.Items.Add("26");
         //将 listView1 控件中索引是 0、1 和 2 的项添加到第一个分组
         listView1.Items[0].Group = listView1.Groups[0];
         listView1.Items[1].Group = listView1.Groups[0];
         listView1.Items[2].Group = listView1.Groups[0];
         //将 listView1 控件中索引是 3、4 和 5 的项添加到第二个分组
         listView1.Items[3].Group = listView1.Groups[1];
         listView1.Items[4].Group = listView1.Groups[1];
         listView1.Items[5].Group = listView1.Groups[1];
    }

程序的运行结果如图12.11所示。

图12.11 为ListView控件中的项分组

说明

如果想要临时禁用分组功能,可将ShowGroups属性设置为false。

12.3 TreeView控件(树控件)

视频讲解:光盘\TM\lx\12\TreeView控件的使用.exe

图12.12 TreeView控件

TreeView控件(树控件)可以为用户显示节点层次结构,每个节点又可以包含子节点,包含子节点的节点叫父节点。就像在Windows操作系统的Windows资源管理器功能的左窗口中显示文件和文件夹一样。如图12.12所示为TreeView控件。

12.3.1 添加和删除树节点

1.添加节点

使用TreeView控件Nodes属性的Add方法,可以向控件中添加节点。

语法如下:

    public virtual intAdd(TreeNode node)

 node:要添加到集合中的TreeNode。

 返回值:添加到树节点集合中的TreeNode的从零开始的索引值。

【例12.12】 创建一个Windows应用程序,使用TreeView控件Nodes属性的Add方法向控件中添加3个父节点,然后再使用Add方法分别向3个父节点中添加3个子节点,代码如下。(实例位置:光盘\TM\sl\12\9)

    private void Form1_Load(object sender, EventArgs e)
    {
         //为控件建立 3 个父节点
         TreeNode tn1 = treeView1.Nodes.Add("名称");
         TreeNode tn2 = treeView1.Nodes.Add("性别");
         TreeNode tn3 = treeView1.Nodes.Add("年龄");
         //建立 3 个子节点
         TreeNode Ntn1 = new TreeNode("用一生下载你");
         TreeNode Ntn2 = new TreeNode("芸烨湘枫");
         TreeNode Ntn3 = new TreeNode("一生所爱");
         //将以上的 3 个子节点添加到第 1 个父节点中
         tn1.Nodes.Add(Ntn1);
         tn1.Nodes.Add(Ntn2);
         tn1.Nodes.Add(Ntn3);
         //然后再建立 3 个子节点,用于显示性别
         TreeNode Stn1 = new TreeNode("男");
         TreeNode Stn2 = new TreeNode("女");
         TreeNode Stn3 = new TreeNode("男");
         //将这 3 个显示性别的子节点添加到第 2 个父节点中
         tn2.Nodes.Add(Stn1);
         tn2.Nodes.Add(Stn2);
         tn2.Nodes.Add(Stn3);
         //接续建立 3 个子节点用于显示年龄
         TreeNode Atn1 = new TreeNode("28");
         TreeNode Atn2 = new TreeNode("27");
         TreeNode Atn3 = new TreeNode("26");
         //将显示年龄的 3 个子节点添加到第 3 个父节点中
         tn3.Nodes.Add(Atn1);
         tn3.Nodes.Add(Atn2);
         tn3.Nodes.Add(Atn3);
    }

程序的运行结果如图12.13所示。

2.移除节点

使用TreeView控件Nodes属性的Remove方法可以从树节点集合中移除指定的树节点。

语法如下:

    public void Remove(TreeNode node)

node:要移除的TreeNode。

【例12.13】 创建一个Windows应用程序,通过TreeView控件Nodes属性的Remove方法删除选中的子节点,代码如下。(实例位置:光盘\TM\sl\12\10)

    private void Form1_Load(object sender, EventArgs e)
    {
         //建立一个父节点
         TreeNode tn1 = treeView1.Nodes.Add("名称");
         //建立 3 个子节点
         TreeNode Ntn1 = new TreeNode("用一生下载你");
         TreeNode Ntn2 = new TreeNode("芸烨湘枫");
         TreeNode Ntn3 = new TreeNode("一生所爱");
         //将这 3 个子节点添加到父节点中
         tn1.Nodes.Add(Ntn1);
         tn1.Nodes.Add(Ntn2);
         tn1.Nodes.Add(Ntn3);
    }
    private void button1_Click(object sender, EventArgs e)
    {
         //如果用户选择了“名称”证明没有选择要删除的子节点
         if (treeView1.SelectedNode.Text == "名称")
         {
               MessageBox.Show("请选择要删除的子节点");        //弹出提示
         }
         else                                                  //否则
         {
               treeView1.Nodes.Remove(treeView1.SelectedNode); //使用 Remove 方法移除选择项
         }
    }

程序的运行结果如图12.14所示。

图12.13 添加节点

图12.14 删除子节点

12.3.2 获取树控件中选中的节点

在控件的AfterSelect事件中,可以使用EventArgs对象返回对已单击节点对象的引用。通过检查TreeViewEventArgs类(它包含与事件有关的数据),确定单击了哪个节点。下面通过实例演示如何在AfterSelect事件中获取控件中选中节点显示的文本。

说明

在BeforeCheck(在选中树节点复选框前发生)或AfterCheck(在选中树节点复选框后发生)事件中尽可能不要使用TreeNode.Checked属性。

【例12.14】 创建一个Windows应用程序,在控件的AfterSelect事件中获取控件选中节点显示的文本,代码如下。(实例位置:光盘\TM\sl\12\11)

    private void Form1_Load(object sender, EventArgs e)
    {
         //建立一个父节点
         TreeNode tn1 = treeView1.Nodes.Add("名称");
         //建立 3 个子节点
         TreeNode Ntn1 = new TreeNode("用一生下载你");
         TreeNode Ntn2 = new TreeNode("芸烨湘枫");
         TreeNode Ntn3 = new TreeNode("一生所爱");
         //将 3 个子节点添加到父节点中
         tn1.Nodes.Add(Ntn1);
         tn1.Nodes.Add(Ntn2);
         tn1.Nodes.Add(Ntn3);
    }
    private void treeView1_AfterSelect(object sender, TreeViewEventArgs e)
    {
         //在 AfterSelect 事件中获取控件选中节点显示的文本
         label1.Text = "当前选中的节点:" + e.Node.Text;
    }

程序的运行结果如图12.15所示。

图12.15 获取选中的节点

12.3.3 为树控件中的节点设置图标

TreeView控件可在每个节点旁显示图标。图标紧挨着节点文本的左侧。若要显示这些图标,必须使树视图与ImageList控件相关联。为TreeView控件中的节点设置图标的步骤如下。

(1)设置TreeView控件的ImageList属性为想要使用的现有ImageList控件。这些属性可在设计器中使用“属性”窗口进行设置,也可在代码中设置。

【例12.15】 设置控件的ImageList属性为imageList1,代码如下。

    treeView1.ImageList = imageList1;

(2)设置节点的ImageIndex和SelectedImageIndex属性,ImageIndex属性确定正常和展开状态下的节点显示的图像,SelectedImageIndex属性确定选定状态下的节点显示的图像。

【例12.16】 设置控件的ImageIndex属性,确定正常或展开状态下的节点显示的图像的索引为0,设置SelectedImageIndex属性,确定选定状态下的节点显示的图像的索引为1,代码如下。

    treeView1.ImageIndex = 0;
    treeView1.SelectedImageIndex = 1;

【例12.17】 创建一个Windows应用程序,向控件中添加一个父节点和3个子节点。设置TreeView控件的ImageList属性为imageList1,通过设置控件的ImageIndex属性实现正常状况下节点显示的图像的索引为0,然后设置控件的SelectedImageIndex属性,实现选中某个节点后显示的图像的索引为1,代码如下。(实例位置:光盘\TM\sl\12\12)

    private void Form1_Load(object sender, EventArgs e)
    {
         //建立一个父节点
         TreeNode tn1 = treeView1.Nodes.Add("组织结构");
         //建立 3 个子节点
         TreeNode Ntn1 = new TreeNode("C#部门");
         TreeNode Ntn2 = new TreeNode("ASP.NET 部门");
         TreeNode Ntn3 = new TreeNode("VB 部门");
         //将 3 个子节点添加到父节点中
         tn1.Nodes.Add(Ntn1);
         tn1.Nodes.Add(Ntn2);
         tn1.Nodes.Add(Ntn3);
         //设置 imageList1 控件中显示的图像
         imageList1.Images.Add(Image.FromFile("1.png"));
         imageList1.Images.Add(Image.FromFile("2.png"));
         //设置 treeView1 的 ImageList 属性为 imageList1
         treeView1.ImageList = imageList1;
         imageList1.ImageSize = new Size(16,16);
         //设置 treeView1 控件节点的图标在 imageList1 控件中的索引为 0
         treeView1.ImageIndex = 0;
         //选择某个节点后显示的图标在 imageList1 控件中的索引为 1
         treeView1.SelectedImageIndex = 1;
    }

程序的运行结果如图12.16和图12.17所示。

图12.16 运行程序

图12.17 选中节点

说明

在用TreeView控件对各磁盘的文件进行显示时,可以通过该控件的SelectedNode的FullPath属性获取当前所选择节点(文件或文件夹)的目录,需要将该控件的PathSeparator属性值设为“\”。

12.4 DateTimePicker控件(日期控件)

视频讲解:光盘\TM\lx\12\DateTimePicker控件的使用.exe

图12.18 DateTimePicker控件

DateTimePicker控件(日期控件)用于选择日期和时间,DateTimePicker控件只能选择一个时间,而不是连续的时间段,也可以直接输入日期和时间。如图12.18所示为DateTimePicker控件。

12.4.1 使用DateTimePicker控件显示时间

通过将控件的Format属性设置为Time,实现控件只显示时间。Format属性用于获取或设置控件中显示的日期和时间格式。

语法如下:

    public DateTimePickerFormat Format { get; set; }

属性值:DateTimePickerFormat值之一,默认为Long。

DateTimePickerFormat枚举的值及说明如表12.2所示。

表12.2 DateTimePickerFormat枚举的值及说明

【例12.18】 创建一个Windows应用程序,首先将控件的Format属性设置为Time,实现控件只显示时间。然后获取控件中显示的数据,并显示到TextBox控件中,代码如下。(实例位置:光盘\TM\sl\12\13)

    private void Form1_Load(object sender, EventArgs e)
    {
         //设置 dateTimePicker1 的 Format 属性为 Time,使其只显示时间
         dateTimePicker1.Format = DateTimePickerFormat.Time;
         textBox1.Text = dateTimePicker1.Text;          //使用文本框获取控件显示的时间
    }

程序的运行结果如图12.19所示。

图12.19 控件只显示时间

说明

如果想要在该控件内用按钮调整时间值,则需要将ShowUpDown属性设置为true。

12.4.2 使用DateTimePicker控件以自定义格式显示日期

通过DateTimePicker控件的CustomFormat属性可以自定义日期/时间格式字符串。

语法如下:

    public string CustomFormat { get; set; }

属性值:表示自定义日期/时间格式的字符串。

注意

Format属性必须设置为DateTimePickerFormat.Custom,才能影响显示的日期和时间的格式设置。

通过组合格式字符串,可以设置日期和时间格式,所有的有效格式字符串及其说明如表12.3所示。

表12.3 有效格式字符串及其说明

【例12.19】 创建一个Windows应用程序,首先将控件的Format属性必须设置为DateTimePickerFormat.Custom,使用户自定义的时间格式生效。然后将控件的CustomFormat属性设置为自定义的格式,代码如下。(实例位置:光盘\TM\sl\12\14)

    private void Form1_Load(object sender, EventArgs e)
    {
         //设置 dateTimePicker1 的 Format 属性为 Custom,使其用户自定义的时间格式生效
         dateTimePicker1.Format = DateTimePickerFormat.Custom;
         //通过控件的 CustomFormat 属性设置自定义的格式
         dateTimePicker1.CustomFormat = "MMMM dd, yyyy - dddd";
         label1.Text = dateTimePicker1.Text;         //显示当前控件显示的自定义格式的日期
    }

程序的运行结果如图12.20所示。

图12.20 自定义时间格式

12.4.3 返回DateTimePicker控件中选择的日期

调用控件的Text属性以返回与控件中的格式相同的完整值,或调用Value属性的适当方法来返回部分值,这些方法包括Year、Month和Day方法等,使用ToString将信息转换成可显示给用户的字符串。

【例12.20】 创建一个Windows应用程序,首先使用控件的Text属性获取当前控件选择的日期,然后使用Value属性的Year、Month和Day方法获取选择日期的年、月和日,代码如下。(实例位置:光盘\TM\sl\12\15)

    private void Form1_Load(object sender, EventArgs e)
    {
         //使用控件的 Text 属性获取当前控件选择的日期
         textBox1.Text = dateTimePicker1.Text;
         //使用 Value 属性的 Year 方法获取选择日期的年
         textBox2.Text = dateTimePicker1.Value.Year.ToString();
         //使用 Value 属性的 Year 方法获取选择日期的月
         textBox3.Text = dateTimePicker1.Value.Month.ToString();
         //使用 Value 属性的 Year 方法获取选择日期的日
         textBox4.Text = dateTimePicker1.Value.Day.ToString();
    }

程序的运行结果如图12.21所示。

图12.21 获取控件中选择的日期

说明

如果想要直接获取当前系统的日期和时间,可以使用Value属性下的ToShortDateString和ToShortTimeString方法。

12.5 MonthCalendar控件(月历控件)

视频讲解:光盘\TM\lx\12\MonthCalendar控件的使用.exe

MonthCalendar控件(月历控件)提供了一个直观的图形界面,可以让用户查看和设置日期。MonthCalendar控件中可以使用鼠标进行拖曳,用于选择一段连续的时间,此段连续的时间包括时间的起始和结束。如图12.22所示为MonthCalendar控件。

图12.22 MonthCalendar控件

12.5.1 更改MonthCalendar控件的外观

MonthCalendar控件允许用多种方法自定义月历的外观。例如,可以设置配色方案并选择显示或隐藏周数和当前日期。

1.更改月历的配色方案

设置TitleBackColor、TitleForeColor和TrailingForeColor等属性可以更改月历控件的配色方案。TitleBackColor属性用于设置日历标题区的背景色,TitleForeColor属性用于设置日历标题区的前景色,TrailingForeColor属性用于设置没有完全显示的日期的颜色。

【例12.21】 创建一个Windows应用程序,将控件的标题背景设置为蓝色、控件标题上的文字设置为黄色,以及将控件中不属于当前月份的其他日期的颜色设置为红色,代码如下。(实例位置:光盘\TM\sl\12\16)

    private void Form1_Load(object sender, EventArgs e)
    {
         //设置控件的 TitleBackColor 属性,使控件的标题背景为蓝色
         monthCalendar1.TitleBackColor = System.Drawing.Color.Blue;
         //设置控件的 TrailingForeColor 属性,使控件其他日期的颜色为红色
         monthCalendar1.TrailingForeColor = System.Drawing.Color.Red;
         //设置控件的 TitleForeColor 属性,使标题上的文字为黄色
         monthCalendar1.TitleForeColor = System.Drawing.Color.Yellow;
    }

程序的运行结果如图12.23所示。

说明

可以使用该控件的ShowTodayCircle和ShowToday属性来判断是否在该控件上用正方形标识当天的日期,或是在控件底端是否显示当前日期。

2.显示周数

将ShowWeekNumbers属性设置为true,实现在控件中显示周数。也可以用代码或在“属性”窗口中设置此属性。周数以单独的列出现在一周的第一天的左边。

【例12.22】 创建一个Windows应用程序,将控件的ShowWeekNumbers属性设置为true,在控件中显示周数,代码如下。(实例位置:光盘\TM\sl\12\17)

    private void Form1_Load(object sender, EventArgs e)
    {
         monthCalendar1.ShowWeekNumbers = true;//ShowWeekNumbers 属性设置为 true 显示周数
    }

程序的运行结果如图12.24所示。

图12.23 更改控件的配色方案

图12.24 显示周数

12.5.2 在MonthCalendar控件中显示多个月份

MonthCalendar控件最多可同时显示12个月。默认情况下,控件只显示1个月,但可以通过设置CalendarDimensions属性指定显示多少个月以及它们在控件中的排列方式。当更改月历尺寸时,控件的大小也会随之改变,因此应确保窗体上有足够的空间供新尺寸使用。

【例12.23】 创建一个Windows应用程序,设置控件的CalendarDimensions属性,使控件在水平和垂直方向都显示2个月份,代码如下。(实例位置:光盘\TM\sl\12\18)

    private void Form1_Load(object sender, EventArgs e)
    {
         //设置控件的 CalendarDimensions 属性,使控件在水平和垂直方向都显示 2 个月份
         monthCalendar1.CalendarDimensions = new Size(2, 2);
    }

程序的运行结果如图12.25所示。

图12.25 控件中显示多个月份

说明

CalendarDimensions属性一次只显示一个日历年,并且最多可显示12个月。行和列的有效组合得到的最大乘积为12,对于大于12的值,将在最适合的基础上修改显示。

12.5.3 在MonthCalendar控件中以粗体显示特定日期

MonthCalendar控件能以粗体显示特殊的日期或重复出现的日子,这样做可以引起对特殊日期的注意。可以使用AddBoldedDate方法在月历中添加以粗体显示的日期,并调用UpdateBoldedDates方法重绘粗体格式的日期以反映在粗体格式日期的列表中设置的日期。

【例12.24】 创建一个Windows应用程序,首先创建一个DateTime对象,在这个对象中指定需要以粗体显示的日期。然后使用AddBoldedDate方法在月历中添加以粗体显示的日期,最后调用UpdateBoldedDates方法重绘粗体格式的日期,代码如下。(实例位置:光盘\TM\sl\12\19)

    private void Form1_Load(object sender, EventArgs e)
    {
         //实例化 DateTime 类,使其值为 2008 年 3 月 2 日
         DateTime myVacation1 = new DateTime(2008, 3, 2);
         //使用 AddBoldedDate 方法在月历中将 2008 年 3 月 2 日以粗体显示
         monthCalendar1.AddBoldedDate(myVacation1);
         //调用 UpdateBoldedDates 方法重绘粗体格式的日期
         monthCalendar1.UpdateBoldedDates();
    }

程序的运行结果如图12.26所示。

图12.26 粗体显示特定日期

12.5.4 在MonthCalendar控件中选择日期范围

如果要在MonthCalendar控件中选择日期范围,必须设置SelectionStart和SelectionEnd属性。这两个属性分别用于设置日期的起始和结束。

【例12.25】 创建一个Windows应用程序,在控件的DateChanged事件中获取SelectionStart和SelectionEnd属性的值,当控件中选择的日期发生更改时引发DateChanged事件。运行程序,选择某个日期作为起始日期,然后按住Shift键后,再选择结束日期,代码如下。(实例位置:光盘\TM\sl\12\20)

   private void Form1_Load(object sender, EventArgs e)
   {
        //获取控件当前的日期和时间
        textBox1.Text = monthCalendar1.TodayDate.ToString();
   }
   private void monthCalendar1_DateChanged(object sender, DateRangeEventArgs e)
   {
        //通过 SelectionStart 属性获取用户选择的起始日期
        textBox2.Text = monthCalendar1.SelectionStart.ToString();
        //通过 SelectionEnd 属性获取用户选择的结束日期
        textBox3.Text = monthCalendar1.SelectionEnd.ToString();
   }

程序的运行结果如图12.27所示。

图12.27 设置控件中选择日期的范围

12.6 其他高级控件

视频讲解:光盘\TM\lx\12\其他高级控件的使用.exe

除了上述的常用高级控件外,窗体中还包含其他高级控件,包括ErrorProvider控件、HelpProvider控件、Timer控件和ProgressBar控件等。下面详细介绍这几种控件的一些常见用法。

12.6.1 使用ErrorProvider控件验证文本框输入

图12.28 ErrorProvider控件

ErrorProvider控件可以在不打扰用户的情况下向用户显示有错误发生。当验证用户在窗体中的输入或显示数据集内的错误时,一般要用到该控件。如图12.28所示为ErrorProvider控件。

ErrorProvider控件通过SetError方法设置指定控件的错误描述字符串。

语法如下:

    public void SetError(Control control,string value)

 control:要为其设置错误描述字符串的控件。

 value:错误描述字符串。

判断文本框中输入的数据是否准确,需要在控件的Validating事件中进行判断,然后设置ErrorProvider控件的错误描述字符串,当控件正在验证时会引发此事件。

说明

如果将BlinkRate属性设置为零,BlinkStyle属性将自动被设置为NeverBlink。

【例12.26】 创建一个Windows应用程序,在窗体中添加3个TextBox控件,在每个控件的Validating事件中判断是否输入了数据。如果输入的数据为空,则调用ErrorProvider控件的SetError方法设置错误描述字符串。当输入订货数量的文本框中没有输入数字时,会显示错误字符串“请输入一个数字”,并在此文本框的后面显示错误图标。如果输入数字,则错误图标消失,代码如下。(实例位置:光盘\TM\sl\12\21)

程序的运行结果如图12.29和图12.30所示。

图12.29 在订货数量文本框中输入字符

图12.30 输入正确的数据

12.6.2 使用HelpProvider控件调用帮助文件

HelpProvider控件可以将帮助文件(.htm文件或.chm文件)与Windows应用程序相关联,为特定对话框或对话框中的特定控件提供区分上下文的帮助,打开帮助文件到特定部分。如目录、索引或搜索功能的主页,如图12.31所示为HelpProvider控件。

通过设置控件的HelpNamespace属性以及SetShowHelp方法,实现当按F1键时打开指定的帮助文件功能。

HelpNamespace属性可以设置一个值,该值指定与HelpProvider对象关联的帮助文件名。

语法如下:

    public virtual string HelpNamespace { get; set; }

属性值:帮助文件的名称。

SetShowHelp方法用于指定是否显示指定控件的帮助信息。

语法如下:

    public virtual void SetShowHelp(Control ctl,bool value)

 ctl:控制其帮助信息已打开或关闭。

 value:如果显示控件的帮助信息,则为true;否则为false。

说明

如果没有对HelpNamespace属性进行设置,则必须使用SetHelpString方法提供帮助文本。

【例12.27】 创建一个Windows应用程序,首先在程序的根目录中建立一个命名为helpPage.htm的帮助文件,然后设置HelpNamespace属性是helpPage.htm文件的路径,最后设置控件的SetShowHelp方法指定是否显示指定控件的帮助信息,代码如下。(实例位置:光盘\TM\sl\12\22)

    private void Form1_Load(object sender, EventArgs e)
    {
         //设置帮助文件的位置
         string strPath = Application.StartupPath.Substring(0, Application.StartupPath.Substring(0,Application.
         StartupPath. LastIndexOf("\\")).LastIndexOf("\\"));
         strPath += @"\helpPage.htm";
         //设置 helpProvider1 控件的 pNamespace 属性,设置帮助文件的路径
         helpProvider1.HelpNamespace = strPath;
         //设置 SetShowHelp 方法指定是否显示指定控件的帮助信息
         helpProvider1.SetShowHelp(this,true);
    }

程序的运行结果如图12.32所示。

图12.31 HelpProvider控件

图12.32 按F1键打开帮助文件

12.6.3 使用Timer控件设置时间间隔

Timer控件可以定期引发事件,此控件是为Windows窗体环境设计的。时间间隔的长度由Interval属性定义,其值以毫秒为单位。若启用了该组件,则每个时间间隔引发一个Tick事件,在Tick事件中添加要执行的代码。如图12.33所示为Timer控件。

Interval属性用于设置计时器开始计时的时间间隔。

语法如下:

    public int Interval { get; set; }

属性值:计时器每次开始计时之间的毫秒数,该值不小于1。

当指定的计时器间隔已过去而且计时器处于启用状态时会引发控件的Tick事件。Enabled属性用于设置是否启用计时器。

语法如下:

    public virtual bool Enabled { get; set; }

属性值:如果计时器当前处于启用状态,则为true;否则为false。默认为false。

【例12.28】 创建一个Windows应用程序,窗体加载时,设置Timer控件的Interval属性为1000毫秒(1秒),使计时器的时间间隔为1秒。然后在Timer控件的Tick事件中,使文本框中显示当前的系统时间。在按钮的Click事件中设置Enabled属性,以启用或停止计时器,代码如下。(实例位置:光盘\TM\sl\12\23)

程序的运行结果如图12.34所示。

说明

在启动和停止计时器时,也可以应用Start和Stop方法来实现。

图12.33 Timer控件

图12.34 制作系统时钟

12.6.4 使用ProgressBar控件显示程序运行进度条

图12.35 ProgressBar控件

ProgressBar控件通过水平放置的方框中显示适当数目的矩形,指示工作的进度。工作完成时,进度条被填满。进度条用于帮助用户了解等待一项工作完成的进度。如图12.35所示为ProgressBar控件。

ProgressBar控件比较重要的属性有Value、Minimum和Maximum。Minimum和Maximum属性主要用于设置进度条的最小值和最大值,Value属性表示操作过程中已完成的进度。而控件的Step属性用于指定Value属性递增的值,然后调用PerformStep方法来递增该值。

注意

ProgressBar控件只能以水平方向显示,如果想改变该控件的显示样式,可以用ProgressBarRenderer类来实现,如纵向进度条,或在进度条上显示文本。

【例12.29】 创建一个Windows应用程序,首先设置控件的Minimum和Maximum属性分别为0和500,确定进度条的最小值和最大值。然后设置Step属性,使Value属性递增值为1。最后在for语句中调用PerformStep方法递增该值,使进度条不断前进,直至for语句中设置为最大值为止,代码如下。(实例位置:光盘\TM\sl\12\24)

程序的运行结果如图12.36所示。

图12.36 显示进度条

12.7 小结

本章重点讲解了Windows应用程序的一些高级控件,高级控件主要包括ImageList控件、ListView控件、TreeView控件、DateTimePicker控件、MonthCalendar控件、ErrorProvider控件、HelpProvider控件、Timer控件和ProgressBar控件。在讲解的过程中,列举了这些控件的一些常用设置,使读者能够快速地掌握控件的一些常见用法。这些高级控件在开发应用程序过程中会经常用到,希望读者能够认真领会。

12.8 实践与练习

(1)尝试开发一个程序,用来直接将文本框中输入的“书名目录”添加到图书目录列表中,并且如果有重复,可以弹出提示信息(提示:可以使用ListView控件实现)。(答案位置:光盘\TM\sl\12\25)

(2)尝试开发一个程序,要求使用TreeView控件制作一个类似Windows资源管理器的目录结构,并能在该目录结构中选择目录,显示在ListView控件中。(答案位置:光盘\TM\sl\12\26)

(3)尝试开发一个程序,要求显示操作文件的进度。(答案位置:光盘\TM\sl\12\27)

第13章 数据访问技术

第13章
数据访问技术

视频讲解:84分钟

数据库是一门复杂的技术,其在当前的软件开发中得到了广泛的应用。数据库的出现为数据存储技术带来了新的方向,也产生了一门复杂的学问。为了使客户端能够访问服务器中的数据库,可以使用各种数据访问方法或技术,ADO.NET就是这样一种技术。本章详细介绍了数据访问技术,讲解过程中为了便于读者理解结合了大量的实例。

通过阅读本章,您可以:

 了解如何创建和删除数据库

 了解如何创建和删除数据表

 了解简单的SQL语句应用

 掌握连接数据库对象(Connection)

 掌握执行SQL语句对象(Command)

 掌握读取数据对象(DataReader)

 掌握数据适配器对象(DataAdapter)

 掌握数据集对象(DataSet)

13.1 数据库基础

视频讲解:光盘\TM\lx\13\数据库基础.exe

13.1.1 数据库简介

数据库是按照数据结构来组织、存储和管理数据的仓库,是存储在一起的相关数据的集合。使用数据库可以减少数据的冗余度,节省数据的存储空间。其具有较高的数据独立性和易扩充性,实现了数据资源的充分共享。计算机系统中只能存储二进制的数据,而数据存在的形式却是多种多样的。数据库可以将多样化的数据转换成二进制的形式,使其能够被计算机识别。同时,可以将存储在数据库中的二进制数据以合理的方式转化为人们可以识别的逻辑数据。

随着数据库技术的发展,为了进一步提高数据库存储数据的高效性和安全性,随即产生了关系型数据库。关系型数据库是由许多数据表组成的,数据表又是由许多条记录组成的,而记录又是由许多的字段组成的,每个字段对应一个对象。根据实际的要求,设置字段的长度、数据类型、是否必须存储数据。

数据库的种类有很多,常见的分类有以下几种:

 按照是否支持联网分为单机版数据库和网络版数据库。

 按照存储的容量分为小型数据库、中型数据库、大型数据库和海量数据库。

 按照是否支持关系分为非关系型数据库和关系型数据库。

13.1.2 SQL语言简介

SQL是一种数据库查询和程序设计语言,用于存取数据以及查询、更新和管理关系型数据库系统。SQL的含义是“结构化查询语言(Structured Query Language)”。目前,SQL语言有两个不同的标准,分别是美国国家标准学会(ANSI)和国际标准化组织(ISO)。SQL是一种计算机语言,可以用它与数据库交互。SQL本身不是一个数据库管理系统,也不是一个独立的产品。但SQL是数据库管理系统不可缺少的组成部分,它是与DBMS通信的一种语言和工具。由于它功能丰富,语言简洁,使用方法灵活,所以备受用户和计算机业界的青睐,被众多计算机公司和软件公司采用。经过多年的发展,SQL语言已成为关系型数据库的标准语言。

说明

在编写SQL语句时,要注意SQL语句中各关键字要以空格来分隔。

13.1.3 数据库的创建及删除

数据库主要用于存储数据及数据库对象(如表、索引)。下面以Microsoft SQL Server 2012为例,介绍如何通过管理器来创建和删除数据库。

1.创建数据库

(1)在Windows 8操作系统的开始界面中找到SQL Server 2012的SQL Server Management Studio,单击打开如图13.1所示的“连接到服务器”对话框,在该对话框中选择登录的服务器名称和身份验证方式,然后输入登录用户名和登录密码。

图13.1 “连接到服务器”对话框

(2)单击“连接”按钮,连接到指定的SQL Server 2012服务器,然后展开服务器节点,选中“数据库”节点,单击鼠标右键,在弹出的快捷菜单中选择“新建数据库”命令,如图13.2所示。

图13.2 选择“新建数据库”命令

说明

在创建数据库之前,首先要在SQL Server 2012中打开数据库的连接。

(3)打开如图13.3所示的“新建数据库”对话框,在该对话框中输入新建数据库的名称,选择数据库所有者和存放路径,这里的数据库所有者一般为默认。

图13.3 “新建数据库”对话框

(4)单击“确定”按钮,即可新建一个数据库,如图13.4所示。

图13.4 新建的数据库

2.删除数据库

删除数据库的方法很简单,只需在要删除的数据库上单击鼠标右键,在弹出的快捷菜单中选择“删除”命令即可,如图13.5所示。

说明

如果数据库以后还要被使用,可以将数据库进行分离,在数据库上单击鼠标右键,在弹出的快捷菜单中选择“任务”/“分离”命令。

图13.5 删除数据库

13.1.4 数据表的创建及删除

数据库创建完毕,接下来要在数据库中创建数据表。下面还是以上述的数据库为例,介绍如何在数据库中创建和删除数据表。

1.创建数据表

(1)单击数据库名左侧的“+”,打开该数据库的子项目,在子项目中的“表”项上单击鼠标右键,在弹出的快捷菜单中选择“新建表”命令,如图13.6所示。

图13.6 选择“新建表”命令

(2)在SQL Server 2012管理器的右边显示一个新表,这里输入要创建的表中所需要的字段,并设置主键,如图13.7所示。

(3)单击“保存”按钮,弹出“选择名称”对话框,如图13.8所示,输入要新建的数据表的名称,单击“确定”按钮,即可在数据库中添加一个数据表。

图13.7 添加字段

说明

在创建表结构时,有些字段可能需要设置初始值(如int型字段),可以在默认值文本框中输入相应的值。

2.删除数据表

如果要删除数据库中的某个数据表,只需右击数据表,在弹出的快捷菜单中选择“删除”命令即可,如图13.9所示。

图13.8 “选择名称”对话框

图13.9 删除数据表

13.1.5 简单SQL语句的应用

通过SQL语句,可以实现对数据库进行查询、插入、更新和删除操作。使用的SQL语句分别是Select语句、Insert语句、Update语句和Delete语句,下面简单介绍这几种语句。

1.查询数据

通常使用Select语句查询数据,Select语句是从数据库中检索数据并查询,并将查询结果以表格的形式返回。

语法如下:

    SELECT select_list
    [ INTO new_table ]
    FROM table_source
    [ WHERE search_condition ]
    [ GROUP BY group_by_expression ]
    [ HAVING search_condition ]
    [ ORDER BY order_expression [ASC| DESC ]]

语法中的参数说明如表13.1所示。

表13.1 Select语句参数说明

为使读者更好地了解Select语句的用法,下面举例说明如何使用Select语句。

【例13.1】 数据库db_CSharp的数据表tb_test中存储了一些商品的信息,使用Select语句查询数据表tb_test中商品的新旧程度为“二手”的数据,代码如下。

    select * from tb_test where 新旧程度='二手'

查询结果如图13.10所示。

图13.10 Select语句查询数据

说明

如果想要在数据库中查找空值,那么其条件必须为where字段名=' ' or字段名=null。

2.添加数据

在SQL语句中,使用Insert语句向数据表中添加数据。

语法如下:

    INSERT[INTO]
      {table_name WITH(<table_hint_limited>[…n])
    |view_name
    |rowset_function_limited
    }
    {[(column_list)]
      {VALUES
       ({DEFAULT|NULL|expression}[,..n])
       |derived_table
       |execute_statement
      }
    }
    |DEFAULT VALUES

语法中的参数说明如表13.2所示。

表13.2 Insert语句参数说明

注意

用户在使用Insert语句添加数据时,必须注意以下几点。

① 插入项的顺序和数据类型必须与表或视图中列的顺序和数据类型相对应。

② 如果表中某列定义为不允许NULL,则插入数据时,该列必须存在合法值。

③ 如果某列是字符型或日期型数据类型,则插入的数据应该加上单引号。

【例13.2】 使用Insert语句,向数据表tb_test中添加一条新的商品信息,代码如下。

    insert into tb_test(商品名称,商品价格,商品类型,商品产地,新旧程度) values('洗衣机',890,'家电','进口','全新')

程序的运行结果如图13.11所示。

图13.11 Insert语句添加数据

3.更新数据

使用Update语句更新数据,可以修改一个列或者几个列中的值,但一次只能修改一个表。

语法如下:

    UPDATE
      { table_name WITH(<table_hint_limited>[,…n])
      |view_name
      |rowset_function_limited
      }
      SET
      {column_name={expression|DEFAULT|NULL}
      |@variable=expression
      |@variable=column=expression}[,…n]
      {{[FROM{<table_source>}[,…n]]
      [WHERE
        <search_condition>]}
      |
      [WHERE CURRENT OF
      {{[GLOBAL]cursor_name}|cursor_variable_name}
      ]}
      [OPTION(<query_hint>[,…n])]

语法中的参数说明如表13.3所示。

表13.3 Update语句参数说明

说明

UPDATE语句将记入日志。如果要替换或修改大块的text、ntext或image数据,请使用WRITETEXT或UPDATETEXT语句而不要使用UPDATE语句。

下面通过一个例子演示如何使用Update语句更新数据表中的数据。

【例13.3】 由于进口商品价格上调,所以洗衣机的价格随之上调,使用Update语句更新数据表tb_test中洗衣机的商品价格,代码如下。

    //Update 语句更新数据表 tb_test 中洗衣机的商品价格
    update tb_test set 商品价格=1500 where 商品名称='洗衣机'

程序的运行结果如图13.12所示。

图13.12 更新商品信息

4.删除数据

使用Delete语句删除数据,可以使用一个单一的Delete语句删除一行或多行。当表中没有行满足Where子句中指定的条件时,就没有行会被删除,也没有错误产生。

语法如下:

    DELETE
       [ FROM ]
           { table_name WITH ( < table_hint_limited > [ ,...n ] )
           | view_name
           | rowset_function_limited
           }
           [ FROM { < table_source > } [ ,...n ] ]
       [ WHERE
           { < search_condition >
           | { [ CURRENT OF
                    { { [ GLOBAL ] cursor_name }
                          | cursor_variable_name
                    }
                ] }
           }
    ]
    [ OPTION ( < query_hint > [ ,...n ] ) ]

语法中的参数说明如表13.4所示。

表13.4 Delete语句参数说明

【例13.4】 删除数据表tb_test中商品名称为“洗衣机”,并且商品产地是“进口”的商品信息,代码如下。

    delete from tb_test where 商品名称='洗衣机' and 商品产地='进口'

程序的运行结果如图13.13所示。

图13.13 Delete语句删除数据

13.2 ADO.NET简介

视频讲解:光盘\TM\lx\13\ADO.NET简介.exe

ADO.NET是一组向.NET程序员公开数据访问服务的类。ADO.NET为创建分布式数据共享应用程序提供了一组丰富的组件。它提供了一系列的方法,用于支持对Microsoft SQL Server和XML等数据源进行访问,还提供了通过OLEDB和XML公开的数据源提供一致访问的方法。数据客户端应用程序可以使用ADO.NET来连接到这些数据源,并查询、添加、删除和更新所包含的数据。

ADO.NET支持两种访问数据的模型:无连接模型和连接模型。无连接模型将数据下载到客户机上,并在客户机上将数据封装到内存中,然后可以像访问本地关系数据库一样访问内存中的数据(例如DataSet)。连接模型依赖于逐记录的访问,这种访问要求打开并保持与数据源的连接。

这里可以用趣味形象化的方式理解ADO.NET对象模型的各个部分,如图13.14所示,参照图13.14可以用对比的方法来形象地理解ADO.NET中每个对象的作用。

下面根据图13.14对ADO.NET对象模型之间的关系进行一下描述。

图13.14 趣味理解ADO.NET对象模型

(1)数据库好比水源,存储了大量的数据。

(2)Connection对象好比伸入水中的进水龙头,保持与水的接触,只有它与水进行了“连接”,其他对象才可以抽到水。

(3)Command对象则像抽水机,为抽水提供动力和执行方法,通过“水龙头”,然后把水返给上面的“水管”。

(4)DataAdapter、DataReader对象就像输水管,担任着水的传输任务,并起着桥梁的作用。DataAdapter对象像一根输水管,通过发动机,把水从水源输送到水库里进行保存。而DataReader对象也是一种水管,和DataAdapter对象不同的是,它不把水输送到水库里面,而是单向地直接把水送到需要水的用户那里或田地里,所以要比在水库中转一下速度更快。

(5)DataSet对象则是一个大水库,把抽上来的水按一定关系的池子进行存放。即使撤掉“抽水装置”(断开连接,离线状态),也可以保持“水”的存在。这也正是ADO.NET的核心。

(6)DataTable对象则像水库中的每个独立的水池子,分别存放不同种类的水。一个大水库由一个或多个这样的水池子组成。

13.3 连接数据库:Connection对象

视频讲解:光盘\TM\lx\13\Connection对象的使用.exe

13.3.1 Connection对象概述

Connection对象是一个连接对象,主要功能是建立与物理数据库的连接。其主要包括4种访问数据库的对象类,也可称为数据提供程序,分别介绍如下:

 SQL Server数据提供程序,位于System.Data.SqlClient命名空间。

 ODBC数据提供程序,位于System.Data.Odbc命名空间。

 OLEDB数据提供程序,位于System.Data.OleDb命名空间。

 Oracle数据提供程序,位于System.Data.OracleClient命名空间。

说明

根据使用数据库的不同,引入不同的命名空间,然后通过命名空间中的Connection对象连接类连接数据库。例如,连接SQL Server数据库,首先要通过using System.Data.SqlClient命令引用SQL Server数据提供程序,然后才能调用空间下的SqlConnection类连接数据库。

13.3.2 连接数据库

以SQL Server数据库为例,如果要连接SQL Server数据库,必须使用System.Data.SqlClient命名空间下的SqlConnection类。所以首先要通过using System.Data.SqlClient命令引用命名空间,连接数据库之后,通过调用SqlConnection对象的Open方法打开数据库。通过SqlConnection对象的State属性判断数据库的连接状态。

语法如下:

    public override ConnectionState State { get; }

属性值:ConnectionState枚举。

ConnectionState枚举的值及说明如表13.5所示。

表13.5 ConnectionState枚举的值及说明

【例13.5】 创建一个Windows应用程序,在窗体中添加一个TextBox控件、一个Button控件和一个Label控件,分别用于输入要连接的数据库名称、执行连接数据库的操作以及显示数据库的连接状态,然后引入System.Data.SqlClient命名空间,使用SqlConnection类连接数据库,代码如下。(实例位置:光盘\TM\sl\13\1)

程序的运行结果如图13.15所示。

图13.15 连接数据库

13.3.3 关闭连接

当对数据库操作完毕后,要关闭与数据库的连接,释放占用的资源。通过调用SqlConnection对象的Close方法或Dispose方法关闭与数据库的连接,这两种方法的主要区别是:Close方法用于关闭一个连接,而Dispose方法不仅关闭一个连接,而且还清理连接所占用的资源。当使用Close方法关闭连接后,可以再调用Open方法打开连接,不会产生任何错误。而如果使用Dispose方法关闭连接,就不可以再次直接用Open方法打开连接,必须再次重新初始化连接再打开。

【例13.6】 创建一个Windows应用程序,首先向窗体中添加一个TextBox控件和一个RichTextBox控件,分别用于输入连接的数据库名称和显示连接信息及错误提示。然后再添加3个Button控件,分别用于连接数据库、调用Close方法关闭连接,再调用Open方法打开连接以及调用Dispose方法关闭并释放连接,然后调用Open方法打开连接,代码如下。(实例位置:光盘\TM\sl\13\2)

程序的运行结果如图13.16和图13.17所示。

图13.16 调用Close方法关闭连接

图13.17 调用Dispose方法关闭并释放连接

说明

在编写应用程序时,对数据库操作完成后,要及时关闭数据库的连接,以防止在对数据库进行其他操作时,数据库被占用。

13.4 执行SQL语句:Command对象

视频讲解:光盘\TM\lx\13\Command对象的使用.exe

13.4.1 Command对象概述

Command对象是一个数据命令对象,主要功能是向数据库发送查询、更新、删除、修改操作的SQL语句。Command对象主要有以下几种方式。

 SqlCommand:用于向SQL Server数据库发送SQL语句,位于System.Data.SqlClient命名空间。

 OleDbCommand:用于向使用OLEDB公开的数据库发送SQL语句,位于System.Data.OleDb命名空间。例如,Access数据库和MySQL数据库都是OLEDB公开的数据库。

 OdbcCommand:用于向ODBC公开的数据库发送SQL语句,位于System.Data.Odbc命名空间。有些数据库没有提供相应的连接程序,则可以配置好ODBC连接后,使用OdbcCommand。

 OracleCommand:用于向Oracle数据库发送SQL语句,位于System.Data.OracleClient命名空间。

说明

在使用OracleCommand向Oracle数据库发送SQL语句时,要引入System.Data.OracleClient命名空间。但是默认情况下没有此命名空间,此时,需要将程序集System.Data.OracleClient引入到项目中。引入程序集的方法是在项目名称上单击鼠标右键,在弹出的快捷菜单中选择“添加引用”命令,打开“添加引用”对话框。在该对话框中选择System.Data.OracleClient程序集,单击“确定”按钮,即可将其添加到项目中。

13.4.2 设置数据源类型

Command对象有3个重要的属性,分别是Connection属性、CommandText属性和CommandType属性。Connection属性用于设置SqlCommand使用的SqlConnection。CommandText属性用于设置要对数据源执行的SQL语句或存储过程。CommandType属性用于设置指定CommandText的类型。CommandType属性的值是CommandType枚举值,CommandType枚举有3个枚举成员,分别介绍如下。

 StoredProcedure:存储过程的名称。

 TableDirect:表的名称。

 Text:SQL文本命令。

如果要设置数据源的类型,便可以通过设置CommandType属性来实现,下面通过实例演示如何使用Command对象的这3个属性,以及如何设置数据源类型。

【例13.7】 创建一个Windows应用程序,向窗体中添加一个Button控件、一个TextBox控件和一个Label控件,分别用于执行SQL语句、输入要查询的数据表名称以及显示数据表中数据的数量,在Button控件的Click事件中设置Command对象的Connection属性、CommandText属性和CommandType属性,代码如下。(实例位置:光盘\TM\sl\13\3)

程序的运行结果如图13.18所示。

图13.18 SqlCommand对象执行查询语句

13.4.3 执行SQL语句

Command对象需要取得将要执行的SQL语句,通过调用该类提供的多种方法,向数据库提交SQL语句。下面详细介绍SqlCommand对象中的几种执行SQL语句的方法。

1.ExecuteNonQuery方法

执行SQL语句,并返回受影响的行数,在使用SqlCommand向数据库发送增、删、改命令时,通常使用ExecuteNonQuery方法执行发送的SQL语句。

语法如下:

    public override int ExecuteNonQuery()

返回值:受影响的行数。

【例13.8】 创建一个Windows应用程序,在三八妇女节那天,公司决定为每位女员工颁发奖金50元。这样,就需要向数据发送更新命令,将数据库中所有女员工的奖金数额加上50,所以要使用ExecuteNonQuery方法执行发送的SQL语句,并获取受影响的行数,代码如下。(实例位置:光盘\TM\sl\13\4)

    SqlConnection conn;                         //声明一个 SqlConnection 变量
    private void button1_Click(object sender, EventArgs e)
    {
         //实例化 SqlConnection 变量 conn
         conn = new SqlConnection("server=.;database=db_CSharp;uid=sa;pwd=");
         conn.Open();                           //打开连接
    SqlCommand cmd = new SqlCommand();          //创建一个 SqlCommand 对象
    //设置 Connection 属性,指定其使用 conn 连接数据库
    cmd.Connection = conn;
    //设置 CommandText 属性,设置其执行的 SQL 语句
    cmd.CommandText = "update tb_command set 奖金=50 where 性别='女'";
    //设置 CommandType 属性为 Text,使其只执行 SQL 语句文本形式
    cmd.CommandType = CommandType.Text;
    //使用 ExecuteNonQuery 方法执行 SQL 语句
         int i = Convert.ToInt32(cmd.ExecuteNonQuery());
         label2.Text = "共有" + i.ToString() + "名女员工获得奖金";
    }

程序的运行结果如图13.19所示。

图13.19 对数据表执行更新操作

说明

如果想要执行存储过程,应将CommandType属性设置为StoredProcedure,将CommandText属性设置为存储过程的名称。

2.ExecuteReader方法

执行SQL语句,并生成一个包含数据的SqlDataReader对象的实例。

语法如下:

    public SqlDataReader ExecuteReader()

返回值:一个SqlDataReader对象。

【例13.9】 创建一个Windows应用程序,根据select*from tb_command语句进行查询,调用ExecuteReader方法返回一个包含tb_command表中所有数据的SqlDataReader对象,代码如下。(实例位置:光盘\TM\sl\13\5)

程序的运行结果如图13.20所示。

图13.20 获取员工姓名

3.ExecuteScalar方法

执行SQL语句,返回结果集中的第一行的第一列。

语法如下:

    public override Object ExecuteScalar()

返回值:结果集中第一行的第一列或空引用(如果结果集为空)。

在例13.7中,已经使用ExecuteScalar方法获取指定数据表中的数据数量,此处不再赘述,读者可参见例13.7中的代码,理解ExecuteScalar方法的使用,ExecuteScalar方法通常与聚合函数一起使用,常见的聚合函数如表13.6所示。

表13.6 常见的聚合函数及说明

13.5 读取数据:DataReader对象

视频讲解:光盘\TM\lx\13\DataReader对象的使用.exe

13.5.1 DataReader对象概述

DataReader对象是数据读取器对象,提供只读向前的游标,如果应用程序需要每次从数据库中取出最新的数据,或者只是需要快速读取数据,并不需要修改数据,那么就可以使用DataReader对象进行读取。对于不同的数据库连接,有不同的DataReader类型。

 在System.Data.SqlClient命名空间下时,可以调用SqlDataReader类。

 在System.Data.OleDb命名空间下时,可以调用OleDbDataReader类。

 在System.Data.Odbc命名空间下时,可以调用OdbcDataReader类。

 在System.Data.Oracle命名空间下时,可以调用OracleDataReader类。

在使用DataReader对象读取数据时,可以使用ExecuteReader方法,根据SQL语句的结果创建一个SqlDataReader对象。

说明

在创建DataRelation时,它首先验证是否可以建立关系。在创建DataRelation和将其添加到DataRelationCollection(DataSet的DataRelation对象的集合)之间的这段时间,可以对父行或子行进行其他更改。

【例13.10】 使用ExecuteReader方法创建一个读取tb_command表中所有数据的SqlDataReader对象,代码如下。

    conn = new SqlConnection("server=.;database=db_CSharp;uid=sa;pwd="); //连接数据库
    conn.Open();                                                         //打开数据库
    SqlCommand cmd = new SqlCommand();                                   //创建 SqlCommand 对象
    cmd.Connection = conn;                                               //设置对象的连接
    cmd.CommandText = "select * from tb_command";                        //设置 SQL 语句
    cmd.CommandType = CommandType.Text;                                  //设置以文本形式执行 SQL 语句
    //使用 ExecuteReader 方法创建 SqlDataReader 对象
    SqlDataReader sdr = cmd.ExecuteReader();

13.5.2 判断查询结果中是否有值

可以通过SqlDataReader对象的HasRows属性获取一个值,该值指示SqlDataReader是否包含一行或多行,即判断查询结果中是否有值。

语法如下:

    public override bool HasRows { get; }

属性值:如果SqlDataReader包含一行或多行,则为true;否则为false。

【例13.11】 创建一个Windows应用程序,向窗体中添加一个TextBox控件和一个Button控件,分别用于输入要查询的表名以及执行查询操作,通过SqlDataReader对象的HasRows属性进行判断。如果SqlDataReader包含一行或多行,则为true;否则为false,代码如下。(实例位置:光盘\TM\sl\13\6)

图13.21 判断指定的数据表中是否有值

程序的运行结果如图13.21所示。

13.5.3 读取数据

如果要读取数据表中的数据,通过ExecuteReader方法,根据SQL语句创建一个SqlDataReader对象后,再调用SqlDataReader对象的Read方法读取数据。Read方法使SqlDataReader前进到下一条记录,SqlDataReader的默认位置在第一条记录前面。因此,必须调用Read方法访问数据。对于每个关联的SqlConnection,一次只能打开一个SqlDataReader,在第一个关闭之前,打开另一个的任何尝试都将失败。

语法如下:

    public override bool Read()

返回值:如果存在多个行,则为true;否则为false。

在使用完SqlDataReader对象后,要使用Close方法关闭SqlDataReader对象。

语法如下:

    public override void Close()

【例13.12】 关闭SqlDataReader对象,代码如下。

    //实例化 SqlConnection 变量 conn
    SqlConnection conn = new SqlConnection("server=.;database=db_CSharp;uid=sa;pwd=");
    //打开连接
    conn.Open();
    //创建一个 SqlCommand 对象
    SqlCommand cmd = new SqlCommand("select * from "+textBox1.Text.Trim(), conn);
    //使用 ExecuteReader 方法创建 SqlDataReader 对象
    SqlDataReader sdr = cmd.ExecuteReader();
    sdr.Close();

例13.10中通过ExecuteReader方法,根据SQL语句的结果创建一个SqlDataReader对象,读取tb_ command表中的所有数据,所以此处不再赘述,读者可以参见例13.10的代码。

注意

在使用SqlDataReader对象之前,必须打开数据库连接。如果针对一个SqlConnection,创建多个SqlDataReader对象,则创建下一个SqlDataReader对象之前,要通过Close方法关闭上一个SqlDataReader对象。

13.6 数据适配器:DataAdapter对象

视频讲解:光盘\TM\lx\13\DataAdapter对象的使用.exe

13.6.1 DataAdapter对象概述

DataAdapter对象是一个数据适配器对象,是DataSet与数据源之间的桥梁。DataAdapter对象提供了4个属性,用于实现与数据源之间的互通。

 SelectCommand属性:向数据库发送查询SQL语句。

 DeleteCommand属性:向数据库发送删除SQL语句。

 InsertCommand属性:向数据库发送插入SQL语句。

 UpdateCommand属性:向数据库发送更新SQL语句。

在对数据库进行操作时,只要将这4个属性设置成相应的SQL语句即可。DataAdapter对象中还有几个主要的方法,具体如下。

(1)Fill方法用数据填充DataSet。

语法如下:

    public int Fill(DataSet dataSet,string srcTable)

 dataSet:要用记录和架构(如果必要)填充的DataSet。

 srcTable:用于表映射的源表的名称。

 返回值:已在DataSet中成功添加或刷新的行数,这不包括受不返回行的语句影响的行。

说明

当创建DataAdapter的实例时,将其读/写属性设置为初始值,MissingMappingAction属性设置为MissingMappingAction.Passthrough, MissingSchemaAction属性设置为MissingSchemaAction.Add。

(2)Update方法更新数据库时,DataAdapter将调用DeleteCommand、InsertCommand以及Update Command属性。

语法如下:

    public int Update(DataTable dataTable)

 dataTable:用于更新数据源的DataTable。

 返回值:DataSet中成功更新的行数。

例如,如果使用DataAdapter对象的Fill方法从数据源中提取数据并填充到DataSet时,就会用到SelectCommand属性中设置的命令对象。

13.6.2 填充DataSet数据集

通过DataAdapter对象的Fill方法填充DataSet数据集,Fill方法使用Select语句从数据源中检索数据。与Select命令关联的Connection对象必须有效,但不需要将其打开。

注意

DataSet和DataTable对象从MarshalByValueComponent(实现IComponent并提供可远程控制的组件的基实现)继承而来,并支持用于远程处理ISerializable接口。

【例13.13】 创建一个Windows应用程序,向窗体中添加一个Button控件和一个DataGridView控件,分别用于执行数据绑定以及显示数据表中的数据。当单击Button控件后,程序首先连接数据库,然后创建一个SqlDataAdapter对象,使用该对象的Fill方法填充DataSet数据集,最后设置DataGridView控件的数据源,显示查询的数据,代码如下。(实例位置:光盘\TM\sl\13\7)

    SqlConnection conn;
    private void button1_Click(object sender, EventArgs e)
    {
         //实例化 SqlConnection 变量 conn
         conn = new SqlConnection("server=.;database=db_CSharp;uid=sa;pwd=");
         //创建一个 SqlCommand 对象
         SqlCommand cmd=new SqlCommand("select * from tb_command",conn);
         //创建一个 SqlDataAdapter 对象
         SqlDataAdapter sda = new SqlDataAdapter();
         //设置 SqlDataAdapter 对象的 SelectCommand 属性为 cmd
         sda.SelectCommand = cmd;
         //创建一个 DataSet 对象
         DataSet ds = new DataSet();
         //使用 SqlDataAdapter 对象的 Fill 方法填充 DataSet 数据集
         sda.Fill(ds,"cs");
         //设置 dataGridView1 控件的数据源
         dataGridView1.DataSource = ds.Tables[0];
    }

程序的运行结果如图13.22所示。

图13.22 Fill方法填充DataSet数据集

13.6.3 更新数据源

使用DataAdapter对象的Update方法,可以将DataSet(在13.7节中将会介绍DataSet对象)中修改过的数据及时更新到数据库中。在调用Update方法之前,要实例化一个CommandBuilder类,它能自动根据DataAdapter的SelectCommand的SQL语句判断其他的InsertCommand、UpdateCommand和DeleteCommand。这样,就不用设置DataAdapter的InsertCommand、UpdateCommand和DeleteCommand属性,直接使用DataAdapter的Update方法来更新DataSet、DataTable或DataRow数组即可。

【例13.14】 创建一个Windows应用程序,查询tb_command表中的所有数据并显示在DataGrid View控件中,单击某条数据,会显示其详细信息。当对某条数据进行修改后,单击“修改”按钮,使用DataAdapter对象的Update方法更新数据源,代码如下。(实例位置:光盘\TM\sl\13\8)

程序的运行结果如图13.23所示。

图13.23 更新数据源

说明

在DataTable对象上可以多次使用Fill方法。如果主键存在,则传入行会与已有的匹配行合并。如果主键不存在,则传入行会追加到DataTable中。

13.7 数据集:DataSet对象

视频讲解:光盘\TM\lx\13\DataSet对象的使用.exe

13.7.1 DataSet对象概述

DataSet对象就像存放于内存中的一个小型数据库。它可以包含数据表、数据列、数据行、视图、约束以及关系。通常,DataSet的数据来源于数据库或者XML,为了从数据库中获取数据,需要使用数据适配器(DataAdapter)从数据库中查询数据。关于数据适配器(DataAdapter)在第13.6节已经做过介绍,此处不再赘述。

【例13.15】 使用数据适配器(DataAdapter)从数据库中查询数据,调用其Fill方法填充DataSet对象,代码如下。

    //连接数据库
    conn = new SqlConnection("server=.;database=db_CSharp;uid=sa;pwd=");
    DataSet ds = new DataSet();                                             //创建一个 DataSet
    SqlDataAdapter sda = new SqlDataAdapter("select * from tb_test", conn); //创建一个 SqlDataAdapter 对象
    sda.Fill(ds);                                                           //使用 Fill 方法填充 DataSet

13.7.2 合并DataSet内容

可以使用DataSet的Merge方法将DataSet、DataTable或DataRow数组的内容并入现有的DataSet中。Merge方法将指定的DataSet及其架构与当前的DataSet合并,在此过程中,将根据给定的参数保留或放弃在当前DataSet中的更改并处理不兼容的架构。

语法如下:

    public void Merge(
         DataSet dataSet,
         bool preserveChanges,
         MissingSchemaAction missingSchemaAction
    )

 dataSet:其数据和架构将被合并到DataSet中。

 preserveChanges:要保留当前DataSet中的更改,则为true;否则为false。

 missingSchemaAction:MissingSchemaAction枚举值之一。

MissingSchemaAction枚举成员及说明如表13.7所示。

表13.7 MissingSchemaAction枚举成员及说明

注意

当DataSet对象为null时,无法进行合并。

【例13.16】 创建一个Windows应用程序,向窗体中添加一个DataGridView控件。首先获取数据表tb_test中的数据,并存储在DataSet对象ds中,然后再获取数据表tb_man中的数据,存储在另一个DataSet对象ds1中。最后调用DataSet对象的Merge方法,将ds与ds1合并,代码如下。(实例位置:光盘\TM\sl\13\9)

    SqlConnection conn;
    private void Form1_Load(object sender, EventArgs e)
    {
         //实例化 SqlConnection 变量 conn,连接数据库
         conn = new SqlConnection("server=.;database=db_CSharp;uid=sa;pwd=");
         //创建两个 DataSet
         DataSet ds = new DataSet();
         DataSet ds1 = new DataSet();
         //创建一个 SqlDataAdapter 对象
         SqlDataAdapter sda = new SqlDataAdapter("select * from tb_test", conn);
         //使用 Fill 方法填充 DataSet
         sda.Fill(ds);
         //创建一个 SqlDataAdapter 对象
         SqlDataAdapter sda1 = new SqlDataAdapter("select * from tb_man", conn);
         //创建一个 SqlCommandBuilder 对象
         SqlCommandBuilder sbl = new SqlCommandBuilder(sda1);
         //使用 Fill 方法填充 DataSet
         sda1.Fill(ds1);
         //使用 Merge 方法将 ds 合并到 ds1 中
         ds1.Merge(ds,true,MissingSchemaAction.AddWithKey);
         //设置 dataGridView1 控件的数据源
         dataGridView1.DataSource = ds1.Tables[0];
    }

程序的运行结果如图13.24所示。

图13.24 合并DataSet

13.7.3 复制DataSet内容

为了在不影响原始数据的情况下使用数据,或者使用DataSet中数据的子集,可以创建DataSet的副本。当复制DataSet时,可以:

 创建DataSet的原样副本,其中包含架构、数据、行状态信息和行版本。

 创建包含现有DataSet的架构但仅包含已修改行的DataSet。可以返回已修改的所有行或者指定特定的DataRowState。有关行状态的更多信息,可参见行状态与行版本。

 仅复制DataSet的架构(即关系结构),而不复制任何行。可以使用ImportRow将行导入现有的DataTable。

可以使用DataSet对象的Copy方法创建包含架构和数据的DataSet的原样副本。Copy方法的功能是复制指定DataSet的结构和数据。

语法如下:

    public DataSet Copy()

返回值:新的DataSet,具有与该DataSet相同的结构(表架构、关系和约束)和数据。

【例13.17】 创建一个Windows应用程序,向窗体中添加两个DataGridView控件和一个Button控件。第一个DataGridView控件用于显示数据表tb_test中的数据,当单击Button控件后,通过DataSet对象的Copy方法复制第一个DataGridView控件的DataSet,并作为第二个DataGridView控件的数据源,代码如下。(实例位置:光盘\TM\sl\13\10)

    SqlConnection conn;                             //声明一个 SqlConnection 变量
    DataSet ds;                                     //声明一个 DataSet 变量
    private void Form1_Load(object sender, EventArgs e)
    {
         //实例化 SqlConnection 变量 conn,连接数据库
         conn = new SqlConnection("server=.;database=db_CSharp;uid=sa;pwd=");
         //创建一个 SqlCommand 对象
         SqlCommand cmd = new SqlCommand("select * from tb_test",conn);
         //创建一个 SqlDataAdapter 对象
         SqlDataAdapter sda = new SqlDataAdapter();
         //设置 SqlDataAdapter 对象的 SelectCommand 属性,设置执行的 SQL 语句
         sda.SelectCommand = cmd;
         //实例化 DataSet
         ds = new DataSet();
         //使用 SqlDataAdapter 对象的 Fill 方法填充 DataSet
         sda.Fill(ds,"test");
         //设置 dataGridView1 的数据源
         dataGridView1.DataSource = ds.Tables[0];
    }
    private void button1_Click(object sender, EventArgs e)
    {
         DataSet ds1 = ds.Copy();                  //调用 DataSet 的 Copy 方法复制 ds 中的内容
         dataGridView2.DataSource = ds1.Tables[0]; //将 ds1 作为 dataGridView2 的数据源
    }

程序的运行结果如图13.25所示。

图13.25 复制DataSet

13.8 小结

本章主要介绍了数据库的基础知识,需掌握什么是ADO.NET。在ADO.NET中提供了连接数据库对象(Connection对象)、执行SQL语句对象(Command对象)、读取数据对象(DataReader对象)、数据适配器对象(DataAdapter对象)以及数据集对象(DataSet对象)。这些对象是操作数据库的重要对象,需要读者重点掌握。在讲解这些对象的过程中,列举了大量的实例,通过实例可以更好地理解所讲的内容。

13.9 实践与练习

(1)尝试开发一个程序,要求实现向数据库中插入数据的功能。(答案位置:光盘\TM\sl\13\11)

(2)尝试开发一个程序,要求实现修改数据表中数据的功能。(答案位置:光盘\TM\sl\13\12)

(3)尝试开发一个程序,要求使用SqlDataAdapter数据适配器和DataSet数据集获得数据库中的数据。(答案位置:光盘\TM\sl\13\13)

第14章 DataGridView数据控件

第14章
DataGridView数据控件

视频讲解:47分钟

开发WinForms应用程序需要使用数据库存储数据。使用DataGridView控件可以快速地将数据库中的数据显示给用户,并且可以通过DataGridView控件直接对数据进行操作,大大增强了操作数据库的效率。本章将详细地介绍DataGridView数据控件,讲解过程中为了便于读者理解结合了大量的举例。

通过阅读本章,您可以:

 了解什么是DataGridView数据控件

 掌握如何通过DataGridView数据控件显示数据

 掌握如何获取DataGridView数据控件的活动单元格

 掌握如何通过DataGridView数据控件批量修改数据

 掌握如何设置选中控件中某行时显示不同的颜色

 掌握如何禁止在控件中添加和删除行

 掌握如何手动添加数据

14.1 DataGridView控件概述

视频讲解:光盘\TM\lx\14\DataGridView控件概述.exe

DataGridView控件提供一种强大而灵活的以表格形式显示数据的方式。可以使用DataGridView控件来显示少量数据的只读视图,也可以对其进行缩放以显示特大数据集的可编辑视图。使用DataGridView控件,可以显示和编辑来自多种不同类型的数据源的表格数据。将数据绑定到DataGridView控件非常简单和直观,在大多数情况下,只需设置DataSource属性即可。DataGridView控件具有极高的可配置性和可扩展性,它提供了大量的属性、方法和事件,可以用来对该控件的外观和行为进行自定义。当需要在Windows窗体应用程序中显示表格数据时,首先考虑使用DataGridView控件。若要以小型网格显示只读值或者使用户能够编辑具有数百万条记录的表,DataGridView控件将提供可以方便地进行编程以及有效地利用内存的解决方案。

14.2 在DataGridView控件中显示数据

视频讲解:光盘\TM\lx\14\在DataGridView控件中显示数据.exe

通过DataGridView控件显示数据表中的数据,首先需要使用DataAdapter对象查询指定的数据,然后通过该对象的Fill方法填充DataSet(关于DataAdapter对象以及Fill方法可参见第15章的内容),最后设置DataGridView控件的DataSource属性为DataSet的表格数据。DataSource属性用于获取或设置DataGridView控件所显示数据的数据源。

语法如下:

    public Object DataSource { get; set; }

属性值:包含DataGridView控件要显示的数据的对象。

【例14.1】 创建一个Windows应用程序,向窗体中添加一个DataGridView控件,然后将数据表tb_emp中的数据绑定到控件中,代码如下。(实例位置:光盘\TM\sl\14\1)

    private void Form1_Load(object sender, EventArgs e)
    {
         //实例化 SqlConnection 变量 conn,连接数据库
         SqlConnection conn = new SqlConnection("server=.;database=db_CSharp;uid=sa;pwd=");
         //创建一个 SqlDataAdapter 对象
         SqlDataAdapter sda = new SqlDataAdapter("select * from tb_emp",conn);
         //创建一个 DataSet 对象
         DataSet ds = new DataSet();
         //使用 SqlDataAdapter 对象的 Fill 方法填充 DataSet
         sda.Fill(ds,"emp");
         //设置 dataGridView1 控件数据源
         dataGridView1.DataSource = ds.Tables[0];
    }

程序的运行结果如图14.1所示。

图14.1 显示tb_emp数据表中的数据

说明

在用DataGridView控件显示数据时,可以将Columns[列的索引号]属性的Visible属性设置为false,以隐藏指定的列。

14.3 获取DataGridView控件中的当前单元格

视频讲解:光盘\TM\lx\14\获取DataGridView控件中的当前单元格.exe

若要与DataGridView进行交互,通常要求通过编程方式发现哪个单元格处于活动状态。如果需要更改当前单元格,可通过DataGridView控件的CurrentCell属性来获取当前单元格信息。

CurrentCell属性用于获取当前处于活动状态的单元格。

语法如下:

    public DataGridViewCell CurrentCell { get; set; }

属性值:表示当前单元格的DataGridViewCell,如果没有当前单元格,则为空引用。默认值是第一列中的第一个单元格,如果控件中没有单元格,则为空引用。

【例14.2】 创建一个Windows应用程序,向窗体中添加一个DataGridView控件、一个Button控件和一个Label控件,主要用于显示数据、获取指定单元格信息以及显示单元格信息。当单击Button控件之后,会通过DataGridView的CurrentCell属性来获取当前单元格信息,代码如下。(实例位置:光盘\TM\sl\14\2)

    SqlConnection conn;                   //声明一个 SqlConnection 变量
    SqlDataAdapter sda;                   //声明一个 SqlDataAdapter 变量
    DataSet ds = null;                    //声明一个 DataSet 变量
    private void Form1_Load(object sender, EventArgs e)
    {
         //实例化 SqlConnection 变量 conn,连接数据库
         conn = new SqlConnection("server=.;database=db_CSharp;uid=sa;pwd=");
         //实例化 SqlDataAdapter 对象
         sda = new SqlDataAdapter("select * from tb_teacher", conn);
         //实例化 DataSet 对象
         ds = new DataSet();
         //使用 SqlDataAdapter 对象的 Fill 方法填充 DataSet
         sda.Fill(ds, "teacher");
         //设置 dataGridView1 控件的数据源
         dataGridView1.DataSource = ds.Tables[0];
    }
    private void button1_Click(object sender, EventArgs e)
    {
         //使用 CurrentCell.RowIndex 和 CurrentCell.ColumnIndex 获取数据的行和列坐标
                   string msg = String.Format("第{0}行,第{1}列", dataGridView1. CurrentCell.RowIndex,
         dataGridView1.CurrentCell.ColumnIndex);
         label1.Text = "选择的单元格为:" + msg;
    }

程序的运行结果如图14.2所示。

图14.2 获取单元格的信息

说明

可以通过DataGridView控件的SelectedCells属性集获取该控件中被选中的单元格信息。

14.4 直接在DataGridView控件中修改数据

视频讲解:光盘\TM\lx\14\直接在DataGridView控件中修改数据.exe

在DataGridView控件中修改数据,主要用到DataTable的ImportRow方法和DataAdapter对象的Update方法。实现的过程是通过DataTable的ImportRow方法将更改后的数据复制到一个DataTable中,然后通过DataAdapter对象的Update方法,将DataTable中的数据更新到数据库中。

ImportRow方法用于将DataRow复制到DataTable中,保留任何属性设置以及初始值和当前值。

语法如下:

    public void ImportRow(DataRow row)

row:要导入的DataRow。

DataAdapter对象的Update方法在第15章已经做过详细介绍,此处不再赘述。下面通过一个实例演示如何在DataGridView控件中直接修改数据,然后进行批量更新。

注意

默认情况下,用户可以通过在当前DataGridView文本框单元格中输入或按F2键来编辑该单元格的内容。在控件单元格中编辑内容的前提是DataGridView控件已启用以及单元格、行、列和控件的ReadOnly属性都设置为false。ReadOnly属性用于指示用户是否可以编辑DataGridView控件的单元格。

【例14.3】 创建一个Windows应用程序,向窗体中添加一个DataGridView控件和两个Button控件。DataGridView控件用于显示、修改数据,两个Button控件分别用于加载数据和将修改后的数据更新到数据库中,代码如下。(实例位置:光盘\TM\sl\14\3)

程序的运行结果如图14.3所示。

图14.3 在DataGridView控件中修改数据

14.5 当选中DataGridView控件中的行时显示不同的颜色

视频讲解:光盘\TM\lx\14\当选中DataGridView控件中的行时显示不同的颜色.exe

可以利用DataGridView控件的SelectionMode、ReadOnly和SelectionBackColor属性实现当选中DataGridView控件中的行时显示不同的颜色。

 SelectionMode用于设置如何选择DataGridView的单元格。

语法如下:

    public DataGridViewSelectionMode SelectionMode { get; set; }

属性值:DataGridViewSelectionMode值之一,默认为RowHeaderSelect。

DataGridViewSelectionMode枚举值及说明如表14.1所示。

表14.1 DataGridViewSelectionMode枚举值及说明

说明

在更改SelectionMode属性的值时,会清除当前的选择,所以在更改行的颜色时,要注意更改和选中的顺序。

 ReadOnly属性用于设置是否可以编辑DataGridView控件的单元格。

语法如下:

    public bool ReadOnly { get; set; }

属性值:如果用户不能编辑DataGridView控件的单元格,则为true;否则为false。默认为false。

【例14.4】 禁止用户编辑DataGridView控件的单元格,代码如下。

    dataGridView1.ReadOnly = true;

 SelectionBackColor属性用于设置DataGridView单元格在被选定时的背景色。

语法如下:

    public Color SelectionBackColor { get; set; }

属性值:Color,它表示选定单元格的背景色,默认为Empty。

SelectionBackColor属性包含在DataGridViewCellStyle类中,所以调用此属性之前要调用DataGridViewCellStyle属性。

【例14.5】 创建一个Windows应用程序,向窗体中添加一个DataGridView控件,用于显示tb_emp表中的所有数据。然后通过DataGridView控件的SelectionMode、ReadOnly和SelectionBackColor属性实现选中某一行时,行的背景变色,代码如下。(实例位置:光盘\TM\sl\14\4)

    SqlConnection conn;                                 //声明 SqlConnection 变量
    private void Form1_Load(object sender, EventArgs e)
    {
         //实例化 SqlConnection 变量 conn,连接数据库
         conn = new SqlConnection("server=.;database=db_CSharp;uid=sa;pwd=");
         //实例化 SqlDataAdapter 对象
         SqlDataAdapter sda = new SqlDataAdapter("select * from tb_emp", conn);
         //实例化 DataSet 对象
         DataSet ds = new DataSet();
         //使用 SqlDataAdapter 对象的 Fill 方法填充 DataSet
         sda.Fill(ds);
         //设置 dataGridView1 控件的数据源
         dataGridView1.DataSource = ds.Tables[0];
         //设置 SelectionMode 属性为 FullRowSelect 使控件能够整行选择
         dataGridView1.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
         //设置 dataGridView1 控件的 ReadOnly 属性,使其为只读
         dataGridView1.ReadOnly = true;
         //设置 dataGridView1 控件的 DefaultCellStyle.SelectionBackColor 属性,使其选择行为黄绿色
         dataGridView1.DefaultCellStyle.SelectionBackColor = Color.YellowGreen;
    }

程序的运行结果如图14.4所示。

图14.4 选中某行时显示不同的颜色

14.6 禁止在DataGridView控件中添加和删除行

视频讲解:光盘\TM\lx\14\禁止在DataGridView控件中添加和删除行.exe

通过设置DataGridView控件的公共属性AllowUserToAddRows、AllowUserToDeleteRows和 ReadOnly,可以禁止在DataGridView控件中添加和删除行。AllowUserToAddRows属性设置一个值,该值指示是否向用户显示添加行的选项;AllowUserToDeleteRows属性设置一个值,该值指示是否允许用户从DataGridView中删除行;ReadOnly属性设置一个指示网格是否处于只读模式的值。

【例14.6】 禁止在DataGridView控件中添加和删除行,可以通过下面的代码实现。

    dataGridView1.AllowUserToAddRows = false;     //禁止添加行
    dataGridView1.AllowUserToDeleteRows = false;  //禁止删除行
    dataGridView1.ReadOnly = true;                //控件中的数据为只读

14.7 使用Columns和Rows属性添加数据

视频讲解:光盘\TM\lx\14\使用Columns和Rows属性添加数据.exe

通过设置DataGridView控件的Columns和Rows属性值,可以向数据控件DataGridView添加数据项,使其能够手动添加数据。

 Columns属性用于获取一个包含控件中所有列的集合。

语法如下:

    public DataGridViewColumnCollection Columns { get; }

属性值:一个DataGridViewColumnCollection,包含DataGridView控件中的所有列。

 Rows属性获取一个集合,该集合包含DataGridView控件中的所有行。

语法如下:

    public DataGridViewRowCollection Rows { get; }

属性值:一个DataGridViewRowCollection,包含DataGridView控件中的所有行。

说明

如果想要在DataGridView控件的单元格中添加下拉列表,可以通过DataGridViewComboBoxColumn类来实现。

【例14.7】 创建一个Windows应用程序,向窗体中添加一个DataGridView控件,在窗体的Load事件中通过Columns和Rows属性,向控件中手动添加数据,代码如下。(实例位置:光盘\TM\sl\14\5)

    private void Form1_Load(object sender, EventArgs e)
    {
         //指定 DataGridView 控件显示的列数
         dataGridView1.ColumnCount = 4;
         dataGridView1.ColumnHeadersVisible = true;          //显示列标题
         //设置 DataGridView 控件标题列的样式
         DataGridViewCellStyle columnHeaderStyle = new DataGridViewCellStyle();
         //设置列标题的背景颜色
         columnHeaderStyle.BackColor = Color.Beige;
         //设置列标题的字体大小、样式
         columnHeaderStyle.Font = new Font("Verdana", 10, FontStyle.Bold);
         dataGridView1.ColumnHeadersDefaultCellStyle = columnHeaderStyle;
         //设置 DataGridView 控件的标题列名
         dataGridView1.Columns[0].Name = "编号";
         dataGridView1.Columns[1].Name = "姓名";
         dataGridView1.Columns[2].Name = "年龄";
         dataGridView1.Columns[3].Name = "性别";
         //建立 6 行数据
         string[] row1 = new string[] { "0001", "小吕", "28","男" };
         string[] row2 = new string[] { "0002", "小张", "27", "男" };
         string[] row3 = new string[] { "0003", "小郭", "24", "女" };
         string[] row4 = new string[] { "0004", "小贯", "21", "女" };
         string[] row5 = new string[] { "0005", "小陈", "20", "女" };
         string[] row6 = new string[] { "0006", "小梁", "23", "男" };
         object[] rows = new object[] { row1, row2, row3, row4, row5, row6 };
         foreach (string[] rowArray in rows)                    //使用 foreach 语句循环添加
         {
              dataGridView1.Rows.Add(rowArray);                 //向控件中添加数据
         }
    }

程序的运行结果如图14.5所示。

图14.5 使用Columns和Rows属性添加数据

14.8 小结

本章主要介绍了DataGridView控件,以及如何绑定DataGridView控件。DataGridView控件是程序开发中比较重要的数据绑定控件,通过此控件可以非常方便地将数据库中的数据显示出来。通过DataGridView控件还可以直接对数据进行修改,并支持批量修改的功能。在开发应用程序的过程中,会经常用到DataGridView控件,希望读者能够认真学习本章的内容,真正掌握如何使用DataGridView控件。

14.9 实践与练习

(1)尝试开发一个程序,要求在DataGridView控件中实现一个下拉列表。(答案位置:光盘\TM\sl\14\6)

(2)尝试开发一个程序,要求获得DataGridView控件中选定的单元格、行和列坐标。(答案位置:光盘\TM\sl\14\7)

(3)尝试开发一个程序,要求能够动态设置DataGridView控件中的字体样式。(答案位置:光盘\TM\sl\14\8)

第15章 LINQ数据访问技术

第15章
LINQ数据访问技术

视频讲解:62分钟

LINQ(Language-Integrated Query,语言集成查询)是微软公司提供的一项新技术,它能够将查询功能直接引入到.NET Framework所支持的编程语言中。查询操作可以通过编程语言自身来传达,而不是以字符串形式嵌入到应用程序代码中。LINQ主要包括LINQ to SQL、LINQ to DataSet、LINQ to Objects和LINQ to XML 4种关键技术,本章将对其进行详细讲解。

通过阅读本章,您可以:

 了解LINQ的基本概念

 熟悉var关键字的使用方法

 掌握Lambda表达式的使用方法

 掌握LINQ查询表达式的使用方法

 掌握使用LINQ操作SQL Server数据库

 掌握使用LINQ操作数组及集合

 掌握使用LINQ操作DataSet数据集

 掌握使用LINQ操作XML

15.1 LINQ基础

视频讲解:光盘\TM\lx\15\LINQ基础.exe

15.1.1 LINQ概述

语言集成查询(LINQ)可以为C#和Visual Basic提供强大的查询功能。LINQ引入了标准的、易于学习的查询和更新数据模式,可以对其技术进行扩展以支持几乎任何类型的数据存储。Visual Studio 2015包含LINQ提供程序的程序集,这些程序集支持将LINQ与.NET Framework集合、SQL Server数据库、ADO.NET数据集和XML文档一起使用,从而在对象领域和数据领域之间架起了一座桥梁。

LINQ主要由3部分组成:LINQ to ADO.NET、LINQ to Objects和LINQ to XML。其中,LINQ to ADO.NET可以分为两部分:LINQ to SQL和LINQ to DataSet。LINQ可以查询或操作任何存储形式的数据,其组成说明如下:

 LINQ to SQL组件,可以查询基于关系数据库的数据,并对这些数据进行检索、插入、修改、删除、排序、聚合、分区等操作。

 LINQ to DataSet组件,可以查询DataSet对象中的数据,并对这些数据进行检索、过滤、排序等操作。

 LINQ to Objects组件,可以查询Ienumerable或Ienumerable<T>集合,也就是可以查询任何可枚举的集合,如数据(Array和ArrayList)、泛型列表List<T>、泛型字典Dictionary<T>等,以及用户自定义的集合,而不需要使用LINQ提供程序或API。

 LINQ to XML组件,可以查询或操作XML结构的数据(如XML文档、XML片段、XML格式的字符串等),并提供了修改文档对象模型的内存文档和支持LINQ查询表达式等功能,以及处理XML文档的全新的编程接口。

LINQ可以查询或操作任何存储形式的数,如对象(集合、数组、字符串等)、关系(关系数据库、ADO.NET数据集等)以及XML。LINQ架构如图15.1所示。

图15.1 LINQ架构

15.1.2 使用var创建隐型局部变量

C#1.0、1.1及2.0版本中,如果要声明一个变量,必须指定变量的类型,但在C#后期版本中声明变量时,可以不明确指定其数据类型,而使用关键字var来声明。var关键字用来创建隐型局部变量,它指示编译器根据初始化语句右侧的表达式推断变量的类型。推断类型可以是内置类型、匿名类型、用户定义类型、.NET Framework类库中定义的类型或任何表达式。

例如,使用var关键字声明一个隐型局部变量,并赋值为2014。代码如下。

    var number = 2014;            //声明隐型局部变量

在很多情况下,var是可选的,它只是提供了语法上的便利。但在使用匿名类型初始化变量时,需要使用它,这在LINQ查询表达式中很常见。由于只有编译器知道匿名类型的名称,因此必须在源代码中使用var。如果已经使用var初始化了查询变量,则还必须使用var作为对查询变量进行循环访问的foreach语句中迭代变量的类型。

【例15.1】 创建一个控制台应用程序,首先定义一个字符串数组,然后通过定义隐型查询表达式将字符串数组中的单词分别转换为大写和小写,最后循环访问隐型查询表达式,并输出相应的大小写单词。代码如下。(实例位置:光盘\TM\sl\15\1)

    static void Main(string[] args)
    {
         string[] strWords = { "MingRi", "XiaoKe", "MRBccd" };    //定义字符串数组
         //定义隐型查询表达式
         var ChangeWord =
               from word in strWords
               select new { Upper = word.ToUpper(), Lower = word.ToLower() };
         //循环访问隐型查询表达式
         foreach (var vWord in ChangeWord)
         {
               Console.WriteLine("大写: {0}, 小写: {1}", vWord.Upper, vWord.Lower);//转换后的单词
         }
         Console.ReadLine();
    }

图15.2 var关键字的使用

程序的运行结果如图15.2所示。

使用隐式类型的变量时,需要遵循以下规则。

 只有在同一语句中声明和初始化局部变量时,才能使用var;不能将该变量初始化为null。

 不能将var用于类范围的域。

 由var声明的变量不能用在初始化表达式中,例如“var v =v++;”,这样会产生编译时错误。

 不能在同一语句中初始化多个隐式类型的变量。

 如果一个名为var的类型位于范围中,则当尝试用var关键字初始化局部变量时,将产生编译时错误。

15.1.3 Lambda表达式的使用

Lambda表达式是一个匿名函数,它可以包含表达式和语句,并且可用于创建委托或表达式目录树类型。所有Lambda表达式都使用Lambda运算符“=>”,(读为goes to)。Lambda运算符的左边是输入参数(如果有),右边包含表达式或语句块。例如,Lambda表达式x=>x*x读作x goes to x times x。Lambda表达式的基本形式如下。

    (input parameters) => expression

其中,input parameters表示输入参数,expression表示表达式。

说明

(1)Lambda表达式用在基于方法的LINQ查询中,作为诸如Where和Where(IQueryable, String, Object[])等标准查询运算符方法的参数。

(2)使用基于方法的语法在Enumerable类中调用Where方法时(像在LINQ to Objects和LINQ to XML中那样),参数是委托类型Func<T,TResult>,使用Lambda表达式创建委托最为方便。

注意

在is或as运算符的左侧不允许使用Lambda表达式。

【例15.2】 创建一个控制台应用程序,首先定义一个字符串数组,然后通过使用Lambda表达式查找数组中包含“C#”的字符串。代码如下。(实例位置:光盘\TM\sl\15\2)

    static void Main(string[] args)
    {
         //声明一个数组并初始化
         string[] strLists = new string[] { "明日科技", "C#编程词典", "C#编程词典珍藏版" };
         //使用 Lambda 表达式查找数组中包含“C#”的字符串
         string[] strList = Array.FindAll(strLists, s => (s.IndexOf("C#") >= 0));
         //使用 foreach 语句遍历输出
         foreach (string str in strList)
         {
              Console.WriteLine(str);
         }
         Console.ReadLine();
    }

图15.3 Lambda表达式的使用

程序的运行结果如图15.3所示。

下列规则适用于Lambda表达式中的变量范围:

 捕获的变量将不会被作为垃圾回收,直至引用变量的委托超出范围为止。

 在外部方法中看不到Lambda表达式内引入的变量。

 Lambda表达式无法从封闭方法中直接捕获ref或out参数。

 Lambda表达式中的返回语句不会导致封闭方法返回。

 Lambda表达式不能包含其目标位于所包含匿名函数主体外部或内部的goto语句、break语句或continue语句。

15.1.4 LINQ查询表达式

语言集成查询(LINQ)是一组技术的名称,这些技术建立在将查询功能直接集成到C#语言(以及Visual Basic和可能的任何其他.NET语言)的基础上。借助于LINQ,查询现在已是高级语言构造,就如同类、方法和事件等。

对于编写查询的开发人员来说,LINQ最明显的“语言集成”部分是查询表达式。查询表达式是使用C#中引入的声明性查询语法编写的。通过使用查询语法,开发人员可以使用最少的代码对数据源执行复杂的筛选、排序和分组操作,使用相同的基本查询表达式模式来查询和转换SQL数据库、ADO.NET数据集、XML文档和流以及.NET集合中的数据等。

使用LINQ查询表达式时,需要注意以下几点:

 查询表达式可用于查询和转换来自任意支持LINQ的数据源中的数据。例如,单个查询可以从SQL数据库检索数据,并生成XML流作为输出。

 查询表达式容易掌握,因为它们使用许多常见的C#语言构造。

 查询表达式中的变量都是强类型的,但许多情况下不需要显式提供类型,因为编译器可以推断类型。

 在循环访问foreach语句中的查询变量之前,不会执行查询。

 在编译时,根据C#规范中设置的规则将查询表达式转换为“标准查询运算符”方法调用。任何可以使用查询语法表示的查询都可以使用方法语法表示,但是多数情况下查询语法更易读和简洁。

 作为编写LINQ查询的一项规则,建议尽量使用查询语法,只在必需的情况下才使用方法语法。

 一些查询操作,如Count或Max等,由于没有等效的查询表达式子句,因此必须表示为方法调用。

 查询表达式可以编译为表达式目录树或委托,具体取决于查询所应用到的类型。其中,IEnumerable<T>查询编译为委托,IQueryable和IQueryable<T>查询编译为表达式目录树。

LINQ查询表达式包含8个基本子句,分别为from、select、group、where、orderby、join、let和into,其说明如表15.1所示。

表15.1 LINQ查询表达式子句及说明

【例15.3】 创建一个控制台应用程序,首先定义一个字符串数组,然后使用LINQ查询表达式查找数组中长度小于7的所有项并输出。代码如下。(实例位置:光盘\TM\sl\15\3)

    static void Main(string[] args)
    {
         //定义一个字符串数组
         string[] strName = new string[] { "明日科技","C#编程词典","C#从基础到项目实战","C#范例手册" };
         //定义 LINQ 查询表达式,从数组中查找长度小于 7 的所有项
         IEnumerable<string> selectQuery =
             from Name in strName
             where Name.Length<7
             select Name;
         //执行 LINQ 查询,并输出结果
         foreach (string str in selectQuery)
         {
             Console.WriteLine(str);
         }
         Console.ReadLine();
    }

程序的运行结果如图15.4所示。

图15.4 LINQ查询表达式的使用

15.2 使用LINQ操作SQL Server数据库

视频讲解:光盘\TM\lx\15\LINQ操作SQL Server数据库.exe

15.2.1 使用LINQ查询SQL Server数据库

使用LINQ查询SQL数据库时,首先需要创建LinqToSql类文件。创建LinqToSql类文件的步骤如下:

(1)启动Visual Studio 2015开发环境,建立一个目标框架为Framework SDK 4.6的项目。

(2)在“解决方案资源管理器”窗口中选中当前项目,单击鼠标右键,在弹出的快捷菜单中选择“添加”/“添加新项”命令,弹出“添加新项”对话框,如图15.5所示。

图15.5 添加新项

(3)在如图15.5所示的“添加新项”对话框中选择“LINQ to SQL类”,并输入名称,单击“添加”按钮,添加一个LinqToSql类文件。

(4)在“服务器资源管理器”窗口中连接SQL Server 2012数据库,然后将指定数据库中的表映射到LinqDB.dbml中(可以将表拖曳到设计视图中),如图15.6所示。

图15.6 数据表映射到dbml文件

(5)LinqDB.dbml文件将自动创建一个名称为LinqDBDataContext的数据上下文类,为数据库提供查询或操作数据库的方法,LINQ数据源创建完毕。LinqDBDataContext类中的程序代码均自动生成,如图15.7所示。

创建完LinqToSql类文件之后,接下来就可以使用它了。下面通过一个例子讲解如何使用LINQ查询SQL Server数据库。

图15.7 LinqDBDataContext类中自动生成程序代码

【例15.4】 创建一个Windows应用程序,在Form1窗体中添加一个ComboBox控件,用来选择查询条件;添加一个TextBox控件,用来输入查询关键字;添加一个Button控件,用来执行查询操作;添加一个DataGridView控件,用来显示数据库中的数据。(实例位置:光盘\TM\sl\15\4)

首先在当前项目中依照上面所讲的步骤创建一个LinqToSql类文件,然后在Form1窗体中定义一个string类型变量,用来记录数据库连接字符串,并声明Linq连接对象。代码如下。

    //定义数据库连接字符串
    string strCon = "Data Source=(local);Database=db_CSharp;Uid=sa;Pwd=;";
    linqtosqlClassDataContext linq;                    //声明 Linq 连接对象

Form1窗体加载时,首先将数据库中的所有员工信息显示到DataGridView控件中。实现代码如下:

    private void Form1_Load(object sender, EventArgs e)
    {
         BindInfo();
    }

上面的代码中用到了BindInfo方法,该方法为自定义的无返回值类型方法,主要用来使用LinqToSql技术根据指定条件查询员工信息,并将查询结果显示在DataGridView控件中。BindInfo方法实现代码如下。

单击“查询”按钮,调用BindInfo方法查询员工信息,并将查询结果显示到DataGridView控件中。“查询”按钮的Click事件代码如下。

    private void btnQuery_Click(object sender, EventArgs e)
    {
         BindInfo();
    }

程序的运行结果如图15.8所示。

图15.8 使用LINQ查询SQL Server数据库

15.2.2 使用LINQ管理SQL Server数据库

使用LINQ管理SQL Server数据库时,主要有添加、修改和删除3种操作,本节将分别进行详细讲解。

1.添加数据

使用LINQ向SQL Server数据库中添加数据时,需要用到InsertOnSubmit方法和SubmitChanges方法。其中,InsertOnSubmit方法用来将处于pending insert状态的实体添加到SQL数据表中。其语法格式如下。

    void InsertOnSubmit(Object entity)

其中,entity表示要添加的实体。

SubmitChanges方法用来记录要插入、更新或删除的对象,并执行相应命令以实现对数据库的更改。其语法格式如下。

    public void SubmitChanges()

【例15.5】 创建一个Windows应用程序,Form1窗体设计为如图15.9所示界面。首先在当前项目中创建一个LinqToSql类文件,然后在Form1窗体中定义一个string类型的变量,用来记录数据库连接字符串,并声明LINQ连接对象。代码如下。(实例位置:光盘\TM\sl\15\5)

    //定义数据库连接字符串
    string strCon = "Data Source=(local);Database=db_CSharp;Uid=sa;Pwd=;";
    linqtosqlClassDataContext linq;              //声明 LINQ 连接对象

在Form1窗体中单击“添加”按钮,首先创建Linq连接对象;然后创建tb_Employee类对象(该类为对应的tb_Employee数据表类),为tb_Employee类对象中的各个属性赋值;最后调用LINQ连接对象中的InsertOnSubmit方法添加员工信息,并调用其SubmitChanges方法将添加员工操作提交服务器。“添加”按钮的Click事件代码如下。

    private void btnAdd_Click(object sender, EventArgs e)
    {
         linq = new linqtosqlClassDataContext(strCon);      //创建 LINQ 连接对象
         tb_Employee employee = new tb_Employee();          //创建 tb_Employee 类对象
         //为 tb_Employee 类中的员工实体赋值
         employee.ID = txtID.Text;
         employee.Name = txtName.Text;
         employee.Sex = cboxSex.Text;
         employee.Age = Convert.ToInt32(txtAge.Text);
         employee.Tel = txtTel.Text;
         employee.Address = txtAddress.Text;
         employee.QQ = Convert.ToInt32(txtQQ.Text);
         employee.Email = txtEmail.Text;
         linq.tb_Employee.InsertOnSubmit(employee);             //添加员工信息
         linq.SubmitChanges();                                  //提交操作
         MessageBox.Show("数据添加成功");
         BindInfo();
    }

上面的代码中用到了BindInfo方法,该方法为自定义的无返回值类型方法,主要用来获取所有员工信息,并绑定到DataGridView控件上。BindInfo方法实现代码如下。

程序的运行结果如图15.9所示。

图15.9 添加数据

2.修改数据

使用LINQ修改SQL Server数据库中的数据时,需要用到SubmitChanges方法。该方法在“添加数据”中已经作过详细介绍,在此不再赘述。

【例15.6】 创建一个Windows应用程序,将Form1窗体设计为如图15.10所示界面。首先在当前项目中创建一个LinqToSql类文件,然后在Form1窗体中定义一个string类型的变量,用来记录数据库连接字符串,并声明Linq连接对象。代码如下。(实例位置:光盘\TM\sl\15\6)

    //定义数据库连接字符串
    string strCon = "Data Source=(local);Database=db_CSharp;Uid=sa;Pwd=;";
    linqtosqlClassDataContext linq;              //声明 Linq 连接对象

当在DataGridView控件中选中某条记录时,根据选中记录的员工编号查找其详细信息,并显示在对应的文本框中。实现代码如下。

在Form1窗体中单击“修改”按钮,首先判断是否选择了要修改的记录,如果没有,弹出提示信息;否则创建Linq连接对象,并从该对象中的tb_Employee表中查找是否有相关记录,如果有,为tb_Employee表中的字段赋值,并调用Linq连接对象中的SubmitChanges方法修改指定编号的员工信息。“修改”按钮的Click事件代码如下。

上面的代码中用到了BindInfo方法,该方法为自定义的无返回值类型方法,主要用来获取所有员工信息,并绑定到DataGridView控件上。BindInfo方法实现代码如下:

程序的运行结果如图15.10所示。

图15.10 修改数据

3.删除数据

使用LINQ删除SQL Server数据库中的数据时,需要用到DeleteAllOnSubmit方法和SubmitChanges方法。其中SubmitChanges方法在“添加数据”中已经作过详细介绍,这里主要讲解DeleteAllOnSubmit方法。

DeleteAllOnSubmit方法用来将集合中的所有实体置于pending delete状态,其语法格式如下。

    void DeleteAllOnSubmit(IEnumerable entities)

其中,entities表示要移除所有项的集合。

【例15.7】 创建一个Windows应用程序,在Form1窗体中添加一个ContextMenuStrip控件,用来作为“删除”快捷菜单;添加一个DataGridView控件,用来显示数据库中的数据,将DataGridView控件的ContextMenuStrip属性设置为contextMenuStrip1。(实例位置:光盘\TM\sl\15\7)

首先在当前项目中依照上面所讲的步骤创建一个LinqToSql类文件;然后在Form1窗体中定义一个string类型的变量,用来记录数据库连接字符串,并声明Linq连接对象;再声明一个string类型的变量,用来记录选中的员工编号。代码如下。

    //定义数据库连接字符串
    string strCon = "Data Source=(local);Database=db_CSharp;Uid=sa;Pwd=;";
    linqtosqlClassDataContext linq;             //声明 LINQ 连接对象
    string strID = "";                          //记录选中的员工编号

在DataGridView控件中选择行时,记录当前选中行的员工编号,并赋值给定义的全局变量。代码如下。

    private void dgvInfo_CellClick(object sender, DataGridViewCellEventArgs e)
    {
         strID = Convert.ToString(dgvInfo[0, e.RowIndex].Value).Trim(); //获取选中的员工编号
    }

在DataGridView控件上单击鼠标右键,在弹出的快捷菜单中选择“删除”命令,首先判断要删除的员工编号是否为空,如果为空,则弹出提示信息;否则创建Linq连接对象,并从该对象中的tb_Employee表中查找是否有相关记录,如果有,则调用Linq连接对象中的DeleteAllOnSubmit方法删除员工信息,并调用其SubmitChanges方法将删除员工操作提交服务器。“删除”命令的Click事件代码如下。

上面的代码中用到了BindInfo方法,该方法为自定义的无返回值类型方法,主要用来获取所有员工信息,并绑定到DataGridView控件上。BindInfo方法实现代码如下。

程序的运行结果如图15.11所示。

图15.11 删除数据

15.3 使用LINQ操作其他数据

视频讲解:光盘\TM\lx\15\LINQ操作其他数据.exe

15.3.1 使用LINQ操作数组和集合

对数组和集合进行操作时可以使用LINQ to Objects技术(一种新的处理集合的方法)。如果采用旧方法,程序开发人员必须编写指定如何从集合中检索数据的复杂的foreach循环,而采用LINQ to Objects技术,只需编写描述要检索的内容的声明性代码。LINQ to Objects能够直接使用LINQ查询IEnumerable或IEnumerable<T>集合,而不需要使用LINQ提供程序或API,可以说,使用LINQ能够查询任何可枚举的集合,例如数组、泛型列表等。

下面通过一个实例讲解如何使用LINQ技术操作数组和集合。

【例15.8】 创建一个控制台应用程序,在Main方法中定义一个一维数组,然后使用LINQ技术从该数组中查找及格范围内的分数,最后循环访问查询结果并输出。实现代码如下。(实例位置:光盘\TM\sl\15\8)

图15.12 使用LINQ操作数组和集合

程序的运行结果如图15.12所示。

15.3.2 使用LINQ操作DataSet数据集

对DataSet数据集进行操作时可以使用LINQ to DataSet技术(LINQ to ADO.NET中的一种独立技术),使查询DataSet对象更加方便、快捷。下面对LINQ to DataSet技术中的常用方法进行详细讲解。

(1)AsEnumerable方法。AsEnumerable方法可以将DataTable对象转换为EnumerableRowCollection<DataRow>对象,其语法格式如下。

    public static EnumerableRowCollection<DataRow> AsEnumerable(this DataTable source)

 source:可枚举的源DataTable。

 返回值:一个IEnumerable<T>对象,其泛型参数T为DataRow。

(2)CopyToDataTable方法。CopyToDataTable方法用来将IEnumerable<T>对象中的数据赋值到DataTable对象中,其语法格式如下。

    public static DataTable CopyToDataTable<T>(this IEnumerable<T> source) where T : DataRow

 source:源IEnumerable<T>序列。

 返回值:一个DataTable,其中包含作为DataRow对象的类型的输入序列。

(3)AsDataView方法。AsDataView方法用来创建并返回支持LINQ的DataView对象,其语法格式如下。

    public static DataView AsDataView<T>(this EnumerableRowCollection<T> source) where T : DataRow

 source:从中创建支持LINQ的DataView的源LINQ to DataSet查询。

 返回值:支持LINQ的DataView对象。

(4)Take方法。Take方法用来从序列的开头返回指定数量的连续元素,其语法格式如下。

    public static IEnumerable<TSource> Take<TSource>(this IEnumerable<TSource> source,int count)

 source:要从其返回元素的序列。

 count:要返回的元素数量。

 返回值:一个IEnumerable<T>,包含输入序列开头的指定数量的元素。

(5)Sum方法。Sum方法用来计算数值序列之和,其语法格式如下。

    public static decimal Sum(this IEnumerable<decimal> source)

 source:一个要计算和的Decimal值序列。

 返回值:序列值之和。

说明

上面介绍的几种方法都有多种重载形式,这里只介绍了其常用的重载形式。

下面通过一个实例讲解如何在DataGridView控件中显示DataSet数据集中的数据。

【例15.9】 创建一个Windows应用程序,在Form1窗体中添加一个DataGridView控件,用来显示DataSet数据集中的数据。窗体加载时,首先将数据库中的数据填充到DataSet数据集中,然后使用LINQ技术从DataSet数据集中查找信息并显示在DataGridView控件中。实现代码如下。(实例位置:光盘\TM\sl\15\9)

    private void Form1_Load(object sender, EventArgs e)
    {
         //数据库连接字符串
         string strCon = "Data Source=(local);Database=db_CSharp;Uid=sa;Pwd=;";
         SqlConnection sqlcon;                                           //声明 SqlConnection 对象
         SqlDataAdapter sqlda;                                           //声明 SqlDataAdapter 对象
         DataSet myds;                                                   //声明 DataSet 数据集对象
         sqlcon = new SqlConnection(strCon);                             //创建数据库连接对象
         sqlda = new SqlDataAdapter("select * from tb_Salary", sqlcon);  //创建数据库桥接器对象
         myds = new DataSet();                                           //创建数据集对象
         sqlda.Fill(myds, "tb_Salary");                                  //填充 DataSet 数据集
         //使用 LINQ 从数据集中查询所有数据
         var query = from salary in myds.Tables["tb_Salary"].AsEnumerable()
         select salary;
         DataTable myDTable = query.CopyToDataTable<DataRow>();          //将查询结果转换为 DataTable 对象
         dataGridView1.DataSource = myDTable;                            //显示查询到的数据集中的信息
    }

程序的运行结果如图15.13所示。

图15.13 使用LINQ操作DataSet数据集

15.3.3 使用LINQ操作XML

对XML文件进行操作时可以使用LINQ to XML技术(LINQ技术中的一种,提供了修改文档对象模型的内存文档,并支持LINQ查询表达式等功能)。下面对LINQ to XML技术中的常用方法进行详细讲解。

(1)XElement类的Load方法。Xelement类表示一个XML元素,其Load方法用来从文件加载Xelement。该方法语法格式如下。

    public static XElement Load(string uri)

 uri:一个URI字符串,用来引用要加载到新XElement中的文件。

 返回值:一个包含指定文件内容的XElement。

(2)XElement类的SetAttributeValue方法。SetAttributeValue方法用来设置属性的值、添加属性或移除属性,其语法格式如下。

    public void SetAttributeValue(XName name,Object value)

 name:一个XName,其中包含要更改的属性的名称。

 value:分配给属性的值。如果该值为null,则移除该属性;否则,会将值转换为其字符串表示形式,并分配给该属性的Value属性。

(3)XElement类的Add方法。Add方法用来将指定的内容添加为此XContainer的子级,其语法格式如下。

    public void Add(Object content)

content表示要添加的包含简单内容的对象或内容对象集合。

(4)XElement类的ReplaceNodes方法。ReplaceNodes方法用来使用指定的内容替换此文档或元素的子节点,其语法格式如下。

    public void ReplaceNodes(Object content)

content表示一个用于替换子节点的包含简单内容的对象或内容对象集合。

(5)XElement类的Save方法。Save方法用来序列化此元素的基础XML树,可以将输出保存到文件、XmlTextWriter、TextWriter或XmlWriter。其语法格式如下。

    public void Save(string fileName)

fileName是一个包含文件名称的字符串。

(6)XDocument类的Save方法。XDocument类表示XML文档,其Save方法用来将此XDocument序列化为文件、TextWriter或XmlWriter。该方法语法格式如下。

    public void Save(string fileName)

fileName是一个包含文件名称的字符串。

(7)XDeclaration类。XDeclaration类表示一个XML声明,其构造函数语法格式如下。

    public XDeclaration(string version,string encoding,string standalone)

 version:XML的版本,通常为“1.0”。

 encoding:XML文档的编码。

 standalone:包含yes或no的字符串,用来指定XML是独立的还是需要解析外部实体。

说明

使用LINQ to XML技术中的类时,需要添加System.Linq.Xml命名空间。

下面通过一个实例讲解如何使用LINQ技术对XML文件进行操作。

【例15.10】 创建一个Windows应用程序,Form1窗体设计为如图15.14所示界面。在Form1窗体中先定义两个字符串类型的全局变量,分别用来记录XML文件路径及选中的ID编号。代码如下。(实例位置:光盘\TM\sl\15\10)

    static string strPath = "Employee.xml";   //记录 XML 文件路径
    static string strID = "";                 //记录选中的 ID 编号

Form1窗体加载时,将XML文件中的数据显示在DataGridView控件。Form1窗体的Load事件代码如下。

    private void Form1_Load(object sender, EventArgs e)
    {
         getXmlInfo();          //窗体加载时加载 XML 文件
    }

上面的代码中用到了getXmlInfo方法,该方法为自定义的无返回值类型方法,主要用来将XML文件中的内容绑定到DataGridView控件中。getXmlInfo方法实现代码如下。

    #region 将 XML 文件内容绑定到 DataGridView 控件
    /// <summary>
    ///将 XML 文件内容绑定到 DataGridView 控件
    /// </summary>
    private void getXmlInfo()
    {
         DataSet myds = new DataSet();               //创建 DataSet 数据集对象
         myds.ReadXml(strPath);                      //读取 XML 结构
         dataGridView1.DataSource = myds.Tables[0];  //在 DataGridView 中显示 XML 文件中的信息
    }
    #endregion

单击“添加”按钮,使用LINQ to XML技术向指定的XML文件中插入用户输入的数据,并重新保存XML文件。“添加”按钮的Click事件代码如下。

当用户在DataGridView控件中选择某记录时,使用LINQ to XML技术在XML文件中查找选中记录的详细信息,并显示到相应的文本框和下拉列表中。实现代码如下。

单击“修改”按钮,首先判断是否选定要修改的记录,如果已经选定,则使用LINQ to XML技术修改XML文件中的指定记录,并重新保存XML文件。“修改”按钮的Click事件代码如下。

单击“删除”按钮,首先判断是否选定要删除的记录,如果已经选定,则使用LINQ to XML技术删除XML文件中的指定记录,并重新保存XML文件。“删除”按钮的Click事件代码如下。

程序的运行结果如图15.14所示。

图15.14 使用LINQ操作XML文件

15.4 小结

本章对LINQ技术进行了详细讲解,其中主要涉及LINQ to SQL、LINQ to DataSet、LINQ to Objects和LINQ to XML 4种关键技术,并通过实例讲解了这4种关键技术的使用。LINQ技术是C#中的一种非常实用的技术,通过使用LINQ技术,可以在很大程度上方便程序开发人员对各种数据的访问。通过本章的学习,读者应熟练掌握LINQ技术的基础语法,并能够在实际应用中使用LINQ技术。

15.5 实践与练习

(1)尝试开发一个程序,要求使用LINQ to Objects技术演示如何获取选定文件的详细信息。(答案位置:光盘\TM\sl\15\11)

(2)尝试开发一个程序,要求使用LINQ to DataSet技术演示如何分类获取公司员工的薪水。(答案位置:光盘\TM\sl\15\12)

(3)尝试开发一个程序,要求使用LINQ技术实现防止SQL注入式攻击的功能。(答案位置:光盘\TM\sl\15\13)

第16章 程序调试与异常处理

第16章
程序调试与异常处理

视频讲解:40分钟

开发应用程序的代码必须安全、准确。但是在编写的过程中,不可避免地会出现错误,但有的错误不容易被发觉,从而导致程序运行错误。为了排除这些非常隐蔽的错误,要对编写好的代码进行程序调试,这样才能确保应用程序成功运行。另外,开发程序时,不仅要注意程序代码的准确性与合理性,还要处理程序中可能出现的异常情况,.NET框架提供了一套称为结构化异常处理的标准错误机制,在这种机制中,如果出现错误或者任何预期之外的事件,都会引发异常。本章将对.NET中的程序调试与异常处理进行详细讲解。

通过阅读本章,您可以:

 了解什么是程序调试及异常处理

 掌握如何启动、中断和停止调试

 掌握如何在代码中插入断点

 掌握如何单步执行代码

 掌握如何将程序运行到指定的位置

 掌握常用的异常处理语句的使用

16.1 程序调试概述

视频讲解:光盘\TM\lx\16\程序调试概述.exe

程序调试是在程序中查找错误的过程,在开发过程中,程序调试是检查代码并验证它能够正常运行的有效方法。另外,在开发时,如果发现程序不能正常工作,就必须找出并解决有关问题。

在测试期间进行程序调试是很有用的,因为它对希望产生的代码结果提供了另外一级的验证。发布程序之后,程序调试提供了重新创建和检测程序错误的方法,程序调试可以帮助查找代码中的错误。

16.2 常用的程序调试操作

视频讲解:光盘\TM\lx\16\常用的程序调试操作.exe

为了保证代码能够正常运行,要对代码进行程序调试。常用的程序调试包括断点操作、开始、中断和停止程序的执行、单步执行程序以及使程序运行到指定的位置。下面将对这几种常用的程序调试操作进行详细的介绍。

16.2.1 断点操作

断点通知调试器,应用程序在某点上(暂停执行)或某情况发生时中断。发生中断时,称程序和调试器处于中断模式。进入中断模式并不会终止或结束程序的执行,所有元素(如函数、变量和对象)都保留在内存中。执行可以在任何时候继续。

插入断点有3种方式:在要设置断点行旁边的灰色空白中单击;右击设置断点的代码行,在弹出的快捷菜单中选择“断点”/“插入断点”命令,如图16.1所示;单击要设置断点的代码行,选择菜单中的“调试”/“切换断点”命令,如图16.2所示。

插入断点后,就会在设置断点的行旁边的灰色空白处出现一个红色圆点,并且该行代码也呈高亮显示,如图16.3所示。

删除断点主要有3种方式,分别如下:

 可以单击设置了断点的代码行左侧的红色圆点。

 在设置了断点的代码行左侧的红色圆点上单击鼠标右键,在弹出的快捷菜单中选择“删除断点”命令。

 在设置了断点的代码行上单击鼠标右键,在弹出的快捷菜单中选择“断点”/“删除断点”命令,如图16.4所示。

图16.1 右键快捷菜单插入断点

图16.2 菜单栏插入断点

图16.3 插入断点后效果图

图16.4 右键快捷菜单删除断点

16.2.2 开始执行

开始执行是最基本的调试功能之一,从“调试”菜单(如图16.5所示)中选择“启动调试”命令或在源窗口中右击,可执行代码中的某行,然后从弹出的快捷菜单中选择“运行到光标处”命令,如图16.6所示。

图16.5 “调试”菜单

图16.6 某行代码的右键菜单

除了使用上述的方法开始执行外,还可以直接单击工具栏中的按钮,启动调试,如图16.7所示。

图16.7 工具栏中的启动调试按钮

如果选择“启动调试”命令,则应用程序启动并一直运行到断点。可以在任何时刻中断执行,以检查值、修改变量或检查程序状态,如图16.8所示。

如果选择“运行到光标处”命令,则应用程序启动并一直运行到断点或光标位置,具体要看是断点在前还是光标在前,可以在源窗口中设置光标位置。如果光标在断点的前面,则代码首先运行到光标处,如图16.9所示。

图16.8 运行到断点

图16.9 运行到光标处

16.2.3 中断执行

当执行到达一个断点或发生异常时,调试器将中断程序的执行。选择“调试”/“全部中断”命令后,调试器将停止所有在调试器下运行的程序的执行。程序并不退出,可以随时恢复执行。调试器和应用程序现在处于中断模式。“调试”菜单中“全部中断”命令如图16.10所示。

除了通过选择“调试”/“全部中断”命令中断执行外,也可以单击工具栏中的按钮中断执行,如图16.11所示。

图16.10 “调试”/“全部中断”命令

图16.11 工具栏中的中断执行按钮

16.2.4 停止执行

停止执行意味着终止正在调试的进程并结束调试会话,可以通过选择菜单中的“调试”/“停止调试”命令来结束运行和调试。也可以选择工具栏中的按钮停止执行。

16.2.5 单步执行和逐过程执行

单步执行意味着调试器每次只执行一行代码,单步执行主要是通过逐语句、逐过程和跳出这3种命令实现的。“逐语句”和“逐过程”的主要区别是当某一行包含函数调用时,“逐语句”仅执行调用本身,然后在函数内的第一个代码行处停止。而“逐过程”执行整个函数,然后在函数外的第一行处停止。如果位于函数调用的内部并想返回到调用函数时,应使用“跳出”,“跳出”将一直执行代码,直到函数返回,然后在调用函数中的返回点处中断。

图16.12 单步执行的3种命令

当启动调试后,可以单击工具栏中的按钮执行“逐语句”操作、单击按钮执行“逐过程”操作和单击按钮执行“跳出”操作,如图16.12所示。

说明

除了在工具栏中单击这3个按钮外,还可以通过快捷键执行这3种操作,启动调试后,按下F11键执行“逐语句”操作,按下F10键执行“逐过程”操作,按Shift+F10键执行“跳出”操作。

16.2.6 运行到指定位置

如果希望程序运行到指定的位置,可以在指定代码行上单击鼠标右键,在弹出的快捷菜单中选择“运行到光标处”命令,这样当程序运行到光标处时就会自动暂停;另外,也可以在指定的位置插入断点,同样可以使程序运行到插入断点的代码行时自动暂停。

16.3 异常处理概述

视频讲解:光盘\TM\lx\16\异常处理概述.exe

在编写程序时,不仅要关心程序的正常操作,还应该检查代码错误及可能发生的各类不可预期的事件。在现代编程语言中,异常处理是解决这些问题的主要方法。异常处理是一种功能强大的机制,用于处理应用程序可能产生的错误或是其他可以中断程序执行的异常情况。异常处理可以捕捉程序执行所发生的错误,通过异常处理可以有效、快速地构建各种用来处理程序异常情况的程序代码。

异常处理实际上就相当于大楼失火时(发生异常),烟雾感应器捕获到高于正常密度的烟雾(捕获异常),将自动喷水进行灭火(处理异常)。

在.NET类库中,提供了针对各种异常情形所设计的异常类,这些类包含了异常的相关信息。配合异常处理语句,应用程序能够轻易地避免程序执行时可能中断应用程序的各种错误。.NET框架中公共异常类如表16.1所示,这些异常类都是System.Exception的直接或间接子类。

表16.1 公共异常类及说明

16.4 异常处理语句

视频讲解:光盘\TM\lx\16\异常处理语句及使用.exe

C#程序中,可以使用异常处理语句处理异常。主要的异常处理语句有throw语句、try…catch语句和try…catch…finally语句,通过这3个异常处理语句,可以对可能产生异常的程序代码进行监控。下面将对这3个异常处理语句进行详细讲解。

16.4.1 try…catch语句

try…catch语句允许在try后面的大括号{}中放置可能发生异常情况的程序代码,对这些程序代码进行监控。在catch后面的大括号{}中则放置处理错误的程序代码,以处理程序发生的异常。try…catch语句的基本格式如下。

    try
    {
        被监控的代码
    }
    catch(异常类名 异常变量名)
    {
        异常处理
    }

在catch子句中,异常类名必须为System.Exception或从System.Exception派生的类型。当catch子句指定了异常类名和异常变量名后,就相当于声明了一个具有给定名称和类型的异常变量,此异常变量表示当前正在处理的异常。

说明

只捕捉能够合法处理的异常,而不要在catch子句中创建特殊异常的列表。

【例16.1】 创建一个控制台应用程序,声明一个object类型的变量obj,其初始值为null。然后将obj强制转换成int类型赋给int类型变量N,使用try…catch语句捕获异常,代码如下。(实例位置:光盘\TM\sl\16\1)

程序的运行结果如图16.13所示。

图16.13 捕获异常

查看运行结果,抛出了异常。因为声明的object变量obj被初始化为null,然后又将obj强制转换成int类型,这样就产生了异常,由于使用了try…catch语句,所以将这个异常捕获,并将异常输出。

上述实例是直接使用System.Exception类捕获异常,下面以System.OverflowException类为例介绍如何使用其他异常类捕获异常。

【例16.2】 创建一个控制台应用程序,声明3个int类型的变量Inum1、Inum2和Num,并将变量Inum1和Inum2分别初始化为6000000。然后使Num等于Inum1和Inum2的乘积,最后引发System.OverflowException类异常,代码如下。(实例位置:光盘\TM\sl\16\2)

程序的运行结果为“引发OverflowException异常”。

16.4.2 throw语句

throw语句用于主动引发一个异常,使用throw语句可以在特定的情形下,自行抛出异常。throw语句的基本格式如下。

    throw ExObject

ExObject:所要抛出的异常对象,这个异常对象是派生自System.Exception类的类对象。

说明

通常throw语句与try…catch或try…finally语句一起使用。当引发异常时,程序查找处理此异常的catch语句。也可以用throw语句重新引发已捕获的异常。

【例16.3】 创建一个控制台应用程序,创建一个int类型的方法MyInt,此方法有两个string类型的参数a和b。在这个方法中,使a做分子,b做分母,如果分母的值是0,则通过throw语句抛出DivideByZeroException异常,这个异常被此方法中的catch子句捕获并输出,代码如下。(实例位置:光盘\TM\sl\16\3)

图16.14 分母为0抛出异常

程序的运行结果如图16.14所示。

16.4.3 try…catch…finally语句

将finally语句与try…catch语句结合,形成try…catch…finally语句。finally语句同样以区块的方式存在,它被放在所有try…catch语句的最后面,程序执行完毕,最后都会跳到finally语句区块,执行其中的代码。无论程序是否产生异常,最后都会执行finally语句区块中的程序代码,其基本格式如下。

   try
   {
        被监控的代码
   }
   catch(异常类名 异常变量名)
   {
        异常处理
   }
   …
   finally
   {
        程序代码
   }

对于try…catch…finally语句的理解并不复杂,它只是比try…catch语句多了一个finally语句,如果程序中有一些在任何情形中都必须执行的代码,那么就可以将它们放在finally语句的区块中。

说明

使用catch子句是为了允许处理异常。无论是否引发了异常,使用finally子句即可执行清理代码。如果分配了昂贵或有限的资源(如数据库连接或流),则应将释放这些资源的代码放置在finally块中。

【例16.4】 创建一个控制台应用程序,声明一个string类型变量str,并初始化为“用一生下载你”。然后声明一个object变量obj,将str赋给obj。最后声明一个int类型的变量i,将obj强制转换成int类型后赋给变量i,这样必然会导致转换错误,抛出异常。然后在finally语句中输出“程序执行完毕…”,这样,无论程序是否抛出异常,都会执行finally语句中的代码,代码如下。(实例位置:光盘\TM\sl\16\4)

程序的运行结果为:

    指定的转换无效。
    程序执行完毕…

16.5 小结

本章主要对程序调试及异常处理进行了详细讲解。在讲解过程中,首先对程序调试及异常处理的作用进行了简单介绍,然后重点讲解了常用的程序调试操作及异常处理语句的使用。程序调试和异常处理在程序开发过程中起着非常重要的作用,一个完善的程序,在其开发过程中必然会对可能出现的所有异常进行处理,并进行逐步调试,以保证程序的可用性。通过学习本章,读者应掌握C#中的异常处理语句的使用,并能熟练使用常用的程序调试操作对开发的程序进行调试。

16.6 实践与练习

(1)尝试开发一个程序,要求使用异常处理语句捕捉连接数据库过程中出现的错误。(答案位置:光盘\TM\sl\16\5)

(2)尝试开发一个程序,要求在使用两个不同类型的数据进行加法计算时,使用异常处理语句捕获由于数据类型错误而出现的异常。(答案位置:光盘\TM\sl\16\6)

第3篇 高级应用

第3篇 高级应用

 第17章 面向对象技术高级应用

 第18章 迭代器和分部类

 第19章 泛型的使用

 第20章 文件及数据流技术

 第21章 GDI+图形图像技术

 第22章 Windows打印技术

 第23章 网络编程技术

 第24章 注册表技术

 第25章 线程的使用

本篇介绍面向对象技术高级应用,包括迭代器和分部类、泛型的使用、文件及数据流技术、GDI+图形图像技术、Windows打印技术、网络编程技术、注册表技术以及线程的使用等。学习完这一部分,读者应该能够开发文件流程序、图形图像程序、打印程序、多媒体程序、网络程序和多线程应用程序等。

第17章 面向对象技术高级应用

第17章
面向对象技术高级应用

视频讲解:41分钟

本章将介绍面向对象技术中的几种比较高级的技术,主要包括抽象类与抽象方法、接口、密封类和密封方法等,这些内容相对于前面章节中所讲的内容更复杂,但为了能够使开发人员开发出结构良好、组织严密、扩展性好及运行稳定的程序,它们又是必不可少的。

通过阅读本章,您可以:

 了解抽象类及抽象方法的基本概念

 掌握抽象类及抽象方法的声明及使用方法

 了解接口的基本概念

 掌握接口的声明及使用

 掌握接口的多重继承的实现

 熟悉显式接口成员的实现方法

 了解密封类及密封方法的基本概念

 掌握密封类及密封方法的声明及使用方法

17.1 抽象类与抽象方法

视频讲解:光盘\TM\lx\17\抽象类与抽象方法.exe

通常可以说四边形具有4条边,或者更具体一点,平行四边形是具有对边平行且相等特性的特殊四边形,等腰三角形是腰相等的三角形,这些描述都是合乎情理的,但对于图形对象却不能使用具体的语言进行描述,它有几条边,究竟是什么图形,没有人能说清楚,这种类在C#中被定义为抽象类。在抽象类中声明方法时,如果加上abstract关键字,则为抽象方法。本节将对抽象类及抽象方法进行详细介绍。

17.1.1 抽象类概述及声明

在解决实际问题时,一般将父类定义为抽象类,需要使用这个父类进行继承与多态处理。回想继承和多态原理,继承树中越是在上方的类越抽象,如鸽子类继承鸟类、鸟类继承动物类等。在多态机制中,并不需要将父类初始化对象,我们需要的只是子类对象,所以在Java语言中设置抽象类不可以实例化对象,因为图形类不能抽象出任何一种具体图形,但它的子类却可以。

C#中声明抽象类时需要使用abstract关键字,具体语法格式如下。

    访问修饰符 abstract class 类名:基类或接口
    {
        //类成员
    }

说明

声明抽象类时,除abstract关键字、class关键字和类名外,其他的都是可选项。

其中,abstract是定义抽象类的关键字。

使用abstract关键字定义的类称为抽象类,而使用这个关键字定义的方法称为抽象方法,抽象方法没有方法体,这个方法本身没有任何意义,除非它被重写,而承载这个抽象方法的抽象类就必须被继承,实际上抽象类除了被继承之外没有任何意义。

反过来讲,如果声明一个抽象的方法,就必须将承载这个抽象方法的类定义为抽象类,不可能在非抽象类中获取抽象方法。换句话说,只要类中有一个抽象方法,此类就被标记为抽象类。

抽象类被继承后需要实现其中所有的抽象方法,也就是保证相同的方法名称、参数列表和相同返回值类型创建出非抽象方法,当然也可以是抽象方法。图17.1说明了继承抽象类的关系。

从图17.1中可以看出,继承抽象类的所有子类需要将抽象类中的抽象方法进行覆盖。这样在多态机制中,可以将父类修改为抽象类,将draw()方法设置为抽象方法,然后每个子类都重写这个方法来处理。但这又会出现我们刚探讨多态时讨论的问题,程序中会有太多冗余的代码,同时这样的父类局限性很大,也许某个不需要draw()方法的子类也不得不重写draw()方法。如果将draw()方法放置在另外一个类中,这样让那些需要draw()方法的类继承该类,而不需要draw()方法的类继承图形类,但所有的子类都需要图形类,因为这些类是从图形类中被导出的,同时某些类还需要draw()方法,但是在C#中规定,类不能同时继承多个父类,面临这种问题,接口的概念便出现了。

图17.1 抽象类继承关系

【例17.1】 下面代码声明一个抽象类,该抽象类中包含一个int类型的变量和一个无返回值类型方法。实现代码如下。

    public abstract class myClass
    {
         public int i;
         public void method()
         { }
    }

17.1.2 抽象方法概述及声明

抽象方法就是在声明方法时,加上abstract关键字,声明抽象方法时需要注意以下两点。

 抽象方法必须声明在抽象类中。

 声明抽象方法时,不能使用virtual、static和private修饰符。

抽象方法声明引入了一个新方法,但不提供该方法的实现,由于抽象方法不提供任何实际实现,因此抽象方法的方法体只包含一个分号。

当从抽象类派生一个非抽象类时,需要在非抽象类中重写抽象方法,以提供具体的实现,重写抽象方法时使用override关键字。

【例17.2】 下面代码声明一个抽象类,该抽象类中声明一个抽象方法。实现代码如下。

    public abstract class myClass
    {
         public abstract void method();   //抽象方法
    }

17.1.3 抽象类与抽象方法的使用

本节通过一个实例介绍如何在程序中使用抽象类与抽象方法。

【例17.3】 创建一个控制台应用程序,其中声明一个抽象类myClass,该抽象类中声明了两个属性和一个方法,其中,为两个属性提供了具体实现,方法为抽象方法。然后声明一个派生类DriveClass,继承自myClass,在DriveClass派生类中重写myClass抽象类中的抽象方法,并提供具体的实现。最后在主程序类Program的Main方法中实例化DriveClass派生类的一个对象,使用该对象实例化抽象类,并使用抽象类对象访问抽象类中的属性和派生类中重写的方法。程序代码如下。(实例位置:光盘\TM\sl\17\1)

程序的运行结果为BH0001 TM。

17.2 接口

视频讲解:光盘\TM\lx\17\接口.exe

由于C#中的类不支持多重继承,但是客观世界出现多重继承的情况又比较多,因此为了避免传统的多重继承给程序带来的复杂性等问题,同时保证多重继承带给程序员的诸多好处,提出了接口概念。通过接口可以实现多重继承的功能。本节将对接口进行详细讲解。

17.2.1 接口的概念及声明

接口是抽象类的延伸,可以将它看作是纯粹的抽象类,接口中的所有方法都没有方法体。对于17.1.1小节中遗留的问题,可以将draw()方法封装到一个接口中,使需要draw()方法的类实现这个接口,同时也继承图形类,这就是接口存在的必要性。在图17.2中描述了各个子类继承图形类后使用接口的关系。

图17.2 使用接口继承关系

接口是一种用来定义程序的协议,它描述可属于任何类或结构的一组相关行为。接口可由方法、属性、事件和索引器或这4种成员类型的任何组合构成,但不能包含字段。

类和结构可以像类继承基类或结构一样从接口继承,而且可以继承多个接口。当类或结构继承接口时,它继承成员定义但不继承实现。若要实现接口成员,类中的对应成员必须是公共的、非静态的,并且与接口成员具有相同的名称和签名。类的属性和索引器可以为接口上定义的属性或索引器定义额外的访问器。例如,接口可以声明一个带有get访问器的属性,而实现该接口的类可以声明同时带有get和set访问器的同一属性。但是,如果属性或索引器使用显式实现,则访问器必须匹配。

接口可以继承其他接口,类可以通过其继承的基类或接口多次继承某个接口,在这种情况下,如果将该接口声明为新类的一部分,则类只能实现该接口一次。如果没有将继承的接口声明为新类的一部分,其实现将由声明它的基类提供。基类可以使用虚拟成员实现接口成员。在这种情况下,继承接口的类可通过重写虚拟成员来更改接口行为。

说明

接口可以包含方法、属性、索引器和事件作为成员,但是并不能设置这些成员的具体值,也就是说,只能定义,不能给它里面定义的东西赋值。

综上所述,接口具有以下特征。

 接口类似于抽象基类:继承接口的任何非抽象类型都必须实现接口的所有成员。

 不能直接实例化接口。

 接口可以包含事件、索引器、方法和属性。

 接口不包含方法的实现。

 类和结构可从多个接口继承。

 接口自身可从多个接口继承。

C#中声明接口时,使用interface关键字,其语法格式如下。

    修饰符 interface 接口名称: 继承的接口列表
    {
        接口内容;
    }

说明

① 声明接口时,除interface关键字和接口名称外,其他的都是可选项。

② 可以使用new、public、protected、internal和private等修饰符声明接口,但接口成员必须是公共的。

【例17.4】 下面的代码声明了一个接口,该接口中包含编号和姓名两个属性,还包含一个自定义方法ShowInfo,该方法用来显示定义的编号和属性,代码如下。

    interface ImyInterface
    {
         string ID//编号(可读可写)
         {
              get;
              set;
         }
         string Name//姓名(可读可写)
         {
              get;
              set;
         }
         void ShowInfo();        //显示定义的编号和姓名
    }

17.2.2 接口的实现与继承

接口的实现通过类继承来实现,一个类虽然只能继承一个基类,但可以继承任意接口。声明实现接口的类时,需要在基类列表中包含类所实现的接口的名称。

【例17.5】 创建一个控制台应用程序,该程序在例17.4的基础上实现,Program类继承自接口ImyInterface,并实现了该接口中的所有属性和方法,然后在Main方法中实例化Program类的一个对象,并使用该对象实例化ImyInterface接口,最后通过实例化的接口对象访问派生类中的属性和方法。程序代码如下。(实例位置:光盘\TM\sl\17\2)

按Ctrl+F5键查看运行结果,如图17.3所示。

图17.3 接口的实现实例运行结果

上面的实例中只继承了一个接口,接口还可以多重继承,使用多重继承时,要继承的接口之间用逗号(,)分隔。

【例17.6】 创建一个控制台应用程序,其中声明了3个接口IPeople、ITeacher和IStudent,其中,ITeacher和IStudent继承自IPeople,然后使用Program类继承这3个接口,并分别实现这3个接口中的属性和方法。程序代码如下。(实例位置:光盘\TM\sl\17\3)

按Ctrl+F5键查看运行结果,如图17.4所示。

图17.4 接口的实现实例运行结果

17.2.3 显式接口成员实现

如果类实现两个接口,并且这两个接口包含具有相同签名的成员,那么在类中实现该成员将导致两个接口都使用该成员作为它们的实现。然而,如果两个接口成员实现不同的功能,则可能会导致其中一个接口的实现不正确或两个接口的实现都不正确,这时可以显式地实现接口成员,即创建一个仅通过该接口调用并且特定于该接口的类成员。显式接口成员实现是使用接口名称和一个句点命名该类成员来实现的。

【例17.7】 创建一个控制台应用程序,其中声明了两个接口ImyInterface1和ImyInterface2,在这两个接口中声明了一个同名方法Add,然后定义一个类MyClass,该类继承自已经声明的两个接口,在MyClass类中实现接口中的方法时,由于ImyInterface1和ImyInterface2接口中声明的方法名相同,这里使用了显式接口成员实现,最后在主程序类Program的Main方法中使用接口对象调用接口中定义的方法。程序代码如下。(实例位置:光盘\TM\sl\17\4)

程序的运行结果为:

    8
    15

注意

① 显式接口成员实现中不能包含访问修饰符、abstract、virtual、override或static修饰符。

② 显式接口成员属于接口的成员,而不是类的成员,因此,不能使用类对象直接访问,只能通过接口对象来访问。

17.2.4 抽象类与接口

抽象类和接口都包含可以由派生类继承的成员,它们都不能直接实例化,但可以声明它们的变量。如果这样做,就可以使用多态性把继承这两种类型的对象指定给它们的变量。接着通过这些变量来使用这些类型的成员,但不能直接访问派生类中的其他成员。

抽象类和接口的区别主要有以下几点:

 它们的派生类只能继承一个基类,即只能直接继承一个抽象类,但可以继承任意多个接口。

 抽象类中可以定义成员的实现,但接口中不可以。

 抽象类中可以包含字段、构造函数、析构函数、静态成员或常量等,接口中不可以。

 抽象类中的成员可以是私有的(只要它们不是抽象的)、受保护的、内部的或受保护的内部成员(受保护的内部成员只能在应用程序的代码或派生类中访问),但接口中的成员必须是公共的。

说明

抽象类和接口这两种类型用于完全不同的目的。抽象类主要用作对象系列的基类,共享某些主要特性,例如共同的目的和结构;接口则主要用于类,这些类在基础水平上有所不同,但仍可以完成某些相同的任务。

17.3 密封类与密封方法

视频讲解:光盘\TM\lx\17\密封类与密封方法.exe

如果所有的类都可以被继承,那么很容易导致继承的滥用,进而使类的层次结构体系变得十分复杂,这样使开发人员对类的理解和使用变得十分困难。为了避免滥用继承,C#中提出了密封类的概念。本节将对类和方法的密封进行详细介绍。

17.3.1 密封类概述及声明

密封类可以用来限制扩展性,如果密封了某个类,则其他类不能从该类继承;如果密封了某个成员,则派生类不能重写该成员的实现。默认情况下,不应密封类型和成员。密封可以防止对库的类型和成员进行自定义,但也会影响某些开发人员对可用性的认识。

C#中使用密封类时,如果类满足如下条件,则应将其密封。

 类包含带有安全敏感信息的继承的受保护成员。

 类继承多个虚成员,并且密封每个成员的开发和测试开销明显大于密封整个类。

 类是一个要求使用反射进行快速搜索的属性。密封属性可提高反射在检索属性时的性能。

C#中声明密封类时需要使用sealed关键字,具体语法格式如下。

    访问修饰符 sealed class 类名:基类或接口
    {
        //类成员
    }

说明

① 密封类不能作为基类被继承,但它可以继承别的类或接口。

② 在密封类中不能声明受保护成员或虚成员,因为受保护成员只能从派生类进行访问,而虚成员只能在派生类中重写。

③ 由于密封类的不可继承性,因此密封类不能声明为抽象的,即sealed修饰符不能与abstract修饰符同时使用。

【例17.8】 下面的代码声明一个密封类,该密封类中包含一个int类型的变量和一个无返回值类型的方法,它们只能通过实例化密封类的对象来访问,而不能被继承。实现代码如下。

    public sealed class myClass         //声明密封类
    {
         public int = 0;
         public void method()
         {
              Console.WriteLine("密封类");
         }
    }

17.3.2 密封方法概述及声明

并不是每个方法都可以声明为密封方法,密封方法只能用于对基类的虚方法进行实现,并提供具体的实现。所以,声明密封方法时,sealed修饰符总是和override修饰符同时使用。

【例17.9】 下面的代码声明一个类myClass1,该类中声明一个虚方法Method,然后声明一个密封类myClass2,该类继承自myClass1类,在密封类myClass2中密封并重写myClass1类中的虚方法Method。实现代码如下。

    public class myClass1
    {
         public virtual void Method()
         {
              Console.WriteLine("基类中的虚方法");
         }
    }
    public sealed class myClass2:myClass1
    {
         public sealed override void Method()      //密封并重写基类中的虚方法 Method
         {
              base.Method();
              Console.WriteLine("密封类中重写后的方法");
         }
    }

说明

上面的代码中,密封并重写基类中的虚方法Method时,用到了“base.Method();”语句,该语句表示调用基类中的Method方法。base关键字主要是为派生类调用基类成员提供一种简写的方法。

17.3.3 密封类与密封方法的使用

密封类除了不能被继承外,与非密封类的用法大致相同,而密封方法则必须通过重写基类中的虚方法来实现。下面通过一个实例讲解如何在程序中使用密封类和密封方法。

【例17.10】 创建一个控制台应用程序,其中声明一个类myClass1,该类中声明了一个虚方法ShowInfo,用来显示信息。然后声明一个密封类myClass2,继承自myClass1类,在myClass2密封类中声明两个公共属性,分别用来表示用户编号和名称,然后密封并重写myClass1基类中的虚方法ShowInfo,并提供具体的实现。最后在主程序类Program的Main方法中实例化MyClass2密封类的一个对象,然后使用该对象访问myClass2密封类中的公共属性和密封方法。程序代码如下。(实例位置:光盘\TM\sl\17\5)

程序的运行结果为BH0001 TM。

17.4 小结

本章介绍了面向对象技术的高级应用,主要包括抽象类、抽象方法、接口、密封类及密封方法等内容。接口的存在可以使C#中存在多重继承,这样使程序的结构更加合理。abstract关键字和sealed关键字分别实现了抽象和密封的定义,这两种方法使程序的设计更加严密。这里需要注意的是,在声明密封方法时,必须通过重写基类中的虚方法实现。

17.5 实践与练习

(1)尝试开发一个程序,要求定义一个接口,该接口中封装了矩形的长和宽,而且还包含一个自定义的方法,用来计算矩形的面积,然后定义一个类,继承自该接口,在该类中实现接口中的自定义方法。(答案位置:光盘\TM\sl\17\6)

(2)尝试开发一个程序,要求自定义一个抽象类,用来计算圆形的面积。(答案位置:光盘\TM\sl\17\7)

(3)尝试开发一个程序,要求使用密封类封装用户名和密码,然后在主程序Program类中通过密封类的对象对用户名和密码动态赋值并输出。(答案位置:光盘\TM\sl\17\8)

第18章 迭代器和分部类

第18章
迭代器和分部类

视频讲解:20分钟

迭代器在集合类中经常使用,而分部类则提供了一种将一个类分成多个类的方法,这对于有大量代码的类非常实用。本章将对C#中的迭代器和分部类进行讲解。

通过阅读本章,您可以:

 了解什么是迭代器

 掌握如何创建迭代器

 了解什么是分部类

 掌握如何使用分部类

18.1 迭代器

视频讲解:光盘\TM\lx\18\迭代器.exe

18.1.1 迭代器概述

迭代器是可以返回相同类型的值的有序序列的一段代码,可用作方法、运算符或get访问器的代码体。迭代器代码使用yield return语句依次返回每个元素,yield break语句将终止迭代。可以在类中实现多个迭代器,每个迭代器都必须像任何类成员一样有唯一的名称,并且可以在foreach语句中被客户端代码调用。迭代器的返回类型必须为IEnumerable或IEnumerator中的任意一种。

图18.1 用迭代法进行报数

可用一个形象的例子说明一下迭代器,当士兵排好队时,都必须从头到尾进行报数,缺一不可。如图18.1所示。

18.1.2 迭代器的使用

创建迭代器最常用的方法是对IEnumerator接口实现GetEnumerator方法,下面通过一个实例演示如何使用迭代器。

【例18.1】 创建一个Windows应用程序,向窗体中添加一个RichTextBox控件。创建一个名为Family的类,其继承IEnumerable接口,该接口公开枚举数,该枚举数支持在非泛型集合上进行简单迭代,然后对IEnumerator接口实现GetEnumerator方法创建迭代器,最后在窗体的Load事件中使用foreach语句遍历Family类中的内容并输出,代码如下。(实例位置:光盘\TM\sl\18\1)

程序的运行结果如图18.2所示。

图18.2 迭代器的使用

18.2 分部类

视频讲解:光盘\TM\lx\18\分部类.exe

18.2.1 分部类概述

分部类使程序的结构更加合理,代码的组织更加紧密。可以将类、结构或接口的定义拆分到两个或多个源文件中。每个源文件包含类定义的一部分,编译应用程序时,Visual Studio 2015会把所有部分组合起来,这样的类被称为分部类。分部类主要应用在以下方面:

 当项目比较庞大时,使用分部类可以拆分一个类至几个文件中,这样的处理可以使不同的开发人员同时进行工作,避免了效率的低下。

 使用自动生成的源时,无须重新创建源文件即可将代码添加到类中。Visual Studio 2015在创建Windows窗体和Web服务包装代码等时都使用此方法。开发人员无须编辑Visual Studio 2015所创建的文件,即可创建使用这些类的代码。

分部类就相当于将一个会计部门(类、结构或接口)分成两个部门,这两个部门可以单独对公司各部门的账目进行审核。在繁忙时期,两个会计部门也可以相互调动人员(这里的人员相当于类中的方法、属性、变量等),或是合成一个整体进行工作。

18.2.2 分部类的使用

定义分部类时需要使用partial关键字,分部类的每个部分都必须包含一个partial关键字,并且其声明必须与其他部分位于同一命名空间。开发分部类时,要成为同一类型的各个部分的所有分部类型定义都必须在同一程序集或同一模块(.exe或.dll文件)中进行定义,分部类定义不能跨越多个模块。

【例18.2】 创建一个Windows应用程序,向窗体中添加3个TextBox控件,分别用于输入要进行算术运算的值以及显示运算后的结果。再向窗体中添加一个ComboBox控件和一个Button控件,分别用于选择执行哪种运算和执行运算。通过分部类创建4个方法分别用于执行加、减、乘、除运算,并将返回运算后的结果,代码如下。(实例位置:光盘\TM\sl\18\2)

程序的运行结果如图18.3所示。

图18.3 分部类的使用

说明

在设置分部类时,各个部分必须具有相同的可访问性,如public、private等。

18.3 小结

本章介绍了C#中的迭代器和分部类,它们的实用性很强,为开发程序提供了更方便、灵活的选择。迭代器可以使开发人员方便地使用foreach语句访问类中的字段值,而分部类使程序的结构更加灵活,协同工作更加方便。迭代器和分部类的知识并不复杂,通过本章所提供的实例,希望读者能够灵活运用这些知识。

18.4 实践与练习

(1)尝试开发一个程序,要求使用迭代器输出春、夏、秋、冬4个季节。(答案位置:光盘\TM\sl\18\3)

(2)尝试开发一个程序,要求在分部类中定义两个方法,分别用于输出“用一生下载你”和“芸烨湘枫”两个不同的字符串。(答案位置:光盘\TM\sl\18\4)

第19章 泛型的使用

第19章
泛型的使用

视频讲解:23分钟

泛型是C#和公共语言运行库(CLR)中的一个功能,泛型的概念在Java语言的较新版本中也已经被支持,这是一种可以使程序支持不同类型的技术。它将类型参数的概念引入.NET Framework,类型参数将一个或多个类型的指定推迟到客户端代码声明并实例化该类或方法。

通过阅读本章,您可以:

 了解什么是泛型

 了解泛型的类型参数T

 掌握如何使用泛型

 掌握如何使用泛型接口

 掌握如何使用泛型方法

19.1 泛型概述

视频讲解:光盘\TM\lx\19\泛型概述.exe

泛型是用于处理算法、数据结构的一种编程方法。泛型的目标是采用广泛适用和可交互性的形式来表示算法和数据结构,以使它们能够直接用于软件构造。泛型类、结构、接口、委托和方法可以根据它们存储和操作的数据的类型来进行参数化。泛型能在编译时提供强大的类型检查,减少数据类型之间的显示转换、装箱操作和运行时的类型检查。泛型类和泛型方法同时具备可重用性、类型安全和效率高等特性,这是非泛型类和非泛型方法无法具备的。泛型通常用在集合和在集合上运行的方法中。

泛型主要是提高了代码的重用性,例如,可以将泛型看成是一个可以回收的包装箱A。如果在包装箱A上贴上苹果标签,就可以在包装箱A里装上苹果进行发送;如果在包装箱A上贴上地瓜标签,就可以在包装箱A里装上地瓜进行发送。

19.2 泛型的使用

视频讲解:光盘\TM\lx\19\泛型的使用.exe

在以下内容中将会详细介绍泛型的类型参数T,以及如何创建泛型接口和泛型方法,并且通过实例演示泛型接口和泛型方法在程序中的应用。

19.2.1 类型参数T

泛型的类型参数T可以看作是一个占位符,它不是一种类型,它仅代表了某种可能的类型。在定义泛型时T出现的位置可以在使用时用任何类型来代替。类型参数T的命名准则如下。

 使用描述性名称命名泛型类型参数,除非单个字母名称完全可以让人了解它表示的含义,而描述性名称不会有更多的意义。

【例19.1】 使用代表一定意义的单词作为类型参数T的名称,代码如下。

    public interface ISessionChannel<Session>
    public delegate TOutput Converter<Input, Output>

 将T作为描述性类型参数名的前缀。

【例19.2】 使用T作为类型参数名的前缀,代码如下。

    public interface ISessionChannel<TSession>
    {
         TSession Session { get; }
    }

19.2.2 泛型接口

泛型接口的声明形式如下。

    interface 【接口名】<T>
    {
         【接口体】
    }

声明泛型接口时,与声明一般接口的唯一区别是增加了一个<T>。一般来说,声明泛型接口与声明非泛型接口遵循相同的规则。泛型类型声明所实现的接口必须对所有可能的构造类型都保持唯一。否则就无法确定该为某些构造类型调用哪个方法。

说明

在实例化泛型时也可以使用约束对类型参数的类型种类施加限制,约束是使用where上下文关键字指定的。下面列出了6种类型的约束。

(1)T:结构—类型参数必须是值类型。可以指定除Nullable以外的任何值类型。

(2)T:类—类型参数必须是引用类型。这一点也适用于任何类、接口、委托或数组类型。

(3)T:new()—类型参数必须具有无参数的公共构造函数。当与其他约束一起使用时,new()约束必须最后指定。

(4)T:<基类名>—类型参数必须是指定的基类或派生自指定的基类。

(5)T:<接口名称>—类型参数必须是指定的接口或实现指定的接口。可以指定多个接口约束。约束接口也可以是泛型的。

(6)T:U—为T提供的类型参数必须是为U提供的参数或派生自为U提供的参数。这称为裸类型约束。

【例19.3】 创建一个控制台应用程序,首先创建一个Factory类,在此类中建立一个CreateInstance方法。然后再创建一个泛型接口,在这个泛型接口中调用CreateInstance方法。根据类型参数T,获取其类型,代码如下。(实例位置:光盘\TM\sl\19\1)

程序的运行结果如图19.1所示。

图19.1 泛型接口应用

19.2.3 泛型方法

泛型方法的声明形式如下。

    【修饰符】 Void 【方法名】<类型参数 T>
    {
         【方法体】
    }

泛型方法是在声明中包括了类型参数T的方法。泛型方法可以在类、结构或接口声明中声明,这些类、结构或接口本身可以是泛型或非泛型。如果在泛型类型声明中声明泛型方法,则方法体可以同时引用该方法的类型参数T和包含该方法的声明的类型参数T。

说明

泛型方法可以使用多类型参数进行重载。

【例19.4】 创建一个控制台应用程序,通过定义一个泛型方法,查找数组中某个数字的位置,代码如下。(实例位置:光盘\TM\sl\19\2)

程序的运行结果为“6在数组中的位置为5”。

19.3 小结

泛型的应用比较复杂,既有泛型类,也有泛型接口、泛型方法等。虽然合理地使用泛型类可以减少重复的代码,但这往往需要高级的技巧和合理的设计。这些内容虽然对初学者而言比较难,但是对于高质量编程是必不可少的。

19.4 实践与练习

(1)尝试开发一个程序,要求在程序中使用泛型存储10以下的数字,然后再循环输出。(答案位置:光盘\TM\sl\19\3)

(2)尝试开发一个程序,要求在程序中实现泛型类的继承,其用法与普通类的继承基本相同。(答案位置:光盘\TM\sl\19\4)

第20章 文件及数据流技术

第20章
文件及数据流技术

视频讲解:83分钟

在软件开发过程中经常需要对文件及文件夹进行操作,例如读写、移动、复制、删除文件及创建、移动、删除、遍历文件夹等,C#中与文件、文件夹及文件读写有关的类都位于System.IO命名空间下。本章将详细介绍如何在C#中对文件、文件夹进行操作,以及如何对文件进行数据流读写。

通过阅读本章,您可以:

 了解System.IO命名空间中的常用类

 掌握File类和Directory类的使用

 掌握FileInfo类和DirectoryInfo类的使用

 掌握文件的基本操作

 掌握文件夹的基本操作

 了解流操作类

 掌握文件流类的使用

 了解如何对文本文件进行写入与读取

 了解如何对二进制文件进行写入与读取

20.1 System.IO命名空间

视频讲解:光盘\TM\lx\20\System.IO命名空间.exe

System.IO命名空间包含允许在数据流和文件上进行同步和异步读取及写入的类型。这里需要注意文件和流的差异,文件是一些具有永久存储及特定顺序的字节组成的一个有序的、具有名称的集合,因此,关于文件,人们常会想到目录路径、磁盘存储、文件和目录名等方面。相反,流提供一种向后备存储写入字节和从后备存储读取字节的方式。后备存储可以为多种存储媒介之一,正如除磁盘外存在多种后备存储一样,除文件流之外也存在多种流。例如,网络流、内存流和磁带流等。

System.IO命名空间中的类及说明如表20.1所示。

表20.1 System.IO命名空间中的类及说明

20.1.1 File类和Directory类

File类和Directory类分别用来对文件和各种目录进行操作,这两个类可以被实例化,但不能被其他类继承。

File类和Directory类就好比一个工厂,文件和文件夹就好比工厂所制作的产品,而工厂和产品的关系主要表现在以下几个方面:工厂可以自行开发产品(文件和文件夹的创建);也可以对该产品进行批量生产(文件和文件夹的复制);将产品进行销售(文件和文件夹的移动);以及将有质量问题的产品进行回收消除(文件和文件夹删除)。

本节将对File类和Directory类进行详细介绍。

1.File类

File类支持对文件的基本操作,它包括用于创建、复制、删除、移动和打开文件的静态方法,并协助创建FileStream对象。File类中一共包含40多个方法,这里只列出其常用的几种,如表20.2所示。

表20.2 File类的常用方法及说明

说明

① 由于File类中的所有方法都是静态的,所以如果只想执行一个操作,那么使用File类中方法的效率比使用相应的FileInfo类中的方法可能更高。

② File类的静态方法对所有方法都执行安全检查,因此如果打算多次重用某个对象,可考虑改用FileInfo类中的相应方法,因为并不总是需要安全检查。

【例20.1】 下面演示如何使用File类中的方法,程序开发步骤如下。(实例位置:光盘\TM\sl\20\1)

(1)新建一个Windows应用程序,命名为Test01,默认窗体为Form1.cs。

(2)在Form1窗体中添加一个TextBox控件和一个Button控件,其中,TextBox控件用来输入要创建的文件路径及名称,Button控件用来执行创建文件操作。

(3)程序主要代码如下。

程序的运行结果如图20.1所示。

图20.1 File类的使用

注意

使用与文件、文件夹及流相关的类时,首先需要添加System.IO命名空间。

2.Directory类

Directory类公开了用于创建、移动、枚举、删除目录和子目录的静态方法,这里介绍一些该类中的常用方法,如表20.3所示。

表20.3 Directory类的常用方法及说明

【例20.2】 下面演示如何使用Directory类中的方法,程序开发步骤如下。(实例位置:光盘\TM\sl\20\2)

(1)新建一个Windows应用程序,命名为Test02,默认窗体为Form1.cs。

(2)在Form1窗体中添加一个TextBox控件和一个Button控件,其中,TextBox控件用来输入要创建的文件夹路径及名称,Button控件用来执行创建文件夹操作。

(3)程序主要代码如下。

    private void button1_Click(object sender, EventArgs e)
    {
         if (textBox1.Text == string.Empty)    //判断输入的文件夹名称是否为空
         {
              MessageBox.Show("文件夹名称不能为空!");
         }
         else
         {
             if (Directory.Exists(textBox1.Text)) //使用 Directory 类的 Exists 方法判断要创建的文件夹是否存在
             {
                   MessageBox.Show("该文件夹已经存在");
             }
             else
             {
                   Directory.CreateDirectory(textBox1.Text); //使用 Directory 类的 CreateDirectory 方法创建文件夹
             }
         }
    }

程序的运行结果如图20.2所示。

图20.2 Directory类的使用

说明

在用Directory类对文件夹进行操作时,其文件夹的路径必须正确,否则会触发异常。

20.1.2 FileInfo类和DirectoryInfo类

使用FileInfo类和DirectoryInfo类可以方便地对文件和文件夹进行操作,本节将对这两个类进行详细介绍。

1.FileInfo类

FileInfo类和File类之间许多方法调用都是相同的,但是FileInfo类没有静态方法,该类中的方法仅可以用于实例化的对象。File类是静态类,所以它的调用需要字符串参数为每一个方法调用规定文件位置。因此如果要在对象上进行单一方法调用,则可以使用静态File类,在这种情况下静态调用速度要快一些,因为.NET框架不必执行实例化新对象并调用其方法的过程。如果要在文件上执行几种操作,则实例化FileInfo对象使用其方法就更好一些。这样会提高效率,因为对象将在文件系统上引用正确的文件,而静态类就必须每次都寻找文件。

在20.1.1节中已用工厂对File类和Directory类进行讲解,这里还是以工厂对FileInfo类和DirectoryInfo类进行一下讲解。这两个类就好比工厂对产品(文件和文件夹)的记录清单,如产品名称(文件和文件夹的名称)、产品的库地址和销售商地址(文件和文件夹的目录)、产品的创建及检测时间(文件和文件夹的访问时间)、产品二次加工的时间(文件和文件夹的写入时间)以及当前产品为成品(文件只读)等。

FileInfo类的常用属性及说明如表20.4所示。

表20.4 FileInfo类的常用属性及说明

说明

如果想要对某个对象进行重复操作,应使用FileInfo类。

【例20.3】 下面演示如何使用FileInfo类中的属性及方法,程序开发步骤如下。(实例位置:光盘\TM\sl\20\3)

(1)新建一个Windows应用程序,命名为Test03,默认窗体为Form1.cs。

(2)在Form1窗体中添加一个TextBox控件和一个Button控件,其中,TextBox控件用来输入要创建的文件路径及名称,Button控件用来执行创建文件操作。

(3)程序主要代码如下。

程序的运行结果如图20.3所示。

图20.3 FileInfo类的使用

2.DirectoryInfo类

DirectoryInfo类和Directory类之间的关系,与FileInfo类和File类之间的关系十分类似,这里不再赘述。下面介绍DirectoryInfo类的常用属性。

DirectoryInfo类的常用属性及说明如表20.5所示。

表20.5 DirectoryInfo类的常用属性及说明

【例20.4】 下面演示如何使用DirectoryInfo类中的属性及方法,程序开发步骤如下。(实例位置:光盘\TM\sl\20\4)

(1)新建一个Windows应用程序,命名为Test04,默认窗体为Form1.cs。

(2)在Form1窗体中添加一个TextBox控件和一个Button控件,其中,TextBox控件用来输入要创建的文件夹路径及名称,Button控件用来执行创建文件夹操作。

(3)程序主要代码如下。

程序的运行结果如图20.4所示。

图20.4 DirectoryInfo类的使用

20.2 文件基本操作

视频讲解:光盘\TM\lx\20\文件基本操作.exe

对于文件的基本操作大体可以分为判断文件是否存在、创建文件、复制或移动文件、删除文件以及获取文件基本信息。通过对本节的学习,读者可以轻松地掌握文件的这几种基本操作。

20.2.1 判断文件是否存在

判断文件是否存在时,可以使用File类的Exists方法或者FileInfo类的Exists属性来实现,下面分别对它们进行介绍。

1.File类的Exists方法

确定指定的文件是否存在,语法如下。

    public static bool Exists(string path)

 path:要检查的文件。

 返回值:如果调用方具有要求的权限并且path包含现有文件的名称,则为true;否则为false。如果path为空引用或零长度字符串,则此方法也返回false。如果调用方不具有读取指定文件所需的足够权限,则不引发异常并且该方法返回false,这与path是否存在无关。

说明

在用Exists方法时,如果路径为空,则会触发异常。

【例20.5】 下面代码使用File类的Exists方法判断C盘根目录下是否存在Test.txt文件。

    File.Exists("C:\\Test.txt");
2.FileInfo类的Exists属性

获取指示文件是否存在的值,语法如下。

    public override bool Exists { get; }

属性值:如果该文件存在,则为true;如果该文件不存在或该文件是目录,则为false。

【例20.6】 下面的代码首先实例化一个FileInfo对象,然后使用该对象调用FileInfo类中的Exists属性判断C盘根目录下是否存在Test.txt文件。

    FileInfo finfo = new FileInfo("C:\\Test.txt");
    if (finfo.Exists)
    {}

20.2.2 创建文件

创建文件可以使用File类的Create方法或者FileInfo类的Create方法来实现,下面分别对它们进行介绍。

1.File类的Create方法

该方法为可重载方法,它有以下4种重载形式。

    public static FileStream Create(string path)
    public static FileStream Create(string path,int bufferSize)
    public static FileStream Create(string path,int bufferSize,FileOptions options)
    public static FileStream Create(string path,int bufferSize,FileOptions options,FileSecurity fileSecurity)

参数说明如表20.6所示。

表20.6 File类的Create方法参数说明

说明

在用create方法创建目录时,如果路径为空,或是文件夹为只读,则会触发异常。

【例20.7】 下面的代码调用File类的Create方法在C盘根目录下创建一个Test.txt文本文件。

    File.Create("C:\\Test.txt");
2.FileInfo类的Create方法

语法如下:

    public FileStream Create()

返回值:新文件,默认情况下,该方法将向所有用户授予对新文件的完全读写访问权限。

【例20.8】 下面的代码首先实例化了一个FileInfo对象,然后使用该对象调用FileInfo类的Create方法在C盘根目录下创建一个Test.txt文本文件。

    FileInfo finfo = new FileInfo("C:\\Test.txt");
    finfo.Create();

20.2.3 复制或移动文件

复制或移动文件时,可以使用File类的Copy方法、Move方法或者FileInfo类的CopyTo方法、MoveTo方法来实现,下面分别对它们进行介绍。

1.File类的Copy方法

该方法为可重载方法,它有以下两种重载形式。

    public static void Copy(string sourceFileName,string destFileName)
    public static void Copy(string sourceFileName,string destFileName,bool overwrite)

 sourceFileName:要复制的文件。

 destFileName:目标文件的名称,不能是目录。如果是第一种重载形式,则该参数不能是现有文件。

 overwrite:如果可以改写目标文件,则为true;否则为false。

【例20.9】 下面的代码调用File类的Copy方法将C盘根目录下的Test.txt文本文件复制到D盘根目录下。

    File.Copy("C:\\Test.txt","D:\\Test.txt");
2.File类的Move方法

将指定文件移到新位置,并提供指定新文件名的选项,语法如下。

    public static void Move(string sourceFileName,string destFileName)

 sourceFileName:要移动的文件的名称。

 destFileName:文件的新路径。

说明

在对文件进行移动时,如果目标文件已存在,则发生异常。

【例20.10】 下面的代码调用File类的Move方法将C盘根目录下的Test.txt文本文件移动到D盘根目录下。

    File.Move("C:\\Test.txt","D:\\Test.txt") ;
3.FileInfo类的CopyTo方法

该方法为可重载方法,它有以下两种重载形式。

    public FileInfo CopyTo(string destFileName)
    public FileInfo CopyTo(string destFileName,bool overwrite)

 destFileName:要复制到的新文件的名称。

 overwrite:若为true,则允许改写现有文件;否则为false。

 返回值:第一种重载形式的返回值为带有完全限定路径的新文件。第二种重载形式的返回值为新文件,或者如果overwrite为true,则为现有文件的改写。如果文件存在,且overwrite为false,则会发生IOException。

【例20.11】 下面的代码首先实例化了一个FileInfo对象,然后使用该对象调用FileInfo类的CopyTo方法将C盘根目录下的Test.txt文本文件复制到D盘根目录下。如果D盘根目录下已经存在Test.txt文本文件,则将其替换。

    FileInfo finfo = new FileInfo("C:\\Test.txt");
    finfo. CopyTo("D:\\Test.txt",true);
4.FileInfo类的MoveTo方法

将指定文件移到新位置,并提供指定新文件名的选项,语法如下。

    public void MoveTo(string destFileName)

destFileName:要将文件移动到路径,可以指定另一个文件名。

【例20.12】 下面的代码首先实例化了一个FileInfo对象,然后使用该对象调用FileInfo类的MoveTo方法将C盘根目录下的Test.txt文本文件移动到D盘根目录下。

    FileInfo finfo = new FileInfo("C:\\Test.txt");
    finfo. MoveTo("D:\\Test.txt") ;

20.2.4 删除文件

删除文件可以使用File类的Delete方法或者FileInfo类的Delete方法来实现,下面分别对它们进行介绍。

1.File类的Delete方法

该方法是指删除指定的文件,语法如下。

    public static void Delete(string path)

path:要删除的文件的名称。

说明

如果当前删除的文件正在被使用,则删除时会发生异常。

【例20.13】 下面的代码调用File类的Delete方法删除C盘根目录下的Test.txt文本文件。

    File.Delete("C:\\Test.txt");
2.FileInfo类的Delete方法

该方法是指永久删除文件,语法如下。

    public override void Delete()

【例20.14】 下面的代码首先实例化了一个FileInfo对象,然后使用该对象调用FileInfo类的Delete方法删除C盘根目录下的Test.txt文本文件。

    FileInfo finfo = new FileInfo("C:\\Test.txt");
    finfo. Delete ();

20.2.5 获取文件的基本信息

获取文件的基本信息时,主要用到了FileInfo类中的各种属性。下面通过一个实例说明如何获取文件的基本信息。

【例20.15】 下面演示如何获取文件的基本信息,程序开发步骤如下。(实例位置:光盘\TM\sl\20\5)

(1)新建一个Windows应用程序,命名为Test05,默认窗体为Form1.cs。

(2)在Form1窗体中添加一个OpenFileDialog控件、一个TextBox控件和一个Button控件,其中,OpenFileDialog控件用来显示“打开”对话框,TextBox控件用来显示选择的文件名,Button控件用来打开“打开”对话框并获取选择文件的基本信息。

(3)程序主要代码如下。

    private void button1_Click(object sender, EventArgs e)
    {
         if (openFileDialog1.ShowDialog() == DialogResult.OK)
         {
              textBox1.Text = openFileDialog1.FileName;
              FileInfo finfo = new FileInfo(textBox1.Text);             //实例化 FileInfo 对象
              string strCTime, strLATime, strLWTime, strName, strFName, strDName, strISRead;
              long lgLength; 
              strCTime = finfo.CreationTime.ToShortDateString();        //获取文件创建时间
              strLATime = finfo.LastAccessTime.ToShortDateString();     //获取上次访问该文件的时间
              strLWTime = finfo.LastWriteTime.ToShortDateString();      //获取上次写入文件的时间
              strName = finfo.Name;                                     //获取文件名称
              strFName = finfo.FullName;                                //获取文件的完整目录
              strDName = finfo.DirectoryName;                           //获取文件的完整路径
              strISRead = finfo.IsReadOnly.ToString();                  //获取文件是否只读
              lgLength = finfo.Length;                                  //获取文件长度
              MessageBox.Show("文件信息:\n 创建时间:" + strCTime + " 上次访问时间:" + strLATime + "\n 上次
         写入时间:" + strLWTime + " 文件名称:" + strName + "\n 完整目录:" + strFName + "\n 完整路径:" + strDName
         + "\n 是否只读:" + strISRead + " 文件长度:" + lgLength);
              }
    }

运行程序,单击“浏览”按钮,弹出“打开”对话框,选择文件,单击“打开”按钮,在弹出的对话框中将显示所选文件的基本信息。程序的运行结果如图20.5所示。

图20.5 获取文件的基本信息

20.3 文件夹的基本操作

视频讲解:光盘\TM\lx\20\文件夹的基本操作.exe

对于文件夹的基本操作大体可以分为:判断文件夹是否存在、创建文件夹、移动文件夹、删除文件夹以及遍历文件夹中的文件。通过对本节的学习,读者可以轻松地掌握文件夹的这几种基本操作。

20.3.1 判断文件夹是否存在

判断文件夹是否存在时,可以使用Directory类的Exists方法或者DirectoryInfo类的Exists属性来实现,下面分别对它们进行介绍。

1.Directory类的Exists方法

确定给定路径是否引用磁盘上的现有目录,语法如下。

    public static bool Exists(string path)

 path:要测试的路径。

 返回值:如果path引用现有目录,则为true;否则为false。

说明

允许path参数指定相对或绝对路径信息。相对路径信息被解释为相对于当前的工作目录。

【例20.16】 下面的代码使用Directory类的Exists方法判断C盘根目录下是否存在Test文件夹。

    Directory.Exists("C:\\Test ");
2.DirectoryInfo类的Exists属性

获取指示目录是否存在的值,语法如下。

    public override bool Exists { get; }

属性值:如果目录存在,则为true;否则为false。

【例20.17】 下面的代码首先实例化一个DirectoryInfo对象,然后使用该对象调用DirectoryInfo类中的Exists属性判断C盘根目录下是否存在Test文件夹。

    DirectoryInfo dinfo = new DirectoryInfo ("C:\\Test");
    if (dinfo.Exists)
    {}

20.3.2 创建文件夹

创建文件夹可以使用Directory类的CreateDirectory方法或者DirectoryInfo类的Create方法来实现,下面分别对它们进行介绍。

1.Directory类的CreateDirectory方法

该方法为可重载方法,它有以下两种重载形式。

    public static DirectoryInfo CreateDirectory(string path)
    public static DirectoryInfo CreateDirectory(string path,DirectorySecurity directorySecurity)

 path:要创建的目录路径。

 directorySecurity:要应用于此目录的访问控制。

 返回值:第一种重载形式的返回值为由path指定的DirectoryInfo;第二种重载形式的返回值为新创建的目录的DirectoryInfo对象。

说明

当path参数中的目录已经存在或者path的某些部分无效时,将发生异常。path参数指定目录路径,而不是文件路径。

【例20.18】 下面的代码调用Directory类的CreateDirectory方法在C盘根目录下创建一个Test文件夹。

    Directory. CreateDirectory("C:\\Test ");
2.DirectoryInfo类的Create方法

该方法为可重载方法,它有以下两种重载形式。

    public void Create()
    public void Create(DirectorySecurity directorySecurity)

directorySecurity:主要应用于此目录的访问控制。

【例20.19】 下面的代码首先实例化了一个DirectoryInfo对象,然后使用该对象调用DirectoryInfo类的Create方法在C盘根目录下创建一个Test文件夹。

    DirectoryInfo dinfo = new DirectoryInfo("C:\\Test ");
    dinfo.Create();

20.3.3 移动文件夹

移动文件夹时,可以使用Directory类的Move方法或者DirectoryInfo类的MoveTo方法来实现,下面分别对它们进行介绍。

1.Directory类的Move方法

将文件或目录及其内容移到新位置,语法如下。

    public static void Move(string sourceDirName,string destDirName)

 sourceDirName:要移动的文件或目录的路径。

 destDirName:指向sourceDirName的新位置的路径。

【例20.20】 下面的代码调用Directory类的Move方法将C盘根目录下的Test文件夹移动到C盘根目录下的“新建文件夹”文件夹中。

    Directory.Move("C:\\Test ","C:\\新建文件夹\\Test") ;

说明

使用Move方法移动文件夹时需要统一磁盘根目录,例如,C盘下的文件夹只能移动到C盘中的某个文件夹下。同样,使用MoveTo方法移动文件夹时也是如此,下面不再强调。

2.DirectoryInfo类的MoveTo方法

将DirectoryInfo对象及其内容移动到新路径,语法如下。

    public void MoveTo(string destDirName)

destDirName:要将此目录移动到的目标位置的名称和路径。目标不能是另一个具有相同名称的磁盘卷或目录,它可以是要将此目录作为子目录添加到其中的一个现有目录。

【例20.21】 下面的代码首先实例化了一个DirectoryInfo对象,然后使用该对象调用DirectoryInfo类的MoveTo方法将C盘根目录下的Test文件夹移动到C盘根目录下的“新建文件夹”文件夹中。

    DirectoryInfo dinfo = new DirectoryInfo("C:\\Test ");
    dinfo. MoveTo("C:\\新建文件夹\\Test") ;

20.3.4 删除文件夹

删除文件夹可以使用Directory类的Delete方法或者DirectoryInfo类的Delete方法来实现,下面分别对它们进行介绍。

1.Directory类的Delete方法

该方法为可重载方法,它有以下两种重载形式。

    public static void Delete(string path)
    public static void Delete(string path,bool recursive)

 path:要移除的空目录/目录的名称。

 recursive:若要移除path中的目录、子目录和文件,则为true;否则为false。

【例20.22】 下面的代码调用Directory类的Delete方法删除C盘根目录下的Test文件夹。

    Directory.Delete("C:\\Test");
2.DirectoryInfo类的Delete方法

该方法是指永久删除文件,语法如下。

    public override void Delete()
    public void Delete(bool recursive)

recursive:若为true,则删除此目录、其子目录以及所有文件;否则为false。

说明

第一种重载形式,如果DirectoryInfo为空,则删除它。第二种重载形式,删除DirectoryInfo对象,并指定是否要删除子目录和文件。

【例20.23】 下面的代码首先实例化了一个DirectoryInfo对象,然后使用该对象调用DirectoryInfo类的Delete方法删除C盘根目录下的Test文件夹。

    DirectoryInfo dinfo = new DirectoryInfo("C:\\Test");
    dinfo. Delete();

20.3.5 遍历文件夹

遍历文件夹时,可以分别使用DirectoryInfo类提供的GetDirectories方法、GetFiles方法和GetFileSystemInfos方法。下面对这3个方法进行详细讲解。

1.GetDirectories方法

该方法用来返回当前目录的子目录。该方法为可重载方法,它有以下3种重载形式。

    public DirectoryInfo[] GetDirectories()
    public DirectoryInfo[] GetDirectories(string searchPattern)
    public DirectoryInfo[] GetDirectories(string searchPattern,SearchOption searchOption)

 searchPattern:搜索字符串,如用于搜索所有以单词System开头的目录的“System*”。

 searchOption:SearchOption枚举的一个值,指定搜索操作是应仅包含当前目录还是应包含所有子目录。

 返回值:第一种重载形式的返回值为DirectoryInfo对象的数组。第二种和第三种重载形式的返回值为与searchPattern匹配的DirectoryInfo类型的数组。

2.GetFiles方法

返回当前目录的文件列表。该方法为可重载方法,它有以下3种重载形式。

    public FileInfo[] GetFiles()
    public FileInfo[] GetFiles(string searchPattern)
    public FileInfo[] GetFiles(string searchPattern,SearchOption searchOption)

 searchPattern:搜索字符串(如“*.txt”)。

 searchOption:SearchOption枚举的一个值,指定搜索操作是应仅包含当前目录还是应包含所有子目录。

 返回值:FileInfo类型数组。

3.GetFileSystemInfos方法

返回表示某个目录中所有文件和子目录的FileSystemInfo类型数组。该方法为可重载方法,它有以下两种重载形式。

    public FileSystemInfo[] GetFileSystemInfos()
    public FileSystemInfo[] GetFileSystemInfos(string searchPattern)

 searchPattern:搜索字符串。

 返回值:第一种重载形式的返回值为FileSystemInfo项的数组。第二种重载形式的返回值为与搜索条件匹配的FileSystemInfo对象的数组。

说明

一般遍历文件夹时,都使用GetFileSystemInfos方法,因为GetDirectories方法只遍历文件夹中的子文件夹,GetFiles方法只遍历文件夹中的文件,而GetFileSystemInfos方法遍历文件夹中的所有子文件夹及文件。

【例20.24】 下面演示如何遍历指定的文件夹,程序开发步骤如下。(实例位置:光盘\TM\sl\20\6)

(1)新建一个Windows应用程序,命名为Test06,默认窗体为Form1.cs。

(2)在Form1窗体中添加一个FolderBrowserDialog控件、一个TextBox控件、一个Button控件和一个ListView控件,其中,FolderBrowserDialog控件用来显示“浏览文件夹”对话框,TextBox控件用来显示选择的文件夹路径及名称,Button控件用来打开“浏览文件夹”对话框并获取选择文件夹中的子文件夹及文件,ListView控件用来显示选择的文件夹中的子文件夹及文件信息。

(3)程序主要代码如下。

运行程序,单击“浏览”按钮,弹出“浏览文件夹”对话框,选择文件夹,单击“确定”按钮,将选择的文件夹中所包含的子文件夹及文件信息显示在ListView控件中。程序的运行结果如图20.6所示。

图20.6 遍历文件夹

20.4 数据流

视频讲解:光盘\TM\lx\20\数据流.exe

数据流提供了一种向后备存储写入字节和从后备存储读取字节的方式,它是在.NET Framework中执行读写文件操作时一种非常重要的介质。下面对数据流进行详细讲解。

20.4.1 流操作类介绍

.NET Framework使用流来支持读取和写入文件,开发人员可以将流视为一组连续的一维数据,包含开头和结尾,并且其中的游标指示了流中的当前位置。

1.流操作

流中包含的数据可能来自内存、文件或TCP/IP套接字。流包含以下几种可应用于自身的基本操作。

 读取。将数据从流传输到数据结构(如字符串或字节数组)中。

 写入。将数据从数据源传输到流中。

 查找。查询和修改在流中的位置。

2.流的类型

在.NET Framework中,流由Stream类来表示,该类构成了所有其他流的抽象类。不能直接创建Stream类的实例,但是必须使用它实现其中的一个类。

C#中有许多类型的流,但在处理文件输入/输出(I/O)时,最重要的类型为FileStream类,它提供读取和写入文件的方式。可在处理文件I/O时使用的其他流主要包括BufferedStream、CryptoStream、MemoryStream和NetworkStream等。

20.4.2 文件流类

FileStream类公开以文件为主的Stream,它表示在磁盘或网络路径上指向文件的流。一个FileStream类的实例实际上代表一个磁盘文件,它通过Seek方法进行对文件的随机访问,也同时包含了流的标准输入、标准输出、标准错误等。FileStream默认对文件的打开方式是同步的,但它同样很好地支持异步操作。

对文件流的操作,实际上可以将文件看作是电视信号发送塔要发送的一个电视节目(文件),将电视节目转换成模拟数字信号(文件的二进制流),按指定的发送序列发送到指定的接收地点(文件的接收地址)。

说明

FileStream对象支持使用Seek方法对文件进行随机访问。Seek允许将读取/写入位置移动到文件中的任意位置。

1.FileStream类的常用属性

FileStream类的常用属性及说明如表20.7所示。

表20.7 FileStream类的常用属性及说明

2.FileStream类的常用方法

FileStream类的常用方法及说明如表20.8所示。

表20.8 FileStream类的常用方法及说明

3.使用FileStream类操作文件

要用FileStream类操作文件就要先实例化一个FileStream对象,FileStream类的构造函数具有许多不同的重载形式,其中包括了一个最重要的参数,即FileMode枚举。

FileMode枚举规定了如何打开或创建文件,其包括的枚举成员及说明如表20.9所示。

表20.9 FileMode类的枚举成员及说明

【例20.25】 下面的代码通过使用FileStream类对象打开Test.txt文本文件并对其进行读写访问。

    FileStream aFile = new FileStream("Test.txt",FileMode.OpenOrCreate,FileAccess.ReadWrite)

注意

文件要放在程序运行目录下,否则就需要给构造函数传递绝对路径。

20.4.3 文本文件的写入与读取

文本文件的写入与读取主要是通过StreamWriter类和StreamReader类来实现的,下面对这两个类进行详细讲解。

1.StreamWriter类

StreamWriter是专门用来处理文本文件的类,可以方便地向文本文件中写入字符串。同时也负责重要的转换和处理向FileStream对象写入工作。

说明

StreamWriter类默认使用UTF8Encoding编码来进行实例化。

StreamWriter类的常用属性及说明如表20.10所示。

表20.10 StreamWriter类的常用属性及说明

StreamWriter类的常用方法及说明如表20.11所示。

表20.11 StreamWriter类的常用方法及说明

2.StreamReader类

StreamReader是专门用来读取文本文件的类,StreamReader可以从底层Stream对象创建StreamReader对象的实例,而且也能指定编码规范参数。创建StreamReader对象后,它提供了许多用于读取和浏览字符数据的方法。

StreamReader类的常用方法及说明如表20.12所示。

表20.12 StreamReader类的常用方法及说明

【例20.26】 下面演示如何对文本文件进行写入与读取,程序开发步骤如下。(实例位置:光盘\TM\sl\20\7)

(1)新建一个Windows应用程序,命名为Test07,默认窗体为Form1.cs。

(2)在Form1窗体中添加一个SaveFileDialog控件、一个OpenFileDialog控件、一个TextBox控件和两个Button控件。其中,SaveFileDialog控件用来显示“另存为”对话框,OpenFileDialog控件用来显示“打开”对话框,TextBox控件用来输入要写入文本文件的内容和显示选中文本文件的内容,Button控件分别用来打开“另存为”对话框并执行文本文件写入操作和打开“打开”对话框并执行文本文件读取操作。

(3)程序主要代码如下。

图20.7 文本文件的写入与读取

运行程序,单击“写入”按钮,弹出“另存为”对话框,输出要保存的文件名,单击“保存”按钮,将文本框中的内容写入到文件中。单击“读取”按钮,弹出“打开”对话框,选择要读取的文件,单击“打开”按钮,将选择的文件的内容显示在文本框中。程序的运行结果如图20.7所示。

20.4.4 二进制文件的写入与读取

二进制文件的写入与读取主要是通过BinaryWriter类和BinaryReader类来实现的,下面对这两个类进行详细讲解。

1.BinaryWriter类

BinaryWriter类以二进制形式将基元类型写入流,并支持用特定的编码写入字符串,其常用方法及说明如表20.13所示。

表20.13 BinaryWriter类的常用方法及说明

2.BinaryReader类

BinaryReader用特定的编码将基元数据类型读作二进制值,其常用方法及说明如表20.14所示。

表20.14 BinaryReader类的常用方法及说明

【例20.27】 下面演示如何对二进制文件进行写入与读取,程序开发步骤如下。(实例位置:光盘\TM\sl\20\8)

(1)新建一个Windows应用程序,命名为Test08,默认窗体为Form1.cs。

(2)在Form1窗体中,添加一个SaveFileDialog控件、一个OpenFileDialog控件、一个TextBox控件和两个Button控件。其中,SaveFileDialog控件用来显示“另存为”对话框,OpenFileDialog控件用来显示“打开”对话框,TextBox控件用来输入要写入二进制文件的内容和显示选中二进制文件的内容,Button控件分别用来打开“另存为”对话框并执行二进制文件写入操作和打开“打开”对话框并执行二进制文件读取操作。

(3)程序主要代码如下。

说明

本实例的运行结果图与例20.26中的运行结果图类似,只是写入和读取文件的方式不同,这里不再给出实例运行结果图。

20.5 小结

本章主要介绍了文件的处理技术及如何以数据流形式写入和读取文件。程序中对文件进行操作及读取数据流时主要用到了System.IO命名空间下的各种类。本章首先对操作文件和文件夹的类进行了详细讲解,并通过理论结合实例的形式重点讲解了文件和文件夹的基本操作,然后对数据流操作类进行了详细讲解,并重点讲解了如何以数据流形式分别对文本文件和二进制文件进行写入和读取操作。学习完本章后,读者应该能够了解文件及数据流操作的理论知识,并能在实际开发中熟练利用这些理论知识对文件及数据流进行各种操作。

20.6 实践与练习

(1)尝试开发一个程序,实现批量复制文件功能。(答案位置:光盘\TM\sl\20\9)

(2)尝试开发一个程序,主要用来提取指定文件夹中的目录。(答案位置:光盘\TM\sl\20\10)

(3)尝试开发一个程序,该程序用来读写内存数据流。(答案位置:光盘\TM\sl\20\11)

第21章 GDI+图形图像技术

第21章
GDI+图形图像技术

视频讲解:72分钟

用户界面上的窗体和控件非常有用,非常引人注目,有时还需要在屏幕上使用颜色和图形对象。例如,可能需要使用线条或弧线来开发游戏,或者需要使用许多移动的圆来开发屏保程序。在这种情况下,只使用WinForms控件是不够的,还需要使用图形功能。通过使用图形,开发人员可以轻松地绘制他们的用户界面屏幕,并提供颜色、图形和对象。WinForms中的图形通过GDI+实现,GDI+是图形设备接口的高级版本。本章将详细介绍GDI+图形图像技术,讲解过程中为了便于读者理解结合了大量的举例。

通过阅读本章,您可以:

 了解什么是GDI+技术

 了解GDI+操作的基础类(Graphics类)

 了解Pen类

 了解Brush类

 掌握如何绘制直线和矩形

 掌握如何绘制椭圆、弧形和扇形

 掌握如何绘制多边形

 掌握如何绘制柱形图、折线图以及饼形图

21.1 GDI+绘图基础

视频讲解:光盘\TM\lx\21\GDI+绘图基础.exe

21.1.1 GDI+概述

GDI+指的是.NET Framework中提供二维图形、图像处理等功能,是构成Windows操作系统的一个子系统,它提供了图形图像操作的应用程序编程接口(API)。使用GDI+可以用相同的方式在屏幕或打印机上显示信息,而无须考虑特定显示设备的细节。GDI+类提供程序员用以绘制的方法,这些方法随后会调用特定设备的驱动程序。GDI+将应用程序与图形硬件分隔,使程序员能够创建与设备无关的应用程序。GDI+主要用于在窗体上绘制各种图形图像,可以用于绘制各种数据图形、数学仿真等。GDI+可以在窗体程序中产生很多自定义的图形,便于开发人员展示各种图形化的数据。

GDI+就好像是一个绘图仪,它可以将已经制作好的图形绘制在指定的模板中,并可以对图形的颜色、线条粗细、位置等进行设置。

21.1.2 创建Graphics对象

Graphics类是GDI+的核心,Graphics对象表示GDI+绘图表面,提供将对象绘制到显示设备的方法。Graphics与特定的设备上下向关联,是用于创建图形图像的对象。Graphics类封装了绘制直线、曲线、图形、图像和文本的方法,是进行一切GDI+操作的基础类。创建Graphics对象有以下3种方法。

(1)在窗体或控件的Paint事件中创建,将其作为PaintEventArgs的一部分。在为控件创建绘制代码时,通常会使用此方法来获取对图形对象的引用。

【例21.1】 在Paint事件中创建Graphics对象,代码如下。

    private void Form1_Paint(object sender, PaintEventArgs e)   //窗体的 Paint 事件
    {
         Graphics g = e.Graphics;                               //创建 Graphics 对象
    }

(2)调用控件或窗体的CreateGraphics方法以获取对Graphics对象的引用,该对象表示控件或窗体的绘图画面。如果在已存在的窗体或控件上绘图,应该使用此方法。

【例21.2】 在窗体的Load事件中,通过CreateGraphics方法创建Graphics对象,代码如下。

    private void Form1_Load(object sender, EventArgs e)    //窗体的 Load 事件
    {
         Graphics g;                                        //声明一个 Graphics 对象
         //使用 CreateGraphics 方法创建 Graphics 对象
         g = this.CreateGraphics();
    }

(3)由从Image继承的任何对象创建Graphics对象,此方法在需要更改已存在的图像时十分有用。

【例21.3】 在窗体的Load事件中,通过FromImage方法创建Graphics对象,代码如下。

    private void Form1_Load(object sender, EventArgs e)    //窗体的 Load 事件
    {
         Bitmap mbit = new Bitmap(@"C:\ls.bmp");           //实例化 Bitmap 类
         //通过 FromImage 方法创建 Graphics 对象
         Graphics g = Graphics.FromImage(mbit);
    }

21.1.3 创建Pen对象

Pen类主要用于绘制线条,或者将线条组合成其他几何形状。Pen类的构造函数如下。

    public Pen(Color color,float width)

 color:设置Pen的颜色。

 width:设置Pen的宽度。

【例21.4】 创建一个Pen对象,使其颜色为蓝色,宽度为2,代码如下。

    Pen mypen1 = new Pen(Color.Blue, 2);      //实例化一个 Pen 类,并设置其颜色和宽度

21.1.4 创建Brush对象

Brush类主要用于填充几何图形,如将正方形和圆形填充其他颜色。Brush类是一个抽象基类,不能进行实例化。若要创建一个画笔对象,需使用从Brush派生出的类,如SolidBrush、HatchBrush等,下面对这些派生出的类进行详细介绍。

1.SolidBrush类

SolidBrush类定义单色画笔,画笔用于填充图形形状,如矩形、椭圆、扇形、多边形和封闭路径。

语法如下:

    public SolidBrush(Color color)

color:表示此画笔的颜色。

说明

当不再需要返回的Graphics时,必须通过调用其Dispose方法来释放它。Graphics只在当前窗口消息期间有效。

【例21.5】 创建一个Windows应用程序,通过Brush对象将绘制的矩形填充为红色,代码如下。(实例位置:光盘\TM\sl\21\1)

    private void button1_Click(object sender, EventArgs e)
    {
         Graphics ghs = this.CreateGraphics();                   //创建 Graphics 对象
         Brush mybs = new SolidBrush(Color.Red);                 //使用 SolidBrush 类创建一个 Brush 对象
         Rectangle rt = new Rectangle(10,10,100,100);            //绘制一个矩形
         ghs.FillRectangle(mybs,rt);                             //用 Brush 填充 Rectangle
    }

程序的运行结果如图21.1所示。

2.HatchBrush类

HatchBrush类提供了一种特定样式的图形,用来制作填满整个封闭区域的绘图效果。HatchBrush类位于System.Drawing.Drawing2D命名空间下。

语法如下:

    public HatchBrush(HatchStyle hatchstyle,Color foreColor)

 hatchstyle:HatchStyle值之一,表示此HatchBrush所绘制的图案。

 foreColor:Color结构,它表示此HatchBrush所绘制线条的颜色。

【例21.6】 创建一个Windows应用程序,利用HatchStyle值,创建5个长条图示,代码如下。(实例位置:光盘\TM\sl\21\2)

程序的运行结果如图21.2所示。

3.LinerGradientBrush类

LinerGradientBrush类提供一种渐变色彩的特效,填满图形的内部区域。

语法如下:

    public LinerGradientBrush(Point point1, Point point2,Color color1, Color color2)

语法中的参数说明如表21.1所示。

图21.1 将矩形填充为红色

图21.2 绘制长条图示

表21.1 参数说明

说明

在使用LinerGradientBrush类时,必须在命名空间中添加System.Drawing.Drawing2D。

【例21.7】 创建一个Windows应用程序,通过LinerGradientBrush类绘制线形渐变图形,代码如下。(实例位置:光盘\TM\sl\21\3)

    private void button1_Click(object sender, EventArgs e)
    {
         Point p1 = new Point(100,100);                         //实例化两个 Point 类
         Point p2 = new Point(150,150);
         //实例化 LinerGradientBrush 类,设置其使用黑色和白色进行渐变
         LinearGradientBrush lgb = new LinearGradientBrush(p1,p2,Color.Black,Color.White);
         Graphics ghs = this.CreateGraphics();                   //实例化 Graphics 类
         //设置 WrapMode 属性指示该 LinearGradientBrush 的环绕模式
         lgb.WrapMode = WrapMode.TileFlipX;
         ghs.FillRectangle(lgb,15,15,150,150);                   //填充绘制矩形
    }

程序的运行结果如图21.3所示。

图21.3 绘制渐变图形

21.2 基本图形的绘制

视频讲解:光盘\TM\lx\21\基本图形的绘制.exe

介绍完GDI+图形图像技术的几个基本对象,下面通过这些基本对象绘制常见的几何图形。常见的几何图形包括直线、矩形和椭圆等。通过对本节的学习,读者能够轻松掌握这些图形的绘制方法。

21.2.1 GDI+中的直线和矩形

1.绘制直线

调用Graphics类中的DrawLine方法,结合Pen对象可以绘制直线。DrawLine方法有以下两种构造函数。

(1)第一种用于绘制一条连接两个Point结构的线。

语法如下:

    public void DrawLine(Pen pen,Point pt1,Point pt2)

 pen:Pen对象,它确定线条的颜色、宽度和样式。

 pt1:Point结构,它表示要连接的第一个点。

 pt2:Point结构,它表示要连接的第二个点。

说明

当参数pt1的值小于pt2时,所绘制的线将逆向绘制。

(2)第二种用于绘制一条连接由坐标指定的两个点的线条。

语法如下:

    public void DrawLine(Pen pen,int x1,int y1,int x2,int y2)

DrawLine方法的参数说明如表21.2所示。

表21.2 DrawLine方法的参数说明

【例21.8】 创建一个Windows应用程序,向窗体中添加两个Button按钮,分别用于执行绘制直线的两种方法,代码如下。(实例位置:光盘\TM\sl\21\4)

    private void button1_Click(object sender, EventArgs e)
    {
         Pen blackPen = new Pen(Color.Black, 3);        //实例化 Pen 类
         Point point1 = new Point(10, 50);              //实例化一个 Point 类
         Point point2 = new Point(100, 50);             //再实例化一个 Point 类
         Graphics g = this.CreateGraphics();            //实例化一个 Graphics 类
         g.DrawLine(blackPen, point1, point2);          //调用 DrawLine 方法绘制直线
    }
    private void button2_Click(object sender, EventArgs e)
    {
         Graphics graphics = this.CreateGraphics();      //实例化 Graphics 类
         Pen myPen = new Pen(Color.Black, 3);            //实例化 Pen 类
         graphics.DrawLine(myPen, 150, 30, 150, 100);    //调用 DrawLine 方法绘制直线
    }

图21.4 绘制直线

程序的运行结果如图21.4所示。

2.绘制矩形

通过Graphics类中的DrawRectangle方法,可以绘制矩形图形。该方法可以绘制由坐标对、宽度和高度指定的矩形。

语法如下:

    public void DrawRectangle(Pen pen,int x,int y,int width,int height)

DrawRectangle方法的参数说明如表21.3所示。

表21.3 DrawRectangle方法的参数说明

说明

当参数width和height的值为负数时,矩形框将不在窗体中显示。

【例21.9】 创建一个Windows应用程序,向窗体中添加一个Button控件,用于调用Graphics类中的DrawRectangle方法绘制矩形,代码如下。(实例位置:光盘\TM\sl\21\5)

    private void button1_Click(object sender, EventArgs e)
    {
         Graphics graphics = this.CreateGraphics();      //声明一个 Graphics 对象
         Pen myPen = new Pen(Color.Black, 8);            //实例化 Pen 类
         //调用 Graphics 对象的 DrawRectangle 方法,绘制矩形
         graphics.DrawRectangle(myPen, 10,10, 150, 100);
    }

程序的运行结果如图21.5所示。

图21.5 绘制矩形

21.2.2 GDI+中的椭圆、圆弧和扇形

1.绘制椭圆

通过Graphics类中的DrawEllipse方法可以轻松地绘制椭圆。此方法可以绘制由一对坐标、高度和宽度指定的椭圆。

语法如下:

    public void DrawEllipse(Pen pen,int x,int y,int width,int height)

DrawEllipse方法的参数说明如表21.4所示。

表21.4 DrawEllipse方法的参数说明

【例21.10】 创建一个Windows应用程序,通过Graphics类中的DrawEllipse方法绘制一个线条宽度为3的黑色椭圆,代码如下。(实例位置:光盘\TM\sl\21\6)

    private void button1_Click(object sender, EventArgs e)
    {
         Graphics graphics = this.CreateGraphics();    //创建 Graphics 对象
         Pen myPen = new Pen(Color.Black, 3);          //创建 Pen 对象
         graphics.DrawEllipse(myPen,100,50,100,50);    //绘制椭圆
    }

程序的运行结果如图21.6所示。

注意

在设置画笔(pen)的粗细时,如果其值小于等于0,那么,按默认值1来设置画笔的粗细。

图21.6 绘制椭圆

2.绘制圆弧

通过Graphics类中的DrawArc方法,可以绘制圆弧。此方法可以绘制由一对坐标、宽度和高度指定的圆弧。

语法如下:

    public void DrawArc(Pen pen,Rectangle rect,float startAngle,float sweepAngle)

DrawArc方法的参数说明如表21.5所示。

表21.5 DrawArc方法的参数说明

【例21.11】 创建一个Windows应用程序,使用Graphics类中的DrawArc方法绘制一条线条宽度为3的黑色圆弧,代码如下。(实例位置:光盘\TM\sl\21\7)

    private void button1_Click(object sender, EventArgs e)
    {
         Graphics ghs = this.CreateGraphics();                   //实例化 Graphics 类
         Pen myPen = new Pen(Color.Black, 3);                    //实例化 Pen 类
         Rectangle myRectangle = new Rectangle(70, 20, 100, 60); //定义一个 Rectangle 结构
         //调用 Graphics 对象的 DrawArc 方法绘制圆弧
         ghs.DrawArc(myPen, myRectangle, 210, 120);
    }

程序的运行结果如图21.7所示。

图21.7 绘制圆弧

3.绘制扇形

通过Graphics类中的DrawPie方法可以绘制扇形。此方法可以绘制由一个坐标对、宽度、高度以及两条射线所指定的扇形。

语法如下:

    public void DrawPie(Pen pen,float x,float y,float width,float height,float startAngle,float sweepAngle)

DrawPie方法的参数说明如表21.6所示。

表21.6 DrawPie方法的参数说明

说明

用DrawPie方法绘制扇形时,其扇形是参数x、y、width、height所绘制的矩形中内切圆(椭圆)中的一部分。

【例21.12】 创建一个Windows应用程序,通过Graphics类中的DrawPie方法绘制一个线条宽度为3的黑色扇形,它的起始坐标分别为50和50,代码如下。(实例位置:光盘\TM\sl\21\8)

    private void button1_Click(object sender, EventArgs e)
    {
         Graphics ghs = this.CreateGraphics();        //实例化 Graphics 类
         Pen mypen = new Pen(Color.Black,3);          //实例化 Pen 类
         ghs.DrawPie(mypen,50,50,120,100,210,120);    //绘制扇形
    }

图21.8 绘制扇形

程序的运行结果如图21.8所示。

21.2.3 GDI+中的多边形

多边形是有3条或更多直边的闭合图形。例如,三角形是有3条边的多边形,矩形是有4条边的多边形,五边形是有5条边的多边形。若要绘制多边形,需要Graphics对象、Pen对象和Point(或PointF)对象数组。

(1)Graphics对象提供DrawPolygon方法。

Graphics类中的DrawPolygon方法用于绘制由一组Point结构定义的多边形。

语法如下:

    public void DrawPolygon(Pen pen,Point[] points)

 pen:Pen对象,用于确定多边形的颜色、宽度和样式。

 points:Point结构数组,这些结构表示多边形的顶点。

(2)Pen对象存储用于呈现多边形的线条属性,例如宽度和颜色。

(3)Point对象数组存储将由直线连接的点。

说明

如果多边形数组中的最后一个点和第一个点不重合,则这两个点指定多边形的最后一条边。

【例21.13】 创建一个Windows应用程序,通过Graphics类中的DrawPolygon方法绘制多边形,其参数分别是Pen对象和Point对象数组,绘制一个线条宽度为3的黑色多边形,代码如下。(实例位置:光盘\TM\sl\21\9)

    private void button1_Click(object sender, EventArgs e)
    {
         Graphics ghs = this.CreateGraphics();                                 //实例化 Graphics 类
         Pen myPen = new Pen(Color.Black,3);                                   //实例化 Pen 类
         Point point1 = new Point(80, 20);                                     //实例化 Point 类
         Point point2 = new Point(40, 50);                                     //实例化 Point 类
         Point point3 = new Point(80, 80);                                     //实例化 Point 类
         Point point4 = new Point(160, 80);                                    //实例化 Point 类
         Point point5 = new Point(200, 50);                                    //实例化 Point 类
         Point point6 = new Point(160, 20);                                    //实例化 Point 类
         Point[] myPoints ={ point1, point2, point3, point4, point5, point6 }; //创建 Point 结构数组
         //调用 Graphics 对象的 DrawPolygon 方法绘制一个多边形
         ghs.DrawPolygon(myPen, myPoints);
    }

程序的运行结果如图21.9所示。

图21.9 绘制多边形

21.3 GDI+绘图的应用

视频讲解:光盘\TM\lx\21\GDI+绘图的应用.exe

介绍完GDI+的基础部分,下面通过GDI+绘制一些常用的图形,其中包括柱形图、折线图和饼形图。通过对本节的学习,读者可以掌握这些常用图形的绘制方法。

21.3.1 绘制柱形图

柱形图也称为条形图,是程序开发中比较常用的一种图表技术。柱形图是通过Graphics类中的FillRectangle方法实现的,此方法用于填充由一对坐标、一个宽度和一个高度指定的矩形的内部。

语法如下:

    public void FillRectangle(Brush brush, int x, int y, int width, int height)

FillRectangle方法的参数说明如表21.7所示。

表21.7 FillRectangle方法的参数说明

【例21.14】 创建一个Windows应用程序,首先制作一个投票窗体。然后在另一个窗体中通过柱形图显示最后的投票结果,显示投票结果窗体的代码如下。(实例位置:光盘\TM\sl\21\10)

程序的运行结果如图21.10所示。

图21.10 柱形图展示投票结果

说明

如果想要实现动态的柱形图表,在重新绘制前,要以柱形图表的绘制区域以及当前控件的背景颜色对柱形图进行清空。

21.3.2 绘制折线图

折线图可以很直观地反映出相关数据的变化趋势,折线图主要是通过绘制点和折线实现的。绘制点是通过Graphics类中的FillEllipse方法实现的。

语法如下:

    public void FillEllipse(Brush brush,int x,int y,int width,int height)

FillEllipse方法的参数说明如表21.8所示。

表21.8 FillEllipse方法的参数说明

绘制折线是通过Graphics类中的DrawLine方法实现的,此方法在第21.2.1节已经介绍过,此处不再赘述。

【例21.15】 创建一个Windows应用程序,通过折线图反映某工厂产品的月生产量。直观地反映了该工厂产品月生产量的走势,代码如下。(实例位置:光盘\TM\sl\21\11)

程序的运行结果如图21.11所示。

图21.11 折线图展示产品的月生产量

说明

用DrawString方法绘制文本时,文本的长度必须在所绘制的矩形区域内,如果超出区域,必须用format参数指定截断方式,否则将在最近的单词处截断。

21.3.3 绘制饼形图

饼形图可以很直观地查看不同数据所占的比例情况,通过Graphics类中的FillPie方法,可以方便地绘制出饼形图。

语法如下:

    public void FillPie(Brush brush, int x, int y, int width, int height, int startAngle, int sweepAngle)

FillPie方法的参数说明如表21.9所示。

表21.9 FillPie方法的参数说明

说明

如果sweepAngle参数大于360°或小于-360°,则将其分别视为360°或-360°。

【例21.16】 创建一个Windows应用程序,通过饼形图展示公司中不同年龄段的员工比例,并将各个年龄段所占的百分比显示出来,代码如下。(实例位置:光盘\TM\sl\21\12)

程序的运行结果如图21.12所示。

图21.12 饼形图展示员工年龄段的比例

21.4 小结

本章详细介绍了GDI+基本绘图知识,其中包括Graphics对象、Pen对象和Brush对象。Graphics类是一切GDI+操作的基础类,通过GDI+可以绘制直线、矩形、椭圆、弧形、扇形和多边形等几何图形。通过这些基本图形还可以绘制出柱形图、折线图和饼形图等。GDI+技术在应用程序开发中应用广泛,希望读者能够认真学习本章的知识。

21.5 实践与练习

(1)尝试开发一个程序,要求使用GDI+技术绘制一段波形图。(答案位置:光盘\TM\sl\21\13)

(2)尝试开发一个程序,要求绘制柱形图分析每年的商品月销售情况。(答案位置:光盘\TM\sl\21\14)

(3)尝试开发一个程序,要求绘制折线图分析各月份的网站流量情况。(答案位置:光盘\TM\sl\21\15)

(4)尝试开发一个程序,要求绘制饼形图分析公司的男女职工比例情况。(答案位置:光盘\TM\sl\21\16)

第22章 Windows打印技术

第22章
Windows打印技术

视频讲解:28分钟

视频讲解:光盘\TM\lx\22\Windows打印控件的使用.exe

Windows应用程序中提供了一组打印控件,包括PageSetupDialog、PrintDialog、PrintDocument、PrintPreviewControl和PrintPreviewDialog控件,开发程序时,开发人员可以直接使用这些控件控制打印的文本和数据格式。本章详细介绍了Windows应用程序中打印控件的使用。

通过阅读本章,您可以:

 掌握PageSetupDialog控件的使用

 掌握PrintDialog控件的使用

 掌握PrintDocument控件的使用

 掌握PrintPreviewControl控件的使用

 掌握PrintPreviewDialog控件的使用

22.1 PageSetupDialog控件

PageSetupDialog控件用于设置页面详细信息以便打印。允许用户设置边框和边距调整量、页眉和页脚以及纵向或横向打印。在介绍如何通过PageSetupDialog控件设置页面之前,要介绍该控件的一些属性,通过这些属性可以方便地对页面进行设置。PageSetupDialog控件的常用属性及说明如表22.1所示。

表22.1 PageSetupDialog控件的常用属性及说明

说明

在创建PageSetupDialog类的实例时,读/写属性将被设置为初始值。将AllowMargins、AllowOrientation、AllowPaper、AllowPrinter、ShowNetwork属性设置为true,将Document、MinMargins、PageSettings、PrinterSettings属性设置为null,将ShowHelp属性设置为false。

下面对这几种常见的属性进行详细介绍。

1.Document属性

该属性用于获取页面设置的PrintDocument。

语法如下:

    public PrintDocument Document { get; set; }

属性值:从中获得页面设置的PrintDocument。

2.AllowMargins属性

该属性用于设置是否启用对话框的边距部分。

语法如下:

    public bool AllowMargins { get; set; }

属性值:如果启用了对话框的边距部分,则为true;否则为false。默认为true。

3.AllowOrientation属性

该属性用于设置是否启用对话框的方向部分(横向对纵向)。

语法如下:

    public bool AllowOrientation { get; set; }

属性值:如果启用了对话框的方向部分,则为true;否则为false。默认为true。

4.AllowPaper属性

该属性用于设置是否启用对话框的纸张部分(纸张大小和纸张来源)。

语法如下:

    public bool AllowPaper { get; set; }

属性值:如果启用了对话框的纸张部分,则为true;否则为false。默认为true。

5.AllowPrinter属性

该属性用于设置是否启用“打印机”按钮。

语法如下:

    public bool AllowPrinter { get; set; }

【例22.1】 创建一个Windows应用程序,向窗体中添加一个PrintDocument控件、一个PageSetupDialog控件和一个Button控件。在Button控件的Click事件中设置PageSetupDialog控件的相应属性,代码如下。(实例位置:光盘\TM\sl\22\1)

    private void button1_Click(object sender, EventArgs e)
    {
         //设置 PageSetupDialog 控件的 Document 属性,设置操作文档
         pageSetupDialog1.Document = printDocument1;
         this.pageSetupDialog1.AllowMargins = true;           //启用边距
         this.pageSetupDialog1.AllowOrientation = true;       //启用对话框的方向部分
         this.pageSetupDialog1.AllowPaper = true;             //启用对话框的纸张部分
         this.pageSetupDialog1.AllowPrinter = true;           //启用“打印机”按钮
         this.pageSetupDialog1.ShowDialog();                   //显示页面设置对话框
    }

运行程序,单击工具栏中的“打印”按钮,打开“页面设置”对话框,可对页面进行设置,如图22.1所示。

图22.1 “页面设置”对话框

22.2 PrintDialog控件

PrintDialog控件用于选择打印机、选择要打印的页以及确定其他与打印相关的设置。通过PrintDialog控件可以选择全部打印、打印选定的页范围或打印选定内容。PrintDialog控件的常用属性及说明如表22.2所示。

表22.2 PrintDialog控件的常用属性及说明

注意

PrintDialog类与System.Windows.Forms.PrintDialog不同,后者用于Windows窗体应用程序。

下面对这几种常见的属性进行详细介绍。

1.Document属性

该属性用于获取PrinterSettings的PrintDocument对象。

语法如下:

    public PrintDocument Document { get; set; }

属性值:PrinterSettings的PrintDocument对象。

2.AllowCurrentPage属性

该属性用于设置是否显示“当前页”选项按钮。

语法如下:

    public bool AllowCurrentPage { get; set; }

属性值:如果显示“当前页”选项按钮,则为true;否则为false。默认为false。

3.AllowPrintToFile属性

该属性用于设置是否启用“打印到文件”复选框。

语法如下:

    public bool AllowPrintToFile { get; set; }

属性值:如果启用“打印到文件”复选框,则为true;否则为false。默认为true。

4.AllowSelection属性

该属性用于设置是否启用“选择”选项按钮。

语法如下:

    public bool AllowSelection { get; set; }

属性值:如果启用“选择”选项按钮,则为true;否则为false。默认为false。

5.AllowSomePages属性

该属性用于设置是否启用“页”选项按钮。

语法如下:

    public bool AllowSomePages { get; set; }

属性值:如果启用“页”选项按钮,则为true;否则为false。默认为false。

【例22.2】 创建一个Windows应用程序,向窗体中添加一个PrintDialog控件、一个PrintDocument控件和一个Button控件。在Button控件的Click事件中设置PrintDialog控件的相应属性,最后打开“打印”设置窗体,代码如下。(实例位置:光盘\TM\sl\22\2)

    private void button1_Click(object sender, EventArgs e)
    {
         //设置 PrintDialog 控件的 Document 属性,设置操作文档
         printDialog1.Document = printDocument1;
         printDialog1.AllowPrintToFile = true;       //启用“打印到文件”复选框
         printDialog1.AllowCurrentPage = true;       //显示“当前页”按钮
         printDialog1.AllowSelection = true;        //启用“选择”按钮
         printDialog1.AllowSomePages = true;        //启用“页”按钮
         printDialog1.ShowDialog();
    }

运行程序,单击“打印机设置”按钮,如图22.2所示。

图22.2 PrintDialog组件应用

22.3 PrintDocument控件

PrintDocument控件设置打印的文档。PrintDocument控件中比较常见的是控件的PrintPage事件和Print方法。PrintPage事件在需要为当前页打印的输出时发生。调用Print方法开始文档的打印进程。下面通过实例演示如何使用PrintDocument控件。

注意

在打印开始后,通过DefaultPageSettings属性更改页设置对正在打印的页没有任何影响。

【例22.3】 创建一个Windows应用程序,向窗体中添加一个Button控件、一个PrintDocument控件和一个PrintPreviewDialog控件。在PrintDocument控件的PrintPage事件中绘制打印的内容,然后在Button按钮的Click事件下设置PrintPreviewDialog的属性预览打印文档,并调用PrintDocument控件Print方法开始文档的打印进程,代码如下。(实例位置:光盘\TM\sl\22\3)

运行程序,单击“打印”按钮,弹出“打印预览”窗体,如图22.3所示。

图22.3 打印预览

22.4 PrintPreviewControl控件

PrintPreviewControl控件用于按文档打印时的外观显示文档。该控件只为用户提供一个预览打印文档的功能,因此通常只有在希望编写自己的打印预览用户界面时才使用PrintPreviewControl控件。rintPreviewControl控件比较重要的是Document属性,该属性用于设置要预览的文档。

语法如下:

    public PrintDocument Document { get; set; }

属性值:PrintDocument表示要预览的文档。

说明

在创建PrintPreviewControl类的实例时,一些读/写属性被设置为初始值。如AutoZoom为true、Document为null、Columns为1、Rows为0、StartPage为0。

下面通过实例演示如何使用PrintPreviewControl控件。

【例22.4】 创建一个Windows应用程序,向窗体中添加一个PrintPreviewControl控件和一个PrintDocument控件。在PrintDocument控件的PrintPage事件中绘制图像,然后在窗体的Load事件中设置PrintPreviewControl控件的Document属性,代码如下。(实例位置:光盘\TM\sl\22\4)

    private void Form1_Load(object sender, EventArgs e)
    {
         //设置 printPreviewControl1 控件的 Document 属性,设置要预览的文档
         printPreviewControl1.Document = printDocument1;
    }
    private void printDocument1_PrintPage(object sender, System.Drawing.Printing.PrintPageEventArgs e)
    {
         //声明一个 string 类型变量用于存储图片位置
         string str = Application.StartupPath.Substring(0, Application.StartupPath.Substring(0, Application.
         StartupPath. LastIndexOf("\\")).LastIndexOf("\\"));
         str += @"\img.jpg";
         e.Graphics.DrawImage(Image.FromFile(str), 10,10, 607,452); //使用 DrawImage 方法绘制图像
    }

程序的运行结果如图22.4所示。

图22.4 PrintPreviewControl控件的应用

22.5 PrintPreviewDialog控件

PrintPreviewDialog控件用于显示文档打印后的外观。该控件包含打印、放大、显示一页或多页和关闭此对话框的按钮。PrintPreviewDialog控件的常见属性和方法有Document属性、UseAntiAlias属性和ShowDialog方法。

1.Document属性

该属性用于设置要预览的文档。

语法如下:

    public PrintDocument Document { get; set; }

属性值:PrintDocument表示要预览的文档。

【例22.5】 设置PrintPreviewDialog控件的Document属性为printDocument1,代码如下。

    printPreviewDialog1.Document = this.printDocument1;       //设置预览文档
2.UseAntiAlias属性

该属性用于设置打印是否使用操作系统的防锯齿功能。

语法如下:

    public bool UseAntiAlias { get; set; }

属性值:如果使用防锯齿功能,则为true;否则为false。

【例22.6】 设置UseAntiAlias属性为true,开启防锯齿功能,代码如下。

    printPreviewDialog1.UseAntiAlias = true;      //设置 UseAntiAlias 属性为 true,开启防锯齿功能
3.ShowDialog方法

该方法将窗体显示为模式对话框,并将当前活动窗口设置为它的所有者。

语法如下:

    public DialogResult ShowDialog ()

返回值:DialogResult值之一。

【例22.7】 调用PrintPreviewDialog控件的ShowDialog方法,显示预览窗口,代码如下。

    printPreviewDialog1.ShowDialog();           //使用 ShowDialog 方法,显示预览窗口

说明

关于PrintPreviewDialog控件的用法,在22.3节中已经涉及过。所以此处不再举例介绍,希望读者能够参见22.3节中的实例。

22.6 小结

本章主要介绍了Windows打印控件的使用,Windows打印控件为用户提供打印文档的窗口,以及与打印相关的各种设置,主要包括PrintDocument、PrintPreviewDialog、PrintPreviewControl、PrintDialog和PageSetupDialog控件等。读者要仔细阅读本章内容,并掌握打印控件的使用方法。

22.7 实践与练习

(1)尝试开发一个程序,要求使用Windows打印组件打印窗体中的数据。(答案位置:光盘\TM\sl\22\5)

(2)尝试开发一个程序,要求使用Windows打印组件打印学生证书。(答案位置:光盘\TM\sl\22\6)

第23章 网络编程技术

第23章
网络编程技术

视频讲解:72分钟

随着社会的快速发展,互联网正渐渐普及到全民的日常生活当中,网络已经逐渐成为人们工作和生活中必不可少的一部分。C#作为一种编程语言,它提供了对网络编程的全面支持,例如,开发人员可以通过C#语言制作一个简单的局域网聊天室等。本章将详细讲解网络编程方面的相关知识。

通过阅读本章,您可以:

 掌握System.Net命名空间下主要类的使用

 掌握System.Net.Sockets命名空间下主要类的使用

 掌握System.Net.Mail命名空间下主要类的使用

 了解POP3协议

 掌握Web页面浏览器的实现过程

 掌握局域网聊天程序的实现过程

 掌握如何使用C#实现电子邮件的发送与接收

23.1 网络编程基础

视频讲解:光盘\TM\lx\23\网络编程基础.exe

使用C#进行网络编程时,通常都需要用到System.Net命名空间、System.Net.Sockets命名空间和System.Net.Mail命名空间。下面对这3个命名空间及它们包含的主要类进行详细讲解。

23.1.1 System.Net命名空间

System.Net命名空间为当前网络上使用的多种协议提供了简单的编程接口,而它所包含的WebRequest类和WebResponse类形成了所谓的可插接式协议的基础,可插接式协议是网络服务的一种实现,它使用户能够开发出使用Internet资源的应用程序,而不必考虑各种不同协议的具体细节。下面对System.Net命名空间中的主要类进行详细讲解。

1.Dns类

Dns类是一个静态类,它从Internet域名系统(DNS)检索关于特定主机的信息。在IPHostEntry类的实例中返回来自DNS查询的主机信息。如果指定的主机在DNS数据库中有多个入口,则IPHostEntry包含多个IP地址和别名。Dns类中的常用方法及说明如表23.1所示。

表23.1 Dns类的常用方法及说明

说明

Dns类是一个静态类,它从Internet域名系统(DNS)检索关于特定主机的信息。在IPHostEntry(为Internet主机地址信息提供容器)类的实例中返回来自DNS查询的主机信息。如果指定的主机在DNS数据库中有多个入口,则IPHostEntry包含多个IP地址和别名。

【例23.1】 下面演示Dns类的使用方法,程序开发步骤如下。(实例位置:光盘\TM\sl\23\1)

(1)新建一个Windows应用程序,命名为1,默认窗体为Form1.cs。

(2)在Form1窗体中添加4个TextBox控件和一个Button控件,其中,TextBox控件分别用来输入主机地址和显示主机IP地址、本地主机名、DNS主机名,Button控件用来调用Dns类中的各个方法获得主机IP地址、本地主机名和DNS主机名,并显示在相应的文本框中。

(3)程序主要代码如下。

程序的运行结果如图23.1所示。

图23.1 Dns类的使用

2.IPAddress类

IPAddress类包含计算机在IP网络上的地址,它主要用来提供网际协议(IP)地址。IPAddress类中的常用字段、属性、方法及说明如表23.2所示。

表23.2 IPAddress类的常用字段、属性、方法及说明

说明

IPHostEntry类将一个域名系统(DNS)主机名与一组别名和一组匹配的IP地址关联。

【例23.2】 下面演示IPAddress类的使用方法,程序开发步骤如下。(实例位置:光盘\TM\sl\23\2)

(1)新建一个Windows应用程序,命名为2,默认窗体为Form1.cs。

(2)在Form1窗体中添加一个TextBox控件、一个Button控件和一个Label控件,其中,TextBox控件用来输入主机的网络地址或IP地址,Button控件用来调用IPAddress类中的各个属性获取指定主机的IP地址信息,Label控件用来显示获得的IP地址信息。

(3)程序主要代码如下。

    private void button1_Click(object sender, EventArgs e)
    {
         label2.Text = string.Empty;                               //初始化 Label 标签
         IPAddress[] ips = Dns.GetHostAddresses(textBox1.Text);    //获得指定主机的 IP 地址族
         foreach (IPAddress ip in ips) //循环遍历得到的 IP 地址
         {
              //在 Label 标签中显示得到的 IP 地址信息
              label2.Text = "网际协议地址:" + ip.Address + "\nIP 地址的地址族:"
                  + ip.AddressFamily.ToString() + "\n 是否 IPv6 链接本地地址:" + ip.IsIPv6LinkLocal;
         }
    }

图23.2 IPAddress类的使用

程序的运行结果如图23.2所示。

3.IPEndPoint类

IPEndPoint类包含应用程序连接到主机上的服务所需的主机和本地或远程端口信息。通过组合服务的主机IP地址和端口号,IPEndPoint类形成到服务的连接点,它主要用来将网络端点表示为IP地址和端口号。IPEndPoint类中的常用字段、属性及说明如表23.3所示。

表23.3 IPEndPoint类的常用字段、属性及说明

说明

在设置端口号时,其值必须大于等于0或小于等于0x0000FFFF。

【例23.3】 下面演示IPEndPoint类的使用方法,程序开发步骤如下。(实例位置:光盘\TM\sl\23\3)

(1)新建一个Windows应用程序,命名为3,默认窗体为Form1.cs。

(2)在Form1窗体中添加一个TextBox控件、一个Button控件和一个Label控件,其中,TextBox控件用来输入IP地址,Button控件用来调用IPEndPoint类中的各个属性获取终结点的IP地址和端口号,Label控件用来显示获得的IP地址和端口号。

(3)程序主要代码如下。

    private void button1_Click(object sender, EventArgs e)
    {
         //实例化 IPEndPoint 类对象
         IPEndPoint IPEPoint = new IPEndPoint(IPAddress.Parse(textBox1.Text), 80);
         //使用 IPEndPoint 类对象获取终结点的 IP 地址和端口号
         label2.Text = "IP 地址:"+IPEPoint.Address.ToString() + "\n 端口号:" + IPEPoint.Port;
    }

图23.3 IPEndPoint类的使用

程序的运行结果如图23.3所示。

4.WebClient类

WebClient类提供向URI标识的任何本地、Intranet或Internet资源发送数据以及从这些资源接收数据的公共方法。WebClient类中的常用属性、方法及说明如表23.4所示。

表23.4 WebClient类的常用属性、方法及说明

说明

默认情况下,WebClient实例不发送可选的HTTP报头。如果要发送可选报头,必须将该报头添加到Headers(哈希表)集合中。

【例23.4】 下面演示WebClient类的使用方法,程序开发步骤如下。(实例位置:光盘\TM\sl\23\4)

(1)新建一个Windows应用程序,命名为4,默认窗体为Form1.cs。

(2)在Form1窗体中添加一个TextBox控件、一个Button控件和一个RichTextBox控件,其中,TextBox控件用来输入标准网络地址,Button控件用来获取指定网址中的网页内容,并将内容保存到一个文本文件中,RichTextBox控件用来显示从指定网址中获取的网页内容。

(3)程序主要代码如下。

    private void button1_Click(object sender, EventArgs e)
    {
         richTextBox1.Text = string.Empty;
         WebClient wclient = new WebClient();      //实例化 WebClient 类对象
         wclient.BaseAddress = textBox1.Text;      //设置 WebClient 的基 URI
         wclient.Encoding = Encoding.UTF8;         //指定下载字符串的编码方式
         //为 WebClient 类对象添加报头
         wclient.Headers.Add("Content-Type", "application/x-www-form-urlencoded");
         //使用 OpenRead 方法获取指定网站的数据,并保存到 Stream 流中
         Stream stream = wclient.OpenRead(textBox1.Text);
         //使用流 StreamReader 声明一个流读取变量 sreader
         StreamReader sreader = new StreamReader(stream);
         string str = string.Empty;//声明一个变量,用来保存一行从 WebClient 下载的数据
         //循环读取从指定网站获得的数据
         while ((str = sreader.ReadLine()) != null)
         {
             richTextBox1.Text += str + "\n";
         }
         //调用 WebClient 对象的 DownloadFile 方法将指定网站的内容保存到文件中
         wclient.DownloadFile(textBox1.Text, DateTime.Now.ToFileTime() + ".txt");
         MessageBox.Show("保存到文件成功");
    }

程序的运行结果如图23.4所示。

图23.4 WebClient类的使用

5.WebRequest类和WebResponse类

WebRequest类是.NET Framework的请求/响应模型的抽象基类,用于访问Internet数据。使用该请求/响应模型的应用程序可以用协议不可知的方式从Internet请求数据,在这种方式下,应用程序处理WebRequest类的实例,而协议特定的子类则执行请求的具体细节。

WebResponse类也是抽象基类,应用程序可以使用WebResponse类的实例以协议不可知的方式参与请求和响应事务,而从WebResponse类派生的协议类携带请求的详细信息。另外,需要注意的是,客户端应用程序不直接创建WebResponse对象,而是通过对WebRequest实例调用GetResponse方法来进行创建。

WebRequest类中的常用属性、方法及说明如表23.5所示。

表23.5 WebRequest类的常用属性、方法及说明

WebResponse类中的常用属性、方法及说明如表23.6所示。

表23.6 WebResponse类的常用属性、方法及说明

说明

客户端应用程序不直接创建WebResponse对象,而是通过对WebRequest实例调用GetResponse方法来进行创建。

【例23.5】 下面演示WebRequest类和WebResponse类的使用方法,程序开发步骤如下。(实例位置:光盘\TM\sl\23\5)

(1)新建一个Windows应用程序,命名为5,默认窗体为Form1.cs。

(2)在Form1窗体中添加一个TextBox控件、一个Button控件和一个RichTextBox控件,其中,TextBox控件用来输入标准网络地址,Button控件用来调用WebRequest和WebResponse类中的属性、方法获取指定网站的网页请求信息和网页内容,RichTextBox控件用来显示根据指定网址获取的网页请求信息及网页内容。

(3)程序主要代码如下。

    private void button1_Click(object sender, EventArgs e)
    {
         richTextBox1.Text = string.Empty;
         //实例化一个 WebRequest 对象
         WebRequest webrequest = WebRequest.Create(textBox1.Text);
         //设置用于对 Internet 资源请求进行身份验证的网络凭据
         webrequest.Credentials = CredentialCache.DefaultCredentials;
         //调用 WebRequest 对象的各种属性获取 WebRequest 请求的相关信息
         richTextBox1.Text = "请求数据的内容长度:" + webrequest.ContentLength;
         richTextBox1.Text += "\n 该请求的协议方法:" + webrequest.Method;
         richTextBox1.Text += "\n 访问 Internet 的网络代理:" + webrequest.Proxy;
         richTextBox1.Text += "\n 与该请求关联的 Internet URI:" + webrequest.RequestUri;
         richTextBox1.Text += "\n 超时时间:" + webrequest.Timeout;
         //调用 WebRequest 对象的 GetResponse 方法实例化一个 WebResponse 对象
         WebResponse webresponse = webrequest.GetResponse();
         //获取 WebResponse 响应的 Internet 资源的 URI
         richTextBox1.Text += "\n 响应该请求的 Internet URI:" + webresponse.ResponseUri;
         //调用 WebResponse 对象的 GetResponseStream 方法返回数据流
         Stream stream = webresponse.GetResponseStream();
         //使用创建的 Stream 对象创建一个 StreamReader 流读取对象
         StreamReader sreader = new StreamReader(stream);
         //读取流中的内容,并显示在 RichTextBox 控件中
         richTextBox1.Text += "\n" + sreader.ReadToEnd();
         sreader.Close();
         stream.Close();
         webresponse.Close();
    }

程序的运行结果如图23.5所示。

图23.5 WebRequest类和WebResponse类的使用

23.1.2 System.Net.Sockets命名空间

System.Net.Sockets命名空间主要提供制作Sockets网络应用程序的相关类,其中Socket类、TcpClient类、TcpListener类和UdpClient类较为常用,下面对它们进行详细介绍。

1.Socket类

Socket类为网络通信提供了一套丰富的方法和属性,它主要用于管理连接,实现Berkeley通信端套接字接口。同时,它还定义了绑定、连接网络端点及传输数据所需的各种方法,提供处理端点连接传输等细节所需要的功能。WebRequest、TcpClient和UdpClient等类在内部使用该类。Socket类的常用属性及说明如表23.7所示。

表23.7 Socket类的常用属性及说明

Socket类的常用方法及说明如表23.8所示。

表23.8 Socket类的常用方法及说明

说明

如果当前使用的是面向连接的协议(如TCP),则服务器可以使用Listen方法侦听连接。如果当前使用的是无连接协议(如UDP),则根本不需要侦听连接。调用ReceiveFrom方法可接受任何传入的数据报。使用SendTo方法可将数据报发送到远程主机。

【例23.6】 下面演示Socket类的使用方法,程序开发步骤如下。(实例位置:光盘\TM\sl\23\6)

(1)新建一个Windows应用程序,命名为6,默认窗体为Form1.cs。

(2)在Form1窗体中添加两个TextBox控件和一个Button控件,其中,TextBox控件分别用来输入要连接的主机及端口号,Button控件用来连接远程主机,并获得其上的主页面内容。

(3)程序主要代码如下。

程序的运行结果如图23.6和图23.7所示。

图23.6 Socket类的使用

图23.7 详细信息

2.TcpClient类和TcpListener类

TcpClient类用于在同步阻止模式下通过网络来连接、发送和接收流数据。为使TcpClient连接并交换数据,使用Tcp ProtocolType类创建的TcpListener实例或Socket实例必须侦听是否有传入的连接请求。可以使用下面两种方法之一连接到该侦听器。

 创建一个TcpClient,并调用3个可用的Connect方法之一。

 使用远程主机的主机名和端口号创建TcpClient,此构造函数将自动尝试一个连接。

TcpListener类用于在阻止同步模式下侦听和接受传入的连接请求。可使用TcpClient类或Socket类来连接TcpListener,并且可以使用IPEndPoint、本地IP地址及端口号或者仅使用端口号来创建TcpListener实例对象。

注意

如果要在同步阻止模式下发送无连接数据报,请使用UdpClient类。

TcpClient类的常用属性、方法及说明如表23.9所示。

表23.9 TcpClient类的常用属性、方法及说明

TcpListener类的常用属性、方法及说明如表23.10所示。

表23.10 TcpListener类的常用属性、方法及说明

注意

Stop方法关闭TcpListenerStop(侦听),但不会关闭任何已接受的连接。

【例23.7】 下面演示TcpClient类和TcpListener类的使用方法,程序开发步骤如下。(实例位置:光盘\TM\sl\23\7)

(1)新建一个Windows应用程序,命名为7,默认窗体为Form1.cs。

(2)在Form1窗体中添加两个TextBox控件、一个Button控件和一个RichTextBox控件,其中,TextBox控件分别用来输入要连接的主机及端口号,Button控件用来执行连接远程主机操作,RichTextBox控件用来显示远程主机的连接状态。

(3)程序主要代码如下。

    private void button1_Click(object sender, EventArgs e)
    {
         TcpListener tcplistener = null;                             //实例化一个 TcpListener 对象,并初始化为空
         IPAddress ipaddress = IPAddress.Parse(textBox1.Text);       //实例化一个 IPAddress 对象,用来表示网络 IP
                                                                      地址
         //定义一个 int 类型变量,用来存储端口号
         int port = Convert.ToInt32(textBox2.Text);
         tcplistener = new TcpListener(ipaddress, port);             //初始化 TcpListener 对象
         tcplistener.Start();                                        //开始 TcpListener 侦听
         richTextBox1.Text = "等待连接...\n";
         TcpClient tcpclient = null;                                 //实例化一个 TcpClient 对象,并赋值为空
         if (tcplistener.Pending())                                  //判断是否有挂起的连接请求
               tcpclient = tcplistener.AcceptTcpClient();            //使用 AcceptTcpClient 初始化 TcpClient 对象
         else
               tcpclient = new TcpClient(textBox1.Text, port);       //使用 TcpClient 的构造函数初始化 TcpClient 对象
         richTextBox1.Text += "连接成功!\n"; 
         tcpclient.Close();                                          //关闭 TcpClient 连接
         tcplistener.Stop();                                         //停止 TcpListener 侦听
    }

程序的运行结果如图23.8所示。

图23.8 TcpClient类的使用

3.UdpClient类

UdpClient类用于在阻止同步模式下发送和接收无连接UDP数据报。因为UDP是无连接传输协议,所以不需要在发送和接收数据前建立远程主机连接,但可以选择使用下面两种方法之一来建立默认远程主机。

 使用远程主机名和端口号作为参数创建UdpClient类的实例。

 创建UdpClient类的实例,然后调用Connect方法。

UdpClient类的常用属性、方法及说明如表23.11所示。

表23.11 UdpClient类的常用属性、方法及说明

说明

如果已指定了默认远程主机,则不要使用主机名或IPEndPoint调用Send方法,否则将引发异常。

【例23.8】 下面演示如何使用UdpClient类中的属性及方法,程序开发步骤如下。(实例位置:光盘\TM\sl\23\8)

(1)新建一个Windows应用程序,命名为8,默认窗体为Form1.cs。

(2)在Form1窗体中添加3个TextBox控件、一个Button控件和一个RichTextBox控件,其中,TextBox控件分别用来输入远程主机名、端口号及要发送的信息,Button控件用来向指定的主机发送信息,RichTextBox控件用来显示接收到的信息。

(3)程序主要代码如下。

    private void button1_Click(object sender, EventArgs e)
    {
         richTextBox1.Text = string.Empty;
         UdpClient udpclient = new UdpClient(Convert.ToInt32(textBox2.Text)); //实例化 UdpClient 对象
         //调用 UdpClient 对象的 Connect 建立默认远程主机
         udpclient.Connect(textBox1.Text, Convert.ToInt32(textBox2.Text));
         //定义一个字节数组,用来存放发送到远程主机的信息
         Byte[] sendBytes = Encoding.Default.GetBytes(textBox3.Text);
         //调用 UdpClient 对象的 Send 方法将 Udp 数据报发送到远程主机
         udpclient.Send(sendBytes, sendBytes.Length);
         //实例化 IPEndPoint 对象,用来显示响应主机的标识
         IPEndPoint ipendpoint = new IPEndPoint(IPAddress.Any, 0);
         //调用 UdpClient 对象的 Receive 方法获得从远程主机返回的 Udp 数据报
         Byte[] receiveBytes = udpclient.Receive(ref ipendpoint);
         //将获得的 Udp 数据报转换为字符串形式
         string returnData = Encoding.Default.GetString(receiveBytes);
         richTextBox1.Text = "接收到的信息:" + returnData.ToString();
         //使用 IPEndPoint 对象的 Address 和 Port 属性获得响应主机的 IP 地址和端口号
         richTextBox1.Text += "\n 这条信息来自主机" + ipendpoint.Address.ToString()
             + "上的" + ipendpoint.Port.ToString() + "端口";
         udpclient.Close();                                         //关闭 UdpClient 连接
    }

图23.9 UdpClient类的使用

程序的运行结果如图23.9所示。

23.1.3 System.Net.Mail命名空间

System.Net.Mail命名空间包含用于将电子邮件发送到简单邮件传输协议(SMTP)服务器进行传送的类,其中MailMessage类用来表示邮件的内容,Attachment类用来创建邮件附件,SmtpClient类用来将电子邮件传输到指定用于邮件传送的SMTP主机。下面对这3个类进行详细讲解。

1.MailMessage类

MailMessage类表示可以使用SmtpClient类发送的电子邮件,它主要用于指定邮件的发送地址、收件人地址、邮件正文及附件等。MailMessage类的常用属性及说明如表23.12所示。

表23.12 MailMessage类的常用属性及说明

说明

如果要将附件添加到MailMessage对象中,可为该附件创建一个Attachment,再将其添加到由AlternateViews返回的集合中,使用Body属性可以指定文本版本,使用AlternateViews集合可以指定具有其他MIME类型的视图。使用MediaTypeNames类成员可以指定电子邮件附件的媒体类型信息。

【例23.9】 下面实例化一个MailMessage邮件发送类对象,并通过其属性设置邮件的发送人、接收人、主题和内容,代码如下。

    MailAddress from = new MailAddress("tsoft@163.com");  //设置邮件发送人
    MailAddress to = new MailAddress("tsoft@163.com");    //设置邮件接收人
    MailMessage message = new MailMessage(from,to);       //实例化一个 MailMessage 类对象
    message.Subject = "邮件测试";                         //设置发送邮件的主题
    message.Body = "邮件正文";                            //设置发送邮件的内容
2.Attachment类

Attachment类表示电子邮件的附件,它需要与MailMessage类一起使用。创建完电子邮件的附件之后,若要将附件添加到邮件中,需要将附件添加到MailMessage.Attachments集合中。Attachment类的常用属性、方法及说明如表23.13所示。

表23.13 Attachment类的常用属性、方法及说明

【例23.10】 下面实例化一个MailMessage邮件发送类对象,同时通过Attachment类创建一个附件,并设置该附件的时间信息,最后调用MailMessage邮件发送类对象的Attachments属性的Add方法将创建的附件添加到要发送的邮件中,代码如下。

    string file = "邮件测试.txt";
    MailAddress from = new MailAddress("tsoft@163.com");   //设置邮件发送人
    MailAddress to = new MailAddress("tsoft@163.com");     //设置邮件接收人
    MailMessage message = new MailMessage(from,to);        //实例化一个 MailMessage 类对象
    //为要发送的邮件创建附件信息
    Attachment myAttachment = new Attachment(Server.MapPath(ddlAccessories.Items[i].Value), System.
    Net.Mime.MediaTypeNames.Application.Octet);
    System.Net.Mime.ContentDisposition disposition = myAttachment.ContentDisposition; //为附件添加时间信息
    disposition.CreationDate = System.IO.File.GetCreationTime(file);
    disposition.ModificationDate = System.IO.File.GetLastWriteTime(file);
    disposition.ReadDate = System.IO.File.GetLastAccessTime(file);
    message.Attachments.Add(myAttachment);                        //将创建的附件添加到邮件中
3.SmtpClient类

SmtpClient类用于将电子邮件发送到SMTP服务器以便传递。使用SmtpClient类实现发送电子邮件功能时,必须指定以下信息:

 用来发送电子邮件的SMTP主机服务器。

 身份验证凭据(如果SMTP服务器要求)。

 发件人的电子邮件地址。

 收件人的电子邮件地址。

 邮件内容。

SmtpClient类的常用属性、方法及说明如表23.14所示。

表23.14 SmtpClient类的常用属性、方法及说明

说明

当正在传输电子邮件时,再次调用SendAsync或Send方法,则会触发InvalidOperationException异常(当前状态无效时引发的异常)。

【例23.11】 下面演示如何在程序中发送邮件,程序开发步骤如下。(实例位置:光盘\TM\sl\23\9)

(1)新建一个Windows应用程序,命名为9,默认窗体为Form1.cs。

(2)在Form1窗体中添加一个Button控件,用来执行发送邮件操作。

(3)程序主要代码如下。

    private void button1_Click(object sender, EventArgs e)
    {
         string file = "邮件测试.txt";
         MailAddress from = new MailAddress("tsoft@163.com");        //设置邮件发送人
         MailAddress to = new MailAddress("tsoft@163.com");          //设置邮件接收人
         MailMessage message = new MailMessage(from,to);            //实例化一个 MailMessage 类对象
         message.Subject = "邮件测试";                               //设置发送邮件的主题
         message.Body = "邮件正文";                                  //设置发送邮件的内容
         //为要发送的邮件创建附件信息
         Attachment myAttachment = new Attachment(file, System.Net.Mime.MediaTypeNames.Application.Octet);
         //为附件添加时间信息
         System.Net.Mime.ContentDisposition disposition = myAttachment.ContentDisposition;
         disposition.CreationDate = System.IO.File.GetCreationTime(file);
         disposition.ModificationDate = System.IO.File.GetLastWriteTime(file);
         disposition.ReadDate = System.IO.File.GetLastAccessTime(file);
         message.Attachments.Add(myAttachment);                      //将创建的附件添加到邮件中
         SmtpClient client = new SmtpClient("192.168.1.97", 25);     //实例化 SmtpClient 邮件发送类对象
         client.Credentials = new System.Net.NetworkCredential("tsoft", "111"); //设置用于验证发件人身份的凭据
         client.Send(message);                                       //发送邮件
         MessageBox.Show("发送成功");
    }

23.1.4 POP3协议

POP(Post Office Protocol,邮局协议)协议用于电子邮件的接收,现在常用第3版,所以称POP3。通过POP协议,客户机登录到服务器后,可以对自己的邮件进行删除,或是下载到本地。如表23.15所示为POP3协议的常用命令。

表23.15 POP3的常用命令及描述

注意

SMTP服务器使用的端口号一般为25,POP服务器使用的端口号一般为110。

使用POP3协议实现电子邮件的接收功能时,首先需要配置POP3服务。具体操作步骤如下:

(1)首先在“Windows组件向导”对话框中选中“电子邮件服务”复选框,依次单击“下一步”按钮,完成电子邮件服务的安装操作。“Windows组件向导”对话框如图23.10所示。

图23.10 “Windows组件向导”对话框

(2)当添加完电子邮件服务后,打开“管理工具”窗口,这时将会出现一项新的功能“POP3服务”,如图23.11所示。

(3)双击打开“POP3服务”对话框,添加一个新域(如163.com),单击“确定”按钮,如图23.12所示。

图23.11 “管理工具”窗口

图23.12 “添加域”对话框

(4)添加完域后,选中相应的域,并在指定域内添加邮箱名和密码,然后单击“确定”按钮,即可添加一个邮箱。“添加邮箱”对话框如图23.13所示。

注意

所有域都在本机上分出一定的空间来存放信息,默认位置为C:\Inetpub\mailroot\Mailbox。

图23.13 “添加邮箱”对话框

23.2 开发网络应用程序

视频讲解:光盘\TM\lx\23\开发网络应用程序.exe

网络应用程序的种类有很多,本节通过Web页面浏览器、局域网聊天程序和电子邮件的发送与接收等几种常见的网络应用程序,讲解如何使用C#语言开发网络应用程序。

23.2.1 创建Web页面浏览器

【例23.12】 下面演示如何使用C#制作一个Web页面浏览器,程序开发步骤如下。(实例位置:光盘\TM\sl\23\10)

(1)新建一个Windows应用程序,命名为10,默认窗体为Form1.cs。

(2)在Form1窗体中添加一个TextBox控件、一个Button控件和一个WebBrowser控件,其中,TextBox控件用来输入要浏览的网页地址,Button控件用来执行浏览网页操作,WebBrowser控件用来显示要浏览的网页。

(3)程序主要代码如下。

    private void button1_Click(object sender, EventArgs e)
    {
         Uri address = new Uri(textBox1.Text);    //创建一个 Uri 类型的变量,用来存储要浏览的网页地址
         WebBrowser1.Url = address;               //在 WebBrowser 控件中显示指定的网页
    }
    private void textBox1_KeyPress(object sender, KeyPressEventArgs e)
    {
         if (e.KeyChar == 13)                     //判断是否按下 Enter 键
         {
               if (textBox1.Text != "")
               {
                   button1_Click(sender, e);
               }
         }
    }

运行程序,在“网址”文本框中输入要浏览的网页地址,按Enter键或单击“转到”按钮,即可浏览指定的网页。程序的运行结果如图23.14所示。

图23.14 “Web页面浏览器”窗口

说明

在使用WebBrowser类时会占用大量资源,当使用完后必须调用Dispose方法,以确保及时释放所有资源。

23.2.2 局域网聊天程序

【例23.13】 下面演示如何使用C#制作一个局域网聊天程序,程序开发步骤如下。(实例位置:光盘\TM\sl\23\11)

(1)新建一个Windows应用程序,命名为11,这时程序自动创建了一个解决方案,名称为11。

(2)将默认创建的Windows窗体删除,然后在名称为11的解决方案下添加一个类和两个Windows项目,其中,类文件用来封装接收信息和发送信息的方法,两个Windows项目分别用来作为聊天程序的服务器端和客户端。

(3)类文件的主要代码如下。

注意

① 编写公共类文件时,需要在命名空间区域添加using System.Text、using System.Net.Sockets、using System.Net和using System.Windows.Forms命名空间。

② 编写完公共类之后,需要使用“csc/t:library类文件名”命令在“Visual Studio 2015命令提示”工具中将类文件编译为.dll文件,以便在服务器端和客户端添加调用。

(4)服务器端主要代码如下。

    Class1 class1 = new Class1();
    private void Form1_Load(object sender, EventArgs e)
    {
         this.Hide();                         //隐藏当前窗体
         class1.StartListener();              //调用公共类中的方法接收信息
    }

(5)客户端主要代码如下。

图23.15 局域网聊天程序

程序的运行结果如图23.15所示。

23.2.3 电子邮件的发送与接收

【例23.14】 下面演示如何使用C#实现电子邮件的发送与接收功能,程序开发步骤如下。(实例位置:光盘\TM\sl\23\12)

(1)新建一个Windows应用程序,命名为12,默认窗体为Form1.cs。

(2)在Form1窗体中添加一个MenuStrip控件,用来作为菜单栏。

(3)添加两个Windows窗体,分别命名为frmSend.cs和frmReceive.cs,其中,frmSend.cs用来作为“邮件发送”窗体,frmReceive.cs用来作为“邮件接收”窗体。

(4)“邮件发送”窗体主要代码如下。

说明

Encoding用于对Unicode字符进行操作,而不是对任意二进制数据(如字节数组)进行操作。

(5)“邮件接收”窗体主要代码如下。

(6)运行程序,在主窗体中单击“邮件发送”菜单项,打开“邮件发送”窗口,如图23.16所示。

(7)单击“邮件接收”菜单项,打开“邮件接收”窗口,如图23.17所示。

图23.16 电子邮件的发送

图23.17 电子邮件的接收

23.3 小结

本章主要讲解了使用C#进行网络编程的基础知识。使用C#进行网络编程时,主要用到了System.Net、System.Net.Sockets和System.Net.Mail命名空间中的类,本章对这3个命名空间及其主要的类进行了详细讲解,并通过实例演示了各个类的使用方法,最后使用讲解的知识开发了3个典型的网络应用程序,分别为Web页面浏览器、局域网聊天程序和电子邮件的发送与接收。通过本章的学习,读者应该熟练掌握使用C#进行网络编程的基础知识,并能使用这些知识开发出比较成熟的网络应用程序。

23.4 实践与练习

(1)尝试开发一个程序,要求使用TCP/IP协议实现网络聊天功能。(答案位置:光盘\TM\sl\23\13)

(2)尝试开发一个程序,要求使用SMTP协议发送带附件的邮件。(答案位置:光盘\TM\sl\23\14)

第24章 注册表技术

第24章
注册表技术

视频讲解:21分钟

注册表是一个庞大的数据库系统,它记录了用户安装在计算机上的软件、硬件信息和每一个程序的相互关系。注册表中存放着很多参数,直接控制着整个系统的启动、硬件驱动程序的装载以及应用程序的运行。本章将详细介绍Windows注册表,讲解过程中为了便于读者理解结合了大量的实例。

通过阅读本章,您可以:

 了解什么是注册表

 了解操作注册表的Registry和RegistryKey类

 了解如何读取注册表信息

 掌握如何创建和修改注册表信息

 掌握3种删除注册表信息的方法

24.1 注册表基础

视频讲解:光盘\TM\lx\24\注册表基础.exe

24.1.1 Windows注册表概述

Windows注册表包含Windows安装、用户喜好以及已安装软件和设备的所有配置信息的核心存储库。现在商用软件基本上都使用注册表来存储这些信息,COM组件必须把它的信息存储在注册表中,才能由客户程序调用。注册表的层次结构非常类似于文件系统,它记录了用户账号、服务器硬件以及应用程序的设置信息等。同INI文件相比,注册表可以控制的数据更多,而且不仅仅限于处理字符串类型的数据。注册表也包含了一些系统配置的信息,这些信息根据操作系统的不同而不同。选择“开始”/“运行”命令,在“打开”文本框中输入regedit后,单击“确定”按钮即可打开“注册表编辑器”窗口,如图24.1所示。

图24.1 “注册表编辑器”窗口

注册表就好像是记录信息的存储器,这些信息既可以在存储器中进行记录,也可以在存储器中进行修改和读取。同样,用户也可以在存储器以键/值对的形式进行记录的操作。

24.1.2 Registry和RegistryKey类

.NET Framework提供了访问注册表的类,比较常用的是Registry和RegistryKey类,这两个类都在Microsoft.Win32命名空间中。下面详细介绍这两个类。

1.Registry类

Registry类不能被实例化,它的作用只是实例化RegistryKey类,以便开始在注册表中浏览。Registry类是通过静态属性来提供这些实例的,这些属性共有7个,如表24.1所示。

表24.1 Registry类的常用属性及说明

说明

Registry类主要用于存储有关用户首选项的信息、存储本地计算机的配置信息、存储有关类型和类及其属性的信息、存储有关默认用户配置的信息、存储软件组件的性能信息、存储非用户特定的硬件信息、存储动态数据。

【例24.1】 要获得一个表示HKLM键的RegistryKey实例,代码如下。

    RegistryKey hklm = Registry.LocalMachine;
2.RegistryKey类

RegistryKey实例表示一个注册表项,这个类的方法可以浏览子键、创建新键、读取或修改键中的值。也就是说,该类可以完成对注册表项的所有操作。除了设置键的安全级别之外,RegistryKey类可以用于完成对注册表的所有操作。下面介绍RegistryKey类的常用属性和方法,分别如表24.2和表24.3所示。

表24.2 RegistryKey类的常用属性及说明

表24.3 RegistryKey类的常用方法及说明

说明

RegistryKey类的常用属性和方法在使用时,将会做详细的讲解,此处只给出此类中比较重要的属性和方法以及用途。

24.2 在C#中操作注册表

视频讲解:光盘\TM\lx\24\在C#中操作注册表.exe

在C#中注册表的基本操作主要包括读取注册表中的信息、删除注册表中的信息以及创建和删除注册表信息,下面对这几种注册表的基本操作进行详细的介绍。

24.2.1 读取注册表中的信息

读取注册表中的信息主要是通过RegistryKey类中的OpenSubKey方法、GetSubKeyNames方法和GetValueNames方法实现的。下面分别介绍这几种方法。

1.OpenSubKey方法

此方法用于检索指定的子项。

语法如下:

    public RegistryKey OpenSubKey(string name)

 name:要以只读方式打开的子项的名称或路径。

 返回值:请求的子项;如果操作失败,则为空引用。

说明

如果要打开的项不存在,该方法将返回null引用,而不是引发异常。

【例24.2】 使用OpenSubKey方法打开HKEY_LOCAL_MACHINE\SOFTWARE子键,代码如下。

    private void Form1_Load(object sender, EventArgs e)
    {
         RegistryKey regkey = Registry.LocalMachine;        //创建 RegistryKey 实例
         //使用 OpenSubKey 方法打开 HKEY_LOCAL_MACHINE\SOFTWARE 键
         RegistryKey registrykey = regkey.OpenSubKey(@"SOFTWARE");
    }
2.GetSubKeyNames方法

此方法用于检索包含所有子项名称的字符串数组。

语法如下:

    public string[] GetSubKeyNames()

返回值:包含当前项的子项名称的字符串数组。

说明

如果当前项已被删除,或是用户没有读取该项的权限,将触发异常。

【例24.3】 创建一个Windows应用程序,通过GetSubKeyNames方法检索HKEY_LOCAL_MACHINE\SOFTWARE子键下包含的所有子项名称的字符串数组,代码如下。(实例位置:光盘\TM\sl\24\1)

    //添加 using Microsoft.Win32;命名空间
    private void Form1_Load(object sender, EventArgs e)
    {
         RegistryKey regkey = Registry.LocalMachine;       //创建 RegistryKey 实例
         //使用 OpenSubKey 方法打开 HKEY_LOCAL_MACHINE\SOFTWARE 键
         RegistryKey sys = regkey.OpenSubKey(@"SOFTWARE");
         //调用 foreach 语句读取 HKEY_LOCAL_MACHINE\SOFTWARE 键下的所有项目
         foreach (string str in sys.GetSubKeyNames())
         {
             richTextBox1.Text += str + "\n";
         }
    }

程序的运行结果如图24.2所示。

图24.2 检索指定子键下的所有子项名称

3.GetValueNames方法

此方法用于检索包含与此项关联的所有值名称的字符串数组。

语法如下:

    public string[] GetValueNames()

返回值:包含当前项的值名称的字符串数组。

说明

如果没有找到此项的值名称,则返回一个空数组。如果在注册表项设置了一个具有默认值的名称为空字符串的项,则GetValueNames方法返回的数组中包含该空字符串。

【例24.4】 创建一个Windows应用程序,读取HKEY_LOCAL_MACHINE\SOFTWARE子键下的项目信息,首先通过Registry类实例化一个RegistryKey类对象,然后利用对象的OpenSubKey方法打开指定的键。最后通过循环将所有键值全部提取出来并显示在ListBox控件中,代码如下。(实例位置:光盘\TM\sl\24\2)

程序的运行结果如图24.3所示。

图24.3 检索子键下的项目

24.2.2 创建和修改注册表信息

1.创建注册表信息

通过RegistryKey类的CreateSubKey方法和SetValue方法可以创建注册表信息,下面介绍这两种方法。

(1)CreateSubKey方法主要用于创建一个新子项或打开一个现有子项以进行写访问。

语法如下:

    public RegistryKey CreateSubKey(string subkey)

 subkey:要创建或打开的子项的名称或路径。

 返回值:RegistryKey对象,表示新建的子项或空引用。如果为subkey指定了零长度字符串,则返回当前的RegistryKey对象。

(2)SetValue方法主要用于设置注册表项中的名称/值对的值。

语法如下:

    public void SetValue(string name,Object value)

 name:要存储的值的名称。

 value:要存储的数据。

说明

SetValue方法用于从非托管代码中访问托管类,不应从托管代码调用。

下面通过实例演示如何通过RegistryKey类的CreateSubKey方法和SetValue方法创建一个子键。

【例24.5】 创建一个Windows应用程序,然后在主键HKEY_LOCAL_MACHINE的HARDWARE键下创建一个名为LS的子键,然后在这个子键下再创建一个名为SHJ的子键,在SHJ子键下创建一个名为value、数据值是1234的键值,代码如下。(实例位置:光盘\TM\sl\24\3)

    //添加 using Microsoft.Win32;命名空间
    private void button1_Click(object sender, EventArgs e)
    {
         try
         {
             //创建 RegistryKey 实例
             RegistryKey hklm = Registry.LocalMachine;
             //使用 OpenSubKey 方法打开 HKEY_LOCAL_MACHINE\HARDWARE 键
             RegistryKey software = hklm.OpenSubKey("HARDWARE", true);
             //使用 CreateSubKey 方法创建名为 LS 的子键
             RegistryKey main1 = software.CreateSubKey("LS");
             //使用 CreateSubKey 方法在 LS 键下创建一个名为 SHJ 的子键
             RegistryKey ddd = main1.CreateSubKey("SHJ");
             //在子键 SHJ 下建立一个名为 value 的键值,数据值为 1234
             ddd.SetValue("value", "1234");
             MessageBox.Show("创建成功");
         }
         catch (Exception ex)
         {
             MessageBox.Show(ex.Message);
         }
    }

运行程序,单击“创建子键”按钮,结果如图24.4所示。

图24.4 创建子键

2.修改注册表信息

由于注册表中的信息十分重要,所以一般不要对其进行写的操作。也可能是这个原因,在.Net Framework中并没有提供修改注册表键的方法。而只是提供了一个危害性相对较小的SetValue方法,通过这个方法,可以修改键值。在使用SetValue方法时,如果它检测到指定的键值不存在,就会创建一个新的键值。关于SetValue方法在前面已经做过介绍,所以此处不再做过多的讲解,下面通过一个实例演示如何通过SetValue方法修改注册表信息。

【例24.6】 创建一个Windows应用程序,将主键HKEY_LOCAL_MACHINE\HARDWARE\LS\SHJ下名为value的键值的数据值修改为abcd,代码如下。(实例位置:光盘\TM\sl\24\4)

    //添加 using Microsoft.Win32;命名空间
    private void button1_Click(object sender, EventArgs e)
    {
         try
         {
             //创建 RegistryKey 实例
             RegistryKey hklm = Registry.LocalMachine;
             //使用 OpenSubKey 方法打开 HKEY_LOCAL_MACHINE\HARDWARE 键
             RegistryKey software = hklm.OpenSubKey("HARDWARE", true);
             //使用 OpenSubKey 方法打开 LS 键
             RegistryKey dddw = software.OpenSubKey("LS", true);
             //使用 OpenSubKey 方法打开 LS 键下的 SHJ 子键
             RegistryKey regkey = dddw.OpenSubKey("SHJ ", true);
             //然后使用 SetValue 方法修改指定的键值
             regkey.SetValue("value", "abcd");
             MessageBox.Show("修改成功");
         }
         catch (Exception ex)
         {
             MessageBox.Show(ex.Message);
         }
    }

程序运行前后对比如图24.5和图24.6所示。

图24.5 修改注册表信息之前

图24.6 修改注册表信息之后

24.2.3 删除注册表中的信息

删除注册表中的信息主要通过RegistryKey类中的DeleteSubKey方法、DeleteSubKeyTree方法和DeleteValue方法来实现。这3种方法的功能各有不同,希望读者注意。

1.DeleteSubKey方法

此方法用于删除不包含任何子键的子键。

语法如下:

    public void DeleteSubKey(string subkey,bool throwOnMissingSubKey)

 subkey:要删除的子键的名称。

 throwOnMissingSubKey:如果值为true,在程序调用时,删除的子键不存在,则产生一个错误信息。如果值为false,在程序调用时,删除的子键不存在,也不产生错误信息,程序依然正确运行。

说明

如果删除的项有子级子项,将触发异常。必须将子项删除后,才能删除该项。

【例24.7】 创建一个Windows应用程序,通过RegistryKey类的DeleteSubKey方法删除HKEY_ LOCAL_MACHINE\HARDWARE\LS键下的SHJ子键,代码如下。(实例位置:光盘\TM\sl\24\5)

    //添加 using Microsoft.Win32;命名空间
    private void button1_Click(object sender, EventArgs e)
    {
         try
         {
             //创建 RegistryKey 实例
             RegistryKey hklm = Registry.LocalMachine;
             //使用 OpenSubKey 方法打开 HKEY_LOCAL_MACHINE\HARDWARE 键
             RegistryKey software = hklm.OpenSubKey("HARDWARE", true);
             //打开 LS 子键
             RegistryKey no1 = software.OpenSubKey("LS", true);
             //使用 DeleteSubKey 方法删除名称为 SHJ 的子键
             no1.DeleteSubKey("SHJ", false);
             MessageBox.Show("删除成功");
         }
         catch (Exception ex)
         {
             MessageBox.Show(ex.Message);
         }
    }

运行程序,删除子键的前后对比如图24.7和图24.8所示。

图24.7 删除子键之前

图24.8 删除子键之后

2.DeleteSubKeyTree方法

DeleteSubKeyTree方法用于彻底删除指定的子键目录,包括删除该子键以及该子键以下的全部子键。由于此方法的破坏性非常强,所以在使用时要特别谨慎。

语法如下:

    public void DeleteSubKeyTree(string subkey)

subkey:要彻底删除的子键名称。

说明

当删除的项为null时,则触发异常。

【例24.8】 创建一个Windows应用程序,通过DeleteSubKeyTree方法将彻底删除HKEY_LOCAL_MACHINE\HARDWARE\LS键下的子键,代码如下。(实例位置:光盘\TM\sl\24\6)

    //添加 using Microsoft.Win32;命名空间
    private void button1_Click(object sender, EventArgs e)
    {
         try
         {
             //创建 RegistryKey 实例
             RegistryKey hklm = Registry.LocalMachine;
             //打开 HARDWARE 子键
             RegistryKey software = hklm.OpenSubKey("HARDWARE", true);
             //打开 LS 子键
             RegistryKey no1 = software.OpenSubKey("LS", true);
             //使用 DeleteSubKeyTree 方法彻底删除 SHJ 子键的目录
             no1.DeleteSubKeyTree("SHJ");
             MessageBox.Show("删除成功");
         }
         catch (Exception ex)
         {
             MessageBox.Show(ex.Message);
         }
    }

彻底删除子键前后对比如图24.9和图24.10所示。

图24.9 彻底删除子键之前

图24.10 彻底删除子键之后

3.DeleteValue方法

DeleteValue方法主要用于删除指定的键值。

语法如下:

    public void DeleteValue(string name)

name:要删除的值的名称。

说明

如果在找不到指定值的情况下使用该值,又不想引发异常,可以使用DeleteValue(string name,bool throwOnMissingValue)重载方法。如果throwOnMissingValue参数为true时,将不引发异常。

【例24.9】 创建一个Windows应用程序,通过DeleteValue方法删除HKEY_LOCAL_MACHINE\HARDWARE\LS\SHJ键下的名称为value的键值,代码如下。(实例位置:光盘\TM\sl\24\7)

    //添加 using Microsoft.Win32;命名空间
    private void button1_Click(object sender, EventArgs e)
    {
         try
         {
             //创建 RegistryKey 实例
             RegistryKey hklm = Registry.LocalMachine;
             //打开 HARDWARE 子键
             RegistryKey software = hklm.OpenSubKey("HARDWARE", true);
             //打开 LS 子键
             RegistryKey no1 = software.OpenSubKey("LS", true);
             //打开 SHJ 子键
             RegistryKey no2 = no1.OpenSubKey("SHJ", true);
             //使用 DeleteValue 方法删除名称为 value 的键值
             no2.DeleteValue("value");
             MessageBox.Show("删除键值成功");
         }
         catch (Exception ex)
         {
             MessageBox.Show(ex.Message);
    }
}

运行程序,删除键值前后对比如图24.11和图24.12所示。

图24.11 删除value键值之前

图24.12 删除value键值之后

24.3 小结

本章主要介绍了注册表相关的内容,.NET Framework提供了Registry和RegistryKey类用于操作注册表,这两个类在Microsoft.Win32命名空间下。RegistryKey类中提供了许多方法用于操作注册表,本章详细地介绍了创建、读取、修改和删除注册表信息的方法。

24.4 实践与练习

(1)尝试开发一个程序,要求模仿“注册表编辑器”列出系统信息。(答案位置:光盘\TM\sl\24\8)

(2)尝试开发一个程序,要求控制程序的试用次数为30次。(答案位置:光盘\TM\sl\24\9)

(3)尝试开发一个程序,要求通过注册表实现“加快开/关机速度”、“加快自动刷新率”和“加快菜单显示速度”等系统优化功能。(答案位置:光盘\TM\sl\24\10)

第25章 线程的使用

第25章
线程的使用

视频讲解:30分钟

在Windows应用程序中常常需要执行长时间运行的操作,例如一个算术复杂的运算等,这时,操作的执行速度就显得非常重要,开发人员可以使用线程对要执行的操作分段执行,这样就可以大大提高程序的运行速度和性能。本章将对线程及其基本操作进行详细讲解。

通过阅读本章,您可以:

 了解线程的基本分类及概念

 掌握如何创建线程

 掌握如何暂停和恢复线程的执行

 掌握如何休眠线程

 掌握如何终止线程的执行

 掌握如何设置线程执行的优先级

 掌握如何实现线程同步

25.1 线程简介

视频讲解:光盘\TM\lx\25\线程简介.exe

每个正在操作系统上运行的应用程序都是一个进程,一个进程可以包括一个或多个线程。线程是操作系统分配处理器时间的基本单元,在进程中可以有多个线程同时执行代码。每个线程都维护异常处理程序、调度优先级和一组系统用于在调度该线程前保存线程上下文的结构。线程上下文包括为使线程在线程的宿主进程地址空间中无缝地继续执行所需的所有信息,包括线程的CPU寄存器组和堆栈。本节将对线程进行详细讲解。

进程就好像是一个公司,公司中的每个员工就相当于线程,公司想要运转就必须得有负责人,负责人应相当于主线程。

25.1.1 单线程简介

单线程顾名思义,就是只有一个线程。默认情况下,系统为应用程序分配一个主线程,该线程执行程序中以Main方法开始和结束的代码。

【例25.1】 新建一个Windows应用程序,程序会在Program.cs文件中自动生成一个Main方法,该方法就是主线程的启动入口点。Main方法代码如下。

    [STAThread]
    static void Main()
    {
         Application.EnableVisualStyles();                     //启用应用程序的可视样式
         Application.SetCompatibleTextRenderingDefault(false); //新控件使用 GDI+
         Application.Run(new Form1());
    }

说明

在以上代码中,Application类的Run方法主要用于设置当前项目的主窗体,这里设置的是Form1。

25.1.2 多线程简介

一般情况下,需要用户交互的软件都必须尽可能快地对用户的活动做出反应,以便提供丰富多彩的用户体验,但同时它又必须执行必要的计算以便尽可能快地将数据呈现给用户,这时可以使用多线程来实现。

1.多线程的优点

要提高对用户的响应速度并且处理所需数据以便几乎同时完成工作,使用多线程是一种最为强大的技术,在具有一个处理器的计算机上,多线程可以通过利用用户事件之间很小的时间段在后台处理数据来达到这种效果。例如,通过使用多线程,在另一个线程正在重新计算同一应用程序中的电子表格的其他部分时,用户可以编辑该电子表格。

单个应用程序域可以使用多线程来完成以下任务:

 通过网络与Web服务器和数据库进行通信。

 执行占用大量时间的操作。

 区分具有不同优先级的任务。

 使用户界面可以在将时间分配给后台任务时仍能快速做出响应。

2.多线程的缺点

使用多线程有好处,同时也有坏处,建议一般不要在程序中使用太多的线程,这样可以最大限度地减少操作系统资源的使用,并可提高性能。

如果在程序中使用了多线程,可能会产生如下问题。

 系统将为进程、AppDomain对象和线程所需的上下文信息使用内存。因此,可以创建的进程、AppDomain对象和线程的数目会受到可用内存的限制。

 跟踪大量的线程将占用大量的处理器时间。如果线程过多,则其中大多数线程都不会产生明显的进度。如果大多数当前线程处于一个进程中,则其他进程中的线程的调度频率就会很低。

 使用许多线程控制代码执行非常复杂,并可能产生许多bug。

 销毁线程需要了解可能发生的问题并对那些问题进行处理。

25.2 线程的基本操作

视频讲解:光盘\TM\lx\25\线程的基本操作.exe

C#中对线程进行操作时,主要用到了Thread类,该类位于System.Threading命名空间下。通过使用Thread类,可以对线程进行创建、暂停、恢复、休眠、终止及设置优先权等操作。另外,还可以通过使用Monitor类、Mutex类和lock关键字控制线程间的同步执行。本节将对Thread类及线程的基本操作进行详细讲解。

25.2.1 Thread类

Thread类位于System.Threading命名空间下,System.Threading命名空间提供一些可以进行多线程编程的类和接口。除同步线程活动和访问数据的类(Mutex、Monitor、Interlocked和AutoResetEvent等)外,该命名空间还包含一个ThreadPool类(它允许用户使用系统提供的线程池)和一个Timer类(它在线程池的线程上执行回调方法)。

Thread类主要用于创建并控制线程、设置线程优先级并获取其状态。一个进程可以创建一个或多个线程以执行与该进程关联的部分程序代码,线程执行的程序代码由ThreadStart委托或ParameterizedThreadStart委托指定。

线程运行期间,不同的时刻会表现为不同的状态,但它总是处于由ThreadState定义的一个或多个状态中。用户可以通过使用ThreadPriority枚举为线程定义优先级,但不能保证操作系统会接受该优先级。

Thread类的常用属性及说明如表25.1所示。

表25.1 Thread类的常用属性及说明

Thread类的常用方法及说明如表25.2所示。

表25.2 Thread类的常用方法及说明

【例25.2】 下面演示使用Thread类的相关方法和属性,开始运行一个线程,并获得该线程的相关信息,程序开发步骤如下。(实例位置:光盘\TM\sl\25\1)

(1)新建一个Windows应用程序,命名为1,默认窗体为Form1.cs。

(2)在Form1窗体中添加一个RichTextBox控件,用来显示获得的线程相关信息。

(3)程序主要代码如下。

说明

在程序中使用线程时,需要在命名空间区域添加using System.Threading命名空间,下面遇到时将不再提示。

运行程序,先后弹出如图25.1和图25.2所示的对话框,然后显示如图25.3所示的主窗体,并在主窗体中显示获得的线程相关信息。

图25.1 线程开始运行

图25.2 线程运行结束

图25.3 主窗体

25.2.2 创建线程

创建一个线程非常简单,只需将其声明并为其提供线程起始点处的方法委托即可。创建新的线程时,需要使用Thread类,Thread类具有接受一个ThreadStart委托或ParameterizedThreadStart委托的构造函数,该委托包装了调用Start方法时由新线程调用的方法。创建了Thread类的对象之后,线程对象已存在并已配置,但并未创建实际的线程,这时,只有在调用Start方法后,才会创建实际的线程。

Start方法用来使线程被安排执行,它有两种重载形式,下面分别进行介绍。

(1)导致操作系统将当前实例的状态更改为ThreadState.Running,语法如下。

    public void Start()

(2)使操作系统将当前实例的状态更改为ThreadState.Running,并选择提供包含线程执行的方法要使用的数据的对象,语法如下。

    public void Start(Object parameter)

parameter:一个对象,包含线程执行的方法要使用的数据。

注意

如果线程已经终止,就无法通过再次调用Start方法来重新启动。

【例25.3】 创建一个控制台应用程序,其中自定义一个静态的void类型方法createThread。然后在Main方法中通过实例化Thread类对象创建一个新的线程。最后调用Start方法启动该线程,代码如下。(实例位置:光盘\TM\sl\25\2)

注意

线程的入口(本例中为createThread)不带任何参数。

25.2.3 线程的挂起与恢复

创建完一个线程并启动之后,还可以挂起、恢复、休眠或终止它,本节主要对线程的挂起与恢复进行讲解。

线程的挂起与恢复分别可以通过调用Thread类中的Suspend方法和Resume方法实现,下面对这两个方法进行详细介绍。

1.Suspend方法

该方法用来挂起线程,如果线程已挂起,则不起作用,语法如下。

    public void Suspend()

说明

调用Suspend方法挂起线程时,.NET允许要挂起的线程再执行几个指令,目的是为了到达.NET认为线程可以安全挂起的状态。

2.Resume方法

该方法用来继续已挂起的线程,语法如下。

    public void Resume()

说明

通过Resume方法来恢复被暂停的线程时,无论调用了多少次Suspend方法,调用Resume方法均会使另一个线程脱离挂起状态,并导致该线程继续执行。

【例25.4】 创建一个控制台应用程序,其中通过实例化Thread类对象创建一个新的线程,然后调用Start方法启动该线程,之后先后调用Suspend方法和Resume方法挂起和恢复创建的线程,代码如下。(实例位置:光盘\TM\sl\25\3)

25.2.4 线程休眠

线程休眠主要通过Thread类的Sleep方法实现,该方法用来将当前线程阻止指定的时间,它有两种重载形式,下面分别进行介绍。

(1)将当前线程挂起指定的时间,语法如下。

    public static void Sleep(int millisecondsTimeout)

millisecondsTimeout:线程被阻止的毫秒数。指定零以指示应挂起此线程以使其他等待线程能够执行,指定Infinite以无限期阻止线程。

(2)将当前线程阻止指定的时间,语法如下。

    public static void Sleep(TimeSpan timeout)

timeout:线程被阻止的时间量的TimeSpan。指定零以指示应挂起此线程以使其他等待线程能够执行,指定Infinite以无限期阻止线程。

【例25.5】 要使当前线程休眠一秒钟,代码如下。

    Thread.Sleep(1000); //使线程休眠一秒钟

25.2.5 终止线程

终止线程可以分别使用Thread类的Abort方法和Join方法实现,下面对这两个方法进行详细介绍。

1.Abort方法

Abort方法用来终止线程,它有两种重载形式,下面分别进行介绍。

(1)终止线程,在调用此方法的线程上引发ThreadAbortException异常,以开始终止此线程的过程,语法如下。

    public void Abort()

(2)终止线程,在调用此方法的线程上引发ThreadAbortException异常,以开始终止此线程并提供有关线程终止的异常信息的过程,语法如下。

    public void Abort(Object stateInfo)

stateInfo:一个对象,它包含应用程序特定的信息(如状态),该信息可供正被终止的线程使用。

【例25.6】 创建一个控制台应用程序,在其中开始一个线程,然后调用Thread类的Abort方法终止已开启的线程,代码如下。(实例位置:光盘\TM\sl\25\4)

注意

线程的Abort方法用于永久地停止托管线程。调用Abort方法时,公共语言运行库在目标线程中引发ThreadAbortException异常,目标线程可捕捉此异常。一旦线程被终止,它将无法重新启动。

2.Join方法

Join方法用来阻止调用线程,直到某个线程终止时为止,它有3种重载形式,下面分别进行介绍。

(1)在继续执行标准的COM和SendMessage消息处理期间阻止调用线程,直到某个线程终止为止,语法如下。

    public void Join()

(2)在继续执行标准的COM和SendMessage消息处理期间阻止调用线程,直到某个线程终止或经过了指定时间为止,语法如下。

    public bool Join(int millisecondsTimeout)

 millisecondsTimeout:等待线程终止的毫秒数。

 返回值:如果线程已终止,则为true;如果线程在经过了millisecondsTimeout参数指定的时间量后未终止,则为false。

(3)在继续执行标准的COM和SendMessage消息处理期间阻止调用线程,直到某个线程终止或经过了指定时间为止,语法如下。

    public bool Join(TimeSpan timeout)

 timeout:等待线程终止的时间量的TimeSpan。

 返回值:如果线程已终止,则为true;如果线程在经过了timeout参数指定的时间量后未终止,则为false。

【例25.7】 创建一个控制台应用程序,其中调用了Thread类的Join方法等待线程终止,代码如下。(实例位置:光盘\TM\sl\25\5)

注意

如果在应用程序中使用了多线程,辅助线程还没有执行完毕,在关闭窗体时必须关闭辅助线程,否则会引发异常。

25.2.6 线程的优先级

线程的优先级指定一个线程相对于另一个线程的相对优先级。每个线程都有一个分配的优先级。在公共语言运行库内创建的线程最初被分配为Normal优先级,而在公共语言运行库外创建的线程,在进入公共语言运行库时将保留其先前的优先级。

线程是根据其优先级而调度执行的,用于确定线程执行顺序的调度算法随操作系统的不同而不同。在某些操作系统下,具有最高优先级(相对于可执行线程而言)的线程经过调度后总是首先运行。如果具有相同优先级的多个线程都可用,则程序将遍历处于该优先级的线程,并为每个线程提供一个固定的时间片段来执行。只要具有较高优先级的线程可以运行,具有较低优先级的线程就不会执行。如果在给定的优先级上不再有可运行的线程,则程序将移到下一个较低的优先级并在该优先级上调度线程以执行。如果具有较高优先级的线程可以运行,则具有较低优先级的线程将被抢先,并允许具有较高优先级的线程再次执行。除此之外,当应用程序的用户界面在前台和后台之间移动时,操作系统还可以动态调整线程的优先级。

说明

一个线程的优先级不影响该线程的状态,该线程的状态在操作系统可以调度该线程之前必须为Running。

线程的优先级值及说明如表25.3所示。

表25.3 线程的优先级值及说明

开发人员可以通过访问线程的Priority属性来获取和设置其优先级。Priority属性用来获取或设置一个值,该值指示线程的调度优先级,其语法如下。

    public ThreadPriority Priority { get; set; }

属性值:ThreadPriority值之一。默认值为Normal。

【例25.8】 创建一个控制台应用程序,其中创建了两个Thread线程类对象,并设置第一个Thread类对象的优先级为最低,然后调用Start方法开启这两个线程,代码如下。(实例位置:光盘\TM\sl\25\6)

程序的运行结果如图25.4所示。

图25.4 设置线程的优先级

25.2.7 线程同步

在应用程序中使用多个线程的一个好处是每个线程都可以异步执行。对于Windows应用程序,耗时的任务可以在后台执行,而使应用程序窗口和控件保持响应。对于服务器应用程序,多线程处理提供了用不同线程处理每个传入请求的能力。否则,在完全满足前一个请求之前,将无法处理每个新请求。然而,线程的异步性意味着必须协调对资源(如文件句柄、网络连接和内存)的访问。否则,两个或更多的线程可能在同一时间访问相同的资源,而每个线程都不知道其他线程的操作,结果将产生不可预知的数据损坏。

线程同步是指并发线程高效、有序地访问共享资源所采用的技术,所谓同步,是指某一时刻只有一个线程可以访问资源,只有当资源所有者主动放弃了代码或资源的所有权时,其他线程才可以使用这些资源。

线程同步可以分别使用C#中的lock关键字、Monitor类和Mutex类实现,下面对这几种实现方法进行详细介绍。

1.使用C#中的lock关键字实现线程同步

lock关键字可以用来确保代码块完成运行,而不会被其他线程中断,它是通过在代码块运行期间为给定对象获取互斥锁来实现的。

lock语句以关键字lock开头,它有一个作为参数的对象,在该参数的后面还有一个一次只能由一个线程执行的代码块。lock语句语法格式如下。

    Object thisLock = new Object();
    lock (thisLock)
    {
         //要运行的代码块
    }

提供给lock语句的参数必须为基于引用类型的对象,该对象用来定义锁的范围。严格地说,提供给lock语句的参数只是用来唯一标识由多个线程共享的资源,所以它可以是任意类实例。然而,此参数通常表示需要进行线程同步的资源。例如,如果一个容器对象将被多个线程使用,则可以将该容器传递给lock语句,而lock语句中的代码块将访问该容器。只要其他线程在访问该容器前先锁定该容器,则对该对象的访问将是安全同步的。

通常,最好避免锁定public类型或不受应用程序控制的对象实例。例如,如果该实例可以被公开访问,则lock(this)可能会有问题。因为不受控制的代码也可能会锁定该对象,这将可能导致死锁,即两个或更多个线程等待释放同一个对象。出于同样的原因,锁定公共数据类型(相比于对象)也可能导致问题,锁定字符串尤其危险。因为字符串被公共语言运行库(CLR)“暂留”,这意味着整个程序中任何给定字符串都只有一个实例。因此,只要在应用程序进程中的任何具有相同内容的字符串上放置了锁,就将锁定应用程序中该字符串的所有实例。因此,最好锁定不会被暂留的私有或受保护成员。

说明

事实上lock语句是用Monitor类来实现的,它等效于try/finally语句块,使用lock关键字通常比直接使用Monitor类更可取,一方面是因为lock更简洁,另一方面是因为lock确保了即使受保护的代码引发异常,也可以释放基础监视器。这是通过finally关键字来实现的,无论是否引发异常它都执行关联的代码块。

【例25.9】 创建一个控制台应用程序,其中自定义了一个LockThread方法,该方法中使用lock关键字锁定当前线程,然后在Main方法中通过Program的类对象调用LockThread自定义方法,代码如下。(实例位置:光盘\TM\sl\25\7)

2.使用Monitor类实现线程同步

Monitor类提供了同步对象的访问机制,它通过向单个线程授予对象锁来控制对对象的访问,对象锁提供限制访问代码块(通常称为临界区)的能力。当一个线程拥有对象锁时,其他任何线程都不能获取该锁。

Monitor类的主要功能如下:

(1)它根据需要与某个对象相关联。

(2)它是未绑定的,也就是说可以直接从任何上下文调用它。

(3)不能创建Monitor类的实例。

Monitor类的常用方法及说明如表25.4所示。

表25.4 Monitor类的常用方法及说明

注意

使用Monitor类锁定的是对象(即引用类型)而不是值类型。

【例25.10】 创建一个控制台应用程序,其中自定义了一个LockThread方法,该方法中首先使用Monitor类的Enter方法锁定当前的线程。然后再调用Monitor类的Exit方法释放当前的线程。最后在Main方法中通过Program的类对象调用LockThread自定义方法,代码如下。(实例位置:光盘\TM\sl\25\8)

说明

从例25.9和例25.10来看,这两个例子实现的功能是相同的,但似乎使用lock关键字更简单一些,那为何还要使用Monitor类呢?因为使用Monitor类有更好的控制能力,例如,它可以使用Wait方法指示活动的线程等待一段时间,当线程完成操作时,还可以使用Pulse方法或PulseAll方法通知等待中的线程。

3.使用Mutex类实现线程同步

当两个或更多线程需要同时访问一个共享资源时,系统需要使用同步机制来确保一次只有一个线程使用该资源。Mutex类是同步基元,它只向一个线程授予对共享资源的独占访问权。如果一个线程获取了互斥体,则要获取该互斥体的第二个线程将被挂起,直到第一个线程释放该互斥体。Mutex类与监视器类似,它防止多个线程在某一时间同时执行某个代码块,然而与监视器不同的是,Mutex类可以用来使跨进程的线程同步。

可以使用WaitHandle.WaitOne方法请求互斥体的所属权,拥有互斥体的线程可以在对WaitOne方法的重复调用中请求相同的互斥体而不会阻止其执行,但线程必须调用同样多次数的ReleaseMutex方法以释放互斥体的所属权。Mutex类强制线程标识,因此互斥体只能由获得它的线程释放。

当用于进程间同步时,Mutex称为“命名Mutex”,因为它将用于另一个应用程序,因此它不能通过全局变量或静态变量共享。必须给它指定一个名称,才能使两个应用程序访问同一个Mutex对象。

Mutex类的常用方法及说明如表25.5所示。

表25.5 Mutex类的常用方法及说明

使用Mutex类实现线程同步很简单,首先实例化一个Mutex类对象,它的构造函数中比较常用的有public Mutex(bool initallyOwned)。其中,参数initallyOwned指定了创建该对象的线程是否希望立即获得其所有权,当在一个资源得到保护的类中创建Mutex类对象时,常将该参数设置为false。然后在需要单线程访问的地方调用其等待方法,等待方法请求Mutex对象的所有权。这时,如果该所有权被另一个线程所拥有,则阻塞请求线程,并将其放入等待队列中,请求线程将保持阻塞,直到Mutex对象收到了其所有者线程发出将其释放的信号为止。所有者线程在终止时释放Mutex对象,或者调用ReleaseMutex方法来释放Mutex对象。

说明

尽管Mutex类可以用于进程内的线程同步,但是使用Monitor类通常更为可取,因为Monitor监视器是专门为.NET Framework而设计的,因而它可以更好地利用资源。相比之下,Mutex类是Win32构造的包装。尽管Mutex类比监视器更为强大,但是相对于Monitor类,它所需要的互操作转换更消耗计算机资源。

【例25.11】 创建一个控制台应用程序,其中自定义了一个LockThread方法,该方法中首先使用Mutex类对象的WaitOne方法阻止当前线程。然后再调用Mutex类对象的ReleaseMutex方法释放Mutex对象,即释放当前的线程。最后在Main方法中通过Program的类对象调用LockThread自定义方法,代码如下。(实例位置:光盘\TM\sl\25\9)

    static void Main(string[] args)
    {
         Program myProgram = new Program(); //实例化类对象
         myProgram.LockThread();            //调用锁定线程方法
    }
    void LockThread()
    {
         Mutex myMutex=new Mutex(false);    //实例化 Mutex 类对象
         myMutex.WaitOne();                 //阻止当前线程
         Console.WriteLine("锁定线程以实现线程同步");
         myMutex.ReleaseMutex();            //释放 Mutex 对象
    }

25.3 小结

本章首先对线程的分类做了一个简单的介绍,然后详细讲解了C#中进行线程编程的主要类Thread,并对线程编程的各种基本操作进行了详细讲解。通过本章的学习,读者应该熟练掌握使用C#进行线程编程的基础知识,并能在实际开发中应用线程处理各种多任务问题。

25.4 实践与练习

(1)尝试开发一个程序,要求通过使用线程休眠控制图片以百叶窗效果显示。(答案位置:光盘\TM\sl\25\10)

(2)尝试开发一个程序,要求创建一个线程,用来连接数据库,并显示其中的数据。然后再创建一个线程,用来向数据库中插入数据,最后得到的结果为插入数据后的数据表中的内容。(答案位置:光盘\TM\sl\25\11)

第4篇 项目实战

第4篇 项目实战

 第26章 企业人事管理系统

本篇通过一个大型、完整的企业人事管理系统,运用软件工程的设计思想,让读者学习如何进行软件项目的实践开发。书中按照编写项目计划书→系统设计→数据库设计→创建项目→实现项目→运行项目→解决开发常见问题的过程进行介绍,带领读者一步一步亲身体验开发项目的全过程。

第26章 企业人事管理系统

第26章
企业人事管理系统

视频讲解:114分钟

人事管理是现代企业管理工作不可缺少的一部分,是推动企业走向科学化、规范化的必要条件。员工是企业生存的主要元素,员工的增减、变动将直接影响到企业的整体运作。企业员工越多、分工越细、联系越密,所要做的统计工作就越多,人事管理的难度就越大。随着企业的不断壮大,自动化的企业人事管理系统就显得非常必要,本章将通过使用C#4.6+SQL Server 2012技术开发一个企业人事管理系统。

通过阅读本章,您可以:

 掌握如何用自定义方法对不同的数据表进行添加、修改的操作

 掌握如何用自定义方法快速实现多条件的查询

 掌握如何在数据库中读取或写入图片

 掌握如何将数据信息以自定义表格的形式插入到Word中

 掌握如何将数据信息导出到Excel中

 掌握如何向Word中插入数据库中的图片

26.1 系统分析

视频讲解:光盘\TM\lx\26\系统分析.exe

26.1.1 需求分析

基于其他企业人事管理软件的不足,要求能够制作一个系统,通过其可以方便、快捷地对职工信息进行添加、修改、删除等操作,并且可以在数据库中存储相应职工的照片。为了能够更好地存储职工信息,可以将职工信息添加到Word文档或者Excel表格,这样,不但便于保存,还可以通过Word文档或者Excel表格进行打印。

26.1.2 可行性分析

根据《GB8567—88计算机软件产品开发文件编制指南》中可行性分析的要求,制定可行性研究报告如下。

1.引言

(1)编写目的

为了给软件开发企业的决策层提供是否进行项目实施的参考依据,现以文件的形式分析项目的风险、项目需要的投资与效益。

(2)背景

×××科技有限公司是一家以计算机软件技术为核心的高科技型企业,为了更好地对公司内部的人员进行管理,现需要委托其他公司开发一个人事管理相关的软件,项目名称为“企业人事管理系统”。

2.可行性研究的前提

(1)要求

 可以真正地实现对企业人事的管理。

 系统的功能要符合本企业的实际情况。

 系统的功能操作要方便、易懂,不要有多余或复杂的操作。

 可以方便地对人事信息进行输出打印。

(2)目标

方便对企业内部的人事档案及岗位调动等进行管理。

(3)交付时间

项目需要在两个月内交付用户使用,系统分析人员需要3天内到位,用户需要5天时间确认需求分析文档,去除其中可能出现的问题。例如用户可能临时有事,占用7天时间确认需求分析。那么程序开发人员需要在50天的时间内进行系统设计、程序编码、系统测试、程序调试和系统打包部署工作,其间,还包括了员工每周的休息时间。

3.投资及效益分析

(1)支出

根据预算,公司计划投入8个人,为此需要支付9万元的工资及各种福利待遇。项目的安装、调试以及用户培训、员工出差等费用支出需要2.5万元。在项目后期维护阶段预计需要投入3万元的资金,累计项目投入需要14.5万元资金。

(2)收益

客户提供项目开发资金30万元,对于项目后期进行的改动,采取协商的原则,根据改动规模额外提供资金。因此,从投资与收益的效益比上,公司大致可以获得15.5万元的利润。

项目完成后,会给公司提供资源储备,包括技术、经验的积累。

4.结论

根据上面的分析,在技术上不会存在问题,因此项目延期的可能性很小。在效益上,公司投入8个人、两个月的时间获利15.5万元,比较可观。另外,公司还可以储备项目开发的经验和资源。因此,认为该项目可以开发。

26.1.3 编写项目计划书

根据《GB8567—88计算机软件产品开发文件编制指南》中的项目开发计划要求,结合单位实际情况,设计项目计划书如下。

1.引言

(1)编写目的

为了能使项目按照合理的顺序开展,并保证按时、高质量地完成,现拟订项目计划书,将项目开发生命周期中的任务范围、团队组织结构、团队成员的工作任务、团队内外沟通协作方式、开发进度、检查项目工作等内容描述出来,作为项目相关人员之间的共识、约定以及项目生命周期内的所有项目活动的行动基础。

(2)背景

企业人事管理系统是本公司与×××科技有限公司签订的待开发项目,项目性质为人事管理类型,可以方便企业管理者对企业内部的人事变更、调动等管理,项目周期为两个月。项目背景规划如表26.1所示。

表26.1 项目背景规划

2.概述

(1)项目目标

项目应当符合SMART原则,把项目要完成的工作用清晰的语言描述出来。企业人事管理系统的主要目标是为企业的管理者提供一套能够方便地对企业内部人员的变更及调动等进行管理的软件。

(2)应交付成果

项目开发完成后,交付的内容如下:

 以光盘的形式提供企业人事管理系统的源程序、系统数据库文件、系统打包文件和系统使用说明书。

 系统发布后,进行无偿维护和服务6个月,超过6个月进行系统有偿维护与服务。

(3)项目开发环境

开发本项目所用的操作系统可以是Windows 7或Windows 8,开发工具为Visual Studio 2015,数据库采用SQL Server 2012。

(4)项目验收方式与依据

项目验收分为内部验收和外部验收两种方式。项目开发完成后,首先进行内部验收,由测试人员根据用户需求和项目目标进行验收。项目在通过内部验收后,然后交给客户进行外部验收,验收的主要依据为需求规格说明书。

3.项目团队组织

(1)组织结构

本公司针对该项目组建了一个由公司副经理、项目经理、系统分析员、软件工程师、界面设计师和测试人员构成的开发团队,团队结构如图26.1所示。

图26.1 项目开发团队结构

(2)人员分工

为了明确项目团队中每个人的任务分工,现制定人员分工表,如表26.2所示。

表26.2 人员分工表

26.2 系统设计

视频讲解:光盘\TM\lx\26\系统设计.exe

26.2.1 系统目标

根据企业对人事管理的要求,制定企业人事管理系统目标如下。

 操作简单方便、界面简洁美观。

 在查看员工信息时,可以对当前员工的家庭情况、培训情况进行添加、修改、删除操作。

 方便快捷地全方位数据查询。

 按照指定的条件对员工进行统计。

 可以将员工信息以表格的形式导出到Word文档中以便进行打印。

 可以将员工信息导出到Excel表格中以便进行打印。

 灵活的数据备份、还原及清空功能。

 由于该系统的使用对象较多,所以要有较好的权限管理。

 能够在当前运行的系统中重新进行登录。

 系统运行稳定、安全可靠。

26.2.2 系统功能结构

企业人事管理系统的功能结构如图26.2所示。

图26.2 企业人事管理系统功能结构

26.2.3 系统业务流程图

企业人事管理系统的业务流程如图26.3所示。

图26.3 企业人事管理系统的业务流程

注意

在制作项目前,必须根据其实现目标制作业务流程图。

26.2.4 系统编码规范

开发程序时,往往会有多人参与,为了使程序的结构与代码风格标准化,以便于使每个参与开发的人员尽可能直观地查看和理解其他人编写的代码,需要在编码之前制定一套统一的编码规范。下面介绍一套C#中常用的编码规范供读者参考。

1.数据库命名规范

(1)数据库

数据库命名以字母db开头(小写),后面加数据库相关英文单词或缩写。下面将举例进行说明,如表26.3所示。

表26.3 数据库命名

(2)数据表

数据表以字母tb开头(小写),后面加数据表相关英文单词或缩写。下面将举例进行说明,如表26.4所示。

表26.4 数据表命名

(3)字段

字段一律采用英文单词或词组(可利用翻译软件)命名,如找不到专业的英文单词或词组,可以用相同意义的英文单词或词组代替。下面将举例进行说明,如表26.5所示。

表26.5 字段命名

(4)视图

视图命名以字母view开头(小写),后面加表示该视图作用的相关英文单词或缩写。下面将举例进行说明,如表26.6所示。

表26.6 视图命名

(5)存储过程

存储过程命名以字母proc开头(小写),后面加表示该存储过程作用的相关英文单词或缩写。下面将举例进行说明,如表26.7所示。

表26.7 存储过程命名

(6)触发器

触发器命名以字母trig开头(小写),后面加表示该触发器作用的相关英文单词或缩写。下面将举例进行说明,如表26.8所示。

表26.8 触发器命名

说明

在数据库中使用命名规范,有助于其他用户更好地理解数据表及表中各字段的内容。

2.程序代码命名规范

(1)变量及对象名称定义规则

根据不同的程序需要,编写代码时都需要定义一定的变量或常量。下面介绍一种常见的变量及常量命名规则,如表26.9所示。

表26.9 变量及常量命名规则

(2)数据类型简写规则

程序中定义常量、变量或方法等内容时,常常需要指定类型。下面介绍一种常见的数据类型简写规则,如表26.10所示。

表26.10 数据类型简写规则

(3)控件命名规则

所有的对象名称都为自然名称的拼音简写,出现冲突可采用不同的简写规则。另外,在编码过程中涉及不到编码的控件,其名称可以取默认名称。控件命名规则如表26.11所示。

表26.11 控件命名规则

上面介绍的是一套C#中常用的编码规范,下面对本系统中比较特殊的编码规范进行说明。

(1)窗体命名规范

在创建一个窗体时,首先对窗体的ID进行命名,本系统中统一命名为“F_+窗体名称”,其中窗体名称最好是英文形式的窗体说明,便于开发者通过窗体ID就能知道该窗体的作用。例如登录窗体,ID名为F_Login。

在窗体中调用其他窗体时,必须对调用窗体进行引用,其引用的变量名为“Frm+窗体名称”,如登录窗体的引用名为FrmLogin。

(2)添加、修改操作中各控件的命名规范

在对数据进行编辑时,如果数据表中的字段过多,很难将窗体中对应的控件值组合成SQL语句。为了便于对数据库中的信息进行添加、修改操作,各字段所对应的控件命名为“表名(或部分表名)_数字”,这里的数字是根据数据表中相应字段的顺序进行编号的。例如将一个控件与tb_WorkResume(工作简历表)数据表中的第3个字段建立关系,应将其Name属性设为Word_2。

(3)查询操作中各控件的命名规范

当使用多字段对数据表中的数据进行查询时,将窗体中相应的控件值组合成查询语句是非常麻烦的,为了能够快速组合查询条件,可以将设置查询条件的控件命名为“表名_相应字段名”。当查询条件需要逻辑运算符时,将记录逻辑运算符的控件命名为“相应字段名_+Sign”。这样即可通过字段名来组合查询条件。例如查询年龄大于30的职工,年龄的字段名为Age,条件控件名为Find_Age,逻辑控件名为Age_Sign,通过条件控件和逻辑控件即可组合成查询条件。

说明

在项目中使用良好的命名规则,有助于开发者快速了解对编写后的变量、方法、类、窗体以及各控件的用处。

26.3 系统运行环境

视频讲解:光盘\TM\lx\26\系统运行环境.exe

本系统的程序运行环境具体如下。

 系统开发平台:Microsoft Visual Studio 2015。

 系统开发语言:C#。

 数据库管理软件:Microsoft SQL Server 2012。

 运行平台:Windows 7(SP1)/Windows 8/Windows 8.1。

 运行环境:Microsoft .NET Framework SDK v4.6。

 分辨率:最佳效果1024×768像素。

26.4 数据库与数据表设计

视频讲解:光盘\TM\lx\26\数据库与数据表设计.exe

开发应用程序时,对数据库的操作是必不可少的。数据库设计是根据程序的需求及其实现功能所制定的,数据库设计的合理性将直接影响程序的开发过程。

26.4.1 数据库分析

企业人事管理系统主要用来记录一个企业中所有员工的基本信息以及每个员工的工作简历、家庭成员、奖惩记录等,数据量是根据企业员工的多少来决定的。SQL Server 2012作为目前常用的一种数据库,该数据库系统在安全性、准确性和运行速度方面有绝对的优势,并且处理数据量大、效率高,所以本系统采用了SQL Server 2012数据库作为后台数据库。数据库命名为db_PWMS,其中包含了23张数据表,用于存储不同的信息,详细信息如图26.4所示。

图26.4 企业人事管理系统中用到的数据表

26.4.2 创建数据库

在SQL Server 2012中创建数据库db_PWMS的具体步骤如下。

(1)在Windows 8.1操作系统的开始界面中找到SQL Server 2012的SQL Server Management Studio,单击打开。

(2)打开如图26.5所示的“连接到服务器”对话框,在该对话框中选择登录的服务器名称和身份验证方式,然后输入登录用户名和登录密码。

图26.5 “连接到服务器”对话框

(3)单击“连接”按钮,连接到指定的SQL Server 2012服务器,然后展开服务器节点,选中“数据库”节点,单击鼠标右键,在弹出的快捷菜单中选择“新建数据库”命令,如图26.6所示。

图26.6 选择“新建数据库”命令

说明

在创建数据库之前,首先要在数据库SQL Server 2012中打开数据库的连接。

(4)打开如图26.7所示的“新建数据库”对话框,在该对话框中输入新建的数据库的名称“db_PWMS”,选择数据库所有者和存放路径,这里的数据库所有者一般为默认。

图26.7 “新建数据库”对话框

(5)单击“确定”按钮,即可新建一个db_PWMS数据库,如图26.8所示。

图26.8 新建的db_PWMS数据库

26.4.3 创建数据表

在已经创建的数据库db_PWMS中创建23个数据表,创建完成后的数据表及其记录数据如图26.9所示。

下面以tb_Login表为例介绍创建数据表的过程。

(1)展开新建的db_PWMS数据库节点,选中“表”节点,单击鼠标右键,在弹出的快捷菜单中选择“新建表”命令,如图26.10所示。

图26.9 创建完成后的数据表及其记录数据

图26.10 选择“新建表”命令

(2)在SQL Server 2012管理器的右边显示一个新表,这里输入要创建的表中所需要的字段,并设置主键,如图26.11所示。

(3)单击“保存”按钮,弹出“选择名称”对话框,如图26.12所示,输入要新建的表名“tb_Login”,单击“确定”按钮,即可在数据库中添加一个表。

图26.11 添加字段

图26.12 “选择名称”对话框

说明

在创建数据表前,首先要根据项目实际要求制定相关的数据表结构,然后在数据库中创建相应的数据表。

由于篇幅有限,所以其他数据表的创建过程不再介绍,相信读者能够举一反三,结合下面给出的数据表结构,其他数据表的创建也不成问题。

(1)tb_UserPope(用户权限表)

表tb_UserPope用于保存每个操作员使用程序的相关权限,该表的结构如表26.12所示。

表26.12 用户权限表

(2)tb_PopeModel(权限模块表)

tb_PopeModel表用于保存程序中所涉及的所有权限名称,该表的结构如表26.13所示。

表26.13 权限模块表

(3)tb_EmployeeGenre(职工类别表)

tb_EmployeeGenre表用于保存职工类别的相关信息,该表的结构如表26.14所示。

表26.14 职工类别表

(4)tb_Staffbasic(职工基本信息表)

tb_Staffbasic表用于保存职工的基本信息,该表的结构如表26.15所示。

表26.15 职工基本信息表

(5)tb_Family(家庭关系表)

tb_Family表用于保存家庭关系的相关信息,该表的结构如表26.16所示。

表26.16 家庭关系表

(6)tb_WordResume(工作简历表)

tb_WordResume表用于保存工作简历的相关信息,该表的结构如表26.17所示。

表26.17 工作简历表

(7)tb_RANDP(奖惩表)

tb_RANDP表用于保存职工奖惩记录的信息,该表的结构如表26.18所示。

表26.18 奖惩表

(8)tb_Individual(个人简历表)

tb_Individual表用于保存职工个人简历的信息,该表的结构如表26.19所示。

表26.19 个人简历表

说明

在设计数据表时,应在相应字段的说明部分对字段的用处进行说明,以便于在对数据表进行操作时,快速了解各字段的用处。

(9)tb_DayWordPad(日常记事本)

tb_DayWordPad表用于保存人事方面的一些日常事情,该表的结构如表26.20所示。

表26.20 日常记事本表

(10)tb_TrainNote(培训记录表)

tb_TrainNote表用于保存职员培训记录的相关信息,该表的结构如表26.21所示。

表26.21 培训记录表

(11)tb_AddressBook(通讯录)

tb_AddressBook表用于保存职员的其他联系信息,该表的结构如表26.22所示。

表26.22 通讯录

说明

由于篇幅有限,这里只列举了重要的数据表的结构,其他的数据表结构可参见光盘中的数据库文件。

26.4.4 数据表逻辑关系

为了使读者能够更好地了解职工基本信息表与其他各表之间的关系,在这里给出数据表关系图,如图26.13所示。通过图26.13的表关系可以看出,职工基本信息表的一些字段,可以在相关联的表中获取指定的值,并通过职工基本信息表的ID值与家庭关系表、培训记录表、奖惩表等建立关系。

为了使读者能够更好地理解登录表与用户权限表、权限模板表之间的关系,下面给出其表间关系图,如图26.14所示。通过图26.14可以看出,在用户登录时,可以根据用户ID在用户权限表中调用相关的权限。当添加用户时,可以通过权限模板表中的信息,将权限名称自动添加到用户权限表中,以方便在前台中对用户进行添加操作。

说明

制作数据表的关系图是十分重要的,只有将各表之间的关系制定清楚,才可以通过表关系制作相应的触发器,或者是在开发项目时当对一个表进行操作后,相应的关系表也会随之改变。

图26.13 职工基本信息表与各表之间的关系

图26.14 登录表与用户权限表、权限模板表之间的关系

26.5 创建项目

视频讲解:光盘\TM\lx\26\创建项目.exe

在Visual Studio 2015开发环境中创建PWMS项目的具体步骤如下:

(1)在Windows 8.1操作系统的开始界面找到Visual Studio 2015,单击打开。

(2)打开Visual Studio 2015开发环境,在菜单栏中选择“文件”/“新建”/“项目”命令,打开如图26.15所示的“新建项目”对话框。在“项目类型”列表中选择VisualC#节点,在右侧的“Visual Studio已安装的模板”列表中选择“Windows窗体应用程序”选项,在“名称”文本框中输入项目名称,这里输入“PWMS”,在“位置”下拉列表框中选择存放项目文件的目标地址,单击“确定”按钮,即可创建一个空白的PWMS项目。

图26.15 “新建项目”对话框

(3)创建完PWMS项目之后,为了方便以后的开发工作和规范系统的整体架构,可以把系统中可能用到的文件夹先创建出来(例如,创建一个名为DataClass的文件夹,用于保存程序中用到的数据库文件),这样在开发时,只需将所创建的类文件或窗体文件保存到相应的文件夹中即可。在项目中创建文件夹非常简单,只需选中当前项目,单击鼠标右键,在弹出的快捷菜单中选择“添加”/“新建文件夹”命令即可,如图26.16所示。

(4)按照以上步骤,依次创建企业人事管理系统中可能用到的文件夹,并重命名。下面给出创建完成后的效果,如图26.17所示。

图26.16 选择“添加”/“新建文件夹”命令

图26.17 文件夹组织结构图

说明

为了使项目结构更加清晰,在项目中创建指定的文件夹,并在文件夹中创建相应的类、窗体等。

26.6 公共类设计

视频讲解:光盘\TM\lx\26\公共类设计.exe

在开发应用程序时,可以将数据库的相关操作以及对一些控件的设置、遍历等封装在自定义类中,以便于在开发程序时调用,这样,可以提高代码的重用性。本系统创建了MyMeans和MyModule两个公共类,分别存放在DataClass和ModuleClass文件夹中。下面对这两个公共类中比较重要的方法进行详细讲解。

26.6.1 MyMeans公共类

该类封装了本系统中所有与数据库连接的方法,可以通过该类的方法与数据库建立连接,并对数据信息进行添加、修改、删除以及读取等操作。在命名空间区域引用using System.Data.SqlClient命名空间,主要代码如下。

    using System.Data.SqlClient;
    namespace PWMS.DataClass
    {
       class MyMeans
       {
           #region 全局变量
           public static string Login_ID = "";         //定义全局变量,记录当前登录的用户编号
           public static string Login_Name = "";       //定义全局变量,记录当前登录的用户名
           //定义静态全局变量,记录“基础信息”各窗体中的表名、SQL 语句以及要添加和修改的字段名
           public static string Mean_SQL = "", Mean_Table = "", Mean_Field = "";
           //定义一个 SqlConnection 类型的静态公共变量 My_con,用于判断数据库是否连接成功
           public static SqlConnection My_con;
           //定义 SQL Server 2012 连接字符串,用户在使用时,将 Data Source 改为自己的 SQL Server 2012 服务
            器名
           public static string M_str_sqlcon = "Data Source=XIAOKE;Database=db_PWMS;User
    id=sa;PWD=";
           public static int Login_n = 0;               //用户登录与重新登录的标识
           //存储职工基本信息表中的 SQL 语句
           public static string AllSql = "Select * from tb_Staffbasic";
           #endregion
……自定义方法,如 getcon()、con_close()、getcom()等方法
} }

下面对MyMeans类中的自定义方法进行详细介绍。

注意

在项目中连接SQL Server 2012数据库是用本机名称,如果在其他计算机上运行该项目,则应用本地计算机的名称进行连接。

1.getcon方法

getcon方法是用static定义的静态方法,其功能是建立与数据库的连接,然后通过SqlConnection对象的Open方法打开与数据库的连接,并返回SqlConnection对象的信息。代码如下。

    public static SqlConnection getcon()
    {
         My_con = new SqlConnection(M_str_sqlcon); //用 SqlConnection 对象与指定的数据库相连接
         My_con.Open();                            //打开数据库连接
         return My_con;                            //返回 SqlConnection 对象的信息
    }
2.con_close方法

con_close方法的主要功能是对数据库操作后,通过该方法判断是否与数据库连接。如果连接,则关闭数据库连接。代码如下。

    public void con_close()
    {
         if (My_con.State == ConnectionState.Open) {  //判断是否打开与数据库的连接
              My_con.Close();                          //关闭数据库的连接
              My_con.Dispose();                        //释放 My_con 变量的所有空间
         }
    }
3.getcom方法

getcom方法的主要功能是用SqlDataReader对象以只读的方式读取数据库中的信息,并以SqlDataReader对象进行返回,其中SQLstr参数表示传递的SQL语句。代码如下。

    public SqlDataReader getcom(string SQLstr)
    {
         getcon();                                       //打开与数据库的连接
         //创建一个 SqlCommand 对象,用于执行 SQL 语句
         SqlCommand My_com = My_con.CreateCommand();
         My_com.CommandText = SQLstr;                    //获取指定的 SQL 语句
         SqlDataReader My_read = My_com.ExecuteReader(); //执行 SQL 语句,生成一个 SqlDataReader 对象
         return My_read;
    }
4.getsqlcom方法

getsqlcom方法的主要功能是通过SqlCommand对象执行数据库中的添加、修改和删除的操作,并在执行完后,关闭与数据库的连接,其中SQLstr参数表示传递的SQL语句。代码如下。

    public void getsqlcom(string SQLstr)
    {
         getcon();                          //打开与数据库的连接
         //创建一个 SqlCommand 对象,用于执行 SQL 语句
         SqlCommand SQLcom = new SqlCommand(SQLstr, My_con);
         SQLcom.ExecuteNonQuery();          //执行 SQL 语句
         SQLcom.Dispose();                  //释放所有空间
         con_close();                       //调用 con_close()方法,关闭数据库连接
    }
5.getDataSet方法

getDataSet方法的主要功能是通过SqlCommand对象执行数据库中的添加、修改和删除的操作,并在执行完后,关闭与数据库的连接,其中SQLstr参数表示传递的SQL语句。代码如下。

    public DataSet getDataSet(string SQLstr, string tableName)
     {
         getcon();                                //打开与数据库的连接
         SqlDataAdapter SQLda = new SqlDataAdapter(SQLstr, My_con);
         DataSet My_DataSet = new DataSet();      //创建 DataSet 对象
         SQLda.Fill(My_DataSet, tableName);
         con_close();                             //关闭数据库的连接
         return My_DataSet;                       //返回 DataSet 对象的信息
     }

说明

为了可以在项目中对不同的数据表进行操作,可以将数据库的连接/断开、数据表的添加、修改、删除、查询用指定的方法进行封装,以便于重复调用。

26.6.2 MyModule公共类

该类将系统中所有窗体的动态调用以及动态生成添加、修改、删除和查询的SQL语句等全部封装到了指定的自定义方法中,以便在开发程序时进行重复调用,这样可以大大简化程序的开发过程。由于该类中应用了可视化组件的基类和对数据库进行操作的相关对象,所以在命名空间区域引用using System.Windows.Forms和using System.Data.SqlClient命名空间。主要代码如下。

因篇幅有限,下面只对几个比较重要的方法进行介绍。

1.Show_Form方法

Show_Form方法通过FrmName参数传递的窗体名称,调用相应的子窗体,因本系统中存在公共窗体,也就是在同一个窗体模块中可以显示不同的窗体,所以用参数n来进行标识。调用公共窗体,实际上就是通过不同的SQL语句,在显示窗体时以不同的数据进行显示,以实现不同窗体的显示效果。主要代码如下。

说明

在开发项目时,窗体的调用是必不可少的,可以将窗体的调用过程写在一个方法中,并通过窗体名称显示指定的窗体。

2.GetMenu方法

GetMenu方法的主要功能是将MenuStrip菜单中的菜单项按照级别动态添加到TreeView控件的相应节点中,其中treeV参数表示要添加节点的TreeView控件,MenuS参数表示要获取信息的MenuStrip菜单。主要代码如下。

说明

在设置节点时,同一级别上的每个节点必须具有唯一的Value属性值。

3.Clear_Control方法

Clear_Control方法的主要功能是清空可视化控件集中指定控件的文本信息及图片,主要用于在添加数据信息时,对相应文本框进行清空。其中Con参数表示可视化控件的控件集合。主要代码如下。

4.Part_SaveClass方法

Part_SaveClass方法的主要功能是通过部分控件名BoxName与i值(数字)相结合,在可视化控件集中查找指定的控件,并根据Sarr参数中的字段名,组合成添加或修改语句,将生成后的语句存储在公共变量ADDs中。主要代码如下。

Part_SaveClass方法中的参数说明如表26.23所示。

表26.23 Part_SaveClass方法中的参数说明

注意

在Part_SaveClass方法中所查找的控件名,必须以BoxName_i格式命名(如Word_1)。

5.Find_Grids方法

Find_Grids方法的主要功能是查找指定可视化控件集中控件名包含TName参数值的所有控件,并根据控件名称获取相应表的字段名。当查找的控件为TextBox时,根据当前控件的部分名称查找相应的ComboBox控件(用来记录逻辑运算符),通过ANDSign参数将具有相关性的控件组合成查询条件,存入到公共变量FindValue中。主要代码如下。

Find_Grids方法中的参数说明如表26.24所示。

表26.24 Find_Grids方法中的参数说明

注意

在Find_Grids方法中所查找的条件控件ComboBox或TextBox,必须以“TName+相应字段名”命名(如查找民族类别的控件,其控件名为Find_Folk, Find_就是传递的TName参数值,Folk为相应表的字段名)。存储逻辑运算符的ComboBox控件必须以“相应表的字段名+_Sign”命名(如当TextBox控件名为Find_Age时,相应的ComboBox控件名为Age_Sign)。这样,便于根据控件名称进行组合。

6.GetAutocoding方法

GetAutocoding方法的主要功能是在添加数据时自动获取添加数据的编号。其实现过程是通过表名和ID字段在表中查找最大的ID值,并将ID值加1进行返回。当表中无记录时,返回“0001”。TableName参数表示进行自动编号的表名,ID参数表示数据表的编号字段。主要代码如下。

7.TreeMenuF方法

TreeMenuF方法在单击TreeView控件的节点时被调用,其主要功能是通过所选节点的文本名称,在MenuStrip控件中进行遍历查找。如果找到,并且为可用状态,则通过Show_Form方法动态调用相关的窗体。代码如下。

说明

ToolStripDropDownItem类主要用于将指定的项装载到下拉列表中,可以通过将DropDown属性设置为ToolStripDropDown,以及设置ToolStripDropDown的Items属性来执行此操作。可通过DropDownItems属性访问已添加的下拉项。

8.MainPope方法

MainPope方法的主要功能是通过当前登录用户的名称,在权限用户表中查询当前用户的所有权限,并根据权限设置菜单栏中各菜单项的可用状态。其中,MenuS参数是要设置的菜单栏控件,UName参数为当前用户的名称。代码如下。

9.Amend_Pope方法

Amend_Pope方法的主要功能是修改指定用户的权限,其中,GBox参数是包含权限复选框的容器控件,TID参数为当前用户的编号。代码如下。

说明

(CheckBox) Control主要是将Control强制转换成CheckBox类,在强制转换前必须通过Control的GetType()方法的Name属性获取其控件类型。如果类型不对,则触发异常。

26.7 登录模块设计

视频讲解:光盘\TM\lx\26\登录模块设计.exe

 本模块使用的数据表:tb_Login。

图26.18 系统登录

登录模块主要是通过输入正确的用户名和密码进入主窗体,它可以提高程序的安全性,保护数据资料不外泄。登录模块运行结果如图26.18所示。

26.7.1 设计登录窗体

新建一个Windows窗体,命名为F_Login.cs,主要用于实现系统的登录功能,将窗体的FormBorderStyle属性设置为None,以便去掉窗体的标题栏。F_Login窗体用到的主要控件如表26.25所示。

表26.25 登录窗体用到的主要控件

26.7.2 按Enter键时移动鼠标焦点

当用户在“用户名”文本框中输入值,并按下Enter键时,将鼠标焦点移动到“密码”文本框中。当在“密码”文本框中输入值,并按下Enter键时,将鼠标焦点移动到“登录”按钮上。实现代码如下。

说明

KeyPressEventArgs指定在用户按键时撰写的字符。例如,当用户按Shift+a键时,KeyChar属性返回一个大写字母A。

26.7.3 登录功能的实现

当用户输入用户名和密码后,单击“登录”按钮进行登录。在“登录”按钮的Click事件中,首先判断用户名和密码是否为空。如果为空,则弹出提示框,通知用户将登录信息填写完整。否则将判断用户名和密码是否正确,如果正确,则进入本系统。详细代码如下。

26.8 系统主窗体设计

视频讲解:光盘\TM\lx\26\系统主窗体设计.exe

 本模块使用的数据表:tb_UserPope。

主窗体是程序操作过程中必不可少的,它是人机交互中的重要环节。通过主窗体,用户可以调用系统相关的各子模块,快速掌握本系统中所实现的各个功能。企业人事管理系统中,当成功登录窗体验证后,用户将进入主窗体。主窗体分为4个部分,最上面是系统菜单栏,可以通过它调用系统中的所有子窗体。菜单栏下面是工具栏,它以按钮的形式使用户能够方便地调用最常用的子窗体。窗体的左边是一个树形导航菜单,该导航菜单中的各节点是根据菜单栏中的项自动生成的。窗体的最下面用状态栏显示当前登录的用户名。主窗体运行结果如图26.19所示。

图26.19 企业人事管理系统主窗体

26.8.1 设计菜单栏

菜单栏运行效果如图26.20所示。

图26.20 菜单栏运行效果

本系统的菜单栏是通过MenuStrip控件实现的,设计菜单栏的具体步骤如下:

(1)从工具箱中拖放一个MenuStrip控件置于企业人事管理系统的主窗体中。

(2)为菜单栏中的各个菜单项设置菜单名称,如图26.21所示。在输入菜单名称时,系统会自动产生输入下一个菜单名称的提示。

图26.21 为菜单栏添加项

(3)选中菜单项,单击其“属性”窗口中的DropDownItems属性后面的按钮,弹出“项集合编辑器”对话框,如图26.22所示。该对话框中可以为菜单项设置Name名称,也可以继续通过单击其DropDownItems属性后面的按钮添加子项。

图26.22 为菜单栏中的项命名并添加子项

菜单栏设计完成之后,单击菜单栏中的各菜单项调用相应的子窗体,为了使程序的制作过程更加简便,将所有子窗体的调用封装到MyModule公共类的Show_Form方法中,只需要获取当前调用窗体的名称及标识,即可调用相应的窗体。下面以单击“人事管理”/“人事档案管理”菜单项为例进行说明。代码如下。

    private void Tool_Staffbasic_Click(object sender, EventArgs e)
    {
         //用 MyModule 公共类中的 Show_Form()方法调用各窗体
         MyMenu.Show_Form(sender.ToString().Trim(), 1);
    }

说明

sender.ToString().Trim()表示获取当前对象的Text属性值,即当前单击菜单项的文本。如果调用的是“基础信息管理”/“基础数据”下的子菜单项,则把Show_Form方法中的1改为2,因为“基础数据”菜单下的所有子菜单项调用的是一个公共窗体。

26.8.2 设计工具栏

工具栏运行效果如图26.23所示。

本系统的工具栏是通过ToolStrip控件实现的,设计工具栏的具体步骤如下:

(1)从工具箱中拖放一个ToolStrip控件置于企业人事管理系统的主窗体中,单击ToolStrip控件后面的下拉按钮,可以选择为工具栏添加哪种控件,如图26.24所示。

图26.23 工具栏运行效果

图26.24 为工具栏添加控件

(2)为工具栏添加完控件之后,选中添加的工具栏项,单击鼠标右键,在弹出的快捷菜单中选择“设置图像”命令,可以为工具栏项设置显示的图像,如图26.25所示。

(3)工具栏中的项默认只显示图像,如果需要同时显示文本和图像,可以选中工具栏项,单击鼠标右键,在弹出的快捷菜单中选择DisplayStyle/ImageAndText命令,如图26.26所示。

图26.25 选择“设置图像”命令

图26.26 选择DisplayStyle/ImageAndText命令

按照以上步骤,依次添加工具栏项。

工具栏主要是为用户提供一种方便的操作系统常用功能的方式,它在实现时主要调用菜单栏中相应菜单项的Click事件即可。例如,“人事档案管理”工具栏项的Click事件代码如下。

    private void Button_Staffbasic_Click(object sender, EventArgs e)
    {
         if (Tool_Staffbasic.Enabled==true)
               Tool_Staffbasic_Click(sender, e);      //调用人事档案管理菜单项的单击事件
         else
               MessageBox.Show("当前用户无权限调用" + "\"" + ((ToolStripButton)sender).Text + "\"" + "窗体");
    }

说明

单击(Click)事件中的sender参数是事件源,用于引用引发事件的实例。

26.8.3 设计导航菜单

导航菜单运行效果如图26.27所示。

图26.27 导航菜单运行效果

本系统的导航菜单是通过TreeView控件实现的,导航菜单中的项根据菜单栏自动生成,它主要调用了公共类MyModule下的GetMenu方法。代码如下。

    //实例化公共类 MyModule 的一个对象
    ModuleClass.MyModule MyMenu = new PWMS.ModuleClass.MyModule();
    MyMenu.GetMenu(treeView1, menuStrip1);    //使用菜单栏中的项填充导航菜单

当使用树形导航菜单的下拉列表打开相应的子窗体时,可以在TreeView控件的节点单击事件(NodeMouseClick)中调用相应的子窗体。代码如下。

    private void treeView1_NodeMouseClick(object sender, TreeNodeMouseClickEventArgs e)
    {
         if (e.Node.Text.Trim() == "系统退出")    //如果当前节点的文本为“系统退出”
         {
               Application.Exit();                //关闭应用程序
         }
         MyMenu.TreeMenuF(menuStrip1, e);         //用 MyModule 公共类中的 TreeMenuF()方法调用各窗体
    }

说明

TreeMenuF方法是MyModule公共类中定义的,用来通过当前节点的文本信息在menuStrip1控件中进行遍历查找。如果找到,并且为可用状态,则调用相应窗体;否则弹出“当前用户无权限调用×××窗体”对话框。

26.8.4 设计状态栏

状态栏运行效果如图26.28所示。

本系统的状态栏是通过StatusStrip控件实现的,设计状态栏的具体步骤如下:

(1)从工具箱中拖放一个StatusStrip控件置于企业人事管理系统的主窗体中,单击StatusStrip控件后面的下拉按钮,可以选择为状态栏添加哪种控件,如图26.29所示。

图26.28 状态栏运行效果

图26.29 为状态栏添加控件

(2)本系统中的状态栏主要显示欢迎信息和当前登录的用户,因此这里用到3个StatusLabel控件,其中前两个StatusLabel控件的Text属性分别设置为“||欢迎使用企业人事管理系统||”和“当前登录用户:”,第3个StatusLabel控件用来显示当前登录的用户名。状态栏设计完成之后的效果如图26.30所示。

图26.30 状态栏设计效果

在状态栏中显示当前登录用户名的实现代码如下。

    statusStrip1.Items[2].Text = DataClass.MyMeans.Login_Name;     //在状态栏显示当前登录的用户名

26.9 人事档案管理模块设计

视频讲解:光盘\TM\lx\26\人事档案管理模块设计.exe

 本模块使用的数据表:tb_Folk、tb_Culture、tb_Visage、tb_EmployeeGenre、tb_Business、tb_Laborage、tb_Branch、tb_Duthcall、tb_City、tb_Staffbasic、tb_WorkResume、tb_Family、tb_TrainNote、tb_RANDP、tb_Individual。

人事档案管理窗体是用来对职工的基本信息、家庭情况、工作简历、培训记录等进行浏览以及添加、修改、删除的操作。在主窗体中,可以通过菜单栏中的“人事管理”/“人事档案管理”调用人事档案浏览窗体,也可以通过工具栏中的“人事档案管理”按钮或导航菜单中的下拉列表进行调用。人事档案管理窗体由4部分组成,分别由分类查询、浏览按钮、职工名称表和信息操作组成。其中分类查询主要是通过职工的类别,对职工进行简单查询。浏览按钮是通过按钮对职工名称表进行浏览。职工名称表用来显示当前所记录的所有职工名称。信息操作是用来对职工的相关信息进行添加、修改、删除、浏览等操作,并可以将职工的基本信息在Word文档或者Excel表格中以自定义表格的形式进行显示。人事档案管理窗体运行结果如图26.31所示。

图26.31 “人事档案管理”窗体

说明

由于人事档案管理模块中有多个面板,但它们实现的功能大部分是相同的,因此下面以“职工基本信息”面板为例进行讲解。

26.9.1 设计人事档案管理窗体

新建一个Windows窗体,命名为F_MainFile.cs,主要用于对企业的人事档案信息进行管理。F_MainFile窗体用到的主要控件如表26.26所示。

表26.26 人事档案管理窗体用到的主要控件

续表

续表

图26.32 “查询类型”下拉列表Items属性设置

26.9.2 添加/修改人事档案信息

单击“添加”按钮,首先调用MyModule公共类中的Clear_Control方法,将指定控件集下的控件进行清空,然后根据表名和ID字段调用MyModule公共类中的GetAutocoding方法进行自动编号。代码如下。

    private void Sta_Add_Click(object sender, EventArgs e)
    {
         MyMC.Clear_Control(tabControl1.TabPages[0].Controls);       //清空职工基本信息的相应文本框
         S_0.Text = MyMC.GetAutocoding("tb_Staffbasic", "ID");       //自动添加编号
         hold_n = 1;                                                  //用于记录添加操作的标识
         MyMC.Ena_Button(Sta_Add, Sta_Amend, Sta_Cancel, Sta_Save, 0, 0, 1, 1);
         groupBox5.Text = "当前正在添加信息";
         Img_Clear.Enabled = true;                                    //使图片选择按钮为可用状态
         Img_Save.Enabled = true;
    }

单击“修改”按钮,该按钮的功能只是用hold_n标识记录当前为修改状态,并修改其他相关按钮的可用状态。代码如下。

    private void Sta_Amend_Click(object sender, EventArgs e)
    {
         hold_n = 2;                            //用于记录修改操作的标识
         MyMC.Ena_Button(Sta_Add, Sta_Amend, Sta_Cancel, Sta_Save, 0, 0, 1, 1);
         groupBox5.Text = "当前正在修改信息";
         Img_Clear.Enabled = true;              //使图片选择按钮为可用状态
         Img_Save.Enabled = true;
    }

说明

自定义变量hold_n是用于添加和修改操作的标识,如果hold_n值不为1或2时,将不做任何操作。

单击“保存”按钮,根据hold_n标识判断执行的是添加操作还是修改操作,并调用“取消”按钮的单击事件功能,将各按钮的状态恢复到初始状态。代码如下。

在添加和修改人事档案信息时,当为职工选择头像后,需要将选择的头像转换成字节数组,然后再存放到数据库中。将头像转换成字节数组的实现代码如下。

说明

BinaryReader类是用特定的编码将基元数据类型读作二进制值。如果该流为null,或是已关闭,将触发异常。

26.9.3 删除人事档案信息

单击“删除”按钮,将职工基本信息表中的当前记录全部删除,同时根据当前记录的编号,删除工作简历表、家庭关系表、培训记录表、奖惩记录表和个人简历表中的相关记录。代码如下。

    private void Stu_Delete_Click(object sender, EventArgs e)
    {
         if (dataGridView1.RowCount < 2) //判断 dataGridView1 控件中是否有记录
         {
               MessageBox.Show("数据表为空,不可以删除。");
               return;
         }
         //删除职工信息表中的当前记录及其他相关表中的信息
         MyDataClass.getsqlcom("Delete tb_Staffbasic where ID='" + S_0.Text.Trim() + "'");
         MyDataClass.getsqlcom("Delete tb_WorkResume where Stu_ID='" + S_0.Text.Trim() + "'");
         MyDataClass.getsqlcom("Delete tb_Family where Sta_ID='" + S_0.Text.Trim() + "'");
         MyDataClass.getsqlcom("Delete tb_TrainNote where Sta_ID='" + S_0.Text.Trim() + "'");
         MyDataClass.getsqlcom("Delete tb_RANDP where Sta_ID='" + S_0.Text.Trim() + "'");
         MyDataClass.getsqlcom("Delete tb_WorkResume where Sta_ID='" + S_0.Text.Trim() + "'");
         MyDataClass.getsqlcom("Delete tb_Individual where ID='" + S_0.Text.Trim() + "'");
         Sta_Cancel_Click(sender, e);               //调用“取消”按钮的单击事件
    }

26.9.4 单条件查询人事档案信息

图26.33 单条件查询人事档案信息运行效果

单条件查询人事档案信息运行效果如图26.33所示。当在“查询类型”下拉列表框中选择查询的类型时,“查询条件”下拉列表框中的值随之改变,然后在“查询条件”下拉列表框中选择要查询的内容,系统根据选择的查询条件调用自定义方法Condition_Lookup在数据库中的相关记录,并显示在DataGridView控件中。单条件查询人事档案信息的实现代码如下。

实现单条件查询人事档案信息时,用到了自定义方法Condition_Lookup,该方法用来根据指定的条件查找职工信息,并显示在DataGridView控件中。Condition_Lookup方法的实现代码如下。

    #region 按条件显示“职工基本信息”表的内容
    /// <summary>
    ///通过公共变量动态进行查询
    /// </summary>
    /// <param name="C_Value">条件值</param>
    public void Condition_Lookup(string C_Value)
    {
         MyDS_Grid = MyDataClass.getDataSet("Select * from tb_Staffbasic where " + tem_Field + "='" +
    tem_Value + "'", "tb_Staffbasic");
         dataGridView1.DataSource = MyDS_Grid.Tables[0];
         textBox1.Text = Grid_Inof(dataGridView1);       //显示职工信息表的当前记录
    }
    #endregion

26.9.5 逐条查看人事档案信息

“浏览按钮”区域中的4个按钮主要实现逐条查看人事档案信息功能,其运行效果如图26.34所示。

图26.34 逐条查看人事档案信息运行效果

当单击图26.34中的4个按钮时,程序根据所按按钮的ID判断将要执行“第一条”、“上一条”、“下一条”和“最后一条”4项操作中的哪项操作。“浏览按钮”区域中的4个按钮的实现代码如下。

说明

在设置具有焦点的单元格时,可以用dataGridView1[列数,行数]来指定单元格的位置。

26.9.6 将人事档案信息导出为Word文档

人事档案信息导出的Word文档如图26.35所示。

图26.35 导出的Word文档

为了便于职工信息的存储及打印,单击“导出Word”按钮,可以将职工信息以表格的形式存入到Word文档中。将人事档案信息导出为Word文档的实现代码如下。

说明

在C#中如果想要对Word文档进行操作,必须对Word进行引用。其添加步骤为:首先,在“解决方案资源管理器”中的“引用”上单击鼠标右键,在弹出的快捷菜单中选择“添加引用”项;然后,在打开的“引用管理器”窗体中选择“程序集”/“扩展”;最后,在该选择卡中选择Microsoft.Office.Interop.Word,单击“确定”按钮即可。

26.9.7 将人事档案信息导出为Excel表格

人事档案信息导出的Excel表格如图26.36所示。

图26.36 导出的Excel表格

为了便于职工信息的存储及打印,单击“导出Excel”按钮,可以将职工信息导入到Excel表格中。将人事档案信息导出为Excel表格的实现代码如下。

说明

在C#中如果想要对Excel进行操作,必须对Excel进行引用。其添加步骤为:首先,在“解决方案资源管理器”中的“引用”上单击鼠标右键,在弹出的快捷菜单中选择“添加引用”项;然后,在打开的“引用管理器”窗体中选择COM/“类型库”;最后,在该选择卡中选择Microsoft Excel版本号Object Library,单击“确定”按钮即可。

26.10 人事资料查询模块设计

视频讲解:光盘\TM\lx\26\人事资料查询模块设计.exe

 本模块使用的数据表:tb_Staffbasic。

在人事资料查询窗体中,可以通过在“基本信息”和“个人信息”区域中设置查询条件,对职工基本信息进行查询。人事资料查询窗体运行结果如图26.37所示。

图26.37 “人事资料查询”窗体

26.10.1 设计人事资料查询窗体

新建一个Windows窗体,命名为F_Find.cs,主要用于对企业的人事档案信息进行查询。F_Find窗体用到的主要控件如表26.27所示。

表26.27 “人事资料查询”窗体用到的主要控件

26.10.2 多条件查询人事资料

在窗体上设置完查询条件后,单击“查询”按钮进行查询,该按钮通过调用MyModule公共类中的Find_Grids方法将指定控件集上的控件组合成查询语句,然后调用MyMeans公共类中的getDataSet方法在数据表中根据组合的查询语句查询记录,并显示在dataGridView1控件上。代码如下。

说明

在DataSet中可以存储多个数据集,如果想要显示某一数据集,可以用DataSet.Tables[索引号]实现。

26.11 通讯录模块设计

视频讲解:光盘\TM\lx\26\通讯录模块设计.exe

 本模块使用的数据表:tb_AddressBook。

通讯录模块主要对企业人事管理系统中的通讯录信息进行管理,包括对通讯录信息的添加、修改、删除和查询等操作。通讯录窗体运行结果如图26.38所示。

图26.38 “通讯录”窗体

26.11.1 设计通讯录窗体

新建一个Windows窗体,命名为F_AddressList,主要用于对企业的通讯录信息进行管理。F_AddressList窗体用到的主要控件如表26.28所示。

表26.28 通讯录窗体用到的主要控件

26.11.2 添加/修改通讯录信息

添加通讯录和修改通讯录窗体的运行效果分别如图26.39和图26.40所示。

图26.39 添加通讯录

图26.40 修改通讯录

在F_AddressList窗体中单击“添加”/“修改”按钮,实例化F_Address窗体的一个对象,并分别为该对象的Tag属性赋值为1和2,以标识在F_Address窗体中将执行哪种操作。“添加”/“修改”按钮的实现代码如下。

说明

当在一个窗体中进行不同的操作时,如添加和修改操作,可以通过指定的标识来进行判断,这样可以避免窗体的重复制作。

在“添加”/“修改”按钮的Click事件中用到了ShowAll方法,该方法为自定义的无返回值类型方法,它主要用来将通讯录信息显示在DataGridView控件中,并根据DataGridView控件中的行数确定“修改”按钮和“删除”按钮的可用状态。ShowAll方法的实现代码如下。

F_Address窗体在加载时,首先判断其Tag属性值。如果为1,则为“通讯录添加”窗体,这时调用MyModule公共类中的GetAutocoding方法自动生成一个通讯录编号。如果为2,则为“通讯录修改”窗体,这时需要将指定通讯录编号的所有信息显示在相应的TextBox文本框中。F_Address窗体的Load事件代码如下。

    private void F_Address_Load(object sender, EventArgs e)
    {
         if ((int)(this.Tag) == 1) //判断窗体的 Tag 属性值是否为 1,以确定执行添加操作
    {
         //自动生成通讯录编号
              Address_ID = MyMC.GetAutocoding("tb_AddressBook", "ID");
         }
         if ((int)this.Tag == 2)   //判断窗体的 Tag 属性值是否为 2,以确定执行添加操作
         {
              //根据指定条件查找通讯录信息,并将结果存储在 DataSet 数据集中
              MyDS_Grid = MyDataClass.getDataSet("select ID,Name,Sex,Phone,Handset,WorkPhone,QQ,E_Mail
    from tb_AddressBook where ID='" + ModuleClass.MyModule.Address_ID + "'", "tb_AddressBook");
              Address_ID = MyDS_Grid.Tables[0].Rows[0][0].ToString();          //记录通讯录编号
              this.Address_1.Text = MyDS_Grid.Tables[0].Rows[0][1].ToString(); //显示姓名
              this.Address_2.Text = MyDS_Grid.Tables[0].Rows[0][2].ToString(); //显示性别
              this.Address_3.Text = MyDS_Grid.Tables[0].Rows[0][3].ToString(); //显示电话
              this.Address_4.Text = MyDS_Grid.Tables[0].Rows[0][4].ToString(); //显示手机号
              this.Address_5.Text = MyDS_Grid.Tables[0].Rows[0][5].ToString(); //显示工作电话
              this.Address_6.Text = MyDS_Grid.Tables[0].Rows[0][6].ToString(); //显示 QQ 号码
              this.Address_7.Text = MyDS_Grid.Tables[0].Rows[0][7].ToString(); //显示 E-mail
         }
    }

在F_Address窗体中单击“保存”按钮,判断“姓名”文本框是否为空。如果不为空,则执行通讯录添加或修改操作,并在添加或修改成功后将该窗体关闭,否则弹出“人员姓名不能为空”信息提示框。“保存”按钮的实现代码如下。

    private void button1_Click(object sender, EventArgs e)
    {
         if (this.Address_1.Text != "")
         {
               //调用公共类中的方法组合 SQL 语句
               MyMC.Part_SaveClass("ID,Name,Sex,Phone,Handset,WorkPhone,QQ,E_Mail", Address_ID, "", this.group
    Box1. Controls, "Address_", "tb_AddressBook", 8, (int)this.Tag);
               MyDataClass.getsqlcom(ModuleClass.MyModule.ADDs);   //执行 SQL 语句
               this.Close();                                       //关闭当前窗体
         }
         else
               MessageBox.Show("人员姓名不能为空。");
    }

说明

窗体的Tag属性是一个Object类型的值,在对该属性进行赋值时可以是int、string、bool型。如果对该属性进行读取,必须根据相应的类型对其进行强制转换。

26.11.3 删除通讯录信息

在F_AddressList窗体中单击“删除”按钮,调用MyMeans公共类中的getsqlcom方法执行删除通讯录信息操作。“删除”按钮的实现代码如下。

    private void Address_Delete_Click(object sender, EventArgs e)
    {
         if (MessageBox.Show("确定要删除该条信息吗?", "提示", MessageBoxButtons.OKCancel, MessageBoxIcon.
    Question) == DialogResult.OK)           //判断是否在弹出的对话框中单击“确定”按钮
         {
            //调用公共类中的方法删除选中的通讯录信息
            MyDataClass.getsqlcom("Delete tb_AddressBook where ID='" + ModuleClass.MyModule.Address_
    ID + "'");
            ShowAll();
         }
    }

26.11.4 查询通讯录信息

图26.41 查询通讯录信息的运行效果

查询通讯录信息的运行效果如图26.41所示。

当在“查询类型”下拉列表框中选择查询的类型,并在“查询条件”文本框中输入查询关键字后,单击“查询”按钮,程序根据选择的查询类型和输入的查询关键字在数据表中查找相关记录,并显示在DataGridView控件中,同时根据DataGridView控件中的行数确定“修改”按钮和“删除”按钮的可用状态。查询通讯录信息的实现代码如下。

说明

在窗体中对数据进行添加、修改和删除操作时,为了尽可能减少误操作,可以将不可用或已用过的按钮设为不可用状态。例如,当数据表为空时,“删除”按钮将设为不可用状态。

26.12 用户设置模块设计

视频讲解:光盘\TM\lx\26\用户设置模块设计.exe

 本模块使用的数据表:tb_Login、tb_UserPope。

图26.42 “用户设置”窗体

用户设置模块主要对企业人事管理系统中的用户信息进行管理,包括对用户信息的添加、修改和删除等操作,而且还可以为指定的用户设置操作权限。另外,如果要对管理员信息进行修改、删除和设置操作权限等操作,系统会提示不能对管理员进行操作。用户设置窗体运行结果如图26.42所示。

26.12.1 设计用户设置窗体

新建一个Windows窗体,命名为F_User.cs,主要用于对该系统的用户信息进行管理。F_User窗体用到的主要控件如表26.29所示。

表26.29 用户设置窗体用到的主要控件

26.12.2 添加/修改用户信息

添加用户信息和修改用户信息窗体的运行效果分别如图26.43和图26.44所示。

图26.43 添加用户信息

图26.44 修改用户信息

在F_User窗体中单击工具栏中的“添加”/“修改”按钮,实例化F_UserAdd窗体的一个对象,并分别为该对象的Tag属性赋值为1和2,以标识在F_UserAdd窗体中将执行哪种操作。工具栏中的“添加”/“修改”按钮的实现代码如下。

在F_UserAdd窗体中单击“保存”按钮,判断“用户名”文本框和“密码”文本框是否为空。如果为空,则弹出提示信息;否则,根据该窗体的Tag属性值判断是执行用户添加操作,还是执行用户修改操作。“保存”按钮的实现代码如下。

26.12.3 删除用户基本信息

在F_User窗体中单击工具栏中的“删除”按钮,判断要删除的用户是不是管理员。如果是,则弹出提示信息,提示不能修改管理员信息;否则,删除选中的用户信息,同时删除其权限信息。工具栏中“删除”按钮的实现代码如下。

说明

在对数据表中的数据进行删除后,必须对窗体中的数据进行更新,以显示最新的数据内容。

26.12.4 设置用户操作权限

在F_User窗体中单击工具栏中的“权限”按钮,弹出F_UserPope窗体,如图26.45所示。

用户权限设置窗体中可以设置用户的权限,在该窗体中选中要拥有权限的复选框,单击“保存”按钮,调用MyModule公共类中的Amend_Pope方法为用户设置权限,同时将MyMeans公共类中的静态变量Login_n设置为2,以便在调用“重新登录”窗体时,使用新设置的权限对其进行初始化。设置用户操作权限的实现代码如下。

    private void User_Save_Click(object sender, EventArgs e)
    {
         //调用公共类的 Amend_Pope 方法为指定的用户设置权限
         MyMC.Amend_Pope(groupBox2.Controls, ModuleClass.MyModule.User_ID);
         //判断登录用户的编号是否与修改的用户编号相同
         if (DataClass.MyMeans.Login_ID == ModuleClass.MyModule.User_ID)
              //将静态变量 Login_n 设置为 2,以便在调用“重新登录”窗体时,使用新设置的权限对其进行初始化
              DataClass.MyMeans.Login_n = 2;
    }

图26.45 设置用户操作权限的运行效果

26.13 数据库维护模块设计

视频讲解:光盘\TM\lx\26\数据库维护模块设计.exe

图26.46 “备份/还原数据库”窗体

数据库维护模块主要对企业人事管理系统中的数据信息进行备份和还原操作,其运行结果如图26.46所示。

26.13.1 设计数据库维护窗体

新建一个Windows窗体,命名为F_HaveBack.cs,主要用于对该系统的数据进行备份和还原。F_HaveBack窗体用到的主要控件如表26.30所示。

表26.30 数据库维护窗体用到的主要控件

26.13.2 备份数据库

在“备份数据库”选项卡中单击“备份”按钮,程序首先判断是将备份文件存放到默认路径下,还是存放到用户选择的路径下,然后对数据库文件进行备份。备份数据库的实现代码如下。

    private void button1_Click(object sender, EventArgs e)
    {
         string Str_dar = "";
         if (radioButton1.Checked == true)      //判断默认路径是否选中
               Str_dar = System.Environment.CurrentDirectory + "\\bar\\";
         if (radioButton2.Checked == true)      //判断自定义路径是否选中
               Str_dar = textBox2.Text+ "\\";
         if (textBox2.Text == "" & radioButton2.Checked == true)
         {
               MessageBox.Show("请选择备份数据库文件的路径。");
               return;
         }
         try
         {
               //定义数据库备份的 SQL 语句
               Str_dar = "backup database db_PWMS to disk='" + Str_dar+System.DateTime.Now.ToShortDateString().
    Replace("/","")+MyMC.Time_Format(System.DateTime.Now.ToString())+".bak" + "'";
               //调用公共类中的方法执行数据库备份操作
               MyDataClass.getsqlcom(Str_dar);
               MessageBox.Show("数据备份成功!", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
         }
         catch (Exception ex)
         {
               MessageBox.Show(ex.Message, "提示", MessageBoxButtons.OK, MessageBoxIcon.Information);
         }
    }

说明

在对数据库中的数据进行备份时,必须关闭当前数据库的所有连接。

26.13.3 还原数据库

还原数据库运行效果如图26.47所示。

图26.47 还原数据库运行效果

在“还原数据库”选项卡中单击“还原”按钮,程序首先调用kill命令,将与db_PWMS数据库有关的进程全部强行关闭,然后重新备份该数据库的日志文件,同时对该数据库进行还原操作。还原数据库的实现代码如下。

26.14 运行项目

视频讲解:光盘\TM\lx\26\运行项目.exe

模块设计及代码编写完成之后,单击Visual Studio 2015开发环境工具栏中的图标,或者在菜单栏中选择“调试”/“启动调试”命令或“调试”/“开始执行(不调试)”命令,运行该项目,弹出企业人事管理系统“登录”对话框,如图26.48所示。

在“登录”对话框中输入用户名和密码,单击“登录”按钮,进入企业人事管理系统的主窗体,然后用户可以通过对主窗体中的菜单栏、工具栏和导航菜单进行操作,以便调用其各个子模块。例如,在主窗体中单击工具栏中的“人事档案管理”按钮,弹出“人事档案管理”窗体,如图26.49所示。在该窗体中,用户可以对人事档案信息进行添加、删除、修改、查询及导出为Word或者Excel等操作。

图26.48 “登录”对话框

图26.49 “人事档案管理”窗体

再如,在主窗体中单击工具栏中的“通讯录”按钮,可以弹出“通讯录”窗体,在“通讯录”窗体中单击“添加”按钮,可以弹出“通讯录添加操作”窗体,如图26.50所示。在该窗体中,用户可以对通讯录信息进行添加操作。

图26.50 “通讯录添加操作”窗体

26.15 开发的常见问题与解决

视频讲解:光盘\TM\lx\26\开发的常见问题与解决.exe

26.15.1 程序为什么会无法运行

图26.51 数据库连接失败的信息提示

问题描述:双击企业人事管理系统的可执行文件运行该程序,弹出如图26.51所示的信息提示,单击“确定”按钮后直接退出应用程序。

解决方法:该错误提示主要是由于无法登录指定的服务器所引起的,只需将程序MyMeans公共类数据库连接字符串中的Data Source属性修改为本机的SQL Server 2012服务器名,并将数据库连接字符串中的User id属性和PWD属性分别修改为本机登录SQL Server 2012服务器的用户名和密码,然后重新生成解决方案即可解决该问题。

26.15.2 为什么无法添加职工基本信息

问题描述:在企业人事管理系统的人事档案管理模块中,添加职工基本信息时,当输入基本信息后,单击“保存”按钮后则出现错误提示,如图26.52所示。

图26.52 添加职工基本信息时出现的错误提示

解决方法:该问题主要是由于输入职工基本信息时,输入了非法的数据而引起的。例如,正常的手机号是11位,如果用户输入的手机号位数超过了11位,就会出现上面的错误提示。解决该问题有两种方法,分别如下。

(1)输入职工基本信息时,严格按照实际情况输入正确的数据。

(2)为“保存”按钮下实现添加职工基本信息的代码捕捉异常,如果出现问题,弹出提示框,实现代码如下。

26.15.3 选择职工头像时出现异常怎么办

问题描述:在企业人事管理系统的人事档案管理模块中,添加职工基本信息时,当单击“选择图片”按钮为职工选择头像时,如果想取消该操作,会出现异常。

解决方法:该问题主要是由于没有判断用户单击的是“打开”按钮还是“取消”按钮而引起的,解决该问题时,只需添加一段判断用户单击的是“打开”按钮的代码即可。代码如下。

    if (openF.ShowDialog(this) == DialogResult.OK)       //如果打开了图片文件

26.15.4 数据库还原不成功应该如何解决

问题描述:在企业人事管理系统的数据库维护模块中,当选择完数据库的备份文件后,单击“还原”按钮,弹出如图26.53所示的信息提示。

解决方法:该错误提示主要是由于db_PWMS数据库正在使用而引起的,只需将SQL Server 2012的SQL Server Management Studio关闭,同时关闭PWMS项目即可解决该问题。

图26.53 数据库还原时出现的信息提示

26.16 小结

本章根据软件的开发流程,对企业人事管理系统的开发过程进行了详细讲解。通过对本章的学习,读者应该能够掌握如何用自定义方法对多个不同的数据表进行添加、修改、删除以及多字段组合查询等操作。另外,还应该掌握如何将数据库中的信息导出到Word文档中,以方便打印。

附录A

附录A

Visual Studio 2015开发环境是一种主要针对C#语言的开发平台,对该环境的熟悉程度将直接影响到开发人员开发项目的效率。为了提高项目开发进程,开发人员必须更好地了解和操作Visual Studio 2015开发环境。为此,这里列出了Visual Studio 2015开发环境中的所有菜单命令及功能,以供读者参考。

Visual Studio 2015开发环境中的菜单栏全部菜单命令及功能如表A-1所示。

表A-1 菜单命令及功能

续表

续表

附录CD

附录CD

链接:http://pan.baidu.com/s/1pLv7N8f

密码:zgfy