如何在 WPF 中动态绘制时间线
声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow
原文地址: http://stackoverflow.com/questions/37949599/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me):
StackOverFlow
How to dynamically draw a timeline in WPF
提问by Magnus Brantheim
I am trying to draw timelines in WPF. It should basically consist of 3 rectangles.
我正在尝试在 WPF 中绘制时间线。它应该基本上由 3 个矩形组成。
It should look something like this (hardcoded using XAML): Timeline
它应该看起来像这样(使用 XAML 硬编码): 时间轴
The large white rectangle should fill all of the available space, the green rectangles represent the start and duration of events which happen on the timeline.
大的白色矩形应该填满所有可用空间,绿色矩形代表时间线上发生的事件的开始和持续时间。
The models representing this is a TimeLineEvent class which has a TimeSpan start and a timespan duration to represent when the event starts and how long it lasts (in ticks or seconds or whatever). There is also a TimeLine class which has an ObservableCollection which holds all of the events on the timeline. It also has a TimeSpan duration which represents how long the timeline itself is.
代表这一点的模型是一个 TimeLineEvent 类,它有一个 TimeSpan 开始和一个时间跨度持续时间来表示事件何时开始以及它持续多长时间(以滴答声或秒数或其他形式)。还有一个 TimeLine 类,它有一个 ObservableCollection 来保存时间线上的所有事件。它还有一个 TimeSpan 持续时间,表示时间线本身的长度。
What I need to do is to be able to dynamically draw the events (green rectangles) on the timeline based on their duration and start, and the ratios between these so that an event is drawn corresponding to when it occurs and for how long. There can be more than one event on a timeline.
我需要做的是能够根据事件的持续时间和开始时间以及它们之间的比率在时间线上动态绘制事件(绿色矩形),以便根据事件发生的时间和持续时间绘制事件。一个时间线上可以有多个事件。
My approach so far has been to make a TimeLine.xaml file which just holds a canvas element. In the code-behind file I have overriden the OnRender method to draw these rectangles, which works with hardcoded values.
到目前为止,我的方法是制作一个仅包含画布元素的 TimeLine.xaml 文件。在代码隐藏文件中,我覆盖了 OnRender 方法来绘制这些矩形,该方法适用于硬编码值。
In the MainWindow.xaml I have created a datatemplate and set the datatype to TimeLine:
在 MainWindow.xaml 中,我创建了一个数据模板并将数据类型设置为 TimeLine:
<DataTemplate x:Key="TimeLineEventsTemplate" DataType="{x:Type local:TimeLine}">
<Border>
<local:TimeLine Background="Transparent"/>
</Border>
</DataTemplate>
Have tried different settings for this, but not sure what I am doing to be honest. I then have a stackpanel which contains a listbox that is using my datatemplate and binding TimeLines, which is an ObservableCollection holding TimeLine objects, in my MainWindow code-behind.
为此尝试了不同的设置,但老实说我不确定我在做什么。然后我有一个堆栈面板,其中包含一个列表框,该列表框使用我的数据模板和绑定 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>
This draws new timelines when I create new Timeline objects, looking like this: Timelines
这吸引新的时间表,当我创建新的时间线的对象,看起来像这样: 时间表
The problem with this is that it does not render the green rectangles properly, to do this I need to know the width of the white rectangle, so that I can use the ratios of the different duration to translate to a position. The problem seems to be that the width property is 0 when the OnRender method is called. I have tried overriding OnRenderSizeChanged, as shown here: In WPF how can I get the rendered size of a control before it actually renders?I have seen in my debug printing that OnRender first gets called, then OnRenderSizeChanged and then I get the OnRender to run again by calling this.InvalidateVisual(); in the override. All the width properties I can get out are still always 0 though which is strange because I can see that it gets rendered and has a size. Have also tried the Measure and Arrange overrides as shown in other posts but have not been able to get out a value other than 0 so far.
这样做的问题是它没有正确渲染绿色矩形,为此我需要知道白色矩形的宽度,以便我可以使用不同持续时间的比率来转换为一个位置。问题似乎是在调用 OnRender 方法时宽度属性为 0。我已经尝试覆盖 OnRenderSizeChanged,如下所示:在 WPF 中,如何在控件实际呈现之前获取控件的呈现大小?我在调试打印中看到,首先调用 OnRender,然后调用 OnRenderSizeChanged,然后通过调用 this.InvalidateVisual(); 使 OnRender 再次运行;在覆盖中。我可以得到的所有宽度属性仍然总是 0 虽然这很奇怪,因为我可以看到它被渲染并具有大小。还尝试了其他帖子中所示的 Measure 和Arrange 覆盖,但到目前为止还无法获得 0 以外的值。
So how can I dynamically draw rectangles on the timeline with correct position and size?
那么如何在时间轴上以正确的位置和大小动态绘制矩形呢?
Sorry if I am missing something obvious here, I have just been working with WPF for a week now and I don't have anyone to ask. Let me know if you would like to see some more code samples. Any help is appreciated :).
对不起,如果我在这里遗漏了一些明显的东西,我刚刚与 WPF 一起工作了一个星期,我没有人要问。如果您想查看更多代码示例,请告诉我。任何帮助表示赞赏:)。
回答by plast1k
Let me just say that for someone who is new to WPF you seem to have a good handle on things.
我只想说,对于 WPF 的新手来说,您似乎对事情有很好的把握。
Anyway, this may be a personal preference, but I usually try to leverage the WPF layout engine as much as possible first, then if absolutely required start poking around with drawing things, specifically because of the difficulties you ran into when determining what is rendered and what isn't, what has a width yet and what doesn't, etc.
无论如何,这可能是个人偏好,但我通常会尝试首先尽可能多地利用 WPF 布局引擎,然后如果绝对需要开始绘制事物,特别是因为您在确定呈现的内容和什么不是,什么有宽度,什么没有,等等。
I'm going to propose a solution sticking mostly to XAML and making use of a multi value converter. There are pros and cons to this compared to other methods which I'll explain, but this was the path of least resistance (for effort anyway ;))
我将提出一个主要坚持 XAML 并使用多值转换器的解决方案。与我将解释的其他方法相比,这有利有弊,但这是阻力最小的路径(无论如何努力;))
Code
代码
EventLengthConverter.cs:
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:
主窗口.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>
Here is what I see when there are two Timelines with two and three events, respectively.

