Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
menu search
person
Welcome To Ask or Share your Answers For Others

Categories

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.

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
217 views
Welcome To Ask or Share your Answers For Others

1 Answer

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


与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
thumb_up_alt 0 like thumb_down_alt 0 dislike
Welcome to ShenZhenJia Knowledge Sharing Community for programmer and developer-Open, Learning and Share
...