Adjust FlowDocumentReader's Scroll Increment When ViewingMode Set to Scroll? - c#

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.

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...
}
}

Instead of using the scroll event, capture the MouseWheel event.
<FlowDocumentReader MouseWheel="...">

Related

UWP get the vertical offset of Datagrid's ScrollViewer

I'm using a datagrid from "Microsoft.Toolkit.Uwp.UI.Controls" and I'm trying to get the scrollViewer that is built into it to be able to get the vertical offset and implement certain behavior when scrolled all the way to the bottom or the middle. So far I've tried a number of methods I found in other different threads like the one below, that scans the visual tree and gets the scrollViewer. But the value of the verticalOffset property of the scrollViewer returned from this method is always zero and it's events like ViewChanged never gets fired, I've tried calling the updateLayout method but it changed nothing.
I've also tried wrapping the datagrid in a scrollviewer and used that instead. While that worked fine, it caused a huge performance issue due to virtualization. So is there any solution to this ?
private ScrollViewer GetScrollViewer(UIElement element)
{
if (element == null) return null;
ScrollViewer retour = null;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element) && retour == null; i++)
{
if (VisualTreeHelper.GetChild(element, i) is ScrollViewer)
{
retour = (ScrollViewer)(VisualTreeHelper.GetChild(element, i));
}
else
{
retour = GetScrollViewer(VisualTreeHelper.GetChild(element, i) as UIElement);
}
}
return retour;
}
As Oleg Mikhailov mentioned the source code, it was not implemented with ScrollViewer, and we have tested with your code, we can't get the scrollviewer instance. for this scenario, you could detect vertical scrollbar's value change event to get vertical offset, please refer the following code.
var scrollbar = MyFindDataGridChildOfType<ScrollBar>(MyDataGrid);
scrollbar.ValueChanged += Scroll_ValueChanged;
private void Scroll_ValueChanged(object sender, RangeBaseValueChangedEventArgs e)
{
Debug.WriteLine(e.NewValue);
// here is vertical value.
}

Drag and Drop custom controls between cells in a grid in WPF

I've got some custom controls which are dynamically added to a custom grid. These controls can span over several columns and rows(which are all the same size). I'd like to drag and drop between the rows and columns. I can drag the individual controls, but they can move anywhere without limit. Even off the grid. I'd like to do it so it can only be dragged inside the grid AND snaps to the column/row it's dragged to.
Is there any easy-ish way to do this?
Honestly, if I could get the current row/column that it's over, then all I'd need to do is set the column/row of it to them and that would probably do it and then just worry about keeping it inside the grid.
I figured out a nice and fun way!
I worked out the position on the grid that the the mouse is on on the MouseUp event and then the relative position of the mouse on the control since it spans several rows/columns.
public void getPosition(UIElement element, out int col, out int row)
{
DControl control = parent as DControl;
var point = Mouse.GetPosition(element);
row = 0;
col = 0;
double accumulatedHeight = 0.0;
double accumulatedWidth = 0.0;
// calc row mouse was over
foreach (var rowDefinition in control.RowDefinitions)
{
accumulatedHeight += rowDefinition.ActualHeight;
if (accumulatedHeight >= point.Y)
break;
row++;
}
// calc col mouse was over
foreach (var columnDefinition in control.ColumnDefinitions)
{
accumulatedWidth += columnDefinition.ActualWidth;
if (accumulatedWidth >= point.X)
break;
col++;
}
}
I then take away the relative positions from the normal positions so that when you drop it, it always drops on the top left of the screen. When I move my controls, I use margins to move it, which screws up the position on the grid at the time, as shown below:
void Chart_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (IsMouseCaptured)
{
Point mouseDelta = Mouse.GetPosition(this);
mouseDelta.Offset(-mouseOffset.X, -mouseOffset.Y);
Margin = new Thickness(
Margin.Left + mouseDelta.X,
Margin.Top + mouseDelta.Y,
Margin.Right - mouseDelta.X,
Margin.Bottom - mouseDelta.Y);
}
}
void Chart_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
mouseOffset = Mouse.GetPosition(this);
CaptureMouse();
parent.currentObject = this;
}
To tackle this, I simply reset the margin.
public void updatePosition()
{
Grid.SetRow(this, (int)position.Y);
Grid.SetColumn(this, (int)position.X);
Margin = new Thickness();
}
I hope this helps someone else since it was rather frustrating for me to find the answer and in the end I managed to get lots of little fragments of how to do things and eventually came up with my own solution.
Is there any easy-ish way to do this?
I'd say that the answer to this question very much depends on your experience using Drag and Drop functionality... for a beginner, I'd say that the answer to this was no, but for someone with some experience and some common sense, it might not be too bad.
To determine which Grid cell the user's mouse is over will not be straight forward. You can handle the PreviewDragOver event and use the VisualTreeHelper.HitTest method to check which control the mouse is currently over:
private void PreviewDragOver(object sender, DragEventArgs e)
{
HitTestResult hitTestResult = VisualTreeHelper.HitTest(adornedUIElement,
e.GetPosition(adornedUIElement));
Control controlUnderMouse = hitTestResult.VisualHit.GetParentOfType<Control>();
}
The GetParentOfType method is a useful extension method that I created, but you can convert it to a normal method easily enough:
public static T GetParentOfType<T>(this DependencyObject element) where T : DependencyObject
{
Type type = typeof(T);
if (element == null) return null;
DependencyObject parent = VisualTreeHelper.GetParent(element);
if (parent == null && ((FrameworkElement)element).Parent is DependencyObject) parent = ((FrameworkElement)element).Parent;
if (parent == null) return null;
else if (parent.GetType() == type || parent.GetType().IsSubclassOf(type)) return parent as T;
return GetParentOfType<T>(parent);
}
Of course, once you have a Control in your controlUnderMouse variable, you'll still have some considerable work to do as you work your way through the UIElements until you get to the Grid... you can of course make further use of the GetParentOfType method to make your job easier.

