跳转至

常用控件4

菜单栏

在 WinForms 中,菜单栏由 MenuStrip 控件表示,菜单项是 ToolStripMenuItem 对象。

通常,一个标准的窗口(Form)会包含一个 MenuStrip

using System.Windows.Forms;
using System.Drawing;

public class MyWindow : Form
{
    public MyWindow()
    {
        this.Size = new Size(600, 200);

        // 创建菜单栏
        MenuStrip menuStrip = new MenuStrip();

        // --- 一级菜单 ---
        ToolStripMenuItem fileMenu = new ToolStripMenuItem("文件");
        ToolStripMenuItem editMenu = new ToolStripMenuItem("编辑");
        ToolStripMenuItem helpMenu = new ToolStripMenuItem("帮助");

        // --- 文件菜单的子项 ---
        ToolStripMenuItem addAction = new ToolStripMenuItem("添加", Image.FromFile("folder.png"));
        addAction.Click += new EventHandler(AddAction_Click);
        fileMenu.DropDownItems.Add(addAction);

        fileMenu.DropDownItems.Add(new ToolStripSeparator()); // 分隔符

        ToolStripMenuItem deleteAction = new ToolStripMenuItem("删除");
        deleteAction.Click += new EventHandler(DeleteAction_Click);
        fileMenu.DropDownItems.Add(deleteAction);

        // --- 编辑菜单的子项 (二级菜单) ---
        ToolStripMenuItem insertMenu = new ToolStripMenuItem("插入");
        editMenu.DropDownItems.Add(insertMenu);

        ToolStripMenuItem insertChartAction = new ToolStripMenuItem("插入图表");
        insertMenu.DropDownItems.Add(insertChartAction);

        // 将一级菜单添加到菜单栏
        menuStrip.Items.Add(fileMenu);
        menuStrip.Items.Add(editMenu);
        menuStrip.Items.Add(helpMenu);

        // 将菜单栏设置为主菜单
        this.MainMenuStrip = menuStrip;
        this.Controls.Add(menuStrip);
    }

    private void AddAction_Click(object sender, EventArgs e)
    {
        Console.WriteLine("添加被点击");
    }

    private void DeleteAction_Click(object sender, EventArgs e)
    {
        Console.WriteLine("删除被点击");
    }
}

菜单项被点击时会触发 Click 事件。

工具栏

工具栏(Toolbar)通常位于菜单栏下方,提供对常用功能的快速访问。在 WinForms 中,工具栏由 ToolStrip 控件表示。

ToolStrip 是一个灵活的容器,它可以承载多种类型的项(ToolStripItem),而不仅仅是按钮。

常用工具栏项

ToolStripItems 集合可以添加以下常见的控件:

  • ToolStripButton: 标准的工具栏按钮,可以显示文本、图像或两者结合。
  • ToolStripLabel: 用于显示静态文本或图像的标签。
  • ToolStripSeparator: 一条垂直线,用于在视觉上对工具栏项进行分组。
  • ToolStripDropDownButton: 一个带有下拉箭头的按钮,点击后可以显示一个下拉菜单(可以关联一个 ContextMenuStrip)。
  • ToolStripSplitButton: 结合了标准按钮和下拉按钮的功能。它有一个主按钮区域用于执行默认操作,以及一个下拉箭头用于显示其他相关操作。
  • ToolStripComboBox: 在工具栏中嵌入一个下拉组合框。
  • ToolStripTextBox: 在工具栏中嵌入一个文本输入框。
  • ToolStripProgressBar: 在工具栏中显示一个进度条,通常用于指示长时间运行任务的进度。

创建和使用工具栏

可以通过代码或 Visual Studio 设计器来创建和配置工具栏。


下面的代码演示了如何创建一个包含多种项的工具栏,并将其添加到窗体中。

// 1. 创建工具栏容器
ToolStrip toolStrip = new ToolStrip();
toolStrip.GripStyle = ToolStripGripStyle.Hidden; // 可以隐藏用于移动工具栏的手柄

// 2. 创建并配置工具栏项

