Tuesday, March 9, 2021

Modeless Form/Window - Do Something And Update Its View - Updated

Using modeless from/window in AutoCAD plugin brings an unique challenge to programmers because of it being presented in ApplicationContext. One of most common issues with using modeless form/window is how to handle user interaction on the form/window, if the user interaction is meant to make changes to current drawing (MdiActiveDocument), such as user clicking a button on the form/window in order to create entities or update entities in drawing, and afterward update/refresh the modeless form/window to reflect the changes just made to the drawing.

It has been strongly recommended since the earlier stage of AutoCAD .NET API that the drawing database change action triggered from modeless form/window should be wrapped in a command and executed by calling SendStringToExecute() method, rather than directly from the form/window's user interaction event handler.

However, if the action executed by SendStringToExecute() causes drawing change and the change needs to be reflected in the modeless form/window, a proper care is needed in order for the form/window to wait until the sent command complete before the form/window update begins. 

A recent question on this issue was posted in Autodesk's .NET discussion forum and I posted a reply by suggesting use SendStringToExecute(), and I also proposed to use CommandEnded event handler to trigger modeless UI update. I just completed a sample project, so that anyone who is interested in this topic would be able to see a more complete picture of handling this issue. 

This is a simplified WinForm UI (the principle in in this article would apply to WPF UI, of course):


Here the form's code behind:
using System;
using System.Windows.Forms;
 
using Autodesk.AutoCAD.ApplicationServices;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace ModelessDialogWithAction
{
    public partial class EntitiesForm : Form
    {
        private readonly DocumentCollection _dwgManager = null;
        private string _currentCommand = "";
 
        public EntitiesForm()
        {
            InitializeComponent();
        }
 
        public EntitiesForm(DocumentCollection dwgManager) : this()
        {
            _dwgManager = dwgManager;
            _dwgManager.DocumentActivated += (o, e) =>
              {
                  if (e.Document.UnmanagedObject != DocumentPointer)
                  {
                      try
                      {
                          Cursor.Current = Cursors.WaitCursor;
                          RefreshEntityData(e.Document);
                      }
                      finally
                      {
                          Cursor.Current = Cursors.Default;
                      }
                  }
              };
        }
 
        public IntPtr DocumentPointer { private setget; }
 
        public void RefreshEntityData(Document dwg)
        {
            try
            {
                Cursor.Current = Cursors.WaitCursor;
 
                ListViewEntities.Items.Clear();
 
                var ents = CadUtil.CollectEntitiesInModelSpace(dwg);
                foreach (var ent in ents)
                {
                    var item = new ListViewItem(ent.EntityType);
                    item.SubItems.Add(ent.EntityId.ToString());
                    item.SubItems.Add(ent.EntityLayer);
 
                    ListViewEntities.Items.Add(item);
                }
 
                LabelCount.Text = ListViewEntities.Items.Count.ToString();
            }
            finally
            {
                Cursor.Current = Cursors.Default;
            }
 
            DocumentPointer = dwg.UnmanagedObject;
        }
 
        private void ExecuteCommand(string commandName)
        {
            _dwgManager.MdiActiveDocument.CommandEnded += MdiActiveDocument_CommandEnded;
            _currentCommand = commandName;
            CadApp.MainWindow.Focus();
            _dwgManager.MdiActiveDocument.SendStringToExecute(
                commandName + "\n"truefalsefalse);
        }
 
        private void MdiActiveDocument_CommandEnded(object sender, CommandEventArgs e)
        {
            if (e.GlobalCommandName.ToUpper() == _currentCommand.ToUpper())
            {
                _dwgManager.MdiActiveDocument.CommandEnded -= MdiActiveDocument_CommandEnded;
 
                RefreshEntityData(
                    _dwgManager.MdiActiveDocument);
            }
        }
 
        private void ButtonAddLine_Click(object sender, EventArgs e)
        {
            ExecuteCommand(MyCommands.ADD_LINE_COMMAND);
        }
 
        private void ButtonAddCircle_Click(object sender, EventArgs e)
        {
            ExecuteCommand(MyCommands.ADD_CIRCLE_COMMAND);
        }
 
        private void EntitiesForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            e.Cancel = true;
            Visible = false;
        }
 
        private void ButtonClose_Click(object sender, EventArgs e)
        {
            Visible = false;
        }
    }
}
Here is the main class (CommandClass) that initializes/shows the modeless form, and defines 2 CommandMethods for the modeless form to call with SendStringToExecute(), noticing that I set CommandFlags.NoHistory with these 2 commands, which is not mandatory, but is meant for the 2 commands not being seen by user easily.
using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(ModelessDialogWithAction.MyCommands))]
[assemblyExtensionApplication(typeof(ModelessDialogWithAction.MyCommands))]
 
