WPF MouseEnter/MouseLeave/MouseMove Event Tunneling - c#

I am trying to make a button where the contents change when the mouse enters the button.
Currently, this is the code that I'm working with:
Xaml
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<Button x:Name="One"
Content="ONE"
Height="50"
Width="Auto"
MouseEnter="One_OnMouseEnter"
MouseLeave="One_OnMouseLeave" />
<Button x:Name="Two"
PreviewMouseMove="Two_OnMouseEnter">
<Grid>
<Ellipse Fill="Black"
Height="40"
Width="40" />
<Label Content="TWO"
Foreground="White"
VerticalAlignment="Center"
HorizontalAlignment="Center" />
</Grid>
</Button>
</StackPanel>
</StackPanel>
C# Code-Behind File
private void One_OnMouseEnter(object sender, MouseEventArgs e)
{
Button b = sender as Button;
if (b != null)
{
b.Foreground = Brushes.Purple;
b.FontSize = 24;
}
}
private void One_OnMouseLeave(object sender, MouseEventArgs e)
{
Button b = sender as Button;
if (b != null)
{
b.Foreground = Brushes.Black;
b.FontSize = 12;
}
}
private void Two_OnMouseEnter(object sender, MouseEventArgs e)
{
Ellipse el = sender as Ellipse;
if (el != null)
{
el.Height = 60;
el.Width = 60;
el.Fill = Brushes.White;
}
Label l = sender as Label;
if (l != null)
{
l.Foreground = Brushes.Black;
}
Grid g = sender as Grid;
if (g != null)
{
g.Height = 200;
g.Width = 200;
}
}
The first button that's there works as expected.
When the mouse moves onto the "One" button, the text contents changes as expected. Text color changes to purple, and font size increases.
I am trying to do something similar with the second button. Increase the size and change the color of the elliptical, change the color of the label, and change the size of the grid.
The problem is that the second button does not seem to respond as expected. I've tried to use PreviewMouseMove, which I understand to use a Tunneling routing strategy, which should trigger on the Button's child elements. I have used breakpoints to check, and the event seems to only trigger with the sender being the Button.
My question is: Why isn't the event being raised on the children as I've read that the Tunneling routing strategy is supposed to work and what can I do to fix it?
Also, the MouseEnter and MouseLeave events seem to follow the Bubbling routing strategy, but the behavior more closely resembles what I want to do. Can I force this to use a Tunneling routing strategy?
edit:
In order to further explain the goal of this project:
What I am intending to do, is to have a more complete understanding of Event Tunneling in WPF.
This morning, I examined the book more and found a way to make this work when the mouse enters the area of each specific child, which is an improvement.
Here is the new code within the C# Code-Behind File:
private void Two_OnMouseEnter(object sender, MouseEventArgs e)
{
Ellipse el = e.OriginalSource as Ellipse;
if (el != null)
{
el.Height = 60;
el.Width = 60;
el.Fill = Brushes.Orange;
}
TextBlock t = e.OriginalSource as TextBlock;
if (t != null)
{
t.Foreground = Brushes.Blue;
}
Grid g = e.OriginalSource as Grid;
if (g != null)
{
g.Height = 200;
g.Width = 200;
}
}
The difference with this code is that it uses the MouseEventArgs e object within the method signature, casting e.OriginalSource as the object type instead of casting the sender object.
In addition, this code is called using the PreviewMouseMove Event call in the XAML file:
<Button x:Name="Two" PreviewMouseMove="Two_OnMouseEnter">
which appears to only allow the contents to change when the mouse enters the area, but not when the mouse leaves the area. Which leads me back to part of my original question: can I force MouseEnter and MouseLeave to follow the Tunneling Routing Strategy?

