c#四个基本技巧
- 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
- 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
- 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。
如何在C#中使用Win32和其他库
这一次我们将深入探讨如何在C#中使用Win32 和其他现有库。
C# 用户经常提出两个问题:“我为什么要另外编写代码来使用内置于Windows 中的功能?在框架中为什么没有相应的
内容可以为我完成这一任务?”当框架小组构建他们的.NET部分时,他们评估了为使.NET 程
序员可以使用Win32 而需要完成的工作,结果发现Win32 API 集非常庞大。
他们没有足够的资
源为所有Win32 API 编写托管接口、加以测试并编写文档,因此只能优先处理最重要的部分。
许多常用操作都有托管接口,但是还有许多完整的Win32 部分没有托管接口。
平台调用(P/Invoke) 是完成这一任务的最常用方法。
要使用P/Invoke,您可以编写一个
如何调用函数的原型,然后运行时将使用此信息进行调用。
另一种方法是使用Managed
Extensions to C++ 来包装函数,这部分内容将在以后的专栏中介绍。
要理解如何完成这一任务,最好的办法是通过示例。
在某些示例中,我只给出了部分代码
完整的代码可以通过下载获得。
简单示例
在第一个示例中,我们将调用Beep() API 来发出声音。
首先,我需要为Beep() 编写适当
定义。
查看MSDN 中的定义,我发现它具有以下原型:
要用C# 来编写这一原型,需要将Win32 类型转换成相应的C# 类型。
由于DWORD 是
字节的整数,因此我们可以使用int 或uint 作为C# 对应类型。
由于int 是CLS 兼容类型(
用于所有.NET 语言),以此比uint 更常用,并且在多数情况下,它们之间的区别并不重要。
bool 类型与BOOL 对应。
现在我们可以用C# 编写以下原型:
public static extern bool Beep(int frequency, int duration);
这是相当标准的定义,只不过我们使用了extern 来指明该函数的实际代码在别处。
此原型
BOOL Beep(
DWORD dwFreq, // 声音频率
DWORD dwDuration // 声音持续时间
);
告诉运行时如何调用函数;现在我们需要告诉它在何处找到该函数。
我们需要回顾一下MSDN 中的代码。
在参考信息中,我们发现Beep() 是在kernel32.lib
定义的。
这意味着运行时代码包含在kernel32.dll 中。
我们在原型中添加DllImport 属性将这
息告诉运行时:
[DllImport("kernel32.dll")]
这就是我们要做的全部工作。
下面是一个完整的示例,它生成的随机声音在二十世纪六十
代的科幻电影中很常见。
它的声响足以刺激任何听者!由于DllImport 允许您调用Win32 中的任何代码,因此就有
能调用恶意代码。
所以您必须是完全受信任的用户,运行时才能进行P/Invoke 调用。
枚举和常量
Beep() 可用于发出任意声音,但有时我们希望发出特定类型的声音,因此我们改用MessageBeep()。
MSDN 给出了以下原型:
这看起来很简单,但是从注释中可以发现两个有趣的事实。
首先,uType 参数实际上接受一组预先定义的常量。
using System;
using System.Runtime.InteropServices;
namespace Beep
{
class Class1
{
[DllImport("kernel32.dll")]
public static extern bool Beep(int frequency, int duration);
static void Main(string[] args)
{
Random random = new Random();
for (int i = 0; i < 10000; i++)
{
Beep(random.Next(10000), 100);
}
}
}
}
BOOL MessageBeep(
UINT uType // 声音类型
);
其次,可能的参数值包括-1,这意味着尽管它被定义为uint 类型,但int 会更加适合。
对于uType 参数,使用enum 类型是合乎情理的。
MSDN 列出了已命名的常量,但没有具体值给出任何提示。
由于这一点,我们需要查看实际的API。
如果您安装了Visual Studio? 和C++,则Platform SDK 位于\Program Files\Microsoft Visual Studio .NET\Vc7\PlatformSDK\Include 下。
为查找这些常量,我在该目录中执行了一个findstr。
findstr "MB_ICONHAND" *.h
它确定了常量位于winuser.h 中,然后我使用这些常量来创建我的enum 和原型:
现在我可以用下面的语句来调用它:MessageBeep(BeepType.IconQuestion);
public enum BeepType
{
SimpleBeep = -1,
IconAsterisk = 0x00000040,
IconExclamation = 0x00000030,
IconHand = 0x00000010,
IconQuestion = 0x00000020,
Ok = 0x00000000,
}
[DllImport("user32.dll")]
public static extern bool MessageBeep(BeepType beepType);
处理结构
有时我需要确定我笔记本的电池状况。
Win32 为此提供了电源管理函数。
搜索MSDN 可以找到GetSystemPowerStatus() 函数。
此函数包含指向某个结构的指针,我们尚未对此进行过处理。
要处理结构,我们需要用C 定义结构。
我们从非托管的定义开始:
然后,通过用C# 类型代替C 类型来得到C# 版本。
这样,就可以方便地编写出C# 原型:
在此原型中,我们用“ref”指明将传递结构指针而不是结构值。
这是处理通过指针传递的
构的一般方法。
BOOL GetSystemPowerStatus(
LPSYSTEM_POWER_STA TUS lpSystemPowerStatus );
typedef struct _SYSTEM_POWER_STA TUS { BYTE ACLineStatus;
BYTE BatteryFlag;
BYTE BatteryLifePercent;
BYTE Reserved1;
DWORD BatteryLifeTime;
DWORD BatteryFullLifeTime;
} SYSTEM_POWER_STA TUS, *LPSYSTEM_POWER_STA TUS;
struct SystemPowerStatus
{
byte ACLineStatus;
byte batteryFlag;
byte batteryLifePercent;
byte reserved1;
int batteryLifeTime;
int batteryFullLifeTime;
}
[DllImport("kernel32.dll")]
public static extern bool GetSystemPowerStatus(
ref SystemPowerStatus systemPowerStatus);
此函数运行良好,但是最好将ACLineStatus 和batteryFlag 字段定义为enum:
请注意,由于结构的字段是一些字节,因此我们使用byte 作为该enum 的基本类型。
字符串
虽然只有一种.NET 字符串类型,但这种字符串类型在非托管应用中却有几项独特之处。
以使用具有内嵌字符数组的字符指针和结构,其中每个数组都需要正确的封送处理。
在Win32 中还有两种不同的字符串表示:
ANSI
Unicode
最初的Windows 使用单字节字符,这样可以节省存储空间,但在处理很多语言时都需要
的多字节编码。
Windows NT? 出现后,它使用双字节的Unicode 编码。
为解决这一差别,W
API 采用了非常聪明的做法。
它定义了TCHAR 类型,该类型在Win9x 平台上是单字节字符
WinNT 平台上是双字节Unicode 字符。
对于每个接受字符串或结构(其中包含字符数据)的
数,Win32 API 均定义了该结构的两种版本,用A后缀指明Ansi 编码,用W 指明wide 编(即Unicode)。
如果您将C++ 程序编译为单字节,会获得A变体,如果编译为Unicode,获得W 变体。
Win9x 平台包含Ansi 版本,而WinNT 平台则包含W 版本。
由于P/Invoke 的设计者不想让您为所在的平台操心,因此他们提供了内置的支持来自动
A或W 版本。
如果您调用的函数不存在,互操作层将为您查找并使用A或W 版本。
通过示例能够很好地说明字符串支持的一些精妙之处。
enum ACLineStatus: byte
{
Offline = 0,
Online = 1,
Unknown = 255,
enum BatteryFlag: byte {
High = 1,
Low = 2,
Critical = 4,
Charging = 8, NoSystemBattery = 128, Unknown = 255,
}
简单字符串
下面是一个接受字符串参数的函数的简单示例:
根路径定义为LPCTSTR。
这是独立于平台的字符串指针。
由于不存在名为GetDiskFreeSpace() 的函数,封送拆收器将自动查找“A”或“W”变体并调用相应的函数。
我们使用一个属性来告诉封送拆收器,API 所要求的字符串类型。
以下是该函数的完整定义,就象我开始定义的那样:
不幸的是,当我试图运行时,该函数不能执行。
问题在于,无论我们在哪个平台上,封送
收器在默认情况下都试图查找API 的Ansi 版本,由于LPTStr 意味着在Windows NT 平台上
使用Unicode 字符串,因此试图用Unicode 字符串来调用Ansi 函数就会失败。
有两种方法可以解决这个问题:一种简单的方法是删除MarshalAs 属性。
如果这样做,将始终
用该函数的A版本,如果在您所涉及的所有平台上都有这种版本,这是个很好的方法。
但是,
会降低代码的执行速度,因为封送拆收器要将.NET 字符串从Unicode 转换为多字节,然后调
函数的A版本(将字符串转换回Unicode),最后调用函数的W 版本。
要避免出现这种情况,您需要告诉封送拆收器,要它在Win9x 平台上时查找A版本,而
NT 平台上时查找W 版本。
要实现这一目的,可以将CharSet 设置为DllImport 属性的一部分
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
BOOL GetDiskFreeSpace(
LPCTSTR lpRootPathName, // 根路径
LPDWORD lpSectorsPerCluster, // 每个簇的扇区数
LPDWORD lpBytesPerSector, // 每个扇区的字节数
LPDWORD lpNumberOfFreeClusters, // 可用的扇区数
LPDWORD lpTotalNumberOfClusters // 扇区总数
);
[DllImport("kernel32.dll")]
static extern bool GetDiskFreeSpace(
[MarshalAs(UnmanagedType.LPTStr)]
string rootPathName,
ref int sectorsPerCluster,
ref int bytesPerSector,
ref int numberOfFreeClusters,
ref int totalNumberOfClusters);
在我的非正式计时测试中,我发现这一做法比前一种方法快了大约百分之五。
对于大多数Win32 API,都可以对字符串类型设置CharSet 属性并使用LPTStr。
但是,
有一些不采用A/W 机制的函数,对于这些函数必须采取不同的方法。
字符串缓冲区
.NET 中的字符串类型是不可改变的类型,这意味着它的值将永远保持不变。
对于要将字符
值复制到字符串缓冲区的函数,字符串将无效。
这样做至少会破坏由封送拆收器在转换字符串
创建的临时缓冲区;严重时会破坏托管堆,而这通常会导致错误的发生。
无论哪种情况都不可
获得正确的返回值。
要解决此问题,我们需要使用其他类型。
StringBuilder 类型就是被设计为用作缓冲区的,
们将使用它来代替字符串。
下面是一个示例:
使用此函数很简单:
请注意,StringBuilder 的Capacity 传递的是缓冲区大小。
具有内嵌字符数组的结构
某些函数接受具有内嵌字符数组的结构。
例如,GetTimeZoneInformation() 函数接受指向下结构的指针:
在C# 中使用它需要有两种结构。
一种是SYSTEMTIME,它的设置很简单:[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
public static extern int GetShortPathName(
[MarshalAs(UnmanagedType.LPTStr)]
string path,
[MarshalAs(UnmanagedType.LPTStr)]
StringBuilder shortPath,
int shortPathLength);
StringBuilder shortPath = new StringBuilder(80);
int result = GetShortPathName(
@"d:\test.jpg", shortPath, shortPath.Capacity);
string s = shortPath.ToString();
typedef struct _TIME_ZONE_INFORMA TION {
LONG Bias;
WCHAR StandardName[ 32 ];
SYSTEMTIME StandardDate;
LONG StandardBias;
WCHAR DaylightName[ 32 ];
SYSTEMTIME DaylightDate;
LONG DaylightBias;
} TIME_ZONE_INFORMA TION, *PTIME_ZONE_INFORMA TION;
这里没有什么特别之处;另一种是TimeZoneInformation,它的定义要复杂一些:
此定义有两个重要的细节。
第一个是MarshalAs 属性:
[MarshalAs(UnmanagedType.ByV alTStr, SizeConst = 32)]
查看ByV alTStr 的文档,我们发现该属性用于内嵌的字符数组;另一个是SizeConst,它
于设置数组的大小。
我在第一次编写这段代码时,遇到了执行引擎错误。
通常这意味着部分互操作覆盖了某些存,表明结构的大小存在错误。
我使用Marshal.SizeOf() 来获取所使用的封送拆收器的大小,
果是108 字节。
我进一步进行了调查,很快回忆起用于互操作的默认字符类型是Ansi 或单字
节。
而函数定义中的字符类型为WCHAR,是双字节,因此导致了这一问题。
我通过添加StructLayout 属性进行了更正。
结构在默认情况下按顺序布局,这意味着所有
段都将以它们列出的顺序排列。
CharSet 的值被设置为Unicode,以便始终使用正确的字符类
型。
经过这样处理后,该函数一切正常。
您可能想知道我为什么不在此函数中使用
CharSet.Auto。
这是因为,它也没有A和W 变体,而始终使用Unicode 字符串,因此我采上述方法编码。
struct SystemTime
{
public short wY ear;
public short wMonth; public short wDayOfWeek; public short wDay;
public short wHour; public short wMinute; public short wSecond; public short wMilliseconds; }
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct TimeZoneInformation
{
public int bias;
[MarshalAs(UnmanagedType.ByV alTStr, SizeConst = 32)]
public string standardName;
SystemTime standardDate;
public int standardBias;
[MarshalAs(UnmanagedType.ByV alTStr, SizeConst = 32)]
public string daylightName;
SystemTime daylightDate;
public int daylightBias;
}
具有回调的函数
当Win32 函数需要返回多项数据时,通常都是通过回调机制来实现的。
开发人员将函数传递给函数,然后针对每一项调用开发人员的函数。
在C# 中没有函数指针,而是使用“委托”,在调用Win32 函数时使用委托来代替函数针。
EnumDesktops() 函数就是这类函数的一个示例:
HWINSTA类型由IntPtr 代替,而LPARAM 由int 代替。
DESKTOPENUMPROC 所需作要多一些。
下面是MSDN 中的定义:
我们可以将它转换为以下委托:
完成该定义后,我们可以为EnumDesktops() 编写以下定义:
这样该函数就可以正常运行了。
在互操作中使用委托时有个很重要的技巧:封送拆收器创建了指向委托的函数指针,该函
指针被传递给非托管函数。
但是,封送拆收器无法确定非托管函数要使用函数指针做些什么,
此它假定函数指针只需在调用该函数时有效即可。
结果是如果您调用诸如SetConsoleCtrlHandler() 这样的函数,其中的函数指针将被保存
将来使用,您就需要确保在您的代码中引用委托。
如果不这样做,函数可能表面上能执行,但
将来的内存回收处理中会删除委托,并且会出现错误。
BOOL EnumDesktops(
HWINSTA hwinsta, // 窗口实例的句柄
DESKTOPENUMPROC lpEnumFunc, // 回调函数
LPARAM lParam // 用于回调函数的值
);
BOOL CALLBACK EnumDesktopProc(
LPTSTR lpszDesktop, // 桌面名称
LPARAM lParam // 用户定义的值
);
delegate bool EnumDesktopProc(
[MarshalAs(UnmanagedType.LPTStr)]
string desktopName,
int lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto)] static extern bool EnumDesktops(
IntPtr windowStation,
EnumDesktopProc callback,
int lParam);
其他高级函数
迄今为止我列出的示例都比较简单,但是还有很多更复杂的Win32 函数。
下面是一个示例
前两个参数的处理比较简单:ulong 很简单,并且可以使用UnmanagedType.LPArray 来
送缓冲区。
但第三和第四个参数有一些问题。
问题在于定义ACL 的方式。
ACL 结构仅定义了ACL
头,而缓冲区的其余部分由ACE 组成。
ACE 可以具有多种不同类型,并且这些不同类型的
的长度也不同。
如果您愿意为所有缓冲区分配空间,并且愿意使用不太安全的代码,则可以用C# 进行处理。
但工作量很大,并且程序非常难调试。
而使用C++ 处理此API 就容易得多。
属性的其他选项
DLLImport 和StructLayout 属性具有一些非常有用的选项,有助于P/Invoke 的使用。
下列出了所有这些选项:
DLLImport
CallingConvention
您可以用它来告诉封送拆收器,函数使用了哪些调用约定。
您可以将它设置为您的函数的
用约定。
通常,如果此设置错误,代码将不能执行。
但是,如果您的函数是Cdecl 函数,并且
用StdCall(默认)来调用该函数,那么函数能够执行,但函数参数不会从堆栈中删除,这会
堆栈被填满。
CharSet
控制调用A变体还是调用W 变体。
EntryPoint
此属性用于设置封送拆收器在DLL 中查找的名称。
设置此属性后,您可以将C# 函数重
名为任何名称。
ExactSpelling
DWORD SetEntriesInAcl(
ULONG cCountOfExplicitEntries, // 项数
PEXPLICIT_ACCESS pListOfExplic itEntries, // 缓冲区
PACL OldAcl, // 原始ACL
PACL *NewAcl // 新ACL
);
将此属性设置为true,封送拆收器将关闭A和W 的查找特性。
PreserveSig
COM 互操作使得具有最终输出参数的函数看起来是由它返回的该值。
此属性用于关闭这性。
SetLastError
确保调用Win32 API SetLastError(),以便您找出发生的错误。
StructLayout
LayoutKind
结构在默认情况下按顺序布局,并且在多数情况下都适用。
如果需要完全控制结构成员所
置的位置,可以使用LayoutKind.Explicit,然后为每个结构成员添加FieldOffset 属性。
当您需
创建union 时,通常需要这样做。
CharSet
控制ByV alTStr 成员的默认字符类型。
Pack
设置结构的压缩大小。
它控制结构的排列方式。
如果C 结构采用了其他压缩方式,您可能要设置此属性。
Size
设置结构大小。
不常用;但是如果需要在结构末尾分配额外的空间,则可能会用到此属性从不同位置加载
您无法指定希望DLLImport 在运行时从何处查找文件,但是可以利用一个技巧来达到这一的。
DllImport 调用LoadLibrary() 来完成它的工作。
如果进程中已经加载了特定的DLL,那么使指定的加载路径不同,LoadLibrary() 也会成功。
这意味着如果直接调用LoadLibrary(),您就可以从任何位置加载DLL,然后DllImport LoadLibrary() 将使用该DLL。
由于这种行为,我们可以提前调用LoadLibrary(),从而将您的调用指向其他DLL。
如果
编写库,可以通过调用GetModuleHandle() 来防止出现这种情况,以确保在首次调用P/Invo 之前没有加载该库。
P/Invoke 疑难解答
如果您的P/Invoke 调用失败,通常是因为某些类型的定义不正确。
以下是几个常见问题
1.long != long。
在C++ 中,long 是4 字节的整数,但在C# 中,它是8 字节的整数。
2.字符串类型设置不正确。
用C#设计Windows 应用程序模板
通常windows 应用程序都有相似的特征:控件、菜单、工具条、状态栏等等。
每次我们开始作一个新的windows 应用程序时都是以相同的事情开始:建立项目,添加控件和事件处理器。
如果我们有一个模板,那么我们就可以节约大量的时间了。
在介绍如何建立模板的过程中,将涉及大量的微软.net framework 类库的基本知识。
如果你没有使用集成开发环境那么本文介绍的模板对你将非常有用,如果你使用了visual
这样的集成开发环境你也可以从中了解控件的工作方式,这对你也是很有用的。
写一个windows 应用程序总是从下面的几个步骤开始:
1、创建一个窗体
2、给窗体添加控件
3、添加菜单和菜单项,并绑定到窗体上
4、创建工具条
5、添加状态栏
6、添加事件处理器
在windows 应用程序开发中,你不可能完全跳过这些步骤,你可以对他作些修改,但不可能完全跳过。
下面是完全的模板图:
.ü..?..?..?..?..?..
--------------图1 -----------
创建窗体
在.Net FrameWork 程序设计中,窗体总是System.Windows.Forms.Form 类的子
类,他继承聊着各类的全部特征,你甚至不用添加任何一行代码就可以创建window 窗
体了。
现在我们创建一个form 类的子类myapp,需要注意的是在说明继承时使用冒号:。
using System.Windows.Forms;
public class MyWinApp: Form {
}
与其他程序相似,我们需要一个main()的主方法作为应用程序的入口。
在main()
中使用System.Windows.Forms 的run 方法来开始应用程序和显示窗体。
run 方法有三
种重载方式,对于windows 应用程序,我们采取重载一个窗体对象的方式:
public static void Main() {
MyWinApp form = new MyWinApp();
Application.Run(form);
}
作为选择,可以在main()方法中将两行代码写成一行:
public static void Main() {
Application.Run(new MyWinApp());
}
设置属性
一旦有了窗体,就可以设置它的属性(象高度、宽度、标题等)并指定一个象图 1
那样的图标,可以通过代码或从构造器重调用一个方法来完成这个设置。
在list1 中有
一个方法InitializeComponent,可以将初始化代码写入其中。
使用width 和height 属性设置宽度和高度
this.Width = 400;
this.Height = 300;
使用窗体的text 属性设置标题
this.Text = "My Windows Application"; 设置图标
如果未作说明,windows 应用程序将显示一个缺省图标,可以修改这个图标,方法
是设置窗体的icon 属性,这个属性接受一个System.Drawing.Icon 对象。
Icon 类有几个可以用于初始化Icon 对象的构造函数,需要说明的是最好提供.ico 文件的完全路径。
this.Icon = new Icon(imageFolder + "applicationLogo.ico");
这里imageFolder 是一个静态域,用于指示icon 文件的目录,imageFolder 的定
义如下:
static String imageFolder = "Images" +
Path.DirectorySeparatorChar.ToString();
这告诉应用程序icon 文件在应用程序的image 目录下,这里使用了System.IO.Path
类的DirectorySeparatorChar 属性获得操作系统用于分隔目录和父目录的分隔符,在这
里没有使用"/",这是因为对于windows 操作系统这是正确的,但对于linux 和unix 则不然。
这也是为了证明模板对于其他操作系统也是方便的。
窗体的位置
使窗体居中时非常有用的,要达到这个目的,需要使用StartPosition 属性,并将FormStartPosition 的一个成员赋给他。
this.StartPosition = FormStartPosition.CenterScreen;
当然也可以用form 类的CenterToScreen 方法是窗体居中,但这个方法不能直接使
用。
this.CenterToScreen();
form 类还有其他一些让人感兴趣的属性和方法,这里列出了其中的部分:
1、设置Opacity 属性创建一个透明或半透明的窗体
2、设置modal 属性使窗体为模式的或非模式的
3、通过BackColor 属性改变窗体的背景颜色
4、将TopMost 属性设置为true,以确定窗体在其他所有非最顶部窗体之上
给窗体添加控件
windows 控件均继承自System.Windows.Forms.Control 类,control 类处理用户输
入、安全等,他给窗体的控件提供了一个windows 句柄,以及一些重要的属性,如Name,
Enabled, Text, BackColor, Left, Top, Size, Location, V isible, Width, 和Height。
System.Windows.Forms 名称空间提供了12 个控件,每一个控件都有它自己的属
性和特征,所以在篇文章中我们不可能全部讨论。
给窗体添加控减非常容易,下面的代码给窗体添加了三个控件,分别是:Label, Button, 和TreeView。
Label label;
Button button;
TreeV iew tree;
为了简便,可以在声明的同时实例化这些对象。
Label label = new Label();
Button button = new Button();
TreeV iew tree = new TreeView();
然后在InitializeComponent 方法中设置这些控件的属性,尤其是设置控件的大小
和在窗体中的位置,对于大小可以使用width 和height 属性,比如treeview 控件的大小可以使用下面的属性:
tree.Width = 100;
tree.Height = 100;
确定控件的位置可以使用控件的left 和top 属性,这两个属性决定了控件的左上角
的位置,就像下面的语句决定了treeview 的位置:
tree.Top = 40;
tree.Left = 20;
当然你也可以使用更简单的Location 属性,将System.Drawing.Point 结构的实例
赋给他。
我们用这种方法确定Label 和Button 的位置。
label.Location = new Point(220, 40);
button.Location = new Point(220, 80);
下一步就是要使控件在窗体上可见。
使用Form.ControlCollection 类的add 方法将
每个控件添加到窗体的ControlCollection 中,ControlCollection 可以使用窗体的控件属性访问。
this.Controls.Add(label);
this.Controls.Add(button);
this.Controls.Add(tree);
添加菜单和菜单项
要找到没有菜单的windows 应用程序非常困难,菜单使访问应用程序的功能变得
很简单,在大多数环境下可以最小的使用控件。
菜单比控件占用更少的空间,同时使应用程序显得更有组织。
在System.Windows.Forms 名称空间中,所有与菜单相关的控件都是menu 类的
子类。
menu 是一个抽象类,你不能直接实例化,menu 类有三个子类:
ContextMenu
MainMenu
MenuItem
ContextMenu 类表示快捷菜单,在控件或窗体区域点击鼠标右键时显示。
快捷菜单
常用于组合窗体mainmenu 类的菜单项给出应用程序的上下文,这对于用户时非常有用的。
MainMenu 表示传统的位于窗体顶部的菜单,你可以把它看成窗体菜单结构的容
器。
一个菜单是由MenuItem 表示的菜单项组成的,对于应用程序而言每一个菜单项是一个命令或其它子菜单项的父菜单。
form 类都有一个menu 属性,采用将mainmenu 对象赋给menu 属性的方式将mainmenu 对象绑定到窗体。
在这个模板中,我们没有使用ContextMenu 类,但我们示范了如何使用MainMenu
和MenuItem 类。
我们首先需要在窗体中添加一个菜单,给窗体添加一个MainMenu 对象。
MainMenu mainMenu = new MainMenu();
现在MainMenu 对象中什么都没有,下面我们给他添加一个MenuItem 对象。
在
List1 中主菜单称为fileMenuItem,它的text 属性是&File,&表示他后面的字母带下划线,是该菜单的快捷键。
通过使用Menu 对象的MenuItemCollection 的add 方法为MainMenu 添加一个或几个MenuItem,这个集合可以通过menu 类的MenuItems 属性访问。
MenuItem fileMenuItem = new MenuItem();
mainMenu.MenuItems.Add(fileMenuItem);
我们在fileMenuItem 菜单项中还添加了其它MenuItem,这些MenuItem 是fileMenuItem 的子菜单。
你也可以给子菜单添加子菜单。
图 2 显示了菜单的等级结构。
.ü..?..?..?..?..?..
---------图2---------
菜单项的声明如下:
MenuItem fileNewMenuItem;
MenuItem fileOpenMenuItem;
MenuItem fileSaveMenuItem;
MenuItem fileSaveAsMenuItem;
MenuItem fileMenuWithSubmenu;
MenuItem submenuMenuItem;
MenuItem fileExitMenuItem;
MenuItem 类有几个构造函数,使用这些构造函数实例化你的MenuItem 对象,并添加一个事件处理器。
// the following constructor is the same as:
// menuItem fileNewMenuItem = new MenuItem();
// fileNewMenuItem.Text = "&New";
// fileNewMenuItem.Shortcut = Shortcut.CtrlN;
// fileNewMenuItem.Click += new
// System.EventHandler(this.fileNewMenuItem_Click);
fileNewMenuItem = new MenuItem("&New",
new System.EventHandler(this.fileNewMenuItem_Click), Shortcut.CtrlN); fileOpenMenuItem = new MenuItem("&Open",
new System.EventHandler(this.fileOpenMenuItem_Click), Shortcut.CtrlO); fileSaveMenuItem = new MenuItem("&Save",
new System.EventHandler(this.fileSaveMenuItem Click), Shortcut.CtrlS);
Event handling is discussed further in the section "Adding Event Handlers."
As mentioned, the menu items are added to the fileMenuItem control.
fileMenuItem.MenuItems.Add(fileNewMenuItem);
fileMenuItem.MenuItems.Add(fileOpenMenuItem);
fileMenuItem.MenuItems.Add(fileSaveMenuItem);
fileMenuItem.MenuItems.Add(fileSaveAsMenuItem);
fileMenuItem.MenuItems.Add(fileMenuWithSubmenu);
fileMenuWithSubmenu.MenuItems.Add(submenuMenuItem);
fileMenuItem.MenuItems.Add("-"); // add a separator
fileMenuItem.MenuItems.Add(fileExitMenuItem);
fileSaveAsMenuItem = new MenuItem("Save &As",
new System.EventHandler(this.fileSaveAsMenuItem_Click));
fileMenuWithSubmenu = new MenuItem("&With Submenu");
submenuMenuItem = new MenuItem("Su&bmenu",
new System.EventHandler(this.submenuMenuItem_Click));
fileExitMenuItem = new MenuItem("E&xit",
new System.EventHandler(this.fileExitMenuItem_Click));
注意在菜单项之间可以创建一个分隔符,方法是使用menu 类的MenuItems 集合
的add 方法。
上面的例子中使用的分隔符是"-"。
创建工具条
为了使应用程序的界面更友好,可以在窗体中添加一个工具条。
工具条由
System.Windows.Forms.ToolBar 类描述。
窗体中可有多个工具条,工具条中包含了一
个或多个ToolBarButton 类描述的按钮,可以在每个按钮中插入图像或图标,要达到这个目的你需要一个ImageList 控件作为图像容器。
ImageList imageList = new ImageList();
对于每个图像文件首先要实例化为image 对象,然后将这些图像添加到ImageList
控件中,Image 和Bitmap 类可以在System.Drawing 名称空间中找到。
Image newFileImage = new Bitmap(imageFolder + "newFile.bmp");
Image openFileImage = new Bitmap(imageFolder + "openFile.gif");
Image saveFileImage = new Bitmap(imageFolder + "saveFile.bmp");
Image printImage = new Bitmap(imageFolder + "print.gif");
.
.
.
imageList.Images.Add(newFileImage);
imageList.Images.Add(openFileImage);
imageList.Images.Add(saveFileImage);
imageList.Images.Add(printImage);
注意你可以使用Images 集合的add 方法将image 对象加入到imagelist 控件中。
现在为将这些图加入到控件中,必须将ImageList 控件赋给ToolBar 的ImageList 属性。
toolBar.ImageList = imageList;
然后将ImageList 控件中的图像赋给工具按钮的ImageIndex 属性。
newToolBarButton.ImageIndex = 0;
openToolBarButton.ImageIndex = 1;
saveToolBarButton.ImageIndex = 2;
printToolBarButton.ImageIndex = 3;
象菜单项一样,现在必须把工具按钮加入到工具条中。
toolBar.Buttons.Add(separatorToolBarButton);
toolBar.Buttons.Add(newToolBarButton);
toolBar.Buttons.Add(openToolBarButton);
toolBar.Buttons.Add(saveToolBarButton);
toolBar.Buttons.Add(separatorToolBarButton);
toolBar.Buttons.Add(printToolBarButton);
最后将工具条加入到窗体中。
this.Controls.Add(toolBar);
添加状态条
状态条由System.Windows.Forms.StatusBar 描述,它提供了定制控件的外观的属性,状态条由StatusBarPanel 对象组成,在我们的模板中状态条有两个嵌套板:
StatusBar statusBar = new StatusBar();
StatusBarPanel statusBarPanel1 = new StatusBarPanel();
StatusBarPanel statusBarPanel2 = new StatusBarPanel();
状态条和状态跳上的嵌套板由下面的代码设置:
statusBarPanel1.BorderStyle = StatusBarPanelBorderStyle.Sunken;
statusBarPanel1.Text = "Press F1 for Help";
statusBarPanel1.AutoSize = StatusBarPanelAutoSize.Spring;
statusBarPanel2.BorderStyle = StatusBarPanelBorderStyle.Raised;
statusBarPanel2.ToolTipText = System.DateTime.Now.ToShortTimeString(); statusBarPanel2.Text = System.DateTime.Today.ToLongDateString();
statusBarPanel2.AutoSize = StatusBarPanelAutoSize.Contents;
statusBar.ShowPanels = true;
statusBar.Panels.Add(statusBarPanel1);
statusBar.Panels.Add(statusBarPanel2);
同样我们需要将状态条添加到窗体中:
this.Controls.Add(statusBar);
事件处理器
在windows 程序设计中添加事件处理器是最重要的任务。
事件处理器保证了程序
与用户交互,同时完成其他重要的功能。
在c#中你可以给控件和菜单事件添加事件处
理器以俘获你想处理的事件,下面的代码给Button 控件的click 事件设计了一个事件处理器:
button.Click += new System.EventHandler(this.button_Click);
button_Click 事件处理器必须被处理:
private void button_Click(Object sender, System.EventArgs e) {
MessageBox.Show("Thank you.", "The Event Information");
}
MenuItem 对象在实例化的同时可以给赋以一个事件处理器:
fileNewMenuItem = new MenuItem("&New",
new System.EventHandler(this.fileNewMenuItem_Click), Shortcut.CtrlN); fileOpenMenuItem = new MenuItem("&Open",。