Как динамически нарисовать временную шкалу в WPF

Я пытаюсь рисовать сроки в WPF. Он должен состоять в основном из 3 прямоугольников.

Это должно выглядеть примерно так (жестко закодировано с использованием XAML): Timeline

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

Модели, представляющие это, - класс TimeLineEvent, у которого есть начало TimeSpan и продолжительность временного интервала, чтобы представить, когда событие начинается и как долго оно длится (в тиках или секундах или как угодно). Есть также класс TimeLine, который имеет ObservableCollection, который содержит все события на временной шкале. Он также имеет длительность TimeSpan, которая показывает, какова длительность самой временной шкалы.

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

До сих пор я подходил к созданию файла TimeLine.xaml, который просто содержит элемент canvas. В файле с выделенным кодом я переопределил метод OnRender для рисования этих прямоугольников, который работает с жестко закодированными значениями.

В MainWindow.xaml я создал табличку с данными и установил тип данных на TimeLine:

<DataTemplate x:Key="TimeLineEventsTemplate" DataType="{x:Type local:TimeLine}">
        <Border>
            <local:TimeLine Background="Transparent"/>
        </Border>
    </DataTemplate>

Пробовал разные настройки для этого, но не уверен, что делаю, если честно. Затем у меня есть стековая панель, которая содержит список, использующий мою таблицу данных и привязывающий TimeLines, который является ObservableCollection, содержащим объекты TimeLine, в моем коде позади MainWindow.

<StackPanel Grid.Column="1" Grid.Row="0">
        <ListBox x:Name="listBox"
                 Margin="20 20 20 0"
                 Background="Transparent"
                 ItemTemplate="{StaticResource TimeLineEventsTemplate}"
                 ItemsSource="{Binding TimeLines}"/>
    </StackPanel>

Это рисует новые временные шкалы, когда я создаю новые объекты временной шкалы, выглядящие так:

Проблема в том, что он не отображает зеленые прямоугольники должным образом, для этого мне нужно знать ширину белого прямоугольника, чтобы я мог использовать отношения различной длительности для перевода в позицию. Кажется, проблема в том, что свойство width равно 0, когда вызывается метод OnRender. Я попытался переопределить OnRenderSizeChanged, как показано здесь: В WPF как я могу получить визуализированный размер элемента управления до его фактической визуализации? Я видел в своей отладочной печати, что сначала вызывается OnRender, затем OnRenderSizeChanged, а затем я снова запускаю OnRender, вызывая this.InvalidateVisual(); в переопределении. Все свойства ширины, которые я могу получить, по-прежнему всегда равны 0, что странно, потому что я вижу, что он отображается и имеет размер. Также пробовал переопределять измерения и упорядочения, как показано в других публикациях, но пока не смог получить значение, отличное от 0.

Итак, как я могу динамически рисовать прямоугольники на временной шкале с правильным положением и размером?

Извините, если я упускаю что-то очевидное здесь, я просто работаю с WPF уже неделю, и у меня нет никого, чтобы спросить. Дайте мне знать, если вы хотите увидеть еще несколько примеров кода. Любая помощь приветствуется:).

1 ответ

Решение

Позвольте мне просто сказать, что для новичка в WPF вы, похоже, хорошо разбираетесь в вещах.

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

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

Код

EventLengthConverter.cs:

public class EventLengthConverter : IMultiValueConverter
{

    public object Convert(object[] values, Type targetType, object parameter, System.Globalization.CultureInfo culture)
    {
        TimeSpan timelineDuration = (TimeSpan)values[0];
        TimeSpan relativeTime = (TimeSpan)values[1];
        double containerWidth = (double)values[2];
        double factor = relativeTime.TotalSeconds / timelineDuration.TotalSeconds;
        double rval = factor * containerWidth;

        if (targetType == typeof(Thickness))
        {
            return new Thickness(rval, 0, 0, 0);
        }
        else
        {
            return rval;
        }
    }

