Monday, December 30, 2024

Clipping Blocks

Can we clip a block (BlockReference) by using API code, as we use "CLIP" command? A recent post in AutoCAD .NET discussion forum asked this question. For whatever reasons, the post stayed quite a while without response.

There are 2 built-in commands in AutoCAD - "XClip" and "Clip", they basically does the same thing under the hood with slight differences of command-line options. In this post, I'll focus on clipping single block (BlockReference).

While this would be my first time coding "Clipping", there was a couple of articles from the most famous AutoCAD .NET API blogger Kean Walmsley looong time ago (more than 12 years ago!):

Adding a 2D spatial filter to perform a simple xclip on an external reference in AutoCAD using .NET

Querying for XCLIP information inside AutoCAD using .NET

So, my code here is mostly simple copy/paste of Kean's, with some necessary updates/changes. The code includes 2 CommandMethods: "ClipBlk" and "RemoveClip":

using Autodesk.AutoCAD.DatabaseServices.Filters;
using DriveCadWithCode2025.AutoCADUtilities;
 
[assemblyCommandClass(typeof(AcadMicsTests.MyCommands))]
 
namespace AcadMicsTests
{
    public class MyCommands 
    {
        private const string FILTER_DICT_NAME = "ACAD_FILTER";
        private const string SPATIAL_DICT_NAME = "SPATIAL";
 
        [CommandMethod("ClipBlk")]
        public static void ClipBlockReference()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var res = ed.GetEntity("\nSelect block reference to clip:");
            if (res.Status != PromptStatus.OK) return;
 
            if (RemoveClipFromBlockReference(res.ObjectId))
            {
                ed.Regen();
            }
 
            var opt = new PromptKeywordOptions("\nChoose clip boundary:");
            opt.AppendKeywordsToMessage = true;
            opt.Keywords.Add("Rectangle");
            opt.Keywords.Add("Polygon");
            opt.Keywords.Default = "Rectangle";
            var kres=ed.GetKeywords(opt);
            if (kres.Status != PromptStatus.OK) return;
 
            List<Point3d>? points = null;
            if (kres.StringResult == "Rectangle")
            {
                if (SelectClipWindow(edout Point3d pt1out Point3d pt2))
                {
                    points=new List<Point3d> { pt1pt2 };
                }
            }
            else
            {
                if (SelectClipPolygon(edout List<Point3d>? pts))
                {
                    if (pts != null)
                    {
                        points = pts;
                    }
                }
            }
 
            if (points == nullreturn;
 
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                var blk = (BlockReference)tran.GetObject(res.ObjectId, OpenMode.ForRead);
                if (blk.ExtensionDictionary.IsNull)
                {
                    blk.UpgradeOpen();
                    blk.CreateExtensionDictionary();
                }
 
                var extDict = (DBDictionary)tran.GetObject(
                    blk.ExtensionDictionary, OpenMode.ForWrite);
                DBDictionary filterDict;
                if (!extDict.Contains(FILTER_DICT_NAME))
                {
                    filterDict = new DBDictionary();
                    extDict.SetAt(FILTER_DICT_NAME, filterDict);
                    tran.AddNewlyCreatedDBObject(filterDicttrue);
                }
                else
                {
                    filterDict=(DBDictionary)tran.GetObject(
                        extDict.GetAt(FILTER_DICT_NAME), OpenMode.ForWrite);
                }
 
                if (filterDict.Contains(SPATIAL_DICT_NAME))
                {
                    var id = filterDict.GetAt(SPATIAL_DICT_NAME);
                    filterDict.Remove(SPATIAL_DICT_NAME);
                    var spFilter = tran.GetObject(idOpenMode.ForWrite);
                    spFilter.Erase();
                }
 
                Point2dCollection clipPoints = GetPolygonBoundary(pointsblk.BlockTransform);
 
                var definition = new SpatialFilterDefinition(
                    clipPointsVector3d.ZAxis, 0.0,
                    double.PositiveInfinity, double.NegativeInfinity, true);
                var filter = new SpatialFilter();
                filter.Definition = definition;
                filterDict.SetAt(SPATIAL_DICT_NAME, filter);
                tran.AddNewlyCreatedDBObject(filtertrue);
 
                tran.Commit();
            }
 
