Visual Studio 式的简单面板大小调整、停靠和折叠






4.94/5 (15投票s)
学习创建一个简单的自定义控件,允许用户进行停靠。
引言
时不时地,在阅读编程书籍(在本例中是“WPF 4 unleashed”)时,我会看到一个我想扩展的示例。这样做通常能确保我理解正在讨论的概念,并给我一个有趣的练习项目。Adam Nathan 的书第 5 章中 Visual Studio 式的布局示例是我决定扩展的示例。在书中,Adam 提供了一个可折叠、可停靠和可调整大小的 Visual Studio 式面板的示例。这是为了展示 WPF 布局功能如何用于创建复杂的 UI。在他的示例中,Adam 处理了两个硬编码的面板,并提到一个人很可能会将代码抽象成一个自定义控件以供更广泛地使用。这正是我所做的,并且将在本文中尝试描述。
下面讨论的代码将建立在创建两个面板的概念之上,并创建一个自定义控件,该控件允许以通用的方式将面板添加到列(左或右)或行中。这些面板将是可折叠、可停靠和可调整大小的。
上面的图片显示了一个完整的可停靠窗口示例(代码附在本篇文章中)。
背景
虽然我会尽力解释创建此布局自定义控件所使用的基本概念,但我建议您阅读该章节和原始代码示例(我宁愿不抄袭或重复作者已用更好方式解释的信息)。另请注意,尽管我编写的功能代码与本文描述的完全一致,但还有许多改进可以使我的示例成为真正可用/可自定义的自定义控件。在文章结尾,我将提到一些可能需要更改/重构的事项。
为了开始本文,我将描述我试图解决的任务,然后说明我打算如何解决它。
任务
我想要一个 Visual Studio 式的框架,主内容填充整个窗口,我希望能够将可停靠的内容停靠在主内容区的左侧、右侧或底部。我希望能够调整可停靠内容的大小,当固定相对主内容时,以及当未固定时,无论主内容如何。我想选择停靠哪个内容,以及当内容被固定时,如何移除在可停靠内容中激活的按钮。当可停靠内容自由悬停时,我希望能够通过移动到主窗口上来折叠它。最后,我希望能够以声明式的方式在 XAML 中布局我的可停靠内容。
解决方案
首先,选择一个允许用户实时交互式调整大小的容器意味着使用 `Grid`(带 `GridSplitter`)。但是,由于可停靠内容还需要能够独立于主内容进行调整大小(允许重叠),因此我需要多个 `Grid`。
这里使用的解决方案是,对于每个可停靠内容,都将在其构造中使用一个 `Grid`。然后将这些 `Grid` 堆叠在一起,主内容为第 0 层。为了在停靠时同步窗口,我们将使用 `Grid` 的 `SharedSizeGroup` 属性。当一层停靠时,我们将以编程方式向其下方的层添加一个新的共享列或行定义,并向该层本身添加任何已停靠且在其层次结构中处于其上方的层的共享列或行定义。由于我们使用了 `Grid` 的 `SharedSizeGroup` 功能,因此我们能够保持所有层在宽度(列)和高度(行)方面的比例同步。为了模仿可折叠可停靠内容,我们只需将 `Grid` 的 `Visibility` 设置为 `Collapsed`。
我们将使用 `StackPanel` 作为列和行的按钮栏。当所有可停靠内容都停靠时,`StackPanel` 会折叠。
上图显示了一个可停靠内容层覆盖下方主内容的示例。
Using the Code
在详细介绍如何实现上图所示功能之前,我认为现在是时候引导您完成创建自定义控件项目的过程了。
- 创建一个新的解决方案(我将我的解决方案命名为 DockableVSExample)。
- 在解决方案资源管理器中右键单击解决方案,然后添加一个名为 _Common_ 的新文件夹。
- 在 _Common_ 文件夹中添加一个名为 Controls 的新 WPF 自定义控件库。
- 在 _Themes_ 文件夹中添加一个名为 LayeredGrid.xaml 的新资源。
- 将创建 Controls 项目时自动生成的 _controls.cs_ 文件重命名为 _LayeredGrid.cs_。
- 然后,向 Controls 项目添加一个新的 _Layer.cs_ 类文件。
- 在 Generic .xaml 文件(请注意此文件的生成操作应设置为 Page)中,在 `ResourceDictionary.MergedDictionaries` 标签内添加以下条目。
<ResourceDictionary Source="/Controls;component/Themes/LayeredGrid.xaml"/>
完成上述步骤后,我们现在可以处理 _LayeredGrid.xaml_ 文件(因为它需要在处理 `LayeredGrid` 类之前进行设置)。
打开 _LayeredGrid.xaml_ 进行编辑,并在 `ResourceDictionary` 标签中,在 `xmlns:local="clr-namespace:Controls;assembly=Controls"` 标签内添加 `Controls` 命名空间。然后将以下 XAML 添加到 _LayeredGrid.xaml_ 文件中。
<Style TargetType="{x:Type local:LayeredGrid}">
<Setter
Property="Template">
<Setter.Value>
<ControlTemplate>
<DockPanel
VerticalAlignment="Stretch"
HorizontalAlignment="Stretch"
LastChildFill="True"
Name="PART_ParentPanel">
<DockPanel.BitmapEffect>
<BevelBitmapEffect
BevelWidth="15"
EdgeProfile="BulgedUp"/>
</DockPanel.BitmapEffect>
<DockPanel.Resources>
<Color
x:Key="PrimaryColor">
CadetBlue
</Color>
<Color
x:Key="SecondaryColor">
#CC0D1000
</Color>
<SolidColorBrush
x:Key="PrimaryBrush"
Color="{StaticResource PrimaryColor}" />
<SolidColorBrush
x:Key="TextBrush"
Color="Black" />
<SolidColorBrush
x:Key="DisabledColor"
Color="#8CFFFFFF" />
<SolidColorBrush
x:Key="BackgroundBrush"
Color="#FFFFFFFF" />
<Style
x:Key="buttonStyle"
TargetType="{x:Type Button}">
..
..
..
</Style>
<RadialGradientBrush
x:Key="myColorfulLabelBrush"
RadiusX="0.5"
RadiusY="1"
>
<GradientStop
Color="#CC0D1000"
Offset="0.1"/>
<GradientStop
Color="CadetBlue"
Offset="0.9"/>
</RadialGradientBrush>
<RadialGradientBrush
x:Key="myColorfulBorderBrush"
RadiusX="0.4"
RadiusY="0.6"
>
<GradientStop Color="#CC3D2614" Offset="0.3"/>
<GradientStop Color="Gold" Offset="0.8"/>
</RadialGradientBrush>
</DockPanel.Resources>
<StackPanel
Name="PART_BottomCntl"
Background="{StaticResource myColorfulLabelBrush}"
Orientation="Horizontal"
Panel.ZIndex="1"
DockPanel.Dock="Bottom">
</StackPanel>
<StackPanel
Name="PART_LeftCntl"
Background="{StaticResource myColorfulLabelBrush}"
Orientation="Horizontal"
DockPanel.Dock="Left">
<StackPanel.LayoutTransform>
<RotateTransform Angle="90"/>
</StackPanel.LayoutTransform>
</StackPanel>
<StackPanel
Name="PART_RightCntl"
Background="{StaticResource myColorfulLabelBrush}"
Orientation="Horizontal"
DockPanel.Dock="Right">
<StackPanel.LayoutTransform>
<RotateTransform Angle="90"/>
</StackPanel.LayoutTransform>
</StackPanel>
<Grid
Name="PART_MasterGrid"
Grid.IsSharedSizeScope="True">
</Grid>
</DockPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
在 XAML 内容中,有几点值得一提。首先是 `DockPanel`(命名为 `PART_ParentPanel`),它用于布局三个 `StackPanel`(左、右和底部按钮栏)和一个 `Grid`(作为内容占位符)。三个 `StackPanel`(命名为 `PART_LeftCntl`、`PART_RightCntl` 和 `PART_ButtomCntl`)允许放置用于激活可停靠内容层的按钮。`Grid`(命名为 `PART_MasterGrid`)代表所有层将包含的面板容器。`PART_MasterGrid` 设置了附加属性 `IsSharedSizeScope` 为 true,这允许任何子 `Grid` 共享大小信息。
将首先讨论的类是 `Layer` 类。`Layer` 类的目的是定义每个可停靠内容层需要描述其位置(`Left` 或 `Right`)、方向(`Row` 或 `Column`)、级别(一个数字,告诉我放置层的顺序、放在可停靠面板上的名称以及它承载的内容)的属性。这是我将任何我希望 `Layer` 整体继承的属性放在其中的类。类实现如下,易于理解,因此不再进一步解释。
public class Layer : UIElement
{
public enum LayerOrientation
{
Row,
Column
}
public enum LayerColumnLocation
{
Left,
Right
}
public static readonly DependencyProperty LevelProperty;
public static readonly DependencyProperty ContentProperty;
public static readonly DependencyProperty OrientationProperty;
public static readonly DependencyProperty NameProperty;
public static readonly DependencyProperty ColumnLocationProperty;
public int Level
{
get { return (int)GetValue(LevelProperty); }
set { SetValue(LevelProperty, value); }
}
public UIElement Content
{
get { return (UIElement)GetValue(ContentProperty); }
set { SetValue(ContentProperty, value); }
}
public LayerOrientation Orientation
{
get { return (LayerOrientation)GetValue(OrientationProperty); }
set { SetValue(OrientationProperty, value); }
}
public LayerColumnLocation ColumnLocation
{
get { return (LayerColumnLocation)GetValue(ColumnLocationProperty); }
set { SetValue(ColumnLocationProperty, value); }
}
public string Name
{
get { return (string)GetValue(NameProperty); }
set { SetValue(NameProperty, value); }
}
static Layer()
{
LevelProperty = DependencyProperty.Register(
"Level",
typeof(int),
typeof(Layer)
);
ContentProperty = DependencyProperty.Register(
"Content",
typeof(UIElement),
typeof(Layer)
);
OrientationProperty = DependencyProperty.Register(
"Orientation",
typeof(LayerOrientation),
typeof(Layer));
NameProperty = DependencyProperty.Register(
"Name",
typeof(string),
typeof(Layer));
ColumnLocationProperty = DependencyProperty.Register(
"ColumnLocation",
typeof(LayerColumnLocation),
typeof(Layer),
new PropertyMetadata
(
LayerColumnLocation.Left
)
);
}
}
`LayeredGrid` 类(继承自 `ContentControl` 类)充当自定义控件的后端类。`LayeredGrid` 类只有一个名为 `LayersProperty` 的 DependencyProperty。此依赖属性的类型为 `ObservableCollection
下面的代码只是我在接下来要讨论的几组方法中使用的字段。
#region fields
private Grid PART_MasterGrid;
private StackPanel PART_RightCntl;
private StackPanel PART_LeftCntl;
private StackPanel PART_BottomCntl;
private DockPanel PART_ParentPanel;
private readonly ObservableCollection<Layer> _aValues = new ObservableCollection<Layer>();
private readonly List<GridnFloatingBtnCombo> _columnLayers = new List<GridnFloatingBtnCombo>();
private readonly List<GridnFloatingBtnCombo> _rowLayers = new List<GridnFloatingBtnCombo>();
private const string ColumnStr = "column";
private const string RowStr = "row";
private const string LayerStr = "Layer";
private const string PinStr = "btn";
#endregion
上面声明的 `readonly _columnLayers` 和 `_rowLayers` 列表的类型为 `GridnFloatingBtnCombo`。`GridnFloatBtnCombo` 是用于保存每个单独 `Layer` 的 `Grid` 以及与其激活关联的 `Button`(放在按钮栏 `StackPanel` 中的按钮)的类。`GridnFloatingBtnCombo` 定义了一个 `Grid.ColumnDefinition` 列表,其中包含所有面向列的 `Layer` 的 `ColumnDefinition`,这些 `Layer` 的 `Level` 大于与该类关联的 `Layer`。对于类中定义的 `Grid.RowDefinition` 列表,解释相同。类中定义的 `ColumnLocation` 列表与 `ColumnDefintion` 列表同步,并用于将每个 `ColumnDefinition` 与 `Grid` 上的位置关联起来。也就是说,如果 `ColumnLocation` 定义为 `Right`,我们只需将 `ColumnDefintion` 添加到 `Grid.Children` 集合中,否则如果它是 `Left`,我们就将 `ColumnDefintion` 插入到索引 0 的 `Grid` 中。由于可以在索引 0 处插入 `ColumnDefinition`,因此我们需要能够维护主内容的 `Grid` 列索引,因此定义了 `MainContentLocation` 属性。然后 `GridnFloatingBtnCombo` 有两个方法来递增 `MainContentLocation`(`MainContentPositionIncrement()`)和递减 `MainContentLocation`(`MainContentPositionDecrement()`)。`GridnFloatingBtnCombo` 的实现如下所示。
#region layer grid, button btn, columns and rows definition holder class
private class GridnFloatingBtnCombo
{
public readonly Grid Grid;
public readonly Button Btn;
public readonly List<ColumnDefinition> ColumnDefinitions;
public readonly List<Layer.LayerColumnLocation> ColumnLocations;
public readonly List<RowDefinition> RowDefinitions;
public int MainContentLocation { get; private set; }
public GridnFloatingBtnCombo(Grid grid, Button btn)
{
Grid = grid;
Btn = btn;
ColumnDefinitions = new List<ColumnDefinition>();
RowDefinitions = new List<RowDefinition>();
ColumnLocations = new List<Layer.LayerColumnLocation>();
MainContentLocation = 1;
}
public void MainContentPositionIncrement()
{
MainContentLocation++;
}
public void MainContentPositionDecrement()
{
if (MainContentLocation > 1)
MainContentLocation--;
}
}
#endregion
如上所述,`LayeredGrid` 类只有一个名为 `LayersProperty` 的 Dependency Property。由于这是一个需要在代码中进行内部初始化的集合(否则会引发异常),因此我将依赖属性注册为 `readonly` 属性,然后在非静态构造函数中初始化集合。
#region properties and DPs
private static readonly DependencyPropertyKey LayersPropertyKey =
DependencyProperty.RegisterReadOnly("Layers",
typeof (
ObservableCollection
<Layer>),
typeof (LayeredGrid),
new PropertyMetadata(null));
public static readonly DependencyProperty LayersProperty = LayersPropertyKey.DependencyProperty;
public ObservableCollection<Layer> Layers
{
get { return (ObservableCollection<Layer>)GetValue(LayersProperty); }
set { SetValue(LayersProperty, value); }
}
#endregion
#region constructor
static LayeredGrid()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(LayeredGrid),
new FrameworkPropertyMetadata(typeof(LayeredGrid)));
}
/// <summary>
/// Initializes the Layers collection
/// </summary>
public LayeredGrid()
{
SetValue(LayersPropertyKey, _aValues);
}
#endregion
重写的 `OnApplyTemplate` 方法是构建和组合层的过程。此方法获取上面提到的所有 `PART_*` 控件,设置一个父 `Grid`,设置第一个层(第 0 层)并将其添加到父 `Grid`,然后设置所有列层(这些也添加到父 `Grid`),接着设置所有行层,最后将父 `Grid` 添加到 `PART_MasterGrid`。
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
//get the part controls
PART_MasterGrid = GetTemplateChild("PART_MasterGrid") as Grid;
PART_RightCntl = GetTemplateChild("PART_RightCntl") as StackPanel;
PART_LeftCntl = GetTemplateChild("PART_LeftCntl") as StackPanel;
PART_BottomCntl = GetTemplateChild("PART_BottomCntl") as StackPanel;
PART_ParentPanel = GetTemplateChild("PART_ParentPanel") as DockPanel;
//verify master grid exist
if (PART_MasterGrid == null)
return;
//setup parent grid
var parentGrid = new Grid();
SetUpParentGrid(parentGrid);
//set up layers
var layer0 = Layers.FirstOrDefault(x => x.Level == 0);
if (layer0 == null)
return;
var columnLayers =
Layers.Select(x => x)
.Where(x =>
x.Level > 0 &&
x.Orientation == Layer.LayerOrientation.Column)
.OrderBy(x => x.Level);
var rowLayers =
Layers.Select(x => x)
.Where(x=>
x.Level>0 &&
x.Orientation==Layer.LayerOrientation.Row)
.OrderBy(x=> x.Level);
var item = SetupLayer0(layer0,
columnLayers,
rowLayers.Count());
parentGrid.Children.Add(item);
Grid.SetRow(item, 0);
//setup the column grid layers
if (columnLayers.Any())
{
foreach (var layer in columnLayers)
{
SetupColumnLayers(parentGrid, layer, columnLayers.Count());
}
}
//setup the row grid layers
if(rowLayers.Any())
{
foreach (var layer in rowLayers)
{
SetupRowLayers(parentGrid, layer, rowLayers.Count());
}
}
//add parent grid to master grid
PART_MasterGrid.Children.Add(parentGrid);
Grid.SetRow(parentGrid,0);
}
`SetupParentGrid` 方法向父 `Grid` 添加两个 `RowDefinition`。第一个 `RowDefinition` 的 `Height` 为 `star`,用于容纳主内容。
private static void SetUpParentGrid(Grid parent)
{
var row1 = new RowDefinition
{
Height = new GridLength(1, GridUnitType.Star)
};
var row2 = new RowDefinition { Height = GridLength.Auto };
parent.RowDefinitions.Add(row1);
parent.RowDefinitions.Add(row2);
}
`SetupLayer0` 方法添加了三个 `Grid` `ColumnsDefinition`。第一列代表所有左侧可停靠内容的位置,第二列代表主内容的位置,最后第三列代表所有右侧可停靠内容的位置。将一个事件处理程序附加到 `Grid.MouseEnter` 事件,用于折叠任何 `Layer` 的 `Grid`,如果其激活 `Layer` 按钮可见(表示 `Layer` 未停靠)。最后,对于每个行和列 `Layer`,都会将 `RowDefintion` 和 `ColumnDefinition` 添加到 `GridnFloatingBtnCombo` 的实例中,用于 `Layer` 0。需要注意的是,我如何设置 Column/Row 定义的 `SharedSizeGroup` 属性。
private Grid SetupLayer0(Layer layer0, IEnumerable<Layer> columnLayers, int numberofRows)
{
var grid = new Grid { Name = ColumnStr + LayerStr + layer0.Level };
grid.ColumnDefinitions.Add(new ColumnDefinition
{
Width = GridLength.Auto
});
grid.ColumnDefinitions.Add(new ColumnDefinition
{
Width = new GridLength(1, GridUnitType.Star)
});
grid.ColumnDefinitions.Add(new ColumnDefinition
{
Width = GridLength.Auto
});
grid.RowDefinitions.Add(new RowDefinition());
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
if (layer0.Content != null)
grid.Children.Add(layer0.Content);
if (layer0.Content != null)
Grid.SetColumn(layer0.Content,1);
grid.MouseEnter += (o, e) =>
{
for (var i = 1; i < _columnLayers.Count; i++)
{
if (_columnLayers[i].Btn.Visibility == Visibility.Visible)
{
_columnLayers[i].Grid.Visibility = Visibility.Collapsed;
}
}
for (var i = 1; i < _rowLayers.Count; i++)
{
if (_rowLayers[i].Btn.Visibility == Visibility.Visible)
{
_rowLayers[i].Grid.Visibility = Visibility.Collapsed;
}
}
};
var gnb = new GridnFloatingBtnCombo(grid, null);
if (columnLayers.Any())
{
var list = columnLayers.ToList();
for (int i = 0; i < columnLayers.Count(); i++)
{
gnb.ColumnDefinitions.Add(new ColumnDefinition
{
SharedSizeGroup = ColumnStr +
(i + 1) +
list[i].ColumnLocation,
Width = GridLength.Auto
});
gnb.ColumnLocations.Add(list[i].ColumnLocation);
}
}
if (numberofRows > 0)
{
for (int i = 0; i < numberofRows; i++)
{
gnb.RowDefinitions.Add(new RowDefinition
{
SharedSizeGroup = RowStr + (i + 1),
Height = GridLength.Auto
});
}
}
_columnLayers.Add(gnb);
_rowLayers.Add(gnb);
return grid;
}
`SetupColumnLayers` 方法负责创建所有列层,而不考虑 `ColumnLocation`。为了构建一个列 `Layer`,我们创建一个带有三个 `ColumnDefinition` 的 `Grid`,正如 `Layer` 0 一样,但在这种情况下,可停靠内容将放置在 `Left` 列位置的 `Layer` 的第 0 列,而 `Right` 列位置的 `Layer` 将放在第 2 列。这从 `ColumnDefinition` 的 `SharedSizedGroup` 属性的设置方式可以看出。创建了一个带有两个 `RowDefinition` 的内部 `Grid`,第一行包含一个 `DockPanel`,该 `DockPanel` 又包含一个停靠在右侧的停靠固定按钮和一个包含 `Layer` 名称(停靠在左侧)的 `TextBlock`。`GridSplitter` 与内部 `Grid` 一起被添加到 `Layer` 的 `Grid` 中,以允许调整内容大小。停靠固定按钮有一个 Click 事件处理程序,用于处理 `Layer` 的停靠和取消停靠。最后,对于任何 `Level` 大于当前 `Layer` 的 `Layer`,我们创建一个 `ColumnDefinition`(并相应地设置其 `SharedGroupSize` 属性)。
private void SetupColumnLayers(Grid parentGrid, Layer layer, int columnLayerCnt)
{
var grid = new Grid
{
Name = ColumnStr + LayerStr + layer.Level,
Visibility = Visibility.Collapsed
};
grid.ColumnDefinitions.Add(new ColumnDefinition
{
SharedSizeGroup =
layer.ColumnLocation == Layer.LayerColumnLocation.Left
? ColumnStr + layer.Level+layer.ColumnLocation
: null,
Width = GridLength.Auto
});
grid.ColumnDefinitions.Add(new ColumnDefinition
{
Width = new GridLength(1, GridUnitType.Star)
});
grid.ColumnDefinitions.Add(new ColumnDefinition
{
SharedSizeGroup =
layer.ColumnLocation == Layer.LayerColumnLocation.Right
? ColumnStr + layer.Level + layer.ColumnLocation
: null,
Width = GridLength.Auto
});
var internalGrid = new Grid();
internalGrid.RowDefinitions.Add(new RowDefinition
{
Height = GridLength.Auto
});
internalGrid.RowDefinitions.Add(new RowDefinition ());
internalGrid.Background = (RadialGradientBrush) PART_MasterGrid.FindResource( "myColorfulLabelBrush" );
grid.Children.Add(internalGrid);
Grid.SetColumn(internalGrid,
layer.ColumnLocation == Layer.LayerColumnLocation.Left ? 0 : 2
);
var dockpanel = new DockPanel();
internalGrid.Children.Add(dockpanel);
Grid.SetRow(dockpanel, 0);
var btn = new Button
{
Name = ColumnStr + PinStr + layer.Level,
Width = 28.0,
Background = Brushes.Transparent,
BorderBrush = Brushes.Transparent,
Style = (Style) PART_MasterGrid.FindResource( "buttonStyle" ),
Content = new Path
{
Stroke = Brushes.Black,
Fill=Brushes.Gold,
StrokeThickness = 1,
Stretch = Stretch.Fill,
Width = 9.0,
Height = 15,
Data = PinPathgeometry()
}
};
dockpanel.Children.Add(btn);
DockPanel.SetDock(btn, Dock.Right);
btn.Click += (o, e) =>
{
int level = layer.Level;
var item = _columnLayers[level].Btn;
if (item.Visibility == Visibility.Collapsed)
ColumnUndockPane(level, o as Button);
else
ColumnDockPane(level, o as Button);
};
var textblock = new TextBlock
{
Padding = new Thickness(8),
TextTrimming = TextTrimming.CharacterEllipsis,
Foreground = Brushes.Gold,
Text = layer.Name
};
dockpanel.Children.Add(textblock);
DockPanel.SetDock(textblock,Dock.Left);
if (layer.Content != null)
{
internalGrid.Children.Add(layer.Content);
Grid.SetRow(layer.Content, 1);
}
var gridSplitter = new GridSplitter
{
Width = 2,
Background = Brushes.CadetBlue,
HorizontalAlignment =
layer.ColumnLocation == Layer.LayerColumnLocation.Right
? HorizontalAlignment.Left
: HorizontalAlignment.Right
};
grid.Children.Add(gridSplitter);
Grid.SetColumn(gridSplitter,
layer.ColumnLocation == Layer.LayerColumnLocation.Right ? 2 : 0
);
grid.MouseEnter += (o, e) =>
{
var level = layer.Level;
for (var i = (level + 1); i < _columnLayers.Count; i++)
{
if (_columnLayers[i].Btn.Visibility == Visibility.Visible)
_columnLayers[i].Grid.Visibility = Visibility.Collapsed;
}
};
parentGrid.Children.Add(grid);
Grid.SetRow(grid, 0);
var gnb = new GridnFloatingBtnCombo( grid,
AddToColumnStackPanel(layer)
);
if (columnLayerCnt > 0)
{
for (int i = layer.Level; i < columnLayerCnt; i++)
{
gnb.ColumnDefinitions
.Add(new ColumnDefinition
{
SharedSizeGroup = ColumnStr + (i + 1) + layer.ColumnLocation,
Width = GridLength.Auto
});
gnb.ColumnLocations.Add(layer.ColumnLocation);
}
}
_columnLayers.Add(gnb);
}
`SetupRowLayers` 方法,与其列 `Layer` 设置的对应方法一样,创建了一个 `Layer Grid`。然而,在这种情况下,创建了两个 `RowDefinition`,第二个 `RowDefinition` 用于容纳可停靠的内容。与列 `Layer` 设置不同的是,行 `Layer` 设置使用 `DockPanel` 来容纳停靠固定按钮和 `Layer` 的 `Content`。最后,不是将 `Layer` 的 `Grid` 添加到父 `Grid`,而是直接将其添加到 `PART_MasterGrid`(这允许可停靠的行在停靠时跨越可停靠的列)。
private void SetupRowLayers(Grid parentGrid, Layer layer, int numberofRows)
{
var grid = new Grid
{
Name = RowStr + LayerStr + layer.Level,
Visibility = Visibility.Collapsed
};
grid.RowDefinitions.Add(new RowDefinition
{
Height = new GridLength(1,GridUnitType.Star)
});
grid.RowDefinitions.Add(new RowDefinition
{
SharedSizeGroup = RowStr+layer.Level,Height = GridLength.Auto
});
//set up dock panel
var dockpanel = new DockPanel
{
Margin = new Thickness(0, 4, 0, 0),
Background =
(RadialGradientBrush) PART_MasterGrid.FindResource("myColorfulLabelBrush"),
LastChildFill = true
};
grid.Children.Add(dockpanel);
Grid.SetRow(dockpanel, 1);
var gridsplitter = new GridSplitter
{
Height = 4,
Background = Brushes.CadetBlue,
ResizeDirection = GridResizeDirection.Rows,
HorizontalAlignment = HorizontalAlignment.Stretch,
VerticalAlignment = VerticalAlignment.Top
};
grid.Children.Add(gridsplitter);
Grid.SetRow(gridsplitter, 1);
//set up stackpanel
var stackpanel = new StackPanel
{
Height = 25.0,
HorizontalAlignment = HorizontalAlignment.Stretch
};
dockpanel.Children.Add(stackpanel);
DockPanel.SetDock(stackpanel,Dock.Top);
//set up btn
var btn = new Button
{
Name = RowStr + PinStr + layer.Level,
Width = 26.0,
HorizontalAlignment = HorizontalAlignment.Right,
Background = Brushes.Transparent,
BorderBrush = Brushes.Transparent,
Style = (Style) PART_MasterGrid.FindResource("buttonStyle"),
BorderThickness = new Thickness(0)
};
stackpanel.Children.Add(btn);
var path = new Path
{
Stroke = Brushes.Black,
Fill = Brushes.Gold,
StrokeThickness = 1,
Stretch = Stretch.Fill,
Width = 9.0,
Height = 15
};
var pathgeometry = PinPathgeometry();
path.Data = pathgeometry;
btn.Content = path;
btn.Click += (o, e) =>
{
int level = layer.Level;
var pgrid = parentGrid;
var item = _rowLayers[level].Btn;
if (item.Visibility == Visibility.Collapsed)
RowUndockPane(level, o as Button, pgrid);
else
RowDockPane(level, o as Button, pgrid);
};
if (layer.Content != null)
{
dockpanel.Children.Add(layer.Content);
DockPanel.SetDock(layer.Content,Dock.Top);
}
grid.MouseEnter += (o, e) =>
{
var level = layer.Level;
for (var i = 1; i < _rowLayers.Count; i++)
{
if (i == level)
continue;
if (_rowLayers[i].Btn.Visibility == Visibility.Visible)
{
_rowLayers[i].Grid.Visibility = Visibility.Collapsed;
}
}
};
PART_MasterGrid.Children.Add(grid);
Grid.SetRow(grid,0);
var gnb= new GridnFloatingBtnCombo(grid, AddToRowStackPanel(layer));
if (numberofRows > 0)
{
for (int i = layer.Level; i < numberofRows; i++)
{
gnb.RowDefinitions.Add(new RowDefinition
{
SharedSizeGroup = RowStr + (i + 1),
Height = GridLength.Auto
});
}
}
_rowLayers.Add(gnb);
}
`PinPathGeometry` 方法仅负责绘制停靠固定按钮内所有可停靠内容中的固定图标。
private static PathGeometry PinPathgeometry()
{
return new PathGeometry
{
Figures = new PathFigureCollection
{
new PathFigure
{
StartPoint = new Point(10,0),
IsFilled = true,
Segments = new PathSegmentCollection
{
new LineSegment{Point = new Point(10,0)},
new LineSegment{Point = new Point(30,0)},
new LineSegment{Point = new Point(30,5)},
new LineSegment{Point = new Point(10,5)},
new LineSegment{Point = new Point(10,0)}
}
},
new PathFigure
{
StartPoint = new Point(4.5,5),
Segments = new PathSegmentCollection
{
new LineSegment{Point = new Point(40.5,5)}
}
},
new PathFigure
{
StartPoint = new Point(22,5),
Segments = new PathSegmentCollection
{
new LineSegment{Point = new Point(22,10)}
}
}
}
};
}
`AddToColumnStackPanel` 的唯一目的是创建一个按钮,然后该按钮将根据 `Layer` 的 `ColumnLocation` 添加到 `PART_RightCntl` 或 `PART_LeftCntl`。该按钮附加了一个 Click 事件处理程序,该处理程序设置当前 `Layer` 的 `Grid` 的可见性,并折叠任何未停靠的其他列 `Layer`。
private Button AddToColumnStackPanel(Layer layer)
{
var btn = new Button
{
Background = Brushes.Transparent,
BorderBrush = Brushes.Transparent,
BorderThickness = new Thickness(0),
Height = 22,
MinWidth = 65.0,
Padding = new Thickness(10,0,15,0),
FontWeight = FontWeights.Bold,
Style = (Style)PART_MasterGrid.FindResource("buttonStyle"),
Content = layer.Name
};
btn.Click += (o, e) =>
{
var level = layer.Level;
var item = _columnLayers[level];
item.Grid.Visibility = Visibility.Visible;
Grid.SetZIndex(item.Grid, 1);
for (int i = 1; i < _columnLayers.Count; i++)
{
if (i == level)
continue;
var loc = _columnLayers[i];
Grid.SetZIndex(loc.Grid, 0);
if (loc.Btn.Visibility == Visibility.Visible)
loc.Grid.Visibility = Visibility.Collapsed;
}
};
if (layer.ColumnLocation==Layer.LayerColumnLocation.Right)
PART_RightCntl.Children.Add(btn);
else
PART_LeftCntl.Children.Add(btn);
return btn;
}
`AddToRowStackPanel` 的唯一目的是创建一个按钮,然后将其添加到 `PART_BottomCntl` `StackPanel`。该按钮附加了一个 Click 事件处理程序,该处理程序设置当前 `Layer` 的 `Grid` 的可见性,并折叠任何未停靠的其他行 `Layer`。
private Button AddToRowStackPanel(Layer layer)
{
var btn = new Button
{
Background = Brushes.Transparent,
BorderBrush = Brushes.Transparent,
BorderThickness = new Thickness(0),
Height = 24,
Padding = new Thickness(10, 0, 15, 0),
FontWeight = FontWeights.Bold,
Style = (Style)PART_MasterGrid.FindResource("buttonStyle"),
Content = layer.Name
};
btn.Click += (o, e) =>
{
var level = layer.Level;
var item = _rowLayers[level];
item.Grid.Visibility = Visibility.Visible;
Grid.SetZIndex(item.Grid,1);
for(int i=1; i<_rowLayers.Count; i++)
{
if (i==level)
continue;
var loc = _rowLayers[i];
Grid.SetZIndex(loc.Grid,0);
if (loc.Btn.Visibility == Visibility.Visible)
loc.Grid.Visibility = Visibility.Collapsed;
}
};
PART_BottomCntl.Children.Add(btn);
return btn;
}
`ColumnDockPane` 方法负责将分层内容停靠在主内容的左侧或右侧。它通过检查 `Layer` 应该位于哪个列位置来启动该过程。如果它在右侧,则 `Layer` 0 `Grid` 添加一个 `ColumnDefinition`,其 `SharedGroupSize` 等同于正在停靠的当前 `Layer` 的大小,否则如果它在左侧,则 `ColumnDefinition` 插入到索引 0 处,`MainContentLocation` 增加 1。然后,我们调用 `Grid.SetColumn` 将 `Layer` 0 的主内容设置到其新的列位置。下一步是对当前正在停靠的 `Layer` 执行与 `Layer` 0 相同的逻辑(即,对于任何已停靠且其级别大于正在停靠的当前 `Layer` 的 `Layer`,我们都会添加一个 `ColumnDefinition`)。最后,我们对任何已停靠且其级别小于正在停靠的当前 `Layer` 的 `Layer` 执行与添加 `ColumnDefinition`(`SharedGroupSize` 等同于当前 `Layer` 的大小)完全相同的操作。
private void ColumnDockPane(int level, Button btn)
{
var item = _columnLayers[level];
item.Btn.Visibility = Visibility.Collapsed;
var rtTrans = new RotateTransform(90);
btn.LayoutTransform = rtTrans;
if (_columnLayers[0].ColumnLocations[level - 1] == Layer.LayerColumnLocation.Right)
_columnLayers[0].
Grid.
ColumnDefinitions.
Add(_columnLayers[0].ColumnDefinitions[level - 1]);
else
{
_columnLayers[0].MainContentPositionIncrement();
_columnLayers[0].
Grid.
ColumnDefinitions.
Insert(0, _columnLayers[0].ColumnDefinitions[level - 1]);
Grid.SetColumn(_columnLayers[0]. Grid.Children[0],
_columnLayers[0].MainContentLocation);
}
for (var i = level + 1; i < _columnLayers.Count; i++)
{
if (_columnLayers[i].Btn.Visibility != Visibility.Collapsed)
continue;
if (item.ColumnLocations[i-level - 1] == Layer.LayerColumnLocation.Right)
item.Grid.ColumnDefinitions.Add(item.ColumnDefinitions[i - level - 1]);
else
{
item.MainContentPositionIncrement();
item.Grid.ColumnDefinitions.Insert(0, item.ColumnDefinitions[i-level - 1]);
foreach (UIElement child in item.Grid.Children)
{
Grid.SetColumn(child, item.MainContentLocation - 1);
}
}
}
for (var i = 1; i < level; i++)
{
var loc = _columnLayers[i];
if (loc.Btn.Visibility != Visibility.Collapsed)
continue;
if (loc.ColumnLocations[level - 1 - i] == Layer.LayerColumnLocation.Right)
loc.Grid.ColumnDefinitions.Add(loc.ColumnDefinitions[level - 1 - i]);
else
{
loc.MainContentPositionIncrement();
loc.Grid.ColumnDefinitions.Insert(0, loc.ColumnDefinitions[level - 1 - i]);
foreach (UIElement child in loc.Grid.Children)
{
Grid.SetColumn(child, loc.MainContentLocation - 1);
}
}
}
}
`ColumnUndockPane` 方法与 `ColumnDockPane` 方法完全相反,一旦读者理解了后者,它就是不言自明的。
private void ColumnUndockPane(int level, Button btn)
{
var item = _columnLayers[level];
item.Btn.Visibility = Visibility.Visible;
btn.LayoutTransform = null;
item.Grid.Visibility = Visibility.Visible;
for (var i = 0; i < level; i++)
{
if (_columnLayers[i].ColumnLocations[level - 1-i] == Layer.LayerColumnLocation.Left)
{
_columnLayers[i].MainContentPositionDecrement();
if(i==0)
Grid.SetColumn(_columnLayers[i].Grid.Children[0],
_columnLayers[i].MainContentLocation);
else
{
foreach (UIElement child in _columnLayers[i].Grid.Children)
{
Grid.SetColumn(child, _columnLayers[i].MainContentLocation - 1);
}
}
}
_columnLayers[i].
Grid.
ColumnDefinitions.
Remove(_columnLayers[i].ColumnDefinitions[level - 1 - i]);
}
int v = 0;
foreach (var t in item.ColumnDefinitions)
{
if (item.ColumnLocations[v++] == Layer.LayerColumnLocation.Left)
{
item.MainContentPositionDecrement();
foreach (UIElement child in item.Grid.Children)
{
Grid.SetColumn(child, item.MainContentLocation - 1);
}
}
item.Grid.ColumnDefinitions.Remove(t);
}
}
`RowDockPane` 方法比其列等效方法更简单,因为我们不必担心向上或向下的位置。我们只需将与我们正在尝试停靠的当前行 `Layer` 关联的 `RowDefinition` 添加到父 `Grid`。下一步是对当前正在停靠的 `Layer` 执行与 `Layer` 0 相同的逻辑(即,对于任何已停靠且其级别大于正在停靠的当前 `Layer` 的 `Layer`,我们都会添加一个 `RowDefinition`)。最后,我们对任何已停靠且其级别小于正在停靠的当前 `Layer` 的 `Layer` 执行与添加 `RowDefinition`(`SharedGroupSize` 等同于当前 `Layer` 的大小)完全相同的操作。
private void RowDockPane(int level, Button btn, Grid parentGrid)
{
var item = _rowLayers[level];
item.Btn.Visibility = Visibility.Collapsed;
var rtTrans = new RotateTransform(90);
btn.LayoutTransform = rtTrans;
parentGrid.RowDefinitions.Add(_rowLayers[0].RowDefinitions[level - 1]);
for(var i=level+1; i<_rowLayers.Count; i++)
{
if (_rowLayers[i].Btn.Visibility == Visibility.Collapsed)
item.Grid.RowDefinitions.Add(item.RowDefinitions[i-level-1]);
}
for(var i =1; i<level;i++)
{
var loc = _rowLayers[i];
if(loc.Btn.Visibility==Visibility.Collapsed)
loc.Grid.RowDefinitions.Add(loc.RowDefinitions[level-1-i]);
}
}
`RowUndockPane` 与 `RowDockPane` 完全相反,因此无需在此进行解释。
private void RowUndockPane(int level, Button btn, Grid parentGrid)
{
var item = _rowLayers[level];
item.Btn.Visibility = Visibility.Visible;
btn.LayoutTransform = null;
item.Grid.Visibility = Visibility.Visible;
parentGrid.RowDefinitions.Remove(_rowLayers[0].RowDefinitions[level - 1]);
for(int i=1; i<level; i++)
{
_rowLayers[i].Grid.RowDefinitions.Remove(_rowLayers[i].RowDefinitions[level - 1-i]);
}
foreach (RowDefinition t in item.RowDefinitions)
{
item.Grid.RowDefinitions.Remove(t);
}
}
最后,为了展示像上面图片一样的可运行示例,在您创建的解决方案的 _MainWindow.xaml_ 文件中,添加以下 XAML,瞧!您就拥有了一个可运行的停靠应用程序(注释掉工具栏,因为它们包含包含在项目中的图像)。尽管 XAML 在这里放置,但我建议在阅读时将其折叠并下载项目本身。
<Window x:Class="DockableVsExample.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:Controls;assembly=Controls"
Title="MainWindow" Height="800" Width="1024">
<Window.Resources>
<RadialGradientBrush
x:Key="myColorfulLabelBrush"
RadiusX="0.5"
RadiusY="1"
>
<GradientStop Color="#CC0D1000" Offset="0.1"/>
<GradientStop Color="CadetBlue" Offset="0.9"/>
</RadialGradientBrush>
<RadialGradientBrush
x:Key="myColorfulBorderBrush"
RadiusX="0.4"
RadiusY="0.6"
>
<GradientStop Color="#CC3D2614" Offset="0.3"/>
<GradientStop Color="Gold" Offset="0.8"/>
</RadialGradientBrush>
</Window.Resources>
<DockPanel >
<DockPanel.BitmapEffect>
<BevelBitmapEffect BevelWidth="15" EdgeProfile="BulgedUp"/>
</DockPanel.BitmapEffect>
<Menu DockPanel.Dock="Top">
<MenuItem Header="File">
File
</MenuItem>
<MenuItem Header="Edit">
Edit
</MenuItem>
<MenuItem Header="View">
View
</MenuItem>
<MenuItem Header="Project">
Project
</MenuItem>
<MenuItem Header="Build">
Build
</MenuItem>
<MenuItem Header="Data">
Data
</MenuItem>
<MenuItem Header="Tools">
Tools
</MenuItem>
<MenuItem Header="Window">
Window
</MenuItem>
<MenuItem Header="Community">
Community
</MenuItem>
<MenuItem Header="Help">
Help
</MenuItem>
</Menu>
<Border
DockPanel.Dock="Top"
BorderBrush="{StaticResource myColorfulBorderBrush}"
BorderThickness="0">
<Label
Background="{StaticResource myColorfulLabelBrush}"
Foreground="Wheat"
FontWeight="ExtraBlack"
FontSize="16"
HorizontalContentAlignment="Center">
Docking Yeahhh!!!!
</Label>
<Border.BitmapEffect>
<EmbossBitmapEffect />
</Border.BitmapEffect>
</Border>
<StatusBar
DockPanel.Dock="Bottom"
Background="{StaticResource myColorfulLabelBrush}"
Height="15"/>
<controls:LayeredGrid
Grid.Row="1"
Grid.Column="2"
Grid.RowSpan="3">
<controls:LayeredGrid.Layers>
<controls:Layer Level="2" Orientation="Row" Name="Text Manager 2">
<controls:Layer.Content>
<controls:LayeredGrid>
<controls:LayeredGrid.Layers>
<controls:Layer Level="1" Orientation="Row" Name="Logger">
<controls:Layer.Content>
<ListBox
MinHeight="60"
>
<ListBoxItem Content="{Binding Title}"></ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="1" Orientation="Column"
Name="Solution Explorer" ColumnLocation="Right">
<controls:Layer.Content>
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<ToolBar Grid.Row="0">
<ToggleButton >
<Image Source="Images\Home-icon.png"/>
</ToggleButton>
<ToggleButton>
<Image Source="Images\Next-icon.png"/>
</ToggleButton>
<ToggleButton>
<Image Source="Images\Next-icon.png">
<Image.LayoutTransform>
<RotateTransform Angle="180"/>
</Image.LayoutTransform>
</Image>
</ToggleButton>
</ToolBar>
<TreeView
Grid.Row="1"
>
<TreeViewItem Header="Solution Explorer">
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
</TreeViewItem>
</TreeView>
</Grid>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="2" Orientation="Column" Name="Toolbox">
<controls:Layer.Content>
<ListBox >
<ListBoxItem>Button</ListBoxItem>
<ListBoxItem>Label</ListBoxItem>
<ListBoxItem>CheckBox</ListBoxItem>
<ListBoxItem>ListBox</ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="3" Orientation="Column" Name="Toolbox Manager">
<controls:Layer.Content>
<ListBox >
<ListBoxItem>Button</ListBoxItem>
<ListBoxItem>Label</ListBoxItem>
<ListBoxItem>CheckBox</ListBoxItem>
<ListBoxItem>ListBox</ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="4" Orientation="Column" Name="Numbers">
<controls:Layer.Content>
<ListBox >
<ListBoxItem>1</ListBoxItem>
<ListBoxItem>2</ListBoxItem>
<ListBoxItem>3</ListBoxItem>
<ListBoxItem>4</ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="5" Orientation="Column" Name="Names">
<controls:Layer.Content>
<ListBox >
<ListBoxItem>Ty</ListBoxItem>
<ListBoxItem>Tayo</ListBoxItem>
<ListBoxItem>Temitayo</ListBoxItem>
<ListBoxItem>Lauren</ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="0" >
<controls:Layer.Content>
<ListBox
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderBrush="Wheat">
<ListBoxItem>Article #1</ListBoxItem>
<ListBoxItem>Article #2</ListBoxItem>
<ListBoxItem>Article #3</ListBoxItem>
<ListBoxItem>Article #4</ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
</controls:LayeredGrid.Layers>
</controls:LayeredGrid>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="1" Orientation="Column"
Name="Solution Explorer" ColumnLocation="Right">
<controls:Layer.Content>
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<ToolBar Grid.Row="0">
<ToggleButton >
<Image Source="Images\Home-icon.png"/>
</ToggleButton>
<ToggleButton>
<Image Source="Images\Next-icon.png"/>
</ToggleButton>
<ToggleButton>
<Image Source="Images\Next-icon.png">
<Image.LayoutTransform>
<RotateTransform Angle="180"/>
</Image.LayoutTransform>
</Image>
</ToggleButton>
</ToolBar>
<TreeView
Grid.Row="1"
>
<TreeViewItem Header="Solution Explorer">
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
</TreeViewItem>
</TreeView>
</Grid>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="2" Orientation="Column"
Name="Explorer" ColumnLocation="Right">
<controls:Layer.Content>
<Grid >
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition />
</Grid.RowDefinitions>
<TreeView
Grid.Row="1"
>
<TreeViewItem Header="Explorer">
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #4"></TreeViewItem>
<TreeViewItem Header="Project #5"></TreeViewItem>
<TreeViewItem Header="Project #6"></TreeViewItem>
<TreeViewItem Header="Project #7"></TreeViewItem>
<TreeViewItem Header="Project #8"></TreeViewItem>
<TreeViewItem Header="Project #9"></TreeViewItem>
<TreeViewItem Header="Project #10"></TreeViewItem>
<TreeViewItem Header="Project #11"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
<TreeViewItem Header="Project #1"></TreeViewItem>
<TreeViewItem Header="Project #2"></TreeViewItem>
<TreeViewItem Header="Project #3"></TreeViewItem>
</TreeViewItem>
</TreeView>
</Grid>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="3" Orientation="Column" Name="Toolbox">
<controls:Layer.Content>
<ListBox >
<ListBoxItem>Button</ListBoxItem>
<ListBoxItem>Label</ListBoxItem>
<ListBoxItem>CheckBox</ListBoxItem>
<ListBoxItem>ListBox</ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="4" Orientation="Column" Name="Toolbox Manager">
<controls:Layer.Content>
<ListBox >
<ListBoxItem>Button</ListBoxItem>
<ListBoxItem>Label</ListBoxItem>
<ListBoxItem>CheckBox</ListBoxItem>
<ListBoxItem>ListBox</ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="5" Orientation="Column" Name="Numbers">
<controls:Layer.Content>
<ListBox >
<ListBoxItem>1</ListBoxItem>
<ListBoxItem>2</ListBoxItem>
<ListBoxItem>3</ListBoxItem>
<ListBoxItem>4</ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="6" Orientation="Column" Name="Names">
<controls:Layer.Content>
<ListBox >
<ListBoxItem>Ty</ListBoxItem>
<ListBoxItem>Tayo</ListBoxItem>
<ListBoxItem>Temitayo</ListBoxItem>
<ListBoxItem>Lauren</ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="1" Orientation="Row" Name="Text Manager">
<controls:Layer.Content>
<ListBox
MinHeight="60"
>
<ListBoxItem Content="{Binding Title}"></ListBoxItem>
</ListBox>
</controls:Layer.Content>
</controls:Layer>
<controls:Layer Level="0" >
<controls:Layer.Content>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition/>
<RowDefinition/>
<RowDefinition/>
</Grid.RowDefinitions>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition/>
</Grid.ColumnDefinitions>
<Grid
Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="4"
Background="White"></Grid>
<GroupBox
Grid.Row="1"
Grid.Column="0"
Background="White"
VerticalAlignment="Stretch"
VerticalContentAlignment="Stretch"
Header="Recent Projects">…
</GroupBox>
<GroupBox
Grid.Row="2"
Grid.Column="0"
Background="White"
Header ="Getting Started"
>
…
</GroupBox>
<GroupBox
Grid.Row="3"
Grid.Column="0"
Background="White"
Header="Headlines">…
</GroupBox>
<GridSplitter
Width="2"
Grid.Row="0"
Grid.Column="1"
Grid.RowSpan="4"
Background="Transparent"
HorizontalAlignment="Left" />
<ListBox
Grid.Column="2"
Grid.Row="0"
Grid.RowSpan="4"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
BorderBrush="CadetBlue">
<ListBoxItem>Article #1</ListBoxItem>
<ListBoxItem>Article #2</ListBoxItem>
<ListBoxItem>Article #3</ListBoxItem>
<ListBoxItem>Article #4</ListBoxItem>
</ListBox>
</Grid>
</controls:Layer.Content>
</controls:Layer>
</controls:LayeredGrid.Layers>
</controls:LayeredGrid>
</DockPanel>
</Window>
关注点
我在自定义控件中没有实现一些东西,但是如果将此代码用于示例之外的用途,实现它们会很有意义。我将列出一些应该进行的更改,以防有人发现此代码有用。
- 实现的 `Level` 期望用户负责为 `Level` 设置正确的数字(即,不能有空隙,期望编号是连续的)。这一点需要改变。
- 我没有测试过当通过代码动态添加和删除 `Layer` 时自定义控件的行为,但很容易看出自定义控件中的集合更改事件处理程序将需要处理这些情况。
- 自定义控件中使用的样式定义在自定义控件内部,这违背了拥有自定义控件的目的,因为用户无法为控件的样式进行模板化。
这三点可能是我认为必须进行的、使控件适合通用使用的最必要的更改。