Explanation
解释
What you end up with here is nested ItemsControls, one for the top level TimeLine property and one for each timeline's Events. We override the TimeLine ItemControl's ItemsPanel to a simple Grid - we do this to make sure that all of our rectangles use the same origin (to match our data), rather than say a StackPanel.
你最终得到的是嵌套的 ItemsControls,一个用于顶级 TimeLine 属性,一个用于每个时间线的事件。我们将 TimeLine ItemControl 的 ItemsPanel 覆盖为一个简单的 Grid - 我们这样做是为了确保我们所有的矩形都使用相同的原点(以匹配我们的数据),而不是说一个 StackPanel。
Next, each event gets its own rectangle, which we use the EventLengthConverter to calculate the Margin (effectively the offset) and the width. We give the multivalue converter everything it needs, the Timelines Duration, the events Start or Duration, and the container width. The converter will get called anytime one of these values changes. Ideally each rectangle would get a column in the grid and you could just set all of these widths to percentages, but we lose that luxury with the dynamic nature of the data.
接下来,每个事件都有自己的矩形,我们使用 EventLengthConverter 来计算边距(实际上是偏移量)和宽度。我们为多值转换器提供了它需要的一切,时间线持续时间,事件开始或持续时间,以及容器宽度。只要这些值之一发生变化,就会调用转换器。理想情况下,每个矩形都会在网格中获得一列,您可以将所有这些宽度设置为百分比,但由于数据的动态特性,我们失去了这种奢侈。
Pros and Cons
利弊
Events are their own objects in the element tree. You have a ton of control now over how you display events. They don't need to just be rectangles, they can be complex objects with more behavior. As far as reasons against this method - I'm not sure. Someone might argue with performance but I can't imagine this being a practical concern.
事件是元素树中它们自己的对象。您现在可以对显示事件的方式进行大量控制。它们不需要只是矩形,它们可以是具有更多行为的复杂对象。至于反对这种方法的原因 - 我不确定。有人可能会与性能争论,但我无法想象这是一个实际问题。
Tips
提示
You can break these data templates out like you had before, I just included them all together to see the hierarchy more easily in the answer. Also, if you'd like the intent of the converter to be clearer you could create two, something like "EventStartConverter" and "EventWidthConverter", and ditch the check against targetType.
您可以像以前一样拆分这些数据模板,我只是将它们全部包含在一起,以便在答案中更轻松地查看层次结构。此外,如果您希望转换器的意图更清晰,您可以创建两个,例如“EventStartConverter”和“EventWidthConverter”,并放弃对 targetType 的检查。
EDIT:
编辑:
MainViewModel.cs
主视图模型.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);
}
}
}

