Friday, November 30, 2018

Exporting Shape From AutoCAD Civil/Map, 2D or 3D?

It is very common practice when using AutoCAD Civil/Map that user needs to export AutoCAD geometries as Shape file (*.shp). AutoCAD Map provides built-in command "MapExport" for doing this. AutoCAD Map also comes with ObjectARX Map APIs that includes Autodesk.Gis.Map.ImportExport namespace, which allows the shape exporting to be done programmatically. I have a few custom applications written and used from many years since AutoCAD Map 2006, exporting 2D shape files. These applications had been worked as expected, until our recent upgrade from Civil2015 to Civil2018: suddenly these custom applications exported shape file in 3D, which is not we want (for regulatory data submission).

When using "MapExport" command to export shape file, user is given a chance to decide exporting shape as 2D or 3D, as picture below shows:


In our practice at work, when manually exporting shape, we never needed to click "Driver Options.." button to decide exporting to 2D or 3D. AutoCAD Map always defaults to 2D exporting. This is also the case for my Map API code, where I had never needed to have code to set exporting driver option for the object Autodesk.Gis.Map.ImportExport.Exporter, well, until we moved to Civil/Map 2018. However, the built-in command still always defaults to 2D export, even you re-run the command after the previous command where you chose to export to 3D.

So, I had to modify my code of more than 10 years of old to force 2D export, using the methods GetDriveOptions()/SetDriverOptions() of Autodesk.Gis.Map.ImportExport.Exporter class.

Due to the poor AutoCAD Map API documentation, I ran into a obstacle: what is the value for the method SetDriveOptions()'s argument of Autodesk.Gis.Map.ImportExport.NameValueCollection type, in order to export in 2D or 3D? I searched all over the Internet and came back empty-handed. But I eventually figured it out by saving an exporting profile (*.epf file), as shown in picture below:


*.epf file is actually an XML file. The "Driver Options" for 2D/3D exporting can be easily spotted by opening this *.epf file in NotePad, as the picture shows below:



With this information finally available, I went ahead modifying my shape exporting code like this (in red):

public class  MyShapeExporter
{
    private Exporter _exporter = null;
 
    public MyShapeExporter()
    {
 
    }
 
    public void ExportClosedPolylines(IEnumerable<ObjectId> entIds, string shapeFileName)
    {
        try
        {
            _exporter = HostMapApplicationServices.Application.Exporter;
            _exporter.Init("SHP", shapeFileName);
            _exporter.SetStorageOptions(StorageType.FileOneEntityType, GeometryType.Polygon, null);
            _exporter.ClosedPolylinesAsPolygons = true;
            _exporter.SetSelectionSet(new ObjectIdCollection(entIds.ToArray()));
            var options = _exporter.GetDriverOptions();
            var opt = new Autodesk.Gis.Map.Utilities.StringPair("FDO_SHAPE_DIMENSION", "2D");
            if (!options.Contains(opt))
            {
                options.Add(opt);
            }
            _exporter.SetDriverOptions(options);
            _exporter.Export(true);
 
        }
        finally
        {
            _exporter = null;
        }
    }
}


In summary, the code I showed here in red was never needed until we moved from AutoCAD Civil/Map2015 to AutoCAD Civil/Map2018 (it could be since 2016 or 2017, which I never actually used/tested), which indicates some AutoCAD Map API behaviour change, although the manual process with built-in command "MapExport" remains the same from early version to AutoCAD Civil/Map 2018.

Obviously, with this added a few lines of code, we can now explicitly decide to export shape as 2D or 3D.




Wednesday, October 3, 2018

Executing Command from PaletteSet

This article is inspired by the question post in Autodesk's .NET discussion forum here. There could be different solutions to that question and I thought it would be better to put forth mine with actual code provided, which might be too long to post as reply in the discussion forum. So, I decided to post it here for better readability.

When using PaletteSet as UI to allow user to interact with AutoCAD (i.e. letting AutoCAD do particular processing), it is common practice to use Document.SendStringToExecute() to call AutoCAD command, either built-in one, or custom-built one. PaletteSet is a modeless UI, floating on top of AutoCAD window and user can freely change the focus between AutoCAD window, or the PaletteSet window. This would cause issue when a command is active with AutoCAD (usually, it is in the middle of the command, waiting for user input) and user goes to the PaletteSet to trigger another command, as described in the question posted in the discussion forum, such as clicking a button to call a command to insert a block. In this case, the interaction with PaletteSet either results in AutoCAD command line showing error message; or nothing happens at AutoCAD command line - the active command is still waiting to be either completed, or cancelled.

