// 停靠布局保存/恢复(设置页签「保存布局」按钮)。 // 用 DockPanelSuite 自带的 SaveAsXml/LoadFromXml: // 保存 = 哪些窗口开着、停靠在哪(左/右/上/下/浮动/文档)、大小、比例、激活窗口; // 恢复 = 程序启动时若存在 停靠布局.xml 就按它重排,否则用默认布局。 using System; using System.IO; using System.Windows.Forms; namespace 成套报价软件 { public partial class MainForm { /// 布局文件路径(与 exe 同目录)。 private string 停靠布局路径 { get { return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "停靠布局.xml"); } } /// 激活窗口记录文件(独立小文件,不碰布局 XML,避免编码破坏 XML)。 private string 激活窗口路径 { get { return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "停靠布局.激活.txt"); } } /// 窗口内部分割条位置记录文件(SplitContainer 的 SplitterDistance)。 private string 分割位置路径 { get { return Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "停靠布局.分割.txt"); } } /// 构造函数里 CreateDockWindows 之后调用:抓取窗口实例 + 挂按钮 + 恢复上次布局。 internal void 初始化停靠布局() { 捕获停靠窗口实例(); // 保存布局按钮已迁到功能区(ribbonStrip1),点击统一走 ribbonStrip1_ButtonClick 路由 // 分割条位置恢复(Load 一次 + 800ms 后再补一次): // Load 时停靠面板尚未完成首次渲染,随后的窗口尺寸调整会让 SplitContainer // 按比例重算分割条,把已恢复的位置冲掉——延迟二次恢复在最终尺寸定型后生效 this.Load += (s, e) => { 恢复分割位置(); var t = new Timer(); t.Interval = 800; t.Tick += (s2, e2) => { t.Stop(); t.Dispose(); 恢复分割位置(); 校验分割位置(); 恢复激活窗口延迟执行(); }; t.Start(); }; if (File.Exists(停靠布局路径)) { try { try { this.dockSurface1.LoadFromXml(停靠布局路径, 反序列化停靠内容); } catch (InvalidOperationException) { // 默认 Show 已执行时走这里:先把窗口全部隐藏, // 让面板回到未初始化状态,再按布局加载 Log("[停靠布局] 面板已初始化,先隐藏默认窗口再按布局恢复"); foreach (var kv in 全部停靠窗口()) { if (kv.Value != null && kv.Value.Visible) kv.Value.Hide(); } this.dockSurface1.LoadFromXml(停靠布局路径, 反序列化停靠内容); } 补齐缺失窗口实例(); 关闭布局中不存在的窗口(); // ★ AI助手兜底显示:标签保证在(引擎后台自启,点过去即用); // 激活窗口不在构造期切回——窗体未显示时 Activate 是哑的(实测谁最后 // Show 谁就是活动标签,AI助手抢焦点的真凶),改由 Shown+800ms 延迟恢复 AI助手窗口兜底显示(); Log("[停靠布局] 已恢复上次保存的布局"); } catch (Exception ex) { Log("[停靠布局] 恢复失败,回退默认布局: " + ex.Message); // ★ 默认 Show 已被跳过,恢复失败必须补显示,否则停靠区会整个空白 按默认布局显示(); } } else { // 无布局文件(默认 Show 已执行):实例已在 捕获停靠窗口实例 里抓齐 } // 无论哪条路径,最后都把设计器里配置的窗口属性重新应用一遍 // (存在布局文件时窗口实例由本文件按需创建,不带设计器生成代码里的属性) 应用设计器窗口设置(); // ★ AI助手窗口兜底(布局文件里没有它的旧布局/无布局文件/回退默认路径): // 标签在但不当活动标签(激活切回统一走下面的延迟恢复) AI助手窗口兜底显示(); // ★ 活动标签延迟切回:初始化停靠布局跑在构造函数里,窗体未显示时 Activate 不生效 // (实测被"最后 Show 的窗口"覆盖,AI助手抢焦点的真凶)——Shown 首显后立即一次 // +上面 800ms 定时器再补一次,双保险幂等 this.Shown += (s5, e5) => 恢复激活窗口延迟执行(); // ★ 分割条首次显示补恢复(必须在布局恢复后挂:有布局文件时 w1~w9 在此之前是 null, // 窗口实例由 LoadFromXml 按需创建——开头挂会扑空;此刻实例已定型)。 // 非活动标签页窗口点开前尺寸小,保存值超范围被跳过——显示/激活时按真身尺寸补赋。 // VisibleChanged+Activated 双保险:标签点开/窗口激活任一触发即恢复。 foreach (var kv in 全部停靠窗口()) { if (kv.Value == null) { Log("[停靠布局] " + kv.Key + " 无实例,跳过补恢复挂接"); continue; } var w = kv.Value; string 前缀 = kv.Key.Substring(kv.Key.LastIndexOf('.') + 1); w.VisibleChanged += (s3, e3) => { if (w.Visible) 恢复窗口分割条(前缀, w); }; w.Activated += (s4, e4) => 恢复窗口分割条(前缀, w); } Log("[停靠布局] 分割条补恢复挂接完成(VisibleChanged+Activated)"); // ★ 恢复自检:打印每窗口最终停靠状态(与保存的 XML 对比定位"没用布局文件"问题) try { foreach (var kv in 全部停靠窗口()) { if (kv.Value == null) { Log("[布局自检] " + kv.Key + " = null"); continue; } Log("[布局自检] " + kv.Key + " Visible=" + kv.Value.Visible + " DockState=" + kv.Value.DockState + " AutoHide=" + (kv.Value.DockState == WeifenLuo.WinFormsUI.Docking.DockState.DockLeftAutoHide || kv.Value.DockState == WeifenLuo.WinFormsUI.Docking.DockState.DockRightAutoHide || kv.Value.DockState == WeifenLuo.WinFormsUI.Docking.DockState.DockTopAutoHide || kv.Value.DockState == WeifenLuo.WinFormsUI.Docking.DockState.DockBottomAutoHide)); } } catch (Exception ex) { Log("[布局自检] 自检输出失败: " + ex.Message); } } /// AI助手窗口兜底显示:不在布局文件里(旧布局保存于窗口诞生前)也保证标签存在—— /// 引擎随软件后台自启,用户把页签点过去即用;所有调用点都在"恢复激活窗口"之前, /// 活动标签始终按保存布局落,AI助手不当启动焦点(2026-09-21 用户要求)。 private void AI助手窗口兜底显示() { try { if (this.w10 == null) this.w10 = new DockWindow10(); if (!this.w10.Visible) this.w10.Show(this.dockSurface1, WeifenLuo.WinFormsUI.Docking.DockState.DockRight); } catch (Exception ex) { Log("[停靠布局] AI助手窗口显示失败: " + ex.Message); } } /// 把设计器 DockWindows 集合里配置的属性(固定位置/显示关闭按钮) /// 应用到当前窗口实例。设计器改属性后不必关心代码路径,启动时统一回放。 private void 应用设计器窗口设置() { try { foreach (Smart.CustomComponents.DockWindows.DockWindowItem item in this.dockSurface1.DockWindows) { var w = 按类名找窗口(item.ClassName); if (w == null) continue; if (!item.ShowCloseButton) w.CloseButtonVisible = false; // ★ FixedPosition 锁定窗口允许的停靠区(用户要求:右侧工具栏位置固定)。 // 注:布局保存/恢复不受此影响(SaveAsXml 记录 DockState,固定窗口恢复后仍在原区); // 之前"保存布局不生效"的真因是分割条绝对像素问题,已改按比例保存恢复。 if (item.FixedPosition) w.DockAreas = 对应DockAreas(item.DockState); } Log("[停靠布局] 已应用设计器窗口设置(固定位置/关闭按钮)"); } catch (Exception ex) { Log("[停靠布局] 应用设计器窗口设置失败: " + ex.Message); } } /// 设计器集合里的 ClassName → 窗口实例。 private WeifenLuo.WinFormsUI.Docking.DockContent 按类名找窗口(string className) { switch ((className ?? "").Trim()) { case "DockWindow": return this.w1; case "DockWindow2": return this.w2; case "DockWindow3": return this.w3; case "DockWindow4": return this.w4; case "DockWindow5": return this.w5; case "DockWindow6": return this.w6; case "DockWindow7": return this.w7; case "DockWindow8": return this.w8; case "DockWindow9": return this.w9; case "DockWindow10": return this.w10; } return null; } /// FixedPosition=true 时,按设计器 DockState 锁定窗口允许的停靠区 /// (映射表与"生成停靠窗口代码"命令一致)。 private static WeifenLuo.WinFormsUI.Docking.DockAreas 对应DockAreas(WeifenLuo.WinFormsUI.Docking.DockState state) { switch (state) { case WeifenLuo.WinFormsUI.Docking.DockState.DockLeft: case WeifenLuo.WinFormsUI.Docking.DockState.DockLeftAutoHide: return WeifenLuo.WinFormsUI.Docking.DockAreas.DockLeft; case WeifenLuo.WinFormsUI.Docking.DockState.DockRight: case WeifenLuo.WinFormsUI.Docking.DockState.DockRightAutoHide: return WeifenLuo.WinFormsUI.Docking.DockAreas.DockRight; case WeifenLuo.WinFormsUI.Docking.DockState.DockTop: case WeifenLuo.WinFormsUI.Docking.DockState.DockTopAutoHide: return WeifenLuo.WinFormsUI.Docking.DockAreas.DockTop; case WeifenLuo.WinFormsUI.Docking.DockState.DockBottom: case WeifenLuo.WinFormsUI.Docking.DockState.DockBottomAutoHide: return WeifenLuo.WinFormsUI.Docking.DockAreas.DockBottom; case WeifenLuo.WinFormsUI.Docking.DockState.Document: return WeifenLuo.WinFormsUI.Docking.DockAreas.Document; case WeifenLuo.WinFormsUI.Docking.DockState.Float: return WeifenLuo.WinFormsUI.Docking.DockAreas.Float; } return WeifenLuo.WinFormsUI.Docking.DockAreas.DockLeft | WeifenLuo.WinFormsUI.Docking.DockAreas.DockRight | WeifenLuo.WinFormsUI.Docking.DockAreas.DockTop | WeifenLuo.WinFormsUI.Docking.DockAreas.DockBottom | WeifenLuo.WinFormsUI.Docking.DockAreas.Document; } /// 布局里没出现的窗口(保存时被用户关掉的)也要有实例, /// 业务代码通过别名访问它们的控件时才不会空引用——补出来,保持隐藏。 private void 补齐缺失窗口实例() { if (this.w1 == null) this.w1 = new DockWindow(); if (this.w2 == null) this.w2 = new DockWindow2(); if (this.w3 == null) this.w3 = new DockWindow3(); if (this.w4 == null) this.w4 = new DockWindow4(); if (this.w5 == null) this.w5 = new DockWindow5(); if (this.w6 == null) this.w6 = new DockWindow6(); if (this.w7 == null) this.w7 = new DockWindow7(); if (this.w8 == null) this.w8 = new DockWindow8(); if (this.w9 == null) this.w9 = new DockWindow9(); if (this.w10 == null) this.w10 = new DockWindow10(); } /// 从停靠面板的 Contents 里按类型抓取 9 个窗口实例,填到 w1..w9 字段。 /// 只在窗口已显示时有效(无布局文件的默认启动); /// 有布局文件时窗口由 LoadFromXml 按需创建,见 反序列化停靠内容/补齐缺失窗口实例。 internal void 捕获停靠窗口实例() { foreach (WeifenLuo.WinFormsUI.Docking.IDockContent c in this.dockSurface1.Contents) { if (this.w1 == null && c is DockWindow) this.w1 = (DockWindow)c; else if (this.w2 == null && c is DockWindow2) this.w2 = (DockWindow2)c; else if (this.w3 == null && c is DockWindow3) this.w3 = (DockWindow3)c; else if (this.w4 == null && c is DockWindow4) this.w4 = (DockWindow4)c; else if (this.w5 == null && c is DockWindow5) this.w5 = (DockWindow5)c; else if (this.w6 == null && c is DockWindow6) this.w6 = (DockWindow6)c; else if (this.w7 == null && c is DockWindow7) this.w7 = (DockWindow7)c; else if (this.w8 == null && c is DockWindow8) this.w8 = (DockWindow8)c; else if (this.w9 == null && c is DockWindow9) this.w9 = (DockWindow9)c; else if (this.w10 == null && c is DockWindow10) this.w10 = (DockWindow10)c; } } /// 按默认布局显示 9 个停靠窗口(与 CreateDockWindows 的默认位置一致)。 private void 按默认布局显示() { try { 补齐缺失窗口实例(); this.w1.Show(this.dockSurface1, WeifenLuo.WinFormsUI.Docking.DockState.Document); this.w2.Show(this.dockSurface1, WeifenLuo.WinFormsUI.Docking.DockState.Document); this.w3.Show(this.dockSurface1, WeifenLuo.WinFormsUI.Docking.DockState.Document); this.w4.Show(this.dockSurface1, WeifenLuo.WinFormsUI.Docking.DockState.DockRight); this.w5.Show(this.dockSurface1, WeifenLuo.WinFormsUI.Docking.DockState.DockRight); this.w6.Show(this.dockSurface1, WeifenLuo.WinFormsUI.Docking.DockState.DockRight); this.w7.Show(this.dockSurface1, WeifenLuo.WinFormsUI.Docking.DockState.DockRight); this.w8.Show(this.dockSurface1, WeifenLuo.WinFormsUI.Docking.DockState.DockRight); this.w9.Show(this.dockSurface1, WeifenLuo.WinFormsUI.Docking.DockState.DockRight); this.w10.Show(this.dockSurface1, WeifenLuo.WinFormsUI.Docking.DockState.DockRight); // 默认布局最后 Show 的是 AI助手,不当活动标签——启动落在项目页 try { if (this.w1 != null) this.w1.Activate(); } catch { } } catch (Exception ex) { Log("[停靠布局] 默认布局显示失败: " + ex.Message); } } /// 「保存布局」按钮:把当前停靠布局写到 停靠布局.xml。 private void btn保存布局_Click(object sender, EventArgs e) { try { this.dockSurface1.SaveAsXml(停靠布局路径); 记录激活窗口(); 保存分割位置(); Log("[停靠布局] 已保存当前布局"); MessageBox.Show(this, "当前窗口布局已保存。\n" + "(包括:哪些窗口开着、停靠位置、窗口大小、当前选中的窗口、\n" + "以及各窗口内部左右/上下分割条的位置)\n" + "下次启动程序时自动恢复这个布局。", "保存布局", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { MessageBox.Show(this, "保存布局失败: " + ex.Message, "保存布局", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } /// 把当前激活(选中)的停靠窗口写到独立小文件,恢复时切回它。 /// 不改布局 XML——重新写 XML 会破坏 SaveAsXml 的 utf-16 编码声明。 private void 记录激活窗口() { try { // 点按钮时焦点在顶部选项卡上,ActiveContent 会是 null(闪断特性), // 用兼容层记录的"最后活动窗口"兜底 var active1 = this.dockSurface1.ActiveContent as WeifenLuo.WinFormsUI.Docking.DockContent; var active = active1 ?? this.tabControl2.最后活动窗口; string name = active != null ? active.GetType().ToString() : ""; File.WriteAllText(激活窗口路径, name, System.Text.Encoding.UTF8); Log("[停靠布局] 激活窗口=" + (name == "" ? "(无)" : name)); } catch (Exception ex) { Log("[停靠布局] 记录激活窗口失败: " + ex.Message); } } /// 按激活窗口记录文件,把保存时选中的窗口重新激活。 /// 启动后把活动标签切回保存值(有布局按记录激活,无布局落项目页)。 /// ★ AI助手永不作启动活动标签(用户要求):记录值是它时回落项目页。 /// 由 Shown 首显后与 800ms 定时器各调一次,幂等。 private void 恢复激活窗口延迟执行() { try { if (File.Exists(停靠布局路径)) { 恢复激活窗口(); } else if (this.w1 != null) { this.w1.Activate(); } } catch (Exception ex) { Log("[停靠布局] 恢复激活窗口失败: " + ex.Message); } } private void 恢复激活窗口() { try { if (!File.Exists(激活窗口路径)) return; string key = File.ReadAllText(激活窗口路径).Trim(); if (key == "") return; var c = 反序列化停靠内容(key) as WeifenLuo.WinFormsUI.Docking.DockContent; // ★ AI助手不作启动活动标签:旧记录被"常驻抢焦点"污染成它时,回落项目页 if (c is DockWindow10) c = this.w1; if (c != null && c.Visible) { c.Show(this.dockSurface1); c.Activate(); Log("[停靠布局] 已切回选中的窗口: " + key); } } catch (Exception ex) { Log("[停靠布局] 恢复激活窗口失败: " + ex.Message); } } // ============================================================ // 窗口内部分割条(SplitContainer)位置的保存/恢复 // 停靠库的布局 XML 只管窗口之间;分割条独立存到 停靠布局.分割.txt // ============================================================ /// 递归收集某窗口下所有 SplitContainer,行格式"窗口类名简写/控件名=距离"。 private void 收集分割条(Control parent, string 前缀, System.Collections.Generic.List lines) { foreach (Control c in parent.Controls) { var sc = c as SplitContainer; if (sc != null) { // ★ 记"距离,比例":SplitterDistance 是绝对像素,窗口宽度变化后同值视觉位置不同; // 比例=距离/总长,恢复时按当前宽度×比例赋值,任何窗口尺寸下视觉一致 int total = sc.Orientation == Orientation.Horizontal ? sc.Height : sc.Width; double ratio = total > 0 ? (double)sc.SplitterDistance / total : 0; lines.Add(string.Format(System.Globalization.CultureInfo.InvariantCulture, "{0}/{1}={2},{3:0.####}", 前缀, sc.Name, sc.SplitterDistance, ratio)); } if (c.Controls.Count > 0) 收集分割条(c, 前缀, lines); } } /// 递归收集某窗口下所有 SplitContainer 实例到字典(键同上)。 private void 收集分割条实例(Control parent, string 前缀, System.Collections.Generic.Dictionary map) { foreach (Control c in parent.Controls) { var sc = c as SplitContainer; if (sc != null && !map.ContainsKey(前缀 + "/" + sc.Name)) map.Add(前缀 + "/" + sc.Name, sc); if (c.Controls.Count > 0) 收集分割条实例(c, 前缀, map); } } /// 保存 9 个停靠窗口内所有分割条的位置。 private void 保存分割位置() { try { var lines = new System.Collections.Generic.List(); foreach (var kv in 全部停靠窗口()) { if (kv.Value == null) continue; string 前缀 = kv.Key.Substring(kv.Key.LastIndexOf('.') + 1); 收集分割条(kv.Value, 前缀, lines); } File.WriteAllLines(分割位置路径, lines.ToArray(), System.Text.Encoding.UTF8); Log("[停靠布局] 已保存分割条位置 " + lines.Count + " 项"); } catch (Exception ex) { Log("[停靠布局] 保存分割位置失败: " + ex.Message); } } /// 恢复分割条位置(在 Load 事件里调用,此时窗口尺寸已就位)。 private void 恢复分割位置() { try { if (!File.Exists(分割位置路径)) return; var map = new System.Collections.Generic.Dictionary(); foreach (var kv in 全部停靠窗口()) { if (kv.Value == null) continue; string 前缀 = kv.Key.Substring(kv.Key.LastIndexOf('.') + 1); 收集分割条实例(kv.Value, 前缀, map); } int applied = 0; foreach (string line in File.ReadAllLines(分割位置路径)) { int i = line.LastIndexOf('='); if (i <= 0) continue; string key = line.Substring(0, i); string val = line.Substring(i + 1).Trim(); // 格式:"距离,比例"(新) 或 "距离"(旧)——优先按比例换算(窗口宽度无关) int d; double ratio = 0; int comma = val.IndexOf(','); if (comma > 0) { int.TryParse(val.Substring(0, comma), out d); double.TryParse(val.Substring(comma + 1), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out ratio); } else if (!int.TryParse(val, out d)) continue; SplitContainer sc; if (!map.TryGetValue(key, out sc) || sc == null) continue; try { int total = sc.Orientation == Orientation.Horizontal ? sc.Height : sc.Width; if (ratio > 0 && total > 20) d = (int)(total * ratio); // 按比例换算 if (total > 20 && d >= sc.Panel1MinSize && d <= total - sc.Panel2MinSize - sc.SplitterWidth) { sc.SplitterDistance = d; applied++; } } catch { /* 单条失败不影响其余 */ } } Log("[停靠布局] 已恢复分割条位置 " + applied + " 项"); } catch (Exception ex) { Log("[停靠布局] 恢复分割位置失败: " + ex.Message); } } /// 单窗口分割条恢复(窗口首次显示时调用;此时句柄已建、尺寸已定,赋值能落住)。 private void 恢复窗口分割条(string 前缀, WeifenLuo.WinFormsUI.Docking.DockContent w) { try { if (w == null || !File.Exists(分割位置路径)) return; var map = new System.Collections.Generic.Dictionary(); 收集分割条实例(w, 前缀, map); int applied = 0; foreach (string line in File.ReadAllLines(分割位置路径)) { int i = line.LastIndexOf('='); if (i <= 0) continue; string key = line.Substring(0, i); if (!key.StartsWith(前缀 + "/")) continue; string val = line.Substring(i + 1).Trim(); int d; double ratio = 0; int comma = val.IndexOf(','); if (comma > 0) { int.TryParse(val.Substring(0, comma), out d); double.TryParse(val.Substring(comma + 1), System.Globalization.NumberStyles.Float, System.Globalization.CultureInfo.InvariantCulture, out ratio); } else if (!int.TryParse(val, out d)) continue; SplitContainer sc; if (!map.TryGetValue(key, out sc) || sc == null) continue; try { int total = sc.Orientation == Orientation.Horizontal ? sc.Height : sc.Width; if (ratio > 0 && total > 20) d = (int)(total * ratio); if (total > 20 && d >= sc.Panel1MinSize && d <= total - sc.Panel2MinSize - sc.SplitterWidth) { sc.SplitterDistance = d; applied++; } else Log("[停靠布局] " + key + " 显示时仍超范围(记录=" + d + " 可用=" + total + "),跳过"); } catch (Exception ex) { Log("[停靠布局] " + key + " 分割条恢复异常: " + ex.Message); } } if (applied > 0) Log("[停靠布局] " + 前缀 + " 首次显示补恢复分割条 " + applied + " 项"); } catch (Exception ex) { Log("[停靠布局] " + 前缀 + " 补恢复异常: " + ex.Message); } } /// 诊断:回读所有分割条实际值 vs 记录值(区分"被改回"与"显示未刷")。 private void 校验分割位置() { try { if (!File.Exists(分割位置路径)) return; var map = new System.Collections.Generic.Dictionary(); foreach (var kv in 全部停靠窗口()) { if (kv.Value == null) continue; string 前缀 = kv.Key.Substring(kv.Key.LastIndexOf('.') + 1); 收集分割条实例(kv.Value, 前缀, map); } foreach (string line in File.ReadAllLines(分割位置路径)) { int i = line.LastIndexOf('='); if (i <= 0) continue; string key = line.Substring(0, i); string val = line.Substring(i + 1).Trim(); int d; int comma = val.IndexOf(','); if (!int.TryParse(comma > 0 ? val.Substring(0, comma) : val, out d)) continue; SplitContainer sc; if (!map.TryGetValue(key, out sc) || sc == null) { Log("[分割校验] " + key + " 找不到实例"); continue; } Log("[分割校验] " + key + " 记录=" + d + " 实际=" + sc.SplitterDistance + (sc.SplitterDistance == d ? " ✓" : " ✗被改")); } } catch (Exception ex) { Log("[分割校验] 异常: " + ex.Message); } } /// LoadFromXml 回调:布局文件里的 PersistString → 对应停靠窗口实例。 /// 实例不存在时按需创建并缓存到 wN 字段(默认 Show 可能因布局恢复被跳过)。 private WeifenLuo.WinFormsUI.Docking.IDockContent 反序列化停靠内容(string persistString) { // PersistString 可能带 ";窗体文本" 后缀,先取分号前的类型名 int i = persistString != null ? persistString.IndexOf(';') : -1; string key = i > 0 ? persistString.Substring(0, i) : persistString; switch (key) { case "成套报价软件.DockWindow": if (this.w1 == null) this.w1 = new DockWindow(); return this.w1; case "成套报价软件.DockWindow2": if (this.w2 == null) this.w2 = new DockWindow2(); return this.w2; case "成套报价软件.DockWindow3": if (this.w3 == null) this.w3 = new DockWindow3(); return this.w3; case "成套报价软件.DockWindow4": if (this.w4 == null) this.w4 = new DockWindow4(); return this.w4; case "成套报价软件.DockWindow5": if (this.w5 == null) this.w5 = new DockWindow5(); return this.w5; case "成套报价软件.DockWindow6": if (this.w6 == null) this.w6 = new DockWindow6(); return this.w6; case "成套报价软件.DockWindow7": if (this.w7 == null) this.w7 = new DockWindow7(); return this.w7; case "成套报价软件.DockWindow8": if (this.w8 == null) this.w8 = new DockWindow8(); return this.w8; case "成套报价软件.DockWindow9": if (this.w9 == null) this.w9 = new DockWindow9(); return this.w9; case "成套报价软件.DockWindow10": if (this.w10 == null) this.w10 = new DockWindow10(); return this.w10; } return null; } /// 9 个停靠窗口的(类型名, 实例)清单。 private System.Collections.Generic.KeyValuePair[] 全部停靠窗口() { return new System.Collections.Generic.KeyValuePair[] { new System.Collections.Generic.KeyValuePair("成套报价软件.DockWindow", this.w1), new System.Collections.Generic.KeyValuePair("成套报价软件.DockWindow2", this.w2), new System.Collections.Generic.KeyValuePair("成套报价软件.DockWindow3", this.w3), new System.Collections.Generic.KeyValuePair("成套报价软件.DockWindow4", this.w4), new System.Collections.Generic.KeyValuePair("成套报价软件.DockWindow5", this.w5), new System.Collections.Generic.KeyValuePair("成套报价软件.DockWindow6", this.w6), new System.Collections.Generic.KeyValuePair("成套报价软件.DockWindow7", this.w7), new System.Collections.Generic.KeyValuePair("成套报价软件.DockWindow8", this.w8), new System.Collections.Generic.KeyValuePair("成套报价软件.DockWindow9", this.w9), new System.Collections.Generic.KeyValuePair("成套报价软件.DockWindow10", this.w10), }; } /// 保存布局时被用户关掉的窗口不会出现在布局文件里,恢复后把它们隐藏, /// 让界面和保存时完全一致。 private void 关闭布局中不存在的窗口() { string xml = File.ReadAllText(停靠布局路径); foreach (var kv in 全部停靠窗口()) { if (kv.Value == null) continue; bool inLayout = xml.IndexOf(kv.Key, StringComparison.Ordinal) >= 0; if (!inLayout && kv.Value.Visible) { kv.Value.Hide(); Log("[停靠布局] " + kv.Key + " 保存时是关闭状态,保持隐藏"); } } } } }