Click here to Skip to main content
Click here to Skip to main content

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

By , 27 Sep 2007
 
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:

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:

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:

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!

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:

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:

[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)

About the Author

chaiguy1337
President The Little Software Company
Canada Canada
Member
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™.

Sign Up to vote   Poor Excellent
Add a reason or comment to your vote: x
Votes of 3 or less require a comment

Comments and Discussions

 
You must Sign In to use this message board.
Search this forum  
    Spacing  Noise  Layout  Per page   
Suggestionthis approach doesn't work (I use it on Win7 64)memberRay4ik26 Oct '11 - 3:00 
Hi,
 
unfortunately this interesting approach doesn't work Sigh | :sigh: . I have the next situation: when I do left click on icon nothing happends, but if then I do a right click I see left_menustrip! But after right click I see right_menustrip.
 

Also I found more convinient and short method to solve this problem for C#,
use should just write the next in
private void click_timer_Tick( object sender, EventArgs e ) {
            click_timer.Stop(); // make sure it doesn't keep firing!

            // first, switch the icon's contextmenu to the left-click menu
            notifyIcon.ContextMenuStrip = left_menuStrip;
            MethodInfo mi = typeof(NotifyIcon).GetMethod("ShowContextMenu", BindingFlags.Instance | BindingFlags.NonPublic);
            mi.Invoke(notifyIcon, null);
}
 
and you should use
using System.Reflection;
Blush | :O
GeneralRe: this approach doesn't work (I use it on Win7 64)memberMistika129 Aug '12 - 7:09 
The problem is in definition of INPUT structure: it has a union in a non-first field, hence requires to specify FieldOffset. But specifying FieldOffset directly (4) leads to problems on x64 due to 8-byte alignment (and vice versa).
 
To workaround this problem you can :
1. Either don't use union at all. It's fine if you want to use only one kind of input in your application:
[StructLayout(LayoutKind.Sequential)]
public struct INPUT 
{
    public int type;
    public MOUSEINPUT mi;
}
 
2. Or move the union to a separate structure, so you won't be required to specify field offset in the INPUT structure:
[StructLayout(LayoutKind.Explicit)]
public struct InputUnion
{
    [FieldOffset(0)]
    public MOUSEINPUT mi;
    [FieldOffset(0)]
    public KEYBDINPUT ki;
    [FieldOffset(0)]
    public HARDWAREINPUT hi;
}
 
[StructLayout(LayoutKind.Sequential)]
public struct INPUT
{
    public int type;
    public InputUnion iu;
}

GeneralThanks, the MS solution is still flawed, this one works perfectly. Here is a C++/CLR version of the solution.memberAkhilesh Singh28 Nov '08 - 1:15 
I tried the Microsoft solution, which works but the behaviour is still not 100% correct. Finally I settled for this solution which works flawlessly.
 
I am using C++/CLR on VS2005 and ported the code for that as given below. Hope it will be useful for someone. Roll eyes | :rolleyes:
 
		
 
 if (e->Button == System::Windows::Forms::MouseButtons::Left)
 {
	INPUT rightClick[2];
    
	MOUSEINPUT rightDown;
	rightDown.dwFlags = MOUSEEVENTF_RIGHTDOWN + MOUSEEVENTF_ABSOLUTE;
	rightDown.dx = 0; 
	rightDown.dy = 0;
	rightDown.time = 0;
	rightDown.mouseData = 0;
 
	MOUSEINPUT rightUp;
	rightUp.dwFlags = MOUSEEVENTF_RIGHTUP + MOUSEEVENTF_ABSOLUTE;
	rightUp.dx = 0;
	rightUp.dy = 0;
	rightUp.time = 0;
	rightUp.mouseData = 0;
 
	rightClick[0].type = INPUT_MOUSE;
	rightClick[0].mi = rightDown;
 
	rightClick[1].type = INPUT_MOUSE;
	rightClick[1].mi = rightUp;
 
	// finally, send the spoofed right-click to invoke the menu
	::SendInput( 2, rightClick, sizeof(rightClick[0]));
 }
	 