It's not doing anything because the event is coming from the Button (which is therefore the sender).
Something like this would be what you are trying to do:
private void Two_OnMouseEnter(object sender, MouseEventArgs e)
{
Button b = sender as Button;
Grid g = b.Content as Grid;
Ellipse el = g.Children[0] as Ellipse;
Label l = g.Children[1] as Label;
g.Height = 200;
g.Width = 200;
el.Height = 60;
el.Width = 60;
el.Fill = Brushes.White;
l.Foreground = Brushes.Black;
}
UPDATE
To explain the tunneling strategy a bit more, consider the following example (I've removed the event handlers for "One" for convenience):
<StackPanel Orientation="Vertical">
<StackPanel Orientation="Horizontal">
<Button x:Name="One" Content="ONE" Height="50" Width="Auto" />
<Button x:Name="Two">
<Grid PreviewMouseMove="Grid_PreviewMouseMove">
<Ellipse Fill="Black" Height="40" Width="40" PreviewMouseMove="Ellipse_PreviewMouseMove" />
<Label Content="TWO" Foreground="White" VerticalAlignment="Center" HorizontalAlignment="Center"
PreviewMouseMove="Label_PreviewMouseMove"/>
</Grid>
</Button>
</StackPanel>
</StackPanel>
And in the code-behind:
private void Grid_PreviewMouseMove(object sender, MouseEventArgs e)
{
Grid g = sender as Grid;
if (g != null)
{
g.Height = 200;
g.Width = 200;
}
System.Diagnostics.Debug.WriteLine("Sender: " + sender.GetType() + "; Source: " + e.Source.GetType());
}
private void Ellipse_PreviewMouseMove(object sender, MouseEventArgs e)
{
Ellipse el = sender as Ellipse;
if (el != null)
{
el.Height = 60;
el.Width = 60;
el.Fill = Brushes.White;
}
System.Diagnostics.Debug.WriteLine("Sender: " + sender.GetType() + "; Source: " + e.Source.GetType());
}
private void Label_PreviewMouseMove(object sender, MouseEventArgs e)
{
Label l = sender as Label;
if (l != null)
{
l.Foreground = Brushes.Black;
}
System.Diagnostics.Debug.WriteLine("Sender: " + sender.GetType() + "; Source: " + e.Source.GetType());
}
These methods could theoretically be refactored into one method, with some logic applied to detect the Type of the sender, but it would still be in the XAML three times.
One thing to notice is that the Grid won't fire its own events unless you set it's Background property (e.g., to Transparent), though it will still fire when the Ellipse and Label fire.
Another thing to notice is that when the Label fires the event, i.e. when you move the mouse over the Label, the event handler for the Ellipse is not called. This is because the tunneling strategy proceeds logically down the tree, rather than visually. If you look at the "Source: " part in the message in the Output window you'll see what I mean. The same would also be true for bubbling events.
As a general observation, the thing with this kind of strategy is that with events firing from different elements, your control isn't really functioning as one single Button, which looks to be the intention. If you move the mouse in slowly, you'll notice the Ellipse fires first, then the Label once you reach that.
There aren't any PreviewMouseEnter or PreviewMouseLeave events, so tunneling couldn't be used for that anyway.

Related

Prevent WPF controls from overlapping on MouseMove event

I'm working on a dynamic C# WPF application (on Windows 10) that uses a fullscreen Grid. Controls are added to the grid dynamically at runtime (which are managed in a Dictionary<>) and I recently added code to move the controls along the grid with the mouse (also at runtime) using a TranslateTransform (which I am now doubting the viability of).
Is there a way I can prevent the controls from overlapping or "sharing space" on the grid when moving them? In other words, adding some sort of collision detection. Would I use an if statement to check the control margin ranges or something? My move events are shown below:
MainWindow.xaml.cs:
public partial class MainWindow : Window
{
// Orientation variables:
public bool _isInDrag = false;
public Dictionary<object, TranslateTransform> PointDict = new Dictionary<object, TranslateTransform();
public Point _anchorPoint;
public Point _currentPoint;
public MainWindow()
{
InitializeComponent();
}
public static void Control_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (_isInDrag)
{
var element = sender as FrameworkElement;
element.ReleaseMouseCapture();
_isInDrag = false;
e.Handled = true;
}
}
public static void Control_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var element = sender as FrameworkElement;
_anchorPoint = e.GetPosition(null);
element.CaptureMouse();
_isInDrag = true;
e.Handled = true;
}
public static void Control_MouseMove(object sender, MouseEventArgs e)
{
if (_isInDrag)
{
_currentPoint = e.GetPosition(null);
TranslateTransform tt = new TranslateTransform();
bool isMoved = false;
if (PointDict.ContainsKey(sender))
{
tt = PointDict[sender];
isMoved = true;
}
tt.X += _currentPoint.X - _anchorPoint.X;
tt.Y += (_currentPoint.Y - _anchorPoint.Y);
(sender as UIElement).RenderTransform = tt;
_anchorPoint = _currentPoint;
if (isMoved)
{
PointDict.Remove(sender);
}
PointDict.Add(sender, tt);
}
}
}
MainWindow.xaml (example):
<Window x:Name="MW" x:Class="MyProgram.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:MyProgram"
mc:Ignorable="d"
Title="MyProgram" d:DesignHeight="1080" d:DesignWidth="1920" ResizeMode="NoResize" WindowState="Maximized" WindowStyle="None">
<Grid x:Name="MyGrid" />
<Image x:Name="Image1" Source="pic.png" Margin="880,862,0,0" Height="164" Width="162" HorizontalAlignment="Left" VerticalAlignment="Top" MouseLeftButtonDown="Control_MouseLeftButtonDown" MouseLeftButtonUp="Control_MouseLeftButtonUp" MouseMove="Control_MouseMove" />
<TextBox x:Name="Textbox1" Margin="440,560,0,0" HorizontalAlignment="Left" VerticalAlignment="Top" MouseLeftButtonDown="Control_MouseLeftButtonDown" MouseLeftButtonUp="Control_MouseLeftButtonUp" MouseMove="Control_MouseMove" />
</Window>
Edit: It seems that moving a control with a TranslateTransform does not change the margin for that control. Not sure why.
Edit 2: Not getting much traction. If anyone needs clarification on anything, please ask.
Edit 3: Pretty sure I can't use TranslateTransform because it does not change the margin of a given control. Is there an alternative?
Edit 4: Added some 'boilerplate' code for those who want to copy & paste. Let me know if you have any questions about it.
TL;DR: Demo from the bottom of this answer
When you want to modify your UI without adding event handlers to every single control, the way to go is with Adorners. Adorners are (as the name implies) controls that adorn another control to add additional visuals or as in your case functionality. Adorners reside in an AdornerLayer which you can either add yourself or use the one that every WPF Window already has. The AdornerLayer is on top of all your other controls.
You never mentioned what should happen when the user lets go of the mouse button when controls overlap so I just reset the control to its original position if that happens.
At this point I'd usually explain what to keep in mind when moving controls but since your original example even contains the CaptureMouse people usually forget, I think you'll understand the code without further explanation :)
A couple of things you might want to add / improve:
A snap to grid feature (pixel precise movement can be a bit overwhelming for the average user)
Take RenderTransform, LayoutTransform and non-rectangular shapes (if needed) into account when calculating the overlap
Move the editing functionality (enable, disable, etc.) into a separate control and add a dedicated AdornerLayer
Disable interactive controls (Buttons, TextBoxes, ComboBoxes, etc.) in edit-mode
Cancel movement when the user presses Esc
Restrict movement to the bounds of the parent container done
Move the active Adorner to the top of the AdornerLayer
Let the user move multiple controls at once (typically by selecting them with Ctrl)
Previously unanswered question:
Are you saying controls are no longer assigned a margin when using TranslateTransform?
Not at all - You could use a combination of Grid.Row, Grid.Column, Margin, RenderTransform and LayoutTransform but then it would be a nightmare to determine where the control is actually displayed. If you stick with one (In this case for example Margin or LayoutTransform) it is much easier to work with and keep track of. If you ever find yourself in a situation where you need more than one at the same time, you would have to find the actual position by determining the corners of the control by transforming (0, 0) and (ActualWidth, ActualHeight) with TransformToAncestor. Trust me, you don't want to go there - keep it simple, stick with one of them.
The below code is not the "holy grail of how to move things" but it should give you an idea of how to do it and what else you could do with it (resize, rotate, remove controls, etc.). The layouting is based purely on the Left and Top margin of the controls. It shouldn't be to hard to swap out all Margins for LayoutTransforms if you prefer that, as long as you keep it consistent.
Move Adorner
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
public class MoveAdorner : Adorner
{
// The parent of the adorned Control, in your case a Grid
private readonly Panel _parent;
// Same as "AdornedControl" but as a FrameworkElement
private readonly FrameworkElement _child;
// The visual overlay rectangle we can click and drag
private readonly Rectangle _rect;
// Our own collection of child elements, in this example only _rect
private readonly UIElementCollection _visualChildren;
private bool _down;
private Point _downPos;
private Thickness _downMargin;
private List<Rect> _otherRects;
protected override int VisualChildrenCount => _visualChildren.Count;
protected override Visual GetVisualChild(int index)
{
return _visualChildren[index];
}
public MoveAdorner(FrameworkElement adornedElement) : base(adornedElement)
{
_child = adornedElement;
_parent = adornedElement.Parent as Panel;
_visualChildren = new UIElementCollection(this,this);
_rect = new Rectangle
{
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Stretch,
StrokeThickness = 1,
};
SetColor(Colors.LightGray);
_rect.MouseLeftButtonDown += RectOnMouseLeftButtonDown;
_rect.MouseLeftButtonUp += RectOnMouseLeftButtonUp;
_rect.MouseMove += RectOnMouseMove;
_visualChildren.Add(_rect);
}
private void SetColor(Color color)
{
_rect.Fill = new SolidColorBrush(color) {Opacity = 0.3};
_rect.Stroke = new SolidColorBrush(color) {Opacity = 0.5};
}
private void RectOnMouseMove(object sender, MouseEventArgs args)
{
if (!_down) return;
Point pos = args.GetPosition(_parent);
UpdateMargin(pos);
}
private void UpdateMargin(Point pos)
{
double deltaX = pos.X - _downPos.X;
double deltaY = pos.Y - _downPos.Y;
Thickness newThickness = new Thickness(_downMargin.Left + deltaX, _downMargin.Top + deltaY, 0, 0);
//Restrict to parent's bounds
double leftMax = _parent.ActualWidth - _child.ActualWidth;
double topMax = _parent.ActualHeight - _child.ActualHeight;
newThickness.Left = Math.Max(0, Math.Min(newThickness.Left, leftMax));
newThickness.Top = Math.Max(0, Math.Min(newThickness.Top, topMax));
_child.Margin = newThickness;
bool overlaps = CheckForOverlap();
SetColor(overlaps ? Colors.Red : Colors.Green);
}
// Check the current position for overlaps with all other controls
private bool CheckForOverlap()
{
if (_otherRects == null || _otherRects.Count == 0)
return false;
Rect thisRect = GetRect(_child);
foreach(Rect otherRect in _otherRects)
if (thisRect.IntersectsWith(otherRect))
return true;
return false;
}
private Rect GetRect(FrameworkElement element)
{
return new Rect(new Point(element.Margin.Left, element.Margin.Top), new Size(element.ActualWidth, element.ActualHeight));
}
private void RectOnMouseLeftButtonUp(object sender, MouseButtonEventArgs args)
{
if (!_down) return;
Point pos = args.GetPosition(_parent);
UpdateMargin(pos);
if (CheckForOverlap())
ResetMargin();
_down = false;
_rect.ReleaseMouseCapture();
SetColor(Colors.LightGray);
}
private void ResetMargin()
{
_child.Margin = _downMargin;
}
private void RectOnMouseLeftButtonDown(object sender, MouseButtonEventArgs args)
{
_down = true;
_rect.CaptureMouse();
_downPos = args.GetPosition(_parent);
_downMargin = _child.Margin;
// The current position of all other elements doesn't have to be updated
// while we move this one so we only determine it once
_otherRects = new List<Rect>();
foreach (FrameworkElement child in _parent.Children)
{
if (ReferenceEquals(child, _child))
continue;
_otherRects.Add(GetRect(child));
}
}
// Whenever the adorned control is resized or moved
// Update the size of the overlay rectangle
// (Not 100% necessary as long as you only move it)
protected override Size MeasureOverride(Size constraint)
{
_rect.Measure(constraint);
return base.MeasureOverride(constraint);
}
protected override Size ArrangeOverride(Size finalSize)
{
_rect.Arrange(new Rect(new Point(0,0), finalSize));
return base.ArrangeOverride(finalSize);
}
}
Usage
private void DisableEditing(Grid theGrid)
{
// Remove all Adorners of all Controls
foreach (FrameworkElement child in theGrid.Children)
{
var layer = AdornerLayer.GetAdornerLayer(child);
var adorners = layer.GetAdorners(child);
if (adorners == null)
continue;
foreach(var adorner in adorners)
layer.Remove(adorner);
}
}
private void EnableEditing(Grid theGrid)
{
foreach (FrameworkElement child in theGrid.Children)
{
// Add a MoveAdorner for every single child
Adorner adorner = new MoveAdorner(child);
// Add the Adorner to the closest (hierarchically speaking) AdornerLayer
AdornerLayer.GetAdornerLayer(child).Add(adorner);
}
}
Demo XAML
<Grid>
<Button Content="Enable Editing" HorizontalAlignment="Left" Margin="10,10,0,0" VerticalAlignment="Top" Width="100" Click="BtnEnable_Click"/>
<Button Content="Disable Editing" HorizontalAlignment="Left" Margin="115,10,0,0" VerticalAlignment="Top" Width="100" Click="BtnDisable_Click"/>
<Grid Name="grid" Background="AliceBlue" Margin="10,37,10,10">
<Button Content="Button" HorizontalAlignment="Left" Margin="83,44,0,0" VerticalAlignment="Top" Width="75"/>
<Ellipse Fill="#FFF4F4F5" HorizontalAlignment="Left" Height="100" Margin="207,100,0,0" Stroke="Black" VerticalAlignment="Top" Width="100"/>
<Rectangle Fill="#FFF4F4F5" HorizontalAlignment="Left" Height="100" Margin="33,134,0,0" Stroke="Black" VerticalAlignment="Top" Width="100"/>
</Grid>
</Grid>
Expected Result
When editing is disabled controls cannot be moved, interactive controls can be clicked / interacted with without obstruction. When editing mode is enabled, each control is overlayed with an adorner that can be moved. If the target position overlaps with another control, the adorner will turn red and the margin will be reset to the initial position if the user lets go of the mouse button.
There is no other way then to check if there control exists on place where you are moving.
Since you are moving UI elements a lot it is better to use canvas instead of grid where you can layout elements with Top and Left parameters.
Here is modified code of yours that do that
public partial class MainWindow : Window
{
public bool _isInDrag = false;
public Dictionary<object, TranslateTransform> PointDict = new Dictionary<object, TranslateTransform>();
public Point _anchorPoint;
public Point _currentPoint;
public MainWindow()
{
InitializeComponent();
}
public void Control_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
if (_isInDrag)
{
var element = sender as FrameworkElement;
element.ReleaseMouseCapture();
Panel.SetZIndex(element, 0);
_isInDrag = false;
e.Handled = true;
}
}
public void Control_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
var element = sender as FrameworkElement;
_anchorPoint = e.GetPosition(null);
element.CaptureMouse();
Panel.SetZIndex(element, 10);
_isInDrag = true;
e.Handled = true;
}
public void Control_MouseMove(object sender, MouseEventArgs e)
{
if (_isInDrag)
{
_currentPoint = e.GetPosition(null);
FrameworkElement fw = sender as FrameworkElement;
if (fw != null)
{
FrameworkElement fwParent = fw.Parent as FrameworkElement;
if (fwParent != null)
{
Point p = new Point(_currentPoint.X - _anchorPoint.X + Canvas.GetLeft((sender as UIElement)), _currentPoint.Y - _anchorPoint.Y + Canvas.GetTop((sender as UIElement)));
List<HitTestResult> lst = new List<HitTestResult>()
{
VisualTreeHelper.HitTest(fwParent , p),
VisualTreeHelper.HitTest(fwParent, new Point(p.X + fw.Width, p.Y)),
VisualTreeHelper.HitTest(fwParent, new Point(p.X, p.Y + fw.Height)),
VisualTreeHelper.HitTest(fwParent, new Point(p.X + fw.Width, p.Y +fw.Height)),
};
bool success = true;
foreach (var item in lst)
{
if (item != null)
{
if (item.VisualHit != sender && item.VisualHit != fwParent && fw.IsAncestorOf(item.VisualHit) == false)
{
success = false;
break;
}
}
}
if (success)
{
Canvas.SetTop((sender as UIElement), p.Y);
Canvas.SetLeft((sender as UIElement), p.X);
_anchorPoint = _currentPoint;
}
}
}
}
}
}
Xaml
<Window x:Class="ControlsOverlapWpf.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:ControlsOverlapWpf"
mc:Ignorable="d"
Title="MyProgram" d:DesignHeight="500" d:DesignWidth="500" ResizeMode="NoResize" WindowState="Normal" WindowStyle="None">
<Canvas Background="Pink">
<Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown" PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
<Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown" PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
<Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown" PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
<Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown" PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
<Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown" PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
<Button Canvas.Top=" 200" Canvas.Left="200" Height="150" Width="150" Background="Aqua" HorizontalAlignment="Left" VerticalAlignment="Top" PreviewMouseLeftButtonDown="Control_MouseLeftButtonDown" PreviewMouseLeftButtonUp="Control_MouseLeftButtonUp" PreviewMouseMove="Control_MouseMove" />
</Canvas>
</Window>