One way to handle this issue is always test if there is active command in PaletteSet' user interaction event handler first, and only goes ahead when there is no active command.

The other approach is whenever PaletteSet's user interaction even is triggered, always cancel any active command first, just like we usually do with any menu/toolbar/ribbon item macro: prefixing it with "^C^C" to cancel possible active command before the macro is called.

Obviously the latter approach would be desirable in most cases and and more compliant with AutoCAD conventions.

So, here is my solution to the question posted in the forum aforementioned.

Firstly I created a custom PaletteSet. One should ALWAYS derive custom PaletteSet from Autodesk.AutoCAD.Windows.PaletteSet class. DO NOT directly use PaletteSet class without subclass it.

Following is the code of the Palette (System.Windows.Forms.UserControl), which simply has 2 buttons; each button's Tag property is given a valid block name; that means, clicking on each button would trigger a block inserting command. Since the UI is very simple, I only show it code behind here:

using System;
using System.Windows.Forms;
 
namespace SendCommandFromPaletteSet
{
    public partial class BlockPalatte : UserControl
    {
 
        public BlockPalatte()
        {
            InitializeComponent();
        }
 
        public event BeginBlockInsertingEventHandler BeginBlockInserting;
 
        // the 2 buttons' Click event is wired to this event handler
        private void ButtonClick(object sender, EventArgs e)
        {
            var tag = ((Control)sender).Tag;
 
            if (tag!=null)
            {
                var blkName = tag.ToString();
                if (!string.IsNullOrEmpty(blkName))
                {
                    BeginBlockInserting?.Invoke(
                        sender, new BeginBlockInsertingEventArgs(blkName));
                }
            }
        }
    }
}

Here is the class MyBlockPaletteSet:

using System;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Windows;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace SendCommandFromPaletteSet
{
    public class MyBlockPaletteSet : PaletteSet
    {
        private BlockPalatte _blkPalette;
        private bool _doInsertion = false;
        public string CurrentBlockName { private setget; }
 
        public MyBlockPaletteSet() : base(
            "My Block PaletteSet""BlkPs"new Guid("B33AE81D-0FBB-49EA-83D2-62D667EEDDCA"))
        {
            this.Style = PaletteSetStyles.ShowCloseButton |
                 PaletteSetStyles.UsePaletteNameAsTitleForSingle |
                  PaletteSetStyles.Snappable;
 
            this.MinimumSize = new System.Drawing.Size(400, 400);
            this.KeepFocus = true;
 
            _blkPalette = new BlockPalatte();
            Add("Blocks", _blkPalette);
 
            _blkPalette.BeginBlockInserting += BlkPalette_BeginBlockInserting;
        }
 
        private void BlkPalette_BeginBlockInserting(object sender, BeginBlockInsertingEventArgs e)
        {
            if (!string.IsNullOrEmpty(e.BlockName))
            {
                var dwg = CadApp.DocumentManager.MdiActiveDocument;
                CurrentBlockName = e.BlockName;
 
                var cmdActive = Convert.ToInt32(CadApp.GetSystemVariable("CMDACTIVE"));
                if (cmdActive>0)
                {
                    dwg.CommandCancelled += Dwg_CommandCancelled;
 
                    _doInsertion = true;
                    dwg.SendStringToExecute("\x03\x03"falsetruefalse);
                }
                else
                {
                    DoBlockInsert(dwg);
                } 
            }
        }
 
        private void Dwg_CommandCancelled(object sender, CommandEventArgs e)
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
 
            if (_doInsertion)
            {
                dwg.CommandCancelled -= Dwg_CommandCancelled;
                _doInsertion = false;
                DoBlockInsert(dwg);
            }
        }
 
        private void DoBlockInsert(Document dwg)
        {
            CadApp.MainWindow.Focus();
            dwg.SendStringToExecute("InsBlk "truefalsefalse);
        }
    }
 
    public class BeginBlockInsertingEventArgs : EventArgs
    {
        public string BlockName { private setget; }
        public BeginBlockInsertingEventArgs(string blkName)
        {
            BlockName = blkName;
        }
    }
 
    public delegate void BeginBlockInsertingEventHandler(object sender, BeginBlockInsertingEventArgs e);
}

