안녕하십니까 이번 포스팅에선 드디어 소스를 한번 파헤쳐 보겠습니다.

먼저 WinMain의 소스를 봅시다.

한 줄씩 분석해봅시다.

- #include <time.h>

time 함수를 사용하기 위해 선언한 헤더파일입니다. time함수는 랜덤으로 지뢰를 배치할 때 사용됩니다.

이 소스코드에서 선언된 함수들은 전부 앞 포스팅에 설명드렸으므로 넘어가겠습니다.

- HWND buttonHwnd[81];

버튼의 핸들을 저장시킬 배열을 선언합니다. 이렇게 선언된 핸들형 배열은 나중에 버튼이 생성될 때 각각의 버튼 핸들들이 저장됩니다. 핸들을 저장시키는 이유는 나중에 버튼을 삭제할 때 이 버튼의 핸들이 필요하기 때문입니다.

- int arr[9][9];

지뢰가 담긴 타일들을 생성할 때 쓰입니다. Createlandmine과 Createlandmine2함수에 사용됩니다.

- int buttonValue[81];

지뢰가 담긴 타일들이 생성되었을 때 이 타일들의 값들을 1차원 배열로 저장시키기 위해 선언하였습니다.

- int OpenClose_Tile[81];

타일이 눌러진 타일인지 아니면 아직 눌러지지 않은 타일인지의 여부를 저장시키기 위해 선언하였습니다.

- int flagTile[81];

타일이 깃발이 꽃혀져 있는 타일인지 아닌지의 여부를 저장시키기 위해 선언하였습니다. 깃발이 꽃혀져 있는 타일은 눌러지지 않아야 겠지요?

- HINSTANCE Global_hInstance;

WinMain의 인자인 hInstance를 전역변수로 사용하기 위해 선언되었습니다.

- HWND Global_hWnd;

WndProc의 인자인 hWnd를 전역변수로 사용하기 위해 선언하였습니다.

이제 WinMain의 코드를 살펴봅시다.

- Global_hInstance = hInstance;

hInstance의 값을 전역변수로 사용하기 위해 Global_hInstance에 저장시킵니다.

- srand((int)time(NULL));

난수 값을 생성하기 위해 시드를 설정해주는 코드입니다. 이것은 앞 포스팅에 설명드렸으니 넘어갑시다.

- Createlandmine();
  Createlandmine2();

지뢰를 랜덤으로 배치시키고 그 지뢰를 이용하여 주변 타일들을 배치시켜 9*9의 타일을 만듭니다. 즉 이부분이 이제 우리가 게임할 때 쓰일 타일입니다.

- wndclass.hIcon = LoadIcon(hInstance,MAKEINTRESOURCE(IDI_ICON1));

  wndclass.lpszMenuName = MAKEINTRESOURCE(IDR_MENU1);

우리가 아까 만든 아이콘 리소스와 메뉴 리소스를 사용하기 위한 코드입니다.

- hwnd = CreateWindow(L"a",L"지뢰 찾기",WS_OVERLAPPEDWINDOW | WS_VISIBLE,73,73,240,280,NULL, NULL,hInstance,NULL);'

지뢰 찾기 윈도우 창을 생성시킵니다. 생성되는 위치는 (73,73), 크기는 가로 240, 세로 280입니다.

이 외에는 별 달라진 것이 없으므로 이제 WndProc함수로 넘어갑시다.

양이 너무 많아 나눠서 설명드리겠습니다.

- HDC hdc,hdcsrc;

HDC 형의 변수 2개를 선언합니다. 이 변수들은 윈도우 창에 비트맵을 출력시킬 때 쓰입니다.

- PAINTSTRUCT ps;

BeginPaint함수로 DC를 얻어오기 위한 구조체 변수입니다.

- HBITMAP Bitmap,landmine,Number1,Number2,Number3,Number4,Number5;
  HBITMAP Number6,Number7,Number8,space;

각각의 비트맵 리소스를 저장시키기 위해 HBITMAP형의 변수들을 선언하였습니다. 단Bitmap변수는 DeleteObject함수를 사용하기 위해 선언하였습니다.

- int i,j,m=0;

for문에 사용할 i,j변수와 특정 배열의 인덱스로 쓰일 변수 m을 선언하였습니다.

- int x=30,y=30;

비트맵을 출력시킬 때의 x좌표와 y좌표를 저장시키기 위해 사용하였습니다.

- Global_hWnd = hWnd;

WndProc의 인자인 hWnd를 전역변수로 사용하기 위해 Global_hWnd에 저장시킵니다.