// 创建 "添加" 按钮
// 假设项目中有名为 "add.png" 的图像资源
ToolStripButton addButton = new ToolStripButton("添加", Image.FromFile("add.png"));
addButton.ToolTipText = "添加新项目"; // 鼠标悬停时显示的提示
addButton.DisplayStyle = ToolStripItemDisplayStyle.ImageAndText; // 显示图像和文本
addButton.Click += new EventHandler(AddAction_Click); // 关联点击事件

// 创建 "删除" 按钮
ToolStripButton deleteButton = new ToolStripButton("删除", Image.FromFile("delete.png"));
deleteButton.ToolTipText = "删除选中项目";
deleteButton.DisplayStyle = ToolStripItemDisplayStyle.Image; // 只显示图像
deleteButton.Click += new EventHandler(DeleteAction_Click);

// 创建分隔符
ToolStripSeparator separator = new ToolStripSeparator();

// 创建一个下拉菜单按钮
ToolStripDropDownButton optionsButton = new ToolStripDropDownButton("选项", Image.FromFile("options.png"));
optionsButton.ToolTipText = "更多选项";

// 为下拉按钮创建菜单项
ToolStripMenuItem option1 = new ToolStripMenuItem("选项 1");
option1.Click += (sender, e) => { /* 处理选项1点击 */ };
ToolStripMenuItem option2 = new ToolStripMenuItem("选项 2");
option2.Click += (sender, e) => { /* 处理选项2点击 */ };
optionsButton.DropDownItems.Add(option1);
optionsButton.DropDownItems.Add(option2);


// 3. 将项添加到工具栏
toolStrip.Items.Add(addButton);
toolStrip.Items.Add(deleteButton);
toolStrip.Items.Add(separator);
toolStrip.Items.Add(optionsButton);

// ... 可以添加其他类型的项 ...

// 4. 将工具栏添加到窗体控件集合中
this.Controls.Add(toolStrip);

// 假设这是按钮的事件处理器
private void AddAction_Click(object sender, EventArgs e)
{
    MessageBox.Show("点击了“添加”按钮");
}

private void DeleteAction_Click(object sender, EventArgs e)
{
    MessageBox.Show("点击了“删除”按钮");
}

设计器与事件共享

在 Visual Studio 的窗体设计器中,可以更直观地完成以上操作:

  1. 从 "工具箱" 中将 ToolStrip 控件拖拽到窗体上。
  2. 使用 ToolStrip 右上角的小箭头或 Items 集合编辑器来添加和配置 ToolStripButtonToolStripSeparator 等项。
  3. 在 "属性" 窗口中设置每个项的 TextImageDisplayStyleToolTipText 等属性。
  4. 双击工具栏上的按钮,会自动生成 Click 事件处理器。

功能复用: 一个常见的做法是让菜单项(ToolStripMenuItem)和工具栏按钮(ToolStripButton)执行相同的操作。为了避免代码重复,可以让它们的 Click 事件指向同一个事件处理方法。

例如,如果已经有了一个名为 SaveFile_Click 的方法用于处理 "文件" -> "保存" 菜单的点击事件,你可以在设计器中选中 "保存" 工具栏按钮,然后在 "属性" 窗口的 "事件" 选项卡中,找到 Click 事件,并从下拉列表中选择现有的 SaveFile_Click 方法。这样,无论用户点击菜单项还是工具栏按钮,都会调用相同的方法。

状态栏

状态栏由 StatusStrip 控件表示,通常位于窗口底部,用于显示状态信息。

// 创建状态栏
StatusStrip statusStrip = new StatusStrip();

// 添加一个标签到状态栏
ToolStripStatusLabel statusLabel = new ToolStripStatusLabel();
statusStrip.Items.Add(statusLabel);

this.Controls.Add(statusStrip);

// 更新状态栏文本
statusLabel.Text = "文件已打开";

提示框

System.Windows.Forms.MessageBox 类可以方便地显示一个预设样式的对话框,用于向用户显示信息或获取用户的简单选择(是/否,确定/取消等)。

最常用的方法是 MessageBox.Show(),它有多个重载版本。我们通常使用以下参数:

  • text: 对话框中显示的主要信息。
  • caption: 对话框窗口的标题。
  • buttons: 指定显示哪些按钮,是 MessageBoxButtons 枚举中的值,如 OK, YesNo, YesNoCancel
  • icon: 指定显示哪个图标,是 MessageBoxIcon 枚举中的值,如 Information, Warning, Error, Question

