2

I'm writing a plugin for a trading software (C#, winforms, .NET 3.5) and I'd like to draw a crosshair cursor over a panel (let's say ChartPanel) which contains data that might be expensive to paint. What I've done so far is:

  1. I added a CursorControl to the panel
    • this CursorControl is positioned over the main drawing panel so that it covers it's entire area
    • it's Enabled = false so that all input events are passed to the parent ChartPanel
    • it's Paint method is implemented so that it draws lines from top to bottom and from left to right at current mouse position
  2. When MouseMove event is fired, I have two possibilities:
    • A) Call ChartPanel.Invalidate(), but as I said, the underlying data may be expensive to paint and this would cause everything to redraw everytime I move a mouse, which is wrong (but it is the only way I can make this work now)
    • B) Call CursorControl.Invalidate() and before the cursor is drawn I would take a snapshot of currently drawn data and keep it as a background for the cursor that would be just restored everytime the cursor needs to be repainted ... the problem with this is ... I don't know how to do that.

2.B. Would mean to:

  • Turn existing Graphics object into Bitmap (it (the Graphics) is given to me through Paint method and I have to paint at it, so I just can't create a new Graphics object ... maybe I get it wrong, but that's the way I understand it)
  • before the crosshair is painted, restore the Graphics contents from the Bitmap and repaint the crosshair

I can't control the process of painting the expensive data. I can just access my CursorControl and it's methods that are called through the API.

So is there any way to store existing Graphics contents into Bitmap and restore it later? Or is there any better way to solve this problem?


RESOLVED: So after many hours of trial and error I came up with a working solution. There are many issues with the software I use that can't be discussed generally, but the main principles are clear:

  • existing Graphics with already painted stuff can't be converted to Bitmap directly, instead I had to use panel.DrawToBitmap method first mentioned in @Gusman's answer. I knew about it, I wanted to avoid it, but in the end I had to accept, because it seems to be the only way
  • also I wanted to avoid double drawing of every frame, so the first crosshair paint is always drawn directly to the ChartPanel. After the mouse moves without changing the chart image I take a snapshow through DrawToBitmap and proceed as described in chosen answer.
  • The control has to be Opaque (not enabled Transparent background) so that refreshing it doesn't call Paint on it's parent controls (which would cause the whole chart to repaint)

I still experience occasional flicker every few seconds or so, but I guess I can figure that out somehow. Although I picked Gusman's answer, I would like to thank everyone involved, as I used many other tricks mentioned in other answers, like the Panel.BackgroundImage, use of Plot() method instead of Paint() to lock the image, etc.

LukasH
  • 155
  • 1
  • 7
  • Unlike forum sites, we don't use "Thanks", or "Any help appreciated", or signatures on [so]. See "[Should 'Hi', 'thanks,' taglines, and salutations be removed from posts?](http://meta.stackexchange.com/questions/2950/should-hi-thanks-taglines-and-salutations-be-removed-from-posts). – John Saunders Dec 23 '14 at 15:34
  • What about having a second panel layered on top of the other one. This one has a transparent background and is solely used for painting the cursor (with the lines). Also, make sure everything is double buffered, and optimize the complex painting where possible (for example, ensure all calculations are performed inside a "load" function, and not in the paint event) – musefan Dec 23 '14 at 15:38
  • "I can't control the process of painting the expensive data" -- can you at least control and/or react to any changes to the data that would cause the chart to require being updated? If so, then you might be able to (depending on how the chart control is implemented) buffer the chart rendering by subclassing it, overriding `OnPaint()`, and intercepting the rendering. If not, then the big challenge here is detecting when the rendered chart has changed, so that you know when to update your off-screen copy of the control (making the off-screen copy is itself not too hard). – Peter Duniho Dec 23 '14 at 19:38
  • @musefan That's basically what I'm trying to achieve with the CursorControl class. Even though the paint operation might not be as expensive as I mentioned, it is too expensive to be performed with every mouse move. Lots of rectangles, lines and antialiased text... Trouble is that redrawing just the transparent panel leaves a trace of the previous drawn crosshair. It needs to be cleared by something. – LukasH Dec 23 '14 at 20:07
  • @PeterDuniho Subclassing is unfortunately not possible here. I can only add a Paint event listener on the ChartPanel, which btw notifies me of a change on the rendered panel. – LukasH Dec 23 '14 at 20:12
  • "which btw notifies me of a change on the rendered panel" -- no, it doesn't. It notifies you of a need to have the panel drawn on the screen. This will _sometimes_ be due to an actual change, and other times not. You really don't want to be caching the drawing on every `Paint` event. Those happen too often and for reasons other than just that the visual representation of the chart changed. – Peter Duniho Dec 23 '14 at 20:29
  • @PeterDuniho Actually you're right. There is a Plot method as part of API, that is called only when there is a change on visual representation of the chart. It's restricted too much to be used for this purpose, but I can set a switch here that I can pick up later in the OnPaint method. Good point. – LukasH Dec 23 '14 at 20:43
  • That's good news. Excellent news, actually. The remaining issue is why you can't subclass the control. Unfortunately, if you don't have control over the initialization of your form, in a way that lets you decide what control object to actually place in the form (which is the only reason I can think of subclassing would be impossible), it will be difficult or impossible to take advantage of the `Plot()` method you describe. I.e. you can't fix the issue of the control insisting on drawing itself slowly if you aren't able to override the drawing behavior somehow. – Peter Duniho Dec 23 '14 at 20:48

2 Answers2

5

This can be done in several ways, always storing the graphics as a Bitmap. The most direct and efficient way is to let the Panel do all the work for you.

Here is the idea: Most winforms Controls have a two-layered display.

