본문으로 건너뛰기
Previous
Next
Windows API 다이얼로그 | 대화상자와 공용 컨트롤 완벽 가이드

Windows API 다이얼로그 | 대화상자와 공용 컨트롤 완벽 가이드

Windows API 다이얼로그 | 대화상자와 공용 컨트롤 완벽 가이드

이 글의 핵심

다이얼로그는 사용자 입력을 받는 팝업 창입니다. 리소스 에디터로 UI를 디자인하고 DialogBox로 모달, CreateDialog로 비모달 다이얼로그를 생성합니다. ListView, TreeView 같은 공용 컨트롤로 복잡한 데이터를 표시할 수 있습니다.

들어가며

Windows 다이얼로그는 1980년대 중반부터 Windows UI의 핵심 요소였습니다. 설정 창, 파일 열기, 메시지 박스 등 대부분의 팝업 창이 다이얼로그로 구현됩니다. 리소스 에디터(Visual Studio)를 사용하면 코드 없이 UI를 시각적으로 디자인할 수 있습니다.

// 가장 간단한 다이얼로그
INT_PTR CALLBACK DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg) {
        case WM_COMMAND:
            if (LOWORD(wParam) == IDOK || LOWORD(wParam) == IDCANCEL) {
                EndDialog(hDlg, LOWORD(wParam));
                return TRUE;
            }
            break;
    }
    return FALSE;
}

// 호출
DialogBox(hInstance, MAKEINTRESOURCE(IDD_MYDIALOG), hwnd, DlgProc);

왜 다이얼로그를 사용할까?

다이얼로그의 장점:

  1. 빠른 UI 개발 - 리소스 에디터로 드래그 앤 드롭
  2. 자동 레이아웃 - 탭 순서, 키보드 네비게이션 자동 처리
  3. 표준 UI - Windows 표준 다이얼로그 스타일
  4. 재사용성 - 리소스를 여러 곳에서 재사용 가능

1. 다이얼로그 기본

1.1 리소스 스크립트 (RC 파일)

// resource.h
#define IDD_DIALOG1  101
#define IDC_EDIT1    1001
#define IDC_BUTTON1  1002
#define IDOK         1
#define IDCANCEL     2

// dialog.rc
#include "resource.h"

IDD_DIALOG1 DIALOGEX 0, 0, 300, 200
STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "내 다이얼로그"
FONT 9, "맑은 고딕"
BEGIN
    LTEXT           "이름:",IDC_STATIC,10,10,40,14
    EDITTEXT        IDC_EDIT1,60,10,200,14,ES_AUTOHSCROLL
    DEFPUSHBUTTON   "확인",IDOK,100,180,50,14
    PUSHBUTTON      "취소",IDCANCEL,160,180,50,14
END

1.2 모달 다이얼로그 (Modal)

#include <windows.h>
#include "resource.h"

INT_PTR CALLBACK DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg) {
        case WM_INITDIALOG:
            // 다이얼로그 초기화
            SetDlgItemText(hDlg, IDC_EDIT1, L"기본값");
            return TRUE;
            
        case WM_COMMAND:
            switch (LOWORD(wParam)) {
                case IDOK: {
                    // 확인 버튼
                    wchar_t buf[256];
                    GetDlgItemText(hDlg, IDC_EDIT1, buf, 256);
                    
                    MessageBox(hDlg, buf, L"입력한 값", MB_OK);
                    EndDialog(hDlg, IDOK);
                    return TRUE;
                }
                
                case IDCANCEL:
                    // 취소 버튼
                    EndDialog(hDlg, IDCANCEL);
                    return TRUE;
            }
            break;
            
        case WM_CLOSE:
            EndDialog(hDlg, IDCANCEL);
            return TRUE;
    }
    
    return FALSE;  // ★ 처리 안 함 (DefDlgProc 호출 안 함!)
}

// 호출 (부모 윈도우에서)
case WM_COMMAND:
    if (LOWORD(wParam) == ID_FILE_OPEN) {
        INT_PTR result = DialogBox(
            hInstance,
            MAKEINTRESOURCE(IDD_DIALOG1),
            hwnd,      // 부모 윈도우
            DlgProc
        );
        
        if (result == IDOK) {
            MessageBox(hwnd, L"확인 눌렀음", L"알림", MB_OK);
        }
    }
    break;