信息、警告和错误提示

这类对话框通常用于单向通知,用户看完后点击“确定”即可。

  • 信息提示 (Information)

用于告知用户操作已成功或提供一些中性信息。

MessageBox.Show(
    "操作成功,请继续下一步操作。", // 提示信息
    "操作成功",                     // 标题
    MessageBoxButtons.OK,           // 只显示“确定”按钮
    MessageBoxIcon.Information      // 显示“信息”图标
);
  • 警告 (Warning)

用于提醒用户某个操作可能存在风险或不符合预期。

MessageBox.Show(
    "阅读客户协议必须超过1分钟。", 
    "警告", 
    MessageBoxButtons.OK, 
    MessageBoxIcon.Warning
);
  • 错误报告 (Error)

用于告知用户发生了错误,无法继续执行。

MessageBox.Show(
    "请选择数据存储路径!", 
    "错误", 
    MessageBoxButtons.OK, 
    MessageBoxIcon.Error
);

获取用户选择

这类对话框通常用于需要用户做出选择的场景,比如确认一个危险操作。

  • 确认继续 (Question)

MessageBox.Show() 方法会返回一个 DialogResult 枚举值,表示用户点击了哪个按钮。我们可以通过检查这个返回值来执行相应的逻辑。

// 弹出提问对话框,包含“是”和“否”两个按钮
DialogResult choice = MessageBox.Show(
    "确定要删除本文件吗?此操作不可恢复。", // 提示信息
    "请确认",                               // 标题
    MessageBoxButtons.YesNo,                // 显示“是”和“否”按钮
    MessageBoxIcon.Question                 // 显示“问题”图标
);

// 检查用户的选择
if (choice == DialogResult.Yes)
{
    // 用户点击了“是”
    Console.WriteLine("用户选择删除文件。");
    // 在此添加删除文件的代码...
}
else
{
    // 用户点击了“否”或关闭了对话框
    Console.WriteLine("用户取消了删除操作。");
}

注意:当对话框包含“是”和“否”按钮时,如果用户点击窗口右上角的关闭按钮 (X),其行为等同于点击“否”按钮。

输入对话框

与某些 UI 框架不同,WinForms 没有内置一个现成的输入对话框(如 Qt 的 QInputDialog)。不过,我们有两种常用方法来实现这个功能:

  1. 简单方法: 引用 Microsoft.VisualBasic 库,使用其提供的 Interaction.InputBox
  2. 灵活方法: 创建一个自定义的 Form 作为对话框。

方法一:使用 VB 的 InputBox

这是最快的方法,适用于只需要获取一个简单文本输入的场景。

优点: - 代码非常简单,一行调用即可。

缺点: - 需要额外引用 Microsoft.VisualBasic.dll。 - 对话框的样式和功能非常有限,无法自定义。

使用步骤:

  1. 添加引用: 在你的项目解决方案中,右键点击“引用”或“依赖项”,选择“添加引用”,然后从程序集列表中找到并勾选 Microsoft.VisualBasic

  2. 调用代码:

    using Microsoft.VisualBasic;
    
    // ...
    
    // 调用 InputBox
    // 参数: (提示信息, 标题, 默认值)
    string result = Interaction.InputBox("请输入新的目录名称:", "创建目录", "新建文件夹");
    
    // 检查用户是否输入了内容
    // 如果用户点击“取消”或关闭对话框,返回的是空字符串 ""
    if (!string.IsNullOrEmpty(result))
    {
        // 用户点击了“确定”并且输入了内容
        Console.WriteLine($"用户输入: {result}");
        // 在此处理用户输入...
    }
    else
    {
        // 用户点击了“取消”或没有输入任何内容
        Console.WriteLine("用户取消了操作。");
    }
    

方法二:创建自定义对话框

当你需要更复杂的布局、验证逻辑或自定义外观时,最佳选择是创建一个自己的对话框窗口。

优点: - 完全的灵活性,可以设计任何你需要的界面和逻辑。 - 无需额外引用。

缺点: - 需要编写更多的代码。

