OnMouseMove event and drawing anchors on high or low of bars

Greetings

I took the CustomLine.cs indicator that was posted on the forums and attempted to modify it for my needs. So far so good. However, there is a small problem in which I have not been able to solve.

When the drawing indicator is invoked and the user attempts to draw a line on the chart, I would like to draw a small blue rectangle when the mouse pointer touches the high or low of a bar. For the first point of the line, the rectangle is not drawn. However, after the user clicks on the chart to create the first point of the line, the small blue rectangle is drawn when the mouse cursor touches the high or low of the bar. There is another problem which the mouse moves after the creation of the first point, the rectangle is drawn on the highs of bars that the mouse has not touched.

Here is the relevant code. Any help or hint is greatly appreciated:

//class variables
private SharpDX.RectangleF? _snapRectDx = null;
private Point? _snapPointPx = null;
private const float _snapSizePx = 12f; // square size
private const double _snapYTolPx = 8.0; // vertical tolerance
private const int _snapLookaround = 1;
private Point? hoverHitPx = null;
private const double hoverTolPx = 5; // pixel tolerance

public override void OnMouseMove(ChartControl chartControl, ChartPanel chartPanel, ChartScale chartScale, ChartAnchor dataPoint)
{
var prevHover = hoverHitPx;
hoverHitPx = null;
ChartBars bars = chartControl.BarsArray[PanelIndex];

	    // Always check for hover hits, regardless of drawing state
	    if (bars != null)
	    {
	        var mousePt = dataPoint.GetPoint(chartControl, chartPanel, chartScale);
	        int idx = bars.GetBarIdxByTime(chartControl, dataPoint.Time);
	        if (idx >= 0 && idx < bars.Count)
	        {
	            double hi = bars.Bars.GetHigh(idx);
	            double lo = bars.Bars.GetLow(idx);
	            
	            double barX = chartControl.GetXByBarIndex(bars, idx);
	            double hiY = chartScale.GetYByValue(hi);
	            double loY = chartScale.GetYByValue(lo);
	            
	            // Check for proximity to high/low
	            if (Math.Abs(mousePt.Y - hiY) <= hoverTolPx && Math.Abs(mousePt.X - barX) <= chartControl.BarWidth + 2)
	                hoverHitPx = new Point(barX, hiY);
	            else if (Math.Abs(mousePt.Y - loY) <= hoverTolPx && Math.Abs(mousePt.X - barX) <= chartControl.BarWidth + 2)
	                hoverHitPx = new Point(barX, loY);
	        }
	    }
	    
	    if (IsLocked && DrawingState != DrawingState.Building)
	        return;
	    
	    // Snap logic for editing mode
	    Point mousePxPanel = Mouse.GetPosition(chartPanel);
	    int mouseAbsX = (int)(mousePxPanel.X + chartPanel.X);
	    
	    var prevRect = _snapRectDx;
	    
	    // Only use snap rectangles when editing VeEndAnchor
	    if (DrawingState == DrawingState.Editing && editingAnchor == VeEndAnchor)
	    {
	        var snapped = SnapToHiLo(chartControl, chartPanel, chartScale, mouseAbsX, mousePxPanel.Y);
	    }
	    else
	    {
	        _snapRectDx = null;  // Clear snap rect when not editing
	    }
                //non-relevant code removed

}

public override void OnRender(ChartControl chartControl, ChartScale chartScale)
{
// Draw blue rectangle when touching high/low during building
if ((DrawingState == DrawingState.Building || DrawingState == DrawingState.Normal) && hoverHitPx.HasValue)
{
using (var blueBrush = new SharpDX.Direct2D1.SolidColorBrush(RenderTarget, new SharpDX.Color(0, 120, 215, 180))) // Semi-transparent blue
{
var rect = new SharpDX.RectangleF(
(float)(hoverHitPx.Value.X - _snapSizePx / 2f),
(float)(hoverHitPx.Value.Y - _snapSizePx / 2f),
_snapSizePx,
_snapSizePx
);
RenderTarget.FillRectangle(rect, blueBrush);

	            // Optional: draw a border for better visibility
	            using (var borderBrush = new SharpDX.Direct2D1.SolidColorBrush(RenderTarget, new SharpDX.Color(0, 80, 180, 255)))
	            {
	                RenderTarget.DrawRectangle(rect, borderBrush, 1f);
	            }
	        }
	    }
	
	    // Snap rectangle for editing mode (different color to distinguish)
	    if (!IsInHitTest && DrawingState == DrawingState.Editing && _snapRectDx.HasValue)
	    {
	        using (var snapBrush = new SharpDX.Direct2D1.SolidColorBrush(RenderTarget, new SharpDX.Color(255, 165, 0, 180))) // Orange for editing
	        {
	            RenderTarget.FillRectangle(_snapRectDx.Value, snapBrush);
	        }
	    }
               //non relevant code removed

}

