5

I'm trying to extend Button to add a RightClick event.

My customer wants a button to do different things depending on if you left-click or right-click. I expected there to be an easy event for right-clicking, but it turns out there's not.

I'd prefer Button's visual behavior to be identical to the preexisting Click event, but it's proving tough. Button has many graphical behaviors that occur when you click and drag on and off the button.

  • When you click-down, the button lowers. If you drag off the lowered button, it raises (e.g. un-lowers). Any other buttons you drag over will ignore it.
  • When you drag back on the original button, it will lower again.
  • If you drag off and then release, the original button's state should reset (i.e. you can't reclick and drag on again).

These little graphical quirks will look rough if the left-click visuals don't match the right-click visuals.

Currently I'm stuck on this: If right-click and hold the button, then drag off the button, how do I detect if the user un-clicks? I need to know this so I can know not to re-lower the button on re-entry.

A broader question: Am I even on the right track? I couldn't find anyone who's done this before. My code is below.

public class RightClickButton : Button
{
    public event RoutedEventHandler RightClick;

    public RightClickButton()
    {
        this.MouseRightButtonDown += new System.Windows.Input.MouseButtonEventHandler(RightClickButton_MouseRightButtonDown);
        this.MouseRightButtonUp += new System.Windows.Input.MouseButtonEventHandler(RightClickButton_MouseRightButtonUp);
        this.MouseEnter += new System.Windows.Input.MouseEventHandler(RightClickButton_MouseEnter);
        this.MouseLeave += new System.Windows.Input.MouseEventHandler(RightClickButton_MouseLeave);
    }

    void RightClickButton_MouseRightButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
    {
        this.IsPressed = true;
    }

    void RightClickButton_MouseRightButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
    {
        this.IsPressed = false;
        if (RightClick != null)
            RightClick.Invoke(this, e);
    }

    void RightClickButton_MouseLeave(object sender, System.Windows.Input.MouseEventArgs e)
    {
        if (this.IsPressed)
            this.IsPressed = false;
    }