1.3 비모달 다이얼로그 (Modeless)

HWND g_hModelessDlg = NULL;

INT_PTR CALLBACK ModelessDlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    switch (msg) {
        case WM_INITDIALOG:
            return TRUE;
            
        case WM_COMMAND:
            if (LOWORD(wParam) == IDCANCEL) {
                DestroyWindow(hDlg);  // ★ EndDialog 아님!
                g_hModelessDlg = NULL;
                return TRUE;
            }
            break;
            
        case WM_DESTROY:
            g_hModelessDlg = NULL;
            return TRUE;
    }
    
    return FALSE;
}

// 생성
case WM_COMMAND:
    if (LOWORD(wParam) == ID_VIEW_TOOLBAR) {
        if (g_hModelessDlg == NULL) {
            g_hModelessDlg = CreateDialog(
                hInstance,
                MAKEINTRESOURCE(IDD_TOOLBAR),
                hwnd,
                ModelessDlgProc
            );
            ShowWindow(g_hModelessDlg, SW_SHOW);
        } else {
            SetFocus(g_hModelessDlg);
        }
    }
    break;

// ★ 메시지 루프에서 처리 필요!
MSG msg = {};
while (GetMessage(&msg, NULL, 0, 0)) {
    // 비모달 다이얼로그 메시지 처리
    if (g_hModelessDlg == NULL || !IsDialogMessage(g_hModelessDlg, &msg)) {
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }
}

1.4 다이얼로그 데이터 전달

// 데이터 구조체
struct DialogData {
    wchar_t name[256];
    int age;
};

INT_PTR CALLBACK DlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    static DialogData* pData = nullptr;
    
    switch (msg) {
        case WM_INITDIALOG:
            // lParam으로 데이터 받기
            pData = (DialogData*)lParam;
            
            // 초기값 설정
            SetDlgItemText(hDlg, IDC_EDIT_NAME, pData->name);
            SetDlgItemInt(hDlg, IDC_EDIT_AGE, pData->age, FALSE);
            
            return TRUE;
            
        case WM_COMMAND:
            if (LOWORD(wParam) == IDOK) {
                // 데이터 저장
                GetDlgItemText(hDlg, IDC_EDIT_NAME, pData->name, 256);
                pData->age = GetDlgItemInt(hDlg, IDC_EDIT_AGE, NULL, FALSE);
                
                EndDialog(hDlg, IDOK);
                return TRUE;
            }
            break;
    }
    
    return FALSE;
}

// 호출
DialogData data = {};
wcscpy_s(data.name, L"홍길동");
data.age = 30;

INT_PTR result = DialogBoxParam(
    hInstance,
    MAKEINTRESOURCE(IDD_USERINFO),
    hwnd,
    DlgProc,
    (LPARAM)&data  // ★ 데이터 전달
);

if (result == IDOK) {
    wchar_t buf[512];
    swprintf_s(buf, L"이름: %s\n나이: %d", data.name, data.age);
    MessageBox(hwnd, buf, L"결과", MB_OK);
}

2. 공용 컨트롤 초기화

2.1 InitCommonControlsEx

#include <commctrl.h>
#pragma comment(lib, "comctl32.lib")

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE, LPSTR, int nCmdShow)
{
    // 공용 컨트롤 초기화
    INITCOMMONCONTROLSEX icex;
    icex.dwSize = sizeof(INITCOMMONCONTROLSEX);
    icex.dwICC = ICC_LISTVIEW_CLASSES |   // ListView
                 ICC_TREEVIEW_CLASSES |   // TreeView
                 ICC_BAR_CLASSES |        // Toolbar, StatusBar
                 ICC_TAB_CLASSES |        // TabControl
                 ICC_PROGRESS_CLASS |     // ProgressBar
                 ICC_UPDOWN_CLASS;        // Spin Control
    
    if (!InitCommonControlsEx(&icex)) {
        MessageBox(NULL, L"공용 컨트롤 초기화 실패!", L"오류", MB_ICONERROR);
        return 1;
    }
    
    // ... 윈도우 생성
}

