目前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();
}
}
运行效果
特殊版

双滑块版

浙公网安备 33010602011771号