Thursday, March 11, 2021

Drag & Drop In AutoCAD (3) - Drag From PlugIn UI And Drop Onto AutoCAD

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

    Drag & Drop In AutoCAD (1) - The Scenarios

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

In this post I look into the scenario of dragging from an AutoCAD plugin UI and dropping onto AutoCAD's editor. Typically, we may have a form/window UI in our plugin application, where some data representing entities to be created in drawing. User can click a button on the UI to trigger the creation in the drawing with data in the UI; or we can let user to drag certain portion of the UI (i.e. dragging some data) and drop the data onto AutoCAD editor to let AutoCAD do something with the dropped data, usually, it is to create entity or entities with the data at the location of dropping.

If one drags something on the UI and drop it onto the same UI, the code logic of doing it is like:

1. Set the drop target control's AllowDrop property to True;

2. Handle MouseMove event of the dragging target control;

3. In the MouseMove event handler, test if mouse' left button is pressed when mouse is moving;

4. If mouse' left button is pressed, it is dragging; collect data you want to be transferred for dropping and package the data into IDataObject; then call drop target control's DoDragDrop() method with the IDataObject passed in;

5. Handle drop target control's DragDrop event, where you can extract data transferred via IDataObject, and then do whatever with the data suitable to the drop target control.

Now that we want to change the drop target to AutoCAD application, not somewhere on the same UI as the drag target, the code logic for this kind of drag and drop process changes to:

1.Handle MouseMove event of drag target control on the UI;

2. In the MouseMove event handler, test if mouse' left button is pressed when mouse is moving;

3. If mouse' left button is pressed, it is dragging; collect data you want to be transferred for dropping and package the data into IDataObject; then call Application.DoDragDrop(), where you not only pass IDataObject into it, you also need to implement the abstract class of Autodesk.AutoCAD.Windows.DropTarget and pass it into Application.DoDragDrop();

4. In your custom DropTarget class, you override the abstract method OnDrop(): that is where you unpackage data from the IDataObject and do whatever you need to do with the data, mostly, it is to create entity or entities at the dropping location.

I have to point out that dragging from plugin's UI and dropping onto AutoCAD only works with modeless plugin UI.

This is the plugin UI, which is a WinForm modeless dialog:


With the form floating on top of AutoCAD, user can drag one the 2 blue labels and drop onto AutoCAD to generate a line or circle entity; or user can drag a block name from the block list box to insert a block reference at dropping location. Here is the form's code behind:

using System;
using System.Linq;
using System.Windows.Forms;
 
namespace DragFromPluginUiToAcad
{
    public partial class DrawForm : Form
    {
        public DrawForm()
        {
            InitializeComponent();
        }
 
        private void BUttonClose_Click(object sender, EventArgs e)
        {
            Visible = false;
        }
 
        private void InitailizeControls()
        {
            try
            {
                Cursor.Current = Cursors.Default;
                
                var blocks = CadHelper.GetBlockList();
 
                if (blocks.Count() > 0)
                {
                    foreach (var blk in blocks)
                    {
                        BlockListBox.Items.Add(blk);
                    }
                }
            }
            finally
            {
                Cursor.Current = Cursors.Default;
            }
 
            ComboAngle.SelectedIndex = 0;
        }
 
        private void DrawForm_FormClosing(object sender, FormClosingEventArgs e)
        {
            e.Cancel = true;
            Visible = false;
        }
 
        private void DrawForm_Load(object sender, EventArgs e)
        {
            InitailizeControls();
        }
 
        private void Linelabel_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.Button== MouseButtons.Left)
            {
                EntityInfo entInfo = new LineInfo
                {
                    Length = Convert.ToDouble(TextBoxLength.Text),
                    Angle = Convert.ToDouble(ComboAngle.Text)
                };
                EntityDropper.DropEntity(
                    thisEntityType.Line, entInfo, CadHelper.CreateLine);
            }
        }
 
        private void Circlelabel_MouseMove(object sender, MouseEventArgs e)
        {
            if (e.Button == MouseButtons.Left)
            {
                EntityInfo entInfo = new CircleInfo
                {
                    Radius=Convert.ToDouble(TextBoxRadius.Text)
                };
                EntityDropper.DropEntity(
                    thisEntityType.Circle, entInfo, CadHelper.CreateCircle);
            }
        }
 
        private void BlockListBox_MouseMove(object sender, MouseEventArgs e)
        {
            if (BlockListBox.SelectedIndex < 0) return;
 
            if (e.Button == MouseButtons.Left)
            {
                EntityInfo entInfo = new BlockInfo
                {
                    BlockName=BlockListBox.Text
                };
                EntityDropper.DropEntity(
                    thisEntityType.BlockReference, entInfo, CadHelper.CreateBlockReference);
            }
        }
    }
}

As the code shows, the dragging begins by handling MouseMove event when the left mouse button is pressed, and the data used for creating entity is passed into a static method DropEntity() from a utility class EntityDropper, where Application.DoDragDrop() will be called to complete the drag & drop operation.

First, I needed to implement custom class that is derived from abstract class Autodesk.AutoCAD.Windows.DropTarget:

