How to make drag-able WPF window without default windows style? - wpf

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.

Related

Wpf Animating Opacity and Drag Moving Window at the Same Time Makes Window Stutter

I'm using this code to lower window's opacity when user moving it and after drag move is completed i'm incresing window's opacity to it's original value.
private void TopBar_Grid_MouseDown(object sender, MouseButtonEventArgs e)
{
if (e.ChangedButton == MouseButton.Left)
{
var decreaseOpacityAnim = new DoubleAnimation(0.5, (Duration)TimeSpan.FromSeconds(1));
this.BeginAnimation(UIElement.OpacityProperty, decreaseOpacityAnim);
this.DragMove();
var increaseOpacityAnim = new DoubleAnimation(1, (Duration)TimeSpan.FromSeconds(1));
this.BeginAnimation(UIElement.OpacityProperty, increaseOpacityAnim);
}
}
The problem is when user started to move window and the time window's opacity decreasing, window moves stutter. When animation completed window starts moving without stuttering.
Is there anything i can do to fix this?
I handled MouseMove instead of MouseDown. And secondly, you are using both animations while dragging, hence this stutter. Below code works absolutely fine for me.
<Window ...>
<Grid Background="Yellow" MouseMove="Grid_MouseMove_1"/>
</Window>
Code :
private bool animation_is_running = false;
private void Grid_MouseMove_1(object sender, MouseEventArgs e)
{
if (Mouse.LeftButton == MouseButtonState.Pressed)
{
if (!animation_is_running)
{
var decreaseOpacityAnim = new DoubleAnimation(0.5, (Duration)TimeSpan.FromSeconds(1));
this.BeginAnimation(UIElement.OpacityProperty, decreaseOpacityAnim);
animation_is_running = true;
}
this.DragMove();
}
else
{
if (animation_is_running)
{
var increaseOpacityAnim = new DoubleAnimation(1, (Duration)TimeSpan.FromSeconds(1));
this.BeginAnimation(UIElement.OpacityProperty, increaseOpacityAnim);
animation_is_running = false;
}
}
}

How to make everything opacity=0.5 aside from customdialogbox

Im new in WPF and Im creating a custom dialog (or a message box) in WPF.
My problem is I cant change everything behind my custom dialog to Opacity=0.5. Only the form who called the custom dialog..
I just want to show my dialog box like in Windows 8.1
Anything would be highly appreciated.
TIA!
Set a property in your MainWindow that controls Opacity of the LayoutRoot. This will change the opacity of your App when the dialog is displayed.
Sample code:
<Grid Name="LayoutRoot" Opacity="{Binding MainWindowOpacity}">
<StackPanel>
<Button Click="Button_Click" Content="Click Me to Show Dialog"/>
<TextBlock Text="WPF" FontSize="72" Margin="50" Foreground="Orange" HorizontalAlignment="Center"/>
</StackPanel>
</Grid>
and
public partial class MainWindow : Window
{
public double MainWindowOpacity
{
get { return (double)GetValue(MainWindowOpacityProperty); }
set { SetValue(MainWindowOpacityProperty, value); }
}
// Using a DependencyProperty as the backing store for MainWindowOpacity. This enables animation, styling, binding, etc...
public static readonly DependencyProperty MainWindowOpacityProperty =
DependencyProperty.Register("MainWindowOpacity", typeof(double), typeof(MainWindow), new PropertyMetadata(1.0));
public MainWindow()
{
InitializeComponent();
DataContext = this;
Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
}
private void Button_Click(object sender, RoutedEventArgs e)
{
//if (MainWindowOpacity < 1) MainWindowOpacity = 1.0;
//else MainWindowOpacity = 0.5;
MainWindowOpacity = 0.5;
// show dialog
// boilerplate code from http://stackoverflow.com/questions/6417558/modal-dialog-not-showing-on-top-of-other-windows
Window window = new Window()
{
Title = "WPF Modal Dialog",
ShowInTaskbar = false, // don't show the dialog on the taskbar
Topmost = true, // ensure we're Always On Top
ResizeMode = ResizeMode.NoResize, // remove excess caption bar buttons
Owner = Application.Current.MainWindow,
Width = 300,
Height = 200
};
window.ShowDialog();
MainWindowOpacity = 1.0;
}
}
and the result:
You could add a static method to App.xaml like this:
public partial class App : Application
{
public static void SetWindowsToOpacity(double dOpacity, Window ignoreWindow)
{
foreach (Window win in System.Windows.Application.Current.Windows)
{
if (win != ignoreWindow)
{
win.Opacity = dOpacity;
}
}
}
}
Pass in the Dialog window that you do not want to be set to lower opacity and an opacity value for all the rest.
Call the method like this:
private void button_Click(object sender, RoutedEventArgs e)
{
Window1 dialog = new Window1();
App.SetWindowsToOpacity(0.5, dialog);
dialog.ShowDialog();
App.SetWindowsToOpacity(1.0, null);
}

