user3257308
user3257308

Reputation: 13

Custom WPF panel that implements IScrollInfo scrolls, but the scrollbar thumb won't move

I created a custom WPF panel that arranges items around a circle. The panel is intended to allow users to scroll, rotating all the children through the "3:00" position.

I implemented IScrollInfo, and got the panel to scroll both when the scrollbars are clicks and when the scrollwheel is turned, but the thumbs of both the horizontal and vertical scrollbars won't move. They did size according to the ratio between the viewport and extent.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Media;


namespace ListWheel
{
    public class ListWheelPanel : Panel, IScrollInfo
    {
        private Size _MaxChildSize = new(0,0);
        private int _NumberOfChildrenOnHalfCircle;
        private Point _Origin = new(0,0);
        private int topRightPosition;
        private int bottomRightPosition;

        private readonly List<Point> _Positions = new();

        public double Radius
        {
            get { return (double)GetValue(RadiusProperty); }
            set { SetValue(RadiusProperty, value); }
        }

        // Using a DependencyProperty as the backing store for Radius.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty RadiusProperty =
            DependencyProperty.Register("Radius", 
                typeof(double), 
                typeof(ListWheelPanel), 
                new FrameworkPropertyMetadata(1.0, FrameworkPropertyMetadataOptions.AffectsMeasure));


        public int PositionOffset
        {
            get { return (int)GetValue(PositionOffsetProperty); }
            set { SetValue(PositionOffsetProperty, value); }
        }

        // Using a DependencyProperty as the backing store for PositionOffset.  This enables animation, styling, binding, etc...
        public static readonly DependencyProperty PositionOffsetProperty =
            DependencyProperty.Register("PositionOffset", 
                typeof(int), 
                typeof(ListWheelPanel), 
                new FrameworkPropertyMetadata(0,
                    FrameworkPropertyMetadataOptions.AffectsArrange, OnPositionOffsetChanged, ConstrainPositionOffsetToRange));

        private static void OnPositionOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
        {
            ListWheelPanel listWheelPanel = (ListWheelPanel)d;
            listWheelPanel._ScrollViewer.InvalidateScrollInfo();    
        }

        private static object ConstrainPositionOffsetToRange(DependencyObject d, object baseValue)
        {
            ListWheelPanel listWheelPanel = (ListWheelPanel)d;
            return baseValue switch
            {
                int n when n > 0 => 0,
                int n when n <= 0 && n > 2 - listWheelPanel._Positions.Count => n,
                int n when n <= 2 - listWheelPanel._Positions.Count => 3 - listWheelPanel._Positions.Count,
                _ => (object)0,
            };
        }

        private bool _CanHorizontallyScroll = true;
        public bool CanHorizontallyScroll { get => _CanHorizontallyScroll; set => _CanHorizontallyScroll = value; }

        private bool _CanVerticallyScroll = true;
        public bool CanVerticallyScroll { get => _CanVerticallyScroll; set => _CanVerticallyScroll = value; }

        private ScrollViewer _ScrollViewer;
        public ScrollViewer ScrollOwner { get => _ScrollViewer; set => _ScrollViewer = value; }

        // Extent is the total number of children.  
        public double ExtentHeight => Children.Count;
        public double ExtentWidth => Children.Count;

        // Viewport is the number of visible children.
        private double _NumberOfVisibleChildren;
        public double ViewportHeight => _NumberOfVisibleChildren;
        public double ViewportWidth => _NumberOfVisibleChildren;

        // Offset is the position offset
        public double HorizontalOffset => PositionOffset;
        public double VerticalOffset => PositionOffset;


        // The ListWheelPanel arranges children around a circle.
        protected override Size MeasureOverride(Size availableSize)
        {
            
.
.   SKIPPING SOME CODE THAT I DON'T THINK IS PERTINENT
.

            // how many children fit in viewport?
             _NumberOfVisibleChildren = Math.Floor(viewportSize.Height / _MaxChildSize.Height);

            if (_ScrollViewer != null)
                _ScrollViewer.InvalidateScrollInfo();

            return viewportSize;
        }

        private double GetXFromY(double y, bool leftSide)
        {
            double theta = Math.Acos(y / Radius);
            double x = Radius * Math.Sin(theta);
            if (leftSide) 
                return -x; else return x;
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            for (int i = 0; i < Children.Count; i++)
            {
                UIElement child = Children[i];

                int currentPosition = (i + PositionOffset + _Positions.Count) % _Positions.Count;
                Rect newRect;
                if (currentPosition > topRightPosition && currentPosition < bottomRightPosition)
                    newRect = new Rect(_Positions[currentPosition].X - child.DesiredSize.Width, _Positions[currentPosition].Y, child.DesiredSize.Width, child.DesiredSize.Height);
                else
                    newRect = new Rect(_Positions[currentPosition].X, _Positions[currentPosition].Y, child.DesiredSize.Width, child.DesiredSize.Height);

                child.Arrange(newRect);
            }
            return finalSize;
        }