using System;
using System.Windows.Forms;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.Windows;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace DragFromPluginUiToAcad
{
    public class EntityDropTarget : DropTarget
    {
        private readonly Func<DocumentEntityInfoPoint3dObjectId> _createFunction;
        private Point3d _position = Point3d.Origin;
 
        public EntityDropTarget(Func<DocumentEntityInfoPoint3dObjectId> entityCreation)
        {
            _createFunction = entityCreation;
        }
 
        public ObjectId EntityId { private setget; } = ObjectId.Null;
        public override void OnDrop(DragEventArgs e)
        {
            //Convert windows location of mouse into AutoCAD editor's
            //WCS coordinate (Point3d)
            Document dwg = Autodesk.AutoCAD.ApplicationServices.
                Application.DocumentManager.MdiActiveDocument;
 
            System.Drawing.Point pt = new System.Drawing.Point(e.X, e.Y);
            _position = dwg.Editor.PointToWorld(pt);
 
            // Get data passed from Application.DoDragDrop()
            var data = e.Data.GetData(typeof(EntityInfo)) as EntityInfo;
 
            //Create the entity
            EntityId = _createFunction(dwg, data, _position);
 
            CadApp.MainWindow.Focus();
            dwg.Editor.WriteMessage($"\nEntity {EntityId} has been created @{_position}");
        }
    }
}

As the code shows in EntityDropTarget class that the drag & drop process is completed in OnDrop() method, where the data required for creating entity is extracted from IDataObject and then the actual entity generating work is done. In order to decouple the drag & drop process with how entity is to be created, I used an injected function (Func<Document, EntityInfo, Point3d, ObjectId>) to do the entity generating work. This allows me coding different way to generate entity without having to change the code for drag & drop. For example, for most user-friendly way in real production, it would be better to use a Jig to create entity, which would allow user to position the entity more accurately. But in this article, for simplicity, I just supply a simple entity creating process. Also, it is with OnDrop() method, the code can translate the mouse's drop location into AutoCAD's coordinate, so that the code would know where to create the entity. However, because it is quite difficult for user to drop (release let button) at an accurate location without usual AutoCAD drafting assistant (object snapping, coordinate entering), the drop location obtained from OnDrop() method would often be not good enough. That is why I just said, in real production, it would be ideal to use Jig for accurate entity creation.

Here is the utility class that has a static method to actually call Application.DoDragDrop():

using System;
using System.Windows.Forms;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace DragFromPluginUiToAcad
{
    public enum EntityType
    {
        Line=0,
        Circle=1,
        BlockReference=2,
    }
 
    public class EntityDropper
    {
        public static ObjectId  DropEntity(
            System.Windows.Forms.Form userUi, 
            EntityType entType, 
            EntityInfo entityInfo,
            Func<DocumentEntityInfoPoint3dObjectId> entityCreation)
        {
            var target = new EntityDropTarget(entityCreation);
            object data;
            IDataObject dataObject;
            if (entType== EntityType.Line)
            {
                data = (LineInfo)entityInfo;
            }
            else if (entType== EntityType.Circle)
            {
                data = (CircleInfo)entityInfo;
            }
            else
            {
                data = (BlockInfo)entityInfo;
            }
            dataObject = new DataObject();
            dataObject.SetData(typeof(EntityInfo), data);
            
            CadApp.DoDragDrop(userUi, dataObject, DragDropEffects.Copy, target);
 
            return target.EntityId;
        }
    } 
}

Obviously I also need a data class for transferring entity creating information between dragging and dropping via IDataObject:

namespace DragFromPluginUiToAcad
{
    public abstract class EntityInfo
    {
    }
 
    public class LineInfo : EntityInfo
    {
        public double Angle { setget; }
        public double Length { setget; }
    }
 
    public class CircleInfo : EntityInfo
    {
        public double Radius { setget; }
    }
 
    public class BlockInfo : EntityInfo
    {
        public string BlockName { setget; }
    }
}

In order to inject an entity creating function, all the data classes (LineInfo, CircleInfo and BlockInfo) are derived from an abstract class EntityInfo, which does not have its own properties/methods and only serves as a base class, so that the injected entity creating function can have a unified method signature.

Of course I need those injectable entity creating functions:

using System;
using System.Collections.Generic;
using System.Linq;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.Geometry;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
namespace DragFromPluginUiToAcad
{
    public class CadHelper
    {
        public static IEnumerable<string> GetBlockList()
        {
            IEnumerable<string> names = null;
            var dwg = CadApp.DocumentManager.MdiActiveDocument;
            using (var tran = dwg.TransactionManager.StartTransaction())
            {
                var tbl = (BlockTable)tran.GetObject(dwg.Database.BlockTableId, OpenMode.ForRead);
                names = GetBlockNames(tbl, tran);
 
                tran.Commit();
            }
            return names;
        }
 