实现步骤:

  1. 创建自定义窗体: 创建一个新的 Form,例如命名为 InputDialog。在上面放置一个 Label (用于提示), 一个 TextBox (用于输入), 和两个 Button (一个“确定”,一个“取消”)。

  2. 设置按钮的 DialogResult:

    • 选中“确定”按钮,在属性窗口中将其 DialogResult 属性设置为 OK
    • 选中“取消”按钮,将其 DialogResult 属性设置为 Cancel
    • 这样做之后,当用户点击这些按钮时,窗体将自动关闭并返回对应的 DialogResult 值。
  3. 提供一个公共属性来获取输入值:

    下面是 InputDialog 窗体的完整代码示例:

    // InputDialog.cs
    using System.Windows.Forms;
    using System.Drawing;
    
    public class InputDialog : Form
    {
        private Label label;
        private TextBox textBox;
        private Button okButton;
        private Button cancelButton;
    
        // 公共属性,用于从外部获取用户输入的值
        public string InputText { get; private set; }
    
        public InputDialog(string prompt, string title, string defaultValue = "")
        {
            this.Text = title;
            this.FormBorderStyle = FormBorderStyle.FixedDialog;
            this.StartPosition = FormStartPosition.CenterParent;
            this.ClientSize = new Size(300, 120);
            this.MaximizeBox = false;
            this.MinimizeBox = false;
    
            label = new Label() { Left = 20, Top = 20, Text = prompt, AutoSize = true };
            textBox = new TextBox() { Left = 20, Top = 45, Width = 260, Text = defaultValue };
            okButton = new Button() { Text = "确定", Left = 130, Top = 80, Width = 80 };
            cancelButton = new Button() { Text = "取消", Left = 220, Top = 80, Width = 80 };
    
            // 设置按钮的 DialogResult
            okButton.DialogResult = DialogResult.OK;
            cancelButton.DialogResult = DialogResult.Cancel;
    
            // 确定按钮点击时,保存输入值
            okButton.Click += (sender, e) => {
                this.InputText = textBox.Text;
            };
    
            this.Controls.Add(label);
            this.Controls.Add(textBox);
            this.Controls.Add(okButton);
            this.Controls.Add(cancelButton);
    
            // 设置窗体的 AcceptButton 和 CancelButton
            this.AcceptButton = okButton;
            this.CancelButton = cancelButton;
        }
    }
    
  4. 如何调用自定义对话框:

    在主窗体中,像下面这样创建和显示对话框:

    // 在主窗体的代码中
    
    // 创建对话框实例
    var dialog = new InputDialog("请输入您的姓名:", "信息录入");
    
    // 使用 ShowDialog() 以模态方式显示对话框
    // 程序会在此暂停,直到对话框关闭
    DialogResult result = dialog.ShowDialog();
    
    // 检查对话框的返回结果
    if (result == DialogResult.OK)
    {
        // 用户点击了“确定”,通过公共属性获取输入值
        string userInput = dialog.InputText;
        MessageBox.Show($"您好, {userInput}!");
    }
    else
    {
        MessageBox.Show("您取消了输入。");
    }
    

总结

  • 若只需快速、简单地获取一行文本,Interaction.InputBox 是个不错的选择。
  • 对于其他任何需要自定义UI、验证或更复杂交互的场景,都应该创建一个自定义的 Form 对话框。

剪贴板

使用静态类 System.Windows.Forms.Clipboard 来访问系统剪贴板。

// 获取剪贴板文本
string text = Clipboard.GetText();

// 设置剪贴板文本
Clipboard.SetText("新的文本内容");

树控件

System.Windows.Forms.TreeView 控件用于显示节点的分层集合,每个节点都可以包含子节点。它是文件系统浏览器、组织结构图等应用的理想选择。树中的每个节点都是一个 TreeNode 对象。

一、基本用法

你可以通过代码动态地向 TreeViewNodes 集合中添加、删除和修改 TreeNode

// 创建一个 TreeView 实例
TreeView treeView = new TreeView();
treeView.Dock = DockStyle.Fill; // 填充父容器

// --- 添加节点 ---

// 添加根节点
TreeNode rootNode1 = new TreeNode("C:\\");
treeView.Nodes.Add(rootNode1);