3. ListView 컨트롤

3.1 기본 ListView 생성

#define ID_LISTVIEW  2001

case WM_CREATE: {
    CREATESTRUCT* pCS = (CREATESTRUCT*)lParam;
    
    // ListView 생성
    HWND hListView = CreateWindowEx(
        WS_EX_CLIENTEDGE,
        WC_LISTVIEW,
        L"",
        WS_CHILD | WS_VISIBLE | LVS_REPORT | LVS_EDITLABELS,
        10, 10, 500, 300,
        hwnd, (HMENU)ID_LISTVIEW, pCS->hInstance, NULL
    );
    
    // 확장 스타일 (전체 행 선택, 그리드)
    ListView_SetExtendedListViewStyle(hListView,
        LVS_EX_FULLROWSELECT | LVS_EX_GRIDLINES);
    
    // 컬럼 추가
    LVCOLUMN lvc = {};
    lvc.mask = LVCF_TEXT | LVCF_WIDTH | LVCF_SUBITEM;
    
    lvc.pszText = (LPWSTR)L"이름";
    lvc.cx = 150;
    lvc.iSubItem = 0;
    ListView_InsertColumn(hListView, 0, &lvc);
    
    lvc.pszText = (LPWSTR)L"나이";
    lvc.cx = 80;
    lvc.iSubItem = 1;
    ListView_InsertColumn(hListView, 1, &lvc);
    
    lvc.pszText = (LPWSTR)L"직업";
    lvc.cx = 150;
    lvc.iSubItem = 2;
    ListView_InsertColumn(hListView, 2, &lvc);
    
    // 항목 추가
    LVITEM lvi = {};
    lvi.mask = LVIF_TEXT;
    
    lvi.iItem = 0;
    lvi.iSubItem = 0;
    lvi.pszText = (LPWSTR)L"홍길동";
    ListView_InsertItem(hListView, &lvi);
    ListView_SetItemText(hListView, 0, 1, (LPWSTR)L"30");
    ListView_SetItemText(hListView, 0, 2, (LPWSTR)L"개발자");
    
    lvi.iItem = 1;
    lvi.pszText = (LPWSTR)L"김철수";
    ListView_InsertItem(hListView, &lvi);
    ListView_SetItemText(hListView, 1, 1, (LPWSTR)L"25");
    ListView_SetItemText(hListView, 1, 2, (LPWSTR)L"디자이너");
    
    return 0;
}

3.2 ListView 이벤트 처리

case WM_NOTIFY: {
    NMHDR* pNMHDR = (NMHDR*)lParam;
    
    if (pNMHDR->idFrom == ID_LISTVIEW) {
        switch (pNMHDR->code) {
            case NM_CLICK: {
                // 클릭
                NMITEMACTIVATE* pNMIA = (NMITEMACTIVATE*)lParam;
                
                wchar_t buf[256];
                ListView_GetItemText(pNMHDR->hwndFrom, pNMIA->iItem, 0, buf, 256);
                
                wchar_t msg[512];
                swprintf_s(msg, L"선택: %s (행: %d, 열: %d)",
                    buf, pNMIA->iItem, pNMIA->iSubItem);
                SetWindowText(hwnd, msg);
                break;
            }
            
            case NM_DBLCLK: {
                // 더블클릭
                NMITEMACTIVATE* pNMIA = (NMITEMACTIVATE*)lParam;
                
                wchar_t buf[256];
                ListView_GetItemText(pNMHDR->hwndFrom, pNMIA->iItem, 0, buf, 256);
                MessageBox(hwnd, buf, L"더블클릭", MB_OK);
                break;
            }
            
            case LVN_ITEMCHANGED: {
                // 선택 변경
                NMLISTVIEW* pNMLV = (NMLISTVIEW*)lParam;
                
                if (pNMLV->uNewState & LVIS_SELECTED) {
                    // 선택됨
                }
                break;
            }
            
            case LVN_COLUMNCLICK: {
                // 컬럼 헤더 클릭 (정렬 구현)
                NMLISTVIEW* pNMLV = (NMLISTVIEW*)lParam;
                
                wchar_t buf[256];
                swprintf_s(buf, L"컬럼 %d 클릭 (정렬 구현)", pNMLV->iSubItem);
                MessageBox(hwnd, buf, L"알림", MB_OK);
                break;
            }
        }
    }
    break;
}

