No muttering at the back! Reducing the background noise when sending commands to AutoCAD

Since posting three different options for Zooming to a Window or Entity inside AutoCAD, I've had a few discussions with a developer on how best to implement this cleanly. The requirement is to change the AutoCAD view via a smooth view transition (currently not exposed via any kind of view-modification API, only via the ZOOM command), but also to hide the fact we're sending commands to the command-line to do so.

While we were discussing, I remembered an old friend, the NOMUTT system variable, which allows almost all command-line noise to be filtered out - even the "Command:" prompt disappears. While this technique is useful for this specific situation, it's also potentially very useful for many other instances where sending commands are currently the best available method of accessing particular functionality inside AutoCAD.

A word of caution: setting NOMUTT to 1 can lead to severe user-disorientation. Please remember to set the variable back to 0, afterwards!

Here's the modified C# code showing this technique - this time using SendStringToExecute() from the .NET API, rather than the COM API's synchronous SendCommand():

using Autodesk.AutoCAD.ApplicationServices;

using Autodesk.AutoCAD.DatabaseServices;

using Autodesk.AutoCAD.EditorInput;

using Autodesk.AutoCAD.Runtime;

using Autodesk.AutoCAD.Geometry;

namespace ZoomSmoothlyAndQuietly

{

  public class Commands

  {

    // Zoom to a window specified by the user

    [CommandMethod("LZW")]

    static public void LoudZoomWindow()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Editor ed = doc.Editor;

      Point2d min, max;

      if (GetWindowForZoom(ed, out min, out max))

        ZoomWin(ed, min, max, false);

    }

    [CommandMethod("QZW")]

    static public void QuietZoomWindow()

    {

      Document doc =

        Application.DocumentManager.MdiActiveDocument;

      Editor ed = doc.Editor;

      Point2d min, max;

      if (GetWindowForZoom(ed, out min, out max))

        ZoomWin(ed, min, max, true);

    }

    // Get the coordinates for a zoom

    static private bool GetWindowForZoom(

      Editor ed, out Point2d min, out Point2d max

    )

    {

      min = new Point2d();

      max = new Point2d();

      PromptPointOptions ppo =

        new PromptPointOptions(

          "\nSpecify first corner:"

        );

      PromptPointResult ppr =

        ed.GetPoint(ppo);

      if (ppr.Status != PromptStatus.OK)

        return false;

      min =

        new Point2d(ppr.Value.X, ppr.Value.Y);

      PromptCornerOptions pco =

        new PromptCornerOptions(

          "\nSpecify opposite corner: ",

          ppr.Value

        );

      ppr = ed.GetCorner(pco);

      if (ppr.Status != PromptStatus.OK)

        return false;

      max =

        new Point2d(ppr.Value.X, ppr.Value.Y) ;

      return true;

    }

    // Zoom by sending a command

    private static void ZoomWin(

      Editor ed, Point2d min, Point2d max, bool quietly

    )

    {

      string lower =

        min.ToString().Substring(

          1,

          min.ToString().Length - 2

        );

      string upper =

        max.ToString().Substring(

          1,

          max.ToString().Length - 2

        );

      string cmd =

        "_.ZOOM _W " + lower + " " + upper + " ";

      if (quietly)

      {

        // Get the old value of NOMUTT

        object nomutt =

          Application.GetSystemVariable("NOMUTT");

        // Add the string to reset NOMUTT afterwards

        cmd += "_NOMUTT " + nomutt.ToString() + " ";

        // Set NOMUTT to 1, reducing cmd-line noise

        Application.SetSystemVariable("NOMUTT", 1);

      }

      // Send the command(s)

      ed.Document.SendStringToExecute(

        cmd, true, false, !quietly

      );

    }

  }

}

Here's what happens when we run the "loud" command (LZW), and then the "quiet" one (QZW):

Command: LZW

Specify first corner:

Specify opposite corner:

Command: _.ZOOM

Specify corner of window, enter a scale factor (nX or nXP), or

[All/Center/Dynamic/Extents/Previous/Scale/Window/Object] <real time>: _W

Specify first corner: 13.1961936276982,12.972925917324 Specify opposite corner:

30.6221132095147,0.482638801189623

Command: QZW

Specify first corner:

Specify opposite corner:

Command:

Both versions of the command deliver the results in terms of smooth view transitions, but the quiet version reduces the command-line clutter by a) passing a flag to SendStringToExecute() to stop the command from being echoed and b) setting the NOMUTT system variable to prevent the command "muttering" from being echoed. It uses a simple trick to reset the system variable afterwards by appending "_NOMUTT 0 " to the string to be executed (which we don't see, as NOMUTT is still set to 1 :-). Assuming the ZOOM command terminates correctly, the NOMUTT system variable should be reset: there is a slight risk that something causes ZOOM to fail, at which point it might be worth checking NOMUTT from time to time elsewhere in your app: as mentioned earlier, having NOMUTT set to 1 can be very disconcerting for the user. I should, in fact, probably just force NOMUTT to 0 after the ZOOM (if you check the the above code, you'll see we reset it to the prior value, which is generally a good technique when modifying system variables as they retain the value previously chosen by the user). Anyway - I'll leave the final choice up to you, but do be aware of the risk.

Update:

