它应该看起来像这样(使用XAML硬编码):
Timeline
大的白色矩形应填充所有可用空间,绿色矩形表示在时间轴上发生的事件的开始和持续时间.
表示此模型的模型是TimeLineEvent类,它具有TimeSpan开始和时间跨度持续时间,以表示事件何时开始以及持续多长时间(以滴答或秒或其他为单位).还有一个TimeLine类,它有一个ObservableCollection,用于保存时间轴上的所有事件.它还有一个TimeSpan持续时间,表示时间轴本身的长度.
我需要做的是能够根据它们的持续时间和开始动态绘制时间轴上的事件(绿色矩形),以及它们之间的比率,以便绘制与事件发生时间和持续时间相对应的事件.时间轴上可以有多个事件.
到目前为止,我的方法是创建一个仅包含canvas元素的TimeLine.xaml文件.在代码隐藏文件中,我重写了OnRender方法来绘制这些矩形,这些矩形适用于硬编码值.
在MainWindow.xaml中,我创建了一个datatemplate并将数据类型设置为TimeLine:
<DataTemplate x:Key="TimeLineEventsTemplate" DataType="{x:Type local:TimeLine}"> <Border> <local:TimeLine Background="Transparent"/> </Border> </DataTemplate>
尝试了不同的设置,但不确定我要做的是说实话.然后我有一个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>
当我创建新的Timeline对象时,这会绘制新的时间轴,如下所示:
Timelines
这个问题是它没有正确渲染绿色矩形,为此我需要知道白色矩形的宽度,这样我就可以使用不同持续时间的比率转换到一个位置.
问题似乎是在调用OnRender方法时width属性为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布局引擎,然后如果绝对需要开始讨论绘图,特别是因为在确定渲染的内容时遇到的困难和什么不是,什么有宽度,什么没有,等等.
我将提出一个主要针对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); } else { return rval; } } public object[] ConvertBack(object value,Type[] targetTypes,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属性,一个用于每个时间轴的事件.我们将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); first.Events.Add(new TimeLineEvent() { Start = new TimeSpan(0,15,0),Duration = new TimeSpan(0,0) }); first.Events.Add(new TimeLineEvent() { Start = new TimeSpan(0,40,10,0) }); this.TimeLines.Add(first); TimeLine second = new TimeLine(); second.Duration = new TimeSpan(1,0); second.Events.Add(new TimeLineEvent() { Start = new TimeSpan(0,25,0) }); second.Events.Add(new TimeLineEvent() { Start = new TimeSpan(0,30,50,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,value); } } private ObservableCollection<TimeLineEvent> _events = new ObservableCollection<TimeLineEvent>(); public ObservableCollection<TimeLineEvent> Events { get { return _events; } set { Set(() => Events,ref _events,value); } } }