Creating your own AutoCAD progress meter using HTML5 and JavaScript

This week I've spent quite a bit of time looking into future API features. For one of them I needed to create a progress meter, and thought to myself "why not create one in HTML5?" And as it's nothing specific to a future product release, I decided to go ahead and post it now.

For context, here's the way AutoCAD's standard progress meter currently looks, displayed using the code from this previous post:

Standard progress meter

So why would you go head and create your own progress meter? A few different reasons come to mindโ€ฆ yes, AutoCAD has its own, but perhaps you want something more visible (not tucked away in the bottom right corner of the application frame), pausable or more explicitly cancellable. Or perhaps you just want to style it differently โ€“ something we'll take a look at in tomorrow's post.

Even if you don't want to create your own progress meter, the techniques shown in today's post will be valuable if you want to create an HTML UI that's tightly integrated with AutoCAD.

Overall the code is fairly straightforward: as with most HTML5 projects I've embarked upon, I ended up spending more time than expected to get the vertical alignment on the page looking good (mainly because the "old" approach of using tables with the valign attribute no longer works in HTML5โ€ฆ apart from understanding how vertical-align now works, there are still a number of approaches for managing vertical space).

The other big sticking point was around getting the various page elements to display consistently. For instance, very often the caption wouldn't display the first time the dialog was shown in a sessionโ€ฆ I hit my head against this for ages. In the end I found that having the HTML page call back into our .NET app to say "the page has loaded, we're ready to roll" was the cleanest approach.

Here's the progress meter in action, running to completion. You'll notice the dialog is quite bigโ€ฆ that's the minimum size of a modeless dialog. We could also use another modeless container, of course โ€“ such as an HTML palette or even a non-DWG document window โ€“ but for this scenario a modeless window made most sense.

Progress meter - completed

And here it is when it's cancelled partway through:

Progress meter - cancelled

Here's the HTML code:

<!doctype html>

<html>

  <head>

    <title>Progress</title>

    <style>

      body {

        overflow: hidden;

        width: 98%;

        height: 98%;

      }

      hidden {

        display: none;

      }

      progress {

        width: 100%;

      }

      .td-center  {

        text-align: center;

      }

      .td-right {

        text-align: right;

      }

      .center-div  {

        width: 100%;

        padding: 25% 0;

      }

      div {

        font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;

        font-size: large;

        font-weight: bold;

      }

      </style>

    <script

      src="http://app.autocad360.com/jsapi/v2/Autodesk.AutoCAD.js">

    </script>

    <script type="text/javascript">

      var progbar, limit, loaded = false;

 

      function updateProgress(value) {

        progbar.max = limit;

        progbar.value = value;

        progbar.getElementsByTagName('span')[0].innerHTML =

          Math.floor((100 / limit) * value);

      }

 

      function displayValue(prop, val) {

 

        if (prop == "progress") {

          updateProgress(val);

        }

        else if (prop == "limit") {

          limit = val;

        }

        else {

          // Display the specified value in our div for the specified

          // property

 

          var div = document.getElementById(prop);

          if (div != null) {

            if (typeof val === "string") {

              div.innerHTML = val;

            }

            else {

              div.innerHTML = val.toFixed(2);

            }

          }

        }

      }

 

      function showControls(show) {

        var prog = document.getElementById("progress");

        var butt = document.getElementById("cancel");

        if (show) {

          prog.classList.remove("hidden");

          butt.classList.remove("hidden");

        }

        else {

          prog.classList.add("hidden");

          butt.classList.add("hidden");

        }

      }

 

      function start() {

        showControls(true);

      }

 

      function ready() {

        return loaded;

      }

 

      function stop() {

        showControls(false);

        self.close();

      }

 

      function updateControls(args) {

 

        var obj = JSON.parse(args);

 

        var propName = obj.propName;

        var propVal = obj.propValue;

 

        // If the string represents a double (we test using

        // a RegExp), round it to 2 decimal places

 

        var val = 0.0;

        var found = false;

 

        if (typeof propVal === "number") {

          val = propVal;

          found = true;

        }

        else if (typeof propVal === "string") {

          var re = /^[+-] ?[0-9]{0,99}(?:\.[0-9]{1,99})?$/;

          if (propVal.match(re)) {

            val = parseFloat(propVal);

          }

          else {

 

            // Otherwise just display the string

 

            displayValue(propName, propVal);

          }

        }

        if (found) {

          displayValue(propName, val);

        }

      }

 

      // Shaping layer extensions

 

      function pageLoaded() {

        var jsonResponse =

          exec(

            JSON.stringify({

              functionName: 'Ready',

              invokeAsCommand: false,

              functionParams: undefined

            })

          );

        var jsonObj = JSON.parse(jsonResponse);

        if (jsonObj.retCode !== Acad.ErrorStatus.eJsOk) {

          throw Error(jsonObj.retErrorString);

        }

        return jsonObj.result;

      }

 

      function cancelOperation() {

        var jsonResponse =

          exec(

            JSON.stringify({

              functionName: 'CanOp',

              invokeAsCommand: false,

              functionParams: undefined

            })

          );

        var jsonObj = JSON.parse(jsonResponse);

        if (jsonObj.retCode !== Acad.ErrorStatus.eJsOk) {

          throw Error(jsonObj.retErrorString);

        }

        return jsonObj.result;

      }

    </script>

  </head>

  <body>

    <table class="center-div">

      <tr>

        <td class="td-center">

          <div id="caption">&nbsp;</div>

        </td>

      </tr>

      <tr>

        <td class="td-right" width="100%">

          <progress id="progress" class="hidden"

                    value="0" max="100">

            <span>0</span>%

          </progress>

        </td>

        <td class="td-right">

          <button id="cancel" class="hidden"

                  onclick="cancelOperation();">

            Cancel

          </button>

        </td>

      </tr>

      <tr>

        <td class="td-center">

          <div id="extra"></div>

        </td>

      </tr>

    </table>

    <script type="text/javascript">

      (function () {

        registerCallback("updval", updateControls);

        registerCallback("start", start);

        registerCallback("stop", stop);

        progbar = document.getElementById('progress');

 

        document.onkeydown = function (evt) {

          evt = evt || window.event;

          if (evt.keyCode == 27) {

            cancelOperation();

          }

        };

        window.onload = pageLoaded;

      })();

    </script>

  </body>

