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

后端 未结 2 1559
心在旅途
心在旅途 2021-02-19 19:22

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

相关标签:
2条回答
  • 2021-02-19 19:30

    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".

    0 讨论(0)
  • 2021-02-19 19:49

    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.

    0 讨论(0)
提交回复
热议问题