Try this one :

namespace NinjaTrader.NinjaScript.Indicators
{
[Gui.CategoryOrder(“Line Property”, 0)]
[Gui.CategoryOrder(“Labels”, 20)]
[Gui.CategoryOrder(“Alert”, 30)]
[Gui.CategoryOrder(“Email”, 40)]
public class LowHighMarker : Indicator
{
#region Variable Declaration
public enum SideEnum { Left, Right }
public enum PositionEnum { Below, Above }

    private MenuItem  HiLowMarker;
    private MenuItem  menuItem1;
    private MenuItem  menuItem2;
    private MenuItem  menuItem3;
    private Separator mymenuseparator;

    private SharpDX.Point clickPoint = new SharpDX.Point();
    private ChartScale    chartScale;

// private bool inited = false;
private int lastSoundPlay = -1;
private int myxcoord;

    // REMOVED cached DX resources – they must be created inside OnRender

    private List<int>    Mxcoord;
    private List<int>    Mbar;
    private List<double> Mtag;
    private List<string> Mlabel;

// private double alertvalue;
// private string myalertfile = “Alert1.wav”;
private double MyHigh = double.NaN;
private double MyLow = double.NaN;
private string subject = “NinjaTrader Alert Triggered”;
private string body = “”;

    private int    convertedBarIndex = -1;
    private double convertedPrice    = double.NaN;
    private bool   isContextMenuInitialized = false;

// private bool clickSet = false;
#endregion

