Through the Interface

March 2015

Sun Mon Tue Wed Thu Fri Sat
1 2 3 4 5 6 7
8 9 10 11 12 13 14
15 16 17 18 19 20 21
22 23 24 25 26 27 28
29 30 31        



Twitter





March 30, 2015

AutoCAD 2016: Calling commands from external events using .NET

Last week we introduced the ExecuteInCommandContextAsync() method and saw it in action from a context menu click event. In today’s post we’re going to see how it can be used for a lot more: we’re going to use it to respond to external, operating system-level events (although admittedly we’re handling the event in-process to AutoCAD via .NET).

What we’re actually going to do is fire off a command inside AutoCAD – in our case we’re going to use RECTANG to create square polylines – each time we find that a file has been placed in a particular folder (in our case “c:\temp\files”, although the actually location isn’t particularly important).

Here’s what we’re aiming for – this recording shows AutoCAD on the left and an Explorer window pointed at “c:\temp\files” on the right.


A FileSystemWatcher feeding AutoCAD

Of course the specific event we’re responding to – in our case the Changed event  on a FileSystemWatcher – isn’t really the point. The point is that this mechanism allows you to react to things happening outside AutoCAD, calling AutoCAD commands in reaction.

Here’s the C# code, to show how it’s working. Something to bear in mind… we’re only adding squares until there are as many as files in the tracked folder: more work would be needed to remove squares as files get deleted or moved away. But this is simply an example of hooking such an event into AutoCAD – I didn’t see much point in adding that kind of complexity to the code.

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using System;

using System.IO;

 

namespace CommandFromAppContext

{

  public class Commands

  {

    const int columns = 10;

    const string path = "c:\\temp\\files";

 

    private FileSystemWatcher _fsw = null;

    private int _fileNum = 0;

    private int _filesTotal = 0;

    private bool _drawing = false;

 

    [CommandMethod("EC")]

    public void EventCommand()

    {

      var dm = Application.DocumentManager;

      var doc = dm.MdiActiveDocument;

      if (doc == null)

        return;

 

      var ed = doc.Editor;

 

      // We'll start by creating one square for each file in the

      // specified location. The nSquares() function uses some

      // global state for the index and the total count

 

      _fileNum = 0;

      _filesTotal = Directory.GetFiles(path).Length;

 

      nSquares(ed);

 

      // Create a FileSystemWatcher for the path, looking for

      // write changes and drawing more squares as needed

 

      if (_fsw == null)

      {

        _fsw = new FileSystemWatcher(path, "*.*");

        _fsw.Changed += (o, s) => nSquaresInContext(dm, ed, path);

        _fsw.NotifyFilter = NotifyFilters.LastWrite;

        _fsw.EnableRaisingEvents = true;

 

        ed.WriteMessage("\nWatching \"{0}\" for changes.", path);

      }

    }

 

    [CommandMethod("ECX")]

    public void StopEventCommand()

    {

      var dm = Application.DocumentManager;

      var doc = dm.MdiActiveDocument;

      if (doc == null)

        return;

 

      var ed = doc.Editor;

 

      if (_fsw != null)

      {

        _fsw.Dispose();

        _fsw = null;

      }

 

      ed.WriteMessage("\nNo longer watching folder.");

    }

 

#pragma warning disable 1998

 

    private async void nSquaresInContext(

      DocumentCollection dc, Editor ed, string path

    )

    {

      // We'll set the total as it may well have changed (hence the

      // need for global state rather than using an argument)

 

      _filesTotal = Directory.GetFiles(path).Length;

 

      // Protect the command-calling function with a flag to avoid

      // eInvalidInput failures

 

      if (!_drawing)

      {

        _drawing = true;

 

        // Call our square creation function asynchronously

 

        await dc.ExecuteInCommandContextAsync(

          async (o) => nSquares(ed),

          null

        );

 

        _drawing = false;

      }

    }

 

#pragma warning restore 1998

 

    private void nSquares(Editor ed)

    {

      // Draw squares until we have enough (the total might

      // change, hence the need for global state)

 

      for (; _fileNum < _filesTotal; _fileNum++)

      {

        // Determine the position in our grid

 

        int xoff = _fileNum % columns,

            yoff = _fileNum / columns;

 

        // Create our polyline via the RECTANG command

 

        ed.Command(

          "._rectang",

          String.Format("{0},{1}", xoff, yoff),

          String.Format("{0},{1}", xoff + 1, yoff + 1),

          "_regen"

        );

      }

    }

  }

}

I think this is a really powerful mechanism. I’d be very curious to hear how people anticipate using it with AutoCAD 2016.

March 27, 2015

NuGet packages now available for AutoCAD 2016

Some time ago we posted the NuGet packages for AutoCAD 2015’s .NET API. The packages for AutoCAD 2016 are now live, too.

Here’s the report from the NuGet console (accessible in Visual Studio via Tools –> NuGet Package Manager –> Package Manager Console).

PM> Get-Package -filter AutoCAD.NET -ListAvailable

 

Id                   Version    Description/Release Notes

--                   -------    -------------------------

AutoCAD.NET          20.1.0     AutoCAD 2016 API