        protected override void OnRender(DrawingContext dc)
        {
            dc.DrawEllipse(Brushes.Transparent, new Pen(Brushes.Aquamarine, 1), _Origin, Radius, Radius);
            base.OnRender(dc);
        }

        public void LineDown()
        {
            SetVerticalOffset(1);
        }

        public void LineLeft()
        {
            SetHorizontalOffset(-1);
        }

        public void LineRight()
        {
            SetHorizontalOffset(1);
        }

        public Rect MakeVisible(Visual visual, Rect rectangle)
        {
            return rectangle;
        }

        public void MouseWheelDown()
        {
            SetVerticalOffset(1);
        }

        public void MouseWheelLeft()
        {
            SetHorizontalOffset(-1);
        }

        public void MouseWheelRight()
        {
            SetHorizontalOffset(1);
        }

        public void MouseWheelUp()
        {
            SetVerticalOffset(-1);
        }

        public void PageDown()
        {
            SetVerticalOffset(_NumberOfVisibleChildren - 1);
        }

        public void PageLeft()
        {
            SetHorizontalOffset(1 - _NumberOfVisibleChildren);
        }

        public void PageRight()
        {
            SetHorizontalOffset(_NumberOfVisibleChildren - 1);
        }

        public void PageUp()
        {
            SetVerticalOffset(1 - _NumberOfVisibleChildren);
        }

        public void SetHorizontalOffset(double offset)
        {
            PositionOffset -= (int)offset;
        }

        public void SetVerticalOffset(double offset)
        {
            PositionOffset -= (int)offset;
        }
    }
}

I used the new panel with the following code:

<ScrollViewer CanContentScroll="True"
              HorizontalScrollBarVisibility="Visible">

    <lw:ListWheelPanel Radius="160"
                       ClipToBounds="True">
        <lw:ListWheelPanel.Resources>
            <Style TargetType="{x:Type TextBlock}">
                <Setter Property="FontSize"
                        Value="24" />
            </Style>
        </lw:ListWheelPanel.Resources>
        <TextBlock Text="Item 1" />
        <TextBlock Text="Item 2" />
        <TextBlock Text="Item 3" />
        <TextBlock Text="Item 4" />
        <TextBlock Text="Item 5" />
        <TextBlock Text="Item 6" />
        <TextBlock Text="Item 7" />
        <TextBlock Text="Item 8" />
        <TextBlock Text="Item 9" />
        <TextBlock Text="Item 10" />
        <TextBlock Text="Item 11" />
        <TextBlock Text="Item 12" />
        <TextBlock Text="Item 13" />
        <TextBlock Text="Item 14" />
        <TextBlock Text="Item 15" />
        <TextBlock Text="Item 16" />
        <TextBlock Text="Item 17" />
        <TextBlock Text="Item 18" />
        <TextBlock Text="Item 19" />
        <TextBlock Text="Item 20" />

    </lw:ListWheelPanel>

</ScrollViewer>

Here is how it looks:

Rendered ListWheelPanel with children.

Everything works except the thumbs on the scrollbars. What gives?

Upvotes: 0

Views: 29

Answers (1)

user3257308
user3257308

Reputation: 13

I think I know what I did. I made my offset negative while I was jiggering how everything laid out. While the math worked fine to make everything move, it seemed to interfere with how the scrollbar thumb worked. When I updated my math to ensure that the offset was positive, everything started working again. FYI, here is the corrected code:

        private static object ConstrainPositionOffsetToRange(DependencyObject d, object baseValue)
        {
            ListWheelPanel listWheelPanel = (ListWheelPanel)d;
            return baseValue switch
            {
                int n when n < 0 => 0,
                int n when n >= 0 && n < listWheelPanel.Children.Count => n,
                int n when n >= listWheelPanel.Children.Count => listWheelPanel.Children.Count - 1,
                _ => (object)0,
            };
        }
.
.
.
        protected override Size ArrangeOverride(Size finalSize)
        {
            for (int i = 0; i < Children.Count; i++)
            {
                UIElement child = Children[i];

                int currentPosition = (i - PositionOffset + _Positions.Count) % _Positions.Count;
                Rect newRect;
                if (currentPosition > topRightPosition && currentPosition < bottomRightPosition)
                    newRect = new Rect(_Positions[currentPosition].X - child.DesiredSize.Width, _Positions[currentPosition].Y, child.DesiredSize.Width, child.DesiredSize.Height);
                else
                    newRect = new Rect(_Positions[currentPosition].X, _Positions[currentPosition].Y, child.DesiredSize.Width, child.DesiredSize.Height);

                child.Arrange(newRect);
            }
            return finalSize;
        }
.
.
.
        public void SetHorizontalOffset(double offset)
        {
            PositionOffset += (int)offset;
        }

        public void SetVerticalOffset(double offset)
        {
            PositionOffset += (int)offset;
        }
    }
}

Upvotes: 0

Related Questions