简体   繁体   English

如何在WPF中动态绘制时间轴

[英]How to dynamically draw a timeline in WPF

I am trying to draw timelines in WPF. 我想在WPF中绘制时间表。 It should basically consist of 3 rectangles. 它应该基本上由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). 表示此模型的模型是TimeLineEvent类,它具有TimeSpan开始和时间跨度持续时间,以表示事件何时开始以及持续多长时间(以滴答或秒或其他为单位)。 There is also a TimeLine class which has an ObservableCollection which holds all of the events on the timeline. 还有一个TimeLine类,它有一个ObservableCollection,用于保存时间轴上的所有事件。 It also has a TimeSpan duration which represents how long the timeline itself is. 它还有一个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. 到目前为止,我的方法是创建一个仅包含canvas元素的TimeLine.xaml文件。 In the code-behind file I have overriden the OnRender method to draw these rectangles, which works with hardcoded values. 在代码隐藏文件中,我重写了OnRender方法来绘制这些矩形,这些矩形适用于硬编码值。

In the MainWindow.xaml I have created a datatemplate and set the datatype to TimeLine: 在MainWindow.xaml中,我创建了一个datatemplate并将数据类型设置为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. 然后我有一个stackpanel,它包含一个列表框,它使用我的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>

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. 问题似乎是在调用OnRender方法时width属性为0。 I have tried overriding OnRenderSizeChanged, as shown here: In WPF how can I get the rendered size of a control before it actually renders? 我已经尝试重写OnRenderSizeChanged,如下所示: 在WPF中如何在实际渲染之前获取控件的渲染大小? 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(); 我在调试打印中看到OnRender首先被调用,然后是OnRenderSizeChanged然后我通过调用this.InvalidateVisual()来让OnRender再次运行。 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. 我可以得到的所有宽度属性仍然总是0虽然这很奇怪,因为我可以看到它被渲染并具有大小。 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. 还尝试了其他帖子中显示的测量和排列覆盖,但到目前为止还没有能够获得除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. 对不起,如果我遗漏了一些明显的东西,我刚刚和WPF一起工作了一个星期,我没有人问。 Let me know if you would like to see some more code samples. 如果您想查看更多代码示例,请与我们联系。 Any help is appreciated :). 任何帮助表示赞赏:)。

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. 我将提出一个主要针对XAML并使用多值转换器的解决方案。 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 ;)) 与我将解释的其他方法相比,这有利有弊,但这是阻力最小的路径(无论如何努力;))

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: 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>

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. 你最终得到的是嵌套的ItemsControls,一个用于顶级TimeLine属性,一个用于每个时间轴的事件。 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. 我们将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. 接下来,每个事件都有自己的矩形,我们使用EventLengthConverter来计算边距(实际上是偏移量)和宽度。 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. 理想情况下,每个矩形都会在网格中得到一个列,您可以将所有这些宽度设置为百分比,但我们会因数据的动态特性而失去这种奢侈。

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 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);
        }
    }
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM