Drag&Drop in a WPF TreeView on the Scrollbar - c#

we're using the MVVM pattern in our application and in a window, we have two TreeViews allowing to drag items from the first and drop it on the second tree. To avoid code behind, we're using behaviours to bind the drag and drop against the ViewModel.
The behaviour is implemented pretty much like this example and working like a charm, with one bug.
The scenario is a tree which is bigger than the window displaying it, therefore it has a vertical scroll bar. When an item is selected and the user wants to scroll, the program starts drag and drop (which prevents the actual scrolling and therefore isn't what we want).
This isn't very surprising as the scrollbar is contained in the TreeView control. But I'm unable to determine safely if the mouse is over the scrollbar or not.
The TreeViewItems are represented by a theme using Borders, Panels and so on, so a simple InputHitTest isn't as simple as one may think.
Has anybody already encountered the same problem?
If more code coverage of the problem is required, I can paste some lines from the .xaml.
Edit
Incorporating Nikolays link I solved the problem using a IsMouseOverScrollbar method, if anyone has this problem in the future the code from above must be altered in the following way:
private static void PreviewMouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton != MouseButtonState.Pressed || startPoint == null)
return;
if (!HasMouseMovedFarEnough(e))
return;
if (IsMouseOverScrollbar(sender, e.GetPosition(sender as IInputElement)))
{
startPoint = null;
return;
}
var dependencyObject = (FrameworkElement)sender;
var dataContext = dependencyObject.GetValue(FrameworkElement.DataContextProperty);
var dragSource = GetDragSource(dependencyObject);
if (dragSource.GetDragEffects(dataContext) == DragDropEffects.None)
return;
DragDrop.DoDragDrop(
dependencyObject, dragSource.GetData(dataContext), dragSource.GetDragEffects(dataContext));
}
private static bool IsMouseOverScrollbar(object sender, Point mousePosition)
{
if (sender is Visual)
{
HitTestResult hit = VisualTreeHelper.HitTest(sender as Visual, mousePosition);
if (hit == null) return false;
DependencyObject dObj = hit.VisualHit;
while(dObj != null)
{
if (dObj is ScrollBar) return true;
if ((dObj is Visual) || (dObj is Visual3D)) dObj = VisualTreeHelper.GetParent(dObj);
else dObj = LogicalTreeHelper.GetParent(dObj);
}
}
return false;
}

Take a look at this implementation of Drag and Drop behaviour for ListView by Josh Smith. It has code to deal with scrollbars and some other unobvious problems of DnD (like drag treshold, precise mouse coordinates and such). This behaviour can be easily adopted to work with TreeViews too.

I had the same Problem. I solved it by placing the TreeView inside a ScrollViewer.
<ScrollViewer Grid.Column="0">
<TreeView BorderThickness="0" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" MouseMove="DeviceTree_OnMouseMove" PreviewMouseLeftButtonDown="DeviceTree_OnPreviewMouseLeftButtonDown" Name="DeviceTree" ItemsSource="{Binding Devices}"/>
</ScrollViewer>

Related

Is it possible to create Windows 10 desktop apps with list boxes that scroll smoothly like they do on macOS? [duplicate]

