.NET 2.0 C# Treeview Drag/Drop within TreeNodes - c#

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.

Related

C# TreeView, event when childnode is selected

I have a question concerning TreeViews and their Nodes in C#.
What I currently try to do. I have a TreeView and next to it a TableLayoutPanel. When I click of the Nodes, I want to call a specific Method and display the Data
in the TableLayoutPanel. Displaying the data works fine, but my problem is that I dont know exactly how to determine what Node/ChildNode has been selected.
I have a TreeView that looks like this
Root1
R1Child1
R1Child2
Root2
R2Child1
R2Child2
Root3
R3Child1
R3Child2
I currently handle this by an AfterSelect Method and just check the selected Node for the Text.
private void treeHardware_AfterSelect(object sender, TreeViewEventArgs e)
{
if (e.Node.Text == SysInfo.CPU.Name)
{
deleteRows();
initFixedRows();
updateTableCPU();
}
else if (e.Node.Text == ramNameIdent)
{
deleteRows();
initFixedRows();
updateTableRAM(e.Node.Index);
}
else if (e.Node.Text == "Memory")
{
deleteRows();
initFixedRows();
loadRAMDetails(0);
loadRAMOverview();
}
else if( e.Node.Text == "Mainboard")
{
deleteRows();
initFixedRows();
updateTableMainboard();
}
else
{
Console.WriteLine("ERROR");
}
}
In my Opinion this is a very unpractical way to check what Node has been clicked, because it just checks Strings, and it isnt very effective..
Next Problem, for Memory Node. I display all installed Physical Memories and add each of them as a ChildNode. Now when I click one of them, it should display the Data of the selected Memory in my TableLayoutPanel. But It always just shows the "last" one.
Hope hat you understand what I mean...
If not, just ask for more Information :-)
Cheers,
Consti
Use the Tag Property , put an ID in the tag property that is unique for every node

Drag and Drop custom controls between cells in a grid in WPF

I've got some custom controls which are dynamically added to a custom grid. These controls can span over several columns and rows(which are all the same size). I'd like to drag and drop between the rows and columns. I can drag the individual controls, but they can move anywhere without limit. Even off the grid. I'd like to do it so it can only be dragged inside the grid AND snaps to the column/row it's dragged to.
Is there any easy-ish way to do this?
Honestly, if I could get the current row/column that it's over, then all I'd need to do is set the column/row of it to them and that would probably do it and then just worry about keeping it inside the grid.
I figured out a nice and fun way!
I worked out the position on the grid that the the mouse is on on the MouseUp event and then the relative position of the mouse on the control since it spans several rows/columns.
public void getPosition(UIElement element, out int col, out int row)
{
DControl control = parent as DControl;
var point = Mouse.GetPosition(element);
row = 0;
col = 0;
double accumulatedHeight = 0.0;
double accumulatedWidth = 0.0;
// calc row mouse was over
foreach (var rowDefinition in control.RowDefinitions)
{
accumulatedHeight += rowDefinition.ActualHeight;
if (accumulatedHeight >= point.Y)
break;
row++;
}
// calc col mouse was over
foreach (var columnDefinition in control.ColumnDefinitions)
{
accumulatedWidth += columnDefinition.ActualWidth;
if (accumulatedWidth >= point.X)
break;
col++;
}
}
I then take away the relative positions from the normal positions so that when you drop it, it always drops on the top left of the screen. When I move my controls, I use margins to move it, which screws up the position on the grid at the time, as shown below:
void Chart_PreviewMouseMove(object sender, MouseEventArgs e)
{
if (IsMouseCaptured)
{
Point mouseDelta = Mouse.GetPosition(this);
mouseDelta.Offset(-mouseOffset.X, -mouseOffset.Y);
Margin = new Thickness(
Margin.Left + mouseDelta.X,
Margin.Top + mouseDelta.Y,
Margin.Right - mouseDelta.X,
Margin.Bottom - mouseDelta.Y);
}
}
void Chart_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
mouseOffset = Mouse.GetPosition(this);
CaptureMouse();
parent.currentObject = this;
}
To tackle this, I simply reset the margin.
public void updatePosition()
{
Grid.SetRow(this, (int)position.Y);
Grid.SetColumn(this, (int)position.X);
Margin = new Thickness();
}
I hope this helps someone else since it was rather frustrating for me to find the answer and in the end I managed to get lots of little fragments of how to do things and eventually came up with my own solution.
Is there any easy-ish way to do this?
I'd say that the answer to this question very much depends on your experience using Drag and Drop functionality... for a beginner, I'd say that the answer to this was no, but for someone with some experience and some common sense, it might not be too bad.
To determine which Grid cell the user's mouse is over will not be straight forward. You can handle the PreviewDragOver event and use the VisualTreeHelper.HitTest method to check which control the mouse is currently over:
private void PreviewDragOver(object sender, DragEventArgs e)
{
HitTestResult hitTestResult = VisualTreeHelper.HitTest(adornedUIElement,
e.GetPosition(adornedUIElement));
Control controlUnderMouse = hitTestResult.VisualHit.GetParentOfType<Control>();
}
The GetParentOfType method is a useful extension method that I created, but you can convert it to a normal method easily enough:
public static T GetParentOfType<T>(this DependencyObject element) where T : DependencyObject
{
Type type = typeof(T);
if (element == null) return null;
DependencyObject parent = VisualTreeHelper.GetParent(element);
if (parent == null && ((FrameworkElement)element).Parent is DependencyObject) parent = ((FrameworkElement)element).Parent;
if (parent == null) return null;
else if (parent.GetType() == type || parent.GetType().IsSubclassOf(type)) return parent as T;
return GetParentOfType<T>(parent);
}
Of course, once you have a Control in your controlUnderMouse variable, you'll still have some considerable work to do as you work your way through the UIElements until you get to the Grid... you can of course make further use of the GetParentOfType method to make your job easier.

