In this article, will walk through the development of a simple AutoCAD plugin using C++/CLI. The plugin will add a feature to draw circles dynamically, where users can specify the radius and the plugin will generate a random color for each circle.
We will break the plugin down into three major parts:
- AutoCAD Database Helper Class
- UI Form Implementation
- ObjectARX Entry Point
Part 1: AutoCAD Database Helper Class
In AutoCAD, entities such as circles, lines, and other objects are stored in the AutoCAD database. The AcDbHelper
class in our plugin facilitates the creation and manipulation of AutoCAD entities.
Smart Pointer for AutoCAD Database Objects
We start by creating a unique_db_ptr
template class to manage AutoCAD objects with a smart pointer. This ensures that AutoCAD objects are correctly cleaned up after use.
template struct unique_db_ptr : public std::unique_ptr
{
unique_db_ptr(T* t) : std::unique_ptr(t, closeOrDeleteDbObj) { }
static unique_db_ptr create()
{
T* newObj = new T();
return unique_db_ptr(newObj);
}
// Helper function for smart pointer cleanup
static void closeOrDeleteDbObj(AcDbObject* pObj)
{
if (pObj->objectId().isNull())
delete pObj;
else
pObj->close();
}
};
Adding Entities to the Database
We also define a method to add entities like circles to AutoCAD's model space, ensuring they are correctly appended to the database.
static bool addToDb(AcDbEntity* pEnt, AcDbDatabase* pDb = nullptr)
{
if (!pDb)
pDb = acdbHostApplicationServices()->workingDatabase();
unique_db_ptr ent(pEnt);
AcDbBlockTable* pBt;
if (Acad::eOk != pDb->getBlockTable(pBt, AcDb::kForRead))
return false;
unique_db_ptr bt(pBt);
AcDbBlockTableRecord* pMs;
if (Acad::eOk != pBt->getAt(ACDB_MODEL_SPACE, pMs, AcDb::kForWrite))
return false;
return Acad::eOk == unique_db_ptr(pMs)->appendAcDbEntity(ent.get());
}
Creating and Adding Circles
The createCircle
method creates a circle with a specified radius and color index, then adds it to the
database:
static bool createCircle(const AcGePoint3d& center, double radius, int colorIndex = 1)
{
auto circlePtr = unique_db_ptr::create();
if (!circlePtr)
return false;
AcDbCircle* circle = circlePtr.get();
circle->setDatabaseDefaults();
circle->setRadius(radius);
circle->setColorIndex(colorIndex);
circle->setCenter(center);
return addToDb(circle);
}
Part 2: UI Form Implementation
Now, let’s move on to creating the user interface (UI) that interacts with the AutoCAD database. We’ll use Windows Forms in C++/CLI for this purpose.
MainForm Class
The MainForm
class represents the UI, containing a button to draw a circle and a numeric input for the
circle radius. It uses a task-based asynchronous method to perform the drawing operation on the main AutoCAD thread.
public ref class MainForm : public Form
{
private:
Button^ drawButton;
NumericUpDown^ radiusInput;
Label^ radiusLabel;
void InitializeComponent()
{
//adding controls to the form and initialising properties
}
Task^ DrawCircleAsync(System::Object^ data)
{
//draw the circle
}
void DrawButton_Click(System::Object^ sender, System::EventArgs^ e)
{
// Handle click event
auto dm = Autodesk::AutoCAD::ApplicationServices::Core::Application::DocumentManager;
// Create the delegate with the correct syntax
auto callback = gcnew Func
ExecuteInCommandContextAsync Method
In this code, the ExecuteInCommandContextAsync
method is used to execute a callback (in this case, the
DrawCircleAsync
method) within AutoCAD's command context.
The primary reason for using this API is to
ensure that any interactions with AutoCAD, especially those that modify the drawing or perform operations on the
AutoCAD database, are executed on the correct thread, which is the AutoCAD main thread.
Why Use ExecuteInCommandContextAsync
?
AutoCAD, being a single-threaded application, has strict rules about how commands and modifications to the AutoCAD database should be performed. Interacting with AutoCAD from another thread, such as a UI thread, can cause problems because it bypasses AutoCAD's synchronization mechanisms, leading to potential crashes, invalid operations, or unexpected behavior.
SynchronizationContext Issue
When you display a WinForm dialog (like MainForm
in your code), it restores the 'previous'
SynchronizationContext
, which in this case is the default context. This default context attempts to
execute continuations using the thread pool, not the main AutoCAD thread. Since AutoCAD requires that commands (such
as modifying the database or interacting with the drawing) be run on its main thread, executing on a background
thread (via the thread pool) can cause synchronization issues.
AutoCAD doesn't like this because it expects all UI operations and database modifications to occur on the main AutoCAD thread. If you try to execute these operations on a different thread (e.g., using the thread pool), AutoCAD's internal threading model will not handle it correctly.
How ExecuteInCommandContextAsync
Solves This
By using ExecuteInCommandContextAsync
, the callback (i.e., the DrawCircleAsync
method) is
explicitly executed within AutoCAD's command context. This method ensures that the operation is correctly scheduled
on the AutoCAD thread, which means:
- Correct Threading: It guarantees that the AutoCAD API calls are made on the main AutoCAD thread, which is crucial for thread safety.
- Synchronization Context: It sets up the correct synchronization context for the operation, so any continuation (like UI updates or database operations) that happens after this method call will respect AutoCAD's threading model and ensure that UI updates (like enabling buttons) are done on the UI thread without causing conflicts.
- Asynchronous Execution: It allows the asynchronous execution of AutoCAD commands without
blocking the UI thread or causing AutoCAD to freeze while waiting for the operation to complete. The
GetResult()
method ensures that the UI thread waits for the task to finish before proceeding.
Part 3: ObjectARX Entry Point
The CArxNetCoreApp
class represents the entry point of our AutoCAD plugin. It registers the
application and launches the UI dialog.
class CArxNetCoreApp : public AcRxArxApp
{
public:
virtual AcRx::AppRetCode On_kInitAppMsg(void* pkt) {
return AcRxArxApp::On_kInitAppMsg(pkt); }
virtual AcRx::AppRetCode On_kUnloadAppMsg(void* pkt) {
return AcRxArxApp::On_kUnloadAppMsg(pkt); }
virtual void RegisterServerComponents() {}
static void MADGUIToolLaunch()
{
try
{
auto form = gcnew UIForms::MainForm();
Autodesk::AutoCAD::ApplicationServices::Application::ShowModelessDialog(form);
}
catch (System::Exception^ ex)
{
acutPrintf(L"\nException occurred: %s", ex->Message);
}
}
};
Conclusion
This plugin demonstrates how to integrate AutoCAD with C++/CLI, offering a simple yet powerful tool to interact with AutoCAD's database, create entities, and provide a user-friendly interface for drawing circles. The use of smart pointers, task-based asynchronous methods, and Windows Forms for UI design highlights the flexibility and power of combining C++/CLI with AutoCAD's ObjectARX SDK.
By following the steps outlined in this tutorial, you can build and expand upon this basic plugin to add more features and functionality to AutoCAD.