изменяемый размер сегмента на временной шкале: привязать размер сегмента к модели просмотра

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

Я представляю такой сегмент в моей модели представления:

public class Moment : ViewModelBase
{
    [Reactive] public double From { get; set; }
    [Reactive] public double Duration { get; set; }
}

И попытался реализовать представление, используя сетку с разделителями сетки следующим образом:

<Grid ColumnDefinitions="Auto,3,Auto,3,*" HorizontalAlignment="Stretch">
    <Panel Grid.Column="0" Name="SpacerLeft"
           Width="{Binding From}" />
    <GridSplitter Grid.Column="1" Background="cyan" />
    <Rectangle Grid.Column="2" Name="SpacerSegment"
               HorizontalAlignment="Stretch" Fill="red" Height="40"
               Width="{Binding Duration}">
    </Rectangle>
    <GridSplitter Grid.Column="3" Background="cyan" />
    <Panel Grid.Column="4" Name="SpacerRight"/>
</Grid>

Это работает так, как будто я могу визуально изменить размер сегмента:

пример перетаскивания временной шкалы gif

Тем не менее, я изо всех сил пытаюсь вернуть изменения размера в модель представления, которую вы также можете видеть в красной области, не меняя размер. Я не смог найти способ получить изменения ширины элементов управления SpacerLeft или SpacerSegment.

Вместо этого я попытался удалить выделенные свойства Width и вместо этого привязать ColumnDefinitions сетки. Я добавил это свойство в свою модель представления:

public ColumnDefinitions ColumnDefinitions
{
    get => ColumnDefinitions.Parse($"{From},3,{Duration},3,*");
    set
    {
        From = value[0].ActualWidth;
        Duration = value[2].ActualWidth;
    }
}

и изменил представление XAML на это:

<Grid ColumnDefinitions="{Binding ColumnDefinitions}" HorizontalAlignment="Stretch">
    <Panel Grid.Column="0" Name="SpacerLeft" />
    <GridSplitter Grid.Column="1" Background="cyan" />
    <Rectangle Grid.Column="2" Name="SpacerSegment"
               HorizontalAlignment="Stretch" Fill="red" Height="40">
    </Rectangle>
    <GridSplitter Grid.Column="3" Background="cyan" />
    <Panel Grid.Column="4" Name="SpacerRight"/>
</Grid>

Но это не скомпилируется по причине, которая, к сожалению, не имеет для меня особого смысла:

InvalidCastException: Unable to cast object of type 'Avalonia.Data.Binding' to type 'Avalonia.Controls.ColumnDefinition'. System.InvalidCastException: Unable to cast object of type 'Avalonia.Data.Binding' to type 'Avalonia.Controls.ColumnDefinition'.
  at Avalonia.Collections.AvaloniaList`1.System.Collections.IList.Add(Object value) in /_/src/Avalonia.Base/Collections/AvaloniaList.cs:line 520
  at Builder_1ee6d795025442edb279bcc7110e88eb_avares://AvaloniaOutseekClient/Views/MomentsSourceView.axaml.XamlClosure_2.Build(IServiceProvider )
  at Avalonia.Markup.Xaml.XamlIl.Runtime.XamlIlRuntimeHelpers.<>c__DisplayClass0_0.<DeferredTransformationFactoryV1>b__0(IServiceProvider sp) in /_/src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs:line 28
  at Avalonia.Markup.Xaml.Templates.TemplateContent.Load(Object templateContent) in /_/src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs:line 17
  at Avalonia.Markup.Xaml.Templates.DataTemplate.Build(Object data, IControl existing) in /_/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs:line 33
  at Avalonia.Controls.Presenters.ContentPresenter.CreateChild() in /_/src/Avalonia.Controls/Presenters/ContentPresenter.cs:line 356
  ...

person Felk    schedule 25.04.2021    source источник


Ответы (1)


Первая проблема заключается в попытке напрямую привязаться к ColumnDefinitions сетки. Вместо этого лучше всего использовать форму длинного определения и прямую привязку к ширине каждого столбца:

<Grid>
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="{Binding ...}" />
        <ColumnDefinition Width="Auto" />
        <!-- ... -->
    </Grid.ColumnDefinitions>
</Grid>

Также необходимо явно указать Mode=TwoWay, так как привязка к ширине ColumnDefinition по умолчанию не двусторонняя.

Далее, поскольку свойство Width ColumnDefinition на самом деле имеет тип GridLength, а не double, требуется IValueConverter, как описано здесь.

На данный момент XAML сетки должен выглядеть так

<Grid HorizontalAlignment="Stretch">
    <Grid.ColumnDefinitions>
        <ColumnDefinition Width="{Binding From, Mode=TwoWay, Converter={StaticResource GridLengthConverter}}" />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="{Binding Duration, Mode=TwoWay, Converter={StaticResource GridLengthConverter}}" />
        <ColumnDefinition Width="Auto" />
        <ColumnDefinition Width="*" />
    </Grid.ColumnDefinitions>
    <Panel Grid.Column="0" Name="SpacerLeft" />
    <GridSplitter Grid.Column="1" Background="cyan" />
    <Rectangle Grid.Column="2" Name="SpacerSegment"
               HorizontalAlignment="Stretch" Fill="red" Height="40">
    </Rectangle>
    <GridSplitter Grid.Column="3" Background="cyan" />
    <Panel Grid.Column="4" Name="SpacerRight"/>
</Grid>

Кроме того, чтобы исправить левый разделитель сетки, перемещающий выделение, а не расширяющий его влево, модель представления Moment должна представлять сегмент со свойством From и To вместо свойства From и Duration, а свойство Duration должно быть получено из вместо них два других. Это требует подключения некоторых событий изменения свойств вручную вместо того, чтобы полагаться на [Reactive]:

private double _from;
private double _to;

public double From
{
    get => _from;
    set
    {
        if (Equals(_from, value)) 
            return;
        this.RaisePropertyChanging(nameof(Duration));
        this.RaisePropertyChanging();
        _from = value;
        this.RaisePropertyChanged();
        this.RaisePropertyChanged(nameof(Duration));
    }
}

public double To
{
    get => _to;
    set
    {
        if (Equals(_to, value)) 
            return;
        this.RaisePropertyChanging(nameof(Duration));
        this.RaisePropertyChanging();
        _to = value;
        this.RaisePropertyChanged();
        this.RaisePropertyChanged(nameof(Duration));
    }
}

public double Duration
{
    get => To - From;
    set => To = From + value;
}

С этими изменениями результат выглядит так:

демонстрация слайдера сегментов

person Felk    schedule 26.04.2021