AutoCAD.NET.Core     20.1.0     AutoCAD 2016 core object model API

AutoCAD.NET.Model    20.1.0     AutoCAD 2016 drawing object model API

To install the 2016 versions of the assemblies into your project, you can use the following command, once again in the NuGet Console.

PM> Install-Package AutoCAD.NET -Version 20.1

And the 2015 are still available too, of course:

PM> Install-Package AutoCAD.NET -Version 20.0

I actually just learned a nice trick: you can use tab to autocomplete the version number inside the NuGet console, as you can see below. Very handy!


NuGet package manager console with autocomplete

I hadn’t realised there was a 20.0.1 version posted – I’ll check in on that and report its significance.

As promised last time, on Monday we’ll look at another possible use of DocumentCollection.ExecuteInCommandAsync() in AutoCAD 2016.

Update:

It turns out 20.0.1 was a very minor bugfix. There were some versioning issues with 20.0.0 and we’d also omitted the Brep API’s .NET assembly from that version.

March 25, 2015

AutoCAD 2016: Calling commands from AutoCAD events using .NET

It’s time to start looking in more detail at some of the new API capabilities in AutoCAD 2016. To give you a sense of what to expect in terms of a timeline, this week we’ll look at a couple of uses for DocumentCollection.ExecuteInCommandContextAsync() and next week we’ll look at point cloud floorplan extraction and (hopefully) security and signing.

The first use of ExecuteInCommandContextAsync() I wanted to highlight was one raised in a blog comment a couple of months ago. The idea is simple enough: we want to be able to launch a command reliably from an event handler, in our case the Click event of a ContextMenuExtension’s MenuItem. Before now you would have to use Document.SendStringToExecute(), as we saw in this previous post – calling a command in another way would typically lead to an eInvalidInput exception.

There are certainly advantages to avoiding SendStringToExecute() in this scenario: while commands that use the pickfirst selection set are OK – including ones you implement yourself – using Command() or CommandAsync() gives you greater control over which entities you choose to pass entities to the command being called (whether it accepts pickfirst selection or not).

By the way, as mentioned briefly in a comment on the last post, in AutoCAD 2015 you will find the DocumentCollection.BeginExecuteInCommandContext() method, which was the former name for ExecuteInCommandContextAsync() (it’s taken from the ObjectARX method it calls through to). If you try to make the below code work in AutoCAD 2015 with the previous method name, you’re probably going to hit this error: ‘Unknown command: “EXECUTEFUNCTION”’.

Before we look at the code, here’s a recording of what we want it to do:


Context menu to scale by 5

It’s pretty simple in concept, at least. Here’s the C# code that makes it work:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Geometry;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Windows;

using System;

 

namespace ContextMenuApplication

{

  public class Commands : IExtensionApplication

  {

    public void Initialize()

    {

      ScaleMenu.Attach();

    }

    public void Terminate()

    {

      ScaleMenu.Detach();

    }

  }

 

  public class ScaleMenu

  {

    private static ContextMenuExtension cme;

 

    public static void Attach()

    {

      if (cme == null)

      {

        cme = new ContextMenuExtension();

        MenuItem mi = new MenuItem("Scale by 5");

        mi.Click += new EventHandler(OnScale);

        cme.MenuItems.Add(mi);

      }

      RXClass rxc = Entity.GetClass(typeof(Entity));

      Application.AddObjectContextMenuExtension(rxc, cme);

    }

 

    public static void Detach()

    {

      RXClass rxc = Entity.GetClass(typeof(Entity));

      Application.RemoveObjectContextMenuExtension(rxc, cme);

    }

 

    private static async void OnScale(Object o, EventArgs e)

    {

      var dm = Application.DocumentManager;

      var doc = dm.MdiActiveDocument;

      var ed = doc.Editor;

 

      // Get the selected objects

 

      var psr = ed.GetSelection();

      if (psr.Status != PromptStatus.OK)

        return;

 

      try

      {

        // Ask AutoCAD to execute our command in the right context

 

        await dm.ExecuteInCommandContextAsync(

          async (obj) =>

          {

            // Scale the selected objects by 5 relative to 0,0,0

 

            await ed.CommandAsync(

              "._scale", psr.Value, "", Point3d.Origin, 5

            );

          },

          null

        );

      }

      catch (System.Exception ex)

      {

        ed.WriteMessage("\nException: {0}\n", ex.Message);

      }

    }

  }

}

We might have used Editor.Command() rather than Editor.CommandAsync(), but ExecuteInCommandContextAsync() is expecting an asynchronous task to be passed in, so doing so would lead to a warning about the async lambda running synchronously. Ultimately it works comparably, but the above code makes the C# compiler happier, so I’ve left it that way. I’ve also chosen to await the call to ExecuteInCommandContextAsync(), although for this type of operation it’s probably not strictly needed.

In the next post we’re going to take a look at calling AutoCAD commands based on external events: we’re going to hook up a FileSystemWatcher to check for changes to a folder and call a command inside AutoCAD each time a file gets created there.

March 23, 2015

AutoCAD 2016 for developers

