Detect Click on a Child Control Hidden by Other Overlapped Child Control - winforms

I need a clever algorithm to detect which edge was clicked, while not looking into every edge in the graph.
As you can see in the image above, the areas marked by dots show clicked edge incorrectly because they're overlapped by edge [2->3]
I tried GetChildAtPoint but it returns only the topmost child.
class Edge : Control {
public Movable From {get;set;}
public Movable To {get;set;}
...}
void Edge_Click(object sender, EventArgs e)
{
var clicked = (Edge)sender;
var mpc = this.PointToClient(MousePosition);
var clk0 = this.GetChildAtPoint(mpc) as Edge;
Edge clk = null;
if (clk0 != null) {
if (clk0.GetType() == typeof(Edge))
clk = clk0 as Edge;
}
MessageBox.Show(string.Format("({0}, {1}): {2} - {3}=>{4} - {5}: {6}",
mpc.X, mpc.Y, clicked.Parent.GetType().ToString(),
clicked.From.Text, clicked.To.Text,
(clicked == clk).ToString(),
clk == null ? "null" : clk.From.Text + "=>" + clk.To.Text));
}
How could I iterate over all child windows/controls under some point?

The only thing I can think of is to iterate through your Form's Control Collection and check if the Bounds of each control contains your point.
This may or may not be feasible for you depending on the number of Controls that you have.
foreach (Control c in this.Controls )
{
if (c.Bounds.Contains(mpc))
{
//process the match here
}
}

Related

WPF Canvas OnRender behavior

I try to realize the correct behavior in the overrided OnRender method in the custon Canvas. I face with the magic number 4 and can't find it's source.
Example of my override is below. The managers like m_<ManagerName>M are separeted classes that do the draw logic.
public class MyCanvas : Canvas {
...
int m_counter = 0;
protected override void OnRender(DrawingContext dc)
{
m_counter++;
if (m_counter == 4 && Leasings != null)
{
m_counter = 0;
ClearManagers();
if (m_gridM != null)
{
var rowsI = Leasings.Select(l => l.RowIndex).Distinct();
foreach (var i in rowsI)
m_gridM.DrawRow(i, dc);
m_gridM.DrawColumns(DayCount, dc);
}
if (m_barM != null)
{
foreach (var bm in Leasings)
{
m_barM.DrawBar(bm, dc);
}
}
if (m_textM != null)
{
m_textM.Load(Leasings);
m_textM.DrawText(dc);
}
}
base.OnRender(dc);
}
...
}
The problem is I can't understand why this method have called 4th times in a row on window loaded (or what the first action is) and 3 or 2 times when the binded data has been changed.
I have got that draw have happend successfully on 4th iteration only. I don't know why and added the counter with the condition for 4th iteration. It works fine.
When I tried to redraw I got only 3 or sometimes 2 calls of the OnRender. So the condition for draw is not working and even if I change it for this case the data have not been drawed.
What my actions do fire the OnRender method? Why the draw is successfull on the last call OnLoad and not hapend (or visible) when I try to refresh the data?

WPF VirtualizingWrapPanel Selected Item Change cause scroll

I have implemented a virtualizing panel that works pretty well, the panel implements IScrollInfo. I am in the process of getting it to work using a keyboard. The Panel is used in a ListView as the ListView.ItemsPanel. When the user presses down the selected item correctly changes but no scrolling occurs and when you reach the last visible item you can not press down any longer. I am trying to figure out how to have the panel aware of the selected item and scroll appropriately. I have tried searching around but I cant seem to find anything related to what I want to do. A point in the right direction would be greatly appreciated.
Edit: Here is the code for the VirtualizingWrapPanel
So I found a solution that works fairly well, I am not sure if it is the best route to take but it seems to work fine.
In the ArrangeOverride method while looping over the children I do the following.
var listViewItem = child as ListViewItem;
if (listViewItem != null)
{
listViewItem.Selected -= ListViewItemSelected;
listViewItem.Selected += ListViewItemSelected;
}
In MeasureOverride I make a call to CleanUpItems in here we will need to unsubscribe from the selected event.
var listViewItem = children[i] as ListViewItem;
if (listViewItem != null)
{
listViewItem.Selected -= ListViewItemSelected;
}
I also added the following three functions.
private void ListViewItemSelected(object sender, RoutedEventArgs e)
{
var listViewItem = sender as ListViewItem;
if(listViewItem == null) return;
var content = listViewItem.Content as CollectionViewGroup;
if(content != null) return; //item is a group header dont click
var items = ItemContainerGenerator as ItemContainerGenerator;
if(items == null) return;
BringIndexIntoView(items.IndexFromContainer(listViewItem));
listViewItem.Focus();
}
protected override void BringIndexIntoView(int index)
{
var offset = GetOffsetForFirstVisibleIndex(index);
SetVerticalOffset(offset.Height);
}
private Size GetOffsetForFirstVisibleIndex(int index)
{
int childrenPerRow = CalculateChildrenPerRow(_extent);
var actualYOffset = ((index / childrenPerRow) * ChildDimension.Height) - ((ViewportHeight - ChildDimension.Height) / 2);
if (actualYOffset < 0)
{
actualYOffset = 0;
}
Size offset = new Size(_offset.X, actualYOffset);
return offset;
}
The GetOffsetForFirstVisibleIndex function will probably vary depending on your implementation but that should be enough info if anyone else is having trouble coming up with a solution.

