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:
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.
And here it is when it’s cancelled partway through:
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"> </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.