Friday, November 13, 2009

6.4 Using Threads in GUI Applications



< BACK  NEXT >

[oR]


6.4
Using Threads in GUI Applications


The book entitled Visual C++: Developing Professional Applications for Windows 98 and NT using MFC, by Marshall Brain and Lance Lovette (ISBN 0-13-085121-3), shows how to use C++ and the Microsoft Foundation Class library to create GUI applications for Windows. In this section we will look at two different ways to use threads in a typical GUI application.



Listing 6.6 contains the code for a simple Mandelbrot set program that does not use threads. See Appendix A for compilation instructions. When you run this program, you can choose the Draw option in the File menu to cause the window to redraw. Unless you are working with a very zippy machine, do not initially make the window any larger than its default size. Now resize the window so that redrawing takes between 10 and 15 seconds.





Listing 6.6 A simple Mandelbrot set program created with the MFC class hierarchy and C++


// mandel0.cpp

#include <afxwin.h>
#include "menus.h"
#define NUM_ITERATIONS 64

const double left = -1.0;
const double right = 1.0;
const double top = -1.0;
const double bottom = 1.0;

DWORD colors[64];

typedef struct
{
double real;
double imag;
} complex;

// Define the application object class
class CManApp : public CWinApp
{
public:
virtual BOOL InitInstance();
};

// Define the edit window class
class CManWindow : public CFrameWnd
{
public:
CManWindow();
void RunMandel();
void SetPix(int x, int y, WORD iter);
afx_msg void OnPaint();
afx_msg void OnDoDraw();
afx_msg void OnExit();
DECLARE_MESSAGE_MAP()
};

// Create an instance of the application object
CManApp manApp;

// member function used to set pixel colors in
// the window
void CManWindow::SetPix(int x, int y, WORD iter)
{
CClientDC dc(this);
dc.SetPixel(x, y, colors[iter]);
}

// member function used to redraw the
// mandelbrot set
void CManWindow::RunMandel()

{
CRect r;
double xstep, ystep;
double x, y;
int i,j;
WORD iter;
complex k;
complex z;
double real, imag, spread;

GetClientRect(&r);

ystep = (double) (bottom - top) / r.Height();
xstep = (double) (right - left) / r.Width();

for (y=top, j=0; y <= bottom; y += ystep, j++)
{
for (x=left, i=0; x<=right; x += xstep, i++)
{
k.real = x;
k.imag = y;
z.real=z.imag=0.0;

for (iter=0; iter<NUM_ITERATIONS-1;
iter++)
{
real = z.real + k.real;
imag = z.imag + k.imag;
z.real = real * real -
imag * imag;
z.imag = 2 * real * imag;
spread = z.real * z.real +
z.imag * z.imag;
if (spread > 4.0)
break;
}
((CManWindow *)manApp.m_pMainWnd)->
SetPix(i, j, iter);
}
}
}

// The message map
BEGIN_MESSAGE_MAP(CManWindow, CFrameWnd)
ON_WM_PAINT()
ON_COMMAND(IDM_DODRAW, OnDoDraw)
ON_COMMAND(IDM_EXIT, OnExit)
END_MESSAGE_MAP()

// Handler for the Draw menu option
void CManWindow::OnDoDraw()
{
// clear the window
CClientDC dc(this);
CRect r;
GetClientRect(&r);
dc.PatBlt(0, 0, r.Width(), r.Height(),
WHITENESS);
// Redraw the set
RunMandel();
}

// Handler for WM_PAINT messages
void CManWindow::OnPaint()
{
// Do not do anything in response to
// paint events
ValidateRect(NULL);
}

// Handler for the Exit menu option
void CManWindow::OnExit()
{
DestroyWindow();
}