3.3 ListView 정렬

// 정렬 콜백
int CALLBACK CompareFunc(LPARAM lParam1, LPARAM lParam2, LPARAM lParamSort)
{
    // lParam1, lParam2: 각 항목의 lParam 값
    // lParamSort: ListView_SortItems의 lParamSort
    
    // 예: 문자열 비교
    wchar_t* str1 = (wchar_t*)lParam1;
    wchar_t* str2 = (wchar_t*)lParam2;
    
    return wcscmp(str1, str2);
}

// 정렬 실행
ListView_SortItems(hListView, CompareFunc, 0);

4. TreeView 컨트롤

4.1 기본 TreeView 생성

#define ID_TREEVIEW  2002

case WM_CREATE: {
    CREATESTRUCT* pCS = (CREATESTRUCT*)lParam;
    
    // TreeView 생성
    HWND hTreeView = CreateWindowEx(
        WS_EX_CLIENTEDGE,
        WC_TREEVIEW,
        L"",
        WS_CHILD | WS_VISIBLE | TVS_HASLINES | TVS_HASBUTTONS | TVS_LINESATROOT,
        10, 10, 300, 400,
        hwnd, (HMENU)ID_TREEVIEW, pCS->hInstance, NULL
    );
    
    // 루트 항목
    TVINSERTSTRUCT tvis = {};
    tvis.hParent = TVI_ROOT;
    tvis.hInsertAfter = TVI_LAST;
    tvis.item.mask = TVIF_TEXT | TVIF_PARAM;
    tvis.item.pszText = (LPWSTR)L"내 컴퓨터";
    tvis.item.lParam = (LPARAM)0;
    HTREEITEM hRoot = TreeView_InsertItem(hTreeView, &tvis);
    
    // 자식 항목들
    tvis.hParent = hRoot;
    tvis.item.pszText = (LPWSTR)L"C: 드라이브";
    HTREEITEM hC = TreeView_InsertItem(hTreeView, &tvis);
    
    tvis.item.pszText = (LPWSTR)L"D: 드라이브";
    HTREEITEM hD = TreeView_InsertItem(hTreeView, &tvis);
    
    // C 드라이브 하위 항목
    tvis.hParent = hC;
    tvis.item.pszText = (LPWSTR)L"Program Files";
    TreeView_InsertItem(hTreeView, &tvis);
    
    tvis.item.pszText = (LPWSTR)L"Windows";
    TreeView_InsertItem(hTreeView, &tvis);
    
    tvis.item.pszText = (LPWSTR)L"Users";
    TreeView_InsertItem(hTreeView, &tvis);
    
    // 루트 확장
    TreeView_Expand(hTreeView, hRoot, TVE_EXPAND);
    
    return 0;
}

4.2 TreeView 이벤트 처리

case WM_NOTIFY: {
    NMHDR* pNMHDR = (NMHDR*)lParam;
    
    if (pNMHDR->idFrom == ID_TREEVIEW) {
        switch (pNMHDR->code) {
            case TVN_SELCHANGED: {
                // 선택 변경
                NMTREEVIEW* pNMTV = (NMTREEVIEW*)lParam;
                
                wchar_t buf[256];
                TVITEM item = {};
                item.mask = TVIF_TEXT;
                item.hItem = pNMTV->itemNew.hItem;
                item.pszText = buf;
                item.cchTextMax = 256;
                TreeView_GetItem(pNMHDR->hwndFrom, &item);
                
                wchar_t msg[512];
                swprintf_s(msg, L"선택: %s", buf);
                SetWindowText(hwnd, msg);
                break;
            }
            
            case NM_DBLCLK: {
                // 더블클릭
                HTREEITEM hItem = TreeView_GetSelection(pNMHDR->hwndFrom);
                
                wchar_t buf[256];
                TVITEM item = {};
                item.mask = TVIF_TEXT;
                item.hItem = hItem;
                item.pszText = buf;
                item.cchTextMax = 256;
                TreeView_GetItem(pNMHDR->hwndFrom, &item);
                
                MessageBox(hwnd, buf, L"더블클릭", MB_OK);
                break;
            }
            
            case TVN_ITEMEXPANDING: {
                // 확장/축소 전
                NMTREEVIEW* pNMTV = (NMTREEVIEW*)lParam;
                
                if (pNMTV->action == TVE_EXPAND) {
                    // 확장 중
                } else if (pNMTV->action == TVE_COLLAPSE) {
                    // 축소 중
                }
                break;
            }
        }
    }
    break;
}

