From 3daac3648af8d69b299dbd5e1c7ba2c9c0a4d0dc Mon Sep 17 00:00:00 2001 From: sladro Date: Mon, 15 Dec 2025 11:54:32 +0800 Subject: [PATCH] =?UTF-8?q?=E7=AD=89=E5=BE=85=E6=89=BE=E7=94=A8=E6=88=B7?= =?UTF-8?q?=E7=A1=AE=E8=AE=A4=E5=9B=BE=E7=BA=B8=E5=8C=B9=E9=85=8D=E7=AD=89?= =?UTF-8?q?=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Cad/TemplateAnnotationOptionsExtractor.cs | 218 +++++++++ Cad/TemplateDrawingService.cs | 128 +++++ Cad/TemplateLayoutAnnotationExtractor.cs | 283 +++++++++++ Cad/TemplateModelSheetExtractor.cs | 564 ++++++++++++++++++++++ CadParamPluging.csproj | 14 + Common/DropdownOptions.cs | 77 +++ Common/DropdownOptionsStore.cs | 130 +++++ Domain/Models/TemplateInfo.cs | 2 + UI/LayoutSelectForm.cs | 118 +++++ UI/ModelSheetSelectForm.cs | 118 +++++ UI/ParamDrawingPanel.cs | 376 +++++++++++++-- UI/SettingsForm.cs | 234 ++++++++- UI/TextInputForm.cs | 84 ++++ 13 files changed, 2304 insertions(+), 42 deletions(-) create mode 100644 Cad/TemplateAnnotationOptionsExtractor.cs create mode 100644 Cad/TemplateLayoutAnnotationExtractor.cs create mode 100644 Cad/TemplateModelSheetExtractor.cs create mode 100644 Common/DropdownOptions.cs create mode 100644 Common/DropdownOptionsStore.cs create mode 100644 UI/LayoutSelectForm.cs create mode 100644 UI/ModelSheetSelectForm.cs create mode 100644 UI/TextInputForm.cs diff --git a/Cad/TemplateAnnotationOptionsExtractor.cs b/Cad/TemplateAnnotationOptionsExtractor.cs new file mode 100644 index 0000000..dffc831 --- /dev/null +++ b/Cad/TemplateAnnotationOptionsExtractor.cs @@ -0,0 +1,218 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Autodesk.AutoCAD.DatabaseServices; +using CadParamPluging.Common; + +namespace CadParamPluging.Cad +{ + public sealed class TemplateAnnotationExtractionResult + { + public DropdownOptions Options { get; set; } + public TemplateAnnotationExtractionSummary Summary { get; set; } + } + + public sealed class TemplateAnnotationExtractionSummary + { + public int LayoutCountScanned { get; set; } + public int EntityCountScanned { get; set; } + public int TextObjectCountScanned { get; set; } + public int MatchedLineCount { get; set; } + } + + public static class TemplateAnnotationOptionsExtractor + { + private static readonly Regex FieldRegex = new Regex( + @"^\s*(交付状态|工艺方法|结构特征)\s*[::]\s*(.+?)\s*$", + RegexOptions.Compiled); + + private static readonly Regex MTextFormatRegex = new Regex(@"\\[A-Za-z]+[^;]*;", RegexOptions.Compiled); + + public static TemplateAnnotationExtractionResult Extract(string templatePath) + { + if (string.IsNullOrWhiteSpace(templatePath)) + { + throw new ArgumentNullException(nameof(templatePath)); + } + + var options = new DropdownOptions(); + var summary = new TemplateAnnotationExtractionSummary(); + + using (var db = new Database(false, true)) + { + ReadTemplateDatabase(db, templatePath); + db.CloseInput(true); + + using (var tr = db.TransactionManager.StartTransaction()) + { + var layoutDict = (DBDictionary)tr.GetObject(db.LayoutDictionaryId, OpenMode.ForRead); + foreach (DBDictionaryEntry entry in layoutDict) + { + var layout = (Layout)tr.GetObject(entry.Value, OpenMode.ForRead); + summary.LayoutCountScanned++; + + var btr = (BlockTableRecord)tr.GetObject(layout.BlockTableRecordId, OpenMode.ForRead); + foreach (ObjectId id in btr) + { + summary.EntityCountScanned++; + + var obj = tr.GetObject(id, OpenMode.ForRead); + if (obj is Entity ent) + { + foreach (var line in ExtractLinesFromEntity(ent, tr, summary)) + { + TryParseLine(line, options, summary); + } + } + } + } + + tr.Commit(); + } + } + + options.Normalize(); + + return new TemplateAnnotationExtractionResult + { + Options = options, + Summary = summary + }; + } + + private static void ReadTemplateDatabase(Database db, string templatePath) + { + // AutoCAD .NET 不同版本 ReadDwgFile 的重载参数略有差异,这里优先使用 FileShare 以允许文件被其它进程占用。 + try + { + db.ReadDwgFile(templatePath, FileShare.ReadWrite, true, string.Empty); + } + catch (MissingMethodException) + { + db.ReadDwgFile(templatePath, FileOpenMode.OpenForReadAndAllShare, true, string.Empty); + } + } + + private static IEnumerable ExtractLinesFromEntity(Entity ent, Transaction tr, TemplateAnnotationExtractionSummary summary) + { + if (ent is DBText text) + { + summary.TextObjectCountScanned++; + return SplitLines(text.TextString); + } + + if (ent is MText mt) + { + summary.TextObjectCountScanned++; + var plain = SimplifyMText(mt.Contents); + return SplitLines(plain); + } + + if (ent is BlockReference br) + { + var lines = new List(); + foreach (ObjectId attId in br.AttributeCollection) + { + var att = tr.GetObject(attId, OpenMode.ForRead) as AttributeReference; + if (att == null) + { + continue; + } + + summary.TextObjectCountScanned++; + lines.AddRange(SplitLines(att.TextString)); + } + return lines; + } + + return Enumerable.Empty(); + } + + private static IEnumerable SplitLines(string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return Enumerable.Empty(); + } + + return (text ?? string.Empty) + .Replace("\r\n", "\n") + .Replace("\r", "\n") + .Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => (s ?? string.Empty).Trim()) + .Where(s => s.Length > 0); + } + + private static string SimplifyMText(string contents) + { + if (string.IsNullOrEmpty(contents)) + { + return string.Empty; + } + + var s = contents + .Replace("\\P", "\n") + .Replace("\\p", "\n") + .Replace("\\~", " "); + + s = s.Replace("{", string.Empty).Replace("}", string.Empty); + s = MTextFormatRegex.Replace(s, string.Empty); + return s; + } + + private static void TryParseLine(string line, DropdownOptions options, TemplateAnnotationExtractionSummary summary) + { + if (string.IsNullOrWhiteSpace(line)) + { + return; + } + + var m = FieldRegex.Match(line); + if (!m.Success) + { + return; + } + + var key = m.Groups[1].Value; + var rawVal = m.Groups[2].Value; + var values = SplitValues(rawVal); + + if (values.Count == 0) + { + return; + } + + summary.MatchedLineCount++; + + switch (key) + { + case "交付状态": + options.DeliveryStatuses.AddRange(values); + break; + case "工艺方法": + options.ProcessMethods.AddRange(values); + break; + case "结构特征": + options.StructuralFeatures.AddRange(values); + break; + } + } + + private static List SplitValues(string raw) + { + if (string.IsNullOrWhiteSpace(raw)) + { + return new List(); + } + + var parts = Regex.Split(raw, @"\s*[;;,,\|、/]+\s*") + .Select(s => (s ?? string.Empty).Trim()) + .Where(s => s.Length > 0) + .ToList(); + + return parts; + } + } +} diff --git a/Cad/TemplateDrawingService.cs b/Cad/TemplateDrawingService.cs index b6af407..c2f1592 100644 --- a/Cad/TemplateDrawingService.cs +++ b/Cad/TemplateDrawingService.cs @@ -1,4 +1,9 @@ +using System; +using System.Collections.Generic; +using System.Linq; using Autodesk.AutoCAD.ApplicationServices; +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.AutoCAD.Runtime; using CadParamPluging.Domain.Models; namespace CadParamPluging.Cad @@ -11,5 +16,128 @@ namespace CadParamPluging.Cad Application.DocumentManager.MdiActiveDocument = doc; return doc; } + + public static void KeepOnlyLayout(Document doc, string layoutName) + { + if (doc == null) + { + throw new ArgumentNullException(nameof(doc)); + } + + if (string.IsNullOrWhiteSpace(layoutName)) + { + return; + } + + using (doc.LockDocument()) + { + var db = doc.Database; + var prevDb = HostApplicationServices.WorkingDatabase; + HostApplicationServices.WorkingDatabase = db; + try + { + List layoutNames; + using (var tr = db.TransactionManager.StartTransaction()) + { + var layoutDict = (DBDictionary)tr.GetObject(db.LayoutDictionaryId, OpenMode.ForRead); + layoutNames = layoutDict.Cast().Select(e => e.Key).ToList(); + tr.Commit(); + } + + if (!layoutNames.Any(n => string.Equals(n, layoutName, StringComparison.OrdinalIgnoreCase))) + { + return; + } + + var lm = LayoutManager.Current; + lm.CurrentLayout = layoutName; + + foreach (var name in layoutNames) + { + if (string.Equals(name, "Model", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + if (string.Equals(name, layoutName, StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + try + { + lm.DeleteLayout(name); + } + catch + { + // ignore + } + } + } + finally + { + HostApplicationServices.WorkingDatabase = prevDb; + } + } + } + + public static void KeepOnlyModelWindow(Document doc, Extents3d window) + { + if (doc == null) + { + throw new ArgumentNullException(nameof(doc)); + } + + using (doc.LockDocument()) + { + var db = doc.Database; + using (var tr = db.TransactionManager.StartTransaction()) + { + var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead); + var ms = (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForWrite); + + var erased = 0; + var kept = 0; + + foreach (ObjectId id in ms) + { + var ent = tr.GetObject(id, OpenMode.ForWrite) as Entity; + if (ent == null) + { + continue; + } + + try + { + var ext = ent.GeometricExtents; + if (!Intersects(ext, window)) + { + ent.Erase(true); + erased++; + } + else + { + kept++; + } + } + catch + { + // 无法获取范围的对象默认保留,避免误删关键对象 + kept++; + } + } + + tr.Commit(); + } + } + } + + private static bool Intersects(Extents3d a, Extents3d b) + { + return a.MinPoint.X <= b.MaxPoint.X + && a.MaxPoint.X >= b.MinPoint.X + && a.MinPoint.Y <= b.MaxPoint.Y + && a.MaxPoint.Y >= b.MinPoint.Y; + } } } diff --git a/Cad/TemplateLayoutAnnotationExtractor.cs b/Cad/TemplateLayoutAnnotationExtractor.cs new file mode 100644 index 0000000..c58822b --- /dev/null +++ b/Cad/TemplateLayoutAnnotationExtractor.cs @@ -0,0 +1,283 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Autodesk.AutoCAD.DatabaseServices; + +namespace CadParamPluging.Cad +{ + public sealed class LayoutAnnotation + { + public string LayoutName { get; set; } + public List DeliveryStatuses { get; set; } + public List ProcessMethods { get; set; } + public List StructuralFeatures { get; set; } + + public LayoutAnnotation() + { + DeliveryStatuses = new List(); + ProcessMethods = new List(); + StructuralFeatures = new List(); + } + } + + public static class TemplateLayoutAnnotationExtractor + { + private static readonly Regex FieldRegex = new Regex( + @"^\s*(交付状态|工艺方法|结构特征)\s*[::]\s*(.+?)\s*$", + RegexOptions.Compiled); + + private static readonly Regex MTextFormatRegex = new Regex(@"\\[A-Za-z]+[^;]*;", RegexOptions.Compiled); + + public static List ExtractPerLayout(string templatePath) + { + if (string.IsNullOrWhiteSpace(templatePath)) + { + throw new ArgumentNullException(nameof(templatePath)); + } + + var result = new List(); + + using (var db = new Database(false, true)) + { + ReadTemplateDatabase(db, templatePath); + db.CloseInput(true); + + using (var tr = db.TransactionManager.StartTransaction()) + { + var layoutDict = (DBDictionary)tr.GetObject(db.LayoutDictionaryId, OpenMode.ForRead); + foreach (DBDictionaryEntry entry in layoutDict) + { + var layout = (Layout)tr.GetObject(entry.Value, OpenMode.ForRead); + var layoutName = layout.LayoutName; + + // 通常图纸在 PaperSpace,Model 不作为候选。 + if (string.Equals(layoutName, "Model", StringComparison.OrdinalIgnoreCase)) + { + continue; + } + + var ann = new LayoutAnnotation { LayoutName = layoutName }; + + var btr = (BlockTableRecord)tr.GetObject(layout.BlockTableRecordId, OpenMode.ForRead); + var visitedBlocks = new HashSet(); + foreach (ObjectId id in btr) + { + var obj = tr.GetObject(id, OpenMode.ForRead); + if (obj is Entity ent) + { + ExtractInto(ent, tr, visitedBlocks, ann); + } + } + + Normalize(ann); + if (HasAnyValue(ann)) + { + result.Add(ann); + } + } + + tr.Commit(); + } + } + + return result; + } + + private static void ReadTemplateDatabase(Database db, string templatePath) + { + try + { + db.ReadDwgFile(templatePath, FileShare.ReadWrite, true, string.Empty); + } + catch (MissingMethodException) + { + db.ReadDwgFile(templatePath, FileOpenMode.OpenForReadAndAllShare, true, string.Empty); + } + } + + private static void ExtractInto(Entity ent, Transaction tr, HashSet visitedBlocks, LayoutAnnotation ann) + { + if (ent is DBText text) + { + foreach (var line in SplitLines(text.TextString)) + { + TryParseLine(line, ann); + } + return; + } + + if (ent is MText mt) + { + var plain = SimplifyMText(mt.Contents); + foreach (var line in SplitLines(plain)) + { + TryParseLine(line, ann); + } + return; + } + + if (ent is BlockReference br) + { + foreach (ObjectId attId in br.AttributeCollection) + { + var att = tr.GetObject(attId, OpenMode.ForRead) as AttributeReference; + if (att == null) + { + continue; + } + + foreach (var line in SplitLines(att.TextString)) + { + TryParseLine(line, ann); + } + } + + // 说明文字可能在块定义内部(非属性),需要递归扫描块内容。 + var blockId = br.BlockTableRecord; + if (blockId.IsNull || visitedBlocks.Contains(blockId)) + { + return; + } + + visitedBlocks.Add(blockId); + var blockBtr = tr.GetObject(blockId, OpenMode.ForRead) as BlockTableRecord; + if (blockBtr == null) + { + return; + } + + foreach (ObjectId childId in blockBtr) + { + var child = tr.GetObject(childId, OpenMode.ForRead) as Entity; + if (child != null) + { + ExtractInto(child, tr, visitedBlocks, ann); + } + } + } + } + + private static IEnumerable SplitLines(string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return Enumerable.Empty(); + } + + return (text ?? string.Empty) + .Replace("\r\n", "\n") + .Replace("\r", "\n") + .Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => (s ?? string.Empty).Trim()) + .Where(s => s.Length > 0); + } + + private static string SimplifyMText(string contents) + { + if (string.IsNullOrEmpty(contents)) + { + return string.Empty; + } + + var s = contents + .Replace("\\P", "\n") + .Replace("\\p", "\n") + .Replace("\\~", " "); + + s = s.Replace("{", string.Empty).Replace("}", string.Empty); + s = MTextFormatRegex.Replace(s, string.Empty); + return s; + } + + private static void TryParseLine(string line, LayoutAnnotation ann) + { + if (string.IsNullOrWhiteSpace(line)) + { + return; + } + + var m = FieldRegex.Match(line); + if (!m.Success) + { + return; + } + + var key = m.Groups[1].Value; + var val = (m.Groups[2].Value ?? string.Empty).Trim(); + if (val.Length == 0) + { + return; + } + + val = NormalizeValue(val); + + switch (key) + { + case "交付状态": + ann.DeliveryStatuses.Add(val); + break; + case "工艺方法": + ann.ProcessMethods.Add(val); + break; + case "结构特征": + ann.StructuralFeatures.Add(val); + break; + } + } + + private static void Normalize(LayoutAnnotation ann) + { + ann.DeliveryStatuses = NormalizeList(ann.DeliveryStatuses); + ann.ProcessMethods = NormalizeList(ann.ProcessMethods); + ann.StructuralFeatures = NormalizeList(ann.StructuralFeatures); + } + + private static string NormalizeValue(string s) + { + if (string.IsNullOrWhiteSpace(s)) + { + return string.Empty; + } + + var trimmed = s.Trim(); + var chars = trimmed.Where(c => !char.IsWhiteSpace(c)).ToArray(); + return new string(chars); + } + + private static List NormalizeList(IEnumerable values) + { + var result = new List(); + if (values == null) + { + return result; + } + + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var raw in values) + { + var v = (raw ?? string.Empty).Trim(); + if (v.Length == 0) + { + continue; + } + + if (seen.Add(v)) + { + result.Add(v); + } + } + + return result; + } + + private static bool HasAnyValue(LayoutAnnotation ann) + { + return ann != null + && ((ann.DeliveryStatuses != null && ann.DeliveryStatuses.Count > 0) + || (ann.ProcessMethods != null && ann.ProcessMethods.Count > 0) + || (ann.StructuralFeatures != null && ann.StructuralFeatures.Count > 0)); + } + } +} diff --git a/Cad/TemplateModelSheetExtractor.cs b/Cad/TemplateModelSheetExtractor.cs new file mode 100644 index 0000000..5093a06 --- /dev/null +++ b/Cad/TemplateModelSheetExtractor.cs @@ -0,0 +1,564 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using Autodesk.AutoCAD.DatabaseServices; +using Autodesk.AutoCAD.Geometry; + +namespace CadParamPluging.Cad +{ + public sealed class ModelSheetCandidate + { + public string Name { get; set; } + public List DeliveryStatuses { get; set; } + public List ProcessMethods { get; set; } + public List StructuralFeatures { get; set; } + public Extents3d Window { get; set; } + + public ModelSheetCandidate() + { + DeliveryStatuses = new List(); + ProcessMethods = new List(); + StructuralFeatures = new List(); + } + } + + public static class TemplateModelSheetExtractor + { + private static readonly Regex FieldRegex = new Regex( + @"^\s*(交付状态|工艺方法|结构特征)\s*[::]\s*(.+?)\s*$", + RegexOptions.Compiled); + + private static readonly Regex MTextFormatRegex = new Regex(@"\\[A-Za-z]+[^;]*;", RegexOptions.Compiled); + + private sealed class TextMatch + { + public string Field; + public string Value; + public Point3d Pos; + } + + private sealed class VLine + { + public double X; + public double YMin; + public double YMax; + } + + private sealed class HLine + { + public double Y; + public double XMin; + public double XMax; + } + + public static List ExtractCandidates(string templatePath) + { + if (string.IsNullOrWhiteSpace(templatePath)) + { + throw new ArgumentNullException(nameof(templatePath)); + } + + var matches = new List(); + var vLines = new List(); + var hLines = new List(); + + using (var db = new Database(false, true)) + { + ReadTemplateDatabase(db, templatePath); + db.CloseInput(true); + + using (var tr = db.TransactionManager.StartTransaction()) + { + var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead); + var ms = (BlockTableRecord)tr.GetObject(bt[BlockTableRecord.ModelSpace], OpenMode.ForRead); + + var visitedBlocks = new HashSet(); + foreach (ObjectId id in ms) + { + var ent = tr.GetObject(id, OpenMode.ForRead) as Entity; + if (ent != null) + { + CollectEntity(ent, Matrix3d.Identity, tr, visitedBlocks, matches, vLines, hLines); + } + } + + tr.Commit(); + } + } + + return BuildCandidates(matches, vLines, hLines); + } + + private static void ReadTemplateDatabase(Database db, string templatePath) + { + try + { + db.ReadDwgFile(templatePath, FileShare.ReadWrite, true, string.Empty); + } + catch (MissingMethodException) + { + db.ReadDwgFile(templatePath, FileOpenMode.OpenForReadAndAllShare, true, string.Empty); + } + } + + private static void TryCollectLine(Line ln, List vLines, List hLines) + { + var a = ln.StartPoint; + var b = ln.EndPoint; + var dx = Math.Abs(a.X - b.X); + var dy = Math.Abs(a.Y - b.Y); + const double tol = 1e-3; + + if (dx <= tol && dy > tol) + { + vLines.Add(new VLine + { + X = (a.X + b.X) / 2.0, + YMin = Math.Min(a.Y, b.Y), + YMax = Math.Max(a.Y, b.Y) + }); + return; + } + + if (dy <= tol && dx > tol) + { + hLines.Add(new HLine + { + Y = (a.Y + b.Y) / 2.0, + XMin = Math.Min(a.X, b.X), + XMax = Math.Max(a.X, b.X) + }); + } + } + + private static void CollectEntity(Entity ent, Matrix3d transform, Transaction tr, HashSet visitedBlocks, List matches, List vLines, List hLines) + { + if (ent is Line ln) + { + var a = ln.StartPoint.TransformBy(transform); + var b = ln.EndPoint.TransformBy(transform); + TryCollectLinePoints(a, b, vLines, hLines); + return; + } + + if (ent is DBText t) + { + var p = t.Position.TransformBy(transform); + TryCollectText(t.TextString, p, matches); + return; + } + + if (ent is MText mt) + { + var p = mt.Location.TransformBy(transform); + var plain = SimplifyMText(mt.Contents); + TryCollectText(plain, p, matches); + return; + } + + if (ent is BlockReference br) + { + // 属性 + foreach (ObjectId attId in br.AttributeCollection) + { + var att = tr.GetObject(attId, OpenMode.ForRead) as AttributeReference; + if (att == null) + { + continue; + } + + var p = att.Position.TransformBy(transform); + TryCollectText(att.TextString, p, matches); + } + + // 块定义内部 + var blockId = br.BlockTableRecord; + if (blockId.IsNull || visitedBlocks.Contains(blockId)) + { + return; + } + visitedBlocks.Add(blockId); + + var btr = tr.GetObject(blockId, OpenMode.ForRead) as BlockTableRecord; + if (btr == null) + { + return; + } + + var nextTransform = br.BlockTransform * transform; + foreach (ObjectId childId in btr) + { + var child = tr.GetObject(childId, OpenMode.ForRead) as Entity; + if (child != null) + { + CollectEntity(child, nextTransform, tr, visitedBlocks, matches, vLines, hLines); + } + } + } + } + + private static void TryCollectLinePoints(Point3d a, Point3d b, List vLines, List hLines) + { + var dx = Math.Abs(a.X - b.X); + var dy = Math.Abs(a.Y - b.Y); + const double tol = 1e-3; + + if (dx <= tol && dy > tol) + { + vLines.Add(new VLine + { + X = (a.X + b.X) / 2.0, + YMin = Math.Min(a.Y, b.Y), + YMax = Math.Max(a.Y, b.Y) + }); + return; + } + + if (dy <= tol && dx > tol) + { + hLines.Add(new HLine + { + Y = (a.Y + b.Y) / 2.0, + XMin = Math.Min(a.X, b.X), + XMax = Math.Max(a.X, b.X) + }); + } + } + + private static void TryCollectText(string raw, Point3d pos, List matches) + { + foreach (var line in SplitLines(raw)) + { + var m = FieldRegex.Match(line); + if (!m.Success) + { + continue; + } + + var field = m.Groups[1].Value; + var value = NormalizeValue(m.Groups[2].Value); + if (value.Length == 0) + { + continue; + } + + matches.Add(new TextMatch { Field = field, Value = value, Pos = pos }); + } + } + + private static IEnumerable SplitLines(string text) + { + if (string.IsNullOrWhiteSpace(text)) + { + return Enumerable.Empty(); + } + + return (text ?? string.Empty) + .Replace("\r\n", "\n") + .Replace("\r", "\n") + .Split(new[] { '\n' }, StringSplitOptions.RemoveEmptyEntries) + .Select(s => (s ?? string.Empty).Trim()) + .Where(s => s.Length > 0); + } + + private static string SimplifyMText(string contents) + { + if (string.IsNullOrEmpty(contents)) + { + return string.Empty; + } + + var s = contents + .Replace("\\P", "\n") + .Replace("\\p", "\n") + .Replace("\\~", " "); + s = s.Replace("{", string.Empty).Replace("}", string.Empty); + s = MTextFormatRegex.Replace(s, string.Empty); + return s; + } + + private static string NormalizeValue(string s) + { + if (string.IsNullOrWhiteSpace(s)) + { + return string.Empty; + } + + var trimmed = s.Trim(); + var chars = trimmed.Where(c => !char.IsWhiteSpace(c)).ToArray(); + return new string(chars); + } + + private static List BuildCandidates(List matches, List vLines, List hLines) + { + if (matches == null || matches.Count == 0) + { + return new List(); + } + + var clusters = Cluster(matches); + var result = new List(); + var idx = 1; + + foreach (var cluster in clusters) + { + var delivery = cluster.Where(m => m.Field == "交付状态").Select(m => m.Value).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + var process = cluster.Where(m => m.Field == "工艺方法").Select(m => m.Value).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + var structural = cluster.Where(m => m.Field == "结构特征").Select(m => m.Value).Distinct(StringComparer.OrdinalIgnoreCase).ToList(); + + if (delivery.Count == 0 || process.Count == 0 || structural.Count == 0) + { + continue; + } + + var textExt = ExtentsFromPoints(cluster.Select(m => m.Pos)); + + // 优先使用“最近的四边线”构建矩形,更适配你这种 4 条直线外框。 + var rectExt = FindRectangleByNearestLines(textExt, vLines, hLines) ?? FindNearestRectangleExtents(textExt, vLines, hLines); + var window = rectExt.HasValue ? Union(rectExt.Value, Expand(textExt, 200.0)) : Expand(textExt, 2000.0); + + result.Add(new ModelSheetCandidate + { + Name = $"Model#{idx}", + DeliveryStatuses = delivery, + ProcessMethods = process, + StructuralFeatures = structural, + Window = window + }); + + idx++; + } + + return result; + } + + private static List> Cluster(List matches) + { + var n = matches.Count; + var parent = Enumerable.Range(0, n).ToArray(); + + int Find(int x) + { + while (parent[x] != x) { parent[x] = parent[parent[x]]; x = parent[x]; } + return x; + } + + void UnionIdx(int a, int b) + { + var ra = Find(a); + var rb = Find(b); + if (ra != rb) parent[rb] = ra; + } + + var threshold = EstimateThreshold(matches); + var threshold2 = threshold * threshold; + + for (var i = 0; i < n; i++) + { + for (var j = i + 1; j < n; j++) + { + var dx = matches[i].Pos.X - matches[j].Pos.X; + var dy = matches[i].Pos.Y - matches[j].Pos.Y; + var d2 = dx * dx + dy * dy; + if (d2 <= threshold2) + { + UnionIdx(i, j); + } + } + } + + var groups = new Dictionary>(); + for (var i = 0; i < n; i++) + { + var r = Find(i); + if (!groups.TryGetValue(r, out var list)) + { + list = new List(); + groups[r] = list; + } + list.Add(matches[i]); + } + + return groups.Values.ToList(); + } + + private static double EstimateThreshold(List matches) + { + if (matches.Count < 2) + { + return 1000.0; + } + + var dists = new List(); + for (var i = 0; i < matches.Count; i++) + { + var best = double.MaxValue; + for (var j = 0; j < matches.Count; j++) + { + if (i == j) continue; + var dx = matches[i].Pos.X - matches[j].Pos.X; + var dy = matches[i].Pos.Y - matches[j].Pos.Y; + var d = Math.Sqrt(dx * dx + dy * dy); + if (d < best) best = d; + } + if (best < double.MaxValue) dists.Add(best); + } + + dists.Sort(); + var median = dists[dists.Count / 2]; + if (median <= 0) median = 500.0; + var thr = median * 4.0; + if (thr < 500.0) thr = 500.0; + if (thr > 20000.0) thr = 20000.0; + return thr; + } + + private static Extents3d ExtentsFromPoints(IEnumerable pts) + { + var arr = (pts ?? Enumerable.Empty()).ToArray(); + if (arr.Length == 0) + { + return new Extents3d(new Point3d(0, 0, 0), new Point3d(0, 0, 0)); + } + + var minX = arr.Min(p => p.X); + var minY = arr.Min(p => p.Y); + var maxX = arr.Max(p => p.X); + var maxY = arr.Max(p => p.Y); + return new Extents3d(new Point3d(minX, minY, 0), new Point3d(maxX, maxY, 0)); + } + + private static Extents3d Expand(Extents3d e, double margin) + { + return new Extents3d( + new Point3d(e.MinPoint.X - margin, e.MinPoint.Y - margin, 0), + new Point3d(e.MaxPoint.X + margin, e.MaxPoint.Y + margin, 0)); + } + + private static Extents3d Union(Extents3d a, Extents3d b) + { + return new Extents3d( + new Point3d(Math.Min(a.MinPoint.X, b.MinPoint.X), Math.Min(a.MinPoint.Y, b.MinPoint.Y), 0), + new Point3d(Math.Max(a.MaxPoint.X, b.MaxPoint.X), Math.Max(a.MaxPoint.Y, b.MaxPoint.Y), 0)); + } + + private static Extents3d? FindNearestRectangleExtents(Extents3d anchor, List vLines, List hLines) + { + if (vLines == null || hLines == null || vLines.Count < 2 || hLines.Count < 2) + { + return null; + } + + var cx = (anchor.MinPoint.X + anchor.MaxPoint.X) / 2.0; + var cy = (anchor.MinPoint.Y + anchor.MaxPoint.Y) / 2.0; + + var vCand = vLines.OrderBy(l => Math.Abs(l.X - cx)).Take(30).ToList(); + var hCand = hLines.OrderBy(l => Math.Abs(l.Y - cy)).Take(30).ToList(); + + const double tol = 5.0; + Extents3d? best = null; + double bestScore = double.MaxValue; + + for (var i = 0; i < vCand.Count; i++) + { + for (var j = i + 1; j < vCand.Count; j++) + { + var left = vCand[i].X < vCand[j].X ? vCand[i] : vCand[j]; + var right = vCand[i].X < vCand[j].X ? vCand[j] : vCand[i]; + var width = right.X - left.X; + if (width < 1000) continue; + + for (var a = 0; a < hCand.Count; a++) + { + for (var b = a + 1; b < hCand.Count; b++) + { + var bottom = hCand[a].Y < hCand[b].Y ? hCand[a] : hCand[b]; + var top = hCand[a].Y < hCand[b].Y ? hCand[b] : hCand[a]; + var height = top.Y - bottom.Y; + if (height < 1000) continue; + + // span check + if (left.YMin > bottom.Y + tol || left.YMax < top.Y - tol) continue; + if (right.YMin > bottom.Y + tol || right.YMax < top.Y - tol) continue; + if (bottom.XMin > left.X + tol || bottom.XMax < right.X - tol) continue; + if (top.XMin > left.X + tol || top.XMax < right.X - tol) continue; + + var rect = new Extents3d( + new Point3d(left.X, bottom.Y, 0), + new Point3d(right.X, top.Y, 0)); + + var rcx = (left.X + right.X) / 2.0; + var rcy = (bottom.Y + top.Y) / 2.0; + var dist = Math.Sqrt((rcx - cx) * (rcx - cx) + (rcy - cy) * (rcy - cy)); + var area = width * height; + var score = dist + area * 1e-9; // distance first, area tie-break + + if (score < bestScore) + { + bestScore = score; + best = rect; + } + } + } + } + } + + return best; + } + + private static Extents3d? FindRectangleByNearestLines(Extents3d anchor, List vLines, List hLines) + { + if (vLines == null || hLines == null || vLines.Count == 0 || hLines.Count == 0) + { + return null; + } + + var cx = (anchor.MinPoint.X + anchor.MaxPoint.X) / 2.0; + var cy = (anchor.MinPoint.Y + anchor.MaxPoint.Y) / 2.0; + const double tol = 50.0; + + var left = vLines + .Where(l => l.X < cx - tol && l.YMin - tol <= cy && l.YMax + tol >= cy) + .OrderByDescending(l => l.X) + .FirstOrDefault(); + + var right = vLines + .Where(l => l.X > cx + tol && l.YMin - tol <= cy && l.YMax + tol >= cy) + .OrderBy(l => l.X) + .FirstOrDefault(); + + var bottom = hLines + .Where(l => l.Y < cy - tol && l.XMin - tol <= cx && l.XMax + tol >= cx) + .OrderByDescending(l => l.Y) + .FirstOrDefault(); + + var top = hLines + .Where(l => l.Y > cy + tol && l.XMin - tol <= cx && l.XMax + tol >= cx) + .OrderBy(l => l.Y) + .FirstOrDefault(); + + if (left == null || right == null || bottom == null || top == null) + { + return null; + } + + var width = right.X - left.X; + var height = top.Y - bottom.Y; + if (width < 1000 || height < 1000) + { + return null; + } + + var rect = new Extents3d(new Point3d(left.X, bottom.Y, 0), new Point3d(right.X, top.Y, 0)); + if (anchor.MinPoint.X < rect.MinPoint.X - tol || anchor.MaxPoint.X > rect.MaxPoint.X + tol || + anchor.MinPoint.Y < rect.MinPoint.Y - tol || anchor.MaxPoint.Y > rect.MaxPoint.Y + tol) + { + return null; + } + + return rect; + } + } +} diff --git a/CadParamPluging.csproj b/CadParamPluging.csproj index 09abb53..df9aa28 100644 --- a/CadParamPluging.csproj +++ b/CadParamPluging.csproj @@ -60,9 +60,14 @@ + + + + + @@ -74,9 +79,18 @@ UserControl + + Form + + + Form + Form + + Form + diff --git a/Common/DropdownOptions.cs b/Common/DropdownOptions.cs new file mode 100644 index 0000000..ce19aa9 --- /dev/null +++ b/Common/DropdownOptions.cs @@ -0,0 +1,77 @@ +using System; +using System.Collections.Generic; + +namespace CadParamPluging.Common +{ + public class DropdownOptions + { + public List DeliveryStatuses { get; set; } + public List ProcessMethods { get; set; } + public List StructuralFeatures { get; set; } + public List SpecialConditions { get; set; } + + public DropdownOptions() + { + DeliveryStatuses = new List(); + ProcessMethods = new List(); + StructuralFeatures = new List(); + SpecialConditions = new List(); + } + + public static DropdownOptions CreateDefault() + { + return new DropdownOptions + { + DeliveryStatuses = new List { "结构", "示例" }, + ProcessMethods = new List { "基础平面", "示例图" }, + StructuralFeatures = new List { "A1", "A3" }, + SpecialConditions = new List { "无" } + }; + } + + public DropdownOptions Clone() + { + return new DropdownOptions + { + DeliveryStatuses = new List(DeliveryStatuses ?? new List()), + ProcessMethods = new List(ProcessMethods ?? new List()), + StructuralFeatures = new List(StructuralFeatures ?? new List()), + SpecialConditions = new List(SpecialConditions ?? new List()) + }; + } + + public void Normalize() + { + DeliveryStatuses = NormalizeList(DeliveryStatuses); + ProcessMethods = NormalizeList(ProcessMethods); + StructuralFeatures = NormalizeList(StructuralFeatures); + SpecialConditions = NormalizeList(SpecialConditions); + } + + private static List NormalizeList(IEnumerable values) + { + var result = new List(); + if (values == null) + { + return result; + } + + var seen = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var raw in values) + { + var v = (raw ?? string.Empty).Trim(); + if (v.Length == 0) + { + continue; + } + + if (seen.Add(v)) + { + result.Add(v); + } + } + + return result; + } + } +} diff --git a/Common/DropdownOptionsStore.cs b/Common/DropdownOptionsStore.cs new file mode 100644 index 0000000..6f3b99e --- /dev/null +++ b/Common/DropdownOptionsStore.cs @@ -0,0 +1,130 @@ +using System; +using System.IO; +using System.Xml.Serialization; + +namespace CadParamPluging.Common +{ + public static class DropdownOptionsStore + { + private const string FolderName = "CadParamPluging"; + private const string FileName = "dropdown-options.xml"; + + public static string GetFilePath() + { + var dir = Path.Combine( + Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), + FolderName); + return Path.Combine(dir, FileName); + } + + public static DropdownOptions Load() + { + try + { + var path = GetFilePath(); + if (!File.Exists(path)) + { + return DropdownOptions.CreateDefault(); + } + + using (var stream = File.OpenRead(path)) + { + var serializer = new XmlSerializer(typeof(DropdownOptions)); + var obj = serializer.Deserialize(stream) as DropdownOptions; + if (obj == null) + { + return DropdownOptions.CreateDefault(); + } + + obj.Normalize(); + EnsureNonEmptyByFallbackToDefault(obj); + return obj; + } + } + catch + { + return DropdownOptions.CreateDefault(); + } + } + + public static void Save(DropdownOptions options) + { + if (options == null) + { + throw new ArgumentNullException(nameof(options)); + } + + options.Normalize(); + ValidateNonEmpty(options); + + var path = GetFilePath(); + var dir = Path.GetDirectoryName(path); + if (!string.IsNullOrEmpty(dir)) + { + Directory.CreateDirectory(dir); + } + + var tempPath = path + ".tmp"; + var serializer = new XmlSerializer(typeof(DropdownOptions)); + using (var stream = File.Create(tempPath)) + { + serializer.Serialize(stream, options); + } + + if (File.Exists(path)) + { + File.Delete(path); + } + + File.Move(tempPath, path); + } + + private static void ValidateNonEmpty(DropdownOptions options) + { + if (options.DeliveryStatuses == null || options.DeliveryStatuses.Count == 0) + { + throw new InvalidOperationException("交付状态不能为空。"); + } + + if (options.ProcessMethods == null || options.ProcessMethods.Count == 0) + { + throw new InvalidOperationException("工艺方法不能为空。"); + } + + if (options.StructuralFeatures == null || options.StructuralFeatures.Count == 0) + { + throw new InvalidOperationException("结构特征不能为空。"); + } + + if (options.SpecialConditions == null || options.SpecialConditions.Count == 0) + { + throw new InvalidOperationException("特殊条件不能为空。"); + } + } + + private static void EnsureNonEmptyByFallbackToDefault(DropdownOptions options) + { + var defaults = DropdownOptions.CreateDefault(); + + if (options.DeliveryStatuses == null || options.DeliveryStatuses.Count == 0) + { + options.DeliveryStatuses = defaults.DeliveryStatuses; + } + + if (options.ProcessMethods == null || options.ProcessMethods.Count == 0) + { + options.ProcessMethods = defaults.ProcessMethods; + } + + if (options.StructuralFeatures == null || options.StructuralFeatures.Count == 0) + { + options.StructuralFeatures = defaults.StructuralFeatures; + } + + if (options.SpecialConditions == null || options.SpecialConditions.Count == 0) + { + options.SpecialConditions = defaults.SpecialConditions; + } + } + } +} diff --git a/Domain/Models/TemplateInfo.cs b/Domain/Models/TemplateInfo.cs index 089fa79..d69f06c 100644 --- a/Domain/Models/TemplateInfo.cs +++ b/Domain/Models/TemplateInfo.cs @@ -5,5 +5,7 @@ namespace CadParamPluging.Domain.Models public string Name { get; set; } public string FilePath { get; set; } public string Description { get; set; } + + public string LayoutName { get; set; } } } diff --git a/UI/LayoutSelectForm.cs b/UI/LayoutSelectForm.cs new file mode 100644 index 0000000..b73a484 --- /dev/null +++ b/UI/LayoutSelectForm.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; +using CadParamPluging.Cad; + +namespace CadParamPluging.UI +{ + public class LayoutSelectForm : Form + { + private readonly ListBox _list; + private readonly List _items; + + public LayoutAnnotation Selected { get; private set; } + + public LayoutSelectForm(IEnumerable candidates) + { + Text = "选择图纸"; + Size = new Size(520, 360); + StartPosition = FormStartPosition.CenterParent; + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + ShowInTaskbar = false; + + _items = (candidates ?? Enumerable.Empty()).ToList(); + + var layout = new TableLayoutPanel + { + Dock = DockStyle.Fill, + Padding = new Padding(10), + RowCount = 3, + ColumnCount = 1 + }; + layout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100f)); + layout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + + var lbl = new Label + { + Text = "匹配到多张图纸,请选择:", + Dock = DockStyle.Fill, + AutoSize = true + }; + + _list = new ListBox { Dock = DockStyle.Fill }; + foreach (var it in _items) + { + _list.Items.Add(Format(it)); + } + if (_list.Items.Count > 0) + { + _list.SelectedIndex = 0; + } + + var buttons = new FlowLayoutPanel + { + Dock = DockStyle.Fill, + FlowDirection = FlowDirection.RightToLeft, + AutoSize = true + }; + + var btnCancel = new Button { Text = "取消", DialogResult = DialogResult.Cancel }; + var btnOk = new Button { Text = "确定" }; + btnOk.Click += (_, __) => OnOk(); + + buttons.Controls.Add(btnCancel); + buttons.Controls.Add(btnOk); + + layout.Controls.Add(lbl, 0, 0); + layout.Controls.Add(_list, 0, 1); + layout.Controls.Add(buttons, 0, 2); + Controls.Add(layout); + + AcceptButton = btnOk; + CancelButton = btnCancel; + } + + private void OnOk() + { + var idx = _list.SelectedIndex; + if (idx < 0 || idx >= _items.Count) + { + MessageBox.Show(this, "请选择一项。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + Selected = _items[idx]; + DialogResult = DialogResult.OK; + Close(); + } + + private static string Format(LayoutAnnotation it) + { + var d = it?.DeliveryStatuses != null && it.DeliveryStatuses.Count > 0 ? string.Join("/", it.DeliveryStatuses) : "(无)"; + var p = it?.ProcessMethods != null && it.ProcessMethods.Count > 0 ? string.Join("/", it.ProcessMethods) : "(无)"; + var s = it?.StructuralFeatures != null && it.StructuralFeatures.Count > 0 ? string.Join("/", it.StructuralFeatures) : "(无)"; + return $"{it?.LayoutName} | 交付状态={d} | 工艺方法={p} | 结构特征={s}"; + } + + public static bool TrySelect(IWin32Window owner, IEnumerable candidates, out LayoutAnnotation selected) + { + using (var f = new LayoutSelectForm(candidates)) + { + var r = f.ShowDialog(owner); + if (r == DialogResult.OK) + { + selected = f.Selected; + return selected != null; + } + } + + selected = null; + return false; + } + } +} diff --git a/UI/ModelSheetSelectForm.cs b/UI/ModelSheetSelectForm.cs new file mode 100644 index 0000000..3255fd1 --- /dev/null +++ b/UI/ModelSheetSelectForm.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Drawing; +using System.Linq; +using System.Windows.Forms; +using CadParamPluging.Cad; + +namespace CadParamPluging.UI +{ + public class ModelSheetSelectForm : Form + { + private readonly ListBox _list; + private readonly List _items; + + public ModelSheetCandidate Selected { get; private set; } + + public ModelSheetSelectForm(IEnumerable candidates) + { + Text = "选择图纸"; + Size = new Size(560, 360); + StartPosition = FormStartPosition.CenterParent; + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + ShowInTaskbar = false; + + _items = (candidates ?? Enumerable.Empty()).ToList(); + + var layout = new TableLayoutPanel + { + Dock = DockStyle.Fill, + Padding = new Padding(10), + RowCount = 3, + ColumnCount = 1 + }; + layout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100f)); + layout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + + var lbl = new Label + { + Text = "匹配到多张图纸,请选择:", + Dock = DockStyle.Fill, + AutoSize = true + }; + + _list = new ListBox { Dock = DockStyle.Fill }; + foreach (var it in _items) + { + _list.Items.Add(Format(it)); + } + if (_list.Items.Count > 0) + { + _list.SelectedIndex = 0; + } + + var buttons = new FlowLayoutPanel + { + Dock = DockStyle.Fill, + FlowDirection = FlowDirection.RightToLeft, + AutoSize = true + }; + + var btnCancel = new Button { Text = "取消", DialogResult = DialogResult.Cancel }; + var btnOk = new Button { Text = "确定" }; + btnOk.Click += (_, __) => OnOk(); + + buttons.Controls.Add(btnCancel); + buttons.Controls.Add(btnOk); + + layout.Controls.Add(lbl, 0, 0); + layout.Controls.Add(_list, 0, 1); + layout.Controls.Add(buttons, 0, 2); + Controls.Add(layout); + + AcceptButton = btnOk; + CancelButton = btnCancel; + } + + private void OnOk() + { + var idx = _list.SelectedIndex; + if (idx < 0 || idx >= _items.Count) + { + MessageBox.Show(this, "请选择一项。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + Selected = _items[idx]; + DialogResult = DialogResult.OK; + Close(); + } + + private static string Format(ModelSheetCandidate it) + { + var d = it?.DeliveryStatuses != null && it.DeliveryStatuses.Count > 0 ? string.Join("/", it.DeliveryStatuses) : "(无)"; + var p = it?.ProcessMethods != null && it.ProcessMethods.Count > 0 ? string.Join("/", it.ProcessMethods) : "(无)"; + var s = it?.StructuralFeatures != null && it.StructuralFeatures.Count > 0 ? string.Join("/", it.StructuralFeatures) : "(无)"; + return $"{it?.Name} | 交付状态={d} | 工艺方法={p} | 结构特征={s}"; + } + + public static bool TrySelect(IWin32Window owner, IEnumerable candidates, out ModelSheetCandidate selected) + { + using (var f = new ModelSheetSelectForm(candidates)) + { + var r = f.ShowDialog(owner); + if (r == DialogResult.OK) + { + selected = f.Selected; + return selected != null; + } + } + + selected = null; + return false; + } + } +} diff --git a/UI/ParamDrawingPanel.cs b/UI/ParamDrawingPanel.cs index c5f38bc..e9bb712 100644 --- a/UI/ParamDrawingPanel.cs +++ b/UI/ParamDrawingPanel.cs @@ -1,6 +1,7 @@ using System; using System.Drawing; using System.Globalization; +using System.Linq; using System.Windows.Forms; using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; @@ -25,11 +26,15 @@ namespace CadParamPluging.UI private TextBox _txtTemplatePath; private TemplateInfo _selectedTemplate; + private Extents3d? _selectedModelWindow; + private string _selectedSheetName; public ParamDrawingPanel() { Dock = DockStyle.Fill; + var dropdownOptions = DropdownOptionsStore.Load(); + var layout = new TableLayoutPanel { Dock = DockStyle.Fill, @@ -40,10 +45,10 @@ namespace CadParamPluging.UI Padding = new Padding(8) }; - _cbProjectType = CreateComboBox(new[] { "结构", "示例" }); - _cbDrawingType = CreateComboBox(new[] { "基础平面", "示例图" }); - _cbSheetSize = CreateComboBox(new[] { "A1", "A3" }); - _cbScale = CreateComboBox(new[] { "1:50", "1:100" }); + _cbProjectType = CreateComboBox(dropdownOptions.DeliveryStatuses); + _cbDrawingType = CreateComboBox(dropdownOptions.ProcessMethods); + _cbSheetSize = CreateComboBox(dropdownOptions.StructuralFeatures); + _cbScale = CreateComboBox(dropdownOptions.SpecialConditions); _txtWidth = CreateTextBox("5000"); _txtHeight = CreateTextBox("3000"); @@ -191,29 +196,170 @@ namespace CadParamPluging.UI { try { + if (_selectedTemplate == null || string.IsNullOrWhiteSpace(_selectedTemplate.FilePath)) + { + AppendLog("请先选择模板文件。"); + return; + } + + AppendLog("开始匹配图纸..."); + var tplParams = CollectTemplateParams(); - _selectedTemplate = DomainFacade.SelectTemplate(tplParams); - _lblTemplateInfo.Text = $"已匹配: {_selectedTemplate.Name}"; - _txtTemplatePath.Text = _selectedTemplate.FilePath; - AppendLog($"模板: {_selectedTemplate.FilePath}"); + // 1) 优先按 Layout 匹配(适用于多布局模板) + var layoutCandidates = TemplateLayoutAnnotationExtractor.ExtractPerLayout(_selectedTemplate.FilePath); + var layoutMatches = layoutCandidates.Where(c => LayoutMatches(c, tplParams)).ToList(); + + if (layoutMatches.Count > 0) + { + LayoutAnnotation selectedLayout; + if (layoutMatches.Count == 1) + { + selectedLayout = layoutMatches[0]; + } + else + { + if (!LayoutSelectForm.TrySelect(this, layoutMatches, out selectedLayout)) + { + AppendLog("用户取消选择图纸。"); + return; + } + } + + _selectedTemplate.LayoutName = selectedLayout.LayoutName; + _selectedModelWindow = null; + _selectedSheetName = selectedLayout.LayoutName; + _lblTemplateInfo.Text = $"{_selectedTemplate.FilePath} | {selectedLayout.LayoutName}"; + AppendLog($"已匹配图纸: {selectedLayout.LayoutName}"); + return; + } + + // 2) 如果 Layout 为空或未匹配,按 ModelSpace 多张图纸模式匹配 + var modelCandidates = TemplateModelSheetExtractor.ExtractCandidates(_selectedTemplate.FilePath); + var modelMatches = modelCandidates.Where(c => ModelMatches(c, tplParams)).ToList(); + + if (modelMatches.Count == 0) + { + _selectedTemplate.LayoutName = null; + _selectedModelWindow = null; + _selectedSheetName = null; + _lblTemplateInfo.Text = _selectedTemplate.FilePath; + AppendLog("未找到匹配图纸。"); + AppendLog($"候选图纸数量: {modelCandidates.Count}"); + foreach (var c in modelCandidates.Take(10)) + { + AppendLog($"候选: {c.Name} | 交付状态={Preview(c.DeliveryStatuses)} | 工艺方法={Preview(c.ProcessMethods)} | 结构特征={Preview(c.StructuralFeatures)}"); + } + return; + } + + ModelSheetCandidate selectedModel; + if (modelMatches.Count == 1) + { + selectedModel = modelMatches[0]; + } + else + { + if (!ModelSheetSelectForm.TrySelect(this, modelMatches, out selectedModel)) + { + AppendLog("用户取消选择图纸。"); + return; + } + } + + _selectedTemplate.LayoutName = null; + _selectedModelWindow = selectedModel.Window; + _selectedSheetName = selectedModel.Name; + _lblTemplateInfo.Text = $"{_selectedTemplate.FilePath} | {selectedModel.Name}"; + AppendLog($"已匹配图纸: {selectedModel.Name}"); } catch (BusinessException bex) { - _selectedTemplate = null; - _lblTemplateInfo.Text = "未匹配模板"; - _txtTemplatePath.Text = ""; - AppendLog($"模板选择错误: {bex.Message}"); + if (_selectedTemplate != null) + { + _selectedTemplate.LayoutName = null; + } + _selectedModelWindow = null; + _selectedSheetName = null; + _lblTemplateInfo.Text = _selectedTemplate?.FilePath ?? "未匹配图纸"; + AppendLog($"图纸匹配错误: {bex.Message}"); } catch (Exception ex) { - _selectedTemplate = null; - _lblTemplateInfo.Text = "未匹配模板"; - _txtTemplatePath.Text = ""; - AppendLog($"异常: {ex.Message}"); - Logger.Error("MatchTemplate", ex); + if (_selectedTemplate != null) + { + _selectedTemplate.LayoutName = null; + } + _selectedModelWindow = null; + _selectedSheetName = null; + _lblTemplateInfo.Text = _selectedTemplate?.FilePath ?? "未匹配图纸"; + AppendLog($"匹配失败: {ex.Message}"); + Logger.Error("MatchLayout", ex); } } + private static bool LayoutMatches(LayoutAnnotation ann, TemplateParams tplParams) + { + if (ann == null || tplParams == null) + { + return false; + } + + return ContainsIgnoreCase(ann.DeliveryStatuses, tplParams.ProjectType) + && ContainsIgnoreCase(ann.ProcessMethods, tplParams.DrawingType) + && ContainsIgnoreCase(ann.StructuralFeatures, tplParams.SheetSize); + } + + private static bool ContainsIgnoreCase(System.Collections.Generic.IEnumerable values, string expected) + { + if (string.IsNullOrWhiteSpace(expected)) + { + return true; + } + + if (values == null) + { + return false; + } + + var exp = NormalizeValue(expected); + return values.Any(v => string.Equals(NormalizeValue(v), exp, StringComparison.OrdinalIgnoreCase)); + } + + private static string NormalizeValue(string s) + { + if (string.IsNullOrWhiteSpace(s)) + { + return string.Empty; + } + + var trimmed = s.Trim(); + var chars = trimmed.Where(c => !char.IsWhiteSpace(c)).ToArray(); + return new string(chars); + } + + private static string Preview(System.Collections.Generic.IEnumerable values) + { + if (values == null) + { + return "(无)"; + } + + var arr = values.Where(v => !string.IsNullOrWhiteSpace(v)).Select(v => v.Trim()).ToArray(); + return arr.Length == 0 ? "(无)" : string.Join("/", arr); + } + + private static bool ModelMatches(ModelSheetCandidate ann, TemplateParams tplParams) + { + if (ann == null || tplParams == null) + { + return false; + } + + return ContainsIgnoreCase(ann.DeliveryStatuses, tplParams.ProjectType) + && ContainsIgnoreCase(ann.ProcessMethods, tplParams.DrawingType) + && ContainsIgnoreCase(ann.StructuralFeatures, tplParams.SheetSize); + } + private void OnSelectTemplate() { try @@ -235,8 +381,11 @@ namespace CadParamPluging.UI }; _txtTemplatePath.Text = filePath; - _lblTemplateInfo.Text = $"已选择: {fileName}"; + + _lblTemplateInfo.Text = filePath; AppendLog($"手动选择模板: {filePath}"); + + TryReplaceDropdownOptionsFromTemplate(filePath); } } } @@ -247,6 +396,106 @@ namespace CadParamPluging.UI } } + private void TryReplaceDropdownOptionsFromTemplate(string templatePath) + { + try + { + var result = TemplateAnnotationOptionsExtractor.Extract(templatePath); + var options = result?.Options; + + if (options == null || !HasAnyOptions(options)) + { + AppendLog("未从模板标注解析到参数,下拉列表不变。"); + return; + } + + AppendLog("已从模板标注解析到参数,询问是否替换下拉设置。"); + + var preview = BuildOptionsPreview(options, 5); + var confirm = MessageBox.Show( + this, + $"检测到模板参数(去重后):\n\n{preview}\n\n是否用模板参数替换当前下拉列表并保存?\n(特殊条件不从模板解析,将保持当前设置)", + "模板参数", + MessageBoxButtons.YesNo, + MessageBoxIcon.Question); + + if (confirm != DialogResult.Yes) + { + AppendLog("用户取消替换下拉设置。"); + return; + } + + var current = DropdownOptionsStore.Load(); + var updated = (current ?? DropdownOptions.CreateDefault()).Clone(); + + if (options.DeliveryStatuses != null && options.DeliveryStatuses.Count > 0) + { + updated.DeliveryStatuses = options.DeliveryStatuses.ToList(); + } + + if (options.ProcessMethods != null && options.ProcessMethods.Count > 0) + { + updated.ProcessMethods = options.ProcessMethods.ToList(); + } + + if (options.StructuralFeatures != null && options.StructuralFeatures.Count > 0) + { + updated.StructuralFeatures = options.StructuralFeatures.ToList(); + } + + updated.Normalize(); + DropdownOptionsStore.Save(updated); + ReloadDropdownOptions(updated); + AppendLog("已从模板替换下拉设置并保存。"); + } + catch (Exception ex) + { + AppendLog($"模板解析/替换失败: {ex.Message}"); + Logger.Error("TemplateDropdownOptions", ex); + } + } + + private static bool HasAnyOptions(DropdownOptions options) + { + return options != null + && ((options.DeliveryStatuses != null && options.DeliveryStatuses.Count > 0) + || (options.ProcessMethods != null && options.ProcessMethods.Count > 0) + || (options.StructuralFeatures != null && options.StructuralFeatures.Count > 0)); + } + + private static string BuildOptionsPreview(DropdownOptions options, int maxPerField) + { + return string.Join( + Environment.NewLine, + new[] + { + $"交付状态: {PreviewValues(options?.DeliveryStatuses, maxPerField)}", + $"工艺方法: {PreviewValues(options?.ProcessMethods, maxPerField)}", + $"结构特征: {PreviewValues(options?.StructuralFeatures, maxPerField)}" + }); + } + + private static string PreviewValues(System.Collections.Generic.IEnumerable values, int max) + { + if (values == null) + { + return "(无)"; + } + + var arr = values.Where(v => !string.IsNullOrWhiteSpace(v)).Select(v => v.Trim()).ToArray(); + if (arr.Length == 0) + { + return "(无)"; + } + + if (arr.Length <= max) + { + return string.Join(", ", arr); + } + + return string.Join(", ", arr.Take(max)) + $" ... (共 {arr.Length} 项)"; + } + private void OnGenerateDrawing() { try @@ -254,16 +503,36 @@ namespace CadParamPluging.UI var tplParams = CollectTemplateParams(); var drawingParams = CollectDrawingParams(); - if (_selectedTemplate == null) + if (_selectedTemplate == null || string.IsNullOrWhiteSpace(_selectedTemplate.FilePath)) { - _selectedTemplate = DomainFacade.SelectTemplate(tplParams); - _lblTemplateInfo.Text = $"已匹配: {_selectedTemplate.Name}"; + AppendLog("请先选择模板文件。"); + return; + } + + if (string.IsNullOrWhiteSpace(_selectedTemplate.LayoutName) && !_selectedModelWindow.HasValue) + { + AppendLog("未匹配图纸,尝试自动匹配..."); + OnMatchTemplate(); + if (_selectedTemplate == null || (string.IsNullOrWhiteSpace(_selectedTemplate.LayoutName) && !_selectedModelWindow.HasValue)) + { + return; + } } DomainFacade.ValidateParameters(tplParams, drawingParams); var doc = TemplateDrawingService.CreateDocumentFromTemplate(_selectedTemplate); + if (_selectedModelWindow.HasValue) + { + TemplateDrawingService.KeepOnlyModelWindow(doc, _selectedModelWindow.Value); + } + else + { + TemplateDrawingService.KeepOnlyLayout(doc, _selectedTemplate.LayoutName); + } + AppendLog($"已生成图纸窗口,并仅保留图纸: {_selectedSheetName ?? _selectedTemplate.LayoutName}"); + using (doc.LockDocument()) using (var ctx = new CadContext(doc)) { @@ -320,12 +589,14 @@ namespace CadParamPluging.UI { try { - using (var form = new SettingsForm()) + var current = DropdownOptionsStore.Load(); + using (var form = new SettingsForm(current)) { if (form.ShowDialog() == DialogResult.OK) { - // TODO: Save settings - AppendLog("设置已保存 (暂无实际生效项)"); + DropdownOptionsStore.Save(form.Result); + ReloadDropdownOptions(form.Result); + AppendLog("设置已保存"); } } } @@ -336,14 +607,33 @@ namespace CadParamPluging.UI } } + private void ReloadDropdownOptions(DropdownOptions options) + { + if (options == null) + { + return; + } + + ResetComboBoxItems(_cbProjectType, options.DeliveryStatuses); + ResetComboBoxItems(_cbDrawingType, options.ProcessMethods); + ResetComboBoxItems(_cbSheetSize, options.StructuralFeatures); + ResetComboBoxItems(_cbScale, options.SpecialConditions); + } + private TemplateParams CollectTemplateParams() { + var specialCondition = _cbScale.Text?.Trim(); + if (string.Equals(specialCondition, "无", StringComparison.OrdinalIgnoreCase)) + { + specialCondition = null; + } + return new TemplateParams { ProjectType = _cbProjectType.Text?.Trim(), DrawingType = _cbDrawingType.Text?.Trim(), SheetSize = _cbSheetSize.Text?.Trim(), - Scale = _cbScale.Text?.Trim() + Scale = specialCondition }; } @@ -356,14 +646,19 @@ namespace CadParamPluging.UI }; } - private static ComboBox CreateComboBox(string[] items) + private static ComboBox CreateComboBox(System.Collections.Generic.IEnumerable items) { var cb = new ComboBox { DropDownStyle = ComboBoxStyle.DropDownList, Width = 160 }; - cb.Items.AddRange(items); + + var arr = (items ?? Enumerable.Empty()).Where(s => !string.IsNullOrWhiteSpace(s)).Select(s => s.Trim()).ToArray(); + if (arr.Length > 0) + { + cb.Items.AddRange(arr); + } if (cb.Items.Count > 0) { cb.SelectedIndex = 0; @@ -371,6 +666,33 @@ namespace CadParamPluging.UI return cb; } + private static void ResetComboBoxItems(ComboBox cb, System.Collections.Generic.IEnumerable items) + { + if (cb == null) + { + return; + } + + var previous = cb.Text; + var arr = (items ?? Enumerable.Empty()).Where(s => !string.IsNullOrWhiteSpace(s)).Select(s => s.Trim()).ToArray(); + + cb.BeginUpdate(); + try + { + cb.Items.Clear(); + if (arr.Length > 0) + { + cb.Items.AddRange(arr); + var idx = Array.FindIndex(arr, v => string.Equals(v, previous, StringComparison.OrdinalIgnoreCase)); + cb.SelectedIndex = idx >= 0 ? idx : 0; + } + } + finally + { + cb.EndUpdate(); + } + } + private static TextBox CreateTextBox(string defaultText) { return new TextBox { Width = 160, Text = defaultText }; diff --git a/UI/SettingsForm.cs b/UI/SettingsForm.cs index 406b013..73919b2 100644 --- a/UI/SettingsForm.cs +++ b/UI/SettingsForm.cs @@ -1,15 +1,24 @@ using System; using System.Drawing; +using System.Linq; using System.Windows.Forms; +using CadParamPluging.Common; namespace CadParamPluging.UI { public class SettingsForm : Form { - public SettingsForm() + private readonly ListBox _lbDeliveryStatuses; + private readonly ListBox _lbProcessMethods; + private readonly ListBox _lbStructuralFeatures; + private readonly ListBox _lbSpecialConditions; + + public DropdownOptions Result { get; private set; } + + public SettingsForm(DropdownOptions current) { Text = "设置"; - Size = new Size(400, 300); + Size = new Size(560, 420); StartPosition = FormStartPosition.CenterScreen; FormBorderStyle = FormBorderStyle.FixedDialog; MaximizeBox = false; @@ -26,20 +35,27 @@ namespace CadParamPluging.UI layout.RowStyles.Add(new RowStyle(SizeType.Percent, 100f)); // Content area layout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); // Buttons area - // Placeholder for future settings - var contentPanel = new GroupBox + _lbDeliveryStatuses = new ListBox(); + _lbProcessMethods = new ListBox(); + _lbStructuralFeatures = new ListBox(); + _lbSpecialConditions = new ListBox(); + + var options = (current ?? DropdownOptions.CreateDefault()).Clone(); + options.Normalize(); + + FillList(_lbDeliveryStatuses, options.DeliveryStatuses); + FillList(_lbProcessMethods, options.ProcessMethods); + FillList(_lbStructuralFeatures, options.StructuralFeatures); + FillList(_lbSpecialConditions, options.SpecialConditions); + + var tabs = new TabControl { - Text = "常规设置", Dock = DockStyle.Fill }; - var lblPlaceholder = new Label - { - Text = "更多设置项即将推出...", - AutoSize = true, - Location = new Point(20, 30), - ForeColor = Color.Gray - }; - contentPanel.Controls.Add(lblPlaceholder); + tabs.TabPages.Add(BuildEditableTab("交付状态", _lbDeliveryStatuses)); + tabs.TabPages.Add(BuildEditableTab("工艺方法", _lbProcessMethods)); + tabs.TabPages.Add(BuildEditableTab("结构特征", _lbStructuralFeatures)); + tabs.TabPages.Add(BuildEditableTab("特殊条件", _lbSpecialConditions)); // Bottom buttons var flowButtons = new FlowLayoutPanel @@ -50,12 +66,13 @@ namespace CadParamPluging.UI }; var btnCancel = new Button { Text = "取消", DialogResult = DialogResult.Cancel }; - var btnOk = new Button { Text = "确定", DialogResult = DialogResult.OK }; + var btnOk = new Button { Text = "确定" }; + btnOk.Click += (_, __) => OnOk(); flowButtons.Controls.Add(btnCancel); flowButtons.Controls.Add(btnOk); - layout.Controls.Add(contentPanel, 0, 0); + layout.Controls.Add(tabs, 0, 0); layout.Controls.Add(flowButtons, 0, 1); Controls.Add(layout); @@ -63,5 +80,192 @@ namespace CadParamPluging.UI AcceptButton = btnOk; CancelButton = btnCancel; } + + private static void FillList(ListBox listBox, System.Collections.Generic.IEnumerable items) + { + listBox.Items.Clear(); + if (items == null) + { + return; + } + + foreach (var item in items) + { + if (!string.IsNullOrWhiteSpace(item)) + { + listBox.Items.Add(item.Trim()); + } + } + + if (listBox.Items.Count > 0) + { + listBox.SelectedIndex = 0; + } + } + + private TabPage BuildEditableTab(string title, ListBox listBox) + { + var page = new TabPage(title); + + var layout = new TableLayoutPanel + { + Dock = DockStyle.Fill, + ColumnCount = 2, + RowCount = 1, + Padding = new Padding(8) + }; + layout.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100f)); + layout.ColumnStyles.Add(new ColumnStyle(SizeType.AutoSize)); + + listBox.Dock = DockStyle.Fill; + + var actions = new FlowLayoutPanel + { + Dock = DockStyle.Fill, + FlowDirection = FlowDirection.TopDown, + WrapContents = false, + AutoSize = true + }; + + var btnAdd = new Button { Text = "新增", AutoSize = true }; + btnAdd.Click += (_, __) => OnAddItem(title, listBox); + + var btnEdit = new Button { Text = "修改", AutoSize = true }; + btnEdit.Click += (_, __) => OnEditItem(title, listBox); + + var btnDelete = new Button { Text = "删除", AutoSize = true }; + btnDelete.Click += (_, __) => OnDeleteItem(title, listBox); + + actions.Controls.Add(btnAdd); + actions.Controls.Add(btnEdit); + actions.Controls.Add(btnDelete); + + layout.Controls.Add(listBox, 0, 0); + layout.Controls.Add(actions, 1, 0); + page.Controls.Add(layout); + return page; + } + + private void OnAddItem(string category, ListBox listBox) + { + if (!TextInputForm.TryGetValue(this, $"{category} - 新增", "请输入内容:", string.Empty, out var value)) + { + return; + } + + value = (value ?? string.Empty).Trim(); + if (value.Length == 0) + { + MessageBox.Show(this, "内容不能为空。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + listBox.Items.Add(value); + listBox.SelectedIndex = listBox.Items.Count - 1; + } + + private void OnEditItem(string category, ListBox listBox) + { + var selected = listBox.SelectedItem as string; + if (string.IsNullOrWhiteSpace(selected)) + { + MessageBox.Show(this, "请先选择一项再修改。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + if (!TextInputForm.TryGetValue(this, $"{category} - 修改", "请输入内容:", selected, out var value)) + { + return; + } + + value = (value ?? string.Empty).Trim(); + if (value.Length == 0) + { + MessageBox.Show(this, "内容不能为空。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + var idx = listBox.SelectedIndex; + listBox.Items[idx] = value; + listBox.SelectedIndex = idx; + } + + private void OnDeleteItem(string category, ListBox listBox) + { + var selected = listBox.SelectedItem as string; + if (string.IsNullOrWhiteSpace(selected)) + { + MessageBox.Show(this, "请先选择一项再删除。", "提示", MessageBoxButtons.OK, MessageBoxIcon.Information); + return; + } + + var confirm = MessageBox.Show( + this, + $"确认删除:{selected}?", + $"{category} - 删除", + MessageBoxButtons.YesNo, + MessageBoxIcon.Warning); + + if (confirm != DialogResult.Yes) + { + return; + } + + var idx = listBox.SelectedIndex; + listBox.Items.RemoveAt(idx); + if (listBox.Items.Count > 0) + { + listBox.SelectedIndex = Math.Min(idx, listBox.Items.Count - 1); + } + } + + private void OnOk() + { + var result = new DropdownOptions + { + DeliveryStatuses = _lbDeliveryStatuses.Items.Cast().Select(o => (o as string) ?? string.Empty).ToList(), + ProcessMethods = _lbProcessMethods.Items.Cast().Select(o => (o as string) ?? string.Empty).ToList(), + StructuralFeatures = _lbStructuralFeatures.Items.Cast().Select(o => (o as string) ?? string.Empty).ToList(), + SpecialConditions = _lbSpecialConditions.Items.Cast().Select(o => (o as string) ?? string.Empty).ToList() + }; + + result.Normalize(); + + var err = ValidateResult(result); + if (err != null) + { + MessageBox.Show(this, err, "提示", MessageBoxButtons.OK, MessageBoxIcon.Warning); + return; + } + + Result = result; + DialogResult = DialogResult.OK; + Close(); + } + + private static string ValidateResult(DropdownOptions options) + { + if (options.DeliveryStatuses == null || options.DeliveryStatuses.Count == 0) + { + return "交付状态至少需要 1 项。"; + } + + if (options.ProcessMethods == null || options.ProcessMethods.Count == 0) + { + return "工艺方法至少需要 1 项。"; + } + + if (options.StructuralFeatures == null || options.StructuralFeatures.Count == 0) + { + return "结构特征至少需要 1 项。"; + } + + if (options.SpecialConditions == null || options.SpecialConditions.Count == 0) + { + return "特殊条件至少需要 1 项。"; + } + + return null; + } } } diff --git a/UI/TextInputForm.cs b/UI/TextInputForm.cs new file mode 100644 index 0000000..0df6bda --- /dev/null +++ b/UI/TextInputForm.cs @@ -0,0 +1,84 @@ +using System; +using System.Drawing; +using System.Windows.Forms; + +namespace CadParamPluging.UI +{ + public class TextInputForm : Form + { + private readonly TextBox _txt; + + public string Value => _txt.Text?.Trim(); + + public TextInputForm(string title, string label, string initialValue) + { + Text = title; + Size = new Size(360, 150); + StartPosition = FormStartPosition.CenterParent; + FormBorderStyle = FormBorderStyle.FixedDialog; + MaximizeBox = false; + MinimizeBox = false; + ShowInTaskbar = false; + + var layout = new TableLayoutPanel + { + Dock = DockStyle.Fill, + Padding = new Padding(10), + RowCount = 3, + ColumnCount = 1 + }; + layout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + layout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + layout.RowStyles.Add(new RowStyle(SizeType.AutoSize)); + + var lbl = new Label + { + Text = label, + AutoSize = true, + Dock = DockStyle.Fill + }; + + _txt = new TextBox + { + Dock = DockStyle.Fill, + Text = initialValue ?? string.Empty + }; + + var buttons = new FlowLayoutPanel + { + Dock = DockStyle.Fill, + FlowDirection = FlowDirection.RightToLeft, + AutoSize = true + }; + + var btnCancel = new Button { Text = "取消", DialogResult = DialogResult.Cancel }; + var btnOk = new Button { Text = "确定", DialogResult = DialogResult.OK }; + buttons.Controls.Add(btnCancel); + buttons.Controls.Add(btnOk); + + layout.Controls.Add(lbl, 0, 0); + layout.Controls.Add(_txt, 0, 1); + layout.Controls.Add(buttons, 0, 2); + Controls.Add(layout); + + AcceptButton = btnOk; + CancelButton = btnCancel; + } + + public static bool TryGetValue(IWin32Window owner, string title, string label, string initialValue, out string value) + { + using (var form = new TextInputForm(title, label, initialValue)) + { + var result = form.ShowDialog(owner); + if (result == DialogResult.OK) + { + value = form.Value; + return true; + } + } + + value = null; + return false; + } + } +}