I'm on your side, when times get rough.

2011-09-23

[Win32 C] Add a button to Titlebar (Windows Classic theme)

Filed under: Programming — Peter_KIM @ 08:36

Windows 운영 체제에서 아래의 그림과 같이 타이틀 바에 버튼을 생성할 수 있습니다.

image

image

Windows XP 이후부터는 운영체제에서 제공하는 테마(Theme)팩을 적용하여, 다양한 형태로 창의 표시 형태를 변경할 수 있습니다. 여기에서는 Windows 95 시절부터 현재까지 지속적으로 제공되는 고전(Windows Classic) 스타일의 윈도우 형태에서 작업을 하겠습니다.

윈도우 메인 함수에서, 아래의 코드와 같이 다이얼로그를 생성합니다.

int PASCAL WinMain(HINSTANCE hInstance,

         HINSTANCE hPrevInstance,

         LPSTR     lpCmdLine,

         int       nCmdShow)

{

         LONG dwBaseUint = 0;

         __int16 nBaseunitX = 0;

         __int16 nBaseunitY = 0;

 

         // Retrieves the system’s dialog base units,

         // which are the average width and height of characters in the system font.

         dwBaseUint = GetDialogBaseUnits();

 

         // The low-order word of the return value contains the horizontal dialog box base unit,

         // and the high-order word contains the vertical dialog box base unit.

         nBaseunitX = LOWORD(dwBaseUint);

         nBaseunitY = HIWORD(dwBaseUint);

 

         // Each horizontal base unit is equal to 4 horizontal dialog template units;

         // Each vertical base unit is equal to 8 vertical dialog template units

         g_MainDlgTemplate.cx = (__int16)MulDiv(400, 4, nBaseunitX);

         g_MainDlgTemplate.cy = (__int16)MulDiv(400, 8, nBaseunitY);

         // The style of the dialog box.

         g_MainDlgTemplate.style = DS_CENTER | DS_CONTEXTHELP | WS_POPUP | WS_SYSMENU | WS_CAPTION;

         //g_MainDlgTemplate.style = DS_CENTER | WS_OVERLAPPEDWINDOW;

         // g_MainDlgTemplate.dwExtendedStyle = WS_EX_TOOLWINDOW;

         // Creates a modal dialog box from a dialog box template in memory.

         DialogBoxIndirectA(hInstance, &g_MainDlgTemplate, GetDesktopWindow(), (DLGPROC)DialogProc);

 

         return 0;

}

다이얼로그 템플릿의 스타일에 따라서, 타이틀 바와 그 위에 그려지는 버튼의 크기와 위치가 달라질 수 있습니다.

INT_PTR CALLBACK DialogProc(HWND hDlg, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
         BOOL bOverButton = FALSE;
 
         switch (uMsg)
         {
         case WM_INITDIALOG:
                 OnInitialDialog(hDlg);
                 return TRUE;
 
         case WM_NCRBUTTONDOWN:
                 if (OnNcRButtonDown(hDlg, wParam, lParam))
                          return TRUE;
                 break;
 
         case WM_NCLBUTTONDOWN:
                 bOverButton = OnNcLButtonDown(hDlg, wParam, lParam);
                 DrawTitlebarButton(hDlg, bOverButton);
                 break;
 
         case WM_LBUTTONUP:
                 bOverButton = OnLButtonUp(hDlg, lParam);
                 DrawTitlebarButton(hDlg, bOverButton);
                 break;
 
         case WM_MOUSEMOVE:
                 if (g_bButtonPushed) {
                          bOverButton = OnMouseMove(hDlg, lParam);
                          DrawTitlebarButton(hDlg, g_bButtonPushed && bOverButton);
                 } 
                 break;
 
         case WM_NCPAINT:
                 DefWindowProc(hDlg, uMsg, wParam, lParam);
                 DrawTitlebarButton(hDlg, FALSE);
                 return TRUE;
 
         case WM_NCACTIVATE:
         case WM_ACTIVATE:
         case WM_ACTIVATEAPP:
                 DrawTitlebarButton(hDlg, FALSE);
         break;
 
         case WM_COMMAND:
                 switch (LOWORD(wParam))
                 {
                 case IDOK:
                 case IDCANCEL:
                          // Destroys a modal dialog box.
                          EndDialog(hDlg, LOWORD(wParam));
                          return TRUE;
                 }
                 break;
 
         case WM_DESTROY:
                 PostQuitMessage(0);
                 break;
         }
         return FALSE;
}

