Как динамически рисовать временную шкалу в WPF
Я пытаюсь нарисовать временные рамки в WPF. Он должен в основном состоять из 3 прямоугольников.
Это должно выглядеть примерно так (жестко закодировано с помощью XAML): хронология
большой белый прямоугольник должен заполнить все доступное пространство, зеленые прямоугольники обозначают начало и продолжительность событий, которые происходят на временной шкале.
модели, представляющие это TimeLineEvent класс, который имеет TimeSpan start и TimeSpan продолжительность представляет, когда начинается событие и как долго оно длится (в ТИКах или секундах или что-то еще). Существует также класс TimeLine, который имеет ObservableCollection, который содержит все события на временной шкале. Он также имеет продолжительность TimeSpan, которая представляет, как долго сама временная шкала.
что мне нужно сделать, это иметь возможность динамически рисовать события (зеленые прямоугольники) на временной шкале на основе их продолжительности и начала, а также соотношения между ними, чтобы событие было нарисовано соответствует тому, когда это происходит и как долго. На временной шкале может быть несколько событий.
мой подход до сих пор состоял в том, чтобы сделать временную шкалу.XAML-файл, который просто содержит элемент canvas. В файле кода я переопределил метод OnRender для рисования этих прямоугольников, который работает с жестко закодированными значениями.
в файле MainWindow.xaml я создал datatemplate и установил тип данных в TimeLine:
<DataTemplate x:Key="TimeLineEventsTemplate" DataType="{x:Type local:TimeLine}">
<Border>
<local:TimeLine Background="Transparent"/>
</Border>
</DataTemplate>
пробовал разные настройки для этот, но не уверен, что я делаю, чтобы быть честным. Затем у меня есть stackpanel, который содержит listbox, который использует мой 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 для запуска снова назвав этот.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 на простую сетку - мы делаем это, чтобы убедиться, что все наши прямоугольники используют один и тот же источник (для соответствия нашим данным), а не говорят StackPanel.
далее, каждое событие получает свой собственный прямоугольник, который мы используем EventLengthConverter для вычисления поля (эффективно смещение)и ширины. Мы даем многозначному преобразователю все, что ему нужно, длительность временных линий, начало или продолжительность событий и ширину контейнера. Преобразователь будет вызываться в любое время, когда одно из этих значений изменится. В идеале каждый прямоугольник получит столбец в сетке, и вы можете просто установить все эти ширины в процентах, но мы теряем эту роскошь с динамическим характером данных.
плюсы и Минусы
события - это их собственные объекты в дереве элементов. Теперь у вас есть тонна контроля над тем, как вы отображаете события. Они не должны быть прямоугольники, они могут быть сложными объектами с большим поведением. Что касается причин против этого метода-я не уверен. Кто-то может спорить с производительностью, но я не могу представить, что это практическая проблема.
советы
вы можете разбить эти шаблоны данных, как и раньше, я просто включил их все вместе чтобы увидеть иерархию в ответ. Кроме того, если вы хотите, чтобы намерение конвертера было более ясным, вы могли бы создать два, что-то вроде "EventStartConverter" и "EventWidthConverter", и отменить проверку против targetType.
EDIT:
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);
}
}
}