After our quick look at AutoCAD 2016 from a user perspective, let’s now spend some time looking at the things important to developers in this latest release.


Compatibility

Off the bat it’s worth stating that AutoCAD 2016 is a DWG compatible release: it’s using the same file format as AutoCAD 2013, 2014 and 2015. It’s also a binary application compatible release: ObjectARX apps written for AutoCAD 2015 should work in 2016 and it’s likely that .NET apps built for AutoCAD 2014 will work, too. That said, some changes have been made to the security model for this release of AutoCAD, so you may want to make sure these changes haven’t impacted your application…


Security

You’ll see straight away when you try to load your application that there’s a bit more going on in the 2016 version of this dialog.

Security dialog in AutoCAD 2016

Firstly, the word “unsigned” indicates something very important: we’re encouraging developers to sign their executables – and even their LISP files – to improve security. An increasing number of AutoCAD customers – often the larger ones, as you might expect – are requiring application modules to be signed. This is certainly a topic that’s worth go into more deeply in a future post.

The other addition to the dialog is the “Always load this application” checkbox. This tells AutoCAD to continue loading this module from a non-trusted location, with the caveat that if the module changes the user will be prompted again.

A number of sysvars related to AutoCAD’s security features can now be locked by CAD managers:

  • SECURELOAD
  • TRUSTEDPATHS
  • TRUSTEDDOMAINS (see update below)
  • LEGACYCODESEARCH
  • ACADLSPASDOC (see update below)

LEGACYCODESEARCH is important to note: AutoCAD’s default “find file” behaviour has been changed neither to search the current working folder nor the folder of the active drawing. This will make it much harder for people to write viruses that travel around with AutoCAD drawings.

On the subject of sysvars, I do think the System Variable Monitor – which we mentioned last time – will be a useful diagnostic tool for developers. If you know you’re using sysvars in your code – such as CMDECHO, CMDDIA, FILEDIA, etc. – try adding them to the monitor and see whether you get any notifications about your commands not setting them back properly. If you get any you can bet your users will, too.

Sysvar monitor

Now let’s take a look at the new .NET API features (equivalent – and in a few cases more – functionality is also available via ObjectARX).


Reality computing

A number of point cloud-related features have been added to the product and are also available via the API.

You can create point cloud extension definitions (PointCloudDefEx objects) by attaching .RCS/.RCP files. These are analogous to block definitions. You can also create point cloud extension entities (PointCloudEx objects) that “insert” these into the drawing. A whole slew of properties and methods are exposed from both these classes.

A number of capabilities have been added relating to extraction of features from point clouds. Firstly, the Section class now has a “slice” type, and can be used to slice through a point cloud. Correspondingly it’s possible to use a PointCloudCrop object to crop a point cloud relative to a plane.


Even silly point clouds can be sectioned

The extraction itself can also be driven programmatically. I’ve put together a sample that performs an extraction and adds the extracted geometry to the drawing: we’ll take a look at that in an upcoming post.


Rendering

A number of capabilities related to the newly introduced RapidRT rendering system have been added to the API in this release. You can add and control image-based lighting, for instance, as well as managing the various settings related to RapidRT.

As this is a binary application compatible release, when we change an API significantly we make sure we do so via a new class until we can safely break API compatibility in a future release. So there’s a new Autodesk.AutoCAD.GraphicsSystem.Manager2 class which contains GetOffScreenDevice() and GetOffScreenView() methods making use of the RapidRT engine.


Miscellaneous

Here’s a quick round-up of some of the more interesting miscellaneous API enhancements in this release…

The Spline.ToPolyline() method has a couple of new Boolean parameters allowing you to specify that you wish to create arc segments as well as requesting the generation of lightweight polylines.

The Dimension.TextDefinedSize property allows you to control the width associated with long dimension text.

The MText class has a couple of new properties: ContentsRTF allows you to extract a version of the contents in Rich Text Format, while ShowBorders allows you to query or control whether an MText object’s borders are visible.

We mentioned the ability for CAD managers to lock certain system variables. There’s a corresponding Variable.IsLocked property allowing you to test this programmatically.

And finally my personal favourite miscellaneous enhancement in this release… you can now request code to be executed within a command context from the application context using DocumentCollection.ExecuteInCommandContextAsync(). Now this may not sound like much, but this one method allows you to do some really interesting things, such as calling commands from AutoCAD event handlers or even OS-level events. This is definitely a method of which we’re going to make a great deal of use, both in a few near-term posts and in the longer term.

Update:

It turns out that while the documentation for ACADLSPASDOC and TRUSTEDDOMAINS state they “may be locked by CAD managers”, the CAD Manager Control Utility currently does not, in fact, allow you to lock these two sysvars. Thanks to Dieter and Karen for tracking this one down: we’ll make sure the docs get fixed during the next update.

March 19, 2015

AutoCAD 2016

It’s that time again! Over the coming days you’re going to be hearing lots about the next release of AutoCAD, codenamed “Maestro”.AutoCAD 2016 makes a splash

Before taking a look at AutoCAD 2016 from a developer’s perspective – which we’ll do next time – in this post we’re going to take a quick look at its user features.

