目前Avalonia暂时没有内置RangeSlider,自己实现一个。由于官方的刻度,没有自己画出来,我们单独画出来就行。自行多暴露官方Slider的属性。

RangeSlider类 - 特殊版

    public class RangeSlider : ContentControl
    {
        private readonly Slider slider;
        private readonly Rectangle selectionRect;
        private readonly Grid grid;
        private readonly Canvas ticksCanvas;
        private Track? track;

        private bool isDragging = false;
        private Point dragStartPoint;
        private double initialStart;
        private double initialEnd;

        public static readonly StyledProperty<double> ValueProperty =
            AvaloniaProperty.Register<RangeSlider, double>(nameof(Value), 0);

        public double Value
        {
            get => GetValue(ValueProperty);
            set => SetValue(ValueProperty, value);
        }

        public static readonly StyledProperty<double> MinimumProperty =
            AvaloniaProperty.Register<RangeSlider, double>(nameof(Minimum), 0);

        public double Minimum
        {
            get => GetValue(MinimumProperty);
            set => SetValue(MinimumProperty, value);
        }

        public static readonly StyledProperty<double> MaximumProperty =
            AvaloniaProperty.Register<RangeSlider, double>(nameof(Maximum), 100);

        public double Maximum
        {
            get => GetValue(MaximumProperty);
            set => SetValue(MaximumProperty, value);
        }

        public static readonly StyledProperty<double> SelectionStartProperty =
            AvaloniaProperty.Register<RangeSlider, double>(nameof(SelectionStart), 0);

        public double SelectionStart
        {
            get => GetValue(SelectionStartProperty);
            set => SetValue(SelectionStartProperty, value);
        }

        public static readonly StyledProperty<double> SelectionEndProperty =
            AvaloniaProperty.Register<RangeSlider, double>(nameof(SelectionEnd), 0);

        public double SelectionEnd
        {
            get => GetValue(SelectionEndProperty);
            set => SetValue(SelectionEndProperty, value);
        }

        public static readonly StyledProperty<double> TickFrequencyProperty =
            AvaloniaProperty.Register<RangeSlider, double>(nameof(TickFrequency), 1);

        public double TickFrequency
        {
            get => GetValue(TickFrequencyProperty);
            set => SetValue(TickFrequencyProperty, value);
        }

        public static readonly StyledProperty<TickPlacement> TickPlacementProperty =
            AvaloniaProperty.Register<RangeSlider, TickPlacement>(nameof(TickPlacement), TickPlacement.None);

        public TickPlacement TickPlacement
        {
            get => GetValue(TickPlacementProperty);
            set => SetValue(TickPlacementProperty, value);
        }

        public static readonly StyledProperty<Orientation> OrientationProperty =
            AvaloniaProperty.Register<RangeSlider, Orientation>(nameof(Orientation), Orientation.Horizontal);

        public Orientation Orientation
        {
            get => GetValue(OrientationProperty);
            set => SetValue(OrientationProperty, value);
        }

        public static readonly StyledProperty<bool> IsDirectionReversedProperty =
            AvaloniaProperty.Register<RangeSlider, bool>(nameof(IsDirectionReversed), false);

        public bool IsDirectionReversed
        {
            get => GetValue(IsDirectionReversedProperty);
            set => SetValue(IsDirectionReversedProperty, value);
        }

        public static readonly StyledProperty<bool> IsSnapToTickEnabledProperty =
            AvaloniaProperty.Register<RangeSlider, bool>(nameof(IsSnapToTickEnabled), false);

        public bool IsSnapToTickEnabled
        {
            get => GetValue(IsSnapToTickEnabledProperty);
            set => SetValue(IsSnapToTickEnabledProperty, value);
        }

        public static readonly StyledProperty<double> MinSelectionLengthProperty =
            AvaloniaProperty.Register<RangeSlider, double>(nameof(MinSelectionLength), 0);

        public double MinSelectionLength
        {
            get => GetValue(MinSelectionLengthProperty);
            set => SetValue(MinSelectionLengthProperty, value);
        }

        public static readonly StyledProperty<double> MaxSelectionLengthProperty =
            AvaloniaProperty.Register<RangeSlider, double>(nameof(MaxSelectionLength), double.PositiveInfinity);

        public double MaxSelectionLength
        {
            get => GetValue(MaxSelectionLengthProperty);
            set => SetValue(MaxSelectionLengthProperty, value);
        }

        public static readonly StyledProperty<IBrush> SelectionFillProperty =
            AvaloniaProperty.Register<RangeSlider, IBrush>(nameof(SelectionFill), Brushes.LightBlue);

        public IBrush SelectionFill
        {
            get => GetValue(SelectionFillProperty);
            set => SetValue(SelectionFillProperty, value);
        }

        public RangeSlider()
        {
            grid = new Grid();
            Content = grid;

            ticksCanvas = new Canvas
            {
                ZIndex = 0,
                IsHitTestVisible = false
            };

            selectionRect = new Rectangle
            {
                ZIndex = 1,
                Fill = SelectionFill,
                VerticalAlignment = VerticalAlignment.Stretch,
                HorizontalAlignment = HorizontalAlignment.Left,
                RadiusX = 2,
                RadiusY = 2,
            };

            slider = new Slider
            {
                VerticalAlignment = VerticalAlignment.Stretch,
                HorizontalAlignment = HorizontalAlignment.Stretch,
            };

            grid.Children.Add(ticksCanvas);
            grid.Children.Add(selectionRect);
            grid.Children.Add(slider);

            slider[!!Slider.ValueProperty] = this[!!ValueProperty];
            slider[!!Slider.MinimumProperty] = this[!!MinimumProperty];
            slider[!!Slider.MaximumProperty] = this[!!MaximumProperty];
            slider[!!Slider.TickFrequencyProperty] = this[!!TickFrequencyProperty];
            slider[!!Slider.TickPlacementProperty] = this[!!TickPlacementProperty];
            slider[!!Slider.OrientationProperty] = this[!!OrientationProperty];
            slider[!!Slider.IsDirectionReversedProperty] = this[!!IsDirectionReversedProperty];
            slider[!!Slider.IsSnapToTickEnabledProperty] = this[!!IsSnapToTickEnabledProperty];

            this.GetObservable(SelectionStartProperty).Subscribe(_ => UpdateSelectionRect());
            this.GetObservable(SelectionEndProperty).Subscribe(_ => UpdateSelectionRect());
            this.GetObservable(SelectionFillProperty).Subscribe(_ => selectionRect.Fill = SelectionFill);
            slider.GetObservable(Slider.OrientationProperty).Subscribe(_ => { UpdateSelectionRect(); UpdateTicks(); });
            slider.GetObservable(Slider.ValueProperty).Subscribe(_ => UpdateSelectionRectZIndex());
            slider.GetObservable(Slider.TickFrequencyProperty).Subscribe(_ => UpdateTicks());
            slider.GetObservable(Slider.TickPlacementProperty).Subscribe(_ => UpdateTicks());
            slider.GetObservable(Slider.BoundsProperty).Subscribe(_ =>
            {
                UpdateSelectionRect();
                UpdateTicks();
            });

            selectionRect.PointerPressed += SelectionRect_PointerPressed;
            selectionRect.PointerMoved += SelectionRect_PointerMoved;
            selectionRect.PointerReleased += SelectionRect_PointerReleased;
        }

        protected override void OnLoaded(RoutedEventArgs e)
        {
            base.OnLoaded(e);
            track = slider.GetPropertyValue<Track?>("Track");
            UpdateSelectionRect();
            UpdateTicks();
        }

        private double ClampToRange(double value) =>
            Math.Clamp(value, slider.Minimum, slider.Maximum);

        private double SnapToTick(double value)
        {
            if (!IsSnapToTickEnabled || TickFrequency <= 0) return value;
            return Math.Round(value / TickFrequency) * TickFrequency;
        }

        private (double Start, double End) ClampSelection(double start, double end)
        {
            if (start > end) (start, end) = (end, start);

            double range = end - start;
            range = Math.Clamp(range, MinSelectionLength, MaxSelectionLength);
            start = ClampToRange(start);
            end = start + range;

            if (end > slider.Maximum)
            {
                end = slider.Maximum;
                start = end - range;
            }
            return (start, end);
        }

        private (double Start, double End) SnapSelection(double start, double end)
        {
            if (!IsSnapToTickEnabled || TickFrequency <= 0)
                return (start, end);

            double snappedStart = SnapToTick(start);
            double snappedEnd = SnapToTick(end);
            return (snappedStart, snappedEnd);
        }

        private void ApplySelection(double start, double end)
        {
            (start, end) = ClampSelection(start, end);
            (start, end) = SnapSelection(start, end);
            SelectionStart = start;
            SelectionEnd = end;
        }

        private void SelectionRect_PointerPressed(object? sender, PointerPressedEventArgs e)
        {
            if (e.GetCurrentPoint(selectionRect).Properties.IsLeftButtonPressed)
            {
                isDragging = true;
                dragStartPoint = e.GetPosition(slider);
                initialStart = SelectionStart;
                initialEnd = SelectionEnd;
                e.Pointer.Capture(selectionRect);
                e.Handled = true;
            }
        }

        private void SelectionRect_PointerMoved(object? sender, PointerEventArgs e)
        {
            if (!isDragging)
                return;

            var pos = e.GetPosition(slider);
            double deltaPx = slider.Orientation == Orientation.Horizontal
                ? pos.X - dragStartPoint.X
                : pos.Y - dragStartPoint.Y;

            double totalPx = slider.Orientation == Orientation.Horizontal
                ? slider.Bounds.Width
                : slider.Bounds.Height;

            double valueDelta = deltaPx / totalPx * (slider.Maximum - slider.Minimum);
            ApplySelection(initialStart + valueDelta, initialEnd + valueDelta);
            e.Handled = true;
        }

        private void SelectionRect_PointerReleased(object? sender, PointerReleasedEventArgs e)
        {
            if (isDragging)
            {
                isDragging = false;
                e.Pointer.Capture(null);
                e.Handled = true;
            }
        }

        private void UpdateSelectionRectZIndex()
        {
            double min = slider.Minimum;
            double max = slider.Maximum;
            double start = Math.Clamp(SelectionStart, min, max);
            double end = Math.Clamp(SelectionEnd, min, max);
            if (start > end) (start, end) = (end, start);

            double current = slider.Value;
            selectionRect.SetZIndex(current >= start && current <= end ? 0 : 1);
        }

        private void UpdateSelectionRect()
        {
            if (slider.Bounds.Width <= 0 || slider.Bounds.Height <= 0)
                return;

            double min = slider.Minimum;
            double max = slider.Maximum;
            double total = max - min;
            if (total <= 0)
                return;

            double start = Math.Clamp(SelectionStart, min, max);
            double end = Math.Clamp(SelectionEnd, min, max);
            if (start > end) (start, end) = (end, start);

            UpdateSelectionRectZIndex();

            var (thumbWidth, thumbHeight) = (track?.Thumb != null)
                ? (track.Thumb.Bounds.Width / 2, track.Thumb.Bounds.Height / 2)
                : (0.0, 0.0);

            double width = slider.Bounds.Width;
            double height = slider.Bounds.Height;
            bool isReversed = slider.IsDirectionReversed;

            double availableLength = slider.Orientation == Orientation.Horizontal
                ? Math.Max(0, width - thumbWidth)
                : Math.Max(0, height - thumbHeight);

            double startRatio = (start - min) / total;
            double endRatio = (end - min) / total;

            double startPx = isReversed
                ? availableLength - endRatio * availableLength
                : startRatio * availableLength;

            double endPx = isReversed
                ? availableLength - startRatio * availableLength
                : endRatio * availableLength;

            startPx += slider.Orientation == Orientation.Horizontal ? thumbWidth / 2 : thumbHeight / 2;
            endPx += slider.Orientation == Orientation.Horizontal ? thumbWidth / 2 : thumbHeight / 2;

            double rangePx = Math.Abs(endPx - startPx);

            if (slider.Orientation == Orientation.Horizontal)
            {
                selectionRect.Width = rangePx;
                selectionRect.Height = height * 0.4;
                selectionRect.Margin = new Thickness(Math.Min(startPx, endPx), height * 0.3, 0, 0);
                selectionRect.HorizontalAlignment = HorizontalAlignment.Left;
                selectionRect.VerticalAlignment = VerticalAlignment.Top;
            }
            else
            {
                selectionRect.Height = rangePx;
                selectionRect.Width = width * 0.4;
                selectionRect.Margin = new Thickness(width * 0.3, Math.Min(startPx, endPx), 0, 0);
                selectionRect.VerticalAlignment = VerticalAlignment.Top;
                selectionRect.HorizontalAlignment = HorizontalAlignment.Left;
            }
        }

        private void UpdateTicks()
        {
            ticksCanvas.Children.Clear();

            if (TickPlacement == TickPlacement.None || TickFrequency <= 0)
                return;

            double min = slider.Minimum;
            double max = slider.Maximum;
            double total = max - min;
            if (total <= 0)
                return;

            double step = TickFrequency;
            double width = slider.Bounds.Width;
            double height = slider.Bounds.Height;
            double tickSize = 4;
            bool isReversed = slider.IsDirectionReversed;

            var (thumbHalfWidth, thumbHalfHeight) = (track?.Thumb != null)
                ? (track.Thumb.Bounds.Width / 2, track.Thumb.Bounds.Height / 2)
                : (0.0, 0.0);

            double availableLength = slider.Orientation == Orientation.Horizontal
                ? Math.Max(0, width - 2 * thumbHalfWidth)
                : Math.Max(0, height - 2 * thumbHalfHeight);

            for (double v = min; v <= max + 0.0001; v += step)
            {
                double offset = (v - min) / total;
                double pos = isReversed
                    ? availableLength - offset * availableLength
                    : offset * availableLength;

                pos += slider.Orientation == Orientation.Horizontal ? thumbHalfWidth : thumbHalfHeight;

                pos = Math.Clamp(pos,
                    slider.Orientation == Orientation.Horizontal ? thumbHalfWidth : thumbHalfHeight,
                    slider.Orientation == Orientation.Horizontal ? width - thumbHalfWidth : height - thumbHalfHeight);

                switch (TickPlacement)
                {
                    case TickPlacement.TopLeft:
                        if (slider.Orientation == Orientation.Horizontal)
                        {
                            ticksCanvas.Children.Add(new Line
                            {
                                StartPoint = new Point(pos, 0),
                                EndPoint = new Point(pos, tickSize),
                                Stroke = Brushes.Gray,
                                StrokeThickness = 1
                            });
                        }
                        else
                        {
                            ticksCanvas.Children.Add(new Line
                            {
                                StartPoint = new Point(0, pos),
                                EndPoint = new Point(tickSize, pos),
                                Stroke = Brushes.Gray,
                                StrokeThickness = 1
                            });
                        }
                        break;

                    case TickPlacement.BottomRight:
                        if (slider.Orientation == Orientation.Horizontal)
                        {
                            ticksCanvas.Children.Add(new Line
                            {
                                StartPoint = new Point(pos, height - tickSize),
                                EndPoint = new Point(pos, height),
                                Stroke = Brushes.Gray,
                                StrokeThickness = 1
                            });
                        }
                        else
                        {
                            ticksCanvas.Children.Add(new Line
                            {
                                StartPoint = new Point(width - tickSize, pos),
                                EndPoint = new Point(width, pos),
                                Stroke = Brushes.Gray,
                                StrokeThickness = 1
                            });
                        }
                        break;

                    case TickPlacement.Outside:
                        if (slider.Orientation == Orientation.Horizontal)
                        {
                            ticksCanvas.Children.Add(new Line
                            {
                                StartPoint = new Point(pos, 0),
                                EndPoint = new Point(pos, tickSize),
                                Stroke = Brushes.Gray,
                                StrokeThickness = 1
                            });
                            ticksCanvas.Children.Add(new Line
                            {
                                StartPoint = new Point(pos, height - tickSize),
                                EndPoint = new Point(pos, height),
                                Stroke = Brushes.Gray,
                                StrokeThickness = 1
                            });
                        }
                        else
                        {
                            ticksCanvas.Children.Add(new Line
                            {
                                StartPoint = new Point(0, pos),
                                EndPoint = new Point(tickSize, pos),
                                Stroke = Brushes.Gray,
                                StrokeThickness = 1
                            });
                            ticksCanvas.Children.Add(new Line
                            {
                                StartPoint = new Point(width - tickSize, pos),
                                EndPoint = new Point(width, pos),
                                Stroke = Brushes.Gray,
                                StrokeThickness = 1
                            });
                        }
                        break;
                }
            }
        }
    }