다음은 switch문 코드를 봅시다.

- case WM_CREATE:
   CreateButton();
   break;

WM_CREATE 메세지를 받았을 경우 버튼을 생성합니다. 대체적으로 WM_CREATE메세지는 한 번만 발생하므로 버튼도 한 번만 생성됩니다.

- case WM_CONTEXTMENU

WM_CONTEXTMENU메세지를 받았을 경우의 처리입니다. MSDN에서 이 WM_CONTEXTMENU메세지의 설명을 봅시다.

풀이하면 윈도우 창에서 마우스 오른쪽 버튼을 눌렀을 경우 발생하는 메세지라고 되어있습니다. 그런데 말입니다. 이와 같은 동작을 할 경우 발생하는 메세지가 또 하나 있지요. 그것은 바로 WM_RBUTTONUP 혹은 WM_RBUTTONDOWN입니다.

보통 윈도우창에서 오른쪽마우스 처리를 한다면 대부분 WM_RBUTTONUP메세지 혹은 WM_RBUTTONDOWN메세지에서 처리를 합니다. 그런데 왜 저는 WM_CONTEXTMENU메세지로 처리를 하였을까요?

WM_RBUTTONUP이나 WM_RBUTTONDOWN 메세지는 다른 윈도우 창에서는 발생하지 않습니다. 이것이 무슨 말이냐 하면 이 WndProc함수가 처리되는 하나의 큰 윈도우 창에서만 발생을 하고 그 외의 윈도우 창, 예를 들면 자식 윈도우 창 같은 곳에서는 발생을 하지 않습니다.

우리는 버튼을 만들 때 CreateWindow함수로 만들었습니다. 즉 클래스가 Button인 윈도우 창이지요. 이렇게 생성된 button 윈도우에서 오른쪽 마우스를 누를 경우 WndProc에서는 WM_RBUTTONUP 혹은 WM_RBUTTONDOWN 메세지가 발생하지 않습니다. 실제로 브레이크를 걸로 디버깅을 해보시면 오른쪽마우스를 눌러도 메세지가 발생하지 않습니다. 그래서 이를 막기 위해 다른 윈도우창에서도 우측 마우스를 눌렀을 때 발생하는 메세지인 WM_CONTEXTMENU 메세지를 사용하였습니다.

- for(i=0;i<81;i++)

우측 마우스를 눌렀을 경우 어떤 버튼에서 우측마우스를 눌렀는지 검사하기 위해 for문을 사용하였습니다. 즉 모든 버튼을 체크한다는 뜻이지요.

-  if(buttonHwnd[i] == (HWND)wParam && flagTile[i] == 1)
     SendMessage((HWND)wParam, BM_SETIMAGE,IMAGE_BITMAP,(LPARAM)LoadBitmap(Global_hInstance, MAKEINTRESOURCE(Flag)));
     flagTile[i] = 0;

버튼핸들과 우측 마우스를 누른 버튼의 핸들이 같고 그 버튼이 깃발이 꽃혀져 있지 않은 버튼일 경우의 처리입니다. flagTile에서 값이 1인 것은 깃발이 꽃혀져 있지 않다는 것을 의미합니다. WM_CONTEXTMENU메세지에서 wParam인자는 누른 윈도우 창의 핸들을 의미하므로 wParam인자를 HWND형으로 바꾸어 버튼 핸들과 비교합니다.

이 if문이 참이라는 것은 깃발이 꽃혀져 있지 않은 버튼에 우측마우스를 눌렀다는 뜻이므로 SendMessage함수를 사용하여 해당하는 버튼의 이미지를 flag가 세워진 이미지 리소스(Flag)로 변경하도록 하였습니다. SendMessage함수는 앞 포스팅에서 함수를 설명할 때 설명드렸으므로 넘어가겠습니다.

깃발 세운 이미지로 버튼을 바꾸었으면 해당 flagTile의 값을 0으로 설정합니다.
    
- else if(buttonHwnd[i] == (HWND)wParam && flagTile[i] == 0)
     SendMessage((HWND)wParam, BM_SETIMAGE,IMAGE_BITMAP,(LPARAM)LoadBitmap(Global_hInstance, MAKEINTRESOURCE(QM)));
     flagTile[i] = 2;

이번 if문은 해당하는 버튼의 flagTile값이 0일 경우입니다. flagTile값이 0이라는 것은 해당 버튼이 깃발이 꽃혀져 있는 상태임을 의미합니다. 오리지널 지뢰찾기에서는 깃발이 표시된 버튼에 우측마우스를 누르면 ?가 적힌 이미지로 바뀝니다. 그것을 재현하기 위해 버튼의 이미지를 물음표가 적힌 이미지 리소스(QM)로 바꿔줍니다. 그리고 해당 flagTile의 값을 2로 설정합니다.

