WPF horizontal scrollbar is slow and doesn't move to click

I'm making a custom control which has a scrollviewer that can display "long" data horizontally. Think something like a video editor, or a timeline type control. I noticed two issues: 1. If the scroll area is large, I can't click/hold/drag the thumb very far. I have to keep doing it over and over and move it a little bit each time. 2. I can't click on the space between the thumb and and another part of the scrollbar to move the thumb to where the cursor is.
<UserControl>
<Grid>
<ScrollViewer
x:Name="PART_Scroll"
HorizontalScrollBarVisibility="Auto"
PreviewMouseDown="OnPreviewMouseDown"
PreviewMouseMove="OnPreviewMouseMove"
PreviewMouseUp="OnPreviewMouseUp"
PreviewMouseWheel="OnPreviewMouseWheel"
VerticalScrollBarVisibility="Disabled">
<StackPanel Orientation="Vertical">
<Canvas
x:Name="TicksCanvas"
Height="28"
Background="White" />
<Canvas
x:Name="BlocksSurface"
Height="120">
<!-- ItemsControl uses Canvas for positioning -->
<ItemsControl
x:Name="PART_Items"
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Canvas>
</StackPanel>
</ScrollViewer>
</Grid>
</UserControl>
<UserControl>
<Grid>
<ScrollViewer
x:Name="PART_Scroll"
HorizontalScrollBarVisibility="Auto"
PreviewMouseDown="OnPreviewMouseDown"
PreviewMouseMove="OnPreviewMouseMove"
PreviewMouseUp="OnPreviewMouseUp"
PreviewMouseWheel="OnPreviewMouseWheel"
VerticalScrollBarVisibility="Disabled">
<StackPanel Orientation="Vertical">
<Canvas
x:Name="TicksCanvas"
Height="28"
Background="White" />
<Canvas
x:Name="BlocksSurface"
Height="120">
<!-- ItemsControl uses Canvas for positioning -->
<ItemsControl
x:Name="PART_Items"
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Canvas>
</StackPanel>
</ScrollViewer>
</Grid>
</UserControl>
3 Replies
Nacho Man Randy Cabbage
here's relevant code behind: Relevant code-behind of the control for zooming/panning/scrolling. Some of it might not be necessary but wanted
public ZoomableTimeline()
{
InitializeComponent();
Loaded += (_, __) => RebuildAll();
SizeChanged += (_, __) => Redraw();
}

private void RebuildAll()
{
// Set the total content width so we can scroll
var width = Math.Max(0, TotalTicks * PixelsPerTick);
TicksCanvas.Width = width;
BlocksSurface.Width = width;
RedrawTicks();
}

private void Redraw() => RebuildAll();

private double ViewportWidth() => PART_Scroll?.ViewportWidth > 0 ? PART_Scroll.ViewportWidth : ActualWidth;

private Point? _dragStart;
private double _dragOriginOffset;

private void OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
{
_dragStart = e.GetPosition(PART_Scroll);
_dragOriginOffset = PART_Scroll.HorizontalOffset;
Mouse.Capture((IInputElement)sender);
}
}

private void OnPreviewMouseMove(object sender, MouseEventArgs e)
{
if (_dragStart is Point start && e.LeftButton == MouseButtonState.Pressed)
{
var cur = e.GetPosition(PART_Scroll);
var dx = cur.X - start.X;
PART_Scroll.ScrollToHorizontalOffset(Math.Max(0, _dragOriginOffset + dx));
RedrawTicks(); // redraws the ticks/labels as panning/scrolling
}
}

private void OnPreviewMouseUp(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
{
_dragStart = null;
Mouse.Capture(null);
}
}

// zooms into/out of the canvas
private void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
// calls RebuildAll()
}
public ZoomableTimeline()
{
InitializeComponent();
Loaded += (_, __) => RebuildAll();
SizeChanged += (_, __) => Redraw();
}

private void RebuildAll()
{
// Set the total content width so we can scroll
var width = Math.Max(0, TotalTicks * PixelsPerTick);
TicksCanvas.Width = width;
BlocksSurface.Width = width;
RedrawTicks();
}

private void Redraw() => RebuildAll();

private double ViewportWidth() => PART_Scroll?.ViewportWidth > 0 ? PART_Scroll.ViewportWidth : ActualWidth;

private Point? _dragStart;
private double _dragOriginOffset;

private void OnPreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
{
_dragStart = e.GetPosition(PART_Scroll);
_dragOriginOffset = PART_Scroll.HorizontalOffset;
Mouse.Capture((IInputElement)sender);
}
}

private void OnPreviewMouseMove(object sender, MouseEventArgs e)
{
if (_dragStart is Point start && e.LeftButton == MouseButtonState.Pressed)
{
var cur = e.GetPosition(PART_Scroll);
var dx = cur.X - start.X;
PART_Scroll.ScrollToHorizontalOffset(Math.Max(0, _dragOriginOffset + dx));
RedrawTicks(); // redraws the ticks/labels as panning/scrolling
}
}

private void OnPreviewMouseUp(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
{
_dragStart = null;
Mouse.Capture(null);
}
}

// zooms into/out of the canvas
private void OnPreviewMouseWheel(object sender, MouseWheelEventArgs e)
{
// calls RebuildAll()
}
Nacho Man Randy Cabbage
here's a gif showing the problem
No description
Nacho Man Randy Cabbage
hmm, commenting out the mouse events fixes the first issue. i'm guessing its' in the PreviewMouseMove event. I allow them to pan by holding/dragging directly on the canvas, but that affects the scrollbar. oh, I need to move those preview mouse events to the stackpanel, not the scrollviewer

Did you find this page helpful?