1

我在一个基于 MVVM 的 WPF 应用程序中开发的时间线遇到了一个小障碍。

我的愿望是能够告诉用户希望查看什么“bin”,因为通常有比视图中显示的事件更多的事件。这本质上是一个小的堆栈面板,在代码隐藏中开发并显示在用户选择的 bin 上方。更多细节如下。

基本背景是这样的:

  • 我正在记录 ViewModel 中发生的事件,我将它们称为事件。

  • 我在时间轴上的视图中将它们显示为 50x50 画布(每个事件一个)。这里的细微差别是我的空间非常有限,所以我将显示的实际事件数量限制为略微偏移的 3 个堆栈(想象一下卡片堆叠在一起,显示下方卡片的顶部和右侧) 的每个时间块。

  • 在时间的每个刻度(每 30 秒),画布向左滚动 75 个像素,并且所有绘制的元素也随之移动,当然。这也为事件设置了“垃圾箱”。基本上 0 到 29.9 秒之间的所有内容都在 bin 0 中,30 - 59.9 是 bin 1 等等。

  • 我正在使用链接到负责显示绑定事件的 ItemsControl 的 PreviewMouseLeftButtonDown 事件。

就目前而言,一切正常。我可以收集鼠标点击,我一直在尝试使用一些基本的数学来确定点击了哪组事件。这是基于单击时鼠标的 X 位置并考虑到 ScrollViewer 窗口的任何滚动。不幸的是,我没有得到正确的“bin”。

我尝试过的其他事情:

  • 在 canvas 元素中添加一个标签,其中包含有关它所属的 bin 的信息。这不起作用,因为我得到的代码中的 ItemsSource 包含每个事件,而不仅仅是那些被点击的事件。这就是我访问 ItemsSource 的方式。

    private void ItemsControl_PreviewMouseLeftButtonDown( object sender, MouseButtonEventArds e)
    {
        var control = (ItemsControl)sender;
        var isource = control.ItemsSource;
        Debug.Assert(isource != null);
    }
    
  • 在将其放入项目控件之前,我已尝试将其嵌套在堆栈面板中。这在我有一个嵌套列表时有效,但在我摆脱了列表列表后它就崩溃了。我也不确定这是否是一个可行的选择。

我为此拥有的 XAML(对于那些可能想要查看它的人)如下:

感兴趣的主要 ItemsControl 是最后一个,因为它包含用于在 ScrollViewer 的画布上绘制事件的数据模板。另外两个仅用于我在时间线底部使用的时间戳。