// 添加子节点
rootNode1.Nodes.Add(new TreeNode("Program Files"));
TreeNode usersNode = new TreeNode("Users");
rootNode1.Nodes.Add(usersNode);

// 在子节点下添加更深层次的节点
usersNode.Nodes.Add(new TreeNode("Public"));
usersNode.Nodes.Add(new TreeNode("Default"));


// 添加另一个根节点
TreeNode rootNode2 = new TreeNode("D:\\");
treeView.Nodes.Add(rootNode2);

// 默认展开某个节点
rootNode1.Expand();


你可以通过 ImageList 控件为 TreeView 的节点设置图标。

  1. 创建一个 ImageList 并向其中添加图片。
  2. TreeViewImageList 属性指向这个 ImageList 实例。
  3. 设置每个 TreeNodeImageIndexSelectedImageIndex 属性。ImageIndex 是节点平时显示的图标索引,SelectedImageIndex 是节点被选中时显示的图标索引。
// 1. 创建 ImageList 并添加图标
ImageList imageList = new ImageList();
imageList.Images.Add(Image.FromFile("folder_closed.png")); // 索引 0: 文件夹关闭
imageList.Images.Add(Image.FromFile("folder_open.png"));   // 索引 1: 文件夹打开

// 2. 关联 TreeView
treeView.ImageList = imageList;

// 3. 为节点设置图标
TreeNode nodeWithIcon = new TreeNode("有图标的节点");
nodeWithIcon.ImageIndex = 0;           // 默认图标
nodeWithIcon.SelectedImageIndex = 1; // 选中时的图标
treeView.Nodes.Add(nodeWithIcon);

二、常用操作与事件

响应节点选择

当用户选择一个新节点后,会触发 AfterSelect 事件。这是最常用的事件之一。

treeView.AfterSelect += (sender, e) => {
    // e.Node 是当前被选中的节点
    if (e.Node != null)
    {
        // 获取节点文本和层级
        string nodeText = e.Node.Text;
        int nodeLevel = e.Node.Level; // 根节点层级为 0
        MessageBox.Show($"你选择了: '{nodeText}',在第 {nodeLevel} 层。");
    }
};

节点复选框

TreeViewCheckBoxes 属性设置为 true 可以在每个节点旁边显示一个复选框。你可以遍历所有节点来检查哪些被选中。

treeView.CheckBoxes = true;

// 遍历并检查节点勾选状态的方法
private void CheckNodes(TreeNodeCollection nodes)
{
    foreach (TreeNode node in nodes)
    {
        if (node.Checked)
        {
            Console.WriteLine($"节点 '{node.Text}' 已被勾选。");
        }
        // 递归检查子节点
        if (node.Nodes.Count > 0)
        {
            CheckNodes(node.Nodes);
        }
    }
}

// 在某个事件(如按钮点击)中调用
// CheckNodes(treeView.Nodes);
注意: 勾选父节点不会自动勾选/取消勾选所有子节点,需要自己通过 AfterCheck 事件来实现联动。

编辑节点标签

要允许用户编辑节点的文本,需将 LabelEdit 属性设为 true。你可以通过 BeforeLabelEditAfterLabelEdit 事件来控制编辑过程。

treeView.LabelEdit = true;

// 在编辑开始前触发,可以取消编辑
treeView.BeforeLabelEdit += (sender, e) => {
    // 例如,禁止编辑根节点
    if (e.Node.Parent == null)
    {
        e.CancelEdit = true; // 取消编辑
        MessageBox.Show("不能编辑根节点!");
    }
};

// 在编辑结束后触发,可以验证输入
treeView.AfterLabelEdit += (sender, e) => {
    // e.Label 是编辑后的新文本
    if (string.IsNullOrWhiteSpace(e.Label))
    {
        e.CancelEdit = true; // 新文本无效,撤销编辑
        MessageBox.Show("节点名称不能为空!");
        e.Node.BeginEdit(); // 让用户重新编辑
    }
    else
    {
        // 编辑成功,e.Label 会自动应用到 e.Node.Text
        Console.WriteLine($"节点已重命名为: {e.Label}");
    }
};

右键上下文菜单

通过处理 NodeMouseClick 事件,可以方便地在用户右键点击节点时显示一个 ContextMenuStrip