- else if(buttonHwnd[i] == (HWND)wParam && flagTile[i] == 2)
     SendMessage((HWND)wParam, BM_SETIMAGE,IMAGE_BITMAP,(LPARAM)LoadBitmap(Global_hInstance, MAKEINTRESOURCE(Tile)));
     flagTile[i] = 1;

마지막 if문은 해당하는 버튼의 flagTile값이 2일 경우입니다. 값이 2라는 것은 ?가 적힌 버튼이라는 것을 의미하므로 원상태로 복구시키기 위해 Tile 리소스를 사용하여 원래 상태의 이미지로 바꿉니다. 그리고 해당하는 flagTile의 값을 원래 상태의 값인 1로 설정합니다.

이제 WM_COMMAND 메세지 부분을 살펴봅시다. 일단 코드를 보여드리겠습니다.

하나씩 살펴봅시다.

WM_COMMAND메세지는 메뉴를 누를 때 발생하는 메세지입니다. 우리가 만든 버튼 윈도우도 하나의 메뉴로 인식하므로 버튼을 눌렀을 때 이 메세지가 발생하게 됩니다.

- switch(wParam)
    default:

어떤 메뉴를 눌렀냐에 따라 처리를 다르게 하기 위해 사용하였습니다. 일단 우리가 만든 메뉴 리소스(새게임, 통계 ..... )에 있는 메뉴처리는 제외하였습니다. 그럼 남는 메뉴는 무엇일까요? 바로 우리가 만든 버튼 메뉴이겠지요. 그런데 우리가 만든 버튼 개수만 해도 81개가 됩니다. 그걸 일일이 case문으로 해주기엔 좀 버거우므로 default로 하여 case로 지정한 것 외의 나머지 버튼을 처리하도록 하였습니다.

- if(flagTile[wParam] == 1)
      DestroyWindow(buttonHwnd[wParam]);
      OpenClose_Tile[wParam] = 0;

버튼을 누르면 해당하는 버튼의 이름이 wParam에 들어있다는 것은 다 아실 겁니다 .이를 이용하여 누른 버튼의 flagTile값이 1일 경우를 if문으로 작성해주었습니다. flagTile의 값이 1이라는 것은 정상적인 타일을 의미합니다. 즉 정상적인 타일을 눌렀을 경우에는 DestroyWindow함수를 사용하여 해당하는 버튼의 핸들을 지웁니다. 핸들이 지워지면 그 핸들에 해당하는 윈도우 창은 사라지게 됩니다. 즉 버튼을 벗겨내는 것을 재현한 것이지요. 그리고 해당하는 버튼의 OpenClose_Tile값을 0으로 주어 이 버튼은 벗겨진 버튼이라는 것을 정의하였습니다.

- switch(buttonValue[wParam])
       case 0:
        for(i=0;i<81;i++)
          if(buttonValue[i] == 0)
             DestroyWindow(buttonHwnd[i]);
        MessageBox(hWnd,L"지뢰를 밟으셨습니다.",
          L"콰콰쾅",MB_ICONERROR | MB_OK);
        PostQuitMessage(0);
        break;
       case 9:
        for(i=0;i<9;i++)
           sp_check();
        break;

버튼을 지우고 나면 이제 밑에 깔린 타일의 값에 따라 처리륻 다르게 해야겠지요? 이를 위해 switch문을 사용하였습니다. switch의 인자로는 buttonValue배열이 사용되었습니다. buttonValue는 해당하는 버튼의 타일값이 저장되어있으므로 그 타일값에 따라 처리를 해주시면 됩니다.

먼저 타일값이 0일 경우입니다. 타일값이 0이라는 것은 지뢰를 의미하지요. 즉 지뢰를 밟았을 경우입니다. 이 때는 게임오버가 된 것이니 그 게임오버를 구현해주셔야 합니다. 먼저 for문을 이용하여 모든 버튼을 검사합니다. 그리고 지뢰타일이 있는 버튼을 전부 찾아내어 삭제시킵니다. 그리고 메세지박스함수를 이용하여 지뢰를 밟았다는 메세지를 띄우고 PostQuitMessage함수를 사용하여 윈도우를 종료시킵니다.

case가 9일 경우의 처리입니다. 즉 누른 버튼의 타일 값이 빈타일일 경우를 의미합니다. 빈타일은 주변에 지뢰가 없는 타일을 의미하는데 오리지널 지뢰찾기에서 빈타일을 눌렀을 경우