    #region OnStateChange
    protected override void OnStateChange()
    {
        try
        {
            if (State == State.SetDefaults)
            {
                Description = @"Mark important S/R levels on the chart.";
                Name        = "_LowHighMarker";
                Calculate   = Calculate.OnBarClose;
                IsOverlay   = true;
                DisplayInDataBox      = false;
                DrawOnPricePanel      = true;
                DrawHorizontalGridLines = true;
                DrawVerticalGridLines   = true;
                PaintPriceMarkers       = true;
                ScaleJustification      = NinjaTrader.Gui.Chart.ScaleJustification.Right;
                IsSuspendedWhileInactive = true;

                Side      = SideEnum.Right;
                Size      = 22;
                Position  = PositionEnum.Above;
                Bold      = false;
                Opacity   = 255;
                SoundFile = @"Alert1.wav";
                sendEMail = false;
                EmailTo   = @"noone@noone.com";
                EmailBody = @"Body of the email";
                soundAlerts = false;
                LineWidth   = 3;
                Infocolor   = Brushes.Blue;
                MyHighLabel = "High:";
                MyLowLabel  = "Low:";
                MA0DashStyle = DashStyleHelper.Solid;
            }
            else if (State == State.Historical)
            {
                if (ChartControl == null) return;
                ChartControl.Dispatcher.Invoke(() =>
                {
                    if (HiLowMarker == null)
                    {
                        ChartControl.ContextMenuOpening  += ChartControl_ContextMenuOpening;
                        ChartControl.ContextMenuClosing  += ChartControl_ContextMenuClosing;

                        mymenuseparator = new Separator();
                        HiLowMarker = new MenuItem { Header = "Support/Resistance Marker", Name = "HiLowMarker" };
                        menuItem1 = new MenuItem { Header = "Mark the High", Name = "menuItem1" };
                        menuItem2 = new MenuItem { Header = "Mark the Low",  Name = "menuItem2" };
                        menuItem3 = new MenuItem { Header = "Clear All",     Name = "menuItem3" };

                        menuItem1.Click += menuItem1_Click;
                        menuItem2.Click += menuItem2_Click;
                        menuItem3.Click += menuItem3_Click;

                        if (ChartPanel != null)
                            ChartPanel.MouseRightButtonDown += OnMouseClick;
                    }
                });

                if (ChartPanel != null)
                {
                    foreach (ChartScale scale in ChartPanel.Scales)
                        if (scale.ScaleJustification == ScaleJustification)
                            { chartScale = scale; break; }
                }
            }
            else if (State == State.DataLoaded)
            {
                Mxcoord = new List<int>();
                Mbar    = new List<int>();
                Mtag    = new List<double>();
                Mlabel  = new List<string>();
            }
            else if (State == State.Terminated)
            {
                if (ChartControl == null) return;
                ChartControl.Dispatcher.Invoke(() =>
                {
                    if (menuItem1 != null) menuItem1.Click -= menuItem1_Click;
                    if (menuItem2 != null) menuItem2.Click -= menuItem2_Click;
                    if (menuItem3 != null) menuItem3.Click -= menuItem3_Click;
                    ChartControl.ContextMenuOpening  -= ChartControl_ContextMenuOpening;
                    ChartControl.ContextMenuClosing  -= ChartControl_ContextMenuClosing;
                    if (ChartPanel != null)
                        ChartPanel.MouseRightButtonDown -= OnMouseClick;

                    if (HiLowMarker      != null && ChartControl.ContextMenu.Items.Contains(HiLowMarker))
                        ChartControl.ContextMenu.Items.Remove(HiLowMarker);
                    if (mymenuseparator  != null && ChartControl.ContextMenu.Items.Contains(mymenuseparator))
                        ChartControl.ContextMenu.Items.Remove(mymenuseparator);
                });

                Mxcoord?.Clear(); Mbar?.Clear(); Mtag?.Clear(); Mlabel?.Clear();
            }
        }
        catch (Exception ex)
        {
            Log("LowHighMarker: OnStateChange – " + ex.Message, LogLevel.Error);
        }
    }
    #endregion

    #region Mouse / Menu handlers
    private void OnMouseClick(object sender, MouseEventArgs e)
    {
        if (ChartControl == null || ChartPanel == null || chartScale == null) return;

        try
        {
            double clickX = e.GetPosition(ChartControl as IInputElement).X;
            double clickY = e.GetPosition(ChartControl as IInputElement).Y;

            clickPoint.X = ChartingExtensions.ConvertToHorizontalPixels(clickX, ChartControl.PresentationSource);
            clickPoint.Y = ChartingExtensions.ConvertToVerticalPixels  (clickY, ChartControl.PresentationSource);

            ChartControl.InvalidateVisual();
            e.Handled = true;

            convertedPrice = Instrument?.MasterInstrument?.RoundToTickSize(chartScale.GetValueByY(clickPoint.Y)) ?? double.NaN;

            int mousePointX = ChartControl.MouseDownPoint.X.ConvertToHorizontalPixels(ChartControl.PresentationSource);
            convertedBarIndex = ChartBars?.GetBarIdxByX(ChartControl, mousePointX) ?? -1;

            if (convertedBarIndex >= 0 && convertedBarIndex < BarsArray?[0]?.Count)
            {
                MyHigh = High.GetValueAt(convertedBarIndex);
                MyLow  = Low .GetValueAt(convertedBarIndex);
            }
            else
            {
                MyHigh = double.NaN;
                MyLow  = double.NaN;
            }

            myxcoord = (int)clickPoint.X;
        }
        catch (Exception ex)
        {
            Log("LowHighMarker: OnMouseClick – " + ex.Message, LogLevel.Error);
            MyHigh = MyLow = double.NaN;
            convertedBarIndex = -1;
        }
    }

    private bool ValidateClickData() =>
        myxcoord > 0 &&
        convertedBarIndex >= 0 &&
        convertedBarIndex < BarsArray?[0]?.Count &&
        !double.IsNaN(MyHigh) &&
        !double.IsNaN(MyLow);