// CManWindow constructor
CManWindow::CManWindow()
{
WORD x;
BYTE red=0, green=0, blue=0;

Create( NULL, ONormal Mandel ExampleO,
WS_OVERLAPPEDWINDOW,
CRect(0,0,150,150), NULL, OMainMenuO );
for (x=0; x<64; x++)
{
colors[x] = RGB(red, green, blue);
if (!(red += 64))
if (!(green += 64))
blue += 64;
}
colors[63] = RGB(255,255,255);
}

// Initialize the CManApp m_pMainWnd data member
BOOL CManApp::InitInstance()
{
m_pMainWnd = new CManWindow();
m_pMainWnd -> ShowWindow( m_nCmdShow );
m_pMainWnd -> UpdateWindow();

return TRUE;
}




Listing 6.7 The menus.h file used by Listing 6.6


// menus.h

#define IDM_DODRAW 1001
#define IDM_EXIT 1002




Listing 6.8 The menus.rc resource file used to create the menus for Listing 5.6


// mandel.rc

#include <windows.h>
#include <afxres.h>

#include "menus.h"

MainMenu MENU
{
POPUP "&File"
{
MENUITEM "&Draw", IDM_DODRAW
MENUITEM "E&xit", IDM_EXIT
}
}


When you run the code shown in Listing 6.6, you will notice a serious problem with the application. Once the user selects the Draw option from the menu, the user cannot do anything, because the redrawing code prevents the event loop from running. The user cannot use the menu, quit the program, or minimize the program until the redrawing step completes. If the user does happen to click on anything, the event queue stores the clicks and plays them all rapidly once the redraw completes. If the application's window is full-screen and the machine is slow, it may be several minutes before a redraw completes and therefore before the user can do anything with the application.



The situation presented here is common in any program that creates sophisticated displays. For example, a CAD/CAM program may have to extract 10,000 vectors from a database to refresh the screen for a complex drawing. Mapping programs, three-dimensional rendering programs, complex mathematical visualization programs, and others of the sort, all suffer from the same problem.



Threads offer an easy solution to this problem. By placing all redrawing activity in one thread, you allow the user to continue to use the program while redrawing takes place in the background. From the user's standpoint, this is a major improvement over being locked out of the interface. From a programmer's standpoint, the thread solution is significantly easier to work with than the OnIdle capability that MFC provides for solving this problem in non-threaded environments.



Listing 6.9 contains a version of the Mandelbrot program that has a separate thread to handle redrawing. It works just like the code in Listing 6.6, except that the menus will function properly during redrawing. You can use the Exit option to terminate the program even while redrawing is taking place.





Listing 6.9 A multi-threaded version of the Mandelbrot program that uses a thread to handle redrawing


// mandel1.cpp

#include <afxwin.h>
#include "menus.h"

#define NUM_ITERATIONS 64

const double left = -1.0;
const double right = 1.0;
const double top = -1.0;
const double bottom = 1.0;

DWORD colors[64];

typedef struct
{
double real;
double imag;
} complex;

typedef struct
{
WORD height;
WORD width;
} mandelParams;

// Define the application object class
class CManApp : public CWinApp
{
public:
virtual BOOL InitInstance();
};

// Define the edit window class
class CManWindow : public CFrameWnd
{
private:
HANDLE threadHandle;
mandelParams params;
public:
CManWindow();
void RunMandel();
void SetPix(int x, int y, WORD iter);
afx_msg void OnPaint();
afx_msg void OnDoDraw();
afx_msg void OnExit();
DECLARE_MESSAGE_MAP()
};

// Create an instance of the application object
CManApp manApp;

// member function used to set pixel colors
// in the window
void CManWindow::SetPix(int x, int y, WORD iter)
{
CClientDC dc(this);
dc.SetPixel(x, y, colors[iter]);
}