<UserControl 
    x:Class="Removed.Views.TransitionTimeline"
    x:ClassModifier="internal"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" 
    xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
    mc:Ignorable="d" 
    d:DesignHeight="300" d:DesignWidth="300">
  <UserControl.Resources>
    <Style x:Key="ISTCanvasStyle" TargetType="Canvas">
      <Setter Property="Background" Value="Transparent" />
    </Style>

    <Style x:Key="ISTBorderStyle" TargetType="Border">
      <Setter Property="BorderThickness" Value="3" />
      <Setter Property="BorderBrush" Value="{Binding ColorBrush}" />
      <Setter Property="CornerRadius" Value="8" />
      <Setter Property="HorizontalAlignment" Value="Center" />
      <Setter Property="VerticalAlignment" Value="Center" />
      <Setter Property="Background" Value="#FF555555" />
      <Setter Property="Height" Value="50" />
      <Setter Property="Width" Value="50" />
    </Style>

    <Style x:Key="ISTTextBlockStyle" TargetType="TextBlock">
      <Setter Property="Text" Value="{Binding ShortName}" />
      <Setter Property="HorizontalAlignment" Value="Center" />
      <Setter Property="VerticalAlignment" Value="Center" />
      <Setter Property="FontWeight" Value="Bold" />
      <Setter Property="FontSize" Value="40" />
      <Setter Property="Foreground" Value="White" />
      <Setter Property="TextAlignment" Value="Center" />
    </Style>

    <Style x:Key="EventCountTextStyle" TargetType="TextBlock">
      <Setter Property="Text" Value="{Binding ExtraEvents}" />
      <Setter Property="Canvas.Top" Value="-15" />
      <Setter Property="Canvas.Left" Value="-25" />
      <Setter Property="FontWeight" Value="Bold" />
      <Setter Property="FontSize" Value="13" />
      <Setter Property="Foreground" Value="{Binding EventTextColorBrush}" />
      <Setter Property="TextAlignment" Value="Center" />
    </Style>

    <DataTemplate x:Key="IndividualStateTransitions">
      <Canvas Margin="{Binding Margin}" Style="{ StaticResource ISTCanvasStyle}" >
        <Border Canvas.Left="-12.5" Style="{ StaticResource ISTBorderStyle}" >
          <TextBlock Style="{StaticResource ISTTextBlockStyle}" />
        </Border>
        <TextBlock Style="{StaticResource EventCountTextStyle}" />
      </Canvas>
    </DataTemplate>

    <DataTemplate x:Key="IST">
      <ItemsControl
          ItemsSource="{Binding}" 
          ItemTemplate="{StaticResource IndividualStateTransitions}"
          PreviewMouseLeftButtonDown="ItemsControl_PreviewMouseLeftButtonDown"
          Width="75">
        <ItemsControl.ItemsPanel>
          <ItemsPanelTemplate>
            <Canvas />
          </ItemsPanelTemplate>
        </ItemsControl.ItemsPanel>
        <ItemsControl.ItemContainerStyle>
          <Style TargetType="ContentPresenter">
            <Setter Property="Canvas.ZIndex" Value="{Binding ZIndex}" />
          </Style>
        </ItemsControl.ItemContainerStyle>
      </ItemsControl>
    </DataTemplate>

    <DataTemplate x:Key="BottomTimeBar">
      <Canvas>
        <Line X1="{Binding DashX}" X2="{Binding DashX}" Y1="100" Y2="0" Stroke="#FF646464" StrokeThickness="1" StrokeDashArray="5" StrokeDashCap="Round" />
        <TextBlock Canvas.ZIndex="-999"  Width="50" TextAlignment="Center" Text="{Binding TimerText}" Canvas.Left="{Binding BlockLeft}" 
                       Canvas.Top="85" Foreground="White" Background="#FF444444" FontSize="13" />
      </Canvas>
    </DataTemplate>

  </UserControl.Resources>
  <Grid Name="TimelineGrid" Height="192">
    <Grid.RowDefinitions>
      <RowDefinition Height="92" />
      <RowDefinition Height="100" />
    </Grid.RowDefinitions>

    <Canvas Grid.Row="0" Width="1920" Height="100">
      <ScrollViewer            
          Width="1920"
          Height="100"
          Name="_timelineScrollViewer2" 
          CanContentScroll="True" 
          Background="Transparent"
          HorizontalScrollBarVisibility="Hidden" 
          VerticalScrollBarVisibility="Hidden">
        <Canvas Width="9999" Name="_timelineCanvas2">
        </Canvas>
      </ScrollViewer>
    </Canvas>

    <Canvas Grid.Row="1" Name="MainCanvas" Width="1920" Height="100" >
      <Line X1="0" X2="1920" Y1="0" Y2="0" Stroke="#FFD0D0D0" StrokeThickness="3">
        <Line.Effect>
          <DropShadowEffect BlurRadius="3" ShadowDepth="3" />
        </Line.Effect>
      </Line>
      <Line X1="0" X2="1920" Y1="55" Y2="55" Stroke="#FFD0D0D0" StrokeThickness="3">
        <Line.Effect>
          <DropShadowEffect BlurRadius="3" ShadowDepth="3" />
        </Line.Effect>
      </Line>
      <ScrollViewer            
          Width="1920"
          Height="100"
          Name="_timelineScrollViewer" 
          CanContentScroll="True" 
          Background="Transparent"
          HorizontalScrollBarVisibility="Hidden" 
          VerticalScrollBarVisibility="Hidden"
          PreviewMouseLeftButtonDown="TimelineScrollViewerLeftMouseDown" 
          PreviewMouseLeftButtonUp="LeftMouseUp" 
          PreviewMouseMove="TimelineScrollViewerMouseMove"
          PreviewMouseWheel="TimelineScrollViewerPreviewMouseWheel"
          ScrollChanged="OnTimelineScrollChanged" ClipToBounds="False">
        <Canvas Width="9999" Name="_timelineCanvas">
          <Line Canvas.ZIndex="-1000" Name="CurrentTimeLine" X1="1280" X2="1280" Y1="0" Y2="100" Stroke="#FFD0D0D0" StrokeThickness="3">
            <Line.Effect>
              <DropShadowEffect BlurRadius="3" ShadowDepth="3" />
            </Line.Effect>
          </Line>
          <ItemsControl ItemsSource="{Binding BottomTimeBarData}" ItemTemplate="{StaticResource BottomTimeBar}" Canvas.Left="{Binding LeftScroll}">
            <ItemsControl.ItemsPanel>
              <ItemsPanelTemplate>
                <Canvas />
              </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
          </ItemsControl>
          <ItemsControl ItemsSource="{Binding BottomTimeBarDataPast}" ItemTemplate="{StaticResource BottomTimeBar}" Canvas.Left="{Binding LeftScroll}">
            <ItemsControl.ItemsPanel>
              <ItemsPanelTemplate>
                <Canvas />
              </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
          </ItemsControl>

          <ItemsControl ItemsSource="{Binding DisplayObject}" ItemTemplate="{StaticResource IndividualStateTransitions}" Canvas.Left="{Binding LeftScroll}" Canvas.Top="15" Margin="0.0, 25.5"
                        PreviewMouseLeftButtonDown="ItemsControl_PreviewMouseLeftButtonDown" >
            <ItemsControl.ItemsPanel>
              <ItemsPanelTemplate>
                <StackPanel />
              </ItemsPanelTemplate>
            </ItemsControl.ItemsPanel>
            <ItemsControl.ItemContainerStyle>
              <Style TargetType="ContentPresenter">
                <Setter Property="Canvas.ZIndex" Value="{Binding ZIndex}" />
              </Style>
            </ItemsControl.ItemContainerStyle>
          </ItemsControl>
        </Canvas>
      </ScrollViewer>
    </Canvas>
  </Grid>
</UserControl>

如果您有任何问题,请告诉我。我很乐意澄清发生了什么。最终我正在寻找的是一种确定用户希望查看的“bin”的方法。

更新 1:有关绘制对象的附加信息。

我已经定义了自己的内部类,因此我可以将它用作我的 ObservableCollection 的数据类型,这就是在这种情况下绑定的内容。

internal class DisplayObjects : INotifyPropertyChanged
{
    private int _elementbin;
    public int ElementBin
    {
        get { return _elementbin; }
        set
        {
            _elementbin = value;

            if ( PropertyChanged != null )
                PropertyChanged( this, new PropertyChangedEventArgs( "ElementBin" ) );
        }
    }

    private string _shortName;
    public string ShortName
    {
        get { return _shortName; }
        set
        {
            _shortName = value;

            if ( PropertyChanged != null )
                PropertyChanged( this, new PropertyChangedEventArgs( "ShortName" ) );
        }
    }

    private string _margin;
    public string Margin
    {
        get { return _margin; }
        set
        {
            _margin = value;

            if ( PropertyChanged != null )
                PropertyChanged( this, new PropertyChangedEventArgs( "Margin" ) );
        }
    }

    private string _extraevents;
    public string ExtraEvents
    {
        get { return _extraevents; }
        set
        {
            _extraevents = value;

            if ( PropertyChanged != null )
                PropertyChanged( this, new PropertyChangedEventArgs( "ExtraEvents" ) );
        }
    }

    public Color EventTextColor { get; set; }

    public SolidColorBrush EventTextColorBrush
    {
        get
        {
            return new SolidColorBrush( EventTextColor );
        }
    }

    private int _zindex;
    public int ZIndex
    {
        get { return _zindex; }
        set
        {
            _zindex = value;

            if ( PropertyChanged != null )
                PropertyChanged( this, new PropertyChangedEventArgs( "ZIndex" ) );
        }
    }

    public Color Color { get; set; }

    public SolidColorBrush ColorBrush
    {
        get { return new SolidColorBrush( Color ); }
    }

    public event PropertyChangedEventHandler PropertyChanged;
}

我订阅了一个事件聚合器,因此我可以在事件发送时捕获它们。这触发了我的OnEventReceived方法。关于此方法,您需要了解的只是它从接收到的事件中获取信息,并根据事件发生的时间确定它应该落入哪个 bin。这会触发对我的DrawObject方法的调用。

