简介这份资源是 WeifenLuo.WinFormsUI.Docking 的源代码与示例工程面向使用 C# 开发 Windows Forms 应用、需要实现类似 Visual Studio 可停靠面板布局的开发者。它解决的是多文档界面中面板拖放、停靠、浮动与自动隐藏等复杂布局逻辑的重复实现问题适合具备一定 WinForms 基础、希望深入理解 DockPanel 内部机制并做二次定制的中高级开发者。压缩包共 196 个文件约 540KB以 85 个 cs 源码文件、15 个 resx 资源、2 个 csproj 工程文件及 3 个 sln 解决方案为主另含 bmp、ico 等界面素材与 bat 构建脚本便于直接编译运行和调试。目前已有 489 人学习下载。通过源码可掌握 DockState 枚举、DockControlArea 属性、DockWindows 集合与 AutoHide 等核心机制示例项目则演示了自定义 DockContent、浮动子窗口及关闭、最小化等行为的实现方式是研究可停靠布局控件不可多得的完整参考。1. 从一次“窗口乱飞”的事故说起WeifenLuo.WinFormsUI.Docking 到底解决什么问题如果你维护过一个超过三年的 WinForms 上位机、工控配置工具或者内部数据管理端大概率见过这种场面主窗体上十几个功能面板有人喜欢把日志拖到左边有人非要浮在第二屏还有人把参数面板关掉之后再也找不回来。用户抱怨“布局又乱了”你打开设计器一看全是硬编码的Location和Size改一个面板位置要动三处代码。WeifenLuo.WinFormsUI.Docking 就是冲着这类问题来的——它给 WinForms 补上了一套停靠Docking框架让面板能像 Visual Studio 那样拖拽、停靠、浮动、自动隐藏并且把布局序列化成 XML 存下来下次启动原样恢复。这个库在 WinForms 圈子里流传很广很多内部工具、仿真软件、测试平台都用它做壳。它适合谁适合还在维护 .NET Framework WinForms 项目、又不想把整个 UI 重写成 WPF 或 Avalonia 的团队。你不需要引入庞大的 UI 框架只要把主窗体换成DockPanel把原来的Panel换成DockContent就能拿到一套可持久化的多文档界面。代价是它年代久远API 风格偏老文档零散网上能搜到的例子经常缺关键步骤。这篇笔记就按“先跑通最小例子再讲布局持久化和踩坑”的顺序把源代码和例子里真正要用的部分拆开讲。2. 把官方例子跑起来之前先搞清 DockPanel 的骨架2.1 三个核心类型DockPanel、DockContent、DockStateWeifenLuo.WinFormsUI.Docking 的模型很薄核心就三个概念。DockPanel是主容器继承自Panel你把它铺在主窗体上所有停靠行为都发生在它内部。DockContent是能被停靠的窗体它继承自Form但重写了关闭逻辑默认点关闭是隐藏而不是销毁。DockState是枚举描述一个内容当前处于什么状态DockState.DockLeft、DockState.DockRight、DockState.DockTop、DockState.DockBottom、DockState.Float、DockState.Document、DockState.Hidden。理解这三者的关系后面所有配置都不会迷路。DockPanel负责布局算法和序列化DockContent负责自己的生命周期DockState是两者之间的契约。常见做法是主窗体只放一个DockPanel每个功能模块做成一个继承DockContent的窗体在启动时按业务需要调用Show并传入目标DockState。2.2 最小可运行工程从空窗体到可拖拽面板先建一个 .NET Framework 4.7.2 的 WinForms 项目通过 NuGet 安装WeifenLuo.WinFormsUI.Docking。如果你拿到的是一份源代码包通常里面会有WinFormsUI.Docking项目直接引用它也可以。下面是最小主窗体代码。// MainForm.cs using WeifenLuo.WinFormsUI.Docking; public partial class MainForm : Form { private DockPanel dockPanel; public MainForm() { InitializeComponent(); // 1. 创建 DockPanel 并铺满主窗体 dockPanel new DockPanel { Dock DockStyle.Fill, DocumentStyle DocumentStyle.DockingWindow, // 文档区用窗口样式 Theme new VS2015LightTheme() // 主题旧版本可能没有这行 }; Controls.Add(dockPanel); // 2. 创建两个 DockContent 并停靠 var leftPanel new DockContent { Text 工具箱, TabText 工具箱 }; leftPanel.Show(dockPanel, DockState.DockLeft); var docPanel new DockContent { Text 主文档, TabText 主文档 }; docPanel.Show(dockPanel, DockState.Document); } }这段代码做了三件事把DockPanel填满主窗体、创建一个左侧停靠面板、创建一个文档区面板。DocumentStyle决定文档区标签的绘制方式DockingWindow是最接近 Visual Studio 的样式。Theme属性在新版本里才有如果你用的版本没有删掉即可不影响功能。逻辑说明DockContent.Show(DockPanel, DockState)是入口方法它会把当前窗体注册到DockPanel的内容集合里并根据DockState计算停靠位置。参数DockState.Document表示放进中间文档区DockState.DockLeft表示贴左边。注意DockContent的Close行为默认是隐藏如果你希望真正销毁需要重写OnFormClosing或者调用Dispose。2.3 用 DockContent 还是普通 Form继承时的两个硬性约束很多新手直接把普通Form传给Show编译能过但运行会抛异常。DockPanel要求内容必须是DockContent或其子类。所以你的功能窗体要改成继承DockContent。改的时候有两个硬性约束第一不要在DockContent子类的构造函数里访问DockPanel因为此时还没ShowDockPanel为 null第二如果你重写了OnFormClosing记得判断关闭原因否则用户点关闭时可能把整个应用退出。public class ToolBoxContent : DockContent { public ToolBoxContent() { Text 工具箱; // 这里不要访问 DockPanel } protected override void OnFormClosing(FormClosingEventArgs e) { // 如果是用户点关闭只隐藏不销毁 if (e.CloseReason CloseReason.UserClosing) { e.Cancel true; Hide(); return; } base.OnFormClosing(e); } }参数说明CloseReason.UserClosing表示用户主动关闭此时取消关闭并隐藏符合停靠面板的交互习惯。如果是CloseReason.ApplicationExitCall或CloseReason.WindowsShutDown就放行让程序正常退出。这个细节在官方例子里有体现但很多人抄的时候漏掉导致主窗体关闭时面板还在阻止退出。3. 布局持久化把用户拖出来的布局存成 XML 再读回来3.1 序列化 API 的两个方法SaveAsXml 和 LoadFromXmlDockPanel自带布局序列化核心就两个方法SaveAsXml(string fileName)和LoadFromXml(string fileName, DeserializeDockContent deserializeDockContent)。保存时它会把当前所有内容的停靠状态、尺寸、浮动窗口位置写成一个 XML 文件。读取时它需要你提供一个回调根据 XML 里记录的类型名重新创建对应的DockContent实例。// 保存布局 private void SaveLayout() { dockPanel.SaveAsXml(layout.xml); } // 读取布局 private void LoadLayout() { if (File.Exists(layout.xml)) { dockPanel.LoadFromXml(layout.xml, GetContentFromPersistString); } } // 回调根据持久化字符串创建内容 private IDockContent GetContentFromPersistString(string persistString) { // persistString 通常是 命名空间.类名, 程序集名 if (persistString.Contains(ToolBoxContent)) return new ToolBoxContent(); if (persistString.Contains(MainDocumentContent)) return new MainDocumentContent(); return null; }逻辑说明SaveAsXml不需要额外参数直接写文件。LoadFromXml的第二个参数是委托DeserializeDockContent它会在解析 XML 时对每个内容节点调用一次你返回对应的实例DockPanel负责恢复位置。如果返回 null该内容会被跳过。参数persistString的格式和程序集限定名有关用Contains判断类名是最稳妥的做法不要用全匹配因为不同编译配置下程序集名可能带版本号。3.2 反序列化回调里最容易写错的三个点第一个点回调里创建的内容不能立刻调用Show。LoadFromXml内部会接管显示逻辑你只需要new出来返回即可。第二个点如果某个内容在 XML 里存在但你的回调返回 nullDockPanel不会报错但那个面板会消失用户会以为布局丢了。第三个点回调可能被调用多次每次都要返回新实例不能返回同一个单例否则第二次会抛“内容已属于某个 DockPanel”的异常。private IDockContent GetContentFromPersistString(string persistString) { // 每次都必须 new不能缓存实例 if (persistString.Contains(ToolBoxContent)) return new ToolBoxContent(); if (persistString.Contains(LogContent)) return new LogContent(); if (persistString.Contains(PropertyContent)) return new PropertyContent(); // 未知类型返回 nullDockPanel 会跳过 return null; }参数说明persistString里除了类名还可能包含程序集版本、区域文化等信息。用Contains判断类名可以兼容不同版本。如果你把类重命名了旧布局文件里的字符串就对不上回调返回 null面板消失。所以发布后尽量不要改DockContent子类的类名或者做好版本迁移。3.3 默认布局与用户布局的优先级处理实际项目里通常需要“首次启动用默认布局之后用用户保存的布局”。做法是在主窗体Load事件里判断布局文件是否存在。如果存在就LoadFromXml否则手动创建默认面板并调用Show。注意LoadFromXml必须在所有默认面板创建之前调用否则会出现重复内容。private void MainForm_Load(object sender, EventArgs e) { if (File.Exists(layout.xml)) { dockPanel.LoadFromXml(layout.xml, GetContentFromPersistString); } else { // 默认布局 new ToolBoxContent().Show(dockPanel, DockState.DockLeft); new LogContent().Show(dockPanel, DockState.DockBottom); new MainDocumentContent().Show(dockPanel, DockState.Document); } }逻辑说明LoadFromXml会清空当前DockPanel的内容再重建所以不要先创建默认面板再加载。如果你需要“恢复默认布局”按钮可以先dockPanel.Contents.Clear()或者遍历关闭所有内容再重新创建默认面板。参数上没有特殊要求关键是顺序。4. 避坑与排查布局丢失、主题报错、关闭异常的现场记录4.1 现象重启后布局变成一堆浮动窗口原因LoadFromXml时回调返回了 null或者返回的实例类型和保存时不一致。DockPanel找不到对应内容会把剩余内容按默认规则摆放看起来就像布局乱了。解决在回调里加日志打印persistString确认每个节点都能匹配到类名。如果类名改过要么改回来要么在回调里做旧名称映射。4.2 现象编译报错“找不到 VS2015LightTheme”原因主题类在新版本里拆到了单独的包或者你引用的源代码版本较老根本没有主题系统。解决删掉Theme赋值行用默认样式或者通过 NuGet 安装对应的主题包。不要强行反射创建主题类版本不匹配时内部渲染器会抛空引用。4.3 现象点面板关闭按钮整个程序退出原因DockContent子类没有重写OnFormClosing或者重写时没有取消UserClosing。默认Form关闭会销毁窗体如果这是最后一个窗体应用就退出了。解决按 2.3 的代码重写OnFormClosing对UserClosing执行e.Cancel true; Hide();。注意Hide()之后面板还在DockPanel.Contents里下次可以通过Show恢复。4.4 现象浮动窗口拖到第二屏后保存的坐标是负数原因多显示器环境下浮动窗口的坐标可能是负值SaveAsXml会原样记录。如果下次启动时第二屏不在窗口会跑到屏幕外。解决在LoadFromXml之后遍历dockPanel.Contents对DockState.Float的内容检查Bounds是否落在Screen.AllScreens的范围内不在就改回DockState.DockRight或居中。4.5 现象频繁调用 Show 导致标签页重复原因同一个DockContent实例多次调用Show或者每次都用new创建但没关闭旧实例。DockPanel允许同一类型多个实例但用户看到的就是重复标签。解决对单例面板在模块管理器里缓存实例Show之前判断IsHidden或DockState已经显示的就调用Activate()。5. 进阶技巧用隐藏面板做懒加载以及一个我常备的布局校验方法5.1 隐藏面板 按需 Show减少启动时的构造开销大型工具启动时如果一次性创建所有DockContent构造函数里的数据绑定、图表初始化会拖慢启动。我一般把非首屏面板先创建但不Show或者只创建轻量壳等用户第一次点击菜单时再Show。DockContent支持先Hide()再Show(dockPanel, DockState.Hidden)但更干净的做法是维护一个Dictionarystring, DockContent菜单点击时判断是否存在不存在就new并Show。private Dictionarystring, DockContent _cache new Dictionarystring, DockContent(); private void ShowOrCreateT(string key, DockState state) where T : DockContent, new() { if (!_cache.TryGetValue(key, out var content) || content.IsDisposed) { content new T(); _cache[key] content; } content.Show(dockPanel, state); content.Activate(); }逻辑说明_cache按 key 缓存实例避免重复创建。IsDisposed判断防止窗体已被销毁后继续使用。Activate()让已显示的面板获得焦点而不是新开一个标签。参数state可以按业务传DockState.Document或DockState.DockRight。5.2 布局文件版本号给 XML 加一个自定义属性SaveAsXml生成的 XML 根节点是DockPanel你可以读取后插入一个自定义属性记录布局版本。下次加载前先检查版本如果版本太旧就丢弃用默认布局。这样发布新版本时改了面板结构不会因为旧布局文件导致面板错乱。private void SaveLayoutWithVersion() { dockPanel.SaveAsXml(layout.xml); var doc XDocument.Load(layout.xml); doc.Root.SetAttributeValue(LayoutVersion, 2); doc.Save(layout.xml); } private bool IsLayoutVersionMatch() { if (!File.Exists(layout.xml)) return false; var doc XDocument.Load(layout.xml); var version doc.Root?.Attribute(LayoutVersion)?.Value; return version 2; }参数说明LayoutVersion是自定义属性DockPanel加载时会忽略它不影响反序列化。你只需要在LoadFromXml之前调用IsLayoutVersionMatch不匹配就跳过加载。这个方法我用了很多次比每次改结构就删用户布局文件温和得多。5.3 一个校验习惯加载后遍历内容检查 DockState每次LoadFromXml之后我会写一段临时校验代码遍历dockPanel.Contents打印每个内容的DockState和Text。如果发现某个面板的DockState是Hidden但用户没主动隐藏过说明回调或 XML 有问题。这个习惯帮我提前发现过好几次类名不匹配的坑。foreach (IDockContent content in dockPanel.Contents) { if (content is DockContent dc) { System.Diagnostics.Debug.WriteLine(${dc.Text} - {dc.DockState}); } }这段代码只在调试时用发布前删掉。DockState输出Hidden时检查该内容是否在 XML 里有记录但回调返回了 null。如果输出Float但坐标异常按 4.4 处理。我自己的教训是不要等到用户反馈“布局又乱了”才去查序列化每次改完面板结构先手动保存、重启、再保存确认 XML 里节点数量对得上。这个库不复杂但细节都在这些边角里。希望帮到你。本文还有配套的精品资源点击获取