Как реализовать присоединяемые свойства для ItemTemplate и ItemsSource

Я пытаюсь использовать WPF Grid в качестве ItemsControl, используя прикрепленные свойства для создания масштабируемой клавиатуры пианино. Каждая клавиша на клавиатуре может занимать от 1 до 3 столбцов в зависимости от того, что предшествует и следует за ней, и будет занимать 1 строку, если она резкая, или 2, если натуральная. У меня уже есть 2 прикрепленных свойства для динамической установки числа столбцов сетки и количества строк (хотя их необходимо будет отрегулировать так, чтобы они поддерживали настройку ширины / высоты каждого столбца / строки).

Теперь мне нужно реализовать два присоединяемых свойства для ItemsSource (Ключи) и ItemTemplate (PianoKeyView). Мне нужно использовать это на элементе управления Grid, потому что ItemsControl только поддерживает UniformGrid в качестве сетки для его ItemsPanel а также не присваивает определенные элементы определенным столбцам / строкам. Моя клавиатура пианино потребует 17 столбцов на октаву клавиш, но ItemsControl создаст только 12 столбцов в UniformGrid поскольку к нему будет передано только 12 ключей. Я включил изображение 1-октавной фортепианной клавиатуры с указателем каждого необходимого столбца.

PianoKeyboard Grid Column Indices

Это мой код для клавиатуры в ее нынешнем виде, мне не хватает реализации для GridExtensions.ItemsSource а также GridExtensions.ItemTemplate, GridExtensions статический класс, содержащий присоединяемые свойства

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

И это код для обработчика ItemTemplateChanged для присоединяемого свойства ItemTemplate в GridExtensions, обратите внимание на два 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());
    }
}

1 ответ

Решение

Чего я пытался достичь с Grid непосредственно может быть достигнуто с ItemsControl с ItemsPanel из Grid,

Оказывается, недостающий кусок, который был нужен, был Style с TargetType из ContentPresenter, В этом стиле, прикрепляемый Grid свойства, такие как Grid.RowSpan, Grid.Column а также Grid.ColumnSpan устанавливаются через соответствующие конвертеры, которые принимают ItemControl и DataContext Key и возвращают требуемое целое число. Здесь также можно задать Z-индекс клавиш, чтобы острые клавиши отображались над естественными клавишами.

<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с завернутыми в их ContentPresenters будет помещен в.

<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> в зависимости от обстоятельств, чтобы размер каждого столбца / строки также можно было передать свойствам (что я и делаю в этом случае через преобразователь, который принимает все PianoKeyViewModels и возвращает ColumnDefinition/RowDefinition для каждого с соответствующим размером звездочки. Назначение для RowDefinitions предназначен только для крайнего случая, при котором на клавиатуре требуются только натуральные или острые клавиши (например, от B до C, от E до F или от одной резкой или натуральной клавиши). Код для присоединенного свойства RowDefinitions по сути та же логика, что и со свойством ColumnDefinitions (работающим только с RowDefinitions вместо ColumnDefinitions), поэтому я просто опубликую его для 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);
    }
}

Дополнительные сведения о прикрепленных свойствах см. В разделе "Использование сетки в качестве панели для ItemsControl" ( http://blog.scottlogic.com/2010/11/15/using-a-grid-as-the-panel-for-an-itemscontrol.html), где я нашел исходный код, из которого я получил вышеуказанный статический класс. В противном случае ключевыми элементами здесь являются:

  1. Назначьте присоединяемые свойства панели (например, Grid.Column а также Panel.ZOrder) к ContentPresenter стиль в ItemsControl.ItemContainerStyle,
  2. Установить ItemsControl.ItemsPanel для ItemsPanelTemplate из Grid и установить сетку ColumnDefinitions а также RowDefinitions оттуда.

Я не разместил здесь все свои конвертеры, так как этот ответ уже стал довольно длинным, но кто-то дал мне знать, если они считают, что имеют отношение к публикации. В противном случае, здесь был конечный результат...

PianoKeyboardView

Другие вопросы по тегам