Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

Pro Visual C++-CLI And The .NET 2.0 Platform (2006) [eng]-1

.pdf
Скачиваний:
70
Добавлен:
16.08.2013
Размер:
24.18 Mб
Скачать

498

C H A P T E R 1 1 G R A P H I C S U S I N G G D I +

Figure 11-18. The right side of a happy face

Optimizing GDI+

You have many ways to optimize GDI+. This section describes the most obvious and easiest-to- implement methods.

Did you notice something about your Paint event handler method in the previous example? It executed every line in itself even if it was only repainting a small sliver of the graphic display. Wouldn’t it be better and faster if only the parts of the Paint event handler method that need executing were executed? Let’s see how you can do this.

The first thing you have to figure out is how to let a draw or fill method know that it needs to be executed.

What do all the draw and fill routines have in common in the preceding example? They all have a bounding rectangle. This rectangle indicates the area that it is supposed to update. Okay, so you know the area each draw or fill method needs to update.

Rectangle Head = Drawing::Rectangle(125, 25, 250, 250);

g->FillEllipse(Brushes::Yellow, Head);

Next, you want to know if this area is the same as what needs to be updated on the drawing surface. Remember way back near the beginning of the chapter where I wrote that the PaintEventArgs parameter provides two pieces of information: the Graphics and the ClipRectangle? This clip rectangle is the area that needs to be updated.

Drawing::Rectangle ClipRect = pea->ClipRectangle;

You now have two rectangles: one that specifies where it will update and another that specifies where it needs to be updated. So by intersecting these two rectangles, you can figure out if the draw routine needs to be executed, because when the intersection is not empty you know that the draw or fill needs to be executed.

if (!(Rectangle::Intersect(ClipRect, Head)).IsEmpty)

{

//...Execute draw or fill method

}

The neat thing about this is that if you surround every draw and fill method with this comparison, when the Paint event handler is executed, only the draw or fill methods that need to be executed are.

C H A P T E R 1 1 G R A P H I C S U S I N G G D I +

499

There is one more wrinkle, though. The clip area is based on the client area and not the scroll area. This sounds familiar, doesn’t it? So you have to adjust the clip area by the negative of

AutoScrollPosition.

ClipRect.Offset(-AutoScrollPosition.X, -AutoScrollPosition.Y);

Why negative? You’re doing the exact opposite of what you did in the previous example. This time you’re moving the object on the drawing surface and keeping the drawing surface still. In the previous example, you kept the objects still and moved the drawing surface (well, it’s not really doing this but it’s easier to picture this way).

Listing 11-17 shows the scrollable happy face program with this optimization.

Listing 11-17. An Optimized Scrollable Happy Face

namespace

OptimizedHappyFace

{

 

using

namespace System;

using

namespace System::ComponentModel;

using

namespace System::Collections;

using

namespace System::Windows::Forms;

using

namespace System::Data;

using

namespace System::Drawing;

public ref class Form1 : public System::Windows::Forms::Form

{

public:

Form1(void)

{

InitializeComponent();

Head = Drawing::Rectangle(125, 25, 250, 250);

Mouth = Drawing::Rectangle(200, 175, 100, 50);

LEye = Drawing::Rectangle(200, 100, 25, 25);

REye = Drawing::Rectangle(275, 100, 25, 25);

b4pen = gcnew Pen(Color::Black, 4);

}

protected:

~Form1()

{

if (components)

{

delete components;

}

}

private:

System::ComponentModel::Container ^components;

System::Drawing::Rectangle Head;

System::Drawing::Rectangle Mouth;

System::Drawing::Rectangle LEye;

System::Drawing::Rectangle REye;

Pen^ b4pen;

500

C H A P T E R 1 1 G R A P H I C S U S I N G G D I +

#pragma region Windows Form Designer generated code void InitializeComponent(void)

{

this->SuspendLayout();

this->AutoScrollMinSize = System::Drawing::Size(400,400);

this->AutoScaleDimensions = System::Drawing::SizeF(6, 13); this->AutoScaleMode = System::Windows::Forms::AutoScaleMode::Font; this->ClientSize = System::Drawing::Size(292, 273);

this->Name = L"Form1";

this->Text = L"Optimized Happy Face"; this->Paint +=

gcnew System::Windows::Forms::PaintEventHandler(this, &Form1::Form1_Paint);

this->ResumeLayout(false);

}

#pragma endregion

private:

System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e)

{

Graphics^ g = e->Graphics;

Drawing::Rectangle ClipRect = e->ClipRectangle;

ClipRect.Offset(-AutoScrollPosition.X, -AutoScrollPosition.Y);

g->TranslateTransform((float)AutoScrollPosition.X, (float)AutoScrollPosition.Y);

if (!(Rectangle::Intersect(ClipRect, Head)).IsEmpty)

{

g->FillEllipse(Brushes::Yellow, Head); g->DrawEllipse(b4pen, Head);

if (!(Rectangle::Intersect(ClipRect, Mouth)).IsEmpty)

{

g->FillPie(Brushes::White, Mouth, 0, 180); g->DrawPie(b4pen, Mouth, 0, 180);

}

if (!(Rectangle::Intersect(ClipRect, LEye)).IsEmpty)

{

g->FillEllipse(Brushes::White, LEye); g->DrawEllipse(b4pen, LEye);

}

if (!(Rectangle::Intersect(ClipRect, REye)).IsEmpty)

{

g->FillEllipse(Brushes::White, REye); g->DrawEllipse(b4pen, REye);

}

}

}

};

}