// 假设你已经创建了一个 ContextMenuStrip 名为 contextMenuForNode
// 并且在其中添加了一些 ToolStripMenuItem

treeView.NodeMouseClick += (sender, e) => {
    // 如果是鼠标右键点击
    if (e.Button == MouseButtons.Right)
    {
        // 选中被右键点击的节点
        treeView.SelectedNode = e.Node;
        // 在鼠标位置显示上下文菜单
        contextMenuForNode.Show(treeView, e.Location);
    }
};

为节点附加自定义数据

TreeNodeTag 属性是 object 类型,可以让你将任何自定义数据对象与节点关联起来。

public class MyData { public int ID; public string Info; }

// 创建数据对象并附加到节点
TreeNode dataNode = new TreeNode("显示给用户的文本");
dataNode.Tag = new MyData { ID = 101, Info = "这是附加的详细信息" };
treeView.Nodes.Add(dataNode);

// 在事件中获取并使用数据
treeView.AfterSelect += (sender, e) => {
    if (e.Node != null && e.Node.Tag is MyData data)
    {
        // 将 Tag 转换回 MyData 类型
        MessageBox.Show($"节点ID: {data.ID}, 信息: {data.Info}");
    }
};

三、高级功能:拖放节点

TreeView 支持节点拖放,可以用来重新排列节点。

treeView.AllowDrop = true; // 允许接收拖放

// 1. 当开始拖拽一个节点时触发
treeView.ItemDrag += (sender, e) => {
    // 开始拖放操作,数据为被拖拽的节点
    DoDragDrop(e.Item, DragDropEffects.Move);
};

// 2. 当拖拽物进入控件范围时触发
treeView.DragEnter += (sender, e) => {
    // 检查拖拽的数据是否是 TreeNode
    if (e.Data.GetDataPresent(typeof(TreeNode)))
    {
        e.Effect = DragDropEffects.Move; // 显示“移动”光标
    }
    else
    {
        e.Effect = DragDropEffects.None;
    }
};

// 3. 当拖拽物在控件上移动时触发
treeView.DragOver += (sender, e) => {
    // 获取鼠标下的目标节点
    Point targetPoint = treeView.PointToClient(new Point(e.X, e.Y));
    TreeNode targetNode = treeView.GetNodeAt(targetPoint);
    // 可以在此添加逻辑,如自动展开目标节点
    // targetNode?.Expand();
};

// 4. 当拖放操作完成时触发
treeView.DragDrop += (sender, e) => {
    // 获取被拖拽的源节点
    TreeNode draggedNode = (TreeNode)e.Data.GetData(typeof(TreeNode));

    // 获取鼠标下的目标节点
    Point targetPoint = treeView.PointToClient(new Point(e.X, e.Y));
    TreeNode targetNode = treeView.GetNodeAt(targetPoint);

    // 如果目标节点和源节点相同,或目标是源的子节点,则不执行操作
    if (draggedNode.Equals(targetNode) || draggedNode.Contains(targetNode))
    {
        return;
    }

    // 从原位置移除被拖拽的节点
    draggedNode.Remove();

    if (targetNode == null)
    {
        // 如果目标为空,则移动到根层级
        treeView.Nodes.Add(draggedNode);
    }
    else
    {
        // 移动到目标节点下
        targetNode.Nodes.Add(draggedNode);
        targetNode.Expand(); // 展开目标节点
    }
};

MDI (多文档界面)

MDI (Multiple-Document Interface) 是一种经典的桌面应用程序界面模式,它允许在一个主窗口(父窗口)内部同时打开和管理多个子窗口。每个子窗口通常代表一个独立的文档或任务。比如,一个简单的多文档文本编辑器或一个可以同时打开多张图片的看图软件。

虽然现在更流行标签页界面(Tabbed Interface),但了解 MDI 在维护旧项目或某些特定场景下仍然很有用。

一、创建 MDI 基本步骤

  1. 设置 MDI 父窗体 (Parent Form)

首先,需要一个主窗体作为 MDI 容器。只需将其 IsMdiContainer 属性设置为 true 即可。

