明经CAD社区

 找回密码
 注册

QQ登录

只需一步,快速开始

搜索
12
返回列表 发新帖

[Kean专集] Kean专题(5)—Commands

   关闭 [复制链接]
 楼主| 发表于 2009-5-19 18:07 | 显示全部楼层
十一、
August 08, 2008
Catching exceptions thrown from dialogs inside AutoCAD using .NET
Another juicy tidbit from an internal Q&A session.
The question...
How can I throw an exception from within a modal form on mine inside AutoCAD, and catch it in the calling command? I have tried try/catch, but nothing seems to work.
Here's my command definition:
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.Runtime;
  3. using System.Windows.Forms;
  4. using MyApplication;
  5. using acApp =
  6.   Autodesk.AutoCAD.ApplicationServices.Application;
  7. namespace CatchMeIfYouCan
  8. {
  9.   public class Commands
  10.   {
  11.     [CommandMethod("CATCH")]
  12.     static public void CatchDialogException()
  13.     {
  14.       try
  15.       {
  16.         MyForm form = new MyForm();
  17.         DialogResult res =
  18.           acApp.ShowModalDialog(form);
  19.       }
  20.       catch (System.Exception ex)
  21.       {
  22.         MessageBox.Show(
  23.           "Caught using catch: " +
  24.           ex.Message,
  25.           "Exception"
  26.         );
  27.       }
  28.     }
  29.   }
  30. }
And here's the code that throws an exception from behind button inside my form:
  1. using System;
  2. using System.Windows.Forms;
  3. namespace MyApplication
  4. {
  5.   public partial class MyForm : Form
  6.   {
  7.     public MyForm()
  8.     {
  9.       InitializeComponent();
  10.     }
  11.     private void button1_Click(
  12.       object sender,
  13.       EventArgs e
  14.     )
  15.     {
  16.       throw new Exception(
  17.         "Something bad happened."
  18.       );
  19.     }
  20.   }
  21. }
The answer, once again, came from our AutoCAD Engineering team, in this case from one of our API Test Engineers based in Singapore...
I was able to catch the exception using the ThreadExceptionEventHandler event. Here is your modified command:
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.EditorInput;
  3. using Autodesk.AutoCAD.Runtime;
  4. using Autodesk.AutoCAD.Windows;
  5. using System.Windows.Forms;
  6. using System.Threading;
  7. using MyApplication;
  8. using acApp =
  9.   Autodesk.AutoCAD.ApplicationServices.Application;
  10. using sysApp =
  11.   System.Windows.Forms.Application;
  12. namespace CatchMeIfYouCan
  13. {
  14.   public class Commands
  15.   {
  16.     [CommandMethod("CATCH")]
  17.     static public void CatchDialogException()
  18.     {
  19.       try
  20.       {
  21.         sysApp.ThreadException +=
  22.           new ThreadExceptionEventHandler(
  23.             delegate(
  24.               object o,
  25.               ThreadExceptionEventArgs args
  26.             )
  27.             {
  28.               MessageBox.Show(
  29.                 "Caught using event: " +
  30.                 args.Exception.Message,
  31.                 "Exception"
  32.               );
  33.             }
  34.           );
  35.         MyForm form = new MyForm();
  36.         DialogResult res =
  37.           acApp.ShowModalDialog(form);
  38.       }
  39.       catch (System.Exception ex)
  40.       {
  41.         MessageBox.Show(
  42.           "Caught using catch: " +
  43.           ex.Message,
  44.           "Exception"
  45.         );
  46.       }
  47.     }
  48.   }
  49. }
I gave this a try, myself, and saw that when we click the button on the dialog shown by the CATCH command, our event handler picks up the exception and displays it via a MessageBox:

One thing to note: when running this from the debugger, Visual Studio will break, telling us that there's an exception that has gone unhandled:

This can be ignored - and probably disabled inside Visual Studio - but it could be annoying, if you use this technique throughout your code. Perhaps someone who's implemented this approach - or another that works for them - can add a comment on how they handle this scenario?

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

x
 楼主| 发表于 2009-5-19 18:11 | 显示全部楼层
十二、
September 05, 2008
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():
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.DatabaseServices;
  3. using Autodesk.AutoCAD.EditorInput;
  4. using Autodesk.AutoCAD.Runtime;
  5. using Autodesk.AutoCAD.Geometry;
  6. namespace ZoomSmoothlyAndQuietly
  7. {
  8.   public class Commands
  9.   {
  10.     // Zoom to a window specified by the user
  11.     [CommandMethod("LZW")]
  12.     static public void LoudZoomWindow()
  13.     {
  14.       Document doc =
  15.         Application.DocumentManager.MdiActiveDocument;
  16.       Editor ed = doc.Editor;
  17.       Point2d min, max;
  18.       if (GetWindowForZoom(ed, out min, out max))
  19.         ZoomWin(ed, min, max, false);
  20.     }
  21.     [CommandMethod("QZW")]
  22.     static public void QuietZoomWindow()
  23.     {
  24.       Document doc =
  25.         Application.DocumentManager.MdiActiveDocument;
  26.       Editor ed = doc.Editor;
  27.       Point2d min, max;
  28.       if (GetWindowForZoom(ed, out min, out max))
  29.         ZoomWin(ed, min, max, true);
  30.     }
  31.     // Get the coordinates for a zoom
  32.     static private bool GetWindowForZoom(
  33.       Editor ed, out Point2d min, out Point2d max
  34.     )
  35.     {
  36.       min = new Point2d();
  37.       max = new Point2d();
  38.       PromptPointOptions ppo =
  39.         new PromptPointOptions(
  40.           "\nSpecify first corner:"
  41.         );
  42.       PromptPointResult ppr =
  43.         ed.GetPoint(ppo);
  44.       if (ppr.Status != PromptStatus.OK)
  45.         return false;
  46.       min =
  47.         new Point2d(ppr.Value.X, ppr.Value.Y);
  48.       PromptCornerOptions pco =
  49.         new PromptCornerOptions(
  50.           "\nSpecify opposite corner: ",
  51.           ppr.Value
  52.         );
  53.       ppr = ed.GetCorner(pco);
  54.       if (ppr.Status != PromptStatus.OK)
  55.         return false;
  56.       max =
  57.         new Point2d(ppr.Value.X, ppr.Value.Y) ;
  58.       return true;
  59.     }
  60.     // Zoom by sending a command
  61.     private static void ZoomWin(
  62.       Editor ed, Point2d min, Point2d max, bool quietly
  63.     )
  64.     {
  65.       string lower =
  66.         min.ToString().Substring(
  67.           1,
  68.           min.ToString().Length - 2
  69.         );
  70.       string upper =
  71.         max.ToString().Substring(
  72.           1,
  73.           max.ToString().Length - 2
  74.         );
  75.       string cmd =
  76.         "_.ZOOM _W " + lower + " " + upper + " ";
  77.       if (quietly)
  78.       {
  79.         // Get the old value of NOMUTT
  80.         object nomutt =
  81.           Application.GetSystemVariable("NOMUTT");
  82.         // Add the string to reset NOMUTT afterwards
  83.         cmd += "_NOMUTT " + nomutt.ToString() + " ";
  84.         // Set NOMUTT to 1, reducing cmd-line noise
  85.         Application.SetSystemVariable("NOMUTT", 1);
  86.       }
  87.       // Send the command(s)
  88.       ed.Document.SendStringToExecute(
  89.         cmd, true, false, !quietly
  90.       );
  91.     }
  92.   }
  93. }
