写在前面
承接篇1。本篇拆解 WinForms 的代码组织核心:设计器生成的 .Designer.cs、partial class 机制、控件树、.resx 资源,以及常用控件和容器布局。
WinForms 开发的 80% 是"在 VS 里拖控件、设属性、挂事件",但这背后设计器替你生成了什么?理解它,你才能脱离设计器手写、排查布局 bug、做自定义控件。
一、partial class:设计器和你的代码各管一半
VS 设计器拖一个 Button 到 Form,生成两个文件:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| // Form1.Designer.cs —— 设计器生成,你别手改(改了下次设计器覆盖)
partial class Form1
{
private System.Windows.Forms.Button button1;
private void InitializeComponent()
{
this.button1 = new System.Windows.Forms.Button();
this.button1.Location = new System.Drawing.Point(100, 80);
this.button1.Name = "button1";
this.button1.Size = new System.Drawing.Size(75, 23);
this.button1.Text = "点我";
this.button1.Click += button1_Click; // 事件挂接也在设计器里
this.ClientSize = new System.Drawing.Size(300, 200);
this.Controls.Add(this.button1);
this.Name = "Form1";
this.Text = "Form1";
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
| // Form1.cs —— 你写的业务逻辑
public partial class Form1 : Form
{
public Form1()
{
InitializeComponent(); // 设计器生成的方法,必须调
}
private void button1_Click(object sender, EventArgs e)
{
MessageBox.Show("Clicked");
}
}
|
要点:
- 两个文件同一个类(partial)——编译器合并。设计器管布局(
.Designer.cs),你管逻辑(Form1.cs)。 InitializeComponent() 必须在构造函数里第一个调——它创建控件实例、设属性、挂事件、建控件树。不调,窗体一片空白。.Designer.cs 不要手改——下次在设计器里改动会覆盖。要改控件树,在设计器里改,或写到 Form1.cs。
记忆法:.Designer.cs = 设计器的"手稿",Form1.cs = 你的工位。两半拼成 Form1。
二、控件树:Controls 集合
每个 Control(包括 Form)有一个 Controls 集合,装它的子控件。整个界面是一棵树:
1
2
3
4
5
6
7
8
| Form (根)
├── MenuStrip (菜单)
├── ToolStrip (工具栏)
├── TableLayoutPanel (中间容器)
│ ├── Label
│ ├── TextBox
│ └── Button
└── StatusStrip (状态栏)
|
1
2
3
4
5
| // 代码里操作控件树
var panel = new TableLayoutPanel { Dock = DockStyle.Fill, ColumnCount = 2 };
panel.Controls.Add(new Label { Text = "姓名" }, 0, 0); // (col, row)
panel.Controls.Add(new TextBox(), 1, 0);
this.Controls.Add(panel); // 加到 Form
|
Controls.Add/Remove/Clear 改树结构。Dock(停靠)和 Anchor(锚定)决定控件随窗体缩放的行为——WinForms 布局的两大基石(第 4 篇会再讲)。- 父控件 Dispose 时会级联 Dispose 子控件(自动)。
三、常用控件一览
WinForms 内置控件丰富,覆盖典型桌面需求:
| 控件 | 用途 | 关键属性/事件 |
|---|
Form | 窗口 | Text/Size/DialogResult/FormClosing |
Button | 按钮 | Text/Click |
TextBox | 文本输入 | Text/ReadOnly/Multiline/TextChanged |
Label | 标签 | Text |
CheckBox / RadioButton | 选择 | Checked/CheckedChanged |
ComboBox | 下拉 | Items/SelectedIndex/SelectedValueChanged |
ListBox / CheckedListBox | 列表 | Items/SelectedIndexChanged |
DataGridView | 表格(数据绑定主力) | DataSource(第 5 篇详讲) |
PictureBox | 图片 | Image |
DateTimePicker | 日期 | Value |
TabControl | 选项卡 | TabPages |
ProgressBar | 进度 | Value/Maximum |
TreeView / ListView | 树/列表视图 | Nodes/Items |
四、容器与布局控件
WinForms 的布局靠容器 + Dock/Anchor,不像 WPF 那样有专门的布局系统(Panel 体系):
| 容器 | 布局方式 |
|---|
Panel | 空容器,子控件绝对坐标或 Dock |
GroupBox | 带标题的 Panel(分组) |
TabControl | 选项卡页 |
SplitContainer | 可拖动的分隔条,两块区域 |
TableLayoutPanel | 表格布局(行/列,百分比/绝对) |
FlowLayoutPanel | 流式布局(从左到右/从上到下自动排) |
1
2
3
4
5
| 布局两大基石:
Dock 停靠(Top/Bottom/Left/Right/Fill),子控件占满父的某侧
Anchor 锚定(Top/Bottom/Left/Right 组合),窗体缩放时控件边距固定
复杂布局组合:TableLayoutPanel/FlowLayoutPanel 做网格/流式,其余靠 Dock/Anchor
|
1
2
3
4
5
| // 典型布局:顶部菜单 + 工具栏 + 中间内容(Fill) + 底部状态栏
menuStrip.Dock = DockStyle.Top;
toolStrip.Dock = DockStyle.Top;
statusStrip.Dock = DockStyle.Bottom;
contentPanel.Dock = DockStyle.Fill; // Fill 要最后加(Z-order 规则)
|
Dock 的 Z-order 顺序很关键:先加的 Dock 控件占外层,Fill 要最后加,否则被其他 Dock 控件挤掉。设计器里"置于底层/顶层"或代码里 Controls.Add 顺序控制。
五、.resx 资源文件
Form1.resx 装窗体的资源:图标、本地化字符串、二进制(图片)等。XML 格式。
1
2
3
4
5
6
7
| <!-- Form1.resx 片段 -->
<data name="$this.Text" xml:space="preserve">
<value>我的窗体</value>
</data>
<data name="button1.Text" xml:space="preserve">
<value>点我</value>
</data>
|
- 本地化:为不同语言建
Form1.zh-CN.resx/Form1.en.resx,编译成卫星程序集,运行时按文化自动切换。 - 设计器属性:很多属性(
Text/Font/Image)实际存在 .resx 里,.Designer.cs 引用它。
1
2
| // 代码里访问项目级资源
var text = Properties.Resources.MyString;
|
六、控件生命周期与 Dispose
1
2
3
4
5
6
| 控件生命周期:
new Control() 构造(托管对象,还没 HWND)
Controls.Add 加入树
HandleCreated 第一次需要时创建 HWND(懒)
... 显示/交互 ...
Dispose() 释放(HWND + 托管资源)
|
关键点:
- HWND 懒创建:控件实际窗口句柄在第一次显示或访问
Handle 时才建。 - 必须 Dispose:控件持有非托管资源(HWND、GDI 对象),
Dispose 释放它们。Form 关闭时自动 Dispose 其控件树;手动创建的控件(没加到树)要自己 Dispose。 Pen/Brush/Bitmap/Font 这些 GDI+ 对象也非托管,用完要 Dispose(第 4 篇详讲)。
1
2
3
4
5
6
| // 典型坑:动态创建的控件忘了 Dispose → GDI 句柄泄漏
for (int i = 0; i < 100; i++)
{
var btn = new Button(); // 没加到 Controls,没人替它 Dispose
// ... 用完没 Dispose → 泄漏
}
|
七、设计时 vs 运行时
设计器在你的控件上跑 ComponentDesigner,会触发控件的某些代码。所以:
- 构造函数别干重活(设计器会 new 你的 Form,重活会拖慢设计器甚至让它崩)。
DesignMode 属性:控件在设计器里跑时为 true,可用来跳过运行时才该做的逻辑(查数据库、连网络)。
1
2
3
4
5
6
| public MyControl()
{
InitializeComponent();
if (!DesignMode) // 只在真正运行时才加载配置
LoadConfig();
}
|
注意:DesignMode 在构造函数里还不可靠(句柄未建),最稳的是在 Load 事件里查。
八、小结
1
2
3
4
5
6
7
| WinForms 代码组织核心:
✓ partial class:.Designer.cs(设计器管布局)+ Form1.cs(你管逻辑)
✓ InitializeComponent() 构造函数必调
✓ 控件树 = Controls 集合的嵌套
✓ 布局靠容器 + Dock/Anchor
✓ .resx 装资源和本地化
✓ 控件持有 HWND,必须 Dispose
|
下一篇:事件模型——WinForms 的老式 event、订阅/退订的讲究、没退订导致的内存泄漏陷阱。