ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

WinForms可停靠窗口实战:WeifenLuo.Docking源码与避坑指南

WinForms可停靠窗口实战:WeifenLuo.Docking源码与避坑指南 简介WeifenLuo.WinFormsUI.Docking是一款面向C#窗体程序开发的开源停靠布局控件可模拟Visual Studio的可停靠面板效果帮助开发者解决多文档界面中子窗口停靠、浮动、自动隐藏等复杂布局问题。资源包内含完整源代码和示例工程压缩后共196个文件、仅540KB以85个C#源文件为主另有55个位图与19个图标文件提供界面样式15个资源描述文件保存布局信息并附工程文件、批处理脚本和可运行示例。目前已有488人学习浏览。通过源码可以理解停靠状态枚举、停靠区域属性、停靠窗口集合、自动隐藏机制等核心概念学习自定义子窗口的关闭、最小化、最大化行为附带的示例项目能帮助快速上手无论初学者还是资深工程师都能从源码研究与实例演练中获得实际收获适合打造专业级IDE风格界面。1. 搜 WeifenLuo.WinFormsUI.Docking 源代码和例子通常是因为手里的 WinForms 项目要做成 Visual Studio 那种“面板能拖、能停、能浮、能自动隐藏”的界面这套库堪称 WinForms 时代的标准答案DockPanel 接管主窗体剩余区域DockContent 变成面板布局状态由库维护还能序列化成 XML。真正动手时缺的往往不是 DLL而是对源码和例子的拆解。下面按这条路径走状态机原理、源码编译与示例跑通、写出第一个可停靠窗口、避坑清单、布局持久化与源码调试。适合三类人WinForms 新手、维护旧系统的人、想读源码学窗口布局算法的进阶者。2. 从 DockContent 与 DockState 看懂停靠状态机源码不白读的前提2.1 DockContent 与 DockPanel 的分工内容与容器的边界我见过不少新手第一个动作就是去源码里翻布局算法却连 DockContent 和 DockPanel 的关系都没分清。这两个类恰恰是整个库的地基。DockPanel 是一个控件通常以DockStyle.Fill塞进主窗体它本身不绘制任何业务界面只负责管理一块矩形区域并在内部把区域切成若干条带。DockContent 则是每个可停靠窗口的基类它继续继承System.Windows.Forms.Form所以你可以在里面放任何 WinForms 控件、享受已有的消息循环和焦点逻辑代价是必须接受 DockPanel 的托管。这个设计的直接好处是业务代码不用学新 API你在 DockContent 里放 TreeView、PropertyGrid、RichTextBox和在一个普通 Form 上放控件没有区别停靠、浮动、自动隐藏这些行为全部被 DockPanel 接管。理解了这一点你再去看源码里的 DockContent.cs会发现它多数代码都在处理状态迁移、序列化标识和事件通知而不是绘制。绘制相关的东西在 DockPanel、DockPane、AutoHideStrip 这几个类里。再往下分一层还要认识 DockPane。DockPanel 的某个停靠区域可能同时摞着多个 DockContent同一区域的一组标签页就由一个 DockPane 管理。DockPane 本身也是一个控件你拖动标签页时本质上是在切换这个标签在哪个 DockPane 里。四个关键对象的分工可以这样记对象职责常见入口DockPanel全局布局容器唯一根主窗体Dock DockStyle.FillDockContent单个业务窗口Show(dockPanel)DockPane一组标签页的容器ActiveContent、CloseActiveContentFloatWindow浮动状态的外层窗体DockState Float时自动创建至于选型常见对比对象是 DevExpress DockManager、Infragistics Docking。如果你的产品允许商业授权付费库确实省心但内部工具、需要定制布局算法、需要阅读源代码排错的项目选 WeifenLuo 这套更实际。它依赖少、源码量可控改起来不用跟黑匣子搏斗。当然代价是文档不多、很多细节要靠示例和源码硬啃这也是我写这篇文章的原因。2.2 DockState 的 8 个状态与合法迁移DockState 是全部核心。它的取值包括Unknown、Float、DockTop、DockBottom、DockLeft、DockRight、Document、Hidden。展开说Float 表示窗口脱离主窗体悬浮在独立窗体里四个 Dock 方向对应主窗体四周的停靠槽Document 对应中间文档区Hidden 表示逻辑存在但界面不显示。Unknown 通常在反序列化前出现运行中的内容不会停留在这个状态。迁移规则分两套路径。用户拖拽时库会先进入浮动预览再根据鼠标落点决定停靠方向代码里可以直接给 DockState 赋值或设置 ShowHint 让首次显示落在某个位置。值得注意的状态是 Hidden它和关闭是两回事关闭会销毁对象Hidden 只是隐藏。很多项目把“关闭面板”改成一个显式 Hide就是为了保留内容状态。想看状态迁移与其猜不如打日志。我建议所有初接这个库的人都在 DockContent 构造里挂一个DockStateChanged事件把它打印出来using WeifenLuo.WinFormsUI.Docking; public class LoggedDockContent : DockContent { public LoggedDockContent() { // DockStateChanged 在每次停靠状态变化后触发 DockStateChanged (s, e) { // e.OldValue 是迁移前的状态当前状态从 e.DockState 取 Console.WriteLine( ${Text}: {e.OldValue} - {e.DockState}); }; } }这段代码的价值在于你会发现一次拖拽往往不是一步到位窗口先变成 Float再落到 DockRight日志里会连续出现两行。很多布局 bug 都是因为代码在状态中途去查 DockState查到 Float 就误判了。所以判断当前停靠位置最好在拖拽结束、也就是第二跳之后再读或者接受中间会有 Float 过渡。2.3 停靠布局的树结构理解嵌套分栏才能看懂序列化输出第三个要紧概念是布局的树形嵌套。DockPanel 不是简单地把区域均分成上中下而是采用嵌套分栏法先占一个方向剩下的矩形再递归给下一个 pane。这种结构与 Visual Studio 的布局一致优点是任意组合都可能缺点是不存在一个“位置表”要表示布局就必须走树形结构。打开布局保存得到的 XML你会看到多层嵌套的节点每层记录一个停靠方向、一个比例、一组内容标识。用下面的结构来想象它layout pane directionLeft percent0.25 content keyProjectExplorer / /pane split pane directionBottom percent0.3 content keyOutput / /pane pane directionDocument content keyEditor / /pane /split /layout注意这是示意不是库生成的原始 XML真实的节点命名和嵌套层级以你当前源码版本为准但方向能帮你理解左边资源管理器窗口占 25% 宽度剩余区域再拆成下部输出窗口和中间文档区。源码里跟这块相关的是 DockPane 的 NestedDockingStatus 和 DockPanel 的布局引擎。初读源码的人最容易绕晕的是递归拆分逻辑我通常从 DockPanel 的四个方向属性入手观察它们返回的 pane 列表再对照 XML 看比例值。把树结构记在脑子里后再去看停靠预览的绘制代码就明白它为什么能画出带方向指示的蓝色高亮框——它只是遍历了树里每个可插入的位置。先理解树再看绘制比直接读源码高效得多。3. 把官方源码和例子跑起来获取、编译、定位演示代码的完整路径3.1 先确定要源码还是 DLL两种来源各取所需标题既然带“源代码和例子”你要拿到的显然不只是编译好的程序集而是整套工程。常见做法是去 GitHub 搜索 WinFormsUI.Docking 或 DockPanel Suite代码托管公开、版本演进清楚。如果你暂时不方便拉取远程仓库也可以先装 NuGet 包把功能跑通之后再补源码阅读。功能使用和源码阅读可以分开不必卡在第一周。# 拉取源代码仓库地址请用你自己搜索到的源替换占位符 git clone repository-url DockingSource cd DockingSource # 查看本地分支与历史 tag按目标框架选择合适的版本 git branch -a git tag -l第一条命令克隆整个仓库第二条把历史分支和 tag 列出来。这个库维护周期长不同 tag 的目标框架可能完全不同先看 tag 再决定切到哪个版本能避免源码和你的项目框架对不上。3.2 编译前的环境准备老 csproj 要用对构建工具这个库经历过 WinForms 的老中青三个时期早期 .NET Framework 2.0中期 4.x后期 netcore/net5。源码里可能同时存在多个 csproj 和多个目标框架直接用新版 Visual Studio 双击旧 sln 会碰见几类问题旧工程目标框架未安装、packages.config 还原失败、引用了不存在的本地库路径。# 用 MSBuild 构建sln 文件名以你实际解压出的为准 msbuild solution.sln /p:ConfigurationRelease如果你习惯 dotnet CLI这里要留个心眼老工程不是 SDK-Style 项目dotnet build不一定认。优先在 Visual Studio 开发者命令提示符里跑 MSBuild或者直接打开 VS 编译。编译完成后核心程序集通常输出在 Release 目录名字以 WeifenLuo.WinFormsUI.Docking 开头。3.3 跑通两个官方示例一个看交互一个看外观官方仓库通常会带两个示例工程一个是偏教学的 DockSample另一个是模仿 Visual Studio 2005 风格的界面演示。前者适合学交互面板拖拽出蓝色停靠预览、拖动标签页合并、右键标签出现上下文菜单、自动隐藏按钮变成垂直 Tab后者适合直接借外观能看出停靠条、标题栏、标签页在不同主题下的状态。把两个工程依次设为启动项目跑一遍重点操作几个动作把左侧窗口拖到右侧、把文档区标签拆分成浮动窗口、点自动隐藏按钮、保存布局再重新加载。体验完交互再回到源码里定位展示点。# 在示例源码里搜索所有 DockContent 的挂载位置 grep -R \.Show(dockPanel DockSample/这条命令会把示例程序里所有 DockContent 的入口一次列出来。以 VS 的全局搜索也能做同样的事。从这些 Show 调用向上溯源能很快理解每个窗口的 ShowHint 与 DockAreas 配置。我拿到陌生源码的习惯是先搜 Show 和 DockState 的赋值点再看构造和属性初始化最后才读绘制。3.4 引用到你自己项目的三种方式按场景选方式优点缺点适合场景DLL 直接引用最快改动最小无法调试库内部只集成不折腾引用源码工程能下断点跟进去编译时间更长工程文件偏老研究源码、排疑难 bugNuGet 包版本干净还原方便定制改动不方便快速起步、原型验证如果你只是想让项目跑起来DLL 引用最快如果你打算长期在这个库上做二次开发比如调整停靠预览颜色、修改自动隐藏条的绘制那就直接引源码工程省得每次用反编译工具找逻辑。4. 用 DockContent 写第一个可停靠窗口最小复现路径与三个必调属性4.1 新建工程与主窗体布局一个 DockPanel 占满窗口前面的铺垫到这里该落地了。新建一个 WinForms 工程目标框架按你的业务环境选之后在主窗体里声明字段并在构造函数里搭好容器。public partial class MainForm : Form { private WeifenLuo.WinFormsUI.Docking.DockPanel _dockPanel; private OutputWindow _outputWindow; public MainForm() { InitializeComponent(); // 1. DockPanel 是布局容器它必须占满主窗体剩余空间 _dockPanel new WeifenLuo.WinFormsUI.Docking.DockPanel { Dock DockStyle.Fill, DocumentStyle DocumentStyle.DockingWindow }; // 2. 最后加入 Controls让它垫底避免盖住工具栏和状态栏 Controls.Add(_dockPanel); // 3. 首次启动直接显示输出窗口位置由 ShowHint 决定 _outputWindow new OutputWindow { ShowHint DockState.DockBottom }; _outputWindow.Show(_dockPanel); } }这里有两个细节。第一DockPanel 一定要最后 Add 到 Controls或者用Controls.SetChildIndex把它放到最底层否则它会遮住先加进来的菜单栏第二DocumentStyle.DockingWindow让文档区也以停靠窗口的形式存在适合内部工具。如果你的产品需要多文档 MDI 风格再换成对应的 DocumentStyle 枚举值。4.2 定义第一个 DockContent三个必调属性一次配好创建 OutputWindow 类继承 DockContent。DockContent 从 Form 来所以你在构造函数里添加控件、设置 Text 的方式与普通窗体完全一致。using WeifenLuo.WinFormsUI.Docking; public class OutputWindow : DockContent { private TextBox _textBox; public OutputWindow() { // 窗口标题与标签文字可以分开设置 Text 输出; TabText 输出; // DockAreas 用位或组合声明允许出现的位置 DockAreas DockAreas.DockLeft | DockAreas.DockRight | DockAreas.DockBottom | DockAreas.DockTop | DockAreas.Document; // 关闭按钮变成隐藏而不是销毁配合菜单重新打开 HideOnClose true; _textBox new TextBox { Dock DockStyle.Fill, Multiline true, ReadOnly true }; Controls.Add(_textBox); } }三个必调属性DockAreas、HideOnClose、Text/TabText。DockAreas 不设置时默认行为是哪些地方都不能停靠窗口只能浮动或隐藏HideOnClose 一旦忘了设用户点右上角 X窗口实例会被 Dispose再想从菜单打开就得重新 New 一个TabText 影响标签页文字Text 影响标题栏两者不一致时优先检查 TabText。4.3 在主窗体里挂载窗口Show 方法才是唯一入口using System.Linq; private void ShowOutputWindow() { // 同一窗口只保留一份实例先查找已存在的 OutputWindow var existing _dockPanel.Contents .OfTypeOutputWindow() .FirstOrDefault(); if (existing ! null) { // 已存在就显示如果之前是 Hidden 状态这里也会恢复出来 existing.Show(); return; } var window new OutputWindow(); window.Show(_dockPanel); }_dockPanel.Contents返回所有已挂载的 IDockContent用 OfType 筛选类型是常见做法。这里容易踩的坑是不做查重每次点菜单都 New 一个 OutputWindow结果用户开五个输出窗口状态还各自独立。窗口显示状态复杂时还可以把existing.DockState DockState.Hidden作为判断条件区分“从未打开”和“只是隐藏”。4.4 显示与隐藏的菜单联动状态不同步是 WinForms 老问题有了窗口还需要菜单或工具栏按钮来控制它。我一般把按钮的勾选状态直接绑定到窗口状态private void tsbOutput_Click(object sender, EventArgs e) { if (_outputWindow null) { _outputWindow new OutputWindow(); _outputWindow.Show(_dockPanel); return; } if (_outputWindow.DockState DockState.Hidden) _outputWindow.Show(_dockPanel); else _outputWindow.Hide(); }注意这里的 Hide 是显式隐藏窗口实例还在和 HideOnClose 是两码事。HideOnClose 影响的是用户点关闭按钮时的行为显式 Hide 影响的是程序主动隐藏时的行为。两者配合才能做到“菜单能开能关、用户也能手动关、关完还能再打开”。5. 避坑源码编译、布局读写与高 DPI 的 5 次翻车记录5.1 编译报错“命名空间中不存在 Docking”现象已经引用了项目或 DLL写using WeifenLuo.WinFormsUI.Docking;依然红色波浪线编译直接失败。原因最常见是同一解决方案里既有 NuGet 包引用又引进了源码工程两个来源的程序集版本打架另一个常见原因是目标框架不匹配比如 .NET 6 项目引用了 net40 编译的老版本 DLL类型加载器直接拒绝。解决在引用管理器里把所有名字里带 Docking 的引用列出来只保留一份。如果必须用源码工程就先删掉 NuGet 包。目标框架方面老项目建议统一到 .NET Framework 4.x 的对应版本新项目优先找支持 netstandard2.0 的分支或 tag。5.2 布局保存后再 LoadFromXml窗口全部变成浮动现象SaveAsXml 保存的 XML 没有报错再 LoadFromXml 之后所有窗口漂在左上角停靠关系全部丢失。原因LoadFromXml 需要一个反序列化回调你没有传库拿到 persistString 后不知道该创建哪种 DockContent只能用空内容顶上布局自然全乱。另一个隐性原因是保存和加载时 DocumentStyle 不一致导致文档区节点识别失败。解决实现DeserializeDockContent委托并在加载时传进去private IDockContent DeserializeContent(string persistString) { // persistString 是随布局 XML 保存的内容标识默认是类型全名 if (persistString typeof(OutputWindow).ToString()) { return new OutputWindow(); } return null; } // 加载时这样调用 _dockPanel.LoadFromXml(layout.xml, DeserializeContent);提示如果业务类型名在重构后变了旧布局文件会全部失效。稳妥做法是在每个 DockContent 里覆写 PersistString 属性给一个稳定字符串标识。5.3 DockContent 点右上角 X 之后窗口彻底消失现象用户关掉一个面板再从菜单里打开找不到窗口实例程序只能重新 New刚填的搜索条件、滚动位置全没了。原因DockContent 继承自 Form默认 Close 会触发 Dispose窗口对象被销毁DockPanel.Contents 中不再有它。解决设置HideOnClose true是最直接的办法。如果还想再控制一下重写 OnClosing 把关闭改成隐藏protected override void OnClosing(CancelEventArgs e) { // 把窗口关闭按钮变成“隐藏”保留对象供下次显示 e.Cancel true; Hide(); base.OnClosing(e); }这里要和菜单里的显式 Hide 区分菜单里是主动隐藏OnClosing 是拦截用户行为。两处都覆盖行为就完全可控。5.4 高 DPI 屏幕下拖拽停靠预览错位现象在 150% 缩放的屏幕上拖动 DockContent蓝色停靠指示框和鼠标位置明显对不上松手后窗口落位也不是用户预期的地方。原因WinForms 进程默认不启用 PerMonitorV2 DPI 感知DockPanel 的停靠计算基于物理像素而窗体坐标基于逻辑像素缩放下两者偏移。解决在 Program.cs 启动入口设置高 DPI 模式Application.SetHighDpiMode(HighDpiMode.PerMonitorV2);同时给 app.manifest 加 dpiAware 声明保证从进程启动阶段就按正确模式创建窗体application xmlnsurn:schemas-microsoft-com:asm.v3 windowsSettings dpiAware xmlnshttp://schemas.microsoft.com/SMI/2005/WindowsSettingstrue/pm/dpiAware /windowsSettings /application设置后再测拖拽指示框和落点应该对齐。这个问题在 4K 显示器上几乎是必现的越早处理越好。5.5 老示例工程包还原失败项目直接无法加载现象打开源码解决方案Visual Studio 提示 packages 缺失或者 NuGet 还原失败工程树里示例项目显示灰色不可用。原因源码维护跨度长老工程用 packages.config新工程用 PackageReference混合状态下 VS 的自动还原容易顾此失彼。解决先手动还原一次。nuget restore solution.sln如果还原后仍然失败打开 csproj 看引用节点。老式 packages.config 项目的引用路径通常带..\packages\前缀检查 packages 文件夹确实存在新式 PackageReference 项目则看包名与版本是否有效。实在不行就把本地 packages 文件夹整个删掉重新还原比逐个排查快得多。6. 进阶布局持久化、源码断点与 DockState 联动6.1 布局记忆两行代码存下用户的停靠习惯工具类软件最实用的功能是把布局存下来下次启动原样恢复。SaveAsXml 和 LoadFromXml 的分工很清晰// 保存布局 _dockPanel.SaveAsXml(layout.xml, Encoding.UTF8); // 加载布局必须提供反序列化回调 _dockPanel.LoadFromXml(layout.xml, DeserializeContent);保存时每个 DockContent 会把 PersistString 写进 XML加载时回调负责按 PersistString 创建对应实例。默认的 PersistString 是类型全名但建议在每个 DockContent 子类里显式指定我一般用常量字符串避免类名重构后布局失联。6.2 源码调试三类最优断点位置引用源码工程的好处是能直接下断点。第一类断点放在 DockContent 的属性赋值处看是谁把状态改成了 Float 或 Hidden第二类放在 DockPanel 布局引擎遍历 pane 的地方观察拆分方向和比例如何计算第三类放在停靠预览绘制处看鼠标命中哪个停靠区域。调试时打开“调用堆栈”窗口能很快看出拖拽操作是从哪个事件入口触发到这里的比自己按 F11 一步一步追快得多。6.3 DockStateChanged 联动界面状态菜单勾选、工具栏按钮 Enabled、面板标题栏文字这些都可以挂到 DockStateChanged 上。窗口被拖到新位置、进入自动隐藏、变成浮动时事件都会触发界面状态跟着同步避免传统 WinForms 里“数据变了按钮没变”的脱节问题。只要注意第五节讲的 Float 中间态在事件里判断最终状态而不是第一个动作联动就可靠。这么多年下来我在这套库上翻车最多的地方永远是序列化要么忘写回调要么 PersistString 被重构改掉。现在我的习惯是每个 DockContent 子类都显式给 PersistString并在构造函数里就挂好状态日志宁可初始化多两行也不要等业务逻辑复杂了再回来猜状态。希望帮到你。本文还有配套的精品资源点击获取
返回列表