To follow on from the last post, we're now going to take a look at adding custom menu items to the default context menu in AutoCAD. The default menu appears when the user right-clicks on the drawing but has no objects selected. This is a good place to put application commands, for instance.
The approach is very similar to the previous one, although I've added some additional commands to control adding and removing the various menus in addition to relying on the module's initialization callback.
Some other notes:
- We're not just adding a single menu item, but are using a cascading menu - three menu items underneath a main application menu item.
- Once again we're using the Autodesk.AutoCAD.Internal namespace (which is unsupported and liable to change, and needs the AcMgdInternal.dll assembly referenced), to use the PostCommandPrompt() function. We would not need this if we called our commands via SendStringToExecute(), of course.
Here's the C# code:
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.EditorInput;
using Autodesk.AutoCAD.Runtime;
using Autodesk.AutoCAD.Windows;
using Autodesk.AutoCAD.Internal;
using System;
namespace ContextMenuApplication
{
public class Commands : IExtensionApplication
{
public void Initialize()
{
CountMenu.Attach();
ApplicationMenu.Attach();
}
public void Terminate()
{
CountMenu.Detach();
ApplicationMenu.Detach();
}
[CommandMethod("ADDCONTEXT")]
static public void AttachContextMenus()
{
CountMenu.Attach();
ApplicationMenu.Attach();
}
[CommandMethod("NOCONTEXT")]
static public void DetachContextMenus()
{
CountMenu.Detach();
ApplicationMenu.Detach();
}
[CommandMethod("COUNT", CommandFlags.UsePickSet)]
static public void CountSelection()
{
Editor ed =
Application.DocumentManager.MdiActiveDocument.Editor;
PromptSelectionResult psr = ed.GetSelection();
if (psr.Status == PromptStatus.OK)
{
ed.WriteMessage(
"\nSelected {0} entities.",
psr.Value.Count
);
}
}
}
public class CountMenu
{
private static ContextMenuExtension cme;
public static void Attach()
{
if (cme == null)
{
cme = new ContextMenuExtension();
MenuItem mi = new MenuItem("Count");
mi.Click += new EventHandler(OnCount);
cme.MenuItems.Add(mi);
}
RXClass rxc = Entity.GetClass(typeof(Entity));
Application.AddObjectContextMenuExtension(rxc, cme);
}
public static void Detach()
{
RXClass rxc = Entity.GetClass(typeof(Entity));
Application.RemoveObjectContextMenuExtension(rxc, cme);
}
private static void OnCount(Object o, EventArgs e)
{
Document doc =
Application.DocumentManager.MdiActiveDocument;
doc.SendStringToExecute("_.COUNT ", true, false, false);
}
}
public class ApplicationMenu
{
private static ContextMenuExtension cme;
public static void Attach()
{
if (cme == null)
{
cme = new ContextMenuExtension();
cme.Title = "Kean's commands";
MenuItem mi1 = new MenuItem("1st");
mi1.Click += new EventHandler(On1st);
cme.MenuItems.Add(mi1);
MenuItem mi2 = new MenuItem("2nd");
mi2.Click += new EventHandler(On2nd);
cme.MenuItems.Add(mi2);
MenuItem mi3 = new MenuItem("3rd");
mi3.Click += new EventHandler(On3rd);
cme.MenuItems.Add(mi3);
}
Application.AddDefaultContextMenuExtension(cme);
}
public static void Detach()
{
Application.RemoveDefaultContextMenuExtension(cme);
}
private static void On1st(Object o, EventArgs e)
{
Editor ed =
Application.DocumentManager.MdiActiveDocument.Editor;
ed.WriteMessage("\nFirst item selected.");
Utils.PostCommandPrompt();
}
private static void On2nd(Object o, EventArgs e)
{
Editor ed =
Application.DocumentManager.MdiActiveDocument.Editor;
ed.WriteMessage("\nSecond item selected.");
Utils.PostCommandPrompt();
}
private static void On3rd(Object o, EventArgs e)
{
Editor ed =
Application.DocumentManager.MdiActiveDocument.Editor;
ed.WriteMessage("\nThird item selected.");
Utils.PostCommandPrompt();
}
}
}
And here are our custom menu items on AutoCAD's default context menu:


Subscribe via RSS
Hi Kean,
I've a question: how do I pass parameters to a command invoked from the context menu?
I can use 'SendStringToExecute' with a command strings and its parametrs (as string) but how does the invoked command method retrieve those parameters?
Any other available method than 'SendStringToExecute' to recall methods from a menu?
TIA,
Cabbi
Posted by: Cabbi | May 22, 2007 at 04:36 PM
Hi Cabbi,
You'd use Editor.GetString()/GetDouble()/GetInteger()/GetPoint() etc. to pick up the command-line input fed in through SendStringToExecute().
You might opt to call the functions directly, but generally it's safer to use the SendStringToExecute() to invoke commands like this (it helps avoid re-entrancy issues, for example).
Regards,
Kean
Posted by: Kean | May 22, 2007 at 07:14 PM
>You might opt to call the functions directly
But if I call functions directly from the menu command and do any transaction with the DB I got an AutoCAD crash. I was assuming this is due to the different contexts of the menu and the drawing, isn't it?
How do I avoid this crash by calling my methods directly?
TIA,
Cabbi
Posted by: Cabbi | May 23, 2007 at 08:28 AM
You might try locking the document, by I *strongly* recommend sticking to using SendStringToExecute. It's a bit more work to expose your functions as commands, but in my opinion the extra effort is worthwhile and they can then be used in many different places (menus, toolbars, etc.).
Kean
Posted by: Kean | May 23, 2007 at 10:28 AM
Thanks a lot!
Yet another stupid question: how do I insert a menu separator?
I tried with "-", "--", "_" but... nothing. Even Goolgeing around didn't helped.
Cabbi
Posted by: Cabbi | May 23, 2007 at 10:51 AM
Try using a blank string:
MenuItem sep = new MenuItem("");
cme.MenuItems.Add(sep);
Regards,
Kean
Posted by: Kean | May 23, 2007 at 01:00 PM
Oh, nooooo, the most stupid option I did not try!?!?
It seems it's time for me to take a little vacation! :oD
Thanks again,
Cabbi
Posted by: Cabbi | May 23, 2007 at 03:15 PM
Good morning Kean.
I am new to programming AutoCAD via .NET and I have a question for you. I used to have a custom context menu in ACAD2005. I would like to do the same using the above method, but my inexperience is frustrating me.
How would I set the on1st method to draw a vertical xline? Or to call any command from the command line. (For example: dist) This would definately reduce my frustration.
Thanks for your help.
Jason
Posted by: Jason | June 06, 2007 at 06:07 PM
Hi Jason,
For calling commands, just search this blog for "SendStringToExecute" - you should find plenty of examples.
To create an XLine, you should be able to pick up some standard code to create and post another type of entity (a polyline, for instance), and then you modify it to use the Autodesk.AutoCAD.DatabaseServices.Xline class.
Regards,
Kean
Posted by: Kean | June 07, 2007 at 09:18 AM
Thanks for the info. I've got it working now. I am a little more comfortable working with VB.NET. Do you know if someone has converted this to VB at all?
Posted by: Jason | June 07, 2007 at 05:29 PM
These days I generally don't post in both C# and VB, mainly because I prefer C#, myself, and the translation tools available are pretty good.
Here are two that I've used:
Carlos Aguilar's "Code Translator"
Kamal Patel's "Convert C# to VB.NET"
Neither provide 100% perfect results, but they do get you most of the way there.
I also use a Visual Studio plugin that uses either of these sites for the conversion directly in Visual Studio 2005.
Kean
Posted by: Kean | June 08, 2007 at 10:09 AM
Good morning Kean.
I do have another question for you. I've got this code working perfectly, just the way I want it. Is there a way to add accelerator keys to the context menu? Just like in the image above, the first letters in Copy and Paste are underlined. Can that be done in this code as well?
Thanks.
Posted by: Jason | July 03, 2007 at 03:50 PM
Hi Jason,
Preceding the accelerator character with an ampersand is the typical way to do this. I gave this a try, and found it works, but not perfectly...
cme = new ContextMenuExtension();
cme.Title = "&&Kean's commands";
MenuItem mi1 = new MenuItem("&&1st");
mi1.Click += new EventHandler(On1st);
cme.MenuItems.Add(mi1);
The accelerator works in both cases, but only displays for the menu, not for the menu item. This isn't ideal, but hopefully it's enough for you.
Regards,
Kean
Posted by: Kean | July 06, 2007 at 12:11 PM
Good morning Kean.
Thanks for that info. I never thought to try two & in front of what I wanted accelerated. I did try one, but that didn't work. Must be a VB thing. (Although I haven't tried there either.)
Thanks again.
Posted by: Jason | July 09, 2007 at 03:33 PM
Hello Kean,
Nice option to create in memory context-menu parts. But the addition will be situated in the lower part of the menu and that's not quit userfriendly for our application. So I was wondering if it's possible to change the order within a context menu.
Thanks for you input.
Posted by: John | August 28, 2007 at 01:26 PM
Hi John,
There's no way to control this through the API, although you might be able to do something with some low-level APIs in the OS (I haven't done so, but it *might* be possible, if you don't mind creating a potential future maintenance problem for yourself).
Regards,
Kean
Posted by: Kean | August 28, 2007 at 03:17 PM
Hi Kean,
Is it possible to have my own menu with out adding to default menu ?
Regards,
Razmin.
Posted by: Razmin | October 17, 2007 at 07:27 AM
Hi Razmin,
Do you mean you want to replace the default context menu, or have your own pull-down menu in the AutoCAD application frame?
Regards,
Kean
Posted by: Kean | October 17, 2007 at 10:58 AM
Hi Kean,
I'm getting some strange (to me) behavior out of ads_queueexpr...
When I first translated this example, It triggered the appropriate events, but the SendString wasn't executed... So I tried a couple of other ways. When I tried calling the methods directly, I had lock mode issues, so I tried ads_queueexpr and at first it seemed nothing was happening, then I found that if I clicked one of my menuitems, then (do anything, like ESC, or pick a button, or type a command) and then my "queued expression" would fire. Strange, no?
Posted by: David Osborne | November 17, 2008 at 11:33 PM
Depends on how your command string is formatted - have you remembered to call it via "(command \"MYCMD\")" ?
Kean
Posted by: Kean | November 17, 2008 at 11:39 PM
I prefer using vl-cmdf, but yes, I called it as a lisp statement.
ex. ads_queueexpr("(vl-cmdf ""STARTRUN"")")
I even tried putting a line feed in after the closing paren on the lisp, and just for giggles I tried it with 'command'. No difference.
I don't really know what was happening with the SendString... because it seems to be working that way now, but this behavior on ads_queueexpr is still repeatable.
The oddest thing is that I use ads_queueexpr to respond to DocumentActivated in order to get saved values out of an extension dictionary, and it works fine there, though so much is going on at that time, it may be that it's getting flushed out by the things going on after it, just like when I hit ESC or something.
Anyway, at this point, my stuff is working with SendStringtoExecute, so there's no need for me to waste any more of your time with it, but it is still strange!
Posted by: David Osborne | November 18, 2008 at 12:25 AM
Hi Ken, try pass parameters to command, but I can´t because generate an error
the code is:
_
Public Sub Arma_Dado(ByVal sss As String)
code...
end sub
how a pass parameter using SendStringtoExecute, for example
SendStringtoExecute("arma_dado"& chr(13) & "D-1", ???,???,???)
Can I do this?
Posted by: Luis Rey | January 09, 2009 at 07:53 PM
You need to use Editor.GetString() (or one of the other GetXXX functions) from within your command to pick up the values passed in via the command-line.
Kean
Posted by: Kean Walmsley | January 12, 2009 at 09:40 AM
Sorry kean, but I need take a value from a usercontrol, when I execute for a first time everything is well, but when I change it on other window and try to run it, don´t recive the parametes: my code is:
(funtion en the command class)
Public Class LU01Commands
_
Public Sub ins_varilla(ByVal pto As Point3d, ByVal varilla As String)
Dim doc As Document = Application.DocumentManager.MdiActiveDocument
Dim db As Database = doc.Database
Dim ed As Editor = doc.Editor
Dim tr As Transaction = db.TransactionManager.StartTransaction()
Dim bt As BlockTable = tr.GetObject(db.BlockTableId, OpenMode.ForRead)
Dim btr As BlockTableRecord = tr.GetObject(bt(BlockTableRecord.ModelSpace), OpenMode.ForWrite)
'more code......
End Sub
End Class
And My code on then pallete (user control) is:
when I press button1.
Private Sub Button3_Click(ByVal sender As System.Object, ByVal e As System.EventArgs)
Dim doc As Document = Application.DocumentManager.MdiActiveDocument
Dim db As Database = doc.Database
Dim ed As Editor = doc.Editor
Try
doc.SendStringToExecute("ins_varilla" & Chr(13), False, False, False)
ed = Nothing
db = Nothing
doc = Nothing
Catch ex As Exception
MsgBox(ex.Message)
End Try
End Sub
Can I use the "SendStringtoExecute" to do or there are other way to do.
Posted by: Luis Rey | January 17, 2009 at 12:46 AM
Luis -
You can't just declare the function with arguments: it needs to be a standard AutoCAD command (defined as always) and then you use GetString() etc. to retrieve the arguments from the command-line.
See this post.
Regards,
Kean
Posted by: Kean Walmsley | January 17, 2009 at 01:38 PM