OxyPlot:松开左键时保持跟踪器打开
OxyPlot: Keep tracker open when left button released
我正在使用带有 C# 和 WPF 的 OxyPlot 2014.1.546。
我的情节有一个自定义跟踪器,当用户单击一个点时会出现。我想包含用于执行与单击点相关的操作的按钮。将它们添加到跟踪器模板非常简单;问题是,默认情况下,一旦用户释放鼠标按钮,跟踪器就会消失,这意味着不可能真正点击它们。
有什么方法可以告诉 OxyPlot 保持跟踪器打开直到用户点击它以外的地方吗?
简短的回答是 OxyPlot 似乎并不直接支持这种行为。在花了一些时间研究反编译的源代码后,我想出了以下解决方案,似乎可行。基本想法是从 OxyPlot 的 built-in TrackerManipulator
派生出我自己的 StayOpenTrackerManipulator
并实例化它以响应点击。我的操纵器覆盖了虚拟 Completed()
函数,当鼠标按钮被释放时框架调用该函数,并将调用推迟到关闭跟踪器的 base-class Completed()
,直到下一次单击鼠标(或直到绘图被修改,或直到鼠标离开)。因为我使用的是 C# 和 WPF,所以我将所有内容都包装在一个附加行为中,可以从 XAML 中使用,如下所示:
<PlotView behaviors:ShowTrackerAndLeaveOpenBehavior.BindToMouseDown="Left" />
但如果需要的话,将内脏取出并以不同的方式重新使用它们会很简单。这是来源:
/// <summary>
/// Normal OxyPlot behavior is to show the tracker when the bound mouse button is pressed,
/// and hide it again when the button is released. With this behavior set, the tracker will stay open
/// until the user clicks the plot outside it (or the plot is modified).
/// </summary>
public static class ShowTrackerAndLeaveOpenBehavior
{
public static readonly DependencyProperty BindToMouseDownProperty = DependencyProperty.RegisterAttached(
"BindToMouseDown", typeof(OxyMouseButton), typeof(ShowTrackerAndLeaveOpenBehavior),
new PropertyMetadata(default(OxyMouseButton), OnBindToMouseButtonChanged));
[AttachedPropertyBrowsableForType(typeof(IPlotView))]
public static void SetBindToMouseDown(DependencyObject element, OxyMouseButton value) =>
element.SetValue(BindToMouseDownProperty, value);
[AttachedPropertyBrowsableForType(typeof(IPlotView))]
public static OxyMouseButton GetBindToMouseDown(DependencyObject element) =>
(OxyMouseButton) element.GetValue(BindToMouseDownProperty);
private static void OnBindToMouseButtonChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is IPlotView plot))
throw new InvalidOperationException($"Can only be applied to {nameof(IPlotView)}");
if (plot.ActualModel == null)
throw new InvalidOperationException("Plot has no model");
var controller = plot.ActualController;
if (controller == null)
throw new InvalidOperationException("Plot has no controller");
if (e.OldValue is OxyMouseButton oldButton && oldButton != OxyMouseButton.None)
controller.UnbindMouseDown(oldButton);
var newButton = GetBindToMouseDown(d);
if (newButton == OxyMouseButton.None)
return;
controller.UnbindMouseDown(newButton);
controller.BindMouseDown(newButton, new DelegatePlotCommand<OxyMouseDownEventArgs>(
AddStayOpenTrackerManipulator));
}
private static void AddStayOpenTrackerManipulator(IPlotView view, IController controller,
OxyMouseDownEventArgs e)
{
controller.AddMouseManipulator(view, new StayOpenTrackerManipulator(view), e);
}
private class StayOpenTrackerManipulator : TrackerManipulator
{
private readonly PlotModel _plotModel;
private bool _isTrackerOpen;
public StayOpenTrackerManipulator(IPlotView plot)
: base(plot)
{
_plotModel = plot?.ActualModel ?? throw new ArgumentException("Plot has no model", nameof(plot));
Snap = true;
PointsOnly = false;
}
public override void Started(OxyMouseEventArgs e)
{
_plotModel.TrackerChanged += HandleTrackerChanged;
base.Started(e);
}
public override void Completed(OxyMouseEventArgs e)
{
if (!_isTrackerOpen)
{
ReallyCompleted(e);
}
else
{
// Completed() is called as soon as the mouse button is released.
// We won't call the base Completed() here since that would hide the tracker.
// Instead, defer the call until one of the hooked events occurs.
// The caller will still remove us from the list of active manipulators as soon as we return,
// but that's good; otherwise the tracker would continue to move around as the mouse does.
new DeferredCompletedCall(_plotModel, () => ReallyCompleted(e)).HookUp();
}
}
private void ReallyCompleted(OxyMouseEventArgs e)
{
base.Completed(e);
// Must unhook or this object will live as long as the model (instead of as long as the manipulation)
_plotModel.TrackerChanged -= HandleTrackerChanged;
}
private void HandleTrackerChanged(object sender, TrackerEventArgs e) =>
_isTrackerOpen = e.HitResult != null;
/// <summary>
/// Monitors events that should trigger manipulator completion and calls an injected function when they fire
/// </summary>
private class DeferredCompletedCall
{
private readonly PlotModel _plotModel;
private readonly Action _completed;
public DeferredCompletedCall(PlotModel plotModel, Action completed)
{
_plotModel = plotModel ?? throw new ArgumentNullException(nameof(plotModel));
_completed = completed ?? throw new ArgumentNullException(nameof(completed));
}
/// <summary>
/// Start monitoring events. Their observer lists will keep us alive until <see cref="Unhook"/> is called.
/// </summary>
public void HookUp()
{
Unhook();
_plotModel.MouseDown += HandleMouseDown;
_plotModel.Updated += HandleUpdated;
_plotModel.MouseLeave += HandleMouseLeave;
}
/// <summary>
/// Stop watching events. If they were the only things keeping us alive, we'll turn into garbage.
/// </summary>
private void Unhook()
{
_plotModel.MouseDown -= HandleMouseDown;
_plotModel.Updated -= HandleUpdated;
_plotModel.MouseLeave -= HandleMouseLeave;
}
private void CallCompletedAndUnhookEvents()
{
_completed();
Unhook();
}
private void HandleUpdated(object sender, EventArgs e) => CallCompletedAndUnhookEvents();
private void HandleMouseLeave(object sender, OxyMouseEventArgs e) => CallCompletedAndUnhookEvents();
private void HandleMouseDown(object sender, OxyMouseDownEventArgs e)
{
CallCompletedAndUnhookEvents();
// Since we're not setting e.Handled to true here, this click will have its regular effect in
// addition to closing the tracker; e.g. it could open the tracker again at the new position.
// Modify this code if that's not what you want.
}
}
}
}
我正在使用带有 C# 和 WPF 的 OxyPlot 2014.1.546。
我的情节有一个自定义跟踪器,当用户单击一个点时会出现。我想包含用于执行与单击点相关的操作的按钮。将它们添加到跟踪器模板非常简单;问题是,默认情况下,一旦用户释放鼠标按钮,跟踪器就会消失,这意味着不可能真正点击它们。
有什么方法可以告诉 OxyPlot 保持跟踪器打开直到用户点击它以外的地方吗?
简短的回答是 OxyPlot 似乎并不直接支持这种行为。在花了一些时间研究反编译的源代码后,我想出了以下解决方案,似乎可行。基本想法是从 OxyPlot 的 built-in TrackerManipulator
派生出我自己的 StayOpenTrackerManipulator
并实例化它以响应点击。我的操纵器覆盖了虚拟 Completed()
函数,当鼠标按钮被释放时框架调用该函数,并将调用推迟到关闭跟踪器的 base-class Completed()
,直到下一次单击鼠标(或直到绘图被修改,或直到鼠标离开)。因为我使用的是 C# 和 WPF,所以我将所有内容都包装在一个附加行为中,可以从 XAML 中使用,如下所示:
<PlotView behaviors:ShowTrackerAndLeaveOpenBehavior.BindToMouseDown="Left" />
但如果需要的话,将内脏取出并以不同的方式重新使用它们会很简单。这是来源:
/// <summary>
/// Normal OxyPlot behavior is to show the tracker when the bound mouse button is pressed,
/// and hide it again when the button is released. With this behavior set, the tracker will stay open
/// until the user clicks the plot outside it (or the plot is modified).
/// </summary>
public static class ShowTrackerAndLeaveOpenBehavior
{
public static readonly DependencyProperty BindToMouseDownProperty = DependencyProperty.RegisterAttached(
"BindToMouseDown", typeof(OxyMouseButton), typeof(ShowTrackerAndLeaveOpenBehavior),
new PropertyMetadata(default(OxyMouseButton), OnBindToMouseButtonChanged));
[AttachedPropertyBrowsableForType(typeof(IPlotView))]
public static void SetBindToMouseDown(DependencyObject element, OxyMouseButton value) =>
element.SetValue(BindToMouseDownProperty, value);
[AttachedPropertyBrowsableForType(typeof(IPlotView))]
public static OxyMouseButton GetBindToMouseDown(DependencyObject element) =>
(OxyMouseButton) element.GetValue(BindToMouseDownProperty);
private static void OnBindToMouseButtonChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
if (!(d is IPlotView plot))
throw new InvalidOperationException($"Can only be applied to {nameof(IPlotView)}");
if (plot.ActualModel == null)
throw new InvalidOperationException("Plot has no model");
var controller = plot.ActualController;
if (controller == null)
throw new InvalidOperationException("Plot has no controller");
if (e.OldValue is OxyMouseButton oldButton && oldButton != OxyMouseButton.None)
controller.UnbindMouseDown(oldButton);
var newButton = GetBindToMouseDown(d);
if (newButton == OxyMouseButton.None)
return;
controller.UnbindMouseDown(newButton);
controller.BindMouseDown(newButton, new DelegatePlotCommand<OxyMouseDownEventArgs>(
AddStayOpenTrackerManipulator));
}
private static void AddStayOpenTrackerManipulator(IPlotView view, IController controller,
OxyMouseDownEventArgs e)
{
controller.AddMouseManipulator(view, new StayOpenTrackerManipulator(view), e);
}
private class StayOpenTrackerManipulator : TrackerManipulator
{
private readonly PlotModel _plotModel;
private bool _isTrackerOpen;
public StayOpenTrackerManipulator(IPlotView plot)
: base(plot)
{
_plotModel = plot?.ActualModel ?? throw new ArgumentException("Plot has no model", nameof(plot));
Snap = true;
PointsOnly = false;
}
public override void Started(OxyMouseEventArgs e)
{
_plotModel.TrackerChanged += HandleTrackerChanged;
base.Started(e);
}
public override void Completed(OxyMouseEventArgs e)
{
if (!_isTrackerOpen)
{
ReallyCompleted(e);
}
else
{
// Completed() is called as soon as the mouse button is released.
// We won't call the base Completed() here since that would hide the tracker.
// Instead, defer the call until one of the hooked events occurs.
// The caller will still remove us from the list of active manipulators as soon as we return,
// but that's good; otherwise the tracker would continue to move around as the mouse does.
new DeferredCompletedCall(_plotModel, () => ReallyCompleted(e)).HookUp();
}
}
private void ReallyCompleted(OxyMouseEventArgs e)
{
base.Completed(e);
// Must unhook or this object will live as long as the model (instead of as long as the manipulation)
_plotModel.TrackerChanged -= HandleTrackerChanged;
}
private void HandleTrackerChanged(object sender, TrackerEventArgs e) =>
_isTrackerOpen = e.HitResult != null;
/// <summary>
/// Monitors events that should trigger manipulator completion and calls an injected function when they fire
/// </summary>
private class DeferredCompletedCall
{
private readonly PlotModel _plotModel;
private readonly Action _completed;
public DeferredCompletedCall(PlotModel plotModel, Action completed)
{
_plotModel = plotModel ?? throw new ArgumentNullException(nameof(plotModel));
_completed = completed ?? throw new ArgumentNullException(nameof(completed));
}
/// <summary>
/// Start monitoring events. Their observer lists will keep us alive until <see cref="Unhook"/> is called.
/// </summary>
public void HookUp()
{
Unhook();
_plotModel.MouseDown += HandleMouseDown;
_plotModel.Updated += HandleUpdated;
_plotModel.MouseLeave += HandleMouseLeave;
}
/// <summary>
/// Stop watching events. If they were the only things keeping us alive, we'll turn into garbage.
/// </summary>
private void Unhook()
{
_plotModel.MouseDown -= HandleMouseDown;
_plotModel.Updated -= HandleUpdated;
_plotModel.MouseLeave -= HandleMouseLeave;
}
private void CallCompletedAndUnhookEvents()
{
_completed();
Unhook();
}
private void HandleUpdated(object sender, EventArgs e) => CallCompletedAndUnhookEvents();
private void HandleMouseLeave(object sender, OxyMouseEventArgs e) => CallCompletedAndUnhookEvents();
private void HandleMouseDown(object sender, OxyMouseDownEventArgs e)
{
CallCompletedAndUnhookEvents();
// Since we're not setting e.Handled to true here, this click will have its regular effect in
// addition to closing the tracker; e.g. it could open the tracker again at the new position.
// Modify this code if that's not what you want.
}
}
}
}