Is it possible to implement smooth scroll in a WPF listview like how it works in Firefox?
When the Firefox browser contained all listview items and you hold down the middle mouse button (but not release), and drag it, it should smoothly scroll the listview items. When you release it should stop.
It looks like this is not possible in winforms, but I am wondering if it is available in WPF?
You can achieve smooth scrolling but you lose item virtualisation, so basically you should use this technique only if you have few elements in the list:
Info here: Smooth scrolling on listbox
Have you tried setting:
ScrollViewer.CanContentScroll="False"
on the list box?
This way the scrolling is handled by the panel rather than the listBox... You lose virtualisation if you do that though so it could be slower if you have a lot of content.
It is indeed possible to do what you're asking, though it will require a fair amount of custom code.
Normally in WPF a ScrollViewer uses what is known as Logical Scrolling, which means it's going to scroll item by item instead of by an offset amount. The other answers cover some of the ways you can change the Logical Scrolling behavior into that of Physical Scrolling. The other way is to make use of the ScrollToVertialOffset and ScrollToHorizontalOffset methods exposed by both ScrollViwer and IScrollInfo.
To implement the larger part, the scrolling when the mouse wheel is pressed, we will need to make use of the MouseDown and MouseMove events.
<ListView x:Name="uiListView"
Mouse.MouseDown="OnListViewMouseDown"
Mouse.MouseMove="OnListViewMouseMove"
ScrollViewer.CanContentScroll="False">
....
</ListView>
In the MouseDown, we are going to record the current mouse position, which we will use as a relative point to determine which direction we scroll in. In the mouse move, we are going to get the ScrollViwer component of the ListView and then Scroll it accordingly.
private Point myMousePlacementPoint;
private void OnListViewMouseDown(object sender, MouseButtonEventArgs e)
{
if (e.MiddleButton == MouseButtonState.Pressed)
{
myMousePlacementPoint = this.PointToScreen(Mouse.GetPosition(this));
}
}
private void OnListViewMouseMove(object sender, MouseEventArgs e)
{
ScrollViewer scrollViewer = ScrollHelper.GetScrollViewer(uiListView) as ScrollViewer;
if (e.MiddleButton == MouseButtonState.Pressed)
{
var currentPoint = this.PointToScreen(Mouse.GetPosition(this));
if (currentPoint.Y < myMousePlacementPoint.Y)
{
scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - 3);
}
else if (currentPoint.Y > myMousePlacementPoint.Y)
{
scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset + 3);
}
if (currentPoint.X < myMousePlacementPoint.X)
{
scrollViewer.ScrollToHorizontalOffset(scrollViewer.HorizontalOffset - 3);
}
else if (currentPoint.X > myMousePlacementPoint.X)
{
scrollViewer.ScrollToHorizontalOffset(scrollViewer.HorizontalOffset + 3);
}
}
}
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;
}
There's some areas it's lacking as it's just a proof of concept but it should definitely get you started in the right direction. To have it constantly scroll once the mouse is moved away from the initial MouseDown point, the scrolling logic could go into a DispatcherTimer or something similar.
Try setting the ScrollViewer.CanContentScroll attached property to false on the ListView. But like Pop Catalin said, you lose item virtualization, meaning all the items in the list get loaded and populated at once, not when a set of items are needed to be displayed - so if the list is huge, it could cause some memory and performance issues.
try setting the listview's height as auto and wrapping it in a scroll viewer.
<ScrollViewer IsTabStop="True" VerticalScrollBarVisibility="Auto">
<ListView></ListView>
</ScrollViewer>
Don't forget to mention the height of ScrollViewer
Hope this helps....
I know this post is 13 years old, but this is still something people want to do.
in newer versions of .Net you can set VirtualizingPanel.ScrollUnit="Pixel"
this way you won't lose virtualization and you get scroll per pixel instead of per item.

WinrtXamlToolkit TreeView expand with single instead of double click

I am using the TreeView from the WinrtXamlToolkit. The default behavior of this control is to expand the nested items on double click of the header. The code responsible for this is here (TreeViewItem.cs line 1205).
private void OnHeaderMouseLeftButtonDown(object sender, PointerRoutedEventArgs e)
{
if (Interaction.AllowMouseLeftButtonDown(e))
{
// If the event hasn't already been handled and this item is
// focusable, then focus (and possibly expand if it was double
// clicked)
if (!e.Handled && IsEnabled)
{
if (Focus(FocusState.Programmatic))
{
e.Handled = true;
}
// Expand the item when double clicked
if (Interaction.ClickCount % 2 == 0)
{
bool opened = !IsExpanded;
UserInitiatedExpansion |= opened;
IsExpanded = opened;
e.Handled = true;
}
}
Interaction.OnMouseLeftButtonDownBase();
OnPointerPressed(e);
}
}
Is there a way to change this behavior to expand the items on single click or tap without actually copying the control and all it's related classes to my project?
It seems like an overkill to do this just to change a few lines of code.
I tried to do drag'n'drop stuff with that TreeView and was in a similar situation. My first move was to actually copy all the TreeView and its related classes and man there are a lot. There's a lot of internal stuff happening and I pretty much gave up interfering with it after a bunch of other stuff stopped working.
So my solution was to just have a specific control inside the ItemTemplate that handled dragging for me. For you this would be a Button whose Click you handle. In the eventhandler you will navigate up the visual tree to your TreeViewItem and change the IsExpanded.

