WPF .NET 5.0中如何实现弧形ListBox与弧形滚动条并解决项重叠问题
Hey there! Let's tackle your WPF curved ListBox issues one by one—first the overlapping items, then the curved scrolling.
Fixing Overlapping Items in CurvedPanel
Your current ArrangeOverride uses a fixed 20-degree decrement for each item, but it doesn't account for the actual width of each child element. That's why they're overlapping. Here's how to adjust the panel to calculate spacing based on item size:
Updated CurvedPanel Code
public class CurvedPanel : StackPanel { public static readonly DependencyProperty RadiusXProperty = DependencyProperty.Register(nameof(RadiusX), typeof(double), typeof(CurvedPanel), new PropertyMetadata(70.0, OnRadiusChanged)); public double RadiusX { get => (double)GetValue(RadiusXProperty); set => SetValue(RadiusXProperty, value); } private static void OnRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((CurvedPanel)d).InvalidateArrange(); } protected override Size MeasureOverride(Size availableSize) { // Measure all children first to get their actual desired sizes foreach (UIElement child in Children) { child.Measure(availableSize); } return base.MeasureOverride(availableSize); } protected override Size ArrangeOverride(Size finalSize) { if (Children.Count == 0) return finalSize; double centerY = finalSize.Height / 2; double radiusY = centerY * 0.8; double totalAngle = 0; // Calculate total angular space needed for all items foreach (UIElement child in Children) { // Convert item width to angle (arc length = radius * radians) double angleInRadians = child.DesiredSize.Width / RadiusX; totalAngle += angleInRadians * (180 / Math.PI); // Convert to degrees } // Start angle to center the curve vertically double currentAngle = 90 + (totalAngle / 2); double centerX = finalSize.Width; // Align to right edge for left panel foreach (UIElement child in Children) { double itemWidthAngle = (child.DesiredSize.Width / RadiusX) * (180 / Math.PI); // Position item using the middle of its angular space double middleAngle = currentAngle - (itemWidthAngle / 2); double angleInRadians = AngleToRadian(middleAngle); Point childPosition = new Point( centerX - RadiusX * Math.Cos(angleInRadians), centerY - radiusY * Math.Sin(angleInRadians) - (child.DesiredSize.Height / 2) ); child.Arrange(new Rect(childPosition, child.DesiredSize)); currentAngle -= itemWidthAngle; } return finalSize; } private double AngleToRadian(double angle) { return angle * (Math.PI / 180); } }
Key improvements:
- Added a
DependencyPropertyforRadiusXso changes trigger layout updates - Calculates exact angular space needed per item based on its width
- Centers items vertically on the curve and avoids overlap by using item-specific angles
Implementing Curved Scrolling
Since PathListBox isn't available in .NET Core, we'll extend the CurvedPanel to support scroll offsets, then bind it to a ScrollViewer's position.
Updated CurvedPanel with Scroll Support
public class CurvedPanel : StackPanel { public static readonly DependencyProperty RadiusXProperty = DependencyProperty.Register(nameof(RadiusX), typeof(double), typeof(CurvedPanel), new PropertyMetadata(70.0, OnRadiusChanged)); public static readonly DependencyProperty ScrollOffsetProperty = DependencyProperty.Register(nameof(ScrollOffset), typeof(double), typeof(CurvedPanel), new PropertyMetadata(0.0, OnScrollOffsetChanged)); public double RadiusX { get => (double)GetValue(RadiusXProperty); set => SetValue(RadiusXProperty, value); } public double ScrollOffset { get => (double)GetValue(ScrollOffsetProperty); set => SetValue(ScrollOffsetProperty, value); } private static void OnRadiusChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((CurvedPanel)d).InvalidateArrange(); } private static void OnScrollOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e) { ((CurvedPanel)d).InvalidateArrange(); } protected override Size MeasureOverride(Size availableSize) { foreach (UIElement child in Children) { child.Measure(availableSize); } return base.MeasureOverride(availableSize); } protected override Size ArrangeOverride(Size finalSize) { if (Children.Count == 0) return finalSize; double centerY = finalSize.Height / 2; double radiusY = centerY * 0.8; double totalAngle = 0; foreach (UIElement child in Children) { double angleInRadians = child.DesiredSize.Width / RadiusX; totalAngle += angleInRadians * (180 / Math.PI); } // Adjust starting angle by scroll offset double currentAngle = 90 + (totalAngle / 2) + ScrollOffset; double centerX = finalSize.Width; foreach (UIElement child in Children) { double itemWidthAngle = (child.DesiredSize.Width / RadiusX) * (180 / Math.PI); double middleAngle = currentAngle - (itemWidthAngle / 2); double angleInRadians = AngleToRadian(middleAngle); Point childPosition = new Point( centerX - RadiusX * Math.Cos(angleInRadians), centerY - radiusY * Math.Sin(angleInRadians) - (child.DesiredSize.Height / 2) ); // Hide items outside the panel bounds if (childPosition.Y + child.DesiredSize.Height > 0 && childPosition.Y < finalSize.Height) { child.Arrange(new Rect(childPosition, child.DesiredSize)); } else { child.Arrange(new Rect(0, -1000, 0, 0)); } currentAngle -= itemWidthAngle; } return finalSize; } private double AngleToRadian(double angle) { return angle * (Math.PI / 180); } }
Update XAML and Add Scroll Handling
Wrap your ListView in a ScrollViewer and add a scroll changed handler:
<ScrollViewer Grid.Column="0" ScrollChanged="LeftMenuScrollChanged" VerticalScrollBarVisibility="Auto" Height="360"> <ListView ItemsSource="{Binding LeftMenuItems}" FlowDirection="RightToLeft"> <ListView.ItemsPanel> <ItemsPanelTemplate> <cc:CurvedPanel x:Name="LeftCurvedPanel" RadiusX="70"/> </ItemsPanelTemplate> </ListView.ItemsPanel> <ListView.ItemTemplate> <DataTemplate> <TextBlock Text="{Binding Text}" Style="{StaticResource MenuItem}" /> </DataTemplate> </ListView.ItemTemplate> </ListView> </ScrollViewer>
Add this code-behind to sync scroll position with panel offset:
private void LeftMenuScrollChanged(object sender, ScrollChangedEventArgs e) { ScrollViewer scrollViewer = sender as ScrollViewer; if (scrollViewer == null || LeftCurvedPanel == null) return; // Calculate total scrollable angle range double totalAngle = 0; foreach (UIElement child in LeftCurvedPanel.Children) { totalAngle += (child.DesiredSize.Width / LeftCurvedPanel.RadiusX) * (180 / Math.PI); } double visibleAngle = 80; // Adjust to match your visible curve area // Convert scroll position to angle offset double scrollRatio = e.VerticalOffset / (scrollViewer.ScrollableHeight); LeftCurvedPanel.ScrollOffset = scrollRatio * (totalAngle - visibleAngle); }
Quick Tips for the Right Panel
For the right-side ListView, adjust the X position calculation in ArrangeOverride to align items to the left edge:
Point childPosition = new Point( RadiusX * Math.Cos(angleInRadians), // Remove centerX subtraction centerY - radiusY * Math.Sin(angleInRadians) - (child.DesiredSize.Height / 2) );
内容的提问来源于stack exchange,提问作者proDeveloperGuru

