Click here to Skip to main content
15,860,859 members
Articles / Programming Languages / C#
Article

Dual Menus: Add a Left-click Menu to your Application's Tray Icon

Rate me:
Please Sign up or sign in to vote.
4.68/5 (14 votes)
27 Sep 2007CPOL5 min read 110.3K   2.4K   78   26
This article will show you a trick to spoof a right-click on your tray icon to support dual contextual menus for your application's NotifyIcon.
Screenshot - rightMenu.png
Figure 1. A normal NotifyIcon context menu.

Introduction

Special thanks to Rick Valstar for the original idea and solution to this problem. Download Rick's original solution in VB.NET above.

This article will show you how to relatively painlessly add a left-click context menu to your program's system tray icon, complete with a double-click timer to allow for double-clicks.

Background

A short while ago I decided it would be neat to equip my application's tray icon with an alternate menu interface that could be activated by a left mouse click, in addition to its existing right-click menu. However, I soon discovered this was easier said than done.

Although at first it may not sound like a very difficult problem if you've never attempted it, adding a left-click menu to a tray icon can be a nightmare if you're not prepared. In fact, the naive (read: intuitive) approach has so many problems with it you might just give it up for a lost cause. But don't give up just yet, there is a solution, but first let's look at exactly why this is such a tricky problem.

The Problem

My first approach was to simply create a ContextMenuStrip, populating it with the appropriate menu items, and Show() it at the cursor's current position, like I've done many times before in other situations. However, as the following image demonstrates, there were a number of problems with this approach:

Screenshot - problems.png
  1. A strange ghost form appears in the task bar whenever the context menu is visible. Note that we haven't created any additional form objects at all here! We've only called ContextMenuStrip.Show().
  2. The menu itself stays above the task bar, which results in a different appearance than that of the normal context menu, which appears directly above the cursor, slightly overlapping the top of the task bar (see Figure 1).
  3. To top it all off, the menu doesn't close when you click away from it! I've illustrated this by clicking the Outlook icon. Notice how our context menu is still visible even though it is in the background. The menu's AutoClose property also has no effect here.

Suffice it to say that manually showing a ContextMenuStrip is not going to solve our problem. Instead, we need to be a little more clever.

Let's examine the evidence: we know that the NotifyIcon does support context menus, it just doesn't seem to like anything other than a right-click to activate them. Furthermore, we can set the context menu via the NotifyIcon's ContextMenuStrip property, meaning the menu isn't hard-wired to the tray icon.

The Solution

So what do we do? Well, if the NotifyIcon only likes right-clicks, why upset it? We'll give it a right-click, but trick it into performing on our terms. We do this by listening for left-clicks on the icon, and spoofing a right-click whenever we receive one, after, of course, we swap out the default menu for our alternate one.

So, let's get to business.

Using the Code

The first thing we'll want to do is attach four components to our main form:

Screenshot - components.png
  • A NotifyIcon to represent our application in the system tray
  • A ContextMenuStrip for the left-click menu
  • A ContextMenuStrip for the right-click menu
  • A Timer to delay the left-click menu by the maximum time of a double-click

We'll want to set the timer's interval to match that of the system's double-click time:

C#
public DemoForm() {
    InitializeComponent();
    click_timer.Interval = SystemInformation.DoubleClickTime;
}

We'll also want to initially set the NotifyIcon's ContextMenuStrip to our right-click menu, as that will be the context menu we want to be shown by default.

Next we'll wire up the NotifyIcon's MouseClick event to check for a left-click, and start the timer if we receive one:

C#
private void notifyIcon_MouseClick( object sender, MouseEventArgs e ) {
    if ( e.Button == MouseButtons.Left )
        click_timer.Start();
}

The timer allows us to wait a fraction of a second to see if the user is going to click the mouse again, resulting in a double-click. If this occurs, we'll want to cancel the timer to avoid showing the left-click menu:

