C# Windows 窗体编程入门详解
基于Web的B/S架构应用程序近年来确实非常流行,B/S易于部署、易于维护的特点使Web应用程序开发得到了前所未有的发展。但是,Web应用程序的缺点是,它们有时不能提供丰富的用户体验以及对本机系统环境的控制和利用,例如刷新问题和长时间运行的计算的进度显示。所以在某些情况下仍然需要使用Windows程序来实现客户端功能。由于.Net平台的集成性,加上Winforms框架的良好支持下,Windows应用程序不再难以开发和部署。
通过学习 WinForms 框架的开发技术,你会发现很多乐趣,可以实现自己真正想要的各种 Web 无法实现的功能,实现各种桌面工具。从中领略开发带来的快乐和成就。
1 创建简单的 WinForm 项目
打开 Visual Studio,选择菜单「文件」→「新建项目」命令,在左边的「项目类型」面板中选择「Visual C#」选项,在右边的面板中选择「Windows 窗体应用程序」选项,单击「确定」按钮,创建一个 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,我们可以通过单击鼠标右键添加「应用程序配置文件」。
代码示例:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Forms;
namespace WindowsFormsApplication1
{
static class Program
{
//
// 应用程序的主入口点
//
[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 开发都变得轻而易举了。
2 创建 MDI 窗体应用
MDI 就是所谓的多文档界面,它是从 Windows 2.0 下的 Microsoft Excel 电子表格程序开始引入的,这是因为 Excel 电子表格用户有时需要同时操作多份表格,MDI 正好为这种操作提供了很大的方便,于是就产生了 MDI 程序。在 Windows 系统 3.1 版本中,MDI 得到了更大范围的应用。其中系统中的程序管理器和文件管理器都是 MDI 程序。C#是微软推出的下一代主流程序开发语言,也是一种功能十分强大的程序设计语言,正在受到越来越多的编程人员的喜欢。在 C#中,提供了为实现 MDI 程序设计的很多功能。
实现步骤:
(1)首先,创建一个 Windows 窗体应用程序(参看前文)。
(2)设定当前窗体是一个 MDI 窗体的容器(即 MDI 父窗体),因为只有如此才能够在此主窗体上面添加 MDI 子窗体。
可以在项目上单击鼠标右键,在弹出的快捷菜单中选择「添加窗体」命令,直接添加一个 MDI 父窗体。
通过这种方式添加的父窗体,系统自动为其添加了各种操作菜单和功能,简便了许多。
(3)在 MDI 父窗体实现增加一个子窗体。
Form childForm = new Form();
childForm.MdiParent = this; //this 表示本窗体为其父窗体
childForm.Text = 「窗口」 + childFormNumber++;
childForm.Show();
(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);//所有子窗体排列图标方式
}
(5)在「窗口」菜单下面显示所有已经打开的子窗体列表。
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 等事件中。
3 获取应用程序路径信息
桌面 Windows 程序开发,有时候需要读取当前目录下的文件,有时候需要在当前目录下创建文档;甚至有时候自升级也需要知道应用程序当前所在的目录,所以,获取应用程序路径既是一种常用知识点,也是一种重要的功能。
下面列出几种获取文件路径信息的方法。
代码示例:
//应用程序的可执行文件的路径
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 回车跳转控件焦点
如果你做过客服,你就会明白这个功能对她们而言是多么的重要,每天需要快速地录入客户反馈的信息,所以,录入速度是很重要的。如何让她们节省更多的时间,让她们以最快的速度,最少的操作时间来完成信息的录入成为很关键的因素,尽量避免用鼠标操作则是其关键流程之一。
代码示例:
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 顺序属性
}
}
这样,当我们按回车键时,就会自动从第一个文本框依次按顺序自动跳转,直到最后提交保存。
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();
}
2. 公有字段传递
(1)在 Form1 中定义 public 字段。
把private System.Windows.Forms.TextBox textBox1;,改为public System.Windows.Forms.TextBox textBox1; 。
或者定义一个公共字段,这样更符合面向对象的封装性。
//
// 公共字段
//
public string Msg
{
get
{
return textBox1.Text.Trim();
}
}
(2)Form2 增加一个公共属性和构造函数,用于接收传值。
public partial class Form2 : Form
{
public Form2()
{
InitializeComponent();
}
//
// 公共字段
//
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 对象实例的公共属性值
}
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();
}
这样避免了在子窗体直接调用父窗体对象,有效地降低了二者之间的依赖性及耦合性。父窗体改变后不需要重新编译子窗体。两个窗体都同时依赖于结果对象,更好地满足了面对对象的封装性和「依赖倒置」的原则。
6 实现个性化窗体界面
不知道大家以前是否用过播放器的主题皮肤,可以变幻各种形状的那种,感觉是不是很炫?相信每个编程爱好者都希望自己的程序不仅性能优越而且有一个美观的界面,一个区别于别人的程序的个性化的界面。然而以前烦琐的 API 调用和大量的代码使大家望而却步。现在好了,在 C#中通过少量的代码就可以实现不规则个性化窗体的制作。
先让我们看一下实现效果
我们来实现一个这样的效果,用自己的照片或一张图片,实现无边框个性化形状的窗体,而不是方方正正的那种窗体。飘在空中,是不是很酷?其实,用 C#实现不规则窗体是一件很容易的事情。
下面我就用一个简单的例子来讲述其制作过程。
(1)准备一张照片,对打算使其透明的地方使用白色背景(为了效果,最好是 BMP 位图)。
(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
}
}
最后,按「Ctrl+F5」键测试程序,就可以看到上面我们描述的效果了。
7 无标题窗体拖动的两种方法
上面我们实现了个性化不规则窗体,这个时候整个窗体就是一个图形,没有了标题栏和关闭按钮等,是无法拖动和移动窗体的。
那我们如何拖动这样「秃头」的窗体呢?下面介绍两种方法。
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);
}
通过以上两种方法就可以轻松拖动这个「秃头」窗体了。
8 让程序只启动一次——单实例运行
有时候我们不喜欢同一个程序同时启动两个实例,也就是说避免同时启动相同的应用程序。例如当两个或更多线程需要同时访问一个共享资源时,系统需要使用同步机制来确保一次只有一个线程使用该资源。Mutex 是同步基元,它只向一个线程授予对共享资源的独占访问权。如果一个线程获取了互斥体,则该互斥体的第 2 个线程将被挂起,直到第 1 个线程释放该互斥体。
我们这里在程序启动时,请求一个互斥体,如果能获取对指定互斥的访问权,则继续运行程序,否则退出程序。
代码示例:
//
// 应用程序的主入口点
//
[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 对象的线程都只有等待。
9 实现系统托盘和全局热键呼出
平常我们在使用 QQ 的时候,QQ 的主界面都是隐藏的,但是在右下角的任务栏,我们可以看到有个小图标,通过小图标当前 QQ 的状态,也可以通过热键(快捷键)调出 QQ 显示,这个小图标就叫系统托盘,我们就来实现在 Windows 右下角的系统托盘,并实现通过热键(快捷键)呼出功能。
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);
//
//System wide hotkey wrapper.
//
public class Hotkey : System.Windows.Forms.IMessageFilter
{
System.Collections.Hashtable keyIDs = new System.Collections.Hashtable();
IntPtr hWnd;
//
//Occurs when a hotkey has been pressed.
//
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;
}
}
单击「隐藏窗体」按钮,在右下角可以看到系统托盘。单击「托盘图标闪烁」按钮,小图标会循环变化。
如果窗体是隐藏的,则我们按下「Ctrl+1」组合键,窗体就自动跳出显示了,这就是全局热键呼出。通过这样的功能,我们可以很容易地实现一些基于隐藏服务的工具功能。
10 进程与多线程的区别
进程是指在系统中正在运行的一个应用程序,是分配资源的基本单位。例如,当你运行记事本程序时,就创建了一个用来容纳组成 Notepad.exe 的代码及其所需调用动态链接库的进程。每个进程均运行在其专用且受保护的地址空间内。因此,如果你同时运行记事本的两个复制,则该程序正在使用的数据在各自实例中是彼此独立的。在记事本的一个复制中将无法看到该程序的第 2 个实例打开的数据。
线程是比进程更小的能独立运行的基本单位。线程是系统分配处理器时间资源的基本单元,或者说进程之内独立执行的一个单元。对于操作系统而言,其调度单元是线程。一个进程至少包括一个线程,通常将该线程称为主线程。一个进程从主线程的执行开始进而创建一个或多个附加线程,就是所谓的基于多线程的多任务。多线程的每个线程轮流占用 CPU 运行时间和资源,或者说把 CPU 时间划成片,每个片分给区别线程,这样每个线程轮流「挂起」和「唤醒」,由于时间片很小,所以给人感觉是同时运行的。
实际上线程运行,而进程“不运行”,即进程相对线程而言是“静态的”,而线程则是动态运行的。两个进程彼此获得专用数据或内存的唯一途径就是通过协议来共享内存块。这是一种协作策略。
线程可以设定优先级,高优先级的线程可以安排在低优先级线程之前完成。一个应用程序可以通过使用线程中的方法 setPriority(int)来设置线程的优先级大小。
进程相当于一幢楼,线程相当于这幢楼里面的的居民。一幢楼(进程)里可以有很多的居民(线程),但至少要有一个居民。
楼(进程)是静止不动的,而居民(线程)是动态运行的。
同一幢楼里面的居民可以共享楼里的物品。不同楼之间的居民无法共享对方的物品,除非通过协议来共享公共区域。
当要做一件工作的时候,一个居民做肯定不如多个居民做来得快,所以,多线程处理在一定程度上比单线程要快。因为每个线程各自独立去完成自己的任务。
11 创建多线程应用程序
有的时候我们开发了一个应用程序,当程序执行时整个程序就像死机一样,界面刷新尤其缓慢。那是因为应用程序在控制用户界面的线程上执行了非 UI 处理,会使应用程序的运行缓慢而迟钝。也就是说处理占用了 UI 线程的资源,导致 UI 线程处理缓慢。所以,我们需要通过增加一个线程,来让我们的处理操作从界面线程分离到单独的线程中去处理,这样就不会出现那样的问题了。在.NET 和 C#中编写一个多线程应用程序是非常容易的。即使那些从没有用 C#编写过多线程应用程序的初学者,只需通过以下这些简单的步骤就可以实现。
实现步骤:
(1)引用命名空间。
using System.Threading;
(2)在按钮事件里创建并启动一个新的线程。
Thread mythread; //声明线程对象
private void btnStart_Click(object sender, EventArgs e)
{
//创建一个线程,并指定线程处理函数
mythread = new Thread(new ThreadStart(WriteData));
mythread.Start(); //启动线程
}
//
//线程处理函数示例
//
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 窗体中最重要的一条线程规则:除了极少数的例外情况,都不要在它的创建线程以外的线程中使用控件的任何成员。
如果确有需要,我们可以使用异步委托调用的方式来实现我们的功能:在新线程里调用主线程的控件。
//
//线程处理函数示例
//
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);
//
//设置 Label 控件的文本值
//
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;
}
}
//
//设置进度控件的最大值
//
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;
}
}
//
//设置进度控件的当前值
//
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 线程迁移出来,是避免此类间歇性无响应的最简单方式。
12 WinForm 开发常见问题
1. 如何设置运行时窗体的起始位置
设置窗体属性:StartPosition,有以下几种启动位置:
CenterScreen:窗体将在屏幕的中心显示。
WindowsDefaultLocation:窗体将根据 Windows 的默认规则显示在屏幕上。在不同版本的 Windows 中,此选项的行为可能会有所不同。
CenterParent:窗体将在其父窗体的中心显示。如果没有父窗体,则它将以屏幕的中心为中心。
默认情况下,StartPosition属性设置为WindowsDefaultLocation。
可以在窗体的构造函数或 Load 事件处理程序中设置 StartPosition 属性。
例如,在构造函数中设置StartPosition属性为CenterScreen,可以使用以下代码:
public Form1()
{
InitializeComponent();
this.StartPosition = FormStartPosition.CenterScreen;
}
2. 如何使一个窗体在屏幕的最顶端
让程序窗体总在其他所有窗体最上面,只需设置窗体的TopMost=true;属性即可。
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. 设置窗口背景为渐变色
(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();//重绘窗体
}
5. 模态窗口和非模态窗口
对话框一般分为两种类型:模态类型(modal)与非模态类型(modeless)。所谓模态对话框,就是指除非采取有效的关闭手段,用户的鼠标焦点或者输入光标将一直停留在其上的对话框上。非模态对话框则不会强制此种特性,用户可以在当前对话框及其他窗口间进行切换。
Form2 f2 = new Form2();
f2.Show(); //启动非模态对话框
f2.ShowDialog(); //启动模态对话框,其他窗口将无法操作
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);
}
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」);
学习参考文档:
https://learn.microsoft.com/zh-cn/dotnet/desktop/winforms/getting-started-with-windows-forms?view=netframeworkdesktop-4.8&viewFallbackFrom=netframeworkdesktop-4.8