
第4章 Windows窗体编程你也行
小张准备好了自己的简历,去一家著名的软件公司应聘。那家软件公司名气很大,应聘的人才趋之若鹜。
当小张走进大厅的时候,着实吃了一惊,整个大厅挤满了人,乱哄哄的。小张站在人群的最后面,看着前面围了一圈又一圈的人,想着不知道要等到什么时候才能轮到自己,心中就懊悔起来,为什么自己不早点过来呢。
公司似乎对此番景况也感到有些意外,人群很乱,乱得连公司的工作人员都进不去,小张看到几个工作人员都被堵在外面了。
小张忽然灵机一动。
小张走到那几个工作人员的前面,昂首挺胸,勇敢地对着人群大喊一声:所有应聘的人排成两队!
人们立即循声而来,将目光投到小张身上,看着小张和工作人员站在一起,以为小张就是应聘的组织人员,一个个立即动了起来,刷地一下便排成两条长长的队伍,让出了一条宽敞的道来。
那几位工作人员冲小张微微一笑,小张将大家的简历收在一起,然后抱着几十份简历第一个走进了应聘室,整个过程俨然是一个工作人员的所为。小张将那些简历放在桌上,从包里掏出自己的简历放在最上面。
主考官几乎只是象征性地看了一下小张的简历,就对小张说:“你从今天开始上班,你今天的任务就是协助我们完成招聘工作。”
就这样小张成功了。小张欣喜若狂地走出门,开始维持秩序了。站在队伍的前面,没有人怀疑小张工作人员的身份,更没人知道仅仅十几分钟前,他还是挤在队伍最后面的一名普通应聘者。
在无比激烈的社会竞争中,我们常常被众多的对手所淹没。也许别人已经抢占了先机,比我们拥有更多的优势,但只要善于运用智慧,我们同样可以获得成功。用人单位不仅仅只看重学历和分数,更看重人的综合素质,包括为人处世的态度、人际交往能力及团队精神等。
其实,世间并不缺乏机会,而是缺乏发现机会的眼睛。
基于Web的B/S架构应用程序最近几年确实非常流行,B/S易部署、易维护的特点,使Web应用程序的开发得到空前的发展。但是,Web的应用程序的缺点是有时候不能提供丰富的用户体验,以及对本机系统环境的控制和利用,例如刷新问题及长时间运行计算的进度显示等。使得在某些情况下还是需要通过 Windows 程序来实现客户端功能。由于.NET Framework的平台无关性,现在Windows应用程序也不再难以开发和部署。
本章只是抛砖引玉地总结一些技术点,引起大家的兴趣,真正的详细的知识还需要在日常工作中学习和积累。通过学习 WinForm 的开发,你会发现很多乐趣,可以实现自己真正想要的各种Web无法实现的功能,实现各种桌面工具。从中领略开发带来的快乐和成就感。
4.1 创建简单的WinForm项目
打开VS.NET,选择菜单“文件”→“新建项目”命令,在左边的“项目类型”面板中选择“Visual C#”选项,在右边的面板中选择“Windows 窗体应用程序”选项,单击“确定”按钮,生成的项目及列表如图4-1所示。

