雪山飞狐_lzh 发表于 2009-5-19 14:17:00

Kean专题(5)—Commands

本帖最后由 作者 于 2009-5-19 15:02:55 编辑

原帖:http://through-the-interface.typepad.com/through_the_interface/commands/
一、AutoCAD的命令编程技术
August 11, 2006
Techniques for calling AutoCAD commands programmatically
It's quite common to want to call commands from one or other of AutoCAD's programming environments. While it's cleanest (from a purist's perspective) to use an API to perform the task you want, the quickest way - and the one which will appeal to the pragmatists (and the lazy) among us is often to call a sequence of commands. And there are, of course, a few places in AutoCAD where APIs have not been exposed, so scripting commands is the only way to achieve what you want.
Let's take the simple example of adding a line. Here's what you'd do from the different environments from a low-level API perspective:
LISP - create an association list representing the entity and then (entmake) it
VBA (or COM) - get the ModelSpace object from the active drawing and call AddLine (the COM API is probably the simplest in that respect)
.NET and ObjectARX - open the block table and then the model-space block table record, create a new line object and append it to the model-space (and to the transaction, if you're using them), closing the various objects along the way
Having first started coding for AutoCAD with LISP (for R10), I know that the simplest way to do what you want from that environment is to call the LINE command, passing in the start and end points:
(command "_LINE" "0,0,0" "100,100,0" "")
LISP is great in that respect: as you're not able to define native commands (only LISP) functions, it's perfectly acceptable to use it to script commands to do what you want, rather than rely on low-level APIs.
ObjectARX in particular has potential issues with respect to defining native commands calling commands, as AutoCAD is only "re-entrant" up to 4 levels. Without going into specifics, it's basically best to avoid calling commands using acedCommand() from ObjectARX, unless the command is registered as a LISP function using acedDefun().
While you do have to be careful when calling commands from VBA or ObjectARX, there are a few options available to you.
ObjectARX
ads_queueexpr()
This old favourite is intended to be used from acrxEntryPoint() to execute a sequence of commands after (s::startup) has been called from LISP (as you are not able to use acedCommand() from this context)
You need to declare it yourself (extern "C" void ads_queueexpr( ACHAR *);) before use
It has been unsupported as long as I can remember, but is widely-used and mentioned in the Tips & Techniques section of the ObjectARX Developer's Guide
AcApDocManager::sendStringToExecute()
This function has the advantage of a few more options being available as arguments, mainly around where to execute the string (which document, and whether it be activated), and whether to echo the string to the command-line
::SendMessage()
This is a standard Win32 platform API and so can, in effect, be used to drive AutoCAD from an external client. It uses a structure to pass the string that is often a bit tricky to set up (it became a migration issue when we switched to Unicode, for example)
IAcadDocument::SendCommand()
This COM method is the only way (other than acedCommand() or acedCmd()) to execute a command synchronously from AutoCAD (and even then it may not be completely synchronous if requiring user input)
acedCommand()
This is the ObjectARX equivalent to (command), and is genuinely synchronous. Unfortunately (as mentioned earlier) there are issues with using it directly from a natively-registered command, so I'd recommend only using it from acedDefun()-registered commands (see the ObjectARX documentation and the below sample for more details)
VBA (some of which also applies to VB)
ThisDrawing.SendCommand
This is the same as IAcadDocument::SendCommand() from C++
SendKeys
This is just a simple technique to send key-strokes to the command-line
SendMessage
This is just the Win32 API mentioned above, but declared and called from VB(A)
So, now for some sample code...


ObjectARX sample code
The first can be dropped into an ObjectARX Wizard-defined project (I used Visual Studio 2005 and ObjectARX 2007). You'll need to make sure "COM-client" is selected and you name your project "SendingCommands" (or you search and replace to change the name in the below code to something you prefer).
The code creates points along a line (from 0,0 to 5,5), using different techniques to send the command to AutoCAD. I would, of course, use the proper ObjectARX APIs to do this (creating an AcDbPoint etc.) - I just used this as an example of a command that could be sent.
It creates the first point on load, using ads_queueexpr(), and defines commands (TEST1, TEST2, TEST3 and TEST4) for the subsequent tests (the last being an acedDefun()-registered command).#include "StdAfx.h"
#include "resource.h"

#define szRDS _RXST("Adsk")

extern "C" void ads_queueexpr( ACHAR *);

//----- ObjectARX EntryPoint
class CSendingCommandsApp : public AcRxArxApp {

public:
CSendingCommandsApp () : AcRxArxApp () {}

virtual AcRx::AppRetCode On_kInitAppMsg (void *pkt) {
    // You *must* call On_kInitAppMsg here
    AcRx::AppRetCode retCode =AcRxArxApp::On_kInitAppMsg (pkt) ;
    return (retCode) ;
}

virtual AcRx::AppRetCode On_kUnloadAppMsg (void *pkt) {
    // You *must* call On_kUnloadAppMsg here
    AcRx::AppRetCode retCode =AcRxArxApp::On_kUnloadAppMsg (pkt) ;
    return (retCode) ;
}

virtual AcRx::AppRetCode On_kLoadDwgMsg(void * pkt) {
    AcRx::AppRetCode retCode =AcRxArxApp::On_kLoadDwgMsg (pkt) ;
    ads_queueexpr( _T("(command\"_POINT\" \"1,1,0\")") );
    return (retCode) ;
}

virtual void RegisterServerComponents () {
}

public:

// - AdskSendingCommands._SendStringToExecTest command (do not rename)
static void AdskSendingCommands_SendStringToExecTest(void)
{
    acDocManager->sendStringToExecute(curDoc(), _T("_POINT 2,2,0 "));   
}

static void SendCmdToAcad(ACHAR *cmd)
{
    COPYDATASTRUCT cmdMsg;
    cmdMsg.dwData = (DWORD)1;
    cmdMsg.cbData = (DWORD)(_tcslen(cmd) + 1) * sizeof(ACHAR);
    cmdMsg.lpData = cmd;
    SendMessage(adsw_acadMainWnd(), WM_COPYDATA, NULL, (LPARAM)&cmdMsg);
}

// - AdskSendingCommands._SendMessageTest command (do not rename)
static void AdskSendingCommands_SendMessageTest(void)
{
    SendCmdToAcad(_T("_POINT 3,3,0 "));
}

// - AdskSendingCommands._SendCommandTest command (do not rename)
static void AdskSendingCommands_SendCommandTest(void)
{
    try {
      IAcadApplicationPtr pApp = acedGetIDispatch(TRUE);
      IAcadDocumentPtr pDoc;
      pApp->get_ActiveDocument(&pDoc);
      pDoc->SendCommand( _T("_POINT 4,4,0 ") );
    }
    catch(_com_error& e) {
      acutPrintf(_T("\nCOM error: %s"), (ACHAR*)e.Description());
    }
}

// ----- ads_test4 symbol (do not rename)
static int ads_test4(void)
{
    acedCommand(RTSTR, _T("_POINT"), RTSTR,_T("5,5,0"), RTNONE);
    acedRetVoid();
    return (RSRSLT);
}
} ;

// ----------------------------------------------------------

ACED_ARXCOMMAND_ENTRY_AUTO(CSendingCommandsApp, AdskSendingCommands, _SendStringToExecTest, TEST1, ACRX_CMD_TRANSPARENT, NULL)
ACED_ARXCOMMAND_ENTRY_AUTO(CSendingCommandsApp, AdskSendingCommands, _SendMessageTest, TEST2, ACRX_CMD_TRANSPARENT, NULL)
ACED_ARXCOMMAND_ENTRY_AUTO(CSendingCommandsApp, AdskSendingCommands, _SendCommandTest, TEST3, ACRX_CMD_TRANSPARENT, NULL)
ACED_ADSCOMMAND_ENTRY_AUTO(CSendingCommandsApp, test4, true)
// ----------------------------------------------------------
IMPLEMENT_ARX_ENTRYPOINT(CSendingCommandsApp)

VBA sample code
This sample defines VBA macros that create points from 6,6 to 8,8, using techniques that mirror the ones shown in the ObjectARX sample.Option Explicit OnPrivate Const WM_COPYDATA = &H4A
Private Type COPYDATASTRUCT
    dwData As Long
    cbData As Long
    lpData As String
End Type
Private Declare Function SendMessage Lib "user32" Alias _
"SendMessageA" (ByVal hwnd As Long, ByVal wMsg As Long, ByVal _
wParam As Long, ByVal lParam As Any) As Long
Public Sub SendMessageToAutoCAD(ByVal message As String)
    Dim data As COPYDATASTRUCT
    Dim str As String
    str = StrConv(message, vbUnicode) 'converts to Unicode
    data.dwData = 1
    data.lpData = str
    data.cbData = (Len(str) + 2)
    SendMessage(ThisDrawing.Application.hwnd, WM_COPYDATA, 0, data)
End Sub
Public Sub Test4()
    ThisDrawing.SendCommand("_point 6,6,0 ")
End Sub
Public Sub Test5()
    SendKeys("_point 7,7,0 ")
End Sub
Public Sub Test6()
    SendMessageToAutoCAD("_point 8,8,0 ")
End Sub


雪山飞狐_lzh 发表于 2009-5-19 15:01:00

九、为命令添加别名
January 31, 2008
Adding aliases for custom AutoCAD commands
I had an interesting question come in by email and thought I'd share it via this post. To summarise the request, the developer needed to allow command aliasing for their custom commands inside AutoCAD. The good news is that there's a standard mechanism that works for both built-in and custom commands: acad.pgp.
acad.pgp is now found in this location on my system:
C:\Documents and Settings\walmslk\Application Data\Autodesk\AutoCAD 2008\R17.1\enu\Support\acad.pgpWe can edit this text file to add our own command aliases at the end:
NL,*NETLOAD
MCC, *MYCUSTOMCOMMANDHere we've simply created an alias for the standard NETLOAD command (a simple enough change, but one that saves me lots of time when developing .NET modules) and for a custom command called MYCUSTOMCOMMAND. If I type NL and then MCC at the AutoCAD command-line, after saving the file and re-starting AutoCAD, I see:
Command: NL
NETLOAD
Command: MCC
Unknown command "MYCUSTOMCOMMAND".Press F1 for help.Now the fact that MCC displays an error is fine - I don't actually have a module loaded that implements the MYCUSTOMCOMMAND command - but we can see that it found the alias and used it (and the MYCUSTOMCOMMAND name could also be used to demand-load an application, for instance).

雪山飞狐_lzh 发表于 2009-5-19 14:22:00

<p>二、取消一个处于激活状态的命令</p><p>August 14, 2006<br/>Cancelling an active command in AutoCAD<br/>Thanks to Alexander Rivilis for this topic (he submitted a comment to my previous post that got me thinking about this addendum).</p><p>When you're asking AutoCAD to execute commands by submitting them to its command-line, it helps to make sure no command is currently active. The accepted approach is to send two escape characters to the command-line... this is adopted by our menu &amp; toolbar macros that consistently start with ^C^C in MNU and now CUI files.</p><p>Why two? The first is to cancel the "most active" command, and the second is to drop out of either the dimension mode or the outermost command, if the innermost was actually being executed transparently. Strictly speaking there are probably a few cases where you might actually need three escape characters. Type "DIM VERT 'ZOOM" into the command-line, and you'll need three escapes to get back to the "Command:" prompt, but that's fairly obscure - two is generally considered enough for most realistic situations.</p><p>So... what constitutes an&nbsp; escape character? Well, luckily we no longer need to thumb back to the&nbsp; ASCII table in the back of our AutoCAD R12 Programming manuals to find this out. It's ASCII code 27, which is represented as 1B in hexadecimal notation.</p><p>A common way to send the character from C++ is to send the string "\x1B" for each escape character to AutoCAD. From VB you would tend to use Chr$(27) for the same purpose.</p><p>In terms of your choice for sending the character(s) across to AutoCAD... you can generally use one of the functions suggested previously, sendStringToExecute(), SendMessage() or SendCommand(). It won't work from ads_queueexpr(), but then given its typical usage context you shouldn't need to use it for this. Also, as Alexander very rightly pointed out in his comment, you might use PostMessage() or acedPostCommand() (more on this second one later).</p><p>SendMessage() and PostMessage() are very similar - you can check this article for the differences. I tend to prefer SendMessage() for synchronous use, as it waits to have an effect before returning, but PostMessage() is still good to have around in the toolbox (it's good for firing off keystrokes into a window's message loop).</p><p>acedPostCommand() is another undocumented function, which needs declaring in your code before use:</p><p>extern Adesk::Boolean acedPostCommand(const ACHAR* );</p><p>What's particularly interesting about this function is its use of the special keyword, which should get you back to the "Command:" prompt irrespective of command nesting:</p><p>acedPostCommand(_T("CANCELCMD"));</p><p>Then, of course, you can continue to use it as shown in my previous post:</p><p>acedPostCommand(_T("_POINT 6,6,0 "));</p><p>One quick note about cancelling commands from acedCommand(). If you pass RTNONE as the only argument to acedCommand(), this will be interpreted as an escape by AutoCAD.</p>

雪山飞狐_lzh 发表于 2009-5-19 14:25:00

本帖最后由 作者 于 2009-5-25 16:14:37 编辑

三、在VLisp中调用命令的例子
August 28, 2006
Calling DIMARC from Visual LISP
This is an interesting little problem that came in from Japan last week. Basically the issue is understanding how to call the DIMARC command programmatically from LISP using (command).
The DIMARC command was introduced in AutoCAD 2006 to dimension the length of arcs or polyline arc segments. What's interesting about this problem is that it highlights different aspects of entity selection in LISP.
Firstly, let's take a look at the DIMARC command (I've put the keyboard-entered text in red below):Command: DIMARC
Select arc or polyline arc segment:
Specify arc length dimension location, or :
Dimension text = 10.6887
The first prompt is for the arc or polyline arc segment, the second is for the dimension location (we're not going to worry about using other options).
The classic way to select an entity is to use (entsel):Command: (entsel)
Select object: (<Entity name: 7ef8e050> (22.1694 32.2269 0.0))
This returns a list containing both the entity-name and the point picked when selecting the entity. This is going to be particularly useful in this problem, as the DIMARC command needs to know where the point was picked: this is because it has to support picking of a "complex" entity, as it works not only on arcs but on arc segments of polylines. If you use the classic technique of using (car)stripping off the point, to just pass in the entity name, it fails:Command: DIMARC
Select arc or polyline arc segment: (car (entsel))
Select object: <Entity name: 7ef8e050>
Command:
Whereas just passing the results of (entsel) works fine:Command: DIMARC
Select arc or polyline arc segment: (entsel)
Select object: (<Entity name: 7ef8e050> (22.2696 32.2269 0.0))
Specify arc length dimension location, or :
Dimension text = 24.3844
So it's clear we need to keep the point in there. So we know what we have to do to select arc entities - we can pass in the results of (entsel).
Polyline entities are trickier. You can pass in the entity name, with or without the point, and the command won't work:Command: DIMARC
Select arc or polyline arc segment: (entsel)
Select object: (<Entity name: 7ef8e058> (51.0773 29.5726 0.0))
Object selected is not an arc or polyline arc segment.
Select arc or polyline arc segment:
But we can pass in the point picked by (entsel), and let the DIMARC command do the work of determining whether the segment picked was actually an arc or not:Command: DIMARC
Select arc or polyline arc segment: (cadr (entsel))
Select object: (52.6305 29.5726 0.0)
Specify arc length dimension location, or :
Dimension text = 9.4106
So now we're ready to put it all together.
[ Note: The below example does not take into consideration the user cancelling from (getpoint) or (entsel) - that is left as an exercise for the reader. ](defun c:myDimArc(/ en pt ed)
(setq en (entsel "\nSelect arc or polyline arc segment: ")
      pt (getpoint "\nSpecify arc length dimension location: ")
      ed (entget (car en))
)
; If the object selected is a 2D polyline (old or new)
; then pass the point instead of the entity name
(if (member (cdr (assoc 0 ed)) '("POLYLINE" "LWPOLYLINE"))
    (setq en (cadr en))
)
(command "_.DIMARC" en pt)
(princ)
)
This is just an example of how you need to call the DIMARC command programmatically - with this technique you clearly lose the visual feedback from the dragging of the entity during selection (what we call the "jig" in ObjectARX). And this is ultimately an unrealistic example - you're more likely to need to call DIMARC automatically for a set of entities in a drawing than just define a simple command to request user input and pass it through.
One thing this example does demonstrate is that different commands have different needs in terms of entity selection information (some just need a selection set or an entity name, some need more that than).

雪山飞狐_lzh 发表于 2009-5-19 14:34:00

本帖最后由 作者 于 2009-5-25 16:17:53 编辑

四、如何向.Net定义的命令传递参数
September 01, 2006
Passing arguments to .NET-defined commands
Another case of planets aligning: two different people have asked me this question in two days...
It's quite common for commands to require users to provide additional input during execution. This information might be passed in via a script or a (command) from LISP, or it may simply be typed manually or pasted in by the user.
The fact is, though, that commands don't actually take arguments. It may seem like they do, but they don't. What they do is ask for user input using dialogs or the command-line.
Here are a few tips on how to support passing of information into your commands...
Define a version of your command that asks for input via the command-line
It's very easy to define beautiful UIs in .NET applications. You absolutely should do so. But it's also helpful to provide an alternative that can be called via the command-line and from scripts. I'd suggest a couple of things here for your commands:
Define a standard version (e.g. "MYCOMMAND") and a command-line version ("-MYCOMMAND"). It's common to prefix command-line versions of commands in AutoCAD with a hyphen (see -PURGE vs. PURGE, for instance).
An alternative is to check for the values of FILEDIA and CMDDIA system variables - see the AutoCAD documentation on these commands to understand what effect they're meant to have on commands.
When implementing a command-line version of your command, you simply use the standard user input functions (GetXXX()) available in the Autodesk.AutoCAD.EditorInput namespace. Here's some VB.NET code showing this:
    <CommandMethod("TST")> _
    Public Sub Test()
      Dim ed As Editor
      ed = Application.DocumentManager.MdiActiveDocument.Editor
      Dim name As PromptResult = ed.GetString(vbCr + "Enter name: ")
      ed.WriteMessage("You entered: " + name.StringResult)
    End Sub
When it's run, you get this (typed text in red):
Command: tst
Enter name: Hello
You entered: Hello
Command:By the way, I tend to put a vbCr (or "\n" in ObjectARX) in front of prompts being used in GetXXX() functions, as it's also possible to terminate text input with a space in AutoCAD: this means that even is a space is entered to launch the command, the prompt string displays on a new line.
Define a LISP version of your command
The <LispFunction> attribute allows you to declare .NET functions as LISP-callable. A very good technique is to separate the guts of your command into a separate function that is then called both by your command (after it has asked the user for the necessary input) and by the LISP-registered function, which unpackages the arguments passed into it.
To understand more about how to implement LISP-callable functions in .NET, see this previous post.
July 31, 2006
Breathing fresh life into LISP applications with a modern GUI
This recent entry on Jimmy Bergmark's JTB World Blog brought to my attention the fact that ObjectDCL is about to become an Open Source project. Chad Wanless, the father of ObjectDCL, was a very active ADN member for many years, but - according to this post on the ObjectARX discussion group - is now unable to spend time working on ObjectDCL due to a severe medical condition. In case Chad is reading this... Chad - all of us here at ADN wish you a speedy recovery and all the best for your future endeavours.
Ignoring the background behind the decision to post the technology as Open Source, this is good news for developers with legacy codebases that include user interfaces implemented using DCL (Dialog Control Language).
I did want to talk a little about what other options are available to developers of LISP applications that make use of DCL today. There are a couple of approaches to calling modules implementing new user interfaces for LISP apps, whether through COM or through .NET. I'll talk about both, but I will say that .NET is the most future-proof choice at this stage.
Both techniques work on the principle that you redesign your user interface using VB6 or VB.NET/C#, and call through to these functions from LISP. Reality is often more complex - you may have more complex interactions from (for instance) particular buttons in your dialog - but these examples demonstrate what you can do to replace a fairly simple UI where you pass the initial variables into a function and receive the modified variables at the other end, once the user closes the dialog. You can also extend it to handle more complex situations, but there may be much more work needed - perhaps even use of AutoCAD's managed API from within the dialog code.
COM: Using a VB ActiveX DLL from Visual LISP
For an in-depth description of this technique, ADN members can find the information here. I apologise to those who are not able to access this content, but I don't want to dillute the issue by copying/pasting the whole article into this blog. Especially as this technique is describing the use of VB6, which is no longer at the forefront of Microsoft's development efforts.
The approach is to create an ActiveX DLL project in VB6, which is simply a COM module implementing code that can be referenced and called using a ProgID. AutoCAD's COM Automation interface exposes a method called GetInterfaceObject from the Application object that simply calls the equivalent of CreateObject on the ProgID passed in, but within AutoCAD's memory space. Once you've loaded a module using GetInterfaceObject, not only can you then call code displaying fancy VB-generated UIs from LISP, but because the code is in the same memory space as AutoCAD, it executes very quickly - on a par with VBA, ObjectARX or the managed API in terms of the speed with which it can access AutoCAD's object model.
.NET: Defining LISP-callable functions from a .NET application
The following technique is really the more future-proof approach, and has become possible since the implementation of "LISP callable wrappers" in AutoCAD 2007's managed API. Essentially it comes down to the ability to declare specific .NET functions as being LISP-callable. If you look back at one of my early posts about creating a .NET application, you'll notice the use of an attribute to declare a command, such as <CommandMethod("MyCommand")>. With AutoCAD 2007 you can simply use <LispFunction("MyLispFunction")> to denote a function that can be called directly from LISP.
From there it's simply a matter of unpackaging the arguments passed in and packaging up the results (the bit in-between is where you get to have fun, using .NET capabilities to create beautiful user interfaces or to integrate with other systems etc., etc.). Here's some code to show the handling of arguments and packaging of the results:
<LispFunction("LISPfunction")> _
Public Function VBfunction(ByVal rbfArgs As ResultBuffer) As ResultBuffer
    'Get the arguments passed in...
    Dim arInputArgs As Array
    Dim realArg1 As Double
    Dim intArg2 As Integer
    Dim strArg3 As String
    arInputArgs = rbfArgs.AsArray
    realArg1 = CType(arInputArgs.GetValue(0), TypedValue).Value
    intArg2 = CType(arInputArgs.GetValue(1), TypedValue).Value
    strArg3 = CType(arInputArgs.GetValue(2), TypedValue).Value
    'Do something interesting here...
    '...
    '...
    'Package the results...
    'Use RTREAL (5001) for doubles
    'Use RTSTR (5003) for strings
    'Use RTSHORT (5005) for integers
    Dim rbfResult As ResultBuffer
    rbfResult = New ResultBuffer( _
      New TypedValue(CInt(5001), 3.14159), _
      New TypedValue(CInt(5003), 42), _
      New TypedValue(CInt(5005), "Goodbye!"))
    Return rbfResult
End Function
The code assumes the first argument passed in will be a real, followed by an integer and then finally a string. Here's what happens if you call the code like this:
Command: (LISPfunction 1.234 9876 "Hello!")
(3.14159 42 "Goodbye!")Here are a couple of useful articles on the ADN site regarding this:
LispFunction examples for AutoLISP to .NET
      
.NET ResultBuffer returns dotted pairs to Visual LISP instead of normal list

雪山飞狐_lzh 发表于 2009-5-19 14:43:00

五、使用委托取消命令
October 31, 2006
Blocking AutoCAD commands from .NET
Thanks to Viru Aithal from the DevTech team in India team for this idea (he reminded me of this technique in a recent ADN support request he handled).
A quick disclaimer: the technique shown in this entry could really confuse your users, if implemented with inadequate care. Please use it for good, not evil.
I talked at some length in previous posts about MDI in AutoCAD, and how various commands lock documents when they need to work on them. When commands try to lock (or unlock) a document, an event gets fired. You can respond to this event in your AutoCAD .NET module, asking AutoCAD to veto the operation requesting the lock.
For some general background on events in .NET, I’d suggest checking out this information on MSDN, both from the Developer’s Guide and from MSDN Magazine. To see how they work in AutoCAD, take a look at the EventsWatcher sample on the ObjectARX SDK, under samples/dotNet/EventsWatcher. This is a great way to see what information is provided via AutoCAD events.
It seems that almost every command causes the DocumentLockModeChanged event to be fired – I would say every command, but I can imagine there being commands that don’t trigger it, even if the technique appears to work even for commands that have no reason to lock the current document, such as HELP.
As you can imagine, having something block commands that a user expects to use could result in a serious drop in productivity, but there are genuine cases where you might have a legitimate need to disable certain product functionality, such as to drive your users through a more automated approach to performing the same task. This is not supposed to act as a fully-fledged security layer – an advanced user could easily prevent a module from being loaded, which invalidates this technique – but it could still be used to help stop the majority of users from doing something that might (for instance) break the drawing standards implemented in their department.
So, let’s take a look at what needs to happen to define and register our event handler.
Firstly, we must declare a callback function somewhere in our code that we will register as an event handler. In this case we’re going to respond to the DocumentLockModeChanged event, and so will take as one of our arguments an object of type DocumentLockModeChangedEventArgs:
VB.NET:
Private Sub vetoLineCommand (ByVal sender As Object, _
ByVal e As DocumentLockModeChangedEventArgs)
If (e.GlobalCommandName = "LINE") Then
    e.Veto()
End If
End Sub
C#:
private void vetoLineCommand(
object sender,
DocumentLockModeChangedEventArgs e)
{
if (e.GlobalCommandName == "LINE")
{
    e.Veto();
}
}

This callback function, named vetoLineCommand(), simply checks whether the command that is requesting the change in document lock mode is the LINE command, and, if so, it then vetoes it. In our more complete sample, later on, we’ll maintain a list of commands to veto, which can be manipulated by the user using come commands we define (you will not want to do this in your application – this is for your own testing during development).
Next we need to register the command. This event belongs to the DocumentManager object, but the technique for registering events is different between VB.NET and C#. In VB.NET you use the AddHandler() keyword, referring to the DocumentLockModeChanged event from the DocumentManager object – in C# you add it directly to the DocumentLockModeChanged property. This could be done from a command or from the Initialize() function, as I’ve done in the complete sample further below.
VB.NET:
Dim dm As DocumentCollection = Application.DocumentManager()
AddHandler dm.DocumentLockModeChanged, AddressOf vetoLineCommandC#:
DocumentCollection dm = Application.DocumentManager;
dm.DocumentLockModeChanged += new DocumentLockModeChangedEventHandler(vetoLineCommand);That's really all there is to it... as mentioned, the complete sample maintains a list of commands that are currently being vetoed. I've made this shared/static, and so it will be shared across documents (so if you try to modify this in concurrent drawings, you might get some interesting results).
Here's the complete sample (just to reiterate - use this technique both at your own risk and with considerable caution).
VB.NET:
Imports Autodesk.AutoCAD.Runtime
Imports Autodesk.AutoCAD.ApplicationServices
Imports Autodesk.AutoCAD.EditorInput
Imports System.Collections.Specialized
Namespace VetoTest
Public Class VetoCmds
    Implements IExtensionApplication
    Shared cmdList As StringCollection _
      = New StringCollection
    Public Sub Initialize() _
      Implements IExtensionApplication.Initialize
      Dim dm As DocumentCollection
      dm = Application.DocumentManager()
      AddHandler dm.DocumentLockModeChanged, _
      AddressOf vetoCommandIfInList
    End Sub
    Public Sub Terminate() _
      Implements IExtensionApplication.Terminate
      Dim dm As DocumentCollection
      dm = Application.DocumentManager()
      RemoveHandler dm.DocumentLockModeChanged, _
      AddressOf vetoCommandIfInList
    End Sub
    <CommandMethod("ADDVETO")> _
    Public Sub AddVeto()
      Dim ed As Editor _
      = Application.DocumentManager.MdiActiveDocument.Editor
      Dim pr As PromptResult
      pr = ed.GetString(vbLf + _
      "Enter command to veto: ")
      If (pr.Status = PromptStatus.OK) Then
      If (cmdList.Contains(pr.StringResult.ToUpper())) Then
          ed.WriteMessage(vbLf + _
            "Command already in veto list.")
      Else
          cmdList.Add(pr.StringResult.ToUpper())
          ed.WriteMessage( _
            vbLf + "Command " + _
            pr.StringResult.ToUpper() + _
            " added to veto list.")
      End If
      End If
    End Sub
    <CommandMethod("LISTVETOES")> _
    Public Sub ListVetoes()
      Dim ed As Editor _
      = Application.DocumentManager.MdiActiveDocument.Editor
      ed.WriteMessage( _
      "Commands currently on veto list: " + vbLf)
      For Each str As String In cmdList
      ed.WriteMessage(str + vbLf)
      Next
    End Sub
    <CommandMethod("REMVETO")> _
    Public Sub RemoveVeto()
      ListVetoes()
      Dim ed As Editor _
      = Application.DocumentManager.MdiActiveDocument.Editor
      Dim pr As PromptResult
      pr = ed.GetString( _
      "Enter command to remove from veto list: ")
      If (pr.Status = PromptStatus.OK) Then
      If (cmdList.Contains(pr.StringResult.ToUpper())) Then
          cmdList.Remove(pr.StringResult.ToUpper())
      Else
          ed.WriteMessage(vbLf + _
            "Command not found in veto list.")
      End If
      End If
    End Sub
    Private Sub vetoCommandIfInList(ByVal sender As Object, _
      ByVal e As DocumentLockModeChangedEventArgs)
      If (cmdList.Contains(e.GlobalCommandName.ToUpper())) Then
      e.Veto()
      End If
    End Sub
End Class
End Namespace
C#:
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.Runtime;
using System;
using System.Collections.Specialized;
namespace VetoTest
{
public class VetoCmds : IExtensionApplication
{
    static StringCollection cmdList =
      new StringCollection();
    public void Initialize()
    {
      DocumentCollection dm =
      Application.DocumentManager;
      dm.DocumentLockModeChanged += new
      DocumentLockModeChangedEventHandler(
          vetoCommandIfInList
      );
    }
    public void Terminate()
    {
      DocumentCollection dm;
      dm = Application.DocumentManager;
      dm.DocumentLockModeChanged -= new
      DocumentLockModeChangedEventHandler(
          vetoCommandIfInList
      );
    }
   
    public void AddVeto()
    {
      Editor ed =
      Application.DocumentManager.MdiActiveDocument.Editor;
      PromptResult pr =
      ed.GetString("\nEnter command to veto: ");
      if (pr.Status == PromptStatus.OK)
      {
      if (cmdList.Contains(pr.StringResult.ToUpper()))
      {
          ed.WriteMessage(
            "\nCommand already in veto list."
          );
      }
      else
      {
          cmdList.Add(pr.StringResult.ToUpper());
          ed.WriteMessage(
            "\nCommand " +
            pr.StringResult.ToUpper() +
            " added to veto list.");
      }
      }
    }
   
    public void ListVetoes()
    {
      Editor ed =
      Application.DocumentManager.MdiActiveDocument.Editor;
      ed.WriteMessage("Commands currently on veto list:\n");
      foreach (string str in cmdList)
      {
      ed.WriteMessage(str + "\n");
      }
    }
   
    public void RemoveVeto()
    {
      ListVetoes();
      Editor ed =
      Application.DocumentManager.MdiActiveDocument.Editor;
      PromptResult pr;
      pr =
      ed.GetString(
          "Enter command to remove from veto list: "
      );
      if (pr.Status == PromptStatus.OK)
      {
      if (cmdList.Contains(pr.StringResult.ToUpper()))
      {
          cmdList.Remove(pr.StringResult.ToUpper());
      }
      else
      {
          ed.WriteMessage(
            "\nCommand not found in veto list."
          );
      }
      }
    }
    private void vetoCommandIfInList(
      object sender,
      DocumentLockModeChangedEventArgs e)
    {
      if (cmdList.Contains(e.GlobalCommandName.ToUpper()))
      {
      e.Veto();
      }
    }
}
}
In terms of usage, it should be simple enough to work out while playing with it:
ADDVETO adds commands to the veto list
LISTVETOES lists the commands on the veto list
REMVETO removes commands from the veto list
These commands are really only intended for you to try out different commands on the veto list, to see what happens. If you need to control use of command(s) from your application, you should not allow your users to manipulate the list of blocked commands themselves.

雪山飞狐_lzh 发表于 2009-5-19 14:45:00

六、替换 "Open" 命令
March 04, 2007
Replacing AutoCAD's OPEN command using .NET
This very good question came in by email from Patrick Nikoletich:
I was wondering what the preferred method for overriding the default “Open” common dialog in AutoCAD 2007 would be? I can catch the event but can’t send AutoCAD a command to cancel the request so I can launch my Win Form instead.
This is quite a common requirement - especially for people integrating document management systems with AutoCAD.
The simplest way to accomplish this - and this technique goes back a bit - is to undefine the OPEN command, replacing it with your own implementation. The classic way to do this from LISP was to use the (command) function to call the UNDEFINE command on OPEN, and then use (defun) to implement your own (c:open) function.
This technique can be adapted for use with .NET. The following C# code calls the UNDEFINE command in its initialization and then implements an OPEN command of its own.
A few notes on the implementation:
I'm using the COM SendCommand(), rather than SendStringToExecute(), as it is called synchronously and is executed before the command gets defined
Unfortunately this causes the UNDEFINE command to be echoed to the command-line, an undesired side effect.
I have not tested this being loaded on AutoCAD Startup - it may require some work to get the initialization done appropriately, if this is a requirement (as SendCommand is called on the ActiveDocument).
I've implemented the OPEN command very simply - just to request a filename from the user with a standard dialog - and then call a function to open this file. More work may be needed to tailor this command's behaviour to match AutoCAD's or to match your application requirements.
This is defined as a session command, allowing it to transfer focus to the newly-opened document. It does not close the active document, which AutoCAD's OPEN command does if the document is "default" and unedited (such as "Drawing1.dwg").
And so here's the code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Interop;
using System.Runtime.InteropServices;
namespace RedefineOpen
{
public class CommandRedefinitions
: Autodesk.AutoCAD.Runtime.IExtensionApplication
{
    public void Initialize()
    {
      AcadApplication app =
      (AcadApplication)Application.AcadApplication;
      app.ActiveDocument.SendCommand("_.UNDEFINE _OPEN ");
    }
    public void Terminate(){}
   
    static public void Open()
    {
      DocumentCollection dm = Application.DocumentManager;
      Editor ed = dm.MdiActiveDocument.Editor;
      PromptOpenFileOptions opts =
      new PromptOpenFileOptions(
          "Select a drawing file (for a custom command)"
      );
      opts.Filter = "Drawing (*.dwg)";
      PromptFileNameResult pr = ed.GetFileNameForOpen(opts);
      if (pr.Status == PromptStatus.OK)
      {
      dm.Open(pr.StringResult);
      }
    }
}
}

雪山飞狐_lzh 发表于 2009-5-19 14:48:00

七、获取 .NET-defined 的命令列表
March 15, 2007
Getting the list of .NET-defined commands in AutoCAD
Here's an interesting question that came in from Kerry Brown:
Is there a way to determine the names of Commands loaded into Acad from assemblies ... either a global list or a list associated with a specific assembly ... or both :-)
I managed to put some code together to do this (although I needed to look into how AutoCAD does it to get some of the finer points). I chose to implement two types of command - one that gets the commands for all the loaded assemblies, and one that works for just the currently-executing assembly. The first one is quite slow, though - it takes time to query every loaded assembly - so I added a command that only queries assemblies with explicit CommandClass attributes.
I didn't write a command to get the commands defined by a specified assembly - that's been left as an exercise for the reader. :-)
Here is the C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System;
using System.Reflection;
using System.Collections.Specialized;
namespace GetLoadedCommands
{
public class Commands
{
   
