Windows Forms 2.0 Programming (Microsoft .NET Development Series)

If you're drawing using page units, transformations, and regions, it's likely that you're seriously into drawing. If that's the case, you'll be interested in ways to optimize your drawings for responsiveness and smooth operation. First and foremost, you should avoid drawing anything that doesn't need drawing. You can do that in one of two ways: redraw only what needs to be redrawn, or don't draw unnecessarily in the first place.

First, invalidate only the portion of your drawing surface that needs to be refreshed. In other words, when drawing the internal state of your form or control, don't invalidate the entire thing if only a small part of the state has changed:

float[] lotsOfNumbers; Region GetRegionWhereNumberIsShown(int number) { ... } public float OneNumber { set { lotsOfNumbers[1] = value; // Don't do this: this.Invalidate(); // Do this: this.Invalidate(GetRegionWhereNumberIsShown(1)); } }

The Invalidate function takes an optional rectangle or region as the first argument, so you must invalidate only the portion that needs redrawing, not the entire client area. Now, when the Paint event is triggered, all drawing outside the invalid rectangle is ignored:

void NumbersForm_Paint(object sender, PaintEventArgs e) { for( int i = 0; i != lotsOfNumbers.Length; ++i ) { DrawNumber(g, i); // Will draw only in invalid rectangle } }

Also, there's an optional second argument that says whether to invalidate children. If the state of your children doesn't need updating, don't invalidate.

What's even better than having drawing operations ignored for efficiency? Not drawing at all. Sometimes, the client area is too small to show all of the state.[6] When that happens, there's no need to draw something that lies entirely outside the visible clip region. To determine whether that's the case, you can use the IsVisible method of the Graphics object, which checks to see whether a point or any part of a rectangle is visible in the current clipped region:

[6] This often involves scrolling, which is covered in Chapter 10: Controls.

Rectangle GetNumberRectangle(int i) { ... } void DrawNumber(Graphics g, int i) { // Avoid something that takes a long time to draw if( !g.IsVisible(GetNumberRectangle(i)) ) return; // Draw something that takes a long time... }

Be careful when calculating the region to invalidate or checking to see whether a hunk of data is in the invalid region; it may take more cycles to do the checking than it does to simply do the drawing. As always, when performance is what you're after, your best bet is to profile various real-world scenarios.

Double Buffering

Another way to make your graphics-intensive programs come out sweet and nice is to eliminate flicker. Flicker is a result of the three-phase painting process Windows employs to render a form, where each phase renders directly to the screen. When flickering occurs, you are seeing the rendering results of each phase in quick succession. The first phase erases the invalid region by painting it with a Windows-level background brush. The second phase sends the PaintBackground event for your form or control to paint the background, something that your base class generally handles for you using the BackColor and BackgroundImage properties. But you can handle it yourself:

// There is no PaintBackground event, only this virtual method protected override void OnPaintBackground(PaintEventArgs e) { // Make sure to paint the entire client area or call the // base class, or else you'll have stuff from below showing through // base.OnPaintBackground(e); e.Graphics.FillRectangle(Brushes.Black, this.ClientRectangle); }

The third and final phase of painting is the Paint event handler itself.

Double buffering is a technique by which you can combine the three phases into a single paint operation and thereby eliminate flicker. To make this work, you apply the three painting phases to a second, internally managed graphics buffer, and, when they're all finished, they're rendered to the screen in one fell swoop. You can enable double buffering in a form or a control by setting the AllPaintingInWmPaint and OptimizedDoubleBuffer styles from the System.Windows.Forms.ControlStyles enumeration to true:[7]

[7] The OptimizedDoubleBuffer style replaces the DoubleBuffer style from previous versions of .NET. You should avoid the DoubleBuffer style except for backwards compatibility.

// Form1.cs partial class Form1 { public Form1() { InitializeComponent(); // Enable double buffering this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true); this.SetStyle(ControlStyles.AllPaintingInWmPaint, true); } ... }

To save time, you can implement double buffering by opening your form in the Windows Forms Designer and setting its DoubleBuffered property to true from the Properties window.[8] DoubleBuffered is false by default and is implemented by the base Control class and marked with the protected modifier, so only classes that derive from Control can set itunless, like Form and UserControl, they shadow it. Consequently, you should set DoubleBuffered to true on all custom controls and user controls to ensure that double buffering is enabled:

[8] You can do the same for user controls from the UserControl Designer.

public partial class CustomControl : Control { public CustomControl() { InitializeComponent(); // Enable double buffering: equivalent to setting // AllPaintingInWmPaint and OptimizedDoubleBuffer // control styles base.DoubleBuffered = true; } }

Manual Double Buffering

Requesting double buffering using either ControlStyles or the DoubleBuffered property is an all-or-nothing approach; each paint operation creates a new buffer, renders to it, renders from the buffer to the screen, and releases the buffer. The more intensive your rendering requirements are, the more likely it is that you'll demand more fine-grained control and flexibility from double buffering. When animating, for example, you probably prefer to retain your double buffer across paintsrather than create and dispose of each paint operationand thus avoid costly memory allocation.

For this, you can do as ControlStyles.OptimizedDoubleBuffer does and use buffered graphics support from System.Drawing. In most cases, you create a buffered graphics context from which you allocate one or more buffered graphics drawing surfaces, each of which represents a graphics surface to which you'll render.

The buffered graphics context is actually the off-screen buffer that you render to. It is exposed via System.Drawing.BufferedGraphicsContext:

namespace System.Drawing { sealed class BufferedGraphicsContext : IDisposable { // Constructor BufferedGraphicsContext(); // Properties Size MaximumBuffer { get; set; } // Methods BufferedGraphics Allocate( Graphics targetGraphics, Rectangle targetRectangle); BufferedGraphics Allocate(IntPtr targetDC, targetRectangle); void Invalidate(); } }

Your first step is to instantiate BufferedGraphicsContext and specify the size of the offscreen buffer using the MaximumBuffer property:

// MainForm.cs partial class MainForm : Form { // Keep buffered graphics context open across method calls // and event handling BufferedGraphicsContext bufferContext; ... public MainForm() { InitializeComponent(); // Allocate the buffer context for a maximum desired size bufferContext = new BufferedGraphicsContext(); bufferContext.MaximumBuffer = this.ClientRectangle.Size; // Animate the gif, if possible if( ImageAnimator.CanAnimate(gif) ) { ImageAnimator.Animate(gif, gif_FrameChanged); } } void gif_FrameChanged(object sender, EventArgs e) { ... } }

After you've created the off-screen graphics buffer, you create a Graphics object that allows you to render to it. You also specify the target graphics surface that your buffered graphics will ultimately render to. Both needs are satisfied by calling BufferedGraphicsContext's Allocate method:

// MainForm.cs class MainForm : Form { // Keep buffered graphics context open across method calls // and event handling BufferedGraphicsContext bufferContext; ... public MainForm() { ... } void gif_FrameChanged(object sender, EventArgs e) { // Create a graphics buffer drawing surface and associate it // with the target graphics surface, which is the host form's // drawing surface in this example Graphics g = this.CreateGraphics(); using( BufferedGraphics frame = bufferContext.Allocate(g, this.ClientRectangle) ) { ... } } ... }

As you can see, Allocate returns a BufferedGraphics object instance:

namespace System.Drawing { sealed class BufferedGraphics : IDisposable { // Constructor static BufferedGraphics(); // Properties public Graphics Graphics { get; } // Methods public void Render(); public void Render(Graphics target); public void Render(IntPtr targetDC); } }

Using the BufferedGraphics instance is a two-step process. First, you paint to the offscreen buffer, using the Graphics object that you acquire from the Graphics property. Then, you call the Render method to blast the bits from your off-screen buffer to the target drawing surface. Both steps are shown here:

void gif_FrameChanged(object sender, EventArgs e) { // Create a graphics buffer drawing surface and associate it // with the target graphics surface, which is the host form's // drawing surface in this example Graphics g = this.CreateGraphics(); using( BufferedGraphics frame = bufferContext.Allocate(g, this.ClientRectangle) ) { // Get next gif frame ImageAnimator.UpdateFrames(gif); // Render to buffered graphics frame.Graphics.DrawImage(gif, this.ClientRectangle); // Render buffered graphics to target drawing surface frame.Render(); } }

By creating a BufferedGraphics object instance, you avoid the effort involved in recreating a new off-screen graphics buffer for every paint cycle. Notice that the BufferedGraphics object is actually created within a using block to ensure that system drawing resources are disposed of as soon as possible. You should also remember to dispose of your BufferedGraphicsContext instance:

void AnimationBufferingForm_FormClosing( object sender, FormClosingEventArgs e) { // Release outstanding system drawing resources bufferContext.Dispose(); }

Double Buffering Performance Considerations

Although double buffering (without the initial erasing of the background) can make all the difference in the user experience, double buffering requires enough memory to capture the entire visible region at the current color quality. At 32 bits per pixel, a 200 x 200 region requires 156 K in additional memory per drawing operation for that region. In memoryconstrained systems, this extra memory usage could degrade instead of improve the user experience.

Other Drawing Options

There are a few other drawing-related ControlStyles you may be interested in:

namespace System.Drawing { enum ControlStyles { ... // Drawing-related control styles UserPaint = 2, // Control that paints itself specially Opaque = 4, // OnPaintBackground skipped, Paint draws // the entire client area ResizeRedraw = 16, // Invalidate entire client area on resize SupportsTransparentBackColor = 2048, // Simulated transparent // controls AllPaintingInWmPaint = 8192, // Collapse drawing phases into // Paint event OptimizedDoubleBuffer = 131072, // Hide drawing until Paint // event returns } }

For example, it's common for controls that need double buffering to want to automatically redraw when they're resized. For this, you use the ResizeRedraw style:

// Form1.cs partial class Form1 { public Form1() { InitializeComponent(); // Double buffering this.DoubleBuffered = true; // Redraw when resized this.SetStyle(ControlStyles.ResizeRedraw, true); } ... }

The ControlStyles settings apply at the point where Windows Forms starts wrapping the functionality of Windows itself, which is the Control base class (Forms ultimately derive from Control). Several of the ControlStyles settings have nothing to do with drawing but rather govern how the Control class interacts with the underlying operating system. For more information, see the reference documentation for the ControlStyles enumeration.

Категории