In this post we’ll learn how to create a windowed directdraw application for windows mobile. A windowed application is not much different from a regular full-screen application but you have to be a little careful because your application has to co-exist with GDI, peacefully. Note that I have used bits of code from
Joel’s code project article. Here is the code I call from
WinMain():
//Create the DirectDraw object the primary and auxillary surfaces
if (InitDirectDraw(g_hWnd))
{
if (!CreateDirectDrawSurfaces(g_hWnd))
{
printf("CreateDirectDrawSurfaces failed!\r\n");
FreeDirectDrawResources();
return FALSE;
}
}
else
{
printf("InitDirectDraw failed!\r\n");
FreeDirectDrawResources();
return FALSE;
}
printf("Checkpoint 1: InitDirectDraw and CreateDirectDrawSurfaces succeeded\r\n");
//Initialize the surfaces with the image
if (!InitSurfaces())
{
printf("InitSurfaces failed!\r\n");
return FALSE;
}
We will see what each one of the functions does, one by one.
BOOL InitDirectDraw(HWND hWnd)
{
HRESULT result;
result = DirectDrawCreate(NULL, &g_ddraw, NULL);
if(SUCCEEDED(result))
{
g_ddraw->SetCooperativeLevel(hWnd, DDSCL_NORMAL);
return TRUE;
}
printf("DirectDrawCreate failed!\r\n");
return FALSE;
}
InitDirectDraw() is simple. The only difference here is the co-operative level, we use DDSCL_NORMAL unlike DDSCL_FULLSCREEN that we used before. The CreateDirectDrawSurfaces() function is below:
BOOL CreateDirectDrawSurfaces(HWND hWnd)
{
DDSURFACEDESC ddsd;
HRESULT hr;
RECT rect = {0,};
ZeroMemory(&ddsd, sizeof(DDSURFACEDESC));
ddsd.dwSize = sizeof(DDSURFACEDESC);
ddsd.dwFlags = DDSD_CAPS;
ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE;
//create the primary surface and set the clipper
if ( (g_ddraw->CreateSurface(&ddsd, &g_primarySurface, NULL)) == DD_OK)
{
if ( (g_ddraw->CreateClipper(0, &g_primaryClipper, NULL)) == DD_OK)
{
if ( (g_primaryClipper->SetHWnd(0, hWnd)) != DD_OK)
{
printf("SetHWnd on primary clip failed hr=0x%x\r\n", hr);
}
if ( (g_primarySurface->SetClipper(g_primaryClipper)) != DD_OK)
{
printf("SetClipper failed hr=0x%x\r\n", hr);
}
}
else
{
printf("CreateClipper failed hr=0x%x\r\n", hr);
return FALSE;
}
}
else
{
printf("CreateSurface failed hr=0x%x\r\n", hr);
return FALSE;
}
//get the size of the client area (this will be used for backbuffer)
GetClientRect(hWnd, &rect);
ZeroMemory(&ddsd, sizeof(DDSURFACEDESC));
//create the back buffer
ddsd.dwSize = sizeof(DDSURFACEDESC);
ddsd.dwFlags = DDSD_WIDTH | DDSD_HEIGHT;
ddsd.dwWidth = rect.right – rect.left;
ddsd.dwHeight = rect.bottom – rect.top;
printf("screen width:%d, height:%d\r\n", ddsd.dwWidth, ddsd.dwHeight);
if ( (g_ddraw->CreateSurface(&ddsd, &g_backBuffer, NULL)) != DD_OK)
{
printf("CreateSurface on back buffer failed hr=0x%x\r\n", hr);
return FALSE;
}
ZeroMemory(&ddsd, sizeof(DDSURFACEDESC));
//create a surface to hold the marbel’s image
ddsd.dwSize = sizeof(DDSURFACEDESC);
ddsd.dwFlags = DDSD_WIDTH | DDSD_HEIGHT;
ddsd.dwWidth = 48; //BMP’s width
ddsd.dwHeight = 48; //BMP’s height
if ( (hr = g_ddraw->CreateSurface(&ddsd, &g_marbleSurface, NULL)) != DD_OK)
{
printf("CreateSurface marble surface failed hr=0x%x\r\n", hr);
return FALSE;
}
return TRUE;
}
We create three surfaces here, a primary surface, a back buffer and an additional surface to hold our marble’s image. Another thing you’ll note is the presence of a DirectDraw Clipper object. The clipper object is used to manage clip lists. Basically, it helps to contain the drawings of our application within the client area of the window. We create the clipper using CreateClipper() method on the directdraw object. Next, we call SetHWnd() on the clipper object and pass in the handle to our main window. The clipper object will use this handle to find out the client area of the window and determine what part to clip. We set the clipper on the primary surface by using SetClipper() method of the directdraw surface object.
We create the back buffer next. You’ll see that we use the client area co-ordinates of the main window to decide the width and height of the back buffer. I did not do this initially and used 240 for width and 320 for height. This led to a problem, the image on the screen appeared skewed. The problem was that while Blt’ing, the destination rect on the primary surface was the size of the client area of the main window, which is a little smaller than 240×320 to make way for the task bar and the menu bar. When the back buffer, whose size is 240×320, was Blt onto a smaller surface the image was shrunk to fit. So the image appeared, well, shrunk.
BOOL InitSurfaces()
{
HBITMAP hbm;
//load the bitmap resource
hbm = DDGetBitmapHandle(g_hInst, szMarbleBitmap);
if (hbm == NULL)
{
printf("DDGetBitmapHandle failed\r\n");
return FALSE;
}
DDCopyBitmap(g_marbleSurface, hbm, 0, 0, 48, 48);
DeleteObject(hbm);
return TRUE;
}
InitSurfaces() initializes the marble surface with the marble’s image. The DD*() function you see above are from ddutil.cpp which come with the SDK samples. Make sure that the null check "if (hbm == NULL)" isn’t written as "if(hbm = NULL)", that’ll save you a good hour of debugging (;
Next up are UpdateFrame() and DisplayFrame() functions. UpdateFrame() updates the back buffer by Blt()’ing the marble surface on it and DisplayFrame() displays the drawing by Blt()’ing the back buffer onto the primary surface.
void DisplayFrame()
{
HRESULT hr;
POINT p;
RECT destRect;
p.x = p.y = 0;
ClientToScreen(g_hWnd, &p);
GetClientRect(g_hWnd, &destRect);
OffsetRect(&destRect, p.x, p.y);
//blt the back buffer on the primary surface
while (TRUE)
{
hr = g_primarySurface->Blt(&destRect, g_backBuffer, NULL, 0, NULL);
if (hr == DD_OK)
break;
if (hr != DDERR_WASSTILLDRAWING)
break;
}
}
Again, we use ClientToScreen(), GetClientRect() and OffsetRect() functions to determine the position of the destination rectangle on the primary surface.
Another important thing is to prevent your application from drawing when it’s not supposed to, for e.g. when it doesn’t have the focus. So we need to maintain a state variable to determine whether we have focus or not and draw only when we our application has the focus. The following code is under the WndProc() function:
case WM_ACTIVATE:
SHHandleWMActivate(hWnd, wParam, lParam, &s_sai, FALSE);
switch(LOWORD(wParam))
{
case WA_ACTIVE:
case WA_CLICKACTIVE:
g_bHasFocus = TRUE;
break;
case WA_INACTIVE:
g_bHasFocus = FALSE;
break;
default:
break;
}
break;
Pretty straight forward. Also,
case WM_CANCELMODE:
g_bHasFocus = FALSE;
DisplayTextOnScreen(TEXT("Tap here to continue.."));
//DisplayFrame();
break;
case WM_ENTERMENULOOP:
g_bHasFocus = !(BOOL)wParam;
break;
case WM_EXITMENULOOP:
g_bHasFocus = TRUE;
break;
WM_ENTERMENULOOP and WM_EXITMENULOOP are called whenever user clicks on a menu or leaves a menu. In case, where the Start menu was pressed, my window did not receive these messages, instead it was sent a WM_CANCELMODE message. WM_CANCELMODE is sent whenever a dialog box or a message box is displayed. But when I click on the Start menu again to close the popup, no message was sent to the main window. So I decided to display a small text on the screen which says "Tap here to continue.." and it works pretty well. But there is one problem however, which I’ll come to in a moment. I use DisplayTextOnScreen() to draw text directly on the primary surface,
void DisplayTextOnScreen(TCHAR *tszStr)
{
HDC hdc;
RECT rc = {0,}, destRect = {0,}, srcRect = {0,};
int nMsg;
SIZE size = {0,};
HRESULT hr;
if (g_primarySurface->GetDC(&hdc) == DD_OK)
{
SetBkColor(hdc, RGB(115, 115, 115));
SetTextColor(hdc, RGB(255, 255, 255));
GetClientRect(g_hWnd, &rc);
nMsg = lstrlen(tszStr);
GetTextExtentPoint(hdc, tszStr, nMsg, &size);
ExtTextOut(hdc,
(rc.right – size.cx)/2 – 25,
rc.bottom – size.cy*2,
//(rc.bottom – size.cy)/2,// – (rc.bottom – size.cy)/4,
ETO_OPAQUE,
NULL,
tszStr,
nMsg,
NULL);
g_primarySurface->ReleaseDC(hdc);
}
}
And of course, you also have to handle WM_LBUTTONDOWN or WM_LBUTTONUP events to find out if the user clicked on the client area and then change the state variable so the marble can continue to move again.
There you go.
And as always, here are a few screenshots,
You can tell which screen is the problem, right? Do you need a clue? Well, the problem is that when I click on Start and then on Menu Screen 4 happens. And vice-versa too. I am yet to figure out how to solve this, I do however have a slight idea of why it happens. Feel free to help me out.
Update: The problem mentioned above (Screen 4) is solved. Look here.