            ed.Regen();
            ed.UpdateScreen();
        }
 
        [CommandMethod("RemoveClip")]
        public static void RemoveClip()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var res = ed.GetEntity("\nSelect a clipped block reference:");
            if (res.Status != PromptStatus.OK) return;
 
            if (RemoveClipFromBlockReference(res.ObjectId))
            {
                ed.Regen();
            }
        }
 
        private static bool SelectClipWindow(Editor edout Point3d pt1out Point3d pt2)
        {
            pt1 = Point3d.Origin;
            pt2 = Point3d.Origin;
 
            var res1 = ed.GetPoint("\nSelect lower-left corner:");
            if (res1.Status == PromptStatus.OK)
            {
                var res2 = ed.GetCorner("\nSelect upper-right corner:"res1.Value);
                if (res2.Status == PromptStatus.OK)
                {
                    pt1 = res1.Value;
                    pt2 = res2.Value;
                    return true;
                }
            }
 
            return false;
        }
 
        private static bool SelectClipPolygon(Editor edout List<Point3d>? points)
        {
            var picker = new PolygonAreaPicker(ed, 1, 25);
            if (picker.PickPolygon())
            {
                points = picker.BoundaryPoints;
                return true;
            }
            else
            {
                points = null;
                return false;
            }
        }
 
        private static bool RemoveClipFromBlockReference(ObjectId blkId)
        {
            var removed = false;
            var db = blkId.Database;
            using (var tran=db.TransactionManager.StartTransaction())
            {
                var blk = tran.GetObject(blkIdOpenMode.ForRead);
                if (!blk.ExtensionDictionary.IsNull)
                {
                    var extDict = (DBDictionary)tran.GetObject(
                        blk.ExtensionDictionary, OpenMode.ForRead);
                    if (extDict.Contains(FILTER_DICT_NAME))
                    {
                        var filterDict = (DBDictionary)tran.GetObject(
                            extDict.GetAt(FILTER_DICT_NAME), OpenMode.ForWrite);
                        if (filterDict.Contains(SPATIAL_DICT_NAME))
                        {
                            var filter = tran.GetObject(
                                filterDict.GetAt(SPATIAL_DICT_NAME), OpenMode.ForWrite);
                            filter.Erase();
                            filterDict.Remove(SPATIAL_DICT_NAME);
                        }
 
                        extDict.UpgradeOpen();
                        extDict.Remove(FILTER_DICT_NAME);
 
                        removed = true;
                    }
                }
 
                tran.Commit();
            }
            return removed;
        }
 
        private static Point2dCollection GetPolygonBoundary(
            IEnumerable<Point3dpointsMatrix3d blkTransform)
        {
            var pts = points
                .Select(pt => pt.TransformBy(blkTransform.Inverse()))
                .Select(pt => new Point2d(pt.X, pt.Y));
            Point2dCollection pt2ds = new Point2dCollection(pts.ToArray());
            return pt2ds;
        }
    }
}

The the private method SelectClipPolygon(), I used a class PolygonAreaPicker for selecting a polygon area. I posted the code of this class in my previously posted article here.

See the video below showing how the code works:



Here are somethings to point out:

1. While the clip boundary seems being created correctly, the boundary created by the code has a noticeable difference from the one created by "CLIP" command: the clip boundary generated by "CLIP" command has a grip, similar to the ones used for control dynamic properties of a dynamic BlockReference. By clicking this grip, the clipping can be flipped to show either external clipping, or internal clipping. However, the clip boundary created by my code does not have this "clipping-flip" grip. Obviously, command "CLIP" does more than just clipping the target BlockReference. Fortunately, the SpatialFilter has a read-write property "Inverted" that we can set by code. Maybe, it is possible to define a grip overrule to mimic the clip boundary created by "CLIP" command. I am not going to dig into it for now.