</html>

I created a C# class that mimics the ProgressMeter protocol โ€“ in fact it derives from the standard ProgressMeter class, adding a few additional capabilities โ€“ to make it easier to switch between the two, as needed. You won't want to put yours in the Autodesk.AutoCAD.Runtime namespace โ€“ I simply did so for my own convenience.

using Autodesk.AutoCAD.ApplicationServices;

using System;

using System.IO;

using System.Reflection;

using System.Runtime.InteropServices;

 

namespace Autodesk.AutoCAD.Runtime

{

  // Use the standard ProgressMeter protocol

 

  public class ProgressMeterHtml : ProgressMeter

  {

    private static bool _ready;

    private static bool _cancelled;

    private int _pos;

 

    [DllImport(

      "AcJsCoreStub.crx", CharSet = CharSet.Auto,

      CallingConvention = CallingConvention.Cdecl,

      EntryPoint = "acjsInvokeAsync")]

    extern static private int acjsInvokeAsync(

      string name, string jsonArgs

    );

 

    // Called by Progress.html when the page has loaded

 

    [JavaScriptCallback("Ready")]

    public string ReadyToStart(string jsonArgs)

    {

      _ready = true;

      return "{\"retCode\":0}";

    }

 

    // Called by Progress.html to cancel the operation

 

    [JavaScriptCallback("CanOp")]

    public string CancelOperation(string jsonArgs)

    {

      _cancelled = true;

      return "{\"retCode\":0}";

    }

 

    // Constructor

 

    public ProgressMeterHtml()

    {

      // Initialize static members

 

      _ready = false;

      _cancelled = false;

 

      // Load Progress.html from this module's folder

 

      var asm = Assembly.GetExecutingAssembly();

      var loc =

        Path.GetDirectoryName(asm.Location) + "\\progress.html";

 

      Application.ShowModelessWindow(new System.Uri(loc));

 

      // Wait for the page to load fully to avoid refresh issues

 

      while (!_ready)

      {

        System.Threading.Thread.Sleep(500);

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

      }

 

      // Initialize our progress counter

 

      _pos = 0;

    }

 

    // Start the progress meter without a caption

 

    public override void Start()

    {

      acjsInvokeAsync("start", "{}");

    }

 

    // Start the progress meter with a caption

 

    public override void Start(string displayString)

    {

      Start();

      Caption(displayString);

    }

 

    // Set the limit

 

    public override void SetLimit(int max)

    {

      SendProperty("limit", max);

    }

 

