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;
}
}
}
Related
I'm trying to move/rotate/scale a model using the TransformManipulator3D control in UWP.
I do get it to work but the camera does not stop moving when I'm manipulating the model. So when I try to move/rotate/scale the model the camera also moves and this behavior causes the actual move/scale/rotate to be very wonky and not at all smooth or what is expected.
What am I missing and how can I get this behavior to stop from happening?
Thank you.
I think you need to change your camera mouse gesture to a different mouse button.
I got it working without having to change mouse gestures for the camera.
I did the following:
private void viewer_OnMouse3DDown(object sender, MouseDown3DEventArgs e)
{
if(e.HitTestResult != null && e.HitTestResult.ModelHit is MeshGeometryModel3D model && GroupModel.Any(x => x == model))
{
if(ViewerContext.Selected == model)
{
ViewerContext.Selected.PostEffects = null;
ViewerContext.Selected = null;
return;
}
if(ViewerContext.Selected != null)
{
ViewerContext.Selected.PostEffects = null;
}
ViewerContext.Selected = model;
ViewerContext.Selected.PostEffects = string.IsNullOrEmpty(ViewerContext.Selected.PostEffects) ? "border[color:#00FFDE]" : null;
return;
}
if (e.HitTestResult != null && e.HitTestResult.ModelHit is MeshGeometryModel3D pointer && !GroupModel.Any(x => x == pointer))
{
ViewerContext.IsRotationEnabled = false;
ViewerContext.IsMoveEnabled = false;
ViewerContext.IsPanEnabled = false;
return;
}
}
and
private void viewer_OnMouse3DUp(object sender, MouseUp3DEventArgs e)
{
ViewerContext.IsRotationEnabled = true;
ViewerContext.IsMoveEnabled = true;
ViewerContext.IsPanEnabled = true;
}
The pointer variable checks if the ModelHit is part of the TransformManipulator3D control.
I followed the UWP sample for the PostEffects.
The ViewerContext is the ViewModel holding the Viewport3DX properties to know when the camera can again rotate, move or pan.
The GroupModel holds all the models as MeshGeometryModel3D objects.
The TransformManipulator3D control binds to the Selected property on ViewerContext which is also of type MeshGeometryModel3D.
I'm trying to get WPF Slider value after the user finishing to drag the thumb or clicking to move to specific point.
i want to save the new value in db by listening to some kind of event. how can i do this?
i tried the solutions on this question but i end up with nothing -
WPF: Slider with an event that triggers after a user drags
each time it enters the event lots of times
thanks.
xaml:
<Slider Value="{Binding VoipVolume}" MouseLeftButtonUp="slider_MouseLeftButtonUp"/>
codebehind:
public double VoipVolume
{
get { return (double)GetValue(VoipVolumeProperty); }
set
{
SetValue(VoipVolumeProperty, value);
VolumeChanged(value);
}
}
private void VolumeChanged(object value)
{
StationViewModel viewModel = this.DataContext as StationViewModel;
if (viewModel != null)
{
if (end)
{
viewModel.OnVolumeChange((float)VoipVolume);
end = false;
}
}
}
bool end = false;
private void slider_MouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
end = true;
}
Possibly duplicate. hope this may help you out: stackoverflow.com/a/723547/1611490
<Slider Thumb.DragCompleted="MySlider_DragCompleted" />
and under MySlider_DragCompleted event, update your property of the VM or get the value.
if your are using FW 4.5 or above, try using the "Dalay" property.
http://www.jonathanantoine.com/2011/09/21/wpf-4-5-part-4-the-new-bindings-delay-property/
We didn't have a choice, in the end we use TickFrequency because we need to save the changes in the slider to db, and we don't want it to happen for every 0.001..
thanks anyway..
I have a series(10) of selectable TextBoxes. I need to be able to click on one of them and select all the text boxes on which the mouse is moved until the click is released.
I used the following code but I am unable to hit the MouseMove on the other TextBoxes. It always hits the TextBox on which the Click was made.
class SelectableTextBox: TextBox
{
public Boolean IsSelected { get; protected set; }
public void select(Boolean value)
{
this.IsSelected = value;
if (value)
{
this.Background = System.Windows.Media.Brushes.Aqua;
}
else
{
this.Background = System.Windows.Media.Brushes.White;
}
}
}
private void onPreviewMouseLeftButtonDown(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
SelectableTextBox textBox = (SelectableTextBox)sender;
this.SelectionStartedRight = !textBox.IsSelected;
textBox.select(!textBox.IsSelected);
}
private void onPreviewMouseMove(object sender, System.Windows.Input.MouseEventArgs e)
{
SelectableTextBox textBox = (SelectableTextBox)sender;
if (this.SelectionStartedRight)
{
textBox.select(true);
}
}
private void onPreviewMouseLeftButtonUp(object sender, System.Windows.Input.MouseButtonEventArgs e)
{
SelectableTextBox textBox = (SelectableTextBox)sender;
this.SelectionStartedRight = false;
}
Try using MouseEnter event instead of MouseMove. Attach MouseEnter to your selectable textboxes. That would ensure that only they trigger the desired event handler and its code.
If you decide to stay with the global handler, be careful when you convert sender to a specific control type. You need to account for those times when it's not the "expected" control:
SelectableTextBox textBox = sender as SelectableTextBox;
if (textBox != null)
{
// The rest of the code here...
}
select is a Linq keyword, so you might want to rename that method to avoid any conflicts down the road. While I don't think it'd be causing any issues, I would change it.
You don't have to follow this convention, but in C# its customary to use an uppercase for the first letter of a method. Also note that the default access modifier for a class is internal... just want to make sure you're aware of that, as you continue your development.
Your last method, onPreviewMouseLeftButtonUp(...) has the following code in it:
SelectableTextBox textBox = (SelectableTextBox)sender;
Not only is it unsafe, as I've described above, but it also does absolutely nothing.
Lastly... and this is just me, I would probably move the code that handles changing the state of a selectable textbox (selected or not selected) into its class, since it belongs there. Why should anything else be in charge of how it handles its state. Keep things where they belong and you'll have a much easier time testing, debugging and maintaining your code. Don't fall into the "I'll refactor it later" trap... it'll rarely happen.
Here's my crude example. I let the class handle its MouseEnter event and simply check if the Mouse.LeftButton is down at that time. You would have to expand on it, but it should be a solid start:
Made some edits per OP's requests in the comments.
Preview:
XAML:
<Window x:Class="SelectableTextBoxes.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SelectableTextBoxes"
Title="MainWindow" Height="350" Width="525">
<StackPanel>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
<local:SelectableTextBox Height="20" Width="100" Margin="10"/>
</StackPanel>
</Window>
C# (Pardon me for putting the SelectableTextBox into the same file...):
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace SelectableTextBoxes
{
// Move this class into its own file (it's here for prototyping).
public class SelectableTextBox : TextBox
{
private bool _isSelected;
public bool IsSelected
{
get { return _isSelected; }
set
{
if (_isSelected != value)
{
_isSelected = value;
// If the value changes, sets new color.
SetSelectionColor();
}
}
}
public SelectableTextBox()
{
// For handling an initial click if it happens in the textbox.
PreviewMouseDown += SelectableTextBox_PreviewMouseDown;
// For handling selection when mouse enters the textbox
// and left mouse button is down.
MouseEnter += SelectableTextBox_MouseEnter;
// To handle mouse capture (release it).
GotMouseCapture += SelectableTextBox_GotMouseCapture;
}
// Handles the mouse down event within the textbox.
void SelectableTextBox_PreviewMouseDown(object sender, MouseButtonEventArgs e)
{
if (!IsSelected)
{
IsSelected = true;
}
// If one of the Shift keys is down, return, since
// we don't want to deselect others.
if (Keyboard.IsKeyDown(Key.LeftShift) || Keyboard.IsKeyDown(Key.RightShift))
{
return;
}
// This part makes a poor assumption that the parent is
// always going to be a Panel... expand on this code to
// cover other types that may contain more than one element.
var parent = VisualTreeHelper.GetParent(this) as Panel;
if (parent != null)
{
foreach (var child in parent.Children)
{
// If a child is not of a correct type, it'll be null.
var tbx = child as SelectableTextBox;
// This is where we check to see if it's null or this instance.
if (tbx != null && tbx != this)
{
tbx.IsSelected = false;
}
}
}
}
// When textbox receives focus, this event fires... we need to release
// the mouse to continue selection.
void SelectableTextBox_GotMouseCapture(object sender, MouseEventArgs e)
{
ReleaseMouseCapture();
}
// Sets selection state to true if the left mouse button is
// down while entering.
void SelectableTextBox_MouseEnter(object sender, MouseEventArgs e)
{
if (Mouse.LeftButton == MouseButtonState.Pressed)
{
IsSelected = true;
}
}
// Sets the background color based on selection state.
private void SetSelectionColor()
{
if (IsSelected)
{
Background = Brushes.LightCyan;
}
else
{
Background = Brushes.White;
}
}
}
// Window code... should be on its own, but I placed the two
// classes together while prototyping.
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
}
}
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.
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="...">