// the thread function which does the drawing
DWORD MandelThread(mandelParams *params)
{
double xstep, ystep;
double x, y;
int i,j;
WORD iter;
complex k;
complex z;
double real, imag, spread;

ystep = (double) (bottom - top) /
params->height;
xstep = (double) (right - left) / params->width;
for (y=top, j=0; y <= bottom; y += ystep, j++)
{
for (x=left, i=0; x<=right; x += xstep, i++)
{
k.real = x;
k.imag = y;
z.real=z.imag=0.0;

for (iter=0; iter<NUM_ITERATIONS-1;
iter++)
{
real = z.real + k.real;
imag = z.imag + k.imag;
z.real = real * real -
imag * imag;
z.imag = 2 * real * imag;
spread = z.real * z.real +
z.imag * z.imag;
if (spread > 4.0)
break;
}
((CManWindow *)manApp.m_pMainWnd)->
SetPix(i, j, iter);
}
}
return(0);
}

// member function used to instigate
// the drawing thread
void CManWindow::RunMandel()
{
DWORD threadID;
CRect r;

GetClientRect(&r);
params.height=r.Height();
params.width=r.Width();

threadHandle=CreateThread(NULL, 0,
(LPTHREAD_START_ROUTINE) MandelThread,
&params, 0, &threadID);
}

// The message map
BEGIN_MESSAGE_MAP(CManWindow, CFrameWnd)
ON_WM_PAINT()
ON_COMMAND(IDM_DODRAW, OnDoDraw)
ON_COMMAND(IDM_EXIT, OnExit)
END_MESSAGE_MAP()

// Handler for the Start/Stop menu option
void CManWindow::OnDoDraw()
{
DWORD threadStatus;
BOOL status;

status = GetExitCodeThread(threadHandle,
&threadStatus);
if (threadStatus == STILL_ACTIVE)
{
// stop the existing thread
TerminateThread(threadHandle, 0);
CloseHandle(threadHandle);
}
// clear the window
CClientDC dc(this);
CRect r;
GetClientRect(&r);
dc.PatBlt(0, 0, r.Width(), r.Height(),
WHITENESS);
// redraw
RunMandel();
}

// Handler for WM_PAINT messages
void CManWindow::OnPaint()
{
ValidateRect(NULL);
}

// Handler for the Exit menu option
void CManWindow::OnExit()
{
CloseHandle(threadHandle);
DestroyWindow();
}

// CManWindow constructor
CManWindow::CManWindow()
{
WORD x;
BYTE red=0, green=0, blue=0;

Create( NULL, OThreaded Mandel ExampleO,
WS_OVERLAPPEDWINDOW,
CRect(0,0,150,150), NULL, OMainMenuO );
for (x=0; x<64; x++)
{
colors[x] = RGB(red, green, blue);
if (!(red += 64))
if (!(green += 64))
blue += 64;
}
colors[63] = RGB(255,255,255);
}

// Initialize the CManApp m_pMainWnd data member
BOOL CManApp::InitInstance()
{
m_pMainWnd = new CManWindow();
m_pMainWnd -> ShowWindow( m_nCmdShow );
m_pMainWnd -> UpdateWindow();

return TRUE;
}


If you compare the code in Listing 6.6 and Listing 6.9, you will find only minor differences. First, the OnDoDraw function in Listing 6.9 must check to see if a redrawing thread is active before it starts a new one. The GetExitCodeThread function is used here, but the WaitForSingleObject function seen in the previous section would work just as well. If the OnDoDraw function finds an active thread, it kills the thread. It then clears the window and calls RunMandel, just as Listing 6.6 does.



The RunMandel function now has two pieces. The first piece, RunMandel itself, sets up a parameter structure and calls CreateThread to start the thread function. The thread function is called MandelThread, and it contains all of the actual redrawing code. The code is not any different from the redrawing code seen in Listing 6.6, except that the threaded version takes its width and height from the parameter structure.



You can see here that the addition of a thread to an MFC program is fairly straightforward. In any application that you create, you can place the redrawing step in the background by following the same pattern seen in this code. It is also easy to place any sort of recalculation code in a thread using the same techniques.





< BACK  NEXT >


No comments:

Post a Comment