7

WPF でタイムラインを描画しようとしています。基本的に 3 つの長方形で構成されている必要があります。

次のようになります (XAML を使用してハードコードされています)

大きな白い四角形は利用可能なスペースをすべて埋め、緑の四角形はタイムラインで発生するイベントの開始と期間を表します。

これを表すモデルは、TimeSpan 開始と、イベントの開始時刻と持続時間 (ティックまたは秒など) を表すタイムスパン期間を持つ TimeLineEvent クラスです。タイムライン上のすべてのイベントを保持する ObservableCollection を持つ TimeLine クラスもあります。また、タイムライン自体の長さを表す TimeSpan 期間もあります。

私がする必要があるのは、イベント (緑色の四角形) をタイムライン上で動的に描画できるようにすることです。これは、イベントの継続時間と開始時間、およびこれらの比率に基づいて、イベントがいつ発生し、どのくらいの長さに対応して描画されるようにするかということです。タイムラインには複数のイベントが存在する場合があります。

これまでの私のアプローチは、canvas 要素を保持するだけの TimeLine.xaml ファイルを作成することでした。コード ビハインド ファイルでは、OnRender メソッドをオーバーライドして、これらの四角形を描画します。これは、ハードコードされた値で機能します。

MainWindow.xaml でデータ テンプレートを作成し、データ型を TimeLine に設定しました。

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

このためにさまざまな設定を試しましたが、正直なところ何をしているのかわかりません。次に、データ テンプレートを使用し、TimeLine オブジェクトを保持する ObservableCollection である TimeLines をバインドするリスト ボックスを 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 オブジェクトを作成すると、次のように新しいタイムラインが描画されます

これに関する問題は、緑色の長方形が適切にレンダリングされないことです。これを行うには、白い長方形の幅を知る必要があります。これにより、異なるデュレーションの比率を使用して位置に変換できます。問題は、OnRender メソッドが呼び出されたときに width プロパティが 0 であるようです。ここに示すように、OnRenderSizeChanged をオーバーライドしようとしました: WPF では、実際にレンダリングする前にコントロールのレンダリングされたサイズを取得するにはどうすればよいですか? デバッグ印刷で、OnRender が最初に呼び出され、次に OnRenderSizeChanged が呼び出され、次に this.InvalidateVisual(); を呼び出して OnRender が再度実行されることを確認しました。オーバーライドで。私が得ることができるすべての幅のプロパティはまだ常に 0 ですが、レンダリングされてサイズがあることがわかるので、これは奇妙です。他の投稿に示されているように、メジャーとアレンジのオーバーライドも試しましたが、これまでのところ 0 以外の値を取得できませんでした。

では、正しい位置とサイズでタイムライン上に四角形を動的に描画するにはどうすればよいでしょうか?

ここで明らかな何かが欠けている場合は申し訳ありません。私はWPFを1週間使用していて、尋ねる人がいません。コードサンプルをもっと見たい場合はお知らせください。どんな助けでも大歓迎です:)。

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>

これは、それぞれ 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);
        }
    }
}
于 2016-06-21T18:34:33.770 に答える