RangeSlider类 - 双滑块版

    public class RangeSlider : ContentControl
    {
        // 依赖属性定义
        public static readonly StyledProperty<double> MinimumProperty =
            RangeBase.MinimumProperty.AddOwner<RangeSlider>();
        public static readonly StyledProperty<double> MaximumProperty =
            RangeBase.MaximumProperty.AddOwner<RangeSlider>();
        public static readonly StyledProperty<double> SelectionStartProperty =
            AvaloniaProperty.Register<RangeSlider, double>(nameof(SelectionStart), 0.0, defaultBindingMode: BindingMode.TwoWay);
        public static readonly StyledProperty<double> SelectionEndProperty =
            AvaloniaProperty.Register<RangeSlider, double>(nameof(SelectionEnd), 0.0, defaultBindingMode: BindingMode.TwoWay);
        public static readonly StyledProperty<Orientation> OrientationProperty =
            AvaloniaProperty.Register<RangeSlider, Orientation>(nameof(Orientation), Orientation.Horizontal);
        public static readonly StyledProperty<IBrush> SelectionFillProperty =
            AvaloniaProperty.Register<RangeSlider, IBrush>(nameof(SelectionFill), Brushes.DodgerBlue);
        public static readonly StyledProperty<IBrush> TrackFillProperty =
            AvaloniaProperty.Register<RangeSlider, IBrush>(nameof(TrackFill), Brushes.LightGray);
        public static readonly StyledProperty<double> ThumbSizeProperty =
            AvaloniaProperty.Register<RangeSlider, double>(nameof(ThumbSize), 14.0);

        // 新增刻度相关属性
        public static readonly StyledProperty<double[]> TicksProperty =
            AvaloniaProperty.Register<RangeSlider, double[]>(nameof(Ticks), Array.Empty<double>());
        public static readonly StyledProperty<double> TickFrequencyProperty =
            AvaloniaProperty.Register<RangeSlider, double>(nameof(TickFrequency), 0d);
        public static readonly StyledProperty<TickPlacement> TickPlacementProperty =
            AvaloniaProperty.Register<RangeSlider, TickPlacement>(nameof(TickPlacement), TickPlacement.None);
        public static readonly StyledProperty<bool> IsSnapToTickEnabledProperty =
            AvaloniaProperty.Register<RangeSlider, bool>(nameof(IsSnapToTickEnabled), false);
        public static readonly StyledProperty<double> TickLengthProperty =
            AvaloniaProperty.Register<RangeSlider, double>(nameof(TickLength), 7d);

        // 属性封装,表达式体简写
        public double Minimum { get => GetValue(MinimumProperty); set => SetValue(MinimumProperty, value); }
        public double Maximum { get => GetValue(MaximumProperty); set => SetValue(MaximumProperty, value); }
        public double SelectionStart { get => GetValue(SelectionStartProperty); set => SetValue(SelectionStartProperty, value); }
        public double SelectionEnd { get => GetValue(SelectionEndProperty); set => SetValue(SelectionEndProperty, value); }
        public Orientation Orientation { get => GetValue(OrientationProperty); set => SetValue(OrientationProperty, value); }
        public IBrush SelectionFill { get => GetValue(SelectionFillProperty); set => SetValue(SelectionFillProperty, value); }
        public IBrush TrackFill { get => GetValue(TrackFillProperty); set => SetValue(TrackFillProperty, value); }
        public double ThumbSize { get => GetValue(ThumbSizeProperty); set => SetValue(ThumbSizeProperty, value); }
        public double[] Ticks { get => GetValue(TicksProperty); set => SetValue(TicksProperty, value); }
        public double TickFrequency { get => GetValue(TickFrequencyProperty); set => SetValue(TickFrequencyProperty, value); }
        public TickPlacement TickPlacement { get => GetValue(TickPlacementProperty); set => SetValue(TickPlacementProperty, value); }
        public bool IsSnapToTickEnabled { get => GetValue(IsSnapToTickEnabledProperty); set => SetValue(IsSnapToTickEnabledProperty, value); }
        public double TickLength { get => GetValue(TickLengthProperty); set => SetValue(TickLengthProperty, value); }

        // 控件部件
        private Canvas? layoutCanvas;
        private Border? trackBackground;
        private Border? selectionRange;
        private Thumb? startThumb;
        private Thumb? endThumb;
        private Canvas? tickCanvas;
        private bool isDraggingStart;
        private bool isDraggingEnd;

        static RangeSlider()
        {
            TemplateProperty.OverrideDefaultValue<RangeSlider>(CreateControlTemplate());
        }

        public RangeSlider()
        {
            this.GetObservable(OrientationProperty).Subscribe(_ => UpdateLayout());
            this.GetObservable(SelectionStartProperty).Subscribe(_ => UpdateLayout());
            this.GetObservable(SelectionEndProperty).Subscribe(_ => UpdateLayout());
            this.GetObservable(MinimumProperty).Subscribe(_ => UpdateLayout());
            this.GetObservable(MaximumProperty).Subscribe(_ => UpdateLayout());
        }

        protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
        {
            base.OnApplyTemplate(e);
            layoutCanvas = e.NameScope.Find<Canvas>("PART_LayoutCanvas");
            trackBackground = e.NameScope.Find<Border>("PART_TrackBackground");
            selectionRange = e.NameScope.Find<Border>("PART_SelectionRange");
            startThumb = e.NameScope.Find<Thumb>("PART_StartThumb");
            endThumb = e.NameScope.Find<Thumb>("PART_EndThumb");
            tickCanvas = e.NameScope.Find<Canvas>("PART_TickCanvas");

            if (startThumb is not null)
            {
                startThumb.DragStarted += (_, __) => isDraggingStart = true;
                startThumb.DragDelta += OnThumbDragDelta;
                startThumb.DragCompleted += (_, __) => isDraggingStart = false;
            }
            if (endThumb is not null)
            {
                endThumb.DragStarted += (_, __) => isDraggingEnd = true;
                endThumb.DragDelta += OnThumbDragDelta;
                endThumb.DragCompleted += (_, __) => isDraggingEnd = false;
            }

            UpdateLayout();
        }

        protected override Size MeasureOverride(Size availableSize)
        {
            double thumb = ThumbSize;
            if (Orientation == Orientation.Horizontal)
            {
                double width = double.IsInfinity(availableSize.Width) ? 100 : availableSize.Width;
                return new Size(Math.Max(width, thumb), thumb);
            }
            else
            {
                double height = double.IsInfinity(availableSize.Height) ? 100 : availableSize.Height;
                return new Size(thumb, Math.Max(height, thumb));
            }
        }

        protected override Size ArrangeOverride(Size finalSize)
        {
            var result = base.ArrangeOverride(finalSize);
            UpdateLayout();
            return result;
        }

        private new void UpdateLayout()
        {
            if (layoutCanvas is null || trackBackground is null || selectionRange is null || startThumb is null || endThumb is null)
                return;

            double min = Minimum;
            double max = Maximum;
            double start = Math.Clamp(SelectionStart, min, max);
            double end = Math.Clamp(SelectionEnd, min, max);
            if (start > end) (start, end) = (end, start);

            double range = max - min;
            if (range <= 0) range = 1;
            double thumbSize = ThumbSize;
            double halfThumb = thumbSize / 2;

            if (Orientation == Orientation.Horizontal)
            {
                double canvasWidth = layoutCanvas.Bounds.Width;
                double trackWidth = canvasWidth - thumbSize;
                double startPos = ((start - min) / range) * trackWidth;
                double endPos = ((end - min) / range) * trackWidth;

                Canvas.SetLeft(startThumb, startPos);
                Canvas.SetLeft(endThumb, endPos);

                Canvas.SetLeft(selectionRange, startPos + halfThumb);
                selectionRange.Width = Math.Max(0, endPos - startPos);

                trackBackground.Height = thumbSize / 2;
                selectionRange.Height = thumbSize / 2;
                Canvas.SetTop(selectionRange, thumbSize / 4);
                Canvas.SetTop(trackBackground, thumbSize / 4);
            }
            else
            {
                double canvasHeight = layoutCanvas.Bounds.Height;
                double trackHeight = canvasHeight - thumbSize;
                double startPos = trackHeight - ((start - min) / range) * trackHeight;
                double endPos = trackHeight - ((end - min) / range) * trackHeight;

                Canvas.SetTop(startThumb, startPos);
                Canvas.SetTop(endThumb, endPos);

                Canvas.SetTop(selectionRange, endPos + halfThumb);
                selectionRange.Height = Math.Max(0, startPos - endPos);

                trackBackground.Width = thumbSize / 2;
                selectionRange.Width = thumbSize / 2;
                Canvas.SetLeft(selectionRange, thumbSize / 4);
                Canvas.SetLeft(trackBackground, thumbSize / 4);
            }

            DrawTicks();
        }

        private void OnThumbDragDelta(object? sender, VectorEventArgs e)
        {
            double min = Minimum;
            double max = Maximum;
            double range = max - min;
            if (range <= 0) return;

            double delta, canvasSize;
            double thumbSize = ThumbSize;

            if (Orientation == Orientation.Horizontal)
            {
                delta = e.Vector.X;
                canvasSize = layoutCanvas!.Bounds.Width - thumbSize;
            }
            else
            {
                delta = -e.Vector.Y;
                canvasSize = layoutCanvas!.Bounds.Height - thumbSize;
            }

            double valueDelta = (delta / canvasSize) * range;

            if (isDraggingStart)
            {
                double candidate = SelectionStart + valueDelta;
                candidate = SnapIfNeeded(candidate);
                candidate = Math.Clamp(candidate, min, SelectionEnd);
                SelectionStart = candidate;
            }
            else if (isDraggingEnd)
            {
                double candidate = SelectionEnd + valueDelta;
                candidate = SnapIfNeeded(candidate);
                candidate = Math.Clamp(candidate, SelectionStart, max);
                SelectionEnd = candidate;
            }
        }
        private double SnapIfNeeded(double value)
        {
            if (!IsSnapToTickEnabled) return value;

            var allTicks = new List<double>();
            if (Ticks?.Length > 0)
                allTicks.AddRange(Ticks);
            if (TickFrequency > 0)
            {
                for (double t = Minimum; t <= Maximum; t += TickFrequency)
                    allTicks.Add(t);
            }
            if (allTicks.Count == 0) return value;

            return allTicks.OrderBy(t => Math.Abs(t - value)).First();
        }

        private void DrawTicks()
        {
            if (tickCanvas == null || TickPlacement == TickPlacement.None) return;
            if (layoutCanvas == null) return;

            tickCanvas.Children.Clear();

            double min = Minimum, max = Maximum, range = max - min;
            if (range <= 0) return;

            double canvasSize = Orientation == Orientation.Horizontal ? layoutCanvas.Bounds.Width : layoutCanvas.Bounds.Height;
            if (canvasSize <= 0) return;

            const double tickMargin = 4;
            double tickLength = TickLength;

            var ticks = new List<double>();
            if (Ticks != null && Ticks.Length > 0)
                ticks.AddRange(Ticks.Where(t => t >= min && t <= max));
            if (TickFrequency > 0)
            {
                for (double t = min; t <= max; t += TickFrequency)
                    if (!ticks.Contains(t)) ticks.Add(t);
            }
            ticks.Sort();

            foreach (var tick in ticks)
            {
                // 统一计算刻度位置和方向相关参数
                double posRatio = (tick - min) / range;
                if (posRatio < 0) posRatio = 0;
                if (posRatio > 1) posRatio = 1;

                if (Orientation == Orientation.Horizontal)
                {
                    double horizontalMaxPos = canvasSize - 1;
                    double pos = posRatio * horizontalMaxPos;

                    double visibleTickLength = tickLength - tickMargin;
                    if (visibleTickLength < 0) visibleTickLength = 0;

                    // 创建上方刻度线
                    Border topLine = new Border { Background = Brushes.Black, Width = 1, Height = tickLength };
                    Canvas.SetLeft(topLine, pos);

                    // 创建下方刻度线(根据情况)
                    Border bottomLine = new Border { Background = Brushes.Black, Width = 1, Height = visibleTickLength };
                    Canvas.SetLeft(bottomLine, pos);

                    switch (TickPlacement)
                    {
                        case TickPlacement.TopLeft:
                            Canvas.SetTop(topLine, -tickMargin);
                            tickCanvas.Children.Add(topLine);
                            break;
                        case TickPlacement.BottomRight:
                            Canvas.SetTop(bottomLine, ThumbSize - visibleTickLength);
                            tickCanvas.Children.Add(bottomLine);
                            break;
                        case TickPlacement.Outside:
                            Canvas.SetTop(topLine, -tickMargin);
                            Canvas.SetTop(bottomLine, ThumbSize - visibleTickLength);
                            tickCanvas.Children.Add(topLine);
                            tickCanvas.Children.Add(bottomLine);
                            break;
                    }
                }
                else // 竖直方向
                {
                    double verticalMaxPos = canvasSize - 1;
                    double pos = posRatio * verticalMaxPos;
                    double topPos = verticalMaxPos - pos;
                    if (topPos < 0) topPos = 0;

                    double leftLineVisibleWidth = tickLength;
                    double leftLineLeftPos = 0 - tickMargin;

                    if (leftLineLeftPos < 0)
                    {
                        leftLineVisibleWidth += leftLineLeftPos;
                        if (leftLineVisibleWidth < 0) leftLineVisibleWidth = 0;
                    }

                    Border leftLine = new Border { Background = Brushes.Black, Width = tickLength, Height = 1 };
                    Border rightLine = new Border { Background = Brushes.Black, Width = leftLineVisibleWidth, Height = 1 };

                    switch (TickPlacement)
                    {
                        case TickPlacement.TopLeft:
                            Canvas.SetLeft(leftLine, leftLineLeftPos);
                            Canvas.SetTop(leftLine, topPos);
                            tickCanvas.Children.Add(leftLine);
                            break;
                        case TickPlacement.BottomRight:
                            double rightLeft = ThumbSize - leftLineVisibleWidth + tickMargin / 2;
                            Canvas.SetLeft(rightLine, rightLeft);
                            Canvas.SetTop(rightLine, topPos);
                            tickCanvas.Children.Add(rightLine);
                            break;
                        case TickPlacement.Outside:
                            Canvas.SetLeft(leftLine, leftLineLeftPos);
                            Canvas.SetTop(leftLine, topPos);
                            Canvas.SetLeft(rightLine, ThumbSize - leftLineVisibleWidth + tickMargin / 2);
                            Canvas.SetTop(rightLine, topPos);
                            tickCanvas.Children.Add(leftLine);
                            tickCanvas.Children.Add(rightLine);
                            break;
                    }
                }
            }
        }

        private static IControlTemplate CreateControlTemplate()
        {
            return new FuncControlTemplate<RangeSlider>((control, scope) =>
            {
                var canvas = new Canvas { Name = "PART_LayoutCanvas", Background = Brushes.Transparent };
                scope.Register("PART_LayoutCanvas", canvas);

                var trackBackground = new Border
                {
                    Name = "PART_TrackBackground",
                    Background = control.TrackFill,
                    CornerRadius = new CornerRadius(3),
                };
                scope.Register("PART_TrackBackground", trackBackground);
                canvas.Children.Add(trackBackground);

                var selectionRange = new Border
                {
                    Name = "PART_SelectionRange",
                    Background = control.SelectionFill,
                    CornerRadius = new CornerRadius(3),
                };
                scope.Register("PART_SelectionRange", selectionRange);
                canvas.Children.Add(selectionRange);

                var tickCanvas = new Canvas { Name = "PART_TickCanvas", IsHitTestVisible = false };
                scope.Register("PART_TickCanvas", tickCanvas);
                canvas.Children.Add(tickCanvas);

                var startThumb = CreateThumbTemplate("PART_StartThumb", Brushes.White, Brushes.DodgerBlue, control);
                var endThumb = CreateThumbTemplate("PART_EndThumb", Brushes.White, Brushes.DodgerBlue, control);
                scope.Register("PART_StartThumb", startThumb);
                scope.Register("PART_EndThumb", endThumb);
                canvas.Children.Add(startThumb);
                canvas.Children.Add(endThumb);

                trackBackground.Bind(WidthProperty, canvas.GetObservable(BoundsProperty).Select(b => b.Width));
                trackBackground.Bind(HeightProperty, canvas.GetObservable(BoundsProperty).Select(b => b.Height));

                return new Border { Background = Brushes.Transparent, Child = canvas };
            });
        }

        private static Thumb CreateThumbTemplate(string name, IBrush background, IBrush borderBrush, RangeSlider control)
            => new()
            {
                Name = name,
                Width = control.ThumbSize,
                Height = control.ThumbSize,
                Template = new FuncControlTemplate<Thumb>((thumb, scope) =>
                    new Border
                    {
                        Background = background,
                        BorderBrush = borderBrush,
                        BorderThickness = new Thickness(2),
                        CornerRadius = new CornerRadius(control.ThumbSize / 2),
                        Width = control.ThumbSize,
                        Height = control.ThumbSize,
                        HorizontalAlignment = HorizontalAlignment.Center,
                        VerticalAlignment = VerticalAlignment.Center
                    })
            };
    }