타이틀 바가 표시되는 영역은 응용 프로그램의 클라언트 영역이 아니므로 WM_NC*** 계열의 메시지를 처리해서 작업을 해야 합니다. 타이틀 바, 윈도우 경계등의 비 클라이언트 영역을 그릴 때, WM_NCPAINT 메시지가 발생하는데, 이 때에 버튼 모양을 그려주면 됩니다. 혹시, CreateWindow() 함수를 이용하여, 타이틀 바 영역에 버튼을 만들어서 표시 할 수 있는 것은 아니냐는 분들이 계실지 모르겠습니다. CreateWindow() 함수는 윈도우의 클라이언트 영역 위에서 컨트롤을 생성하는 것이므로, 이 함수를 이용하는 것은 불가능합니다. 아래의 코드를 참조하십시오.

CreateWindowExA(0, "BUTTON", "…….",

         BS_AUTOCHECKBOX | BS_PUSHLIKE | BS_VCENTER | BS_CENTER | WS_TABSTOP | WS_VISIBLE | WS_CHILD,

         0, -50, 100, 100, hDlg, (HMENU)NULL, NULL, NULL);

위 코드를 이용하여, 버튼을 생성한 경우, 타이틀 바에 의하여, 아래의 그림과 같이 생성된 버튼의 일부분이 가려지게 됩니다.

image

그러므로, 아래와 같이 버튼의 모양을 그려보겠습니다.

VOID DrawTitlebarButton4WinClassic(HWND hDlg, BOOL bPushed)
{
#if (WINVER >= 0x0500)
         TITLEBARINFO ti = {0}; // 타이틀바의 정보
         RECT rcWnd = {0}; // 윈도우 크기
         HDC hDC = NULL; 
 
         POINT ptLeftTop = {0};
         SIZE  szBtn = {0};
         RECT  rcBtn = {0};
         PRECT pRect = NULL;
 
         DWORD dwStyleEx = 0;
         __int32 nButtonCount = 0;
 
         // 타이틀바의 정보를 획득한다.
         ti.cbSize = sizeof(TITLEBARINFO);
         if (!GetTitleBarInfo(hDlg, &ti))
                 return;
         // 타이틀바가 표시되지 않으면 함수를 종료한다.
         if (STATE_SYSTEM_FOCUSABLE != ti.rgstate[0])
                 return;
 
         // 윈도우 크기를 구한다.
         GetWindowRect(hDlg, &rcWnd);
         // 윈도우 스타일을 확인한다.
#if (WINVER >= 0x0501)
         dwStyleEx = GetWindowLongPtr(hDlg, GWL_EXSTYLE);
#else
         dwStyleEx = GetWindowLong(hDlg, GWL_EXSTYLE);
#endif
 
         if ((dwStyleEx & WS_EX_TOOLWINDOW) == WS_EX_TOOLWINDOW) {
                 nButtonCount = 1;
                 // SM_CXSMSIZE, SM_CYSMSIZE :The width, height of small caption buttons, in pixels.
                 szBtn.cx = GetSystemMetrics(SM_CXSMSIZE);
                 szBtn.cy = GetSystemMetrics(SM_CYSMSIZE);
         } else {
                 //2 Minimize button. 
                 //3 Maximize button. 
                 //4 Help button.
                 //5 Close button. 
                 if (ti.rgstate[2] == 0) { 
                          // 최소화 버튼이 활성화되어 있으면, 버튼의 갯수는 3개이다.
                          nButtonCount = 3;
                 }
                 else if (ti.rgstate[4] == 0) {
                          // HELP 버튼이 활성화된 경우, Close 버튼도 함께 고려해야한다.
                          nButtonCount = (ti.rgstate[5] == 0) ? 2: 0;
                 } 
                 else if (ti.rgstate[5] == 0) {
                          nButtonCount = 1;
                 } 
 
                  // SM_CXSIZE, SM_CYSIZE :The width, height of a button in a window caption or title bar, in pixels..
#if (WINVER >= 0x0501)
                 if (IsThemeActive()) {
                          szBtn.cx = GetThemeSysSize(NULL, SM_CXSIZE);
                          szBtn.cy = GetThemeSysSize(NULL, SM_CYSIZE);
                 }
                 else 
#endif 
                 {
                          szBtn.cx = GetSystemMetrics(SM_CXSIZE);
                          szBtn.cy = GetSystemMetrics(SM_CYSIZE);
                 }
         }
         // 버튼의 위치를 구한다.
         ptLeftTop.x = ti.rcTitleBar.right - (szBtn.cx * (nButtonCount + 1));
         ptLeftTop.y = ti.rcTitleBar.top;
 
         pRect = &g_rcButton;
         // 윈도우의 Frame 위치로부터, Non Client 영역에 버튼이 그려질 위치를 구한다.
         pRect->left = ptLeftTop.x - rcWnd.left;
         pRect->right = pRect->left + szBtn.cx;
         pRect->top = ptLeftTop.y - rcWnd.top - 1;
         pRect->bottom = pRect->top + szBtn.cy + 1;
 
         // 윈도우 DC 객체를 획득한다.
         hDC = GetWindowDC(hDlg);
         // 실제 버튼의 영역과 버튼이 그려질 영역을 구분하여 그린다.
         CopyRect(&rcBtn, pRect);
         if (nButtonCount > 1) {
                 rcBtn.left += 2;
         } else { 
                 rcBtn.right -= 2;
         }
         rcBtn.top += 3;
         rcBtn.bottom -= 2;
         DrawFrameControl(hDC, &rcBtn, DFC_BUTTON, bPushed ? DFCS_BUTTONPUSH | DFCS_PUSHED : DFCS_BUTTONPUSH);
         // DC 객체를 해제한다.
         ReleaseDC(hDlg, hDC);
 
#endif
}

여기에서 주의 할 점은 DrawFrameControl 함수를 사용하게 되면, 운영 체제에 테마가 설정되어 있더라도, 고전 형식으로 타이틀 바가 그려지게 됩니다. 때에 따라서, 윈도우가 아래의 그림과 같이 비 정상적으로 표시 될 수 있습니다.

image

그러므로, 이 프로그램에서는 아래의 코드와 같이 SetWindowTheme() 함수를 이용하여, 응용 프로그램의 테마를 해제하겠습니다.

VOID OnInitialDialog(HWND hDlg)
{
#if (WINVER >= 0x0501)
         if (IsThemeActive())
                 SetWindowTheme(hDlg, L””, L””);
#endif
}

타이틀 바에 그려 넣은 버튼 모양이 버튼의 기능을 수행하도록 만들기 위하여, 마우스 왼쪽 버튼의 클릭, 마우스 포인터 이동 등의 동작에 대한 이벤트를 처리합니다. , 응용 프로그램이 다시 그려지는 경우, 그려 넣은 버튼 모양이 타이틀 바에 표시되도록 해야 합니다. 위의 코드에서 알 수 있겠지만, 마우스 클릭 이벤트는 본래, 버튼의 눌림(DOWN)과 해제(UP) 이벤트의 조합입니다. SPY++ 같은 응용 프로그램으로 확인 할 수 있듯이, 타이틀 바의 시스템 버튼을 클릭하면, WM_NCLBUTTONDOWN, WM_LBUTTONUP 이벤트가 발생합니다. WM_NCLBUTTONUP 이벤트를 처리하는 것이 아님에 주의해야 합니다. 이 때에, 마우스 포인터의 위치 값이 LPARAM 인자와 함께 전달되는데, WM_NC*** 계열의 이벤트와 WM_*** 계열의 이벤트에서 전달되는 위치 값은 서로 상이합니다. 이런 이유는 WM_*** 계열의 이벤트는 클라이언트 영역의 좌상(left-top) 단의 값을 기준으로 증가 또는 감소한 값을 전달하는 반면, WM_NC*** 계열의 이벤트는 운영 체제가 표시하는 화면 영역의 좌상(left-top) 단을 기준으로 결정한 값을 반환하기 때문입니다. 그러므로, WM_*** 계열의 이벤트에서 전달되는 위치 값을 ClientToScreen() 함수를 이용하여, 좌표를 시스템의 데스크 탑 좌표로 변경해야 합니다.

이벤트를 처리하는 함수는 아래와 같습니다.

BOOL OnMouseMove(HWND hDlg, LPARAM lParam)
{
         POINT ptMove = {0};
 
         ptMove.x = GET_X_LPARAM(lParam); 
         ptMove.y = GET_Y_LPARAM(lParam);
 
         ClientToScreen(hDlg, &ptMove);
 
         return IsOverButton(hDlg, &ptMove);
}
 
BOOL OnNcMouseMove(HWND hDlg, WPARAM wParam, LPARAM lParam)
{
         POINT ptMove = {0};
         BOOL bOver = FALSE;
 
         ptMove.x = GET_X_LPARAM(lParam); 
         ptMove.y = GET_Y_LPARAM(lParam);
 
         if (wParam == HTCAPTION)
                 bOver = IsOverButton(hDlg, &ptMove);
 
         return bOver;
}
 
BOOL OnNcRButtonDown(HWND hDlg, WPARAM wParam, LPARAM lParam)
{
         POINT ptDown = {0};
 
         ptDown.x = GET_X_LPARAM(lParam); 
         ptDown.y = GET_Y_LPARAM(lParam); 
 
         if (wParam == HTCAPTION)
                 return IsOverButton(hDlg, &ptDown);
 
         return FALSE;
}
 
BOOL OnNcLButtonDown(HWND hDlg, WPARAM wParam, LPARAM lParam)
{
         POINT ptDown = {0};
 
         ptDown.x = GET_X_LPARAM(lParam); 
         ptDown.y = GET_Y_LPARAM(lParam); 
 
         if (wParam == HTCAPTION)
                 g_bButtonPushed = IsOverButton(hDlg, &ptDown);
         else
                 g_bButtonPushed = FALSE;
 
         if (g_bButtonPushed)
                 SetCapture(hDlg);
 
         return g_bButtonPushed;
}
 
BOOL OnLButtonUp(HWND hDlg, LPARAM lParam)
{
         POINT ptDown = {0};
 
         ReleaseCapture();
 
         if (g_bButtonPushed)
         {
                 ptDown.x = GET_X_LPARAM(lParam); 
                 ptDown.y = GET_Y_LPARAM(lParam); 
                 ClientToScreen(hDlg, &ptDown);
 
                 if (IsOverButton(hDlg, &ptDown))
                          MessageBoxA(hDlg, "Button Clicked", "", MB_OK | MB_ICONINFORMATION);
         }
         g_bButtonPushed = FALSE;
 
         return FALSE;
}
 
BOOL IsOverButton(HWND hDlg, LPPOINT lp)
{
         RECT rcWnd = {0};
         RECT rcBtn = {0};
 
         GetWindowRect(hDlg, &rcWnd);
 
         rcBtn.left = rcWnd.left + g_rcButton.left;
         rcBtn.top = rcWnd.top + g_rcButton.top;
         rcBtn.right = rcBtn.left + RECTWIDTH(&g_rcButton);
         rcBtn.bottom = rcBtn.top + RECTHEIGHT(&g_rcButton);
         OffsetRect(&rcBtn, 0, 1);
 
         return PtInRect(&rcBtn, *lp);
}
 

코드를 이용하여, 표시되는 타이틀 바의 모습은 아래의 그림과 같습니다.

image

전체 소스 코드는 아래의 주소에서 얻을 수 있습니다.

https://skydrive.live.com/embedicon.aspx/.Public/TitlebarButton.7z?cid=1bbcdfedee1c617e&sc=documents

Create a free website or blog at WordPress.com.