C H A P T E R 1 1 G R A P H I C S U S I N G G D I +

501

Notice that in the code I threw in one more optimization in OptimizedHappyFace.exe. The Paint event handler method doesn’t draw the mouth or eyes if the head doesn’t need to be painted. I can do this because the mouth and eyes are completely enclosed within the head, so if the head doesn’t need painting, there’s no way that the mouth or eyes will either.

Double Buffering

Double buffering is the technique of using a secondary off-screen buffer to render your entire screen image. Then, in one quick blast, you move the completed secondary buffer onto your primary onscreen form or control.

The use of double buffering speeds up the rendering process and makes image movement much smoother by reducing flickering. Let’s give the happy face some life and let it slide repeatedly across the form.

Unbuffer Method

The first example in Listing 11-18 shows how you can implement this without double buffering. (There are other ways of doing this—some of them are probably more efficient.) There is nothing new in the code. You start by creating a Timer and telling it to invalidate the form each time it is triggered. Then you render the happy face repeatedly, shifting it over to the right and slowing it by changing the origin with the TranslateTransform() method. When the happy face reaches the end of the screen, you reset the happy face back to the left and start again.

Listing 11-18. Sliding the Happy Face the Ugly Way

namespace

SingleBuffering

{

 

using

namespace System;

using

namespace System::ComponentModel;

using

namespace System::Collections;

using

namespace System::Windows::Forms;

using

namespace System::Data;

using

namespace System::Drawing;

public ref class Form1 : public System::Windows::Forms::Form

{

public:

Form1(void)

{

InitializeComponent();

X = -250; // Preset to be just left of window

}

protected:

~Form1()

{

if (components)

{

delete components;

}

}

502

C H A P T E R 1 1 G R A P H I C S U S I N G G D I +

private:

System::Windows::Forms::Timer^ timer1; System::ComponentModel::IContainer^ components;

float X;

// Actual x coordinate of Happy face

#pragma region Windows Form Designer generated code

void InitializeComponent(void)

{

this->components = (gcnew System::ComponentModel::Container()); this->timer1 =

(gcnew System::Windows::Forms::Timer(this->components)); this->SuspendLayout();

//

//timer1

this->timer1->Enabled = true; this->timer1->Interval = 10;

this->timer1->Tick +=

gcnew System::EventHandler(this, &Form1::timer1_Tick);

//Form1

//

this->AutoScaleDimensions = System::Drawing::SizeF(6, 13); this->AutoScaleMode = System::Windows::Forms::AutoScaleMode::Font; this->ClientSize = System::Drawing::Size(500, 300);

this->Name = L"Form1"; this->Text = L"Form1"; this->Paint +=

gcnew System::Windows::Forms::PaintEventHandler(this, &Form1::Form1_Paint);

this->ResumeLayout(false);

}

#pragma endregion private:

System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e)

{

Graphics^ g = e->Graphics;

// Move image at end of line start from beginning if (X < ClientRectangle.Width)

X += 1.0;

else

X = -250.0; g->TranslateTransform(X, 25.0);

// redraw images from scratch

Pen^ b4pen = gcnew Pen(Color::Black, 4);

C H A P T E R 1 1 G R A P H I C S U S I N G G D I +

503

Drawing::Rectangle Head = Drawing::Rectangle(0, 0, 250, 250);

g->FillEllipse(Brushes::Yellow, Head);

g->DrawEllipse(b4pen, Head);

Drawing::Rectangle Mouth = Drawing::Rectangle(75, 150, 100, 50);

g->FillPie(Brushes::White, Mouth,0,180);

g->DrawPie(b4pen, Mouth, 0, 180);

Drawing::Rectangle LEye = Drawing::Rectangle(75, 75, 25, 25);

g->FillEllipse(Brushes::White, LEye);

g->DrawEllipse(b4pen, LEye);

Drawing::Rectangle REye = Drawing::Rectangle(150, 75, 25, 25);

g->FillEllipse(Brushes::White, REye);

g->DrawEllipse(b4pen, REye);

delete b4pen;

}

System::Void timer1_Tick(System::Object^ sender, System::EventArgs^ e)

{

// Move the image Invalidate();

}

};

}