    // Advance the progress meter

 

    public override void MeterProgress()

    {

      SendProperty("progress", ++_pos);

    }

 

    // Stop the progess meter, whether it's finished or the

    // operation has been cancelled

 

    public override void Stop()

    {

      Caption(_cancelled ? "Cancelled" : "Completed");

      AdditionalInfo(" ");

 

      // We'll wait for a second and then close the dialog

 

      System.Threading.Thread.Sleep(1000);

      acjsInvokeAsync("stop", "{}");

    }

 

    // Cancels the current operation

 

    public void Cancel()

    {

      _cancelled = true;

    }

 

    // Returns whether the operation has been cancelled

 

    public bool Cancelled

    {

      get { return _cancelled; }

    }

 

    // Sets the dialog's caption

 

    public void Caption(string displayString)

    {

      SendProperty("caption", displayString);

    }

 

    // Sets the additional information text

 

    public void AdditionalInfo(string displayString)

    {

      SendProperty("extra", displayString);

    }

 

    // Helper function to set a property in the HTML page

 

    private void SendProperty(string name, object val)

    {

      bool enclose = val.GetType() == typeof(String);

      var args =

        "{\"propName\":\"" + name + "\",\"propValue\":" +

        (enclose ? "\"" : "") + val.ToString() +

        (enclose ? "\"" : "") + "}";

      acjsInvokeAsync("updval", args);

    }

  }

}

The calling code is almost identical to what we saw in the original ProgressMeter post:

using Autodesk.AutoCAD.Runtime;

using System.Windows.Forms;

 

namespace ProgressMeterTest

{

  public class Cmds

  {

    [CommandMethod("PB")]

    public void ProgressBarHtml()

    {

      const int ticks = 50;

 

      var pm = new ProgressMeterHtml();

      pm.Start("Testing Progress Bar");

      pm.AdditionalInfo("Show something extra");

      pm.SetLimit(ticks);

 

      // Now our lengthy operation

 

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

      {

        System.Threading.Thread.Sleep(50);

 

        // Increment progress meter...

 

        pm.MeterProgress();

        Application.DoEvents();

 

        if (pm.Cancelled)

          break;

      }

      pm.Stop();

    }

  }

}

That's it for today's post. Tomorrow we'll take a look at styling the HTML to see what we can do with it.

15 responses to “Creating your own AutoCAD progress meter using HTML5 and JavaScript”

  1. Hello Kean, Im new to HTML5 and Java Script. How do I get your code HTML and Java Script into a modeless dialog? Basicly what would the project look like in terms of files and code locations.

    Thanks,
    matt

    1. Hi Matt,

      The C# code should be built into a Class Library, as usual. No particular requirements about file naming, project structure, etc.

      The HTML page should simply be saved in a file called "Progress.html" in the same location as the Class Library .DLL.

      Sorry for not spelling that out in the post!

      Kean

      1. Thank you, My main mistake was attempting this in 2013. Even if you put in in the correct place, AutoCAD 2013 doesnt like it. ๐Ÿ˜‰ Tried in 2015 and its much happier. Thanks again,
        matt

        1. Right, yes... it might work in 2014, too, but definitely wouldn't in 2013.

          Kean

  2. Fun project! Thanks, Kean.

    1. My pleasure, Ben. ๐Ÿ™‚

      Kean

  3. if I include the CommandClass line, then this demo doesn't work...
    [assembly: CommandClass(typeof(ProgressMeterTest.Cmds))]

    1. Kean Walmsley Avatar

      How does it fail? Is the command not found, or does it generate some kind of error?

      Kean

      1. it fails on the JSON.parse from inpage javascript to the "Ready" function... jsonObj.result = 2

        1. Kean Walmsley Avatar

          You may need to also flag Autodesk.AutoCAD.Runtime.ProgressMeterHtml as having commands...

          Kean

          1. yes, I do.
            Thank you

            1. Kean Walmsley Avatar

              My pleasure. Does it now work for you?

              Kean

              1. yes. I had thought the tag was to identify a class of command methods, but know better now.

  4. Hello .. I have a site in PHP that manager documents. I need create a script that "SAVE AS" dwg in Autocad 2010 version. Is Possible?

    1. You could connect to the Forge Design Automation API (a REST API) and use that. I don't know enough about PHP's capabilities, though - I suggest posting to the online forums.

      Kean

Leave a Reply to DFSolley Cancel reply

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