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.