    public object[] ConvertBack(object value, Type[] targetTypes, object parameter, System.Globalization.CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

MainWindow.xaml:

<Window x:Class="timelines.MainWindow"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:local="clr-namespace:timelines"
    DataContext="{Binding Source={StaticResource Locator}, Path=Main}"
    Title="MainWindow" Height="350" Width="525">
<Window.Resources>
    <local:EventLengthConverter x:Key="mEventLengthConverter"/>
</Window.Resources>
<Grid>
    <ItemsControl ItemsSource="{Binding Path=TimeLines}">
        <ItemsControl.ItemTemplate>
            <DataTemplate>
                <ItemsControl x:Name="TimeLine" ItemsSource="{Binding Path=Events}">
                    <ItemsControl.ItemsPanel>
                        <ItemsPanelTemplate>
                            <Grid x:Name="EventContainer" Height="20" Margin="5" Background="Gainsboro"/>
                        </ItemsPanelTemplate>
                    </ItemsControl.ItemsPanel>
                    <ItemsControl.ItemTemplate>
                        <DataTemplate>
                            <Rectangle Grid.Column="1" Fill="Green" VerticalAlignment="Stretch" HorizontalAlignment="Left">
                                <Rectangle.Margin>
                                    <MultiBinding Converter="{StaticResource mEventLengthConverter}">
                                        <Binding ElementName="TimeLine" Path="DataContext.Duration"/>
                                        <Binding Path="Start"/>
                                        <Binding ElementName="EventContainer" Path="ActualWidth"/>
                                    </MultiBinding>
                                </Rectangle.Margin>
                                <Rectangle.Width>
                                    <MultiBinding Converter="{StaticResource mEventLengthConverter}">
                                        <Binding ElementName="TimeLine" Path="DataContext.Duration"/>
                                        <Binding Path="Duration"/>
                                        <Binding ElementName="EventContainer" Path="ActualWidth"/>
                                    </MultiBinding>
                                </Rectangle.Width>
                            </Rectangle>
                        </DataTemplate>
                    </ItemsControl.ItemTemplate>
                </ItemsControl>
            </DataTemplate>
        </ItemsControl.ItemTemplate>
    </ItemsControl>
</Grid>

Вот что я вижу, когда есть две временные шкалы с двумя и тремя событиями соответственно.

объяснение

Здесь вы получите вложенные ItemsControls, один для свойства TimeLine верхнего уровня и один для событий каждой временной шкалы. Мы переопределяем ItemsPanel TimeLine ItemControl к простой Grid - мы делаем это, чтобы убедиться, что все наши прямоугольники используют один и тот же источник (для соответствия нашим данным), а не как StackPanel.

Затем каждое событие получает свой собственный прямоугольник, который мы используем EventLengthConverter для вычисления поля (фактически смещения) и ширины. Мы предоставляем многозначному конвертеру все, что ему нужно, длительность шкалы времени, события Start или Duration и ширину контейнера. Преобразователь будет вызываться каждый раз, когда изменяется одно из этих значений. В идеале каждый прямоугольник получит столбец в сетке, и вы можете просто установить все эти значения ширины в процентах, но мы теряем эту роскошь из-за динамического характера данных.

Плюсы и минусы

События - это их собственные объекты в дереве элементов. Теперь вы можете контролировать то, как вы отображаете события. Они не должны быть просто прямоугольниками, они могут быть сложными объектами с большим поведением. Что касается причин против этого метода - я не уверен. Кто-то может поспорить с производительностью, но я не могу представить, что это практическая проблема.

подсказки

Вы можете разбить эти шаблоны данных, как и раньше, я просто включил их все вместе, чтобы легче увидеть иерархию в ответе. Кроме того, если вы хотите, чтобы намерение преобразователя было более понятным, вы можете создать два, что-то вроде "EventStartConverter" и "EventWidthConverter", и отменить проверку против targetType.

РЕДАКТИРОВАТЬ:

MainViewModel.cs

public class MainViewModel : ViewModelBase
{
    /// <summary>
    /// Initializes a new instance of the MainViewModel class.
    /// </summary>
    public MainViewModel()
    {

        TimeLine first = new TimeLine();
        first.Duration = new TimeSpan(1, 0, 0);
        first.Events.Add(new TimeLineEvent() { Start = new TimeSpan(0, 15, 0), Duration = new TimeSpan(0, 15, 0) });
        first.Events.Add(new TimeLineEvent() { Start = new TimeSpan(0, 40, 0), Duration = new TimeSpan(0, 10, 0) });
        this.TimeLines.Add(first);

        TimeLine second = new TimeLine();
        second.Duration = new TimeSpan(1, 0, 0);
        second.Events.Add(new TimeLineEvent() { Start = new TimeSpan(0, 0, 0), Duration = new TimeSpan(0, 25, 0) });
        second.Events.Add(new TimeLineEvent() { Start = new TimeSpan(0, 30, 0), Duration = new TimeSpan(0, 15, 0) });
        second.Events.Add(new TimeLineEvent() { Start = new TimeSpan(0, 50, 0), Duration = new TimeSpan(0, 10, 0) });
        this.TimeLines.Add(second);
    }


    private ObservableCollection<TimeLine> _timeLines = new ObservableCollection<TimeLine>();
    public ObservableCollection<TimeLine> TimeLines
    {
        get
        {
            return _timeLines;
        }
        set
        {
            Set(() => TimeLines, ref _timeLines, value);
        }
    }

}

public class TimeLineEvent : ObservableObject
{
    private TimeSpan _start;
    public TimeSpan Start
    {
        get
        {
            return _start;
        }
        set
        {
            Set(() => Start, ref _start, value);
        }
    }


    private TimeSpan _duration;
    public TimeSpan Duration
    {
        get
        {
            return _duration;
        }
        set
        {
            Set(() => Duration, ref _duration, value);
        }
    }

}


public class TimeLine : ObservableObject
{
    private TimeSpan _duration;
    public TimeSpan Duration
    {
        get
        {
            return _duration;
        }
        set
        {
            Set(() => Duration, ref _duration, value);
        }
    }


    private ObservableCollection<TimeLineEvent> _events = new ObservableCollection<TimeLineEvent>();
    public ObservableCollection<TimeLineEvent> Events
    {
        get
        {
            return _events;
        }
        set
        {
            Set(() => Events, ref _events, value);
        }
    }
}
Другие вопросы по тегам