namespace ModelessDialogWithAction
{
    public class MyCommands : IExtensionApplication
    {
        public const string ADD_LINE_COMMAND = "ADDLINE";
        public const string ADD_CIRCLE_COMMAND = "ADDCIRCLE";
 
        private static EntitiesForm _entitiesForm = null;
 
        #region IExtensionApplication implementing
 
        public void Initialize()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                ed.WriteMessage($"\nInitializing custom add-in \"{this.GetType().Name}\"...");
 
                _entitiesForm = new EntitiesForm(CadApp.DocumentManager);
                _entitiesForm.RefreshEntityData(CadApp.DocumentManager.MdiActiveDocument);
 
                ed.WriteMessage($"\nIntializing done.\n");
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nInitializing error:\n{ex.Message}\n");
            }
        }
 
        public void Terminate()
        {
 
        }
 
        #endregion
 
        [CommandMethod("ShowForm"CommandFlags.Session)]
        public static void ShowForm()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            if (_entitiesForm.DocumentPointer!=dwg.UnmanagedObject)
            {
                _entitiesForm.RefreshEntityData(dwg);
            }
 
            CadApp.ShowModelessDialog(_entitiesForm);
        }
 
        [CommandMethod(ADD_LINE_COMMAND, CommandFlags.NoHistory)]
        public static void AddLineCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                CadUtil.AddLine(dwg);
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nError:\n{ex.Message}\n");
            }
        }
 
        [CommandMethod(ADD_CIRCLE_COMMAND, CommandFlags.NoHistory)]
        public static void AddCircleCommand()
        {
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            var ed = dwg.Editor;
 
            try
            {
                CadUtil.AddCircle(dwg);
            }
            catch (System.Exception ex)
            {
                ed.WriteMessage($"\nError:\n{ex.Message}\n");
            }
        }
    }
}
Finally, here is a helper class that does the drawing/database changes, which can be executed from UI/CommandMethod:
using System.Collections.Generic;
using System.Linq;
 
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
 
namespace ModelessDialogWithAction
{
    public class EntityInfo
    {
        public ObjectId EntityId { setget; }
        public string EntityType { setget; }
        public string EntityLayer { setget; }
    }
 
    public class CadUtil
    {
        public static IEnumerable<EntityInfo> CollectEntitiesInModelSpace(Document dwg)
        {
            var lst = new List<EntityInfo>();
 
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                var model = (BlockTableRecord)tran.GetObject(
                    SymbolUtilityServices.GetBlockModelSpaceId(dwg.Database), OpenMode.ForRead);
                foreach (ObjectId id in model)
                {
                    var ent = (Entity)tran.GetObject(id, OpenMode.ForRead);
                    lst.Add(new EntityInfo
                    {
                        EntityId = id,
                        EntityType = id.ObjectClass.DxfName,
                        EntityLayer = ent.Layer
                    });
                }
 
                tran.Commit();
            }
 
            return from e in lst 
                   orderby 
                   e.EntityType ascending, 
                   e.EntityLayer ascending, 
                   e.EntityId.ToString() ascending 
                   select e;
        }
 
        public static ObjectId AddLine(Document dwg)
        {
            var res = dwg.Editor.GetPoint("\nSelect line's Start Point:");
            if (res.Status== PromptStatus.OK)
            {
                var start = res.Value;
                var opt = new PromptPointOptions(
                    "\nSelect line's End Point:");
                opt.UseBasePoint = true;
                opt.BasePoint = start;
                opt.UseDashedLine = true;
                res = dwg.Editor.GetPoint(opt);
                if (res.Status== PromptStatus.OK)
                {
                    var end = res.Value;
                    return CreateLine(dwg.Database, start, end);
                }
            }
            dwg.Editor.WriteMessage("\n*Cancel*");
            return ObjectId.Null;
        }
 
        public static ObjectId AddCircle(Document dwg)
        {
            var res = dwg.Editor.GetPoint("\nSelect circle's Center Point:");
            if (res.Status == PromptStatus.OK)
            {
                var center = res.Value;
                var opt = new PromptDoubleOptions(
                    "\nEnter circle's Radius:");
                opt.AllowNegative = false;
                opt.AllowNone = false;
                opt.DefaultValue = 300.0;
                
                var dRes = dwg.Editor.GetDouble(opt);
                if (dRes.Status == PromptStatus.OK)
                {
                    var radius = dRes.Value;
                    return CreateCircle(dwg.Database, center, radius);
                }
            }
            dwg.Editor.WriteMessage("\n*Cancel*");
            return ObjectId.Null;
        }
 