    void RightClickButton_MouseEnter(object sender, System.Windows.Input.MouseEventArgs e)
    {
        if (this.IsFocused && Mouse.RightButton == MouseButtonState.Pressed)
            this.IsPressed = true;
    }
}
Grant Birchmeier
  • 17,809
  • 11
  • 63
  • 98
  • See related question for one of my sticking points: [Can I get a MouseLeave event while Mouse.Capture() is active?](http://stackoverflow.com/questions/9961050/can-i-get-a-mouseleave-event-while-mouse-capture-is-active) – Grant Birchmeier Apr 01 '12 at 01:05

6 Answers6

3

Thanks to an answer given to my other question, this is what I came up with.

It seems there might be some differences between XP and Win7 behavior. I'll need to test further on Win7.

public class RightClickButton : Button
{
    public event RoutedEventHandler RightClick;

    private bool _clicked = false;

    public RightClickButton()
    {
        this.MouseRightButtonDown += new System.Windows.Input.MouseButtonEventHandler(RightClickButton_MouseRightButtonDown);
        this.MouseRightButtonUp += new System.Windows.Input.MouseButtonEventHandler(RightClickButton_MouseRightButtonUp);
    }

    // Subclasses can't invoke this event directly, so supply this method
    protected void TriggerRightClickEvent(System.Windows.Input.MouseButtonEventArgs e)
    {
        if (RightClick != null)
            RightClick(this, e);
    }

    void RightClickButton_MouseRightButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
    {
        this.IsPressed = true;
        CaptureMouse();
        _clicked = true;
    }

    void RightClickButton_MouseRightButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
    {
        ReleaseMouseCapture();

        if(this.IsMouseOver && _clicked)
        {
            if (RightClick != null)
                RightClick.Invoke(this, e);
        }

        _clicked = false;
        this.IsPressed = false;
    }

    protected override void OnMouseMove(MouseEventArgs e)
    {
        base.OnMouseMove(e);

        if (this.IsMouseCaptured)
        {
            bool isInside = false;

            VisualTreeHelper.HitTest(
                this,
                d =>
                {
                    if (d == this)
                    {
                        isInside = true;
                    }

                    return HitTestFilterBehavior.Stop;
                },
                ht => HitTestResultBehavior.Stop,
                new PointHitTestParameters(e.GetPosition(this)));

            if (isInside)
                this.IsPressed = true;
            else
                this.IsPressed = false;
        }
    }
}
Community
  • 1
  • 1
Grant Birchmeier
  • 17,809
  • 11
  • 63
  • 98
1

Just adding my own 2c. This is based on the answer given by @Grant with the following changes:

  1. This properly uses the _rightButtonDown flag, not the CaptureMouse property to determine processing since CaptureMouse may have been set elsewhere (i.e. this adheres to proper encapsulation.)

  2. This has support for the ClickMode property allowing for a value of Press.

  3. This properly uses the OnRightClick handler whose name not only now follows the standard convention, but which also handles all calls to raise the RightClick event rather than duplicating the code. It was also marked as virtual to properly support subclass capabilities in that if you override that function, you can handle/block all RightClick events.

  4. I named it EnhancedButton in case I want to add additional functionality.

  5. I have included an example of how you would define a style to make this properly appear in a ToolBar

Note: The proper way to implement the RightClick event on a control is as a RoutedEvent, not a standard CLR event. However, I'll leave that up to the reader to implement as to keep this example simple.

Here's the code for the EnhancedButton class:

public class EnhancedButton : Button
{
    private bool _rightButtonDown = false;

    public EnhancedButton()
    {
        MouseRightButtonDown += RightClickButton_MouseRightButtonDown;
        MouseRightButtonUp   += RightClickButton_MouseRightButtonUp;
    }

    #region RightClick Event

        public event RoutedEventHandler RightClick;

        protected virtual void OnRightClick(MouseButtonEventArgs e)
        {
            RightClick?.Invoke(this, e);
        }

    #endregion RightClick Event

    #region Event Handlers

        private void RightClickButton_MouseRightButtonDown(object sender, MouseButtonEventArgs e)
        {
            IsPressed = true;

            _rightButtonDown = true;
            CaptureMouse();

            if(ClickMode == ClickMode.Press)
                OnRightClick(e);
        }

        private void RightClickButton_MouseRightButtonUp(object sender, MouseButtonEventArgs e)
        {
            if(!_rightButtonDown)
                return;

            ReleaseMouseCapture();

            IsPressed = false;

            if(IsMouseOver && ClickMode == ClickMode.Release)
                OnRightClick(e);

            _rightButtonDown = false;
        }

    #endregion Event Handlers

    #region Overrides

        protected override void OnMouseMove(MouseEventArgs e)
        {
            base.OnMouseMove(e);

            if(!_rightButtonDown)
                return;

            bool isInside = false;

            VisualTreeHelper.HitTest(this, d =>
            {
                if (d == this)
                    isInside = true;

                return HitTestFilterBehavior.Stop;
            },
            ht => HitTestResultBehavior.Stop,
            new PointHitTestParameters(e.GetPosition(this)));

            IsPressed = isInside;
        }

    #endregion Overrides
}

And here's how you define a style to make this appear like all other ToolBar buttons. Add this to your Toolbar's resources, or better yet, style your toolbars to set this in the style so you don't have to add it manually everywhere.

<Style TargetType="{x:Type myControls:EnhancedButton}" BasedOn="{StaticResource {x:Static ToolBar.ButtonStyleKey}}" />

Now it's a more-compatible drop-in replacement for the standard Button control.

Mark A. Donohoe
  • 28,442
  • 25
  • 137
  • 286
1

If you look at the source code for ButtonBase, you'll see that it would be difficult to easily duplicate what's happening in the OnMouseLeftButtonDown and OnMouseLeftButtonUp overrides.

A simple hack which seems to work fairly well is to derive from Button as follows:

public class MyButton : Button
{
    protected override void OnMouseRightButtonDown(MouseButtonEventArgs e)
    {
        base.OnMouseRightButtonDown(e); // this line probably optional/not required
        base.OnMouseLeftButtonDown(e);
    }

    protected override void OnMouseRightButtonUp(MouseButtonEventArgs e)
    {
        base.OnMouseRightButtonUp(e); // this line probably optional/not required
        base.OnMouseLeftButtonUp(e);
    }
}

Then use <MyButton> in place of <Button> in your Xaml.

I can't guaranteee this will work correctly in all scenarios, but it does seem to work as required in that the visual behaviour of the button is the same when right or left clicking.

Phil
  • 42,255
  • 9
  • 100
  • 100
  • I tried this exactly, and it is not effective. If you right-click-and-hold and then drag off the button, the button raise/lower behavior is different than if you did it with left-click. – Grant Birchmeier Apr 02 '12 at 17:34
  • Where would I see the source code for ButtonBase? I was under the impression that it was closed-source. – Grant Birchmeier Apr 02 '12 at 17:35
  • Works for me in a simple test app. Use Redgate's Reflector or JetBrains DotPeek (which is free) to decompile the source code for ButtonBase (or anything else). Of if you have Resharper, just navigate straight to it. – Phil Apr 02 '12 at 17:50
  • If you right-click-drag off the button, the button raises? I literally copied your source code into my class and commented everything else out. I don't understand how we can be seeing two different things. – Grant Birchmeier Apr 02 '12 at 17:56
  • Check this out: https://skydrive.live.com/redir.aspx?cid=a7a31d7d3256ccd4&resid=A7A31D7D3256CCD4!283&parid=A7A31D7D3256CCD4!256&authkey=!AFY8siUQrJfRnhA And they're right button clicks. – Phil Apr 02 '12 at 18:14
  • I'm using VS2010 on Win 7 if that's any help. – Phil Apr 02 '12 at 18:26
  • I'm using XP. That could explain it. (And this solution will need to work for XP users.) – Grant Birchmeier Apr 02 '12 at 18:50
0

Off course you can do that, why not?

Just implement the Events for the Mouse-Up event and check there if the right mouse button has been pressed down - if true, then call your delegate/event you added before, e.g. OnRightMouseClick.

Have a look on the MSDN page of Routed Events - and yes, you're on the right track.

Greetings,

Mario Fraiß
  • 1,095
  • 7
  • 15
  • The graphical interactions are trickier than perhaps you are treating them. For instance, say I right-click the button and hold it, then I drag the mouse off the button. How can the button recognize the mouse-up if the mouse-up occurs when the mouse is outside the button? – Grant Birchmeier Mar 31 '12 at 23:56
  • (continued) Mouse.Capture doesn't seem to work to recognize the mouse-up after mouse-leave. When the button captures the mouse, this capture seems to preent the MouseEnter and MouseLeave events from working. – Grant Birchmeier Mar 31 '12 at 23:58
0

Sure, wpf lets you bind commands to controls. First wire up the window's command bindings.

<Window.CommandBindings>
    <CommandBinding Command="{x:Static custom:Window1.RightClickCommand}" 
                    Executed="YourRightClickMethodInCSharpFile" />
</Window.CommandBindings>  

Then register the for right click binding on the button

<Button Content="Ima Button!">
    <Button.InputBindings>
        <MouseBinding Command="{x:Static custom:Window1.RightClickCommand}" 
                      MouseAction="RightClick" />
    </Button.InputBindings>
</Button>       

source: WPF Command bindings - Mouse clicks

Rob Rodi
  • 3,476
  • 20
  • 19
  • Did you read my entire write-up? I can react to the right-click event, it's the graphical side effects that I'm trying to duplicate. – Grant Birchmeier Mar 31 '12 at 23:18
0

You can use PreviewMouseDown event. It would be something like this

    private void buttonTest_PreviewMouseDown(object sender, MouseButtonEventArgs e)
    {
        if (e.RightButton == MouseButtonState.Pressed)
        {
            MessageBox.Show("Right");
        }
        else if (e.LeftButton == MouseButtonState.Pressed)
        {
            MessageBox.Show("Left");
        }

        e.Handled = true;
    }
Rodrigo Vedovato
  • 1,008
  • 6
  • 11
  • Did you read my entire write-up? I can react to the right-click event, it's the graphical side effects that I'm trying to duplicate. – Grant Birchmeier Mar 31 '12 at 23:17