From 35a35a6035352598a138c42d5a9b92ad54ad0d9f Mon Sep 17 00:00:00 2001 From: vienha Date: Sat, 5 Sep 2026 18:26:49 +0700 Subject: [PATCH] Add DwgExportManager app for batch DWG/DXF export to PDF/PNG Adds a new WPF tool (plus self-contained WiX installer) that browses a folder of drawings, lets the user pick Model/Layout and a per-file PNG paper size, and batch-exports to PDF and/or PNG through AutoCAD COM. Exposes AcadComUtils publicly so the new project can reuse ScriptPro's existing AutoCAD COM helpers, and ignores the publish/ build output dir. Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_011fpqFb1EZqvV2c7PURqcuq --- .gitignore | 4 + DrawingListUC/AcadComUtils.cs | 2 +- DwgExportManager/AcadSession.cs | 191 +++++++++++ DwgExportManager/App.xaml | 7 + DwgExportManager/App.xaml.cs | 8 + DwgExportManager/DwgExportManager.csproj | 68 ++++ DwgExportManager/ExportEngine.cs | 222 ++++++++++++ DwgExportManager/LayoutReader.cs | 45 +++ DwgExportManager/MainWindow.xaml | 133 ++++++++ DwgExportManager/MainWindow.xaml.cs | 319 ++++++++++++++++++ DwgExportManager/MessageFilter.cs | 82 +++++ DwgExportManager/Models/DwgFileItem.cs | 67 ++++ .../DwgExportManagerSetup.wixproj | 39 +++ DwgExportManagerSetup/Product.wxs | 59 ++++ MoTaNghiepVu.md | 70 ++++ README.md | 4 +- ScriptProPlus.sln | 22 ++ TestFiles/PlotAllLayoutsToPDF_A1.scr | 68 ++++ TestFiles/PlotAllLayoutsToPNG.scr | 72 ++++ 19 files changed, 1480 insertions(+), 2 deletions(-) create mode 100644 DwgExportManager/AcadSession.cs create mode 100644 DwgExportManager/App.xaml create mode 100644 DwgExportManager/App.xaml.cs create mode 100644 DwgExportManager/DwgExportManager.csproj create mode 100644 DwgExportManager/ExportEngine.cs create mode 100644 DwgExportManager/LayoutReader.cs create mode 100644 DwgExportManager/MainWindow.xaml create mode 100644 DwgExportManager/MainWindow.xaml.cs create mode 100644 DwgExportManager/MessageFilter.cs create mode 100644 DwgExportManager/Models/DwgFileItem.cs create mode 100644 DwgExportManagerSetup/DwgExportManagerSetup.wixproj create mode 100644 DwgExportManagerSetup/Product.wxs create mode 100644 MoTaNghiepVu.md create mode 100644 TestFiles/PlotAllLayoutsToPDF_A1.scr create mode 100644 TestFiles/PlotAllLayoutsToPNG.scr diff --git a/.gitignore b/.gitignore index acf7cba..f3ffb81 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ x86/ [Oo]bj/ Binaries/ Standalone/ +publish/ # Visual Studio .vs/ @@ -43,3 +44,6 @@ Thumbs.db ehthumbs.db Desktop.ini +# VS project upgrade/migration reports (auto-generated) +UpgradeLog*.htm + diff --git a/DrawingListUC/AcadComUtils.cs b/DrawingListUC/AcadComUtils.cs index 848ec5c..aa3042d 100644 --- a/DrawingListUC/AcadComUtils.cs +++ b/DrawingListUC/AcadComUtils.cs @@ -16,7 +16,7 @@ namespace DrawingListUC { - internal static class AcadComUtils + public static class AcadComUtils { // -------------------- PUBLIC API -------------------- diff --git a/DwgExportManager/AcadSession.cs b/DwgExportManager/AcadSession.cs new file mode 100644 index 0000000..ac69db2 --- /dev/null +++ b/DwgExportManager/AcadSession.cs @@ -0,0 +1,191 @@ +using System; +using System.Reflection; +using System.Runtime.InteropServices; +using System.Threading; +using DrawingListUC; + +namespace DwgExportManager +{ + // Dieu khien AutoCAD qua COM (attach vao instance dang chay hoac tao moi), + // mo/kich hoat ban ve va chuyen tab Model/Layout. + // Tai su dung AcadComUtils - nghiep vu ket noi AutoCAD da co san trong ScriptPro (DrawingListUC). + public class AcadSession + { + // HRESULT AutoCAD/COM tra ve khi ung dung dang ban (dang hien dialog, dang ve lai man hinh...) + // ScriptUI/DrawingListUC khong gap loi nay vi no chay tren luong UI (STA) co dang ky + // IOleMessageFilter (xem MessageFilter.cs) de COM tu dong retry. DwgExportManager goi COM + // ca tu luong nen (BackgroundWorker, MTA) - IOleMessageFilter KHONG co tac dung voi MTA, + // nen phai tu bat loi va retry thu cong o day. + private const int RPC_E_CALL_REJECTED = unchecked((int)0x80010001); + private const int RPC_E_SERVERCALL_RETRYLATER = unchecked((int)0x8001010A); + private const int MaxRetries = 60; // toi da ~15s (60 x 250ms) + private const int RetryDelayMs = 250; + + private object _acadApp; + + // Goi InvokeMember co tu dong retry khi AutoCAD bao "busy" + private static object Invoke(object target, string name, BindingFlags flags, object[] args) + { + int attempt = 0; + while (true) + { + try + { + return target.GetType().InvokeMember(name, flags, null, target, args); + } + catch (COMException ex) when ( + (ex.HResult == RPC_E_CALL_REJECTED || ex.HResult == RPC_E_SERVERCALL_RETRYLATER) && + attempt < MaxRetries) + { + attempt++; + Thread.Sleep(RetryDelayMs); + } + } + } + + public object EnsureAcadRunning() + { + if (_acadApp != null) + { + try + { + // Kiem tra COM object con song khong (AutoCAD co the da bi dong tay) + Invoke(_acadApp, "Visible", BindingFlags.GetProperty, null); + return _acadApp; + } + catch + { + _acadApp = null; + } + } + + _acadApp = AcadComUtils.TryGetAnyRunningAcad(); + if (_acadApp == null) + _acadApp = AcadComUtils.CreateLatestAutoCADInstance(); + + AcadComUtils.SetVisible(_acadApp, true); + return _acadApp; + } + + // Mo file (hoac kich hoat lai neu da mo san) va tra ve AcadDocument (COM object) + public object OpenDocument(string dwgPath) + { + object acadApp = EnsureAcadRunning(); + + object documents = Invoke(acadApp, "Documents", BindingFlags.GetProperty, null); + + int count = (int)Invoke(documents, "Count", BindingFlags.GetProperty, null); + + for (int i = 0; i < count; i++) + { + object existing = Invoke(documents, "Item", BindingFlags.InvokeMethod, new object[] { i }); + + string fullName = (string)Invoke(existing, "FullName", BindingFlags.GetProperty, null); + + if (string.Equals(fullName, dwgPath, StringComparison.OrdinalIgnoreCase)) + { + ActivateDocument(existing); + return existing; + } + } + + object opened = Invoke(documents, "Open", BindingFlags.InvokeMethod, + new object[] { dwgPath, false, " " }); + + // Cho AutoCAD ve xong ban ve vua mo truoc khi goi tiep lenh COM khac, + // giam kha nang gap "application is busy" ngay sau khi Open. + Thread.Sleep(800); + + ActivateDocument(opened); + return opened; + } + + public void ActivateDocument(object document) + { + try + { + Invoke(document, "Activate", BindingFlags.InvokeMethod, null); + } + catch { } + + try + { + AcadComUtils.SetVisible(_acadApp, true); + } + catch { } + } + + // Chuyen sang tab Model hoac Layout co ten tuong ung + public void SetActiveTab(object document, string tabName) + { + if (string.IsNullOrEmpty(tabName) || + string.Equals(tabName, "Model", StringComparison.OrdinalIgnoreCase)) + return; + + try + { + object layouts = Invoke(document, "Layouts", BindingFlags.GetProperty, null); + + object layout = Invoke(layouts, "Item", BindingFlags.InvokeMethod, new object[] { tabName }); + + Invoke(document, "ActiveLayout", BindingFlags.SetProperty, new object[] { layout }); + + // Cho AutoCAD chuyen tab xong truoc khi goi tiep lenh COM khac + Thread.Sleep(300); + } + catch + { + // Khong tim thay layout (co the da bi doi ten) - giu nguyen tab hien tai + } + } + + public void SetVariable(object document, string varName, object value) + { + try + { + Invoke(document, "SetVariable", BindingFlags.InvokeMethod, new object[] { varName, value }); + } + catch { } + } + + public int GetIntVariable(object document, string varName, int defaultValue) + { + try + { + object result = Invoke(document, "GetVariable", BindingFlags.InvokeMethod, new object[] { varName }); + return Convert.ToInt32(result); + } + catch + { + return defaultValue; + } + } + + public string GetStringVariable(object document, string varName, string defaultValue) + { + try + { + object result = Invoke(document, "GetVariable", BindingFlags.InvokeMethod, new object[] { varName }); + return result?.ToString() ?? defaultValue; + } + catch + { + return defaultValue; + } + } + + public void SendCommand(object document, string commandText) + { + Invoke(document, "SendCommand", BindingFlags.InvokeMethod, new object[] { commandText }); + } + + public void CloseDocument(object document) + { + try + { + Invoke(document, "Close", BindingFlags.InvokeMethod, new object[] { false, "" }); + } + catch { } + } + } +} diff --git a/DwgExportManager/App.xaml b/DwgExportManager/App.xaml new file mode 100644 index 0000000..9cfaf80 --- /dev/null +++ b/DwgExportManager/App.xaml @@ -0,0 +1,7 @@ + + + + diff --git a/DwgExportManager/App.xaml.cs b/DwgExportManager/App.xaml.cs new file mode 100644 index 0000000..b0e7618 --- /dev/null +++ b/DwgExportManager/App.xaml.cs @@ -0,0 +1,8 @@ +using System.Windows; + +namespace DwgExportManager +{ + public partial class App : Application + { + } +} diff --git a/DwgExportManager/DwgExportManager.csproj b/DwgExportManager/DwgExportManager.csproj new file mode 100644 index 0000000..b7f6609 --- /dev/null +++ b/DwgExportManager/DwgExportManager.csproj @@ -0,0 +1,68 @@ + + + + net8.0-windows + true + true + WinExe + DwgExportManager + DwgExportManager + latest + disable + disable + AnyCPU;x64 + PerMonitorV2 + true + + + + + DwgExportManager + + DwgExportManager + Copyright © 2026 + 1.0.0.0 + 1.0.0.0 + + + + DEBUG;TRACE + bin\Debug\ + full + true + + + + TRACE + $(SolutionDir)Binaries\ + pdbonly + true + + + + DEBUG;TRACE + $(SolutionDir)Binaries\x64\Debug\ + full + true + x64 + + + + TRACE + $(SolutionDir)Binaries\x64\Release\ + pdbonly + true + x64 + + + + + + + + + + + + + diff --git a/DwgExportManager/ExportEngine.cs b/DwgExportManager/ExportEngine.cs new file mode 100644 index 0000000..815ddd5 --- /dev/null +++ b/DwgExportManager/ExportEngine.cs @@ -0,0 +1,222 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Threading; + +namespace DwgExportManager +{ + public enum ExportFormat + { + PdfOnly, + PngOnly, + PdfAndPng + } + + // Nghiep vu xuat hang loat: voi moi file, mo trong AutoCAD, chuyen den tab + // (Model/Layout) da chon tren luoi, plot ra PDF va/hoac PNG, roi dong lai + // khong luu. Tham so plot (kho A1 / Extents+Fit+Center) giu nguyen theo 2 script + // PlotAllLayoutsToPDF_A1.scr va PlotAllLayoutsToPNG.scr da tao truoc do, chi khac + // la chi xuat DUY NHAT tab nguoi dung chon cho tung file. Rieng kho anh PNG co the + // chon khac nhau cho tung dong (pngPaperSize), mac dinh DefaultPngPaperSize neu bo trong. + // File xuat ra dat CUNG TEN voi file DWG goc, luu trong thu muc con "danxuat" + // nam ngay trong thu muc goc (thu muc chua cac file DWG). + public class ExportEngine + { + // Ten thu muc con (nam ngay trong thu muc goc chua cac file DWG) de chua ket qua xuat + private const string OutputFolderName = "danxuat"; + + private const string PdfDevice = "DWG To PDF.pc3"; + private const string PdfPaperSize = "ISO A1 (594.00 x 841.00 MM)"; + + private const string PngDevice = "PublishToWeb PNG.pc3"; + private const string DefaultPngPaperSize = "1600 x 1280 Pixels"; + + private readonly AcadSession _session; + + public ExportEngine(AcadSession session) + { + _session = session; + } + + // Tra ve true neu xuat thanh cong, false neu co loi (se ghi vao out error) + public bool ExportFile( + string dwgPath, string tabName, ExportFormat format, + Func isStopRequested, ManualResetEventSlim pauseEvent, + out string error, string pngPaperSize = null) + { + error = null; + object document = null; + try + { + pauseEvent.Wait(); + if (isStopRequested()) return false; + + document = _session.OpenDocument(dwgPath); + _session.SetActiveTab(document, tabName); + + _session.SetVariable(document, "FILEDIA", 0); + _session.SetVariable(document, "BACKGROUNDPLOT", 0); + + // Bat log lenh cua AutoCAD de doc lai noi dung khi -PLOT that bai + // (vd sai ten kho giay/thiet bi) - SendCommand khong nem loi .NET + // khi AutoCAD tu choi lenh, nen phai doc log moi biet ly do that. + _session.SetVariable(document, "LOGFILEMODE", 1); + string logFile = _session.GetStringVariable(document, "LOGFILENAME", null); + + string rootFolder = Path.GetDirectoryName(dwgPath); + string outputFolder = Path.Combine(rootFolder, OutputFolderName); + Directory.CreateDirectory(outputFolder); + + string baseName = Path.GetFileNameWithoutExtension(dwgPath); + bool isModel = string.Equals(tabName, "Model", StringComparison.OrdinalIgnoreCase); + string layoutArg = isModel ? "Model" : tabName; + + var failures = new List(); + + if (format == ExportFormat.PdfOnly || format == ExportFormat.PdfAndPng) + { + pauseEvent.Wait(); + if (isStopRequested()) return false; + + string pdfPath = Path.Combine(outputFolder, baseName + ".pdf"); + long logPos = GetLogLength(logFile); + Plot(document, layoutArg, PdfDevice, PdfPaperSize, pdfPath); + WaitForIdle(document, 120_000); + + if (!File.Exists(pdfPath)) + failures.Add(BuildPlotFailureMessage("PDF", PdfDevice, PdfPaperSize, logFile, logPos)); + } + + if (format == ExportFormat.PngOnly || format == ExportFormat.PdfAndPng) + { + pauseEvent.Wait(); + if (isStopRequested()) return false; + + string effectivePngPaperSize = string.IsNullOrWhiteSpace(pngPaperSize) + ? DefaultPngPaperSize : pngPaperSize; + + string pngPath = Path.Combine(outputFolder, baseName + ".png"); + long logPos = GetLogLength(logFile); + Plot(document, layoutArg, PngDevice, effectivePngPaperSize, pngPath); + WaitForIdle(document, 120_000); + + if (!File.Exists(pngPath)) + failures.Add(BuildPlotFailureMessage("PNG", PngDevice, effectivePngPaperSize, logFile, logPos)); + } + + _session.SetVariable(document, "FILEDIA", 1); + + if (failures.Count > 0) + { + error = string.Join(" | ", failures); + return false; + } + + return true; + } + catch (Exception ex) + { + error = ex.Message; + return false; + } + finally + { + if (document != null) + _session.CloseDocument(document); + } + } + + private static long GetLogLength(string logFile) + { + try + { + if (string.IsNullOrEmpty(logFile) || !File.Exists(logFile)) + return 0; + return new FileInfo(logFile).Length; + } + catch + { + return 0; + } + } + + // Doc phan noi dung MOI duoc ghi vao log lenh cua AutoCAD (tinh tu logPos) + // de biet AutoCAD tra loi gi cho lenh -PLOT vua gui (vd bao khong tim + // thay kho giay/thiet bi). + private static string ReadLogTail(string logFile, long logPos, int maxChars = 600) + { + try + { + if (string.IsNullOrEmpty(logFile) || !File.Exists(logFile)) + return null; + + using (var fs = new FileStream(logFile, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) + { + if (logPos > fs.Length) logPos = 0; + fs.Seek(logPos, SeekOrigin.Begin); + using (var sr = new StreamReader(fs)) + { + string text = sr.ReadToEnd().Trim(); + if (text.Length > maxChars) + text = text.Substring(text.Length - maxChars); + return text; + } + } + } + catch + { + return null; + } + } + + private static string BuildPlotFailureMessage( + string formatName, string device, string paperSize, string logFile, long logPos) + { + string msg = $"Không tạo được {formatName} (kiểm tra thiết bị '{device}' / khổ giấy '{paperSize}' có tồn tại trên máy này không)"; + string tail = ReadLogTail(logFile, logPos); + if (!string.IsNullOrEmpty(tail)) + msg += $" — AutoCAD trả lời: {tail}"; + return msg; + } + + private void Plot(object document, string layoutArg, string device, string paperSize, string outFile) + { + string cmd = + "_.-PLOT\n" + + "Yes\n" + // Detailed plot configuration? Yes + layoutArg + "\n" + // Layout can plot + device + "\n" + // Output device + paperSize + "\n" + // Paper size + "Millimeters\n" + // Paper units + "Landscape\n" + // Orientation + "No\n" + // Plot upside down? + "Extents\n" + // Plot area + "Fit\n" + // Plot scale + "Center\n" + // Plot offset + "Yes\n" + // Plot with plot styles + ".\n" + // Giu nguyen bang plot style hien tai + "Yes\n" + // Plot with lineweights + "\n" + // Shade plot setting mac dinh + outFile + "\n" + // Ten file xuat ra + "No\n" + // Save changes to page setup? No + "Yes\n"; // Proceed with plot + + _session.SendCommand(document, cmd); + Thread.Sleep(1000); + } + + // Cho AutoCAD xu ly xong lenh plot (CMDACTIVE == 0) truoc khi lam tiep, + // toi da timeoutMs, tranh dong file khi PDF/PNG chua ghi xong. + private void WaitForIdle(object document, int timeoutMs) + { + DateTime end = DateTime.UtcNow.AddMilliseconds(timeoutMs); + while (DateTime.UtcNow < end) + { + int cmdActive = _session.GetIntVariable(document, "CMDACTIVE", 0); + if (cmdActive == 0) + return; + Thread.Sleep(300); + } + } + } +} diff --git a/DwgExportManager/LayoutReader.cs b/DwgExportManager/LayoutReader.cs new file mode 100644 index 0000000..f72ea49 --- /dev/null +++ b/DwgExportManager/LayoutReader.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; +using System.IO; +using ACadSharp; +using ACadSharp.IO; + +namespace DwgExportManager +{ + // Doc nhanh danh sach Layout (khong tinh Model) tu file DWG/DXF bang ACadSharp, + // KHONG can mo AutoCAD - dung de do vao ComboBox "Model/Layout" tren luoi. + public static class LayoutReader + { + public static List ReadLayoutNames(string filePath) + { + var names = new List(); + try + { + string ext = Path.GetExtension(filePath).ToLowerInvariant(); + + CadDocument doc = ext == ".dxf" + ? DxfReader.Read(filePath) + : DwgReader.Read(filePath); + + if (doc?.Layouts != null) + { + foreach (var layout in doc.Layouts) + { + string name = layout?.Name; + if (!string.IsNullOrEmpty(name) && + !string.Equals(name, "Model", StringComparison.OrdinalIgnoreCase)) + { + names.Add(name); + } + } + } + } + catch + { + // File loi, ma hoa, hoac phien ban chua ho tro: bo qua, + // luoi se chi con lua chon "Model". + } + return names; + } + } +} diff --git a/DwgExportManager/MainWindow.xaml b/DwgExportManager/MainWindow.xaml new file mode 100644 index 0000000..b11004a --- /dev/null +++ b/DwgExportManager/MainWindow.xaml @@ -0,0 +1,133 @@ + + + + + 800 x 600 Pixels + 1024 x 768 Pixels + 1600 x 1280 Pixels + 2048 x 1536 Pixels + 3200 x 2400 Pixels + + + + + + + + + + + + + + + + + + +