        private static ObjectId CreateLine(Database db, Point3d startPt, Point3d endPt)
        {
            var line = new Line(startPt, endPt);
            line.SetDatabaseDefaults(db);
 
            return AddEntityToModelSpace(db, line);
        }
 
        private static ObjectId CreateCircle(Database db, Point3d centerPt, double radius)
        {
            var circle = new Circle();
            circle.Center = centerPt;
            circle.Radius = radius;
            circle.SetDatabaseDefaults(db);
 
            return AddEntityToModelSpace(db, circle);
        }
 
        private static ObjectId AddEntityToModelSpace(Database db, Entity ent)
        {
            var id = ObjectId.Null;
            using (var tran = db.TransactionManager.StartTransaction())
            {
                var model = (BlockTableRecord)tran.GetObject(
                    SymbolUtilityServices.GetBlockModelSpaceId(db), OpenMode.ForWrite);
                model.AppendEntity(ent);
                tran.AddNewlyCreatedDBObject(ent, true);
 
                tran.Commit();
            }
            return id;
        }
    }
}
There is no DocumentLock is used in the code, in spite the adding entity action is triggered from modeless form/window, because the command executed by SendStringToExecute() does the document locking/unlock automatically. 
Following video clip shows the modeless form/window gets updated properly by handling CommandEnded event, targeting the 2 CommandMethods. However, depending on what data is shown on the form/window, only handling the commands directly triggered by the form/window may not enough for the form/window to reflect the data changes, but this is beyond my discussion here.

Update
A comment of this article asks how to let AutoCAD zoom to an entity that is selected in a DataGridView (or any control that contains selectable items, for that matter) on the modeless form/window.
Because the action of user interaction on the modeless form/window (selecting an item in a container control) that results in AutoCAD zooming to certain entity only change the editor's current view, not the drawing database, and there is no feedback requiring the modeless form/window to refresh/update, we can directly execute zooming code from the modeless form/window without having to go the route of using SendStringToExecute()
First, I added a static method in the CadUtil class for zooming to an entity operation:
public static void ZoomToEntity(ObjectId entId)
{
    GetZoomWindow(entId, out double[] corner1, out double[] corner2);
    dynamic comApp = CadApp.AcadApplication;
    comApp.ZoomWindow(corner1, corner2);
}
 
private static void GetZoomWindow(ObjectId entId, 
    out double[] corner1, out double[] corner2)
{
    var ext = GetEntityGeoExtents(entId);
    var h = ext.MaxPoint.Y - ext.MinPoint.Y;
    var w = ext.MaxPoint.X - ext.MinPoint.X;
    var len = Math.Max(h, w)/4.0;
 
    var pt1 = new Point3d(ext.MinPoint.X - len, ext.MinPoint.Y - len, ext.MinPoint.Z);
    var pt2 = new Point3d(ext.MaxPoint.X + len, ext.MaxPoint.Y + len, ext.MaxPoint.Z);
 
    corner1 = pt1.ToArray();
    corner2 = pt2.ToArray();
}
 
private static Extents3d GetEntityGeoExtents(ObjectId entId)
{
    Extents3d ext;
 
    using (var tran=entId.Database.TransactionManager.StartTransaction())
    {
        var ent = (Entity)tran.GetObject(entId, OpenMode.ForRead);
        ext = ent.GeometricExtents;
 
        tran.Commit();
    }
 
    return ext;
}
In order to react to user's action of selecting an item in a container control on the form/window, I updated the form by adding a check box:

The updated form's code behind:
using System;
using System.Windows.Forms;
 