2. When a clip is applied to a BlockReference, the SpatialFilter that creates the clipping effect is persisted as ExtensionDictionary of the Blockreference. That is, we can examine a BlockReference's ExtensionDictionary to see if it contains a "ACAD_FILTER" dictionary and a DBDictionaryEntry keyed as "SPATIAL". So, theoretically, we can open the existing SpatialFilter object in a Transaction with the SpatialFilter's ObjectId stored in the DBDictionaryEntry. Then I thought, if I want to change the boundary of an existing clipped BlockReference, I could simply open the SpatialFilter in transaction and define a new SpatialFilterDefinition and set it to the SpatialFilter.Definition property, which is set/write-able. Well, as it turns out: yes, I can open the SpatialFilter object for read/write with the ObjectId from the ExtensionDictionary, but it seems the opened SpatialFilter object is read-only, that is, we can look up its "Definition" property to get the boundary's points; as soon as we try to assign the property a newly defined SpatialFilterDefinition, AutoCAD crashes. Therefore, if I want to updated an existing BlockReference's clipping boundary, I need to first delete the attached "ACAD_FILTER" ExtensionDictionary, and then create a new one with a newly defined SpatialFilterDefinition.

3. As described in 2, when updating an existing BlockReference clipping boundary, we need to remove existing one and then create a new one. I found out the 2 steps have to be done in separate Transactions. Or, the clipped BlockReference may not displayed properly.

All in all, the code of the 2 commands showed here basically demonstrates how to use spatial filter API to clip a BlockReference.











Sunday, December 29, 2024

Manually Selecting Points for a Polygon

It is a quite common coding task for AutoCAD programming that users are asked to indicate an area/polygon, or its boundary, by selecting a series of points. So, the coding practice is to allow the user to select points in a loop until enough points are selected or the loop is cancelled. During the point picking, it is important to provide the user proper visual feedback to indicate the area/boundary to be formed. Surely, we can use code to mimic "LINE"/"POLYLINE" command to let the user to continuously draw Line/Polyline as needed. However, we often only need to obtain a collection of points that can form an area/boundary, and do not actually want Line/Polyline entities are actually created. Hence the topic of this article: providing visual help while the points being picked without adding entities into the drawing.

At first, I did a version of it by simply handling Editor_PointMonitor event when Editor.GetPoint() is called, so the area/boundary is shown as Transient graphic. The code is rather simple and straightforward.

Then, I though it would make the visual hint area more appealing to the user's eyes if the visual hint area could be filled with background of certain transparency. But using Transient graphics in Editor.PointMonitor event handler would not let me draw such area easily: I may have to dynamically generate a Hatch/MPolygon/Region based on the dynamically drawn polyline. So, I decided to go a different route by using a DrawJig, which provides me a WorldDraw object in the overridden WorldDraw() method, which I can use to fill the background of the area.

After I did the 2 polygon picking classes, I thought why not have the code of the both classes posted here? Then, since both the classes have similar portions of code, so I create a base class and derive the 2 point-picking classes from it.

Here is the base/abreact class:

namespace DriveCadWithCode2025.AutoCADUtilities
{
    public abstract class PolygonPointsPicker
    {
        private readonly Editor _ed;
        private readonly List<Point3d> _points = new List<Point3d>();
 
        public PolygonPointsPicker(Editor edint boundaryColorIndex = 2)
        {
            _ed= ed;
            BoundaryColorIndex = boundaryColorIndex;
        }
 
        public List<Point3d> BoundaryPoints => _points;
        protected Editor ThisEditor => _ed;
        protected Polyline? Boundary { getset; } = null;
 
        protected int BoundaryColorIndex { get; }
 
        public virtual bool PickPolygon()
        {
            if (!PickFirstTwoPoints())
            {
                _ed.WriteMessage("\n*Cancel*\n");
                return false;
            }
            else
            {
                return true;
            }
        }
 
