0

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?

thatguy
  • 21,059
  • 6
  • 30
  • 40
reonZ
  • 135
  • 2
  • 10
  • By `if the window is on the right side of the screen and vice-versa` you mean when a window is *docked* to the left or right side of the screen? – thatguy Jul 17 '20 at 21:51
  • I was more thinking about when the window crosses the middle of the screen, i am not sure yet about docking though, those windows are frameless overlays that the user can move around on top of another application. – reonZ Jul 17 '20 at 21:56

2 Answers2

1

Positioning the ScrollBar inside of your ScrollViewer depending on the window position requires you to know:

  • The Size of the containing window to know where the center is
  • The window location to determine whether it crossed the center of the screen or not
  • The screen size to know where the center of the screen even is

What makes it addionally difficult are the following factors

  • You need to get the information from your ScrollViewer that is a child of the window that could change
  • Size changes and location changes can also change the side of the screen
  • Screen sizes are difficult to get in WPF, especially in multi-monitor setups

I 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 the custom scroll viewer

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

Creating a control template

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}"/>

Showing the result

In in your main XAML create the control like this to enable both scrollbars and see the result.

<local:AdaptingScrollViewer HorizontalScrollBarVisibility="Visible"
                            VerticalScrollBarVisibility="Visible"/>
thatguy
  • 21,059
  • 6
  • 30
  • 40
1

All you need to do in order to put a scrollbar on the left of a scrollviewer rather than right is to set the flowdirection to RightToLeft.

You will then need to explicitly set the flowdirection to lefttoright on a container within that to stop it affecting contents.

Here's some experimental markup to consider:

        <Grid>
        <ScrollViewer  FlowDirection="RightToLeft"
                       Name="sv"
                       >
            <StackPanel  FlowDirection="LeftToRight">
                <TextBlock Text="Banana"/>
                <TextBlock Text="Banana"/>
                <TextBlock Text="Banana"/>
            </StackPanel>
        </ScrollViewer>
        <ToggleButton HorizontalAlignment="Center" VerticalAlignment="Top"
                      Click="ToggleButton_Click"
                      Content="Flow"
                          />
        </Grid>
    </Window>
Andy
  • 11,864
  • 2
  • 17
  • 20