using Autodesk.AutoCAD.ApplicationServices;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace ModelessDialogWithAction
{
    public partial class EntitiesForm : Form
    {
        private readonly DocumentCollection _dwgManager = null;
        private string _currentCommand = "";
 
        public EntitiesForm()
        {
            InitializeComponent();
        }
 
        public EntitiesForm(DocumentCollection dwgManager) : this()
        {
            _dwgManager = dwgManager;
            _dwgManager.DocumentActivated += (o, e) =>
              {
                  if (e.Document.UnmanagedObject != DocumentPointer)
                  {
                      try
                      {
                          Cursor.Current = Cursors.WaitCursor;
                          RefreshEntityData(e.Document);
                      }
                      finally
                      {
                          Cursor.Current = Cursors.Default;
                      }
                  }
              };
        }
 
        public IntPtr DocumentPointer { private setget; }
        public Autodesk.AutoCAD.DatabaseServices.ObjectId SelectedEntity { private setget; } =
            Autodesk.AutoCAD.DatabaseServices.ObjectId.Null;
        
        public void RefreshEntityData(Document dwg)
        {
            try
            {
                Cursor.Current = Cursors.WaitCursor;
 
                ListViewEntities.Items.Clear();
 
                var ents = CadUtil.CollectEntitiesInModelSpace(dwg);
                foreach (var ent in ents)
                {
                    var item = new ListViewItem(ent.EntityType);
                    item.SubItems.Add(ent.EntityId.ToString());
                    item.SubItems.Add(ent.EntityLayer);
 
                    item.Tag = ent.EntityId;
                    item.Selected = false;
 
                    ListViewEntities.Items.Add(item);
                }
 
                SelectedEntity = Autodesk.AutoCAD.DatabaseServices.ObjectId.Null;
                LabelCount.Text = ListViewEntities.Items.Count.ToString();
            }
            finally
            {
                Cursor.Current = Cursors.Default;
            }
 
            DocumentPointer = dwg.UnmanagedObject;
        }
 
        private void ExecuteCommand(string commandName)
        {
            _dwgManager.MdiActiveDocument.CommandEnded += MdiActiveDocument_CommandEnded;
            _currentCommand = commandName;
            CadApp.MainWindow.Focus();
            _dwgManager.MdiActiveDocument.SendStringToExecute(
                commandName + "\n"truefalsefalse);
        }
 
        private void MdiActiveDocument_CommandEnded(object sender, CommandEventArgs e)
        {
            if (e.GlobalCommandName.ToUpper() == _currentCommand.ToUpper())
            {
                _dwgManager.MdiActiveDocument.CommandEnded -= MdiActiveDocument_CommandEnded;
 
                RefreshEntityData(
                    _dwgManager.MdiActiveDocument);
            }
        }
 
        private void ButtonAddLine_Click(object sender, EventArgs e)
        {
            ExecuteCommand(MyCommands.ADD_LINE_COMMAND);
        }
 
        private void ButtonAddCircle_Click(object sender, EventArgs e)
        {
            ExecuteCommand(MyCommands.ADD_CIRCLE_COMMAND);
        }
 
        private void EntitiesForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            e.Cancel = true;
            Visible = false;
        }
 
        private void ButtonClose_Click(object sender, EventArgs e)
        {
            Visible = false;
        }
 
        private void ListViewEntities_SelectedIndexChanged(object sender, EventArgs e)
        {
            if (!CheckBoxZoom.Checked) return;
            if (ListViewEntities.SelectedItems.Count == 0) return;
 
            SelectedEntity = (Autodesk.AutoCAD.DatabaseServices.ObjectId)
                ListViewEntities.SelectedItems[0].Tag;
 
            // Directly execute the zooming code without using 
            // SendStringToExecute()
            CadUtil.ZoomToEntity(SelectedEntity);
        }
    }
}
The lines in red is modified code. As shown in ListViewEntities_SelectIndexChanged event handler, I simply to have the entity zooming code directly executed from the modeless form without using SendStringToExecute() and handling CommandEnded event for UI update, because it is not needed in this case. Following video clip shows the code in action:




Saturday, February 6, 2021

Drag & Drop In AutoCAD (2) - Drag From External Application And Drop OnTo AutoCAD

This is the second of this series on Drag & Drop operation in AutoCAD. See previous ones here:

    Drag & Drop In AutoCAD (1) - The Scenarios

In this post I look into the scenario of dragging from external application and dropping onto a running AutoCAD session.

There are 2 types of external applications user could be dragging something from:

1. Applications that we, as programmers, do not have much control on how the dragging is handled. For example, we cannot control what happens when dragging document content from a Word document into AutoCAD, or we do not have control when the document content is drooped onto AutoCAD editor. The behaviours of drag & drop from both applications (Word and AutoCAD) are designed/built in them and exposed as API for us to intercept into the drag & drop process to manipulate it. So, I'll leave this case out of the discussion here. 