As the code shows, the user interaction event (clicking the buttons) in the Palette is bubbled to the custom PaletteSet, where the actual command execution is called (via SendStringToExecute()). Also, custom PaletteSet conveys the user input information (what block is to insert).

The trick of dealing the issue raised in aforementioned question is to test if there is active command with current active document by examine system variable "CMDACTIVE". If no, go ahead to send command to execution; if yes, hook up the CommandCancelled event of active drawing, and then send "^C^C" as command to cancel active command, what ever it is, and then set a flag to indicate new command is waiting to be executed. Therefore, the Command_Cancelled event handler would be triggered and pending command from user interaction with Palette is sent to execution.

Following is the command class that shows the custom PaletteSet and does the actual work: inserting multiple blocks in a loop whenever user clicks a button in the Palette. To simplify the code, I only have code to let user to pick insertion point in a loop until user either cancels the loop, or click the button in the Palette, which cancels the active point picking loop. Here is the code:

using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(SendCommandFromPaletteSet.Commands))]
 
namespace SendCommandFromPaletteSet
{
    public class Commands
    {
        private static MyBlockPaletteSet _blkPs = null;
 
        [CommandMethod("BlkPs"CommandFlags.Session)]
        public static void RunCommand()
        {
            if (_blkPs==null)
            {
                _blkPs = new MyBlockPaletteSet();
            }
 
            _blkPs.Visible = true;
        }
 
        [CommandMethod("InsBlk"CommandFlags.NoHistory )]
        public static void InsertBlock()
        {
            if (_blkPs == null ||
                !_blkPs.Visible) return;
 
            var blkName = _blkPs.CurrentBlockName;
                InsertBlock(blkName);
        }
 
        #region private methods
 
        private static void InsertBlock(string blkName)
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            var count = 0;
            while(true)
            {
                if (!PickInsertionPoint(ed, out Point3d pt))
                {
                    break;
                }
                else
                {
                    count++;
                    ed.WriteMessage($"Inserting block \"{blkName}\" #{count}...");
                }
            }
        }
 
        private static bool PickInsertionPoint(Editor ed, out Point3d pt)
        {
            pt = Point3d.Origin;
 
            var res = ed.GetPoint("\nSelect block position:");
            if (res.Status== PromptStatus.OK)
            {
                pt = res.Value;
                return true;
            }
            else
            {
                return false;
            }
        }
 
        #endregion
    }
}

Watch this video clip for the code in action. As the video clip shows, when there is active command waiting for user input, be it AutoCAD built-in command, or a custom command, or the command started by the PaletteSet, whenever user clicks a button in the PaletteSet, the active command is cancelled, and whatever command tied to the button-click starts.



Friday, September 28, 2018

Showing Modeless Form with Help of PerDocumentClass

Sometimes, we want to provide UI as modeless form that floats on top of AutoCAD, presenting useful information to user and allowing user to still interact with either AutoCAD or the UI. However, if the data to be presented is document specific, the modeless form has to be updated/refreshed whenever the active document in AutoCAD changes.

I wrote an article on this topic a few years ago, where 2 approaches were described: using singleton form instance, or multiple form instances. If the data used in the UI is document specific, using singleton form would require UI refreshing, which in turn may require lengthy data re-collecting/re-loading. In that article, the multiple-form approach relies on non-static CommandMethod call to instantiate the CommandClass per document, so that the UI can be member of the CommandClass and be created somehow automatically.

However, to make AutoCAD plug-in code cleaner, more modular, one may not want to put document specific data models and/or UI components directly in a CommandClass. Also, per-document CommandClass is only instantiated when a non-static CommandMethod is called. So, what if you want the per-document data available before a non-static CommandMethod is called against each document?

In this article, I demonstrate how to take advantage of PerDocumentClassAttribute class, introduced by AutoCAD 2015, to show modeless form easily with document specific data.

Firstly, Kean Wamsley had posted 2 articles about "PerDocumentClassAttribute" here and here. One may want to read them first before following me further here.

Here is a PerDocumentClass that holds some data for each document opened in AutoCAD and holds an UI (a modeless form) to allow user to view/edit the per-document data:

using System;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyPerDocumentClass(typeof(PerDocModelessForm.MyDocData))]
 