5. TabControl 컨트롤

5.1 기본 TabControl 생성

#define ID_TABCONTROL  2003

HWND g_hTab1 = NULL;
HWND g_hTab2 = NULL;
HWND g_hTab3 = NULL;

case WM_CREATE: {
    CREATESTRUCT* pCS = (CREATESTRUCT*)lParam;
    
    // TabControl 생성
    HWND hTab = CreateWindow(
        WC_TABCONTROL,
        L"",
        WS_CHILD | WS_VISIBLE | WS_CLIPSIBLINGS,
        10, 10, 500, 400,
        hwnd, (HMENU)ID_TABCONTROL, pCS->hInstance, NULL
    );
    
    // 탭 추가
    TCITEM tie = {};
    tie.mask = TCIF_TEXT;
    
    tie.pszText = (LPWSTR)L"일반";
    TabCtrl_InsertItem(hTab, 0, &tie);
    
    tie.pszText = (LPWSTR)L"고급";
    TabCtrl_InsertItem(hTab, 1, &tie);
    
    tie.pszText = (LPWSTR)L"정보";
    TabCtrl_InsertItem(hTab, 2, &tie);
    
    // 각 탭의 클라이언트 영역 계산
    RECT rcTab;
    GetClientRect(hTab, &rcTab);
    TabCtrl_AdjustRect(hTab, FALSE, &rcTab);
    
    // 탭 1 내용 (에디트)
    g_hTab1 = CreateWindow(
        L"EDIT", L"일반 탭 내용",
        WS_CHILD | WS_VISIBLE | ES_MULTILINE | ES_AUTOVSCROLL | WS_VSCROLL,
        rcTab.left, rcTab.top,
        rcTab.right - rcTab.left, rcTab.bottom - rcTab.top,
        hTab, NULL, pCS->hInstance, NULL
    );
    
    // 탭 2 내용 (체크박스들)
    g_hTab2 = CreateWindow(
        L"BUTTON", L"고급 옵션 1",
        WS_CHILD | BS_AUTOCHECKBOX,
        rcTab.left + 10, rcTab.top + 10, 200, 20,
        hTab, NULL, pCS->hInstance, NULL
    );
    
    // 탭 3 내용 (정적 텍스트)
    g_hTab3 = CreateWindow(
        L"STATIC", L"버전: 1.0\n작성자: pkglog.com",
        WS_CHILD,
        rcTab.left + 10, rcTab.top + 10, 300, 100,
        hTab, NULL, pCS->hInstance, NULL
    );
    
    // 첫 번째 탭만 표시
    ShowWindow(g_hTab1, SW_SHOW);
    ShowWindow(g_hTab2, SW_HIDE);
    ShowWindow(g_hTab3, SW_HIDE);
    
    return 0;
}

case WM_NOTIFY: {
    NMHDR* pNMHDR = (NMHDR*)lParam;
    
    if (pNMHDR->idFrom == ID_TABCONTROL && pNMHDR->code == TCN_SELCHANGE) {
        // 탭 변경
        int sel = TabCtrl_GetCurSel(pNMHDR->hwndFrom);
        
        // 모두 숨기기
        ShowWindow(g_hTab1, SW_HIDE);
        ShowWindow(g_hTab2, SW_HIDE);
        ShowWindow(g_hTab3, SW_HIDE);
        
        // 선택된 탭만 표시
        switch (sel) {
            case 0: ShowWindow(g_hTab1, SW_SHOW); break;
            case 1: ShowWindow(g_hTab2, SW_SHOW); break;
            case 2: ShowWindow(g_hTab3, SW_SHOW); break;
        }
    }
    break;
}

