Displaying a progress meter during long operations in AutoCAD using .NET

It's often desirable to show a progress meter during lengthy operations. Although there's currently no public API to make use of AutoCAD's progress meter from .NET, there are nevertheless a couple of approaches to doing so.

In this post I'll show how to do this using P/Invoke (using some code borrowed from Fenton Webb, from DevTech Americas) and in my next post I'll show how to use the "internal" AutoCAD managed assembly.

Here's the C# code that uses P/Invoke, which should work for AutoCAD 2007 and 2008:

using Autodesk.AutoCAD.Runtime;

using System.Runtime.InteropServices;

using System.Windows.Forms;

namespace ProgressMeterTest

{

  public class Cmds

  {

    [DllImport(

      "acad.exe",

      CharSet = CharSet.Auto,

      CallingConvention = CallingConvention.Cdecl,

      EntryPoint = "?acedSetStatusBarProgressMeter@@YAHPB_WHH@Z"

      //This should work for AutoCAD 2006...

      //EntryPoint = "?acedSetStatusBarProgressMeter@@YAHPBDHH@Z"

  )]

    private static extern int

      acedSetStatusBarProgressMeter(

        string label,

        int minPos,

        int maxPos

      );

    [DllImport(

      "acad.exe",

      CharSet = CharSet.Auto,

      CallingConvention = CallingConvention.Cdecl,

      EntryPoint = "?acedSetStatusBarProgressMeterPos@@YAHH@Z"

    )]

    private static extern int

      acedSetStatusBarProgressMeterPos(int pos);

    [DllImport(

      "acad.exe",

      CharSet = CharSet.Auto,

      CallingConvention = CallingConvention.Cdecl,

      EntryPoint = "?acedRestoreStatusBar@@YAXXZ"

    )]

    private static extern int acedRestoreStatusBar();

    [CommandMethod("PB")]

    public void ProgressBar()

    {

      acedSetStatusBarProgressMeter("Testing Progress Bar", 0, 100);

      for (int i = 0; i <= 100; i++)

      {

        for (int j = 0; j <= 10; j++)

        {

          System.Threading.Thread.Sleep(1);

          acedSetStatusBarProgressMeterPos(i);

          // This allows AutoCAD to repaint

          Application.DoEvents();

        }

      }

      acedRestoreStatusBar();

    }

  }

}

And here's what you see when it runs:

Progress_meter_1

Update:

Thanks to Chris Bray for pointing out the above technique (and the one I was about to show in Part 2) is unnecessary from AutoCAD 2007 onwards. A new class was introduced in AutoCAD 2007 called Autodesk.AutoCAD.Runtime.ProgressMeter.

Here's some C# code that demonstrates the use of this class:

using Autodesk.AutoCAD.Runtime;

using System.Runtime.InteropServices;

using System.Windows.Forms;

namespace ProgressMeterTest

{

  public class Cmds

  {

    [CommandMethod("PB")]

    public void ProgressBarManaged()

    {

      ProgressMeter pm = new ProgressMeter();

      pm.Start("Testing Progress Bar");

      pm.SetLimit(100);

      // Now our lengthy operation

      for (int i = 0; i <= 100; i++)

      {

        System.Threading.Thread.Sleep(5);

        // Increment Progress Meter...

        pm.MeterProgress();

        // This allows AutoCAD to repaint

        Application.DoEvents();

      }

      pm.Stop();

    }

  }

}

The original code is still the technique to use for AutoCAD 2005 & 2006, although you will need to uncomment the line in the DllImport attribute for acedSetStatusBarProgressMeter(), to make sure it uses the EntryPoint with the non-Unicode string argument ("?acedSetStatusBarProgressMeter@@YAHPBDHH@Z"). You'll clearly also need to comment out the current EntryPoint assignment, of course.

I'll forego the Part 2 post (and rename this one from Part 1), as there's really no need to look any other technique for this, at this stage.