SlidersCompared.axaml代码

<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
        xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
          xmlns:rs="using:RangeSlider.Avalonia.Controls"
        Height="312.8" Width="439.2"
        x:Class="AvaloniaUI.SlidersCompared"
        Title="SlidersCompared">
    <Grid>
        <StackPanel Margin="10">
            <TextBlock Margin="0,0,0,5">Normal Slider (Max=100, Val=<InlineUIContainer>
                <TextBlock Text="{Binding #slider1.Value}"/>
            </InlineUIContainer>)</TextBlock>
            <Slider Name="slider1" Maximum="100" Value="10"></Slider>

            <TextBlock Margin="0,15,0,5">Slider with Tick Marks (TickFrequency=10, TickPlacement=BottomRight)</TextBlock>
            <Slider Maximum="100" Value="10" TickFrequency="10" TickPlacement="BottomRight"></Slider>

            <TextBlock Margin="0,15,0,5">Slider with Irregular Tick Marks (Ticks=0,5,10,15,25,50,100)</TextBlock>
            <Slider Maximum="100" Value="10" Ticks="0,5,10,15,25,50,100" TickPlacement="BottomRight"    
                    TickFrequency="5" IsSnapToTickEnabled="True"></Slider>

            <TextBlock Margin="0,15,0,5" TextWrapping="Wrap">Slider with a Selection Range (IsSelectionRangeEnabled=True, SelectionStart=25, SelectionEnd=75)</TextBlock>
            <RangeSlider Maximum="100" Value="10" TickFrequency="10" TickPlacement="BottomRight"
                                  SelectionStart="25" SelectionEnd="75"></RangeSlider >
        </StackPanel>
    </Grid>
</Window>

SlidersCompared.axaml.cs代码

using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;

namespace AvaloniaUI;

public partial class SlidersCompared : Window
{
    public SlidersCompared()
    {
        InitializeComponent();
    }
}

运行效果

特殊版

image

双滑块版

image

 

posted on 2025-07-25 15:46  dalgleish  阅读(39)  评论(0)    收藏  举报