首先,对于一个新手来说,你似乎已经掌握了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:
xmlns:x="http:
xmlns:local="clr-namespace:timelines"
DataContext=", Path=Main}"
Title="MainWindow" Height="350" Width="525">
<Window.Resources>
<local:EventLengthConverter x:Key="mEventLengthConverter"/>
</Window.Resources>
<Grid>
<ItemsControl ItemsSource="">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ItemsControl x:Name="TimeLine" ItemsSource="">
<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="">
<Binding ElementName="TimeLine" Path="DataContext.Duration"/>
<Binding Path="Start"/>
<Binding ElementName="EventContainer" Path="ActualWidth"/>
</MultiBinding>
</Rectangle.Margin>
<Rectangle.Width>
<MultiBinding Converter="">
<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>
当有两个时间轴,分别有两个和三个事件时,我看到的是这样的情况。
![enter image description here](https://istack.dev59.com/md8GJ.webp)
解释
在这里,我们使用了嵌套的ItemsControls,一个用于顶层TimeLine属性,另一个用于每个时间轴的事件。我们覆盖了TimeLine ItemControl的ItemsPanel,将其设置为简单的网格 - 我们这样做是为了确保我们的所有矩形都使用相同的原点(以匹配我们的数据),而不是StackPanel等其他控件。
接下来,每个事件都有它自己的矩形,我们使用EventLengthConverter计算Margin(实际上是偏移量)和宽度。我们给多值转换器提供了它所需的一切,包括时间轴的持续时间、事件的开始或持续时间以及容器的宽度。当其中任何一个值发生变化时,都会调用转换器。理想情况下,每个矩形都应该在网格中占据一列,并且你可以将所有这些宽度设置为百分比,但由于数据的动态性,我们失去了这种便利。
优缺点
事件是元素树中的独立对象。现在您可以完全控制如何显示这些事件。它们不仅需要成为矩形,还可以是具有更多行为的复杂对象。至于这种方法的缺点 - 我不确定。有人可能会对性能提出异议,但我无法想象这会是一个实际的问题。
提示
您可以像以前一样将这些数据模板拆分,我只是将它们全部包含在一起,以便更轻松地查看答案中的层次结构。此外,如果您希望转换器的意图更清晰,可以创建两个转换器,例如“EventStartConverter”和“EventWidthConverter”,并放弃对targetType的检查。
编辑:
MainViewModel.cs
public class MainViewModel : ViewModelBase
{
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);
}
}
}