In the case of a Panel the two layers are its BackgroundImage and its Control surface. The same is true for many other controls, like Label, CheckBox, RadioButton or Button.

(One interesting exception is PictureBox, which in addition has an (Foreground) Image. )

So we can move the expensive stuff into the BackgroundImage and draw the crosshair on the surcafe. In our case, the Panel, all nice extras are in place and you could pick all values for the BackgroundImageLayout property, including Tile, Stretch, Center or Zoom. We choose None.

Now we add one flag to your project:

bool panelLocked = false;

and a function to set it as needed:

void lockPanel( bool lockIt)
{
    if (lockIt)
    {    
        Bitmap bmp = new Bitmap(panel1.ClientSize.Width, panel1.ClientSize.Width);
        panel1.DrawToBitmap(bmp, panel1.ClientRectangle);
        panel1.BackgroundImage = bmp;
    }
    else
    {
        if (panel1.BackgroundImage != null)
            panel1.BackgroundImage.Dispose();
        panel1.BackgroundImage = null;
    }
    panelLocked = lockIt;
}

Here you can see the magic at work: Before we actually lock the Panel from doing the expensive stuff, we tell it to create a snapshot of its graphics and put it into the BackgroundImage..

Now we need to use the flag to control the Paint event:

private void panel1_Paint(object sender, PaintEventArgs e)
{
    Size size =  panel1.ClientSize;

    if (panelLocked)
    {
        // draw a full size cross-hair cursor over the whole Panel
        // change this to suit your own needs!
        e.Graphics.DrawLine(Pens.Red, 0, mouseCursor.Y, size.Width - 1, mouseCursor.Y);
        e.Graphics.DrawLine(Pens.Red, mouseCursor.X, 0, mouseCursor.X, size.Height);
    }

    // expensive drawing, you insert your own stuff here..
    else
    {
        List<Pen> pens = new List<Pen>();
        for (int i = 0; i < 111; i++)
            pens.Add(new Pen(Color.FromArgb(R.Next(111), 
                     R.Next(111), R.Next(111), R.Next(111)), R.Next(5) / 2f));

        for (int i = 0; i < 11111; i++)
            e.Graphics.DrawEllipse(pens[R.Next(pens.Count)], R.Next(211), 
                                   R.Next(211), 1 + R.Next(11), 1 + R.Next(11));
    }

}

Finally we script the MouseMove of the Panel:

private void panel1_MouseMove(object sender, MouseEventArgs e)
{
   mouseCursor = e.Location;
   if (panelLocked) panel1.Invalidate();
}

using a second class level variable:

 Point mouseCursor = Point.Empty;

You call lockPanel(true) or lockPanel(false) as needed..

If you implement this directly you will notice some flicker. This goes away if you use a double-buffered Panel:

class DrawPanel : Panel
{
    public DrawPanel() { this.DoubleBuffered = true; }
}

This moves the crosshair over the Panels in a perfectly smooth way. You may want to turn on & off the Mouse cursor upon MouseLeave and MouseEnter..

enter image description here

TaW
  • 53,122
  • 8
  • 69
  • 111
2

Why don't you clone all the graphics in the ChartPanel over your CursorControl?

All the code here must be placed inside your CursorControl.

First, create a property which will hold a reference to the chart and hook to it's paint event, something like this:

ChartPanel panel;

public ChartPanel Panel
{ 
    get{ return panel; } 

    set{ 

         if(panel != null)
            panel.Paint -= CloneAspect;

         panel = value;

         panel.Paint += CloneAspect;
    }

}

Now define the CloneAspect function which will render the control's appearance to a bitmap whenever a Paint opperation has been done in the Chart panel:

Bitmap aspect;

void CloneAspect(object sender, PaintEventArgs e)
{

    if(aspect == null || aspect.Width != panel.Width || aspect.Height != panel.Height)
    {

         if(aspect != null)
            aspect.Dispose();

         aspect = new Bitmap(panel.Width, panel.Height, System.Drawing.Imaging.PixelFormat.Format32bppPArgb);

    }

    panel.DrawToBitmap(aspect, new Rectangle(0,0, panel.Width, panel.Height);

}

Then in the OnPaint overriden method do this:

public override void OnPaint(PaintEventArgs e)
{
    e.Graphics.DrawImage(aspect);

    //Now draw the cursor
    (...)
}

And finally wherever you create the chart and the customcursor you do:

CursorControl.Panel = ChartPanel;

And voila, you can redraw as many times you need without recalculating the chart's content.

Cheers.

Gusman
  • 14,905
  • 2
  • 34
  • 50
  • There is no reason to call `CreateGraphics()` in `OnPaint()` or a `Paint` event handler. The whole point of the `Paint` event is that the `Graphics` instance to which you are required to draw is passed to you. – Peter Duniho Dec 23 '14 at 19:32
  • Also, using the `Paint` event as the trigger to render the chart control to the bitmap doesn't really help; it means the rendering will be buffered for _every_ on-screen update, which can happen for a variety of reasons, not just the data changing (e.g. part of the window was exposed by an overlapping window, the window was restored or resized, etc.) – Peter Duniho Dec 23 '14 at 19:41
  • This is something I've tried just after I asked this question, but I ran into two issues here: 1. Panel.DrawToBitmap() triggers the Paint event of the ChartPanel again, so the expensive Painting is now done twice. 2. Paint() on the parent panel repaints even the CursorControl, so we end up with a recursion. This could be handled, but if the duplicate call can be somehow avoided, then I'd like to avoid it. – LukasH Dec 23 '14 at 20:25
  • @PeterDuniho yes, that is a big mistake, i'm a bit ill and my mind goes crazy sometimes :). LukasH, there should be no recursion if your control is placed over the chart (not inside), fills all it's area and it's opaque. – Gusman Dec 24 '14 at 12:14