2. Applications we developed, thus, we  have the control at the dragging end of the drag & drop operation. That is, we can choose to use UI components that support drag operation (i.e. having MouseMove event and support DoDragDrop() method) to let drag & drop operation begins at dragging end (our application side). However, at the drop end, AutoCAD runs as a different application, thus, when the drag & drop operation is initialized from our application, we do not have control on how AutoCAD receive the data being dragged and dropped onto it: AutoCAD will do what is was designed to do. There are some dropping behaviours of AutoCAD are known to most AutoCAD users:

  • Insert block
  • Open drawing
  • Insert Text/MText entity
  • Insert OLE object
There could be other behaviours that I am not aware of, but inserting block, opening drawing and inserting MText are the most encountered dropping task when AutoCAD being at the dropping end of drag & drop operation.

Here I built a WinForm desktop application to demonstrate how file name and text string can be dragged from the application, dropped onto AutoCAD and what happens in AutoCAD.

The WinForm application's UI includes a Listbox that can be filled with file names, and a Textbox where user can input text string. There are different options (represented by Radiobuttons) for dragging content in Listbox and/or Textbox that would result in different dropping effect at AutoCAD end.



Here is the code behind the form:
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.IO;
 
namespace DragFromExeToAcad
{
    public partial class MainForm : Form
    {
        private readonly string _tempTextFile = "";
 
        public MainForm()
        {
            InitializeComponent();
 
            string folder =
            Path.Combine(Environment.GetFolderPath(
                Environment.SpecialFolder.LocalApplicationData), "Temp");
            _tempTextFile = folder + "\\Dragged_Text.txt";
        }
 
        private void SelectFiles()
        {
            IEnumerable<string> files = null;
            using (var dlg = new OpenFileDialog()
            {
                Title="Select Files",
                Multiselect=true
            })
            {
                if (dlg.ShowDialog()== DialogResult.OK)
                {
                    files = dlg.FileNames;
                }
            }
 
            if (files!=null)
            {
                DragFileListBox.Items.Clear();
                foreach (var f in files)
                {
                    DragFileListBox.Items.Add(f);
                }
            }
 
        }
 
        private void SaveAsTempFile(string textString)
        {
            if (!string.IsNullOrEmpty(_tempTextFile))
            {
                try
                {
                    File.Delete(_tempTextFile);
                }
                catch { }
            }
 
            File.WriteAllText(_tempTextFile, textString);
        }
 
        
 
        private void ExitButton_Click(object sender, EventArgs e)
        {
            this.Close();
        }
 
        private void SelectFileButton_Click(object sender, EventArgs e)
        {
            SelectFiles();
        }
 
        private void DragTextBox_MouseMove(object sender, MouseEventArgs e)
        {
            if (DragTextBox.Text.Trim().Length == 0) return;
            if (e.Button != MouseButtons.Left) return;
 
            string txt = DragTextBox.SelectionLength == 0 ?
                DragTextBox.Text.Trim() : DragTextBox.SelectedText.Trim();
 
            DataObject data;
            if (TextFileRadioButton.Checked)
            {
                // drag the text string as a temporary *.txt file
                SaveAsTempFile(txt);
                data = new DataObject();
                data.SetFileDropList(
                    new System.Collections.Specialized.StringCollection { _tempTextFile });
            }
            else
            {
                data = new DataObject(txt);
            }
            
            DragTextBox.DoDragDrop(data, DragDropEffects.All);
        }
 
        private void DragFileListBox_MouseMove(object sender, MouseEventArgs e)
        {
            if (DragFileListBox.SelectedItems.Count == 0) return;
            if (e.Button != MouseButtons.Left) return;
 
            var data = new DataObject();
 
            var files = new System.Collections.Specialized.StringCollection();
            foreach (var item in DragFileListBox.SelectedItems)
            {
                files.Add(item.ToString());
            }
            data.SetFileDropList(files);
 
            // start drag & drop operation
            DragFileListBox.DoDragDrop(data, DragDropEffects.All);
        }
 
        private void MainForm_FormClosed(object sender, FormClosedEventArgs e)
        {
            if (!string.IsNullOrEmpty(_tempTextFile))
            {
                try
                {
                    File.Delete(_tempTextFile);
                }
                catch { }
            }
        }
    }
}

The mostly used drag & and drop operation from external application onto AutoCAD would be dragging file name, or dragging text string. Here are some explanations about the code and dropping behaviours of AutoCAD.

1. Dragging file name

When dragging file name (single, or multiple), the file name should be stuffed into DataObject with method SetDropFileList(StringCollection), instead of one of the overloaded SetData() methods.