图4-1 Windows窗体应用程序
项目组成文件介绍如下。
● Form1.cs:是窗体文件,Form1 .cs展开后包含Form1.Designer.cs和Form1.resx。
Form1.Designer.cs:是设计器自动生成的,主要是界面设计代码。
Form1.resx:是设计窗体时所嵌入的资源。
● Program.cs:是主程序的入口。
● Properties:是项目组件,定义了程序集的属性。由Assemblyinfo.cs、Resources.resx、
Settings.settings等组成。
Assemblyinfo.cs:是用来设置项目的一些属性。
Resources.resx:是图片资源文件。
Settings.settings:配置文件。
● WinForm应用程序默认没有创建App.config,我们可以通过单击鼠标右键添加“应用程序配置文件”。
代码示例: (示例位置:光盘\code\ch04\4.1\Program.cs)
using System; using System.Collections.Generic; using System.Linq; using System.Windows.Forms; namespace WindowsFormsApplication1 { static class Program { // <summary> // 应用程序的主入口点 // </summary> [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); Application.Run(new Form1()); } } }
代码要点:
● Main()是应用程序的主入口点,任何Winform程序都必须有一个且只能有一个Main函数。
● [STAThread]属性把COM线程模型设置为单线程单元(Sinle-Threaded Apartment, STA)。
● Application.Run(new Form1());表示要在当前线程运行显示的窗体。
Form1.cs的代码:
using System; using System.ComponentModel; using System.Data; using System.Drawing; using System.Linq; using System.Text; using System.Windows.Forms; namespace WindowsFormsApplication1 { public partial class Form1 : Form { public Form1() { InitializeComponent(); } } }
Form1就是刚才Run要首先运行的窗体,目前是空白窗体,因为我们还没有放任何东西在上面。
public Form1()是窗体类的构造函数,生成窗体前首先会运行这个。
InitializeComponent();是实现窗体各种元素的初始化功能。
WinForm也有和WebForm一样的Form1_Load事件。
其实这个代码和WebForm的代码很相似,只是有两点不同:
● 引用:using System.Windows.Forms。
● 继承:public partial class Form1 : Form。
其他基本和 WebForm相同。剩下的就是我们往窗体上拖各种控件来实现我们的各种功能了。VS.NET 开发工具的高度集成性和相同的编程模型,使得无论是 WinForm 开发还是WebForm开发都变得轻而易举了。
4.2 创建MDl窗体应用
MDI就是所谓的多文档界面,它是从Windows 2.0下的Microsoft Excel电子表格程序开始引入的,这是因为Excel电子表格用户有时需要同时操作多份表格,MDI正好为这种操作提供了很大的方便,于是就产生了MDI程序。在Windows系统3.1版本中,MDI得到了更大范围的应用。其中系统中的程序管理器和文件管理器都是MDI程序。C#是微软推出的下一代主流程序开发语言,也是一种功能十分强大的程序设计语言,正在受到越来越多的编程人员的喜欢。在C#中,提供了为实现MDI程序设计的很多功能。本节将通过一个具体的例子来详细地介绍在Visual C#中的MDI编程。(完整代码示例位置:光盘\code\ch04\4.2)
实现步骤:
(1)首先,创建一个Windows 窗体应用程序(同4.1节)。
(2)设定当前窗体是一个MDI窗体的容器(即MDI父窗体),因为只有如此才能够在此主窗体上面添加MDI子窗体。如图4-2所示设置窗体属性。

图4-2
也可以在项目上单击鼠标右键,在弹出的快捷菜单中选择“添加窗体”命令,直接添加一个MDI父窗体,如图4-3所示。

图4-3 添加窗体
通过这种方式添加的父窗体,系统自动为其添加了各种操作菜单和功能,简便了许多。
(3)在MDI父窗体实现增加一个子窗体。
Form childForm = new Form();
childForm.MdiParent = this; //this表示本窗体为其父窗体
childForm.Text = "窗口" + childFormNumber++;
childForm.Show();
显示效果如图4-4所示。

图4-4 MDI窗体
(4)子窗体的排列显示。
private void CascadeToolStripMenuItem_Click(object sender, EventArgs e) { LayoutMdi(MdiLayout.Cascade);//层叠子窗体 } private void TileVerticalToolStripMenuItem_Click(object sender, EventArgs e) { LayoutMdi(MdiLayout.TileVertical);//垂直平铺子窗体 } private void TileHorizontalToolStripMenuItem_Click(object sender, EventArgs e) { LayoutMdi(MdiLayout.TileHorizontal);//水平平铺子窗体 } private void ArrangeIconsToolStripMenuItem_Click(object sender, EventArgs e) { LayoutMdi(MdiLayout.ArrangeIcons);//所有子窗体排列图标方式 }
显示效果如图4-5所示。

图4-5 层叠子窗体
(5)在“窗口”菜单下面显示所有已经打开的子窗体列表,如图4-6所示。

图4-6 窗口菜单
WindowMenu.MdiList = true ;
(6)关闭所有子窗体。
private void CloseAllToolStripMenuItem_Click(object sender, EventArgs e) { //关闭所有子窗体 foreach (Form childForm in MdiChildren) { childForm.Close(); } }
(7)避免重复打开同一子窗体。
private void optionsToolStripMenuItem_Click(object sender, EventArgs e) { FormOption frmOption = new FormOption(); ShowMdiForm(frmOption); } //显示子窗体 private void ShowMdiForm(Form frm) { if (!CheckMdiForm(frm.Name)) { frm.MdiParent = this; frm.Show(); } else { frm.Activate(); } } //检查该窗体是否已经打开 private bool CheckMdiForm(string FormName) { bool hasForm = false; foreach (Form f in this.MdiChildren) //循环检查是否存在 { if (f.Name == FormName) { hasForm = true; } } return hasForm; }
(8)更改MDI主窗体背景。
原理:MDI窗口有一个叫MdiClient的窗口对象作为其主背景窗口,修改MDI窗口的背景就是修改该MdiClient对象的背景。MdiClient是以MDI窗口的一个ChildControl的形式存在的,因此我们可以通过遍历MDI窗口的Controls对象集来获得。
//MDI窗口有一个叫MdiClient的窗口对象作为主背景窗口, //修改MDI窗口的背景就是修改该MdiClient对象的背景 private System.Windows.Forms.MdiClient m_MdiClient; public Form1() { InitializeComponent(); int iCnt = this.Controls.Count; for (int i = 0; i < iCnt; i++) { if (this.Controls[i].GetType().ToString() == "System.Windows.Forms.MdiClient") { //遍历MDI窗口的Controls对象集来获得MdiClient this.m_MdiClient = (System.Windows.Forms.MdiClient)this.Controls[i]; break; } } //使用固定颜色 //this.m_MdiClient.BackColor = System.Drawing.Color.AliceBlue; this.m_MdiClient.BackColor = System.Drawing.Color.FromArgb((( System.Byte)(49)), ((System.Byte)(152)), ((System.Byte)(109)));//使用自定义颜色值 }
另外,在具体应用中,可能要考虑把这些东西放置到Paint或erasebkground等事件中。
4.3 获取应用程序路径信息
桌面 Windows 程序开发,有时候需要读取当前目录下的文件,有时候需要在当前目录下创建文档;甚至有时候自升级也需要知道应用程序当前所在的目录,所以,获取应用程序路径既是一种常用知识点,也是一种重要的功能。
下面列出几种获取文件路径信息的方法。
代码示例: (示例位置:光盘\code\ch04\4.3)
//应用程序的可执行文件的路径 string apppath = Application.ExecutablePath; //指定路径字符串的父目录信息 string str = Path.GetDirectoryName(apppath); //指定的路径字符串的扩展名 str = Path.GetExtension(apppath); //不带扩展名的指定路径字符串的文件名 str = Path.GetFileNameWithoutExtension(apppath); //指定路径字符串的文件名和扩展名 str = Path.GetFileName(apppath); //是否包括文件扩展名 bool t = Path.HasExtension(apppath); //指定路径字符串的绝对路径 str = Path.GetFullPath(apppath); //指定路径的根目录信息 str = Path.GetPathRoot(apppath); //当前系统的临时文件夹的路径 str = Path.GetTempPath(); //是绝对路径信息还是相对路径信息 t = Path.IsPathRooted(apppath); //路径字符串中分隔符 char c = Path.DirectorySeparatorChar; //在环境变量中分隔路径字符串的分隔符 c = Path.PathSeparator;
4.4 回车跳转控件焦点
如果你做过客服,你就会明白这个功能对她们而言是多么的重要,每天需要快速地录入客户反馈的信息,所以,录入速度是很重要的。如何让她们节省更多的时间,让她们以最快的速度,最少的操作时间来完成信息的录入成为很关键的因素,尽量避免用鼠标操作则是其关键流程之一。
代码示例: (示例位置:光盘\code\ch04\4.4)
1. 判断按键,手工跳转到指定文本框
private void textBox1_KeyDown(object sender, KeyEventArgs e) { if (e.KeyValue == 13)//if(e.KeyCode==Keys.Enter)//回车 { this.textBox2.Focus(); } }
2.根据控件TabIndex属性顺序跳转
private void textBox1_KeyDown(object sender, KeyEventArgs e) { if (e.KeyCode == Keys.Enter) { //激活“Tab”键顺序中的下一个控件,需设置textBox4的TabIndex顺序属性 this.SelectNextControl(this.ActiveControl, true, true, true, true); } }
3.模拟发送“Tab”键
this.textBox1.KeyDown += new System.Windows.Forms.KeyEventHandler( this.EnterToTab); this.textBox2.KeyDown += new System.Windows.Forms.KeyEventHandler( this.EnterToTab); private void EnterToTab(object sender, System.Windows.Forms.KeyEventArgs e) { if (e.KeyValue == 13)//if(e.KeyCode==Keys.Enter) { SendKeys.Send("{TAB}"); //等同于按Tab键,需设置textBox4的TabIndex顺序属性 } }
这样,当我们按回车键时,就会自动从第一个文本框依次按顺序自动跳转,直到最后提交保存。
运行效果如图4-7所示。

图4-7 回车跳转控件焦点
4.5 窗体间传递复杂数据
一个稍微复杂一点的程序一般都有两个或者更多的窗体。在程序设计中,数据不仅要在同一个窗体中传递,还要在窗体间传递。有时往往需要在相互调用的窗体间传递比较复杂的数据,甚至需要子窗体修改父窗体的内容。这里我们总结几种窗体间数据传递的方法。(完整代码示例位置:光盘\code\ch04\4.5)
4.5.1 构造传递
(1)在项目中建立Form1和Form2两个窗体。将Form1中的输入值传递给Form2并显示。
(2)重载Form2的构造函数,接收参数作为传值:
public partial class Form2 : Form { public Form2() { InitializeComponent(); } public Form2(string msg) { InitializeComponent(); label1.Text = msg; } }
(3)在Form1中创建并调用Form2:
private void button1_Click(object sender, EventArgs e) { Form2 f2 = new Form2(textBox1.Text.Trim()); f2.Show(); }
程序运行效果如图4-8和图4-9所示。

图4-8 Form 1运行效果

图4-9 Form 2 运行效果
4.5.2 公有字段传递
(1)在Form1中定义public字段。
把private System.Windows.Forms.TextBox textBox1;,改为public System.Windows.Forms.TextBox textBox1; 。
或者定义一个公共字段,这样更符合面向对象的封装性。
// <summary> // 公共字段 // </summary> public string Msg { get { return textBox1.Text.Trim(); } }
(2)Form2增加一个公共属性和构造函数,用于接收传值。
public partial class Form2 : Form { public Form2() { InitializeComponent(); } // <summary> // 公共字段 // </summary> public string Msg { get { return label1.Text.Trim(); } set { label1.Text = value; } } public Form2(Form1 f1) //重载构造函数 { InitializeComponent(); //在Form2中取Form1中的公共字段,比f1.textBox1具有更好的封装性 label1.Text = f1.Msg; } }
(3)在Form1中创建并调用Form2。
Form2 f2; private void button2_Click(object sender, EventArgs e) //创建并传值给Form2 { f2 = new Form2(this); f2.Show(); } private void button3_Click(object sender, EventArgs e) //更新Form2中控件的值 { f2.Msg = textBox1.Text; //更新现有Form2对象实例的公共属性值 }
运行效果如图4-10和图4-11所示。

图4-10 Form 1运行效果

图4-11 Form 2运行效果
4.5.3 委托与事件传递
功能:实现在子窗体中改变父窗体的内容,通过委托和事件来传值给父窗体。
(1)定义一个结果对象,用来存放子窗体返回的结果。同时定义一个事件,可以让子窗体修改父窗体的状态。代码如下:
//声明delegate对象 public delegate void TextChangedHandler(string s); public class CallObject { //用来存放子窗体返回的结果 public string ResultValue = ""; //定义事件对象,可以让子窗体修改父窗体的状态 public event TextChangedHandler SelTextChanged; //以调用delegate的方式写事件触发函数 public void ChangeSelText(string s) { if (SelTextChanged != null) { //调用delegate SelTextChanged(s); } } }
(2)在子窗体添加一个构造函数,以接收结果对象。
private CallObject co; public Form4(CallObject cov): this() { this.co = cov; }
(3)在父窗体中创建子窗体,并订阅cResult事件:
private void btnCallF4_Click(object sender, EventArgs e) { CallObject co = new CallObject(); //用+=操作符将事件添加到队列中 co.SelTextChanged += new TextChangedHandler(EventResultChanged); Form4 f4 = new Form4(co); f4.ShowDialog(); txtF4EventResult.Text = "Form4的返回值是:\r\n" + co.ResultValue; } //事件方法 private void EventResultChanged(string s) { txtF4Select.Text = s; }
(4)在子窗体中改变选择,通过CallObject传递到父窗体。
private void radbtn_A_CheckedChanged(object sender, EventArgs e) { co.ChangeSelText("A"); } private void radbtn_B_CheckedChanged(object sender, EventArgs e) { co.ChangeSelText("B"); } private void radbtn_C_CheckedChanged(object sender, EventArgs e) { co.ChangeSelText("C"); } private void btnSend_Click(object sender, EventArgs e) { co.ResultValue = textBox1.Text; Close(); }
这样避免了在子窗体直接调用父窗体对象,有效地降低了二者之间的依赖性及耦合性。父窗体改变后不需要重新编译子窗体。两个窗体都同时依赖于结果对象,更好地满足了面对对象的封装性和“依赖倒置”的原则,程序运行结果如图4-12所示。

图4-12 程序运行结果
4.6 实现个性化窗体界面
不知道大家以前是否用过播放器的主题皮肤,可以变幻各种形状的那种,感觉是不是很炫?相信每个编程爱好者都希望自己的程序不仅性能优越而且有一个美观的界面,一个区别于别人的程序的个性化的界面。然而以前烦琐的API调用和大量的代码使大家望而却步。现在好了,在C#中通过少量的代码就可以实现不规则个性化窗体的制作。(完整代码示例位置:光盘\code\ch04\4.6)
先让我们看一下实现效果,如图4-13所示。

图4-13 个性化窗体界面
这是用自己的照片或一张图片,实现的无边框个性化形状的窗体,而不是方方正正的那种窗体。飘在空中,是不是很酷?其实,用C#实现不规则窗体是一件很容易的事情。
下面我就用一个简单的例子来讲述其制作过程。
(1)准备一张照片,对打算使其透明的地方使用白色背景(为了效果,最好是 BMP位图),如图4-14所示。

图4-14 白色背景
(2)新建一个Windows 窗体应用程序。
(3)设置窗体的属性。
①将 FormBorderStyle 属性设置为 None。
②设置窗体大小和图片大小相同或者通过代码动态获取图片大小设置窗体。
③将 TransparencyKey 属性设置为位图文件的背景色,本例中为白色(此属性告诉应用程序窗体中的哪些部分需要设置为透明)。
④实现MainForm_Paint事件,在窗体上绘制图片。
(4)代码实现:
using System; using System.Drawing; using System.Windows.Forms; using System.Drawing.Drawing2D; //添加引用 namespace WindowsFormsApplication1 { public partial class MainForm : Form { string imgfile = "tu.bmp";//要显示的图片 public MainForm() { InitializeComponent(); } private void MainForm_Load(object sender, EventArgs e) { this.drawimg();//重画窗体大小 } #region private void drawimg() { GraphicsPath graphPath = new GraphicsPath();//创建GraphicsPath Bitmap bmp = new Bitmap(imgfile);//加载图片 Rectangle rect = new Rectangle(); Color col = bmp.GetPixel(1, 1); //使用左上角的一点的颜色作为我们透明色 //Color col=Color.FromArgb(255,255,255);//也可以用指定颜色 for (int n = 0; n < bmp.Width; n++) //遍历图片所有列 { for (int m = 0; m < bmp.Height; m++) //遍历图片所有行 { if (bmp.GetPixel(n, m).Equals(col)) //如果是透明颜色,则不做处理 { } else { rect = new Rectangle(n, m, 1, 1); //创建非透明点区域 graphPath.AddRectangle(rect); //将非透明点区域加到graphPath } } } this.Region = new Region(graphPath); //将窗体区域设置为非透明点区域图像 } private void MainForm_Paint(object sender, PaintEventArgs e) { Graphics dc = e.Graphics; Bitmap bmp = new Bitmap(imgfile); dc.DrawImage(bmp, 0, 0, bmp.Width, bmp.Height); } #endregion } }
最后,按“F5”键测试程序,就可以看到如图4-13所示的个性窗体了。
4.7 无标题窗体拖动的两种方法
上面我们实现了个性化不规则窗体,这个时候整个窗体就是一个图形,没有了标题栏和关闭按钮等,是无法拖动和移动窗体的。
那我们如何拖动这样“秃头”的窗体呢?下面介绍两种方法。(完整代码示例位置:光盘
\code\ch04\4.6)
1.通过鼠标事件实现
在窗体类中加入如下代码:
private Point m_point = new Point(0, 0); //记录位置 private void MainForm_MouseDown(object sender, MouseEventArgs e) { } m_point = new Point(e.X, e.Y); //鼠标按下时记下位置坐标 private void MainForm_MouseMove(object sender, MouseEventArgs e) { if (e.Button == MouseButtons.Left) //按左键 { Point cp = Control.MousePosition; //得到鼠标坐标 //跟随鼠标移动 this.Location = new Point(cp.X - m_point.X, cp.Y - m_point.Y); } }
2.调用API实现
(1)添加引用:using System.Runtime.InteropServices;。
(2)引入API库文件:
[DllImport("user32.dll")] public static extern bool ReleaseCapture(); //为当前的应用程序释放鼠标捕获 [DllImport("user32.dll")] public static extern bool SendMessage(IntPtr hwnd, int wMsg, int wParam, int lParam);
(3)定义消息常量:
public const int WM_SYSCOMMAND = 0x0112;//单击窗口左上角那个图标时的系统消息 public const int SC_MOVE = 0xF010; //表示移动消息 public const int HTCAPTION = 0x0002; //表示鼠标在窗口标题栏时的系统消息
(4)添加 MouseDown 消息事件:
private void MainForm_MouseDown(object sender, MouseEventArgs e) { ReleaseCapture(); //释放鼠标 //向当前窗体发送消息,消息是移动+表示鼠标在标题栏上 SendMessage(this.Handle, WM_SYSCOMMAND, SC_MOVE + HTCAPTION, 0); }
通过以上两种方法就可以轻松拖动这个“秃头”窗体了。
4.8 让程序只启动一次——单实例运行
有时候我们不喜欢同一个程序同时启动两个实例,也就是说避免同时启动相同的应用程序。例如当两个或更多线程需要同时访问一个共享资源时,系统需要使用同步机制来确保一次只有一个线程使用该资源。Mutex是同步基元,它只向一个线程授予对共享资源的独占访问权。如果一个线程获取了互斥体,则该互斥体的第2个线程将被挂起,直到第1个线程释放该互斥体。
我们这里在程序启动时,请求一个互斥体,如果能获取对指定互斥的访问权,则继续运行程序,否则退出程序。
代码示例: (示例位置:光盘\code\ch04\4.8)
// <summary> // 应用程序的主入口点 // </summary> [STAThread] static void Main() { Application.EnableVisualStyles(); Application.SetCompatibleTextRenderingDefault(false); System.Threading.Mutex mutex = new System.Threading.Mutex(false, "SINGLE_INSTANCE_MUTEX"); if (!mutex.WaitOne(0, false)) //请求互斥体的所属权 { mutex.Close(); mutex = null; } if (mutex != null) { Application.Run(new Form1()); } else { MessageBox.Show("程序已经启动!"); } }
我们可以把Mutex看做一辆出租车,把线程看做乘客。乘客首先等车,然后上车,最后下车。当一个乘客在车上时,其他乘客就只有等他下车以后才可以上车。而线程与 Mutex对象的关系也正是如此,线程使用Mutex.WaitOne()方法等待Mutex对象被释放(请求互斥体的所属权),如果它等待的 Mutex 对象被释放了,它就自动拥有这个对象,直到它调用Mutex.ReleaseMutex()方法释放这个对象,而在此期间,其他想要获取这个 Mutex 对象的线程都只有等待。
4.9 实现系统托盘和热键呼出
平常我们在使用QQ的时候,QQ的主界面都是隐藏的,但是在右下角的任务栏,我们可以看到有个小图标,通过小图标当前QQ的状态,也可以通过热键(快捷键)调出QQ显示,这个小图标就叫系统托盘,我们本节就来实现在 Windows 右下角的系统托盘,并实现通过热键(快捷键)呼出功能。(完整代码示例位置:光盘\code\ch04\4.9)
1.实现系统托盘
(1)新建一个Windows 窗体应用程序。
(2)在当前窗体添加一个 contextMenuStrip1上下文菜单控件,用做任务栏显示时的右键菜单。并添加几个子菜单,如正常显示、隐藏、退出等。
(3)添加NotifyIcon1控件,用于显示任务栏图标。并设置 ContextMenuStrip属性等于contextMenuStrip1。
(4)当应用程序(窗体)启动时在任务栏显示图标。
private void Form1_Load(object sender, EventArgs e) { notifyIcon1.Icon = new System.Drawing.Icon("online.ico"); notifyIcon1.Visible = true; notifyIcon1.Text = "Online"; }
(5)让图标不停地变幻闪烁。
添加timer控件,并设置Interval间隔执行时间为1000(单位是毫秒)。添加timer1_Tick事件。当发生事件或需要闪烁时,可以设置timer1.Enabled = true; 让timer开始运行。
private void timer1_Tick(object sender, EventArgs e) { if (notifyIcon1.Text == "Online") { notifyIcon1.Icon = new System.Drawing.Icon("offline.ico"); notifyIcon1.Text = "Offline"; } else { notifyIcon1.Icon = new System.Drawing.Icon("online.ico"); notifyIcon1.Text = "Online"; } }
(6)双击系统托盘图标,显示正常窗体,添加notifyIcon1_MouseDoubleClick事件。
private void notifyIcon1_MouseDoubleClick(object sender, MouseEventArgs e) { this.Show(); this.Focus(); }
2.实现热键呼出
(1)设置热键通用类:Hotkey。
//声明委托 public delegate void HotkeyEventHandler(int HotKeyID); //<summary> //System wide hotkey wrapper. //</summary> public class Hotkey : System.Windows.Forms.IMessageFilter { System.Collections.Hashtable keyIDs = new System.Collections.Hashtable(); IntPtr hWnd; //<summary> //Occurs when a hotkey has been pressed. //</summary> public event HotkeyEventHandler OnHotkey; public enum KeyFlags { MOD_ALT = 0x1, MOD_CONTROL = 0x2, MOD_SHIFT = 0x4, MOD_WIN = 0x8 } //调用API [System.Runtime.InteropServices.DllImport("user32.dll")] public static extern UInt32 RegisterHotKey(IntPtr hWnd, UInt32 id, UInt32 fsModifiers, UInt32 vk); [System.Runtime.InteropServices.DllImport("user32.dll")] public static extern UInt32 UnregisterHotKey(IntPtr hWnd, UInt32 id); [System.Runtime.InteropServices.DllImport("kernel32.dll")] public static extern UInt32 GlobalAddAtom(String lpString); [System.Runtime.InteropServices.DllImport("kernel32.dll")] public static extern UInt32 GlobalDeleteAtom(UInt32 nAtom); //构造窗口句柄的热键 public Hotkey(IntPtr hWnd) { this.hWnd = hWnd; //添加消息筛选器以便在向目标传送Windows 消息时监视这些消息 System.Windows.Forms.Application.AddMessageFilter(this); } //注册一个系统热键 public int RegisterHotkey(System.Windows.Forms.Keys Key, KeyFlags keyflags) { UInt32 hotkeyid = GlobalAddAtom(System.Guid.NewGuid().ToString()); RegisterHotKey((IntPtr)hWnd, hotkeyid, (UInt32)keyflags, (UInt32)Key); keyIDs.Add(hotkeyid, hotkeyid); return (int)hotkeyid; } //撤销一个系统热键 public void UnregisterHotkeys() { System.Windows.Forms.Application.RemoveMessageFilter(this); foreach (UInt32 key in keyIDs.Values) { UnregisterHotKey(hWnd, key); GlobalDeleteAtom(key); } } public bool PreFilterMessage(ref System.Windows.Forms.Message m) { if (m.Msg == 0x312) /*WM_HOTKEY*/ { if (OnHotkey != null) { foreach (UInt32 key in keyIDs.Values) { if ((UInt32)m.WParam == key) { OnHotkey((int)m.WParam); return true; } } } } return false; } }
(2)声明热键对象。
Hotkey hotkey; int Hotkey1;//声明一个热键变量
(3)在窗体的构造函数中,创建并设置热键为“Ctrl+1”。
public Form1() { InitializeComponent(); //构造热键对象实例 hotkey = new Hotkey(this.Handle); //注册热键为“Ctrl+1” Hotkey1 = hotkey.RegisterHotkey(System.Windows.Forms.Keys.D1, Hotkey.KeyFlags.MOD_CONTROL); //设置热键事件 hotkey.OnHotkey += new HotkeyEventHandler(OnHotkey); }
(4)重载监控事件,捕获Form中按下何键。
protected override bool ProcessCmdKey(ref System.Windows.Forms.Message msg, System.Windows.Forms.Keys keyData) { if (keyData == Keys.A) { MessageBox.Show(keyData.ToString()); } return base.ProcessCmdKey(ref msg, keyData); }
(5)增加热键监控处理方法:
public void OnHotkey(int HotkeyID) { if (HotkeyID == Hotkey1) { this.Visible = true; } else { this.Visible = false; } }
运行效果如图4-15所示。

图4-15 实现系统托盘
单击“隐藏窗体”按钮,在右下角可以看到系统托盘,如图4-16所示。单击“托盘图标闪烁”按钮,小图标会循环变化,如图4-17所示。

图4-16 隐藏窗体

图4-17 托盘图标闪烁
如果窗体是隐藏的,则我们按下“Ctrl+1”组合键,窗体就自动跳出显示了,这就是热键呼出。通过这样的功能,我们可以很容易地实现一些基于隐藏服务的工具功能。
4.10 进程与多线程的区别
进程是指在系统中正在运行的一个应用程序,是分配资源的基本单位。例如,当你运行记事本程序时,就创建了一个用来容纳组成Notepad.exe的代码及其所需调用动态链接库的进程。每个进程均运行在其专用且受保护的地址空间内。因此,如果你同时运行记事本的两个复制,则该程序正在使用的数据在各自实例中是彼此独立的。在记事本的一个复制中将无法看到该程序的第2个实例打开的数据。
线程是比进程更小的能独立运行的基本单位。线程是系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元。对于操作系统而言,其调度单元是线程。一个进程至少包括一个线程,通常将该线程称为主线程。一个进程从主线程的执行开始进而创建一个或多个附加线程,就是所谓的基于多线程的多任务。多线程的每个线程轮流占用CPU 运行时间和资源,或者说把 CPU 时间划成片,每个片分给区别线程,这样每个线程轮流“挂起”和“唤醒”,由于时间片很小,所以给人感觉是同时运行的。
线程的状态如表4-1所示。
表4-1 线程的状态

实际上线程运行,而进程不运行,即进程是静态的,线程是动态的。两个进程彼此获得专用数据或内存的唯一途径就是通过协议来共享内存块。这是一种协作策略。
线程可以设定优先级,高优先级的线程可以安排在低优先级线程之前完成。一个应用程序可以通过使用线程中的方法setPriority(int)来设置线程的优先级大小。
趣味理解
进程相当于一幢楼,线程相当于这幢楼里面的的居民。一幢楼(进程)里可以有很多的居民(线程),但至少要有一个居民。
楼(进程)是静止不动的,而居民(线程)是动态运行的。
同一幢楼里面的居民可以共享楼里的物品。不同楼之间的居民无法共享对方的物品,除非通过协议来共享公共区域。
当要做一件工作的时候,一个居民做肯定不如多个居民做来得快,所以,多线程处理在一定程度上比单线程要快。因为每个线程各自独立去完成自己的任务。
进程与线程对照如表4-2所示。
表4-2 进程与线程对照表

4.11 创建多线程应用程序
有的时候我们开发了一个应用程序,当程序执行时整个程序就像死机一样,界面刷新尤其缓慢。那是因为应用程序在控制用户界面的线程上执行了非UI处理,会使应用程序的运行缓慢而迟钝。也就是说处理占用了UI线程的资源,导致UI线程处理缓慢。所以,我们需要通过增加一个线程,来让我们的处理操作从界面线程分离到单独的线程中去处理,这样就不会出现那样的问题了。在.NET和C#中编写一个多线程应用程序是非常容易的。即使那些从没有用 C#编写过多线程应用程序的初学者,只需通过以下这些简单的步骤就可以实现。(完整代码示例位置:光盘\code\ch04\4.11)
实现步骤:
(1)引用命名空间。
using System.Threading;
(2)在按钮事件里创建并启动一个新的线程。
Thread mythread; //声明线程对象 private void btnStart_Click(object sender, EventArgs e) { //创建一个线程,并指定线程处理函数 mythread = new Thread(new ThreadStart(WriteData)); mythread.Start(); //启动线程 } //<summary> //线程处理函数示例 //</summary> protected void WriteData() { for (int i = 0; i <= 10000; i++) { //做数据处理 } }
(3)暂停线程。
private void btnSleep_Click(object sender, EventArgs e) { Thread.Sleep(10000); //线程被阻止的毫秒数。指定零(0)以指示应挂起此线程以使其他等待线程能够执行 }
(4)停止线程。
private void btnStop_Click(object sender, EventArgs e) { if (mythread.IsAlive) { mythread.Abort(); } }
Abort方法用于永久地杀死一个线程。在调用Abort前一定要判断线程是否还激活。
(5)挂起和恢复线程。
private void btnContinue_Click(object sender, EventArgs e) { if (mythread.ThreadState == ThreadState.Running) { mythread.Suspend();//挂起线程 } if (mythread.ThreadState == ThreadState.Suspended) { mythread.Resume();//恢复线程 } }
在 .NET Framework 2.0版中,Thread.Suspend和Thread.Resume方法已标记为过时,并将从未来版本中移除。这里不再多讲。
(6)在线程中调用控件。
Windows 窗体体系结构对线程使用制定了严格的规则。如果只是编写单线程应用程序,则没必要知道这些规则,这是因为单线程的代码不可能违反这些规则。然而,一旦采用多线程,就需要理解 Windows 窗体中最重要的一条线程规则:除了极少数的例外情况,都不要在它的创建线程以外的线程中使用控件的任何成员。
如果确有需要,我们可以使用异步委托调用的方式来实现我们的功能:在新线程里调用主线程的控件。
//<summary> //线程处理函数示例 //</summary> protected void WriteData() { SetprogressBar1Max(10000); //设定进度条控件的最大值 for (int i = 0; i <= 10000; i++) { SetlblStatuText("当前是:" + i.ToString()); SetprogressBar1Val(i); //设定进度条控件的当前值 } } //定义委托 delegate void SetlblStatuCallback(string text); delegate void SetProBar1MaxCallback(int val); delegate void SetProBar1ValCallback(int val); //<summary> //设置Label控件的文本值 //</summary> public void SetlblStatuText(string text) { if (this.lblTip.InvokeRequired) { SetlblStatuCallback d = new SetlblStatuCallback(SetlblStatuText); this.Invoke(d, new object[] { text }); } else { this.lblTip.Text = text; } } //<summary> //设置进度控件的最大值 //</summary> public void SetprogressBar1Max(int val) { if (this.progressBar1.InvokeRequired) { SetProBar1MaxCallback d = new SetProBar1MaxCallback(SetprogressBar1Max); this.Invoke(d, new object[] { val }); } else { this.progressBar1.Maximum = val; } } //<summary> //设置进度控件的当前值 //</summary> public void SetprogressBar1Val(int val) { if (this.progressBar1.InvokeRequired) { SetProBar1ValCallback d = new SetProBar1ValCallback(SetprogressBar1Val); this.Invoke(d, new object[] { val }); } else { this.progressBar1.Value = val; } }
使用多线程代码可以使UI在执行耗时较长的任务时不会停止响应,从而显著提高应用程序的反应速度。异步委托调用是将执行速度缓慢的代码从 UI 线程迁移出来,是避免此类间歇性无响应的最简单方式。
4.12 WinForm开发常见问题
本节将为你讲述一些关于WinForm开发的常见问题。
4.12.1 如何设置运行时窗体的起始位置
设置窗体属性:StartPosition,有以下几种启动位置,如表4-3所示。
表4-3 窗体位置

4.12.2 如何使一个窗体在屏幕的最顶端
让程序窗体总在其他所有窗体最上面,只需设置窗体的TopMost=true;属性即可。
4.12.3 实现窗体渐显效果
窗体的渐显是指窗体在显示的时候,不是一下子就显示的,而是渐渐地显示的,当然,在Windows XP中已经可以通过设置支持这样的效果了,我们来看看如何通过程序自己实现这样的效果。
这里主要通过窗体的Opacity属性来实现,这个属性代表窗体的不透明度级别。
通过添加timer控件来实现:
private void Form1_Load(object sender, EventArgs e) { this.timer1.Enabled = true; this.Opacity = 0; } private void timer1_Tick(object sender, EventArgs e) { if (this.Opacity < 1) { this.Opacity = this.Opacity + 0.05; } else { this.timer1.Enabled = false; } }
4.12.4 设置窗口背景为渐变色
(1)添加引用:
using System.Drawing.Drawing2D;
(2)添加窗体的Paint事件,用颜色填充窗体区域:
private void Form1_Paint(object sender, System.Windows.Forms.PaintEventArgs e) { Graphics g = e.Graphics; Color FColor = Color.Blue; //蓝色 Color TColor = Color.Yellow; //黄色 Brush b = new LinearGradientBrush(this.ClientRectangle, FColor, TColor, LinearGradientMode.ForwardDiagonal); g.FillRectangle(b, this.ClientRectangle); }
(3)当窗体改变尺寸的时候,背景色没有按新尺寸被重新设置,所以需要添加 Resize事件:
private void Form1_Resize(object sender, EventArgs e) { this.Invalidate();//重绘窗体 }
运行效果如图4-18所示。

图4-18 渐变色窗体背景
4.12.5 模态窗口和非模态窗口
对话框一般分为两种类型:模态类型(modal)与非模态类型(modeless)。所谓模态对话框,就是指除非采取有效的关闭手段,用户的鼠标焦点或者输入光标将一直停留在其上的对话框上。非模态对话框则不会强制此种特性,用户可以在当前对话框及其他窗口间进行切换。
Form2 f2 = new Form2(); f2.Show(); //启动非模态对话框 f2.ShowDialog(); //启动模态对话框,其他窗口将无法操作
4.12.6 屏蔽窗口右上角的关闭
操作
单击右上角的“关闭”按钮或按“Alt+F4”组合键时不关闭窗口,而是最小化窗口。
方法1:在窗体类中重写OnClosing方法,处理关闭消息。
protected override void OnClosing(CancelEventArgs e) { if (this.Visible == true) { e.Cancel = true; this.WindowState = FormWindowState.Minimized; //Hide();//或其他自定义操作 } }
方法2:在窗体类中重写 WndProc 方法,处理 Windows 消息。
protected override void WndProc(ref Message m) { const int WM_SYSCOMMAND = 0x0112; const int SC_CLOSE = 0xF060; if (m.Msg == WM_SYSCOMMAND && (int)m.WParam == SC_CLOSE) { // 用户点关闭按钮时 this.WindowState = FormWindowState.Minimized; return; } base.WndProc(ref m); }
4.12.7 调用执行外部的程序
有时需要在我们的程序中调用系统的程序或启动第三方的可执行程序。
先引用命名空间:
using System.Diagnostics;
例如启动一个可执行程序:
Process proc = new Process(); proc.StartInfo.FileName = "test.exe";//注意路径 proc.StartInfo.Arguments = "";//运行参数 proc.StartInfo.WindowStyle = ProcessWindowStyle.Maximized;//启动窗口状态 proc.Start();
例如启动IE打开网址:
Process proc = new Process();//启动IE打开网址 Process.Start("IExplore.exe", "http://www.maticsoft.com");
(本节完整代码示例位置:光盘\code\ch04\4.12)
本章常见技术面试题
✧ 什么是MDI窗体?
✧ 窗体间如何传递数据?
✧ 进程与多线程有何区别?
✧ 什么是模态窗口?什么是非模态窗口?
常见面试技巧之经典问题巧回答
✧ 你为什么选择这份工作?
这是面试官用来测试应聘者对工作的理解度的问题,藉以了解求职者只是基于对工作的憧憬还是确实的兴趣来应征这份工作。由于软件开发工作属于相对单调枯燥的工作,从事该职业的人需要一定的“定性”,你的回答应以个人的兴趣配合工作内容特质,表现出高度的诚意,例如“个人比较喜欢开发这个职业,它具有一定的创造性,喜欢编程所带来的那种乐趣和成就感”等,这样才可以为自己铺下迈向成功之路。
✧ 你有什么业余爱好?
业余爱好能在一定程度上反映应聘者的性格、观念、心态,这是招聘单位提问的主要原因;最好不要说自己没有业余爱好;不要说自己有那些庸俗的、令人感觉不好的爱好;最好不要说自己仅限于读书、听音乐、上网,否则可能令面试官怀疑应聘者性格孤僻;最好能有一些户外的业余爱好来“点缀”你的形象。突出面试者的乐群性和协作能力。
这个问题一般也问得不多,在面试大学生时提问的概率高些。如果在面试有工作经验的人士的时候,提这个问题主要目的是为了消除面试者的紧张感,使其放松,这个问题本身没有什么特别意义。
本章小结
本章主要介绍了建立Windows的客户应用程序的基础知识,讲解了窗体的数据传递的几种方法,个性化窗体的实现,以及常见用户体验特效的制作方法。以实例的方式让读者体验了WinForm程序开发的方法和技巧,便于在实际的项目工作中得以应用。通过这一章的学习,读者了解了C/S和B/S在编程模式上的差别,为以后从事多种类型的项目开发奠定基础。