To kick things off here are the new commands and system variables, here are those that have changed and here are those that have been removed. To cherry-pick from the top of the new commands list, CLOSEALLOTHER will close all open drawings other than the one you’re currently working on: I know that one’s going to come in handy.

Here’s a quick run-down of the main new features…

Snap to geometric center

A new object snap mode – geometric center, or GCE at the command-line – allows you to snap to the center of closed polylines, whether regular (such as the ones shown below) or irregular.


Snap to geometric center

Revision cloud creation

The REVCLOUD command has some new options: you can easily create rectangular or polygonal revision clouds, for instance, as well as modify them afterwards.


Revcloud

Smart dimensioning

The DIM command now infers the kind of dimension you want to create as you hover over/select geometry.


Dim

You’ll also see enhancements around wrapping of long dimension text in this release.

System variable monitor

This feature is intended for users to make sure they don’t have impolite apps or scripts running that don’t reset sysvars to their previous settings when they’ve finished what they’re doing. But this is also likely to be a very handy debugging tool for application developers to identify when their applications are misbehaving in this respect.

Sysvar monitor

Higher quality graphics display

Those of you with capable graphics cards will see some nice improvements in terms of the display of curves in AutoCAD 2016. We’ve gone beyond the line-smoothing work in 2015 to make greater use of the GPU for elaboration of curves and line-weights.

Line quality in 2016

Coordination models

You can now attach Navisworks and BIM360 models (.NWC, .NWD) directly into AutoCAD using the new CMATTACH command, allowing you to model within and around them. The “BIM underlay” feature team did a great job delivering the ability to attach large models with very good performance.

Coordination model

Point cloud sectioning and floorplan extraction

Fans of Reality Computing are really going to like AutoCAD’s new 2D floorplan extraction: it’s great to see some point cloud feature extraction capabilities being introduced in the product. The SECTIONPLANE command has been updated to generate the new “slice” section which works with point cloud objects. You can then use the PCEXTRACTSECTION command to generate geometry based on this slice through the point cloud:

Section extraction options for point clouds

PDF enhancements

AutoCAD 2016 creates better PDFs:

  • They are smaller – typically half the size or less than the 2015 output – and are generated more quickly
  • All text is now searchable and selectable (unless you choose otherwise), even with with multibyte and Unicode characters and SHX fonts (which have the source text added as a comment)
  • Hyperlinks are maintained, whether embedded URLs or links between drawing content (e.g. callouts linking to named views)
  • Publishing sheet sets results in PDFs with named pages, and can be launched directly from the Sheet Set Manager as a one-click option
  • There are now a number of PC3 files containing commonly-used output settings, usable from PLOT, PUBLISH and EXPORTPDF commands

PDF options

  • PDF underlays now have much better performance

It’s definitely worth taking another look at AutoCAD’s PDF support if you haven’t in a while.

Rendering improvements

A lot of work has been done on rendering, in this release. We’ve simplified the options available – creating a number of handy presets, such as “Coffee-Break Quality” :-) – and introduced image-based lighting. Many of these enhancements relate to our integration of Autodesk’s RapidRT engine into AutoCAD.

Render presets palette

Help enhancements

The very handy “UI finder” capability that is built into our user documentation has been extended to find pretty much anything in AutoCAD’s UI, including entries on the app menu and the status bar.


UI finder and the app menu

That's it for this non-exhaustive look at AutoCAD 2016’s new features. Next time we’ll focus more on what the release brings for developers.

March 18, 2015

Docking a WPF window inside AutoCAD – Part 3

In this post we’re wrapping up this mini-series on docking, which is part of a much broader story arc around a “command-line helper” tool, of course. But then we’re reaching the end of that, too, I suspect, as the app’s just about done. Hopefully it’s ready for posting to Autodesk Labs, at least.

Last time we added right-click dragging to allow our keywords window to be moved around without interrupting the active command. Now we’re taking it a step further to preview docking at one of the four corners of the drawing window, as well as to actually dock the dialog when the right mouse-button gets released, of course.

Here’s a recording of this in action:


CmdLineHelper with draggable docking

And here’s the “completed” project (we’ll see what changes need to be made once we start to get feedback from users).

One really interesting problem took some debugging: we’re storing a dictionary of docking locations and their corresponding enumeration values so we can check on each “mouse move” to see whether the cursor is near enough to one of the locations to preview it. Because this list’s contents will change with the size or position of the screen, we recreate it each time a drag operation begins. Very curiously, calling Clear() on this dictionary from CommandEnded() (we have a StopDragging() helper that gets called from there, just in case “on right mouse-button up” doesn’t get called) resulted in our “unknown command” event handler not firing! I’m used to the need to be careful about drawing modifications from CommandEnded() – these can kill your undo file, for instance – but this was a completely non-AutoCAD-centric data structure. Setting it to null rather than clearing it causes the code to work properly, but this was a really interesting (and obscure) bug that I thought it was worth mentioning. Please post a comment if you’ve experienced something similar and have an idea about what’s going on.