Horizontal scroll on ListView

I'm currently capturing the PointerMoved event on the page to use with a horizontal menu. So the user can swipe left/right and the page will animate accordingly.
This works when the user touches a static element (TextBlock etc.) but if they touch a ListView it captures the touch events.
How can I implement the ListView so when the user scrolls vertically it works as normal, but when the user scrolls horizontally it passes the events to my code?
It is possible, but you will need a small trick. As a refference I put here Rob Caplan's article.
Let's start:
First - where are your events? - answer is simple - while you have ScrollViewer enabled, all events are intercepted by it and handeled. You ListView will get only PointerEntered event and just after it PointerExited, all further proccesing is handeled by ScrollViewer. That is the problem. But as I've said there is a method to do what you want.
For this purpose lets assume that you have defined your ListView only with VerticalScroll:
<ListView Name="myList" ScrollViewer.HorizontalScrollMode="Disabled">
Of course it is possible to do for both directions, but it's a simple example.
Now let's have a look at constructor of a Page:
PointerPoint firstPoint = null;
ScrollViewer listScrollviewer = null;
public MainPage()
{
this.InitializeComponent();
myList.ItemsSource = yourItemSource;
myList.PointerEntered += myList_PointerEntered;
myList.PointerMoved += myList_PointerMoved;
}
Nothing weird here - I just subscribe to events, and declare two variables firstPoint and listScrollviewer, which I'll need later.
We will need also to get our ScrollViewer of our ListView - the following method will do the job:
public static ScrollViewer GetScrollViewer(DependencyObject depObj)
{
if (depObj is ScrollViewer) return depObj as ScrollViewer;
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
var child = VisualTreeHelper.GetChild(depObj, i);
var result = GetScrollViewer(child);
if (result != null) return result;
}
return null;
}
Now - to enable our events we will need to disable the ScrollViewer:
private ScrollViewer DisableScrolling(DependencyObject depObj)
{
ScrollViewer foundOne = GetScrollViewer(depObj);
if (foundOne != null) foundOne.VerticalScrollMode = ScrollMode.Disabled;
return foundOne;
}
We will disable the ScrollViewer upon PointerEntered event which is fired. In this step we will also remember the pressed PointerPoint - as we have disable Scrollviewer, we will have to scroll it manually - that is what we need this PointerPoint for.
private void myList_PointerEntered(object sender, PointerRoutedEventArgs e)
{
firstPoint = e.GetCurrentPoint(myList);
if (listScrollviewer == null) listScrollviewer = DisableScrolling(myList);
}
Finally our PointerMoved event, which now wil be fired as we had disabled ScrollViewer - moving ScrollViewer + other code you need to put there:
private void myList_PointerMoved(object sender, PointerRoutedEventArgs e)
{
if (listScrollviewer != null)
{
PointerPoint secondPoint = e.GetCurrentPoint(myList);
double verticalDifference = secondPoint.Position.Y - firstPoint.Position.Y;
listScrollviewer.ChangeView(null, listScrollviewer.VerticalOffset - verticalDifference, null);
}
// some other code you need
}
Few remarks:
this method still needs much tuning, but hopefuly will show you how to achieve your goal,
you may need also to separate some small horizontal movements from vertical ones,
if your ListView or other Control has horizontal scroll, then you will also need to disable and handle it,
this method won't probably work so smooth like original ScrollViewer.
I've also put a simple working example here at OneDrive.

