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.
Related
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.
}
I have a Canvas (not InkCanvas) inside a Scrollviewer. Both are not created in XAML but in code behind. I want to draw lines on my Canvas with Pen and Mouse input, everything works just fine but now I tested the whole thing with a pen as input device and the Scrollviewer seems to recognize it as touch input because the whole thing starts scrolling.
My question is: Is it possible to tell the Scrollviewer to ignore all inputs from a device type? Because it also seems like the Scrollviewer is 'eating' the events which should be fired from the Canvas.
Here my Scrollviewer init:
private void SetUpScrollViewer()
{
scroll = new ScrollViewer();
scroll.VerticalScrollMode = ScrollMode.Auto;
scroll.HorizontalScrollMode = ScrollMode.Auto;
scroll.VerticalScrollBarVisibility = ScrollBarVisibility.Visible;
scroll.HorizontalScrollBarVisibility = ScrollBarVisibility.Visible;
scroll.ZoomMode = ZoomMode.Enabled;
scroll.ManipulationMode = ManipulationModes.System;
scroll.HorizontalAlignment = HorizontalAlignment.Left;
scroll.VerticalAlignment = VerticalAlignment.Top;
scroll.IsZoomInertiaEnabled = false;
scroll.MinZoomFactor = 1;
scroll.MaxZoomFactor = 5;
}
Those are the events I use in my Canvas:
public void EnableDrawingOnCanvas(Canvas canvas)
{
//Adding the needed event handler.
canvas.PointerPressed += Canvas_PointerPressed;
canvas.PointerMoved += Canvas_PointerMoved;
canvas.PointerReleased += Canvas_PointerReleased;
canvas.PointerExited += Canvas_PointerExited;
}
And those events all check if the input device is everything but touch like this
if (e.Pointer.PointerDeviceType != Windows.Devices.Input.PointerDeviceType.Touch){...}
But with those Events I can only check the input device for my Canvas and if I add a Event to the Scrollviewer it won't get passed to the Canvas afaik.
You can bind a PointerPressed event to your ScrollViewer and check if the e.Pointer.PointerDeviceType equals PointerDeviceType.Pen. Then you can disable the VerticalScrollMode the HorizontalScrollMode and the ZoomMode like in the code below.
If you want to reactivate the ScrollViewer you can bind a PointerExited event to your ScrollViewer and reenable everything.
private void Scroll_PointerPressed(object sender, PointerRoutedEventArgs e)
{
if (e.Pointer.PointerDeviceType == PointerDeviceType.Pen)
{
scroll.VerticalScrollMode = ScrollMode.Disabled;
scroll.HorizontalScrollMode = ScrollMode.Disabled;
scroll.ZoomMode = ZoomMode.Disabled;
}
}
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.
Is it possible to implement smooth scroll in a WPF listview like how it works in Firefox?
When the Firefox browser contained all listview items and you hold down the middle mouse button (but not release), and drag it, it should smoothly scroll the listview items. When you release it should stop.
It looks like this is not possible in winforms, but I am wondering if it is available in WPF?
You can achieve smooth scrolling but you lose item virtualisation, so basically you should use this technique only if you have few elements in the list:
Info here: Smooth scrolling on listbox
Have you tried setting:
ScrollViewer.CanContentScroll="False"
on the list box?
This way the scrolling is handled by the panel rather than the listBox... You lose virtualisation if you do that though so it could be slower if you have a lot of content.
It is indeed possible to do what you're asking, though it will require a fair amount of custom code.
Normally in WPF a ScrollViewer uses what is known as Logical Scrolling, which means it's going to scroll item by item instead of by an offset amount. The other answers cover some of the ways you can change the Logical Scrolling behavior into that of Physical Scrolling. The other way is to make use of the ScrollToVertialOffset and ScrollToHorizontalOffset methods exposed by both ScrollViwer and IScrollInfo.
To implement the larger part, the scrolling when the mouse wheel is pressed, we will need to make use of the MouseDown and MouseMove events.
<ListView x:Name="uiListView"
Mouse.MouseDown="OnListViewMouseDown"
Mouse.MouseMove="OnListViewMouseMove"
ScrollViewer.CanContentScroll="False">
....
</ListView>
In the MouseDown, we are going to record the current mouse position, which we will use as a relative point to determine which direction we scroll in. In the mouse move, we are going to get the ScrollViwer component of the ListView and then Scroll it accordingly.
private Point myMousePlacementPoint;
private void OnListViewMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.MiddleButton == MouseButtonState.Pressed)
{
myMousePlacementPoint = this.PointToScreen(Mouse.GetPosition(this));
}
}
private void OnListViewMouseMove(object sender, MouseEventArgs e)
{
ScrollViewer scrollViewer = ScrollHelper.GetScrollViewer(uiListView) as ScrollViewer;
if (e.MiddleButton == MouseButtonState.Pressed)
{
var currentPoint = this.PointToScreen(Mouse.GetPosition(this));
if (currentPoint.Y < myMousePlacementPoint.Y)
{
scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - 3);
}
else if (currentPoint.Y > myMousePlacementPoint.Y)
{
scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset + 3);
}
if (currentPoint.X < myMousePlacementPoint.X)
{
scrollViewer.ScrollToHorizontalOffset(scrollViewer.HorizontalOffset - 3);
}
else if (currentPoint.X > myMousePlacementPoint.X)
{
scrollViewer.ScrollToHorizontalOffset(scrollViewer.HorizontalOffset + 3);
}
}
}
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;
}
There's some areas it's lacking as it's just a proof of concept but it should definitely get you started in the right direction. To have it constantly scroll once the mouse is moved away from the initial MouseDown point, the scrolling logic could go into a DispatcherTimer or something similar.
Try setting the ScrollViewer.CanContentScroll attached property to false on the ListView. But like Pop Catalin said, you lose item virtualization, meaning all the items in the list get loaded and populated at once, not when a set of items are needed to be displayed - so if the list is huge, it could cause some memory and performance issues.
try setting the listview's height as auto and wrapping it in a scroll viewer.
<ScrollViewer IsTabStop="True" VerticalScrollBarVisibility="Auto">
<ListView></ListView>
</ScrollViewer>
Don't forget to mention the height of ScrollViewer
Hope this helps....
I know this post is 13 years old, but this is still something people want to do.
in newer versions of .Net you can set VirtualizingPanel.ScrollUnit="Pixel"
this way you won't lose virtualization and you get scroll per pixel instead of per item.
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="...">