    private void menuItem1_Click(object sender, RoutedEventArgs e)
    {
        try { 
			if (ValidateClickData()) 
				{ 
					Mxcoord.Add(myxcoord); 
					Mbar.Add(convertedBarIndex); 
					Mtag.Add(MyHigh); 
					Mlabel.Add(MyHighLabel ?? "High:"); 
					ChartControl?.InvalidateVisual();   // <-- NEW
				} 
		}
        catch (Exception ex) { Log("menuItem1_Click – " + ex.Message, LogLevel.Error); }
    }

    private void menuItem2_Click(object sender, RoutedEventArgs e)
    {
        try { 
			if (ValidateClickData()) 
				{ 
					Mxcoord.Add(myxcoord); 
					Mbar.Add(convertedBarIndex); 
					Mtag.Add(MyLow ); 
					Mlabel.Add(MyLowLabel  ?? "Low:" ); 
					ChartControl?.InvalidateVisual();   // <-- NEW
				} 
		}
        catch (Exception ex) { Log("menuItem2_Click – " + ex.Message, LogLevel.Error); }
    }

    private void menuItem3_Click(object sender, RoutedEventArgs e)
    {
        try { 
				Mxcoord?.Clear(); 
				Mbar?.Clear(); 
				Mtag?.Clear(); 
				Mlabel?.Clear(); 
				ChartControl?.InvalidateVisual(); 
			}
        catch (Exception ex) { Log("menuItem3_Click – " + ex.Message, LogLevel.Error); }
    }
    #endregion

    #region OnBarUpdate & Alert helpers
    protected override void OnBarUpdate()
    {
        if (State != State.Realtime || ChartControl == null || Mtag?.Count == 0 || CurrentBar < 20)
            return;

        try
        {
            for (int i = 0; i < Mtag.Count; i++)
            {
                double level = Mtag[i];
                if (Close[1] < level && Close[0] > level && lastSoundPlay != CurrentBar)
                    HandleAlert("Crossed Above", level, i);
                else if (Close[1] > level && Close[0] < level && lastSoundPlay != CurrentBar)
                    HandleAlert("Crossed Below", level, i);
            }
        }
        catch (Exception ex)
        {
            Log("LowHighMarker: OnBarUpdate – " + ex.Message, LogLevel.Error);
        }
    }

    private void HandleAlert(string alertType, double alertValue, int markerIndex)
    {
        if (State != State.Realtime) return;

        try
        {
            lastSoundPlay = CurrentBar;
            string label  = (markerIndex >= 0 && markerIndex < Mlabel?.Count) ? Mlabel[markerIndex] : "Level";
            string msg    = $"{alertType} Alert at ({label}) {alertValue:F2}";

            if (soundAlerts && !string.IsNullOrWhiteSpace(SoundFile))
                PlaySound(SoundFile);

            Alert($"{Name}_{Instrument?.FullName}", Priority.High, msg, SoundFile, 60, Infocolor, Brushes.White);

            if (sendEMail && !string.IsNullOrWhiteSpace(EmailTo))
            {
                string subj = $"Cross Alert on {Instrument?.FullName} {BarsPeriod?.Value} {BarsPeriod?.BarsPeriodType}";
                string body = $"Price crossed {alertValue:F2} on {Instrument?.FullName} at {Time[0]}\n{EmailBody}";
                SendMail(EmailTo, subj, body);
            }
        }
        catch (Exception ex)
        {
            Log("HandleAlert – " + ex.Message, LogLevel.Error);
        }
    }
    #endregion

    #region Context-Menu events
    private void ChartControl_ContextMenuOpening(object sender, ContextMenuEventArgs e)
    {
        var chartControl = sender as ChartControl;
        if (chartControl == null || chartControl.ChartObjects.Any(o => o.IsSelected)) return;

        try
        {
            if (!isContextMenuInitialized)
            {
                chartControl.Dispatcher.Invoke(() =>
                {
                    if (!chartControl.ContextMenu.Items.Contains(mymenuseparator))
                        chartControl.ContextMenu.Items.Add(mymenuseparator);
                    if (!chartControl.ContextMenu.Items.Contains(HiLowMarker))
                        chartControl.ContextMenu.Items.Add(HiLowMarker);

                    HiLowMarker.Items.Clear();
                    HiLowMarker.Items.Add(menuItem1);
                    HiLowMarker.Items.Add(menuItem2);
                    HiLowMarker.Items.Add(menuItem3);
                });
                isContextMenuInitialized = true;
            }
        }
        catch (Exception ex)
        {
            Log("ContextMenuOpening – " + ex.Message, LogLevel.Error);
        }
    }

