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>
これは、それぞれ 2 つと 3 つのイベントを持つ 2 つのタイムラインがある場合に表示されるものです。

説明
ここで最終的に得られるのは、最上位の TimeLine プロパティ用に 1 つ、各タイムラインのイベント用に 1 つ、ネストされた ItemsControls です。TimeLine ItemControl の ItemsPanel を単純な Grid にオーバーライドします。これは、StackPanel ではなく、すべての四角形が (データと一致するように) 同じオリジンを使用するようにするためです。
次に、各イベントは独自の四角形を取得します。これを EventLengthConverter を使用して Margin (事実上のオフセット) と幅を計算します。複数値コンバーターに必要なすべてのもの、タイムラインの期間、イベントの開始または期間、およびコンテナーの幅を指定します。コンバーターは、これらの値のいずれかが変更されるたびに呼び出されます。理想的には、各四角形がグリッド内の列を取得し、これらすべての幅をパーセンテージに設定できますが、データの動的な性質によりその余裕が失われます。
長所と短所
イベントは、要素ツリー内の独自のオブジェクトです。イベントの表示方法を細かく制御できるようになりました。それらは単なる長方形である必要はなく、より多くの動作を持つ複雑なオブジェクトにすることができます。この方法に反対する理由については、よくわかりません。誰かがパフォーマンスについて議論するかもしれませんが、これが実際的な問題であるとは想像できません。
チップ
以前と同じように、これらのデータ テンプレートを分割できます。回答で階層をより簡単に確認できるように、それらをすべてまとめて含めました。また、コンバーターの意図をより明確にしたい場合は、「EventStartConverter」と「EventWidthConverter」のようなものを 2 つ作成し、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);
}
}
}