namespace PerDocModelessForm
{
    public class MyDocData : IDisposable
    {
        private const string USERDATA_KEY = "My_PER_DOC_DATA";
 
        public string UserName { private setget; }
        public string DrawingName { private setget; }
        public string DwgNote1 { internal setget; }
        public string DwgNote2 { internal setget; }
        public MyDocDataView DataView { private setget; }
        public bool WasShown { internal setget; }
 
        private IntPtr _dwgPointer = IntPtr.Zero;
        private bool _saved = false;
        private Document _dwg = null;
 
        public MyDocData(Document dwg)
        {
            _dwg = dwg;
            DwgNote1 = "";
            DwgNote2 = "";
            UserName = CadApp.GetSystemVariable("LOGINNAME").ToString();
            DrawingName = dwg.Name;
            DataView = new MyDocDataView(this);
 
            _dwgPointer = dwg.UnmanagedObject;
            _saved = false;
 
            dwg.UserData.Add(USERDATA_KEY, this);
 
            // Update DrawingName property if file is saved to a new file name
            dwg.Database.SaveComplete += (o, e) =>
            {
                if (e.FileName.ToUpper()!=DrawingName.ToUpper())
                {
                    DrawingName = e.FileName;
                }
            };
 
            // Show the view when document is activated, if
            // the view was shown when the document was activate prevuoisly
            CadApp.DocumentManager.DocumentActivated += (o, e) =>
            {
                if (e.Document.UnmanagedObject == _dwgPointer)
                {
                    if (WasShown)
                    {
                        DataView.Visible = true;
                    }
                }
            };
 
            // Hide the data view if the document is deactivated
            CadApp.DocumentManager.DocumentToBeDeactivated += (o, e) =>
            {
                if (e.Document.UnmanagedObject == _dwgPointer)
                {
                    if (DataView.Visible)
                    {
                        WasShown = true;
                        DataView.Visible = false;
                    }
                }
            };
 
            // Save myDocData to somewhere
            CadApp.DocumentManager.DocumentToBeDestroyed += (o, e) =>
            {
                var doc = e.Document;
                if (doc.UnmanagedObject == _dwg.UnmanagedObject)
                {
                    var data = doc.UserData[USERDATA_KEY] as MyDocData;
                    SaveDwgNotes(data);
                }
            };
        }
 
        public static MyDocData Create(Document dwg)
        {
            return new MyDocData(dwg);
        }
 
        public static void ShowDocData(Document dwg)
        {
            var data = dwg.UserData[USERDATA_KEY] as MyDocData;
            CadApp.ShowModelessDialog(CadApp.MainWindow.Handle, data.DataView, true);
        }
 
        public void Dispose()
        {
            if (DataView!=null)
            {
                DataView.Dispose();
            }
        }
 
        #region private methods
 
        private void SaveDwgNotes(MyDocData data)
        {
            if (!data._saved)
            {
                System.Windows.Forms.MessageBox.Show(
                    "Saving drawing note data to somewhere...",
                    "Per Document Data in \"" + System.IO.Path.GetFileName(_dwg.Name) + "\"...",
                    System.Windows.Forms.MessageBoxButtons.OK,
                    System.Windows.Forms.MessageBoxIcon.Information);
 
                //To DO: save drawing note data to somewhere
 
 
                data._saved = true;
            }
        }
 
        #endregion
    }
}

Here is the command that allows user to view/edit data for each document:

using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(PerDocModelessForm.MyCommands))]
 
namespace PerDocModelessForm
{
    public class MyCommands
    {
        [CommandMethod("ShowData")]
        public static void RunCommandA()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            MyDocData.ShowDocData(dwg);
        }
    }
}


This video clip shows the code in action

Some Thought

PerDocumentClass greatly simplifies the process of creating and cleaning "per-document" data in AutoCAD - a multiple document application: the class is instantiated to each existing open document when the .NET assembly, where the PerDocumentClass is defined, is loaded and to each newly opened document.

Using PerDocumentClass would make data segregation in the principle of "separate-concerns" much easier. My code here shows how multiple modeless forms are used for documents in AutoCAD with each only being associated to specific document.

In this example of using modeless form, I use Windows Form. Using WPF window would be the same. Using PaletteSet for each document is doable in the same way, but might not be desirable, because PaletteSet is designed to run at application level, as an UI (pallete) container, especially if a GUID is used to instantiate a PaletteSet, and AutoCAD remembers a PaletteSet based on its GUID per-application.