    static public void ListCommandsFromThisAssembly()
    {
      // Just get the commands for this assembly
      DocumentCollection dm =
      Application.DocumentManager;
      Editor ed =
      dm.MdiActiveDocument.Editor;
      Assembly asm =
      Assembly.GetExecutingAssembly();
      string[] cmds = GetCommands(asm, false);
      foreach (string cmd in cmds)
      {
      ed.WriteMessage(cmd + "\n");
      }
    }
   
    static public void ListMarkedCommands()
    {
      // Get the commands for all assemblies,
      //but only those with explicit
      // CommandClass attributes (much quicker)
      StringCollection cmds = new StringCollection();
      DocumentCollection dm =
      Application.DocumentManager;
      Editor ed =
      dm.MdiActiveDocument.Editor;
      Assembly[] asms =
      AppDomain.CurrentDomain.GetAssemblies();
      foreach (Assembly asm in asms)
      {
      cmds.AddRange(GetCommands(asm, true));
      }
      foreach (string cmd in cmds)
      {
      ed.WriteMessage(cmd + "\n");
      }
    }
   
    static public void ListCommands()
    {
      // Get the commands for all assemblies,
      // marked or otherwise (much slower)
      StringCollection cmds = new StringCollection();
      DocumentCollection dm =
      Application.DocumentManager;
      Editor ed =
      dm.MdiActiveDocument.Editor;
      Assembly[] asms =
      AppDomain.CurrentDomain.GetAssemblies();
      foreach (Assembly asm in asms)
      {
      cmds.AddRange(GetCommands(asm, false));
      }
      foreach (string cmd in cmds)
      {
      ed.WriteMessage(cmd + "\n");
      }
    }
    private static string[] GetCommands(
      Assembly asm,
      bool markedOnly
    )
    {
      StringCollection sc = new StringCollection();
      object[] objs =
      asm.GetCustomAttributes(
          typeof(CommandClassAttribute),
          true
      );
      Type[] tps;
      int numTypes = objs.Length;
      if (numTypes > 0)
      {
      tps = new Type;
      for(int i=0; i < numTypes; i++)
      {
          CommandClassAttribute cca =
            objs as CommandClassAttribute;
          if (cca != null)
          {
            tps = cca.Type;
          }
      }
      }
      else
      {
      // If we're only looking for specifically
      // marked CommandClasses, then use an
      // empty list
      if (markedOnly)
          tps = new Type;
      else
          tps = asm.GetExportedTypes();
      }
      foreach (Type tp in tps)
      {
      MethodInfo[] meths = tp.GetMethods();
      foreach (MethodInfo meth in meths)
      {
          objs =
            meth.GetCustomAttributes(
            typeof(CommandMethodAttribute),
            true
            );
          foreach (object obj in objs)
          {
            CommandMethodAttribute attb =
            (CommandMethodAttribute)obj;
            sc.Add(attb.GlobalName);
          }
      }
      }
      string[] ret = new string;
      sc.CopyTo(ret,0);
      return ret;
    }
}
}
And here's what happens when you run the various commands:
Command: TC
TC
LCM
LC
Command: LC
layer
TC
LCM
LC
Command: LCM
layerNote: you'll see that our own module's command are not listed by LCM... if you add the CommandClass attribute, as mentioned in this earlier post, then they will be:
[assembly:
CommandClass(
    typeof(GetLoadedCommands.Commands)
)
]