Silverlight RadComboBox make whole texbox area clickable

Is it possible to make the whole text area of the RadComboBox clickable while having IsEditable=true and ReadOnly=True?
I would just set IsEditable = false but unfortunately I need it to be editable in order to display custom text when something is selected (I have it set so multiple things can be selected and present a list of the selected items). If I disable IsEditable then I lose the .Text attribute and can't set a custom text.
My two best bets would be:
1) somehow apply a style that makes the whole textbar clickable and not just the arrow
2) somehow apply custom text display when IsEditable is set to false.
Unfortunately I don't know how to do either so any help would be nice. Thanks
Edit: This would be ideal, except that we're using Silverlight and not ASP.net
http://demos.telerik.com/aspnet-ajax/combobox/examples/functionality/checkboxes/defaultcs.aspx
This is probably more realistic, just to somehow make the text area clickable so it opens the dropdown menu. Just like the ComboBox on the right, minus being able to type. http://demos.telerik.com/aspnet-ajax/combobox/examples/functionality/comboboxvsdropdownlist/defaultcs.aspx
I can think of several solutions, of varying elegance. Here is one that might be suitable to close your remaining gap between the Arrow-ToggleButton and the Text-Input-Area. And now that I think about it... maybe you can get rid of that rather smelly and fragile side-effect-piggybacking with the OpenDropDownOnFocus property (which will break as soon as a click does not change the focus owner).
Register a MouseLeftButtonDown click handler with the RadComboBox, you can choose to get all events, not only unhandled events. Then we can toggle the DropDown from there. But we don't want to interfere with the Arrow-ToggleButton, therefore we check from where the mouse click originated.
public class MyView : UserControl
{
public MyView()
{
InitializeComponent();
MouseButtonEventHandler handler = OnComboBoxClicked;
radComboBox.AddHandler( UIElement.MouseLeftButtonDownEvent, handler,
handledEventsToo: true );
}
private void OnComboBoxClicked( object sender, MouseButtonEventArgs args )
{
if (!args.Handled ||
!args.IsRoutedEventFromToggleButton(
togglebuttonAncestorToStopTheSearch: (UIElement) sender))
{
ToggleDropDown();
}
}
}
and extension methods for easier use:
public static class ControlExtensions
{
public static bool IsRoutedEventFromToggleButton(
this RoutedEventArgs args,
UIElement togglebuttonAncestorToStopTheSearch )
{
ToggleButton toggleButton = ((UIElement) args.OriginalSource)
.GetAncestor<ToggleButton>( togglebuttonAncestorToStopTheSearch );
return toggleButton != null;
}
public static TAncestor GetAncestor<TAncestor>(
this DependencyObject subElement,
UIElement potentialAncestorToStopTheSearch )
where TAncestor : DependencyObject
{
DependencyObject parent;
for (DependencyObject subControl = subElement; subControl != null;
subControl = parent)
{
if (subControl is TAncestor) return (TAncestor) subControl;
if (object.ReferenceEquals( subControl,
potentialAncestorToStopTheSearch )) return null;
parent = VisualTreeHelper.GetParent( subControl );
if (parent == null)
{
FrameworkElement element = subControl as FrameworkElement;
if (element != null)
{
parent = element.Parent;
}
}
}
return null;
}
}
I ended up finding a multiselectcombobox that someone else implemented here:
http://www.telerik.com/support/code-library/a-multiselect-combobox
I didn't need the whole combobox itself since we already had one implemented so I just looked at how the person was displaying a custom message while the combo box IsEditable was set to false.
After looking at that code for a while and seeing how I can make it work for me, I put
<ucControls:RadComboBox.SelectionBoxTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text,ElementName=RadCombo}" />
</DataTemplate>
</ucControls:RadComboBox.SelectionBoxTemplate>
inside the XAML of our own custom MultiSelectComboBox. (RadCombo being the name of the particular control that I wanted the Text to be linked to)
<ucControls:RadComboBox
x:Name="RadCombo"
Text=""
........
<ucControls:RadComboBox.SelectionBoxTemplate>
<DataTemplate>
<TextBlock Text="{Binding Text,ElementName=RadCombo}" />
</DataTemplate>
</ucControls:RadComboBox.SelectionBoxTemplate>
.......
</ucControls:RadComboBox>
Using the built in SelectionBoxTemplate, this basically just added a TextBlock overlay, and the content was bound to the RadComboBox's own Text, so when we would set the Text of the RadComboBox, the TextBlock would update itself.
This was the most effective way for us to do it because it required minimal code changes, and no structure changes since we already had all the code in place for checking boxes and setting a custom text.
Hope this helps someone, best of luck!

