次の方法で共有


How to enable "click through" for .NET 2.0 ToolStrip and MenuStrip

.NET 2.0's ToolStrip classes do not support "Click Through" between forms. For more information on what click through is, I recommend reading https://daringfireball.net/2003/05/interface_details_itunes_vs_safari . In Windows, pretty much all applications use "click through," whereby if you have one window active and click on to another one, the mouse click activates the window and the click is processed by whatever control you had the mouse over.

 

However, you'll notice that Office 2003 and Visual Studio 2005 do not have this type of behavior for at least their menus and toolbars. If you have a window other than Word 2003 active, and hover your mouse over the Word toolbars, you will notice that they do not hot-track. Clicking will first activate the Word window after which you must click again on the toolbar item to activate it.

 

This is fine and good, and I don't disagree with this user interface decision. However, the .NET 2.0 ToolStrip class makes one glaring omission: it doesn't allow you to disable this behavior. Or rather, it does not allow you to enable click-through. This is usually not important for the most common use of the ToolStrip class, that being to create an application with toolbars docking to the edges of its only Form.

 

In Paint.NET we have 5 forms active. There's the main form (in code it is aptly called MainForm), then the four child or owned forms that host the Tools, History, Layers, and Colors windows. In Paint.NET I have done a lot of work to ensure that they all behave for the user as one active window.

 

The problem for us is that the no-click-through behavior persists even within the same application. That is, if the focus is currently on the image canvas in Paint.NET, the moment you try to click on another tool in the Tools window it will actually set focus to the Tools window and not set the tool until you click again. Oops. Actually this isn't really so much of a problem except that you can not configure whether this behavior is used or not.

 

Well I managed to fix that before we had a public release of v2.6, and I'd like to detail the code that's necessary to do this.

 

One thing I highly recommend for anyone doing non-trivial Windows Forms development is to get a copy of Reflector (https://www.aisto.com/roeder/dotnet/) and the disassembly plugin by Denis Bauer (https://www.denisbauer.com/NETTools/FileDisassembler.aspx). Next, open up Reflector and add all of the core .NET assemblies. Then run the disassembler plugin over all of them. The code you get is not perfect -- some variable names are lost during compilation and end up being generic upon disassembly -- but the code flow and techniques are there and are very valuable to have. It's good to be able to answer questions such as, "When I set/get this property, is it copying just the reference or calling Clone()?" (hint: almost always the latter)

 

It took me awhile, but I found that the way this no-click-through is implemented is via the WM_MOUSEACTIVATE notification, and is handled in an overridden WndProc() method. It supports 4 return values which are a 2x2 matrix of "activate" and "eat mouse click." Turns out that the ToolStrip is returning the value corresponding to "activate and eat" (MA_ACTIVATEANDEAT) whereas we want "activate but do NOT eat" (MA_ACTIVATE).

 

Once we know this it is trivial to create a ToolStripEx class that implements this. This does require unmanaged code permission, and is something of a hack, so you should take that into consideration when using this code.

 

/// <summary>

/// This class adds on to the functionality provided in System.Windows.Forms.ToolStrip.

/// </summary>

public class ToolStripEx

    : ToolStrip

{

    private bool clickThrough = false;

 

    /// <summary>

    /// Gets or sets whether the ToolStripEx honors item clicks when its containing form does

    /// not have input focus.

    /// </summary>

    /// <remarks>

    /// Default value is false, which is the same behavior provided by the base ToolStrip class.

    /// </remarks>

    public bool ClickThrough

    {

        get

        {

            return this.clickThrough;

        }

 

        set

        {

            this.clickThrough = value;

        }

    }

 

    protected override void WndProc(ref Message m)

    {

        base.WndProc(ref m);

 

        if (this.clickThrough &&

            m.Msg == NativeConstants.WM_MOUSEACTIVATE &&

            m.Result == (IntPtr)NativeConstants.MA_ACTIVATEANDEAT)

        {

            m.Result = (IntPtr)NativeConstants.MA_ACTIVATE;

        }

    }

}

 

internal sealed class NativeConstants

{

    private NativeConstants()

    {

    }

 

    internal const uint WM_MOUSEACTIVATE = 0x21;

    internal const uint MA_ACTIVATE = 1;

    internal const uint MA_ACTIVATEANDEAT = 2;

    internal const uint MA_NOACTIVATE = 3;

    internal const uint MA_NOACTIVATEANDEAT = 4;

}

 

Then simply use ToolStripEx in place of ToolStrip.

 

The important thing with this hack is that you let the ToolStrip base class process the message first, and only fudge the return value from the window message handling if it is returning the one value you are worried about. Otherwise you're overstepping what you're trying to do (and things don't work right).

 

Also, the MenuStrip class has the same behavior (no click through). It's easy enough to take this hack and make a "MenuStripEx" class. In Paint.NET v2.6, I put the common WndProc functionality into a utility function that both of these classes make use of (PaintDotNet.SystemLayer.UI.ClickThroughWndProc()).

Comments