/*
* 主窗体构造函数(由旧项目 主窗口.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();
}
}
}
}