1. 빈 타일의 주변 타일들을 전부 연다. (주변 버튼을 전부 삭제한다)

2. 1번의 과정에서 생긴 빈타일들의 주변 타일들을 전부 연다.

이 두과정을 거치게 됩니다.

우리는 이 과정을 구현해주기 위해 그냥 간단하게 모든 타일을 체크하여 타일의 값이 9이고 그 타일의 버튼이 없는 경우(벗겨진 경우)를 찾아 그 주변의 타일을 전부 벗겨내는 함수(sp_check)를 사용하였습니다.

다만 함수를 한번만 사용하면 위의 2번 과정을 제대로 수행하기가 어렵기 때문에 sp_check를 여러번 사용하여 2번 과정을 수행할 수 있도록 하였습니다. 물론 지뢰찾기의 맵이 커지면 sp_check도 그만큼 더 많이 수행해야 합니다. sp_check 함수는 앞 포스팅에서 설명드렸으니 기억이 안나시는 분은 다시 한번 보시는 것을 추천합니다.

그 외의 타일 값들은 별 조작 없이 그냥 버튼만 없애면 되므로 생략하였습니다.

- victoryCheck();

이렇게 하나의 버튼을 눌렀을 때의 처리를 다 끝내고 나면 승리를 체크하는 함수인 victoryCheck 함수를 실행합니다. 이 함수는 버튼이 총 10개가 남아있으면 승리 라는 메세지를 출력시키고 남은 10개의 버튼을 삭제시킵니다. 버튼이 총 10개가 남아있을 려면 지뢰인 버튼을 누르지 않아야 하므로 오리지널 지뢰찾기와 승리조건이 똑같아집니다.

다음은 윈도우 창에 출력시키는 부분은 WM_PAINT 메시지를 살펴봅시다. 일단 코드를 먼저 봅시다.

-  hdc = BeginPaint(hWnd,&ps);

dc를 얻어내서 hdc에 저장시킵니다.

-  landmine = LoadBitmap(Global_hInstance, MAKEINTRESOURCE(LM));
   Number1 = LoadBitmap(Global_hInstance, MAKEINTRESOURCE(N1));
   Number2 = LoadBitmap(Global_hInstance, MAKEINTRESOURCE(N2));
   Number3 = LoadBitmap(Global_hInstance, MAKEINTRESOURCE(N3));
   Number4 = LoadBitmap(Global_hInstance, MAKEINTRESOURCE(N4));
   Number5 = LoadBitmap(Global_hInstance, MAKEINTRESOURCE(N5));
   Number6 = LoadBitmap(Global_hInstance, MAKEINTRESOURCE(N6));
   Number7 = LoadBitmap(Global_hInstance, MAKEINTRESOURCE(N7));
   Number8 = LoadBitmap(Global_hInstance, MAKEINTRESOURCE(N8));
   space = LoadBitmap(Global_hInstance, MAKEINTRESOURCE(SP));

비트맵이미지를 불러와 각각의 변수에 저장시킵니다. 비트맵 리소스와 LoadBitmap함수를 사용하였습니다. LoadBitmap함수를 사용하였으므로 각 변수들의 자료형은 HBITMAP입니다.

- for(i=0;i<9;i++)
     for(j=0;j<9;j++)
        buttonValue[m] = arr[i][j];
        m++;

윈도우 창에 비트맵을 9*9형태로 출력시키기 위해 이중 for문을 사용합니다. 그리고 따로 선언해놓은 인덱스변수 m을 사용하여 2차원 배열 arr에 저장된 타일값들을 1차원 배열에 저장시킵니다. 이렇게 하면 buttonValue배열에는 9*9의 타일들의 값이 한줄로 들어가게 됩니다. 하나의 타일값을 저장시키고 나면 인덱스를 하나 증가시켜야 하므로 m++을 사용하였습니다.

- switch(arr[i][j])
      case 0:
       hdcsrc = CreateCompatibleDC(hdc);
       Bitmap = (HBITMAP)SelectObject(hdcsrc,landmine);
       BitBlt(hdc,x,y,18,18,hdcsrc,NULL,NULL,SRCCOPY);
       DeleteObject(Bitmap);
       DeleteDC(hdcsrc);
       break;

지뢰찾기 맵이 저장되어있는 arr배열의 값들에 맞게 비트맵을 출력시키기 위해 switch함수를 사용하여씁니다. 2중 for문에 의해 한 줄씩 출력이 가능하므로 이에 맞춰주기 위해 arr[i][j]를 인자로 사용하였습니다.

