如何为 ItemTemplate 和 ItemsSource 实现可附加属性
How to Implement Attachable Properties for ItemTemplate and ItemsSource
我正在尝试将 WPF 网格用作使用附加属性的 ItemsControl,以创建可缩放的钢琴键盘。键盘中的每个键可能跨越 1 到 3 列,具体取决于它之前和之后的内容,如果是锐利的,将跨越 1 行,如果是自然的,将跨越 2 行。我已经有 2 个附加属性用于动态设置网格的列数和行数(尽管需要调整这些属性以支持每个 column/row 的 width/height 的设置)。
我现在需要实现的是 ItemsSource
(键)和 ItemTemplate
(钢琴键视图)的两个可附加属性。我需要在 Grid 控件上使用它,因为 ItemsControl
仅支持 UniformGrid
作为其 ItemsPanel
的 Grid,并且不将特定项目分配给特定 columns/rows。我的钢琴键盘需要每个八度键有 17 列,但 ItemsControl 只会在 UniformGrid
中创建 12 列,因为只有 12 个键传递给它。我附上了一张 1 倍频程钢琴键盘的图像,其中包含每个必填列的索引。
这是我目前的键盘代码,我缺少 GridExtensions.ItemsSource
和 GridExtensions.ItemTemplate
的实现。 GridExtensions
是一个包含可附加属性的静态 class。
<UserControl x:Class="SphynxAlluro.Music.Wpf.PianoKeyboard.View.PianoKeyboardView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:converters="http://schemas.sphynxalluro.com/converters"
xmlns:local="clr-namespace:SphynxAlluro.Music.Wpf.PianoKeyboard.View"
xmlns:prism="http://www.codeplex.com/prism"
xmlns:sphynxAlluroControls="http://schemas.sphynxalluro.com/controls"
xmlns:wpfBindingExtensions="http://schemas.sphynxalluro.com/bindingExtensions"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="600">
<UserControl.Resources>
<converters:KeysToColumnsCountConverter x:Key="keysToColumnsCountConverter"/>
<converters:KeysToRowsCountConverter x:Key="keysToRowsCountConverter"/>
<converters:IsSharpToRowSpanConverter x:Key="isSharpToRowSpanConverter"/>
<converters:KeysCollectionAndKeyToColumnIndexConverter x:Key="keysCollectionAndKeyToColumnIndexConverter"/>
<converters:KeysCollectionAndKeyToColumnSpanConverter x:Key="keysCollectionAndKeyToColumnSpanConverter"/>
</UserControl.Resources>
<Grid wpfBindingExtensions:GridExtensions.ItemsSource="{Binding Keys}"
wpfBindingExtensions:GridExtensions.ItemsOrientation="Horizontal"
wpfBindingExtensions:GridExtensions.ColumnCount="{Binding Keys, Converter={StaticResource keysToColumnsCountConverter}}"
wpfBindingExtensions:GridExtensions.RowCount="{Binding Keys, Converter={StaticResource keysToRowsCountConverter}}">
<wpfBindingExtensions:GridExtensions.ItemTemplate>
<DataTemplate>
<local:PianoKeyView Grid.RowSpan="{Binding Note.IsSharp, Mode=OneTime, Converter={StaticResource isSharpToRowSpanConverter}}"
DataContext="{Binding}">
<Grid.Column>
<MultiBinding Converter="{StaticResource keysCollectionAndKeyToColumnIndexConverter}" Mode="OneTime">
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="Items"/>
<Binding/>
</MultiBinding>
</Grid.Column>
<Grid.ColumnSpan>
<MultiBinding Converter="{StaticResource keysCollectionAndKeyToColumnSpanConverter}" Mode="OneTime">
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="Items"/>
<Binding/>
</MultiBinding>
</Grid.ColumnSpan>
</local:PianoKeyView>
</DataTemplate>
</wpfBindingExtensions:GridExtensions.ItemTemplate>
</Grid>
这是 GridExtensions 中可附加的 ItemTemplate 属性 的 ItemTemplateChanged 处理程序的代码,请注意行上方的两个 TODO,它们不会编译。
private static void ItemTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var itemTemplate = (DataTemplate)e.NewValue;
var itemsSource = GetItemsSource(d);
var itemsSourceCount = itemsSource.Count();
var itemsOrientation = GetItemsOrientation(d);
var gridChildren = ((Grid)d).Children;
gridChildren.Clear();
switch (itemsOrientation)
{
case Orientation.Horizontal:
foreach (var item in itemsSource)
{
var itemFactory = new FrameworkElementFactory(item.GetType());
//TODO: Find out where the ContentProperty for Grid is.
itemFactory.SetValue(d.ContentProperty, item);
itemTemplate.VisualTree = itemFactory;
//TODO: Find out how to add the applied itemTemplate.
gridChildren.Add(itemTemplate);
}
break;
case Orientation.Vertical:
break;
default:
throw new EnumValueNotSupportedException(itemsOrientation, nameof(itemsOrientation).ToPascalCase());
}
}
我试图通过 Grid
直接实现的目标可以通过具有 Grid
的 ItemsPanel
的 ItemsControl
实现。
原来需要的缺失部分是 Style
和 ContentPresenter
的 TargetType
。在这种风格中,可附加的 Grid
属性,例如 Grid.RowSpan
、Grid.Column
和 Grid.ColumnSpan
可以通过适当的转换器设置,这些转换器接受 ItemsControl 和 Key 的 DataContext 和 return 所需的整数。按键的 Z-Index 也可以在这里设置,这样锐键就会出现在自然键的上方。
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Grid.RowSpan" Value="{Binding Note.IsSharp, Mode=OneTime, Converter={StaticResource isSharpToRowSpanConverter}}"/>
<Setter Property="Grid.Column">
<Setter.Value>
<MultiBinding Converter="{StaticResource keysCollectionAndKeyToColumnIndexConverter}" Mode="OneTime">
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="Items"/>
<Binding/>
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Grid.ColumnSpan">
<Setter.Value>
<MultiBinding Converter="{StaticResource keysCollectionAndKeyToColumnSpanConverter}" Mode="OneTime">
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="Items"/>
<Binding/>
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Panel.ZIndex" Value="{Binding Note.IsSharp, Converter={StaticResource booleanToIntegerConverter}}"/>
</Style>
</ItemsControl.ItemContainerStyle>
这大大简化了 ItemTemplate 如下:
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:PianoKeyView DataContext="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
ItemsControl.ItemsPanel
负责生成 Grid
,然后将这些 PianoKeyView
包裹在 ContentPresenter
中。
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid wpfBindingExtensions:GridExtensions.ColumnDefinitions="{Binding Keys, Converter={StaticResource keysToColumnDefinitionsConverter}}"
wpfBindingExtensions:GridExtensions.RowDefinitions="{Binding Keys, Converter={StaticResource keysToRowDefinitionsConverter}}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
我已经修改了我原来的 ColumnCount
和 RowCount
附加属性,改为采用 IEnumerable<ColumnDefinition>
/IEnumerable<RowDefinition>
适当的,这样每个 [=67= ...一种边缘情况,在这种情况下,键盘中只需要自然键或升键(例如 B 到 C,E 到 F 或单个升键或自然键)。附加的 RowDefinitions 的代码 属性 与使用 ColumnDefinitions 属性(仅使用 RowDefinitions 而不是 ColumnDefinitions)所以我将 post 用于 ColumnDefinitions 的那个:
public static class GridExtensions
{
// Using a DependencyProperty as the backing store for ColumnDefinitions. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ColumnDefinitionsProperty =
DependencyProperty.RegisterAttached(
nameof(ColumnDefinitionsProperty).Substring(0, nameof(ColumnDefinitionsProperty).Length - "Property".Length),
typeof(IEnumerable<ColumnDefinition>),
typeof(GridExtensions),
new PropertyMetadata(Enumerable.Empty<ColumnDefinition>(), ColumnDefinitionsChanged));
public static IEnumerable<ColumnDefinition> GetColumnDefinitions(DependencyObject obj)
=> (IEnumerable<ColumnDefinition>)obj.GetValue(ColumnDefinitionsProperty);
public static void SetColumnDefinitions(DependencyObject obj, IEnumerable<ColumnDefinition> value)
=> obj.SetValue(ColumnDefinitionsProperty, value);
private static void ColumnDefinitionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var columnDefinitionCollection = ((Grid)d).ColumnDefinitions;
var newColumnDefinitions = (IEnumerable<ColumnDefinition>)e.NewValue;
var columnCount = newColumnDefinitions.Count();
columnDefinitionCollection.Clear();
foreach (var newColumnDefinition in newColumnDefinitions)
columnDefinitionCollection.Add(newColumnDefinition);
}
}
有关附加属性的更多信息,请参阅 "Using a Grid as the Panel for an ItemsControl" (http://blog.scottlogic.com/2010/11/15/using-a-grid-as-the-panel-for-an-itemscontrol.html),这是我找到派生上述静态 class 的原始代码的地方。否则,这里的关键要素是:
- 将可附加的面板属性(例如
Grid.Column
和 Panel.ZOrder
)分配给 ItemsControl.ItemContainerStyle
中的 ContentPresenter
样式。
- 将
ItemsControl.ItemsPanel
设置为 Grid
的 ItemsPanelTemplate
并从那里设置网格的 ColumnDefinitions
和 RowDefinitions
。
我没有 post 我所有的转换器都在这里,因为这个答案已经很长了,但是有人让我知道他们是否觉得它们与 post 相关。否则,这是最终结果...
我正在尝试将 WPF 网格用作使用附加属性的 ItemsControl,以创建可缩放的钢琴键盘。键盘中的每个键可能跨越 1 到 3 列,具体取决于它之前和之后的内容,如果是锐利的,将跨越 1 行,如果是自然的,将跨越 2 行。我已经有 2 个附加属性用于动态设置网格的列数和行数(尽管需要调整这些属性以支持每个 column/row 的 width/height 的设置)。
我现在需要实现的是 ItemsSource
(键)和 ItemTemplate
(钢琴键视图)的两个可附加属性。我需要在 Grid 控件上使用它,因为 ItemsControl
仅支持 UniformGrid
作为其 ItemsPanel
的 Grid,并且不将特定项目分配给特定 columns/rows。我的钢琴键盘需要每个八度键有 17 列,但 ItemsControl 只会在 UniformGrid
中创建 12 列,因为只有 12 个键传递给它。我附上了一张 1 倍频程钢琴键盘的图像,其中包含每个必填列的索引。
这是我目前的键盘代码,我缺少 GridExtensions.ItemsSource
和 GridExtensions.ItemTemplate
的实现。 GridExtensions
是一个包含可附加属性的静态 class。
<UserControl x:Class="SphynxAlluro.Music.Wpf.PianoKeyboard.View.PianoKeyboardView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:i="http://schemas.microsoft.com/expression/2010/interactivity"
xmlns:ei="http://schemas.microsoft.com/expression/2010/interactions"
xmlns:converters="http://schemas.sphynxalluro.com/converters"
xmlns:local="clr-namespace:SphynxAlluro.Music.Wpf.PianoKeyboard.View"
xmlns:prism="http://www.codeplex.com/prism"
xmlns:sphynxAlluroControls="http://schemas.sphynxalluro.com/controls"
xmlns:wpfBindingExtensions="http://schemas.sphynxalluro.com/bindingExtensions"
mc:Ignorable="d"
d:DesignHeight="200" d:DesignWidth="600">
<UserControl.Resources>
<converters:KeysToColumnsCountConverter x:Key="keysToColumnsCountConverter"/>
<converters:KeysToRowsCountConverter x:Key="keysToRowsCountConverter"/>
<converters:IsSharpToRowSpanConverter x:Key="isSharpToRowSpanConverter"/>
<converters:KeysCollectionAndKeyToColumnIndexConverter x:Key="keysCollectionAndKeyToColumnIndexConverter"/>
<converters:KeysCollectionAndKeyToColumnSpanConverter x:Key="keysCollectionAndKeyToColumnSpanConverter"/>
</UserControl.Resources>
<Grid wpfBindingExtensions:GridExtensions.ItemsSource="{Binding Keys}"
wpfBindingExtensions:GridExtensions.ItemsOrientation="Horizontal"
wpfBindingExtensions:GridExtensions.ColumnCount="{Binding Keys, Converter={StaticResource keysToColumnsCountConverter}}"
wpfBindingExtensions:GridExtensions.RowCount="{Binding Keys, Converter={StaticResource keysToRowsCountConverter}}">
<wpfBindingExtensions:GridExtensions.ItemTemplate>
<DataTemplate>
<local:PianoKeyView Grid.RowSpan="{Binding Note.IsSharp, Mode=OneTime, Converter={StaticResource isSharpToRowSpanConverter}}"
DataContext="{Binding}">
<Grid.Column>
<MultiBinding Converter="{StaticResource keysCollectionAndKeyToColumnIndexConverter}" Mode="OneTime">
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="Items"/>
<Binding/>
</MultiBinding>
</Grid.Column>
<Grid.ColumnSpan>
<MultiBinding Converter="{StaticResource keysCollectionAndKeyToColumnSpanConverter}" Mode="OneTime">
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="Items"/>
<Binding/>
</MultiBinding>
</Grid.ColumnSpan>
</local:PianoKeyView>
</DataTemplate>
</wpfBindingExtensions:GridExtensions.ItemTemplate>
</Grid>
这是 GridExtensions 中可附加的 ItemTemplate 属性 的 ItemTemplateChanged 处理程序的代码,请注意行上方的两个 TODO,它们不会编译。
private static void ItemTemplateChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var itemTemplate = (DataTemplate)e.NewValue;
var itemsSource = GetItemsSource(d);
var itemsSourceCount = itemsSource.Count();
var itemsOrientation = GetItemsOrientation(d);
var gridChildren = ((Grid)d).Children;
gridChildren.Clear();
switch (itemsOrientation)
{
case Orientation.Horizontal:
foreach (var item in itemsSource)
{
var itemFactory = new FrameworkElementFactory(item.GetType());
//TODO: Find out where the ContentProperty for Grid is.
itemFactory.SetValue(d.ContentProperty, item);
itemTemplate.VisualTree = itemFactory;
//TODO: Find out how to add the applied itemTemplate.
gridChildren.Add(itemTemplate);
}
break;
case Orientation.Vertical:
break;
default:
throw new EnumValueNotSupportedException(itemsOrientation, nameof(itemsOrientation).ToPascalCase());
}
}
我试图通过 Grid
直接实现的目标可以通过具有 Grid
的 ItemsPanel
的 ItemsControl
实现。
原来需要的缺失部分是 Style
和 ContentPresenter
的 TargetType
。在这种风格中,可附加的 Grid
属性,例如 Grid.RowSpan
、Grid.Column
和 Grid.ColumnSpan
可以通过适当的转换器设置,这些转换器接受 ItemsControl 和 Key 的 DataContext 和 return 所需的整数。按键的 Z-Index 也可以在这里设置,这样锐键就会出现在自然键的上方。
<ItemsControl.ItemContainerStyle>
<Style TargetType="ContentPresenter">
<Setter Property="Grid.RowSpan" Value="{Binding Note.IsSharp, Mode=OneTime, Converter={StaticResource isSharpToRowSpanConverter}}"/>
<Setter Property="Grid.Column">
<Setter.Value>
<MultiBinding Converter="{StaticResource keysCollectionAndKeyToColumnIndexConverter}" Mode="OneTime">
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="Items"/>
<Binding/>
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Grid.ColumnSpan">
<Setter.Value>
<MultiBinding Converter="{StaticResource keysCollectionAndKeyToColumnSpanConverter}" Mode="OneTime">
<Binding RelativeSource="{RelativeSource AncestorType=ItemsControl}" Path="Items"/>
<Binding/>
</MultiBinding>
</Setter.Value>
</Setter>
<Setter Property="Panel.ZIndex" Value="{Binding Note.IsSharp, Converter={StaticResource booleanToIntegerConverter}}"/>
</Style>
</ItemsControl.ItemContainerStyle>
这大大简化了 ItemTemplate 如下:
<ItemsControl.ItemTemplate>
<DataTemplate>
<local:PianoKeyView DataContext="{Binding}"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
ItemsControl.ItemsPanel
负责生成 Grid
,然后将这些 PianoKeyView
包裹在 ContentPresenter
中。
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Grid wpfBindingExtensions:GridExtensions.ColumnDefinitions="{Binding Keys, Converter={StaticResource keysToColumnDefinitionsConverter}}"
wpfBindingExtensions:GridExtensions.RowDefinitions="{Binding Keys, Converter={StaticResource keysToRowDefinitionsConverter}}"/>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
我已经修改了我原来的 ColumnCount
和 RowCount
附加属性,改为采用 IEnumerable<ColumnDefinition>
/IEnumerable<RowDefinition>
适当的,这样每个 [=67= ...一种边缘情况,在这种情况下,键盘中只需要自然键或升键(例如 B 到 C,E 到 F 或单个升键或自然键)。附加的 RowDefinitions 的代码 属性 与使用 ColumnDefinitions 属性(仅使用 RowDefinitions 而不是 ColumnDefinitions)所以我将 post 用于 ColumnDefinitions 的那个:
public static class GridExtensions
{
// Using a DependencyProperty as the backing store for ColumnDefinitions. This enables animation, styling, binding, etc...
public static readonly DependencyProperty ColumnDefinitionsProperty =
DependencyProperty.RegisterAttached(
nameof(ColumnDefinitionsProperty).Substring(0, nameof(ColumnDefinitionsProperty).Length - "Property".Length),
typeof(IEnumerable<ColumnDefinition>),
typeof(GridExtensions),
new PropertyMetadata(Enumerable.Empty<ColumnDefinition>(), ColumnDefinitionsChanged));
public static IEnumerable<ColumnDefinition> GetColumnDefinitions(DependencyObject obj)
=> (IEnumerable<ColumnDefinition>)obj.GetValue(ColumnDefinitionsProperty);
public static void SetColumnDefinitions(DependencyObject obj, IEnumerable<ColumnDefinition> value)
=> obj.SetValue(ColumnDefinitionsProperty, value);
private static void ColumnDefinitionsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var columnDefinitionCollection = ((Grid)d).ColumnDefinitions;
var newColumnDefinitions = (IEnumerable<ColumnDefinition>)e.NewValue;
var columnCount = newColumnDefinitions.Count();
columnDefinitionCollection.Clear();
foreach (var newColumnDefinition in newColumnDefinitions)
columnDefinitionCollection.Add(newColumnDefinition);
}
}
有关附加属性的更多信息,请参阅 "Using a Grid as the Panel for an ItemsControl" (http://blog.scottlogic.com/2010/11/15/using-a-grid-as-the-panel-for-an-itemscontrol.html),这是我找到派生上述静态 class 的原始代码的地方。否则,这里的关键要素是:
- 将可附加的面板属性(例如
Grid.Column
和Panel.ZOrder
)分配给ItemsControl.ItemContainerStyle
中的ContentPresenter
样式。 - 将
ItemsControl.ItemsPanel
设置为Grid
的ItemsPanelTemplate
并从那里设置网格的ColumnDefinitions
和RowDefinitions
。
我没有 post 我所有的转换器都在这里,因为这个答案已经很长了,但是有人让我知道他们是否觉得它们与 post 相关。否则,这是最终结果...