事件和回调函数
我们知道Win32 API创建的界面本身就是事件驱动的,Winform中也是如此。我们的窗口创建、销毁,按钮点击等操作,都可以注册对应的处理回调函数。
注册回调函数
事件用于通知应用程序或用户某个操作或状态发生了变化。例如,按钮点击、窗口加载、鼠标移动等都可以触发相应的事件。在C#中,事件是基于委托类型的特殊类成员,它允许对象将响应事件的代码(也被称为事件处理程序)与事件关联起来,同时事件对委托做了封装,外部代码只能订阅(+=)和退订(-=),不能直接触发事件。
在Visual Studio界面设计器中,我们选中一个控件后,可以在右侧属性面板中找到对应的事件列表(那个经典的闪电图标按钮)。

双击对应事件后,会自动创建并跳转到相关逻辑代码的编辑界面。
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show("按钮被点击了");
}
VisualStudio自动帮我们创建了一个空函数,我们编写了一些内容。我们可以看到函数上有两个参数:
sender:该对象其实就是事件发送控件,这里其实就是被点击的Button对象e:事件参数对象,其中包含事件的额外信息,比如鼠标的点击坐标,键盘的按键等
实际上,我们可以查看设计器自动生成的代码,它其实就是通过绑定委托的方式,向按钮对象注册了一个回调函数。
this.button1.Click += new System.EventHandler(this.button1_Click);
事件参数的类型
不同的事件使用不同类型的事件参数,它们并不能强制互相转换。例如,Click事件由EventHandler委托定义,其参数e就是普通的EventArgs,它不包含鼠标坐标信息,企图在Click事件中把e强转成MouseEventArgs获取坐标是行不通的。如果我们需要鼠标的点击坐标,应该使用MouseClick或MouseDown事件,它们的委托是MouseEventHandler,参数类型为MouseEventArgs,其中包含坐标和按键信息。
private void button1_MouseClick(object sender, MouseEventArgs e)
{
MessageBox.Show(string.Format("点击位置({0}, {1}),按键:{2}", e.X, e.Y, e.Button));
}
类似的对应关系还有:键盘事件KeyDown、KeyUp的参数是KeyEventArgs,KeyPress的参数是KeyPressEventArgs,拖放事件DragDrop的参数是DragEventArgs。订阅事件前,先看清事件委托的参数类型,就知道能从事件里拿到什么信息了。
鼠标事件
常用的鼠标事件包括:
MouseDown/MouseUp:鼠标按键按下 / 抬起MouseClick/MouseDoubleClick:单击 / 双击(要求按下抬起发生在控件区域内)MouseMove:鼠标在控件上移动,拖动逻辑通常配合MouseDown记录起点、MouseMove处理移动、MouseUp结束来实现MouseWheel:滚轮滚动,MouseEventArgs.Delta表示滚动量MouseEnter/MouseLeave/MouseHover:鼠标进入 / 离开 / 悬停
键盘事件
键盘事件有三个,按下按键时的触发顺序是:KeyDown -> KeyPress -> KeyUp。
KeyDown/KeyUp:按键物理按下和抬起,KeyEventArgs.KeyCode是Keys枚举,可以区分F1、方向键、Ctrl等功能键,还可以通过KeyEventArgs.Control、Shift、Alt判断组合键KeyPress:只针对能产生字符的按键触发,功能键不触发,KeyPressEventArgs.KeyChar直接给出字符,适合文本输入处理
默认情况下键盘事件先发给当前拥有焦点的控件,不过这会导致一个问题,如果界面上有按钮等控件抢占了焦点,窗体自身就收不到键盘事件了。解决办法是把窗体的KeyPreview属性设为true,这样键盘事件会先经过窗体,再传给焦点控件,我们就可以在窗体级别统一处理快捷键了,如果我们处理后不想让事件继续传递,可以采用类似如下写法。
private void MainForm_KeyDown(object sender, KeyEventArgs e)
{
if (e.Control && e.KeyCode == Keys.S)
{
SaveDocument();
e.Handled = true; // 标记事件已处理
e.SuppressKeyPress = true; // 阻止按键继续传给焦点控件
}
}
手动订阅与多控件共享处理函数
除了在设计器中双击生成,我们也可以在代码中手动订阅事件,这在动态创建控件时非常常用。
Button button = new Button { Text = "动态按钮" };
button.Click += button_Click; // 方法组语法
button.Click += (s, e) => MessageBox.Show("Hi"); // Lambda表达式
一个事件可以绑定多个处理函数,一个处理函数也可以同时服务多个控件。例如界面上有一排功能相似的按钮,我们可以让它们的Click事件都指向同一个处理函数,再通过sender区分是谁触发的。
private void ToolButton_Click(object sender, EventArgs e)
{
Button clicked = (Button)sender;
MessageBox.Show("你点击了:" + clicked.Text);
}
设计器生成的订阅代码只是普通的C#语句,如果控件被代码动态移除或重建,要注意事件订阅可能重复绑定导致处理函数执行多次,必要时记得先用-=退订再+=订阅。