It took a little work to add the docking preview, itself: I used a new, transparent WPF Window – sized at exactly the drawing area – to which we add a Canvas containing a Rectangle. The Window gets resized whenever a drag operation starts (in case the outer window has changed size) and whenever we find the mouse is close enough to one of our dock locations we set the rectangle to the right size & location. I ended up choosing a standard gray – matching the colour used for the command-line docking – but that’s a simple detail to change if you need to make your own docking preview more visible.

All in all it works well: the ability to move the dialog around during a command – docking, as needed – does make the app much more usable. Hopefully you’ll also find this useful for your own “dockable” windows inside AutoCAD.

March 17, 2015

Docking a WPF window inside AutoCAD – Part 2

I’m happy to say that the implementation I mentioned in the last post ended up being pretty straightforward. Which is actually great, as I have some important posts to work on for next week. :-)

Today we’re going to take a look at the next stage of the “command-line helper” implementation: basic right-click movement of the global keywords dialog, so we can set a custom location for the dialog without needing to use the KWSDOCK command.

Here’s the code in action:


CmdLineHelper with right-click dialog move

The main work for this stage was to add support for right-click, mouse move and right mouse-button up events, making sure that the dialog is displaced accurately irrespective of where the mouse gets moved. And that’s actually a really nice feature of this version: you can move the dialog off the main AutoCAD window, if you want to (this wasn’t supported in the KWSDOCK command as we’re using Editor.GetPoint() to let the user select custom locations).

I’ve turned off the mouse cursor in the above video, but you would see a “all-direction scrolling” cursor: the pan cursor isn’t a standard one in Windows, so I just used something that was good enough for this particular app.

There were some other minor things to work through, but hopefully the C# code in this version of the project is straightforward to understand.

In the next part in this series we’re going to extend this implementation to support docking – and preview of dock locations – when you right-click drag the dialog close to corners of the drawing window.

March 16, 2015

Docking a WPF window inside AutoCAD – Part 1

During the course of this week we’re going to look at extending the command-line helper sample posted last week by allowing our global keywords window to “dock” to the four corners of the drawing window as well as to remain fixed at a custom location somewhere on the screen. I use the term “dock” here loosely, as we’re really just placing it in one of the corners of the drawing window. If we wanted a modeless dialog that was properly docked into AutoCAD then we’d almost certainly want to use a PaletteSet.

Here’s a quick video demonstrating the KWSDOCK command, which allows the user to select one of the four corners or a custom location:


CmdLineHelper with docking

While the changes aren’t very extensive, it doesn’t make sense to embed the complete code in this post. Here’s a link to the updated C# project for you to look at in depth.

Having flexibility around the location of this kind of dialog is pretty important: it would quickly get annoying if you weren’t able to adjust it (whether because the window was too far from the area of interest or obscuring something you really needed access to).

One suggestion was to have the dialog placed at the cursor. Of course you couldn’t have it follow the cursor – that would clearly make it impossible to use the mouse to select a keyword from the list – but even having it placed where the cursor was at the start of the command feels strange: you very often have the cursor near your area of interest when you start a command. Having the option of a “fixed” custom location is one way to mitigate against this.

In the next part we’re going to take a look at implementing this via a right-click/drag operation directly on the window, to avoid having to run a separate command. I see this happening in much the same way as AutoCAD or Visual Studio previews the docking of windows: they both preview the location when you drag the window close enough. I’m not sure exactly how I’m going to do it, yet, but I’m aiming to do something similar for this keywords window.

March 12, 2015

Building bridges

Last night the section of motorway closest to my home was closed from 10pm to 6am. They closed it to install a new cycle bridge intended primarily for children from one local village to cycle safely to school in another. Closing a motorway is considered quite an event around here, so a small crowd gathered to watch the proceedings in spite of the hour and the pre-spring nip in the air. Admittedly the drinks laid on by the local government – tea or wine, depending on your preferred approach for keeping warm – didn’t hurt the mood.

At around 11pm the engineer responsible from the project, Mr. Droz, addressed the gathered crowd to describe what was about to happen. The main piece being fitted was a 45m span weighing 40 metric tons (the overall bridge will end up being 100 tons). For this 45m span the tolerance was 5-10mm. The bridge would be anchored on the south side and they expected the steelwork to vary in total length by 3-4cm between the coldest winter and the hottest summer. Many of the design options related to the bridge had been chosen with a view to allowing the bridge to be put in place in a single night, minimising the impact on the motorway beneath it.

I chatted a bit later with Mr. Droz afterwards, asking him (and I always feel a bit strange asking this question – I don’t want to sound like I’m selling something) what software had been used on the project. I was relieved when he said “AutoCAD, of course!” and then – after I’d mentioned working for Autodesk – went on to extoll the virtues of the product, also describing how they’d modelled the bridge fully in 3D.

We stood there watching the bridge lift into the air. I could tell Mr. Droz was getting quite emotional – it was the culmination of 4 years of his hard work – and we reflected on how satisfying it was to have contributed in some (in my case very small) way to the creation of something that will be used by future generations.Our new cycle bridge

