7

我正在尝试在 WPF 中绘制时间线。它应该基本上由3个矩形组成。

它应该看起来像这样(使用 XAML 硬编码): 时间轴

大的白色矩形应该填充所有可用空间,绿色矩形代表时间线上发生的事件的开始和持续时间。

代表这一点的模型是一个 TimeLineEvent 类,它有一个 TimeSpan 开始和一个时间跨度持续时间来表示事件何时开始以及持续多长时间(以滴答声或秒数或其他单位)。还有一个 TimeLine 类,它有一个 ObservableCollection,它保存时间线上的所有事件。它还有一个 TimeSpan 持续时间,表示时间线本身有多长。

我需要做的是能够根据事件的持续时间和开始以及它们之间的比率在时间轴上动态绘制事件(绿色矩形),以便根据事件发生的时间和持续时间绘制事件。时间线上可以有多个事件。

到目前为止,我的方法是制作一个仅包含一个画布元素的 TimeLine.xaml 文件。在代码隐藏文件中,我重写了 OnRender 方法来绘制这些矩形,它适用于硬编码值。

在 MainWindow.xaml 我创建了一个数据模板并将数据类型设置为 TimeLine:

<DataTemplate x:Key="TimeLineEventsTemplate" DataType="{x:Type local:TimeLine}">
        <Border>
            <local:TimeLine Background="Transparent"/>
        </Border>
    </DataTemplate>

为此尝试了不同的设置,但说实话我不确定我在做什么。然后,我有一个堆栈面板,其中包含一个列表框,该列表框使用我的数据模板和绑定 TimeLines,这是一个包含 TimeLine 对象的 ObservableCollection,在我的 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>

当我创建新的时间线对象时,这会绘制新的时间线,如下所示: 时间线

这样做的问题是它没有正确渲染绿色矩形,为此我需要知道白色矩形的宽度,以便我可以使用不同持续时间的比率来转换到一个位置。问题似乎是调用 OnRender 方法时 width 属性为 0。我尝试过覆盖 OnRenderSizeChanged,如下所示:在 WPF 中,如何在控件实际呈现之前获取控件的呈现大小? 我在调试打印中看到首先调用 OnRender,然后调用 OnRenderSizeChanged,然后通过调用 this.InvalidateVisual(); 让 OnRender 再次运行;在覆盖中。我可以得到的所有宽度属性仍然始终为 0,但这很奇怪,因为我可以看到它被渲染并具有大小。还尝试了其他帖子中显示的 Measure 和 Arrange 覆盖,但到目前为止还没有得到除 0 以外的值。

那么如何在时间轴上以正确的位置和大小动态绘制矩形呢?

抱歉,如果我在这里遗漏了一些明显的东西,我刚刚使用 WPF 工作了一周,我没有人可以问。如果您想查看更多代码示例,请告诉我。任何帮助表示赞赏:)。

4

1 回答 1

14

我只想说,对于刚接触 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);
        }
    }
}
于 2016-06-21T18:34:33.770 回答