基线:全量源码首提(D1 版本控制落地,含第一轮优化 B1-B4/A1/A4/缩放修复)

This commit is contained in:
dqb-dev
2026-10-01 22:15:22 +08:00
commit 1e65792b09
499 changed files with 204579 additions and 0 deletions
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.Windows.Forms;
namespace MyApp
{
/// <summary>
/// 新建项目 窗口(简化版,不依赖企业信息)。
/// UI 布局见 新建项目.ui.cs,本文件只含交互逻辑。
/// </summary>
public partial class 新建项目 : Form
{
// 价格格式化选项(与主项目 价格格式化.cs 保持一致)
private static readonly string[] 小数位选项 =
{
"保留整数", "保留1位小数", "保留2位小数", "保留3位小数",
"保留4位小数", "保留5位小数", "保留6位小数"
};
private static readonly string[] 取整方式选项 =
{
"不取整", "四舍五入", "四舍五入到角", "四舍五入到元",
"四舍五入到十元", "向上取整到元", "最近5元", "最近10元"
};
private readonly string _dbPath;
private readonly string _tableName;
public 新建项目() : this(null, null) { }
/// <summary>带数据库路径的构造函数(用于自动生成项目编号)。</summary>
public 新建项目(string dbPath, string tableName)
{
InitializeComponent();
_dbPath = dbPath;
_tableName = tableName;
InitPriceFormatCombos();
this.Load += 新建项目_Load;
}
private void InitPriceFormatCombos()
{
cmb单台价格.Items.Clear();
cmb单台价格.Items.AddRange(小数位选项);
cmb项目总价.Items.Clear();
cmb项目总价.Items.AddRange(小数位选项);
comboBox1.Items.Clear();
comboBox1.Items.AddRange(取整方式选项);
comboBox2.Items.Clear();
comboBox2.Items.AddRange(取整方式选项);
}
private void 新建项目_Load(object sender, EventArgs e)
{
FillAutoFields();
}
/// <summary>自动填充项目日期(当天)和项目编号(yyyyMMdd+3位序号)。</summary>
private void FillAutoFields()
{
DateTime today = DateTime.Today;
txt项目日期.Text = today.ToString("yyyy-MM-dd");
string datePart = today.ToString("yyyyMMdd");
int seq = GetNextSeqForDate(datePart);
txt项目编号.Text = datePart + seq.ToString("000");
txt项目编号.ReadOnly = true;
txt项目编号.TabStop = false;
txt项目编号.BackColor = System.Drawing.SystemColors.Control;
}
/// <summary>查数据库当天最大序号+1,失败返回1。</summary>
private int GetNextSeqForDate(string datePart)
{
if (string.IsNullOrEmpty(datePart) ||
string.IsNullOrEmpty(_dbPath) ||
string.IsNullOrEmpty(_tableName) ||
!System.IO.File.Exists(_dbPath))
{
return 1;
}
try
{
string connStr = "Data Source=" + _dbPath + ";Version=3;";
using (var conn = new System.Data.SQLite.SQLiteConnection(connStr))
{
conn.Open();
string sql =
"SELECT MAX(CAST(SUBSTR(项目编号, 9) AS INTEGER)) " +
"FROM " + _tableName + " " +
"WHERE SUBSTR(项目编号, 1, 8) = '" + datePart + "';";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
object result = cmd.ExecuteScalar();
if (result == null || result == DBNull.Value) return 1;
int maxSeq;
if (int.TryParse(result.ToString(), out maxSeq)) return maxSeq + 1;
return 1;
}
}
}
catch { return 1; }
}
/// <summary>收集对话框字段,以列名为键返回(供 Form3 写入数据库)。</summary>
public Dictionary<string, string> GetInputValues()
{
var values = new Dictionary<string, string>();
AddIfNotEmpty(values, "项目编号", txt项目编号.Text);
AddIfNotEmpty(values, "项目日期", txt项目日期.Text);
AddIfNotEmpty(values, "项目名称", txt项目名称.Text);
AddIfNotEmpty(values, "客户名称", txt客户名称.Text);
AddIfNotEmpty(values, "客户地址", txt客户地址.Text);
AddIfNotEmpty(values, "联系人", txt联系人.Text);
AddIfNotEmpty(values, "联系电话", txt联系电话.Text);
AddIfNotEmpty(values, "备注", txt备注.Text);
AddIfNotEmpty(values, "成套厂名称", txt成套厂名称.Text);
AddIfNotEmpty(values, "成套厂地址", txt成套厂地址.Text);
AddIfNotEmpty(values, "成套厂联系人", txt成套厂联系人.Text);
AddIfNotEmpty(values, "成套厂联系电话", txt成套厂联系电话.Text);
values["单台小数位"] = (cmb单台价格.SelectedItem ?? cmb单台价格.Text ?? "").ToString();
values["项目小数位"] = (cmb项目总价.SelectedItem ?? cmb项目总价.Text ?? "").ToString();
values["单台取整方式"] = (comboBox1.SelectedItem ?? comboBox1.Text ?? "").ToString();
values["项目取整方式"] = (comboBox2.SelectedItem ?? comboBox2.Text ?? "").ToString();
return values;
}
private static void AddIfNotEmpty(Dictionary<string, string> dict, string key, string value)
{
if (!string.IsNullOrWhiteSpace(value)) dict[key] = value.Trim();
}
}
}
File diff suppressed because it is too large Load Diff
+485
View File
@@ -0,0 +1,485 @@
// ============================================================
// CadBridge:WinForms 宿主与网页 CAD(CAD-Viewer)之间的互操作桥
// · 下行(C#→网页):PostMessage 发 startPick/zoomToHandle/deleteHandles/clearHighlight
// · 上行(网页→C#):WebMessageReceived 收 paPicked/entityErased/paExited
// · 扒图结果与实体删除事件以 C# 事件形式提供给主窗口(替代 AutoCAD 版
// 的 PaTextSelector.BatchTextsPicked 与 Database.ObjectErased)
// ============================================================
using System;
using System.Collections.Generic;
using System.Web.Script.Serialization;
using Microsoft.Web.WebView2.Core;
using Microsoft.Web.WebView2.WinForms;
namespace 成套报价软件
{
public static class CadBridge
{
private static WebView2 _wv;
private static readonly JavaScriptSerializer _json = new JavaScriptSerializer();
/// <summary>网页 CAD 是否已挂载(WebView2 已附加)。</summary>
public static bool IsViewerReady
{
get { return _wv != null && _wv.CoreWebView2 != null; }
}
/// <summary>网页 CAD 当前打开的图纸名(去扩展名;paPicked 消息携带更新,未带为空)。
/// 扒图写入箱号「图纸」列用。</summary>
public static string CurrentDrawingName { get; private set; }
/// <summary>记录网页 CAD 上报的图纸名(去扩展名;路径分隔符兼容 / 与 \)。</summary>
private static void SetCurrentDrawingName(string raw)
{
if (string.IsNullOrWhiteSpace(raw)) return;
try
{
CurrentDrawingName = System.IO.Path.GetFileNameWithoutExtension(raw.Replace('/', '\\'));
}
catch { CurrentDrawingName = raw; }
}
/// <summary>网页 CAD 中实体被删除(句柄十六进制字符串)。</summary>
public static event Action<string> EntityErased;
/// <summary>网页 CAD 加载渲染进度变化(文本已拼好,直接显示:
/// "加载中 35% · 阶段 PARSE · 12.3s" / "渲染完成 · 45.2s" / "打开失败")。</summary>
public static event Action<string> 加载状态;
/// <summary>网页 CAD 扒图完成一批(图号/箱号/元件)。</summary>
public static event Action<MyApp.PaMode, List<MyApp.PaTextResult>> PaPicked;
/// <summary>用户按 ESC 退出网页扒图连续模式。</summary>
public static event Action PaExited;
/// <summary>用户按 F1(焦点在网页或宿主时都触发)——元件两两合并。</summary>
public static event Action F1Requested;
/// <summary>用户按 F2——元件三个一组合并。</summary>
public static event Action F2Requested;
/// <summary>用户按 F3——元件四个一组合并。</summary>
public static event Action F3Requested;
/// <summary>触发 F1 请求(消息过滤器等宿主侧来源调用)。</summary>
public static void RaiseF1()
{
if (F1Requested != null) F1Requested();
}
/// <summary>触发 F2 请求(消息过滤器等宿主侧来源调用)。</summary>
public static void RaiseF2()
{
if (F2Requested != null) F2Requested();
}
/// <summary>触发 F3 请求(消息过滤器等宿主侧来源调用)。</summary>
public static void RaiseF3()
{
if (F3Requested != null) F3Requested();
}
/// <summary>把 WebView2 挂到桥上(在控件初始化完成后调用一次)。</summary>
public static void Attach(WebView2 wv)
{
if (_wv == wv) return;
if (_wv != null) _wv.WebMessageReceived -= OnMessage;
_wv = wv;
if (_wv != null) _wv.WebMessageReceived += OnMessage;
}
private static void OnMessage(object sender, CoreWebView2WebMessageReceivedEventArgs e)
{
try
{
if (e.TryGetWebMessageAsString() != null) return; // 只处理 JSON 消息
}
catch { }
try
{
var msg = _json.Deserialize<Dictionary<string, object>>(e.WebMessageAsJson);
if (msg == null || !msg.ContainsKey("type")) return;
string type = msg["type"] as string;
if (type == "paPicked")
{
int mode = Convert.ToInt32(msg["mode"]);
// 网页端随批携带当前图纸文件名(未带时保留上次值)
if (msg.ContainsKey("fileName"))
SetCurrentDrawingName(Convert.ToString(msg["fileName"]));
// 诊断:记录 fileName 是否上报(排查网页旧缓存/未带字段)——logs\cadbridge.log
try
{
string logDir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs");
if (!System.IO.Directory.Exists(logDir)) System.IO.Directory.CreateDirectory(logDir);
System.IO.File.AppendAllText(System.IO.Path.Combine(logDir, "cadbridge.log"),
DateTime.Now.ToString("HH:mm:ss.fff") + " paPicked mode=" + mode +
" fileName=[" + (msg.ContainsKey("fileName") ? Convert.ToString(msg["fileName"]) : "<无字段>") +
"] Current=[" + (CurrentDrawingName ?? "") + "]" + Environment.NewLine);
}
catch { }
var results = new List<MyApp.PaTextResult>();
var arr = msg["results"] as System.Collections.ArrayList;
if (arr != null)
{
foreach (Dictionary<string, object> item in arr)
{
results.Add(new MyApp.PaTextResult
{
Text = Convert.ToString(item["text"]) ?? "",
FrameHandle = Convert.ToString(item["frameHandle"]) ?? "",
TextHandle = Convert.ToString(item["textHandle"]) ?? ""
});
}
}
// WebMessageReceived 在 UI 线程触发,可直接调用主窗口
if (PaPicked != null)
PaPicked((MyApp.PaMode)mode, results);
}
else if (type == "drawingName")
{
// 网页打开图纸时主动上报文件名(不等扒图)——右键新建箱号也要用到
SetCurrentDrawingName(Convert.ToString(msg["fileName"]));
}
else if (type == "drawingOpened")
{
// ★ 网页图纸就绪(真名通知)——读该图的蒙版回灌。
// 存储主位置=图纸旁边的 sidecar 文件 <图纸完整路径>.cadmask.json
// (与 PDF 的 xxx.pdf.pdfmask.json 同模式,数据跟图纸文件走);
// sidecar 不存在时兜底读软件目录的旧存档(cadbt\扒图蒙版\,迁移期兼容)
string key = msg.ContainsKey("key") ? Convert.ToString(msg["key"]) : "";
string dwgPath = msg.ContainsKey("dwgPath") ? Convert.ToString(msg["dwgPath"]) : "";
// ★ 去重:同 key 3 秒内的重复通知丢弃(实测出现过 16 连发风暴,
// 每发都触发回灌+挂起,恢复调度全部错乱)
if (key == _lastNotifyKey && (DateTime.Now - _lastNotifyAt).TotalSeconds < 3)
{
记日志("drawingOpened 重复通知已忽略: [" + key + "]");
return;
}
_lastNotifyKey = key;
_lastNotifyAt = DateTime.Now;
记日志("收到 drawingOpened key=[" + key + "] dwgPath=[" + dwgPath + "]");
_lastMaskKey = key;
_lastDwgPath = dwgPath;
回灌蒙版文件(key, dwgPath);
}
else if (type == "framesSaved")
{
// ★ 网页每次画框/删框/合并后上报——写入该图纸的蒙版存档
string key = msg.ContainsKey("key") ? Convert.ToString(msg["key"]) : "";
string dwgPath = msg.ContainsKey("dwgPath") ? Convert.ToString(msg["dwgPath"]) : "";
if (!string.IsNullOrWhiteSpace(key) && msg.ContainsKey("frames"))
{
try
{
string json = _json.Serialize(msg["frames"]);
// 主存储:图纸旁边的 sidecar(有完整路径时);兜底:软件目录旧位置
if (!string.IsNullOrWhiteSpace(dwgPath))
{
string sidecar = dwgPath + ".cadmask.json";
System.IO.File.WriteAllText(sidecar, json);
}
else
{
string f = 蒙版文件路径(key);
System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(f));
System.IO.File.WriteAllText(f, json);
}
}
catch (Exception exW) { 记日志("蒙版文件写入失败[" + key + "]: " + exW.Message); }
}
}
else if (type == "entityErased")
{
string handle = Convert.ToString(msg["handle"]);
if (!string.IsNullOrWhiteSpace(handle) && EntityErased != null)
EntityErased(handle);
}
else if (type == "paError")
{
string errMsg = Convert.ToString(msg["message"]);
记日志("paError: " + errMsg); // ★ 先落日志(模态弹窗会打断渲染,进度类上报绝不弹)
System.Windows.Forms.MessageBox.Show(
"网页 CAD 返回错误:\n" + errMsg,
"扒数据", System.Windows.Forms.MessageBoxButtons.OK,
System.Windows.Forms.MessageBoxIcon.Warning);
}
else if (type == "loadProgress")
{
// ★ 三段进度:pct 0~100=文件解析;CONVERT_DONE=解析完/正式渲染开始;
// RENDER=渲染中(r=渲染百分比);RENDER_DONE=渲染完成(画面就绪,secs=全程耗时)
int pct = msg.ContainsKey("pct") ? Convert.ToInt32(msg["pct"]) : 0;
string stage = msg.ContainsKey("stage") ? Convert.ToString(msg["stage"]) : "";
string secs = msg.ContainsKey("secs") ? Convert.ToString(msg["secs"]) : "0";
int r = msg.ContainsKey("r") ? Convert.ToInt32(msg["r"]) : 0;
string text;
if (pct < 0)
{
text = "打开失败(文件无法解析)";
记日志("[加载] " + text);
}
else if (stage == "RENDER_DONE")
{
text = "渲染完成 · " + secs + "s";
记日志("[加载] 渲染完成(含正式渲染), 全程 " + secs + " 秒");
}
else if (stage == "RENDER")
{
text = "渲染中 " + r + "% · " + secs + "s";
if ((DateTime.Now - _lastProgLogAt).TotalSeconds >= 5)
{
_lastProgLogAt = DateTime.Now;
记日志("[加载] " + text);
}
}
else if (stage == "CONVERT_DONE")
{
text = "解析完成(" + secs + "s), 正式渲染中...";
记日志("[加载] 文件解析完成 " + secs + " 秒, 正式渲染开始");
}
else if (pct >= 100)
{
text = "解析完成(" + secs + "s)";
}
else
{
text = "加载中 " + pct + "% · " + stage + " · " + secs + "s";
if ((DateTime.Now - _lastProgLogAt).TotalSeconds >= 5)
{
_lastProgLogAt = DateTime.Now;
记日志("[加载] " + text);
}
}
if (加载状态 != null) 加载状态(text);
}
else if (type == "webLog")
{
// ★ 网页端诊断通道:只写 cadbridge.log,绝不弹窗(蒙版恢复各阶段/页面异常)
记日志(Convert.ToString(msg["message"]));
}
else if (type == "f1")
{
if (F1Requested != null) F1Requested();
}
else if (type == "f2")
{
if (F2Requested != null) F2Requested();
}
else if (type == "f3")
{
if (F3Requested != null) F3Requested();
}
else if (type == "paExited")
{
if (PaExited != null) PaExited();
}
}
catch (Exception)
{
// 消息格式异常直接忽略,不影响宿主
}
}
private static void Post(object msg)
{
if (!IsViewerReady) return;
try
{
_wv.CoreWebView2.PostWebMessageAsJson(_json.Serialize(msg));
}
catch { }
}
// ---------- 蒙版文件持久化(cadbt\扒图蒙版\<图纸名键>.json) ----------
/// <summary>图纸键(网页 framesKey 同款:cadbt_pa_frames:名字)对应的蒙版文件路径。
/// 键里的':'换成'_'并清洗文件名非法字符。</summary>
private static string 蒙版文件路径(string key)
{
string safe = "";
char[] bad = System.IO.Path.GetInvalidFileNameChars();
foreach (char c in (key ?? ""))
{
bool invalid = c == ':' || c == '\\' || c == '/';
if (!invalid) foreach (char b in bad) if (b == c) { invalid = true; break; }
safe += invalid ? '_' : c;
}
string dir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "cadbt", "扒图蒙版");
return System.IO.Path.Combine(dir, safe + ".json");
}
/// <summary>宿主"打开CAD图纸"按钮待加载的图纸完整路径(取文件通道按此回字节)。</summary>
internal static string 待加载图纸路径 = "";
/// <summary>最后一次 drawingOpened 的 key/完整路径(手动"显示蒙版"按钮回灌用)。</summary>
private static string _lastMaskKey = "";
private static string _lastDwgPath = "";
private static string _lastNotifyKey = "";
private static DateTime _lastNotifyAt = DateTime.MinValue;
/// <summary>中间进度日志节流(loadProgress 每秒一条,日志 5 秒一条)。</summary>
private static DateTime _lastProgLogAt = DateTime.MinValue;
/// <summary>宿主端"打开CAD图纸"按钮:设置待加载路径并通知网页从同源通道加载。
/// 蒙版 sidecar(<dwgPath>.cadmask.json)就存在图纸旁边(与 PDF 同模式)。</summary>
public static void OpenLocalDrawing(string dwgPath, string name)
{
待加载图纸路径 = dwgPath;
Post(new { type = "openLocalFile", url = "http://localhost:5173/localfile/current", name = name, dwgPath = dwgPath });
记日志("宿主打开CAD图纸: " + dwgPath);
}
/// <summary>手动"显示蒙版"按钮:先回灌存档(网页只 stash),再下令绘制——
/// 用户点它时图纸必然可见=渲染稳定,与扒图场景同级,绕开自动时机的所有坑。</summary>
public static void ReloadMasks()
{
if (!string.IsNullOrWhiteSpace(_lastMaskKey))
{
回灌蒙版文件(_lastMaskKey, _lastDwgPath);
}
Post(new { type = "drawMasks" });
记日志("【显示蒙版】按钮: 回灌+绘制指令已发送");
}
/// <summary>读该图纸的蒙版并回灌网页(restoreFramesFromFile)。
/// 优先级:①图纸旁边的 sidecar(<dwgPath>.cadmask.json) ②软件目录旧存档(迁移兼容)。</summary>
private static void 回灌蒙版文件(string key, string dwgPath)
{
try
{
if (string.IsNullOrWhiteSpace(key)) return;
string json = null, 来源 = "";
if (!string.IsNullOrWhiteSpace(dwgPath))
{
string sidecar = dwgPath + ".cadmask.json";
if (System.IO.File.Exists(sidecar))
{
json = System.IO.File.ReadAllText(sidecar);
来源 = "sidecar(图纸同目录)";
}
}
if (json == null)
{
string f = 蒙版文件路径(key);
if (System.IO.File.Exists(f))
{
json = System.IO.File.ReadAllText(f);
来源 = "旧位置(软件目录)";
}
}
if (json == null)
{
记日志("蒙版存档不存在(跳过回灌): key=" + key + " dwgPath=" + dwgPath);
return;
}
var frames = _json.Deserialize<System.Collections.ArrayList>(json);
if (frames != null && frames.Count > 0)
{
Post(new { type = "restoreFramesFromFile", frames = frames });
记日志("蒙版回灌(" + 来源 + "): " + key + " 共 " + frames.Count + " 框");
}
}
catch (Exception exR) { 记日志("蒙版文件回灌失败[" + key + "]: " + exR.Message); }
}
/// <summary>cadbridge 诊断日志(logs\cadbridge.log,与 paPicked 诊断同文件)。</summary>
private static void 记日志(string msg)
{
try
{
string logDir = System.IO.Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs");
if (!System.IO.Directory.Exists(logDir)) System.IO.Directory.CreateDirectory(logDir);
System.IO.File.AppendAllText(System.IO.Path.Combine(logDir, "cadbridge.log"),
DateTime.Now.ToString("HH:mm:ss.fff") + " " + msg + Environment.NewLine);
}
catch { }
}
/// <summary>
/// 发送扒图命令。接受原 CAD 命令名(CADPATU/CADPAXH/CADPAYJ/CADPACL)
/// 或网页命令名(patu/paxh/payj)。
/// </summary>
public static void SendPaCommand(string cmd)
{
if (string.IsNullOrWhiteSpace(cmd)) return;
string c = cmd.Trim().ToUpperInvariant();
int mode = 0;
if (c == "CADPATU" || c == "PATU") mode = 0;
else if (c == "CADPAXH" || c == "PAXH") mode = 1;
else if (c == "CADPAYJ" || c == "PAYJ") mode = 2;
else if (c == "CADPACL" || c == "PACL")
{
ClearHighlight();
return;
}
else return;
Post(new { type = "startPick", mode = mode });
}
/// <summary>缩放居中到指定 Handle 的实体并高亮(点击表格行定位框)。</summary>
public static void ZoomToFrameHandle(string handle)
{
if (string.IsNullOrWhiteSpace(handle)) return;
Post(new { type = "zoomToHandle", handle = handle });
}
/// <summary>批量删除指定 Handle 的实体(删表格行联动删框)。</summary>
public static void DeleteFramesByHandles(IEnumerable<string> handles)
{
if (handles == null) return;
var list = new List<string>();
foreach (string h in handles)
if (!string.IsNullOrWhiteSpace(h)) list.Add(h);
if (list.Count == 0) return;
Post(new { type = "deleteHandles", handles = list });
}
/// <summary>
/// 合并蒙版:保留 keep 句柄的蒙版并扩大到所有 removes 蒙版的并集,
/// 被合并行的蒙版从网页 CAD 移除(元件合并时蒙版同步合并)。
/// </summary>
public static void MergeMasks(string keep, System.Collections.Generic.IEnumerable<string> removes)
{
if (string.IsNullOrWhiteSpace(keep)) return;
var list = new List<string>();
if (removes != null)
foreach (string h in removes)
if (!string.IsNullOrWhiteSpace(h) && h != keep) list.Add(h);
if (list.Count == 0) return;
Post(new { type = "mergeMasks", keep = keep, removes = list });
}
/// <summary>清除网页 CAD 中的所有高亮。</summary>
public static void ClearHighlight()
{
Post(new { type = "clearHighlight" });
}
/// <summary>显示箱号组虚线框:把该箱号的箱号图框+全部元件图框求并集,
/// 网页 CAD 画一个橙色虚线矩形(点元件表/箱号表行时调用)。</summary>
public static void SetGroupFrames(System.Collections.Generic.IEnumerable<string> handles)
{
if (handles == null) return;
var list = new List<string>();
foreach (string h in handles)
if (!string.IsNullOrWhiteSpace(h)) list.Add(h);
Post(new { type = "groupHighlight", handles = list });
}
/// <summary>清除箱号组虚线框(点图号表行时)。</summary>
public static void ClearGroupFrames()
{
Post(new { type = "groupHighlightClear" });
}
/// <summary>画组虚线框并缩放到"装下整个组框"(点箱号/元件行跳转时)。
/// focusHandle=点中的那个框:网页端对它画黄色高亮框。</summary>
public static void ZoomToGroupFrames(System.Collections.Generic.IEnumerable<string> handles, string focusHandle = null)
{
if (handles == null) return;
var list = new List<string>();
foreach (string h in handles)
if (!string.IsNullOrWhiteSpace(h)) list.Add(h);
Post(new { type = "zoomToGroup", handles = list, focus = focusHandle ?? "" });
}
}
}
+27
View File
@@ -0,0 +1,27 @@
namespace 成套报价软件
{
partial class CadQuoteWindow
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null)) components.Dispose();
base.Dispose(disposing);
}
#region Designer generated code
private void InitializeComponent()
{
this.SuspendLayout();
//
// CadQuoteWindow
//
this.Name = "CadQuoteWindow";
this.ResumeLayout(false);
}
#endregion
}
}
@@ -0,0 +1,36 @@
// ============================================================
// 停靠窗口:报价插件工具栏(左侧)——cadbt 独立版 QuoteWindow 原样搬入
// 把 Form3 报价界面(cadbt 侧,namespace MyApp)作为子控件嵌入。
// ============================================================
using System.Windows.Forms;
namespace 成套报价软件
{
/// <summary>停靠窗口:报价插件工具栏(DockState.DockLeft),宿主为 MyApp.Form3。</summary>
public partial class CadQuoteWindow : WeifenLuo.WinFormsUI.Docking.DockContent
{
private MyApp.Form3 _form3;
/// <summary>内嵌的报价界面实例(主窗口功能区按钮跨窗口调用用)。</summary>
public MyApp.Form3 报价表单
{
get { return _form3; }
}
public CadQuoteWindow()
{
InitializeComponent();
this.BackColor = System.Drawing.Color.White;
Text = "报价工具";
TabText = "报价工具";
var form = new MyApp.Form3();
_form3 = form;
form.TopLevel = false;
form.FormBorderStyle = FormBorderStyle.None;
form.Dock = DockStyle.Fill;
Controls.Add(form);
form.Show();
}
}
}
+27
View File
@@ -0,0 +1,27 @@
namespace 成套报价软件
{
partial class CadViewerWindow
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null)) components.Dispose();
base.Dispose(disposing);
}
#region Designer generated code
private void InitializeComponent()
{
this.SuspendLayout();
//
// CadViewerWindow
//
this.Name = "CadViewerWindow";
this.ResumeLayout(false);
}
#endregion
}
}
@@ -0,0 +1,260 @@
// ============================================================
// 停靠窗口:CAD 查看器(中央文档区)
// WebView2 承载网页版 CAD-Viewer(localhost:5173),并挂到 CadBridge。
// ★ 网页服务自启动:安装布局 {app}\cadbt\node\node.exe + {app}\cadbt\CAD-Viewer\dist\server.cjs
// (静态产物零依赖,客户机无需 Node 环境/pnpm dev;主程序同款端口探测模式)
// ============================================================
using System;
using System.Diagnostics;
using System.IO;
using System.Net.Sockets;
using System.Windows.Forms;
using Microsoft.Web.WebView2.WinForms;
namespace 成套报价软件
{
/// <summary>停靠窗口:网页 CAD 查看器(DockState.Document)。</summary>
public partial class CadViewerWindow : WeifenLuo.WinFormsUI.Docking.DockContent
{
private WebView2 _webView;
private Label _hint;
/// <summary>静态服务进程句柄(退出时杀进程树)。</summary>
private static Process _serverProcess;
public CadViewerWindow()
{
InitializeComponent();
this.BackColor = System.Drawing.Color.White;
Text = "CAD 查看器";
TabText = "CAD 查看器";
FormClosed += (s, e) => 停止网页服务();
BuildToolbar();
挂加载状态();
LoadViewer();
}
/// <summary>CAD 网页服务端口(与 server.cjs 一致)。</summary>
private const int 服务端口 = 5173;
/// <summary>顶部工具栏:显示蒙版按钮(蒙版存档就位后手动加载——
/// 打开图纸统一走网页自带的上传界面,实测渲染无问题)。
/// 右侧状态标签实时显示加载渲染进度(大图 40~60 秒,期间黑屏是正常加载态)。</summary>
private ToolStripLabel _statusLabel;
private void BuildToolbar()
{
var ts = new ToolStrip();
ts.Dock = DockStyle.Top;
ts.GripStyle = ToolStripGripStyle.Hidden;
var btnMask = new ToolStripButton("显示蒙版");
btnMask.Font = new System.Drawing.Font("Microsoft YaHei UI", 9F);
btnMask.Click += (s, e) => 成套报价软件.CadBridge.ReloadMasks();
ts.Items.Add(btnMask);
_statusLabel = new ToolStripLabel("等待打开图纸(用网页里的选择文件按钮)");
_statusLabel.Font = new System.Drawing.Font("Microsoft YaHei UI", 9F);
_statusLabel.ForeColor = System.Drawing.Color.DimGray;
ts.Items.Add(new ToolStripSeparator());
ts.Items.Add(_statusLabel);
Controls.Add(ts);
}
/// <summary>订阅网页加载进度,实时刷到工具栏状态标签(完成后显示总耗时)。</summary>
private void 挂加载状态()
{
成套报价软件.CadBridge.加载状态 += On加载状态;
FormClosed += (s, e) => 成套报价软件.CadBridge.加载状态 -= On加载状态;
}
private void On加载状态(string text)
{
try
{
if (_statusLabel == null || IsDisposed) return;
if (InvokeRequired)
BeginInvoke((MethodInvoker)delegate { 刷状态(text); });
else
刷状态(text);
}
catch { }
}
private void 刷状态(string text)
{
if (_statusLabel == null) return;
_statusLabel.Text = text;
// 完成显示绿,失败红,进行中灰
if (text.IndexOf("渲染完成") >= 0) _statusLabel.ForeColor = System.Drawing.Color.Green;
else if (text.IndexOf("失败") >= 0) _statusLabel.ForeColor = System.Drawing.Color.Red;
else _statusLabel.ForeColor = System.Drawing.Color.DimGray;
}
private async void LoadViewer()
{
try
{
_webView = new WebView2();
_webView.Dock = DockStyle.Fill;
Controls.Add(_webView);
// ★ WinForms 停靠按控件集合逆序排版:Fill 控件必须处在最前(最低索引),
// 最后分剩余空间;不加这句,先加的工具栏(Top)会叠在 WebView 上面盖住 CAD 画面
_webView.BringToFront();
await _webView.EnsureCoreWebView2Async(null);
CadBridge.Attach(_webView);
// ★ 禁用浏览器快捷键(F5/Ctrl+R/Ctrl+F 等):刷新会丢掉所有扒图蒙版
_webView.CoreWebView2.Settings.AreBrowserAcceleratorKeysEnabled = false;
// ★ 先确保静态服务在线(复刻主程序 StartPriceWebView 的探测+拉起模式)
启动网页服务();
// 服务不可达时给用户明确提示
_webView.CoreWebView2.NavigationCompleted += (s, e) =>
{
if (!e.IsSuccess && _hint == null)
ShowLoadHint("无法连接网页 CAD 服务(端口 " + 服务端口 + ")。\r\n请确认 cadbt\\node\\node.exe 与 cadbt\\CAD-Viewer\\dist 目录完整,然后点击\"重新加载\"。");
};
// ★ URL 带时间戳:dist 网页热更新后强制 WebView2 拉新 index.html
// (旧 node 进程无 no-store 头时会启发式缓存,网页端跑旧 bundle
// 导致宿主新消息无人处理——组框/高亮全失效)
_webView.CoreWebView2.Navigate("http://localhost:" + 服务端口 + "/?v=" + DateTime.Now.Ticks);
}
catch (Exception ex)
{
ShowLoadHint("WebView2 初始化失败: " + ex.Message + "\r\n请确认系统已安装 Microsoft Edge WebView2 运行时。");
}
}
/// <summary>启动 CAD 网页静态服务(端口已通=复用;不通=node server.cjs 后台拉起)。</summary>
private void 启动网页服务()
{
try
{
if (端口已通()) return; // 上次实例或手动 dev server 已在服务
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
// 安装布局:{app}\cadbt\node\node.exe + {app}\cadbt\CAD-Viewer\dist\server.cjs
// (make_package 已展平 packages 层,与独立 cadbt.exe 共用同一套文件)
string nodeExe = Path.Combine(baseDir, "cadbt", "node", "node.exe");
string serverScript = Path.Combine(baseDir, "cadbt", "CAD-Viewer", "dist", "server.cjs");
// ★ 开发机回退:仓库根在 bin\Debug 上三级(仓库根\成套报价软件\bin\Debug)。
// 旧代码只回退两级,算出 ...\成套报价软件\成套报价软件\cadbt\...(多一层项目目录),
// 开发版一直起不了服务——此前全靠装机目录实例顶着 5173 端口才没暴露
// (2026-09-24 cad服务.log 实证:缺少 node.exe 或 server.cjs 路径带双层项目名)。
// 两级三级都探测,哪个存在用哪个(防未来目录层级再变)。
if (!File.Exists(nodeExe))
{
string 三级 = Path.GetFullPath(Path.Combine(baseDir, @"..\..\..\cadbt\cadbt\bin\Debug\node\node.exe"));
string 二级 = Path.GetFullPath(Path.Combine(baseDir, @"..\..\cadbt\cadbt\bin\Debug\node\node.exe"));
nodeExe = File.Exists(三级) ? 三级 : (File.Exists(二级) ? 二级 : 三级);
}
if (!File.Exists(serverScript))
{
string 三级 = Path.GetFullPath(Path.Combine(baseDir, @"..\..\..\cadbt\cadbt\CAD-Viewer\packages\cad-viewer-example\dist\server.cjs"));
string 二级 = Path.GetFullPath(Path.Combine(baseDir, @"..\..\cadbt\cadbt\CAD-Viewer\packages\cad-viewer-example\dist\server.cjs"));
serverScript = File.Exists(三级) ? 三级 : (File.Exists(二级) ? 二级 : 三级);
}
if (!File.Exists(nodeExe) || !File.Exists(serverScript))
{
WriteDiag("[CAD服务] 缺少 node.exe 或 server.cjs: node=" + nodeExe + " script=" + serverScript);
return;
}
var psi = new ProcessStartInfo
{
FileName = nodeExe,
Arguments = "\"" + Path.GetFullPath(serverScript) + "\"",
WorkingDirectory = Path.GetDirectoryName(Path.GetFullPath(serverScript)),
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden,
};
_serverProcess = Process.Start(psi);
WriteDiag("[CAD服务] 已启动 node PID=" + (_serverProcess == null ? "?" : _serverProcess.Id.ToString()));
// 等端口就绪(最多 8 秒)
for (int i = 0; i < 40; i++)
{
System.Threading.Thread.Sleep(200);
if (端口已通()) { WriteDiag("[CAD服务] 端口就绪, 耗时 " + (i + 1) * 200 + "ms"); return; }
}
WriteDiag("[CAD服务] 端口 8 秒未就绪(继续导航,由 NavigationCompleted 提示)");
}
catch (Exception ex) { WriteDiag("[CAD服务] 启动异常: " + ex.Message); }
}
/// <summary>快速探测本机端口。</summary>
private static bool 端口已通()
{
try
{
using (var client = new TcpClient())
{
var ar = client.BeginConnect("127.0.0.1", 服务端口, null, null);
bool ok = ar.AsyncWaitHandle.WaitOne(400);
if (ok && client.Connected) { client.EndConnect(ar); return true; }
}
}
catch { }
return false;
}
/// <summary>退出清理:杀 node 静态服务进程树(taskkill /T 覆盖可能的子进程)。</summary>
private void 停止网页服务()
{
try
{
if (_serverProcess != null && !_serverProcess.HasExited)
{
int pid = _serverProcess.Id;
Process.Start(new ProcessStartInfo("taskkill", "/PID " + pid + " /T /F")
{
CreateNoWindow = true, UseShellExecute = false, WindowStyle = ProcessWindowStyle.Hidden
});
WriteDiag("[CAD服务] 已停止 PID=" + pid);
}
_serverProcess = null;
}
catch (Exception ex) { WriteDiag("[CAD服务] 停止异常: " + ex.Message); }
}
/// <summary>简单诊断日志(logs\cad服务.log)。</summary>
private static void WriteDiag(string msg)
{
try
{
string dir = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "logs");
Directory.CreateDirectory(dir);
File.AppendAllText(Path.Combine(dir, "cad服务.log"),
DateTime.Now.ToString("HH:mm:ss.fff") + " " + msg + Environment.NewLine);
}
catch { }
}
private void ShowLoadHint(string text)
{
if (_hint != null) return;
_hint = new Label();
_hint.Dock = DockStyle.Fill;
_hint.TextAlign = System.Drawing.ContentAlignment.MiddleCenter;
_hint.Text = text;
var reload = new Button();
reload.Text = "重新加载";
reload.Dock = DockStyle.Bottom;
reload.Click += (s, e) =>
{
Controls.Remove(_hint);
_hint.Dispose();
_hint = null;
启动网页服务();
if (_webView != null && _webView.CoreWebView2 != null)
_webView.CoreWebView2.Navigate("http://localhost:" + 服务端口 + "/");
};
Controls.Add(reload);
Controls.Add(_hint);
_hint.BringToFront();
}
}
}
@@ -0,0 +1,85 @@
// ============================================================
// 文档区停靠窗口:CAD 扒图工作区
// = 独立 cadbt.exe 主窗口的完整布局原样搬入:
// 左 报价工具(CadQuoteWindow,内嵌 Form3 报价三表)
// 中 CAD 查看器(CadViewerWindow,WebView2 网页 CAD)
// 内部用 Smart DockSurface 承载(与独立版 MainForm 相同的停靠结构,
// 内部布局可拖拽);F1/F2/F3 全局消息过滤器随窗口启停。
// ============================================================
using System;
using System.Windows.Forms;
namespace 成套报价软件
{
/// <summary>CAD 扒图工作区(DockState.Document):报价三表 + 网页 CAD 查看器。</summary>
public class Cad扒图窗口 : WeifenLuo.WinFormsUI.Docking.DockContent
{
private readonly F1MessageFilter _f1Filter = new F1MessageFilter();
private CadQuoteWindow _quote;
private CadViewerWindow _viewer;
/// <summary>内嵌的报价界面(主窗口功能区按钮跨窗口调用用;未创建为 null)。</summary>
public MyApp.Form3 报价表单
{
get { return _quote != null ? _quote.报价表单 : null; }
}
public Cad扒图窗口()
{
Text = "CAD扒图";
TabText = "CAD扒图";
this.BackColor = System.Drawing.Color.White;
// 与独立版 MainForm.CreateDockWindows_dockSurface1 相同的内部结构
var dockSurface = new Smart.CustomComponents.DockSurface();
dockSurface.Dock = DockStyle.Fill;
Controls.Add(dockSurface);
dockSurface.EnsureDefaultTheme(); // 必须:Show 前保证有效主题
_quote = new CadQuoteWindow();
_quote.CloseButtonVisible = false;
_quote.TabText = "报价工具";
_quote.Text = "报价工具";
_quote.Show(dockSurface, WeifenLuo.WinFormsUI.Docking.DockState.DockLeft);
_viewer = new CadViewerWindow();
_viewer.CloseButtonVisible = false;
_viewer.TabText = "CAD 查看器";
_viewer.Text = "CAD 查看器";
_viewer.Show(dockSurface, WeifenLuo.WinFormsUI.Docking.DockState.Document);
// ★ 全局 F1/F2/F3 过滤器:焦点在任何 WinForms 控件上都触发元件合并
// (焦点在网页时由网页端转发,两路都汇聚到 CadBridge.F*Requested)
Application.AddMessageFilter(_f1Filter);
FormClosed += (s, e) =>
{
Application.RemoveMessageFilter(_f1Filter);
// 显式关内部窗口:Form3 解绑桥事件、CAD 查看器停 node 服务
try { if (_quote != null && !_quote.IsDisposed) _quote.Close(); } catch { }
try { if (_viewer != null && !_viewer.IsDisposed) _viewer.Close(); } catch { }
};
}
/// <summary>把 F1/F2/F3 按键(0x70/0x71/0x72)转成 CadBridge 事件,不拦截按键本身。</summary>
private class F1MessageFilter : IMessageFilter
{
public bool PreFilterMessage(ref Message m)
{
const int WM_KEYDOWN = 0x0100;
if (m.Msg == WM_KEYDOWN && (int)m.WParam == 0x70)
{
try { CadBridge.RaiseF1(); } catch { }
}
else if (m.Msg == WM_KEYDOWN && (int)m.WParam == 0x71)
{
try { CadBridge.RaiseF2(); } catch { }
}
else if (m.Msg == WM_KEYDOWN && (int)m.WParam == 0x72)
{
try { CadBridge.RaiseF3(); } catch { }
}
return false;
}
}
}
}
+355
View File
@@ -0,0 +1,355 @@
// ============================================================
// GlmOcrClient:智谱 GLM-OCR 云端接口(版面解析 layout_parsing)
// · POST {BaseUrl} Authorization: Bearer {ApiKey}
// {"model":"glm-ocr","file":"data:image/png;base64,..."}
// · 响应 layout_details[页][{label,bbox_2d(归一化),content,width,height}]
// → label=text 的块转 OcrItem(bbox 映射回裁剪图像素坐标)
// · 手写字识别(模型内置,无需专用参数);价格 ~0.2 元/百万 tokens
// · 纯 C# HTTP,无需 Python;引擎切换由 OcrClient 路由
// ============================================================
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web.Script.Serialization;
namespace 成套报价软件
{
/// <summary>智谱 GLM-OCR 客户端(单实例)。</summary>
internal sealed class GlmOcrClient
{
private readonly JavaScriptSerializer _json = new JavaScriptSerializer();
private string _apiKey;
private string _baseUrl;
/// <summary>状态提示(未配置Key/请求失败等),接 OcrClient.StatusChanged。</summary>
public event Action<string> StatusChanged;
public GlmOcrClient()
{
_baseUrl = ReadAppSetting("GlmBaseUrl",
"https://open.bigmodel.cn/api/paas/v4/layout_parsing");
try
{
// net48 下显式启用 TLS1.2(大模型 API 必须)
ServicePointManager.SecurityProtocol =
ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12;
}
catch { }
}
/// <summary>Key 由 OcrClient 从 baidu_ocr.json 的 glm_api_key 注入(热更新)。</summary>
public void SetApiKey(string key)
{
_apiKey = key == null ? "" : key.Trim();
}
public bool IsConfigured
{
get { return !string.IsNullOrWhiteSpace(_apiKey); }
}
private static string ReadAppSetting(string key, string def)
{
try
{
var v = System.Configuration.ConfigurationManager.AppSettings[key];
return string.IsNullOrWhiteSpace(v) ? def : v;
}
catch { return def; }
}
/// <summary>识别一块位图 → 文本块列表(带 bbox 像素坐标)。</summary>
public Task<List<OcrItem>> RecognizeAsync(Bitmap crop, int timeoutMs)
{
if (crop == null || crop.Width <= 0 || crop.Height <= 0)
return Task.FromResult(new List<OcrItem>());
if (!IsConfigured)
{
Notify("GLM OCR 未配置 API Key(OCR 设置窗口里填智谱 API Key)");
var tcs0 = new TaskCompletionSource<List<OcrItem>>();
tcs0.SetException(new Exception(
"智谱 GLM-OCR 未配置 API Key。\r\n请到 open.bigmodel.cn 获取 Key," +
"在 OCR 设置窗口选择引擎\"智谱 GLM-OCR\"并填入 Key 后保存。"));
return tcs0.Task;
}
return Task.Factory.StartNew(() =>
{
var sw = System.Diagnostics.Stopwatch.StartNew();
try
{
var result = RequestAndParse(crop, timeoutMs);
Notify("智谱GLM-OCR·" + result.Count + "条 " + sw.ElapsedMilliseconds + "ms");
return result;
}
catch
{
throw;
}
});
}
private List<OcrItem> RequestAndParse(Bitmap crop, int timeoutMs)
{
byte[] png;
using (var ms = new MemoryStream())
{
crop.Save(ms, ImageFormat.Png);
png = ms.ToArray();
}
// 服务端期望 data URI 或 URL(SDK 自动包装 data URI,此处显式包装)
string fileField = "data:image/png;base64," + Convert.ToBase64String(png);
string body = _json.Serialize(new Dictionary<string, object>
{
{ "model", "glm-ocr" },
{ "file", fileField }
});
var req = (HttpWebRequest)WebRequest.Create(_baseUrl);
req.Method = "POST";
req.ContentType = "application/json";
req.Headers.Add("Authorization", "Bearer " + _apiKey);
req.Timeout = timeoutMs;
req.ReadWriteTimeout = timeoutMs;
req.Proxy = null; // 尊重直连,避免系统代理拖慢
var bytes = Encoding.UTF8.GetBytes(body);
req.ContentLength = bytes.Length;
using (var rs = req.GetRequestStream())
{
rs.Write(bytes, 0, bytes.Length);
}
string respText;
try
{
using (var resp = (HttpWebResponse)req.GetResponse())
using (var sr = new StreamReader(resp.GetResponseStream(),
new UTF8Encoding(false)))
{
respText = sr.ReadToEnd();
}
}
catch (WebException wex)
{
string detail = "";
try
{
if (wex.Response != null)
using (var sr = new StreamReader(wex.Response.GetResponseStream(),
new UTF8Encoding(false)))
detail = sr.ReadToEnd();
}
catch { }
string hint = wex.Response == null
? ("网络不可达: " + wex.Message)
: ("HTTP " + (int)((HttpWebResponse)wex.Response).StatusCode + " " + detail);
throw new Exception("GLM OCR 请求失败: " + hint);
}
return ParseResponse(respText, crop.Width, crop.Height);
}
/// <summary>解析 layout_parsing 响应 → OcrItem 列表(text 块)。</summary>
private List<OcrItem> ParseResponse(string respText, int cropW, int cropH)
{
var msg = _json.Deserialize<Dictionary<string, object>>(respText);
if (msg == null) throw new Exception("GLM 响应解析失败(空响应)");
if (msg.ContainsKey("error"))
{
var err = msg["error"] as Dictionary<string, object>;
string code = err != null && err.ContainsKey("code") ? Convert.ToString(err["code"]) : "?";
string message = err != null && err.ContainsKey("message") ? Convert.ToString(err["message"]) : respText;
throw new Exception("GLM OCR 错误 [" + code + "]: " + message);
}
var items = new List<OcrItem>();
var pages = msg.ContainsKey("layout_details")
? msg["layout_details"] as System.Collections.ArrayList : null;
if (pages == null || pages.Count == 0)
throw new Exception("GLM 响应没有 layout_details(内容: " +
(respText.Length > 300 ? respText.Substring(0, 300) : respText) + ")");
// 单页输入 → 取第一页的块
var blocks = pages[0] as System.Collections.ArrayList;
if (blocks == null) return items;
foreach (Dictionary<string, object> b in blocks)
{
string label = b.ContainsKey("label") ? Convert.ToString(b["label"]) : "";
string content = b.ContainsKey("content") ? Convert.ToString(b["content"]) : "";
if (string.IsNullOrWhiteSpace(content)) continue;
var bbox = b.ContainsKey("bbox_2d") ? b["bbox_2d"] as System.Collections.ArrayList : null;
if (bbox == null || bbox.Count < 4) continue;
// ★ 实测返回像素坐标;官方文档描述归一化(0~1)。按值域自动兼容:
// 四个值全部 ≤1.5 → 归一化,乘图尺寸;否则直接当像素用。
float a0 = Convert.ToSingle(bbox[0]), a1 = Convert.ToSingle(bbox[1]);
float a2 = Convert.ToSingle(bbox[2]), a3 = Convert.ToSingle(bbox[3]);
bool normalized = a0 <= 1.5f && a1 <= 1.5f && a2 <= 1.5f && a3 <= 1.5f;
float x1 = normalized ? a0 * cropW : a0;
float y1 = normalized ? a1 * cropH : a1;
float x2 = normalized ? a2 * cropW : a2;
float y2 = normalized ? a3 * cropH : a3;
// 裁剪到图内(超界坐标画在页面外会导致蒙版不可见)
x1 = Math.Max(0, Math.Min(cropW, x1));
y1 = Math.Max(0, Math.Min(cropH, y1));
x2 = Math.Max(0, Math.Min(cropW, x2));
y2 = Math.Max(0, Math.Min(cropH, y2));
if (label == "table")
{
// ★ 框选多行文字常被判定为表格块(content 为 <table> HTML):
// 每个 <tr> 一条文本(单元格空格拼接),行高均分定位。
// 必须在 CleanHtml 剥标签之前解析结构。
AddTableItems(items, content, x1, y1, x2, y2);
continue;
}
if (label != "text") continue; // 图片/公式块跳过
content = CleanHtmlShared(content); // 居中块会返回 <div align=..>..</div> 等 HTML
if (string.IsNullOrWhiteSpace(content)) continue;
// 多行文本块拆行(content 按 \n,块内均匀分布);清理 Markdown 标记
// ★ 行间上下各收缩 15%(≤3px):蒙版之间留空隙,不连成一片
string[] lines = content.Split(new[] { '\r', '\n' },
StringSplitOptions.RemoveEmptyEntries);
float lineH = (y2 - y1) / lines.Length;
float pad = Math.Min(lineH * 0.15f, 3f);
for (int i = 0; i < lines.Length; i++)
{
string text = CleanMarkdownShared(lines[i]).Trim();
if (text.Length == 0) continue;
float ty1 = y1 + lineH * i + pad;
float ty2 = y1 + lineH * (i + 1) - pad;
if (ty2 - ty1 < 2) { ty1 = y1 + lineH * i; ty2 = ty1 + Math.Max(2, lineH); }
items.Add(new OcrItem
{
Text = text,
Box = new[]
{
new PointF(x1, ty1),
new PointF(x2, ty1),
new PointF(x2, ty2),
new PointF(x1, ty2)
},
Score = 1f
});
}
}
return items;
}
private static readonly System.Text.RegularExpressions.Regex _trRegex =
new System.Text.RegularExpressions.Regex("<tr[^>]*>(.*?)</tr>",
System.Text.RegularExpressions.RegexOptions.Singleline |
System.Text.RegularExpressions.RegexOptions.IgnoreCase |
System.Text.RegularExpressions.RegexOptions.Compiled);
private static readonly System.Text.RegularExpressions.Regex _tdRegex =
new System.Text.RegularExpressions.Regex("<t[dh][^>]*>(.*?)</t[dh]>",
System.Text.RegularExpressions.RegexOptions.Singleline |
System.Text.RegularExpressions.RegexOptions.IgnoreCase |
System.Text.RegularExpressions.RegexOptions.Compiled);
/// <summary>表格块 → 每 tr 一条 item(td 内容空格拼接),行高均分 bbox。</summary>
private static void AddTableItems(List<OcrItem> items, string tableHtml,
float x1, float y1, float x2, float y2)
{
var rows = new List<string>();
foreach (System.Text.RegularExpressions.Match tr in _trRegex.Matches(tableHtml))
{
var cells = new List<string>();
foreach (System.Text.RegularExpressions.Match td in _tdRegex.Matches(tr.Groups[1].Value))
{
string cell = CleanHtmlShared(td.Groups[1].Value).Trim();
if (cell.Length > 0) cells.Add(cell);
}
if (cells.Count > 0) rows.Add(string.Join(" ", cells.ToArray()));
}
if (rows.Count == 0) return;
float rowH = (y2 - y1) / rows.Count;
float pad = Math.Min(rowH * 0.15f, 3f); // 行间留空隙,蒙版不连片
for (int i = 0; i < rows.Count; i++)
{
float ty1 = y1 + rowH * i + pad;
float ty2 = y1 + rowH * (i + 1) - pad;
if (ty2 - ty1 < 2) { ty1 = y1 + rowH * i; ty2 = ty1 + Math.Max(2, rowH); }
items.Add(new OcrItem
{
Text = rows[i],
Box = new[]
{
new PointF(x1, ty1),
new PointF(x2, ty1),
new PointF(x2, ty2),
new PointF(x1, ty2)
},
Score = 1f
});
}
}
private static readonly System.Text.RegularExpressions.Regex _htmlTag =
new System.Text.RegularExpressions.Regex("<[^>]*>",
System.Text.RegularExpressions.RegexOptions.Compiled);
/// <summary>
/// 剥离 GLM content 里的 HTML 片段(居中块返回 div 等)与常见实体;
/// 标签替换为空格,避免前后文字误粘连;连续空格合并。
/// </summary>
internal static string CleanHtmlShared(string s)
{
if (string.IsNullOrEmpty(s)) return s;
if (s.IndexOf('<') >= 0)
{
s = _htmlTag.Replace(s, " ");
s = s.Replace("&nbsp;", " ").Replace("&amp;", "&")
.Replace("&lt;", "<").Replace("&gt;", ">")
.Replace("&quot;", "\"").Replace("&apos;", "'");
}
while (s.Contains(" ")) s = s.Replace(" ", " ");
return s;
}
/// <summary>
/// 清理 GLM 返回 content 里的 Markdown 标记(★保守规则,不伤正文):
/// · 行首 "##"/"#" 标题井号(仅行首;"2#配电箱" 中间的 # 保留)
/// · 成对 **加粗** 星号、行首 "- "/"* " 列表标记
/// </summary>
internal static string CleanMarkdownShared(string s)
{
if (string.IsNullOrEmpty(s)) return s;
// 行首标题:# 开头且后跟空格/更多#/行尾 → 剥掉行首所有 #
string t = s.TrimStart();
if (t.StartsWith("#"))
{
int i = 0;
while (i < t.Length && t[i] == '#') i++;
// "#..." 后是空格或行尾才视为标题标记
if (i >= t.Length || t[i] == ' ' || t[i] == '\t')
t = t.Substring(i).TrimStart();
// 否则(如 "#5 线缆")保留原样
else t = s;
}
// 行首列表标记
if (t.StartsWith("- ") || t.StartsWith("* "))
t = t.Substring(2);
// 成对 ** (加粗) 去星号
if (t.Contains("**"))
t = t.Replace("**", "");
return t;
}
private void Notify(string s)
{
var h = StatusChanged;
if (h != null) h(s);
}
}
}
+447
View File
@@ -0,0 +1,447 @@
// ============================================================
// OcrClient:OCR 引擎路由(baidu_ocr.json 的 engine 字段选择)
// · engine=baidu(默认):百度 OCR 高精度含位置版(HTTP 直连)
// · engine=glm:智谱 GLM-OCR 版面解析(手写字强,GlmOcrClient)
// · Key 都在 OcrPaths.ConfigPath 的 baidu_ocr.json(OCR设置窗口写):
// api_key/secret_key=百度;glm_api_key=智谱
// 百度未保存过/Key 为空 → 内置默认 Key 直接可用
// · 百度 access_token 缓存同目录 baidu_token.json(29 天,提前1天过期)
// · 未配 Key / 网络失败直接抛异常(界面提示),无本地回退
// ============================================================
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web.Script.Serialization;
namespace 成套报价软件
{
/// <summary>
/// OCR 配置/缓存/本体的路径解析(兼容独立版 pdfbt 的旧布局):
/// 主程序 exe 旁优先;装过独立版 pdfbt 的机器回退 pdfbt\ 子目录旧文件。
/// </summary>
public static class OcrPaths
{
/// <summary>
/// baidu_ocr.json:主 exe 旁已存在→直接用;否则看 pdfbt\ 子目录(独立版
/// pdfbt.exe 保存过的配置);都没有→主 exe 旁(首次保存的新文件)。
/// </summary>
public static string ConfigPath
{
get
{
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
string p = Path.Combine(baseDir, "baidu_ocr.json");
if (File.Exists(p)) return p;
string legacy = Path.Combine(baseDir, "pdfbt", "baidu_ocr.json");
if (File.Exists(legacy)) return legacy;
return p;
}
}
/// <summary>百度 token 缓存,与配置文件同目录(换 Key 后删它重换)。</summary>
public static string TokenCachePath
{
get { return Path.Combine(Path.GetDirectoryName(ConfigPath), "baidu_token.json"); }
}
}
public class OcrItem
{
public string Text;
public PointF[] Box; // 裁剪图像素坐标,4 点(左上起顺时针)
public float Score;
public RectangleF Bounds
{
get
{
if (Box == null || Box.Length == 0) return RectangleF.Empty;
float l = Box[0].X, t = Box[0].Y, r = Box[0].X, b = Box[0].Y;
foreach (var p in Box)
{
if (p.X < l) l = p.X; if (p.X > r) r = p.X;
if (p.Y < t) t = p.Y; if (p.Y > b) b = p.Y;
}
return RectangleF.FromLTRB(l, t, r, b);
}
}
}
public class OcrClient : IDisposable
{
public event Action<string> StatusChanged;
private readonly JavaScriptSerializer _json = new JavaScriptSerializer();
private string _ak = "", _sk = "";
private string _token;
private DateTime _tokenExpire = DateTime.MinValue;
// ★ 引擎路由:baidu(默认)/glm(智谱 GLM-OCR)/qianfan(百度 PaddleOCR-VL)/uapis(UApiPro)/umi(Umi-OCR 本地)
private string _engine = "baidu";
private readonly GlmOcrClient _glm = new GlmOcrClient();
private readonly QianfanOcrClient _qianfan = new QianfanOcrClient();
private readonly UapisOcrClient _uapis = new UapisOcrClient();
private readonly UmiOcrClient _umi = new UmiOcrClient();
/// <summary>当前 OCR 引擎(baidu/glm/qianfan/uapis/umi)。</summary>
public string Engine
{
get { return _engine; }
}
// 内置默认 Key(Base64 混淆,exe 内非明文):未保存过配置/Key 为空时直接使用
private static readonly string _defAk = Encoding.UTF8.GetString(Convert.FromBase64String("R0hjbjVxRmJLcG5TY3NyZHlIV1M4dzBB"));
private static readonly string _defSk = Encoding.UTF8.GetString(Convert.FromBase64String("VkMxVVU2RXlxN28wcGRLNElCZ1A4ZnNhWjIyQ3RlMWM="));
public OcrClient()
{
_glm.StatusChanged += msg =>
{
var h = StatusChanged;
if (h != null) h(msg);
};
_qianfan.StatusChanged += msg =>
{
var h = StatusChanged;
if (h != null) h(msg);
};
_uapis.StatusChanged += msg =>
{
var h = StatusChanged;
if (h != null) h(msg);
};
_umi.StatusChanged += msg =>
{
var h = StatusChanged;
if (h != null) h(msg);
};
LoadConfig();
}
/// <summary>读 baidu_ocr.json(OCR设置窗口保存后调用可热更新 Key/引擎)。</summary>
public void LoadConfig()
{
try
{
string p = OcrPaths.ConfigPath;
if (File.Exists(p))
{
var jsv = _json.Deserialize<Dictionary<string, string>>(File.ReadAllText(p));
if (jsv != null)
{
_ak = jsv.ContainsKey("api_key") ? (jsv["api_key"] ?? "") : "";
_sk = jsv.ContainsKey("secret_key") ? (jsv["secret_key"] ?? "") : "";
_engine = jsv.ContainsKey("engine") ? (jsv["engine"] ?? "baidu") : "baidu";
_glm.SetApiKey(jsv.ContainsKey("glm_api_key") ? (jsv["glm_api_key"] ?? "") : "");
_qianfan.SetApiKey(jsv.ContainsKey("qianfan_api_key") ? (jsv["qianfan_api_key"] ?? "") : "");
_uapis.SetApiKey(jsv.ContainsKey("uapis_api_key") ? (jsv["uapis_api_key"] ?? "") : "");
_umi.SetBaseUrl(jsv.ContainsKey("umi_url") ? (jsv["umi_url"] ?? "") : "");
}
}
}
catch { }
// 缺省回退内置默认 Key:装完即用,无需打开设置点保存;用户保存过则文件值优先
if (string.IsNullOrEmpty(_ak)) _ak = _defAk;
if (string.IsNullOrEmpty(_sk)) _sk = _defSk;
if (_engine != "baidu" && _engine != "glm" && _engine != "qianfan" && _engine != "uapis" && _engine != "umi")
_engine = "baidu";
}
public bool HasKey
{
get { return !string.IsNullOrEmpty(_ak) && !string.IsNullOrEmpty(_sk); }
}
private void Notify(string msg)
{
var h = StatusChanged;
if (h != null) h(msg);
}
/// <summary>获取 access_token(内存→文件缓存→在线换取)。失败抛异常。</summary>
private string GetToken()
{
if (_token != null && DateTime.Now < _tokenExpire) return _token;
string cachePath = OcrPaths.TokenCachePath;
try
{
if (File.Exists(cachePath))
{
var c = _json.Deserialize<Dictionary<string, object>>(File.ReadAllText(cachePath));
if (c != null && c.ContainsKey("token") && c.ContainsKey("expire"))
{
double exp;
double.TryParse(Convert.ToString(c["expire"]), out exp);
string tok = Convert.ToString(c["token"]);
if (tok.Length > 0 && DateTime.Now < new DateTime(1970, 1, 1).AddSeconds(exp))
{
_token = tok;
_tokenExpire = new DateTime(1970, 1, 1).AddSeconds(exp);
return _token;
}
}
}
}
catch { }
if (!HasKey)
throw new Exception("未配置百度 OCR Key(工具栏 OCR设置 填写 API Key / Secret Key)");
string url = "https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials"
+ "&client_id=" + Uri.EscapeDataString(_ak)
+ "&client_secret=" + Uri.EscapeDataString(_sk);
using (var wc = new WebClient())
{
wc.Encoding = Encoding.UTF8;
wc.Headers["Content-Type"] = "application/x-www-form-urlencoded";
string body = wc.UploadString(url, "POST", "");
var resp = _json.Deserialize<Dictionary<string, object>>(body);
if (resp == null || !resp.ContainsKey("access_token"))
throw new Exception("百度 token 获取失败: " +
(resp != null && resp.ContainsKey("error_description")
? Convert.ToString(resp["error_description"])
: body.Substring(0, Math.Min(120, body.Length))));
_token = Convert.ToString(resp["access_token"]);
double expires;
double.TryParse(Convert.ToString(resp.ContainsKey("expires_in") ? resp["expires_in"] : "2592000"), out expires);
_tokenExpire = DateTime.Now.AddSeconds(Math.Max(3600, expires - 86400));
try
{
var cache = new Dictionary<string, object>
{
{ "token", _token },
{ "expire", (_tokenExpire - new DateTime(1970, 1, 1)).TotalSeconds }
};
File.WriteAllText(cachePath, _json.Serialize(cache));
}
catch { }
return _token;
}
}
/// <summary>识别位图(300DPI 裁剪)。失败抛异常;成功通知引擎来源。
/// 竖排文字(实为旋转 90° 的横排)直接识别必乱码:细高框或平均置信度偏低时,
/// 旋转 ±90° 重识别并按平均置信度选优,坐标逆向还原回原图。</summary>
public Task<List<OcrItem>> RecognizeAsync(Bitmap crop, int timeoutMs)
{
// ★ 引擎路由:智谱 GLM-OCR / 千帆 PaddleOCR-VL(版面解析,不走旋转重试)
if (_engine == "glm")
return _glm.RecognizeAsync(crop, timeoutMs);
if (_engine == "qianfan")
return _qianfan.RecognizeAsync(crop, timeoutMs);
if (_engine == "uapis")
return _uapis.RecognizeAsync(crop, timeoutMs);
if (_engine == "umi")
return _umi.RecognizeAsync(crop, timeoutMs);
var tcs = new TaskCompletionSource<List<OcrItem>>();
Task.Factory.StartNew(delegate
{
try
{
var items = RecognizeOnce(crop, timeoutMs);
try
{
if (crop.Height >= crop.Width * 1.15f || AvgScore(items) < 0.88f)
{
float best = AvgScore(items);
foreach (bool cw in new bool[] { true, false })
{
using (var turned = new Bitmap(crop))
{
turned.RotateFlip(cw ? RotateFlipType.Rotate90FlipNone : RotateFlipType.Rotate270FlipNone);
var rr = RecognizeOnce(turned, timeoutMs);
UnrotateItems(rr, cw, crop.Width, crop.Height);
float s = AvgScore(rr);
if (s > best + 0.01f) { best = s; items = rr; }
}
}
}
}
catch { /* 旋转重试失败:保留原方向结果 */ }
Notify("百度OCR·高精度含位置版");
tcs.TrySetResult(items);
}
catch (Exception ex)
{
tcs.TrySetException(ex);
}
});
return tcs.Task;
}
/// <summary>单方向识别一次(编码→上传→解析);Box 为入参图像的像素坐标。</summary>
private List<OcrItem> RecognizeOnce(Bitmap img, int timeoutMs)
{
// 编码为百度限额内(高精度版 base64 后 ≤4MB)的 JPEG;返回缩放比用于还原识别坐标
float sendScale;
string b64 = EncodeFit(img, out sendScale);
string tok = GetToken();
string url = "https://aip.baidubce.com/rest/2.0/ocr/v1/accurate?access_token="
+ Uri.EscapeDataString(tok);
// Base64 的表单转义不能用 Uri.EscapeDataString:.NET Framework 对超长字符串
// (约 32K 字符)抛"无效的 URI: URI 字符串太长",大图 base64 必中
string form = "image=" + EscapeBase64(b64)
+ "&language_type=CHN_ENG&probability=true";
using (var wc = new WebClient())
{
wc.Encoding = Encoding.UTF8;
wc.Headers["Content-Type"] = "application/x-www-form-urlencoded";
string body = wc.UploadString(url, "POST", form);
var resp = _json.Deserialize<Dictionary<string, object>>(body);
if (resp == null)
throw new Exception("百度OCR返回解析失败");
if (resp.ContainsKey("error_code") && Convert.ToInt64(resp["error_code"]) != 0)
throw new Exception("百度OCR错误 " + Convert.ToString(resp["error_code"]) + ": "
+ Convert.ToString(resp.ContainsKey("error_msg") ? resp["error_msg"] : ""));
var items = new List<OcrItem>();
var arr = resp.ContainsKey("words_result") ? resp["words_result"] as System.Collections.ArrayList : null;
if (arr != null)
{
foreach (Dictionary<string, object> w in arr)
{
var loc = (w != null && w.ContainsKey("location")) ? w["location"] as Dictionary<string, object> : null;
if (loc == null) continue;
float l = LocF(loc, "left"), t = LocF(loc, "top");
float w2 = LocF(loc, "width"), h = LocF(loc, "height");
float score = 0.95f;
var prob = w.ContainsKey("probability") ? w["probability"] as Dictionary<string, object> : null;
if (prob != null && prob.ContainsKey("average"))
{
float sv;
if (float.TryParse(Convert.ToString(prob["average"]), out sv)) score = sv;
}
items.Add(new OcrItem
{
Text = Convert.ToString(w.ContainsKey("words") ? w["words"] : ""),
// location 是发送图的像素坐标,除以缩放比还原到原始裁剪图坐标
Box = new PointF[]
{
new PointF(l / sendScale, t / sendScale),
new PointF((l + w2) / sendScale, t / sendScale),
new PointF((l + w2) / sendScale, (t + h) / sendScale),
new PointF(l / sendScale, (t + h) / sendScale)
},
Score = score
});
}
}
return items;
}
}
/// <summary>平均置信度(无结果时 -1,便于选优时落败)。</summary>
private static float AvgScore(List<OcrItem> items)
{
if (items == null || items.Count == 0) return -1f;
float sum = 0;
foreach (var it in items) sum += it.Score;
return sum / items.Count;
}
/// <summary>把顺/逆时针旋转 90° 后识别出的 Box 坐标逆向还原到原图坐标系
/// (还原后重排为 左上/右上/右下/左下,Bounds 语义不变)。</summary>
private static void UnrotateItems(List<OcrItem> items, bool clockwise, int w, int h)
{
foreach (var it in items)
{
if (it.Box == null) continue;
float l = float.MaxValue, t = float.MaxValue, r = float.MinValue, b = float.MinValue;
foreach (var p in it.Box)
{
float x, y;
if (clockwise) { x = p.Y; y = h - 1f - p.X; }
else { x = w - 1f - p.Y; y = p.X; }
if (x < l) l = x; if (x > r) r = x;
if (y < t) t = y; if (y > b) b = y;
}
it.Box = new PointF[]
{
new PointF(l, t), new PointF(r, t),
new PointF(r, b), new PointF(l, b)
};
}
}
private static float LocF(Dictionary<string, object> loc, string key)
{
float v;
return (loc != null && loc.ContainsKey(key) &&
float.TryParse(Convert.ToString(loc[key]), out v)) ? v : 0f;
}
/// <summary>编码为百度限额内的 JPEG(高精度版要求 base64 后 ≤4MB):
/// 质量 90/75/60 递降试编码,均超限则按 0.8 倍降采样重试(最多 8 轮,必然收敛)。
/// scale=发送图/原图(≤1),调用方用它把返回坐标还原到原图。</summary>
private static string EncodeFit(Bitmap img, out float scale)
{
scale = 1f;
string last = null;
for (int round = 0; round < 8; round++)
{
int w = Math.Max(1, (int)Math.Round(img.Width * scale));
int h = Math.Max(1, (int)Math.Round(img.Height * scale));
using (Bitmap cur = (w == img.Width && h == img.Height) ? new Bitmap(img) : new Bitmap(img, w, h))
{
foreach (long q in new long[] { 90, 75, 60 })
{
last = EncodeJpeg(cur, q);
// base64 字符数≈UTF-8 字节数;3.9M 留余量防 4M 硬限被卡
if (last.Length <= 3900000) return last;
}
}
scale *= 0.8f;
}
return last;
}
private static string EncodeJpeg(Bitmap bmp, long quality)
{
using (var ms = new MemoryStream())
{
var codec = GetJpegEncoder();
if (codec != null)
{
using (var ep = new EncoderParameters(1))
{
ep.Param[0] = new EncoderParameter(System.Drawing.Imaging.Encoder.Quality, quality);
bmp.Save(ms, codec, ep);
}
}
else bmp.Save(ms, ImageFormat.Jpeg);
return Convert.ToBase64String(ms.ToArray());
}
}
/// <summary>Base64 的 URI 表单转义:仅 + / = 三个字符转 %XX,字母数字无需转义。
/// 不能用 Uri.EscapeDataString:.NET Framework 对约 32K 以上字符串抛"URI 字符串太长"。</summary>
private static string EscapeBase64(string s)
{
var sb = new StringBuilder(s.Length + s.Length / 2);
for (int i = 0; i < s.Length; i++)
{
char c = s[i];
if (c == '+') sb.Append("%2B");
else if (c == '/') sb.Append("%2F");
else if (c == '=') sb.Append("%3D");
else sb.Append(c);
}
return sb.ToString();
}
private static ImageCodecInfo GetJpegEncoder()
{
foreach (var c in ImageCodecInfo.GetImageEncoders())
if (c.FormatID == ImageFormat.Jpeg.Guid) return c;
return null;
}
public void Dispose() { }
}
}
+302
View File
@@ -0,0 +1,302 @@
namespace 成套报价软件
{
/// <summary>OCR 设置对话框:引擎选择(百度/智谱GLM) + 对应 Key(存 baidu_ocr.json)。</summary>
public partial class OcrSettingsForm : System.Windows.Forms.Form
{
/// <summary>必需的设计器变量。</summary>
private System.ComponentModel.IContainer components = null;
/// <summary>清理正在使用的资源。</summary>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows 窗体设计器生成的代码
private void InitializeComponent()
{
this.lblEngine = new System.Windows.Forms.Label();
this.cboEngine = new System.Windows.Forms.ComboBox();
this.lblApiKey = new System.Windows.Forms.Label();
this.txtApiKey = new System.Windows.Forms.TextBox();
this.lblSecretKey = new System.Windows.Forms.Label();
this.txtSecretKey = new System.Windows.Forms.TextBox();
this.lblGlmKey = new System.Windows.Forms.Label();
this.txtGlmKey = new System.Windows.Forms.TextBox();
this.lblQianfanKey = new System.Windows.Forms.Label();
this.txtQianfanKey = new System.Windows.Forms.TextBox();
this.lblUapisKey = new System.Windows.Forms.Label();
this.txtUapisKey = new System.Windows.Forms.TextBox();
this.lblUmiUrl = new System.Windows.Forms.Label();
this.txtUmiUrl = new System.Windows.Forms.TextBox();
this.btnUmi下载 = new System.Windows.Forms.Button();
this.btnUmi检测 = new System.Windows.Forms.Button();
this.lblUmi状态 = new System.Windows.Forms.Label();
this.btn帮助 = new System.Windows.Forms.Button();
this.lbl说明 = new System.Windows.Forms.Label();
this.btn确定 = new System.Windows.Forms.Button();
this.btn取消 = new System.Windows.Forms.Button();
this.SuspendLayout();
//
// lblEngine
//
this.lblEngine.AutoSize = true;
this.lblEngine.Location = new System.Drawing.Point(12, 18);
this.lblEngine.Name = "lblEngine";
this.lblEngine.Size = new System.Drawing.Size(89, 18);
this.lblEngine.TabIndex = 0;
this.lblEngine.Text = "识别引擎:";
//
// cboEngine
//
this.cboEngine.DropDownStyle = System.Windows.Forms.ComboBoxStyle.DropDownList;
this.cboEngine.Items.AddRange(new object[] {
"百度 OCR 高精度(含位置)",
"智谱 GLM-OCR(版面解析/手写强)",
"百度 PaddleOCR-VL(千帆版面解析)",
"UApiPro 通用OCR(免费积分/带位置)",
"Umi-OCR 本地引擎(离线免费,需先运行Umi-OCR)"});
this.cboEngine.Location = new System.Drawing.Point(139, 15);
this.cboEngine.Name = "cboEngine";
this.cboEngine.Size = new System.Drawing.Size(389, 26);
this.cboEngine.TabIndex = 1;
this.cboEngine.SelectedIndexChanged += new System.EventHandler(this.cboEngine_SelectedIndexChanged);
//
// lblApiKey
//
this.lblApiKey.AutoSize = true;
this.lblApiKey.Location = new System.Drawing.Point(12, 55);
this.lblApiKey.Name = "lblApiKey";
this.lblApiKey.Size = new System.Drawing.Size(80, 18);
this.lblApiKey.TabIndex = 2;
this.lblApiKey.Text = "API Key:";
//
// txtApiKey
//
this.txtApiKey.Location = new System.Drawing.Point(139, 52);
this.txtApiKey.Name = "txtApiKey";
this.txtApiKey.Size = new System.Drawing.Size(389, 28);
this.txtApiKey.TabIndex = 3;
this.txtApiKey.Text = "GHcn5qFbKpnScsrdyHWS8w0A";
this.txtApiKey.UseSystemPasswordChar = true;
//
// lblSecretKey
//
this.lblSecretKey.AutoSize = true;
this.lblSecretKey.Location = new System.Drawing.Point(12, 89);
this.lblSecretKey.Name = "lblSecretKey";
this.lblSecretKey.Size = new System.Drawing.Size(107, 18);
this.lblSecretKey.TabIndex = 4;
this.lblSecretKey.Text = "Secret Key:";
//
// txtSecretKey
//
this.txtSecretKey.Location = new System.Drawing.Point(139, 89);
this.txtSecretKey.Name = "txtSecretKey";
this.txtSecretKey.Size = new System.Drawing.Size(389, 28);
this.txtSecretKey.TabIndex = 5;
this.txtSecretKey.Text = "VC1UU6Eyq7o0pdK4IBgP8fsaZ22Cte1c";
this.txtSecretKey.UseSystemPasswordChar = true;
//
// lblGlmKey
//
this.lblGlmKey.AutoSize = true;
this.lblGlmKey.Location = new System.Drawing.Point(12, 55);
this.lblGlmKey.Name = "lblGlmKey";
this.lblGlmKey.Size = new System.Drawing.Size(125, 18);
this.lblGlmKey.TabIndex = 2;
this.lblGlmKey.Text = "智谱 API Key:";
//
// txtGlmKey
//
this.txtGlmKey.Location = new System.Drawing.Point(139, 52);
this.txtGlmKey.Name = "txtGlmKey";
this.txtGlmKey.Size = new System.Drawing.Size(389, 28);
this.txtGlmKey.TabIndex = 3;
this.txtGlmKey.UseSystemPasswordChar = true;
//
// lblQianfanKey
//
this.lblQianfanKey.AutoSize = true;
this.lblQianfanKey.Location = new System.Drawing.Point(12, 55);
this.lblQianfanKey.Name = "lblQianfanKey";
this.lblQianfanKey.Size = new System.Drawing.Size(125, 18);
this.lblQianfanKey.TabIndex = 2;
this.lblQianfanKey.Text = "千帆 API Key:";
//
// txtQianfanKey
//
this.txtQianfanKey.Location = new System.Drawing.Point(139, 52);
this.txtQianfanKey.Name = "txtQianfanKey";
this.txtQianfanKey.Size = new System.Drawing.Size(389, 28);
this.txtQianfanKey.TabIndex = 3;
this.txtQianfanKey.UseSystemPasswordChar = true;
//
// lblUapisKey
//
this.lblUapisKey.AutoSize = true;
this.lblUapisKey.Location = new System.Drawing.Point(12, 55);
this.lblUapisKey.Name = "lblUapisKey";
this.lblUapisKey.Size = new System.Drawing.Size(116, 18);
this.lblUapisKey.TabIndex = 2;
this.lblUapisKey.Text = "UApiPro Key:";
//
// txtUapisKey
//
this.txtUapisKey.Location = new System.Drawing.Point(139, 52);
this.txtUapisKey.Name = "txtUapisKey";
this.txtUapisKey.Size = new System.Drawing.Size(389, 28);
this.txtUapisKey.TabIndex = 3;
this.txtUapisKey.UseSystemPasswordChar = true;
//
// lblUmiUrl
//
this.lblUmiUrl.AutoSize = true;
this.lblUmiUrl.Location = new System.Drawing.Point(12, 55);
this.lblUmiUrl.Name = "lblUmiUrl";
this.lblUmiUrl.Size = new System.Drawing.Size(89, 18);
this.lblUmiUrl.TabIndex = 2;
this.lblUmiUrl.Text = "服务地址:";
//
// txtUmiUrl
//
this.txtUmiUrl.Location = new System.Drawing.Point(139, 52);
this.txtUmiUrl.Name = "txtUmiUrl";
this.txtUmiUrl.Size = new System.Drawing.Size(389, 28);
this.txtUmiUrl.TabIndex = 3;
this.txtUmiUrl.Text = "http://127.0.0.1:1224";
//
// btnUmi下载
//
this.btnUmi下载.Location = new System.Drawing.Point(139, 86);
this.btnUmi下载.Name = "btnUmi下载";
this.btnUmi下载.Size = new System.Drawing.Size(139, 28);
this.btnUmi下载.TabIndex = 4;
this.btnUmi下载.Text = "下载 OCR 本体";
this.btnUmi下载.UseVisualStyleBackColor = true;
this.btnUmi下载.Click += new System.EventHandler(this.btnUmi下载_Click);
//
// btnUmi检测
//
this.btnUmi检测.Location = new System.Drawing.Point(304, 86);
this.btnUmi检测.Name = "btnUmi检测";
this.btnUmi检测.Size = new System.Drawing.Size(138, 28);
this.btnUmi检测.TabIndex = 5;
this.btnUmi检测.Text = "检测 OCR 本体";
this.btnUmi检测.UseVisualStyleBackColor = true;
this.btnUmi检测.Click += new System.EventHandler(this.btnUmi检测_Click);
//
// lblUmi状态
//
this.lblUmi状态.ForeColor = System.Drawing.SystemColors.GrayText;
this.lblUmi状态.Location = new System.Drawing.Point(12, 118);
this.lblUmi状态.Name = "lblUmi状态";
this.lblUmi状态.Size = new System.Drawing.Size(506, 20);
this.lblUmi状态.TabIndex = 6;
//
// btn帮助
//
this.btn帮助.Location = new System.Drawing.Point(12, 200);
this.btn帮助.Name = "btn帮助";
this.btn帮助.Size = new System.Drawing.Size(90, 28);
this.btn帮助.TabIndex = 10;
this.btn帮助.Text = "帮助";
this.btn帮助.UseVisualStyleBackColor = true;
//
// lbl说明
//
this.lbl说明.ForeColor = System.Drawing.SystemColors.GrayText;
this.lbl说明.Location = new System.Drawing.Point(12, 142);
this.lbl说明.Name = "lbl说明";
this.lbl说明.Size = new System.Drawing.Size(506, 46);
this.lbl说明.TabIndex = 7;
this.lbl说明.Text = "百度引擎已内置默认 Key,可直接使用;换自己的 Key 更稳。\r\n智谱 GLM-OCR(手写字/版面强)需到 open.bigmodel.cn 获取 API K" +
"ey,约 0.2 元/百万 tokens。\r\nUmi-OCR 为本机离线引擎(免费无限次):本体将从网盘自动下载到程序目录,识别时自动启动、关窗自动关闭。";
//
// btn确定
//
this.btn确定.DialogResult = System.Windows.Forms.DialogResult.OK;
this.btn确定.Location = new System.Drawing.Point(367, 200);
this.btn确定.Name = "btn确定";
this.btn确定.Size = new System.Drawing.Size(75, 28);
this.btn确定.TabIndex = 8;
this.btn确定.Text = "保存";
this.btn确定.UseVisualStyleBackColor = true;
this.btn确定.Click += new System.EventHandler(this.btn确定_Click);
//
// btn取消
//
this.btn取消.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.btn取消.Location = new System.Drawing.Point(449, 200);
this.btn取消.Name = "btn取消";
this.btn取消.Size = new System.Drawing.Size(75, 28);
this.btn取消.TabIndex = 9;
this.btn取消.Text = "取消";
//
// OcrSettingsForm
//
this.AcceptButton = this.btn确定;
this.CancelButton = this.btn取消;
this.ClientSize = new System.Drawing.Size(539, 244);
this.Controls.Add(this.lblEngine);
this.Controls.Add(this.cboEngine);
this.Controls.Add(this.lblApiKey);
this.Controls.Add(this.txtApiKey);
this.Controls.Add(this.lblSecretKey);
this.Controls.Add(this.txtSecretKey);
this.Controls.Add(this.lblGlmKey);
this.Controls.Add(this.txtGlmKey);
this.Controls.Add(this.lblQianfanKey);
this.Controls.Add(this.txtQianfanKey);
this.Controls.Add(this.lblUapisKey);
this.Controls.Add(this.txtUapisKey);
this.Controls.Add(this.lblUmiUrl);
this.Controls.Add(this.txtUmiUrl);
this.Controls.Add(this.btnUmi下载);
this.Controls.Add(this.btnUmi检测);
this.Controls.Add(this.lblUmi状态);
this.Controls.Add(this.lbl说明);
this.Controls.Add(this.btn帮助);
this.Controls.Add(this.btn确定);
this.Controls.Add(this.btn取消);
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "OcrSettingsForm";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterParent;
this.Text = "OCR 设置";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label lblEngine;
private System.Windows.Forms.ComboBox cboEngine;
private System.Windows.Forms.Label lblApiKey;
private System.Windows.Forms.TextBox txtApiKey;
private System.Windows.Forms.Label lblSecretKey;
private System.Windows.Forms.TextBox txtSecretKey;
private System.Windows.Forms.Label lblGlmKey;
private System.Windows.Forms.TextBox txtGlmKey;
private System.Windows.Forms.Label lblQianfanKey;
private System.Windows.Forms.TextBox txtQianfanKey;
private System.Windows.Forms.Label lblUapisKey;
private System.Windows.Forms.TextBox txtUapisKey;
private System.Windows.Forms.Label lblUmiUrl;
private System.Windows.Forms.TextBox txtUmiUrl;
private System.Windows.Forms.Button btnUmi下载;
private System.Windows.Forms.Button btnUmi检测;
private System.Windows.Forms.Label lblUmi状态;
private System.Windows.Forms.Button btn帮助;
private System.Windows.Forms.Label lbl说明;
private System.Windows.Forms.Button btn确定;
private System.Windows.Forms.Button btn取消;
}
}
@@ -0,0 +1,232 @@
// OcrSettingsForm:OCR 引擎与 Key 设置(百度 OCR / 智谱 GLM-OCR / 千帆 / UApiPro / Umi-OCR 本地)
// 保存 → OcrPaths.ConfigPath 的 baidu_ocr.json(engine/api_key/secret_key/glm_api_key/qianfan_api_key/uapis_api_key/umi_url)
// + 热更新 OcrClient + 作废百度 token 缓存。
using System;
using System.Collections.Generic;
using System.IO;
using System.Windows.Forms;
using System.Web.Script.Serialization;
namespace 成套报价软件
{
public partial class OcrSettingsForm : Form
{
private readonly OcrClient _ocr;
public OcrSettingsForm(OcrClient ocr)
{
InitializeComponent();
this.BackColor = System.Drawing.Color.White;
_ocr = ocr;
// 回填现有配置(默认 Key 在 Designer 的 Text 里,密码遮罩显示;保存过则文件值优先)
string engine = "baidu";
try
{
string cfgPath = OcrPaths.ConfigPath;
if (File.Exists(cfgPath))
{
var jsv = new JavaScriptSerializer().Deserialize<Dictionary<string, string>>(File.ReadAllText(cfgPath));
if (jsv != null)
{
if (jsv.ContainsKey("engine")) engine = jsv["engine"] ?? "baidu";
txtApiKey.Text = jsv.ContainsKey("api_key") ? (jsv["api_key"] ?? "") : "";
txtSecretKey.Text = jsv.ContainsKey("secret_key") ? (jsv["secret_key"] ?? "") : "";
txtGlmKey.Text = jsv.ContainsKey("glm_api_key") ? (jsv["glm_api_key"] ?? "") : "";
txtQianfanKey.Text = jsv.ContainsKey("qianfan_api_key") ? (jsv["qianfan_api_key"] ?? "") : "";
txtUapisKey.Text = jsv.ContainsKey("uapis_api_key") ? (jsv["uapis_api_key"] ?? "") : "";
txtUmiUrl.Text = jsv.ContainsKey("umi_url") && !string.IsNullOrWhiteSpace(jsv["umi_url"])
? jsv["umi_url"] : "http://127.0.0.1:1224";
}
}
}
catch { }
cboEngine.SelectedIndex =
engine == "glm" ? 1 :
engine == "qianfan" ? 2 :
engine == "uapis" ? 3 :
engine == "umi" ? 4 : 0;
UpdateEnginePanels();
}
/// <summary>按引擎下拉显示对应的输入区(百度=两格;智谱/千帆/UApiPro=Key一格;Umi-OCR=服务地址一格)。</summary>
private void UpdateEnginePanels()
{
int sel = cboEngine.SelectedIndex;
bool glm = sel == 1;
bool qf = sel == 2;
bool ua = sel == 3;
bool umi = sel == 4;
bool baidu = !glm && !qf && !ua && !umi;
lblApiKey.Visible = baidu;
txtApiKey.Visible = baidu;
lblSecretKey.Visible = baidu;
txtSecretKey.Visible = baidu;
lblGlmKey.Visible = glm;
txtGlmKey.Visible = glm;
lblQianfanKey.Visible = qf;
txtQianfanKey.Visible = qf;
lblUapisKey.Visible = ua;
txtUapisKey.Visible = ua;
lblUmiUrl.Visible = umi;
txtUmiUrl.Visible = umi;
btnUmi下载.Visible = umi;
btnUmi检测.Visible = umi;
lblUmi状态.Visible = umi;
if (umi) RefreshUmiStatus();
}
/// <summary>刷新 Umi 本体三态状态显示。</summary>
private void RefreshUmiStatus()
{
try { lblUmi状态.Text = UmiOcrHost.DetectText(); } catch { }
}
/// <summary>检测 OCR 本体:刷新状态(未安装/已安装未运行/运行中)。</summary>
private void btnUmi检测_Click(object sender, EventArgs e)
{
RefreshUmiStatus();
MessageBox.Show(this, UmiOcrHost.DetectText(), "检测 OCR 本体",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
/// <summary>下载 OCR 本体:网盘直链(~179MB)→解压到程序目录 Umi-OCR\→验证→删 zip。</summary>
private void btnUmi下载_Click(object sender, EventArgs e)
{
if (UmiOcrHost.IsInstalled)
{
var r = MessageBox.Show(this, "OCR 本体已存在,重新下载将覆盖安装。继续吗?", "下载 OCR 本体",
MessageBoxButtons.OKCancel, MessageBoxIcon.Question);
if (r != DialogResult.OK) return;
}
btnUmi下载.Enabled = false;
btnUmi检测.Enabled = false;
lblUmi状态.ForeColor = System.Drawing.Color.DimGray;
lblUmi状态.Text = "正在下载… 0%";
int lastPct = -1;
UmiOcrHost.DownloadAndInstallAsync(pct =>
{
if (pct == lastPct) return;
lastPct = pct;
try { this.BeginInvoke((MethodInvoker)delegate
{
lblUmi状态.Text = pct >= 100 ? "下载完成,正在解压安装…" : "正在下载… " + pct + "%";
}); } catch { }
}).ContinueWith(t =>
{
try { this.BeginInvoke((MethodInvoker)delegate
{
btnUmi下载.Enabled = true;
btnUmi检测.Enabled = true;
lblUmi状态.ForeColor = System.Drawing.SystemColors.GrayText;
if (t.IsFaulted)
{
Exception root = t.Exception;
while (root is AggregateException && root.InnerException != null) root = root.InnerException;
lblUmi状态.Text = "下载/安装失败: " + root.Message;
MessageBox.Show(this, "下载/安装失败:\n" + root.Message, "下载 OCR 本体",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
else
{
RefreshUmiStatus();
MessageBox.Show(this, "OCR 本体安装完成!\n" + UmiOcrHost.DetectText() +
"\n\n选 Umi-OCR 引擎保存后即可使用(识别时自动启动)。",
"下载 OCR 本体", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}); } catch { }
});
}
private void cboEngine_SelectedIndexChanged(object sender, EventArgs e)
{
UpdateEnginePanels();
}
/// <summary>帮助:全部引擎的 Key 获取方式与本地 OCR 设置流程。</summary>
private void btn帮助_Click(object sender, EventArgs e)
{
string help =
"════════ OCR 识别引擎使用帮助 ════════\r\n" +
"\r\n" +
"【1. 百度 OCR 高精度(含位置)】 默认引擎,已内置 Key 可直接用\r\n" +
" · 获取自己的 Key(更稳,免费额度足够日常):\r\n" +
" ① 打开 console.baidu.com 登录百度账号\r\n" +
" ② 进入\"产品服务 / 文字识别\"→ 创建应用\r\n" +
" ③ 勾选\"文字识别\"全部接口,创建完成\r\n" +
" ④ 在应用详情里复制 API Key 和 Secret Key 填入本窗口\r\n" +
"\r\n" +
"【2. 智谱 GLM-OCR(版面解析/手写强)】\r\n" +
" · 到 open.bigmodel.cn 注册并实名 → API Keys 页面新建密钥\r\n" +
" · 把密钥填入\"智谱 API Key\"\r\n" +
" · 计费约 0.2 元/百万 tokens,适合手写体/复杂版面\r\n" +
"\r\n" +
"【3. 百度 PaddleOCR-VL(千帆版面解析)】\r\n" +
" · 到 console.bce.baidu.com → 千帆 ModelBuilder → 应用管理\r\n" +
" · 创建应用后复制 API Key 填入\"千帆 API Key\"\r\n" +
" · 需在千帆控制台开通 PaddleOCR-VL 模型服务\r\n" +
"\r\n" +
"【4. UApiPro 通用OCR(免费积分/带位置)】\r\n" +
" · 到 uapis.cn 注册账号 → 控制台 → API Keys → 新建\r\n" +
" · 把 Key 填入\"UApiPro Key\"\r\n" +
" · 注册送免费积分(约 4 积分/次识别),小量使用零成本\r\n" +
"\r\n" +
"【5. Umi-OCR 本地引擎(离线免费,无限次)】\r\n" +
" · 本地引擎无需任何 Key,识别全在本机完成,数据不出电脑\r\n" +
" · 设置流程:\r\n" +
" ① 选本引擎后点\"下载 OCR 本体\"(约 179MB,自动下载安装)\r\n" +
" ② 点\"检测 OCR 本体\"确认状态为\"运行中\"或\"已安装\"\r\n" +
" ③ 点\"保存\"完成,之后识别时自动启动、关窗自动关闭\r\n" +
" · 服务地址保持默认 http://127.0.0.1:1224 即可\r\n" +
" (只有你在 Umi-OCR 软件里改过 HTTP 端口才需要改这里)\r\n" +
"\r\n" +
"════════════════════════════\r\n" +
"提示: Key 全部以密码方式显示,点\"保存\"后立即生效;\r\n" +
"所有 Key 统一保存在程序目录 baidu_ocr.json。";
MessageBox.Show(this, help, "OCR 使用帮助",
MessageBoxButtons.OK, MessageBoxIcon.Information);
}
private void btn确定_Click(object sender, EventArgs e)
{
try
{
string cfgPath = OcrPaths.ConfigPath;
string engineValue =
cboEngine.SelectedIndex == 1 ? "glm" :
cboEngine.SelectedIndex == 2 ? "qianfan" :
cboEngine.SelectedIndex == 3 ? "uapis" :
cboEngine.SelectedIndex == 4 ? "umi" : "baidu";
var jsv = new JavaScriptSerializer();
File.WriteAllText(cfgPath, jsv.Serialize(new Dictionary<string, string>
{
{ "engine", engineValue },
{ "api_key", txtApiKey.Text.Trim() },
{ "secret_key", txtSecretKey.Text.Trim() },
{ "glm_api_key", txtGlmKey.Text.Trim() },
{ "qianfan_api_key", txtQianfanKey.Text.Trim() },
{ "uapis_api_key", txtUapisKey.Text.Trim() },
{ "umi_url", string.IsNullOrWhiteSpace(txtUmiUrl.Text.Trim()) ? "http://127.0.0.1:1224" : txtUmiUrl.Text.Trim() }
}));
// 热更新引擎/Key + 作废百度 token 缓存(下次识别重新换取)
if (_ocr != null)
{
_ocr.LoadConfig();
try { File.Delete(OcrPaths.TokenCachePath); } catch { }
}
// 选了 Umi-OCR 但还没装本体:提示引导下载(不阻断保存)
if (engineValue == "umi" && !UmiOcrHost.IsInstalled)
{
MessageBox.Show(this, "Umi-OCR 本体尚未安装。\n请点\"下载 OCR 本体\"完成安装后即可使用。",
"OCR 设置", MessageBoxButtons.OK, MessageBoxIcon.Information);
}
}
catch (Exception ex)
{
MessageBox.Show(this, "保存失败: " + ex.Message, "OCR 设置",
MessageBoxButtons.OK, MessageBoxIcon.Warning);
}
}
}
}
+29
View File
@@ -0,0 +1,29 @@
// ============================================================
// 扒图数据类型(与原 cadcj 插件 / 网页端 cadbridge.ts 保持一致)
// ============================================================
using System.Collections.Generic;
namespace MyApp
{
/// <summary>
/// 扒数据模式:图号/箱号/元件
/// </summary>
public enum PaMode
{
Tuhao, // 扒图号
Xianghao, // 扒箱号
Yuanjian // 扒元件
}
/// <summary>
/// 扒图结果:一条文本 + 它对应的彩色框的 Handle(句柄字符串)
/// 用于关联表格数据行与网页 CAD 框实体,实现点击行定位、删除行删框。
/// ★ TextHandle:源文本实体自身的 Handle,用于判断"是否同一个文本"。
/// </summary>
public class PaTextResult
{
public string Text;
public string FrameHandle;
public string TextHandle = "";
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+609
View File
@@ -0,0 +1,609 @@
namespace MyAppPdf
{
public partial class 新建项目 : System.Windows.Forms.Form
{
/// <summary>必需的设计器变量。</summary>
private System.ComponentModel.IContainer components = null;
/// <summary>清理正在使用的资源。</summary>
protected override void Dispose(bool disposing)
{
if (disposing && (components != null))
{
components.Dispose();
}
base.Dispose(disposing);
}
#region Windows 窗体设计器生成的代码
private void InitializeComponent()
{
this.lbl项目编号 = new System.Windows.Forms.Label();
this.txt项目编号 = new System.Windows.Forms.TextBox();
this.lbl项目日期 = new System.Windows.Forms.Label();
this.txt项目日期 = new System.Windows.Forms.TextBox();
this.lbl项目名称 = new System.Windows.Forms.Label();
this.txt项目名称 = new System.Windows.Forms.TextBox();
this.lbl客户名称 = new System.Windows.Forms.Label();
this.txt客户名称 = new System.Windows.Forms.TextBox();
this.btn选择客户 = new System.Windows.Forms.Button();
this.lbl客户地址 = new System.Windows.Forms.Label();
this.txt客户地址 = new System.Windows.Forms.TextBox();
this.lbl联系人 = new System.Windows.Forms.Label();
this.txt联系人 = new System.Windows.Forms.TextBox();
this.lbl联系电话 = new System.Windows.Forms.Label();
this.txt联系电话 = new System.Windows.Forms.TextBox();
this.lbl成套厂名称 = new System.Windows.Forms.Label();
this.txt成套厂名称 = new System.Windows.Forms.TextBox();
this.lbl成套厂地址 = new System.Windows.Forms.Label();
this.txt成套厂地址 = new System.Windows.Forms.TextBox();
this.lbl成套厂联系人 = new System.Windows.Forms.Label();
this.txt成套厂联系人 = new System.Windows.Forms.TextBox();
this.lbl成套厂联系电话 = new System.Windows.Forms.Label();
this.txt成套厂联系电话 = new System.Windows.Forms.TextBox();
this.lbl报出方式 = new System.Windows.Forms.Label();
this.cmb报出方式 = new System.Windows.Forms.ComboBox();
this.lbl箱柜模板 = new System.Windows.Forms.Label();
this.cmb箱柜模板 = new System.Windows.Forms.ComboBox();
this.lbl单台价格 = new System.Windows.Forms.Label();
this.cmb单台价格 = new System.Windows.Forms.ComboBox();
this.lbl项目总价 = new System.Windows.Forms.Label();
this.lbl备注 = new System.Windows.Forms.Label();
this.txt备注 = new System.Windows.Forms.TextBox();
this.btn确定 = new System.Windows.Forms.Button();
this.btn取消 = new System.Windows.Forms.Button();
this.comboBox1 = new System.Windows.Forms.ComboBox();
this.label1 = new System.Windows.Forms.Label();
this.label2 = new System.Windows.Forms.Label();
this.comboBox2 = new System.Windows.Forms.ComboBox();
this.cmb项目总价 = new System.Windows.Forms.ComboBox();
this.SuspendLayout();
//
// lbl项目编号
//
this.lbl项目编号.BackColor = System.Drawing.SystemColors.Control;
this.lbl项目编号.Font = new System.Drawing.Font("宋体", 9F);
this.lbl项目编号.ForeColor = System.Drawing.SystemColors.ControlText;
this.lbl项目编号.Location = new System.Drawing.Point(20, 25);
this.lbl项目编号.Name = "lbl项目编号";
this.lbl项目编号.Size = new System.Drawing.Size(70, 23);
this.lbl项目编号.TabIndex = 0;
this.lbl项目编号.Text = "项目编号:";
this.lbl项目编号.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// txt项目编号
//
this.txt项目编号.BackColor = System.Drawing.SystemColors.Window;
this.txt项目编号.Font = new System.Drawing.Font("宋体", 9F);
this.txt项目编号.ForeColor = System.Drawing.SystemColors.WindowText;
this.txt项目编号.Location = new System.Drawing.Point(95, 25);
this.txt项目编号.Name = "txt项目编号";
this.txt项目编号.Size = new System.Drawing.Size(220, 28);
this.txt项目编号.TabIndex = 0;
//
// lbl项目日期
//
this.lbl项目日期.BackColor = System.Drawing.SystemColors.Control;
this.lbl项目日期.Font = new System.Drawing.Font("宋体", 9F);
this.lbl项目日期.ForeColor = System.Drawing.SystemColors.ControlText;
this.lbl项目日期.Location = new System.Drawing.Point(380, 28);
this.lbl项目日期.Name = "lbl项目日期";
this.lbl项目日期.Size = new System.Drawing.Size(70, 18);
this.lbl项目日期.TabIndex = 1;
this.lbl项目日期.Text = "项目日期:";
//
// txt项目日期
//
this.txt项目日期.BackColor = System.Drawing.SystemColors.Window;
this.txt项目日期.Font = new System.Drawing.Font("宋体", 9F);
this.txt项目日期.ForeColor = System.Drawing.SystemColors.WindowText;
this.txt项目日期.Location = new System.Drawing.Point(455, 25);
this.txt项目日期.Name = "txt项目日期";
this.txt项目日期.Size = new System.Drawing.Size(210, 28);
this.txt项目日期.TabIndex = 1;
//
// lbl项目名称
//
this.lbl项目名称.BackColor = System.Drawing.SystemColors.Control;
this.lbl项目名称.Font = new System.Drawing.Font("宋体", 9F);
this.lbl项目名称.ForeColor = System.Drawing.SystemColors.ControlText;
this.lbl项目名称.Location = new System.Drawing.Point(20, 55);
this.lbl项目名称.Name = "lbl项目名称";
this.lbl项目名称.Size = new System.Drawing.Size(70, 23);
this.lbl项目名称.TabIndex = 2;
this.lbl项目名称.Text = "项目名称:";
this.lbl项目名称.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// txt项目名称
//
this.txt项目名称.BackColor = System.Drawing.SystemColors.Window;
this.txt项目名称.Font = new System.Drawing.Font("宋体", 9F);
this.txt项目名称.ForeColor = System.Drawing.SystemColors.WindowText;
this.txt项目名称.Location = new System.Drawing.Point(95, 55);
this.txt项目名称.Name = "txt项目名称";
this.txt项目名称.Size = new System.Drawing.Size(570, 28);
this.txt项目名称.TabIndex = 2;
//
// lbl客户名称
//
this.lbl客户名称.BackColor = System.Drawing.SystemColors.Control;
this.lbl客户名称.Font = new System.Drawing.Font("宋体", 9F);
this.lbl客户名称.ForeColor = System.Drawing.SystemColors.ControlText;
this.lbl客户名称.Location = new System.Drawing.Point(20, 85);
this.lbl客户名称.Name = "lbl客户名称";
this.lbl客户名称.Size = new System.Drawing.Size(70, 23);
this.lbl客户名称.TabIndex = 3;
this.lbl客户名称.Text = "客户名称:";
this.lbl客户名称.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// txt客户名称
//
this.txt客户名称.BackColor = System.Drawing.SystemColors.Window;
this.txt客户名称.Font = new System.Drawing.Font("宋体", 9F);
this.txt客户名称.ForeColor = System.Drawing.SystemColors.WindowText;
this.txt客户名称.Location = new System.Drawing.Point(95, 85);
this.txt客户名称.Name = "txt客户名称";
this.txt客户名称.Size = new System.Drawing.Size(479, 28);
this.txt客户名称.TabIndex = 3;
//
// btn选择客户
//
this.btn选择客户.BackColor = System.Drawing.SystemColors.Control;
this.btn选择客户.Font = new System.Drawing.Font("宋体", 9F);
this.btn选择客户.ForeColor = System.Drawing.SystemColors.ControlText;
this.btn选择客户.Location = new System.Drawing.Point(580, 85);
this.btn选择客户.Name = "btn选择客户";
this.btn选择客户.Size = new System.Drawing.Size(85, 28);
this.btn选择客户.TabIndex = 4;
this.btn选择客户.Text = "选择客户";
this.btn选择客户.UseVisualStyleBackColor = true;
//
// lbl客户地址
//
this.lbl客户地址.BackColor = System.Drawing.SystemColors.Control;
this.lbl客户地址.Font = new System.Drawing.Font("宋体", 9F);
this.lbl客户地址.ForeColor = System.Drawing.SystemColors.ControlText;
this.lbl客户地址.Location = new System.Drawing.Point(20, 115);
this.lbl客户地址.Name = "lbl客户地址";
this.lbl客户地址.Size = new System.Drawing.Size(70, 23);
this.lbl客户地址.TabIndex = 5;
this.lbl客户地址.Text = "客户地址:";
this.lbl客户地址.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// txt客户地址
//
this.txt客户地址.BackColor = System.Drawing.SystemColors.Window;
this.txt客户地址.Font = new System.Drawing.Font("宋体", 9F);
this.txt客户地址.ForeColor = System.Drawing.SystemColors.WindowText;
this.txt客户地址.Location = new System.Drawing.Point(95, 115);
this.txt客户地址.Name = "txt客户地址";
this.txt客户地址.Size = new System.Drawing.Size(570, 28);
this.txt客户地址.TabIndex = 5;
//
// lbl联系人
//
this.lbl联系人.BackColor = System.Drawing.SystemColors.Control;
this.lbl联系人.Font = new System.Drawing.Font("宋体", 9F);
this.lbl联系人.ForeColor = System.Drawing.SystemColors.ControlText;
this.lbl联系人.Location = new System.Drawing.Point(30, 145);
this.lbl联系人.Name = "lbl联系人";
this.lbl联系人.Size = new System.Drawing.Size(60, 23);
this.lbl联系人.TabIndex = 6;
this.lbl联系人.Text = "联系人:";
this.lbl联系人.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// txt联系人
//
this.txt联系人.BackColor = System.Drawing.SystemColors.Window;
this.txt联系人.Font = new System.Drawing.Font("宋体", 9F);
this.txt联系人.ForeColor = System.Drawing.SystemColors.WindowText;
this.txt联系人.Location = new System.Drawing.Point(95, 145);
this.txt联系人.Name = "txt联系人";
this.txt联系人.Size = new System.Drawing.Size(220, 28);
this.txt联系人.TabIndex = 6;
//
// lbl联系电话
//
this.lbl联系电话.BackColor = System.Drawing.SystemColors.Control;
this.lbl联系电话.Font = new System.Drawing.Font("宋体", 9F);
this.lbl联系电话.ForeColor = System.Drawing.SystemColors.ControlText;
this.lbl联系电话.Location = new System.Drawing.Point(380, 145);
this.lbl联系电话.Name = "lbl联系电话";
this.lbl联系电话.Size = new System.Drawing.Size(70, 21);
this.lbl联系电话.TabIndex = 7;
this.lbl联系电话.Text = "联系电话:";
//
// txt联系电话
//
this.txt联系电话.BackColor = System.Drawing.SystemColors.Window;
this.txt联系电话.Font = new System.Drawing.Font("宋体", 9F);
this.txt联系电话.ForeColor = System.Drawing.SystemColors.WindowText;
this.txt联系电话.Location = new System.Drawing.Point(455, 145);
this.txt联系电话.Name = "txt联系电话";
this.txt联系电话.Size = new System.Drawing.Size(210, 28);
this.txt联系电话.TabIndex = 7;
//
// lbl成套厂名称
//
this.lbl成套厂名称.BackColor = System.Drawing.SystemColors.Control;
this.lbl成套厂名称.Font = new System.Drawing.Font("宋体", 9F);
this.lbl成套厂名称.ForeColor = System.Drawing.SystemColors.ControlText;
this.lbl成套厂名称.Location = new System.Drawing.Point(10, 175);
this.lbl成套厂名称.Name = "lbl成套厂名称";
this.lbl成套厂名称.Size = new System.Drawing.Size(80, 23);
this.lbl成套厂名称.TabIndex = 8;
this.lbl成套厂名称.Text = "成套厂名称:";
this.lbl成套厂名称.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// txt成套厂名称
//
this.txt成套厂名称.BackColor = System.Drawing.SystemColors.Window;
this.txt成套厂名称.Font = new System.Drawing.Font("宋体", 9F);
this.txt成套厂名称.ForeColor = System.Drawing.SystemColors.WindowText;
this.txt成套厂名称.Location = new System.Drawing.Point(95, 175);
this.txt成套厂名称.Name = "txt成套厂名称";
this.txt成套厂名称.Size = new System.Drawing.Size(570, 28);
this.txt成套厂名称.TabIndex = 8;
//
// lbl成套厂地址
//
this.lbl成套厂地址.BackColor = System.Drawing.SystemColors.Control;
this.lbl成套厂地址.Font = new System.Drawing.Font("宋体", 9F);
this.lbl成套厂地址.ForeColor = System.Drawing.SystemColors.ControlText;
this.lbl成套厂地址.Location = new System.Drawing.Point(10, 205);
this.lbl成套厂地址.Name = "lbl成套厂地址";
this.lbl成套厂地址.Size = new System.Drawing.Size(80, 23);
this.lbl成套厂地址.TabIndex = 9;
this.lbl成套厂地址.Text = "成套厂地址:";
this.lbl成套厂地址.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// txt成套厂地址
//
this.txt成套厂地址.BackColor = System.Drawing.SystemColors.Window;
this.txt成套厂地址.Font = new System.Drawing.Font("宋体", 9F);
this.txt成套厂地址.ForeColor = System.Drawing.SystemColors.WindowText;
this.txt成套厂地址.Location = new System.Drawing.Point(95, 205);
this.txt成套厂地址.Name = "txt成套厂地址";
this.txt成套厂地址.Size = new System.Drawing.Size(570, 28);
this.txt成套厂地址.TabIndex = 9;
//
// lbl成套厂联系人
//
this.lbl成套厂联系人.BackColor = System.Drawing.SystemColors.Control;
this.lbl成套厂联系人.Font = new System.Drawing.Font("宋体", 9F);
this.lbl成套厂联系人.ForeColor = System.Drawing.SystemColors.ControlText;
this.lbl成套厂联系人.Location = new System.Drawing.Point(30, 235);
this.lbl成套厂联系人.Name = "lbl成套厂联系人";
this.lbl成套厂联系人.Size = new System.Drawing.Size(60, 23);
this.lbl成套厂联系人.TabIndex = 10;
this.lbl成套厂联系人.Text = "联系人:";
this.lbl成套厂联系人.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// txt成套厂联系人
//
this.txt成套厂联系人.BackColor = System.Drawing.SystemColors.Window;
this.txt成套厂联系人.Font = new System.Drawing.Font("宋体", 9F);
this.txt成套厂联系人.ForeColor = System.Drawing.SystemColors.WindowText;
this.txt成套厂联系人.Location = new System.Drawing.Point(95, 235);
this.txt成套厂联系人.Name = "txt成套厂联系人";
this.txt成套厂联系人.Size = new System.Drawing.Size(220, 28);
this.txt成套厂联系人.TabIndex = 10;
//
// lbl成套厂联系电话
//
this.lbl成套厂联系电话.BackColor = System.Drawing.SystemColors.Control;
this.lbl成套厂联系电话.Font = new System.Drawing.Font("宋体", 9F);
this.lbl成套厂联系电话.ForeColor = System.Drawing.SystemColors.ControlText;
this.lbl成套厂联系电话.Location = new System.Drawing.Point(380, 235);
this.lbl成套厂联系电话.Name = "lbl成套厂联系电话";
this.lbl成套厂联系电话.Size = new System.Drawing.Size(70, 21);
this.lbl成套厂联系电话.TabIndex = 11;
this.lbl成套厂联系电话.Text = "联系电话:";
//
// txt成套厂联系电话
//
this.txt成套厂联系电话.BackColor = System.Drawing.SystemColors.Window;
this.txt成套厂联系电话.Font = new System.Drawing.Font("宋体", 9F);
this.txt成套厂联系电话.ForeColor = System.Drawing.SystemColors.WindowText;
this.txt成套厂联系电话.Location = new System.Drawing.Point(455, 232);
this.txt成套厂联系电话.Name = "txt成套厂联系电话";
this.txt成套厂联系电话.Size = new System.Drawing.Size(210, 28);
this.txt成套厂联系电话.TabIndex = 11;
//
// lbl报出方式
//
this.lbl报出方式.BackColor = System.Drawing.SystemColors.Control;
this.lbl报出方式.Font = new System.Drawing.Font("宋体", 9F);
this.lbl报出方式.ForeColor = System.Drawing.SystemColors.ControlText;
this.lbl报出方式.Location = new System.Drawing.Point(20, 265);
this.lbl报出方式.Name = "lbl报出方式";
this.lbl报出方式.Size = new System.Drawing.Size(70, 23);
this.lbl报出方式.TabIndex = 12;
this.lbl报出方式.Text = "报出方式:";
this.lbl报出方式.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// cmb报出方式
//
this.cmb报出方式.BackColor = System.Drawing.SystemColors.Window;
this.cmb报出方式.DropDownWidth = 200;
this.cmb报出方式.Font = new System.Drawing.Font("宋体", 9F);
this.cmb报出方式.ForeColor = System.Drawing.SystemColors.WindowText;
this.cmb报出方式.ItemHeight = 18;
this.cmb报出方式.Location = new System.Drawing.Point(95, 265);
this.cmb报出方式.Name = "cmb报出方式";
this.cmb报出方式.Size = new System.Drawing.Size(220, 26);
this.cmb报出方式.TabIndex = 12;
this.cmb报出方式.Text = "报出价=表价*采购系数*报出系数";
//
// lbl箱柜模板
//
this.lbl箱柜模板.BackColor = System.Drawing.SystemColors.Control;
this.lbl箱柜模板.Font = new System.Drawing.Font("宋体", 9F);
this.lbl箱柜模板.ForeColor = System.Drawing.SystemColors.ControlText;
this.lbl箱柜模板.Location = new System.Drawing.Point(380, 265);
this.lbl箱柜模板.Name = "lbl箱柜模板";
this.lbl箱柜模板.Size = new System.Drawing.Size(70, 20);
this.lbl箱柜模板.TabIndex = 13;
this.lbl箱柜模板.Text = "箱柜模板:";
//
// cmb箱柜模板
//
this.cmb箱柜模板.BackColor = System.Drawing.SystemColors.Window;
this.cmb箱柜模板.DropDownWidth = 200;
this.cmb箱柜模板.Font = new System.Drawing.Font("宋体", 9F);
this.cmb箱柜模板.ForeColor = System.Drawing.SystemColors.WindowText;
this.cmb箱柜模板.ItemHeight = 18;
this.cmb箱柜模板.Location = new System.Drawing.Point(455, 262);
this.cmb箱柜模板.Name = "cmb箱柜模板";
this.cmb箱柜模板.Size = new System.Drawing.Size(210, 26);
this.cmb箱柜模板.TabIndex = 13;
//
// lbl单台价格
//
this.lbl单台价格.BackColor = System.Drawing.SystemColors.Control;
this.lbl单台价格.Font = new System.Drawing.Font("宋体", 9F);
this.lbl单台价格.ForeColor = System.Drawing.SystemColors.ControlText;
this.lbl单台价格.Location = new System.Drawing.Point(20, 293);
this.lbl单台价格.Name = "lbl单台价格";
this.lbl单台价格.Size = new System.Drawing.Size(70, 23);
this.lbl单台价格.TabIndex = 14;
this.lbl单台价格.Text = "单台价格:";
this.lbl单台价格.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// cmb单台价格
//
this.cmb单台价格.BackColor = System.Drawing.SystemColors.Window;
this.cmb单台价格.DropDownWidth = 200;
this.cmb单台价格.Font = new System.Drawing.Font("宋体", 9F);
this.cmb单台价格.ForeColor = System.Drawing.SystemColors.WindowText;
this.cmb单台价格.ItemHeight = 18;
this.cmb单台价格.Location = new System.Drawing.Point(95, 293);
this.cmb单台价格.Name = "cmb单台价格";
this.cmb单台价格.Size = new System.Drawing.Size(220, 26);
this.cmb单台价格.TabIndex = 14;
this.cmb单台价格.Text = "保留整数";
//
// lbl项目总价
//
this.lbl项目总价.BackColor = System.Drawing.SystemColors.Control;
this.lbl项目总价.Font = new System.Drawing.Font("宋体", 9F);
this.lbl项目总价.ForeColor = System.Drawing.SystemColors.ControlText;
this.lbl项目总价.Location = new System.Drawing.Point(379, 293);
this.lbl项目总价.Name = "lbl项目总价";
this.lbl项目总价.Size = new System.Drawing.Size(70, 20);
this.lbl项目总价.TabIndex = 15;
this.lbl项目总价.Text = "项目总价:";
//
// lbl备注
//
this.lbl备注.BackColor = System.Drawing.SystemColors.Control;
this.lbl备注.Font = new System.Drawing.Font("宋体", 9F);
this.lbl备注.ForeColor = System.Drawing.SystemColors.ControlText;
this.lbl备注.Location = new System.Drawing.Point(44, 352);
this.lbl备注.Name = "lbl备注";
this.lbl备注.Size = new System.Drawing.Size(46, 23);
this.lbl备注.TabIndex = 16;
this.lbl备注.Text = "备注:";
this.lbl备注.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// txt备注
//
this.txt备注.BackColor = System.Drawing.SystemColors.Window;
this.txt备注.Font = new System.Drawing.Font("宋体", 9F);
this.txt备注.ForeColor = System.Drawing.SystemColors.WindowText;
this.txt备注.Location = new System.Drawing.Point(95, 352);
this.txt备注.Multiline = true;
this.txt备注.Name = "txt备注";
this.txt备注.Size = new System.Drawing.Size(570, 80);
this.txt备注.TabIndex = 16;
//
// btn确定
//
this.btn确定.BackColor = System.Drawing.SystemColors.Control;
this.btn确定.DialogResult = System.Windows.Forms.DialogResult.OK;
this.btn确定.Font = new System.Drawing.Font("宋体", 9F);
this.btn确定.ForeColor = System.Drawing.SystemColors.ControlText;
this.btn确定.Location = new System.Drawing.Point(455, 449);
this.btn确定.Name = "btn确定";
this.btn确定.Size = new System.Drawing.Size(100, 28);
this.btn确定.TabIndex = 17;
this.btn确定.Text = "确定";
this.btn确定.UseVisualStyleBackColor = true;
//
// btn取消
//
this.btn取消.BackColor = System.Drawing.SystemColors.Control;
this.btn取消.DialogResult = System.Windows.Forms.DialogResult.Cancel;
this.btn取消.Font = new System.Drawing.Font("宋体", 9F);
this.btn取消.ForeColor = System.Drawing.SystemColors.ControlText;
this.btn取消.Location = new System.Drawing.Point(565, 449);
this.btn取消.Name = "btn取消";
this.btn取消.Size = new System.Drawing.Size(100, 28);
this.btn取消.TabIndex = 18;
this.btn取消.Text = "取消";
this.btn取消.UseVisualStyleBackColor = true;
//
// comboBox1
//
this.comboBox1.BackColor = System.Drawing.SystemColors.Window;
this.comboBox1.DropDownWidth = 210;
this.comboBox1.Font = new System.Drawing.Font("宋体", 9F);
this.comboBox1.ForeColor = System.Drawing.SystemColors.WindowText;
this.comboBox1.ItemHeight = 18;
this.comboBox1.Location = new System.Drawing.Point(455, 322);
this.comboBox1.Name = "comboBox1";
this.comboBox1.Size = new System.Drawing.Size(210, 26);
this.comboBox1.TabIndex = 14;
this.comboBox1.Text = "四舍五入";
//
// label1
//
this.label1.BackColor = System.Drawing.SystemColors.Control;
this.label1.Font = new System.Drawing.Font("宋体", 9F);
this.label1.ForeColor = System.Drawing.SystemColors.ControlText;
this.label1.Location = new System.Drawing.Point(19, 320);
this.label1.Name = "label1";
this.label1.Size = new System.Drawing.Size(70, 23);
this.label1.TabIndex = 14;
this.label1.Text = "单台取整:";
this.label1.TextAlign = System.Drawing.ContentAlignment.MiddleRight;
//
// label2
//
this.label2.BackColor = System.Drawing.SystemColors.Control;
this.label2.Font = new System.Drawing.Font("宋体", 9F);
this.label2.ForeColor = System.Drawing.SystemColors.ControlText;
this.label2.Location = new System.Drawing.Point(379, 324);
this.label2.Name = "label2";
this.label2.Size = new System.Drawing.Size(70, 20);
this.label2.TabIndex = 15;
this.label2.Text = "项目取整:";
//
// comboBox2
//
this.comboBox2.BackColor = System.Drawing.SystemColors.Window;
this.comboBox2.DropDownWidth = 220;
this.comboBox2.Font = new System.Drawing.Font("宋体", 9F);
this.comboBox2.ForeColor = System.Drawing.SystemColors.WindowText;
this.comboBox2.ItemHeight = 18;
this.comboBox2.Location = new System.Drawing.Point(95, 324);
this.comboBox2.Name = "comboBox2";
this.comboBox2.Size = new System.Drawing.Size(220, 26);
this.comboBox2.TabIndex = 14;
this.comboBox2.Text = "四舍五入";
//
// cmb项目总价
//
this.cmb项目总价.BackColor = System.Drawing.SystemColors.Window;
this.cmb项目总价.DropDownWidth = 200;
this.cmb项目总价.Font = new System.Drawing.Font("宋体", 9F);
this.cmb项目总价.ForeColor = System.Drawing.SystemColors.WindowText;
this.cmb项目总价.ItemHeight = 18;
this.cmb项目总价.Location = new System.Drawing.Point(455, 290);
this.cmb项目总价.Name = "cmb项目总价";
this.cmb项目总价.Size = new System.Drawing.Size(210, 26);
this.cmb项目总价.TabIndex = 14;
this.cmb项目总价.Text = "保留整数";
//
// 新建项目
//
this.AutoScaleMode = System.Windows.Forms.AutoScaleMode.None;
this.AutoValidate = System.Windows.Forms.AutoValidate.EnablePreventFocusChange;
this.BackColor = System.Drawing.Color.White;
this.ClientSize = new System.Drawing.Size(678, 482);
this.Controls.Add(this.lbl项目编号);
this.Controls.Add(this.txt项目编号);
this.Controls.Add(this.lbl项目日期);
this.Controls.Add(this.txt项目日期);
this.Controls.Add(this.lbl项目名称);
this.Controls.Add(this.txt项目名称);
this.Controls.Add(this.lbl客户名称);
this.Controls.Add(this.txt客户名称);
this.Controls.Add(this.btn选择客户);
this.Controls.Add(this.lbl客户地址);
this.Controls.Add(this.txt客户地址);
this.Controls.Add(this.lbl联系人);
this.Controls.Add(this.txt联系人);
this.Controls.Add(this.lbl联系电话);
this.Controls.Add(this.txt联系电话);
this.Controls.Add(this.lbl成套厂名称);
this.Controls.Add(this.txt成套厂名称);
this.Controls.Add(this.lbl成套厂地址);
this.Controls.Add(this.txt成套厂地址);
this.Controls.Add(this.lbl成套厂联系人);
this.Controls.Add(this.txt成套厂联系人);
this.Controls.Add(this.lbl成套厂联系电话);
this.Controls.Add(this.txt成套厂联系电话);
this.Controls.Add(this.lbl报出方式);
this.Controls.Add(this.cmb报出方式);
this.Controls.Add(this.lbl箱柜模板);
this.Controls.Add(this.cmb箱柜模板);
this.Controls.Add(this.lbl单台价格);
this.Controls.Add(this.cmb单台价格);
this.Controls.Add(this.lbl项目总价);
this.Controls.Add(this.lbl备注);
this.Controls.Add(this.txt备注);
this.Controls.Add(this.btn确定);
this.Controls.Add(this.btn取消);
this.Controls.Add(this.comboBox1);
this.Controls.Add(this.label1);
this.Controls.Add(this.label2);
this.Controls.Add(this.comboBox2);
this.Controls.Add(this.cmb项目总价);
this.Font = new System.Drawing.Font("宋体", 9F);
this.ForeColor = System.Drawing.SystemColors.ControlText;
this.FormBorderStyle = System.Windows.Forms.FormBorderStyle.FixedDialog;
this.MaximizeBox = false;
this.MinimizeBox = false;
this.Name = "新建项目";
this.StartPosition = System.Windows.Forms.FormStartPosition.CenterScreen;
this.Text = "新建项目";
this.ResumeLayout(false);
this.PerformLayout();
}
#endregion
private System.Windows.Forms.Label lbl项目编号;
private System.Windows.Forms.TextBox txt项目编号;
private System.Windows.Forms.Label lbl项目日期;
private System.Windows.Forms.TextBox txt项目日期;
private System.Windows.Forms.Label lbl项目名称;
private System.Windows.Forms.TextBox txt项目名称;
private System.Windows.Forms.Label lbl客户名称;
private System.Windows.Forms.TextBox txt客户名称;
private System.Windows.Forms.Button btn选择客户;
private System.Windows.Forms.Label lbl客户地址;
private System.Windows.Forms.TextBox txt客户地址;
private System.Windows.Forms.Label lbl联系人;
private System.Windows.Forms.TextBox txt联系人;
private System.Windows.Forms.Label lbl联系电话;
private System.Windows.Forms.TextBox txt联系电话;
private System.Windows.Forms.Label lbl成套厂名称;
private System.Windows.Forms.TextBox txt成套厂名称;
private System.Windows.Forms.Label lbl成套厂地址;
private System.Windows.Forms.TextBox txt成套厂地址;
private System.Windows.Forms.Label lbl成套厂联系人;
private System.Windows.Forms.TextBox txt成套厂联系人;
private System.Windows.Forms.Label lbl成套厂联系电话;
private System.Windows.Forms.TextBox txt成套厂联系电话;
private System.Windows.Forms.Label lbl报出方式;
private System.Windows.Forms.ComboBox cmb报出方式;
private System.Windows.Forms.Label lbl箱柜模板;
private System.Windows.Forms.ComboBox cmb箱柜模板;
private System.Windows.Forms.Label lbl单台价格;
private System.Windows.Forms.ComboBox cmb单台价格;
private System.Windows.Forms.Label lbl项目总价;
private System.Windows.Forms.Label lbl备注;
private System.Windows.Forms.TextBox txt备注;
private System.Windows.Forms.Button btn确定;
private System.Windows.Forms.Button btn取消;
private System.Windows.Forms.ComboBox comboBox1;
private System.Windows.Forms.Label label1;
private System.Windows.Forms.Label label2;
private System.Windows.Forms.ComboBox comboBox2;
private System.Windows.Forms.ComboBox cmb项目总价;
}
}
@@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.Windows.Forms;
namespace MyAppPdf
{
/// <summary>
/// 新建项目 窗口(简化版,不依赖企业信息)。
/// UI 布局见 新建项目.ui.cs,本文件只含交互逻辑。
/// </summary>
public partial class 新建项目 : Form
{
// 价格格式化选项(与主项目 价格格式化.cs 保持一致)
private static readonly string[] 小数位选项 =
{
"保留整数", "保留1位小数", "保留2位小数", "保留3位小数",
"保留4位小数", "保留5位小数", "保留6位小数"
};
private static readonly string[] 取整方式选项 =
{
"不取整", "四舍五入", "四舍五入到角", "四舍五入到元",
"四舍五入到十元", "向上取整到元", "最近5元", "最近10元"
};
private readonly string _dbPath;
private readonly string _tableName;
public 新建项目() : this(null, null) { }
/// <summary>带数据库路径的构造函数(用于自动生成项目编号)。</summary>
public 新建项目(string dbPath, string tableName)
{
InitializeComponent();
_dbPath = dbPath;
_tableName = tableName;
InitPriceFormatCombos();
this.Load += 新建项目_Load;
}
private void InitPriceFormatCombos()
{
cmb单台价格.Items.Clear();
cmb单台价格.Items.AddRange(小数位选项);
cmb项目总价.Items.Clear();
cmb项目总价.Items.AddRange(小数位选项);
comboBox1.Items.Clear();
comboBox1.Items.AddRange(取整方式选项);
comboBox2.Items.Clear();
comboBox2.Items.AddRange(取整方式选项);
}
private void 新建项目_Load(object sender, EventArgs e)
{
FillAutoFields();
}
/// <summary>自动填充项目日期(当天)和项目编号(yyyyMMdd+3位序号)。</summary>
private void FillAutoFields()
{
DateTime today = DateTime.Today;
txt项目日期.Text = today.ToString("yyyy-MM-dd");
string datePart = today.ToString("yyyyMMdd");
int seq = GetNextSeqForDate(datePart);
txt项目编号.Text = datePart + seq.ToString("000");
txt项目编号.ReadOnly = true;
txt项目编号.TabStop = false;
txt项目编号.BackColor = System.Drawing.SystemColors.Control;
}
/// <summary>查数据库当天最大序号+1,失败返回1。</summary>
private int GetNextSeqForDate(string datePart)
{
if (string.IsNullOrEmpty(datePart) ||
string.IsNullOrEmpty(_dbPath) ||
string.IsNullOrEmpty(_tableName) ||
!System.IO.File.Exists(_dbPath))
{
return 1;
}
try
{
string connStr = "Data Source=" + _dbPath + ";Version=3;";
using (var conn = new System.Data.SQLite.SQLiteConnection(connStr))
{
conn.Open();
string sql =
"SELECT MAX(CAST(SUBSTR(项目编号, 9) AS INTEGER)) " +
"FROM " + _tableName + " " +
"WHERE SUBSTR(项目编号, 1, 8) = '" + datePart + "';";
using (var cmd = new System.Data.SQLite.SQLiteCommand(sql, conn))
{
object result = cmd.ExecuteScalar();
if (result == null || result == DBNull.Value) return 1;
int maxSeq;
if (int.TryParse(result.ToString(), out maxSeq)) return maxSeq + 1;
return 1;
}
}
}
catch { return 1; }
}
/// <summary>收集对话框字段,以列名为键返回(供 Form3 写入数据库)。</summary>
public Dictionary<string, string> GetInputValues()
{
var values = new Dictionary<string, string>();
AddIfNotEmpty(values, "项目编号", txt项目编号.Text);
AddIfNotEmpty(values, "项目日期", txt项目日期.Text);
AddIfNotEmpty(values, "项目名称", txt项目名称.Text);
AddIfNotEmpty(values, "客户名称", txt客户名称.Text);
AddIfNotEmpty(values, "客户地址", txt客户地址.Text);
AddIfNotEmpty(values, "联系人", txt联系人.Text);
AddIfNotEmpty(values, "联系电话", txt联系电话.Text);
AddIfNotEmpty(values, "备注", txt备注.Text);
AddIfNotEmpty(values, "成套厂名称", txt成套厂名称.Text);
AddIfNotEmpty(values, "成套厂地址", txt成套厂地址.Text);
AddIfNotEmpty(values, "成套厂联系人", txt成套厂联系人.Text);
AddIfNotEmpty(values, "成套厂联系电话", txt成套厂联系电话.Text);
values["单台小数位"] = (cmb单台价格.SelectedItem ?? cmb单台价格.Text ?? "").ToString();
values["项目小数位"] = (cmb项目总价.SelectedItem ?? cmb项目总价.Text ?? "").ToString();
values["单台取整方式"] = (comboBox1.SelectedItem ?? comboBox1.Text ?? "").ToString();
values["项目取整方式"] = (comboBox2.SelectedItem ?? comboBox2.Text ?? "").ToString();
return values;
}
private static void AddIfNotEmpty(Dictionary<string, string> dict, string key, string value)
{
if (!string.IsNullOrWhiteSpace(value)) dict[key] = value.Trim();
}
}
}
+170
View File
@@ -0,0 +1,170 @@
// ============================================================
// PdfBridge:主窗口与 PDF 查看器之间的桥
// 与 CadBridge 保持相同的 API 形状:
// · 下行(主窗口→查看器):SendPaCommand / ZoomToFrameHandle /
// DeleteFramesByHandles / MergeMasks / ClearHighlight
// · 上行(查看器→主窗口):PaPicked / EntityErased / PaExited /
// F1/F2/F3Requested
// 扒图逻辑:用户在 PDF 上框选 → 裁剪 → OCR 工作进程识别 →
// 按 CAD 版语义组装 results(正向逐条/反向合并) → PaPicked
// → 主窗口入表格;查看器在框选位置画彩色蒙版框。
// ============================================================
using System;
using System.Collections.Generic;
namespace 成套报价软件
{
public static class PdfBridge
{
private static PdfViewerWindow _viewer;
/// <summary>PDF 查看器是否已挂载且打开了文档。</summary>
public static bool IsViewerReady
{
get { return _viewer != null && _viewer.IsDocumentOpen; }
}
/// <summary>当前打开的图纸名(去扩展名;查看器未打开文档为空)。扒图写入箱号「图纸」列用。</summary>
public static string CurrentDrawingName
{
get { return _viewer != null ? _viewer.CurrentDrawingName : ""; }
}
/// <summary>扒图连续模式当前是否激活(供 ESC 全局过滤判断)。</summary>
public static bool PickModeActive
{
get { return _viewer != null && _viewer.PickModeActive; }
}
/// <summary>兼容 CadBridge:查看器中蒙版被外部删除(当前无触发源,保留事件形状)。</summary>
public static event Action<string> EntityErased;
/// <summary>框选识别完成一批(图号/箱号/元件)。</summary>
public static event Action<MyApp.PaMode, List<MyApp.PaTextResult>> PaPicked;
/// <summary>用户按 ESC 退出连续扒图模式。</summary>
public static event Action PaExited;
/// <summary>用户按 F1 —— 元件两两合并。</summary>
public static event Action F1Requested;
/// <summary>用户按 F2 —— 元件三个一组合并。</summary>
public static event Action F2Requested;
/// <summary>用户按 F3 —— 元件四个一组合并。</summary>
public static event Action F3Requested;
/// <summary>把 PDF 查看器挂到桥上(查看器构造时调用一次)。</summary>
public static void Attach(PdfViewerWindow viewer)
{
_viewer = viewer;
}
// ------------------------------------------------------------
// 查看器 → 主窗口的上行事件触发(仅查看器调用)
// ------------------------------------------------------------
public static void RaisePaPicked(MyApp.PaMode mode, List<MyApp.PaTextResult> results)
{
if (PaPicked != null) PaPicked(mode, results);
}
public static void RaisePaExited()
{
if (PaExited != null) PaExited();
}
public static void RaiseEntityErased(string handle)
{
if (!string.IsNullOrWhiteSpace(handle) && EntityErased != null)
EntityErased(handle);
}
public static void RaiseF1() { if (F1Requested != null) F1Requested(); }
public static void RaiseF2() { if (F2Requested != null) F2Requested(); }
public static void RaiseF3() { if (F3Requested != null) F3Requested(); }
// ------------------------------------------------------------
// 主窗口 → 查看器的下行命令
// ------------------------------------------------------------
/// <summary>
/// 发送扒图命令。接受原 CAD 命令名(CADPATU/CADPAXH/CADPAYJ/CADPACL)
/// 或网页命令名(patu/paxh/payj/pacl)。
/// </summary>
public static void SendPaCommand(string cmd)
{
if (string.IsNullOrWhiteSpace(cmd)) return;
string c = cmd.Trim().ToUpperInvariant();
int mode = 0;
if (c == "CADPATU" || c == "PATU") mode = 0;
else if (c == "CADPAXH" || c == "PAXH") mode = 1;
else if (c == "CADPAYJ" || c == "PAYJ") mode = 2;
else if (c == "CADPACL" || c == "PACL")
{
ClearHighlight();
return;
}
else return;
if (_viewer == null) return;
_viewer.StartPick(mode);
}
/// <summary>定位到指定 Handle 的蒙版框(点击表格行定位)。</summary>
public static void ZoomToFrameHandle(string handle)
{
if (string.IsNullOrWhiteSpace(handle) || _viewer == null) return;
_viewer.ZoomToFrame(handle);
}
/// <summary>批量删除指定 Handle 的蒙版(删表格行联动删框)。</summary>
public static void DeleteFramesByHandles(IEnumerable<string> handles)
{
if (handles == null || _viewer == null) return;
var list = new List<string>();
foreach (string h in handles)
if (!string.IsNullOrWhiteSpace(h)) list.Add(h);
if (list.Count == 0) return;
_viewer.DeleteFrames(list);
}
/// <summary>
/// 合并蒙版:保留 keep 的蒙版并扩大到所有 removes 蒙版的并集,
/// 被合并行的蒙版移除(元件合并时蒙版同步合并)。
/// </summary>
public static void MergeMasks(string keep, IEnumerable<string> removes)
{
if (string.IsNullOrWhiteSpace(keep) || _viewer == null) return;
var list = new List<string>();
if (removes != null)
foreach (string h in removes)
if (!string.IsNullOrWhiteSpace(h) && h != keep) list.Add(h);
if (list.Count == 0) return;
_viewer.MergeMasks(keep, list);
}
/// <summary>清除所有高亮。</summary>
public static void ClearHighlight()
{
if (_viewer == null) return;
_viewer.ClearHighlight();
}
/// <summary>显示箱号组虚线框:把该箱号的箱号图框+全部元件图框按页求并集,
/// 每页画一个橙色虚线矩形(点元件表/箱号表行时调用)。
/// focusHandle=点中的框:翻到它所在页并缩放到装下整个组框(跳转适配)。</summary>
public static void SetGroupFrames(IEnumerable<string> handles, string focusHandle = null)
{
if (_viewer == null) return;
_viewer.SetGroupFrames(handles, focusHandle);
}
/// <summary>清除箱号组虚线框(点图号表行/新开 PDF 时)。</summary>
public static void ClearGroupFrames()
{
if (_viewer == null) return;
_viewer.ClearGroupFrames();
}
}
}
+160
View File
@@ -0,0 +1,160 @@
// ============================================================
// PdfDocument:PDF 文档封装(加载/页尺寸/按缩放渲染页位图)
// 坐标系:页面坐标 = PDF 点(1/72 英寸),原点左上(pdfium 渲染后
// 已翻转为屏幕方向,y 向下)。
// ============================================================
using System;
using System.Drawing;
using System.IO;
namespace 成套报价软件
{
/// <summary>已打开的 PDF 文档。非线程安全,只在 UI 线程使用。</summary>
internal sealed class PdfDocument : IDisposable
{
private IntPtr _doc = IntPtr.Zero;
private IntPtr[] _pageHandles; // 惰性加载的页句柄缓存
// ★ FPDF_LoadMemDocument 要求后备缓冲在文档关闭前保持有效且不可移动——
// 原来读入的 byte[] 是局部变量,打开后就没人引用;打开下一个 PDF 的大分配
// 触发 GC 把它搬走/回收,文档内存变垃圾 → 第二个 PDF 打不开(实测用户报障)。
// 钉住(GCHandleType.Pinned)+字段保引用,Close 时才释放。
private byte[] _bytes;
private System.Runtime.InteropServices.GCHandle _pin;
public string FilePath { get; private set; }
public int PageCount { get; private set; }
public float[] PageWidthPt { get; private set; }
public float[] PageHeightPt { get; private set; }
public bool IsOpen
{
get { return _doc != IntPtr.Zero; }
}
/// <summary>打开 PDF(读入内存后走 FPDF_LoadMemDocument,免疫路径问题)。失败抛异常。</summary>
public void Open(string path, string password)
{
Close();
if (!File.Exists(path))
throw new Exception("文件不存在: " + path);
_bytes = File.ReadAllBytes(path);
_pin = System.Runtime.InteropServices.GCHandle.Alloc(_bytes,
System.Runtime.InteropServices.GCHandleType.Pinned);
IntPtr doc = PdfiumNative.LoadDocumentFromBytes(_bytes, password);
if (doc == IntPtr.Zero)
{
FreeBuffer();
throw new Exception("PDF 打开失败:" + PdfiumNative.LastErrorText());
}
int count = PdfiumNative.GetPageCount(doc);
if (count <= 0)
{
PdfiumNative.CloseDocument(doc);
FreeBuffer();
throw new Exception("PDF 没有任何页面。");
}
_doc = doc;
FilePath = path;
PageCount = count;
_pageHandles = new IntPtr[count];
PageWidthPt = new float[count];
PageHeightPt = new float[count];
for (int i = 0; i < count; i++)
{
float w, h;
if (!PdfiumNative.GetPageSize(doc, i, out w, out h))
{
w = 595f; h = 842f; // A4 兜底
}
PageWidthPt[i] = w;
PageHeightPt[i] = h;
}
}
/// <summary>释放文档后备缓冲(钉住句柄+字节数组)。</summary>
private void FreeBuffer()
{
if (_pin.IsAllocated) _pin.Free();
_bytes = null;
}
public void Close()
{
if (_pageHandles != null)
{
for (int i = 0; i < _pageHandles.Length; i++)
PdfiumNative.ClosePage(_pageHandles[i]);
_pageHandles = null;
}
if (_doc != IntPtr.Zero)
{
PdfiumNative.CloseDocument(_doc);
_doc = IntPtr.Zero;
}
FreeBuffer();
FilePath = null;
PageCount = 0;
PageWidthPt = null;
PageHeightPt = null;
}
/// <summary>渲染指定页。scale = 像素/PDF点。失败返回 null。</summary>
public Bitmap RenderPage(int pageIndex, float scale)
{
if (!IsOpen) return null;
if (pageIndex < 0 || pageIndex >= PageCount) return null;
IntPtr page = GetPage(pageIndex);
if (page == IntPtr.Zero) return null;
int w = Math.Max(1, (int)Math.Round(PageWidthPt[pageIndex] * scale));
int h = Math.Max(1, (int)Math.Round(PageHeightPt[pageIndex] * scale));
return PdfiumNative.RenderPageToBitmap(page, w, h);
}
/// <summary>
/// 高清渲染页面局部区域(OCR 用):与屏幕显示缩放无关,
/// 用户任意缩放下框选,OCR 都拿到按 ocrScale 渲染的高清裁剪。
/// 返回位图与 rectPt 一一对应(像素/pt = ocrScale)。
/// </summary>
public Bitmap RenderRegion(int pageIndex, RectangleF rectPt, float ocrScale)
{
if (!IsOpen) return null;
if (pageIndex < 0 || pageIndex >= PageCount) return null;
IntPtr page = GetPage(pageIndex);
if (page == IntPtr.Zero) return null;
// 整页渲染尺寸(控制缩放)
int fullW = Math.Max(1, (int)Math.Round(PageWidthPt[pageIndex] * ocrScale));
int fullH = Math.Max(1, (int)Math.Round(PageHeightPt[pageIndex] * ocrScale));
// 区域矩形(裁到页面范围)
float pw = PageWidthPt[pageIndex], ph = PageHeightPt[pageIndex];
float l = Math.Max(0, rectPt.X), t = Math.Max(0, rectPt.Y);
float r = Math.Min(pw, rectPt.Right), b = Math.Min(ph, rectPt.Bottom);
if (r - l < 1 || b - t < 1) return null;
int startX = (int)Math.Floor(l * ocrScale);
int startY = (int)Math.Floor(t * ocrScale);
int endX = (int)Math.Ceiling(r * ocrScale);
int endY = (int)Math.Ceiling(b * ocrScale);
return PdfiumNative.RenderRegionToBitmap(page, startX, startY,
endX - startX, endY - startY, fullW, fullH);
}
private IntPtr GetPage(int pageIndex)
{
IntPtr page = _pageHandles[pageIndex];
if (page == IntPtr.Zero)
{
page = PdfiumNative.LoadPage(_doc, pageIndex);
_pageHandles[pageIndex] = page;
}
return page;
}
public void Dispose()
{
Close();
}
}
}
+27
View File
@@ -0,0 +1,27 @@
namespace 成套报价软件
{
partial class PdfQuoteWindow
{
private System.ComponentModel.IContainer components = null;
protected override void Dispose(bool disposing)
{
if (disposing && (components != null)) components.Dispose();
base.Dispose(disposing);
}
#region Designer generated code
private void InitializeComponent()
{
this.SuspendLayout();
//
// PdfQuoteWindow
//
this.Name = "PdfQuoteWindow";
this.ResumeLayout(false);
}
#endregion
}
}
@@ -0,0 +1,36 @@
// ============================================================
// 停靠窗口:报价插件工具栏(左侧)——pdfbt 独立版 QuoteWindow 原样搬入
// 把 Form3 报价界面(pdfbt 侧,namespace MyAppPdf)作为子控件嵌入。
// ============================================================
using System.Windows.Forms;
namespace 成套报价软件
{
/// <summary>停靠窗口:报价插件工具栏(DockState.DockLeft),宿主为 MyAppPdf.Form3。</summary>
public partial class PdfQuoteWindow : WeifenLuo.WinFormsUI.Docking.DockContent
{
private MyAppPdf.Form3 _form3;
/// <summary>内嵌的报价界面实例(主窗口功能区按钮跨窗口调用用)。</summary>
public MyAppPdf.Form3 报价表单
{
get { return _form3; }
}
public PdfQuoteWindow()
{
InitializeComponent();
this.BackColor = System.Drawing.Color.White;
Text = "报价工具";
TabText = "报价工具";
var form = new MyAppPdf.Form3();
_form3 = form;
form.TopLevel = false;
form.FormBorderStyle = FormBorderStyle.None;
form.Dock = DockStyle.Fill;
Controls.Add(form);
form.Show();
}
}
}
File diff suppressed because it is too large Load Diff
+228
View File
@@ -0,0 +1,228 @@
// ============================================================
// PdfiumNative:pdfium.dll 原生库 P/Invoke 封装
// dll 来源:pypdfium2 win32 wheel(pypdfium2_raw/pdfium.dll,x86)
// 调用约定:cdecl(pypdfium2 ctypes.CDLL 已验证)
// ============================================================
using System;
using System.Runtime.InteropServices;
namespace 成套报价软件
{
/// <summary>pdfium C API 最小封装(仅本项目用到的函数)。</summary>
internal static class PdfiumNative
{
public const string DllName = "pdfium.dll";
// 渲染标志:渲染注释/批注
public const int FPDF_ANNOT = 0x01;
// FPDF_ERR_* 错误码(GetLastError 返回)
public const int FPDF_ERR_SUCCESS = 0; // 无错误
public const int FPDF_ERR_UNKNOWN = 1; // 未知错误
public const int FPDF_ERR_FILE = 2; // 文件无法打开或读取
public const int FPDF_ERR_FORMAT = 3; // 文件不是 PDF 或已损坏
public const int FPDF_ERR_PASSWORD = 4; // 需要密码或密码错误
public const int FPDF_ERR_SECURITY = 5; // 不支持的安全方案
public const int FPDF_ERR_PAGE = 6; // 页面无法加载
private static bool _inited;
private static readonly object _initLock = new object();
/// <summary>初始化 pdfium(进程内只需一次,线程安全)。</summary>
public static void InitLibrary()
{
lock (_initLock)
{
if (_inited) return;
FPDF_InitLibrary();
_inited = true;
}
}
[DllImport(DllName)]
private static extern void FPDF_InitLibrary();
[DllImport(DllName)]
private static extern void FPDF_DestroyLibrary();
[DllImport(DllName, CharSet = CharSet.Ansi)]
private static extern IntPtr FPDF_LoadDocument(string file_path, string password);
[DllImport(DllName)]
private static extern void FPDF_CloseDocument(IntPtr document);
[DllImport(DllName)]
private static extern uint FPDF_GetLastError();
[DllImport(DllName)]
private static extern int FPDF_GetPageCount(IntPtr document);
[DllImport(DllName)]
private static extern IntPtr FPDF_LoadPage(IntPtr document, int page_index);
[DllImport(DllName)]
private static extern void FPDF_ClosePage(IntPtr page);
// ★ 新版 GetPageSizeByIndexF 是 FS_SIZEF* 参数(易错);用稳定的旧版 double API
[DllImport(DllName)]
private static extern int FPDF_GetPageSizeByIndex(IntPtr document, int page_index,
out double width, out double height);
[DllImport(DllName)]
private static extern IntPtr FPDFBitmap_Create(int width, int height, int alpha);
[DllImport(DllName)]
private static extern void FPDFBitmap_FillRect(IntPtr bitmap, int left, int top,
int width, int height, uint color);
[DllImport(DllName)]
private static extern void FPDFBitmap_Destroy(IntPtr bitmap);
[DllImport(DllName)]
private static extern IntPtr FPDFBitmap_GetBuffer(IntPtr bitmap);
[DllImport(DllName)]
private static extern int FPDFBitmap_GetStride(IntPtr bitmap);
[DllImport(DllName)]
private static extern void FPDF_RenderPageBitmap(IntPtr bitmap, IntPtr page,
int start_x, int start_y, int size_x, int size_y,
int rotate, int flags);
[DllImport(DllName)]
private static extern IntPtr FPDF_LoadMemDocument(byte[] data, int size, string password);
public static IntPtr LoadDocumentFromBytes(byte[] data, string password)
{
InitLibrary();
if (data == null || data.Length == 0) return IntPtr.Zero;
return FPDF_LoadMemDocument(data, data.Length, string.IsNullOrEmpty(password) ? null : password);
}
// ------------------------------------------------------------
// 上层直接使用的安全包装
// ------------------------------------------------------------
/// <summary>打开 PDF 文件。失败返回 IntPtr.Zero,可用 LastErrorText 查原因。</summary>
public static IntPtr LoadDocument(string path, string password)
{
InitLibrary();
if (string.IsNullOrEmpty(path)) return IntPtr.Zero;
return FPDF_LoadDocument(path, string.IsNullOrEmpty(password) ? null : password);
}
public static void CloseDocument(IntPtr doc)
{
if (doc != IntPtr.Zero) FPDF_CloseDocument(doc);
}
public static int GetPageCount(IntPtr doc)
{
if (doc == IntPtr.Zero) return 0;
return FPDF_GetPageCount(doc);
}
/// <summary>取指定页尺寸(PDF 点,1/72 英寸)。失败返回 false。</summary>
public static bool GetPageSize(IntPtr doc, int pageIndex, out float widthPt, out float heightPt)
{
widthPt = 0; heightPt = 0;
if (doc == IntPtr.Zero) return false;
double w, h;
bool ok = FPDF_GetPageSizeByIndex(doc, pageIndex, out w, out h) != 0;
widthPt = (float)w;
heightPt = (float)h;
return ok;
}
public static IntPtr LoadPage(IntPtr doc, int pageIndex)
{
if (doc == IntPtr.Zero) return IntPtr.Zero;
return FPDF_LoadPage(doc, pageIndex);
}
public static void ClosePage(IntPtr page)
{
if (page != IntPtr.Zero) FPDF_ClosePage(page);
}
/// <summary>把整页渲染到 GDI+ 位图(白底不透明)。</summary>
public static System.Drawing.Bitmap RenderPageToBitmap(IntPtr page, int widthPx, int heightPx)
{
return RenderRegionToBitmap(page, 0, 0, widthPx, heightPx, widthPx, heightPx);
}
/// <summary>
/// 把页面局部区域渲染到位图(白底不透明)。
/// 页面整图尺寸 = fullW×fullH 像素;只取 (startX,startY,wPx,hPx) 矩形。
/// 用于:OCR 框选区域按任意分辨率高清重渲染(与显示缩放无关)。
/// </summary>
public static System.Drawing.Bitmap RenderRegionToBitmap(IntPtr page,
int startX, int startY, int wPx, int hPx, int fullW, int fullH)
{
if (page == IntPtr.Zero || wPx <= 0 || hPx <= 0) return null;
if (wPx > 16384 || hPx > 16384) return null;
IntPtr bmp = FPDFBitmap_Create(wPx, hPx, 1);
if (bmp == IntPtr.Zero) return null;
try
{
// 白底不透明(0xAARRGGBB)
FPDFBitmap_FillRect(bmp, 0, 0, wPx, hPx, 0xFFFFFFFF);
// start_x/start_y 为负偏移:位图原点相对页面原点
FPDF_RenderPageBitmap(bmp, page, -startX, -startY, fullW, fullH, 0, FPDF_ANNOT);
IntPtr buffer = FPDFBitmap_GetBuffer(bmp);
int stride = FPDFBitmap_GetStride(bmp);
if (buffer == IntPtr.Zero || stride <= 0) return null;
// pdfium BGRA(top-down) → GDI+ Format32bppArgb(BGRA) 逐行拷贝
var result = new System.Drawing.Bitmap(wPx, hPx,
System.Drawing.Imaging.PixelFormat.Format32bppArgb);
var rect = new System.Drawing.Rectangle(0, 0, wPx, hPx);
var data = result.LockBits(rect,
System.Drawing.Imaging.ImageLockMode.WriteOnly,
System.Drawing.Imaging.PixelFormat.Format32bppArgb);
try
{
for (int y = 0; y < hPx; y++)
{
IntPtr src = new IntPtr(buffer.ToInt64() + (long)y * stride);
IntPtr dst = new IntPtr(data.Scan0.ToInt64() + (long)y * data.Stride);
NativeMethods.CopyMemory(dst, src, (uint)(wPx * 4));
}
}
finally
{
result.UnlockBits(data);
}
return result;
}
finally
{
FPDFBitmap_Destroy(bmp);
}
}
public static string LastErrorText()
{
uint err = FPDF_GetLastError();
switch (err)
{
case FPDF_ERR_SUCCESS: return "成功";
case FPDF_ERR_FILE: return "文件无法打开或读取";
case FPDF_ERR_FORMAT: return "文件不是有效 PDF 或已损坏";
case FPDF_ERR_PASSWORD: return "PDF 已加密,需要密码";
case FPDF_ERR_SECURITY: return "不支持的安全方案";
case FPDF_ERR_PAGE: return "页面内容无法解析";
default: return "未知错误(" + err + ")";
}
}
private static class NativeMethods
{
[DllImport("kernel32.dll", EntryPoint = "RtlMoveMemory", SetLastError = false)]
public static extern void CopyMemory(IntPtr dest, IntPtr src, uint count);
}
}
}
@@ -0,0 +1,121 @@
// ============================================================
// 文档区停靠窗口:PDF 扒图工作区
// = 独立 pdfbt.exe 主窗口的完整布局原样搬入:
// 左 报价工具(PdfQuoteWindow,内嵌 Form3 报价三表)
// 中 PDF 查看器(PdfViewerWindow,pdfium+OCR 框选扒图)
// 内部用 Smart DockSurface 承载;F1/F2/F3 全局消息过滤器随窗口启停
// (焦点在 Form3 树内时跳过,由 Form3.ProcessCmdKey 处理,防双跑);
// Umi-OCR 预热与独立版一致(已装本体且引擎为 umi 时打开即静默拉起)。
// ============================================================
using System;
using System.IO;
using System.Windows.Forms;
namespace 成套报价软件
{
/// <summary>PDF 扒图工作区(DockState.Document):报价三表 + PDF 查看器。</summary>
public class Pdf扒图窗口 : WeifenLuo.WinFormsUI.Docking.DockContent
{
private readonly F1MessageFilter _f1Filter = new F1MessageFilter();
private PdfQuoteWindow _quote;
private PdfViewerWindow _viewer;
/// <summary>内嵌的报价界面(主窗口功能区按钮跨窗口调用用;未创建为 null)。</summary>
public MyAppPdf.Form3 报价表单
{
get { return _quote != null ? _quote.报价表单 : null; }
}
public Pdf扒图窗口()
{
Text = "PDF扒图";
TabText = "PDF扒图";
this.BackColor = System.Drawing.Color.White;
// 与独立版 MainForm.CreateDockWindows_dockSurface1 相同的内部结构
var dockSurface = new Smart.CustomComponents.DockSurface();
dockSurface.Dock = DockStyle.Fill;
Controls.Add(dockSurface);
dockSurface.EnsureDefaultTheme(); // 必须:Show 前保证有效主题
_quote = new PdfQuoteWindow();
_quote.CloseButtonVisible = false;
_quote.TabText = "报价工具";
_quote.Text = "报价工具";
_quote.Show(dockSurface, WeifenLuo.WinFormsUI.Docking.DockState.DockLeft);
_viewer = new PdfViewerWindow();
_viewer.CloseButtonVisible = false;
_viewer.TabText = "PDF 查看器";
_viewer.Text = "PDF 查看器";
_viewer.Show(dockSurface, WeifenLuo.WinFormsUI.Docking.DockState.Document);
// ★ 全局 F1/F2/F3 过滤器:焦点在任何 WinForms 控件上都触发元件合并
Application.AddMessageFilter(_f1Filter);
FormClosed += (s, e) =>
{
Application.RemoveMessageFilter(_f1Filter);
// 显式关内部窗口:Form3 解绑桥事件、查看器停由本程序启动的 Umi-OCR
try { if (_quote != null && !_quote.IsDisposed) _quote.Close(); } catch { }
try { if (_viewer != null && !_viewer.IsDisposed) _viewer.Close(); } catch { }
};
// ★ Umi-OCR 预热(独立版 MainForm 同款):已装本体且当前引擎为 umi 时,
// 打开本工作区就静默拉起(--hide 托盘),不等首次框选;后台线程跑不拖慢窗口
System.Threading.ThreadPool.QueueUserWorkItem(_ =>
{
try
{
string cfg = OcrPaths.ConfigPath;
if (File.Exists(cfg))
{
var jsv = new System.Web.Script.Serialization.JavaScriptSerializer()
.Deserialize<System.Collections.Generic.Dictionary<string, string>>(
File.ReadAllText(cfg));
string eng = jsv != null && jsv.ContainsKey("engine") ? jsv["engine"] : "baidu";
if (eng == "umi" && UmiOcrHost.IsInstalled) UmiOcrLauncher.EnsureRunning();
}
}
catch { }
});
}
/// <summary>
/// 把 F1/F2/F3 按键(0x70/0x71/0x72)转成 PdfBridge 事件。
/// ★ 焦点在报价插件(Form3)树内时不触发——那条路径由 Form3.ProcessCmdKey
/// 处理,此处再触发会双跑一次(F1 合并两遍:第一遍成功、第二遍弹
/// "请先选中至少2行"的误提示)。
/// </summary>
private class F1MessageFilter : IMessageFilter
{
public bool PreFilterMessage(ref Message m)
{
const int WM_KEYDOWN = 0x0100;
bool isFKey = m.Msg == WM_KEYDOWN &&
((int)m.WParam == 0x70 || (int)m.WParam == 0x71 || (int)m.WParam == 0x72);
if (!isFKey) return false;
// 焦点控件在 Form3 树内 → 交给 Form3.ProcessCmdKey,跳过
try
{
var focused = Control.FromHandle(m.HWnd);
for (var c = focused; c != null; c = c.Parent)
{
if (c is MyAppPdf.Form3) return false;
}
}
catch { }
int key = (int)m.WParam;
try
{
if (key == 0x70) PdfBridge.RaiseF1();
else if (key == 0x71) PdfBridge.RaiseF2();
else if (key == 0x72) PdfBridge.RaiseF3();
}
catch { }
return false;
}
}
}
}
@@ -0,0 +1,278 @@
// ============================================================
// QianfanOcrClient:百度千帆 PaddleOCR-VL-0.9B(版面解析)
// · POST https://qianfan.baidubce.com/v2/ocr/paddleocr
// Authorization: Bearer {QianfanApiKey}(千帆 ModelBuilder 的 API Key,永久有效)
// {"model":"paddleocr-vl-0.9b","file":"<纯base64>","fileType":1}
// · 响应 result.layoutParsingResults[0].prunedResult.parsing_res_list[]
// 每块 {block_label, block_content, block_bbox[x1,y1,x2,y2](像素)}
// → 文本块转 OcrItem;表格块逐行拆(与 GLM 处理一致)
// · Key 存 baidu_ocr.json 的 qianfan_api_key(OCR设置窗口写)
// ============================================================
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web.Script.Serialization;
namespace 成套报价软件
{
/// <summary>百度千帆 PaddleOCR-VL 客户端(单实例)。</summary>
internal sealed class QianfanOcrClient
{
private readonly JavaScriptSerializer _json = new JavaScriptSerializer();
private string _apiKey;
private const string BaseUrl = "https://qianfan.baidubce.com/v2/ocr/paddleocr";
/// <summary>状态提示,接 OcrClient.StatusChanged。</summary>
public event Action<string> StatusChanged;
public QianfanOcrClient()
{
try
{
ServicePointManager.SecurityProtocol =
ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12;
}
catch { }
}
public bool IsConfigured
{
get { return !string.IsNullOrWhiteSpace(_apiKey); }
}
/// <summary>Key 由 OcrClient 从 baidu_ocr.json 的 qianfan_api_key 注入(热更新)。</summary>
public void SetApiKey(string key)
{
_apiKey = key == null ? "" : key.Trim();
}
/// <summary>识别一块位图 → 文本块列表(带 bbox 像素坐标)。</summary>
public Task<List<OcrItem>> RecognizeAsync(Bitmap crop, int timeoutMs)
{
if (crop == null || crop.Width <= 0 || crop.Height <= 0)
return Task.FromResult(new List<OcrItem>());
if (!IsConfigured)
{
Notify("千帆 PaddleOCR-VL 未配置 API Key(OCR 设置窗口里填千帆 API Key)");
var tcs0 = new TaskCompletionSource<List<OcrItem>>();
tcs0.SetException(new Exception(
"千帆 PaddleOCR-VL 未配置 API Key。\r\n" +
"请到百度智能云千帆平台(ModelBuilder)创建 API Key," +
"在 OCR 设置窗口选择引擎\"百度 PaddleOCR-VL\"并填入后保存。"));
return tcs0.Task;
}
return Task.Factory.StartNew(() =>
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var result = RequestAndParse(crop, timeoutMs);
Notify("PaddleOCR-VL·" + result.Count + "条 " + sw.ElapsedMilliseconds + "ms");
return result;
});
}
private List<OcrItem> RequestAndParse(Bitmap crop, int timeoutMs)
{
byte[] png;
using (var ms = new MemoryStream())
{
crop.Save(ms, ImageFormat.Png);
png = ms.ToArray();
}
// 千帆要求:base64 直传(无 data URI 前缀),fileType=1 图像
string body = _json.Serialize(new Dictionary<string, object>
{
{ "model", "paddleocr-vl-0.9b" },
{ "file", Convert.ToBase64String(png) },
{ "fileType", 1 }
});
var req = (HttpWebRequest)WebRequest.Create(BaseUrl);
req.Method = "POST";
req.ContentType = "application/json";
req.Headers.Add("Authorization", "Bearer " + _apiKey);
req.Timeout = timeoutMs;
req.ReadWriteTimeout = timeoutMs;
req.Proxy = null;
var bytes = Encoding.UTF8.GetBytes(body);
req.ContentLength = bytes.Length;
using (var rs = req.GetRequestStream())
{
rs.Write(bytes, 0, bytes.Length);
}
string respText;
try
{
using (var resp = (HttpWebResponse)req.GetResponse())
using (var sr = new StreamReader(resp.GetResponseStream(), new UTF8Encoding(false)))
{
respText = sr.ReadToEnd();
}
}
catch (WebException wex)
{
string detail = "";
try
{
if (wex.Response != null)
using (var sr = new StreamReader(wex.Response.GetResponseStream(), new UTF8Encoding(false)))
detail = sr.ReadToEnd();
}
catch { }
string hint = wex.Response == null
? ("网络不可达: " + wex.Message)
: ("HTTP " + (int)((HttpWebResponse)wex.Response).StatusCode + " " + detail);
throw new Exception("PaddleOCR-VL 请求失败: " + hint);
}
return ParseResponse(respText, crop.Width, crop.Height);
}
// ---- 响应解析(行拆分/清理逻辑与 GLM 一致,复用其 shared 方法) ----
private List<OcrItem> ParseResponse(string respText, int cropW, int cropH)
{
var msg = _json.Deserialize<Dictionary<string, object>>(respText);
if (msg == null) throw new Exception("千帆响应解析失败(空响应)");
if (msg.ContainsKey("error"))
{
var err = msg["error"] as Dictionary<string, object>;
string code = err != null && err.ContainsKey("code") ? Convert.ToString(err["code"]) : "?";
string message = err != null && err.ContainsKey("message") ? Convert.ToString(err["message"]) : respText;
throw new Exception("PaddleOCR-VL 错误 [" + code + "]: " + message);
}
var items = new List<OcrItem>();
// result.layoutParsingResults[0].prunedResult.parsing_res_list
var result = msg.ContainsKey("result") ? msg["result"] as Dictionary<string, object> : null;
var layouts = result != null && result.ContainsKey("layoutParsingResults")
? result["layoutParsingResults"] as System.Collections.ArrayList : null;
if (layouts == null || layouts.Count == 0)
throw new Exception("PaddleOCR-VL 响应没有 layoutParsingResults(内容: " +
(respText.Length > 300 ? respText.Substring(0, 300) : respText) + ")");
var first = layouts[0] as Dictionary<string, object>;
var pruned = first != null && first.ContainsKey("prunedResult")
? first["prunedResult"] as Dictionary<string, object> : null;
var blocks = pruned != null && pruned.ContainsKey("parsing_res_list")
? pruned["parsing_res_list"] as System.Collections.ArrayList : null;
if (blocks == null) return items;
foreach (Dictionary<string, object> b in blocks)
{
string label = b.ContainsKey("block_label") ? Convert.ToString(b["block_label"]) : "";
string content = b.ContainsKey("block_content") ? Convert.ToString(b["block_content"]) : "";
if (string.IsNullOrWhiteSpace(content)) continue;
var bbox = b.ContainsKey("block_bbox") ? b["block_bbox"] as System.Collections.ArrayList : null;
if (bbox == null || bbox.Count < 4) continue;
// block_bbox 像素坐标 [x1,y1,x2,y2];裁剪到图内
float x1 = Math.Max(0, Math.Min(cropW, Convert.ToSingle(bbox[0])));
float y1 = Math.Max(0, Math.Min(cropH, Convert.ToSingle(bbox[1])));
float x2 = Math.Max(0, Math.Min(cropW, Convert.ToSingle(bbox[2])));
float y2 = Math.Max(0, Math.Min(cropH, Convert.ToSingle(bbox[3])));
if (x2 - x1 < 2 || y2 - y1 < 2) continue;
if (label == "table")
{
AddTableItems(items, content, x1, y1, x2, y2);
continue;
}
if (label != "text" && label != "title" && label != "paragraph")
continue; // 图片/公式/图表块跳过(title 也是文字,收)
content = GlmOcrClient.CleanHtmlShared(content);
if (string.IsNullOrWhiteSpace(content)) continue;
string[] lines = content.Split(new[] { '\r', '\n' },
StringSplitOptions.RemoveEmptyEntries);
float lineH = (y2 - y1) / lines.Length;
float pad = Math.Min(lineH * 0.15f, 3f);
for (int i = 0; i < lines.Length; i++)
{
string text = GlmOcrClient.CleanMarkdownShared(lines[i]).Trim();
if (text.Length == 0) continue;
float ty1 = y1 + lineH * i + pad;
float ty2 = y1 + lineH * (i + 1) - pad;
if (ty2 - ty1 < 2) { ty1 = y1 + lineH * i; ty2 = ty1 + Math.Max(2, lineH); }
items.Add(new OcrItem
{
Text = text,
Box = new[]
{
new PointF(x1, ty1),
new PointF(x2, ty1),
new PointF(x2, ty2),
new PointF(x1, ty2)
},
Score = 1f
});
}
}
return items;
}
private static readonly System.Text.RegularExpressions.Regex _trRegex =
new System.Text.RegularExpressions.Regex("<tr[^>]*>(.*?)</tr>",
System.Text.RegularExpressions.RegexOptions.Singleline |
System.Text.RegularExpressions.RegexOptions.IgnoreCase |
System.Text.RegularExpressions.RegexOptions.Compiled);
private static readonly System.Text.RegularExpressions.Regex _tdRegex =
new System.Text.RegularExpressions.Regex("<t[dh][^>]*>(.*?)</t[dh]>",
System.Text.RegularExpressions.RegexOptions.Singleline |
System.Text.RegularExpressions.RegexOptions.IgnoreCase |
System.Text.RegularExpressions.RegexOptions.Compiled);
/// <summary>表格块 → 每 tr 一条 item(td 内容空格拼接),行高均分 bbox。</summary>
private static void AddTableItems(List<OcrItem> items, string tableHtml,
float x1, float y1, float x2, float y2)
{
var rows = new List<string>();
foreach (System.Text.RegularExpressions.Match tr in _trRegex.Matches(tableHtml))
{
var cells = new List<string>();
foreach (System.Text.RegularExpressions.Match td in _tdRegex.Matches(tr.Groups[1].Value))
{
string cell = GlmOcrClient.CleanHtmlShared(td.Groups[1].Value).Trim();
if (cell.Length > 0) cells.Add(cell);
}
if (cells.Count > 0) rows.Add(string.Join(" ", cells.ToArray()));
}
if (rows.Count == 0) return;
float rowH = (y2 - y1) / rows.Count;
float pad = Math.Min(rowH * 0.15f, 3f);
for (int i = 0; i < rows.Count; i++)
{
float ty1 = y1 + rowH * i + pad;
float ty2 = y1 + rowH * (i + 1) - pad;
if (ty2 - ty1 < 2) { ty1 = y1 + rowH * i; ty2 = ty1 + Math.Max(2, rowH); }
items.Add(new OcrItem
{
Text = rows[i],
Box = new[]
{
new PointF(x1, ty1),
new PointF(x2, ty1),
new PointF(x2, ty2),
new PointF(x1, ty2)
},
Score = 1f
});
}
}
private void Notify(string s)
{
var h = StatusChanged;
if (h != null) h(s);
}
}
}
+221
View File
@@ -0,0 +1,221 @@
// ============================================================
// UapisOcrClient:UApiPro(uapis.cn) 通用文字识别(带位置)
// · POST https://uapis.cn/api/v1/image/ocr (multipart/form-data)
// X-API-Key: {key}(uapis.cn 控制台注册生成,免费积分计费 4积分/次,4QPS)
// 字段:image_base64(纯base64或DataURI) + need_location="true"
// · 响应 words_result[] { words, location{left,top,width,height},
// vertexes_location[], score } → 转 OcrItem(坐标为发送图像素)
// · Key 存 baidu_ocr.json 的 uapis_api_key(OCR设置窗口写)
// ============================================================
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web.Script.Serialization;
namespace 成套报价软件
{
/// <summary>UApiPro 通用 OCR 客户端(单实例)。</summary>
internal sealed class UapisOcrClient
{
private readonly JavaScriptSerializer _json = new JavaScriptSerializer();
private string _apiKey;
private const string BaseUrl = "https://uapis.cn/api/v1/image/ocr";
/// <summary>状态提示,接 OcrClient.StatusChanged。</summary>
public event Action<string> StatusChanged;
public UapisOcrClient()
{
try
{
ServicePointManager.SecurityProtocol =
ServicePointManager.SecurityProtocol | SecurityProtocolType.Tls12;
}
catch { }
}
public bool IsConfigured
{
get { return !string.IsNullOrWhiteSpace(_apiKey); }
}
/// <summary>Key 由 OcrClient 从 baidu_ocr.json 的 uapis_api_key 注入(热更新)。</summary>
public void SetApiKey(string key)
{
_apiKey = key == null ? "" : key.Trim();
}
/// <summary>识别一块位图 → 文本行列表(带位置)。multipart 上传。</summary>
public Task<List<OcrItem>> RecognizeAsync(Bitmap crop, int timeoutMs)
{
if (crop == null || crop.Width <= 0 || crop.Height <= 0)
return Task.FromResult(new List<OcrItem>());
if (!IsConfigured)
{
Notify("UApiPro OCR 未配置 API Key(OCR 设置窗口里填 UApiPro Key)");
var tcs0 = new TaskCompletionSource<List<OcrItem>>();
tcs0.SetException(new Exception(
"UApiPro(uapis.cn) OCR 未配置 API Key。\r\n" +
"请到 uapis.cn/console 注册生成 API Key," +
"在 OCR 设置窗口选择引擎\"UApiPro 通用OCR\"并填入后保存。"));
return tcs0.Task;
}
return Task.Factory.StartNew(() =>
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var result = RequestAndParse(crop, timeoutMs);
Notify("UApiPro OCR·" + result.Count + "条 " + sw.ElapsedMilliseconds + "ms");
return result;
});
}
private List<OcrItem> RequestAndParse(Bitmap crop, int timeoutMs)
{
byte[] png;
using (var ms = new MemoryStream())
{
crop.Save(ms, ImageFormat.Png);
png = ms.ToArray();
}
string b64 = Convert.ToBase64String(png);
// multipart/form-data 手工拼装(net48 无 MultipartFormDataContent)
string boundary = "----pdfbt" + Guid.NewGuid().ToString("N");
var body = new MemoryStream();
Action<string, string> field = (name, value) =>
{
byte[] part = Encoding.UTF8.GetBytes(
"--" + boundary + "\r\nContent-Disposition: form-data; name=\"" +
name + "\"\r\n\r\n" + value + "\r\n");
body.Write(part, 0, part.Length);
};
field("image_base64", b64);
field("need_location", "true");
field("enable_cls", "true"); // 文字方向校正(歪扫图抗性)
byte[] tail = Encoding.UTF8.GetBytes("--" + boundary + "--\r\n");
body.Write(tail, 0, tail.Length);
var req = (HttpWebRequest)WebRequest.Create(BaseUrl);
req.Method = "POST";
req.ContentType = "multipart/form-data; boundary=" + boundary;
req.Headers.Add("X-API-Key", _apiKey);
req.Timeout = timeoutMs;
req.ReadWriteTimeout = timeoutMs;
req.Proxy = null;
byte[] payload = body.ToArray();
req.ContentLength = payload.Length;
using (var rs = req.GetRequestStream())
{
rs.Write(payload, 0, payload.Length);
}
string respText;
try
{
using (var resp = (HttpWebResponse)req.GetResponse())
using (var sr = new StreamReader(resp.GetResponseStream(), new UTF8Encoding(false)))
{
respText = sr.ReadToEnd();
}
}
catch (WebException wex)
{
string detail = "";
try
{
if (wex.Response != null)
using (var sr = new StreamReader(wex.Response.GetResponseStream(), new UTF8Encoding(false)))
detail = sr.ReadToEnd();
}
catch { }
string hint = wex.Response == null
? ("网络不可达: " + wex.Message)
: ("HTTP " + (int)((HttpWebResponse)wex.Response).StatusCode + " " + detail);
throw new Exception("UApiPro OCR 请求失败: " + hint);
}
return ParseResponse(respText, crop.Width, crop.Height);
}
private List<OcrItem> ParseResponse(string respText, int cropW, int cropH)
{
var msg = _json.Deserialize<Dictionary<string, object>>(respText);
if (msg == null) throw new Exception("UApiPro 响应解析失败(空响应)");
// 业务错误:HTTP 200 但 code 非 200/0;或 HTTP 错误体 {code,message}
if (msg.ContainsKey("code"))
{
string code = Convert.ToString(msg["code"]);
if (code != "200" && code != "0" && code != "success")
{
string m = msg.ContainsKey("message") ? Convert.ToString(msg["message"]) : respText;
throw new Exception("UApiPro OCR 错误 [" + code + "]: " + m);
}
}
var items = new List<OcrItem>();
var arr = msg.ContainsKey("words_result") ? msg["words_result"] as System.Collections.ArrayList : null;
if (arr == null)
throw new Exception("UApiPro 响应没有 words_result(内容: " +
(respText.Length > 300 ? respText.Substring(0, 300) : respText) + ")");
foreach (Dictionary<string, object> w in arr)
{
string text = w.ContainsKey("words") ? Convert.ToString(w["words"]) : "";
if (string.IsNullOrWhiteSpace(text)) continue;
var loc = w.ContainsKey("location") ? w["location"] as Dictionary<string, object> : null;
if (loc == null) continue;
float l = LocF(loc, "left"), t = LocF(loc, "top");
float wd = LocF(loc, "width"), h = LocF(loc, "height");
// 裁剪到图内
l = Math.Max(0, Math.Min(cropW, l));
t = Math.Max(0, Math.Min(cropH, t));
wd = Math.Max(2, Math.Min(cropW - l, wd));
h = Math.Max(2, Math.Min(cropH - t, h));
float score = 1f;
if (w.ContainsKey("score"))
{
float sv;
if (float.TryParse(Convert.ToString(w["score"]), out sv)) score = sv;
}
items.Add(new OcrItem
{
Text = text,
Box = new[]
{
new PointF(l, t),
new PointF(l + wd, t),
new PointF(l + wd, t + h),
new PointF(l, t + h)
},
Score = score
});
}
return items;
}
private static float LocF(Dictionary<string, object> d, string key)
{
if (d == null || !d.ContainsKey(key)) return 0f;
float v;
if (float.TryParse(Convert.ToString(d[key]), out v)) return v;
return 0f;
}
private void Notify(string s)
{
var h = StatusChanged;
if (h != null) h(s);
}
}
}
+143
View File
@@ -0,0 +1,143 @@
// ============================================================
// UmiOcrClient:Umi-OCR 本地 OCR 引擎(hiroi-sora/Umi-OCR,离线免费)
// · 前提:用户在本机运行 Umi-OCR 主程序,全局设置里开启 HTTP 服务(默认开启,端口 1224)
// · POST {BaseUrl}/api/ocr application/json:
// { "base64": "<纯base64无前缀>", "options": { "data.format": "dict" } }
// · 响应: code=100 成功;data=[{ text, score, box:[[x,y]×4 左上/右上/右下/左下], end }]
// code=101 无文本(data 为原因字符串);其余 code 失败
// · 地址存 baidu_ocr.json 的 umi_url(OCR设置窗口写,默认 http://127.0.0.1:1224)
// · 本地引擎无需 API Key,IsConfigured 恒 true
// ============================================================
using System;
using System.Collections.Generic;
using System.Drawing;
using System.Drawing.Imaging;
using System.IO;
using System.Net;
using System.Text;
using System.Threading.Tasks;
using System.Web.Script.Serialization;
namespace 成套报价软件
{
/// <summary>Umi-OCR 本地引擎客户端(单实例)。</summary>
internal sealed class UmiOcrClient
{
private readonly JavaScriptSerializer _json = new JavaScriptSerializer();
private string _baseUrl = "http://127.0.0.1:1224";
/// <summary>状态提示,接 OcrClient.StatusChanged。</summary>
public event Action<string> StatusChanged;
public bool IsConfigured
{
get { return true; } // 本地引擎无需 Key
}
/// <summary>服务地址由 OcrClient 从 baidu_ocr.json 的 umi_url 注入(热更新)。</summary>
public void SetBaseUrl(string url)
{
url = (url == null ? "" : url.Trim());
if (url.Length == 0) url = "http://127.0.0.1:1224";
if (!url.StartsWith("http")) url = "http://" + url;
_baseUrl = url.TrimEnd('/');
}
/// <summary>识别一块位图 → 文本行列表(带位置)。JSON POST 到本机 Umi-OCR。</summary>
public Task<List<OcrItem>> RecognizeAsync(Bitmap crop, int timeoutMs)
{
if (crop == null || crop.Width <= 0 || crop.Height <= 0)
return Task.FromResult(new List<OcrItem>());
return Task.Factory.StartNew(() =>
{
var sw = System.Diagnostics.Stopwatch.StartNew();
var result = RequestAndParse(crop, timeoutMs);
Notify("Umi-OCR·" + result.Count + "条 " + sw.ElapsedMilliseconds + "ms");
return result;
});
}
private List<OcrItem> RequestAndParse(Bitmap crop, int timeoutMs)
{
// PNG base64(无 data: 前缀);PNG 保坐标精度,本地服务无 4MB 限制
string b64;
using (var ms = new MemoryStream())
{
crop.Save(ms, ImageFormat.Png);
b64 = Convert.ToBase64String(ms.ToArray());
}
string req = _json.Serialize(new Dictionary<string, object>
{
{ "base64", b64 },
// ★ ocr.cls=true:纠正文本方向(PaddleOCR 引擎方向分类,即截图OCR界面的"纠正文本方向";
// 界面设置不会带给 HTTP 调用,必须显式传——否则旋转90°的横排文字识别必乱)
{ "options", new Dictionary<string, object> { { "data.format", "dict" }, { "ocr.cls", true } } }
});
string body;
using (var wc = new WebClient())
{
wc.Encoding = Encoding.UTF8;
wc.Headers["Content-Type"] = "application/json";
wc.Proxy = null; // 本地地址不走系统代理(配了代理会把 127.0.0.1 也代理走)
body = wc.UploadString(_baseUrl + "/api/ocr", "POST", req);
}
var resp = _json.Deserialize<Dictionary<string, object>>(body);
if (resp == null || !resp.ContainsKey("code"))
throw new Exception("Umi-OCR 返回解析失败");
int code;
int.TryParse(Convert.ToString(resp["code"]), out code);
if (code == 101)
return new List<OcrItem>(); // 无文本,不算错误
if (code != 100)
throw new Exception("Umi-OCR 错误 code=" + code + ": " +
Convert.ToString(resp.ContainsKey("data") ? resp["data"] : ""));
var items = new List<OcrItem>();
var arr = resp.ContainsKey("data") ? resp["data"] as System.Collections.ArrayList : null;
if (arr != null)
{
foreach (Dictionary<string, object> w in arr)
{
if (w == null) continue;
var boxArr = w.ContainsKey("box") ? w["box"] as System.Collections.ArrayList : null;
if (boxArr == null || boxArr.Count < 4) continue;
var pts = new PointF[4];
for (int i = 0; i < 4; i++)
{
var xy = boxArr[i] as System.Collections.ArrayList;
float x, y;
float.TryParse(Convert.ToString(xy != null && xy.Count > 0 ? xy[0] : "0"), out x);
float.TryParse(Convert.ToString(xy != null && xy.Count > 1 ? xy[1] : "0"), out y);
pts[i] = new PointF(x, y);
}
float score = 0.95f;
if (w.ContainsKey("score"))
{
float sv;
if (float.TryParse(Convert.ToString(w["score"]),
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture, out sv)) score = sv;
}
items.Add(new OcrItem
{
Text = Convert.ToString(w.ContainsKey("text") ? w["text"] : ""),
Box = pts, // box 即发送图像素坐标(顺时针四点),与 OcrItem 语义一致
Score = score
});
}
}
return items;
}
private void Notify(string msg)
{
var h = StatusChanged;
if (h != null) h(msg);
}
}
}
+166
View File
@@ -0,0 +1,166 @@
// ============================================================
// UmiOcrHost:Umi-OCR 本体(网盘 zip)下载/安装/检测 管理器
// · 本体 zip 直链(用户网盘,~179MB,zip 根直接是 Umi-OCR.exe 无顶层目录)
// · 安装 = 下载 zip 到临时文件 → 解压到安装目录 → 验证主程序 → 删 zip
// · 启动/停止在 UmiOcrLauncher(进程管理独立成文件)
// · 安装目录:主 exe 旁 Umi-OCR\;装过独立版 pdfbt 的机器回退 pdfbt\Umi-OCR\ 旧本体
// ============================================================
using System;
using System.IO;
using System.IO.Compression;
using System.Net;
using System.Threading.Tasks;
namespace 成套报价软件
{
internal static class UmiOcrHost
{
/// <summary>本体 zip 直链(用户网盘直连,GET 才有内容,HEAD 会 404,不支持断点续传)。</summary>
public const string DownloadUrl = "http://s333.odn.cc/s/845f64a239b665ec";
private const string InstallDirName = "Umi-OCR";
private const string ExeName = "Umi-OCR.exe"; // 官方 zip 根的主程序固定文件名
private const int HttpPort = 1224;
/// <summary>本体安装目录(主 exe 旁 Umi-OCR\;独立版 pdfbt 旧布局回退)。</summary>
public static string InstallDir
{
get
{
string baseDir = AppDomain.CurrentDomain.BaseDirectory;
string dir = Path.Combine(baseDir, InstallDirName);
if (Directory.Exists(dir)) return dir;
// 独立版 pdfbt 已下载过本体的机器:沿用旧安装
string legacy = Path.Combine(baseDir, "pdfbt", InstallDirName);
if (Directory.Exists(legacy)) return legacy;
return dir;
}
}
/// <summary>安装目录里主程序的固定路径(File.Exists 忽略大小写,覆盖官方 zip 命名)。</summary>
public static string ExePath
{
get { return Path.Combine(InstallDir, ExeName); }
}
/// <summary>本体是否已安装(主程序文件存在)。</summary>
public static bool IsInstalled
{
get { return File.Exists(ExePath); }
}
/// <summary>HTTP 服务(1224 端口)是否在监听。</summary>
public static bool IsHttpUp()
{
try
{
using (var c = new System.Net.Sockets.TcpClient())
{
var ar = c.BeginConnect("127.0.0.1", HttpPort, null, null);
if (ar.AsyncWaitHandle.WaitOne(800))
{
c.EndConnect(ar);
return true;
}
c.Close();
}
}
catch { }
return false;
}
/// <summary>三态:0=未安装,1=已安装未运行,2=运行中(HTTP 在线)。</summary>
public static int Detect()
{
if (!IsInstalled) return 0;
return IsHttpUp() ? 2 : 1;
}
/// <summary>状态文字(设置窗口显示用)。</summary>
public static string DetectText()
{
int st = Detect();
if (st == 0) return "未检测到 OCR 本体(点\"下载 OCR 本体\"自动下载安装)";
if (st == 1) return "已检测到 OCR 本体,服务未运行(识别时会自动启动)";
return "OCR 本体运行中(HTTP " + HttpPort + " 端口在线)";
}
/// <summary>下载 zip → 解压到安装目录 → 验证主程序 → 删 zip。
/// progress: 0~100 下载百分比;解压与收尾阶段回调 100。失败抛异常(内部已清理残留)。
/// ★ 必须用 DownloadFileAsync:同步版 DownloadFile 不触发 DownloadProgressChanged
/// (进度事件只在异步路径发,同步下载会一直停在 0%)。</summary>
public static Task DownloadAndInstallAsync(Action<int> progress)
{
return Task.Factory.StartNew(delegate
{
string tmpZip = Path.Combine(Path.GetTempPath(), "Umi-OCR_" + Guid.NewGuid().ToString("N") + ".zip");
try
{
if (progress != null) progress(0);
// 异步下载包成同步等待(TaskCompletionSource),进度事件才会触发
var dlTcs = new TaskCompletionSource<bool>();
using (var wc = new WebClient())
{
wc.Proxy = null;
wc.DownloadProgressChanged += (s, e) =>
{
if (progress != null) progress(e.ProgressPercentage);
};
wc.DownloadFileCompleted += (s, e) =>
{
if (e.Error != null) dlTcs.TrySetException(e.Error);
else if (e.Cancelled) dlTcs.TrySetException(new Exception("下载已取消"));
else dlTcs.TrySetResult(true);
};
wc.DownloadFileAsync(new Uri(DownloadUrl), tmpZip);
}
dlTcs.Task.GetAwaiter().GetResult(); // 等下载结束(异常原样抛出)
if (progress != null) progress(100);
// 校验是 zip(PK 头)而不是错误页/JSON
using (var fs = File.OpenRead(tmpZip))
{
int b0 = fs.ReadByte(), b1 = fs.ReadByte();
if (b0 != 'P' || b1 != 'K')
throw new Exception("下载的内容不是有效的压缩包(可能是网盘临时故障),请稍后重试");
}
// 解压(覆盖);zip 根直接是 Umi-OCR.exe,目标目录即安装目录
string dst = InstallDir;
if (!Directory.Exists(dst)) Directory.CreateDirectory(dst);
using (var archive = ZipFile.OpenRead(tmpZip))
{
foreach (var entry in archive.Entries)
{
string target = Path.GetFullPath(Path.Combine(dst, entry.FullName));
if (!target.StartsWith(Path.GetFullPath(dst), StringComparison.OrdinalIgnoreCase))
continue; // 防 zip 路径穿越
if (string.IsNullOrEmpty(entry.Name))
{
if (!Directory.Exists(target)) Directory.CreateDirectory(target);
continue;
}
string tdir = Path.GetDirectoryName(target);
if (!string.IsNullOrEmpty(tdir) && !Directory.Exists(tdir)) Directory.CreateDirectory(tdir);
if (File.Exists(target))
{
try { File.Delete(target); } catch { } // 被占用则跳过(旧文件同名多半一致)
}
entry.ExtractToFile(target, false);
}
}
if (!IsInstalled)
throw new Exception("解压完成但未找到 Umi-OCR 主程序,压缩包内容可能不完整");
try { File.Delete(tmpZip); } catch { } // 验证通过后删 zip
}
catch
{
try { if (File.Exists(tmpZip)) File.Delete(tmpZip); } catch { }
throw;
}
});
}
}
}
@@ -0,0 +1,86 @@
// ============================================================
// UmiOcrLauncher:Umi-OCR 本体进程启停(与 UmiOcrHost 拆分)
// · 启动 = 静默启动安装目录里的主程序(--hide 官方参数,主窗口进托盘,
// 后台提供 HTTP 服务 1224 端口) → 轮询端口就绪
// · 停止 = 只停由本程序启动的实例(官方 --quit 指令→超时 Kill),不碰用户自己开的
// · exe 路径 = 安装目录固定文件名(UmiOcrHost.ExePath),参数全部为官方常量,无用户输入
// ============================================================
using System;
using System.Diagnostics;
using System.IO;
namespace 成套报价软件
{
internal static class UmiOcrLauncher
{
/// <summary>Umi-OCR 官方启动参数:主窗口隐藏到托盘,后台提供 HTTP 服务。</summary>
private const string HideArg = "--hide";
/// <summary>Umi-OCR 官方退出参数:向已运行实例发优雅退出指令。</summary>
private const string QuitArg = "--quit";
/// <summary>由本程序启动的实例(停止时只关它,不碰用户自己开的)。</summary>
private static Process _startedProc;
/// <summary>确保本体已安装且 HTTP 服务在线:未安装抛错引导下载;未运行则静默启动
/// (--hide 主窗口进托盘)并等端口就绪(≤15秒)。返回 false=已安装但启动超时(调用方提示)。</summary>
public static bool EnsureRunning()
{
if (!UmiOcrHost.IsInstalled)
throw new Exception("尚未安装 Umi-OCR 本体。\r\n请到 OCR 设置窗口选择 Umi-OCR 引擎,点\"下载 OCR 本体\"。");
if (UmiOcrHost.IsHttpUp()) return true;
string exe = UmiOcrHost.ExePath; // 安装目录固定文件名(InstallDir\Umi-OCR.exe)
var psi = new ProcessStartInfo
{
FileName = exe,
Arguments = HideArg, // Umi-OCR 官方常量参数
WorkingDirectory = Path.GetDirectoryName(exe),
UseShellExecute = false,
CreateNoWindow = true,
WindowStyle = ProcessWindowStyle.Hidden
};
_startedProc = Process.Start(psi);
for (int i = 0; i < 30; i++) // 30 × 500ms = 最多 15 秒
{
if (UmiOcrHost.IsHttpUp()) return true;
System.Threading.Thread.Sleep(500);
}
return false;
}
/// <summary>停止由本程序启动的实例(PdfViewerWindow 关闭时调用;用户自己开的不动)。
/// --hide 启动的窗口在托盘,CloseMainWindow 不可靠:先发官方 --quit 指令优雅退出,超时再 Kill。</summary>
public static void StopIfStartedByUs()
{
var p = _startedProc;
_startedProc = null;
if (p == null) return;
try
{
if (!p.HasExited)
{
// 官方退出指令:向已运行实例发 --quit(依赖其 HTTP 通道)
try
{
if (UmiOcrHost.IsInstalled)
{
string exe = UmiOcrHost.ExePath; // 安装目录固定文件名
var quit = new ProcessStartInfo
{
FileName = exe,
Arguments = QuitArg, // Umi-OCR 官方常量参数
UseShellExecute = false,
CreateNoWindow = true
};
Process.Start(quit);
}
}
catch { }
if (!p.WaitForExit(3000)) p.Kill();
}
}
catch { }
finally { try { p.Dispose(); } catch { } }
}
}
}