// 在你的主窗体(比如 Form1)的构造函数或 Load 事件中设置
public partial class MainForm : Form
{
    public MainForm()
    {
        InitializeComponent();
        this.IsMdiContainer = true; // 将此窗体设置为 MDI 容器
    }
}
  1. 创建 MDI 子窗体 (Child Form)

子窗体就是一个普通的 Form。你可以像设计其他任何窗口一样设计它,例如在上面放一个 RichTextBox 来编辑文本。

// DocumentForm.cs
public class DocumentForm : Form
{
    public DocumentForm()
    {
        // 可在此处添加控件,如文本框等
        RichTextBox editor = new RichTextBox();
        editor.Dock = DockStyle.Fill;
        this.Controls.Add(editor);
    }
}
  1. 从父窗体中创建子窗体

在父窗体中,创建一个子窗体的实例,并将其 MdiParent 属性设置为父窗体自身 (this),然后调用 Show()

通常这个操作由菜单项(如“文件”->“新建”)触发。

// 在 MainForm 的代码中
private int documentCounter = 1;

private void newToolStripMenuItem_Click(object sender, EventArgs e)
{
    DocumentForm childForm = new DocumentForm();
    childForm.MdiParent = this; // 关键:设置父窗口
    childForm.Text = "文档 " + documentCounter++;
    childForm.Show();
}

二、管理 MDI 子窗口

管理 MDI 窗口涉及窗口排列、激活和关闭等操作。

  1. 获取当前激活的子窗口 (ActiveMdiChild)

父窗体的 ActiveMdiChild 属性可以获取当前拥有焦点的子窗口。这对于实现“保存”、“关闭当前窗口”等功能至关重要。

// “关闭当前”菜单项的点击事件
private void closeToolStripMenuItem_Click(object sender, EventArgs e)
{
    // 检查是否有激活的子窗口
    if (this.ActiveMdiChild != null)
    {
        this.ActiveMdiChild.Close();
    }
}
  1. 排列子窗口 (LayoutMdi)

使用父窗体的 LayoutMdi 方法可以方便地排列所有子窗口。该方法接受一个 MdiLayout 枚举值。

  • MdiLayout.Cascade: 层叠排列。
  • MdiLayout.TileHorizontal: 水平平铺。
  • MdiLayout.TileVertical: 垂直平铺。
  • MdiLayout.ArrangeIcons: 排列最小化的窗口图标。
// 在“窗口”菜单下创建对应的菜单项

private void cascadeToolStripMenuItem_Click(object sender, EventArgs e)
{
    this.LayoutMdi(MdiLayout.Cascade);
}

private void tileHorizontalToolStripMenuItem_Click(object sender, EventArgs e)
{
    this.LayoutMdi(MdiLayout.TileHorizontal);
}

private void tileVerticalToolStripMenuItem_Click(object sender, EventArgs e)
{
    this.LayoutMdi(MdiLayout.TileVertical);
}
  1. 自动窗口列表 (MdiWindowListItem)

MenuStrip 有一个非常有用的属性 MdiWindowListItem。你只需将一个顶层菜单项(通常是“窗口”菜单)赋给它,MenuStrip 就会自动在这个菜单的底部添加所有已打开子窗口的列表,并自动处理窗口切换。

// 在 MainForm 的构造函数中,假设你有一个名为 menuStrip1 的 MenuStrip
// 和一个名为 windowToolStripMenuItem 的“窗口”顶级菜单项

public MainForm()
{
    InitializeComponent();
    this.IsMdiContainer = true;

    // 将“窗口”菜单指定为 MDI 窗口列表的容器
    this.menuStrip1.MdiWindowListItem = this.windowToolStripMenuItem;
}
设置后,运行程序并打开多个子窗口,"窗口"菜单下会自动出现子窗口标题列表,点击即可切换。

三、MDI 的现代替代方案

虽然 MDI 很经典,但在现代 UI 设计中,标签页界面 (Tabbed Interface) 更为常见,因为它能更有效地利用屏幕空间,且用户更熟悉。

在 WinForms 中,你可以使用 TabControl 控件来实现类似的功能。每个 TabPage 可以承载与一个独立文档相关的控件。

对于需要更自由窗口布局的应用,SDI(单文档界面)——即每个文档都是一个独立的顶级窗口——也是主流选择。