There is often some difficulty integrating the DirectX APIs with the Microsoft Foundation Classes (MFC). The DirectX SDK samples provide several samples using DirectX with MFC but none of them use the desired document/view architecture nor the application and class wizards. This paper takes the reader step by step setting up a simple DirectX application in the MFC framework using the wizards and the document/view architecture. It is assumed that the reader has some experience with both Direct3D and MFC but not both of them together.
The project we will build is simple; we will load a teapot into a mesh data structure and rotate it about the y-axis. The final rendering will look something like the picture below but will be animated.
[size="5"]Creating the Project and Linking the Libraries
This paper will move at a slow pace and attempt to walk through the contents one step at a time. We will start by creating a new project in Visual Studio. Select MFC AppWizard (exe) as shown in figure (1). Enter a project name such as "Direct3DMFC". Then press the OK button.
On the next screen, select the radio button specifying Single document and make sure the Document/View architecture support check box is checked. See figure (2).
At this point we can simply select the Finish button. This, of course, is all basic knowledge to anyone who uses Visual Studio but is included for completeness.
The next task we need to do is link the DirectX library files with out project. This is done in the same way as a regular (non-MFC) win32 application. Select Project from the menu, then Settings. Once the settings dialog is displayed, select the Link tab. Next, enter the DirectX library files you wish to add to the linker in the edit box labeled Object/library modules. See figure (3).
Note that we also include the multimedia library for some timer functions we use as part of the animation. Also remember that you need to have Visual Studio configured to search the directories where the DirectX library files are.
[size="5"]Defining the Direct3D Base Class
In an attempt to hide or encapsulate the Direct3D code from the MFC code we are going to create a simply base class. The implementation of this base class is up to you. The class Graphics is defined as follows:
class Graphics
{
public:
Graphics();
virtual ~Graphics();
bool create(
HWND hwnd,
int width,
int height,
bool windowed);
IDirect3DDevice8* getDevice();
virtual bool init() = 0;
virtual bool resize(int width, int height) = 0;
virtual bool update(float timeDelta) = 0;
virtual bool render() = 0;
protected:
IDirect3D8* _d3d8;
IDirect3DDevice8* _device;
};
The [font="Courier New"][color="#000080"]init[/color][/font] method is for you to fill out your custom set up, depending on the specifics of your application. Most likely you will do preprocessing here and set up your starting out matrices and other various states that don't need to be set on a frame-by-frame basis.
The [font="Courier New"][color="#000080"]resize[/color][/font] method is called whenever the window, associated with the Direct3D device, is changed. Again what goes on here depends on the specifics of your application, but generally you will simply rebuild your projection matrix.
In the [font="Courier New"][color="#000080"]update[/color][/font] method you will perform operations that need to by done on a frame-by-frame basis. This includes updating the camera position, animation, and collision detection.
Finally, in the [font="Courier New"][color="#000080"]render[/color][/font] method you perform all your draw calls.
[size="5"]Integrating Direct3D With MFC
To integrate Direct3D with MFC we have our [font="Courier New"][color="#000080"]CView[/color][/font] class inherit from [font="Courier New"][color="#000080"]Graphics[/color][/font]. This makes sense because the [font="Courier New"][color="#000080"]CView[/color][/font] class is responsible for drawing our data and we will be using Direct3D to do that drawing, so putting them together is a good idea.
class CDirect3DMFCView : public CView, public Graphics
A dialog box will be displayed as shown in figure 5. From the list box entitled New Virtual Functions select the method [font="Courier New"][color="#000080"]OnInitialUpdate[/color][/font] and then press the Add and Edit button.
In the sample program I implement it as follows:
void CDirect3DMFCView::OnInitialUpdate()
{
CView::OnInitialUpdate();
CRect rect;
GetClientRect(▭);
if( !create(
GetSafeHwnd(),
rect.right,
rect.bottom,
true) )
{
MessageBox("create() - Failed", "CView");
return;
}
if( !init() )
{
MessageBox("init() - Failed", "CView");
return;
}
if( !resize(rect.right, rect.bottom) )
{
MessageBox("resize() - Failed", "CView");
return;
}
}
bool CDirect3DMFCView::init()
{
D3DXMATRIX m;
D3DXMatrixIdentity( &m );
_device->SetTransform(D3DTS_WORLD, &m);
D3DXVECTOR3 eye(0.0f, 0.0f, -10.0f);
D3DXVECTOR3 at(0.0f, 0.0f, 1.0f);
D3DXVECTOR3 look(0.0f, 1.0f, 0.0f);
D3DXMatrixLookAtLH(&m, &eye, &at, &look);
_device->SetTransform(D3DTS_VIEW, &m);
D3DXCreateTeapot(_device, &_teapot, 0);
_device->SetVertexShader(D3DFVF_XYZ);
_device->SetRenderState(
D3DRS_FILLMODE,
D3DFILL_WIREFRAME);
return true;
}
bool CDirect3DMFCView::resize(int width, int height)
{
D3DXMATRIX m;
float aspect = (float)width / (float)height;
float fov = 3.14f / 2.0f;
D3DXMatrixPerspectiveFovLH(
&m,
fov,
aspect,
1.0f,
100.0f);
if( _device )
_device->SetTransform(D3DTS_PROJECTION, &m);
return true;
}
I'll mention now that I added the following variable to the [font="Courier New"][color="#000080"]CDirect3DMFCView[/color][/font] class, [font="Courier New"][color="#000080"]ID3DXMesh* _teapot[/color][/font]. This provides the data structure for the geometry we are going to be rendering in this sample. In the constructor of [font="Courier New"][color="#000080"]CDirect3DMFCView[/color][/font] I initialized this variable to zero and in the destructor I release it.
CDirect3DMFCView::CDirect3DMFCView()
{
_teapot = 0;
}
CDirect3DMFCView::~CDirect3DMFCView()
{
if( _teapot )
_teapot->Release();
}
In the Class Wizard dialog box, make sure that you are on the Message Maps tab. Also make sure that for the Class name edit field you have your projects view class selected (e.g. [font="Courier New"][color="#000080"]CDirect3DMFCView[/color][/font]). Under the Messages list box, scroll down until you find the WM_SIZE ID. Select it and press the Add Function button, then press the Edit Code function. See figure (7).
Visual Studio should launch you to the method in the code editor. Add to it as follows: void CDirect3DMFCView::OnSize(UINT nType, int cx, int cy) { CView::OnSize(nType, cx, cy); resize(cx, cy); }
Not much here, when a resize message occurs we simply call our [font="Courier New"][color="#000080"]resize[/color][/font] method, which in turn recalculates our projection matrix to match the current size of the window.
Before we get to the details of the update and render methods, let's first figure out how we should integrate them into the MFC framework. We have several options available to us, here are a few: 1) Send timer messages to our rendering window at regular intervals that force a paint message. Then put our update and render methods in the [font="Courier New"][color="#000080"]OnDraw[/color][/font] method. 2) Create a separate rendering thread that runs continuously in the background of our primary thread. 3) Modify the primary thread so that rendering and updating will occur when the message queue is empty.
Please keep in mind I have not exhausted the possibilities but this should give you some ideas of how you want to do it. In the sample program I have chosen a form of option one and three. What we'll do is override [font="Courier New"][color="#000080"]CWinApp::OnIdle[/color][/font] and there we will invalidate the rendering window forcing it to redraw itself. It would be more efficient to put the rendering and updating code directly into the idle method; this would allow us to bypass the lag of going through the message pump for rendering. However, it was cleaner for me to just invalidate the main rendering window and since this paper is for learning purposes I thought it best to keep it clean and simple.
Now we will get to the specifics of overriding [font="Courier New"][color="#000080"]CWinApp::OnIdle[/color][/font]. Right click on your application class (e.g. [font="Courier New"][color="#000080"]CDirect3DMFCApp[/color][/font]) from the workspace pane. Select Add Virtual Function from the given list, see figure (8).
A dialog box will launch as seen in figure (9).
From the list box entitled New Virtual Functions select the [font="Courier New"][color="#000080"]OnIdle[/color][/font] method. Then press the Add and Edit button. Implement [font="Courier New"][color="#000080"]OnIdle[/color][/font] as shown below:
BOOL CDirect3DMFCApp::OnIdle(LONG lCount)
{
CWinApp::OnIdle(lCount);
AfxGetMainWnd()->Invalidate(false);
return TRUE;
}
Finally, we insert our update, rendering, and some time calculations into the [font="Courier New"][color="#000080"]CView::OnDraw[/color][/font] method. Note that I have linked winmm.lib into the project and included the header file "mmsystem.h". These are needed to use the multimedia timer functions.
void CDirect3DMFCView::OnDraw(CDC* pDC)
{
CDirect3DMFCDoc* pDoc = GetDocument();
ASSERT_VALID(pDoc);
static float lastTime = (float)timeGetTime();
float currentTime = (float)timeGetTime();
float deltaTime = (currentTime - lastTime) * 0.001f;
update(deltaTime);
render();
lastTime = currentTime;
}
bool CDirect3DMFCView::update(float timeDelta)
{
static float angle = 0.0f;
D3DXMATRIX yRotationMatrix;
D3DXMatrixRotationY( &yRotationMatrix, angle );
D3DXMATRIX scalingMatrix;
D3DXMatrixScaling( &scalingMatrix, 4.0f, 4.0f, 4.0f );
D3DXMATRIX productMatrix;
D3DXMatrixMultiply(
&productMatrix,
&yRotationMatrix,
&scalingMatrix);
if( _device )
_device->SetTransform(
D3DTS_WORLD,
&productMatrix);
angle += (3.14f / 12.0f) * timeDelta;
if(angle > 6.28f)
angle = 0.0f;
return true;
}
bool CDirect3DMFCView::render()
{
if( _device )
{
_device->Clear(
0,
0,
D3DCLEAR_TARGET|D3DCLEAR_ZBUFFER,
0xffffffff, 1.0f, 0);
_device->BeginScene();
_teapot->DrawSubset(0);
_device->EndScene();
_device->Present(NULL, NULL, NULL, NULL);
}
return true;
}
BOOL CDirect3DMFCView::OnEraseBkgnd(CDC* pDC)
{
return FALSE;
}
[size="5"]Conclusion
I hope you have enjoyed this article and found it useful. I wrote this because I see many messages on newsgroups and forums requesting information on how to set up DirectX with MFC. Hopefully, this article has helped you out and in the future when someone is requesting DX/MFC information they will be pointed to this article. As we have learned, integrating the two APIs is not difficult, it's simply a matter of knowing where DirectX fits in the MFC framework. For comments and suggestions I can be reached at [email="eckiller@home.com"]eckiller@home.com[/email]. And remember to look out for my DirectX 9 (Mostly Direct3D) book coming out in late 2002.