        protected Polyline CreateGhostPolyline()
        {
            var poly = new Polyline();
            for (int i = 0; i < BoundaryPoints.Count; i++)
            {
                poly.AddVertexAt(inew Point2d(
                    BoundaryPoints[i].X, BoundaryPoints[i].Y), 0.0, 0.0, 0.0);
            }
 
            poly.AddVertexAt(BoundaryPoints.Count, new Point2d(
                BoundaryPoints[BoundaryPoints.Count - 1].X, 
                BoundaryPoints[BoundaryPoints.Count - 1].Y), 0.0, 0.0, 0.0);
 
            poly.ColorIndex = BoundaryColorIndex;
 
            if (poly.NumberOfVertices > 2)
            {
                poly.Closed = true;
            }
 
            return poly;
        }
 
        private bool PickFirstTwoPoints()
        {
            var res = _ed.GetPoint("Select polygon's first point:");
            if (res.Status != PromptStatus.OK) return false;
 
            _points.Add(res.Value);
 
            var opt = new PromptPointOptions("\nSelect polygon's next point:");
            opt.UseBasePoint = true;
            opt.BasePoint = res.Value;
            opt.UseDashedLine = true;
            res = _ed.GetPoint(opt);
            if (res.Status != PromptStatus.OK) return false;
 
            _points.Add(res.Value);
            return true;
        }
    }
}

Here is the first point-picking class, which uses Transient to draw a closed polyline by handling Editor.PointMonitor event:

using Autodesk.AutoCAD.GraphicsInterface;
 
namespace DriveCadWithCode2025.AutoCADUtilities
{
    public class PolygonBoundaryPicker : PolygonPointsPicker
    {
        private readonly TransientManager _tsMng = 
            TransientManager.CurrentTransientManager;
 
        public PolygonBoundaryPicker(Editor edint boundaryColorIndex = 2) : 
            base(edboundaryColorIndex)
        { 
 
        }
 
        public override bool PickPolygon()
        {
            if(!base.PickPolygon()) return false;
 
            var ok = false;
            try
            {
                ThisEditor.PointMonitor += Editor_PointMonitor;
                while(true)
                {
                    Boundary = CreateGhostPolyline();
                    _tsMng.AddTransient(
                        Boundary, 
                        TransientDrawingMode.DirectTopmost, 
                        128, 
                        new IntegerCollection());
 
                    var opt = new PromptPointOptions("\nSelect polygon's next point:");
                    if (BoundaryPoints.Count>=3)
                    {
                        opt.AllowNone = true;
                        opt.Keywords.Add("Done");
                        opt.Keywords.Default = "Done";
                    }
                    var pRes = ThisEditor.GetPoint(opt);
                    if (pRes.Status== PromptStatus.OK)
                    {
                        BoundaryPoints.Add(pRes.Value);
                        ClearGhostPolyline();
                    }
                    else if (pRes.Status == PromptStatus.Keyword)
                    {
                        ok = true;
                        break;
                    }
                    else
                    {
                        break;
                    }
                }
            }
            finally
            {
                ThisEditor.PointMonitor -= Editor_PointMonitor;
                ClearGhostPolyline();
            }
 
            return ok;
        }
 
        private void Editor_PointMonitor(object senderPointMonitorEventArgs e)
        {
            if (Boundary == null || Boundary.NumberOfVertices < 3) return;
 
            var pt=new Point2d(e.Context.RawPoint.X, e.Context.RawPoint.Y);
            Boundary.SetPointAt(Boundary.NumberOfVertices-1, pt);
            _tsMng.UpdateTransient(Boundary, new IntegerCollection());
        }
 
        #region private methods
 
        private void ClearGhostPolyline()
        {
            if (Boundary != null)
            {
                _tsMng.EraseTransient(Boundary, new IntegerCollection());
                Boundary.Dispose();
                Boundary = null;
            }
        }
 
        #endregion
    }
}

The second point-picking class, as aforementioned, contains a private custom DrawJig class used for drawing the area with background fill. Here is its code:

using Autodesk.AutoCAD.GraphicsInterface;
using CadDb = Autodesk.AutoCAD.DatabaseServices;
 
