Not sure about the above comment on using rectangles, but we use double buffering. In a nutshell, you create a Bitmap (with a size of what you need, and in your case it would be the size of the panel). Once created, create a Graphics object from the Bitmap. At this point your have now created an offscreen buffer.
Rendering:
For all drawing calls (DrawString, etc) in your OnPaint method, use the graphics object from the Bitmap that you created. At this moment, you are drawing into memory and not the screen.
Once the drawing is done, you copy the offscreen buffer to the screen. To do this, use the DrawImage method of the Graphics object that was passed to the OnPaint method. The parameter to this call is the Bitmap that was created for the offscreen buffer.
Why does this work?
The flickering that you are seeing is called "Tearing". Your eye is catching the actual drawing to the screen. The double buffer limits this by doing all the drawing to memory, and when it's done, it copies it to screen in 1 call.
Hope this helps!