Download source code here.

Thursday, July 26, 2018

Custom Command Showing Or Not Showing Dialog Box - 2 of 2

This is the second post on the topic of showing or not showing dialog box within custom command execution. The part 1 is here.

While an AutoCAD operation started with a command often needs to deal with files, thus needs file path/name to be inputted into the operation process with File Open/Save dialog box or at command line, even more often, the operation needs other data inputs than file path/name. Depending on the data input requirements, dialog box is used as more user-friendly UI and better input quality control means. However, in order to make a command script-able, a version of the same command often exists that collects all inputs from command line. The convention is to prefix "-" the regular command name as that command's command-line-only version. So, when we do create our own custom command with CommandMethod class, we should also try to follow this convention, if we want our custom command to be script-able.

Below is the code example:

[CommandMethod("DoThis")]
[CommandMethod("-DoThis")]
[CommandMethod("DiaDoThis")]
public static void DoSomething()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    var cmd = CadApp.GetSystemVariable("CMDNAMES").ToString().ToUpper();
    var showDialog = !cmd.StartsWith("-") && !cmd.StartsWith("DIA");
 
    var userInput = showDialog ?
        GetInputFromDialog() :
        GetInputFromCommandLine();
 
    string msg;
    if (userInput=="YES")
        msg = $"The command proceeded {(showDialog?"with dialog box input":"with command line input")}.";
    else
        msg = $"The command cancelled {(showDialog ? "with dialog box input" : "with command line input")}.";
 
    CadApp.ShowAlertDialog(msg);
}
 
private static string GetInputFromDialog()
{
    var msg = "The process needs you to answer:\n\n" +
        "YES to continue\n" +
        "NO to caancel\n\n" +
        "Do you want to continue?";
    var res = System.Windows.Forms.MessageBox.Show(
        msg, "My Dialog Box",
        System.Windows.Forms.MessageBoxButtons.YesNo,
        System.Windows.Forms.MessageBoxIcon.Question,
        System.Windows.Forms.MessageBoxDefaultButton.Button1);
 
    return res == System.Windows.Forms.DialogResult.Yes ? "YES" : "NO";
}
 
private static string GetInputFromCommandLine()
{
    var ed = CadApp.DocumentManager.MdiActiveDocument.Editor;
    var input = "NO";
 
    var opt = new PromptKeywordOptions(
        "\nContinue the process?");
    opt.AppendKeywordsToMessage = true;
    opt.Keywords.Add("Yes");
    opt.Keywords.Add("No");
    opt.Keywords.Default = "Yes";
 
    var res = ed.GetKeywords(opt);
    if (res.Status== PromptStatus.OK)
    {
        input = res.StringResult.ToUpper();
    }
 
    return input;
}

The code is really simple, where I just used a message box, as a dialog box, to collect user input, but the interesting part is the multiple CommandMethodAttribute used to decorate the same methods as multiple custom commands. And notice that I also prefixed one of the command name with "Dia", implying it is dialog box version of the command, instead of prefixing the command with "-". This is just meant to show how I can use the different command name to run different version of the command, dialog box version, or command line version.

See this video clip for the code in action.

Wednesday, July 25, 2018

Custom Command Showing Or Not Showing Dialog Box - 1 of 2

Most AutoCAD users should know that toggle system variable "FILEDIA" would change AutoCAD built-in command behaviour, if the command needs user to supply valid file path/name during the command execution: either showing a Open/Save file dialog box for user to select file path/name, or asking user to enter file path/name at command line. The command line input is especially important, because it makes the command script-able (that is, the command can be included in a list of script macro with file path/name supplied, so that the script executing would not be interrupted by File Open/Save dialog box, waiting for user input).

Also, some AutoCAD built-in commands show dialog boxes other then File Open/Save dialog box. By AutoCAD convention, if we execute these command with prefix "-", the command would execute its command-line version for user inputs, if the command does have a command-line version.

When we do our custom AutoCAD programming, we should try our best to follow these AutoCAD convention to make our custom command script-able, whenever it is possible and/or necessary.

This post discusses making custom command showing/not showing File Open/Save dialog box, according to system variable "FILEDIA" value.

It turned out, it is rather easy, in terms of writing code, as shown below.