Viewport3D Mouse Event doesn't fire when hitting background

I set up a Viewport3D with a MouseEventHandler
[...]
Main3DWindow.MouseUp += new MouseButtonEventHandler(mainViewport_MouseUp);
[...]
void mainViewport_MouseUp (object sender, MouseButtonEventArgs e) {
Point location = e.GetPosition(Main3DWindow);
ModelVisual3D result = GetHitTestResult(location);
if (result == null) {
_CurrentData.Unselect();
return;
}
_CurrentData.SelectItemFromObjectList(result);
}
And it works pretty fine when an object is clicked.
My expectation was: If no object is clicked (because the user clicked at the background) the result is null. But in fact the mainViewport_MouseUp-method is not even called.
My question: how can i detect clicks on the background of the Viewport3D?
It is as you wrote, it wont be fired.
I solved that by defining events on border and put viewport into border. Sample is from XAML:
<Border
MouseWheel="mainViewport_MouseWheel"
MouseMove="mainViewport_MouseMove"
MouseLeftButtonDown="mainViewport_MouseLeftButtonDown"
Background="Black">
<Viewport3D
Name="mainViewport"
ClipToBounds="True"
Grid.Row="0"
Grid.Column="0"
Grid.ColumnSpan="3"
Margin="0,0,0,0">
.....
</Viewport3D>
</Border>
And in the code:
private void mainViewport_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
Point location = e.GetPosition(mainViewport);
try
{
ModelVisual3D result = (ModelVisual3D)GetHitTestResult(location);
//some code.......
}
catch
{
//some code .......
}
}