namespace DriveCadWithCode2025.AutoCADUtilities
{
    public class PolygonAreaPicker : PolygonPointsPicker
    {
        private class PolygonPointJig : DrawJig
        {
            private CadDb.Polyline _polyline;
            private Point3d _prevPoint;
            private Point3d _currPoint;
            private readonly int _transparencyPercentile;
            public PolygonPointJig(CadDb.Polyline polylineint backgroundTransparency = 50)
            {
                _polyline = polyline;
                _transparencyPercentile = backgroundTransparency;
                var lastPt = _polyline.GetPoint3dAt(_polyline.NumberOfVertices - 1);
                _prevPoint = lastPt;
                _currPoint = lastPt;
            }
 
            public Point3d CurrentPoint => _currPoint;
 
            protected override SamplerStatus Sampler(JigPrompts prompts)
            {
                var opt = new JigPromptPointOptions("\nSelect polygon's next point:");
                if (_polyline.NumberOfVertices > 3)
                {
                    opt.UserInputControls = UserInputControls.NullResponseAccepted;
                    opt.Keywords.Add("Done");
                    opt.Keywords.Default = "Done";
                }
 
                var res = prompts.AcquirePoint(opt);
                if (res.Status == PromptStatus.OK)
                {
                    if (res.Value.IsEqualTo(_prevPoint))
                    {
                        return SamplerStatus.NoChange;
                    }
                    else
                    {
                        _prevPoint = _currPoint;
                        _currPoint = res.Value;
                        return SamplerStatus.OK;
                    }
                }
                else if (res.Status == PromptStatus.Keyword)
                {
                    return SamplerStatus.OK;
                }
                else
                {
                    return SamplerStatus.Cancel;
                }
            }
 
            protected override bool WorldDraw(WorldDraw draw)
            {
                draw.Geometry.Draw(_polyline);
 
                var pt = new Point2d(_currPoint.X, _currPoint.Y);
                _polyline.SetPointAt(_polyline.NumberOfVertices - 1, pt);
 
                Point3dCollection pts = new Point3dCollection();
                for (int i = 0; i < _polyline.NumberOfVertices; i++)
                {
                    pts.Add(_polyline.GetPoint3dAt(i));
                }
 
                draw.SubEntityTraits.FillType = FillType.FillAlways;
                draw.SubEntityTraits.Transparency =
                    AcadGenericUtilities.GetTransparencyByPercentage(_transparencyPercentile);
                draw.SubEntityTraits.Color = 2;
                draw.Geometry.Polygon(pts);
 
                return true;
            }
        }
 
        private int _transparencyPercentile = 50;
 
        public PolygonAreaPicker(
            Editor ed, 
            int boundaryColorIndex = 2,  
            int backgroundTransparency=50) : base(edboundaryColorIndex)
        {
            _transparencyPercentile = backgroundTransparency;
        }
 
        public override bool PickPolygon()
        {
            if (!base.PickPolygon()) return false;
 
            var ok = false;
 
            try
            {
                while(true)
                {
                    Boundary = CreateGhostPolyline();
                    var jig = new PolygonPointJig(Boundary);
                    var jigRes = ThisEditor.Drag(jig);
                    if (jigRes.Status== PromptStatus.OK)
                    {
                        BoundaryPoints.Add(jig.CurrentPoint);
                        Boundary.Dispose();
                        Boundary = null;
                        jig = null;
                    }
                    else if (jigRes.Status == PromptStatus.Keyword)
                    {
                        ok = true;
                        break;
                    }
                    else
                    {
                        break;
                    }
                }
            }
            finally
            {
                if (Boundary != null)
                {
                    Boundary.Dispose();
                }
            }
 
            return ok;
        }
    }
}

In the provate jig class's WorldDraw() method, I used a static method from a referenced AutoCAD utility class for getting Transparency value in the range of 0 to 100, where 0 means no transparency and 100 for total transparency:

public static Transparency GetTransparencyByPercentage(int transparencyPercent)
{
    if (transparencyPercent > 90) transparencyPercent = 90;
    var alpha = Convert.ToInt32(Math.Floor(255 * ((100 - transparencyPercent) / 100.0)));
 
    return new Transparency((byte)alpha);
}