34 responses to “Displaying a progress meter during long operations in AutoCAD using .NET”

  1. Kean.
    I've been using the following method to display a ProgressMeter in AutoCAD from .NET, avoiding P/Invoke. Should I change to use your method or is this the other internal method you speak of?

    int numItems = 100;
    int counter = 0;
    // Create progress meter
    ProgressMeter pm = new ProgressMeter();
    // Set it's caption
    pm.Start("Plotting Roads");
    // Set it's maximum value
    pm.SetLimit(numItems);
    // Now our lengthy operation
    while (counter < numItems)
    {
    //
    // Do my process here
    //

    // Sleep a little bit
    System.Threading.Thread.Sleep(1);

    // Update Screen
    Application.UpdateScreen();

    // Increment Progress Meter...
    pm.MeterProgress();
    // ...and our loop counter
    counter++;
    }
    // We're done, clear the Progress Meter
    pm.Stop();
  2. Kean Walmsley Avatar

    Hi Chris,

    Oops - my research was inadequate on this one... I missed the inbuilt class (and took Fenton's word for this one not being available).

    Please do what you're doing now - I'll check after the weekend what's what with the inbuilt class (perhaps it was introduced more recently, and Fenton was answering the question for an older release).

    More soon...

    Kean

  3. Kean Walmsley Avatar

    Hi Chris,

    OK - I see that Autodesk.AutoCAD.Runtime.ProgressMeter got exposed back in 2007. I'll update the post to reflect that.

    Thanks again,

    Kean

  4. Hi Kean,

    Why the need for Application.DoEvents. Also, where in the Autodesk documentation does it state that Application.DoEvents is supposed to be used in conjunction with the ProgressMeter Class?

    Thanks,

    Ron

  5. Kean Walmsley Avatar

    Hi Ron,

    It doesn't have to be used - it's just a choice of mine. I tend to use it to allow AutoCAD to still receive events (such as to repaint etc.) while undergoing long operations. But it's nothing to do with AutoCAD, it's a general Windows programming approach.

    Try commenting that line out and then switching between AutoCAD and another app while running the "PB" command. Even if you add some code to repaint the display you get some imperfect behaviour, so I tend for this option.

    If anyone has a better approach or a conflicting opinion, I'd be happy to hear it. 🙂

    Cheers,

    Kean

  6. Hi Kean,

    If you don't mind I have one more question. Is there a reason that you're using System.Threading.Thread.Sleep()?

    Please excuse my question if it seems basic, but I am trying very hard to learn AutoCAD with .NET and when I see something that warrants asking a question I just have to ask.

    Ron

  7. Kean Walmsley Avatar

    Hi Ron,

    Not at all - it's a very valid question.

    That line is only there to make the operation take long enough for you to watch the progress meter move slowly (or somewhat slowly) across. If it isn't there you just see a flash.

    Feel free to keep the (post-related) questions coming - I'm sure there are others out there with similar thoughts.

    Kean

  8. Fernando Malard Avatar
    Fernando Malard

    Hi Kean,

    Just one more thing.
    Depending on how heavy is the process behind the progress bar if you leave AutoCAD window and get back the screen and the progress bar could appear frozen.

    As you have suggested inside a comment at my Blog´s article, we need do yield AutoCAD during the process. At that time, you have pointed to use:

    System.Windows.Forms.Application.DoEvents();

    In this case, we can call DoEvents() over AutoCAD application during the loop or is there a better way to do that?

    Regards,
    Fernando.

  9. Kean Walmsley Avatar

    Hi Fernando,

    Yes - from my perspective DoEvents is the way to go, just as the above two code fragments have shown.

    It's probably also worth thinking about trapping the escape key, to allow the task to be cancelled. I wrote this post showing how to do this, but I haven't tried to plug the two together, as yet.

    Cheers,

    Kean

  10. Hi,

    I just stumbled upon this code as I was searching for a progress bar to implement in my acad .Net functions. A few things though, in Acad 2010, I had to remove:

    Application.DoEvents()

    Once I did the code ran fine with the test implementation. However, the one question I have is that's great this works when you have a set of defined integer variables, but how can I implement the progress bar to track the completion of a bunch of functions in .Net? What I mean is, say i have 3 subs in my acad .Net enviornment that all do different things (1. opens and attaches a drawing, 2. querys a bunch of objects from the attached drawing, 3. saves the new drawing) but are part of a single button click event. I'd like the progress bar to track the completion of the entire process.

    Any info would be greatly appreciated.

  11. Curious that Application.DoEvents() would be a problem (I wonder if there's something else causing it, but anyway - I'm glad it works for you).

    Metering code execution visibly is a dark art, indeed, which is why percentage complete information is very rarely accurate. You would have to find some way to estimate the probable execution time and then work out when best to call into the progress meter from your code to make it tick.

    Tricky stuff, though - good luck!

    Kean

  12. How do it in VB6 or VBA?

  13. Sorry - that's beyond the scope of this blog (and my skills). You might try asking on the appropriate discussion group.

    Kean

  14. Kean, thanks for the great post! This got me up and going using the ProgressMeter. I ended up taking the implementation a step further since there started to be a similar pattern throughout my application -> Create ProgressMeter, Start, SetLimit, Do Work, Stop, Dispose.

    I created the following static class:
    -----------------------------------------------
    public static class DrawingProgressMeter
    {
    public static void MeterEvent(string description, int size, Action<progressmeter> action)
    {
    ProgressMeter meter = new ProgressMeter();
    meter.Start(description);
    meter.SetLimit(size);

    action(meter);

    meter.Stop();
    meter.Dispose();
    }
    }
    -----------------------------------------------

    Then, whenever I need to meter a portion of code, I would just use the DrawingProgressMeter like this:

    -----------------------------------------------
    int size = 1000;
    DrawingProgressMeter.MeterEvent("Test", size, delegate(ProgressMeter meter)
    {
    for (int i = 0; i < size; i++)
    {
    System.Windows.Forms.Application.DoEvents(); System.Threading.Thread.Sleep(50);
    meter.MeterProgress();
    }
    });
    -----------------------------------------------

    Thought I'd share in case anyone else found this approach useful.

    Thanks again!
    Jon

  15. Very interesting - thanks for sharing this, Jon. 🙂

    Kean

  16. I ended up adding an overload that will take in a collection, this way the method will figure out the 'size' and it will perform the incrementing of the meter.

    ---------------------------------
    public static void ProgressMeterWrapper(IEnumerable<object> collectionToIterate, string message, Action<object> action)
    {
    meter.SetLimit(collectionToIterate.Count());
    meter.Start(message);

    foreach (object obj in collectionToIterate)
    {
    action(obj);
    IncrementMeter();
    }

    meter.Stop();
    meter.Dispose();
    }
    ---------------------------------

  17. Cattoor Bjorn Avatar

    Thanks works like a charm,

    no need for doevents or update screen ... (in 2012 anyway, did not test in lower versions).

  18. For long single operations that cannot be iterated, a good tip is to change the cursor to a spinning wheel. Otherwise the cursor just disappears and the user will not know what is happening.

    I've been using this technique when calling the Solid3d.CreateSweptSolid() method, which depending on the complexity of the geometry hangs for a bit while processing.

    Refer to the following post for details:

    WaitCursorPost

    Cheers,
    Art

  19. Thanks for sharing this suggestion, Art!

    Kean

  20. MexicanCustard (The Swamp) Avatar
    MexicanCustard (The Swamp)

    Kean, I've got a long operation that calls Database.SaveAs() multiple times during the opreation. It seems that this method has its own ProgresMeter and overwrites then turns off my ProgresMeter. Is there a way to keep this from happening?

  21. Not that I'm aware of. You might want to display your own uber-status bar as a floating window (if you wanted to get clever it could even be over the AutoCAD one, but not necessarily).

    Kean

  22. Does someone know a way to force the ProgressMeter to refresh itself with steps of 1%? Now it refreshes itself automatically with steps of usually 5 or 6%, even in long during processes.
    The P/Invoke method Kean mentioned does not have this issue.

    Cheers,
    Henk

  23. Hi Henk,

    Good question - for me it also updates in chunks, which is almost certainly for performance reasons. You might try contacting ADN (directly or via the AutoCAD .NET discussion group), to see if they have any suggestions.

    Regards,

    Kean

  24. Hello Kean,

    if have found this earlier... 🙂 I'll keep this in mind for the future.
    Since with this approach I can show a built-in Progressbar for something my AddIn does, is there any Progress-reporting in Builtin methods? I think of showing the number of items inserted using WblockCloneObjects with a Collection of Objectids... Or may I loop the WblockCloneObjects through my Objectids to catch the progress?
    Thanks in advance,
    Daniel

  25. Ok, tested it on 360 BlockReferences 🙂
    Inserting the Objects 1 by 1 in a loop kills the performance and the using of WblockCloneObjects looses it's sense 🙂
    Any hints on this were appreciated 🙂
    Thanks,
    Daniel

  26. Hello Daniel,

    Unfortunately any single long-running operation can't be (or at least isn't) profiled with that granularity. Of course passing some kind of callback into the function would be ideal, but that's not how things have been implemented.

    You could try breaking things up into individual - or smaller - sets to be called with, but that's not always possible.

    Sorry not to be of more help,

    Kean

  27. Hello Kean,

    every information is helpful, even if it states, that something is (currently) not possible 🙂
    Thank you for your quick answer!

    I guess I will start some Wait animation on my progress-form for this long term methods - so the user is at least informed, that something is going on 🙂

    Regards,
    Daniel

  28. Hello,
    here the way I'm using it 🙂

    friend Class AcadPM
    Inherits Progressmeter
    Public Sub d_StartProgress (byval msg as string, byval limit as integer)
    me.start(msg)
    me.Setlimit(limit)
    end sub
    end class

    friend Class Globals
    friend shared property acPM as new AcadPM
    end class

    ...anywhere in my code...

    globals.acPm.d_StartProgress ("Test", 100)
    for each something in something
    DoStuff
    globals.acpm.MeterProgress
    next
    globals.acpm.Stop

    Just in case, someone could have interest 🙂
    BR,
    Daniel

    PS: I tend to use "Friend" Fields, to avoid potential conflicts with other Assemblies 🙂

  29. sorry, actually it would be:

    globals.acPm.d_StartProgress ("Test", something.count)

    Daniel

  30. How to use in Autocad2013?I use fine in autocad2006,but error in autocad2013

    1. Kean Walmsley Avatar

      What error do you get? I used this code recently in an AutoCAD 2014 project, so it can still work.

      Kean

      1. Thank you for your replay.i have resolved.in autocad2013x64 the function have changed.[DllImport("accore.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl, EntryPoint = "?acedSetStatusBarProgressMeter@@YAHPEB_WHH@Z")]
        private static extern int acedSetStatusBarProgressMeter(string label, int minPos, int maxPos);
        [DllImport("accore.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl, EntryPoint = "?acedSetStatusBarProgressMeterPos@@YAHH@Z")]
        private static extern int acedSetStatusBarProgressMeterPos(int pos);
        [DllImport("accore.dll", CharSet = CharSet.Auto, CallingConvention = CallingConvention.Cdecl, EntryPoint = "?acedRestoreStatusBar@@YAXXZ")]
        private static extern int acedRestoreStatusBar();

        1. Kean Walmsley Avatar

          I see - you were using the original code. Did you try using the updated version at the bottom?

          Kean

          1. Yes.It have run good

Leave a Reply to Fernando Malard Cancel reply

Your email address will not be published. Required fields are marked *