雪山飞狐_lzh 发表于 2009-5-19 14:52:00

<p>八、</p><p>August 17, 2007<br/>CommandComplete bonus tool<br/>Most members of my team (DevTech) have a background in software development, having developed code professionally in previous jobs. People often join DevTech because they enjoy the variety and flexibility the role brings as well as the direct communication we have with the Autodesk development community.</p><p>That said, we still like to hone our coding skills - some of this we get from fielding questions we receive from ADN members but it's also helpful to work on the odd software project. Back in the day - during my stint living in India - DevTech was part of Autodesk Consulting and so members of the team were often involved in developing code for customers. These days our mandate is clearer, to focus on providing services through the Autodesk Developer Network.</p><p>The majority of the software projects we currently work on are samples for publication, whether as DevNotes on the ADN site or as samples we ship with the ObjectARX SDK. During the last year or so we've been running an internal initiative called "DevTech Labs", through which we develop customer-oriented features and bonus tools for posting to Autodesk Labs.</p><p>The first fruit of these labours is now available: the CommandComplete bonus tool. Sreekar Devatha, a member of the DevTech team based in India, was the primary developer on this project. I think he's done a great job on this very useful tool. :-)</p><p>This post from Scott Sheppard lists some of the feedback we've received to date. There's also further feedback received by email and the discussion group that we'll take into account when working on an update to this tool.</p><p></p>