[CommandMethod("MyCmd")]
public static void RunMyCommand()
{
    var dwg = CadApp.DocumentManager.MdiActiveDocument;
    var ed = dwg.Editor;
 
    try
    {
        var fDia = Convert.ToInt32(CadApp.GetSystemVariable("FILEDIA"));
        var fileName = GetFileNameFromEditor(ed, fDia);
 
        if (string.IsNullOrEmpty(fileName))
        {
            CadApp.ShowAlertDialog("Command is cancelled.");
        }
        else
        {
            CadApp.ShowAlertDialog($"Command carries on with drawing:\n\n{fileName}");
 
            // Continue the execution with the valid file name, 
            // such as read the DWG file into side database
        }
    }
    catch (System.Exception ex)
    {
        ed.WriteMessage("\nError:\n{0}.", ex.Message);
    }
    finally
    {
        Autodesk.AutoCAD.Internal.Utils.PostCommandPrompt();
    }
}
 
private static string GetFileNameFromEditor(Editor ed, int fileDia)
{
    while (true)
    {
        var opt = new PromptOpenFileOptions("\nOpen a drawing file");
        opt.DialogCaption = "Open Drawing";
        opt.Filter = "AutoCAD Drawing *.dwg|*.dwg";
        if (ed.Document.IsNamedDrawing)
        {
            opt.InitialDirectory = System.IO.Path.GetDirectoryName(ed.Document.Name);
        }
        opt.PreferCommandLine = fileDia == 0;
 
        var res = ed.GetFileNameForOpen(opt);
        if (res.Status == PromptStatus.OK)
        {
            var valid = true;
 
            if (opt.PreferCommandLine)
            {
                if (!System.IO.File.Exists(res.StringResult))
                {
                    ed.WriteMessage(
                        $"\nFile not found:\n{res.StringResult}!");
                    valid = false;
                }
            }
 
            if (valid)
            {
                return res.StringResult;
            }
        }
        else
        {
            return "";
        }
    }
}

As the code shows, simply use Editor.GetFileNameForOpen[Save] in conjunction with PromptOpen[Save]FileOption does the trick: simply setting PreferCommandLine property of the PromptFileOptions class to False/True makes Editor.GetFileNameForOpen[Save]() method to either show File Open/Save dialog box, or ask user to enter file path/name at command line. In my code, if the file path/name is to be entered at command line, the code needs to valid the entered file path/name to make sure the file existence (for opening, of course. If it is for saving, creating necessary folder, prompt saving overwriting is warranted in following code).

See this video clip showing how the code works.


Monday, July 9, 2018

Change Width of DBText/AttributeReference - Part Three: Real Time Change

In 2 previous posts (here and here), the situation I tried to deal with is to fit a string of text into a limited space, such as a table's field with given width.

Obviously, besides shrinking the text string's width by reducing its WidthFactor, user can also rephrase the word/characters of the text string to reduce the width while keeping the meaning of the text. This way, the text would be presented visually the same (i.e. in the same WidthFactor).

With DBText, since it can be edited in place, so, user can change the wording of the text and see if the text would fit into the space while typing. But the the case of AttributeReference, the user can only either edit it in attribute editing dialog box, or in Properties window. For the former, when the attribute in edited in the dialog box, the real change occurs with the typing, which is good, because you can see if the text's width exceeds out of the allowed space at real time. For the latter, use can only see the change when the editing is completed (the focus leaves the edited field in Properties window).

After doing previous 2 posts, I thought why not doing one more article on changing text/attribute at real time by giving user chances of either changing text string's wording, or WidthFactor. The other reason of writing once more on this topic is that I have not seen sample code on the net about real time change of text entity while typing.

Here is the code of CommandClass RealTimeTextEditor:

using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(RealTimeTextEdit.RealTimeTextEditor))]
 
namespace RealTimeTextEdit
{
    public class RealTimeTextEditor
    {
        private Document _dwg;
        private string _defaultTarget = "Text";
        
        public RealTimeTextEditor()
        {
            SelectedId = ObjectId.Null;
            OriginalWidthFactor = 1.0;
            OriginalText = "";
            _dwg = CadApp.DocumentManager.MdiActiveDocument;
        }
 
        #region properties
 
        internal ObjectId SelectedId { private setget; }
        internal string OriginalText { private setget; }
        internal double OriginalWidthFactor { private setget; }
 
        #endregion
 
        #region public methods
 