How to detect if the scroll viewer reaches bottom in winrt

I'm wondering what's the best approach to detect if a ScrollViewer reaches the bottom, right etc.
I think I can achieve that by using both PointerWheelChanged for mouse and ManipulationDelta for touch. In these event handlers, I can record the HorizontalOffset to find out when will the scroller reach the end. But I think there could be a better way to do it.
I've found this article. But the compression visual states seem not working in winrt. The CurrentStateChanging event method is not getting called.
I also checked another article. But it just works for scroll bar, not a generic approach.
Anyone knows what's the best way to solve this problem?
XAML:
<ScrollViewer
x:Name="sv"
ViewChanged="OnScrollViewerViewChanged">
<Rectangle
x:Name="rect"
Width="2000"
Height="2000"
Fill="Yellow"
Margin="10" />
</ScrollViewer>
Code behind:
private void OnScrollViewerViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
var verticalOffset = sv.VerticalOffset;
var maxVerticalOffset = sv.ScrollableHeight; //sv.ExtentHeight - sv.ViewportHeight;
if (maxVerticalOffset < 0 ||
verticalOffset == maxVerticalOffset)
{
// Scrolled to bottom
rect.Fill = new SolidColorBrush(Colors.Red);
}
else
{
// Not scrolled to bottom
rect.Fill = new SolidColorBrush(Colors.Yellow);
}
}
For UWP I got it like this
<ScrollViewer Name="scroll" ViewChanged="scroll_ViewChanged">
<ListView />
</ScrollViewer>
private void scroll_ViewChanged(object sender, ScrollViewerViewChangedEventArgs e)
{
var scrollViewer = (ScrollViewer)sender;
if (scrollViewer.VerticalOffset == scrollViewer.ScrollableHeight)
btnNewUpdates.Visibility = Visibility.Visible;
}
private void btnNewUpdates_Click(object sender, RoutedEventArgs e)
{
itemGridView.ScrollIntoView(itemGridView.Items[0]);
btnNewUpdates.Visibility = Visibility.Collapsed;
}

