Tuesday, September 28, 2010

Pluggable PaletteSet

In ObjectARX .NET API, AutoCAD.Windows.PaletteSet class makes creating dockable floating winidow in AutoCAD a pretty easy thing to do. Many AutoCAD programmers use PaletteSet as a UI container to host a series of Palettes (Windows Form User Controls, usually).

Prior to AutoCAD 2009, PaletteSet is sealed class, e.g. it cannot be inherited as a base class to derive your own custom PaletteSet. Since AutoCAD 2009, PaletteSet class is not sealed any more. This opens a possiblity for our AutoCAD programmer to create a custom, generic PaletteSet UI container once, and develop pluggable palette based on business need and plug into the PaletteSet when needed (without any code change to the PaletteSet).

In this article, I'll show how to use interface to define a pluggable PaletteSet. Interface is commonly used to define a set operations/properties for different classes to implement. It is one of the OO programming basic concept and used in .NET programming very often. However, there are many AutoCAD programmers who may not be a professional software developers and may too focused on AutoCAD API itself and did not explore some "advanced" programming technique enough, such as using "Interface" to simplify development tasks.

For this article, I demonstrate how to use Interface to create a pluggable PaletteSet. In Visual Stadio 2008, I started a class library project called MyPaletteSet, which includes 3 code files.

First code file is a interface that defines Palette that will be hosted in the pluggable PaletteSet: IThePalette. Later, when creating a Win Form UserControl as Palette, the UserControl will implement the interface. This, no matter how do you design your UserControls and how different they would be, to the hosting PaletteSet, they all are IThePalette. That is, the hosting PaletteSet does not have knowledge of each individual UserControl it hosts, as long as the UserControl is an IThePalette.

Here is the code:

Code Snippet
  1. namespace MyPaletteSet
  2. {
  3.     public interface IThePalette
  4.     {
  5.         ThePaletteSet PaletteSet { set; get; }
  6.         string PaletteName { get; }
  7.         void Close();
  8.         void ClosePaletteSet();
  9.     }
  10. }

Second code file is the custom PaletteSet, derived from Autodesk.AutoCAD.Windows.PaletteSet. As aforementioned, it is only possible when you use AutoCAD 2009 or later.

Here is the code:

Code Snippet
  1. using System;
  2. using System.Drawing;
  3. using System.Collections.Generic;
  4.  
  5. using Autodesk.AutoCAD.Windows;
  6. using Autodesk.AutoCAD.ApplicationServices;
  7.  
  8. namespace MyPaletteSet
  9. {
  10.     public class ThePaletteSet : PaletteSet
  11.     {
  12.         private static Guid _guid =
  13.             new Guid("B9169E25-3EC1-442F-B518-46B2DA174A2F");
  14.         private DocumentCollection _dwgManager = null;
  15.         private List<IThePalette> _paltettes = new List<IThePalette>();
  16.  
  17.         public event DocumentCollectionEventHandler DwgBecameCurrent;
  18.  
  19.         public ThePaletteSet()
  20.             : base("ThePaletteSet",null, _guid)
  21.         {
  22.             this.Style = PaletteSetStyles.ShowAutoHideButton |
  23.                 PaletteSetStyles.ShowCloseButton |
  24.                 PaletteSetStyles.Snappable;
  25.  
  26.             this.Opacity = 100;
  27.             this.Dock = DockSides.None;
  28.             this.DockEnabled = DockSides.None;
  29.  
  30.             this.Size = new Size(500, 400);
  31.             this.MinimumSize = new Size(250, 200);
  32.  
  33.             _dwgManager = Autodesk.AutoCAD.ApplicationServices
  34.                 .Application.DocumentManager;
  35.  
  36.           //Handle DocumentCollection events to bubble up the event for IThePalette
  37.             _dwgManager.DocumentBecameCurrent +=
  38.                 new DocumentCollectionEventHandler(_dwgManager_DocumentBecameCurrent);
  39.         }
  40.  
  41.         private void _dwgManager_DocumentBecameCurrent(
  42.             object sender, DocumentCollectionEventArgs e)
  43.         {
  44.             if (DwgBecameCurrent != null)
  45.             {
  46.                 DwgBecameCurrent(this, e);
  47.             }
  48.         }
  49.  
  50.         public void AddPalette(IThePalette palette)
  51.         {
  52.             bool exists = false;
  53.             foreach (IThePalette plt in _paltettes)
  54.             {
  55.                 if (plt.PaletteName.ToUpper() == palette.PaletteName.ToUpper())
  56.                 {
  57.                     exists = true;
  58.                     break;
  59.                 }
  60.             }
  61.  
  62.             if (!exists)
  63.             {
  64.                 System.Windows.Forms.Control ctl =
  65.                     palette as System.Windows.Forms.Control;
  66.  
  67.                 //Add to paletteset
  68.                 this.Add(palette.PaletteName, ctl);
  69.  
  70.                 _paltettes.Add(palette);
  71.                 palette.PaletteSet = this;
  72.             }
  73.         }
  74.  
  75.         public void RemovePalette(string paletteName)
  76.         {
  77.             if (_paltettes.Count == 0) return;
  78.  
  79.             for (int i = 0; i < _paltettes.Count; i++)
  80.             {
  81.                 if (_paltettes[i].PaletteName.ToUpper() == paletteName.ToUpper())
  82.                 {
  83.                     System.Windows.Forms.Control ctl =
  84.                         _paltettes[i] as System.Windows.Forms.Control;
  85.  
  86.                     this.Remove(i);
  87.                     _paltettes.RemoveAt(i);
  88.  
  89.                     ctl.Dispose();
  90.  
  91.                     if (_paltettes.Count == 0) this.Visible = false;
  92.  
  93.                     return;
  94.                 }
  95.             }
  96.         }
  97.  
  98.         public void ActivatePalette(string paletteName)
  99.         {
  100.             if (_paltettes.Count == 0) return;
  101.  
  102.             for (int i = 0; i < _paltettes.Count; i++)
  103.             {
  104.                 if (_paltettes[i].PaletteName.ToUpper() == paletteName.ToUpper())
  105.                 {
  106.                     this.Activate(i);
  107.                     return;
  108.                 }
  109.             }
  110.         }
  111.     }
  112. }

Pay attention to this line of code:

public event DocumentCollectionEventHandler DwgBecameCurrent;

and this line of code:

_dwgManager.DocumentBecameCurrent += new .......

Some of your Palettes may be designed to handle drawing based data. Since PaletteSet is a floating/modeless window, when current drawing changed in AutoCAD, the data shown in certain palette should be refreshed because of current drawing change (like AutoCAD's "Properties" window). Therefore, the this type of palette must be able to handle various events originally raised by DocumentCollection. So, here I simply handle the DocumentCollection events in the custom PaletteSet and bubble the events up. It is up to the individual Palette to subscribe the events when necessary. To simplfy the example, I only handle and raise the DocumentBecameCurrent event. In real production code, we could bubble up all the DocumentCollection events. The third code file is contains a static help method to create an instance of the custom PaletteSet. Here is the code:

Code Snippet
  1. using Autodesk.AutoCAD.Runtime;
  2.  
  3. [assembly: ExtensionApplication(typeof(MyPaletteSet.ThePaletteSetInitializer))]
  4.  
  5. namespace MyPaletteSet
  6. {
  7.     public class ThePaletteSetInitializer : IExtensionApplication
  8.     {
  9.         private static ThePaletteSet _paletteSet = null;
  10.  
  11.         public void Initialize()
  12.         {
  13.             //If necessary, add some code
  14.         }
  15.  
  16.         public void Terminate()
  17.         {
  18.             if (_paletteSet != null) _paletteSet.Dispose();
  19.         }
  20.  
  21.         public static ThePaletteSet CreateThePaltetteSet(IThePalette palette)
  22.         {
  23.             if (_paletteSet == null)
  24.             {
  25.                 _paletteSet = new ThePaletteSet();
  26.             }
  27.  
  28.             //Add palette to the PaletteSet
  29.             _paletteSet.AddPalette(palette);
  30.  
  31.             return _paletteSet;
  32.         }
  33.  
  34.         public static ThePaletteSet CreateThePaltetteSet()
  35.         {
  36.             if (_paletteSet == null)
  37.             {
  38.                 _paletteSet = new ThePaletteSet();
  39.             }
  40.  
  41.             return _paletteSet;
  42.         }
  43.     }
  44. }

That's it. Now we have a generic, pluggable PaletteSet. Build the project. From now on, when you want to build a suite of your own tools that have UI to be hosted in a PaletteSet, you can focus to the development of the Palette (Win Form UserControl) and never need to update the PaletteSet host and redeploy it. Let's see a couple of sample palettes. Sample 1: FirstPalette. Start a new class library command. It can be in the same solution as the "MyPaletteSet". But to better understand the "pluggable" nature, it is recommended to do this project in different solution. This will show that you can really focus on developing your Palette without having to update the PaletteSet at all. Once the project created, set reference to the DLL generated in "MyPaletteSet" project (MyPaletteSet.dll). Add a Win Form UserControl, called FirstPalette. It looks like:

Here is the code of the UserControl:

Code Snippet
  1. using System;
  2. using System.Windows.Forms;
  3. using Autodesk.AutoCAD.ApplicationServices;
  4. using MyPaletteSet;
  5.  
  6. namespace FirstPaletteTool
  7. {
  8.     public partial class FirstPalette : UserControl, IThePalette
  9.     {
  10.         private ThePaletteSet _parent = null;
  11.  
  12.         public FirstPalette()
  13.         {
  14.             InitializeComponent();
  15.  
  16.             Document dwg = Autodesk.AutoCAD.ApplicationServices.
  17.                 Application.DocumentManager.MdiActiveDocument;
  18.             if (dwg != null) txtFileName.Text = dwg.Name;
  19.         }
  20.  
  21.         #region IThePalette Members
  22.  
  23.         public string PaletteName
  24.         {
  25.             get { return "First Palette"; }
  26.         }
  27.  
  28.         public ThePaletteSet PaletteSet
  29.         {
  30.             get
  31.             {
  32.                 return _parent;
  33.             }
  34.             set
  35.             {
  36.                 _parent = value;
  37.                 _parent.DwgBecameCurrent +=
  38.                     new DocumentCollectionEventHandler(
  39.                         _parent_DwgBecameCurrent);
  40.             }
  41.         }
  42.  
  43.         void _parent_DwgBecameCurrent(object sender,
  44.             DocumentCollectionEventArgs e)
  45.         {
  46.             txtFileName.Text = e.Document.Name;
  47.         }
  48.  
  49.         public void Close()
  50.         {
  51.             if (_parent != null)
  52.             {
  53.                 _parent.RemovePalette(this.PaletteName);
  54.             }
  55.         }
  56.  
  57.         public void ClosePaletteSet()
  58.         {
  59.             if (_parent != null) _parent.Visible = false;
  60.         }
  61.  
  62.         #endregion
  63.  
  64.         private void button2_Click(object sender, EventArgs e)
  65.         {
  66.             this.ClosePaletteSet();
  67.         }
  68.  
  69.         private void button1_Click(object sender, EventArgs e)
  70.         {
  71.             this.Close();
  72.         }
  73.     }
  74. }

Then add another class file into the project: FirstPaletteCommand. Here is the code:

Code Snippet
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.Runtime;
  3.  
  4. using MyPaletteSet;
  5.  
  6. [assembly: CommandClass(typeof(FirstPaletteTool.FirstPaletteCommand))]
  7.  
  8. namespace FirstPaletteTool
  9. {
  10.     public class FirstPaletteCommand
  11.     {
  12.         private static ThePaletteSet _pltSet;
  13.  
  14.         [CommandMethod("StartFirst", CommandFlags.Session)]
  15.         public static void RunThisMethod()
  16.         {
  17.             Document dwg = Application.DocumentManager.MdiActiveDocument;
  18.  
  19.             try
  20.             {
  21.                 FirstPalette plt = new FirstPalette();
  22.  
  23.                 _pltSet = MyPaletteSet.
  24.                     ThePaletteSetInitializer.CreateThePaltetteSet(plt);
  25.                 _pltSet.Visible = true;
  26.                 _pltSet.ActivatePalette(plt.PaletteName);
  27.             }
  28.             catch(System.Exception ex)
  29.             {
  30.                 dwg.Editor.WriteMessage("\nError: " + ex.Message);
  31.             }
  32.         }
  33.     }
  34. }

Since I want this palette to show per-drawing based data, thus, I make this palette subscribe event "DwgBecameCurrent" raised by the hosting PaletteSet (MyPaletteSet). As you can see, no matter what UI components you place onto this palette (UserControl) and what code logic you will let this palette to execute, you can plug the palette into a common PaletteSet easily. New, let me create another palette: SecondPalette. Start a new class library project, called "SecondPaletteTool", set reference to "MyPaletteSet.dll". Add a Win Form UserControl, which looks like:


Its code is here:

Code Snippet
  1. using System;
  2. using System.Windows.Forms;
  3.  
  4. using MyPaletteSet;
  5.  
  6. namespace SecondPaletteTool
  7. {
  8.     public partial class SecondPalette : UserControl, IThePalette
  9.     {
  10.         private ThePaletteSet _parent = null;
  11.  
  12.         public SecondPalette()
  13.         {
  14.             InitializeComponent();
  15.         }
  16.  
  17.         #region IThePalette Members
  18.  
  19.         public ThePaletteSet PaletteSet
  20.         {
  21.             get
  22.             {
  23.                 return _parent;
  24.             }
  25.             set
  26.             {
  27.                 _parent = value;
  28.             }
  29.         }
  30.  
  31.         public string PaletteName
  32.         {
  33.             get { return "Second Palette"; }
  34.         }
  35.  
  36.         public void Close()
  37.         {
  38.             if (_parent != null)
  39.             {
  40.                 _parent.RemovePalette(this.PaletteName);
  41.             }
  42.         }
  43.  
  44.         public void ClosePaletteSet()
  45.         {
  46.             if (_parent != null) _parent.Visible = false;
  47.         }
  48.  
  49.         #endregion
  50.  
  51.         private void button1_Click(object sender, EventArgs e)
  52.         {
  53.             this.Close();
  54.         }
  55.  
  56.         private void button2_Click(object sender, EventArgs e)
  57.         {
  58.             this.ClosePaletteSet();
  59.         }
  60.     }
  61. }

Notice that this palette does not subscribe event raised by hosting PaletteSet. Add a class into the project: SecondPaletteCommand:

Code Snippet
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.Runtime;
  3.  
  4. using MyPaletteSet;
  5.  
  6. [assembly: CommandClass(typeof(SecondPaletteTool.SecondPaletteCommand))]
  7.  
  8. namespace SecondPaletteTool
  9. {
  10.     public class SecondPaletteCommand
  11.     {
  12.         private static ThePaletteSet _pltSet;
  13.  
  14.         [CommandMethod("StartSecond", CommandFlags.Session)]
  15.         public static void RunThisMethod()
  16.         {
  17.             Document dwg = Application.DocumentManager.MdiActiveDocument;
  18.  
  19.             try
  20.             {
  21.                 SecondPalette plt = new SecondPalette();
  22.  
  23.                 _pltSet = MyPaletteSet.
  24.                     ThePaletteSetInitializer.CreateThePaltetteSet(plt);
  25.                 _pltSet.Visible = true;
  26.                 _pltSet.ActivatePalette(plt.PaletteName);
  27.             }
  28.             catch (System.Exception ex)
  29.             {
  30.                 dwg.Editor.WriteMessage("\nError: " + ex.Message);
  31.             }
  32.         }
  33.     }
  34. }

As you can see, no matter how different the second palette from the first one, it can be plugged into MyPaletteSet in the same way, because, to MyPaletteSet, these 2 palettes are the same type: IMyPalette.

Now, start AutoCAD and "NETLOAD" the 2 palette projects (e.g. load FirstPaletteTool.dll and SecondPaletteTool.dll separately, as if the 2 DLLs are deployed and loaded separately). Enter command "StartFirst" and/or "StartSecond". You can see the corresponding palette will be shown in a common PaletteSet. If you open more than one drawings in AutoCAD and switch the active drawing, you can see the file name shown on the "First Palette" changes accordingly, because this palette handles DwgBecameCurrent event raised by the hosting PaletteSet.

From now on, whenever I want to develop a new AutoCAD tool that would have a modeless window as UI, I can go ahead to develop the UI as Win Form UserControl. Once it is done, I can simply plug it into the common hosting PaletteSet. Since the newly developed palette is in its own project, it can be deployed independently to all existing palettes, yet they are hosted in the same PaletteSet.

In the FirstPalette, I have to add "using Autodesk.AutoCAD.ApplicationServices;" because the palette has to comsume DocumentCollectionEventArgs in the DwgBecameCurrent event handler. In real development code, it is best practice to not let the UserControl to be tied to AutoCAD's dll. In this case, it is better to define my own custom EvenArgs and my own custom EventHandler in the MyPaletteSet project and use them to bubble up the various DocumentCollection events.

Update note: due to the original posted code format, a key part of the code did not show correctly, which were the topic of the comments below. Now I updated the code posting format and show the line of code in red, which was previously shown wrong. - Norman, 2011-01-03.

Thursday, September 9, 2010

Command Method: Static Or Not Static

When starting to use AutoCAD .NET API, the very first code people would be writing is the the command method in a class, e.g. a public method decorated with [CommandMethod()] attribute. However, the command method can be either "static/Shared" (C#/VB.NET) or not "static/Shared" (instance method). Also, in most smaple code one can find, there is rarely one that show the class' constructor is used.


It also puzzles some beginners if the command method is not a "static" one: why an instance method (non-static method) can be called in command without the code creating an object instance of the class?


Here is what the AutoCAD .NET API document has to say:


[quote]
For an instance command method, the method's enclosing type is instantiated separately for each open document. This means that each document gets a private copy of the command's instance data. Thus there is no danger of overwriting document-specific data when the user switches documents. If an instance method needs to share data globally, it can do so by declaring static or Shared member variables.
For a static command method, the managed wrapper runtime module does not need to instantiate the enclosing type. A single copy of the method's data is used, regardless of the document context. Static commands normally do not use per-document data and do not require special consideration for MDI mode.
[/quote]
 
With a few lines of very simple code, we can see what the difference betweem static method and instance command method.
 
Here is the code:

using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;

namespace CommandClassTest
{
    public class MyCommand
    {
        private static int _staticCount = 0;
        private int _instanceCount = 0;

        public MyCommand()
        {
            Document dwg = Autodesk.AutoCAD.ApplicationServices.
                Application.DocumentManager.MdiActiveDocument;
            dwg.Editor.WriteMessage("\n**************");
            dwg.Editor.WriteMessage("\nConstructing...");
            dwg.Editor.WriteMessage("\n**************");
        }

        [CommandMethod("StaticCommand", CommandFlags.Session)]
        public static void RunThisMethod1()
        {
            _staticCount++;
            Document dwg = Autodesk.AutoCAD.ApplicationServices.
                Application.DocumentManager.MdiActiveDocument;
            dwg.Editor.WriteMessage("\n----------------------------");
            dwg.Editor.WriteMessage("\n" + dwg.Name);
            dwg.Editor.WriteMessage("\nStatic command executed.");
            dwg.Editor.WriteMessage("\nStatic command count: {0}", _staticCount);
        }

        [CommandMethod("InstanceCommand", CommandFlags.Session)]
        public void RunThisMethod2()
        {
            _instanceCount++;
            Document dwg = Autodesk.AutoCAD.ApplicationServices.
                Application.DocumentManager.MdiActiveDocument;
            dwg.Editor.WriteMessage("\n----------------------------");
            dwg.Editor.WriteMessage("\n" + dwg.Name);
            dwg.Editor.WriteMessage("\nInstance command executed.");
            dwg.Editor.WriteMessage("\nInstance command count: {0}", _instanceCount);
        }
    }
}

Let's run this code in AutoCAD:

1. Start AutoCAD and "NETLOAD" to DLL;
2. With current drawing "Drawing1.dwg", enter command "InstanceCommand", the command line shows:

Command: instancecommand

**************
Constructing...
**************
----------------------------
Drawing1.dwg
Instance command executed.
Instance command count: 1

As you can see, the class' constructor is called due to the instance (non-static) command method.

3. Now, run "InstanceCommand" command again with "Drawing1.dwg". We now can guess that the constructor of the class will not run again since the class instance has already been created with this drawing (Drawing1.dwg), and the count for instance command will increment to 2. Here is the command line response, as expected:

Command: instancecommand
-------------------------------
Drawing1.dwg
Instance command executed.
Instance command count: 2

4. Let's run the static command. The command line shows:
 
Command: staticcommand
--------------------------
Drawing1.dwg
Static command executed.
Static command count: 1
 
5. Now, open a new drawing in AutoCAD: Drawing2.dwg.
6. Run the static command. we would expect the static command count to increment to 2, like this:
 
Command: staticcommand

--------------------------
Drawing2.dwg
Static command executed.
Static command count: 2

7. Run instance command in Drawing2.dwg. the command shows:
 
Command: instancecommand

**************
Constructing...
**************
----------------------------
Drawing2.dwg
Instance command executed.
Instance command count: 1


As we can see, the class' constructor is called again, because the instance command runs in drawing2.dwg the first time.
 
8. Now go back to drawing1.dwg.
9. Run static command. we surely know the static command count will be 3, in spite of drawing switching:
 
Command: staticcommand

--------------------------
Drawing1.dwg
Static command executed.
Static command count: 2

10. Run instance command in drawing1.dwg, expecting that the instance command count in drawing1.dwg will be 3 and no class' constructor is called. Here is command line shows:
 
Command: instancecommand

-------------------------------
Drawing1.dwg
Instance command executed.
Instance command count: 3

Summery:
 
If your class/class method needs to manipulate data/object accross drawings in an AutoCAD session, you use static method and the data/object can be static data/object member of the class. If your static data/object member of the class has to be initialized, you cannot do it in the class' constructor. In this case, you usually delare the static data/object to a default value/null like this:
 
private static int _count=0;
 
or
 
private static MyObject _obj=null;
 
Then, in the static command method, you do:
 
if (_count==0)
{
    //Initialize to other value if necessary
}
//then use the static data accross drawings
 
or
 
if (_obj==null)
{
    _obj=new MyObject(); //instantiate the data object if it hasn't been.
}
//the use the static object accross drawings
 
If the data/object to be manipulated by the class/class method is drawing specific, for example a drawing tracking/index data object, then you can declare the data/object as the class' data/object member (non-static), and initilize it in the class' constructor. You use instance method to manipulate the data/object

Wednesday, September 1, 2010

Highlight AutoCAD Entity in Your Own Way with Highlight Overrule

Highlighting an AutoCAD entity is usually used as a visual hint that the entity is selected by user. Sometimes, when you develop an AutoCAD application, you may want to highlight some entities in interest in a way that is a bit different from AutoCAD does regularly, so that the entities would visually stand out to catch user's attention.

One way to do your own entity highlighting would be to use TransientGraphics object, which I have discussed previously here.

Now, I'd like show how to customize entity highlighting with Overrule, more precisely, HighlightOverrule. Do use Overrule, you need to use AutoCAD 2010 or later.

Scenario: there are polylines drawing in default color (white). When the mouse cursor hovers on it or clicks on it, AutoCAD highlights the polyline in the same color. I'd like the color to be different when highkighted.

Here is the code:


class MyHighlightOverrule.cs


using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;

namespace HighlightOverruleSample
{
public class MyHighlightOverrule : HighlightOverrule
{
private int _colorIndex = 1;
private int _oldColorIndex;

public MyHighlightOverrule()
{
AddOverrule(RXClass.GetClass(typeof(Polyline)), this, true);
}

public int ColorIndex
{
set { _colorIndex = value; }
get { return _colorIndex; }
}

public override void Highlight(
Entity entity, FullSubentityPath subId, bool highlightAll)
{
Polyline pline = entity as Polyline;
if (pline == null) return;

Database db= entity.Database;
Document dwg=Application.DocumentManager.MdiDocument;

using (DocumentLock dl = dwg.LockDocument())
{
using (Transaction tran = db.TransactionManager.StartTransaction())
{
pline.UpgradeOpen();
_oldColorIndex = pline.ColorIndex;
pline.ColorIndex = _colorIndex;
pline.DowngradeOpen();
}
}

base.Highlight(entity, subId, highlightAll);
}

public override void Unhighlight(
Entity entity, FullSubentityPath subId, bool highlightAll)
{
Polyline pline = entity as Polyline;
if (pline == null) return;

Database db = entity.Database;
Document dwg=Application.DocumentManager.MdiDocument;

using (DocumentLock dl = _dwg.LockDocument())
{
using (Transaction tran = db.TransactionManager.StartTransaction())
{
pline.UpgradeOpen();
pline.ColorIndex = _oldColorIndex;
pline.DowngradeOpen();
}
}

base.Unhighlight(entity, subId, highlightAll);
}
}
}


class Commands.cs


using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;

namespace HighlightOverruleSample
{
public class Commands
{
private static MyHighlightOverrule _hlOverRule = null;

[CommandMethod("MyHLOn")]
public void TurnOnMyHighlight()
{
if (_hlOverRule == null)
{
_hlOverRule = new MyHighlightOverrule();
}

MyHighlightOverrule.Overruling = true;
}

[CommandMethod("MyHLOff")]
public void TurnOffMyHighlight()
{
if (_hlOverRule == null) return;

MyHighlightOverrule.Overruling = false;
}

[CommandMethod("HLColor")]
public void SetHighlightColor()
{
Document dwg = Autodesk.AutoCAD.ApplicationServices.
Application.DocumentManager.MdiActiveDocument;

Editor ed = dwg.Editor;

PromptIntegerOptions opt = new PromptIntegerOptions(
"\nEnter color index (a number form 0 to 7):");

PromptIntegerResult res = ed.GetInteger(opt);

if (res.Status == PromptStatus.OK)
{
_hlOverRule.ColorIndex = res.Value;
}
}
}
}



To see the custom highlight effect, click here.


Of course, you'd be very likely show custom highlight only with polylines (or whatever entities, for that matters) in interest, instead of all polylines. You can use one of the SetXXXXXXFilter() methods of the Overrule object to filter out the polylines/entities to be highlighted in customized way.



Besides changing color, you can change the polyline's other properties that have visual effect when being changed, such as thickness. Well, do remember to save the properties being changed in the overriden Highlight() method and restore them back in the overriden Unhighlight() method.

Tuesday, March 2, 2010

Retrieving and Updating Acad Block's Attributes with Managed ObjectARX API

Dealing block's attribute is one of the most common drafting task, naturally, it also the most common AutoCAD programming task.

Wandering amongst various AutoCAD programming related user groups online, I see there are questions on how to update/retrieve block's attributes raised/answered very often, again and again.

Here I post some code I use to update/retrieve block attribute. I use AutoCAD managed ObjectARX API.

Assumption: before the following code can be used, I assume the targeting block (BlockReference object) is known, e.g. the previous code has already found the tarting block refernce's ObjectId from a drawing's working database.



Document dwg=Application.DocumentManager.MdiDocument;
Database db=dwg.Database;

//code here to find taget block's ObjectId
ObjectId blkRefID=FindBlockReference(...);

Dictionary dic=new Dictionary();

//For each attribute in interest, add an item into
//the Dictionary with tag as key
dic.Add("TAG1","")
dic.Add("TAG2","")
dic.Add("TAG3","")
dic.Add("TAG4","")
dic.Add("TAG5","")

//Retrieve attribute value from the block
RetrieveBlockAttributeValues(db, blkRefID, ref dic);

//Show retrieved attribute value
foreach (KetValuePair item in dic)
{
dwg.Editor.WriteMessage(
"\nAttribute {0}={1}", item.Key, item.Value);
}

//Update attribute value
dic.Add("TAG1","AAAAA")
dic.Add("TAG2","BBBBB")
dic.Add("TAG3","CCCCC")
dic.Add("TAG4","DDDDD")
dic.Add("TAG5","EEEEE")

UpdateBlockAttribiuteValues(db, blkRefID, dic);






Here is the method to retrieve attribute values



private void RetrieveBlockAttributeValues(
Database db, ObjectId blkId, ref Dictionary dic)
{
using (Transaction tran = db.TransactionManager.StartTransaction())
{
//Get BlcolReference
BlockReference blockRef =
(BlockReference)tran.GetObject(blockRefId, OpenMode.ForRead);

foreach (ObjectId id in blockRef.AttributeCollection)
{
AttributeReference attRef =
(AttributeReference)tran.GetObject(id, OpenMode.ForRead);

if (dic.ContainsKey(attRef.Tag.ToUpper()))
{
dic[attRef.Tag.ToUpper()] = attRef.TextString;
}
}

tran.Commit();
}
}






Here is the method to update attribute values



private void UpdateBlockAttributeValues(
Database db, ObjectId blkId, Dictionary dic)
{
using (Transaction tran = db.TransactionManager.StartTransaction())
{
//Get BlcolReference
BlockReference blockRef =
(BlockReference)tran.GetObject(blockRefId, OpenMode.ForRead);

foreach (ObjectId id in blockRef.AttributeCollection)
{
AttributeReference attRef =
(AttributeReference)tran.GetObject(id, OpenMode.ForRead);

if (dic.ContainsKey(attRef.Tag.ToUpper()))
{
attRef.TextString=dic[attRef.Tag.ToUpper()];
}
}

tran.Commit();
}
}



As you can see the code to update and retrieve attributes' value to/from a block is pretty simple.

With a Dictionary object as the parameter used in the 2 methods of retrieving/updating attribute value, you are free to decide which attribute's value to retrieve/update. Say, a block has 10 attributes, named as "A", "B", "C"..., you can add 10 entry in the Dictionary with the 10 tags ("A", "B","C"...) as key, or you can only add 1 entry into the Dictionary, say, "A", should you are only interested in retrieve/update only that single attribute with tag as "A".

Another thing to pay attention is that Dictionary's Key (string) is case-sensitive. So, in my code I use string.ToUpper() to compare Dictionary's key with block attribute's tag when doing retrieving/updating.

From this code sample, one can realize that retrieving/updating block attribute value is an easy task as long as you know the block's tags in interest. If you can obtain a block's attribute tag list, you will never need to re-write code to retrieve/update block's attribute values (e.g. code in afore-mentioned 2 methods). In my next post, I'll talk about how to make block attribute configurable, so that you do not need to modify your code in your Acad applications due to possible changes made to a block/attributes.

Friday, July 24, 2009

Help AutoCAD Users Visually with Transient Graphics – Part 2

In my previous article (Part 1), I showed how to use AutoCAD (2009 or later) Transient Graphics to highlight entities in interest when mouse hover them. In this article, I continue on the topic of using Transient Graphics to help CAD users.

When VBA was adopted in AutoCAD (R14.01, I think), many AutoCAD programmers jumped into it and they were very pleased with VBA making AutoCAD programming a lot more smooth and powerful. However, there was one missing thing that makes Lisp programmer often laughing at VBA programmer: when a VBA programmer tries to make a VBA operation more like AutoCAD built-in command, such as move/copy/insert…, there is no way to generate a ghost image during the command execution, like AutoCAD does, while Lisp programmers can simply do (command “…”). Many VBA programmers take pains in vain trying to find a solution, and I saw questions on this regard posted in VBA forum from time to time.

Of course, this can be done using Jig with C++ ObjectARX in any version of AutoCAD, and since ObjectARX .NET API finally available (AutoCAD 2005, but 2006 is the first useable version), doing a Jig with .NET API is not that difficult any more. But it is still fairly advanced programming tricks in term of ObjectARX .NET API.

Now, with Transient Graphics available since AutoCAD 2009, we can achieve the visual effect similar to AutoCAD Jig API with transient graphics with easier coding (comparing to coding a Jig).

I am going to create a polyline transient graphic jig (yes, polyline again, because my current CAD related work mainly deals with closed polyline. You can easily extend the idea showed here to other types of entity).

Here is the code:



using System;
using System.Collections.Generic;
using System.Text;

using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Geometry;
using Autodesk.AutoCAD.GraphicsInterface;

[assembly: CommandClass(typeof(TransientGraphicsSample.ADSCommands))]

namespace TransientGraphicsSample
{
public class ADSCommands : IExtensionApplication
{
public ADSCommands()
{
//
// TODO: Add constructor logic here
//
}

#region Show TG Jig

[CommandMethod("TGJig")]
public static void ShowTGJig()
{
Document dwg = Application.DocumentManager.MdiActiveDocument;

TGPolylineJig jig = new TGPolylineJig(dwg);

jig.ShowTGJig();
}

#endregion
}
public class TGPolylineJig
{
private Polyline mTGPolyline=null;
private Polyline mTargetPolyline;
private Editor mEditor;
private Database mDB;
private Document mDoc;

private Point3d mBasePoint;
private int mColorIndex = 1;

public TGPolylineJig(Document doc)
{
mDoc = doc;
mDB = doc.Database;
mEditor = doc.Editor;
}

public void ShowTGJig()
{
//Option to pick the target polyline
PromptEntityOptions eOpt =
new PromptEntityOptions("\nPick a polyline:");

//Only allow to pick polyline
eOpt.SetRejectMessage("\nYou must pick a polyline:");
eOpt.AddAllowedClass(typeof(Polyline), true);

//Do the pick
PromptEntityResult eRes = mEditor.GetEntity(eOpt);
if (eRes.Status != PromptStatus.OK) return;

//Get the target polyline
ObjectId targetId = eRes.ObjectId;
Polyline target = GetPolyline(targetId);
if (target == null) return;

mTargetPolyline = target;

//Prompt to pick base point and exit
PromptPointOptions opt1 =
new PromptPointOptions("\nClick base point:");
PromptPointOptions opt2 =
new PromptPointOptions("\nClick anywhere to exit:");

//Pick base point
PromptPointResult res = mEditor.GetPoint(opt1);
if (res.Status != PromptStatus.OK) return;

mBasePoint = res.Value;

//Set base point for second pick so that a rubberband
//will be shown when mouse is moving
opt2.BasePoint = mBasePoint;
opt2.UseBasePoint = true;

//Start monitoring pointer move
mEditor.PointMonitor +=
new PointMonitorEventHandler(mEditor_PointMonitor);

//Start asking user to do second pick (exit)
//so that the "jig" image will move with mouse
mEditor.GetPoint(opt2);

//Once the second point is picked, clear the
//transient graphic "jig". After this, since
//a new point is obtained with the jig;s visual
//help, one can then do copy/move/insert...
ClearTGPolyline();

//End pointer move monitoring
mEditor.PointMonitor -=
new PointMonitorEventHandler(mEditor_PointMonitor);
}

private void mEditor_PointMonitor
(object sender, PointMonitorEventArgs e)
{
//Whenever point moves, draw a new transient
//graphic image at mouse point
DrawTGPolyline(e.Context.RawPoint);

//rotate colorindex, so the jig's color
//changes with mouse move, thus more eye-catching
mColorIndex += 1;
if (mColorIndex > 8) mColorIndex = 1;
}

private Polyline GetPolyline(ObjectId id)
{
Polyline pl = null;

using (Transaction tran =
mDB.TransactionManager.StartOpenCloseTransaction())
{
pl = tran.GetObject(id, OpenMode.ForRead) as Polyline;
tran.Commit();
}

return pl;
}

private void DrawTGPolyline(Point3d movePoint)
{
//Clear existing transient graphic image
ClearTGPolyline();

//Get displacement increments of current mouse
//point against picked base point
double x = movePoint.X - mBasePoint.X;
double y = movePoint.Y - mBasePoint.Y;

//Create a polyline for the transient graphics
mTGPolyline =
new Polyline(mTargetPolyline.NumberOfVertices);

for (int i = 0; i < mTargetPolyline.NumberOfVertices; i++)
{
Point2d p = mTargetPolyline.GetPoint2dAt(i);
Point2d pt = new Point2d(p.X + x, p.Y + y);

mTGPolyline.AddVertexAt(i, pt, 0.0, 1.0, 1.0);
}

mTGPolyline.Closed = mTargetPolyline.Closed;
mTGPolyline.SetDatabaseDefaults();

//Set color
mTGPolyline.ColorIndex = mColorIndex;
mTGPolyline.LineWeight = LineWeight.LineWeight200;

//Add transient graphics
IntegerCollection col = new IntegerCollection();
TransientManager.CurrentTransientManager.
AddTransient
(
mTGPolyline,
TransientDrawingMode.DirectShortTerm,
128,
col
);
}

private void ClearTGPolyline()
{
if (mTGPolyline != null)
{
//Remove transient graphic effect
IntegerCollection intCol =
new IntegerCollection();

TransientManager.CurrentTransientManager.
EraseTransient(mTGPolyline, intCol);

//dispose the polyline used to
//Show transient graphic effect
mTGPolyline.Dispose();
mTGPolyline = null;
}
}
}
}



You can click here to show a short video clip.

As you can see, the code is surprisingly simple. There are a lot of enhancements you can do with this code base. For example, as previously stated, you can use other entity types rather than polyline; you can select a group of entities and do the jig on all the selected entities; you can build a small dialog box to allow user to configure LineType, Color, LineWeight for the transient graphic object before it is shown. One possible enhancement might be a bit difficult and tricky: what if the selected target entity is a BlockReference? Well, someone may want to try it.

I may continue on the topic of transient graphic in next post.

Sunday, July 19, 2009

Help AutoCAD Users Visually with Transient Graphics – Part 1

An AutoCAD drawing could contains tens or hundreds of thousands entities, which makes it very difficult for a CAD user to find certain entities or specific type of entities he/she is trying to work with. Surely, the CAD user can use “quick select” tool, or other means provided by AutoCAD out of box to find locate what he/she is looking for. However, it would be a nice help if we programmers could make a visual help tool, which would visually prompt the CAD user of the targeting entities when he/she move the mouse around over the entities, or do a window selecting (of many entities). Actually AutoCAD has already provided this kind of capability, such as Quick Properties. But to do this kind of stuff in our own program was not an easy task, or not even possible if you do not do C++, until AutoCAD 2009, in which Transient Graphics API is available for .NET API development.

In this article, I’ll show how to use Transient Graphics to visually “highlight” certain type of entity (lightweight polyline in my code, but you can easily modify the code to target other type of entity): when user moves the cursor over certain type of entity, the entity is highlighted, while the cursor moves away, the highlighting graphics goes away, too.

Here is the code, followed by some explanation:




using System;

using System.Collections.Generic;

using System.Text;

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.GraphicsInterface;



[assembly: CommandClass(typeof(TransientGraphicsSample.TGCommands))]



namespace TransientGraphicsSample

{

public class TGCommands

{

public TGCommands()

{

//

// TODO: Add constructor logic here

//

}



#region Polyline Transient Graphics test



private static PolylineTGTracks mTGTracks = null;

private static IPolylineTGFilter mFilter = null;



[CommandMethod("PolyTGOn")]

public static void TurnOnPolylineTG()

{

if (mTGTracks == null) mTGTracks = new PolylineTGTracks();



//Instantiate polyline filter

if (mFilter == null) mFilter = new MyPolylineTGFilter();



Document dwg =

Application.DocumentManager.MdiActiveDocument;



mTGTracks.

SetMouseOverEntityTransientGraphics

(dwg, mFilter, true);

}



[CommandMethod("PolyTGOff")]

public static void TurnOffPolyTG()

{

if (mTGTracks == null) return;



Document dwg =

Application.DocumentManager.MdiActiveDocument;



mTGTracks.SetMouseOverEntityTransientGraphics

(dwg, null, false);

}



#endregion

}



// A class to hold information of transient graphics created

// in a document

public class TGInformation

{

private Document mDwg;

private Editor mEditor;

private Database mDB;



// Targed entity. Should be a Polyine in this example

private ObjectId mCurrentMouseOverObject = ObjectId.Null;



// A polyline object used as transient graphic object

private Polyline mMouseOverTGObject = null;



// Flaging if the transient graphic visual prompt takes effect

private bool mMouseOverEntityMonitored = false;



// Visual prompt's color

private int mTGColorIndex=1;



// Width of the polyline as transient graphic object

private double mLineWidth = 1.0;



// Targeting polyline filter. See more explanation on

// IPolylineTGFilter interface and its implementation

private IPolylineTGFilter mFilter;



// Constructor

public TGInformation

(

Document dwg,

IPolylineTGFilter filter

)

{

mDwg = dwg;

mEditor = mDwg.Editor;

mDB = mDwg.Database;

mFilter = filter;

}



// Overloaded Constructor

public TGInformation

(

Document dwg,

IPolylineTGFilter filter,

double linewidth

)

{

mDwg = dwg;

mEditor = mDwg.Editor;

mDB = mDwg.Database;

mFilter = filter;

mLineWidth = linewidth;

}



public double TGLineWidth

{

set { mLineWidth = value; }

get { return mLineWidth; }

}



// This property indicates if Mouse-Over

// visual effect is set to on or off

public bool MouseOverEntityMonitored

{

set

{

if (mMouseOverEntityMonitored == value)

{

return;

}



if (value)

{

BeginMonitoringMouseOverEntity();

}

else

{

EndMonitoringMouseOverEntity();

}

}

get

{

return mMouseOverEntityMonitored;

}

}



#region private methods



private void BeginMonitoringMouseOverEntity()

{

//Clear previous transient graphic objec

ClearMouseOverTGObject();



mCurrentMouseOverObject = ObjectId.Null;



//Start handling Editor.PointMinotor event

mEditor.PointMonitor +=

new PointMonitorEventHandler(mEditor_PointMonitor);



mMouseOverEntityMonitored = true;

}



private void EndMonitoringMouseOverEntity()

{

//Remove Ediotr.PointMonitor event handler

mEditor.PointMonitor -=

new PointMonitorEventHandler(mEditor_PointMonitor);



mMouseOverEntityMonitored = false;



//Clear exisitn transient graphic object

ClearMouseOverTGObject();

mCurrentMouseOverObject = ObjectId.Null;

}



// Handling Editor.PointMonitor event. so that

//if the cursor is hovering on targeting polyline,

//transient graphic object is created, thus,

//the targeting polyline is highlighted

void mEditor_PointMonitor(object sender,

PointMonitorEventArgs e)

{

FullSubentityPath[] entPaths =

e.Context.GetPickedEntities();



if (entPaths.Length > 0)

{

//When the cursor does hover something

//Get the ObjectId of the entity

FullSubentityPath entPath = entPaths[0];



ObjectId id = entPath.GetObjectIds()[0];



//If the entity is polyline, in this example

if (id.ObjectClass ==

Polyline.GetClass(typeof(Polyline)))

{

//Add your filter here, to decide if

//this polyline is one of the targeting

//polylines in interest

if (mFilter.IsTGTarget(id))

{

//Set the transient graphic highlight

AddOrModifyMouseOverTGObject(id);

}

}

}

else

{

//No entity present below the cursor,

//so clear transient graphic highlight

ClearMouseOverTGObject();

}

}



//Do the transient graphic highlighting

private void AddOrModifyMouseOverTGObject

(ObjectId id)

{

if (mCurrentMouseOverObject == id)

{

//If the cursor is moving over on the

//same targeting polyline, update the

//highlight (changing its color) to

//achieve eye-catching effect

ModifyMouseOverTG();

}

else

{

//Create a new transient graphic object

mCurrentMouseOverObject = id;

AddMouseOverTGObject();

}

}



// Clear existing transient graphic effect

private void ClearMouseOverTGObject()

{

if (mMouseOverTGObject != null)

{

//Remove transient graphic effect

IntegerCollection intCol =

new IntegerCollection();



TransientManager.CurrentTransientManager.

EraseTransient(mMouseOverTGObject, intCol);



//dispose the polyline used to

//show transient graphic effect

mMouseOverTGObject.Dispose();

mMouseOverTGObject = null;

}

}



//Create new transient graphic object

private void AddMouseOverTGObject()

{

//Get the targeting polyline

Polyline target = GetTargetPolyline();



//Create transient graphic polyline,

//which the exactly the same as

//the targeting polyline geometrically

mMouseOverTGObject =

new Polyline(target.NumberOfVertices);



//Set vertices' coordinate and width

for (int i = 0; i < target.NumberOfVertices; i++)

{

Point2d pt = target.GetPoint2dAt(i);

mMouseOverTGObject.AddVertexAt

(i, pt, 0.0, mLineWidth, mLineWidth);

}



//Set color

mMouseOverTGObject.ColorIndex = mTGColorIndex;



//Let the color changes from 1 to 8

//so that when mouse moves, highlighting

//color changes

mTGColorIndex += 1;

if (mTGColorIndex > 8) mTGColorIndex = 1;


mMouseOverTGObject.Closed = target.Closed;

mMouseOverTGObject.SetDatabaseDefaults();



//Add transient graphics

IntegerCollection col = new IntegerCollection();



TransientManager.CurrentTransientManager.

AddTransient

(

mMouseOverTGObject,

TransientDrawingMode.DirectShortTerm,

128,

col

);

}



private void ModifyMouseOverTG()

{

if (mMouseOverTGObject==null)

{

AddMouseOverTGObject();

}

else

{

Polyline target = GetTargetPolyline();



//reset vertices' coordinate, in case the user

//changed the targeting polyline by dragging

//its grip

for (int i = 0;

i < target.NumberOfVertices - 1; i++)

{

Point2d pt = target.GetPoint2dAt(i);

mMouseOverTGObject.SetPointAt(i, pt);

}



//Set color

mMouseOverTGObject.ColorIndex =

mTGColorIndex;



mTGColorIndex += 1;

if (mTGColorIndex > 8) mTGColorIndex = 1;




//Update transient graphics

IntegerCollection col =

new IntegerCollection();



TransientManager.CurrentTransientManager.

UpdateTransient(mMouseOverTGObject, col);

}

}



private Polyline GetTargetPolyline()

{

Polyline target=null;



using (Transaction tran =

mDB.TransactionManager.

StartOpenCloseTransaction())

{

target = tran.GetObject(

mCurrentMouseOverObject,

OpenMode.ForRead) as Polyline;

}



return target;

}



#endregion

}



// A Dictionary collection to hold transient graphics

// information for each opened document

public class PolylineTGTracks :

Dictionary

{

#region public methods



public void SetMouseOverEntityTransientGraphics

(

Document dwg,

IPolylineTGFilter filter,

bool turnOn

)

{

if (turnOn)

{

if (!this.ContainsKey(dwg))

{

TGInformation tgInfo =

new TGInformation(dwg,filter);



this.Add(dwg, tgInfo);

}



//Start monitor mouse moving and

//do the transient graphic highlight

//when targeted polyline is under

//under the mouse cursor

this[dwg].MouseOverEntityMonitored = true;

}

else

{

if (this.ContainsKey(dwg))

{

//Stop transient graphic effect

this[dwg].MouseOverEntityMonitored = false;

}

}

}



//overload, taking a parameter for

//specifc line width

public void SetMouseOverEntityTransientGraphics

(Document dwg, IPolylineTGFilter filter,

double linewidth, bool turnOn)

{

if (turnOn)

{

if (!this.ContainsKey(dwg))

{



TGInformation tgInfo =

new TGInformation(

dwg, filter,linewidth);



this.Add(dwg, tgInfo);

}

else

{

this[dwg].TGLineWidth = linewidth;

}



this[dwg].MouseOverEntityMonitored = true;

}

else

{

if (this.ContainsKey(dwg))

{



this[dwg].MouseOverEntityMonitored



= false;



}

}

}



#endregion

}



// Interface used as filter to find targeting polyline.

// Implement this interface based on your need.

// For example, you can decide if a polyline is the

// targeting entity based on its layer, layout, XData...

public interface IPolylineTGFilter

{

bool IsTGTarget(ObjectId id);

}



//My implementation of IPolylineTGFilter in

//this example: all polylines are targeted

public class MyPolylineTGFilter : IPolylineTGFilter

{



#region PolylineTGFilter Members



public bool IsTGTarget(ObjectId id)

{

//I could do this



//====================



//Entity ent=GetEntityByObjectId(id);



//if ent.Layer!="Layer1") return false;



//=====================



return true;

}



#endregion

}

}



OK, the abundant comments between code lines should be explanative enough, let take look the result of running this code.

To run the code, start AutoCAD (2009/2010), “Netload” the code, draw a couple of polylines, then enter command “PolyTGOn”. Now make cursor move over/out one of the polylines. You would see a very eye-catching highlight appear along the polyline, when the cursor hangs over the target polyline and dispears when the cursor moves out the polyline. See a short video clip here fore the result

You would also notice the highlighting color changes with the move of the cursor, which makes a strong visual prompt, doesn’t it?

Now, open another new drawing and draw a few polylines. However, if you move the mouse cursor over on those polylines, no visual highlighting shows, until you issue command “PolyTGOn” to this drawing.

Of course, if you now switch back to previous drawing, the transient graphic effect is still there until you issue command “PolyTGOff”.

An obviously possible enhancement to this code would be to allow user to configure MyPolylineTGFilter class, so that the transient graphic effect can appear on desired entities according to user inputs.

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.