When you run SingleBuffering.exe, you will see a rather ugly, flickering happy face sort of sliding across the screen. If you have a superpowered computer with a great graphics card, then the flickering may not be that bad, or it may be nonexistent. My computer is actually on the high end graphically, and it still looks kind of pathetic.

Double Buffer Method

I change as little of the original code as possible in the double buffering example in Listing 11-19, which should enable you to focus on only what is needed to implement double buffering.

As the technique’s name suggests, you need an extra buffer. Creating one is simple enough:

dbBitmap = gcnew Bitmap(ClientRectangle.Width, ClientRectangle.Height);

We have not covered the Bitmap class. But for the purposes of double buffering, all you need to know is that you create a bitmap by specifying its width and height. If you want to know more about the Bitmap class, the .NET Framework documentation is quite thorough.

If you recall, though, you don’t call draw and fill methods from a bitmap—you need a Graphics class. Fortunately, it’s also easy to extract the Graphics class out of a bitmap:

dbGraphics = Graphics::FromImage(dbBitmap);

Now that you have a Graphics class, you can clear, draw, and fill it just like you would a formoriginated Graphics class:

dbGraphics->FillEllipse(Brushes::Yellow, Head); dbGraphics->DrawEllipse(b4pen, Head);

504

C H A P T E R 1 1 G R A P H I C S U S I N G G D I +

So how do you implement a double buffer? The process is pretty much the same as for a single buffer, except that instead of drawing to the display device directly, you draw to the buffer. Once the image is complete, you copy the completed image to the display device. Notice you copy the image or buffer and not the graphic.

e->Graphics->DrawImageUnscaled(dbBitmap, 0, 0);

The reason double buffering is faster than single buffering is because writing to memory is faster than writing to the display device. Flickering is not an issue because the image is placed in its complete state onto the screen. There is no momentary delay, as the image is being built in front of your eyes.

Listing 11-19 shows the changes needed to implement double buffering. I don’t claim this is the best way to do it. The goal is to show you what you can do using GDI+.

Listing 11-19. Sliding a Happy Face Double Buffer Style

namespace

DoubleBuffering