6. ProgressBar와 기타 컨트롤

6.1 ProgressBar

#define ID_PROGRESSBAR  2004

HWND g_hProgress = NULL;

case WM_CREATE: {
    CREATESTRUCT* pCS = (CREATESTRUCT*)lParam;
    
    // ProgressBar 생성
    g_hProgress = CreateWindow(
        PROGRESS_CLASS,
        L"",
        WS_CHILD | WS_VISIBLE,
        10, 10, 400, 25,
        hwnd, (HMENU)ID_PROGRESSBAR, pCS->hInstance, NULL
    );
    
    // 범위 설정 (0~100)
    SendMessage(g_hProgress, PBM_SETRANGE, 0, MAKELPARAM(0, 100));
    SendMessage(g_hProgress, PBM_SETSTEP, 1, 0);
    
    // 시작 버튼
    CreateWindow(
        L"BUTTON", L"시작",
        WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON,
        10, 50, 100, 30,
        hwnd, (HMENU)ID_BTN_START, pCS->hInstance, NULL
    );
    
    return 0;
}

case WM_COMMAND:
    if (LOWORD(wParam) == ID_BTN_START) {
        // 타이머로 진행률 시뮬레이션
        SetTimer(hwnd, 1, 100, NULL);
    }
    break;

case WM_TIMER:
    if (wParam == 1) {
        int pos = (int)SendMessage(g_hProgress, PBM_GETPOS, 0, 0);
        
        if (pos >= 100) {
            KillTimer(hwnd, 1);
            SendMessage(g_hProgress, PBM_SETPOS, 0, 0);
            MessageBox(hwnd, L"완료!", L"알림", MB_OK);
        } else {
            SendMessage(g_hProgress, PBM_STEPIT, 0, 0);
        }
    }
    break;

6.2 Slider (TrackBar)

#define ID_SLIDER  2005
#define ID_STATIC_VALUE  2006

case WM_CREATE: {
    CREATESTRUCT* pCS = (CREATESTRUCT*)lParam;
    
    // Slider 생성
    HWND hSlider = CreateWindow(
        TRACKBAR_CLASS,
        L"",
        WS_CHILD | WS_VISIBLE | TBS_AUTOTICKS,
        10, 10, 400, 40,
        hwnd, (HMENU)ID_SLIDER, pCS->hInstance, NULL
    );
    
    // 범위 설정 (0~100)
    SendMessage(hSlider, TBM_SETRANGE, TRUE, MAKELONG(0, 100));
    SendMessage(hSlider, TBM_SETPOS, TRUE, 50);
    SendMessage(hSlider, TBM_SETTICFREQ, 10, 0);
    
    // 값 표시 레이블
    CreateWindow(
        L"STATIC", L"값: 50",
        WS_CHILD | WS_VISIBLE,
        420, 15, 100, 20,
        hwnd, (HMENU)ID_STATIC_VALUE, pCS->hInstance, NULL
    );
    
    return 0;
}

case WM_HSCROLL: {
    if ((HWND)lParam == GetDlgItem(hwnd, ID_SLIDER)) {
        int pos = (int)SendMessage((HWND)lParam, TBM_GETPOS, 0, 0);
        
        wchar_t buf[64];
        swprintf_s(buf, L"값: %d", pos);
        SetDlgItemText(hwnd, ID_STATIC_VALUE, buf);
    }
    break;
}

6.3 Spin Control (Up-Down)

#define ID_EDIT_SPIN  2007
#define ID_SPIN       2008