C#
private void notifyIcon_DoubleClick( object sender, EventArgs e ) {
    click_timer.Stop(); // cancel the left-click menu
    this.RestoreState();
}

Now, the real magic happens in the click timer's Tick event handler, where we construct and send out a fake right-click event at the cursor's current location. Be sure to stop the click timer, otherwise we'll keep getting spoofed right-clicks that will never stop!

C#
private void click_timer_Tick( object sender, EventArgs e ) {
    click_timer.Stop(); // make sure it doesn't keep firing!

    // here's where the magic happens
    // first, switch the icon's contextmenu to the left-click menu
    notifyIcon.ContextMenuStrip = left_menuStrip;
    // construct a right-click spoof input
    INPUT[] rightClick = new INPUT[2];
    
    MOUSEINPUT rightDown = new MOUSEINPUT();
    rightDown.dwFlags = Win32.MOUSEEVENTF_RIGHTDOWN + Win32.MOUSEEVENTF_ABSOLUTE;
    rightDown.dx = 0; // 0,0 means current cursor position
    rightDown.dy = 0;
    rightDown.time = 0;
    rightDown.mouseData = 0;

    MOUSEINPUT rightUp = new MOUSEINPUT();
    rightUp.dwFlags = Win32.MOUSEEVENTF_RIGHTUP + Win32.MOUSEEVENTF_ABSOLUTE;
    rightUp.dx = 0;
    rightUp.dy = 0;
    rightUp.time = 0;
    rightUp.mouseData = 0;

    rightClick[0] = new INPUT();
    rightClick[0].type = Win32.INPUT_MOUSE;
    rightClick[0].mi = rightDown;

    rightClick[1] = new INPUT();
    rightClick[1].type = Win32.INPUT_MOUSE;
    rightClick[1].mi = rightUp;

    // finally, send the spoofed right-click to invoke the menu
    Win32.SendInput( 2, rightClick, Marshal.SizeOf( rightClick[0] ) );
} 

... and finally, we have to be sure to reset the NotifyIcon's ContextMenuStrip property to its default right-click menu, when the user is finished with it:

C#
private void left_menuStrip_Closed( object sender, ToolStripDropDownClosedEventArgs e ) {
    // reset the menu to the default (right-click) menu
    notifyIcon.ContextMenuStrip = right_menuStrip; 
    } 

Now when we build and run, here are the results of a left-click:

Screenshot - leftMenu.png

SendInput()

The key to all of this is the Win32 SendInput() function, which will allow us to spoof a right-click without actually requiring the user to ever press the right mouse button. Because this is a Win32 function, we'll have to access it via Interop services, hence this article is deemed Intermediate and not Beginner.

Thanks to PInvoke.net, we've equipped our demo project with all of the declarations needed to invoke SendInput(). Please download the complete source code for all of the definitions as there are too many to list here.

Here is the signature for the SendInput() function:

C#
[DllImport( "user32.dll", SetLastError = true )]
public static extern uint SendInput( uint nInputs, INPUT[] pInputs, int cbSize );

For details on the SendInput function, check out Microsoft's documentation on MSDN.

Points of Interest

The SendInput function is quite a powerful Win32 function, and this is only one example of how it can come in handy, but it can also be quite dangerous. Please be sure to read the official documentation if you intend to use it yourself to solve a different problem. Remember, spoofed inputs affect all programs, not just the application that creates them.

That said, if you've used SendInput to solve a similar problem, please post a comment and let us know what you used it for.

I hope you found the article useful. Please use the comments section to discuss any points raised, or to ask about anything that wasn't quite clear.

History

  • Thursday, Sep 27, 2007: First publication

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
President The Little Software Company
Canada Canada
My name is Logan Murray and I'm a Canadian. I'm interested primarily in C# and Windows desktop application development (learning WPF at the moment and then hopefully LINQ), though I hope to branch-out more to the web side of things eventually. I am the president and owner of The Little Software Company and am currently working on the Windows version of OrangeNote, to be released soon. Check out my RSS reader, FeedBeast™.