        public static ObjectId CreateLine(Document dwg, EntityInfo lineInfo, Point3d position)
        {
            var newId = ObjectId.Null;
 
            var len = ((LineInfo)lineInfo).Length;
            var ang = ((LineInfo)lineInfo).Angle * Math.PI / 180.0;
 
            var start = position;
            var x = position.X + len * Math.Cos(ang);
            var y = position.Y + len * Math.Sin(ang);
            var end = new Point3d(x, y, position.Z);
 
            using (dwg.LockDocument())
            {
                using (var tran = dwg.TransactionManager.StartTransaction())
                {
                    var line = new Line(start, end);  
                    line.SetDatabaseDefaults();
 
                    var space = (BlockTableRecord)tran.GetObject(
                        dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
                    newId = space.AppendEntity(line);
                    tran.AddNewlyCreatedDBObject(line, true);
 
                    tran.Commit();
                }
            }
 
            return newId;
        }
 
        public static ObjectId CreateCircle(Document dwg, EntityInfo circleInfo, Point3d position)
        {
            var newId = ObjectId.Null;
            using (dwg.LockDocument())
            {
                using (var tran = dwg.TransactionManager.StartTransaction())
                {
                    var circle = new Circle();
                    circle.Center = position;
                    circle.Radius = ((CircleInfo)circleInfo).Radius;
                    circle.SetDatabaseDefaults();
 
                    var space = (BlockTableRecord)tran.GetObject(
                        dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
                    newId = space.AppendEntity(circle);
                    tran.AddNewlyCreatedDBObject(circle, true);
 
                    tran.Commit();
                }
            }
 
            return newId;
        }
 
        public static ObjectId CreateBlockReference(Document dwg, EntityInfo blkInfo, Point3d position)
        {
            var newId = ObjectId.Null;
            using (dwg.LockDocument())
            {
                using (var tran = dwg.TransactionManager.StartTransaction())
                {
                    var bt = (BlockTable)tran.GetObject(
                        dwg.Database.BlockTableId, OpenMode.ForRead);
                    if (bt.Has(((BlockInfo)blkInfo).BlockName))
                    {
                        var blk = new BlockReference(position, bt[((BlockInfo)blkInfo).BlockName]);
                        blk.SetDatabaseDefaults();
 
                        var space = (BlockTableRecord)tran.GetObject(
                            dwg.Database.CurrentSpaceId, OpenMode.ForWrite);
                        newId = space.AppendEntity(blk);
                        tran.AddNewlyCreatedDBObject(blk, true);
 
                    }
                    tran.Commit();
                }
            }
 
            return newId; ;
        }
 
        #region private methods
 
        private static IEnumerable<string> GetBlockNames(BlockTable tbl, Transaction tran)
        {
            var names = new List<string>();
            foreach (ObjectId id in tbl)
            {
                var blk = (BlockTableRecord)tran.GetObject(id, OpenMode.ForRead);
                if (!blk.IsLayout)
                {
                    names.Add(blk.Name);
                }
            }
 
            return from n in names orderby n ascending select n;
        }
 
        #endregion
    }
}

As I have already mentioned, in these injectable entity creating function, I just create entities (Line/Circle/BlockReference) at drop location, for the reason of simplicity. In real production, I would use EntityJig for better usability.

Finally, here is the CommandClass that put everything together to work, which simply show the plugin UI as singleton modeless dialog to allow user drag something from it and drop onto AutoCAD to create entities.

using Autodesk.AutoCAD.Runtime;
using CadApp = Autodesk.AutoCAD.ApplicationServices.Application;
 
[assemblyCommandClass(typeof(DragFromPluginUiToAcad.MyCommands))]
 
namespace DragFromPluginUiToAcad
{
    public class MyCommands 
    {
        private static DrawForm _floatForm = null;
 
        [CommandMethod("AddEntByDrag"CommandFlags.Session)]
        public static void AddEntityFromModelessForm()
        {
            if (_floatForm==null)
            {
                _floatForm = new DrawForm();
            }
 
            CadApp.ShowModelessDialog(CadApp.MainWindow.Handle, _floatForm);
        }
    }
}

See this video for the code in action:


Some Thoughts

While using drag & drop to create entities from custom plugin UI gives us programmers another "fancy way" to deliver CAD user experience, it is more common to trigger the process with proper control on the UI, typically, user would click button for this operation, which can be used with both modal and modeless UI (if the UI is modal, the code needs to call Editor.StartUserInteraction() to temporarily hide the modal UI). However, if the UI is modeless, the action (of creating entities) triggered from UI should be wrapped in a custom CommandMethod and UI's event handler (for the button click) should call SendStringToExecute() (see this article of mine on this topic); but when doing drag & and drop, as shown here, the action triggered from modeless UI (i.e. dragging, and then dropping) is not wrapped in a CommandMethod and no SendStringToExecute() involved.

Again, the dropping part of drag & drop operation does not provide an accurate dropping location as AutoCAD native drafting function does, it may not be as attractive as it looks like at first, considering that user has to hold left button while moving the mouse, in comparison to a single click of a button, or a double-click on something (such as a block icon on Tool Palette, or the latest block inserting palette). But, since drag & drop is a traditional GUI trick, some users may like it, if we can provide it.



No comments:

Post a Comment