        [CommandMethod("MyTextEdit")]
        public void EditText()
        {
            while(true)
            {
                var target = SelectEditTarget(_defaultTarget);
                if (!string.IsNullOrEmpty(target))
                {
                    _defaultTarget = target;
 
                    ObjectId textId = ObjectId.Null;
                    if (_defaultTarget=="Text")
                    {
                        textId = SelectTextEntity();
                    }
                    else
                    {
                        textId = SelectAttributeEntity();
                    }
 
                    if (!textId.IsNull)
                    {
                        GetTextInformation(textId);
                        DoEditWork();
                    }
                }
                else
                {
                    break;
                }
            }
 
            _dwg.Editor.WriteMessage("\n");
        }
 
        #endregion
 
        #region private methods
 
        private string SelectEditTarget(string defaultTarget)
        {
            var target = "";
 
            var opt = new PromptKeywordOptions(
                "\nEdit Text or Attribute?");
            opt.AppendKeywordsToMessage = true;
            opt.Keywords.Add("Text");
            opt.Keywords.Add("Attribute");
            opt.Keywords.Add("eXit");
            opt.Keywords.Default = defaultTarget;
 
            var res = _dwg.Editor.GetKeywords(opt);
            if (res.Status== PromptStatus.OK)
            {
                if (res.StringResult!="eXit")
                {
                    return res.StringResult;
                }
            }
 
            return target;
        }
 
        private ObjectId SelectTextEntity()
        {
            ObjectId textId = ObjectId.Null;
 
            var opt = new PromptEntityOptions(
                "\nSelect a TEXT entity:");
            opt.SetRejectMessage(
                "\nInvalid selection: not a TEXT entity!");
            opt.AddAllowedClass(typeof(DBText), true);
 
            var res = _dwg.Editor.GetEntity(opt);
            if (res.Status== PromptStatus.OK)
            {
                textId = res.ObjectId;
            }
 
            return textId;
        }
 
        private ObjectId SelectAttributeEntity()
        {
            ObjectId attId = ObjectId.Null;
 
            while (true)
            {
                var opt = new PromptNestedEntityOptions(
                    "\nSelect an Attribute in block:");
                opt.AllowNone = false;
 
                var res = _dwg.Editor.GetNestedEntity(opt);
                if (res.Status == PromptStatus.OK)
                {
                    if (res.ObjectId.ObjectClass.DxfName.ToUpper() == "ATTRIB")
                    {
                        attId = res.ObjectId;
                    }
                    else
                    {
                        _dwg.Editor.WriteMessage(
                            "\nInvalid selection: not an attribue in block!");
                    }
                }
                else
                {
                    break;
                }
 
                if (!attId.IsNull) break;
            }
 
            return attId;
        }
 
        private void GetTextInformation(ObjectId textId)
        {
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                var txt = (DBText)tran.GetObject(textId, OpenMode.ForRead);
                OriginalText = txt.TextString;
                OriginalWidthFactor = txt.WidthFactor;
                SelectedId = textId;
                tran.Commit();
            }
        }
 
        private void DoEditWork()
        {
            using (var dlg = new dlgTextBox(this))
            {
                dlg.TextStringChanged += (o, e) =>
                  {
                      UpdateTextEntity(e.TextString);
                  };
 
                dlg.TextWidthFactorChanged += (o, e) =>
                {
                    UpdateTextEntity(e.WidthFactor);
                };
 
                dlg.Left = 50;
                dlg.Top = 50;
 
                var res = CadApp.ShowModalDialog(CadApp.MainWindow.Handle, dlg, false);
 
                if (res== System.Windows.Forms.DialogResult.Cancel)
                {
                    //restore the originals
                    UpdateTextEntity(OriginalText);
                    UpdateTextEntity(OriginalWidthFactor);
                }
            }
        }
 
        private void UpdateTextEntity(string textString)
        {
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                var txt = (DBText)tran.GetObject(SelectedId, OpenMode.ForWrite);
                txt.TextString = textString;
                tran.Commit();
            }
            _dwg.Editor.UpdateScreen();
        }
 
        private void UpdateTextEntity(double widthFactor)
        {
            using (var tran = _dwg.TransactionManager.StartTransaction())
            {
                var txt = (DBText)tran.GetObject(SelectedId, OpenMode.ForWrite);
                txt.WidthFactor = widthFactor;
                tran.Commit();
            }
            _dwg.Editor.UpdateScreen();
        }
 
        #endregion
    }
}