Undo is a problem with this implementation - havin NOMUTT as a separate command leaves it at risk of being undone by the user (leaving the system in a scary, silent state). This post presents an enhanced approach which provides much better support for undo.

10 responses to “No muttering at the back! Reducing the background noise when sending commands to AutoCAD”

  1. Hi Kean.

    I've been using animated view transitions in the pans and zooms that some of my apps do for quite some time.

    I use the undocumented AcMdgViewTransitionServices class from acmdginternal.dll to temporarily enable animated view transitions if they're not curently enabled for scripts, and then just P/Invoke acedCmd() to run the ZOOM command, which of course, makes no noise (with CMDECHO set to 0).

    I have yet to see a good case for why we should avoid the use of acedCmd() from the document context to entirely avoid the kinds of problems your code solves using NOMOTT.

  2. Hi Tony,

    I've actually realised that sending a few escape chars should reduce (probably remove) the risk of NOMUTT remaining set. I need to give it a try, but that seems pretty solid.

    Ultimately it's all down to choice, of course. I'd tend to stick to supported (and ideally managed) APIs, where possible, all else being equal.

    Cheers,

    Kean

  3. Hi Kean,

    a more general question:
    How can I obtain an object which the user selected before entering the command ?

    Mark

  4. Hi Mark,

    Try this previous post.

    Kean

  5. Hi Kean,

    thanks for your tip. I still needed a second try to get it working, because I overlooked the CommandFlags.UsePickSet flag.
    Readers might interest that specifying CommandFlags.Redraw leaves the selection intact after execution of the usercommand, if you omit it, the selection is cleared.

    Instead of using some more natural term as SelectedObject AUTOCAD developers chose SelectImplied which is as arcane as the whole object model. Even simple things turn out to need an unnecessary complicate solution.
    Really annoying is, that there is no (good) documentation on all that stuff.
    Autodesk should take care of this instead of producing videos like "10 easy ways to crash your Autocad".

    But fortunately we have you Kean ๐Ÿ™‚ Your blog turned out to be the most valuable source for information regarding the .NET interface.

    Keep on and thanks again

    Mark

  6. "Ultimately it's all down to choice, of course. I'd tend to stick to supported (and ideally managed) APIs, where possible, all else being equal."

    Perhaps, but I don't regard acedCommand() as an unsupported API and ultimately, most of the managed APIs we use are just wrappers that delegate to native code anyway ๐Ÿ™‚

    Couple that with the various other problems many have with SendStringToExecute's asynchronous nature and the fact that it can only accept string input, is what makes acedCommand() a much more robust solution, from my perspective.

  7. I wasn't actually referring to acedCommand() - I was more talking about the use of acmdginternal.dll being unsupported, although that was only a side point.

    That said, acedCommand() is indeed not supported from .NET. Yes, our managed wrappers call through to unmanaged APIs internally, but you'll notice we haven't wrapped acedCommand(). We support people driving commands by sending them via the command-throat (SendStringToExecute() or COM's SendCommand()).

    I'm not saying you shouldn't do what you're doing, Tony, I'm just sharing a different perspective: while you may consider it less clean to pump commands, from an AutoCAD internals perspective it's actually considered best practice (it removes the possibility of exceeding levels of re-entrancy, for instance and is slightly more future-proof: Microsoft's threat to deprecate fibers will probably impact acedCommand() in some way, at least that's my understanding).

    A big problem I've realised with the above post is related to Undo, but there'll be a post going live on Monday to show a slightly different approach to address that.

    Kean

  8. Hi Kean,
    Your post is very useful to fresh hands in this field.
    Thank you
    Geospatial Utilities

  9. I am using visual studio 2005 and RealDWG 2009 and I trying to open the dwg file using realdwg library in c# application and following is my source code.

    using System;
    using System.Collections.Generic;
    using System.ComponentModel;
    using System.Data;
    using System.Drawing;
    using System.Text;
    using System.Windows.Forms;
    using Autodesk.AutoCAD.DatabaseServices;
    namespace RealDWGTest
    {
    public partial class Form1 : Form
    {
    public Form1()
    {
    InitializeComponent();
    InitializeRealDwgComponents(); // This line shows the error
    }

    private void InitializeRealDwgComponents()
    {
    Autodesk.AutoCAD.Runtime.RuntimeSystem.Initialize(new RealDWGApp(), 1033);
    Database db = new Database();
    db.ReadDwgFile("C:\test.dwg", FileOpenMode.OpenForReadAndAllShare, true, "");
    }
    }

    public class RealDWGApp : Autodesk.AutoCAD.DatabaseServices.HostApplicationServices
    {
    public override string FindFile(string fileName, Database database, FindFileHint hint)
    {
    throw new Exception("The method or operation is not implemented.");
    }
    }
    }

    When I try to run this program following error occurred:
    "Could not load file or assembly 'acdbmgd, Version=17.2.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified."

    Please help to solve this problem.

  10. Santosh -

    This question is off-topic. Please submit it via the ADN website, if you're a member.

    Otherwise you might have some luck at the AutoCAD .NET Customization Discussion Group - I don't believe there's a group dedicated to RealDWG, unfortunately.

    Regards,

    Kean

Leave a Reply to GIS Utilities Cancel reply

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