如何在WPF中动态绘制时间轴
How to dynamically draw a timeline in WPF
我正在尝试在 WPF 中绘制时间线。它基本上应该由 3 个矩形组成。
它应该看起来像这样(使用 XAML 硬编码):
Timeline
大的白色矩形应该填满所有可用的 space,绿色矩形表示时间线上发生的事件的开始和持续时间。
代表这个的模型是一个 TimeLineEvent class,它有一个 TimeSpan 开始和一个时间跨度持续时间来表示事件何时开始以及持续多长时间(以滴答或秒或其他形式)。还有一个 TimeLine class,它有一个 ObservableCollection,它包含时间轴上的所有事件。它还有一个 TimeSpan 持续时间,表示时间线本身有多长。
我需要做的是能够根据事件的持续时间和开始时间以及它们之间的比率在时间轴上动态绘制事件(绿色矩形),以便绘制对应于事件发生时间和多长时间。时间线上可以有多个事件。
到目前为止,我的方法是创建一个只包含 canvas 元素的 TimeLine.xaml 文件。在代码隐藏文件中,我重写了 OnRender 方法来绘制这些矩形,它使用硬编码值。
在 MainWindow.xaml 中,我创建了一个数据模板并将数据类型设置为 TimeLine:
<DataTemplate x:Key="TimeLineEventsTemplate" DataType="{x:Type local:TimeLine}">
<Border>
<local:TimeLine Background="Transparent"/>
</Border>
</DataTemplate>
为此尝试了不同的设置,但老实说我不确定我在做什么。然后我有一个堆栈面板,其中包含一个列表框,该列表框使用我的数据模板并绑定 TimeLines,这是一个 ObservableCollection,在我的 MainWindow 代码隐藏中包含 TimeLine 对象。
<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>
这会在我创建新的时间轴对象时绘制新的时间轴,如下所示:
Timelines
这个问题是它没有正确渲染绿色矩形,为此我需要知道白色矩形的宽度,这样我就可以使用不同持续时间的比率来转换到一个位置.
问题似乎是调用 OnRender 方法时宽度 属性 为 0。我已尝试覆盖 OnRenderSizeChanged,如下所示:In WPF how can I get the rendered size of a control before it actually renders?
我在调试打印中看到首先调用 OnRender,然后调用 OnRenderSizeChanged,然后通过调用 this.InvalidateVisual(); 再次将 OnRender 设置为 运行;在覆盖中。我可以得到的所有宽度属性仍然始终为 0,但这很奇怪,因为我可以看到它被渲染并具有大小。还尝试了其他帖子中显示的测量和排列覆盖,但到目前为止还无法得出 0 以外的值。
那么如何在时间轴上动态绘制位置和大小正确的矩形?
抱歉,如果我在这里遗漏了一些明显的东西,我刚刚使用 WPF 工作了一个星期,我没有人可以问。如果您想查看更多代码示例,请告诉我。任何帮助表示赞赏:)。
我只想说,对于刚接触 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 ItemControl 的 ItemsPanel 覆盖为一个简单的 Grid - 我们这样做是为了确保我们所有的矩形都使用相同的原点(以匹配我们的数据),而不是 StackPanel。
接下来,每个事件都有自己的矩形,我们使用 EventLengthConverter 来计算边距(实际上是偏移量)和宽度。我们为多值转换器提供了它需要的一切,时间线持续时间、事件开始或持续时间以及容器宽度。只要这些值之一发生变化,转换器就会被调用。理想情况下,每个矩形在网格中都有一列,您可以将所有这些宽度设置为百分比,但我们失去了数据的动态特性。
优缺点
事件在元素树中是它们自己的对象。您现在可以完全控制如何显示事件。它们不需要只是矩形,它们可以是具有更多行为的复杂对象。至于反对这种方法的原因——我不确定。有人可能会质疑性能,但我无法想象这是一个实际问题。
提示
您可以像以前一样分解这些数据模板,我只是将它们全部包含在一起以便在答案中更容易地查看层次结构。此外,如果您希望转换器的意图更清晰,您可以创建两个,例如 "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);
}
}
}
我正在尝试在 WPF 中绘制时间线。它基本上应该由 3 个矩形组成。
它应该看起来像这样(使用 XAML 硬编码): Timeline
大的白色矩形应该填满所有可用的 space,绿色矩形表示时间线上发生的事件的开始和持续时间。
代表这个的模型是一个 TimeLineEvent class,它有一个 TimeSpan 开始和一个时间跨度持续时间来表示事件何时开始以及持续多长时间(以滴答或秒或其他形式)。还有一个 TimeLine class,它有一个 ObservableCollection,它包含时间轴上的所有事件。它还有一个 TimeSpan 持续时间,表示时间线本身有多长。
我需要做的是能够根据事件的持续时间和开始时间以及它们之间的比率在时间轴上动态绘制事件(绿色矩形),以便绘制对应于事件发生时间和多长时间。时间线上可以有多个事件。
到目前为止,我的方法是创建一个只包含 canvas 元素的 TimeLine.xaml 文件。在代码隐藏文件中,我重写了 OnRender 方法来绘制这些矩形,它使用硬编码值。
在 MainWindow.xaml 中,我创建了一个数据模板并将数据类型设置为 TimeLine:
<DataTemplate x:Key="TimeLineEventsTemplate" DataType="{x:Type local:TimeLine}">
<Border>
<local:TimeLine Background="Transparent"/>
</Border>
</DataTemplate>
为此尝试了不同的设置,但老实说我不确定我在做什么。然后我有一个堆栈面板,其中包含一个列表框,该列表框使用我的数据模板并绑定 TimeLines,这是一个 ObservableCollection,在我的 MainWindow 代码隐藏中包含 TimeLine 对象。
<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>
这会在我创建新的时间轴对象时绘制新的时间轴,如下所示: Timelines
这个问题是它没有正确渲染绿色矩形,为此我需要知道白色矩形的宽度,这样我就可以使用不同持续时间的比率来转换到一个位置. 问题似乎是调用 OnRender 方法时宽度 属性 为 0。我已尝试覆盖 OnRenderSizeChanged,如下所示:In WPF how can I get the rendered size of a control before it actually renders? 我在调试打印中看到首先调用 OnRender,然后调用 OnRenderSizeChanged,然后通过调用 this.InvalidateVisual(); 再次将 OnRender 设置为 运行;在覆盖中。我可以得到的所有宽度属性仍然始终为 0,但这很奇怪,因为我可以看到它被渲染并具有大小。还尝试了其他帖子中显示的测量和排列覆盖,但到目前为止还无法得出 0 以外的值。
那么如何在时间轴上动态绘制位置和大小正确的矩形?
抱歉,如果我在这里遗漏了一些明显的东西,我刚刚使用 WPF 工作了一个星期,我没有人可以问。如果您想查看更多代码示例,请告诉我。任何帮助表示赞赏:)。
我只想说,对于刚接触 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 ItemControl 的 ItemsPanel 覆盖为一个简单的 Grid - 我们这样做是为了确保我们所有的矩形都使用相同的原点(以匹配我们的数据),而不是 StackPanel。
接下来,每个事件都有自己的矩形,我们使用 EventLengthConverter 来计算边距(实际上是偏移量)和宽度。我们为多值转换器提供了它需要的一切,时间线持续时间、事件开始或持续时间以及容器宽度。只要这些值之一发生变化,转换器就会被调用。理想情况下,每个矩形在网格中都有一列,您可以将所有这些宽度设置为百分比,但我们失去了数据的动态特性。
优缺点
事件在元素树中是它们自己的对象。您现在可以完全控制如何显示事件。它们不需要只是矩形,它们可以是具有更多行为的复杂对象。至于反对这种方法的原因——我不确定。有人可能会质疑性能,但我无法想象这是一个实际问题。
提示
您可以像以前一样分解这些数据模板,我只是将它们全部包含在一起以便在答案中更容易地查看层次结构。此外,如果您希望转换器的意图更清晰,您可以创建两个,例如 "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);
}
}
}