How to use ctrl key + mouse click to select multiple controls?

Probably this question has already an answer here but I was not able to find it..
I have a tabControl with a flowlayoutpanel in each tab page where I can add controls at run time. I can rearrange them, move them across tab pages.. How can I select multiple controls to be able to move them around using ctrl key + mouse click?
This is my drag event so far:
private void control_DragDrop(object sender, DragEventArgs e)
{
Control target = new Control();
target.Parent = sender as Control;
if (target != null)
{
int targetIndex = FindCSTIndex(target.Parent);
if (targetIndex != -1)
{
string cst_ctrl = typeof(CustomControl).FullName;
if (e.Data.GetDataPresent(cst_ctrl))
{
Button source = new Button();
source.Parent = e.Data.GetData(cst_ctrl) as CustomControl;
if (targetIndex != -1)
fl_panel = (FlowLayoutPanel)tabControl1.SelectedTab.Controls[0];
if (source.Parent.Parent.Name == target.Parent.Parent.Parent.Name)
{
this.fl_panel.Controls.SetChildIndex(source.Parent, targetIndex);
}
else
{
target.Parent.Parent.Parent.Controls.Add(source.Parent);
this.fl_panel.Controls.SetChildIndex(source.Parent, targetIndex);
}
}
}
}
}
private int FindCSTIndex(Control cst_ctr)
{
fl_panel = (FlowLayoutPanel)tabControl1.SelectedTab.Controls[0];
for (int i = 0; i < this.fl_panel.Controls.Count; i++)
{
CustomControl target = this.fl_panel.Controls[i] as CustomControl;
if (cst_ctr.Parent == target)
return i;
}
return -1;
}
This is not an easy, nor a common task. But surely doable and depending on preconditions could become trivial without need to spend multi-man-year effort on it ^^.
You have many options:
controls support selection;
container control support children controls selection;
overlay.
Handling selection is pretty easy: have a dictionary (or a control property, possibly using Tag) to store if control is selected or not, show selection somehow, when control is Ctrl-clicked invert selection. You can even provide Shift-key selection.
As #Hans Passant commented, you can use overlay window (invisible window on top of everything) to draw selection reticle there as well as handle selection and dragging itself. Or it could be a custom control with property IsSelected, setting which will draw something (border?) to indicate selection.
Easiest option would be to create SelectionPanel control, which can host any other controls inside, has IsSelected indication and is draggable. When children is added subscribe to MouseUp/MouseDown events or you can only allow to drag if special area of SelectionPanel is clicked. To example, you could have option Enable dragging in your software, when set all SelectionPanels will display special area (header?) which you can drag or Ctrl-click.

Drag WPF Popup control