如果您想尝试模拟这一点,您可以用一些硬编码的东西替换 stvm 的使用。我无法为此提供实际数据。stvm.Color 只是表示事件类型的颜色。stvm.ShortName 只是一个字母。_bins 只是一个List<Int>跟踪相应 bin 中发生的事件总数的方法。 position只是它所属的 bin 中的数字事件。因此,如果这是第一个事件,那将是 1。如果这是第十个事件,它将是 10。

这里的FindLastVisualChild方法只是为了返回给定 bin 的第三个 DisplayObject 的位置索引。这使我可以更新显示“+ X more”的标签。堆栈中的事件。

private void DrawObject( EventViewModel stvm, int position, int bin )
{
    var margin = "";
    var zindex = 0;
    var left = bin * 75.0;
    var textColor = Colors.Transparent;
    var extraEvents = "+ ";
    switch ( position )
    {
        case 1:
            left += 12.5;
            margin = left + ", -5";
            zindex = 3;
            DisplayObject.Add( new DisplayObjects
            {
                Color = stvm.Color,
                ShortName = stvm.ShortName,
                Margin = margin,
                ZIndex = zindex,
                EventTextColor = textColor,
                ExtraEvents = extraEvents,
                ElementBin = bin
            } );
            break;
        case 2:
            left += 22.5;
            margin = left + ", -15";
            zindex = 2;
            DisplayObject.Add( new DisplayObjects
            {
                Color = stvm.Color,
                ShortName = stvm.ShortName,
                Margin = margin,
                ZIndex = zindex,
                EventTextColor = textColor,
                ExtraEvents = extraEvents,
                ElementBin = bin
            } );
            break;
        case 3:
            left += 32.5;
            margin = left + ", -25";
            zindex = 1;
            DisplayObject.Add( new DisplayObjects 
            { 
                Color = stvm.Color, 
                ShortName = stvm.ShortName, 
                Margin = margin, 
                ZIndex = zindex, 
                EventTextColor = textColor, 
                ExtraEvents = extraEvents,
                ElementBin = bin
            } );
            break;
        default:
            //left += 32.5;
            //margin = left + ", -25";
            //DisplayObject.Add( new DisplayObjects { Color = stvm.Color, ShortName = stvm.ShortName, Margin = margin, ZIndex = zindex, EventTextColor = textColor, ExtraEvents = extraEvents } );
            extraEvents += ( _bins[bin] - 3 ) + " more.";
            var test = FindLastVisualChild( bin );
            DisplayObject[test].EventTextColor = Colors.White;
            DisplayObject[FindLastVisualChild( bin )].ExtraEvents = extraEvents;
            break;
    }
}

private int FindLastVisualChild( int bin )
{
    var sum = 0;
    for ( var idx = 0; idx <= bin; idx++ )
        if ( _bins[idx] <= 3 )
            sum += _bins[idx];
        else
            sum += 3;
    return ( sum - 1 );
}
4

2 回答 2

1

你试过在这里使用e.OriginalSource吗?

我显然无法重现您的项目,但据我猜测,您应该能够通过使用以下方法获得单击的 Canvas:

var canvas = e.OriginalSource as Canvas

然后你应该能够轻松地获得相应的项目,因为我理解它是画布的 DataContext:

var item = canvas.DataContext as MyItemViewModel;

但正如我所写,这是猜测。您应该发布更多信息,以便我们在您的应用中更清楚地看到,例如您将项目添加到集合中的代码。

于 2012-07-19T13:22:01.487 回答
0

在这里非常感谢 David 和 Rachel。他们的意见使我得到了我终于开始工作的答案。

就像任何在此之后出现并阅读它的人的参考。这就是我所做的。

IndividualStateTransistions我在Tag = "{Binding ElementBin}"画布上添加的数据模板中。然后,我在将事件过滤到 bin 的代码中添加了该元素所属的 bin 编号。

根据 Rachel 的建议,我也将鼠标单击事件移至此 Canvas。

在后面的代码中,我改编了大卫建议的内容:

在 OnMouseClickEvent 方法中...

var control = (Canvas)sender;
var item = control.DataContext as DisplayObjects;
var itembin = item.ElementBin;

这为我提供了该组事件所属的 bin,并允许我继续下一步。

再次感谢伙计们(和女孩们)!

于 2012-07-19T14:11:49.840 回答