{

 

using

namespace System;

using

namespace System::ComponentModel;

using

namespace System::Collections;

using

namespace System::Windows::Forms;

using

namespace System::Data;

using

namespace System::Drawing;

public ref class Form1 : public System::Windows::Forms::Form

{

public:

Form1(void)

{

InitializeComponent(); this->SetStyle(ControlStyles::Opaque, true);

dbBitmap = nullptr; dbGraphics = nullptr;

X = -250; // Preset to be just left of window

Form1_Resize(nullptr, EventArgs::Empty);

}

protected:

~Form1()

{

if (components)

{

delete components;

}

}

private:

System::Windows::Forms::Timer^ timer1; System::ComponentModel::IContainer^ components;

Bitmap^

dbBitmap;

Graphics^

dbGraphics;

int X;

// Actual x coordinate of Happy face

C H A P T E R 1 1 G R A P H I C S U S I N G G D I +

505

#pragma region Windows Form Designer generated code

void InitializeComponent(void)

{

this->components = (gcnew System::ComponentModel::Container()); this->timer1 =

(gcnew System::Windows::Forms::Timer(this->components)); this->SuspendLayout();

//

//timer1

this->timer1->Enabled = true; this->timer1->Interval = 10;

this->timer1->Tick +=

gcnew System::EventHandler(this, &Form1::timer1_Tick);

//Form1

//

this->AutoScaleDimensions = System::Drawing::SizeF(6, 13); this->AutoScaleMode = System::Windows::Forms::AutoScaleMode::Font; this->ClientSize = System::Drawing::Size(500, 300);

this->Name = L"Form1";

this->Text = L"Sliding Happy Face"; this->Paint +=

gcnew System::Windows::Forms::PaintEventHandler(this, &Form1::Form1_Paint);

this->Resize +=

gcnew System::EventHandler(this, &Form1::Form1_Resize); this->ResumeLayout(false);

}

#pragma endregion

private:

System::Void Form1_Paint(System::Object^ sender, System::Windows::Forms::PaintEventArgs^ e)

{

// Move image at end of line start from beginning if (X < ClientRectangle.Width)

{

X ++;

dbGraphics->TranslateTransform(1.0, 0.0);

}

else

{

X = -250; dbGraphics->TranslateTransform(

(float)-(ClientRectangle.Width+250), 0.0);

}

// Clear background dbGraphics->Clear(Color::White);

506 C H A P T E R 1 1 G R A P H I C S U S I N G G D I +

// redraw image from scratch

Pen^ b4pen = gcnew Pen(Color::Black, 4);

Drawing::Rectangle Head = Drawing::Rectangle(0, 0, 250, 250); dbGraphics->FillEllipse(Brushes::Yellow, Head); dbGraphics->DrawEllipse(b4pen, Head);

Drawing::Rectangle Mouth = Drawing::Rectangle(75, 150, 100, 50); dbGraphics->FillPie(Brushes::White, Mouth,0,180); dbGraphics->DrawPie(b4pen, Mouth, 0, 180);

Drawing::Rectangle LEye = Drawing::Rectangle(75, 75, 25, 25); dbGraphics->FillEllipse(Brushes::White, LEye); dbGraphics->DrawEllipse(b4pen, LEye);

Drawing::Rectangle REye = Drawing::Rectangle(150, 75, 25, 25); dbGraphics->FillEllipse(Brushes::White, REye); dbGraphics->DrawEllipse(b4pen, REye);

// Make the buffer visible e->Graphics->DrawImageUnscaled(dbBitmap, 0, 0);

delete b4pen;

}

System::Void Form1_Resize(System::Object^ sender, System::EventArgs^ e)

{

// Get rid of old stuff if (dbGraphics != nullptr)

{

delete dbGraphics;

}

if (dbBitmap != nullptr)

{

delete dbBitmap;

}

if (ClientRectangle.Width > 0 && ClientRectangle.Height > 0)

{

// Create a bitmap

dbBitmap = gcnew Bitmap(ClientRectangle.Width, ClientRectangle.Height);

// Grab its Graphics

dbGraphics = Graphics::FromImage(dbBitmap);

// Set up initial translation after resize (also at start) dbGraphics->TranslateTransform((float)X, 25.0);

}

}

C H A P T E R 1 1 G R A P H I C S U S I N G G D I +

507

System::Void timer1_Tick(System::Object^ sender, System::EventArgs^ e)

{

// Move the image Invalidate();

}

};

}

Let’s take a look at some of the changes that were needed. I already mentioned the building of a bitmap, so I’ll skip that.

The first difference is that you have to handle the resizing of the form. The reason you must do this is because the secondary off-screen buffer needs to have the same dimensions as the primary on-screen buffer. When a form is resized, the primary buffer changes size; therefore you need to change the secondary buffer.

Notice also that you delete the Graphics class and the Bitmap class. Both of these classes use a lot of resources between them, and disposing of the old one before the new releases those resources. You need to check to make sure they have been initialized, because the first time this method is run they have not been initialized. Also, when the form is minimized you get rid of the buffer, so when the form is expanded you need to build the buffer again.

this->Resize += gcnew System::EventHandler(this, &Form1::Form1_Resize); //...

System::Void Form1_Resize(System::Object^ sender, System::EventArgs^ e)

{

// Get rid of old stuff

if (dbGraphics != nullptr)

{

delete dbGraphics;

}

if (dbBitmap != nullptr)

{

delete dbBitmap;

}

if (ClientRectangle.Width > 0 && ClientRectangle.Height > 0)

{

// Create a bitmap

dbBitmap = gcnew Bitmap(ClientRectangle.Width,ClientRectangle.Height); // Grab its Graphics

dbGraphics = Graphics::FromImage(dbBitmap);

// Set up initial translation after resize (also at start) dbGraphics->TranslateTransform((float)X, 25.0);

}

}

You need to call the Resize event handler yourself (or write some duplicate code) before the Paint event is called the first time, in order to initialize dbBitmap and dbGraphics. I call the method in the constructor:

Form1_Resize(nullptr, EventArgs::Empty);

If you don’t, the Paint event handler will throw a System.NullReferenceException when it first encounters dbGraphics.

The next difference is an important one. It is the setting of the style of the form to opaque. What this does is stop the form from clearing itself when it receives Invalidate().

SetStyle(ControlStyles::Opaque, true);