the WPF Popup control is nice, but somewhat limited in my opinion. is there a way to "drag" a popup around when it is opened (like with the DragMove() method of windows)?
can this be done without big problems or do i have to write a substitute for the popup class myself?
thanks
Here's a simple solution using a Thumb.
Subclass Popup in XAML and codebehind
Add a Thumb with width/height set to 0 (this could also be done in XAML)
Listen for MouseDown events on the Popup and raise the same event on the Thumb
Move popup on DragDelta
XAML:
<Popup x:Class="PopupTest.DraggablePopup" ...>
<Canvas x:Name="ContentCanvas">
</Canvas>
</Popup>
C#:
public partial class DraggablePopup : Popup
{
public DraggablePopup()
{
var thumb = new Thumb
{
Width = 0,
Height = 0,
};
ContentCanvas.Children.Add(thumb);
MouseDown += (sender, e) =>
{
thumb.RaiseEvent(e);
};
thumb.DragDelta += (sender, e) =>
{
HorizontalOffset += e.HorizontalChange;
VerticalOffset += e.VerticalChange;
};
}
}
There is no DragMove for PopUp. Just a small work around, there is lot of improvements you can add to this.
<Popup x:Name="pop" IsOpen="True" Height="200" Placement="AbsolutePoint" Width="200">
<Rectangle Stretch="Fill" Fill="Red"/>
</Popup>
In the code behind , add this mousemove event
pop.MouseMove += new MouseEventHandler(pop_MouseMove);
void pop_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
pop.PlacementRectangle = new Rect(new Point(e.GetPosition(this).X,
e.GetPosition(this).Y),new Point(200,200));
}
}
Building off of Jobi Joy's answer, I found a re-useable solution that allows you to add as a control within xaml of an existing control/page. Which was not possible adding as Xaml with a Name since it has a different scope.
[ContentProperty("Child")]
[DefaultEvent("Opened")]
[DefaultProperty("Child")]
[Localizability(LocalizationCategory.None)]
public class DraggablePopup : Popup
{
public DraggablePopup()
{
MouseDown += (sender, e) =>
{
Thumb.RaiseEvent(e);
};
Thumb.DragDelta += (sender, e) =>
{
HorizontalOffset += e.HorizontalChange;
VerticalOffset += e.VerticalChange;
};
}
/// <summary>
/// The original child added via Xaml
/// </summary>
public UIElement TrueChild { get; private set; }
public Thumb Thumb { get; private set; } = new Thumb
{
Width = 0,
Height = 0,
};
protected override void OnInitialized(EventArgs e)
{
base.OnInitialized(e);
TrueChild = Child;
var surrogateChild = new StackPanel();
RemoveLogicalChild(TrueChild);
surrogateChild.Children.Add(Thumb);
surrogateChild.Children.Add(TrueChild);
AddLogicalChild(surrogateChild);
Child = surrogateChild;
}
}
Another way of achieving this is to set your Popup's placement to MousePoint. This makes the popup initially appear at the position of the mouse cursor.
Then you can either use a Thumb or MouseMove event to set the Popup's HorizontalOffset & VerticalOffset. These properties shift the Popup away from its original position as the user drags it.
Remember to reset HorizontalOffset and VerticalOffset back to zero for the next use of the popup!
The issue with loosing the mouse when moving too fast, could be resolved
This is taken from msdn:
The new window contains the Child content of Popup.
The Popup control maintains a reference to its Child content as a logical child. When the new window is created, the content of Popup becomes a visual child of the window and remains the logical child of Popup. Conversely, Popup remains the logical parent of its Child content.
In the other words, the child of the popup is displayed in standalone window.
So when trying to the following:
Popup.CaptureMouse() is capturing the wrapper window and not the popup itself. Instead using Popup.Child.CaptureMouse() captures the actual popup.
And all other events should be registered using Popup.Child.
Like Popup.Child.MouseMove, Popup.Child.LostCapture and so on
This has been tested and works perfectly fine
Contrary to what others have stated about this, I agree 100% with Jobi Joy's answer (which should honestly be the accepted answer). I saw a comment stating that the solution in the answer would cause memory fragmentation. This is not possible as creating new structs cannot cause memory fragmentation at all; in fact, using structs saves memory because they are stack-allocated. Furthermore, I think that this is actually the correct way to reposition a popup (after all, Microsoft added the PlacementRectangle property for a reason), so it is not a hack. Appending Thumbs and expecting a user to always place a Popup onto a canvas, however, is incredibly hacky and is not always a practical solution.
Private Point startPoint;
private void Window_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
startPoint = e.GetPosition(null);
}
private void Window_MouseMove(object sender, MouseEventArgs e)
{
if (e.LeftButton == MouseButtonState.Pressed)
{
Point relative = e.GetPosition(null);
Point AbsolutePos = new Point(relative.X + this.Left, relative.Y + this.Top);
this.Top = AbsolutePos.Y - startPoint.Y;
this.Left = AbsolutePos.X - startPoint.X;
}
}
This works for dragging my window, but like it was told if i move the mouse to fast, it would get out of window and stop raising the event. Without mentioning the dragging is not smooth at all. Does anyone knows how to do it properly, nice and smooth dragging, without loosing it when dragged too fast??? Post a simple example if possible, other than a whole tutorial that would get beginners like me lost in code. Thanks!

Categories

Resources