我只想说,对于刚接触 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>
这是我在两条时间线分别包含两个和三个事件时看到的内容。
解释
您在这里最终得到的是嵌套的 ItemsControl,一个用于顶级 TimeLine 属性,一个用于每个时间线的事件。我们将 TimeLine ItemControl 的 ItemsPanel 覆盖为一个简单的 Grid - 我们这样做是为了确保我们所有的矩形都使用相同的原点(以匹配我们的数据),而不是说 StackPanel。
接下来,每个事件都有自己的矩形,我们使用 EventLengthConverter 来计算 Margin(实际上是偏移量)和宽度。我们为多值转换器提供它所需的一切,时间线持续时间、事件开始或持续时间以及容器宽度。每当这些值之一发生变化时,都会调用转换器。理想情况下,每个矩形都会在网格中得到一列,您可以将所有这些宽度设置为百分比,但我们会因为数据的动态特性而失去这种奢侈。
优点和缺点
事件是元素树中它们自己的对象。您现在对如何显示事件有很大的控制权。它们不必只是矩形,它们可以是具有更多行为的复杂对象。至于反对这种方法的原因 - 我不确定。有人可能会与性能争论,但我无法想象这是一个实际问题。
尖端
您可以像以前一样拆分这些数据模板,我只是将它们全部包含在一起,以便在答案中更轻松地查看层次结构。此外,如果您希望转换器的意图更清晰,您可以创建两个,例如“EventStartConverter”和“EventWidthConverter”,并放弃对 targetType 的检查。
编辑:
主视图模型.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);
}
}
}