AvalonDock 的实现非常奇怪。我记得当我不得不在项目中使用这个控件时也遇到了一些麻烦。我想我很了解这个控制。在我看来,它的实施非常糟糕。因为出于某种原因,他们决定使用 MVVM 自己实现此控件,而不是简单地使其准备好 MVVM。这使得在高级场景中使用非常不方便。
语义也很混乱。例如,容器不是呈现数据的控件。容器被分配给控件的Model 属性。数据未分配给DataContext。不过看起来不错。
此外,选项卡标题放置行为被破坏(仅允许底部的选项卡标题)。我的修复可能会让您感兴趣,尤其是在动态选项卡标题放置的上下文中。请参阅下面的 Style 以获取预期的选项卡标题放置行为。它只是将LayoutAnchorablePaneControl 的内容包装到DockingPanel 中并旋转标题主机,以便您在Visual Studio 中获得标签标题对齐(按宽度堆叠)。就这样。如果您希望按高度堆叠标题(不旋转),只需将 AnchorablePaneTabPanel 替换为 StackPanel 并移除旋转触发器。
提供的示例基于下面的Style。否则,您将无法将选项卡标题位置传播到视图。
另一个大问题是缺少 DockingManager 类和 AvalonDock 公开的事件。这意味着没有机会观察拖放操作。事实上,DockingManager 只暴露了三个相当无趣的事件。与 LayoutAnchorablePaneControl 等内容主机相同。
由于 AvalonDock 不使用 WPF 框架的拖放 API,因此处理这些事件不是解决方案。
要克服这些缺点,您必须处理少数模型事件之一,在本例中为 LayoutRoot.Updated 事件。
该解决方案仅针对LayoutAnchorablePane 和LayoutAnchorableGroupPane。要解决高级分组或LayoutDocumentPane,您可以按照模式简单地扩展示例。
由于您只需要/请求两列布局,因此该算法将完成这项工作。支持其他更高级的布局安排,但行为并不完美,因为当前并未跟踪所有条件。重点是两列布局。这是一个快速(但不那么脏)且非常简单的解决方案。
您应该考虑明确禁止除两列布局之外的任何布局安排。
此外,AvalonDock 不提供事件来指示视觉布局过程何时完成。当布局模型添加到布局模型树/从布局模型树中删除时,您只会通过LayoutRoot.Updated 事件收到通知。但是您永远不知道可视化树的确切更新时间。我们需要访问可视化容器,以便根据该控件的新位置设置LayoutPanelControl.TabStripPlacement 属性。
为了克服这个问题,我使用Dispatcher 来推迟对然后初始化和渲染的LayoutAnchorablePaneControl 的访问。否则选项卡标题的排列会为时过早,因为控件的布局索引尚未更改。 AvalonDock 只允许跟踪极少的布局模型修改,但根本无法观察实际的对接操作。
所以算法基本上是
- 处理
LayoutRoot.Updated 事件并启动使用 Dispatcher 延迟的实际定位算法
- 遍历所有窗格控件以更新选项卡标题位置。如果允许嵌套,您将拥有一个必须递归遍历的布局树,就像在本示例中对组窗格所做的那样。
- 根据索引确定布局中窗格的位置。
- 根据索引设置
LayoutPanelControl.TabStripPlacement属性:索引为0表示左,等于项数的索引表示右。其他所有索引都介于两者之间。选项卡标题是根据窗格在布局中的位置放置的。
- DockingPanel 将相应地布局选项卡项。如果选项卡标题位于左侧或右侧,则触发器用于旋转它们。
布局中可以有多个LayoutPanelControl 元素(除非您不允许“非法”布局安排来强制执行两列布局)。
MainWindow.xaml.cs
public partial class MainWindow : Window
{
private const Dock DefaultDockPosition = Dock.Bottom;
private void InitializeOnDockingManager_Loaded(object sender, RoutedEventArgs e)
{
var dockingManager = sender as DockingManager;
this.Dispatcher.InvokeAsync(() =>
{
ArrangePanel(dockingManager.LayoutRootPanel);
},
DispatcherPriority.Background);
dockingManager.Layout.Updated += OnLayoutUpdated;
}
private void OnLayoutUpdated(object sender, EventArgs e)
{
var layoutRoot = sender as LayoutRoot;
var dockingManager = layoutRoot.Manager;
this.Dispatcher.InvokeAsync(() =>
{
ArrangePanel(dockingManager.LayoutRootPanel);
},
DispatcherPriority.ContextIdle);
}
private void ArrangePanel(LayoutPanelControl layoutPanelControl)
{
IEnumerable<ILayoutControl> layoutControls = layoutPanelControl.Children
.OfType<ILayoutControl>()
.Where(control =>
control is LayoutAnchorablePaneControl paneControl
&& (paneControl.Model as ILayoutContainer).Children.Any()
|| control is LayoutAnchorablePaneGroupControl or LayoutPanelControl);
int paneControlCount = layoutControls.Count(control => control is not LayoutPanelControl);
int paneControlLayoutPosition = 0;
foreach (ILayoutControl layoutControl in layoutControls)
{
if (layoutControl is LayoutPanelControl layoutPanel)
{
ArrangePanel(layoutPanel);
continue;
}
paneControlLayoutPosition++;
bool isFirst = paneControlLayoutPosition == 1;
bool isLast = paneControlCount == paneControlLayoutPosition;
if (layoutControl is LayoutAnchorablePaneGroupControl paneGroupControl)
{
PositiontabHeadersInPaneGroup((isFirst, isLast), paneGroupControl);
}
else if (layoutControl is LayoutAnchorablePaneControl paneControl)
{
if (paneControlCount == 1)
{
paneControl.TabStripPlacement = DefaultDockPosition;
}
else
{
PositionTabHeadersInPane(paneControl, isFirst, isLast);
}
}
}
}
private static void PositionTabHeadersInPane(LayoutAnchorablePaneControl paneControl, bool isFirst, bool isLast)
=> paneControl.TabStripPlacement =
(isFirst, isLast) switch
{
(true, _) => Dock.Left,
(_, true) => Dock.Right,
_ => DefaultDockPosition
};
private void PositiontabHeadersInPaneGroup((bool IsGroupFirst, bool IsGroupLast) parentPaneGroupPosition, LayoutAnchorablePaneGroupControl paneGroupControl)
{
IEnumerable<ILayoutControl> groupMembers = paneGroupControl.Children
.OfType<ILayoutControl>();
int groupMemberCount = groupMembers.Count();
int layoutPosition = 0;
foreach (ILayoutControl groupMember in groupMembers)
{
layoutPosition++;
bool isFirst = layoutPosition == 1;
bool isLast = layoutPosition == groupMemberCount;
if (groupMember is LayoutAnchorablePaneGroupControl childGroupControl)
{
PositiontabHeadersInPaneGroup((isFirst, isLast), childGroupControl);
}
else if (groupMember is LayoutAnchorablePaneControl paneControl)
{
(bool IsPaneFirstInGroup, bool IsPaneLastInGroup) panePositionInGroup = (isFirst, isLast);
paneControl.TabStripPlacement =
!parentPaneGroupPosition.IsGroupFirst && !parentPaneGroupPosition.IsGroupLast
|| groupMemberCount == 1
? DefaultDockPosition
: (parentPaneGroupPosition, panePositionInGroup, paneGroupControl.Orientation) switch
{
({ IsGroupFirst: true }, { IsPaneFirstInGroup: true }, Orientation.Horizontal) => Dock.Left,
({ IsGroupLast: true }, { IsPaneLastInGroup: true }, Orientation.Horizontal) => Dock.Right,
({ IsGroupFirst: true }, _, Orientation.Vertical) => Dock.Left,
({ IsGroupLast: true }, _, Orientation.Vertical) => Dock.Right,
_ => DefaultDockPosition
};
}
}
}
}
MainWindow.xaml
所需的AnchorablePaneControlStyle 定义如下。
<xcad:DockingManager Loaded="InitializeOnDockingManager_Loaded"
AnchorablePaneControlStyle="{StaticResource AnchorablePaneControlStyle}"
Height="500"
Width="500"
HorizontalAlignment="Left">
<xcad:LayoutRoot>
<xcad:LayoutPanel Orientation="Horizontal">
<xcad:LayoutAnchorablePane>
<xcad:LayoutAnchorable ContentId="properties"
Title="Properties">
<TextBlock Text="123abc" />
</xcad:LayoutAnchorable>
<xcad:LayoutAnchorable Title="AgendaLeft"
ContentId="agendaLeft">
<TextBlock Text="Agenda Content" />
</xcad:LayoutAnchorable>
<xcad:LayoutAnchorable Title="ContactsLeft"
ContentId="contactsLeft">
<TextBlock Text="Contacts Content" />
</xcad:LayoutAnchorable>
</xcad:LayoutAnchorablePane>
</xcad:LayoutPanel>
</xcad:LayoutRoot>
</xcad:DockingManager>
AnchorablePaneControlStyle
<Style x:Key="AnchorablePaneControlStyle"
TargetType="{x:Type xcad:LayoutAnchorablePaneControl}">
<Setter Property="Foreground"
Value="{Binding Model.Root.Manager.Foreground, RelativeSource={RelativeSource Self}}" />
<Setter Property="Background"
Value="{Binding Model.Root.Manager.Background, RelativeSource={RelativeSource Self}}" />
<Setter Property="TabStripPlacement"
Value="Bottom" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type xcad:LayoutAnchorablePaneControl}">
<Grid ClipToBounds="true"
SnapsToDevicePixels="true"
KeyboardNavigation.TabNavigation="Local">
<!--Following border is required to catch mouse events-->
<Border Background="Transparent"
Grid.RowSpan="2" />
<DockPanel>
<xcad:AnchorablePaneTabPanel x:Name="HeaderPanel"
DockPanel.Dock="{TemplateBinding TabStripPlacement}"
Margin="2,0,2,2"
IsItemsHost="true"
KeyboardNavigation.TabIndex="1"
KeyboardNavigation.DirectionalNavigation="Cycle">
<xcad:AnchorablePaneTabPanel.LayoutTransform>
<RotateTransform x:Name="TabPanelRotateTransform" />
</xcad:AnchorablePaneTabPanel.LayoutTransform>
</xcad:AnchorablePaneTabPanel>
<Border x:Name="ContentPanel" DockPanel.Dock="{TemplateBinding TabStripPlacement}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"
KeyboardNavigation.DirectionalNavigation="Contained"
KeyboardNavigation.TabIndex="2"
KeyboardNavigation.TabNavigation="Cycle">
<ContentPresenter x:Name="PART_SelectedContentHost"
ContentSource="SelectedContent"
Margin="{TemplateBinding Padding}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
</DockPanel>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="TabStripPlacement"
Value="Top">
<Trigger.EnterActions>
<StopStoryboard BeginStoryboardName="LeftTabStripPlacementAnimation" />
<StopStoryboard BeginStoryboardName="RightTabStripPlacementAnimation" />
<StopStoryboard BeginStoryboardName="BottomTabStripPlacementAnimation" />
<BeginStoryboard x:Name="TopTabStripPlacementAnimation">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="TabPanelRotateTransform"
Storyboard.TargetProperty="Angle"
To="90"
Duration="0" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
<Trigger Property="TabStripPlacement"
Value="Bottom">
<Trigger.EnterActions>
<StopStoryboard BeginStoryboardName="LeftTabStripPlacementAnimation" />
<StopStoryboard BeginStoryboardName="TopTabStripPlacementAnimation" />
<StopStoryboard BeginStoryboardName="RightTabStripPlacementAnimation" />
<BeginStoryboard x:Name="BottomTabStripPlacementAnimation">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="TabPanelRotateTransform"
Storyboard.TargetProperty="Angle"
To="0"
Duration="0" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
<Trigger Property="TabStripPlacement"
Value="Left">
<Trigger.EnterActions>
<StopStoryboard BeginStoryboardName="TopTabStripPlacementAnimation" />
<StopStoryboard BeginStoryboardName="RightTabStripPlacementAnimation" />
<StopStoryboard BeginStoryboardName="BottomTabStripPlacementAnimation" />
<BeginStoryboard x:Name="LeftTabStripPlacementAnimation">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="TabPanelRotateTransform"
Storyboard.TargetProperty="Angle"
To="90"
Duration="0" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
<Trigger Property="TabStripPlacement"
Value="Right">
<Trigger.EnterActions>
<StopStoryboard BeginStoryboardName="LeftTabStripPlacementAnimation" />
<StopStoryboard BeginStoryboardName="TopTabStripPlacementAnimation" />
<StopStoryboard BeginStoryboardName="BottomTabStripPlacementAnimation" />
<BeginStoryboard x:Name="RightTabStripPlacementAnimation">
<Storyboard>
<DoubleAnimation Storyboard.TargetName="TabPanelRotateTransform"
Storyboard.TargetProperty="Angle"
To="90"
Duration="0" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Setter Property="ItemContainerStyle">
<Setter.Value>
<Style TargetType="{x:Type TabItem}">
<Setter Property="IsSelected"
Value="{Binding IsSelected, Mode=TwoWay}" />
<Setter Property="IsEnabled"
Value="{Binding IsEnabled}" />
<Setter Property="ToolTip"
Value="{Binding ToolTip}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type TabItem}">
<Grid SnapsToDevicePixels="true">
<Border x:Name="Bd"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="1,0,1,1"
Background="{TemplateBinding Background}">
<ContentPresenter x:Name="Content"
ContentSource="Header"
HorizontalAlignment="{Binding HorizontalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"
VerticalAlignment="{Binding VerticalContentAlignment, RelativeSource={RelativeSource AncestorType={x:Type ItemsControl}}}"
RecognizesAccessKey="True"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}" />
</Border>
</Grid>
<ControlTemplate.Triggers>
<Trigger Property="Selector.IsSelected"
Value="true">
<Setter Property="Background"
Value="White" />
<Setter Property="Panel.ZIndex"
Value="1" />
<Setter Property="Margin"
Value="0,-1,-1,-2" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseOver"
Value="true" />
<Condition Property="Selector.IsSelected"
Value="false" />
</MultiTrigger.Conditions>
<Setter Property="Background"
Value="{DynamicResource {x:Static SystemColors.GradientInactiveCaptionBrushKey}}" />
<Setter Property="BorderBrush"
Value="{DynamicResource {x:Static SystemColors.HighlightBrushKey}}" />
<Setter Property="Panel.ZIndex"
Value="0" />
</MultiTrigger>
<Trigger Property="IsEnabled"
Value="false">
<Setter Property="Foreground"
Value="{DynamicResource {x:Static SystemColors.GrayTextBrushKey}}" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<DataTrigger Binding="{Binding RelativeSource={RelativeSource Mode=FindAncestor, AncestorType={x:Type TabControl}}, Path=Items.Count, FallbackValue=1}"
Value="1">
<Setter Property="Visibility"
Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Setter.Value>
</Setter>
<Setter Property="ItemTemplate">
<Setter.Value>
<DataTemplate>
<xcad:LayoutAnchorableTabItem Model="{Binding}" />
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="ContentTemplate"
Value="{StaticResource AnchorablePaneControlContentTemplate}" />
</Style>