c# wpf overlapping controls not receiving mouse events - c#

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.

Related

Wpf rollback|stop transfer of focus and stop other events

I have a really big wpf application with many nested controls using caliburn micro and MVVM.
One control needs a check a condition before(while) the user leaves. If the check fails the focus is transfered back to the said control.
Using Finding ALL child controls WPF
I solved the problem with child control also firing the LostFocus event. And I used the Dispatcher to put the focus back to the control.
my code looks like this:
public void LostKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
var parent = ParentFinder.TryFindParent<ProfileFunctionView>(e.NewFocus as FrameworkElement);
//only fire for outside controls, not child controls
if (parent == null)
{
if (!Apply())
{
var restoreFocus = (System.Threading.ThreadStart)delegate { SyntaxEditor.Focus(); };
Application.Current.Dispatcher.BeginInvoke(restoreFocus);
//stuff should happen here
}
}
}
The problem is that, if I click on a tab control somewhere above my control f.e.. The focus is set back correctly, but than the tab changes. I want to prevent the controls that are clicked outside of my control to react if the condition fails.
Is this possible? Is my approach correct?
Please excuse my confusing title, this problem is really hard to formulate or google. Any help is appreciated.
So, the first thing I would do is use the FocusManager to create a focus scope. Here is the MSDN Documentation.
https://learn.microsoft.com/en-us/dotnet/api/system.windows.input.focusmanager?view=netcore-3.1
Setting a focus scope is essentially a container of elements within the scope. So, for your example I would do something like this.
ProfileFunctionView ()
{
// This will make child controls use this as their FocusScope.
FocusManager.SetIsFocusScope ( this );
}
Next, now that we have created a FocusScope it is easy to check if the element that has focus is in scope or not.
public void LostKeyboardFocus( object sender, KeyboardFocusChangedEventArgs e )
{
if ( sender is UIElement element ) {
var scope = FocusManager.GetFocusScope ( element );
if ( scope is Window ) {
// This is the default focus scope you have actually lost focus.
RestoreFocus();
}
else if ( scope is ProfileFunctionView view )
{
// Haven't lost focus do nothing
return;
}
else
{
// Handle case where focus does not belong to either.
}
}
}
Another method and probably the more appropriate method would be to set the parent elements Focusable property to False. This will prevent the lost focus events from firing in the first place.
Edit: Adding information based on your comment.
If the events are unwanted Disable all controls out side of FocusScope.
ProfileFunctionView ()
{
// This will make child controls use this as their FocusScope.
FocusManager.SetIsFocusScope ( this );
// Better to do this during the Loaded event.
FocusManagerUtility.DisableUIOutsideOfScope ( scope: this );
}
public static class FocusScopeUtility
{
public static void DisableUIOutsideScope ( scope )
{
// Psuedo function to get the top level UIElement in the tree.
var container = GetOwningWindowOrPage ( scope );
// Psuedo function to get traversal of tree
var traversal = GetVisualTreeTraversalAsEnumerable ();
foreach ( var node in traversal )
{
var element = node as UIElement;
if ( FocusScopeUtility.IsInScope ( scope, element )
{
// This line disables the control from receiving input.
element.Enabled = false;
}
}
}
}

Controls in WPF popup sometimes cannot receive input

I have a WPF CheckBox inside a Popup, and I'm finding if it is inside the item template of a TreeView, then the CheckBox does not respond to user input. If it is outside of the TreeView, then there are no problems.
I have created a relatively minimal mock-up here:
https://github.com/logiclrd/TestControlsInPopupsNotWorking
Does anyone know why the CheckBox controls popped up from within the TreeView cannot be checked?
I think this is an oversight in the design of the TreeView. Take a look at this:
Note: Some code excerpts were tidied up to avoid wrapping.
// This method is called when MouseButonDown on TreeViewItem and also listen
// for handled events too. The purpose is to restore focus on TreeView when
// mouse is clicked and focus was outside the TreeView. Focus goes either to
// selected item (if any) or treeview itself
internal void HandleMouseButtonDown()
{
if (!this.IsKeyboardFocusWithin)
{
if (_selectedContainer != null)
{
if (!_selectedContainer.IsKeyboardFocused)
_selectedContainer.Focus();
}
else
{
// If we don't have a selection - just focus the TreeView
this.Focus();
}
}
}
This method is called from TreeViewItem.OnMouseButtonDown, which we can see is a class-level handler that's configured to receive handled events too:
EventManager.RegisterClassHandler(
typeof(TreeViewItem),
Mouse.MouseDownEvent,
new MouseButtonEventHandler(OnMouseButtonDown),
/* handledEventsToo: */ true);
I have verified with the debugger that Handled is set to true by the time the event makes it to the TreeViewItem.
When you press down on the left mouse button over the CheckBox, the CheckBox begins a speculative 'click' operation and marks the event as handled. Normally, an ancestor element wouldn't see a handled event bubble up, but in this case it explicitly asked for them.
The TreeView sees that this.IsKeyboardFocusWithin resolves to false because the focused element is in another visual tree (the popup). It then gives focus back to the TreeViewItem.
Now, if you look in ButtonBase:
protected override void OnLostKeyboardFocus(KeyboardFocusChangedEventArgs e)
{
base.OnLostKeyboardFocus(e);
if (ClickMode == ClickMode.Hover)
{
// Ignore when in hover-click mode.
return;
}
if (e.OriginalSource == this)
{
if (IsPressed)
{
SetIsPressed(false);
}
if (IsMouseCaptured)
ReleaseMouseCapture();
IsSpaceKeyDown = false;
}
}
We see that IsPressed is set to false when focus is lost. If we then go to OnMouseLeftButtonUp, we see this:
bool shouldClick = !IsSpaceKeyDown && IsPressed && ClickMode == ClickMode.Release;
With IsPressed now false, the click operation never completes, all because the TreeViewItem stole focus away from you when you tried to click the button.
As a work-around, I have had success so far with using the NuGet library Ryder (which looks like a freely-usable open-source (MIT license) version of Microsoft Detours) to intercept the HandleMouseButtonDown method in TreeView.
The Ryder library can be found in the NuGet library, and the code behind it can be found here:
https://github.com/6A/Ryder
Hooking the HandleMouseButtonDown method is pretty simple:
var realMethod = typeof(System.Windows.Controls.TreeView).GetMethod("HandleMouseButtonDown", BindingFlags.Instance | BindingFlags.NonPublic);
var replacementMethod = typeof(Program).GetMethod(nameof(TreeView_HandleMouseButtonDown_shim), BindingFlags.Static | BindingFlags.NonPublic);
Redirection.Redirect(realMethod, replacementMethod);
The shim that replaces the method can basically do what the real method does but with a fix that detects the cross-visual-tree focus situation:
static void TreeView_HandleMouseButtonDown_shim(TreeView #this)
{
// Fix as seen in: https://developercommunity.visualstudio.com/content/problem/190202/button-controls-hosted-in-popup-windows-do-not-wor.html
if (!#this.IsKeyboardFocusWithin)
{
// BEGIN NEW LINES OF CODE
var keyboardFocusedControl = Keyboard.FocusedElement;
var focusPathTrace = keyboardFocusedControl as DependencyObject;
while (focusPathTrace != null)
{
if (ReferenceEquals(#this, focusPathTrace))
return;
focusPathTrace = VisualTreeHelper.GetParent(focusPathTrace) ?? LogicalTreeHelper.GetParent(focusPathTrace);
}
// END NEW LINES OF CODE
var selectedContainer = (System.Windows.Controls.TreeViewItem)TreeView_selectedContainer_field.GetValue(#this);
if (selectedContainer != null)
{
if (!selectedContainer.IsKeyboardFocused)
selectedContainer.Focus();
}
else
{
// If we don't have a selection - just focus the treeview
#this.Focus();
}
}
}
Some reflection is needed since this interacts with a private field that is not otherwise exposed from the TreeView class, but as work-arounds go, this is a lot less invasive than what I tried at first, which was importing the entirety of the TreeView class (and related types) from Reference Source into my project in order to alter the one member. :-)

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.

Finding draw order on WPF Canvas.Children Issue

I needed to determine the draw order of the children contained in a canvas. So I ran across this Q/A:
How to compare the relative Z order of two WPF controls that are part of the same logical/visual tree?
This works perfectly most of the time but sometimes I get an error from this code:
private Panel FindWindowRoot(FrameworkElement child)
{
FrameworkElement current = child;
while(current as Window == null)
{
current = (FrameworkElement)VisualTreeHelper.GetParent(current);
}
return ((Window)current).Content as Panel;
}
The call VisualTreeHelper.GetParent(current) ends up throwing an exception "Value cannot be null."
Here's one example of how I use the DrawOrderComparer.
ucVertexControl Control = new ucVertexControl(vertex);
cnvDrawingArea.Children.Add(Control);
SortedChildren = cnvDrawingArea.Children.OfType<FrameworkElement>().OrderByDescending(x => x, new Classes.DrawOrderComparer()).Cast<UIElement>().ToList();
My theory is that the sorting is occurring before the new control even has a parent defined because that gets set later by some event. Problem is I don't know what event that is and if I can listen for it.
Anyone have any ideas?
I believe I found a solution.
I don't know of any event that would fire from the canvas but I do know that each of my user controls have a Loaded event. So I changed this:
ucVertexControl Control = new ucVertexControl(vertex);
cnvDrawingArea.Children.Add(Control);
SortedChildren = cnvDrawingArea.Children.OfType<FrameworkElement>().OrderByDescending(x => x, new Classes.DrawOrderComparer()).Cast<UIElement>().ToList();
To this:
ucVertexControl Control = new ucVertexControl(vertex);
Control.Loaded += new RoutedEventHandler(Control_Loaded);
cnvDrawingArea.Children.Add(Control);
The Control_Loaded function just turns around and calls this method:
private void UpdateSortedChildren()
{
if (cnvDrawingArea.Children.OfType<FrameworkElement>().Any(x => !x.IsLoaded)) return;
SortedChildren = cnvDrawingArea.Children.OfType<FrameworkElement>().OrderByDescending(x => x, new Classes.DrawOrderComparer()).Cast<UIElement>().ToList();
}
Since there are times when I'm adding multiple children in a single call the method will only execute once all the controls have loaded. The errors have gone away so hopefully that was the issue.

.NET 2.0 C# Treeview Drag/Drop within TreeNodes

I am interested in capturing a drag/drop event that will start with the user dragging an existing TreeNode somewhere within the TreeView. While the user is dragging the TreeNode around, I am interested in capturing when the node has been dragged between two tree nodes. When the user does this, I wanted to display a hash mark in-between the tree nodes to designate if the node would be dropped within the node as a child or as a sibling. This hash mark would display either:
- underneath the destination node (to indicate the source node will be dropped as a child of the destination node
OR
- underneath the destination node to the left (to indicate the source node will be dropped as a sibling of the destination node), before or after...
I have made some headway using the DragOver event. I am calculating the mouse location and deriving what the top and bottom nodes are as I drag the mouse around..
int threshold = 8; //Joe(hack)
Point mouseLocation = mouseLocation = treeViewConditions.PointToClient(new Point(e.X, e.Y - threshold));
TreeNode topNode = treeViewConditions.GetNodeAt(mouseLocation);
mouseLocation = treeViewConditions.PointToClient(new Point(e.X + threshold, e.Y));
TreeNode bottomNode = treeViewConditions.GetNodeAt(mouseLocation);
if (topNode != null && bottomNode == null)
{
textBoxDescription.Text = "handling top node";
}
else if (topNode == null && bottomNode != null)
{
textBoxDescription.Text = "handling bottom node";
}
else if (topNode != null && bottomNode != null)
{
if (topNode != bottomNode)
{
textBoxDescription.Text = "between!";
}
else if (topNode == bottomNode)
{
}
}
However in doing this, it just feels dirty. I am wondering if anyone knew of a better way to do accomplish this.
Thanks a ton in advance!
Drawing the 'hash mark' is going to be the real problem. TreeView has a DrawMode property but its DrawItem event doesn't let you draw between the nodes.
You need to handle this by changing the cursor to indicate what is going to happen. Use the GiveFeedback event, set e.UseCustomCursors to false and assign Cursor.Current to a custom cursor that indicates the operation.
This article articulates the same issue and provides an approach somewhat similar to the one you are already following (with the exception that the thresholds are essentially percentages of the height of the tree node). Based on this, and the fact that when I was doing this before, that was the best approach I could find, I think you're basically on track.

Categories