Using Direct3D with MFC

The Microsoft Foundation Classes (MFC) allow us to rapidly create applications for the Windows 32-bit operating systems with full SDK features. Its set of class wizards and C++ classes allow you to easily add complicated Windows controls and features that would be difficult to do using just the Win32 SDK. This article shows you how to integrate Direct3D with MFC to get all the benefits of rapid applications developed via MFC that can support 2D or 3D graphics via Direct3D. The requirements of the article are to have some experience with Direct3D, and you must have Visual Studio as we will be using the wizards to make most parts of the application. If you've never used MFC before, you probably won't have much difficulty as I explain most of the steps that are MFC-centric.

Introduction

First, we'll need to setup a project file in Visual Studio, so fire it up and select 'New' from the file menu. On the project tab of the dialog that pops up, select 'MFC AppWizard (exe)' from the list. Set a name for the project, for this I suggest using the same name as I do or else this article can get confusing. I chose the name Direct3DMFC. Set a location for the project and press OK.

Now the MFC appwizard will pop up asking for the features you want it to incorporate into your project. Since most editors and most applications in general are Single Document Interface (SDI) we will choose this option for our app. Select 'Single Document' in Step 1 of the wizard. Also make sure 'Document/View architecture' is checked. Click Next and onto step 2. We won't be using database support so click Next and skip step 2. Step 3 defaults are also ok so just click Next. Step 4 contains a few options you can play with. Here you can incorporate a docking toolbar, or status bar. I chose to turn off status bar and print preview support as they don't apply much to our sample app. You can click 'Advanced' to see more options on document support and various window styles, but you don't need to mess with that at this point. Ok, click Next and then Next again to skip step 5 (the defaults are fine). On step six, AppWizard will tell you what classes will be created. There isn't anything to do on this page, but I wanted to show it to you so you can get a feel for what is going on. Press Finish now and then OK on the New project information dialog. Your project is now set up, you should save it.

If you expand your source files and header files folders in the file view section of the project explorer, you'll notice there's a lot of them for having no code! These are all generated by the wizard and they handle setting up the mundane details that go along with a window and application. The main files to pay attention to are the Direct3DMFC.h|cpp, Direct3DMFCView.h|cpp, and the Direct3DMFCDoc.h|cpp. Let's talk about views now.

The CDirect3DMFCView Class

The document/view architecture is a way of separating a program's data that must persist across executions (a document file on disk, or just the document) and the various 'views' or renderings of the document we wish to present to the user. So for example, take an HTML file. We can choose to support two views of this file. One is a text editor view that presents the ASCII source of the file with the tags inplace. We can also view it as a web browser would render it with formatted text and images. MFC allows us to support multiple views on single documents and gives us a great way of simply drawing what we want in a nice class.

That class I spoke about is generated by the appwizard and is specific to your application. In this case it is CDirect3DMFCView. You'll notice it is derived from CView class: CView is the base class for all views. So initially if you compile and run your app now, one blank window will popup with any toolbars and/or status bars you asked appwizard to support. Our CDirect3DMFCView class does the default CView rendering which does nothing. This is the place and perfect opporunity to extend the CDirect3DMFCView class to support a Direct3D rendering window.

The Direct3D Integration

To support Direct3D, we'll need a class that encapsulates all the things we want to keep around to use Direct3D. These can be summarized as:

This list translates directly into the members of a class we can use to organize Direct3D. I call this class CD3DGraphics (it is hard to come up with a good name for this!). I present this class now so you can get a look at:

class CD3DGraphics {

public:
    D3DGraphics();
    virtual ~D3DGraphics();

    HRESULT CreateDevice(HWND hwnd, int width, int height);

    LPDIRECT3DDEVICE8 GetDevice() { return m_device; }

    virtual bool Init() = 0;
    virtual bool Resize(int width, height) = 0;
    virtual bool Update(float deltaTime) = 0;
    virtual bool Render() = 0;

private:

    LPDIRECT3D8 m_d3d;
    LPDIRECRT3DDEVICE8 m_device;
    

};

We will be using CD3DGraphics as a base class for our CDirect3DMFCView class. We'll be dropping in implementations of the Init, Resize, Update, and Render methods that will be specific to our application. So, the only work we need to do in the CD3DGraphics.cpp class implementation file is to fill in the constructor/destructor, and to fill in the CreateDevice method. The CreateDevice method takes a HWND (handle to a window) and an initial width and height of the back buffer. The method then creates the D3D8 object, sets up present parameters and ultimately creates the m_device member of the class that has the properties of the windows desktop (because we are making a windowed application). The constructor of this class merely sets the private members to NULL and the destructor safely releases the private members.

Now that the CD3DGraphics class is done, we need to integrate it with our view class. We want the CDirect3DMFCView class to derive from CD3DGraphics. So open Direct3DMFC.h and change the class declaration line:


class CDirect3DMFCView : public CView, public CD3DGraphics

Now since the CD3DGraphics class has pure virtual functions, we need to add their declarations to the CDirect3DMFCView class:

public:
bool Init();
bool Resize(int width, int height);
bool Render();
bool Update(float delaTime);

Now we're ready to get to the meat of the application.

Filling in the Methods of CDirect3DView

Our next step is to get Direct3D setup within the view class. To do this, we need to add some initialization to the CDirect3DMFCView class. Because we only want to perform this initialization once, we'll override the method OnInitialUpdate which comes from the CWnd class which CView is dervied from (which CDirect3DMFCView is dervied from). To quickly override a method in a class supported by the MFC class wizard, open the Class Wizard (press Ctrl+W or Class Wizard from the View menu of Visual C++). Make sure CDirect3DMFCView is selected in the Class Name dropdown box. In the Messages Listbox, select the OnIntialUpdate method and press the 'Add Function button' then press 'Edit Code'. You'll get taken right to the source.

Now we'll want to modify this method to setup Direct3D and perform one-time initialization things:



void CDirect3DMFCView::OnInitialUpdate()
{

CView::OnInitialUpdate();

// get our main window rectangle
CRect rect;
GetClientRect(&rect);

if (FAILED(CreateDevice(GetSafeHwnd(), rect.right, rect.bottom))) {
    // post some kind of error msg
    return;
}

// load any program specific data
if (!Init()) {
    // post some kind of error msg
    return;
}

// size initial viewport
if (!Resize(rect.right, rect.bottom)) {
    return;
}

}


This code is pretty self-explanatory. We call the base class OnInitialUpdate so it can do whatever it needs to do. Then we get our client rectangle dimensions and create the D3D device. We do our specific initalization next with the Init method and then we do the first sizing of the application to get the first correct projection matrix for the viewport.

Before we go any further, we'll have to fill in the functions we're overriding from the CD3DGraphics class. Namely, the Init, Render, Update, and Resize methods:


bool CDirect3DMFCView::Init() {
    // TODO: we'll fill this in with stuff later
    return true;

}

bool CDirect3DMFCView::Render() {
    // verify device 
    if (m_device == NULL)
         return false;

    m_device->Clear(0, NULL, D3DCLEAR_TARGET, D3DCOLOR_XRGB(0,0,0), 1.0f, 0);

    m_device->BeginScene();

    // TODO: render all polygons here

    m_device->EndScene();
    m_device->Present(NULL, NULL, NULL, NULL);

    return true;

}

bool CDirect3DMFCView::Update(float deltaTime) {
    // TODO: update animation, physics or whatever with timestep
    return true;
}

bool CDirect3DMFCView::Resize(int width, int height) {
    // recomputes the perspective projection matrix for a new viewport size
    D3DXMATRIX m;
    float aspectRatio = (float)width/(float)height;
    float FOV = 3.14159f / 2.0f // 2 over pi or 90 degrees
   
    D3DXMatrixPerspectiveFovLH(&m, FOV, aspect, 1.0f, 100.0f);

    if (m_device)
         m_device->SetTransform(D3DTS_PROJECTION, &m);

    return true;

}