WPF: How to walk up the Visual Tree to find a Model3DGroup a clicked 3d-model is in?

I'm displaying a few 3D-models as Model3DGroups.
They are surrounded by Viewport3D which catches MouseDown-events.
I want to determine which Model3DGroup (they all have names) was clicked.
I'm starting with this:
Point location = e.GetPosition(karte.ZAM3DViewport3D);
HitTestResult hitResult = VisualTreeHelper.HitTest(karte.ZAM3DViewport3D, location);
if (hitResult != null )
{
Debug.WriteLine("BREAKPOINT");
// Hit the visual.
}
After hitting the breakpoint set at the WriteLine-command I'm looking at the local-view to find the right variable but can't find it.
Can you help me out which path I need to take to find the group, the modelvisual3d belongs to?
Here is a screenshot of the tree:
I did it by surrounding the Model3DGroup with a ModelUIElement3D.
<ModelUIElement3D MouseDown="ModelUIElement3D_MouseDown" x:Name="LogoMouseDown">
the MouseDown-function handles it this way:
private void ModelUIElement3D_MouseDown(object sender, MouseButtonEventArgs e)
{
if (sender == trololo)
{
RaiseModelClickEvent("auditorium");
}
else if (sender == LogoMouseDown)
{
RaiseModelClickEvent("logo");
}
}
You could use Linq to Visual Tree as it doesn't matter whether the named element you are looking for is Model3DGroup. It is just another Dependency Object (if I understand your question).
Check result's type and then LinqToVT to go up, retrieving it's XAML predecessor:
hitResult.VisualHit.GetType() == typeof(ModelVisual3D)

How do I avoid .Parent.Parent.Parent. etc. when referencing control hierarchies?

I'm trying to fix this ugly code.
RadGrid gv = (RadGrid) (((Control) e.CommandSource).Parent.Parent.Parent.Parent.Parent);
I often need to find the first grid that is the parent of the parent of... etc of a object that just raised an event.
The above tends to break when the layout changes and the number of .Parents increase or decreases.
I don't necessarily have a control Id, so I can't use FindControl().
Is there a better way to find the 1st parent grid?
Control parent = Parent;
while (!(parent is RadGrid))
{
parent = parent.Parent;
}
If you really have to find the grid, then you might something like this:
Control ct = (Control)e.CommandSource;
while (!(ct is RadGrid)) ct = ct.Parent;
RadGrid gv = (RadGrid)ct;
But maybe you can explain why you need a reference to the grid? Maybe there is another/better solution for your problem.
I'm not familiar with the API that you are using, but can you do something like:
Control root = ((Control)e.CommandSource);
while(root.Parent != null)
{
// must start with the parent
root = root.Parent;
if (root is RadGrid)
{
// stop at the first grid parent
break;
}
}
// might throw exception if there was no parent that was a RadGrid
RadGrid gv = (RadGrid)root;
If you have control of the Parent's code, you could use simple recursion to do it, I'd think. Something like:
public Control GetAncestor(Control c)
{
Control parent;
if (parent = c.Parent) != null)
return GetAncestor(parent);
else
return c;
}
I make no claims as to how well that will work, but it should get the idea across. Navigate up the parent chain as far as it goes until there is no parent, then return that object back up the recursion chain. It's brute force, but it will find the first parent no matter how high it is.

c# wpf overlapping controls not receiving mouse events

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.

Categories