根据屏幕上的 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,尤其是在多显示器设置中
我将向您展示一个工作示例,以实现您想要的主屏幕。由于这是另一个问题的另一个障碍,您可以从那里开始并根据您的要求进行调整。
为了解决上述问题,我们将使用SizeChanged
和LocationChanged
事件来检测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>
我有一个包含 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,尤其是在多显示器设置中
我将向您展示一个工作示例,以实现您想要的主屏幕。由于这是另一个问题的另一个障碍,您可以从那里开始并根据您的要求进行调整。
为了解决上述问题,我们将使用SizeChanged
和LocationChanged
事件来检测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>