The argument of DragDropEffects used in DoDragDrop() method not only paly the role of showing a different mouse cursor image, but would also result in AutoCAD doing different things at dropping end. For example, when a file name, which is not a plain text/ASCII file, is dropped onto AutoCAD editor, DragDropEffects.Move makes AutoCAD insert a DWG file as block, or as OLE entity, if applicable; however, DragDropEffects.Copy makes AutoCAD open DWG file, and ignore other types of file. Also, regardless DragDropEffects, dropping DWG file onto anywhere of AutoCAD other than editor, AutoCAD would open the drawing file or files. I recommend to use DragDropEffects.All in general to that AutoCAD would do whatever it is designed to do (or not to do) at dropping end and only try other DragDropEffects specific for your special need, if any.

It is worth being pointed out that when dragging a plain text file (typically as *.txt file), AutoCAD will insert the content of the text file as MText.

2. Dragging text string

When text string is filed into DataObject by calling DataObject.SetData(string), or passed to DataObject via its Constructor (new DataObject(string)), AutoCAD receives it and create an MText entity. However, the MText is not created at the location where it is dropped (when mouse' left button releases), instead, it is created at the upper-left corner of current view of AutoCAD editor.

The trick to fix this issue (i.e. let AutoCAD create the MText at where the text string is dropped) is to save the text string into a temporary plain text file, and add this temporary file name to the DataObject by its SetDropFileList(StringCollection) method. That is why you see the extra code that create text file and clean it up when the UI is closed.

Here is the video clip showing drag & drop operation from an external application to AutoCAD;

The next article I will discuss on dragging from AutoCAD and dropping onto modeless form/window of a plugin application in AutoCAD.










Wednesday, February 3, 2021

Drag & Drop In AutoCAD (1) - The Scenarios

Drag & drop is a common computer UI operation that most computer users know. The most used drag & drop operation is drag one or more files or folders in Windows Explorer to copy/move files/folders, or drag file and drop it into an application to open it. All AutoCAD users should know that when a file is dragged from Windows Explorer and dropped into AutoCAD, it is then be either inserted into currently opened drawing in AutoCAD as block, or be opened in AutoCAD for editing/viewing.

As AutoCAD programmers, we are, of course, more interested in how to incorporate drag & drop operation into our own AutoCAD custom applications.

Firstly I want to make it clear what is standard drag & drop operation known by average computer user: user selects something on an application's UI by pressing down mouse' left button and then holds the left button down while moving the mouse - this is drag; then user moves the mouse to a desired location on the same application, or another running application, releases the mouse left button - this is drop. During the dragging, the mouse cursor should show some some kind of visual hint to indicate whether the dragged data is valid to be dropped at certain location.

In terms of drag & drop in AutoCAD use, there are 5 scenarios that might interest us, as AutoCAD programmers:

  • Drag something from AutoCAD and drop onto an external application, be it developed by ourselves or not
  • Drag something from external application, again, be it developed by ourselves or not, and drop onto AutoCAD
  • Drag something from AutoCAD and drop onto our custom plugin UI (obviously, the UI has to be modeless form/window, or we wouldn't be able to drag something from AutoCAD)
  • Drag something from our custom plugin UI and drop onto AutoCAD. In this case, our custom plugin UI can be either modal or modeless
  • Drag something from our custom plugin UI to external application developed by ourselves, or vice versa

In Windows platform, the data exchange in drag & drop process uses IDataObject interface (System.Windows.Forms.IDataObject for WinForm, and System.Windows.IDataObject for WPF). What happens is that when dragging begins, if the UI component, where the dragging begins, has a MouseMove event and has a DoDragDrop() method, then we can handle the MouseMove event to test if the left button of the move is pressed; if yes, we can then create an instance of IDataObject and stuff some data into it (say, call its SetData() method), followed by calling DoDragDrop() method, which takes the data-filled IDataObject as argument. When the mouse' left button is released at a location of UI component of the same application, or another application, if the UI component allows dropping (AllowDrop property of the UI component), we can handle Drop event, where the data is passed in as IDataObject in the event handler's argument, calling IDataObject.GetData() can retrieve the data. Obviously, depending on the data type/format that was filled into IDataObject, the code on dropping side must be able to recognize the data type/format in order to retrieve/use it as expected. Based on these understandings, now we can see what we can do with each of the 4 aforementioned scenarios.

1. Drag something from AutoCAD (entities in AutoCAD's editor) and drop onto external application. 

In this scenario, there could be 2 different cases in terms of external application: an application has no, or very little accessible API for you, as programmer, to intercept into the drag & drop operation; or an application that we developed, thus we could, it seems at least, control what/how the data dragged from AutoCAD be dropped into your application.

For the former, there is nothing we can do, obviously. 

For the latter, even we have control at the dropping/receiving end, we do not have control what/how data get into IDataObject when user drags from AutoCAD editor. In code debugging process, we can peek into the IDataObject before it is dropped, where we could find quite a few pieces of data and the data most relevant to AutoCAD among them is in the format of System.IO.MemoryStream, which we have no way to translate it into meaningful information without Autodesk exposing a suitable API for it.

Therefore, this drag & drop scenario is not a "do-able" scenario.

2. Drag something from external application and drop onto AutoCAD (editor and/or other droppable area beyond editor).

Again, there could be 2 cases: an application we do not have much control as for what/how the data is placed in IDataObject when dragging begins; and an application we build, hence have control to stuff whatever data we desire into the IDataObject for the dragging.

For the former, again, what happens for the drag & drop operation is totally depends on AutoCAD and the application. For example, when dragging *.dwg file name from Windows Explorer into AutoCAD editor triggers AutoCAD's block insertion, or opens the drawing file in AutoCAD; dropping a plain *.txt file would trigger AutoCAD to insert an MText entity; dropping most other types of file would insert an OLE entity. There is nothing we can control here. It is all depends on how the external application is developed to handle its MouseMove event and how AutoCAD is designed to react to the dropping event.

For the latter, while we can control what to be packaged into IDataObject, AutoCAD on the receiving end would only do what it is built to do, which I am no sure I know all of them, but at least I know:

  • inserting a block/or opening a drawing when a *.dwg file name is dropped
  • inserting MText when a plain text file name or text string is dropped
So, when we develop our own desktop application, if we need somehow to drag something over to an running AutoCAD session, at least we know we could do these things when the business workflow requires them.

3. Drag entities from AutoCAD's editor and drop onto our custom developed plugin's modeless UI. While AutoCAD's API does not provide something to allow us a chance to stuff IDataObject with the data we need, therefore the usual way of using IDataObject to transfer data in drag & drop operation does not work. Fortunately, since our custom plugin works inside AutoCAD process, its .NET API provide different way to let us receive dragged data when they are dropped onto our custom UI. So, it is definitely a do-able task.

4. Drag something from our custom plugin UI and drop onto AutoCAD's editor and let AutoCAD generate whatever data in the drawing, be it database-residing objects or not (most likely, they would be entities). In this case, our UI can be either modeless or modal.

5. Drag something from one side of UI and drop on the other side of UI between our custom plugin application in AutoCAD and external application developed by ourselves. In this case, as long as we make sure both ends understand data inside IDataObject. One thing to be remembered is that since the data types defined in AutoCAD managed APIs cannot be used outside AutoCAD, thus the data transferred between our plugins and our custom external application cannot contain these data type. 

I'll follow this up with a series of posts to demonstrate some code on each of the "do-able" scenarios. Stay tuned.









Wednesday, December 16, 2020

Obtaining Block's Image

 Showing a block's image is a very often-needed task in AutoCAD programming. AutoCAD drawing file stores a bitmap image in it as thumbnail image of the drawing, if the block drawing is saved properly. There are different way to retrieve the thumbnail image from drawing:

1. In the VBA era, there was a very populate COM control available DwgThumbnail.ocx, which can be placed in VBA's UserForm, or stand-alone app's UI. However, when most AutoCAD being used have moved to 64-bit VBA, this 32-bit COM control is no longer useful.

2. With .NET API, one can use Database.ThumbnailBitmap property to retrieve the block's image from block drawing file by opening it as side database.

However, the 2 approached mentioned above only work with block drawing files. What about getting the image of blocks that are in current opening drawing and show them? Well, there is also an .NET API approach to to it:

Autodesk.AutoCAD.Windows.Data.CMLContentSearchPreviews.GetBlockTRThumbnail(BlockTableRecord)

Because the APIs in namespace Autodesk.AutoCAD.Windows.Data is meant for AutoCAD's UI support, the block image generated with this method is in quite low resolution/small size. So, it is often a not satisfied solution to our custom CAD application needs.

Well, there is actually another much better API method right there available to get better images from blocks in drawing, whether the drawing is opened in AutoCAD, or not (i.e. we can open it in AutoCAD as side database). This method is

Autodesk.AutoCAD.Internal.Utils.GetBlockImage()

The good thing with this method is that one can pass image's width and height to let the method generate an image in desired size while keeping the image in relatively good quality.

With this method, I have written a demo app that retrieve images of all blocks of an opened drawing in AutoCAD and displayed them in a PaletteSet. This is quite similar one can see in "Design Center" PaletteSet window where all blocks' image are shown. The demo app also allows to retrieve block images from a not opened drawing (i.e. the drawing is opened as side database when images are retrieved). See the video clip showing how it works:

Form the video one may notice that it takes quite long time to load all block images from a file. That is because for each block the code retrieves 2 images, one is in smaller size (100 x100) while the other is in larger size (300 x 300), because I'd like to show an image in better quality and in larger size when user clicks each small image on the PaletteSet.

As one can imagine, this PaletteSet can be easily modified to simulate AutoCAD's new block inserting palette.

I am not going to post the code of the demo app itself, because the basic thing that makes it work is Internal.Utils.GetBlockImage()

Here are some code that wraps up that method to generate block image as System.IntPtr, System.Drawing.Image, or System.Windows.Media.ImageSource for different use (in Win Form application, or in WPF application):

using System;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Internal;
 
namespace UseInternalNamespace
{
    public class InternalNamespaceTool
    {
        public static System.IntPtr GetBlockImagePointer(
            string blkName, Database db, int imgWidth, int imgHeight,
            Autodesk.AutoCAD.Colors.Color backColor)
        {
            var blockId = GetBlockTableRecordId(blkName, db);
            if (!blockId.IsNull)
            {
                var imgPtr = Utils.GetBlockImage(blockId, imgWidth, imgHeight, backColor);
                return imgPtr;
            }
            else
            {
                throw new ArgumentException(
                    $"Cannot find block definition in current database: \"{blkName}\".");
            }
        }
 
        public static System.IntPtr GetBlockImagePointer(
            ObjectId blockDefinitionId, int imgWidth, int imgHeight,
            Autodesk.AutoCAD.Colors.Color backColor)
        {
            var imgPtr = Utils.GetBlockImage(blockDefinitionId, imgWidth, imgHeight, backColor);
            return imgPtr;
        }
 
        public static System.Drawing.Image GetBlockImage(
            string blkName, Database db, int imgWidth, int imgHeight, 
            Autodesk.AutoCAD.Colors.Color backColor)
        {
            return System.Drawing.Image.FromHbitmap(
                GetBlockImagePointer(blkName, db, imgWidth, imgHeight, backColor));
 
        }
 
        public static System.Drawing.Image GetBlockImage(
            ObjectId blockDefinitionId, int imgWidth, int imgHeight,
            Autodesk.AutoCAD.Colors.Color backColor)
        {
            return System.Drawing.Image.FromHbitmap(
                GetBlockImagePointer(blockDefinitionId, imgWidth, imgHeight, backColor));
        }
 
        public static System.Windows.Media.ImageSource GetImageSource(
            string blkName, Database db, int imgWidth, int imgHeight,
            Autodesk.AutoCAD.Colors.Color backColor)
        {
            var imgPtr = GetBlockImagePointer(blkName, db, imgWidth, imgHeight, backColor);
            return ConvertBitmapToImageSource(imgPtr);
        }
 
        public static System.Windows.Media.ImageSource GetImageSource(
            ObjectId blockDefinitionId, int imgWidth, int imgHeight,
            Autodesk.AutoCAD.Colors.Color backColor)
        {
            var imgPtr = GetBlockImagePointer(blockDefinitionId, imgWidth, imgHeight, backColor);
            return ConvertBitmapToImageSource(imgPtr);
        }
 
        #region private methods
 
        private static ObjectId GetBlockTableRecordId(string blkName, Database db)
        {
            var blkId = ObjectId.Null;
 
            using (var tran = db.TransactionManager.StartTransaction())
            {
                var bt = (BlockTable)tran.GetObject(db.BlockTableId, OpenMode.ForRead);
                if (bt.Has(blkName))
                {
                    blkId = bt[blkName];
                }
                tran.Commit();
            }
 
            return blkId;
        }
 
        private static System.Windows.Media.ImageSource ConvertBitmapToImageSource(IntPtr imgHandle)
        {
            return System.Windows.Interop.Imaging.CreateBitmapSourceFromHBitmap(
                imgHandle,
                IntPtr.Zero,
                System.Windows.Int32Rect.Empty,
                System.Windows.Media.Imaging.BitmapSizeOptions.FromEmptyOptions());
 
        }
 
        #endregion
    }
}





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.