/* * 主窗体构造函数(由旧项目 主窗口.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 成套报价软件 { /// /// 主窗体:构造函数迁移自旧项目 主窗口.cs(事件挂接全部保留)。 /// public partial class MainForm : Form { /// 启动时待导入的项目 zip 路径(cadbt 导出联动传入;空=正常启动)。 private string _startupZip; // ===== 会员登录状态(登录成功后全程保持;会员信息/修改密码窗自动带手机号) ===== public static string 当前登录手机号 = ""; public static string 当前会员类型 = ""; public static string 当前有效期 = ""; /// 当前登录密码(仅内存,不落盘):新版会员协议要求项目云上传/下载/列表带密码参数。 public static string 当前登录密码 = ""; /// 改密重启标记:修改密码成功后关闭主窗口,Program 在释放单实例锁后启动新实例 /// (新实例启动先弹登录窗,客户用新密码重新登录)。 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(); } // ============================================================ // 单实例导出联动:启动参数导入 + 命令文件轮询 // ============================================================ /// 导入并打开一个项目 zip(cadbt/pdfbt 导出联动共用)。 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); } } /// 命令文件轮询 Timer(300ms):二次实例转发来的导出 zip 路径写在该文件,读到即导入。 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)按钮路由与辅助 // ============================================================ /// 打开项目云窗体(上传/下载还原/删除云端项目)。 private void 打开项目云窗口() { using (var dlg = new 项目云窗体(this)) { dlg.ShowDialog(this); // 关闭后本地列表可能因"下载还原"变化,刷新项目页 try { ReloadProjectsFromDatabase(); } catch (Exception exRld) { Log("[项目页] 下载还原后刷新项目列表失败: " + exRld.Message); } } } /// 项目云每日自动上传启动(远程模式+已登录才生效;后台静默)。 private void 启动项目云自动上传() { 项目云自动上传.启动(this); } /// 功能区统一按钮点击:按 RibbonButton.Name(保留原按钮字段名)分发到原处理器。 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; } } /// CAD扒图页签按钮统一入口:工作区未开先自动打开,再对内嵌报价表单执行动作。 private void Cad工作区调用(System.Action 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); } } /// PDF扒图页签按钮统一入口:工作区未开先自动打开,再对内嵌报价表单执行动作。 private void Pdf工作区调用(System.Action 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); } } /// 功能区页签切换的基准索引(检测用户点击页签后的变化)。 private int _lastRibbonActiveIndex = 0; /// MouseUp 时检测功能区活动页签变化:用户点了"项目"/"报价"页签 → 激活对应停靠文档。 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 => { }); } // "设置"页只显示按钮面板,无停靠窗口 } /// 按页签文字找功能区索引(找不到返回 -1)。 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; } /// RibbonButton.Name → icons\ 目录下的图标文件名(与 tools\make_ribbon_icons_pillow.py 一致)。 private static readonly System.Collections.Generic.Dictionary 功能区图标表 = new System.Collections.Generic.Dictionary { { "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" } }; /// 给功能区 24 个按钮设置图标(exe 旁 icons\ 目录,绝对路径;文件缺失跳过)。 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); } } /// 按 Name 在功能区三层集合里找 RibbonButton(找不到返回 null)。 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; } /// 调母排(button15):普通非模态弹窗(原构造器内 lambda 抽出)。 /// 不自动居中、不贴靠右侧、不跟随主窗口移动、不限制大小,直接弹出。 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; /// 打开/激活 GCS 铜排壳体计算窗口(停靠主窗口右侧,宽度固定 471 只跟高度和位置; /// _gcsDocked=true 时主窗口"最大化"实际是占屏幕左半边,GCS 窗口占右半边)。 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; }; } /// 同步 GCS 窗口位置/大小到主窗口右侧。 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(); } } } }