Comments and Discussions

 
Suggestionthis approach doesn't work (I use it on Win7 64) Pin
Ray4ik26-Oct-11 3:00
Ray4ik26-Oct-11 3:00 
GeneralRe: this approach doesn't work (I use it on Win7 64) Pin
Mistika129-Aug-12 7:09
Mistika129-Aug-12 7:09 
GeneralThanks, the MS solution is still flawed, this one works perfectly. Here is a C++/CLR version of the solution. Pin
Akhilesh Singh28-Nov-08 1:15
Akhilesh Singh28-Nov-08 1:15 
GeneralRe: Thanks, the MS solution is still flawed, this one works perfectly. Here is a C++/CLR version of the solution. Pin
chaiguy133728-Nov-08 3:33
chaiguy133728-Nov-08 3:33 
GeneralSimpler solution [modified] Pin
Greg Mulvihill25-Nov-08 13:58
Greg Mulvihill25-Nov-08 13:58 
GeneralRe: Simpler solution Pin
chaiguy133725-Nov-08 14:02
chaiguy133725-Nov-08 14:02 
GeneralRe: Simpler solution Pin
Kurt Fankhauser17-Feb-09 11:22
Kurt Fankhauser17-Feb-09 11:22 
RantWont work in Vista 64 Pin
roachslayer16-Aug-08 23:06
roachslayer16-Aug-08 23:06 
GeneralRe: Wont work in Vista Pin
chaiguy133719-Aug-08 15:34
chaiguy133719-Aug-08 15:34 
GeneralRe: Wont work in Vista Pin
Kurt Fankhauser17-Feb-09 10:26
Kurt Fankhauser17-Feb-09 10:26 
GeneralRe: Wont work in Vista Pin
Mistika1211-Aug-12 2:55
Mistika1211-Aug-12 2:55 
GeneralGreat work Pin
ohgreat_anotheraccount1-Aug-08 1:45
ohgreat_anotheraccount1-Aug-08 1:45 
GeneralRe: Great work Pin
chaiguy13375-Aug-08 16:36
chaiguy13375-Aug-08 16:36 
GeneralActualy Pin
mfrapp29-Jul-08 9:39
mfrapp29-Jul-08 9:39 
GeneralRe: Actualy Pin
chaiguy133729-Jul-08 9:45
chaiguy133729-Jul-08 9:45 
GeneralAlternate way to get a context menu to close properly... Pin
Ray Roberts1-Nov-07 22:22
Ray Roberts1-Nov-07 22:22 
GeneralRe: Alternate way to get a context menu to close properly... Pin
chaiguy13372-Nov-07 6:44
chaiguy13372-Nov-07 6:44 
GeneralRe: Alternate way to get a context menu to close properly... Pin
SoleSoul24-Sep-08 6:57
SoleSoul24-Sep-08 6:57 
GeneralRe: Alternate way to get a context menu to close properly... Pin
chaiguy133724-Sep-08 6:59
chaiguy133724-Sep-08 6:59 
GeneralThanks. Great job & description! Pin
achirlin8-Oct-07 6:14
achirlin8-Oct-07 6:14 
GeneralThanks! Pin
droei7-Oct-07 22:02
droei7-Oct-07 22:02 
GeneralRe: Thanks! Pin
chaiguy13378-Oct-07 7:33
chaiguy13378-Oct-07 7:33 
GeneralGreat Job Pin
merlin98128-Sep-07 4:14
professionalmerlin98128-Sep-07 4:14 
GeneralNice code Pin
Skylinc27-Sep-07 10:31
Skylinc27-Sep-07 10:31 
GeneralNicely done. Pin
Tony Selke27-Sep-07 7:52
Tony Selke27-Sep-07 7:52 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.