Here's what happens when we run the "loud" command (LZW), and then the "quiet" one (QZW):
  1. Command: LZW
  2. Specify first corner:
  3. Specify opposite corner:
  4. Command: _.ZOOM
  5. Specify corner of window, enter a scale factor (nX or nXP), or
  6. [All/Center/Dynamic/Extents/Previous/Scale/Window/Object] <real time>: _W
  7. Specify first corner: 13.1961936276982,12.972925917324 Specify opposite corner:
  8. 30.6221132095147,0.482638801189623
  9. Command: QZW
  10. Specify first corner:
  11. Specify opposite corner:
  12. 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.

 楼主| 发表于 2009-5-19 18:13 | 显示全部楼层
September 15, 2008
More quiet command-calling: adding an inspection dimension inside AutoCAD using .NET
I'm still a little frazzled after transcribing the 18,000 word interview with John Walker (and largely with two fingers - at such times the fact that I've never learned to touch-type is a significant cause of frustration, as you might imagine). I'm also attending meetings all this coming week, so I've gone for the cheap option, once again, of dipping into my magic folder of code generated and provided by my team.
The technique for this one came from a response sent out by Philippe Leefsma, from DevTech EMEA, but he did mention a colleague helped him by suggesting the technique. So thanks to Philippe and our anonymous colleague for the initial idea of calling -DIMINSPECT.
The problem solved by Philippe's code is that no API is exposed via .NET to enable the "inspection" capability of dimensions inside AutoCAD. The protocol is there in ObjectARX, in the AcDbDimension class, but this has - at the time of writing - not been exposed via  the managed API. One option would be to create a wrapper or mixed-mode module to call through to unmanaged C++, but the following approach simply calls through to the -DIMINSPECT command (the command-line version of DIMINSPECT) to set the inspection parameters.
I've integrated - and extended - the technique shown in this previous post to send the command as quietly as possible. One problem I realised with the previous implementation is that UNDO might leave the user in a problematic state - with the NOMUTT variable set to 1. This modified approach does a couple of things differently:
Rather than using the NOMUTT command to set the system variable, it launches another custom command, FINISH_COMMAND
This command has been registered with the NoUndoMarker command-flag, to make it invisible to the undo mechanism (well, at least in terms of automatic undo)
It sets the NOMUTT variable to 0
It should be possible to share this command across others that have the need to call commands quietly
It uses the COM API to create an undo group around the operations we want to consider one "command"
The implementation related to the "quiet command calling" technique is wrapped up in a code region to make it easy to hide
One remaining - and, so far, unavoidable - piece of noise on the command-line is due to our use of the (handent) function: it echoes entity name of the selected dimension.
Here's the C# code:
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.DatabaseServices;
  3. using Autodesk.AutoCAD.EditorInput;
  4. using Autodesk.AutoCAD.Runtime;
  5. using Autodesk.AutoCAD.Interop;
  6. namespace InspectDimension
  7. {
  8.   public class Commands
  9.   {
  10.     [CommandMethod("INSP")]
  11.     static public void InspectDim()
  12.     {
  13.       Document doc =
  14.         Application.DocumentManager.MdiActiveDocument;
  15.       Database db = doc.Database;
  16.       Editor ed = doc.Editor;
  17.       PromptEntityOptions peo =
  18.         new PromptEntityOptions("\nSelect a dimension: ");
  19.       peo.SetRejectMessage(
  20.         "\nEntity must be a dimension."
  21.       );
  22.       peo.AddAllowedClass(typeof(Dimension), false);
  23.       PromptEntityResult per = ed.GetEntity(peo);
  24.       if (per.Status != PromptStatus.OK)
  25.         return;
  26.       Transaction tr =
  27.         db.TransactionManager.StartTransaction();
  28.       using (tr)
  29.       {
  30.         Dimension dim =
  31.           tr.GetObject(per.ObjectId, OpenMode.ForRead)
  32.             as Dimension;
  33.         if (dim != null)
  34.         {
  35.           string shape = "Round";
  36.           string label = "myLabel";
  37.           string rate = "100%";
  38.           string cmd =
  39.             "-DIMINSPECT Add (handent "" +
  40.             dim.Handle + """ + ") \n" +
  41.             shape + "\n" + label + "\n" +
  42.             rate + "\n";
  43.           SendQuietCommand(doc, cmd);
  44.         };
  45.         tr.Commit();
  46.       }
  47.     }
  48.     #region QuietCommandCalling
  49.     const string kFinishCmd = "FINISH_COMMAND";
  50.     private static void SendQuietCommand(
  51.       Document doc,
  52.       string cmd
  53.     )
  54.     {
  55.       // Get the old value of NOMUTT
  56.       object nomutt =
  57.         Application.GetSystemVariable("NOMUTT");
  58.       // Add the string to reset NOMUTT afterwards
  59.       AcadDocument oDoc =
  60.         (AcadDocument)doc.AcadDocument;
  61.       oDoc.StartUndoMark();
  62.       cmd += "_" + kFinishCmd + " ";
  63.       // Set NOMUTT to 1, reducing cmd-line noise
  64.       Application.SetSystemVariable("NOMUTT", 1);
  65.       doc.SendStringToExecute(cmd, true, false, false);
  66.     }
  67.     [CommandMethod(kFinishCmd, CommandFlags.NoUndoMarker)]
  68.     static public void FinishCommand()
  69.     {
  70.       Document doc =
  71.         Application.DocumentManager.MdiActiveDocument;
  72.       AcadDocument oDoc =
  73.         (AcadDocument)doc.AcadDocument;
  74.       oDoc.EndUndoMark();
  75.       Application.SetSystemVariable("NOMUTT", 0);
  76.     }
  77.     #endregion
  78.   }
  79. }
Here are the results of running the INSP command and selecting a dimension object. First the command-line output:
  1. Command: INSP
  2. Select a dimension: <Entity name: -401f99f0>
  3. Command:
复制代码
And now the graphics, before and after calling INSP and selecting the dimension:


In the end the post didn't turn out to be quite as quick to write as expected, but anyway - so it goes. I'm now halfway from Zürich to Washington, D.C., and had the time to kill. I probably won't have the luxury when I'm preparing my post for the middle of the week, unless I end up suffering from jet-lag. :-)

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

x
 楼主| 发表于 2009-5-19 18:19 | 显示全部楼层
十三、防止的.NET模块被加载
September 08, 2008
Preventing a .NET module from being loaded by AutoCAD
This is an interesting one that came up recently during an internal discussion:
During my module's Initialize() function, I want to decide that the module should not actually be loaded. How can I accomplish that?
The answer is surprisingly simple: if you throw an exception during the function, AutoCAD's NETLOAD mechanism will stop loading the application.
For an example, see this C# code:
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.EditorInput;
  3. using Autodesk.AutoCAD.Runtime;
  4. namespace PreventLoad
  5. {
  6.   public class Commands
  7.   : IExtensionApplication
  8.   {
  9.     public void Initialize()
  10.     {
  11.       // This will prevent the application from loading
  12.       throw new Exception();
  13.     }
  14.     public void Terminate(){}
  15.     [CommandMethod("TEST")]
  16.     static public void TestCommand()
  17.     {
  18.       Document doc =
  19.         Application.DocumentManager.MdiActiveDocument;
  20.       doc.Editor.WriteMessage(
  21.         "\nThis should not get called."
  22.       );
  23.     }
  24.   }
  25. }
Here's what happens when we attempt to load the application and then run the TEST command:
  1. Command: FILEDIA
  2. Enter new value for FILEDIA <1>: 0
  3. Command: NETLOAD
  4. Assembly file name: c:\MyApplication.dll
  5. Command: TEST
  6. Unknown command "TEST".  Press F1 for help.
复制代码

 楼主| 发表于 2009-5-19 18:23 | 显示全部楼层
十四、使用代码载入.NET的模块
September 17, 2008
Loading .NET modules programmatically into AutoCAD
The question of how to perform a "NETLOAD" programmatically has come in a few times. Well, it turns out the answer - provided by someone in our Engineering team - is refreshingly simple.
The NETLOAD command actually has a bare-bones implementation: the hard work of parsing the metadata defining commands etc. is done from an AppDomain.AssemblyLoad event-handler. To recreate the NETLOAD command, all you need to do is call Assembly.LoadFrom(), passing in the path to your assembly.
Here's some C# code to demonstrate this:
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.EditorInput;
  3. using Autodesk.AutoCAD.Runtime;
  4. using System.Reflection;
  5. namespace LoadModule
  6. {
  7.   public class Commands
  8.   {
  9.     [CommandMethod("MNL")]
  10.     static public void MyNetLoad()
  11.     {
  12.       Document doc =
  13.         Application.DocumentManager.MdiActiveDocument;
  14.       Editor ed = doc.Editor;
  15.       PromptStringOptions pso =
  16.         new PromptStringOptions(
  17.           "\nEnter the path of the module to load: "
  18.         );
  19.       pso.AllowSpaces = true;
  20.       PromptResult pr = ed.GetString(pso);
  21.       if (pr.Status != PromptStatus.OK)
  22.         return;
  23.       try
  24.       {
  25.         Assembly.LoadFrom(pr.StringResult);
  26.       }
  27.       catch(System.Exception ex)
  28.       {
  29.         ed.WriteMessage(
  30.           "\nCannot load {0}: {1}",
  31.           pr.StringResult,
  32.           ex.Message
  33.         );
  34.       }
  35.     }
  36.   }
  37. }
Incidentally, my preferred way to manage this is to enable demand loading for your assemblies, as per this previous post. But there are certainly situations where the above technique will prove useful.
In the next post: on Friday you should see the second installment of the interview with John Walker...

 楼主| 发表于 2009-5-19 18:54 | 显示全部楼层
十五、自定义AutoCAD的命令显示在上下文菜单中
November 05, 2008
Displaying a context menu during a custom AutoCAD command using .NET
Some of you may have stumbled across these previous posts, which show how to add custom context menu items for specific object types and to the default AutoCAD context menu. There is a third way to create and display context menus inside AutoCAD, and this approach may prove useful to those of you who wish to display context menus during particular custom commands.One word of caution: I've been told that this technique does not currently work for transparent commands, so if your command needs to be called transparently then this may not be the approach for you (you should investigate asking for keywords, instead, as this should work without problem in this context).
Here's the C# code:
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.EditorInput;
  3. using Autodesk.AutoCAD.Runtime;
  4. using Autodesk.AutoCAD.Windows;
  5. using System;
  6. namespace CommandContextMenu
  7. {
  8.   public class Commands
  9.   {
  10.     public class MyContextMenu : ContextMenuExtension
  11.     {
  12.       public MyContextMenu()
  13.       {
  14.         this.Title = "Command context menu";
  15.         MenuItem mi = new MenuItem("Item One");
  16.         mi.Click +=
  17.           new EventHandler(Commands.OnClick);
  18.         this.MenuItems.Add(mi);
  19.         mi = new MenuItem("Item Two");
  20.         mi.Click +=
  21.           new EventHandler(Commands.OnClick);
  22.         this.MenuItems.Add(mi);
  23.         MenuItem smi = new MenuItem("Sub Item One");
  24.         smi.Click +=
  25.           new EventHandler(Commands.OnClick);
  26.         this.MenuItems.Add(smi);
  27.       }
  28.     };
  29.     [CommandMethod(
  30.       "mygroup",
  31.       "mycmd",
  32.       null,
  33.       CommandFlags.Modal,
  34.       typeof(MyContextMenu)
  35.     )]
  36.     public static void MyCommand()
  37.     {
  38.       Editor ed =
  39.         Application.DocumentManager.MdiActiveDocument.Editor;
  40.       ed.GetPoint("\nRight-click before selecting a point:");
  41.     }
  42.     static void OnClick(object sender, EventArgs e)
  43.     {
  44.       Editor ed =
  45.         Application.DocumentManager.MdiActiveDocument.Editor;
  46.       ed.WriteMessage("\nA context menu item was selected.");
  47.     }
  48.   }
  49. }
A couple of points about getting this working: on my system I used AutoCAD's OPTIONS command to make sure the right-click menu gets displayed after a certain delay (I enabled the option from the "User Preferences" -> "Right-Click Customization" and then changed the longer click duration to 100 milliseconds, to save a little time. :-)

The other option for enabling command menus is the SHORTCUTMENU system variable (the documentation inside AutoCAD will tell you about the various values).
Then, once inside the MYCMD command, I was able to right-click and see my custom menu:

From the command-line, we can see that our callback was called successfully:
  1. Command:  MYCMD
  2. Right-click before selecting a point:
  3. A context menu item was selected.
复制代码
If you wish to have separate callbacks for your various items, it's simply a matter of defining separate functions and passing them in as the Click event handler instead of Command.OnClick.

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

x
 楼主| 发表于 2009-5-19 19:09 | 显示全部楼层
十六、从外部引用导入层
May 18, 2009
Importing AutoCAD layers from xrefs using .NET
This post extends the last one which looked at a basic implementation to allow AutoCAD’s standard OFFSET command to work on the contents of external references. I haven’t flagged the specific changes, but the old code (which is almost identical to that in the last post) starts with the Initialize() function.
The previous example created geometry on a temporary layer that exists only as long as the source xref is attached: detaching the xref generally caused dangling layer references. This post evolves the approach and provides a choice to the user (via the XOFFSETLAYER command): to either create the geometry on the current layer (the default behaviour) or to clone across the layers, plus their various linetypes etc., needed for the geometry to survive independently of their originating xref.
I decided to put the code to copy across the layers in a separate command called XOFFSETCPLAYS, which has the advantage of being callable separately from our OFFSET routine (and is generally a bit safer – performing significant operations on AutoCAD databases from event handlers can lead to problems, depending on what exactly you’re doing). The command gets called using SendStringToExecute() so that it gets executed up after control has returned to the AutoCAD application.
The copying of symbol table records from xrefs is a little involved, but I’ve don what I can to comment the approach below. Using Database.wblockCloneObjects() is the cleanest approach, but there is work involved to load the xrefed DWGs into their own Database objects before using this.
Here’s the updated C# code:
  1. using Autodesk.AutoCAD.ApplicationServices;
  2. using Autodesk.AutoCAD.DatabaseServices;
  3. using Autodesk.AutoCAD.EditorInput;
  4. using Autodesk.AutoCAD.Runtime;
  5. using System.Collections.Generic;
  6. namespace XrefOffset
  7. {
  8.   public class XrefOffsetApplication : IExtensionApplication
  9.   {
  10.     // Maintain a list of temporary objects that require removal
  11.     ObjectIdCollection _ids;
  12.     static bool _placeOnCurrentLayer = true;
  13.     public XrefOffsetApplication()
  14.     {
  15.       _ids = new ObjectIdCollection();
  16.     }
  17.     [CommandMethod("XOFFSETLAYER")]
  18.     static public void XrefOffsetLayer()
  19.     {
  20.       Document doc =
  21.         Application.DocumentManager.MdiActiveDocument;
  22.       Database db = doc.Database;
  23.       Editor ed = doc.Editor;
  24.       PromptKeywordOptions pko =
  25.         new PromptKeywordOptions(
  26.           "\nEnter layer option for offset objects"
  27.         );
  28.       const string option1 = "Current";
  29.       const string option2 = "Source";
  30.       pko.AllowNone = true;
  31.       pko.Keywords.Add(option1);
  32.       pko.Keywords.Add(option2);
  33.       pko.Keywords.Default =
  34.         (_placeOnCurrentLayer ? option1 : option2);
  35.       PromptResult pkr =
  36.         ed.GetKeywords(pko);
  37.       if (pkr.Status == PromptStatus.OK)
  38.         _placeOnCurrentLayer =
  39.           (pkr.StringResult == option1);
  40.     }
  41.     [CommandMethod("XOFFSETCPLAYS")]
  42.     static public void XrefOffsetCopyLayers()
  43.     {
  44.       Document doc =
  45.         Application.DocumentManager.MdiActiveDocument;
  46.       Database db = doc.Database;
  47.       Editor ed = doc.Editor;
  48.       Transaction tr =
  49.         doc.TransactionManager.StartTransaction();
  50.       using (tr)
  51.       {
  52.         BlockTableRecord btr =
  53.           (BlockTableRecord)tr.GetObject(
  54.             db.CurrentSpaceId,
  55.             OpenMode.ForRead
  56.           );
  57.         // We will collect the layers used by the various entities
  58.         List<string> layerNames =
  59.           new List<string>();
  60.         // And store a list of the entities to come back and update
  61.         ObjectIdCollection entsToUpdate =
  62.           new ObjectIdCollection();
  63.         // Loop through the contents of the active space, and look
  64.         // for entities that are on dependent layers (i.e. ones
  65.         // that come from attached xrefs)
  66.         foreach (ObjectId entId in btr)
  67.         {
  68.           Entity ent =
  69.             (Entity)tr.GetObject(entId, OpenMode.ForRead);
  70.           // Check the dependent status of the entity's layer
  71.           LayerTableRecord ltr =
  72.             (LayerTableRecord)tr.GetObject(
  73.               ent.LayerId,
  74.               OpenMode.ForRead
  75.             );
  76.           if (ltr.IsDependent && !(ent is BlockReference))
  77.           {
  78.             // Add it to our list and flag the entity for updating
  79.             string layName = ltr.Name;
  80.             if (!layerNames.Contains(layName))
  81.             {
  82.               layerNames.Add(layName);
  83.             }
  84.             entsToUpdate.Add(ent.ObjectId);
  85.           }
  86.         }
  87.         // Sorting the list will allow us to minimise the number
  88.         // of external drawings we need to load (many layers
  89.         // will be from the same xref)
  90.         layerNames.Sort();
  91.         // Get the xref graph, which allows us to get at the
  92.         // names of the xrefed drawings more easily
  93.         XrefGraph xg = db.GetHostDwgXrefGraph(false);
  94.         // We're going to store a list of our xrefed databases
  95.         // for later disposal
  96.         List<Database> xrefs =
  97.           new List<Database>();
  98.         // Collect a list of the layers we want to clone across
  99.         ObjectIdCollection laysToClone =
  100.           new ObjectIdCollection();
  101.         // Loop through the list of layers, only loading xrefs
  102.         // in when they haven't been already
  103.         string currentXrefName = "";
  104.         foreach(string layName in layerNames)
  105.         {
  106.           Database xdb = null;
  107.           // Make sure we have our mangled layer name
  108.           if (layName.Contains("|"))
  109.           {
  110.             // Split it up, so we know the xref name
  111.             // and the root layer name
  112.             int sepIdx = layName.IndexOf("|");
  113.             string xrefName =
  114.               layName.Substring(0, sepIdx);
  115.             string rootName =
  116.               layName.Substring(sepIdx + 1);
  117.             // If the xref is the same as the last loaded,
  118.             // this saves us some effort
  119.             if (xrefName == currentXrefName)
  120.             {
  121.               xdb = xrefs[xrefs.Count-1];
  122.             }
  123.             else
  124.             {
  125.               // Otherwise we get the node for our xref,
  126.               // so we can get its filename
  127.               XrefGraphNode xgn =
  128.                 xg.GetXrefNode(xrefName);
  129.               if (xgn != null)
  130.               {
  131.                 // Create an xrefed database, loading our
  132.                 // drawing into it
  133.                 xdb = new Database(false, true);
  134.                 xdb.ReadDwgFile(
  135.                   xgn.Database.Filename,
  136.                   System.IO.FileShare.Read,
  137.                   true,
  138.                   null
  139.                 );
  140.                 // Add it to the list for later disposal
  141.                 // (we do this after the clone operation)
  142.                 xrefs.Add(xdb);
  143.               }
  144.               xgn.Dispose();
  145.             }
  146.             if (xdb != null)
  147.             {
  148.               // Start a transaction in our loaded database
  149.               // to get at the layer name
  150.               Transaction tr2 =
  151.                 xdb.TransactionManager.StartTransaction();
  152.               using (tr2)
  153.               {
  154.                 // Open the layer table
  155.                 LayerTable lt2 =
  156.                   (LayerTable)tr2.GetObject(
  157.                     xdb.LayerTableId,
  158.                     OpenMode.ForRead
  159.                   );
  160.                 // Add our layer (which we get via its
  161.                 // unmangled name) to the list to clone
  162.                 if (lt2.Has(rootName))
  163.                 {
  164.                   laysToClone.Add(lt2[rootName]);
  165.                 }
  166.                 // Committing is cheaper
  167.                 tr2.Commit();
  168.               }
  169.             }
  170.           }
  171.         }
  172.         // If we have layers to clone, do so
  173.         if (laysToClone.Count > 0)
  174.         {
  175.           // We use wblockCloneObjects to clone between DWGs
  176.           IdMapping idMap = new IdMapping();
  177.           db.WblockCloneObjects(
  178.             laysToClone,
  179.             db.LayerTableId,
  180.             idMap,
  181.             DuplicateRecordCloning.Ignore,
  182.             false
  183.           );
  184.           // Dispose each of our xrefed databases
  185.           foreach (Database xdb in xrefs)
  186.           {
  187.             xdb.Dispose();
  188.           }
  189.           // Open the resultant layer table, so we can check
  190.           // for the existance of our new layers
  191.           LayerTable lt =
  192.             (LayerTable)tr.GetObject(
  193.               db.LayerTableId,
  194.               OpenMode.ForRead
  195.             );
  196.           // Loop through the entities to update
  197.           foreach (ObjectId id in entsToUpdate)
  198.           {
  199.             // Open them each for write, and then check their
  200.             // current layer
  201.             Entity ent =
  202.               (Entity)tr.GetObject(id, OpenMode.ForWrite);
  203.             LayerTableRecord ltr =
  204.               (LayerTableRecord)tr.GetObject(
  205.                 ent.LayerId,
  206.                 OpenMode.ForRead
  207.               );
  208.             // We split the name once again (could use a function
  209.             // for this, but hey)
  210.             string layName = ltr.Name;
  211.             int sepIdx = layName.IndexOf("|");
  212.             string xrefName =
  213.               layName.Substring(0, sepIdx);
  214.             string rootName =
  215.               layName.Substring(sepIdx + 1);
  216.             // If we now have the layer in our database, use it
  217.             if (lt.Has(rootName))
  218.               ent.LayerId = lt[rootName];
  219.           }
  220.         }
  221.         tr.Commit();
  222.       }
  223.     }
  224.     public void Initialize()
  225.     {
  226.       DocumentCollection dm =
  227.         Application.DocumentManager;
  228.       // Remove any temporary objects at the end of the command
  229.       dm.DocumentLockModeWillChange +=
  230.         delegate(
  231.           object sender, DocumentLockModeWillChangeEventArgs e
  232.         )
  233.         {
  234.           if (_ids.Count > 0)
  235.           {
  236.             Transaction tr =
  237.               e.Document.TransactionManager.StartTransaction();
  238.             using (tr)
  239.             {
  240.               foreach (ObjectId id in _ids)
  241.               {
  242.                 DBObject obj =
  243.                   tr.GetObject(id, OpenMode.ForWrite, true);
  244.                 obj.Erase();
  245.               }
  246.               tr.Commit();
  247.             }
  248.             _ids.Clear();
  249.             // Launch a command to bring across our layers
  250.             if (!_placeOnCurrentLayer)
  251.             {
  252.               e.Document.SendStringToExecute(
  253.                 "_.XOFFSETCPLAYS ", false, false, false
  254.               );
  255.             }
  256.           }
  257.         };
  258.       // When a document is created, make sure we handle the
  259.       // important events it fires
  260.       dm.DocumentCreated +=
  261.         delegate(
  262.           object sender, DocumentCollectionEventArgs e
  263.         )
  264.         {
  265.           e.Document.CommandWillStart +=
  266.             new CommandEventHandler(OnCommandWillStart);
  267.           e.Document.CommandEnded +=
  268.             new CommandEventHandler(OnCommandFinished);
  269.           e.Document.CommandCancelled +=
  270.             new CommandEventHandler(OnCommandFinished);
  271.           e.Document.CommandFailed +=
  272.             new CommandEventHandler(OnCommandFinished);
  273.         };
  274.       // Do the same for any documents existing on application
  275.       // initialization
  276.       foreach (Document doc in dm)
  277.       {
  278.         doc.CommandWillStart +=
  279.           new CommandEventHandler(OnCommandWillStart);
  280.         doc.CommandEnded +=
  281.           new CommandEventHandler(OnCommandFinished);
  282.         doc.CommandCancelled +=
  283.           new CommandEventHandler(OnCommandFinished);
  284.         doc.CommandFailed +=
  285.           new CommandEventHandler(OnCommandFinished);
  286.       }
  287.     }
  288.     // When the OFFSET command starts, let's add our selection
  289.     // manipulating event-handler
  290.     void OnCommandWillStart(object sender, CommandEventArgs e)
  291.     {
  292.       if (e.GlobalCommandName == "OFFSET")
  293.       {
  294.         Document doc = (Document)sender;
  295.         doc.Editor.PromptForEntityEnding +=
  296.           new PromptForEntityEndingEventHandler(
  297.             OnPromptForEntityEnding
  298.           );
  299.       }
  300.     }
  301.     // And when the command ends, remove it
  302.     void OnCommandFinished(object sender, CommandEventArgs e)
  303.     {
  304.       if (e.GlobalCommandName == "OFFSET")
  305.       {
  306.         Document doc = (Document)sender;
  307.         doc.Editor.PromptForEntityEnding -=
  308.           new PromptForEntityEndingEventHandler(
  309.             OnPromptForEntityEnding
  310.           );
  311.       }
  312.     }
  313.     // Here's where the heavy lifting happens...
  314.     void OnPromptForEntityEnding(
  315.       object sender, PromptForEntityEndingEventArgs e
  316.     )
  317.     {
  318.       if (e.Result.Status == PromptStatus.OK)
  319.       {
  320.         Editor ed = sender as Editor;
  321.         ObjectId objId = e.Result.ObjectId;
  322.         Database db = objId.Database;
  323.         Transaction tr =
  324.           db.TransactionManager.StartTransaction();
  325.         using (tr)
  326.         {
  327.           // First get the currently selected object
  328.           // and check whether it's a block reference
  329.           BlockReference br =
  330.             tr.GetObject(objId, OpenMode.ForRead)
  331.               as BlockReference;
  332.           if (br != null)
  333.           {
  334.             // If so, we check whether the block table record
  335.             // to which it refers is actually from an XRef
  336.             ObjectId btrId = br.BlockTableRecord;
  337.             BlockTableRecord btr =
  338.               tr.GetObject(btrId, OpenMode.ForRead)
  339.                 as BlockTableRecord;
  340.             if (btr != null)
  341.             {
  342.               if (btr.IsFromExternalReference)
  343.               {
  344.                 // If so, then we programmatically select the object
  345.                 // underneath the pick-point already used
  346.                 PromptNestedEntityOptions pneo =
  347.                   new PromptNestedEntityOptions("");
  348.                 pneo.NonInteractivePickPoint =
  349.                   e.Result.PickedPoint;
  350.                 pneo.UseNonInteractivePickPoint = true;
  351.                 PromptNestedEntityResult pner =
  352.                   ed.GetNestedEntity(pneo);
  353.                 if (pner.Status == PromptStatus.OK)
  354.                 {
  355.                   try
  356.                   {
  357.                     ObjectId selId = pner.ObjectId;
  358.                     // Let's look at this programmatically-selected
  359.                     // object, to see what it is
  360.                     DBObject obj =
  361.                       tr.GetObject(selId, OpenMode.ForRead);
  362.                     // If it's a polyline vertex, we need to go one
  363.                     // level up to the polyline itself
  364.                     if (obj is PolylineVertex3d || obj is Vertex2d)
  365.                       selId = obj.OwnerId;
  366.                     // We don't want to do anything at all for
  367.                     // textual stuff, let's also make sure we
  368.                     // are dealing with an entity (should always
  369.                     // be the case)
  370.                     if (obj is MText || obj is DBText ||
  371.                       !(obj is Entity))
  372.                       return;
  373.                     // Now let's get the name of the layer, to use
  374.                     // later
  375.                     Entity ent = (Entity)obj;
  376.                     LayerTableRecord ltr =
  377.                       (LayerTableRecord)tr.GetObject(
  378.                         ent.LayerId,
  379.                         OpenMode.ForRead
  380.                       );
  381.                     string layName = ltr.Name;
  382.                     // Clone the selected object
  383.                     object o = ent.Clone();
  384.                     Entity clone = o as Entity;
  385.                     // We need to manipulate the clone to make sure
  386.                     // it works
  387.                     if (clone != null)
  388.                     {
  389.                       // Setting the properties from the block
  390.                       // reference helps certain entities get the
  391.                       // right references (and allows them to be
  392.                       // offset properly)
  393.                       clone.SetPropertiesFrom(br);
  394.                       // But we then need to get the layer
  395.                       // information from the database to set the
  396.                       // right layer (at least) on the new entity
  397.                       if (_placeOnCurrentLayer)
  398.                       {
  399.                         clone.LayerId = db.Clayer;
  400.                       }
  401.                       else
  402.                       {
  403.                         LayerTable lt =
  404.                           (LayerTable)tr.GetObject(
  405.                             db.LayerTableId,
  406.                             OpenMode.ForRead
  407.                           );
  408.                         if (lt.Has(layName))
  409.                           clone.LayerId = lt[layName];
  410.                       }
  411.                       // Now we need to transform the entity for
  412.                       // each of its Xref block reference containers
  413.                       // If we don't do this then entities in nested
  414.                       // Xrefs may end up in the wrong place
  415.                       ObjectId[] conts =
  416.                         pner.GetContainers();
  417.                       foreach (ObjectId contId in conts)
  418.                       {
  419.                         BlockReference cont =
  420.                           tr.GetObject(contId, OpenMode.ForRead)
  421.                             as BlockReference;
  422.                         if (cont != null)
  423.                           clone.TransformBy(cont.BlockTransform);
  424.                       }
  425.                       // Let's add the cloned entity to the current
  426.                       // space
  427.                       BlockTableRecord space =
  428.                         tr.GetObject(
  429.                           db.CurrentSpaceId,
  430.                           OpenMode.ForWrite
  431.                         ) as BlockTableRecord;
  432.                       if (space == null)
  433.                       {
  434.                         clone.Dispose();
  435.                         return;
  436.                       }
  437.                       ObjectId cloneId = space.AppendEntity(clone);
  438.                       tr.AddNewlyCreatedDBObject(clone, true);
  439.                       // Now let's flush the graphics, to help our
  440.                       // clone get displayed
  441.                       tr.TransactionManager.QueueForGraphicsFlush();
  442.                       // And we add our cloned entity to the list
  443.                       // for deletion
  444.                       _ids.Add(cloneId);
  445.                       // Created a non-graphical selection of our
  446.                       // newly created object and replace it with
  447.                       // the selection of the container Xref
  448.                       SelectedObject so =
  449.                         new SelectedObject(
  450.                           cloneId,
  451.                           SelectionMethod.NonGraphical,
  452.                           -1
  453.                         );
  454.                       e.ReplaceSelectedObject(so);
  455.                     }
  456.                   }
  457.                   catch
  458.                   {
  459.                     // A number of innocuous things could go wrong
  460.                     // so let's not worry about the details
  461.                     // In the worst case we are simply not trying
  462.                     // to replace the entity, so OFFSET will just
  463.                     // reject the selected Xref
  464.                   }
  465.                 }
  466.               }
  467.             }
  468.           }
  469.           tr.Commit();
  470.         }
  471.       }
  472.     }
  473.     public void Terminate()
  474.     {
  475.     }
  476.   }
  477. }
Depending on the mode selected, the results should be comparable with those from the code in the last post.
Here’s a portion of an xref:

Here are the results of using OFFSET on some of the xref’s contents after loading our module (to get these results you would also need to use XOFFSETLAYER to make sure the original layers get copied across):

Update:
I’ve modified the implementation of the XOFFSETLAYER command to be consistent with the prompts in the OFFSET command (under the Layer option – not sure how I missed that one :-). The ideal would have been to access that setting directly, but that doesn’t seem to be possible, as far as I can tell.


本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?注册

x
 楼主| 发表于 2009-9-7 23:24 | 显示全部楼层

July 10, 2006
Calling ObjectARX functions from a .NET application

One of the really compelling features of .NET is its ability to call "legacy" unmanaged C++ APIs. I say "legacy", but we use this facility regularly to call APIs that are far from being considered defunct (the C++ version of ObjectARX is alive and kicking, believe me! :-).

Autodesk understands that our development partners have invested many years in application development, and can't afford to throw that investment away to support the latest & greatest (and sometimes "flavor of the month") programming technology. For example, over the years we've made sure it was possible to create a VB or VBA user-interface for an existing LISP application or now a .NET user-interface for an ObjectARX application. Sometimes we expose our own interoperability functions to help with this (such as LISP functions to call ActiveX DLLs), and in other cases we advise people on how best to leverage standard Microsoft platform technologies.

So... how do you call an ObjectARX function from VB.NET? The answer is Platform Invoke (or P/Invoke for short). Microsoft has not exposed the full functionality of the Win32 API through the .NET Framework - just as Autodesk has not exposed all of ObjectARX through AutoCAD's Managed API - but P/Invoke helps you get around this.

First, some background on what ObjectARX really is, and how P/Invoke can help us.

ObjectARX is a set of APIs that are exported from DLLs or EXEs. Most exported functions get "decorated" or "mangled" during compilation, unless there is a specific compiler directive not to (this is the case for all the old ADS functions, for instance - they are declared as extern "C" and are therefore not mangled). The compiler assigns a unique name based on the function signature, which makes sense: it is quite legal in C++ to have two functions with the same name, but not with identical arguments and return values. The decorated name includes the full function name inside it, which is why the below technique for finding the correct export works.

[ Note: this technique works well for C-style functions, or C++ static functions. It will not work on instance members (methods of classes), as it is not possible to instantiate an unmanaged object of the class that defines the class from managed code. If you need to expose a class method to managed code, you will need to write & expose some native C++ code that instantiates the class, calls the method and returns the result. ]

To demonstrate the procedure we're going to work through the steps needed to call acedGetUserFavoritesDir() from C# and VB.NET. This function is declared in the ObjectARX headers as:

    extern Adesk::Boolean acedGetUserFavoritesDir( ACHAR* szFavoritesDir );

According to the ObjectARX Reference, "this function provides access to the Windows Favorites directory of the current user."

Step 1 - Identify the location of the export.

Fenton Webb, from DevTech EMEA, provided this handy batch file he uses for just this purpose:

[ Copy and paste this into a file named "findapi.bat", which you then place this into your AutoCAD application folder. You will need to run findapi from a command prompt which knows where to find dumpbin.exe - the Visual Studio Command Prompts created on installing VS will help you with this. ]

    @echo off
    if "%1" == "" goto usage
    :normal
    for %%i IN (*.exe *.dll *.arx *.dbx *.ocx *.ddf) DO dumpbin /exports %%i | findstr "%%i %1"
    goto end
    :usage
    echo findapi "function name"
    :end

You can redirect the output into a text file, of course, for example:

    C:\Program Files\AutoCAD 2007>findapi acedGetUserFavoritesDir > results.txt

It'll take some time to work, as this batch file chunks through all the DLLs, EXEs, etc. in the AutoCAD application folder to find the results (it doesn't stop when it finds one, either - this enhancement is left as an exercise for the reader ;-).

Opening the text file will allow you to see where the acedGetUserFavoritesDir() function is exported:

[ from the results for AutoCAD 2007 ]

    Dump of file acad.exe
            436  1B0 004B4DC0 ?acedGetUserFavoritesDir@@YAHPA_W@Z

A word of warning: the decorated names for functions accepting/returning strings changed between AutoCAD 2006 and 2007, because we are now using Unicode for string definition. Here is the previous declaration for 2004/2005/2006 (which was probably valid for as long as the function was defined, back in AutoCAD 2000i, if I recall correctly):

[ from the results for AutoCAD 2006 ]

    Dump of file acad.exe
            357  161 00335140 ?acedGetUserFavoritesDir@@YAHPAD@Z

This is simply because the function signature has changed from taking a char* to an ACHAR* (a datatype which now resolves to a "wide" or Unicode string in AutoCAD 2007). A change in the function signature results in a change in the decorated name. This is straightforward enough, but it is worth bearing in mind the potential migration issue - a heavy dependency on decorated function names can lead to substantial migration effort if widespread signature changes are made in a release (as with AutoCAD 2007's support of Unicode).

Another warning: you will find a number of other functions exported from the various DLLs/EXEs that do not have corresponding declarations in the ObjectARX headers. These functions - while exposed - are not supported. Which means that you may be able to work out how they can be called, but use them at your own risk (which can be substantial). Unsupported APIs are liable to change (or even disappear) without notice.

Now we've identified where and how the function is exposed, we can create a declaration of this function we can use in our code.

Step 2 - Declare the function correctly in your code.

This is going to be slightly different depending on the programming language you're using.

VB developers will be used to using "Declare" to set-up P/Invoke from their projects. This ends up being translated by the compiler into calls to DllImport, which is also used directly in C#.

These declarations should be made at the class level (not within an individual function definition).

VB.NET

    Private Declare Auto Function acedGetUserFavoritesDir Lib "acad.exe" Alias "?acedGetUserFavoritesDir@@YAHPA_W@Z" (<MarshalAs(UnmanagedType.LPWStr)> ByVal sDir As StringBuilder) As Boolean

C#

    [DllImport("acad.exe", EntryPoint = "?acedGetUserFavoritesDir@@YAHPA_W@Z", CharSet = CharSet.Auto)]
    public static extern bool acedGetUserFavoritesDir([MarshalAs(UnmanagedType.LPWStr)] StringBuilder sDir);

Notes:

   1. It's worth specifying the character set as "Auto" - which is not the default setting. The compiler does a good job of working out whether to use Unicode or ANSI, so it's easiest to trust it to take care of this.
   2. You will need to use the MarshalAs(UnmanagedType.LPWStr) declaration for Unicode string variables in 2007. This is true whether using Strings or StringBuilders.
   3. Use a StringBuilder for an output string parameter, as standard Strings are considered immutable. Strings are fine for input parameters.


Step 3 - Use the function in your code

[ I've omited the standard using/import statements, as well as the class & function declarations, to improve readability. ]

VB.NET

    Dim ed As Editor = Application.DocumentManager.MdiActiveDocument.Editor
    Dim sDir As New StringBuilder(256)
    Dim bRet As Boolean = acedGetUserFavoritesDir(sDir)
    If bRet And sDir.Length > 0 Then
            ed.WriteMessage("Your favorites folder is: " + sDir.ToString)
    End If

C#

    Editor ed = Application.DocumentManager.MdiActiveDocument.Editor;
    StringBuilder sDir = new StringBuilder(256);
    bool bRet = acedGetUserFavoritesDir(sDir);
    if (bRet && sDir.Length > 0)
            ed.WriteMessage("Your favorites folder is: " + sDir.ToString());

Note: we declare the StringBuilder variable (sDir) as being 256 characters long. AutoCAD expects us to provide a sufficiently long buffer for the data to be copied into it.

On my system both code snippets resulted in the following being sent to AutoCAD's command-line:

    Your favorites folder is: C:\My Documents\Favorites

So that's it: you should now be able to call global ObjectARX functions from .NET. This technique can also be used to call your own functions exposed from DLLs... which is one way to allow you to create fancy UIs with .NET and leverage existing C++ code (there are others, such as exposing your own Managed API).

For additional information on using P/Invoke, particularly with Win32, here is a really great resource.

http://pinvoke.net/

您需要登录后才可以回帖 登录 | 注册

本版积分规则

小黑屋|手机版|CAD论坛|CAD教程|CAD下载|联系我们|关于明经|明经通道 ( 粤ICP备05003914号 )  
©2000-2023 明经通道 版权所有 本站代码,在未取得本站及作者授权的情况下,不得用于商业用途

GMT+8, 2024-4-20 19:59 , Processed in 0.380282 second(s), 16 queries , Gzip On.

Powered by Discuz! X3.4

Copyright © 2001-2021, Tencent Cloud.

快速回复 返回顶部 返回列表