简介这份资源是 WeifenLuo.WinFormsUI.Docking 的源代码与示例工程面向使用 C# 开发 Windows Forms 应用、需要实现类似 Visual Studio 可停靠面板布局的开发者。它解决的是多文档界面中面板拖放、停靠、浮动与自动隐藏等复杂布局逻辑的重复实现问题适合具备一定 WinForms 基础、希望深入理解 DockPanel 内部机制的中高级开发者。压缩包共 196 个文件约 540KB以 85 个 cs 源码文件为核心辅以 55 个 bmp 与 19 个 ico 界面资源、15 个 resx 资源文件以及 sln、csproj、bat 构建脚本和 nuspec 打包配置覆盖从源码到编译发布的完整结构。已有 489 人浏览学习。通过阅读源码可掌握 DockState 枚举、DockControlArea 属性、DockWindows 集合与 AutoHide 等关键机制示例项目则演示了自定义 DockContent、浮动子窗口及关闭、最小化等行为的实现方式便于快速上手并定制扩展。1. 从一次“窗口乱飞”的调试说起WeifenLuo.WinFormsUI.Docking 到底解决什么问题如果你维护过一个超过三年的 WinForms 内部工具大概率见过这种场面主窗体上挂了十几个功能面板用户拖一下、关一个、再双击某个菜单想恢复结果布局全乱重启之后又回到出厂状态。更糟的是多显示器切换、分辨率变化、DPI 缩放一叠加面板位置直接飞到屏幕外用户只能删配置文件重来。这类“窗口乱飞”的问题本质不是 WinForms 的锅而是缺少一个成熟的停靠Docking框架来托管这些可浮动、可停靠、可自动隐藏的子窗口。WeifenLuo.WinFormsUI.Docking 就是在这个场景里被反复提起的一个开源库。它做的事情很聚焦给 WinForms 提供一套类似 Visual Studio 那种可拖拽、可停靠、可保存布局的窗口管理系统。你不需要自己写拖拽命中测试、不需要手算停靠区域、不需要维护一套序列化格式它把这些都封装成DockPanel、DockContent、DockState这几个核心概念。适合谁适合那些还在用 WinForms 做桌面工具、又不想把整个 UI 重写成 WPF 或 Avalonia 的团队。它的价值不在于炫技而在于把“布局持久化”和“多文档停靠”这两件脏活干完了。接下来我会按“先跑通、再讲参数、最后说坑”的顺序把源码和例子的使用路径拆开讲。2. 把官方例子跑起来从源码结构到第一个可停靠窗口2.1 源码目录里到底有什么先认清再动手拿到 WeifenLuo.WinFormsUI.Docking 的源码包第一件事不是急着编译而是先分清目录职责。常见做法是把它分成三块核心库项目、示例项目、以及资源与主题文件。核心库通常叫WeifenLuo.WinFormsUI.Docking里面是DockPanel、DockContent、DockPane、DockWindow这些类示例项目一般会有多个比如一个基础停靠演示、一个多文档界面演示、一个主题切换演示。资源部分则包含 VS2012、VS2015 等不同视觉风格的渲染器资源。我一般会先打开解决方案文件确认目标框架。老版本源码可能锁定在 .NET Framework 4.0 或 4.5如果你用的是新版 Visual Studio需要先做一次“重定向到当前框架”的操作否则编译会报引用缺失。这一步不做后面所有调试都是白费。提示不要一上来就改核心库代码。先把示例项目设为启动项目确认它能跑起来再动自己的业务代码。2.2 最小可运行示例一个主窗体加两个停靠面板下面这段代码是我从示例里抽出来的最小结构去掉花哨主题只保留停靠骨架。你可以直接贴到一个新建的 WinForms 项目里前提是已经引用了核心库。using System; using System.Windows.Forms; using WeifenLuo.WinFormsUI.Docking; namespace DockDemo { // 主窗体只负责承载 DockPanel public partial class MainForm : Form { private DockPanel dockPanel; public MainForm() { InitializeComponent(); // 初始化 DockPanel并设置停靠模式 dockPanel new DockPanel { Dock DockStyle.Fill, DocumentStyle DocumentStyle.DockingMdi, // 文档区域采用 MDI 风格 Theme new VS2015LightTheme() // 使用浅色主题 }; Controls.Add(dockPanel); } protected override void OnLoad(EventArgs e) { base.OnLoad(e); // 创建两个可停靠内容 var leftPanel new DockContent(); leftPanel.Text 左侧工具面板; leftPanel.Show(dockPanel, DockState.DockLeft); var rightPanel new DockContent(); rightPanel.Text 右侧属性面板; rightPanel.Show(dockPanel, DockState.DockRight); } } }逻辑说明DockPanel是整个停靠系统的根容器必须设置Dock DockStyle.Fill才能铺满主窗体。DocumentStyle决定文档区域是 MDI 风格还是普通标签风格示例里用DockingMdi是为了兼容老代码。Theme属性接受一个渲染器对象这里用VS2015LightTheme如果你引用的版本没有这个类换成VS2012LightTheme或默认主题即可。参数说明DockState是枚举常用值有DockLeft、DockRight、DockTop、DockBottom、Document、Float、AutoHide。Show方法的第一个参数是父级DockPanel第二个参数决定初始停靠位置。如果你想让面板一开始就浮动传DockState.Float但浮动窗口的初始尺寸需要额外设置FloatPane的边界。2.3 布局保存与还原别让用户每次重启都重排停靠框架最实用的功能是布局持久化。示例里通常会有SaveAsXml和LoadFromXml两个方法对应把当前停靠状态写进 XML 文件以及从 XML 恢复。下面是一个典型的保存与加载封装private const string LayoutFile layout.xml; // 保存布局在窗体关闭前调用 private void SaveLayout() { // 只保存可见的停靠内容避免把临时隐藏的面板也写进去 dockPanel.SaveAsXml(LayoutFile); } // 加载布局在窗体加载时调用失败则回退到默认布局 private void LoadLayout() { if (System.IO.File.Exists(LayoutFile)) { try { dockPanel.LoadFromXml(LayoutFile, DeserializeDockContent); } catch { // 布局文件损坏或版本不匹配时走默认布局 CreateDefaultLayout(); } } else { CreateDefaultLayout(); } } // 反序列化回调根据持久化的类型名重建 DockContent private IDockContent DeserializeDockContent(string persistString) { // persistString 是保存时写入的类型全名 if (persistString typeof(LeftToolPanel).FullName) return new LeftToolPanel(); if (persistString typeof(RightPropertyPanel).FullName) return new RightPropertyPanel(); return null; }逻辑说明SaveAsXml会把每个DockContent的类型全名、停靠状态、尺寸、顺序写进 XML。LoadFromXml在还原时会调用你传入的回调函数让你根据类型名重新构造对象。这一步很关键因为框架不会帮你 new 对象它只负责摆放。参数说明DeserializeDockContent的返回值必须是IDockContent如果你返回null那个面板就会被丢弃。常见错误是类型名对不上比如你改了命名空间或类名旧布局文件就会失效。解决办法是在保存时用自定义的持久化字符串而不是直接依赖FullName。注意布局文件不要和可执行文件放在同一目录尤其是安装到 Program Files 时没有写权限。我一般放到%AppData%下的应用专属目录。3. 核心参数与选型DockPanel、DockContent 和主题渲染器怎么配3.1 DockPanel 的五个必调属性DockPanel是入口它的属性决定了整体行为。下面这张表是我在实际项目里反复调过的五个参数每个都对应一个具体场景。属性常用值作用不调会怎样DocumentStyleDockingMdi/DockingWindow文档区域是 MDI 子窗体还是普通停靠窗多文档切换时行为不符合预期ThemeVS2015LightTheme等视觉渲染器界面停留在 Win7 风格高 DPI 下模糊ShowDocumentIcontrue/false文档标签是否显示图标标签栏拥挤或缺少辨识度RightToLeftLayouttrue/false从右到左布局阿拉伯语等环境下面板顺序反了PreserveTabAreaShapetrue/false保留标签区域形状拖拽时标签区域变形选型理由如果你做的是多文档编辑器DocumentStyle选DockingWindow这样每个文档是一个独立标签切换更自然如果你在迁移老 MDI 程序选DockingMdi子窗体行为更接近旧代码。主题渲染器不要混用一个项目只选一套否则浮动窗口和停靠窗口的边框颜色会对不上。3.2 DockContent 的生命周期与关闭拦截DockContent继承自Form所以它有完整的窗体生命周期。但停靠场景下用户点关闭按钮时你往往需要拦截比如文档有未保存修改或者工具面板只是隐藏而不是销毁。常见做法是重写OnFormClosing根据CloseReason判断。protected override void OnFormClosing(FormClosingEventArgs e) { // 如果是用户点击关闭且文档有未保存内容则取消关闭 if (e.CloseReason CloseReason.UserClosing HasUnsavedChanges) { var result MessageBox.Show(有未保存的修改确定关闭吗, 确认, MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (result DialogResult.No) { e.Cancel true; // 取消关闭 return; } } base.OnFormClosing(e); }逻辑说明CloseReason.UserClosing表示用户主动关闭而不是父窗体关闭或应用程序退出。e.Cancel true会阻止面板关闭但不会阻止它被隐藏。如果你希望关闭后只是隐藏可以在DockState上做文章把面板设为AutoHide而不是真正Close。参数说明HasUnsavedChanges是你自己的业务标志位框架不提供。注意如果父窗体正在关闭CloseReason会是FormOwnerClosing这时候不应该拦截否则程序退不掉。3.3 主题渲染器的选择与高 DPI 适配WeifenLuo 的源码里通常附带多套渲染器VS2012、VS2015、VS2017 等。选哪套取决于你的目标操作系统和视觉要求。VS2015 系列对高 DPI 支持更好但需要你在 app.manifest 里声明 DPI 感知。application xmlnsurn:schemas-microsoft-com:asm.v3 windowsSettings dpiAware xmlnshttp://schemas.microsoft.com/SMI/2005/WindowsSettingstrue/pm/dpiAware dpiAwareness xmlnshttp://schemas.microsoft.com/SMI/2016/WindowsSettingsPerMonitorV2/dpiAwareness /windowsSettings /application逻辑说明dpiAware设为true/pm表示按显示器缩放dpiAwareness设为PerMonitorV2是 Win10 1703 之后的标准。两者同时写老系统读前者新系统读后者。参数说明如果你不做这一步在 150% 缩放下停靠面板的标题栏和按钮会模糊拖拽时还会出现位置偏移。这是血泪经验不是玄学。4. 避坑与排查五个让停靠布局翻车的典型场景4.1 布局文件加载后面板消失现象用户重启程序布局文件明明存在但所有面板都不见了只剩一个空的主窗体。原因DeserializeDockContent回调返回了null或者类型名匹配失败。常见于重构后改了类名或命名空间旧布局文件里的persistString对不上。解决在回调里加日志打印persistString和当前程序集里的类型列表。更稳妥的做法是自定义持久化字符串比如用LeftTool这种短名而不是FullName。加载失败时回退到默认布局不要让用户面对空白窗口。4.2 浮动窗口跑到屏幕外现象用户把面板拖到副屏下次打开时副屏没接面板出现在不可见区域。原因布局文件保存了绝对屏幕坐标但显示器配置变了。解决在LoadFromXml之后遍历所有FloatWindow检查其Bounds是否与当前Screen.AllScreens的工作区相交。如果不相交把位置重置到主屏中央。这个检查要放在窗体Shown事件之后否则屏幕信息还不完整。4.3 关闭主窗体时面板不释放现象程序退出后进程还在任务管理器里或者某些面板的Dispose没被调用。原因DockContent被DockPanel持有引用如果主窗体关闭时没有先Close所有面板框架可能不会自动释放。解决在MainForm.OnFormClosing里先调用dockPanel.SaveAsXml然后遍历dockPanel.Contents逐个Close。注意要区分Document和Tool类型文档面板可能需要先保存。4.4 高 DPI 下拖拽命中偏移现象在 150% 缩放显示器上鼠标拖拽面板时停靠提示框的位置和鼠标差了几十像素。原因框架内部用了固定的像素计算没有按 DPI 缩放。解决确认你用的渲染器版本是否支持 DPI。如果源码里没有ScaleDpi相关逻辑可以在DockPanel初始化后手动设置AutoScaleMode AutoScaleMode.Dpi并检查DockPanel的Scale方法是否被正确调用。实在不行降级到 100% 缩放测试确认是 DPI 问题而不是逻辑问题。4.5 主题切换后部分区域颜色不更新现象运行时切换Theme大部分面板变了但浮动窗口的标题栏还是旧颜色。原因已经创建的FloatWindow不会自动重新应用渲染器需要手动刷新。解决切换主题后遍历dockPanel.FloatWindows对每个窗口调用Invalidate(true)和Refresh。如果还不行先关闭所有浮动窗口切换主题再重新Show。这个操作会丢失浮动窗口的位置所以最好在设置里提示用户重启生效。5. 进阶技巧用自定义 DockContent 和布局版本号管住长期维护5.1 给每个 DockContent 加一个稳定的持久化标识前面提过用FullName做持久化字符串重构就翻车。我现在的习惯是给每个DockContent子类加一个public const string PersistName在保存和加载时都用它。public class LeftToolPanel : DockContent { public const string PersistName LeftToolPanel; // 其他代码... } // 在 DeserializeDockContent 里这样匹配 private IDockContent DeserializeDockContent(string persistString) { switch (persistString) { case LeftToolPanel.PersistName: return new LeftToolPanel(); case RightPropertyPanel.PersistName: return new RightPropertyPanel(); default: return null; } }逻辑说明PersistName是编译期常量不会因为命名空间调整而改变。你可以在保存布局前把DockContent的Text或Tag映射成这个常量但更简单的做法是重写GetPersistString方法如果框架版本支持。如果不支持就在SaveAsXml之前手动替换 XML 里的类型名虽然麻烦但比布局丢失强。参数说明PersistName不要用中文不要用空格保持和类名一致但去掉命名空间前缀。这样即使你把类移到另一个命名空间旧布局文件依然能加载。5.2 布局版本号让旧布局文件自动失效长期维护的项目布局结构会变。比如你删了一个面板或者改了默认停靠位置。如果直接加载旧布局用户会看到一个残缺的界面。我的做法是在布局文件里写一个版本号加载时比对。private const int CurrentLayoutVersion 3; private void SaveLayout() { // 先保存到临时文件再在根节点写入版本号 var tempFile LayoutFile .tmp; dockPanel.SaveAsXml(tempFile); var doc System.Xml.Linq.XDocument.Load(tempFile); doc.Root.SetAttributeValue(LayoutVersion, CurrentLayoutVersion); doc.Save(LayoutFile); System.IO.File.Delete(tempFile); } private void LoadLayout() { if (!System.IO.File.Exists(LayoutFile)) { CreateDefaultLayout(); return; } var doc System.Xml.Linq.XDocument.Load(LayoutFile); var versionAttr doc.Root.Attribute(LayoutVersion); if (versionAttr null || int.Parse(versionAttr.Value) ! CurrentLayoutVersion) { // 版本不匹配丢弃旧布局 CreateDefaultLayout(); return; } dockPanel.LoadFromXml(LayoutFile, DeserializeDockContent); }逻辑说明SaveAsXml生成的 XML 根节点是DockPanel你可以直接在上面加属性。加载时先读版本号不匹配就重建默认布局。这样每次你调整了面板结构只需要把CurrentLayoutVersion加一所有用户的旧布局自动作废不会出现半新半旧的尴尬状态。参数说明版本号用整数不要用日期字符串比较起来更简单。临时文件记得删除否则会在用户目录留下垃圾。5.3 一个验证布局是否正确的笨办法最后分享一个我常用的验证技巧写一个单元测试或控制台小工具模拟保存布局、销毁所有面板、再加载布局然后断言面板数量和停靠状态是否一致。虽然 WinForms 的 UI 测试比较麻烦但你可以只测 XML 的读写逻辑把DockPanel换成 mock 对象。这样每次改布局相关代码跑一遍测试就能发现持久化字符串有没有写错。// 伪代码示意验证持久化字符串映射 [Test] public void PersistName_ShouldMatchAllDockContentTypes() { var types typeof(LeftToolPanel).Assembly.GetTypes() .Where(t typeof(DockContent).IsAssignableFrom(t) !t.IsAbstract); foreach (var t in types) { var field t.GetField(PersistName, BindingFlags.Public | BindingFlags.Static); Assert.IsNotNull(field, ${t.Name} 缺少 PersistName 常量); } }这个测试不碰 UI只检查每个DockContent子类有没有定义PersistName。一旦有人新增面板忘了加测试就会红。希望帮到你。本文还有配套的精品资源点击获取