I'm displaying a FlowDocument in a FlowDocumentReader with the ViewingMode="Scroll". If I use the wheel on my mouse, the document scrolls very slowly. I'd like to increase the scroll step.
I've tried to change the Scroll setting of my mouse in Control Panel, but that doesn't have any effect. I think that WPF ignores that setting for the FlowDocumentScrollViewer.
I've added a Scroll event on the FlowDocument and FlowDocumentReader, but that doesn't fire when I use the mouse wheel.
I've added a Loaded event on the FlowDocumentReader, got the ScrollViewer descendant,
found the ScrollBar ("PART_VerticalScrollBar") from the scroll viewer's template and adjusted the SmallChange & LargeChange properties. That also didn't have any effect.
Anyone have any ideas?
We can modify this in a Control's MouseWheel event, like Sohnee sugested, but then it'd just be solved for one specific case, and you'd have to have access to the FlowDocumentReader, which if your usinging something like MVVM, you wont. Instead, we can create an attached property that we can then set on any element with a ScrollViewer. When defining our attached property, we also are going to want a PropertyChanged callback where we will perform the actual modifications to the scroll speed. I also gave my property a default of 1, the range of speed I'm going to use is .1x to 3x, though you could just as easily do something like 1-10.
public static double GetScrollSpeed(DependencyObject obj)
{
return (double)obj.GetValue(ScrollSpeedProperty);
}
public static void SetScrollSpeed(DependencyObject obj, double value)
{
obj.SetValue(ScrollSpeedProperty, value);
}
public static readonly DependencyProperty ScrollSpeedProperty =
DependencyProperty.RegisterAttached(
"ScrollSpeed",
typeof(double),
typeof(ScrollHelper),
new FrameworkPropertyMetadata(
1.0,
FrameworkPropertyMetadataOptions.Inherits & FrameworkPropertyMetadataOptions.BindsTwoWayByDefault,
new PropertyChangedCallback(OnScrollSpeedChanged)));
private static void OnScrollSpeedChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
}
Now that we have our Attached Property we need to handle the scrolling, to do this, in the OnScrollSpeedChanged we can handle the PreviewMouseWheel event. We want to hook into the PreviewMouseWheel, since it is a tunneling event that will occur before the ScrollViewer can handle the standard MouseWheel event.
Currently, the PreviewMouseWheel handler is taking in the FlowDocumentReader or other thing that we bound it to, however what we need is the ScrollViewer. Since it could be a lot of things: ListBox, FlowDocumentReader, WPF Toolkit Grid, ScrollViewer, etc, we can make a short method that uses the VisualTreeHelper to do this. We already know that the item coming through will be some form of DependancyObject, so we can use some recursion to find the ScrollViewer if it exists.
public static DependencyObject GetScrollViewer(DependencyObject o)
{
// Return the DependencyObject if it is a ScrollViewer
if (o is ScrollViewer)
{ return o; }
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(o); i++)
{
var child = VisualTreeHelper.GetChild(o, i);
var result = GetScrollViewer(child);
if (result == null)
{
continue;
}
else
{
return result;
}
}
return null;
}
private static void OnScrollSpeedChanged(DependencyObject o, DependencyPropertyChangedEventArgs e)
{
var host = o as UIElement;
host.PreviewMouseWheel += new MouseWheelEventHandler(OnPreviewMouseWheelScrolled);
}
Now that we can get the ScrollViwer we can finally modify the scroll speed. We'll need to get the ScrollSpeed property from the DependancyObject that is being sent through. Also, we can use our helper method to get the ScrollViewer that is contained within the element. Once we have these two things, we can get and modify the ScrollViewer's VerticalOffset. I found that dividing the MouseWheelEventArgs.Delta, which is the amount that the mouse wheel changed, by 6 gets approximately the default scroll speed. So, if we multiply that by our ScrollSpeed modifier, we can then get the new offset value. We can then set the ScrollViewer's VerticalOffset using the ScrollToVerticalOffset method that it exposes.
private static void OnPreviewMouseWheelScrolled(object sender, MouseWheelEventArgs e)
{
DependencyObject scrollHost = sender as DependencyObject;
double scrollSpeed = (double)(scrollHost).GetValue(Demo.ScrollSpeedProperty);
ScrollViewer scrollViewer = GetScrollViewer(scrollHost) as ScrollViewer;
if (scrollViewer != null)
{
double offset = scrollViewer.VerticalOffset - (e.Delta * scrollSpeed / 6);
if (offset < 0)
{
scrollViewer.ScrollToVerticalOffset(0);
}
else if (offset > scrollViewer.ExtentHeight)
{
scrollViewer.ScrollToVerticalOffset(scrollViewer.ExtentHeight);
}
else
{
scrollViewer.ScrollToVerticalOffset(offset);
}
e.Handled = true;
}
else
{
throw new NotSupportedException("ScrollSpeed Attached Property is not attached to an element containing a ScrollViewer.");
}
}
Now that we've got our Attached Property set up, we can create a simple UI to demonstrate it. I'm going to create a ListBox, and a FlowDocumentReaders so that we can see how the ScrollSpeed will be affected across multiple controls.
<UniformGrid Columns="2">
<DockPanel>
<Slider DockPanel.Dock="Top"
Minimum=".1"
Maximum="3"
SmallChange=".1"
Value="{Binding ElementName=uiListBox, Path=(ScrollHelper:Demo.ScrollSpeed)}" />
<ListBox x:Name="uiListBox">
<!-- Items -->
</ListBox>
</DockPanel>
<DockPanel>
<Slider DockPanel.Dock="Top"
Minimum=".1"
Maximum="3"
SmallChange=".1"
Value="{Binding ElementName=uiListBox, Path=(ScrollHelper:Demo.ScrollSpeed)}" />
<FlowDocumentReader x:Name="uiReader"
ViewingMode="Scroll">
<!-- Flow Document Content -->
</FlowDocumentReader>
</DockPanel>
</UniformGrid>
Now, when run, we can use the Sliders to modify the scrolling speed in each of the columns, fun stuff.
Instead of using the scroll event, capture the MouseWheel event.
<FlowDocumentReader MouseWheel="...">
Wow. The Rmoore's answer is brilliant, but a little sophisticated. I've simplified it a bit. For those who whether don't use MVVM or can place the code inside class that has access to a target element these 2 methods will be enough for you:
Place this method to your extensions:
public static DependencyObject GetScrollViewer(this DependencyObject o)
{
// Return the DependencyObject if it is a ScrollViewer
if (o is ScrollViewer)
{ return o; }
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(o); i++)
{
var child = VisualTreeHelper.GetChild(o, i);
var result = GetScrollViewer(child);
if (result == null)
{
continue;
}
else
{
return result;
}
}
return null;
}
Then place the second method to a class that has access to a target UI element and subscribe it to a "PreviewMouseWheel" event
private void HandleScrollSpeed(object sender, MouseWheelEventArgs e)
{
try
{
if (!(sender is DependencyObject))
return;
ScrollViewer scrollViewer = (((DependencyObject)sender)).GetScrollViewer() as ScrollViewer;
ListBox lbHost = sender as ListBox; //Or whatever your UI element is
if (scrollViewer != null && lbHost != null)
{
double scrollSpeed = 1;
//you may check here your own conditions
if (lbHost.Name == "SourceListBox" || lbHost.Name == "TargetListBox")
scrollSpeed = 2;
double offset = scrollViewer.VerticalOffset - (e.Delta * scrollSpeed / 6);
if (offset < 0)
scrollViewer.ScrollToVerticalOffset(0);
else if (offset > scrollViewer.ExtentHeight)
scrollViewer.ScrollToVerticalOffset(scrollViewer.ExtentHeight);
else
scrollViewer.ScrollToVerticalOffset(offset);
e.Handled = true;
}
else
throw new NotSupportedException("ScrollSpeed Attached Property is not attached to an element containing a ScrollViewer.");
}
catch (Exception ex)
{
//Do something...
}
}