// 停靠布局保存/恢复(设置页签「保存布局」按钮)。
// 用 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 + " 保存时是关闭状态,保持隐藏");
}
}
}
}
}