case WM_CREATE: {
    CREATESTRUCT* pCS = (CREATESTRUCT*)lParam;
    
    // 에디트
    HWND hEdit = CreateWindowEx(
        WS_EX_CLIENTEDGE,
        L"EDIT", L"50",
        WS_CHILD | WS_VISIBLE | ES_RIGHT,
        10, 10, 80, 25,
        hwnd, (HMENU)ID_EDIT_SPIN, pCS->hInstance, NULL
    );
    
    // Spin Control
    HWND hSpin = CreateWindow(
        UPDOWN_CLASS,
        L"",
        WS_CHILD | WS_VISIBLE | UDS_SETBUDDYINT | UDS_ALIGNRIGHT | UDS_ARROWKEYS,
        0, 0, 0, 0,  // 위치는 자동
        hwnd, (HMENU)ID_SPIN, pCS->hInstance, NULL
    );
    
    // Buddy 설정
    SendMessage(hSpin, UDM_SETBUDDY, (WPARAM)hEdit, 0);
    SendMessage(hSpin, UDM_SETRANGE, 0, MAKELONG(100, 0));
    SendMessage(hSpin, UDM_SETPOS, 0, 50);
    
    return 0;
}

7. 실전 예제: 설정 다이얼로그

// resource.h
#define IDD_SETTINGS        101
#define IDC_TAB             1001
#define IDC_CHECK_AUTO      1002
#define IDC_CHECK_NOTIFY    1003
#define IDC_SLIDER_VOLUME   1004
#define IDC_STATIC_VOLUME   1005
#define IDC_COMBO_LANG      1006

// settings.rc
IDD_SETTINGS DIALOGEX 0, 0, 400, 300
STYLE DS_SETFONT | DS_MODALFRAME | WS_POPUP | WS_CAPTION | WS_SYSMENU
CAPTION "설정"
FONT 9, "맑은 고딕"
BEGIN
    CONTROL         "",IDC_TAB,"SysTabControl32",0x0,7,7,386,265
    DEFPUSHBUTTON   "확인",IDOK,240,280,70,14
    PUSHBUTTON      "취소",IDCANCEL,320,280,70,14
END

// settings.cpp
struct SettingsData {
    BOOL autoSave;
    BOOL showNotify;
    int volume;
    int language;  // 0=한국어, 1=English
};

SettingsData g_settings = {TRUE, TRUE, 50, 0};