I need UI as a modal dialog box where user can re-typing text string, or change WidthFactor. It looks like this:


Here the dialog box' code behind:

using System;
using System.Windows.Forms;
 
using Autodesk.AutoCAD.DatabaseServices;
 
namespace RealTimeTextEdit
{
    public partial class dlgTextBox : Form
    {
        private RealTimeTextEditor _tool = null;
        private ObjectId _textId = ObjectId.Null;
        private bool _setText = false;
 
        internal dlgTextBox()
        {
            InitializeComponent();
        }
 
        internal dlgTextBox(RealTimeTextEditor tool):this()
        {
            _tool = tool;
        }
 
        internal event TextStringChangedEventHandler TextStringChanged;
        internal event TextWidthFactorChangedEventHandler TextWidthFactorChanged;
 
        internal bool Undo { private setget; }
 
        #region private methods
 
        private void ShowSelected()
        {
            if (!_tool.SelectedId.IsNull)
            {
                _setText = true;
                _textId = _tool.SelectedId;
                txtOld.Text = _tool.OriginalText;
                txtNew.Text = _tool.OriginalText;
                txtNew.Enabled = true;
                txtFactor.Text = _tool.OriginalWidthFactor.ToString("#0.00");
                udFactor.Value = Convert.ToDecimal(_tool.OriginalWidthFactor);
                btnUndo.Enabled = false;
                _setText = false;
            }
        }
 
        #endregion
 
        private void txtNew_TextChanged(object sender, EventArgs e)
        {
            if (_setText) return;
 
            btnUndo.Enabled = true;
            TextStringChanged?.Invoke(
                this, 
                new TextStringChangedEventArgs(txtNew.Text));
        }
 
        private void udFactor_ValueChanged(object sender, EventArgs e)
        {
            if (_setText) return;
 
            btnUndo.Enabled = true;
            TextWidthFactorChanged?.Invoke(
                this, 
                new TextWidthFactorChangedEventArgs(Convert.ToDouble(udFactor.Value)));
        }
 
        private void btnUndo_Click(object sender, EventArgs e)
        {
            this.DialogResult = DialogResult.Cancel;
        }
 
        private void btnClose_Click(object sender, EventArgs e)
        {
            this.DialogResult = DialogResult.OK;
        }
 
        private void dlgTextBox_Load(object sender, EventArgs e)
        {
            ShowSelected();
        }
    }
 
    internal class TextStringChangedEventArgs : EventArgs
    {
        internal string TextString { private setget; }
        internal TextStringChangedEventArgs(string textString)
        {
            TextString = textString;
        }
    }
 
    internal delegate void TextStringChangedEventHandler(
        object sender, TextStringChangedEventArgs e);
 
    internal class TextWidthFactorChangedEventArgs : EventArgs
    {
        internal double WidthFactor { private setget; }
        internal TextWidthFactorChangedEventArgs(double widthFactor)
        {
            WidthFactor = widthFactor;
        }
    }
 
    internal delegate void TextWidthFactorChangedEventHandler(
        object sender, TextWidthFactorChangedEventArgs e);
}

As the code shows, the real-time change to the target DBText/AttributeReference is triggered by the UI firing event TextStringChanged and TextWidthFactorChanged. The event handlers (highlighted in red) make the actual change.

See this video that shows the code in action.

It is worth pointing out: in the CommandClass RealTimeTextEditor I use non-static method as the CommandMethod, which means for each Document this command is used, a class instance is created by AutoCAD, so that the user's previous choice of editing target (Text, or Attribute), which is asked at beginning of the command execution, is the default choice (default keyword in the GetKeyword() call). The video clip shows this effect in its last portion.

Note:
Most of my posts come with video clips that show how the code run in AutoCAD. Until this post, I have always used Jing from TechSmith with free video host at screencast.com (thanks for the free stuff!). However, TechSmith recently announced their policy change that from now on, they would only keep video clips for one year for free Jing version user. So, starting from this post, I stopped using free Jing to generate screen capture video. Instead, I use OBS Studio to record screen as MP4 files and store and share them from my Google drive. I'll try to collect all the video clips used in my posts into my Google drive and update the links in the old posts as soon as possible before they are removed from screencast.com (hopefully I can find enough time before the deadline in September :-().

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.