Get the scroll position of a WPF TextBox

I need to add some decoration to the contents of a WPF TextBox control. That works fine basically, I can get the position of specified character indices and layout my other elements accordingly. But it all breaks when the TextBox is scrolled. My layout positions don't match with the displayed text anymore because it has moved elsewhere.
Now I'm pretty surprised that the TextBox class doesn't provide any information about its scrolling state, nor any events when the scrolling has changed. What can I do now?
I used Snoop to find out whether there is some scrolling sub-element that I could ask, but the ScrollContentPresenter also doesn't have any scrolling information available. I'd really like to put my decoration elements right into the scrolled area so that the scrolling can affect them, too, but there can only be a single content control and that's one of the TextBox internals already.
I'm not sure how to capture an event when the textbox has been scrolled (probably use narohi's answer for that), but there is a simple way to see what the current scroll position is:
// Gets or sets the vertical scroll position.
textBox.VerticalOffset
(From http://msdn.microsoft.com/en-us/library/system.windows.controls.primitives.textboxbase.verticaloffset(v=vs.100).aspx)
I'm using it to see if the textbox is scrolled to the end, like this:
public static bool IsScrolledToEnd(this TextBox textBox)
{
return textBox.VerticalOffset + textBox.ViewportHeight == textBox.ExtentHeight;
}
You can get the ScrollViewer with this method by passing in your textbox as the argument and the type ScrollView. Then you may subscribe to the ScrollChanged event.
public static T FindDescendant<T>(DependencyObject obj) where T : DependencyObject
{
if (obj == null) return default(T);
int numberChildren = VisualTreeHelper.GetChildrenCount(obj);
if (numberChildren == 0) return default(T);
for (int i = 0; i < numberChildren; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
if (child is T)
{
return (T)(object)child;
}
}
for (int i = 0; i < numberChildren; i++)
{
DependencyObject child = VisualTreeHelper.GetChild(obj, i);
var potentialMatch = FindDescendant<T>(child);
if (potentialMatch != default(T))
{
return potentialMatch;
}
}
return default(T);
}
Example:
public MainWindow()
{
InitializeComponent();
Loaded += new RoutedEventHandler(MainWindow_Loaded);
}
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
ScrollViewer s = FindDescendant<ScrollViewer>(txtYourTextBox);
s.ScrollChanged += new ScrollChangedEventHandler(s_ScrollChanged);
}
void s_ScrollChanged(object sender, ScrollChangedEventArgs e)
{
// check event args for information needed
}

Record items visible to user in ListBox

I have a ListBox or DataGrid filled with thousands of entries. I would like to know items that the user has looked at (scrolling, searching or otherwise). How can I tell what is visible to the user in the ListBox?
Bonus: Set a timer so that the item has to be shown for a minimum of N milliseconds (in the event the user is just pulling down the scrollbar).
Update: This is a near duplicate of Get items in view within a listbox - but the solution it gives, using "SelectedItems", is not sufficient. I need to know the items whether they are selected or not!
All you need to do is to get the underlying StackPanel that's inside the ListBox. It has enough information about which elements are showing. (It implements the interface IScrollInfo).
To get the underlying StackPanel (or actually VirtualizingStackPanel) from a given ListBox, we'll have to use VisualTreeHelper to go through the Visual Tree and look for the VirtualizingStackPanel, like so:
private VirtualizingStackPanel GetInnerStackPanel(FrameworkElement element)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(element); i++)
{
var child = VisualTreeHelper.GetChild(element, i) as FrameworkElement;
if (child == null) continue;
Debug.WriteLine(child.ToString());
if (child is VirtualizingStackPanel) return child as VirtualizingStackPanel;
var panel = GetInnerStackPanel(child);
if (panel != null)
return panel;
}
return null;
}
Now that we have the StackPanel, we're very close. The StackPanel has the properties VerticalOffset and ViewportHeight (both coming from IScrollInfo) that can give us all the information we need.
private void button1_Click(object sender, RoutedEventArgs e)
{
var theStackPanel = GetInnerStackPanel(MyListBox);
List<FrameworkElement> visibleElements = new List<FrameworkElement>();
for (int i = 0; i < theStackPanel.Children.Count; i++)
{
if (i >= theStackPanel.VerticalOffset && i <= theStackPanel.VerticalOffset + theStackPanel.ViewportHeight)
{
visibleElements.Add(theStackPanel.Children[i] as FrameworkElement);
}
}
MessageBox.Show(visibleElements.Count.ToString());
MessageBox.Show(theStackPanel.VerticalOffset.ToString());
MessageBox.Show((theStackPanel.VerticalOffset + theStackPanel.ViewportHeight).ToString());
}

