I have a window that contains a ScrollViewer
, What i want is to swap the vertical ScrollBar
side to the left if the window is on the right side of the screen and vice-versa.
Here is my current ScrollViewer
template in a ResourceDictionary
:
<Style x:Key="ScrollViewerWithoutCollapsedVerticalScrollBar" TargetType="{x:Type ScrollViewer}">
<Setter Property="OverridesDefaultStyle" Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type ScrollViewer}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Border Grid.Column="0" BorderThickness="0">
<ScrollContentPresenter />
</Border>
<ScrollBar x:Name="PART_VerticalScrollBar"
Grid.Column="1"
Value="{TemplateBinding VerticalOffset}"
Maximum="{TemplateBinding ScrollableHeight}"
ViewportSize="{TemplateBinding ViewportHeight}"
Visibility="{TemplateBinding ComputedVerticalScrollBarVisibility, Converter={StaticResource ComputedScrollBarVisibilityWithoutCollapse}}" />
<ScrollBar x:Name="PART_HorizontalScrollBar"
Orientation="Horizontal"
Grid.Row="1"
Grid.Column="0"
Value="{TemplateBinding HorizontalOffset}"
Maximum="{TemplateBinding ScrollableWidth}"
ViewportSize="{TemplateBinding ViewportWidth}"
Visibility="{TemplateBinding ComputedHorizontalScrollBarVisibility}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
What would be the way to go on from there?
Positioning the ScrollBar
inside of your ScrollViewer
depending on the window position requires you to know:
What makes it addionally difficult are the following factors
ScrollViewer
that is a child of the window that could changeI will show you a working example to achieve what you want for the primary screen. Since this is another block for a another question, you can start from there and adapt it to your requirements.
In order to tackle the issues above, we will use the SizeChanged
and LocationChanged
events to detect changes of a window in size and location. We will use the SystemParameters.PrimaryScreenWidth
to get the screen width, which can, but may not work in multi-monitor setups with different resolutions.
Your control will alter the default ScrollViewer
behavior and appearance. I think it is best to create a custom control to make it reusable, because dealing with this in XAML with other techniques might become messy.
Create a new type AdaptingScrollViewer
that inherits from ScrollViewer
like below. I have commented the code for you to explain how it works.
public class AdaptingScrollViewer : ScrollViewer
{
// We need this dependency property internally, so that we can bind the parent window
// and get notified when it changes
private static readonly DependencyProperty ContainingWindowProperty =
DependencyProperty.Register(nameof(ContainingWindow), typeof(Window),
typeof(AdaptingScrollViewer), new PropertyMetadata(null, OnContainingWindowChanged));
// Getter and setter for the dependency property value for convenient access
public Window ContainingWindow
{
get => (Window)GetValue(ContainingWindowProperty);
set => SetValue(ContainingWindowProperty, value);
}
static AdaptingScrollViewer()
{
// We have to override the default style key, so that we can apply our new style
// and control template to it
DefaultStyleKeyProperty.OverrideMetadata(typeof(AdaptingScrollViewer),
new FrameworkPropertyMetadata(typeof(AdaptingScrollViewer)));
}
public AdaptingScrollViewer()
{
// Relative source binding to the parent window
BindingOperations.SetBinding(this, ContainingWindowProperty,
new Binding { RelativeSource = new RelativeSource(RelativeSourceMode.FindAncestor, typeof(Window), 1) });
// When the control is removed, we want to clean up and remove the event handlers
Unloaded += OnUnloaded;
}
private void OnUnloaded(object sender, RoutedEventArgs e)
{
RemoveEventHandlers(ContainingWindow);
}
// This method is called when the window in the relative source binding changes
private static void OnContainingWindowChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var scrollViewer = (AdaptingScrollViewer)d;
var oldContainingWindow = (Window)e.OldValue;
var newContainingWindow = (Window)e.NewValue;
// If the scroll viewer got detached from the current window and attached to a new
// window, remove the previous event handlers and add them to the new window
scrollViewer.RemoveEventHandlers(oldContainingWindow);
scrollViewer.AddEventHandlers(newContainingWindow);
}
private void AddEventHandlers(Window window)
{
if (window == null)
return;
// Add events to react to changes of the window size and location
window.SizeChanged += OnSizeChanged;
window.LocationChanged += OnLocationChanged;
// When we add new event handlers, then adapt the scrollbar position immediately
SetScrollBarColumn();
}
private void RemoveEventHandlers(Window window)
{
if (window == null)
return;
// Remove the event handlers to prevent memory leaks
window.SizeChanged -= OnSizeChanged;
window.LocationChanged -= OnLocationChanged;
}
private void OnSizeChanged(object sender, SizeChangedEventArgs e)
{
SetScrollBarColumn();
}
private void OnLocationChanged(object sender, EventArgs e)
{
SetScrollBarColumn();
}
private void SetScrollBarColumn()
{
if (ContainingWindow == null)
return;
// Get the column in the control template grid depending on the center of the screen
var column = ContainingWindow.Left <= GetHorizontalCenterOfScreen(ContainingWindow) ? 0 : 2;
// The scrollbar is part of our control template, so we can get it like this
var scrollBar = GetTemplateChild("PART_VerticalScrollBar");
// If someone overwrote our control template and did not add a scrollbar, ignore
// it instead of crashing the application, because everybody makes mistakes sometimes
scrollBar?.SetValue(Grid.ColumnProperty, column);
}
private static double GetHorizontalCenterOfScreen(Window window)
{
return SystemParameters.PrimaryScreenWidth / 2 - window.Width / 2;
}
}
Now our new AdaptingScrollViewer
nees a control template. I took your example and adapted the style and control template and commented the changes, too.
<!-- Target the style to our new type and base it on scroll viewer to get default properties -->
<Style x:Key="AdaptingScrollViewerStyle"
TargetType="{x:Type local:AdaptingScrollViewer}"
BasedOn="{StaticResource {x:Type ScrollViewer}}">
<Setter Property="Template">
<Setter.Value>
<!-- The control template must also target the new type -->
<ControlTemplate TargetType="{x:Type local:AdaptingScrollViewer}">
<Grid>
<Grid.ColumnDefinitions>
<!-- Added a new column for the left position of the scrollbar -->
<ColumnDefinition Width="Auto"/>
<ColumnDefinition />
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition/>
<RowDefinition Height="Auto"/>
</Grid.RowDefinitions>
<Border Grid.Column="1" BorderThickness="0">
<ScrollContentPresenter/>
</Border>
<ScrollBar x:Name="PART_VerticalScrollBar"
Grid.Row="0"
Grid.Column="2"
Value="{TemplateBinding VerticalOffset}"
Maximum="{TemplateBinding ScrollableHeight}"
ViewportSize="{TemplateBinding ViewportHeight}"/>
<!-- Added a column span to correct the horizontal scroll bar -->
<ScrollBar x:Name="PART_HorizontalScrollBar"
Orientation="Horizontal"
Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="3"
Value="{TemplateBinding HorizontalOffset}"
Maximum="{TemplateBinding ScrollableWidth}"
ViewportSize="{TemplateBinding ViewportWidth}"/>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
You need to add the following style after the style above in your resource dictionary, too, so that the AdaptingScrollViewer
get styled automatically.
<Style TargetType="{x:Type local:AdaptingScrollViewer}" BasedOn="{StaticResource AdaptingScrollViewerStyle}"/>
In in your main XAML create the control like this to enable both scrollbars and see the result.
<local:AdaptingScrollViewer HorizontalScrollBarVisibility="Visible"
VerticalScrollBarVisibility="Visible"/>