WPF - Zooming in on an image inside a scroll viewer, and having the scrollbars adjust accordingly

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;
}
}

How to smoothly drag an image box in WPF?

I'm working with WPF 4 and VB.net 2010. My project consists of full-screen windows with a 640x480 grid in the center.
In this project, I want to have various image boxes (which will have the item .png images in them), that the user can drag around and drop in various places on the grid.
In essence, I need to be able to make it possible for the item to be clicked and dragged around the grid, with the image box still visible and the same size as the user moves it around. It should never be able to leave the grid. I also need to be able to determine if the object is over another object, so when the mouse button is released, the dragged object is "dropped", and it triggers a particular block of code.
How do I do this?
Something similar to your 'project', but using C#
From http://pastie.org/1498237
// XAML
<Window x:Class="MyProject.Window1"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Window1" Height="500" Width="500">
<Grid>
<Canvas x:Name="mycanv">
<Image Width="150" x:Name="myimg" Source="some_source.png"/>
</Canvas>
</Grid>
</Window>
//C#
private Point mouseClick;
private double canvasLeft;
private double canvasTop;
public Window1()
{
InitializeComponent();
foreach (object obj in mycanv.Children)
{
try
{
Image img = (Image)obj;
img.PreviewMouseDown += new MouseButtonEventHandler(myimg_MouseDown);
img.PreviewMouseMove += new MouseEventHandler(myimg_MouseMove);
img.PreviewMouseUp += new MouseButtonEventHandler(myimg_MouseUp);
img.TextInput += new TextCompositionEventHandler(myimg_TextInput);
img.LostMouseCapture += new MouseEventHandler(myimg_LostMouseCapture);
img.SetValue(Canvas.LeftProperty, 0.0);
img.SetValue(Canvas.TopProperty, 0.0);
}
catch
{
//do something
}
}
}
void myimg_LostMouseCapture(object sender, MouseEventArgs e)
{
((Image)sender).ReleaseMouseCapture();
}
void myimg_TextInput(object sender, TextCompositionEventArgs e)
{
((Image)sender).ReleaseMouseCapture();
}
void myimg_MouseUp(object sender, MouseButtonEventArgs e)
{
((Image)sender).ReleaseMouseCapture();
}
void myimg_MouseMove(object sender, MouseEventArgs e)
{
if (((Image)sender).IsMouseCaptured)
{
Point mouseCurrent = e.GetPosition(null);
double Left = mouseCurrent.X - canvasLeft;
double Top = mouseCurrent.Y - canvasTop;
((Image)sender).SetValue(Canvas.LeftProperty, canvasLeft + Left);
((Image)sender).SetValue(Canvas.TopProperty, canvasTop + Top);
canvasLeft = Canvas.GetLeft(((Image)sender));
canvasTop = Canvas.GetTop(((Image)sender));
}
}
void myimg_MouseDown(object sender, MouseButtonEventArgs e)
{
mouseClick = e.GetPosition(null);
canvasLeft = Canvas.GetLeft(((Image)sender));
canvasTop = Canvas.GetTop(((Image)sender));
((Image)sender).CaptureMouse();
}

Prevent Automatic Horizontal Scroll in TreeView

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;
}

Resources