I have a project where I have a collection of items I want to bind the Canvas to. The canvas is subclassed to provide the ability to drag and resize elements. The code to drag and resize works fine but the problem is I can't bind the canvas. I found I can do:
<ItemsControl x:Name="Items" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" Grid.Row="1">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<controls:SuperCanvas Background="Transparent" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Path=X, Mode=TwoWay}" />
<Setter Property="Canvas.Top" Value="{Binding Path=Y, Mode=TwoWay}" />
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
And the canvas is bound correctly and items appear on the Canvas. But then my subclassed canvas no longer works correctly. (resize and drag no longer function.)
Here is the code for the SuperCanvas:
sing System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using Meld.Helpers;
namespace Meld.Controls
{
public class SuperCanvas : Canvas
{
private AdornerLayer aLayer;
private bool isDown;
private bool isDragging;
private double originalLeft;
private double originalTop;
private bool selected;
private UIElement selectedElement;
private Point startPoint;
private bool locked;
public SuperCanvas()
{
Loaded += OnLoaded;
}
private void OnLoaded(object sender, RoutedEventArgs e)
{
MouseLeftButtonDown += WorkspaceWindow_MouseLeftButtonDown;
MouseLeftButtonUp += DragFinishedMouseHandler;
MouseMove += WorkspaceWindow_MouseMove;
MouseLeave += Window1_MouseLeave;
PreviewMouseLeftButtonDown += myCanvas_PreviewMouseLeftButtonDown;
PreviewMouseLeftButtonUp += DragFinishedMouseHandler;
}
// Handler for drag stopping on leaving the window
private void Window1_MouseLeave(object sender, MouseEventArgs e)
{
StopDragging();
e.Handled = true;
}
// Handler for drag stopping on user choice
private void DragFinishedMouseHandler(object sender, MouseButtonEventArgs e)
{
StopDragging();
e.Handled = true;
}
// Method for stopping dragging
private void StopDragging()
{
if (isDown)
{
isDown = false;
isDragging = false;
}
}
// Hanler for providing drag operation with selected element
private void WorkspaceWindow_MouseMove(object sender, MouseEventArgs e)
{
if (locked) return;
if (isDown)
{
if ((isDragging == false) &&
((Math.Abs(e.GetPosition(this).X - startPoint.X) >
SystemParameters.MinimumHorizontalDragDistance) ||
(Math.Abs(e.GetPosition(this).Y - startPoint.Y) > SystemParameters.MinimumVerticalDragDistance)))
isDragging = true;
if (isDragging)
{
Point position = Mouse.GetPosition(this);
SetTop(selectedElement, position.Y - (startPoint.Y - originalTop));
SetLeft(selectedElement, position.X - (startPoint.X - originalLeft));
}
}
}
// Handler for clearing element selection, adorner removal
private void WorkspaceWindow_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (locked) return;
if (selected)
{
selected = false;
if (selectedElement != null)
{
aLayer.Remove(aLayer.GetAdorners(selectedElement)[0]);
selectedElement = null;
}
}
}
// Handler for element selection on the canvas providing resizing adorner
private void myCanvas_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
//add code to lock dragging and dropping things.
if (locked)
{
e.Handled = true;
return;
}
// Remove selection on clicking anywhere the window
if (selected)
{
selected = false;
if (selectedElement != null)
{
// Remove the adorner from the selected element
aLayer.Remove(aLayer.GetAdorners(selectedElement)[0]);
selectedElement = null;
}
}
// If any element except canvas is clicked,
// assign the selected element and add the adorner
if (e.Source != this)
{
isDown = true;
startPoint = e.GetPosition(this);
selectedElement = e.Source as UIElement;
originalLeft = GetLeft(selectedElement);
originalTop = GetTop(selectedElement);
aLayer = AdornerLayer.GetAdornerLayer(selectedElement);
aLayer.Add(new ResizingAdorner(selectedElement));
selected = true;
e.Handled = true;
}
}
}
}
EDIT: Forgot the resizing adorner
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
namespace Meld.Helpers
{
internal class ResizingAdorner : Adorner
{
// Resizing adorner uses Thumbs for visual elements.
// The Thumbs have built-in mouse input handling.
Thumb topLeft, topRight, bottomLeft, bottomRight;
// To store and manage the adorner's visual children.
VisualCollection visualChildren;
// Initialize the ResizingAdorner.
public ResizingAdorner(UIElement adornedElement) : base(adornedElement)
{
visualChildren = new VisualCollection(this);
// Call a helper method to initialize the Thumbs
// with a customized cursors.
BuildAdornerCorner(ref topLeft, Cursors.SizeNWSE);
BuildAdornerCorner(ref topRight, Cursors.SizeNESW);
BuildAdornerCorner(ref bottomLeft, Cursors.SizeNESW);
BuildAdornerCorner(ref bottomRight, Cursors.SizeNWSE);
// Add handlers for resizing.
bottomLeft.DragDelta += new DragDeltaEventHandler(HandleBottomLeft);
bottomRight.DragDelta += new DragDeltaEventHandler(HandleBottomRight);
topLeft.DragDelta += new DragDeltaEventHandler(HandleTopLeft);
topRight.DragDelta += new DragDeltaEventHandler(HandleTopRight);
}
// Handler for resizing from the bottom-right.
void HandleBottomRight(object sender, DragDeltaEventArgs args)
{
FrameworkElement adornedElement = this.AdornedElement as FrameworkElement;
Thumb hitThumb = sender as Thumb;
if (adornedElement == null || hitThumb == null) return;
FrameworkElement parentElement = adornedElement.Parent as FrameworkElement;
// Ensure that the Width and Height are properly initialized after the resize.
EnforceSize(adornedElement);
// Change the size by the amount the user drags the mouse, as long as it's larger
// than the width or height of an adorner, respectively.
adornedElement.Width = Math.Max(adornedElement.Width + args.HorizontalChange, hitThumb.DesiredSize.Width);
adornedElement.Height = Math.Max(args.VerticalChange + adornedElement.Height, hitThumb.DesiredSize.Height);
}
// Handler for resizing from the top-right.
void HandleTopRight(object sender, DragDeltaEventArgs args)
{
FrameworkElement adornedElement = this.AdornedElement as FrameworkElement;
Thumb hitThumb = sender as Thumb;
if (adornedElement == null || hitThumb == null) return;
FrameworkElement parentElement = adornedElement.Parent as FrameworkElement;
// Ensure that the Width and Height are properly initialized after the resize.
EnforceSize(adornedElement);
// Change the size by the amount the user drags the mouse, as long as it's larger
// than the width or height of an adorner, respectively.
adornedElement.Width = Math.Max(adornedElement.Width + args.HorizontalChange, hitThumb.DesiredSize.Width);
//adornedElement.Height = Math.Max(adornedElement.Height - args.VerticalChange, hitThumb.DesiredSize.Height);
double height_old = adornedElement.Height;
double height_new = Math.Max(adornedElement.Height - args.VerticalChange, hitThumb.DesiredSize.Height);
double top_old = Canvas.GetTop(adornedElement);
adornedElement.Height = height_new;
Canvas.SetTop(adornedElement, top_old - (height_new - height_old));
}
// Handler for resizing from the top-left.
void HandleTopLeft(object sender, DragDeltaEventArgs args)
{
FrameworkElement adornedElement = AdornedElement as FrameworkElement;
Thumb hitThumb = sender as Thumb;
if (adornedElement == null || hitThumb == null) return;
// Ensure that the Width and Height are properly initialized after the resize.
EnforceSize(adornedElement);
// Change the size by the amount the user drags the mouse, as long as it's larger
// than the width or height of an adorner, respectively.
//adornedElement.Width = Math.Max(adornedElement.Width - args.HorizontalChange, hitThumb.DesiredSize.Width);
//adornedElement.Height = Math.Max(adornedElement.Height - args.VerticalChange, hitThumb.DesiredSize.Height);
double width_old = adornedElement.Width;
double width_new = Math.Max(adornedElement.Width - args.HorizontalChange, hitThumb.DesiredSize.Width);
double left_old = Canvas.GetLeft(adornedElement);
adornedElement.Width = width_new;
Canvas.SetLeft(adornedElement, left_old - (width_new - width_old));
double height_old = adornedElement.Height;
double height_new = Math.Max(adornedElement.Height - args.VerticalChange, hitThumb.DesiredSize.Height);
double top_old = Canvas.GetTop(adornedElement);
adornedElement.Height = height_new;
Canvas.SetTop(adornedElement, top_old - (height_new - height_old));
}
// Handler for resizing from the bottom-left.
void HandleBottomLeft(object sender, DragDeltaEventArgs args)
{
FrameworkElement adornedElement = AdornedElement as FrameworkElement;
Thumb hitThumb = sender as Thumb;
if (adornedElement == null || hitThumb == null) return;
// Ensure that the Width and Height are properly initialized after the resize.
EnforceSize(adornedElement);
// Change the size by the amount the user drags the mouse, as long as it's larger
// than the width or height of an adorner, respectively.
//adornedElement.Width = Math.Max(adornedElement.Width - args.HorizontalChange, hitThumb.DesiredSize.Width);
adornedElement.Height = Math.Max(args.VerticalChange + adornedElement.Height, hitThumb.DesiredSize.Height);
double width_old = adornedElement.Width;
double width_new = Math.Max(adornedElement.Width - args.HorizontalChange, hitThumb.DesiredSize.Width);
double left_old = Canvas.GetLeft(adornedElement);
adornedElement.Width = width_new;
Canvas.SetLeft(adornedElement, left_old - (width_new - width_old));
}
// Arrange the Adorners.
protected override Size ArrangeOverride(Size finalSize)
{
// desiredWidth and desiredHeight are the width and height of the element that's being adorned.
// These will be used to place the ResizingAdorner at the corners of the adorned element.
double desiredWidth = AdornedElement.DesiredSize.Width;
double desiredHeight = AdornedElement.DesiredSize.Height;
// adornerWidth & adornerHeight are used for placement as well.
double adornerWidth = this.DesiredSize.Width;
double adornerHeight = this.DesiredSize.Height;
topLeft.Arrange(new Rect(-adornerWidth / 2, -adornerHeight / 2, adornerWidth, adornerHeight));
topRight.Arrange(new Rect(desiredWidth - adornerWidth / 2, -adornerHeight / 2, adornerWidth, adornerHeight));
bottomLeft.Arrange(new Rect(-adornerWidth / 2, desiredHeight - adornerHeight / 2, adornerWidth, adornerHeight));
bottomRight.Arrange(new Rect(desiredWidth - adornerWidth / 2, desiredHeight - adornerHeight / 2, adornerWidth, adornerHeight));
// Return the final size.
return finalSize;
}
// Helper method to instantiate the corner Thumbs, set the Cursor property,
// set some appearance properties, and add the elements to the visual tree.
void BuildAdornerCorner(ref Thumb cornerThumb, Cursor customizedCursor)
{
if (cornerThumb != null) return;
cornerThumb = new Thumb();
// Set some arbitrary visual characteristics.
cornerThumb.Cursor = customizedCursor;
cornerThumb.Height = cornerThumb.Width = 5;
cornerThumb.Opacity = 0.40;
cornerThumb.Background = new SolidColorBrush(Colors.White);
visualChildren.Add(cornerThumb);
}
// This method ensures that the Widths and Heights are initialized. Sizing to content produces
// Width and Height values of Double.NaN. Because this Adorner explicitly resizes, the Width and Height
// need to be set first. It also sets the maximum size of the adorned element.
void EnforceSize(FrameworkElement adornedElement)
{
if (adornedElement.Width.Equals(Double.NaN))
adornedElement.Width = adornedElement.DesiredSize.Width;
if (adornedElement.Height.Equals(Double.NaN))
adornedElement.Height = adornedElement.DesiredSize.Height;
FrameworkElement parent = adornedElement.Parent as FrameworkElement;
if (parent != null)
{
adornedElement.MaxHeight = parent.ActualHeight;
adornedElement.MaxWidth = parent.ActualWidth;
}
}
// Override the VisualChildrenCount and GetVisualChild properties to interface with
// the adorner's visual collection.
protected override int VisualChildrenCount { get { return visualChildren.Count; } }
protected override Visual GetVisualChild(int index) { return visualChildren[index]; }
}
}
/END EDIT
Any ideas why this doesn't work when used in the ItemsControl like above and how I could go about fixing it?
Each item in an ItemsControl is wrapped by a ContentPresenter. This is the element that needs to be positioned during the drag/drop operation, not the actual element contained. You can see this is the case when you specify the ItemContainerStyle:
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Canvas.Left" Value="{Binding Path=X, Mode=TwoWay}" />
<Setter Property="Canvas.Top" Value="{Binding Path=Y, Mode=TwoWay}" />
</Style>
</ItemsControl.ItemContainerStyle>
In the current code, the originalLeft and originalTop values are initialized using the selectedElement (whose attached properties Canvas.Left and Canvas.Top are not used to position the element) in the PreviewMouseLeftButtonDown event. In the MouseMove event, the left and top values are set for the selectedElement, which has no effect because it's the ContentPresenter that must be positioned.
To fix this, the PreviewMouseLeftButtonDown handler should (something like):
selectedElement = e.Source as UIElement;
selectedPresenter = System.Windows.Media.VisualTreeHelper.GetParent(selectedElement) as ContentPresenter;
originalLeft = GetLeft(selectedPresenter);
originalTop = GetTop(selectedPresenter);
And in the MouseMove handler, you need to use the selectedPresenter and not the selectedElement:
SetTop(selectedPresenter, position.Y - (startPoint.Y - originalTop));
SetLeft(selectedPresenter, position.X - (startPoint.X - originalLeft));
From the code you posted, I couldn't tell how you were populating the ItemsControl or what you were populating it with. In order to test this myself, I had an ObservableCollection bound to the ItemsControl.ItemsSource property and an ItemsControl.ItemTemplate as specified below:
<ItemsControl.ItemTemplate>
<DataTemplate>
<Rectangle Width="100" Height="100" Fill="Blue"></Rectangle>
</DataTemplate>
</ItemsControl.ItemTemplate>
Since the data template was a FrameworkElement with a Width and Height property, the resizing worked just fine. However, I had to make the above changes for the drag/drop to work properly.
Related
I was making a WPF application with Windows style = None, I managed to create and work exit button in my window but i don't know how to make it drag able while pressing left mouse button.
I have created Mouse left button down event in .cs file as below:
private void see(object sender, MouseButtonEventArgs e)
{
this.DragMove();
}
Then I added border in .xaml file to do dragging of the window as below:
<Grid>
<Border BorderThickness="2" BorderBrush="Black" Height="120" Width="100" MouseLeftButtonDown="see" />
</Grid>
Now i don't understand what is the problem here? I will be very thankful if someone help me in this ?
Use a similar pattern to this Window:
public class DragableWindowNoStyle : Window
{
public DragableWindowNoStyle()
{
WindowStyle = WindowStyle.None;
Grid grid = new Grid() { };
_moveBorder = new Border()
{
BorderThickness = new Thickness(2),
BorderBrush = Brushes.Red,
Background = Brushes.Black,
Width = 50,
Height = 20,
HorizontalAlignment= System.Windows.HorizontalAlignment.Center,
VerticalAlignment = System.Windows.VerticalAlignment.Top,
};
grid.Children.Add(_moveBorder);
this.Content = grid;
_moveBorder.PreviewMouseLeftButtonDown += _moveBorder_PreviewMouseLeftButtonDown;
}
Point _startPoint;
bool _isDragging = false;
Border _moveBorder;
private void _moveBorder_PreviewMouseLeftButtonDown(object sender, MouseButtonEventArgs e)
{
if (Mouse.Capture(this))
{
_isDragging = true;
_startPoint = PointToScreen(Mouse.GetPosition(this));
}
}
protected override void OnPreviewMouseMove(MouseEventArgs e)
{
if (_isDragging)
{
Point newPoint = PointToScreen(Mouse.GetPosition(this));
int diffX = (int)(newPoint.X - _startPoint.X);
int diffY = (int)(newPoint.Y - _startPoint.Y);
if (Math.Abs(diffX) > 1 || Math.Abs(diffY) > 1)
{
Left += diffX;
Top += diffY;
InvalidateVisual();
_startPoint = newPoint;
}
}
}
protected override void OnPreviewMouseLeftButtonUp(MouseButtonEventArgs e)
{
if (_isDragging)
{
_isDragging = false;
Mouse.Capture(null);
}
}
}
There is an example of how to create a custom window with resize, drag, minimize, restore and close functionality from scratch available here:
How to create a custom window in WPF: https://blog.magnusmontin.net/2013/03/16/how-to-create-a-custom-window-in-wpf/
You could also customize a window while retaining its standard functionality using the WindowChrome class: https://msdn.microsoft.com/en-us/library/system.windows.shell.windowchrome(v=vs.110).aspx. Then you don't have to implement the resize and drag functionality yourself.
I have put together a simple WPF application to demonstrate the issue I am having. My XAML is below:
<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="427" Width="467" Loaded="MainWindow_OnLoaded">
<Grid>
<ScrollViewer Name="MyScrollViewer" CanContentScroll="True">
<Image Name="MyImage" HorizontalAlignment="Left" VerticalAlignment="Top" MouseWheel="UIElement_OnMouseWheel" MouseDown="MyImage_OnMouseDown" MouseUp="MyImage_OnMouseUp"/>
</ScrollViewer>
</Grid>
</Window>
The code-behind is below:
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace WpfApplication1
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
}
private void UIElement_OnMouseWheel(object sender, MouseWheelEventArgs e)
{
var matrix = MyImage.RenderTransform.Value;
if (e.Delta > 0)
{
matrix.ScaleAt(1.5, 1.5, e.GetPosition(this).X, e.GetPosition(this).Y);
}
else
{
matrix.ScaleAt(1.0 / 1.5, 1.0 / 1.5, e.GetPosition(this).X, e.GetPosition(this).Y);
}
MyImage.RenderTransform = new MatrixTransform(matrix);
}
private WriteableBitmap writeableBitmap;
private void MainWindow_OnLoaded(object sender, RoutedEventArgs e)
{
var image = new WriteableBitmap(new BitmapImage(new Uri(#"C:\myImage.png", UriKind.Absolute)));
MyImage.Width = image.Width;
MyImage.Height = image.Height;
image = BitmapFactory.ConvertToPbgra32Format(image);
writeableBitmap = image;
MyImage.Source = image;
}
private Point downPoint;
private Point upPoint;
private void MyImage_OnMouseDown(object sender, MouseButtonEventArgs e)
{
downPoint = e.GetPosition(MyImage);
}
private void MyImage_OnMouseUp(object sender, MouseButtonEventArgs e)
{
upPoint = e.GetPosition(MyImage);
writeableBitmap.DrawRectangle(Convert.ToInt32(downPoint.X), Convert.ToInt32(downPoint.Y), Convert.ToInt32(upPoint.X), Convert.ToInt32(upPoint.Y), Colors.Red);
MyImage.Source = writeableBitmap;
}
}
}
I have added WriteableBitmapEx using Nuget. If you run this, and replace myImage.png with the location of an actual image on your computer, you will find an application that looks something like this:
You can draw a box on the image by clicking on the top-left of where you want the box to go and dragging to the bottom right of where you want the box to go, you get a red rectangle. You can even zoom in with the middle-mouse, and draw a rectangle up close for more precision, this works as expected.
The problem is that, when you scroll in with the middle mouse, the scroll bars don't re-adjust, which is a requirement of the program I am creating. My question is how do I force the scrollbars on the scrollviewer to re-adjust when the image is zoomed in?
I am convinced it has something to do with the RenderTransform property of the ScrollViewer, and that I need to update it at the same time I update the RenderTransform property of the image (on UIElement_OnMouseWheel) but I am unsure of exactly how to go about this.
You should be using LayoutTransform instead of RenderTransform on your Image.
RenderTransform happens after layout completes and is visual only. LayoutTransform is done before the layout pass and so can notify the ScrollViewer of the new size.
See here for more info: http://msdn.microsoft.com/en-us/library/system.windows.frameworkelement.layouttransform.aspx
For pure scrolling I'd rather use a ScaleTransform, it should adjust the scrollbars accordingly.
You can try below code if it fixes your issue.
private double _zoomValue = 1.0;
private void UIElement_OnMouseWheel(object sender, MouseWheelEventArgs e)
{
if (e.Delta > 0)
{
_zoomValue += 0.1;
}
else
{
_zoomValue -= 0.1;
}
ScaleTransform scale = new ScaleTransform(_zoomValue, _zoomValue);
MyImage.LayoutTransform = scale;
e.Handled = true;
}
Let's assume you have a Canvas_Main inside a ViewBox_CanvasMain, which in turn inside a ScrollViewer_CanvasMain. You want to zoom in by turning the mouse wheel, and the ScrollViewer will adjust the offset automatically so the feature (pointed by the mouse in the Canvas_Main) stays during the zoom in/out. It is complex but here is the code called by the mouse wheel eventhandler:
private void MouseWheelZoom(MouseWheelEventArgs e)
{
if(Canvas_Main.IsMouseOver)
{
Point mouseAtImage = e.GetPosition(Canvas_Main); // ScrollViewer_CanvasMain.TranslatePoint(middleOfScrollViewer, Canvas_Main);
Point mouseAtScrollViewer = e.GetPosition(ScrollViewer_CanvasMain);
ScaleTransform st = ViewBox_CanvasMain.LayoutTransform as ScaleTransform;
if (st == null)
{
st = new ScaleTransform();
ViewBox_CanvasMain.LayoutTransform = st;
}
if (e.Delta > 0)
{
st.ScaleX = st.ScaleY = st.ScaleX * 1.25;
if (st.ScaleX > 64) st.ScaleX = st.ScaleY = 64;
}
else
{
st.ScaleX = st.ScaleY = st.ScaleX / 1.25;
if (st.ScaleX < 1) st.ScaleX = st.ScaleY = 1;
}
#region [this step is critical for offset]
ScrollViewer_CanvasMain.ScrollToHorizontalOffset(0);
ScrollViewer_CanvasMain.ScrollToVerticalOffset(0);
this.UpdateLayout();
#endregion
Vector offset = Canvas_Main.TranslatePoint(mouseAtImage, ScrollViewer_CanvasMain) - mouseAtScrollViewer; // (Vector)middleOfScrollViewer;
ScrollViewer_CanvasMain.ScrollToHorizontalOffset(offset.X);
ScrollViewer_CanvasMain.ScrollToVerticalOffset(offset.Y);
this.UpdateLayout();
e.Handled = true;
}
}
This is going to be straight forward no doubt, but for what ever reason, my mind is drawing a blank on it.
I've got a small, non-resizeable window (325x450) which has 3 Expanders in it, stacked vertically. Each Expander contains an ItemsControl that can potentially have a lot of items in and therefore need to scroll.
What I can't seem to get right is how to layout the Expanders so that they expand to fill any space that is available without pushing other elements off the screen. I can sort of achieve what I'm after by using a Grid and putting each expander in a row with a * height, but this means they are always taking up 1/3 of the window each which defeats the point of the Expander :)
Crappy diagram of what I'm trying to achieve:
This requirement is a little unusal because the you want the state of the Children in the Grid to decide the Height of the RowDefinition they are in.
I really like the layout idea though and I can't believe I never had a similar requirement myself.. :)
For a reusable solution I would use an Attached Behavior for the Grid.
The behavior will subscribe to the Attached Events Expander.Expanded and Expander.Collapsed and in the event handlers, get the right RowDefinition from Grid.GetRow and update the Height accordingly. It works like this
<Grid ex:GridExpanderSizeBehavior.SizeRowsToExpanderState="True">
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition />
<RowDefinition />
</Grid.RowDefinitions>
<Expander Grid.Row="0" ... />
<Expander Grid.Row="1" ... />
<Expander Grid.Row="2" ... />
<!-- ... -->
</Grid>
And here is GridExpanderSizeBehavior
public class GridExpanderSizeBehavior
{
public static DependencyProperty SizeRowsToExpanderStateProperty =
DependencyProperty.RegisterAttached("SizeRowsToExpanderState",
typeof(bool),
typeof(GridExpanderSizeBehavior),
new FrameworkPropertyMetadata(false, SizeRowsToExpanderStateChanged));
public static void SetSizeRowsToExpanderState(Grid grid, bool value)
{
grid.SetValue(SizeRowsToExpanderStateProperty, value);
}
private static void SizeRowsToExpanderStateChanged(object target, DependencyPropertyChangedEventArgs e)
{
Grid grid = target as Grid;
if (grid != null)
{
if ((bool)e.NewValue == true)
{
grid.AddHandler(Expander.ExpandedEvent, new RoutedEventHandler(Expander_Expanded));
grid.AddHandler(Expander.CollapsedEvent, new RoutedEventHandler(Expander_Collapsed));
}
else if ((bool)e.OldValue == true)
{
grid.RemoveHandler(Expander.ExpandedEvent, new RoutedEventHandler(Expander_Expanded));
grid.RemoveHandler(Expander.CollapsedEvent, new RoutedEventHandler(Expander_Collapsed));
}
}
}
private static void Expander_Expanded(object sender, RoutedEventArgs e)
{
Grid grid = sender as Grid;
Expander expander = e.OriginalSource as Expander;
int row = Grid.GetRow(expander);
if (row <= grid.RowDefinitions.Count)
{
grid.RowDefinitions[row].Height = new GridLength(1.0, GridUnitType.Star);
}
}
private static void Expander_Collapsed(object sender, RoutedEventArgs e)
{
Grid grid = sender as Grid;
Expander expander = e.OriginalSource as Expander;
int row = Grid.GetRow(expander);
if (row <= grid.RowDefinitions.Count)
{
grid.RowDefinitions[row].Height = new GridLength(1.0, GridUnitType.Auto);
}
}
}
If you don't mind a little code-behind, you could probably hook into the Expanded/Collapsed events, find the parent Grid, get the RowDefinition for the expander, and set the value equal to * if its expanded, or Auto if not.
For example,
Expander ex = sender as Expander;
Grid parent = FindAncestor<Grid>(ex);
int rowIndex = Grid.GetRow(ex);
if (parent.RowDefinitions.Count > rowIndex && rowIndex >= 0)
parent.RowDefinitions[rowIndex].Height =
(ex.IsExpanded ? new GridLength(1, GridUnitType.Star) : GridLength.Auto);
And the FindAncestor method is defined as this:
public static T FindAncestor<T>(DependencyObject current)
where T : DependencyObject
{
// Need this call to avoid returning current object if it is the
// same type as parent we are looking for
current = VisualTreeHelper.GetParent(current);
while (current != null)
{
if (current is T)
{
return (T)current;
}
current = VisualTreeHelper.GetParent(current);
};
return null;
}
Whenever a node is selected in my treeview, it automatically does a horizontal scroll to that item. Is there a way to disable this?
Handle the RequestBringIntoView event and set Handled to true, and the framework won't try to bring the item into view. For example, do something like this in your XAML:
<TreeView>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<EventSetter Event="RequestBringIntoView" Handler="TreeViewItem_RequestBringIntoView"/>
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
And then this in your code-behind:
private void TreeViewItem_RequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
e.Handled = true;
}
I managed to solve the problem using the following:
<TreeView ScrollViewer.HorizontalScrollBarVisibility="Hidden">
<TreeView.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel MaxWidth="{Binding ActualWidth, RelativeSource={RelativeSource AncestorType=ContentPresenter, AncestorLevel=1}}" />
</ItemsPanelTemplate>
</TreeView.ItemsPanel>
</TreeView>
I bind the width of the StackPanel which renders the ItemsPanel here, to the ActualWidth of ContentPresenter in the TreeView.
It also works nice with the "hacked" Stretching TreeView by: http://blogs.msdn.com/b/jpricket/archive/2008/08/05/wpf-a-stretching-treeview.aspx (I modified that solution not to remove grid column, but to change Grid.Column property of the first Decorator element from 1 to 2).
To offer a slightly simplified version of #lena's answer:
To scroll vertically while preserving the horizontal scroll position, and with no unwanted side effects, in the XAML, add event handlers for RequestBringIntoView and Selected:
<TreeView>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="RequestBringIntoView" Handler="TreeViewItem_RequestBringIntoView"/>
<EventSetter Event="Selected" Handler="OnSelected"/>
...
In the code behind, add two event handlers:
private void TreeViewItem_RequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
// Ignore re-entrant calls
if (mSuppressRequestBringIntoView)
return;
// Cancel the current scroll attempt
e.Handled = true;
// Call BringIntoView using a rectangle that extends into "negative space" to the left of our
// actual control. This allows the vertical scrolling behaviour to operate without adversely
// affecting the current horizontal scroll position.
mSuppressRequestBringIntoView = true;
TreeViewItem tvi = sender as TreeViewItem;
if (tvi != null)
{
Rect newTargetRect = new Rect(-1000, 0, tvi.ActualWidth + 1000, tvi.ActualHeight);
tvi.BringIntoView(newTargetRect);
}
mSuppressRequestBringIntoView = false;
}
private bool mSuppressRequestBringIntoView;
// Correctly handle programmatically selected items
private void OnSelected(object sender, RoutedEventArgs e)
{
((TreeViewItem)sender).BringIntoView();
e.Handled = true;
}
Matthew, I manged to preserve vertical scrolling, and only prevent horizontal scrolling by restoring the horizontal position after a scroll caused by the RequestBringIntoView event .
private double treeViewHorizScrollPos = 0.0;
private bool treeViewResetHorizScroll = false;
private ScrollViewer treeViewScrollViewer = null;
private void TreeViewItemRequestBringIntoView( object sender, RequestBringIntoViewEventArgs e )
{
if ( this.treeViewScrollViewer == null )
{
this.treeViewScrollViewer = this.DetailsTree.Template.FindName( "_tv_scrollviewer_", this.DetailsTree ) as ScrollViewer;
if( this.treeViewScrollViewer != null )
this.treeViewScrollViewer.ScrollChanged += new ScrollChangedEventHandler( this.TreeViewScrollViewerScrollChanged );
}
this.treeViewResetHorizScroll = true;
this.treeViewHorizScrollPos = this.treeViewScrollViewer.HorizontalOffset;
}
private void TreeViewScrollViewerScrollChanged( object sender, ScrollChangedEventArgs e )
{
if ( this.treeViewResetHorizScroll )
this.treeViewScrollViewer.ScrollToHorizontalOffset( this.treeViewHorizScrollPos );
this.treeViewResetHorizScroll = false;
}
I had a similar problem. I needed to prevent horizontal scroll but preserve vertical scroll. My solution is to handle OnRequestBringIntoView method as I want it to behave. I created a ResourceDictionary for a TreeViewItem and added EventSetters for OnSelected and OnRequestBringIntoView methods.
MyResourceDictionary.xaml
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" x:Class="Resources.MyResourceDictionary" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Style TargetType="TreeViewItem" x:Key="treeitem" >
<EventSetter Event="RequestBringIntoView" Handler="OnRequestBringIntoView"/>
<EventSetter Event="Selected" Handler="OnSelected"/>
</Style>
</ResourceDictionary>
MyResourceDictionary.xaml.cs
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
namespace Resources
{
partial class MyResourceDictionary:ResourceDictionary
{
public MyResourceDictionary()
{
InitializeComponent();
}
private void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
e.Handled = true; //prevent event bubbling
var item = (TreeViewItem)sender;
TreeView tree = GetParentTree(item) as TreeView;
if(tree!=null)
{
var scrollViewer = tree.Template.FindName("_tv_scrollviewer_", tree) as ScrollViewer;
if (scrollViewer != null)
{
scrollViewer.ScrollToLeftEnd();//prevent horizontal scroll
Point relativePoint = item.TransformToAncestor(tree).Transform(new Point(0, 0));//get position of a selected item
if (relativePoint.Y <= scrollViewer.ContentVerticalOffset) return;//do no scroll if we select inside one 'scroll screen'
scrollViewer.ScrollToVerticalOffset(relativePoint.Y);//scroll to Y of a selected item
}
}
}
private DependencyObject GetParentTree(DependencyObject item)
{
var target = VisualTreeHelper.GetParent(item);
return target as TreeView != null ? target : GetParentTree(target);
}
private void OnSelected(object sender, RoutedEventArgs e) //handle programmatically selected items
{
var item = (TreeViewItem)sender;
item.BringIntoView();
e.Handled = true;
}
}
}
Following solution is more simple and fully tested and more compatible, You don't need to calculate and change scrollbar offset, what you need is moving horizontal scrollbar to left, since "RequestBringIntoView" event routing strategy is bubbling, you simply need to do it on last item reached event.
Name scrollViewer control "_tv_scrollviewer_"
<TreeView>
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="RequestBringIntoView" Handler="TreeViewItem_RequestBringIntoView"/>
On code behind:
private void OnRequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
var item = (TreeViewItem)sender;
if (item != null)
{
// move horizontal scrollbar only when event reached last parent item
if (item.Parent == null)
{
var scrollViewer = itemsTree.Template.FindName("_tv_scrollviewer_", itemsTree) as ScrollViewer;
if (scrollViewer != null)
Dispatcher.BeginInvoke(DispatcherPriority.Loaded, (Action)(() => scrollViewer.ScrollToLeftEnd()));
}
}
}
#lena's solution of preserving vertical scrolling worked best for me. I've iterated on it a little bit:
private void TreeViewItem_RequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
var treeViewItem = (TreeViewItem)sender;
var scrollViewer = treeView.Template.FindName("_tv_scrollviewer_", treeView) as ScrollViewer;
Point topLeftInTreeViewCoordinates = treeViewItem.TransformToAncestor(treeView).Transform(new Point(0, 0));
var treeViewItemTop = topLeftInTreeViewCoordinates.Y;
if (treeViewItemTop < 0
|| treeViewItemTop + treeViewItem.ActualHeight > scrollViewer.ViewportHeight
|| treeViewItem.ActualHeight > scrollViewer.ViewportHeight)
{
// if the item is not visible or too "tall", don't do anything; let them scroll it into view
return;
}
// if the item is already fully within the viewport vertically, disallow horizontal scrolling
e.Handled = true;
}
What this does is let the ScrollViewer scroll normally if the item isn't already in the viewport vertically. However for the actual "annoying" case (where the item is already visible), it sets e.Handled to true, thus preventing horizontal scrolling.
I had a DataGrid that I wanted to do the same operation on and used POHB's answer mostly. I had to modify it for my solution. The code is shown below. The datagrid is a 2 x 2 datagrid with the first column being thin and the second being very wide (1000+). The first column is frozen. I hope this helps someone out. -Matt
public partial class MyUserControl : UserControl
{
private ScrollContentPresenter _scrollContentPresenter;
private ScrollViewer _scrollViewer;
private double _dataGridHorizScrollPos = 0.0;
private bool _dataGridResetHorizScroll = false;
public MyUserControl()
{
// setup code...
_dataGrid.ApplyTemplate();
_scrollViewer = FindVisualChild<ScrollViewer>(_dataGrid);
_scrollViewer.ScrollChanged += new ScrollChangedEventHandler(DataGridScrollViewerScrollChanged);
_scrollContentPresenter = FindVisualChild<ScrollContentPresenter>(_scrollViewer);
_scrollContentPresenter.RequestBringIntoView += new RequestBringIntoViewEventHandler(_scrollContentPresenter_RequestBringInputView);
}
private void DataGridScrollViewerScrollChanged(object sender, ScrollChangedEventArgs e)
{
if (_dataGridResetHorizScroll)
{
_scrollViewer.ScrollToHorizontalOffset(_dataGridHorizScrollPos);
}
// Note: When the row just before a page change is selected and then the next row on the
// next page is selected, a second event fires setting the horizontal offset to 0
// I'm ignoring those large changes by only recording the offset when it's large. -MRB
else if (Math.Abs(e.HorizontalChange) < 100)
{
_dataGridHorizScrollPos = _scrollViewer.HorizontalOffset;
}
_dataGridResetHorizScroll = false;
}
public T FindVisualChild<T>(DependencyObject depObj) where T : DependencyObject
{
if (depObj != null)
{
for (int i = 0; i < VisualTreeHelper.GetChildrenCount(depObj); i++)
{
DependencyObject child = VisualTreeHelper.GetChild(depObj, i);
if ((child != null) && (child is ScrollViewer))
{
// I needed this since the template wasn't applied yet when
// calling from the constructor
(child as ScrollViewer).ApplyTemplate();
}
if (child != null && child is T)
{
return (T)child;
}
T childItem = FindVisualChild<T>(child);
if (childItem != null) return childItem;
}
}
return null;
}
private void _scrollContentPresenter_RequestBringInputView(object sender, RequestBringIntoViewEventArgs e)
{
_dataGridResetHorizScroll = true;
}
When I expand items in my treeview so that scrolling is necessary, a scrollbar appears. However, it doesn't scroll down for the newly expanded branch of items - they get cropped by the bottom of the control. So as I continue expanding items at the bottom of the tree, I have to keep manually scrolling down to see the new children. Anyone have a suggestion for how make it automatically scroll to show the newly expanded items?
You can use a simple EventSetter in TreeViewItem style to invoke an event handler when the item is selected. Then call BringIntoView for the item.
<TreeView >
<TreeView.ItemContainerStyle>
<Style TargetType="{x:Type TreeViewItem}">
<EventSetter Event="Selected" Handler="TreeViewSelectedItemChanged" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
private void TreeViewSelectedItemChanged(object sender, RoutedEventArgs e)
{
TreeViewItem item = sender as TreeViewItem;
if (item != null)
{
item.BringIntoView();
e.Handled = true;
}
}
On the TreeView, handle the TreeViewItem.Expanded event (you can do this at the TreeView level because of event bubbling). In the Expanded handler, call BringIntoView on the TreeViewItem that raised the event.
You may need a bit of trial and error to get hold of the TreeViewItem in your event handler code. I think (haven't checked) that the sender argument to your Expanded event handler will be the TreeView (since that's where the event handler is attached) rather than the TreeViewItem. And the e.Source or e.OriginalSource may be an element in the TreeViewItem's data template. So you may need to use VisualTreeHelper to walk up the visual tree to find the TreeViewItem. But if you use the debugger to inspect the sender and the RoutedEventArgs this should be trivial to figure out.
(If you're able to get this working and want to bundle it up so you don't have to attach the same event handler to every TreeView, it should be easy to encapsulate it as an attached behaviour which will allow you to apply it declaratively, including via a Style.)
Use a dependency property on an IsSelected trigger:
<Style TargetType="{x:Type TreeViewItem}">
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="commands:TreeViewItemBehavior.BringIntoViewWhenSelected" Value="True" />
</Trigger>
</Style.Triggers>
Here's the code for the dependency property:
public static bool GetBringIntoViewWhenSelected(TreeViewItem treeViewItem)
{
return (bool)treeViewItem.GetValue(BringIntoViewWhenSelectedProperty);
}
public static void SetBringIntoViewWhenSelected(TreeViewItem treeViewItem, bool value)
{
treeViewItem.SetValue(BringIntoViewWhenSelectedProperty, value);
}
public static readonly DependencyProperty BringIntoViewWhenSelectedProperty =
DependencyProperty.RegisterAttached("BringIntoViewWhenSelected", typeof(bool),
typeof(TreeViewItemBehavior), new UIPropertyMetadata(false, OnBringIntoViewWhenSelectedChanged));
static void OnBringIntoViewWhenSelectedChanged(DependencyObject depObj, DependencyPropertyChangedEventArgs e)
{
TreeViewItem item = depObj as TreeViewItem;
if (item == null)
return;
if (e.NewValue is bool == false)
return;
if ((bool)e.NewValue)
item.BringIntoView();
}
Thanks to itowlson's answer, here's the expanded event handler code that works for both of my trees
private static void Tree_Expanded(object sender, RoutedEventArgs e)
{
// ignore checking, assume original source is treeviewitem
var treeViewItem = (TreeViewItem)e.OriginalSource;
var count = VisualTreeHelper.GetChildrenCount(treeViewItem);
for (int i = count - 1; i >= 0; --i)
{
var childItem = VisualTreeHelper.GetChild(treeViewItem, i);
((FrameworkElement)childItem).BringIntoView();
}
// do NOT call BringIntoView on the actual treeviewitem - this negates everything
//treeViewItem.BringIntoView();
}
I modified Jared's answer in combination with the strategy from here: https://stackoverflow.com/a/42238409/2477582
The main advantage is that there aren't n calls of BringIntoView() for n childs. There is only one call of BringIntoView for an area that covers all of the child's heights.
Additionally, the purpose of the referred topic is realized as well. But this part may be removed, if unwanted.
/// <summary>Prevents automatic horizontal scrolling, while preserving automatic vertical scrolling and other side effects</summary>
/// <remarks>Source: https://stackoverflow.com/a/42238409/2477582 </remarks>
private void TreeViewItem_RequestBringIntoView(object sender, RequestBringIntoViewEventArgs e)
{
// Ignore re-entrant calls
if (m_SuppressRequestBringIntoView)
return;
// Cancel the current scroll attempt
e.Handled = true;
// Call BringIntoView using a rectangle that extends into "negative space" to the left of our
// actual control. This allows the vertical scrolling behaviour to operate without adversely
// affecting the current horizontal scroll position.
m_SuppressRequestBringIntoView = true;
try
{
TreeViewItem tvi = sender as TreeViewItem;
if (tvi != null)
{
// take care of children
int ll_ChildCount = VisualTreeHelper.GetChildrenCount(tvi);
double ll_Height = tvi.ActualHeight;
if (ll_ChildCount > 0)
{
FrameworkElement ll_LastChild = VisualTreeHelper.GetChild(tvi, ll_ChildCount - 1) as FrameworkElement;
ll_Height += ll_ChildCount * ll_LastChild.ActualHeight;
}
Rect newTargetRect = new Rect(-1000, 0, tvi.ActualWidth + 1000, ll_Height);
tvi.BringIntoView(newTargetRect);
}
}
catch (Exception ex)
{
m_Log.Debug("Error in TreeViewItem_RequestBringIntoView: " + ex.ToString());
}
m_SuppressRequestBringIntoView = false;
}
The above solution works together with this:
/// <summary>Correctly handle programmatically selected items (needed due to the custom implementation of TreeViewItem_RequestBringIntoView)</summary>
/// <remarks>Source: https://stackoverflow.com/a/42238409/2477582 </remarks>
private void TreeViewItem_Selected(object sender, RoutedEventArgs e)
{
((TreeViewItem)sender).BringIntoView();
e.Handled = true;
}
This part takes care of toggling the elements at each click:
/// <summary>Support for single click toggle</summary>
private void TreeViewItem_MouseUp(object sender, MouseButtonEventArgs e)
{
TreeViewItem tvi = null;
// Source may be TreeViewItem directly, or be a ContentPresenter
if (e.Source is TreeViewItem)
{
tvi = e.Source as TreeViewItem;
}
else if (e.Source is ContentPresenter)
{
tvi = (e.Source as ContentPresenter).TemplatedParent as TreeViewItem;
}
if (tvi == null || e.Handled) return;
tvi.IsExpanded = !tvi.IsExpanded;
e.Handled = true;
}
Finally the XAML part:
<TreeView>
<TreeView.ItemContainerStyle>
<Style TargetType="TreeViewItem">
<EventSetter Event="RequestBringIntoView" Handler="TreeViewItem_RequestBringIntoView" />
<EventSetter Event="Selected" Handler="TreeViewItem_Selected" />
</Style>
</TreeView.ItemContainerStyle>
</TreeView>
A simple event listener on the tree worked for me:
<TreeView Margin="10,40,10,10" Grid.Column="0" x:Name="treeView" HorizontalAlignment="Stretch" VerticalAlignment="Stretch" SelectedItemChanged="TreeView_SelectedItemChanged" />
private void TreeView_SelectedItemChanged(object sender, RoutedPropertyChangedEventArgs<object> e) {
if (e.NewValue == null)
return;
((TreeViewItem)e.NewValue).BringIntoView();
}