Here is the CommandMethod that runs the 2 point-picking classes:

[CommandMethod("PickPolygon")]
public static void TestPolygonPicker()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    var opt = new PromptKeywordOptions("Use Boundary Picker or Area Picker:");
    opt.AppendKeywordsToMessage = true;
    opt.Keywords.Add("Boundary");
    opt.Keywords.Add("Area");
    var res = ed.GetKeywords(opt);
    if (res.Status != PromptStatus.OK) return;
 
    try
    {
        PolygonPointsPicker picker;
 
        if (res.StringResult == "Boundary")
        {
            picker = new PolygonBoundaryPicker(ed, 1);
        }
        else
        {
            picker = new PolygonAreaPicker(ed, 1, 25);
        }
 
        if (picker.PickPolygon())
        {
            var points = picker.BoundaryPoints;
            ed.WriteMessage($"\nPicked points: {points.Count}");
        }
    }
    catch(System.Exception ex)
    {
        CadApp.ShowAlertDialog($"Error:\n{ex.Message}");
    }
}

The videos below show the code in action:



Saturday, December 21, 2024

Using External Text Editor to Modify DBText/MText

When editing DBText or MText, AutoCAD provides "Inplace Text Editor" as default way to edit the text string of DBText/MText entities. AutoCAD also allow external text editor to be used, which can be set up via "Options" dialog box and the option is saved as the system variable "MTEXTED". As the name of this system variable suggested, this text editing option only applies to MText. 

As AutoCAD programmers, we may want to as user to edit DBText/MText entities during our code execution. That is, our code triggers DBText/MText editing, which holds the code execution until user completed the editing. So, how do we do that?

If we want the user to do the DBText/MText editing with AutoCAD built-in InplaceTextEditor, it is rather simple: the InplaceTextEditor class has overloaded methods Invoke() to start the editing inside AutoCAD editor, and hold the the code execution until the user completes the editing. Following code shows how to do it:

[CommandMethod("EditTextInternal")]
public static void EditTextWithInternalEditor()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    var opt = new PromptEntityOptions("\nSelect TEXT/MTEXT:");
    opt.SetRejectMessage("\nInvalid selection: must be TEXT?MTEXT.");
    opt.AddAllowedClass(typeof(DBText), true);
    opt.AddAllowedClass(typeof(MText), true);
    var res = ed.GetEntity(opt);
    if (res.Status != PromptStatus.OK) return;
 
    using (var tran = dwg.TransactionManager.StartTransaction())
    {
        var textEnt = tran.GetObject(res.ObjectId, OpenMode.ForRead);
        if (textEnt is DBText txt)
        {
            ObjectId[] ids = new ObjectId[] { };
            InplaceTextEditor.Invoke(txt,ref ids);
        }
        else if (textEnt is MText mtxt)
        {
            InplaceTextEditor.Invoke(mtxtnew InplaceTextEditorSettings());
        }
 
        tran.Commit();
    }
}

To use external text editor, such as NotePad, which is just a stand-alone EXE, we need to let our code execution start the external EXE process, so that the external EXE can pick up the text string we want to be edited. The code execution in AutoCAD then is put on hold until the user closes the external EXE process. When the external EXE is closed, our code execution in AutoCAD should get notified and then pick up the text string modified by the external EXE. The whole thing sounds quite complicated, but it is actually quite easy to do. There is a recent post in Autodesk's .NET API forum asking a question on this topic. The OP almost gets it done, except missing the code of holding up the code execution to wait for the external EXE to end. Here is the code of doing it:

[CommandMethod("EditTextExternal")]
public static void EditTextWithExternalEditor()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    var opt = new PromptEntityOptions("\nSelect TEXT/MTEXT:");
    opt.SetRejectMessage("\nInvalid selection: must be TEXT?MTEXT.");
    opt.AddAllowedClass(typeof(DBText), true);
    opt.AddAllowedClass(typeof(MText), true);
    var res = ed.GetEntity(opt);
    if (res.Status != PromptStatus.OK) return;
 
    using (var tran = dwg.TransactionManager.StartTransaction())
    {
        var textEnt = tran.GetObject(res.ObjectId, OpenMode.ForRead);
        string text = "";
        bool isMText = false;
        if (textEnt is DBText txt)
        {
            text = txt.TextString;
        }
        else if (textEnt is MText mtxt)
        {
            text = mtxt.Contents;
            isMText = true;
        }
        if (!string.IsNullOrEmpty(text))
        {
            var editedText = EditTextWithNotePadPlus(textisMText);
            if (editedText != text)
            {
                textEnt.UpgradeOpen();
                if (textEnt is DBText)
                {
                    ((DBText)textEnt).TextString=editedText;
                }
                else if (textEnt is MText)
                {
                    ((MText)textEnt).Contents=editedText;
                }
            }
        }
 
        tran.Commit();
    }
}
 
private static string EditTextWithNotePadPlus(string entityTextbool isMText)
{
    var tempFile=$"C:\\Temp\\{Guid.NewGuid()}.txt";
    string inputText;
    if (isMText)
    {
        inputText = entityText.Replace("\\P""\r\n");
    }
    else
    {
        inputText = entityText;
    }
    File.WriteAllText(tempFileinputText);
 
    var notePadPlus = @"C:\Program Files\Notepad++\notepad++.exe";
    string editedText = inputText;
    Process process = new Process();
    process.StartInfo.FileName = notePadPlus;
    process.StartInfo.Arguments = tempFile;
    process.EnableRaisingEvents = true;
    process.Exited += (oe) =>
    {
        editedText=File.ReadAllText(tempFile);
        File.Delete(tempFile);
    };
 
    process.Start();
    process.WaitForExit();
    if (!isMText)
    {
        return editedText.Replace("\n""").Replace("\r""");
    }
    else
    {
        return editedText.Replace("\r\n","\\P");
    }
}

As the code shows, the steps are:

1. Retrieve the text string from the DBText/MText entity for editing;

2. Save the retrieved text string to a temporary text file;

3. Create a Process object with the temporary data file path as the arguments, so that when the Process starts, it will read the text string for editing into its working editor;

4. Handle Process.Exited event, in which the text string being edited by the external EXE will be picked up by reading the temporary text file. Make sure to set Process.EnableRaiseEvents property to true.

5. Start the external EXE Process and call its WaitForExit() method, so that the code follows it will be not be executed until the external EXE Process exits;

6. Read the text value from the temporary file, and then delete the file;

7. Compare the text value read back from the file to the value placed into the file before external EXE processes starts. If the text value has been changed, go ahead to update the DBText/MText.

Some Points to Pay Attention:

1. When using code to trigger text editing, our code can decide to use either InplaceTextEditor or external editor regardless the system variable "MTEXTED". Of course, you can also choose which editor according to the "MTEXTED" system variable. However, AutoCAD's built-in behaviour for DBText editing is to always use InplaceTextEditor, while our code can use external editor to edit DBText. 

2. When using external editor, we may have to deal with some extra issues. For example, DBText is a single-line text, while almost all EXE editor would do multiple-line text editing. So, we have remove "NewLine" and "CarriageReturn" ("\n" and "\r") characters from the edited text string before it is updated to DBText. That is why AutoCAD always use InplaceTextEditor for DBText editing. For MText editing, we have to deal with all the special markups, if the external EXE editor doesn't do it for us (I do not know if there is one that can place MText' markups). In my code here, I did a simple handling on this by replacing "\r\n" and "\\P" accordingly, just for simplicity purpose.

Because of the point 2, I can hardly see there would be many cases where users want to use external EXE editor. But, if there is legitimate reasons to programmatically start external text editor, the code here shows it can be done easily.



Followers

About Me

My photo
After graduating from university, I worked as civil engineer for more than 10 years. It was AutoCAD use that led me to the path of computer programming. Although I now do more generic business software development, such as enterprise system, timesheet, billing, web services..., AutoCAD related programming is always interesting me and I still get AutoCAD programming tasks assigned to me from time to time. So, AutoCAD goes, I go.