Files

1047 lines
60 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/*
* 主窗体构造函数(由旧项目 主窗口.cs 迁移)。
*/
using System;
using System.Collections.Generic;
using System.Data;
using System.Diagnostics;
using System.Drawing;
using System.IO;
using System.Windows.Forms;
using Microsoft.Win32;
using Smart.CustomComponents;
namespace 成套报价软件
{
/// <summary>
/// 主窗体:构造函数迁移自旧项目 主窗口.cs(事件挂接全部保留)。
/// </summary>
public partial class MainForm : Form
{
/// <summary>启动时待导入的项目 zip 路径(cadbt 导出联动传入;空=正常启动)。</summary>
private string _startupZip;
// ===== 会员登录状态(登录成功后全程保持;会员信息/修改密码窗自动带手机号) =====
public static string 当前登录手机号 = "";
public static string 当前会员类型 = "";
public static string 当前有效期 = "";
/// <summary>当前登录密码(仅内存,不落盘):新版会员协议要求项目云上传/下载/列表带密码参数。</summary>
public static string 当前登录密码 = "";
/// <summary>改密重启标记:修改密码成功后关闭主窗口,Program 在释放单实例锁后启动新实例
/// (新实例启动先弹登录窗,客户用新密码重新登录)。</summary>
internal static bool 改密重启;
public MainForm() : this(null) { }
public MainForm(string startupZip)
{
_startupZip = startupZip;
InitializeComponent();
this.BackColor = System.Drawing.Color.White; // ★ 全窗口纯白背景(2026-09-24 用户要求)
// 窗体/任务栏图标:从 exe 内嵌图标提取(编译时 /win32icon 嵌入)
try {
this.Icon = System.Drawing.Icon.ExtractAssociatedIcon(
System.Windows.Forms.Application.ExecutablePath);
} catch { }
CreateDockWindows_dockSurface1();
// 恢复上次保存的停靠布局(设置页「保存布局」按钮保存的),无布局文件时用默认布局
初始化停靠布局();
// 旧 ui.cs 设计器注入的 SQLite 数据加载(Load 事件)
this.Load += (s, e) => LoadAllSqliteData();
// ★ 启动登录成功过的(Program.cs 先弹登录窗):状态栏显示已登录
if (!string.IsNullOrWhiteSpace(当前登录手机号))
this.Load += (s, e) => SetStatus("已登录: " + 当前登录手机号 + " (" + 当前会员类型 + ",有效期至 " + 当前有效期 + ")");
// ★ 项目云每日自动上传(每天首次启动把全部项目静默传到会员服务器;本地模式不传)
this.Load += (s, e) => 启动项目云自动上传();
// ★ AI助手本地接口(127.0.0.1:18790,令牌鉴权;AI×选型页断路器选型替换用)
this.Load += (s, e) => 启动AI助手接口();
// ★ AI引擎随软件启动:Load 后台拉起(已运行则跳过;双击exe/快捷方式/启动器任何入口都生效)
this.Load += (s, e) => System.Threading.ThreadPool.QueueUserWorkItem(delegate { OpenClawHost.启动引擎(); });
// ★ 性能优化:ui.cs 已经自带优化版选中效果(HashSet 缓存 _esSelMatrix,O(1) 查询),
// 不需要摘除。这里只补做一件事——开启 DoubleBuffered(双缓冲),
// 消除多选拖动时的重绘闪烁。ui.cs 没设这个属性(它是 protected,设计器够不到)。
EnableDoubleBuffered(this.advancedDataGridView9);
EnableDoubleBuffered(this.advancedDataGridView4);
EnableDoubleBuffered(this.advancedDataGridView5);
// ★ 功能区按钮图标(exe 旁 icons\ 目录,缺失时跳过不影响启动)
加载功能区图标();
// ★ 所有数据表格:行高不可拖动调整(高度值保持各表现状)
禁止行高调整(this);
// ★ 虚拟模式:三表不再存数据(DataSource 弃用),由内存 DataTable 按需供数。
// 行数再多也秒开,刷新只更新可见区域,粘贴/重算管线可移入后台线程。
this.advancedDataGridView9.VirtualMode = true;
this.advancedDataGridView4.VirtualMode = true;
this.advancedDataGridView5.VirtualMode = true;
this.advancedDataGridView9.CellValueNeeded += Dgv_CellValueNeeded;
this.advancedDataGridView4.CellValueNeeded += Dgv_CellValueNeeded;
this.advancedDataGridView5.CellValueNeeded += Dgv_CellValueNeeded;
this.advancedDataGridView9.CellValuePushed += Dgv_CellValuePushed;
this.advancedDataGridView4.CellValuePushed += Dgv_CellValuePushed;
this.advancedDataGridView5.CellValuePushed += Dgv_CellValuePushed;
// ★ 母排元件行不受"小数位/取整方式"设置限制(价格列按自然值显示),见 Grid5_母排CellFormatting
this.advancedDataGridView5.CellFormatting += Grid5_母排CellFormatting;
// ★ 状态栏:时钟启动(Load 后再刷一次项目名等信息)
启动状态栏时钟();
// ★ 状态栏:选区求和(三表选中数值自动求和,Excel 风格)
this.advancedDataGridView9.SelectionChanged += 选区求和_Refresh;
this.advancedDataGridView4.SelectionChanged += 选区求和_Refresh;
this.advancedDataGridView5.SelectionChanged += 选区求和_Refresh;
this.advancedDataGridView9.CellValueChanged += 选区求和_Refresh;
this.advancedDataGridView4.CellValueChanged += 选区求和_Refresh;
this.advancedDataGridView5.CellValueChanged += 选区求和_Refresh;
this.advancedDataGridView9.CellValuePushed += 选区求和_Refresh;
this.advancedDataGridView4.CellValuePushed += 选区求和_Refresh;
this.advancedDataGridView5.CellValuePushed += 选区求和_Refresh;
// ★ 按 Excel 模板(advancedDataGridView9Table.xlsx 第3行"不可编辑"标记)
// 设置各表的只读列。公式列(采购价/报出价/毛利/利润等)和序号列不允许双击编辑,
// 只有用户输入列(图号/箱号/数量/表价/系数等)可编辑。
// ui.cs 不改,运行时按列名(DataPropertyName)查找设置 ReadOnly。
ApplyReadOnlyColumns(this.advancedDataGridView6, new string[] {
// 项目表:全部13列不可编辑(项目信息通过新建/修改对话框改)
"项目编号", "项目名称", "客户名称", "客户地址", "联系人", "联系电话",
"项目日期", "箱柜台数", "成本总价", "报出总价", "利润", "利润率", "备注"
});
ApplyReadOnlyColumns(this.advancedDataGridView9, new string[] {
// 图号表:序号 + 6个公式列不可编辑;可编辑:图号/图名/台数
"序号", "箱柜数量", "单价", "总价", "成本总价", "毛利", "毛利率"
});
ApplyReadOnlyColumns(this.advancedDataGridView4, new string[] {
// 箱号表:序号 + 7个公式列不可编辑;可编辑:图号/箱号/型号/规格/单位/数量/箱柜类型/备注/图纸
"序号", "单台成本", "成本价", "单台报出", "报出价", "利润", "利润率", "毛利率"
});
ApplyReadOnlyColumns(this.advancedDataGridView5, new string[] {
// 元件表:序号 + 4个公式列不可编辑;可编辑:图号/箱号/元件名称/规格/单位/数量/表价/采购系数/报出系数/...
"序号", "采购单价", "采购价", "报出单价", "报出价"
});
// 启动时窗口显示在屏幕正中间(ui.cs 默认是 WindowsDefaultLocation)
this.StartPosition = FormStartPosition.CenterScreen;
// 把三个报价表格的选择模式从 RowHeaderSelect(点单元格选整行)
// 改成 CellSelect(点单元格只选那一个单元格)。
// 这样右键/点击时只选中一个单元格,不会高亮整行。
this.advancedDataGridView9.SelectionMode = DataGridViewSelectionMode.CellSelect;
this.advancedDataGridView4.SelectionMode = DataGridViewSelectionMode.CellSelect;
this.advancedDataGridView5.SelectionMode = DataGridViewSelectionMode.CellSelect;
// 原有按钮事件
// 企业信息按钮:打开企业信息设置窗口(信息持久化到 shezhi\企业信息.config.xml)
// 备份/恢复按钮:备份和恢复库数据(用户库/柜体库/方案库/收藏/企业信息)
// CAD扒图(button11):检测 CAD 插件是否已安装,未安装则用 PowerShell 运行安装脚本,
// 然后启动 AutoCAD 2018。
// ===== 项目表搜索(button28):根据 comboBox2 条件 + textBox1 关键词筛选 =====
this.button28.Click += button28_Click;
// 回车直接触发搜索
this.textBox1.KeyDown += (s, e) =>
{
if (e.KeyCode == Keys.Enter)
{
e.SuppressKeyPress = true;
button28_Click(s, e);
}
};
// ===== 用户库工具栏 toolStrip1 的事件绑定 =====
// 插入/替换:把用户库(advancedDataGridView7)选中行的数据
// 插入或替换到元件表(advancedDataGridView5)
this.toolStripButton1.Click += UserLib_Insert_Click; // 插入
this.toolStripButton2.Click += UserLib_Replace_Click; // 替换
// 新增/修改/删除:对用户库(advancedDataGridView7)本身的编辑操作
this.toolStripButton3.Click += UserLib_New_Click; // 新增
this.toolStripButton4.Click += UserLib_Modify_Click; // 修改
this.toolStripButton5.Click += UserLib_Delete_Click; // 删除
// 导入/导出/打开模板:用户库的批量数据交换
this.toolStripButton6.Click += UserLib_Import_Click; // 导入
this.toolStripButton7.Click += UserLib_Export_Click; // 导出
this.toolStripButton8.Click += UserLib_OpenTemplate_Click;// 打开模板
// toolStripButton11: 清除(清空用户库表所有数据)
this.toolStripButton11.Click += UserLib_Clear_Click;
// 用户库编辑后自动保存(改完单元格失焦时触发,避免数据丢失)
this.advancedDataGridView7.CellEndEdit += (s2, e2) => SaveUserLibToDatabase();
// ===== 用户库筛选/查找(comboBox3 + textBox2 + button30) =====
// comboBox3:选择元件名称 → 用户库只显示该名称的行(选"全部"恢复)
this.comboBox3.SelectedIndexChanged += UserLib_NameFilter_Changed;
// button30:按"元件名称"和"规格"两列不区分大小写模糊查找(参考项目表 button28)
this.button30.Click += UserLib_Search_Click;
// textBox2:回车也触发查找
this.textBox2.KeyDown += (s, e) =>
{
if (e.KeyCode == System.Windows.Forms.Keys.Enter)
{
e.SuppressKeyPress = true;
UserLib_Search_Click(s, EventArgs.Empty);
}
};
// ===== 柜体库工具栏 toolStrip3 的事件绑定 =====
// 插入/替换:把柜体库(advancedDataGridView10)选中行的数据
// 插入或替换到元件表(advancedDataGridView5)
this.toolStripButton12.Click += GuiTi_Insert_Click; // 插入
this.toolStripButton13.Click += GuiTi_Replace_Click; // 替换
// 新增/修改/删除:对柜体库(advancedDataGridView10)本身的编辑操作
this.toolStripButton14.Click += GuiTi_New_Click; // 新增
this.toolStripButton15.Click += GuiTi_Modify_Click; // 修改
this.toolStripButton16.Click += GuiTi_Delete_Click; // 删除
// 柜体库编辑后自动保存(改完单元格失焦时触发,避免数据丢失)
this.advancedDataGridView10.CellEndEdit += (s2, e2) => SaveGuiTiToDatabase();
// ===== 柜体库筛选/查找(comboBox4 + textBox5 + button31) =====
// comboBox4:选择元件名称 → 柜体库只显示该名称的行(选"全部"恢复)
this.comboBox4.SelectedIndexChanged += GuiTi_NameFilter_Changed;
// button31:按"元件名称"和"规格"两列不区分大小写模糊查找
this.button31.Click += GuiTi_Search_Click;
// textBox5:回车也触发查找
this.textBox5.KeyDown += (s, e) =>
{
if (e.KeyCode == System.Windows.Forms.Keys.Enter)
{
e.SuppressKeyPress = true;
GuiTi_Search_Click(s, EventArgs.Empty);
}
};
// ===== 母排页(tabPage12)铜价刷新按钮:从长江有色网抓取 1#铜 均价和日期 =====
this.btn刷新.Click += btn刷新_Click;
// txt加工费:失焦时自动保存到配置文件(下次启动恢复)
this.txt加工费.Leave += (s, e) => Save铜排参数();
// txt铜价 自动联动:当前铜价(元/吨) 或 加工费(元/吨) 变化时,重算 (铜价+加工费)/1000 → 元/Kg
// (1 吨 = 1000 公斤,所以除以 1000)
this.txt当前铜价.TextChanged += (s, e) => Recalc铜价PerKg();
this.txt加工费.TextChanged += (s, e) => Recalc铜价PerKg();
// txt单价(元/米) 自动联动:铜价(kg)、规格(层/宽/厚)、材质(铜排/铝排) 任一变化时重算
// 公式: 单价(元/米) = 铜价(元/kg) × 层数 × 宽(mm) × 厚(mm) × 密度 / 1000
// (铜排 TMY 密度 8.9 g/cm³,铝排 LMY 密度 2.7 g/cm³)
this.txt铜价.TextChanged += (s, e) => Recalc单价PerMeter();
this.txt层数.TextChanged += (s, e) => Recalc单价PerMeter();
this.txt宽度.TextChanged += (s, e) => Recalc单价PerMeter();
this.txt厚度.TextChanged += (s, e) => Recalc单价PerMeter();
// 材质切换:重新加载规格表、切换标签/参数/价格(CheckedChanged 会在切换瞬间旧/新按钮各触发一次,
// 只在 Checked=true 时执行切换,避免重复)
this.rb铜排.CheckedChanged += (s, e) => { if (this.rb铜排.Checked) 切换母排材质(); };
this.rb铝排.CheckedChanged += (s, e) => { if (this.rb铝排.Checked) 切换母排材质(); };
// ===== 母排页 btn插入 / btn替换:把当前规格作为一行元件插入/替换到元件表(advancedDataGridView5) =====
// 字段对应:元件名称=母排,规格=txt规格,单位=comboBox1,数量=txt长度,表价=txt单价,类型=母排
this.btn插入.Click += btn插入_Click;
this.btn替换.Click += btn替换_Click;
// ===== GCS 铜排壳体计算窗口(停靠主窗口右侧) =====
// ★ G2-2(2026-10-01):原为母排停靠窗 button14 的闭包,提取为公共方法
// 打开GCS母排计算窗口()——功能区「计算」组按钮调用;母排页 button14 已删
// ===== 双击 advancedDataGridView6 单元格跳转到报价页 =====
this.advancedDataGridView6.CellDoubleClick += advancedDataGridView6_CellDoubleClick;
// ===== 三个增强表格的联动(SelectionChanged) =====
this.advancedDataGridView9.SelectionChanged += advancedDataGridView9_SelectionChanged;
this.advancedDataGridView4.SelectionChanged += advancedDataGridView4_SelectionChanged;
this.advancedDataGridView5.SelectionChanged += advancedDataGridView5_SelectionChanged;
// ===== 铜排规格表(tabPage12 / advancedDataGridView8)选中变化:解析规格填入 txt层数/txt宽度/txt厚度 =====
// 规则:
// 无括号 "TMY-15×3" → 层数=1, 宽度=15, 厚度=3
// 有括号 "TMY-2(60×6)" → 层数=2, 宽度=60, 厚度=6
this.advancedDataGridView8.SelectionChanged += advancedDataGridView8_SelectionChanged;
// ===== 项目表右键:加入方案库(整项目) =====
var _menuProject = new ContextMenuStrip();
_menuProject.Items.Add("加入方案库", null, MenuProject_AddToFak_Click);
this.advancedDataGridView6.ContextMenuStrip = _menuProject;
Log(" 项目表菜单已挂到 advancedDataGridView6(加入方案库)");
// ===== 方案窗口:项目页/三表联动/右键菜单已全部自治于 DockWindow8(数据源=方案库目录的方案文件) =====
// ===== 三个增强表格的公式实时重算(CellValueChanged) =====
// 编辑单元格后立即提交,触发 CellValueChanged,自底向上重算派生列。
this.advancedDataGridView5.CurrentCellDirtyStateChanged += Grid_DirtyStateChanged;
this.advancedDataGridView5.CellValueChanged += Yuanjian_CellValueChanged;
this.advancedDataGridView4.CurrentCellDirtyStateChanged += Grid_DirtyStateChanged;
this.advancedDataGridView4.CellValueChanged += Xianghao_CellValueChanged;
this.advancedDataGridView9.CurrentCellDirtyStateChanged += Grid_DirtyStateChanged;
this.advancedDataGridView9.CellValueChanged += Tuhao_CellValueChanged;
// ===== 调试日志:元件表编辑开始/结束 =====
this.advancedDataGridView5.CellBeginEdit += (s, e) =>
{
string cn = (e.ColumnIndex >= 0 && e.ColumnIndex < this.advancedDataGridView5.Columns.Count)
? this.advancedDataGridView5.Columns[e.ColumnIndex].HeaderText : "?";
Log("[BeginEdit] 元件表 行" + e.RowIndex + " 列[" + cn + "]");
};
this.advancedDataGridView5.CellEndEdit += (s, e) =>
{
if (e.ColumnIndex < 0 || e.ColumnIndex >= this.advancedDataGridView5.Columns.Count) return;
string cn = this.advancedDataGridView5.Columns[e.ColumnIndex].HeaderText;
string dp = this.advancedDataGridView5.Columns[e.ColumnIndex].DataPropertyName;
object v = this.advancedDataGridView5.Rows[e.RowIndex].Cells[e.ColumnIndex].Value;
Log("[EndEdit] 元件表 行" + e.RowIndex + " 列[" + cn + "] 值=" + (v ?? "(null)"));
// ★ 只转换公式列集合中的列(用户输入的数值列),其他列保持原样
string colKey = !string.IsNullOrEmpty(dp) ? dp : cn;
if (!s_公式列集合.Contains(colKey)) return;
if (v == null || v is double) return;
string str = v.ToString().Trim();
if (string.IsNullOrEmpty(str)) return;
double d;
if (double.TryParse(str, out d))
{
// ★ 虚拟模式:写内存表+刷新该行(写 Cells.Value 无效)
string colName = this.advancedDataGridView5.Columns[e.ColumnIndex].DataPropertyName;
if (string.IsNullOrEmpty(colName)) colName = this.advancedDataGridView5.Columns[e.ColumnIndex].HeaderText;
写单元格(this.advancedDataGridView5, e.RowIndex, colName, d);
}
};
// ===== 其他表格也加 CellEndEdit,确保公式列格式化一致生效 =====
this.advancedDataGridView4.CellEndEdit += (s, e) =>
{
if (e.ColumnIndex < 0 || e.ColumnIndex >= this.advancedDataGridView4.Columns.Count) return;
string dp = this.advancedDataGridView4.Columns[e.ColumnIndex].DataPropertyName;
string cn = this.advancedDataGridView4.Columns[e.ColumnIndex].HeaderText;
string colKey = !string.IsNullOrEmpty(dp) ? dp : cn;
if (!s_公式列集合.Contains(colKey)) return;
object v = this.advancedDataGridView4.Rows[e.RowIndex].Cells[e.ColumnIndex].Value;
if (v == null || v is double) return;
string str = v.ToString().Trim();
if (string.IsNullOrEmpty(str)) return;
double d;
if (double.TryParse(str, out d))
{
// ★ 虚拟模式:写内存表+刷新该行(写 Cells.Value 无效)
string colName = this.advancedDataGridView4.Columns[e.ColumnIndex].DataPropertyName;
if (string.IsNullOrEmpty(colName)) colName = this.advancedDataGridView4.Columns[e.ColumnIndex].HeaderText;
写单元格(this.advancedDataGridView4, e.RowIndex, colName, d);
}
};
this.advancedDataGridView9.CellEndEdit += (s, e) =>
{
if (e.ColumnIndex < 0 || e.ColumnIndex >= this.advancedDataGridView9.Columns.Count) return;
string dp = this.advancedDataGridView9.Columns[e.ColumnIndex].DataPropertyName;
string cn = this.advancedDataGridView9.Columns[e.ColumnIndex].HeaderText;
string colKey = !string.IsNullOrEmpty(dp) ? dp : cn;
if (!s_公式列集合.Contains(colKey)) return;
object v = this.advancedDataGridView9.Rows[e.RowIndex].Cells[e.ColumnIndex].Value;
if (v == null || v is double) return;
string str = v.ToString().Trim();
if (string.IsNullOrEmpty(str)) return;
double d;
if (double.TryParse(str, out d))
{
// ★ 虚拟模式:写内存表+刷新该行(写 Cells.Value 无效)
string colName = this.advancedDataGridView9.Columns[e.ColumnIndex].DataPropertyName;
if (string.IsNullOrEmpty(colName)) colName = this.advancedDataGridView9.Columns[e.ColumnIndex].HeaderText;
写单元格(this.advancedDataGridView9, e.RowIndex, colName, d);
}
};
// 吞掉 DataError(设了 ValueType=double 后,空值/非数字值可能触发)
this.advancedDataGridView5.DataError += (s, e) => { e.Cancel = true; };
this.advancedDataGridView4.DataError += (s, e) => { e.Cancel = true; };
this.advancedDataGridView9.DataError += (s, e) => { e.Cancel = true; };
this.advancedDataGridView6.DataError += (s, e) => { e.Cancel = true; };
// ★ 修复(2026-09-30):项目页选中行变化 → 刷新状态栏"项目:"标签
// (未进项目时按选中行显示项目名;进项目后按 _currentProjectId 显示)
this.advancedDataGridView6.SelectionChanged += (s, e) =>
{
if (!_isReloading) 刷新状态栏();
};
// ★ 多选移动适配(2026-09-30):报价三表挂"右键多选保持"钩子——
// 右键点已选中的行不清空多选集合(否则右键弹菜单瞬间多选被清成一行,移动只动一行)
HookKeepMulti(this.advancedDataGridView9);
HookKeepMulti(this.advancedDataGridView4);
HookKeepMulti(this.advancedDataGridView5);
// ★ 行头选择(2026-09-30):全部表格改 RowHeaderSelect(Excel 风格)——
// 点单元格=选格(不影响编辑),点行头=选中整行,Ctrl/Shift+点行头=多选整行。
// 递归扫描全控件树(主窗口所有页/方案页/选型页/项目页一次覆盖)。
ApplyRowHeaderSelectAll(this);
// ===== 创建并挂载右键菜单 =====
BuildContextMenus();
// ===== 箱体计算面板(tabPage14)事件绑定 =====
Init箱体计算面板();
// ===== 窗体关闭时把当前项目的报价数据回写数据库 =====
this.FormClosing += 主窗口_FormClosing;
// ===== 窗体加载完成后,由主窗口.cs 完全接管初始化状态 =====
// ui.cs 的 LoadAllSqliteData() 会先加载旧共享库数据(设计器自动生成,不能改),
// 这里在它执行完毕后重新接管:
// 1) 重新从项目库加载 advancedDataGridView6(覆盖 ui.cs 的结果)
// 2) 清空三个报价表(每个项目独立库,启动时不显示任何报价数据)
// 3) 报价数据只有在双击项目行时才从该项目的独立库加载
this.Load += (s, e) =>
{
// 重新加载项目表(由主窗口.cs 控制,确保数据来源正确)
ReloadProjectsFromDatabase();
// 项目表公式列设 Format(REAL 类型加载后是 double,Format 直接生效)
ApplyPriceFormat(this.advancedDataGridView6, _项目小数位文本);
// ★ 行头选择补扫(2026-09-30):覆盖 Load 后才挂载/绑定的表格
// (构造时的首扫覆盖不到延迟创建的 DockWindow)
try { ApplyRowHeaderSelectAll(this); } catch { }
// 清空三个报价表,避免显示旧共享库的历史数据
// ★ 虚拟模式:清空用 RowCount=0(设置 DataSource 会抛异常)
this.advancedDataGridView9.RowCount = 0;
this.advancedDataGridView4.RowCount = 0;
this.advancedDataGridView5.RowCount = 0;
// 刷新状态栏(项目/定位/数据统计)
刷新状态栏();
// 用户库启动后,把不重复的元件名称填入 comboBox3
// (ui.cs 的 LoadSqliteData_advancedDataGridView7 已在 InitializeComponent 里加载完毕)
LoadUserLibDistinctNames();
// 柜体库启动后,把不重复的元件名称填入 comboBox4
// (ui.cs 的 LoadSqliteData_advancedDataGridView10 已在 InitializeComponent 里加载完毕)
LoadGuiTiDistinctNames();
// 方案库只有一个 fak.db,启动时直接加载图号表数据
ReloadFakData();
};
// ===== 监听主区域 tabControl2 切换,离开报价页触发保存 =====
this.tabControl2.SelectedIndexChanged += (s, e) =>
{
// 诊断:确认 tabControl2 切换事件触发,记录当前页
string t2name = (this.tabControl2 != null && this.tabControl2.SelectedTab != null)
? (this.tabControl2.SelectedTab.Text ?? "(null)") : "(null)";
Log("[TabSync] tabControl2.SelectedIndexChanged 触发, 当前页=[" + t2name +
"] _isTabSyncing=" + _isTabSyncing + " _isReloading=" + _isReloading);
// 切换标签时同步启用/禁用网页上的插入/替换按钮(仅在报价页可用)
SyncQuoteButtonsState();
// 切到 tabControl2 某页时,tabControl1 同步切到同名页(按 Text 匹配)
// 用 guard 防止反向触发死循环(下方的 tabControl1.SelectedIndexChanged 也会做同样判断)
if (!_isTabSyncing)
{
SyncTab1ToTab2ByText();
}
// 切到"报价"页(tabPage4)时,如果当前没有打开任何项目,自动恢复最近打开过的项目
// (用户首次启动 / 主动清空 _currentProjectId 后再切回报价页时触发)
if (this.tabControl2 != null && this.tabControl2.SelectedTab == this.tabPage4
&& string.IsNullOrWhiteSpace(_currentProjectId))
{
AutoEnterLastProject();
}
// 离开报价页(从 tabPage4 切到别的 tab)时触发保存。
// _isReloading=true 表示是程序化切换(双击进项目等),跳过避免重复保存。
// ★ 停靠版修正:仅"报价 → 其他"的切换才保存(与旧版一致);
// 停靠面板任意两个非报价窗口之间切换不应触发全量保存。
if (_isReloading) return;
if (this.tabControl2 != null && this.tabControl2.SelectedTab != this.tabPage4
&& this.tabControl2.PreviousSelectedTab == this.tabPage4)
{
SaveAllToDatabase();
}
};
// ===== 功能区(ribbonStrip1):页签切换联动 =====
// ★ G1-2(2026-10-01):按钮点击路由已二合一——Designer 绑定的 RibbonStrip1ButtonClick
// 直接转调统一路由,不再手工二次绑定(避免双处理器)
// 页签切换检测:基库 RibbonStrip 的页签在 OnMouseDown 内切换且无事件,
// MouseUp 时索引必已更新,在此检测变化并联动停靠文档(等效原 tabControl1 联动)
this.ribbonStrip1.MouseUp += ribbonStrip1_MouseUpTabChanged;
// ★ G4(2026-10-01):状态栏"保存"标签点击=立即保存(与 Ctrl+S/功能区保存按钮同一实现)
this.lbl保存.Click += (s, e) =>
{
try { 强制保存_快捷键(); } catch (Exception exSv) { Log("[状态栏保存] " + exSv.Message); }
};
// ★ 启动参数导入:cadbt/pdfbt 导出联动(命令行带 zip 路径)→自动导入并打开该项目
if (!string.IsNullOrWhiteSpace(_startupZip) && System.IO.File.Exists(_startupZip))
{
this.Load += (s, e) => 执行启动导入(_startupZip);
}
// ★ 导入命令文件轮询:主程序已运行时,cadbt/pdfbt 的二次导出经
// Program.cs 单实例转发写命令文件,这里收命令并导入(不新开窗口)
启动导入命令轮询();
// ===== 启动价格查询网页(tabPage5 里的 webView21) =====
StartPriceWebView();
// 主窗口关闭时,确保 python 子进程被清理
this.FormClosing += (s, e) => StopProxyServer();
// ===== 元件库收藏功能已随本地选型页退役(2026-09-13 用户要求删控件;
// 控件 comboBox6/7、button6/12 已从 DockWindow4 移除,收藏方法群留在 主窗口.cs 备查) =====
// ===== 选型页(tabPage10):元件汇总 + 箱号明细 =====
InitSelectionPage();
}
// ============================================================
// 单实例导出联动:启动参数导入 + 命令文件轮询
// ============================================================
/// <summary>导入并打开一个项目 zip(cadbt/pdfbt 导出联动共用)。</summary>
private void 执行启动导入(string zipPath)
{
try
{
Log("[启动导入] 收到导出: " + zipPath);
var pids = ImportAllProjectsFromZip(zipPath);
if (pids != null && pids.Count > 0)
{
// 重算导入项目的汇总列 + 刷新项目列表显示(抄手动导入按钮/保存按钮链路,
// 否则项目页列表看不到新项目,要重启才能看到)
foreach (string pid in pids)
{
try { 计算引擎.刷新项目汇总(GetQuoteDbPath(pid), pid, TuhaoTableName); }
catch (Exception exSum) { Log("[启动导入] 汇总刷新失败 " + pid + ": " + exSum.Message); }
}
ReloadProjectsFromDatabase();
// ★ 打开前先保存上一个项目并清缓存、设 _currentProjectId(逐行抄双击项目行链路;
// 这是 EnterProjectCore 的调用契约,缺了这步箱号/元件联动还挂在旧项目缓存上,
// 元件表会空/错,要重启才正常)
string newPid = pids[0];
if (!string.IsNullOrWhiteSpace(_currentProjectId) && _currentProjectId != newPid)
{
try
{
// 只存明细,不调 RecalcAllFromDatabase(它会在新项目上跑)
if (this.advancedDataGridView5 != null) this.advancedDataGridView5.EndEdit();
if (this.advancedDataGridView4 != null) this.advancedDataGridView4.EndEdit();
if (this.advancedDataGridView9 != null) this.advancedDataGridView9.EndEdit();
if (_tuhaoTable != null)
SaveTableToDatabase(_tuhaoTable, _currentProjectId, TuhaoTableName, null, null, true);
foreach (var kv in _xianghaoCache)
if (kv.Value != null && !string.IsNullOrWhiteSpace(kv.Value.TableName))
SaveTableToDatabase(kv.Value, _currentProjectId, "\"" + kv.Value.TableName + "\"", null, null, true);
foreach (var kv in _yuanjianCache)
if (kv.Value != null && !string.IsNullOrWhiteSpace(kv.Value.TableName))
SaveTableToDatabase(kv.Value, _currentProjectId, "\"" + kv.Value.TableName + "\"", null, null, true);
}
catch (Exception exSave) { Log("[启动导入] 切项目保存异常: " + exSave.Message); }
_xianghaoCache.Clear();
_yuanjianCache.Clear();
_tuhaoTable = null;
}
_currentProjectId = newPid;
Log("[启动导入] 已导入 " + pids.Count + " 个项目, 打开 " + newPid);
EnterProjectCore(newPid);
RestoreProjectSelection(newPid);
SetStatus("已导入并打开项目 " + newPid);
}
}
catch (Exception exImp) { Log("[启动导入] 异常: " + exImp.Message); }
}
/// <summary>命令文件轮询 Timer(300ms):二次实例转发来的导出 zip 路径写在该文件,读到即导入。</summary>
private System.Windows.Forms.Timer _导入命令Timer;
private DateTime _导入命令上次时间 = DateTime.MinValue;
private void 启动导入命令轮询()
{
_导入命令Timer = new System.Windows.Forms.Timer();
_导入命令Timer.Interval = 300;
_导入命令Timer.Tick += (s, e) =>
{
try
{
string cmdPath = Program.导入命令文件路径;
if (!System.IO.File.Exists(cmdPath)) return;
var wt = System.IO.File.GetLastWriteTime(cmdPath);
if (wt <= _导入命令上次时间) return; // 无新命令
string zip = (System.IO.File.ReadAllText(cmdPath) ?? "").Trim();
_导入命令上次时间 = wt;
if (zip.Length == 0) return;
System.IO.File.WriteAllText(cmdPath, "", System.Text.Encoding.UTF8); // 清空防重复
// 带窗口到前台再导入
if (WindowState == FormWindowState.Minimized) WindowState = FormWindowState.Normal;
Activate();
执行启动导入(zip);
}
catch (Exception exPoll) { Log("[启动导入] 轮询异常: " + exPoll.Message); }
};
_导入命令Timer.Start();
}
// ============================================================
// 功能区(ribbonStrip1)按钮路由与辅助
// ============================================================
/// <summary>打开项目云窗体(上传/下载还原/删除云端项目)。</summary>
private void 打开项目云窗口()
{
using (var dlg = new 项目云窗体(this))
{
dlg.ShowDialog(this);
// 关闭后本地列表可能因"下载还原"变化,刷新项目页
try { ReloadProjectsFromDatabase(); } catch (Exception exRld) { Log("[项目页] 下载还原后刷新项目列表失败: " + exRld.Message); }
}
}
/// <summary>项目云每日自动上传启动(远程模式+已登录才生效;后台静默)。</summary>
private void 启动项目云自动上传()
{
项目云自动上传.启动(this);
}
/// <summary>功能区统一按钮点击:按 RibbonButton.Name(保留原按钮字段名)分发到原处理器。</summary>
private void ribbonStrip1_ButtonClick(object sender, Smart.CustomComponents.RibbonButtonEventArgs e)
{
if (e.Button == null || string.IsNullOrEmpty(e.Button.Name)) return;
switch (e.Button.Name)
{
case "button1": button1_Click(sender, e); break; // 新建项目
case "button2": button2_Click(sender, e); break; // 修改项目(项目页)
case "button3": button3_Click(sender, e); break; // 删除项目
case "button5": button5_Click(sender, e); break; // 复制项目
case "button7": button7_Click(sender, e); break; // 导出项目
case "button8": button8_Click(sender, e); break; // 导入项目
case "button37": 打开项目云窗口(); break; // 项目云(上传/下载还原到会员服务器)
case "button11": button11_Click(sender, e); break; // CAD扒图
case "button34": button34_Click(sender, e); break; // PDF扒图
case "button13": button13_Click(sender, e); break; // 调系数
case "button15": 打开调母排窗口(); break; // 调母排
case "btn母排计算": 打开GCS母排计算窗口(); break; // G2-2:GCS垂直母排计算(自母排页button14移入)
case "button16": button16_Click(sender, e); break; // 甲方报表
case "button17": 强制保存_快捷键(); break; // G2-1:保存(与 Ctrl+S 同一实现,状态栏提示不弹窗)
case "button18": button18_Click(sender, e); break; // 导入报价
case "button29": 打开费用模板窗口(); break; // G1-1:模板费用(费用行按公式计入箱号合计)
case "button33": button33_Click(sender, e); break; // 元件排序
case "btnAI设置": // G2-3:自AI助手窗口工具栏移入
using (var dlgAI = new AI助手设置()) { dlgAI.ShowDialog(this); }
break;
case "btnAI重连": // G2-3:自AI助手窗口工具栏移入(引擎重连)
try
{
AI助手窗口兜底显示();
if (this.w10 != null) this.w10.重新连接();
}
catch (Exception exAI) { Log("[功能区·AI重连] " + exAI.Message); }
break;
case "btn登录":
using (var dlg登录 = new 登录())
{
if (dlg登录.ShowDialog(this) == DialogResult.OK)
{
当前登录手机号 = dlg登录.登录手机号;
当前会员类型 = dlg登录.登录会员类型;
当前有效期 = dlg登录.登录有效期;
当前登录密码 = dlg登录.登录密码; // 新协议:项目云上传/下载/列表需带密码(仅内存)
SetStatus("已登录: " + 当前登录手机号 + " (" + 当前会员类型 + ",有效期至 " + 当前有效期 + ")");
Log("[会员] 登录成功: " + 当前登录手机号 + " " + 当前会员类型);
}
}
break; // 登录
case "btn修改密码":
{
var dlg改密 = new 修改密码();
dlg改密.txt手机号码.Text = 当前登录手机号; // 自动带已登录手机号
if (dlg改密.ShowDialog(this) == DialogResult.OK)
{
// ★改密成功=关闭整个主窗口并重启软件重走登录流程(用户 2026-09-26 要求:
// 不保留原窗口,直接关窗→弹登录窗)。退出时释放单实例锁后由 Program 启动新实例
Log("[会员] 密码已修改,主窗口关闭并重启软件等待重新登录");
改密重启 = true;
Close();
}
}
break; // 修改密码
case "btn企业信息": btn企业信息_Click(sender, e); break; // 企业信息
case "btn会员信息":
{
var dlg会员 = new 会员信息();
dlg会员.txt手机号码.Text = 当前登录手机号; // 自动带已登录手机号
dlg会员.ShowDialog(this);
}
break; // 会员信息
case "btn保存布局": btn保存布局_Click(sender, e); break; // 保存布局
case "btn关于软件":
{
var dlg关于 = new 关于软件();
dlg关于.ShowDialog(this);
}
break; // 关于软件
case "btn备份": btn备份_Click(sender, e); break; // 备份
case "btn恢复": btn恢复_Click(sender, e); break; // 恢复
// ---- CAD扒图页签(原左侧报价窗口工具栏 7 钮,未开工作区自动打开) ----
case "btnCad导出数据": Cad工作区调用(f => f.功能区_导出数据()); break;
case "btnCad新建项目": Cad工作区调用(f => f.功能区_新建项目()); break;
case "btnCad删除项目": Cad工作区调用(f => f.功能区_删除项目()); break;
case "btnCad扒图号": Cad工作区调用(f => f.功能区_扒图号()); break;
case "btnCad扒箱号": Cad工作区调用(f => f.功能区_扒箱号()); break;
case "btnCad扒元件": Cad工作区调用(f => f.功能区_扒元件()); break;
case "btnCad设置": Cad工作区调用(f => f.功能区_设置()); break;
// ---- PDF扒图页签(原左侧报价窗口工具栏 7 钮,未开工作区自动打开) ----
case "btnPdf导出数据": Pdf工作区调用(f => f.功能区_导出数据()); break;
case "btnPdf新建项目": Pdf工作区调用(f => f.功能区_新建项目()); break;
case "btnPdf删除项目": Pdf工作区调用(f => f.功能区_删除项目()); break;
case "btnPdf扒图号": Pdf工作区调用(f => f.功能区_扒图号()); break;
case "btnPdf扒箱号": Pdf工作区调用(f => f.功能区_扒箱号()); break;
case "btnPdf扒元件": Pdf工作区调用(f => f.功能区_扒元件()); break;
case "btnPdf设置": Pdf工作区调用(f => f.功能区_设置()); break;
}
}
/// <summary>CAD扒图页签按钮统一入口:工作区未开先自动打开,再对内嵌报价表单执行动作。</summary>
private void Cad工作区调用(System.Action<MyApp.Form3> action)
{
if (_cadWin == null || _cadWin.IsDisposed) button11_Click(null, null);
var f = _cadWin != null ? _cadWin.报价表单 : null;
if (f == null)
{
MessageBox.Show(this, "CAD扒图工作区未就绪,请稍候重试。", "CAD扒图",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try { action(f); }
catch (Exception ex) { Log("[功能区·CAD扒图] " + ex.Message); }
}
/// <summary>PDF扒图页签按钮统一入口:工作区未开先自动打开,再对内嵌报价表单执行动作。</summary>
private void Pdf工作区调用(System.Action<MyAppPdf.Form3> action)
{
if (_pdfWin == null || _pdfWin.IsDisposed) button34_Click(null, null);
var f = _pdfWin != null ? _pdfWin.报价表单 : null;
if (f == null)
{
MessageBox.Show(this, "PDF扒图工作区未就绪,请稍候重试。", "PDF扒图",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
try { action(f); }
catch (Exception ex) { Log("[功能区·PDF扒图] " + ex.Message); }
}
/// <summary>功能区页签切换的基准索引(检测用户点击页签后的变化)。</summary>
private int _lastRibbonActiveIndex = 0;
/// <summary>MouseUp 时检测功能区活动页签变化:用户点了"项目"/"报价"页签 → 激活对应停靠文档。</summary>
private async void ribbonStrip1_MouseUpTabChanged(object sender, MouseEventArgs e)
{
if (this.ribbonStrip1 == null) return;
if (this.ribbonStrip1.ActiveTabIndex == _lastRibbonActiveIndex) return;
_lastRibbonActiveIndex = this.ribbonStrip1.ActiveTabIndex;
string tabName = (this.ribbonStrip1.ActiveTab != null) ? this.ribbonStrip1.ActiveTab.Text : "";
Log("[TabSync] 功能区页签切换: [" + tabName + "]");
if (tabName == "项目" && this.w1 != null)
{
// ★ 先强制提交报价页当前编辑控件,再等直写批提交,最后才读项目库
// 报出系数编辑尤其容易停留在编辑控件里,若不 EndEdit 项目页会抢读旧值
try
{
if (advancedDataGridView5 != null) advancedDataGridView5.EndEdit();
if (advancedDataGridView4 != null) advancedDataGridView4.EndEdit();
if (advancedDataGridView9 != null) advancedDataGridView9.EndEdit();
}
catch { }
await 等待直写完成();
try
{
if (!string.IsNullOrWhiteSpace(_currentProjectId))
计算引擎.刷新项目汇总(GetQuoteDbPath(_currentProjectId), _currentProjectId, TuhaoTableName);
ReloadProjectsFromDatabase();
}
catch (Exception exSyncTab) { Log("[TabSync] 项目页切换刷新异常: " + exSyncTab.Message); }
this.w1.Show(this.dockSurface1);
this.w1.Activate();
}
else if (tabName == "报价" && this.w2 != null)
{
this.w2.Show(this.dockSurface1);
this.w2.Activate();
}
// ★ 扒图页签联动:切到 CAD扒图/PDF扒图 页签 = 打开(或激活)对应工作区窗口
else if (tabName == "CAD扒图")
{
Cad工作区调用(f => { });
}
else if (tabName == "PDF扒图")
{
Pdf工作区调用(f => { });
}
// "设置"页只显示按钮面板,无停靠窗口
}
/// <summary>按页签文字找功能区索引(找不到返回 -1)。</summary>
private int 功能区页索引(string 文字)
{
if (this.ribbonStrip1 == null || 文字 == null) return -1;
for (int i = 0; i < this.ribbonStrip1.Tabs.Count; i++)
if (string.Equals(this.ribbonStrip1.Tabs[i].Text, 文字, StringComparison.OrdinalIgnoreCase))
return i;
return -1;
}
/// <summary>RibbonButton.Name → icons\ 目录下的图标文件名(与 tools\make_ribbon_icons_pillow.py 一致)。</summary>
private static readonly System.Collections.Generic.Dictionary<string, string> 功能区图标表 =
new System.Collections.Generic.Dictionary<string, string>
{
{ "button1", "new-project.png" }, { "button2", "edit-project.png" },
{ "button3", "delete-project.png" },
{ "button5", "copy-project.png" }, { "button7", "export-project.png" },
{ "button8", "import-project.png" },
{ "button37", "backup.png" }, // 项目云
// ★ G1-3(2026-10-01):button4/9/10 幽灵按钮图标映射已删(按钮早已不存在)
{ "button17", "save.png" },
{ "button18", "import-quote.png" }, { "button16", "report.png" },
{ "button15", "busbar.png" }, { "button13", "factor.png" },
{ "btn母排计算", "busbar.png" }, // G2-2:GCS垂直母排计算
{ "button33", "sort-components.png" }, { "button29", "template-cost.png" },
{ "button11", "cad-grab.png" }, { "button34", "pdf-grab.png" },
{ "btnAI设置", "ai-assistant.png" }, { "btnAI重连", "ai-assistant.png" }, // G2-3
{ "btnCad导出数据", "pa-export.png" }, { "btnCad新建项目", "pa-new-project.png" },
{ "btnCad删除项目", "pa-del-project.png" }, { "btnCad扒图号", "pa-tuhao.png" },
{ "btnCad扒箱号", "pa-xianghao.png" }, { "btnCad扒元件", "pa-yuanjian.png" },
{ "btnCad设置", "pa-settings.png" },
{ "btnPdf导出数据", "pa-export.png" }, { "btnPdf新建项目", "pa-new-project.png" },
{ "btnPdf删除项目", "pa-del-project.png" }, { "btnPdf扒图号", "pa-tuhao.png" },
{ "btnPdf扒箱号", "pa-xianghao.png" }, { "btnPdf扒元件", "pa-yuanjian.png" },
{ "btnPdf设置", "pa-settings.png" },
{ "btn登录", "login.png" },
{ "btn修改密码", "password.png" }, { "btn会员信息", "member.png" },
{ "btn企业信息", "company.png" }, { "btn保存布局", "save-layout.png" },
{ "btn关于软件", "about.png" },
{ "btn备份", "backup.png" }, { "btn恢复", "restore.png" }
};
/// <summary>给功能区 24 个按钮设置图标(exe 旁 icons\ 目录,绝对路径;文件缺失跳过)。</summary>
private void 加载功能区图标()
{
try
{
string dir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "icons");
foreach (var kv in 功能区图标表)
{
var btn = FindRibbonButton(kv.Key);
if (btn == null) continue;
string p = System.IO.Path.Combine(dir, kv.Value);
if (!System.IO.File.Exists(p)) continue;
btn.Image = Smart.CustomComponents.RibbonButton.FromFile(p);
}
if (this.ribbonStrip1 != null) this.ribbonStrip1.Invalidate();
}
catch (Exception ex)
{
Log("[功能区] 图标加载异常(忽略): " + ex.Message);
}
}
/// <summary>按 Name 在功能区三层集合里找 RibbonButton(找不到返回 null)。</summary>
private Smart.CustomComponents.RibbonButton FindRibbonButton(string name)
{
if (this.ribbonStrip1 == null) return null;
foreach (Smart.CustomComponents.RibbonTab tab in this.ribbonStrip1.Tabs)
foreach (Smart.CustomComponents.RibbonGroup grp in tab.Groups)
foreach (Smart.CustomComponents.RibbonButton btn in grp.Buttons)
if (btn.Name == name) return btn;
return null;
}
/// <summary>调母排(button15):普通非模态弹窗(原构造器内 lambda 抽出)。
/// 不自动居中、不贴靠右侧、不跟随主窗口移动、不限制大小,直接弹出。</summary>
private 调母排 _调母排窗;
private void 打开调母排窗口()
{
if (_调母排窗 != null && !_调母排窗.IsDisposed)
{
_调母排窗.Activate();
return;
}
if (string.IsNullOrWhiteSpace(_currentProjectId))
{
MessageBox.Show(this, "请先在项目列表双击打开一个项目。", "调母排",
MessageBoxButtons.OK, MessageBoxIcon.Information);
return;
}
_调母排窗 = new 调母排(this);
// 不设置 StartPosition/Location/Size/MaximumSize/MinimumSize,使用窗体自身默认值
_调母排窗.Show(this);
_调母排窗.FormClosed += (s2, e2) => { _调母排窗 = null; };
}
// ============================================================
// GCS 铜排壳体计算窗口(2026-10-01 G2-2:自母排页 button14 闭包提取,功能区按钮调用)
// ============================================================
private GCS母排计算窗口 _gcsForm = null;
private System.Windows.Forms.Timer _gcsSyncTimer = null;
private bool _gcsDocked = false; // GCS 窗口是否正在停靠
private const int gcsFixedWidth = 471;
/// <summary>打开/激活 GCS 铜排壳体计算窗口(停靠主窗口右侧,宽度固定 471 只跟高度和位置;
/// _gcsDocked=true 时主窗口"最大化"实际是占屏幕左半边,GCS 窗口占右半边)。</summary>
private void 打开GCS母排计算窗口()
{
if (_gcsForm != null && !_gcsForm.IsDisposed)
{
_gcsForm.Activate();
同步GCS窗口();
return;
}
_gcsForm = new GCS母排计算窗口();
// 固定宽度(最小=最大宽度),高度不限
_gcsForm.MinimumSize = new Size(gcsFixedWidth, 200);
_gcsForm.MaximumSize = new Size(gcsFixedWidth, Screen.PrimaryScreen.Bounds.Height);
_gcsForm.FormBorderStyle = FormBorderStyle.Sizable;
_gcsForm.StartPosition = FormStartPosition.Manual;
_gcsForm.Show(this);
_gcsDocked = true;
同步GCS窗口();
// 用 Timer 低频同步位置(拖动/调整大小时)
if (_gcsSyncTimer == null)
{
_gcsSyncTimer = new System.Windows.Forms.Timer();
_gcsSyncTimer.Interval = 50;
_gcsSyncTimer.Tick += (s2, e2) => 同步GCS窗口();
}
_gcsSyncTimer.Start();
// 主窗口最小化/最大化/还原时立即同步
EventHandler onStateChanged = (s2, e2) =>
{
if (_gcsForm == null || _gcsForm.IsDisposed) return;
if (this.WindowState == FormWindowState.Minimized)
{
_gcsForm.WindowState = FormWindowState.Minimized;
}
else if (this.WindowState == FormWindowState.Maximized && _gcsDocked)
{
// 停靠模式:拦截最大化,主窗口占左侧(减去GCS宽度),GCS 右侧固定宽度
var screen = Screen.FromHandle(this.Handle).WorkingArea;
int mainW = screen.Width - gcsFixedWidth;
_gcsDocked = false; // 临时关闭,避免递归
this.WindowState = FormWindowState.Normal;
this.Bounds = new Rectangle(screen.X, screen.Y, mainW, screen.Height);
_gcsDocked = true;
_gcsForm.WindowState = FormWindowState.Normal;
_gcsForm.Bounds = new Rectangle(screen.X + mainW, screen.Y, gcsFixedWidth, screen.Height);
}
else
{
// 还原到正常窗口:重新停靠
_gcsForm.WindowState = FormWindowState.Normal;
同步GCS窗口();
}
};
this.SizeChanged -= onStateChanged;
this.SizeChanged += onStateChanged;
this.LocationChanged -= onStateChanged;
this.LocationChanged += onStateChanged;
_gcsForm.FormClosed += (s2, e2) =>
{
if (_gcsSyncTimer != null) { _gcsSyncTimer.Stop(); _gcsSyncTimer.Dispose(); _gcsSyncTimer = null; }
_gcsDocked = false;
_gcsForm = null;
};
}
/// <summary>同步 GCS 窗口位置/大小到主窗口右侧。</summary>
private void 同步GCS窗口()
{
if (_gcsForm == null || _gcsForm.IsDisposed) return;
if (this.WindowState == FormWindowState.Minimized)
{
_gcsForm.WindowState = FormWindowState.Minimized;
return;
}
var screen = Screen.FromHandle(this.Handle).WorkingArea;
if (this.WindowState == FormWindowState.Maximized && _gcsDocked)
{
// 停靠模式下的"最大化":主窗口左半屏(减去GCS固定宽度),GCS 右侧固定宽度
_gcsForm.WindowState = FormWindowState.Normal;
_gcsForm.Bounds = new Rectangle(screen.Right - gcsFixedWidth, screen.Y, gcsFixedWidth, screen.Height);
_gcsForm.BringToFront();
return;
}
// 正常状态:贴主窗口右侧显示,宽度固定
_gcsForm.WindowState = FormWindowState.Normal;
_gcsForm.Location = new Point(this.Right, this.Top);
_gcsForm.Size = new Size(gcsFixedWidth, this.Height);
}
async void DockSurface1ActiveContentChanged(object sender, EventArgs e)
{
// ★ 反向联动:点击项目/报价窗口激活 → 功能区页签跟随切换(与 ribbonStrip1_MouseUpTabChanged 正向联动对称)
// 只认 w1(项目)/w2(报价)两个文档窗口,其他窗口(元件库/方案等)不动功能区
try
{
var active = this.dockSurface1.ActiveContent as WeifenLuo.WinFormsUI.Docking.DockContent;
if (active != null && this.ribbonStrip1 != null)
{
string 目标页 = null;
if (active == this.w1) 目标页 = "项目";
else if (active == this.w2) 目标页 = "报价";
else if (active is Cad扒图窗口) 目标页 = "CAD扒图"; // ★ 扒图工作区:点窗口标签→功能区跳对应页签
else if (active is Pdf扒图窗口) 目标页 = "PDF扒图";
if (目标页 == "项目")
{
// ★ 直接点项目窗口标签:先提交报价页当前编辑控件,再等批提交,最后读项目库
try
{
if (advancedDataGridView5 != null) advancedDataGridView5.EndEdit();
if (advancedDataGridView4 != null) advancedDataGridView4.EndEdit();
if (advancedDataGridView9 != null) advancedDataGridView9.EndEdit();
}
catch { }
await 等待直写完成();
try
{
if (!string.IsNullOrWhiteSpace(_currentProjectId))
计算引擎.刷新项目汇总(GetQuoteDbPath(_currentProjectId), _currentProjectId, TuhaoTableName);
ReloadProjectsFromDatabase();
}
catch (Exception exPg) { Log("[TabSync] 切项目页刷新汇总失败: " + exPg.Message); }
}
if (目标页 != null)
{
int idx = 功能区页索引(目标页);
if (idx >= 0 && this.ribbonStrip1.ActiveTabIndex != idx)
{
this.ribbonStrip1.ActiveTabIndex = idx;
_lastRibbonActiveIndex = idx; // 同步基准,防 MouseUp 正向联动再触发抖动
Log("[TabSync] 文档激活→功能区页签: [" + 目标页 + "]");
}
}
}
}
catch (Exception exTab) { Log("[TabSync] 活动文档联动异常: " + exTab.Message); }
// 停靠面板活动文档变化 → 触发旧 tabControl2.SelectedIndexChanged 兼容事件
if (_tabControl2兼容 != null) {
_tabControl2兼容.RaiseSelectedIndexChanged();
}
}
}
}