You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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 DependencyProperty for RadiusX so 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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.04.30 13:54:08