雪山飞狐_lzh 发表于 2009-5-19 17:41:00

十、在.Net中使用P/Invoke调用ObjectArx
August 04, 2008
Using the P/Invoke Interop Assistant to help call ObjectARX from .NET
Thanks to Gopinath Taget, from DevTech Americas, for letting me know of this tool's existence.
I've often battled to create Platform Invoke signatures for unmanaged C(++) APIs in .NET applications, and it seems this tool is likely to save me some future pain. The tool was introduced in an edition of MSDN Magazine, earlier this year.

While the Interop Assistant can be used for creating function signatures to call into .NET assemblies from unmanaged applications, I'm usually more interested in the reverse: calling unmanaged functions from .NET.
Let's take a quick spin with the tool, working with some functions from the ObjectARX includes folder...
A great place to start is the aced.h file, as it contains quite a few C-standard functions that are callable via P/Invoke.
After installing and launching the tool, the third tab can be used for generation of C# (DllImport) or VB.NET (Declare) signatures for unmanaged function(s). To get started we'll go with an easy one:void acedPostCommandPrompt();After posting the code into the "Native Code Snippet" window, we can generate the results for C#:

And for VB.NET:

Taking a quick look at the generated signature (I'll choose the C# one, as usual), we can see it's slightly incomplete:public partial class NativeMethods
{
/// Return Type: void
[System.Runtime.InteropServices.DllImportAttribute(
    "<Unknown>",
    EntryPoint = "acedPostCommandPrompt")]
public static extern void acedPostCommandPrompt();
}We need to replace "<Unknown>" with the name of the EXE or DLL from which this function is exported: this is information that was not made available to the tool, as we simply copy & pasted a function signature from a header file. This previous post (the one that gives an introduction to P/Invoke) shows a technique for identifying the appropriate module to use.
Now let's try an altogether more complicated header:int acedCmdLookup(const ACHAR* cmdStr, int globalLookup,
                  AcEdCommandStruc* retStruc,
                  int skipUndef = FALSE);To get started, we simply paste this into the "Native Code Snippet" window, once again:

There are a few problems. Copying in the definition for ACHAR (from AdAChar.h), solves the first issue, but copying in the definition for AcEdCommandStruc (from accmd.h) identifies two further issues:

The first is easy to fix - we just copy and paste the definition of AcRxFunctionPtr from accmd.h. The second issue - defining AcEdCommand - is much more complicated, and in this case there isn't a pressing need for us to bother as the information contained in the rest of the AcEdCommandStruc structure is sufficient for this example. So we can simple set this to a void* in the structure:

OK, so we now have our valid P/Invoke signature, which we can integrate into a C# test application:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using System.Runtime.InteropServices;
namespace PInvokeTest
{
public delegate void AcRxFunctionPtr();

public struct AcEdCommandStruc
{
    public AcRxFunctionPtr fcnAddr;
    public int flags;
    public System.IntPtr app;
    public System.IntPtr hResHandle;
    public System.IntPtr cmd;
}

public struct HINSTANCE__
{
    public int unused;
}
public class Commands
{
    [DllImportAttribute(
      "acad.exe",
      EntryPoint = "acedCmdLookup")
    ]
    public static extern int acedCmdLookup(
      
       string cmdStr,
      int globalLookup,
      ref AcEdCommandStruc retStruc,
      int skipUndef
    );
   
    static public void LookupCommand()
    {
      Document doc =
      Application.DocumentManager.MdiActiveDocument;
      Database db = doc.Database;
      Editor ed = doc.Editor;
      PromptStringOptions pso =
      new PromptStringOptions(
          "\nEnter name of command to look-up: "
      );
      PromptResult pr = ed.GetString(pso);
      if (pr.Status != PromptStatus.OK)
      return;
      AcEdCommandStruc cs = new AcEdCommandStruc();
      int res =
      acedCmdLookup(pr.StringResult, 1, ref cs, 1);
      if (res == 0)
      ed.WriteMessage(
          "\nCould not find command definition." +
          " It may not be defined in ObjectARX."
      );
      else
      {
      CommandFlags cf = (CommandFlags)cs.flags;
      ed.WriteMessage(
          "\nFound the definition of {0} command. " +
          " It has command-flags value of \"{1}\".",
          pr.StringResult.ToUpper(),
          cf
      );
      PromptKeywordOptions pko =
          new PromptKeywordOptions(
            "\nAttempt to invoke this command?"
          );
      pko.AllowNone = true;
      pko.Keywords.Add("Yes");
      pko.Keywords.Add("No");
      pko.Keywords.Default = "No";
      PromptResult pkr =
          ed.GetKeywords(pko);
      if (pkr.Status == PromptStatus.OK)
      {
          if (pkr.StringResult == "Yes")
          {
            // Not a recommended way of invoking
            // commands. Use SendCommand() or
            // SendStringToExecute()
            cs.fcnAddr.Invoke();
          }
      }
      }
    }
}
}
Our LC command looks up a command based on its command-name and tells us the flags that were used to define it (Modal, Session, Transparent, UsePickSet, etc.). It will then offer the option to invoke it, as we have the function address as another member of the AcEdCommandStruc populated by the acedCmdLookup() function. I do not recommend using this approach for calling commands - not only are we relying on a ill-advised mechanism in ObjectARX for doing so, we're calling through to it from managed code - we're just using this as a more complicated P/Invoke example and investigating what can (in theory) be achieved with the results. If you're interested in calling commands from .NET, see this post.
By the way, if you're trying to get hold of P/Invoke signature for the Win32 API, I recommend checking here first.
页: [1] 2
查看完整版本: Kean专题(5)—Commands