简体   繁体   中英

Existing Graphics into Bitmap

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.

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 ..

在此处输入图片说明

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.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM