. 색상

색상도 보기와 마찬가지로 프로그램의 외형을 결정하는 중요한 요소이다. 화면의 곳곳에 대해 사용자가 색상을 정의할 수 있도록 해두었다. 입력받아야 할 색상값이 많고 색상을 입력받는 방법이 동일하므로 별도의 페이지를 따로 만들었다.

색상값들은 모두 오너 드로우 콤보박스로 보여주고 입력받는다. 색상값을 입력받는 방법에는 여러 가지 다양한 방법이 있다. 바탕 화면처럼 미리 보기를 보여주고 변경할 대상을 클릭하여 고른 후 색상을 선택 또는 입력하는 방법도 있고 색상 이름을 나열하고 그 옆에 팔레트를 그려 두어 대상을 먼저 선택한 후 색상을 고르도록 하는 방법도 생각해 볼 수 있다.

오너 드로우 콤보박스는 직관적인 인터페이스를 제공하기는 하지만 그다지 예쁜 모양은 아니며 다양한 색상을 고르기에도 좀 불편한 면이 있다. 그럼에도 불구하고 오너 드로우 콤보박스를 선택한 이유는 터치수가 낮기 때문이다. 이 구조에서는 콤보박스 연다, 색상 고른다 방식의 투터치(Two Touch)만으로 신속하게 색상을 바꿀 수 있다. 변경 대상을 먼저 선택하고 RGB값을 각각 입력 또는 선택하고 변경 버튼을 클릭하는 번거로운 방식을 싫어하는 성격이라 이런 단순한 방법을 선택했는데 개발자의 취향에 따라 다양한 변화를 줄 수 있을 것이다.

당근은 두 번의 클릭으로 색상을 선택할 수 있도록 하기 위해 색상값을 좀 특수한 방식으로 다루고 있다. 색상이란 RGB 세 요소의 강도 조합으로 표현되는데 각 요소를 개별적으로 직접 입력 또는 선택하여 원하는 색상을 만들어 내는 방식은 비록 자유로운 선택을 가능하게 하지만 무척 번거로운 일이다. 그래서 이런 직접 선택 방식보다는 미리 정의된 색상표를 제공하고 이 중 하나를 고르는 방식을 유도하였다.

물론 색상선택 대화상자를 호출하여 직접 원하는 색상을 고르는 방법도 같이 제공해야 한다. 다음 구조체는 미리 정의된 색상표 구조체와 기본색상값을 가지는 배열이다. Dangeun.cpp의 선두에 다음 구조체를 선언 및 초기화하도록 하자. 이 구조체는 차후에 문법 정의에도 사용된다.

 

struct arColor

{

     TCHAR Name[16];

     COLORREF Color;

};

 

arColor arPreColor[]={

     {"기본색",RGB(0,0,0)},

     {"사용자 선택색",RGB(0,0,0)},

     {"사용자 선택...",RGB(0,0,0)},

     {"검정",RGB(0,0,0)},

     {"흰색",RGB(255,255,255)},

     {"빨강",RGB(255,0,0)},

     {"초록",RGB(0,255,0)},

     {"파랑",RGB(0,0,255)},

     {"노랑",RGB(255,255,0)},

     {"분홍",RGB(255,0,255)},

     {"하늘색",RGB(0,255,255)},

     {"고동색",RGB(128,0,0)},

     {"진초록",RGB(0,128,0)},

     {"남색",RGB(0,0,128)},

     {"보라색",RGB(180,84,233)},

     {"회색1(32)",RGB(32,32,32)},

     {"회색2(64)",RGB(64,64,64)},

     {"회색3(96)",RGB(96,96,96)},

     {"회색4(128)",RGB(128,128,128)},

     {"회색5(160)",RGB(160,160,160)},

     {"회색6(192)",RGB(192,192,192)},

     {"회색7(220)",RGB(220,220,220)},

     {"연노랑",RGB(255,249,157)},

     {"개나리색",RGB(255,209,87)},

     {"황토색",RGB(207,182,80)},

     {"연파랑",RGB(141,207,244)},

     {"감색",RGB(177,202,147)},

     {"흐린분홍",RGB(182,99,105)},

     {"연두색",RGB(169,212,109)}

};

 

COLORREF arSysColor[10]={0,0,0,0,RGB(192,192,192),RGB(160,160,160),RGB(255,255,0),

     RGB(128,128,128),RGB(255,255,0),RGB(0,0,0)};

 

arColor 구조체는 사용자가 선택할 수 있는 색상의 목록을 정의하는데 색상의 이름인 Name과 실제 색상인 Color로 구성된다. arPreColor는 미리 정의된 색상의 배열, 즉 색상표이다. 첨자 0은 기본색, 1는 사용자가 선택한 색상을 나타내며 첨자 2는 색상이 아니라 색상선택 대화상자를 여는 명령으로 사용된다. 첨자 3 이후가 미리 정의된 색상값이며 자주 선택되는 몇 가지 색상에 대한 RGB값을 가지고 있다.

arSysColor SOption에 정의된 색상 변수들의 기본값을 정의한다. 옵션을 바꾼 상태에서도 기본값으로 돌아갈 수 있어야 하므로 이 정보는 따로 가지고 있어야 한다. 색상표에서 0, 즉 기본색을 누를 때 이 배열을 참조하여 원래 색으로 돌아가게 된다. 앞쪽의 4가지 옵션에 대한 기본색은 시스템 색상이기 때문에 상수로 미리 정할 수 없으며 일단 0으로 초기화해놓고 GetSysColor 함수로 실제 색상을 조사해야 한다. 이 색상을 초기화하는 InitSysColor라는 함수를 만들고 Config 함수에서 호출하였다.

cFore, cSelBack, NumColor SOption의 색상 변수들은 직접 색상값을 가지는 것이 아니라 최상위 바이트에 arPreColor 배열의 첨자를 가진다. 이 첨자가 0이면 기본색이고 1 이상이면 뒤의 3바이트에서 색상값을 추출한다. 그래서 SOption::Init에서는 색상 변수들을 직접 초기화하지 않고 모두 0으로 초기화하여 기본값을 가지도록 하였다. 색상의 기본값에 대한 정보는 SOption이 가지지 않으며 arSysColor 배열이 따로 가지고 있다.

색상 옵션을 어떻게 관리하고 다루는지는 색상 페이지의 대화상자 프로시저를 작성하면서 좀 더 연구해보도록 하자.

 

COLORREF& GetColorFromID(int ID)

{

     switch (ID) {

     case IDC_CFORE:

          return NewOption.cFore;

     case IDC_CBACK:

          return NewOption.cBack;

     case IDC_CSELFORE:

          return NewOption.cSelFore;

     case IDC_CSELBACK:

          return NewOption.cSelBack;

     case IDC_MARCOLOR1:

          return NewOption.MarColor1;

     case IDC_MARCOLOR2:

          return NewOption.MarColor2;

     case IDC_MARKCOLOR:

          return NewOption.MarkColor;

     case IDC_CODECOLOR:

          return NewOption.CodeColor;

     case IDC_CURCOLOR:

          return NewOption.CurColor;

     default:

     case IDC_NUMCOLOR:

          return NewOption.NumColor;

     }

}

 

void InitSysColor()

{

     arSysColor[0]=GetSysColor(COLOR_WINDOWTEXT);

     arSysColor[1]=GetSysColor(COLOR_WINDOW);

     arSysColor[2]=GetSysColor(COLOR_HIGHLIGHTTEXT);

     arSysColor[3]=GetSysColor(COLOR_HIGHLIGHT);

}

 

BOOL CALLBACK ColorDlgProc(HWND hDlg,UINT iMessage,WPARAM wParam,LPARAM lParam)

{

     int i,idx;

     LPMEASUREITEMSTRUCT lpmis;

     LPDRAWITEMSTRUCT lpdis;

     HBRUSH bkBrush, Brush, OldBrush;

     int OldMode;

     CHOOSECOLOR COL;

     static COLORREF crTemp[16];

     COLORREF Color;

 

     switch(iMessage)

     {

     case WM_INITDIALOG:

          for (idx=IDC_CFORE;idx<=IDC_NUMCOLOR;idx++) {

              for (i=0;i<sizeof(arPreColor)/sizeof(arPreColor[0]);i++) {

                   SendDlgItemMessage(hDlg,idx,CB_ADDSTRING,0,0);

              }

          }

          return TRUE;

     case WM_MEASUREITEM:

          lpmis=(LPMEASUREITEMSTRUCT)lParam;

          lpmis->itemHeight=16;

          return TRUE;

          break;

     case WM_DRAWITEM:

          Color=GetColorFromID(wParam);

          lpdis=(LPDRAWITEMSTRUCT)lParam;

 

          if (lpdis->itemState & ODS_SELECTED) {

              bkBrush=CreateSolidBrush(RGB(0,0,255));

          }

          else {

              bkBrush=CreateSolidBrush(RGB(255,255,255));

          }

          FillRect(lpdis->hDC, &lpdis->rcItem, bkBrush);

 

          switch (lpdis->itemID) {

          case 0:

              Brush=CreateSolidBrush(arSysColor[wParam-IDC_CFORE]);

              break;

          case 1:

              Brush=CreateSolidBrush(Color & 0xffffff);

              break;

          default:

              Brush=CreateSolidBrush(arPreColor[lpdis->itemID].Color);

              break;

          }

          OldBrush=(HBRUSH)SelectObject(lpdis->hDC, Brush);

          if (lpdis->itemID != 2) {

              Rectangle(lpdis->hDC,lpdis->rcItem.left+2,lpdis->rcItem.top+1,

                   lpdis->rcItem.left+20, lpdis->rcItem.bottom-1);

          }

 

          OldMode=SetBkMode(lpdis->hDC,TRANSPARENT);

          TextOut(lpdis->hDC,lpdis->rcItem.left+25,lpdis->rcItem.top+2,

              arPreColor[lpdis->itemID].Name,lstrlen(arPreColor[lpdis->itemID].Name));

 

          SetBkMode(lpdis->hDC,OldMode);

          SelectObject(lpdis->hDC, OldBrush);

          DeleteObject(bkBrush);

          DeleteObject(Brush);

          return TRUE;

          break;

     case WM_COMMAND:

          switch (HIWORD(wParam)) {

          case CBN_SELCHANGE:

              idx=SendDlgItemMessage(hDlg,LOWORD(wParam),CB_GETCURSEL,0,0);

              if (idx==2) {

                   memset(&COL, 0, sizeof(CHOOSECOLOR));

                   COL.lStructSize = sizeof(CHOOSECOLOR);

                   COL.hwndOwner=hDlg;

                   COL.lpCustColors=crTemp;

                   if (ChooseColor(&COL)!=0) {

                        GetColorFromID(LOWORD(wParam))=COL.rgbResult | 0x01000000;

                        InvalidateRect(GetDlgItem(hDlg,LOWORD(wParam)),NULL,TRUE);

                   }

                   SendDlgItemMessage(hDlg,LOWORD(wParam),CB_SETCURSEL,1,0);

              } else {

                   GetColorFromID(LOWORD(wParam))=idx << 24 | arPreColor[idx].Color;

              }

              PropSheet_Changed(GetParent(hDlg),hDlg);

              break;

          }

          return TRUE;

     case WM_NOTIFY:

          switch (((LPNMHDR)lParam)->code) {

          case PSN_SETACTIVE:

              g_StartPage=2;

              for (idx=IDC_CFORE;idx<=IDC_NUMCOLOR;idx++) {

                   SendDlgItemMessage(hDlg,idx,CB_SETCURSEL,GetColorFromID(idx) >> 24,0);

              }

              return TRUE;

          case PSN_APPLY:

              ApplyNow();

              return TRUE;

          case PSN_KILLACTIVE:

              return TRUE;

          }

          break;

     }

     return FALSE;

}

 

GetColorFromID함수는 색상을 표현하는 콤보박스의 ID로부터 옵션 구조체의 대응되는 멤버를 구한다. 값을 구하는 것이 아니라 변수의 레퍼런스를 리턴하므로 이 함수의 리턴값을 읽거나 변경할 수 있다. 예를 들어 전경색을 관리하는 IDC_CFORE 콤보박스의 ID를 인수로 전달하면 이 함수는 이 컨트롤과 대응되는 NewOption.cFore 변수 자체를 리턴한다. 함수의 리턴값에 값을 대입하는 형식인 func()=value; C++에서만 가능한 방법인데 흔히 쓰는 문법은 아니지만 사용해보면 편리하다.

ColorDlgProc 함수는 다른 페이지와는 달리 오너 드로우 콤보박스를 사용하기 때문에 전체적인 모양이나 처리하는 메시지의 종류가 좀 다르다. WM_INITDIALOG에서는 모든 콤보박스에 색상표만큼의 항목을 추가한다. 항목 자체는 WM_DRAWITEM에서 그려지므로 문자열을 지정하지 않아도 되며 빈 항목이라도 일단 추가해놓기만 하면 된다.

오너 드로우 콤보박스는 WM_MEASUREITEM, WM_DRAWITEM 메시지에서 그려지는데 일반적인 코드이므로 구체적인 분석은 하지 않기로 한다. 오너 드로우에 대한 자료는 많이 공개되어 있으므로 참고자료를 찾아 보기 바란다. 색상값 항목들의 높이는 모두 16픽셀로 고정되어 있으며 항목 인덱스와 선택 여부에 따라 조금씩 다르게 그려진다. 선택된 항목은 배경을 파란색으로 그리며 나머지 항목은 흰색 배경을 가진다.

현재 항목 인덱스가 0이면 즉, 기본색이면 arSysColor 배열에서 기본색을 구하고 사용자정의색(1)이면 NewOption의 대응되는 색상 변수에서 최상위 바이트를 0으로 만든 RGB값을 구하며 3이상인 경우는 arPreColor 색상표에서 색상을 구한다. 2번 인덱스는 색상이 아니라 색상 대화상자를 보여주라는 명령이기 때문에 콤보박스가 이 인덱스를 가지는 경우는 없다.

WM_COMMAND에서는 2번 항목, 즉 사용자 선택 항목을 선택했을 때 색상선택 대화상자를 보여주고 이 대화상자에서 사용자가 선택한 색상을 대응되는 색상 변수에 대입한다. 기본색이나 색상표의 한 색상을 선택했으면 색상 변수의 상위 바이트에 인덱스를 넣고 하위 3바이트에 실제 색상을 넣는다. 색상 중 하나라도 변경되었으면 적용 버튼을 활성화시켜야 한다.

PSN_SETACTIVE에서는 모든 색상 콤보박스에 현재 선택된 색상을 대입하였다. 색상값이 바뀌는 즉시 NewOption의 색상 변수를 변경하고 있기 때문에 PSN_KILLACTIVE에서는 아무 것도 하지 않아도 된다.