Wpf Live-Charts 显示工具提示基于鼠标光标移动而鼠标悬停在折线图点上

Wpf Live-Charts display tooltip based on mouse cursor move without mouse hover over Line chart point

我正在使用 WPF 实时图表 (https://lvcharts.net)
我想让工具提示根据鼠标光标移动显示点值,如下图link。
我试过了,但我还没有找到无需将鼠标光标悬停在实时图表中的点上即可显示工具提示的方法。

示例:

如果有人做过,能给点建议吗?

解决方法比较简单。 LiveCharts 的问题在于,它没有得到很好的记录。它通过提供一些针对一般要求的示例让您轻松入门。但对于高级场景,默认控件无法提供足够的灵活性来自定义行为或布局。没有关于工作原理或库的 类 用途的详细信息的文档。

检查实施细节后,我发现控件的编写或设计非常糟糕。

无论如何,您请求的这个简单功能是该库缺点的一个很好的例子——可扩展性真的很差。甚至定制也很糟糕。我希望作者会允许使用模板,因为这会使自定义 很多 更容易。扩展现有行为应该很简单,但显然不是,除非您了解未记录的实现细节。
该库不像真正的 WPF 库。我不知道历史,也许它是 WinForms 开发人员的 WinForms 端口。 但它是免费和开源的。这是一个很大的优势。


以下示例在绘图区域上绘制一个光标,该光标捕捉到最近的图表点并突出显示它,同时鼠标正在移动。 自定义工具提示跟随鼠标指针显示有关当前选定图表点的信息:

ViewModel.cs

public class ViewModel : INotifyPropertyChanged
{
  public ViewModel()
  {
    var chartValues = new ChartValues<Point>();
    
    // Create a sine
    for (double x = 0; x < 361; x++)
    {          
      var point = new Point() {X = x, Y = Math.Sin(x * Math.PI / 180)}; 
      chartValues.Add(point);
    }

    SeriesCollection = new SeriesCollection
    {          
      new LineSeries
      {
        Configuration = new CartesianMapper<Point>()
          .X(point => point.X)
          .Y(point => point.Y),
        Title = "Series X",
        Values = chartValues,
        Fill = Brushes.DarkRed
      }
    };
  }

  private ChartPoint selectedChartPoint;
  public ChartPoint SelectedChartPoint
  {
    get => this.selectedChartPoint;
    set
    {
      this.selectedChartPoint = value;
      OnPropertyChanged();
    }
  }

  private double cursorScreenPosition;
  public double CursorScreenPosition
  {
    get => this.cursorScreenPosition;
    set
    {
      this.cursorScreenPosition = value;
      OnPropertyChanged();
    }
  }

  public SeriesCollection SeriesCollection { get; set; }

  public event PropertyChangedEventHandler PropertyChanged;
  protected virtual void OnPropertyChanged([CallerMemberName] string propertyName = null)
  {
    this.PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
  }
}

MainWindow.xaml.cs

partial class MainWindow : Window
{ 
  private void MoveChartCursorAndToolTip_OnMouseMove(object sender, MouseEventArgs e)
  {
    var chart = sender as CartesianChart;

    if (!TryFindVisualChildElement(chart, out Canvas outerCanvas) ||
        !TryFindVisualChildElement(outerCanvas, out Canvas graphPlottingArea))
    {
      return;
    }
    
    var viewModel = this.DataContext as ViewModel;
    Point chartMousePosition = e.GetPosition(chart);

    // Remove visual hover feedback for previous point
    viewModel.SelectedChartPoint?.View.OnHoverLeave(viewModel.SelectedChartPoint);

    // Find current selected chart point for the first x-axis
    Point chartPoint = chart.ConvertToChartValues(chartMousePosition);
    viewModel.SelectedChartPoint = chart.Series[0].ClosestPointTo(chartPoint.X, AxisOrientation.X);

    // Show visual hover feedback for previous point
    viewModel.SelectedChartPoint.View.OnHover(viewModel.SelectedChartPoint);

    // Add the cursor for the x-axis.
    // Since Chart internally reverses the screen coordinates
    // to match chart's coordinate system
    // and this coordinate system orientation applies also to Chart.VisualElements,
    // the UIElements like Popup and Line are added directly to the plotting canvas.
    if (chart.TryFindResource("CursorX") is Line cursorX 
      && !graphPlottingArea.Children.Contains(cursorX))
    {
      graphPlottingArea.Children.Add(cursorX);
    }

    if (!(chart.TryFindResource("CursorXToolTip") is FrameworkElement cursorXToolTip))
    {
      return;
    }

    // Add the cursor for the x-axis.
    // Since Chart internally reverses the screen coordinates
    // to match chart's coordinate system
    // and this coordinate system orientation applies also to Chart.VisualElements,
    // the UIElements like Popup and Line are added directly to the plotting canvas.
    if (!graphPlottingArea.Children.Contains(cursorXToolTip))
    {
      graphPlottingArea.Children.Add(cursorXToolTip);
    }

    // Position the ToolTip
    Point canvasMousePosition = e.GetPosition(graphPlottingArea);
    Canvas.SetLeft(cursorXToolTip, canvasMousePosition.X - cursorXToolTip.ActualWidth);
    Canvas.SetTop(cursorXToolTip, canvasMousePosition.Y);      
  }

  // Helper method to traverse the visual tree of an element
  private bool TryFindVisualChildElement<TChild>(DependencyObject parent, out TChild resultElement)
    where TChild : DependencyObject
  {
    resultElement = null;
    for (var childIndex = 0; childIndex < VisualTreeHelper.GetChildrenCount(parent); childIndex++)
    {
      DependencyObject childElement = VisualTreeHelper.GetChild(parent, childIndex);

      if (childElement is Popup popup)
      {
        childElement = popup.Child;
      }

      if (childElement is TChild)
      {
        resultElement = childElement as TChild;
        return true;
      }

      if (TryFindVisualChildElement(childElement, out resultElement))
      {
        return true;
      }
    }

    return false;
  }
}

MainWindow.xaml

<Window>
  <Window.DataComtext>
    <ViewModel />
  </Window.DataContext>

  <CartesianChart MouseMove="MoveChartCursorAndToolTip_OnMouseMove"
                  Series="{Binding SeriesCollection}"
                  Zoom="X"
                  Height="600">
    <CartesianChart.Resources>

      <!-- The cursor for the x-axis that snaps to the nearest chart point -->
      <Line x:Key="CursorX"
            Canvas.ZIndex="2"
            Canvas.Left="{Binding SelectedChartPoint.ChartLocation.X}"
            Y1="0"
            Y2="{Binding ElementName=CartesianChart, Path=ActualHeight}"
            Stroke="Gray"
            StrokeThickness="1" />

      <!-- The ToolTip that follows the mouse pointer-->
      <Border x:Key="CursorXToolTip"
              Canvas.ZIndex="3"
              Background="LightGray"
              Padding="8"
              CornerRadius="8">
        <StackPanel Background="LightGray">
          <StackPanel Orientation="Horizontal">
            <Path Height="20" Width="20"
                  Stretch="UniformToFill"
                  Data="{Binding SelectedChartPoint.SeriesView.(Series.PointGeometry)}"
                  Fill="{Binding SelectedChartPoint.SeriesView.(Series.Fill)}"
                  Stroke="{Binding SelectedChartPoint.SeriesView.(Series.Stroke)}"
                  StrokeThickness="{Binding SelectedChartPoint.SeriesView.(Series.StrokeThickness)}" />

            <TextBlock Text="{Binding SelectedChartPoint.SeriesView.(Series.Title)}"
                       VerticalAlignment="Center" />
          </StackPanel>
          <TextBlock Text="{Binding SelectedChartPoint.X, StringFormat=X:{0}}" />
          <TextBlock Text="{Binding SelectedChartPoint.Y, StringFormat=Y:{0}}" />
        </StackPanel>
      </Border>
    </CartesianChart.Resources>
    <CartesianChart.AxisY>
      <Axis Title="Y" />
    </CartesianChart.AxisY>
    <CartesianChart.AxisX>
      <Axis Title="X" />
    </CartesianChart.AxisX>
  </CartesianChart>
<Window>