What is the correct way to code the nested methods?

I have created a registration form in silverlight 4, where i have a large number of text-boxes, in front of each text box i have placed a text-block as a required field validator, when any of the textbox left empty while loosing focus, the textblock placed in front of it must become red.
textboxes named textbox1, textbox2 ... and so as the textblocks
the problem is, i do not want code the specific method for each specific textbox, all i want to do is to complete such in just two three methods
here i did some coding which doesn't seems to be correct
private void textBox_LostFocus(object sender, RoutedEventArgs e)
{
var textBox = (TextBox) sender;
if (textbox.Text == "")
{
var textblock = "textblock" + textBox.Name.Remove(0,7);
TextblockColorChange(textblock);
}
}
private void TextblockColorChange(object sender)
{
var textblock = (TextBlock) sender;
textblock.Foreground= new SolidColorBrush(Colors.Red);
}
please suggest some better way to do so..
I'd create a UserControl that contains the TextBlock and the TextBox and use this UserControl everywhere you currently have the TextBlock and TextBox combination. Then this Usercontrol would have the LostFocus logic inside it and update the TextBlock appropriately. This prevents the need to figure out the right name of the control to update.
you need something like this,
XAML part:
<Grid x:Name="LayoutRoot" Background="White">
<StackPanel Orientation="Horizontal" Height="25">
<TextBox Width="150" LostFocus="TextBox_LostFocus"/>
<TextBlock Text="*" Foreground="#FF0000" VerticalAlignment="Center" Margin="10,0,0,0" Visibility="Collapsed"/>
</StackPanel>
</Grid>
C# Part:
private void TextBox_LostFocus(object sender, RoutedEventArgs e)
{
var textbox = sender as TextBox;
if(textbox == null) return;
var stackPanel = textbox.Parent as StackPanel;
if(stackPanel == null) return;
var textBlock = stackPanel.Children.Where(a => a is TextBlock).FirstOrDefault();
if (textBlock == null) return;
if (string.IsNullOrEmpty(textbox.Text)) textBlock.Visibility = Visibility.Visible;
else textBlock.Visibility = Visibility.Collapsed;
}
Whilst I actually prefer Bills approach (although I'd be inclined to use a Templated Control) here is another alternative which is quite fun. In your xaml use this sort of markup:-
<TextBlock Text="Enter Value 1" Foreground="{Binding Tag, ElementName=textBox1, TargetNullValue=Black}" />
<TextBox x:Name="textBox1" LostFocus="txt_LostFocus" />
Your common txt_LostFocus can look like this:-
private void txt_LostFocus(object sender, RoutedEventArgs e)
{
TextBox txt = ((TextBox)sender);
if (String.IsNullOrEmpty(txt.Text))
{
txt.Tag = new SolidColorBrush(Colors.Red);
}
else
{
txt.Tag = null;
}
}
var textblock = "textblock" + textBox.Name.Remove(0,7);
TextblockColorChange(textblock);
This code above will just send a string to TextblockColorChange()
You don't show any other code, but I'm guessing you want to do a FindControl or FindControl like search on that string before passing the result to your code.

PreviewMouseMove firing twice

I have a problem with a simple code. I was looking for a few hours a solution, but no effects.
I have a Canvas and Rectangle. I move Rectangle, if the cursor is outside, delegate pMouseMove fires only once for each pixel. Conversely, if the cursor is at the Rectangle, delagate fires twice for each pixel. I want to run it only once, as if it were outside the Rectangle, how to do it?
XAML:
<Window x:Class="WpfApplication1.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="MainWindow" Height="350" Width="525">
<Canvas x:Name="Can" Height="257" Width="503" Background="Gray">
<TextBox Name="tb" Width="77" Height="20" Canvas.Left="0" Canvas.Top="-21"/>
</Canvas>
</Window>
Code-behind:
public partial class MainWindow : Window
{
Rectangle rect = new Rectangle();
private static int i;
private static string s;
public MainWindow()
{
InitializeComponent();
rect.Height = 50;
rect.Width = 50;
rect.Fill = Brushes.Black;
Can.Children.Add(rect);
Can.PreviewMouseMove += pMouseMove;
}
private void pMouseMove(object sender, MouseEventArgs e)
{
//cursor over Rectangle
Canvas.SetTop(rect, e.GetPosition(Can).Y + 10);
Canvas.SetLeft(rect, e.GetPosition(Can).X + 10);
//cursor outside Rectangle
//Canvas.SetTop(rect, e.GetPosition(Can).Y - 10);
//Canvas.SetLeft(rect, e.GetPosition(Can).X - 10);
//Counter
i++;
tb.Text = i.ToString();
//e.Handled = true;
}
}
Sorry for my bad english
Events in WPF are Routed Events, which effectively means that your Canvas will receive events from the canvas itself and everything inside the canvas. As you noticed, the Canvas's PreviewMouseMove event is receiving events from both the Canvas and the Rectangle.
[Update]
I ran your code and added a line to check the value of the e.OriginalSource to see what originally raised the event. Like this:
private void pMouseMove(object sender, MouseEventArgs e)
{
// print out e.OriginalSource just for learning purposes
Console.WriteLine("OriginalSource:" + e.OriginalSource.ToString());
}
My original answer was to check e.OriginalSource's type because I thought you were receiving the same event twice. But I now see what you are saying: if e.OriginalSource is the Rectangle, the PreviewMouseMove event gets raised twice as often compared to when e.OriginalSource is the Canvas. There's something internal to the Rectangle's implementation that is doing this (only way to find out is to use a tool like Reflector to see the internal logic. However, there is a workaround where you can make the frequency of the event consistent.
You can set rect.IsHitTestVisible = false; and that will eliminate the Rectangle from sending events and being e.OriginalSource -- so that means all PreviewMouseMove events will come from the Canvas. Then you can use VisualTreeHelper.HitTest to check to see if the mouse position is inside the Rectangle.
I just ran this code below and I think this is a way to guarantee consistent raising of events, but still have your hit test capability.
In the constructor:
rect.Fill = Brushes.Black;
rect.IsHitTestVisible = false;
Can.Children.Add(rect);
In the PreviewMouseMove handler:
private void pMouseMove(object sender, MouseEventArgs e)
{
// Debug.WriteLine(e.OriginalSource.ToString());
HitTestResult result = VisualTreeHelper.HitTest(rect, e.GetPosition(sender as UIElement));
if (result != null) {
Debug.WriteLine("Mouse inside rect")
}
else {
Debug.WriteLine("Mouse outside rect");
}
}

Categories

Resources