For some years Autodesk executives have talked about the company striving to be good (doing things responsibly, benefitting our communities and each other), great (from a business perspective) and important (enabling our customers to solve significant problems). Standing out in the cold, watching a new bridge being put in place across an eerily silent motorway, last night I definitely felt part of something important.

March 11, 2015

Adding support for global keywords at AutoCAD’s command-line using .NET – Part 2

Today we’re going to look at the implementation talked about in the last post: we’re going to see how it’s possible to use the Application.PreTranslateMessage() method to hack AutoCAD’s message-loop and basically convert typed keywords into global ones.

This is actually pretty neat (yes, even if I do say so myself :-) and frankly I’m surprised it works. Here’s the overall approach:

  • Track the characters typed into the command-line
    • Add individual characters into a list
    • Backspace removes the tail of the list
    • Arrow-keys invalidate the tracking: if the user accesses entries in the command-history we can’t deal with that, and even navigating left and right along the typed text is tricky
  • When we encounter a termination character – enter or space – the fun really starts…
    • We check the entered string to see whether it matches any in our global keywords list. If it doesn’t match any – or it matches too many – then we let the keyword get processed as normal (which should either result in a local keyword being interpreted or an error)
    • If we find a single keyword match, we swallow the “keydown” message – setting e.Handled to true – which means the enter/space won’t be processed
    • We create a string consisting of the right number of backspace characters (ASCII 8) to erase the existing keyword and append an underscore and the typed keyword
    • We use Document.SendStringToExecute() to send this to the command-line

I didn’t find AutoCAD’s keyword matching code accessible via an API, so I did my best to replicate it. I don’t really like doing that, but it was actually quite fun to think through how AutoCAD detects keywords. I could well have missed edge cases, though – if you come across any strange behaviour, please let me know!


CmdLineHelper

Looking at the above recording – recorded using the French version of AutoCAD 2015 – there are some important points to note: we’re only using global commands and keywords entered via the command-line (as opposed to clicking from the menu, as we saw last time). When a keyword matches one in the global list (such as “arc” or “line” during the PLINE command), an underscore gets prefixed directly in the command-line.

Here’s the complete project for you to play with and here’s the C# source file containing the implementation described above:

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using System;

using System.Collections.Generic;

using System.Collections.Specialized;

using System.Text.RegularExpressions;

 

namespace CmdLineHelper

{

  public class KeywordCommands

  {

    // The keyword display window

 

    private KeywordWindow _window = null;

 

    // We will store the "core" set of keywords for when we have

    // nested keywords that need to be appended

 

    private KeywordCollection _coreKeywords = null;

 

    // Keystrokes to recreate the commands entered

 

    private List<char> _keystrokes = null;

 

    // Flag for whether we're tracking keystrokes or not

 

    private bool _tracking = false;

 

    // The previous value of DYNMODE, which we override to 0

    // during keyword display

 

    private int _dynmode = 0;

 

    // List of "special" commands that need a timer to reset

    // the keyword list

 

    private readonly string[] specialCmds = { "MTEXT" };

 

    // Constants for our keystroke interpretation code

 

    private const int WM_KEYDOWN = 256;

    private const int WM_KEYUP = 257;

    private const int WM_CHAR = 258;

 

    // 37 - left arrow (no char, keydown/up)

    // 38 - up arrow (no char, keydown/up)

    // 39 - right arrow (no char, keydown/up)

    // 40 - down arrow (no char, keydown/up)

    // 46 - delete (no char, keydown/up)

 

    private static readonly List<int> cancelKeys =

      new List<int> { 37, 38, 39, 40, 46 };

 

    // 13 - enter (char + keydown/up)

    // 32 - space (char + keydown/up)

 

    private static readonly List<int> enterKeys =

      new List<int> { 13, 32 };

 

    [CommandMethod("KWS")]

    public void KeywordTranslation()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

 

      var ed = doc.Editor;

 

      if (_window == null)

      {

        _window = new KeywordWindow(Application.MainWindow.Handle);

        _window.Show();

        Application.MainWindow.Focus();

 

        // Add our various event handlers

 

        // For displaying the keyword list...

 

        ed.PromptingForAngle += OnPromptingForAngle;

        ed.PromptingForCorner += OnPromptingForCorner;

        ed.PromptingForDistance += OnPromptingForDistance;

        ed.PromptingForDouble += OnPromptingForDouble;

        ed.PromptingForEntity += OnPromptingForEntity;

        ed.PromptingForInteger += OnPromptingForInteger;

        ed.PromptingForKeyword += OnPromptingForKeyword;

        ed.PromptingForNestedEntity += OnPromptingForNestedEntity;

        ed.PromptingForPoint += OnPromptingForPoint;

        ed.PromptingForSelection += OnPromptingForSelection;

        ed.PromptingForString += OnPromptingForString;

 

        // ... and removing it

 

        doc.CommandWillStart += OnCommandEnded;

        doc.CommandEnded += OnCommandEnded;

        doc.CommandCancelled += OnCommandEnded;

        doc.CommandFailed += OnCommandEnded;

 

        ed.EnteringQuiescentState += OnEnteringQuiescentState;

 

        // We'll also watch keystrokes, to see when global keywords

        // are entered

 

        Application.PreTranslateMessage += OnPreTranslateMessage;

 

        _keystrokes = new List<char>();

 

        // We need to turn off dynamic input: we'll reset the value

        // when we unload or in KWSX

 

        _dynmode = (short)Application.GetSystemVariable("DYNMODE");

        if (_dynmode != 0)

        {

          Application.SetSystemVariable("DYNMODE", 0);

          ed.WriteMessage(

            "\nDynamic input has been disabled and can be re-enabled"

            + " by the KWSX command."

          );

        }

        ed.WriteMessage(

          "\nGlobal keyword dialog enabled. Run KWSX to turn it off."

        );

      }