GeneralRe: Thanks, the MS solution is still flawed, this one works perfectly. Here is a C++/CLR version of the solution.memberchaiguy133728 Nov '08 - 3:33 
Great, thanks for the translation. Good to know it still has some value. Wink | ;)
 
Sad but true: 4/3 of Americans have difficulty with simple fractions.
 
There are 10 types of people in this world: those who understand binary and those who don't.
 
{o,o}.oO( Check out my blog! )
|)””’)          http://pihole.org/
-”-”-

GeneralSimpler solution [modified]membergregmulvihill25 Nov '08 - 13:58 
I appreciate your time and effort in putting this project together. I was looking for just this type of solution when I came across your article. After downloading and testing your code, I found the method you provided for invoking a different context menu in response to a left mouse click event to be inconsistent on my Vista 32 PC. After analyzing and tinkering with the code a bit, I decided there had to be a better way and discovered a posting that provides a simpler solution.
 
http://forums.microsoft.com/MSDN/ShowPost.aspx?PostID=1570925&SiteID=1#1572106[^]
 
I modified it slightly to swap the appropriate context menu.
 
void notifyIcon1_MouseDown(object sender, MouseEventArgs e)
{
	notifyIcon1.ContextMenu = (e.Button == MouseButtons.Right) ? ContextMenu_Right : ContextMenu_Left;
 
	if ((e.Button == MouseButtons.Left) && (1 == e.Clicks))
	{
		MethodInfo oMethodInfo = typeof(NotifyIcon).GetMethod("ShowContextMenu", BindingFlags.Instance | BindingFlags.NonPublic);
		oMethodInfo.Invoke(notifyIcon1, null);
	}
}
 
I hope this helps.
 
modified on Tuesday, November 25, 2008 8:28 PM

GeneralRe: Simpler solutionmemberchaiguy133725 Nov '08 - 14:02 
Interesting solution. I'll keep it in mind if I need to do this again. My solution served me well for the time I needed it, but this does appear to be simpler.
 
Thanks for the input.
 
Sad but true: 4/3 of Americans have difficulty with simple fractions.
 
There are 10 types of people in this world: those who understand binary and those who don't.
 
{o,o}.oO( Check out my blog! )
|)””’)          http://pihole.org/
-”-”-

GeneralRe: Simpler solutionmemberKurt Fankhauser17 Feb '09 - 11:22 
This works great (and it works in Vista64)! However, the context menu responds to the right mouse button UP event. For consistency you probably want to do the same with the left mouse button (i.e., notifyIcon1_MouseUp()). Remember to register for the event:
 
this.notifyIcon1.MouseUp += new MouseEventHandler(this.notifyIcon1_MouseUp);
 
FYI, the link above refers to the MSDN forum thread entitled "Open ContextMenuStrip on left mouse click on NotifyIcon".
RantWont work in Vista 64memberroachslayer16 Aug '08 - 23:06 
The left button never opens the menu after the spoof. Dunno what the deal is, but it sucks. Specifically, I tried this on Vista 64. Works fine in XP.
GeneralRe: Wont work in Vistamemberlogan133719 Aug '08 - 15:34 
It works fine for me in Vista 32, so it must be a 64-bit thing. Unfortunately I don't know enough to garner a clue as to why that may be. If anyone has any thoughts, please post them.
 
“Time and space can be a bitch.”
–Gushie, Quantum Leap
 
{o,o}.oO( Looking for a great RSS reader? Try FeedBeast! )
|)””’)            Built with home-grown CodeProject components!
-”-”-

GeneralRe: Wont work in VistamemberKurt Fankhauser17 Feb '09 - 10:26 
The SendInput() documentation states that under Vista the function can fail when it is blocked by User Interface Privilege Isolation (UIPI) and that no error is reported in this case. Essentially the OS discards the windows message generated by this application because it could affect many other applications (by modifying the input stream). I have not observed this on Vista, but have on Vista 64 so it must have tighter enforcement. It would not surprise me to see a service pack or patch close this hole on Vista as well.
 
What to do...

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

Permalink | Advertise | Privacy | Mobile
Web02 | 2.6.130523.1 | Last Updated 27 Sep 2007
Article Copyright 2007 by chaiguy1337
Everything else Copyright © CodeProject, 1999-2013
Terms of Use
Layout: fixed | fluid