    private void ChartControl_ContextMenuClosing(object sender, ContextMenuEventArgs e)
    {
        try
        {
            if (HiLowMarker != null) HiLowMarker.Items.Clear();
            isContextMenuInitialized = false;
        }
        catch (Exception ex)
        {
            Log("ContextMenuClosing – " + ex.Message, LogLevel.Error);
        }
    }
    #endregion

    #region OnRender
    private SharpDX.Color ConvertBrushToDxColor(System.Windows.Media.Brush brush, float opacity)
    {
        if (brush is System.Windows.Media.SolidColorBrush scb)
        {
            Color c = scb.Color;
            return new SharpDX.Color(c.R, c.G, c.B, (byte)(c.A * opacity));
        }
        return SharpDX.Color.White;
    }

    private SharpDX.Direct2D1.StrokeStyle GetStrokeStyle(DashStyleHelper dash, RenderTarget rt)
    {
        try
        {
            SharpDX.Direct2D1.DashStyle dxDash = SharpDX.Direct2D1.DashStyle.Solid;
			 float[] dashes = null;          // <-- declare local variable
			switch (dash)
			{
			    case DashStyleHelper.Solid:
			        dxDash = SharpDX.Direct2D1.DashStyle.Solid;
			        break;
			    case DashStyleHelper.Dash:
			        dxDash = SharpDX.Direct2D1.DashStyle.Custom;
			        dashes = new[] { 6f, 6f };
			        break;
			    case DashStyleHelper.Dot:
			        dxDash = SharpDX.Direct2D1.DashStyle.Dot;
			        break;
			    case DashStyleHelper.DashDot:
			        dxDash = SharpDX.Direct2D1.DashStyle.Custom;
			        dashes = new[] { 6f, 3f, 1f, 3f };
			        break;
			    case DashStyleHelper.DashDotDot:
			        dxDash = SharpDX.Direct2D1.DashStyle.Custom;
			        dashes = new[] { 6f, 3f, 1f, 3f, 1f, 3f };
			        break;
			}

            var props = new StrokeStyleProperties
            {
                DashStyle = dxDash,
                DashCap   = CapStyle.Flat,
                EndCap    = CapStyle.Flat,
                StartCap  = CapStyle.Flat,
                LineJoin  = LineJoin.Miter
            };

            return new SharpDX.Direct2D1.StrokeStyle(rt.Factory, props, dashes);
        }
        catch
        {
            return new SharpDX.Direct2D1.StrokeStyle(rt.Factory, new StrokeStyleProperties());
        }
    }

