根据屏幕上的 window 位置交换垂直 ScrollBar 边

Swap vertical ScrollBar side based on window position on screen

我有一个包含 ScrollViewer 的 window,如果 window 在右边,我想要的是将垂直的 ScrollBar 边换到左边屏幕的一侧,反之亦然。

这是我当前的 ScrollViewer 模板 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>

从那里继续前进的方式是什么?

根据 window 位置将 ScrollBar 放置在 ScrollViewer 内部需要您知道:

  • 包含window的大小知道中心在哪里
  • 判断是否越过屏幕中心的window位置
  • 知道屏幕中心的屏幕大小

使它变得更加困难的是以下因素

  • 您需要从您的 ScrollViewer 获取信息,该信息是 window 的子项,可能会更改
  • 尺寸变化和位置变化也可以改变屏幕的边
  • 屏幕尺寸为 difficult to get in WPF,尤其是在多显示器设置中

我将向您展示一个工作示例,以实现您想要的主屏幕。由于这是另一个问题的另一个障碍,您可以从那里开始并根据您的要求进行调整。

为了解决上述问题,我们将使用SizeChangedLocationChanged 事件来检测window 大小和位置的变化。我们将使用 SystemParameters.PrimaryScreenWidth 来获取屏幕宽度, 可以,但可能无法在具有不同分辨率的多显示器设置中使用。

您的控件将改变默认的 ScrollViewer 行为和外观。我认为最好创建一个自定义控件以使其可重用,因为在 XAML 中使用其他技术处理此问题可能会变得混乱。

创建自定义滚动查看器

创建一个继承自 ScrollViewer 的新类型 AdaptingScrollViewer,如下所示。我已经为您注释了代码以解释它是如何工作的。

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

正在创建控件模板

现在我们的新 AdaptingScrollViewer 需要一个控件模板。我以您的示例为例,调整了样式和控件模板,并对更改进行了评论。

<!-- 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>

您还需要在资源字典中的上述样式之后添加以下样式,以便 AdaptingScrollViewer 自动设置样式。

<Style TargetType="{x:Type local:AdaptingScrollViewer}" BasedOn="{StaticResource AdaptingScrollViewerStyle}"/>

正在显示结果

在您的主 XAML 中创建这样的控件以启用两个滚动条并查看结果。

<local:AdaptingScrollViewer HorizontalScrollBarVisibility="Visible"
                            VerticalScrollBarVisibility="Visible"/>

要将滚动条放在滚动查看器的左侧而不是右侧,您需要做的就是将流向设置为从右到左。

然后您需要在其中的容器上将流向明确设置为从左到右,以阻止它影响内容。

这里有一些要考虑的实验性标记:

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