      else

      {

        ed.WriteMessage(

          "\nGlobal keyword dialog already enabled."

        );

      }

    }

 

    [CommandMethod("KWSX")]

    public void StopKeywordTranslation()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

 

      var ed = doc.Editor;

 

      if (_window == null)

      {

        // This means KWS hasn't been called...

 

        ed.WriteMessage(

          "\nGlobal keyword dialog already disabled."

        );

 

        return;

      }

      else

      {

        _window.Hide();

        _window = null;

      }

 

      // Remove our various event handlers

 

      // For displaying the keyword list...

 

      ed.PromptingForAngle -= OnPromptingForAngle;

      ed.PromptingForCorner -= OnPromptingForCorner;

      ed.PromptingForDistance -= OnPromptingForDistance;

      ed.PromptingForDouble -= OnPromptingForDouble;

      ed.PromptingForEntity -= OnPromptingForEntity;

      ed.PromptingForInteger -= OnPromptingForInteger;

      ed.PromptingForKeyword -= OnPromptingForKeyword;

      ed.PromptingForNestedEntity -= OnPromptingForNestedEntity;

      ed.PromptingForPoint -= OnPromptingForPoint;

      ed.PromptingForSelection -= OnPromptingForSelection;

      ed.PromptingForString -= OnPromptingForString;

 

      // ... and removing it

 

      doc.CommandWillStart -= OnCommandEnded;

      doc.CommandEnded -= OnCommandEnded;

      doc.CommandCancelled -= OnCommandEnded;

      doc.CommandFailed -= OnCommandEnded;

 

      ed.EnteringQuiescentState -= OnEnteringQuiescentState;

 

      Application.PreTranslateMessage -= OnPreTranslateMessage;

 

      Application.SetSystemVariable("DYNMODE", _dynmode);

 

