Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How can I make WPF ScrollViewer middle-click-scroll?

Clicking the middle mouse button (aka: mouse wheel) and then moving the mouse down slightly lets users scroll in IE, and most Windows apps. This behavior appears to be missing in WPF controls by default? Is there a setting, a workaround, or something obvious that I'm missing?

like image 366
Tion Avatar asked Mar 24 '11 22:03

Tion


2 Answers

I have found how to achieve this using 3 mouse events (MouseDown, MouseUp, MouseMove). Their handlers are attached to the ScrollViewer element in the xaml below:

<Grid>
    <ScrollViewer MouseDown="ScrollViewer_MouseDown" MouseUp="ScrollViewer_MouseUp" MouseMove="ScrollViewer_MouseMove">
            <StackPanel x:Name="dynamicLongStackPanel">

            </StackPanel>
    </ScrollViewer>
    <Canvas x:Name="topLayer" IsHitTestVisible="False" />
</Grid>

It would be better to write a behaviour instead of events in code-behind, but not everyone has the necessary library, and also I don't know how to connect it with the Canvas.

The event handlers:

    private bool isMoving = false;                  //False - ignore mouse movements and don't scroll
    private bool isDeferredMovingStarted = false;   //True - Mouse down -> Mouse up without moving -> Move; False - Mouse down -> Move
    private Point? startPosition = null;
    private double slowdown = 200;                  //The number 200 is found from experiments, it should be corrected



    private void ScrollViewer_MouseDown(object sender, MouseButtonEventArgs e)
    {
        if (this.isMoving == true) //Moving with a released wheel and pressing a button
                this.CancelScrolling();
        else if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Pressed)
        {
            if (this.isMoving == false) //Pressing a wheel the first time
            {
                this.isMoving = true;
                this.startPosition = e.GetPosition(sender as IInputElement);
                this.isDeferredMovingStarted = true; //the default value is true until the opposite value is set

                this.AddScrollSign(e.GetPosition(this.topLayer).X, e.GetPosition(this.topLayer).Y);
            }
        }
    }

    private void ScrollViewer_MouseUp(object sender, MouseButtonEventArgs e)
    {
        if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Released && this.isDeferredMovingStarted != true)
            this.CancelScrolling();
    }

    private void CancelScrolling()
    {
        this.isMoving = false;
        this.startPosition = null;
        this.isDeferredMovingStarted = false;
        this.RemoveScrollSign();
    }

    private void ScrollViewer_MouseMove(object sender, MouseEventArgs e)
    {
        var sv = sender as ScrollViewer;

        if (this.isMoving && sv != null)
        {
            this.isDeferredMovingStarted = false; //standard scrolling (Mouse down -> Move)

            var currentPosition = e.GetPosition(sv);
            var offset = currentPosition - startPosition.Value;
            offset.Y /= slowdown;
            offset.X /= slowdown;

            //if(Math.Abs(offset.Y) > 25.0/slowdown)  //Some kind of a dead space, uncomment if it is neccessary
            sv.ScrollToVerticalOffset(sv.VerticalOffset + offset.Y);
            sv.ScrollToHorizontalOffset(sv.HorizontalOffset + offset.X);
        }
    }

If to remove the method calls AddScrollSign and RemoveScrollSign this example will work. But I have extended it with 2 methods which set scroll icon:

    private void AddScrollSign(double x, double y)
    {
        int size = 50;
        var img = new BitmapImage(new Uri(@"d:\middle_button_scroll.png"));
        var adorner = new Image() { Source = img, Width = size, Height = size };
        //var adorner = new Ellipse { Stroke = Brushes.Red, StrokeThickness = 2.0, Width = 20, Height = 20 };

        this.topLayer.Children.Add(adorner);
        Canvas.SetLeft(adorner, x - size / 2);
        Canvas.SetTop(adorner, y - size / 2);
    }

    private void RemoveScrollSign()
    {
        this.topLayer.Children.Clear();
    }

Example of icons: enter image description hereenter image description here

And one last remark: there are some problems with the way Press -> Immediately Release -> Move. It is supposed to cancel scrolling if a user clicks the mouse left button, or any key of keyboard, or the application looses focus. There are many events and I don't have time to handle them all.

But standard way Press -> Move -> Release works without problems.

like image 191
vortexwolf Avatar answered Nov 15 '22 07:11

vortexwolf


vorrtex posted a nice solution, please upvote him!

I do have some suggestions for his solution though, that are too lengthy to fit them all in comments, that's why I post a separate answer and direct it to him!

You mention problems with Press->Release->Move. You should use MouseCapturing to get the MouseEvents even when the Mouse is not over the ScrollViewer anymore. I have not tested it, but I guess your solution also fails in Press->Move->Move outside of ScrollViewer->Release, Mousecapturing will take care of that too.

Also you mention using a Behavior. I'd rather suggest an attached behavior that doesn't need extra dependencies.

You should definately not use an extra Canvas but do this in an Adorner.

The ScrollViewer itsself hosts a ScrollContentPresenter that defines an AdornerLayer. You should insert the Adorner there. This removes the need for any further dependency and also keeps the attached behavior as simple as IsMiddleScrollable="true".

like image 2
Markus Hütter Avatar answered Nov 15 '22 08:11

Markus Hütter