How can I make WPF ScrollViewer middle-click-scroll?

Clicking the middle mouse button (aka: mouse wheel) and then moving the mouse down slightly lets users scroll in IE, and most Windows apps. This behavior appears to be missing in WPF controls by default? Is there a setting, a workaround, or something obvious that I'm missing?
I have found how to achieve this using 3 mouse events (MouseDown, MouseUp, MouseMove). Their handlers are attached to the ScrollViewer element in the xaml below:
<Grid>
<ScrollViewer MouseDown="ScrollViewer_MouseDown" MouseUp="ScrollViewer_MouseUp" MouseMove="ScrollViewer_MouseMove">
<StackPanel x:Name="dynamicLongStackPanel">
</StackPanel>
</ScrollViewer>
<Canvas x:Name="topLayer" IsHitTestVisible="False" />
</Grid>
It would be better to write a behaviour instead of events in code-behind, but not everyone has the necessary library, and also I don't know how to connect it with the Canvas.
The event handlers:
private bool isMoving = false; //False - ignore mouse movements and don't scroll
private bool isDeferredMovingStarted = false; //True - Mouse down -> Mouse up without moving -> Move; False - Mouse down -> Move
private Point? startPosition = null;
private double slowdown = 200; //The number 200 is found from experiments, it should be corrected
private void ScrollViewer_MouseDown(object sender, MouseButtonEventArgs e)
{
if (this.isMoving == true) //Moving with a released wheel and pressing a button
this.CancelScrolling();
else if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Pressed)
{
if (this.isMoving == false) //Pressing a wheel the first time
{
this.isMoving = true;
this.startPosition = e.GetPosition(sender as IInputElement);
this.isDeferredMovingStarted = true; //the default value is true until the opposite value is set
this.AddScrollSign(e.GetPosition(this.topLayer).X, e.GetPosition(this.topLayer).Y);
}
}
}
private void ScrollViewer_MouseUp(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Middle && e.ButtonState == MouseButtonState.Released && this.isDeferredMovingStarted != true)
this.CancelScrolling();
}
private void CancelScrolling()
{
this.isMoving = false;
this.startPosition = null;
this.isDeferredMovingStarted = false;
this.RemoveScrollSign();
}
private void ScrollViewer_MouseMove(object sender, MouseEventArgs e)
{
var sv = sender as ScrollViewer;
if (this.isMoving && sv != null)
{
this.isDeferredMovingStarted = false; //standard scrolling (Mouse down -> Move)
var currentPosition = e.GetPosition(sv);
var offset = currentPosition - startPosition.Value;
offset.Y /= slowdown;
offset.X /= slowdown;
//if(Math.Abs(offset.Y) > 25.0/slowdown) //Some kind of a dead space, uncomment if it is neccessary
sv.ScrollToVerticalOffset(sv.VerticalOffset + offset.Y);
sv.ScrollToHorizontalOffset(sv.HorizontalOffset + offset.X);
}
}
If to remove the method calls AddScrollSign and RemoveScrollSign this example will work. But I have extended it with 2 methods which set scroll icon:
private void AddScrollSign(double x, double y)
{
int size = 50;
var img = new BitmapImage(new Uri(#"d:\middle_button_scroll.png"));
var adorner = new Image() { Source = img, Width = size, Height = size };
//var adorner = new Ellipse { Stroke = Brushes.Red, StrokeThickness = 2.0, Width = 20, Height = 20 };
this.topLayer.Children.Add(adorner);
Canvas.SetLeft(adorner, x - size / 2);
Canvas.SetTop(adorner, y - size / 2);
}
private void RemoveScrollSign()
{
this.topLayer.Children.Clear();
}
Example of icons:
And one last remark: there are some problems with the way Press -> Immediately Release -> Move. It is supposed to cancel scrolling if a user clicks the mouse left button, or any key of keyboard, or the application looses focus. There are many events and I don't have time to handle them all.
But standard way Press -> Move -> Release works without problems.
vorrtex posted a nice solution, please upvote him!
I do have some suggestions for his solution though, that are too lengthy to fit them all in comments, that's why I post a separate answer and direct it to him!
You mention problems with Press->Release->Move. You should use MouseCapturing to get the MouseEvents even when the Mouse is not over the ScrollViewer anymore. I have not tested it, but I guess your solution also fails in Press->Move->Move outside of ScrollViewer->Release, Mousecapturing will take care of that too.
Also you mention using a Behavior. I'd rather suggest an attached behavior that doesn't need extra dependencies.
You should definately not use an extra Canvas but do this in an Adorner.
The ScrollViewer itsself hosts a ScrollContentPresenter that defines an AdornerLayer. You should insert the Adorner there. This removes the need for any further dependency and also keeps the attached behavior as simple as IsMiddleScrollable="true".

Resources