      ed.WriteMessage(

        "\nGlobal keyword dialog disabled. Run KWS to turn it on."

      );

    }

 

    // Event handlers to display the keyword list

    // (each of these handlers needs a separate function due to the

    // signature, but they all do the same thing)

 

    private void OnPromptingForAngle(

      object sender, PromptAngleOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    private void OnPromptingForCorner(

      object sender, PromptPointOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    private void OnPromptingForDistance(

      object sender, PromptDistanceOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    private void OnPromptingForDouble(

      object sender, PromptDoubleOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    private void OnPromptingForEntity(

      object sender, PromptEntityOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    private void OnPromptingForInteger(

      object sender, PromptIntegerOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    void OnPromptingForKeyword(

      object sender, PromptKeywordOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    private void OnPromptingForNestedEntity(

      object sender, PromptNestedEntityOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    private void OnPromptingForPoint(

      object sender, PromptPointOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    private void OnPromptingForSelection(

      object sender, PromptSelectionOptionsEventArgs e

    )

    {

      // Nested selection sometimes happens (e.g. the HATCH command)

      // so only display keywords when there are some to display

 

      if (e.Options.Keywords.Count > 0)

        DisplayKeywords(e.Options.Keywords, true);

    }

 

    private void OnPromptingForString(

      object sender, PromptStringOptionsEventArgs e

    )

    {

      DisplayKeywords(e.Options.Keywords);

    }

 

    private void OnCommandWillStart(

      object sender, CommandEventArgs e

    )

    {

      HideKeywords();

    }

 

    private void OnCommandEnded(object sender, CommandEventArgs e)

    {

      HideKeywords();

    }

 

    // Event handlers to clear & hide the keyword list

 

    private void OnEnteringQuiescentState(object sender, EventArgs e)

    {

      HideKeywords();

    }

 

    private void OnPreTranslateMessage(

      object sender, PreTranslateMessageEventArgs e

    )

    {

      if (_tracking)

      {

        // Use of the arrow keys or delete kills our tracking

 

        var wp = e.Message.wParam.ToInt32();

        if (

          e.Message.message == WM_KEYDOWN && cancelKeys.Contains(wp)

        )

        {

          _tracking = false;

        }

        else if (

          e.Message.message == WM_KEYDOWN && enterKeys.Contains(wp)

        )

        {

          // Get our characters and then clear the list

 

          var chars = _keystrokes.ToArray();

          _keystrokes.Clear();

 

          // If the keyword list contains our string, send it

          // with a prefix of backspaces (to erase the prior

          // characters) and an underscore

 

          var kw = new string(chars);

          if (_window.ContainsKeyword(kw))

          {

            e.Handled = true;

            LaunchCommand(kw, kw.Length, true);

          }

        }

        else if (e.Message.message == WM_CHAR)

        {

          // If we have a backspace character, remove the last

          // entry in our character list, otherwise add the

          // character to the list

 

          if (wp == 8) // Backspace

          {

            if (_keystrokes.Count > 0)

              _keystrokes.RemoveAt(_keystrokes.Count - 1);

          }

          else if (ValidCharacter(wp)) // Normal character

          {

            _keystrokes.Add((char)wp);

          }

        }

      }

    }

 

    // Helper to display our keyword list

 

    private void DisplayKeywords(

      KeywordCollection kws, bool append = false

    )

    {

      if (!append)

      {

        _coreKeywords = kws;

      }

 

      // First we step through the keywords, collecting those

      // we want to display in a collection

 

      var sc = new StringCollection();

      if (append)

      {

        sc.AddRange(ExtractKeywords(_coreKeywords));

      }

      sc.AddRange(ExtractKeywords(kws));

 

      // If we don't have keywords to display, make sure the

      // current list is cleared/hidden

 

      if (sc.Count == 0)

      {

        _window.ClearKeywords(true);

      }

      else

      {

        // Otherwise we pass the keywords - as a string array -

        // to the display function along with a flag indicating

        // whether the current command is considered "special"

 

        var sa = new string[sc.Count];

        sc.CopyTo(sa, 0);

 

        // We should probably check for transparent/nested

        // command invocation...

 

        var cmd =

          (string)Application.GetSystemVariable("CMDNAMES");

        _window.ShowKeywords(

          sa, Array.IndexOf(specialCmds, cmd) >= 0

        );

 

        //Application.MainWindow.Focus();

 

        // Start tracking keyword keystrokes

 

        _tracking = true;

      }

    }

 

    private string[] ExtractKeywords(KeywordCollection kws)

    {

      var sc = new List<string>();

      if (kws != null && kws.Count > 0)

      {

        foreach (Keyword kw in kws)

        {

          if (kw.Enabled && kw.Visible && kw.GlobalName != "dummy")

          {

            sc.Add(kw.LocalName); // Expected this to be GlobalName

          }

        }

      }

      return sc.ToArray();

    }

 

    private void HideKeywords()

    {

      _keystrokes.Clear();

      _tracking = false;

      _window.ClearKeywords(true);

    }

 

    internal static void GiveAutoCADFocus()

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc != null)

        doc.Window.Focus();

      else

        Application.MainWindow.Focus();

    }

 

    internal static void LaunchCommand(

      string cmd, int numBspaces, bool terminate

    )

    {

      var doc = Application.DocumentManager.MdiActiveDocument;

      if (doc == null)

        return;

 

      doc.SendStringToExecute(

        Backspaces(numBspaces) + "_" + cmd + (terminate ? " " : ""),

        true, false, true

      );

 

      GiveAutoCADFocus();

    }

 

    private static string Backspaces(int n)

    {

      return new String((char)8, n);

    }

 

    private static bool ValidCharacter(int c)

    {

      var r = new Regex("^[a-zA-Z0-9]$");

      return r.IsMatch(Char.ToString((char)c));

    }

 

    internal static bool KeywordsMatch(string typed, string keyword)

    {

      if (Match(typed, keyword))

        return true;

 

      // Find the index of the first uppercase character in

      // the keyword being matched against

 

      var chars = new List<char>(keyword.ToCharArray());

      var upp = chars.Find(c => Char.IsUpper(c));

      var nth = keyword.IndexOf(upp);

 

      // Perform a similar check as the first one, this time

      // starting with the first uppercase character

 

      return

        nth <= 0 ? false : Match(typed, keyword.Substring(nth));

    }

 

    private static bool Match(string typed, string keyword)

    {

      // We can't match a keyword that's shorter than what

      // was typed

 

      if (typed.Length > keyword.Length)

        return false;

 

      // Check the typed keyword against the initial section of the

      // keyword to match of the same length (in lowercase)

 

      var tlow = typed.ToLower();

      var klow = keyword.Substring(0, typed.Length).ToLower();

 

      bool matchComplete = true;

 

      if (keyword.Length > typed.Length)

      {

        var rest = keyword.Substring(typed.Length);

        matchComplete = (rest == rest.ToLower());

      }

 

      return (tlow == klow && matchComplete);

    }

  }

}

I made some additional – actually fairly significant – changes to the project since we saw the code posted:

  • There was an issue with the code I’d posted for the global command helper: even though I thought I’d tested it thoroughly, Editor.Command() didn’t work in this particular context. I went back to Document.SendStringToExecute()… I’ll make sure the post gets updated.
  • We didn’t actually need a Popup window for our global keywords list. We now create a normal WPF window and make AutoCAD its owner, which means it will be minimised and restored along with AutoCAD and doesn’t stay “topmost” in the Z order.

Overall the app is really starting to shape up. We’re thinking about how best to distribute it for feedback (possibly via Labs – we’ll see). If you have the chance to give it a try yourself in the meantime, please post a comment or drop me an email.

Feed/Share

10 Random Posts