이제 이 arr[i][j]값에 맞는 비트맵 이미지를 출력시켜야 하는데 비트맵을 출력시키기 위해선 dc가 2개 필요합니다. 그냥 DC(화면)와 메모리 DC 말입니다. 왜냐하면 비트맵이미지를 출력시킬 때 컴퓨터는 그냥 출력하면 속도가 많이 떨어지기 때문에 약간의 준비과정을 거치기 되는데 그 준비과정을 거칠 때 필요한 것이 바로 메모리 DC이기 때문입니다.  메모리 DC에 미리 출력할 이미지를 등록해 놓고 나중에 화면에 출력할 때 메모리 DC에 있는 이미지를 복사하여 출력하게 되는 것입니다.

우리는 이 메모리 DC를 얻기 위해 CreateCompatibleDC함수를 사용하였습니다. MSDN의 설명을 봅시다.

지정된 DC(화면 DC)와 호환되는 메모리DC를 생성해주는 함수라고 되어있습니다. 인자는 지정할 DC(화면 DC)입니다.

이 함수를 사용하여 기존의 DC와 호환되는 메모리DC를 만들어 hdcsrc에 저장시킵니다.

그 다음은 SelectObject함수를 사용하여 아까 만든 메모리DC에 landmine이란 이미지를 등록시킵니다. 그리고 DeleteObject함수를 사용하기 위해 SelectObject의 반환값을 HBITMAP형태로 Bitmap에 저장시킵니다.

자 이제 비트맵을 출력할 때입니다. 비트맵을 출력하기 위해 우리는 BitBlt함수를 사용하였습니다. 이 함수의 설명을 봅시다.

풀이하면 메모리 DC에 등록된 이미지를 고속으로 복사하여 화면 DC에 출력시키는 함수라고 되어 있습니다. 인자들을 하나씩 살펴봅시다.

* hdcDest

출력할 화면 DC입니다.

* nXDest

이미지를 출력할 x좌표 입니다.

* nYDest

이미지를 출력할 Y좌표입니다.

* nWidth

출력할 이미지의 가로길이 입니다.

* nHeight

출력할 이미지의 세로길이 입니다.

* hdcSrc

복사대상이 될 DC입니다. 여기서는 메모리 DC가 되겠군요.

* nXSrc

복사 대상 이미지의 x좌표입니다. 우리는 하나의 이미지를 통째로 복사할 것이므로 NULL값으로 주었습니다.

* nXSrc

복사 대상 이미지의 y좌표입니다. 우리는 하나의 이미지를 통째로 복사할 것이므로 NULL값으로 주었습니다.

* dwRop

복사할 때의 옵션입니다. 우리는 그냥 복사만 할 것이므로 SRCCOPY옵션을 주었습니다. 다른 옵션의 설명은 MSDN에 있으므로 궁금하신 분은 직접 참고하시면 됩니다.

우리는 arr[i][j]에 맞는 이미지를 하나씩 총 81번 출력할 것이므로 기본 좌표를 x,y로 두고 크기는 18*18로 두었습니다. 그리고 이렇게 하나의 출력이 끝나면 메모리 DC에 등록된 이미지를 DeleteObject함수를 사용해 해제하고, DeleteDC함수를 사용하여 메모리 DC를 삭제합니다.

참고로 기본 좌표인 x와 y는 선언할 때 30으로 두었으며 나머지 case문은 값만 다르고 다 똑같기에 설명을 생략하였습니다.

-   x += 18; 

한칸을 출력했으니 switch문을 빠져나갑니다. 이제 다음 칸을 출력시켜야겠지요? 다음칸을 출력시키기 위해서는 좌표를 (기존좌표+크기)로 바꾸어주어야 하므로 x에 18을 더합니다.

- x = 30;
  y += 18;

한 줄이 다 출력되고 나면 2중 for문에 의해 다음 줄을 출력하게 됩니다. 그러므로 다음 줄을 출력할 수 있도록 x를 기존 값으로 설정하고 y의 값을 비트맵의 크기만큼 증가시킵니다.

- EndPaint(hWnd,&ps);

모든 for문이 다 끝나고 나면 dc를 반환합니다.

네 이것으로 모든 코드 설명이 끝났습니다. 저는 여태 만들어본 프로그램 중에서 가장 어렵고 가장 재밌었던것 같습니다.

다음 포스팅에선 이제 우리가 만든 이 지뢰찾기를 좀 더 다듬어보겠습니다.

이상으로 지뢰찾기 포스팅을 마치겠습니다.

Posted by englishmath