Manipulation event and panning mode

I am developing a WPF for touch enabled device. I am facing a strange problem. My XAML structure is
<ScrollViewer>
<StackPanel orientation="Horizontal">
<!-- Control goes here -->
</StackPanel>
<ScrollViewer>
Now
To enable scrolling on touch I have to set PannigMode to HorizontalOnly
To receive manipulation events I have to set PannigMode to None
Problem is I have to have these two functionality simultaneously.
Is there any work around so that the scrollviewer is scrolled on touch and also the manipulationcompleted event fires.
Please help.
I had the same issue. You have at least two options
use MS Surface Toolkit
fix ScrollViewer
I've chosen the second one. Simply create a custom control, inherit from ScrollViewer, in Generic.xaml you only need to put a <ContentPresenter />, nothing more.
The real job is in the code behind, but also not so complecated. I had to check whether the user touched a button or just wanted to scroll. The trick is to check what is on the touch points and turn on / off the panning mode.
And here is the code:
namespace Demo.Controls
{
public class ScrollViewerWithTouch : ScrollViewer
{
private PanningMode panningMode;
private bool panningModeSet;
static ScrollViewerWithTouch()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(ScrollViewerWithTouch), new FrameworkPropertyMetadata(typeof(ScrollViewerWithTouch)));
}
protected override void OnManipulationCompleted(ManipulationCompletedEventArgs e)
{
base.OnManipulationCompleted(e);
// set it back
this.PanningMode = this.panningMode;
}
protected override void OnManipulationStarted(ManipulationStartedEventArgs e)
{
// figure out what has the user touched
var result = VisualTreeHelper.HitTest(this, e.ManipulationOrigin);
if (result != null && result.VisualHit != null)
{
var hasButtonParent = this.HasButtonParent(result.VisualHit);
// if user touched a button then turn off panning mode, let style bubble down, in other case let it scroll
this.PanningMode = hasButtonParent ? PanningMode.None : this.panningMode;
}
base.OnManipulationStarted(e);
}
protected override void OnTouchDown(TouchEventArgs e)
{
// store panning mode or set it back to it's original state. OnManipulationCompleted does not do it every time, so we need to set it once more.
if (this.panningModeSet == false)
{
this.panningMode = this.PanningMode;
this.panningModeSet = true;
}
else
{
this.PanningMode = this.panningMode;
}
base.OnTouchDown(e);
}
private bool HasButtonParent(DependencyObject obj)
{
var parent = VisualTreeHelper.GetParent(obj);
if ((parent != null) && (parent is ButtonBase) == false)
{
return HasButtonParent(parent);
}
return parent != null;
}
}
}