So now all methods are complete. The Init method initializes program specific data, so since our sample doesn't do anything (yet) it doesn't do anything. The Render function is pretty standard by Direct3D usages. Render clears the screen (with black) begins a scene and ends then presents. If you want to draw polygons, draw them in between the begin/end scene pairs. The Update method is only used for real-time apps. The Resize method recomputes the projection matrix when the screen is resized. So that brings up an issue: we must call Resize whenever the window is sized. To do this, we must override the CView's implementation of OnSize handler. You do this just the way we did with the OnInitialUpdate handler by using the Class Wizard. Here's what we change it to:

void CDirect3DMFCView::OnSize(UINT nType, int cx, int cy)
{

CView::OnSize(nType, cx, cy);
// recompute matrix
Resize(cx, cy);

}


If you look in the MFC docs, OnSize takes three parameters. nType is of no concern to us, but cx and cy hold the dimensions of the client area, which are important to computing the aspect ratio and eventually the perpspective projection matrix itself.

All right, one last final thing we must do. We actually need to call Render somewhere to draw a frame. We do this in CDirect3DMFCView's OnDraw method. You don't need to override this with the class wizard like with OnSize because since this view class is meant to do drawing, the app wizard already has a handler for it. The OnDraw handler the wizard provides calls GetDocument at the top to get the current SDI document. This is useful for separating document from view (I talked about this earilier). Essentially, all the data that makes up a scene should be contained in your document (or at least referenced in your document somewhere) and then you 'Get' the document and do something with its data. That something that we'll do is Render it:


void CDirect3DMFCView::OnDraw(CDC *pDC) {
     CDirect3DMFCDoc *pDoc = GetDocument();
     ASSERT_VALID(pDoc);

     // our code: render the scene
     Render();

}

That's it! You should go ahead and compile now and make sure it all works. You should see a your regular window with the client area now black rather than white because we are clearing the back buffer to black and it is getting presented to the window now. It is pretty boring, but clearly correct and now you can use any D3D commands you want to render any type of scene you want

Adding to the Sample App

Now it's time to do something with the app to demonstrate how easy this is to use. I'm going to draw a wireframe sphere in the window. Also, I'll demonstrate how to make the app real-time by rotating the sphere a certain amount each second. This will use the Update method to accomplish the rotation.

The sphere is created using the D3DXCreateSphere function. One thing I'll change is the clear color of the app back to white so that the black wireframe model is visible. I'm making it wireframe because colorizing and texturing is over the top for the simple example. Also I'm going to cheat and not use the document class because I don't want to deal with it. But in reality, your data should be contained in the document class and not in the view class.

We'll add a D3DXMesh object (which is required by D3DXCreateSphere) to the CDirect3DMFCView class as a protected member:


protected:
LPD3DXMESH m_sphere;

Then we actually create the sphere in the Init method:

bool CDirect3DMFCView::Init() {
    // load sphere 
    D3DXCreateSphere(m_device, 5.0f, 10, 10, &m_sphere, NULL);
       
    // set basic vertex shader
    m_device->SetVertexShader(D3DFVF_XYZ);
    // set to render as a wireframe
    m_device->SetRenderState(D3DRS_FILLMODE, D3DFILL_WIREFRAME);

    return true;
}


and we add some stuff for the sphere to the constructor and destructor:

CDirect3DMFCView::CDirect3DMFCView() {
    m_sphere = NULL;

}

CDirect3DMFCView::~CDirect3DMFCView() {
    if (m_sphere)
        m_sphere->Release();
}


The Init method does a lot of work setting up the view, but dealing with D3D functions and commands in MFC isn't any different now that we have our framework. To draw the sphere, we'll add the drawing command inside a begin/end block in our Render method:

m_sphere->DrawSubset(0);

Now you'll see a sphere on the screen and it is pretty boring.

Making the App Real-Time

Now having an editor or a modeler drawing in real-time isn't extremely necessary but if you want to test animations or physics you'll need to have a real-time loop. Fortunately, this is easy with our current setup. For this example, we'll make the sphere rotate around the Y-axis, 60 degrees per second. We'll design this system so that we'll use real time rather frame based time so that we can achieve maximum framerates and not be jerky on slower systems.

The first thing we'll do is add to the Update method of the CDirect3DMFCView class. This method does all the frame updating and so is a good place to add rotation code:


bool CDirect3DMFCView::Update(float deltaTime) {
     static float angle = 0.0f;
     D3DXMATRIX yRot;

     D3DXMatrixRotationY(&yRot, angle);
     if (m_device) {
          m_device->SetTransform(D3DTS_WORLD, &yRot);
     }
     
     angle += (3.14159f / 6.0f) * deltaTime;
     if (angle > 2.0f*3.14159f)
          angle = 0.0f;

     return true;
}

There are also some changes that need to be made to the OnDraw handler where we call Render. We'll also need to call Update now and pass in the amount of time that has elapsed since the last frame. To do this, we get the current time initially and store it in a static variable. Then on each frame (each OnDraw call) we get the current time. We compute the difference between the current time and the last time and pass that to Update. Then we copy the current time into the last time and we're set for the next frame.

void CDirect3DMFCView::OnDraw(CDC *pDC) {
     CMapEditDoc *pDoc = GetDocument();
     ASSERT_VALID(pDoc);

     static float lastTime = (float)timeGetTime();
     float currTime = (float)timeGetTime();
     float deltaTime = (currTime - lastTime) * 0.001f;
     lastTime = currTime;

     Update(deltaTime);
     Render();

}

This approach assumes that OnDraw will be called as continuously as possible. So how do we call OnDraw? OnDraw is called when the window needs painting. So we should Invalidate the window whenever we aren't processing any messages. To do this, we need to override the OnIdle handler in the CDirect3DMFC class. This class is derived from CWinApp. Override the OnIdle message handler just the same as you did with OnIntialUpdate and OnDraw of the CDirect3DMFCView class but this time you are dealing with the CDirect3DMFC class. We'll get the main window and then invalidate it:

BOOL CDirect3DMFC::OnIdle(LONG lCount) {
    CWinApp::OnIdle(lCount);

    AfxGetMainWnd()->Invalidate(false);

    return TRUE;

}

That's it. But there's one small problem left. If you run this now you'll see some wicked flickering. This is caused by a call to the CView OnEraseBkgnd messing things up. So let's override it and simply code it to return false. If you look in the Docs, OnEraseBkgnd returns nonzero if it erased the background. Since we are erasing the background ourselves with a device->Clear call in Direct3D, we will return 0 to cause the window to remain unpainted and then we can sneak in and paint it ourselves with our Render method in OnDraw.

BOOL CDirect3DMFCView::OnEraseBkgnd(CDC *pDC) {
    return 0;

}

Essentially before this fix, the background was getting erased twice and the first erasure is the default window bush (white). The worse thing is that the regular erase was using a standard GDI window fill which is slug-slow. Our Direct3D Clear routine is much faster and is called only once now.

Run your project and watch the amazing, wonderous rotating wireframe sphere!

Conclusion (and why this stuff is important)

We've barely touched the surface of MFC programming. MFC let's you do stuff with a Windows app that saves tons of time. Using the Win32 SDK is painful but MFC encapsulates lots of stuff into easy to use classes. You'll be able to add all those great user interface controls and do-dads that make GUI apps easy to use.

Some really great notable benefits include:

If you really want to get into MFC programming, check out Programming Windows with MFC by Jeff Prosise. That book rocks.

Source

Download full source (19.0K)