I'm kinda new to this WPF world.
And I'm kinda confused on why my scrollview is consuming my touch event.
My current situation is this:
I have this ScrollViewer with an ItemsControl. In this ItemsControl I'm using a Wrappanel to show a number of Rectangles. My ScrollViewer should make it possible to scroll vertically to show the Rectangles that are wrapped downward.
On each of these Rectangles I made a CustomBehaviour with all kinds of handlers.
One of these handlers is a 'creatively' made way to handle LongPressGestures.
The problem is as follows, before my longpress is detected by the behaviour, my ScrollViewer is capturing my TouchDevice in its PreviewTouchMove handler.
How can I prevent my ScrollViewer from capturing my TouchDevice too early?
How can I make sure that I can scroll my ScrollViewer and do my LongPress, DoubleClick (which still works), SingleClick (which still works as well) and other gestures I might add to this custom behaviour?
I've found similar questions on stackoverflow and google that I just not figure out for my specific case.
Something with a CustomThumb <-- This link solves the problem by making a CustomThumb. Can I somehow re-use this for my behaviour? By capturing the TouchDevice early in my behaviour handlers?
If all things fail. Is there an alternative to this ScrollViewer & CustomBehaviour combination?
EDIT:
In the meantime. I retried the CustomThumb-method. I got the longpress to work now from my CustomBehaviour while the UIElements (with that behaviour) are located on a ScrollViewer.
However the scroll functionality of the ScrollViewer still does not work.
The bounty I added will also be awarded to the person that helps me get that to work properly again (since the answer should lie in the same direction as this CustomThumb solution).
I had a similar issue before. I could not use scrolling, touch and style in the same time. At the end I developped a custom ScrollViewer. It is easier than you think, since you only need to watch / change some basic methods.
In your case you can check whether the user pressed on an empy surface or a list item. If it is a list item, you need to check whether it was a short press (so, touchup also occured after touchdown) or a long one.
Scrolling can be configured with PanningMode. It allows you to scroll with finger all over the usercontrol.
Here is my version of scrollviewer. It turns scrolling mode off when user pressed a button and turned on afterwards.
public class ScrollViewerWithTouch : ScrollViewer
{
/// <summary>
/// Original panning mode.
/// </summary>
private PanningMode panningMode;
/// <summary>
/// Set panning mode only once.
/// </summary>
private bool panningModeSet;
/// <summary>
/// Initializes static members of the <see cref="ScrollViewerWithTouch"/> class.
/// </summary>
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;
}
}
Related
I have a simple ListView in a Universal Windows App. It scrolls just fine both on a touch-enabled device and on my local machine. On the touch device I would like to be able to tap and drag the scroll bar thumb in order to quickly scroll through the list (same as you would get clicking and dragging the scroll thumb with a mouse). But, when I try to select the scroll bar thumb it does not work; the thumb is not selectable.
At first I thought the scroll thumb was just too small, so I fiddled with the default style of ScrollBar to increase the width. Then I tried tweaking other values in the default ScrollViewer style, like the ScrollingIndicatorMode and ScrollingIndicatorStates so that they would always use MouseIndicator, and making sure all IsHitTestVisible are True. To no avail.
It seems like there must be something in the default styles that allows this but I can't find it through trial and error, and no where in the MSDN docs appears to address this styling.
Is this doable in touch mode?
Is this doable in touch mode?
AFAIK, this is not possible currently. The scroll event of ScrollBar can only be triggered by scrolling the content or moving the Thumb by mouse input.
This is by design, but on the touch device if we want to quickly scroll through the list, we just quickly swipe one item for a short distance, the ListView will firstly speed up the scrolling and then slow down and finally stop. Other controls which contain a ScrollViewer behave the same, in the viewport(content of ScrollViewer) when in touch device, moving the thumb is not work, to speed up the scrolling, you can only give quick swipe gesture on the viewport.
Our suggestion is that you may submit a request to add this new features for developing through the Windows Feedback tool.
For posterity's sake I want to post my work around which actually turned out not being quite as difficult as I thought and looks really good now.
The basic idea is to use a Slider control (has a Thumb that is select-able, drag-able, and moves along a track) with a vertical orientation and to sync the Slider's Value property (which changes when dragged) to the ListView's ScrollIntoView method so you can scroll the ListView as the Slider Thumb is dragged. Vice-versa (move the Slider Thumb when the ListView scrolls normally) is also required for a clean, seamless experience. I have some code sample below to get you started (not everything is there to work right away). In addition, edit the Slider's default Style template to make it look exactly like a scroll bar (and even even add a tooltip to display the current value when dragging).
Note: I'm using WinRTXamlToolkit in code-behind to traverse the VisualTree easily.
Code behind:
public sealed partial class MainPage : Page
{
private bool _isScroll = false;
private bool _isSlide = false;
public MainPage()
{
this.InitializeComponent();
var vm = new ViewModel();
vm.ValueChanged += LetterSliderValueChanged;
DataContext = vm;
}
/// <summary>
/// Bring list items into view on the screen based on the value (letter) of the slider
/// </summary>
/// <param name="sender">The view model to bring into view</param>
private void LetterSliderValueChanged(object sender, RoutedEventArgs e)
{
if (_isScroll) return;
if (sender == null) return;
_isSlide = true;
ListView?.ScrollIntoView(sender, ScrollIntoViewAlignment.Leading);
}
/// <summary>
/// Update the position of the slider when the ListView is scrolling from a normal touch
/// </summary>
private void ScrollViewerViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
if (_isSlide)
{
_isSlide = false;
return;
}
_isScroll = true;
var scrollViewer = sender as ScrollViewer;
var scrollBars = scrollViewer.GetDescendantsOfType<ScrollBar>();
var verticalBar = scrollBars.FirstOrDefault(x => x.Orientation == Orientation.Vertical);
// Normalize the scales to move the slider thumb in sync with scrolling
var sliderTotal = LetterSlider.Maximum - LetterSlider.Minimum;
var barTotal = verticalBar.Maximum - verticalBar.Minimum;
var barPercent = verticalBar.Value / barTotal;
LetterSlider.Value = (barPercent * sliderTotal) + LetterSlider.Minimum;
_isScroll = false;
}
/// <summary>
/// Add the slider method to the ListView's ScrollViewer Viewchanged event
/// </summary>
private void ListViewLoaded(object sender, RoutedEventArgs e)
{
var listview = sender as ListView;
if (listview == null) return;
var scrollViewer = listview.GetFirstDescendantOfType<ScrollViewer>();
scrollViewer.ViewChanged -= ScrollViewerViewChanged;
scrollViewer.ViewChanged += ScrollViewerViewChanged;
}
}
XAML:
<Slider x:Name="LetterSlider" Orientation="Vertical"
Value="{Binding SliderValue, Mode=TwoWay, UpdateSourceTrigger=PropertyChanged}"
Maximum="{Binding MaxSliderLetter, Mode=OneTime}"
Minimum="{Binding MinSliderLetter, Mode=OneTime}"
RenderTransformOrigin="0.5,0.5">
<Slider.RenderTransform>
<RotateTransform Angle="180"/>
</Slider.RenderTransform>
</Slider>
View Model:
//Method to update the ListView to show items based on the letter of the slider
public RoutedEventHandler ValueChanged;
public int SliderValue
{
get { return _sliderValue; }
set
{
_sliderValue = value;
NotifyPropertyChanged("SliderValue");
char letter = (char)_sliderValue;
var items = ItemsGroup.FirstOrDefault(i => (char)i.Key == letter);
if (items == null) return;
ValueChanged(items.FirstOrDefault(), new RoutedEventArgs());
}
}
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.
Is there a way to control which items leave the visible area of a stackpanel when resizing?
Thanks
UPDATE 1
I have a fixed number of buttons inside a stackpanel. When resizing the visible area of the stackpanel, each button automatically hides or shows depending on the available space. What I'd like to achieve, through some event, control which of button hides or shows when stackpanel resizing occurs.
The reason is, I'd like to create a minimized version of the button instead of hiding the button.
In the past I have done something like this by altering the ContentTemplate of an object based on the object's size.
Typically I add an event to both the Loaded and SizeChanged events of the parent object, and from there figure out if the control is visible or not. If not, I change the template to a smaller version of the template.
In reference to your comment here about the SizeChanged event not firing, that is probably because you have your objects in a StackPanel, which will grow/shrink to fit the size of it's children, not to match the size of it's parent (the Grid cell).
You can probably also do this using a DataTrigger and Converter on the actual UI object, so it automatically checks to see if the Template should changed when the control's ActualWidth or ActualHeight changes.
I have a helper class I use to determine the exact visibility of a UI control within it's parent object, to find out if it's fully or partially visible, or hidden entirely. The code can be found in this answer, although I'll also copy it here:
public enum ControlVisibility
{
Hidden,
Partial,
Full,
FullHeightPartialWidth,
FullWidthPartialHeight
}
/// <summary>
/// Checks to see if an object is rendered visible within a parent container
/// </summary>
/// <param name="child">UI element of child object</param>
/// <param name="parent">UI Element of parent object</param>
/// <returns>ControlVisibility Enum</returns>
public static ControlVisibility IsObjectVisibleInContainer(
FrameworkElement child, UIElement parent)
{
GeneralTransform childTransform = child.TransformToAncestor(parent);
Rect childSize = childTransform.TransformBounds(
new Rect(new Point(0, 0), new Point(child.Width, child.Height)));
Rect result = Rect.Intersect(
new Rect(new Point(0, 0), parent.RenderSize), childSize);
if (result == Rect.Empty)
{
return ControlVisibility.Hidden;
}
if (result.Height == childSize.Height && result.Width == childSize.Width)
{
return ControlVisibility.Full;
}
if (result.Height == childSize.Height)
{
return ControlVisibility.FullHeightPartialWidth;
}
if (result.Width == childSize.Width)
{
return ControlVisibility.FullWidthPartialHeight;
}
return ControlVisibility.Partial;
}
You can get a control's visibility like this:
ControlVisibility ctrlVisibility =
WPFHelpers.IsObjectVisibleInContainer(button, parent);
if (ctrlVisibility == ControlVisibility.Full
|| isVisible == ControlVisibility.FullWidthPartialHeight)
{
// Set big template
}
else
{
// Set little template
}
The reason why the "SizeChanged" event wasn't firing was that I had set it's height to a fixed value. After setting it to "auto" the event fired when resizing it's parent container.
I am building a canvas control. This root canvas has several overlapping children (canvas as well). This is done so each child can handle its own drawing and I can then compose the final result with any combination of children to get the desired behavior.
This is working very well as far as rendering is concerned. This does not work so well with mouse events however. The way mouse events works are as follow (using previewmousemove as an example):
1- If root canvas is under mouse, fire event
2- Check all children, if one is under mouse, fire event and stop
As such, only the first child I add will receive the mouse move event. The event is not propagated to all children because they overlap.
To overcome this, I attempted the following:
1- Override mouse events in the root canvas
2- For every event, find all children that want to handle the event using VisualTreeHelper.HitTest
3- For all children that returned a valid hit test result (ie: under mouse and willing to handle the event (IsHitTestVisible == true)), ???
This is where I am stuck, I somehow need to send the mouse event to all children, and stop the normal flow of the event to make sure the first child doesn't receive it twice (via handled = true in the event).
By using RaiseEvent with the same event passed on the children, things seem to work but somehow it raises the event on the parent (root canvas) as well. To bypass this I needed to create a copy of the event and set force set the source though it appears to be more of a hack than a solution. Is there a proper way to do what I am trying to do? Code example follows.
public class CustomCanvas : Canvas
{
private List<object> m_HitTestResults = new List<object>();
public new event MouseEventHandler MouseMove;
public CustomCanvas()
{
base.PreviewMouseMove += new MouseEventHandler(CustomCanvas_MouseMove);
}
private void CustomCanvas_MouseMove(object sender, MouseEventArgs e)
{
// Hack here, why is the event raised on the parent as well???
if (e.OriginalSource == this)
{
return;
}
Point pt = e.GetPosition((UIElement)sender);
m_HitTestResults.Clear();
VisualTreeHelper.HitTest(this,
new HitTestFilterCallback(OnHitTest),
new HitTestResultCallback(OnHitTest),
new PointHitTestParameters(pt));
MouseEventArgs tmpe = new MouseEventArgs(e.MouseDevice, e.Timestamp, e.StylusDevice);
tmpe.RoutedEvent = e.RoutedEvent;
tmpe.Source = this;
foreach (object hit in m_HitTestResults)
{
UIElement element = hit as UIElement;
if (element != null)
{
// This somehow raises the event on us as well as the element here, why???
element.RaiseEvent(tmpe);
}
}
var handlers = MouseMove;
if (handlers != null)
{
handlers(sender, e);
}
e.Handled = true;
}
private HitTestFilterBehavior OnHitTest(DependencyObject o)
{
UIElement element = o as UIElement;
if (element == this)
{
return HitTestFilterBehavior.ContinueSkipSelf;
}
else if (element != null && element.IsHitTestVisible && element != this)
{
return HitTestFilterBehavior.Continue;
}
return HitTestFilterBehavior.ContinueSkipSelfAndChildren;
}
private HitTestResultBehavior OnHitTest(HitTestResult result)
{
// Add the hit test result to the list that will be processed after the enumeration.
m_HitTestResults.Add(result.VisualHit);
// Set the behavior to return visuals at all z-order levels.
return HitTestResultBehavior.Continue;
}
I think you should use the preview events because those are RoutingStrategy.Tunnel from Window to the most high control in Z-Order, and normal events are RoutingStrategy.Bubble.
In this RoutedEvents there are a property Handle when it's true the system will stop to traverse the visual tree because someone used this event.
I found your code example interesting so I gave it a try... but I had to make a small modification for it to work properly in my stuff.
I had to change the 2nd "if" in the HitTestFilter method as follow:
if (element == null || element.IsHitTestVisible)
As you can see I removed the useless "element != this" at the end (you already tested that condition in the 1st "if") and I added "element == null" at the beginning.
Why? Because at some point during the filtering the parameter type was System.Windows.Media.ContainerVisual which doesn't inherit from UIElement and so element would be set to null and ContinueSkipSelfAndChildren would be returned. But I don't want to skip the children because my Canvas is contained inside its "Children" collection and UIElements I want to hittest with are contained in Canvas.
Just as #GuerreroTook said, you should solve this by using WPF's RoutedEvents (more information here.
I have a FlowDocumentScrollViewer I want to automatically scroll to the bottom
when text is added.
<FlowDocumentScrollViewer Name="Scroller">
<FlowDocument Foreground="White" Name="docDebug" FontFamily="Terminal">
<Paragraph Name="paragraphDebug"/>
</FlowDocument>
</FlowDocumentScrollViewer>
In code I add Inlines to the Paragraph, but when there is to much text I would
like to be able to simply scroll down using code instead of having the user doing so.
Any suggestions?
try:
Scroller.ScrollViewer.ScrollToEnd();
Where "Scroller" is the name of your FlowDocumentScrollViewer.
EDIT: I wrote this answer a little too quickly. FlowDocumentScrollViewer does not expose a ScrollViewer property. I had actually extended the FlowDocumentScrollViewer class and implemented the ScrollViewer property myself. Here is the implementation:
/// <summary>
/// Backing store for the <see cref="ScrollViewer"/> property.
/// </summary>
private ScrollViewer scrollViewer;
/// <summary>
/// Gets the scroll viewer contained within the FlowDocumentScrollViewer control
/// </summary>
public ScrollViewer ScrollViewer
{
get
{
if (this.scrollViewer == null)
{
DependencyObject obj = this;
do
{
if (VisualTreeHelper.GetChildrenCount(obj) > 0)
obj = VisualTreeHelper.GetChild(obj as Visual, 0);
else
return null;
}
while (!(obj is ScrollViewer));
this.scrollViewer = obj as ScrollViewer;
}
return this.scrollViewer;
}
}
I've faced a similar problem: I wanted a textual area which could hold my text, is able to wrap it, it fills its parent control and is scrollable.
First I've tried to use a TextBlock with a ScrollViewer and I think it worked, but for some reason I've wanted to use a FlowDocument instead with a FlowDocumentScrollViewer. This latter didn't work and I just couldn't leave the fight unattented so I tried to find solutions and this is how I got here. I've tried to apply the workarounds presented in the answers to the original question, however neither solutions worked out for me (I'm using .NET 4.5, maybe it works in other versions, but I don't know about that).
I've tried using a single FlowDocument by itself also, but the control contains some UI elements I didn't want. So, I came up with another solution.
<ScrollViewer VerticalScrollBarVisibility="Auto">
<FlowDocumentScrollViewer HorizontalScrollBarVisibility="Hidden" VerticalScrollBarVisibility="Hidden">
<FlowDocument>
That's right. It works! Calling ScrollViewer.ScrollToBottom() just works! The ScrollViewer enables scrolling and FlowDocumentScrollViewer removes the UI elements from the FlowDocument. Hope it helps!
Apparently my construction had a flaw, because this way the FlowDocument isn't scrollable via a mouse's scrolling wheel. However setting the FlowDocumentScrollViewer control's IsHitTestVisible property to False solves this.
The other answers given here are a bit puzzling, since I don't see any public "ScrollViewer" property on the FlowDocumentScrollViewer.
I hacked around the problem like this. Beware that this method can return null during initialization:
public static ScrollViewer FindScrollViewer(this FlowDocumentScrollViewer flowDocumentScrollViewer)
{
if (VisualTreeHelper.GetChildrenCount(flowDocumentScrollViewer) == 0)
{
return null;
}
// Border is the first child of first child of a ScrolldocumentViewer
DependencyObject firstChild = VisualTreeHelper.GetChild(flowDocumentScrollViewer, 0);
if (firstChild == null)
{
return null;
}
Decorator border = VisualTreeHelper.GetChild(firstChild, 0) as Decorator;
if (border == null)
{
return null;
}
return border.Child as ScrollViewer;
}
This question was asked 7 years ago, now I have the same problem, and I find a simple solution. The follow code add a Section to Flowdocument which same to Paragraph, then scroll to the end.
private void addSection(Section section)
{
section.Loaded += section_Loaded;
fdoc.Blocks.Add(section);
}
private void section_Loaded(object sender, RoutedEventArgs e)//scroll to end
{
var sec = sender as Section;
if (sec != null)
{
sec.BringIntoView();
}
}
This may be a very late answer, but I've found a way to do this.
//after your FlowDocumentScrollViewer(for example, x:Name="fdsv") loaded
ScrollViewer sv = fdsv.Template.FindName("PART_ContentHost", fdsv) as ScrollViewer;
sv.ScrollToBottom();
sv.ScrollToTop();
sv.ScrollToVerticalOffset(100);
// etc.
Check IScrollInfo and ScrollViewer for details.
I hope this helps you.