    protected override void OnRender(ChartControl chartControl, ChartScale chartScale)
    {
        if (chartControl == null || chartScale == null || ChartBars == null ||
            RenderTarget == null || Mxcoord?.Count == 0 || Mtag?.Count == 0 || Mlabel?.Count == 0)
            return;

        try
        {
            // 1. Create resources locally (per RenderTarget)
            var mediaColor = ((System.Windows.Media.SolidColorBrush)Infocolor).Color;
            var dxColor    = new SharpDX.Color4(
                mediaColor.R / 255f, mediaColor.G / 255f, mediaColor.B / 255f,
                (mediaColor.A * (Opacity / 255f)) / 255f);

            using (var brush  = new SharpDX.Direct2D1.SolidColorBrush(RenderTarget, dxColor))
            using (var stroke = GetStrokeStyle(MA0DashStyle, RenderTarget))
            {
                int cnt = Math.Min(Math.Min(Mxcoord.Count, Mtag.Count), Mlabel.Count);

                for (int i = 0; i < cnt; i++)
                {
                    double price = Mtag[i];
                    string lbl   = Mlabel[i];
                    int    x     = Mxcoord[i];

                    float y   = (float)chartScale.GetYByValue(price);
                    float end = ChartPanel.X + ChartPanel.W;

                    RenderTarget.DrawLine(
                        new SharpDX.Vector2(x, y),
                        new SharpDX.Vector2(end, y),
                        brush, LineWidth, stroke);

                    string text = $"{lbl} ({price:F2})";

                    using (var tf = new TextFormat(
                               Core.Globals.DirectWriteFactory,
                               "Arial",
                               Bold ? SharpDX.DirectWrite.FontWeight.Bold : SharpDX.DirectWrite.FontWeight.Normal,
                               FontStyle.Normal,
                               Size))
                    using (var tl = new TextLayout(
                               Core.Globals.DirectWriteFactory,
                               text,
                               tf,
                               500, 50))
                    {
                        float labelX, labelY;
                        labelX = Side == SideEnum.Left
                            ? x + 2
                            : ChartPanel.W - tl.Metrics.Width - 2;

                        labelY = Position == PositionEnum.Above
                            ? y - Size - LineWidth - 2
                            : y + LineWidth + 2;

                        RenderTarget.DrawTextLayout(
                            new SharpDX.Vector2(labelX, labelY),
                            tl, brush);
                    }
                }
            }
        }
        catch (Exception ex)
        {
            Log("LowHighMarker: OnRender – " + ex.Message, LogLevel.Error);
        }
    }
    #endregion

    #region Properties
    [Display(Name = "Label for Lows", GroupName = "Labels", Order = 0)]
    public string MyLowLabel { get; set; }

    [Display(Name = "Label for Highs", GroupName = "Labels", Order = 1)]
    public string MyHighLabel { get; set; }

    [Display(Name = "Font Size", GroupName = "Labels", Order = 2)]
    public int Size { get; set; }

    [Display(Name = "Bold", GroupName = "Labels", Order = 3)]
    public bool Bold { get; set; }

    [Display(Name = "Label Position", GroupName = "Labels", Order = 4)]
    public PositionEnum Position { get; set; }

    [Display(Name = "Label Side", GroupName = "Labels", Order = 5)]
    public SideEnum Side { get; set; }

    [Display(Name = "Send email alert?", GroupName = "Email", Order = 0)]
    public bool sendEMail { get; set; }

    [Display(Name = "Email To:", GroupName = "Email", Order = 2)]
    public string EmailTo { get; set; }

    [Display(Name = "Email Body:", GroupName = "Email", Order = 3)]
    public string EmailBody { get; set; }

    [Display(Name = "Sound alert?", GroupName = "Alert", Order = 0)]
    public bool soundAlerts { get; set; }

    [Display(Name = "Alert sound file", GroupName = "Alert", Order = 1)]
    [PropertyEditor("NinjaTrader.Gui.Tools.FilePathPicker", Filter = "Wav Files (*.wav)|*.wav")]
    public string SoundFile { get; set; }

    [XmlIgnore]
    [Display(Name = "Line & Text Color", GroupName = "Line Property", Order = 3)]
    public System.Windows.Media.Brush Infocolor { get; set; }

    [Browsable(false)]
    public string InfocolorColorSerializable
    {
        get => Serialize.BrushToString(Infocolor);
        set => Infocolor = Serialize.StringToBrush(value);
    }

    [Range(1, 10)]
    [Display(Name = "Line Width", GroupName = "Line Property", Order = 2)]
    public int LineWidth { get; set; }

    [Range(0, 255)]
    [Display(Name = "Line Opacity (0-255)", GroupName = "Line Property", Order = 4)]
    public byte Opacity { get; set; }

    [Display(Name = "Line DashStyle", GroupName = "Line Property", Order = 5)]
    public DashStyleHelper MA0DashStyle { get; set; }
    #endregion
}

}

This forum software is useless for pasting code :rofl:

1 Like

If you know the trick, it actually works pretty well. I explained it in this post.
I really hope more people read this. It gives me anxiety when I see code intermingled with regular text :laughing:

1 Like