INT_PTR CALLBACK SettingsDlgProc(HWND hDlg, UINT msg, WPARAM wParam, LPARAM lParam)
{
    static HWND hTab = NULL;
    static HWND hTabPage1 = NULL;
    static HWND hTabPage2 = NULL;
    
    switch (msg) {
        case WM_INITDIALOG: {
            hTab = GetDlgItem(hDlg, IDC_TAB);
            
            // 탭 추가
            TCITEM tie = {};
            tie.mask = TCIF_TEXT;
            tie.pszText = (LPWSTR)L"일반";
            TabCtrl_InsertItem(hTab, 0, &tie);
            
            tie.pszText = (LPWSTR)L"고급";
            TabCtrl_InsertItem(hTab, 1, &tie);
            
            // 탭 1 페이지 (일반)
            RECT rc;
            GetClientRect(hTab, &rc);
            TabCtrl_AdjustRect(hTab, FALSE, &rc);
            
            // 체크박스들
            CreateWindow(L"BUTTON", L"자동 저장",
                WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX,
                rc.left + 20, rc.top + 20, 200, 20,
                hTab, (HMENU)IDC_CHECK_AUTO, GetModuleHandle(NULL), NULL);
            
            CreateWindow(L"BUTTON", L"알림 표시",
                WS_CHILD | WS_VISIBLE | BS_AUTOCHECKBOX,
                rc.left + 20, rc.top + 50, 200, 20,
                hTab, (HMENU)IDC_CHECK_NOTIFY, GetModuleHandle(NULL), NULL);
            
            // 초기값 설정
            CheckDlgButton(hTab, IDC_CHECK_AUTO, g_settings.autoSave ? BST_CHECKED : BST_UNCHECKED);
            CheckDlgButton(hTab, IDC_CHECK_NOTIFY, g_settings.showNotify ? BST_CHECKED : BST_UNCHECKED);
            
            // 볼륨 슬라이더
            CreateWindow(L"STATIC", L"볼륨:",
                WS_CHILD | WS_VISIBLE,
                rc.left + 20, rc.top + 80, 50, 20,
                hTab, NULL, GetModuleHandle(NULL), NULL);
            
            HWND hSlider = CreateWindow(TRACKBAR_CLASS, L"",
                WS_CHILD | WS_VISIBLE | TBS_AUTOTICKS,
                rc.left + 80, rc.top + 80, 200, 40,
                hTab, (HMENU)IDC_SLIDER_VOLUME, GetModuleHandle(NULL), NULL);
            
            SendMessage(hSlider, TBM_SETRANGE, TRUE, MAKELONG(0, 100));
            SendMessage(hSlider, TBM_SETPOS, TRUE, g_settings.volume);
            
            wchar_t volBuf[32];
            swprintf_s(volBuf, L"%d%%", g_settings.volume);
            CreateWindow(L"STATIC", volBuf,
                WS_CHILD | WS_VISIBLE,
                rc.left + 290, rc.top + 85, 50, 20,
                hTab, (HMENU)IDC_STATIC_VOLUME, GetModuleHandle(NULL), NULL);
            
            // 언어 콤보박스
            CreateWindow(L"STATIC", L"언어:",
                WS_CHILD | WS_VISIBLE,
                rc.left + 20, rc.top + 130, 50, 20,
                hTab, NULL, GetModuleHandle(NULL), NULL);
            
            HWND hCombo = CreateWindow(L"COMBOBOX", L"",
                WS_CHILD | WS_VISIBLE | CBS_DROPDOWNLIST | WS_VSCROLL,
                rc.left + 80, rc.top + 128, 150, 200,
                hTab, (HMENU)IDC_COMBO_LANG, GetModuleHandle(NULL), NULL);
            
            SendMessage(hCombo, CB_ADDSTRING, 0, (LPARAM)L"한국어");
            SendMessage(hCombo, CB_ADDSTRING, 0, (LPARAM)L"English");
            SendMessage(hCombo, CB_SETCURSEL, g_settings.language, 0);
            
            return TRUE;
        }
        
        case WM_HSCROLL: {
            if ((HWND)lParam == GetDlgItem(hTab, IDC_SLIDER_VOLUME)) {
                int pos = (int)SendMessage((HWND)lParam, TBM_GETPOS, 0, 0);
                
                wchar_t buf[32];
                swprintf_s(buf, L"%d%%", pos);
                SetDlgItemText(hTab, IDC_STATIC_VOLUME, buf);
            }
            break;
        }
        
        case WM_NOTIFY: {
            NMHDR* pNMHDR = (NMHDR*)lParam;
            
            if (pNMHDR->idFrom == IDC_TAB && pNMHDR->code == TCN_SELCHANGE) {
                // 탭 변경 처리
            }
            break;
        }
        
        case WM_COMMAND: {
            switch (LOWORD(wParam)) {
                case IDOK: {
                    // 설정 저장
                    g_settings.autoSave = IsDlgButtonChecked(hTab, IDC_CHECK_AUTO);
                    g_settings.showNotify = IsDlgButtonChecked(hTab, IDC_CHECK_NOTIFY);
                    g_settings.volume = (int)SendMessage(
                        GetDlgItem(hTab, IDC_SLIDER_VOLUME), TBM_GETPOS, 0, 0);
                    g_settings.language = (int)SendMessage(
                        GetDlgItem(hTab, IDC_COMBO_LANG), CB_GETCURSEL, 0, 0);
                    
                    EndDialog(hDlg, IDOK);
                    return TRUE;
                }
                
                case IDCANCEL:
                    EndDialog(hDlg, IDCANCEL);
                    return TRUE;
            }
            break;
        }
    }
    
    return FALSE;
}

같이 보면 좋은 글 (내부 링크)

이 주제와 연결되는 다른 글입니다.

  • Windows API 기초 | 메시지 루프와 윈도우 프로시저 완벽 가이드
  • Windows API 컨트롤 | 버튼·에디트·리스트박스 완벽 가이드
  • Windows API GDI | 그래픽 그리기 완벽 가이드

이 글이 도움이 되셨나요? Windows 다이얼로그와 공용 컨트롤을 마스터하는 데 도움이 되었기를 바랍니다!

다음 글에서는 Windows API 파일 I/O와 레지스트리를 다루겠습니다. 💾