Make ListView.ScrollIntoView Scroll the Item into the Center of the ListView (C#)

ListView.ScrollIntoView(object) currently finds an object in the ListView and scrolls to it. If you are positioned beneath the object you are scrolling to, it scrolls the object to the top row. If you are positioned above, it scrolls it into view at the bottom row.
I'd like to have the item be scrolled right into the center of my list view if it is currently not visible. Is there an easy way to accomplish this?
It is very easy to do this in WPF with an extension method I wrote. All you have to do to scroll an item to the center of the view is to call a single method.
Suppose you have this XAML:
<ListView x:Name="view" ItemsSource="{Binding Data}" />
<ComboBox x:Name="box" ItemsSource="{Binding Data}"
SelectionChanged="ScrollIntoView" />
Your ScrollIntoView method will be simply:
private void ScrollIntoView(object sender, SelectionChangedEventArgs e)
{
view.ScrollToCenterOfView(box.SelectedItem);
}
Obviously this could be done using a ViewModel as well rather than referencing the controls explicitly.
Following is the implementation. It is very general, handling all the IScrollInfo possibilities. It works with ListBox or any other ItemsControl, and works with any panel including StackPanel, VirtualizingStackPanel, WrapPanel, DockPanel, Canvas, Grid, etc.
Just put this in a .cs file somewhere in your project:
public static class ItemsControlExtensions
{
public static void ScrollToCenterOfView(this ItemsControl itemsControl, object item)
{
// Scroll immediately if possible
if(!itemsControl.TryScrollToCenterOfView(item))
{
// Otherwise wait until everything is loaded, then scroll
if(itemsControl is ListBox) ((ListBox)itemsControl).ScrollIntoView(item);
itemsControl.Dispatcher.BeginInvoke(DispatcherPriority.Loaded, new Action(() =>
{
itemsControl.TryScrollToCenterOfView(item);
}));
}
}
private static bool TryScrollToCenterOfView(this ItemsControl itemsControl, object item)
{
// Find the container
var container = itemsControl.ItemContainerGenerator.ContainerFromItem(item) as UIElement;
if(container==null) return false;
// Find the ScrollContentPresenter
ScrollContentPresenter presenter = null;
for(Visual vis = container; vis!=null && vis!=itemsControl; vis = VisualTreeHelper.GetParent(vis) as Visual)
if((presenter = vis as ScrollContentPresenter)!=null)
break;
if(presenter==null) return false;
// Find the IScrollInfo
var scrollInfo =
!presenter.CanContentScroll ? presenter :
presenter.Content as IScrollInfo ??
FirstVisualChild(presenter.Content as ItemsPresenter) as IScrollInfo ??
presenter;
// Compute the center point of the container relative to the scrollInfo
Size size = container.RenderSize;
Point center = container.TransformToAncestor((Visual)scrollInfo).Transform(new Point(size.Width/2, size.Height/2));
center.Y += scrollInfo.VerticalOffset;
center.X += scrollInfo.HorizontalOffset;
// Adjust for logical scrolling
if(scrollInfo is StackPanel || scrollInfo is VirtualizingStackPanel)
{
double logicalCenter = itemsControl.ItemContainerGenerator.IndexFromContainer(container) + 0.5;
Orientation orientation = scrollInfo is StackPanel ? ((StackPanel)scrollInfo).Orientation : ((VirtualizingStackPanel)scrollInfo).Orientation;
if(orientation==Orientation.Horizontal)
center.X = logicalCenter;
else
center.Y = logicalCenter;
}
// Scroll the center of the container to the center of the viewport
if(scrollInfo.CanVerticallyScroll) scrollInfo.SetVerticalOffset(CenteringOffset(center.Y, scrollInfo.ViewportHeight, scrollInfo.ExtentHeight));
if(scrollInfo.CanHorizontallyScroll) scrollInfo.SetHorizontalOffset(CenteringOffset(center.X, scrollInfo.ViewportWidth, scrollInfo.ExtentWidth));
return true;
}
private static double CenteringOffset(double center, double viewport, double extent)
{
return Math.Min(extent - viewport, Math.Max(0, center - viewport/2));
}
private static DependencyObject FirstVisualChild(Visual visual)
{
if(visual==null) return null;
if(VisualTreeHelper.GetChildrenCount(visual)==0) return null;
return VisualTreeHelper.GetChild(visual, 0);
}
}
Ray Burns' excellent answer above is WPF specific.
Here is a modified version that works in Silverlight:
public static class ItemsControlExtensions
{
public static void ScrollToCenterOfView(this ItemsControl itemsControl, object item)
{
// Scroll immediately if possible
if (!itemsControl.TryScrollToCenterOfView(item))
{
// Otherwise wait until everything is loaded, then scroll
if (itemsControl is ListBox) ((ListBox)itemsControl).ScrollIntoView(item);
itemsControl.Dispatcher.BeginInvoke( new Action(() =>
{
itemsControl.TryScrollToCenterOfView(item);
}));
}
}
private static bool TryScrollToCenterOfView(this ItemsControl itemsControl, object item)
{
// Find the container
var container = itemsControl.ItemContainerGenerator.ContainerFromItem(item) as UIElement;
if (container == null) return false;
// Find the ScrollContentPresenter
ScrollContentPresenter presenter = null;
for (UIElement vis = container; vis != null ; vis = VisualTreeHelper.GetParent(vis) as UIElement)
if ((presenter = vis as ScrollContentPresenter) != null)
break;
if (presenter == null) return false;
// Find the IScrollInfo
var scrollInfo =
!presenter.CanVerticallyScroll ? presenter :
presenter.Content as IScrollInfo ??
FirstVisualChild(presenter.Content as ItemsPresenter) as IScrollInfo ??
presenter;
// Compute the center point of the container relative to the scrollInfo
Size size = container.RenderSize;
Point center = container.TransformToVisual((UIElement)scrollInfo).Transform(new Point(size.Width / 2, size.Height / 2));
center.Y += scrollInfo.VerticalOffset;
center.X += scrollInfo.HorizontalOffset;
// Adjust for logical scrolling
if (scrollInfo is StackPanel || scrollInfo is VirtualizingStackPanel)
{
double logicalCenter = itemsControl.ItemContainerGenerator.IndexFromContainer(container) + 0.5;
Orientation orientation = scrollInfo is StackPanel ? ((StackPanel)scrollInfo).Orientation : ((VirtualizingStackPanel)scrollInfo).Orientation;
if (orientation == Orientation.Horizontal)
center.X = logicalCenter;
else
center.Y = logicalCenter;
}
// Scroll the center of the container to the center of the viewport
if (scrollInfo.CanVerticallyScroll) scrollInfo.SetVerticalOffset(CenteringOffset(center.Y, scrollInfo.ViewportHeight, scrollInfo.ExtentHeight));
if (scrollInfo.CanHorizontallyScroll) scrollInfo.SetHorizontalOffset(CenteringOffset(center.X, scrollInfo.ViewportWidth, scrollInfo.ExtentWidth));
return true;
}
private static double CenteringOffset(double center, double viewport, double extent)
{
return Math.Min(extent - viewport, Math.Max(0, center - viewport / 2));
}
private static DependencyObject FirstVisualChild(UIElement visual)
{
if (visual == null) return null;
if (VisualTreeHelper.GetChildrenCount(visual) == 0) return null;
return VisualTreeHelper.GetChild(visual, 0);
}
}
Ray Burns' excellent answer above and the comment from Fyodor Soikin:
"Actually, it doesn't work with any other ItemsControl... doesn't work with DataGrid with virtualization turned on..."
Use:
if (listBox.SelectedItem != null)
{
listBox.ScrollIntoView(listBox.SelectedItem);
listBox.ScrollToCenterOfView(listBox.SelectedItem);
}
#all: cant comment at the moment, need 50 reputation
I seem to recall doing something like this myself at some point. As far as my memory goes, what I did was:
Determine if the object is already visible or not.
If it's not visible, get the index of the object you want, and the number of objects currently displayed.
(index you want) - (number of objects displayed / 2) should be the top row, so scroll to that (making sure you don't go negative, of course)
If you look at the template of a Listbox it is simply a scrollviewer with an itemspresenter inside. You'll need to calculate the size of your items and use scroll horizontally or vertically to position the items in your scrollviewer. The april silverlight toolkit has an extension method GetScrollHost that you can call on a listbox to get your underlying scrollviewer.
Once you have that you can use the current Horizontal or Vertical Offset as a frame of reference and move your list accordinly.
The below sample will find the scrollviewer of the listview and use it to scroll the item to me middle of the listview.
XAML:
<Window x:Class="ScrollIntoViewTest.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Height="300" Width="300">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<ListView Grid.Row="0" ItemsSource="{Binding Path=Data}" Loaded="OnListViewLoaded"/>
<ComboBox Grid.Row="1" ItemsSource="{Binding Path=Data}" SelectionChanged="OnScrollIntoView" />
</Grid>
</Window>
Code behind:
using System;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
namespace ScrollIntoViewTest
{
public partial class Window1 : Window
{
public Window1()
{
InitializeComponent();
Data = new List<string>();
for (int i = 0; i < 100; i++)
{
Data.Add(i.ToString());
}
DataContext = this;
}
public List<string> Data { get; set; }
private void OnListViewLoaded(object sender, RoutedEventArgs e)
{
// Assumes that the listview consists of a scrollviewer with a border around it
// which is the default.
Border border = VisualTreeHelper.GetChild(sender as DependencyObject, 0) as Border;
_scrollViewer = VisualTreeHelper.GetChild(border, 0) as ScrollViewer;
}
private void OnScrollIntoView(object sender, SelectionChangedEventArgs e)
{
string item = (sender as ComboBox).SelectedItem as string;
double index = Data.IndexOf(item) - Math.Truncate(_scrollViewer.ViewportHeight / 2);
_scrollViewer.ScrollToVerticalOffset(index);
}
private ScrollViewer _scrollViewer;
}
}
I found an additional approach to solve this problem, assuming some of us just need a way to find out the Height of the visual item according to the item template this would greatly save you time.
Ok, I assume your XAML is structured somehow similiar to this:
:
<Window.Resources>
<DataTemplate x:Key="myTemplate">
<UserControls1:myControl DataContext="{Binding}" />
</DataTemplate>
</Window.Resources>
:
<ListBox Name="myListBox" ItemTemplate="{StaticResource ResourceKey=myTemplate}" />
And you want to calculate in order to scroll to the center but you have no idea what is the current height
of each item in your listbox.. this is how you can find out:
listBoxItemHeight = (double)((DataTemplate)FindResource("myTemplate")).LoadContent().GetValue(HeightProperty);
I used Ray Burns's excellent answer. However, it will not work when the VirtualizingStackPanel.ScrollUnit is set to "Pixel", only when the scroll unit is set to "Item". There is no need for adjustment to logical scrolling when the unit is the pixel. One quick fix will do the trick and the code will work for both cases:
Change
// Adjust for logical scrolling
if (scrollInfo is StackPanel || scrollInfo is VirtualizingStackPanel)
To
// Adjust for logical scrolling
if (scrollInfo is StackPanel || (scrollInfo is VirtualizingStackPanel && VirtualizingPanel.GetScrollUnit(itemsControl) == ScrollUnit.Item))
It will bypass the adjustment for logical scrolling when scrolling by pixel.
If the issue is the scroll inconsistency (the difference between scrolling from above / below), this can be solved by first scrolling to the top of the list, and then scrolling to the desired row + half the visible row count. An additional range check will be required to avoid an IndexOutOfRange.
// we add +1 to row height for grid width
var offset = (int)(mDataGrid.RenderSize.Height / (mDataGrid.MinRowHeight + 1) / 2);
// index is the item's index in the list
if (index + offset >= mDataGrid.Items.Count) offset = 0;
mDataGrid.ScrollIntoView(mDataGrid.Items[0]);
mDataGrid.ScrollIntoView(mDataGrid.Items[index + offsest]);
I know this post is old but I wanted to provide the UWP version of Ray Burns' excellent answer above
public static async void ScrollToCenterOfView(this ItemsControl itemsControl, object item)
{
// Scroll immediately if possible
if (!itemsControl.TryScrollToCenterOfView(item))
{
// Otherwise wait until everything is loaded, then scroll
if (itemsControl is ListBox) ((ListBox)itemsControl).ScrollIntoView(item);
await itemsControl.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
itemsControl.TryScrollToCenterOfView(item);
});
}
}
private static bool TryScrollToCenterOfView(this ItemsControl itemsControl, object item)
{
// Find the container
var container = itemsControl.ContainerFromItem(item) as FrameworkElement;
if (container == null) return false;
var scrollPresenter = container.FindParent(typeof(ScrollContentPresenter)) as ScrollContentPresenter;
if (scrollPresenter == null) return false;
Size size = container.RenderSize;
var center = container.TransformToVisual(scrollPresenter).TransformPoint(new Point(size.Width / 2, size.Height / 2));
center.Y += scrollPresenter.VerticalOffset;
center.X += scrollPresenter.HorizontalOffset;
// Scroll the center of the container to the center of the viewport
if (scrollPresenter.CanVerticallyScroll) scrollPresenter.SetVerticalOffset(CenteringOffset(center.Y, scrollPresenter.ViewportHeight, scrollPresenter.ExtentHeight));
if (scrollPresenter.CanHorizontallyScroll) scrollPresenter.SetHorizontalOffset(CenteringOffset(center.X, scrollPresenter.ViewportWidth, scrollPresenter.ExtentWidth));
return true;
}
public static FrameworkElement FindParent(this FrameworkElement o, Type type)
{
for (var element = VisualTreeHelper.GetParent(o) as FrameworkElement;
element != null;
element = VisualTreeHelper.GetParent(element) as FrameworkElement)
{
if (element?.GetType() == type) return element;
}
return null;
}
private static double CenteringOffset(double center, double viewport, double extent)
{
return Math.Min(extent - viewport, Math.Max(0, center - viewport / 2));
}

Categories