커서 사용

이 섹션에서는 다음 항목에 대해 설명합니다.

커서 만들기

다음 예제에서는 두 개의 커서 핸들을 만듭니다. 하나는 표준 모래 시계 커서용이고 다른 하나는 애플리케이션의 리소스 정의 파일에 리소스로 포함된 사용자 지정 커서용입니다.

HCURSOR hCurs1 = LoadCursor(NULL, IDC_WAIT);
HCURSOR hCurs2 = LoadCursor(hInstance, MAKEINTRESOURCE(IDC_MYICON));

애플리케이션은 사용자 지정 커서를 리소스로 구현하고 런타임에 커서를 만드는 대신 LoadCursor, LoadCursorFromFile또는 LoadImage 사용해야 합니다. 커서 리소스를 사용하면 디바이스 의존성이 방지되고 지역화가 간소화되며 애플리케이션에서 커서 디자인을 공유할 수 있습니다.

다음 예제에서는 CreateCursor 함수를 사용하여 런타임에 사용자 지정 단색 커서를 만듭니다. 이 예제는 시스템에서 커서 마스크를 해석하는 방법을 설명하기 위해 여기에 포함되어 있습니다.

커서 맵의 각 픽셀은 단일 문자로 표시됩니다.

기호 AND 비트 XOR 비트 디스플레이
1 0 투명
X 1 1 화면 반전
o 0 1 하얀
+ 0 0 검정
#define CURSOR_SIZE 32

// Symbol encoding: ' '=transparent  'o'=white  '+'=black
static const char *const yin_cursor[CURSOR_SIZE] = {
    "              ++++              ",
    "          ++++oooo+             ",
    "        ++oooooo++              ",
    "       +ooooooo+                ",
    "     +oooooooo+                 ",
    "    +oooooooo+                  ",
    "    +oooooooo+                  ",
    "   +oooooooo+                   ",
    "  +ooooooooo+                   ",
    "  +oooooooo+                    ",
    " +ooooooooo+                    ",
    " +ooooooooo+                    ",
    " +oooooooooo+                   ",
    "+ooooooooooo+                   ",
    "+oooooooooooo+                  ",
    "+ooooooooooooo++                ",
    "+ooooooooooooooo+               ",
    "+oooooooooooooooo++             ",
    "+oooooooooooooooooo+            ",
    " +ooooooooooooooooo+            ",
    " +oooooooo+++ooooooo+           ",
    " +ooooooo+++++oooooo+           ",
    "  +oooooo+++++oooooo+           ",
    "  +oooooo+++++ooooo+            ",
    "   +oooooo+++oooooo+            ",
    "    +ooooooooooooo+             ",
    "    +ooooooooooooo+             ",
    "     ++oooooooooo+              ",
    "       +oooooooo+               ",
    "        ++oooo++                ",
    "          ++++                  ",
    "                                ",
};

// Pack an XPM-style cursor map into separate 1bpp AND and XOR bit planes.
// Output buffers must be sized: ((w + 15) / 16) * 2 * h bytes.
static void PackCursorMasks(const char *const rows[], int w, int h,
                             BYTE *pbAnd, BYTE *pbXor)
{
    int stride = ((w + 15) / 16) * 2;  // WORD-aligned row stride, per CreateCursor contract
    ZeroMemory(pbAnd, stride * h);
    ZeroMemory(pbXor, stride * h);
    for (int y = 0; y < h; y++) {
        for (int x = 0; x < w; x++) {
            BYTE bit = (BYTE)(0x80 >> (x % 8));
            int  idx = y * stride + x / 8;
            char sym = rows[y][x];
            if (sym == ' ' || sym == 'X') pbAnd[idx] |= bit;
            if (sym == 'o' || sym == 'X') pbXor[idx] |= bit;
        }
    }
}

// Row stride for a 32-pixel-wide 1bpp mask: ((32 + 15) / 16) * 2 = 4 bytes.
BYTE abAnd[CURSOR_SIZE * 4];
BYTE abXor[CURSOR_SIZE * 4];
PackCursorMasks(yin_cursor, CURSOR_SIZE, CURSOR_SIZE, abAnd, abXor);

// hInstance is the application's HINSTANCE from WinMain.
// Call DestroyCursor when the cursor is no longer needed.
HCURSOR hCurs3 = CreateCursor(
    hInstance,  // application instance
    19,         // hot spot x
    2,          // hot spot y
    CURSOR_SIZE,
    CURSOR_SIZE,
    abAnd,
    abXor);

자세한 내용은 비트맵참조하세요.

커서 함수를 사용하여 마우스스트랩 만들기

다음 예제에서는 SetCursorPos, GetCursorPos, CreateCursor, CreateIcon, SetCursor 및 DrawIconEx 함수를 사용하여 간단한 마우스 트랩을 만듭니다. 창 가운데에 양 모양 아이콘이 그려집니다. 커서가 3초 동안 움직이지 않으면 양 아이콘으로 이동해 음 모양으로 바뀝니다. 마우스를 이동하면 트랩이 다시 설정됩니다.

// PackCursorMasks, yin_cursor, and CURSOR_SIZE are defined in the preceding example.

#define ICON_SIZE     32
#define YIN_HOT_X     19
#define YIN_HOT_Y      2
#define IDT_CURSOR     1
#define TRAP_DELAY  3000    // ms of inactivity before the cursor snaps to the icon

// Symbol encoding: ' '=transparent  'o'=white  '+'=black
static const char *const yang_icon[ICON_SIZE] = {
    "                                ",
    "                  ++++          ",
    "                ++++++++        ",
    "               ++++++++++       ",
    "              +++++++++++++     ",
    "             +++++++++++++++    ",
    "             +++++++++++++++    ",
    "            ++++++ooo++++++++   ",
    "            +++++ooooo++++++++  ",
    "           ++++++ooooo++++++++  ",
    "           ++++++ooooo+++++++++ ",
    "           +++++++ooo++++++++++ ",
    "            +++++++++++++++++++ ",
    "            ++++++++++++++++++++",
    "             +++++++++++++++++++",
    "              ++++++++++++++++++",
    "                ++++++++++++++++",
    "                 +++++++++++++++",
    "                   +++++++++++++",
    "                   ++++++++++++ ",
    "                    +++++++++++ ",
    "                    +++++++++++ ",
    "                    ++++++++++  ",
    "                   +++++++++++  ",
    "                   ++++++++++   ",
    "                  ++++++++++    ",
    "                  ++++++++++    ",
    "                 ++++++++++     ",
    "                +++++++++       ",
    "              ++++++++++        ",
    "             +++++++++          ",
    "              ++++              ",
};

static HICON   hYang;
static HCURSOR hYin;
static POINT   ptLast;
static BOOL    bTrapped;

LRESULT CALLBACK MainWndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
    switch (uMsg)
    {
        case WM_CREATE:
        {
            HINSTANCE hInstance = ((LPCREATESTRUCT)lParam)->hInstance;

            BYTE abAndIcon[ICON_SIZE * 4];
            BYTE abXorIcon[ICON_SIZE * 4];
            PackCursorMasks(yang_icon, ICON_SIZE, ICON_SIZE, abAndIcon, abXorIcon);
            hYang = CreateIcon(hInstance, ICON_SIZE, ICON_SIZE, 1, 1,
                               abAndIcon, abXorIcon);

            BYTE abAndCursor[CURSOR_SIZE * 4];
            BYTE abXorCursor[CURSOR_SIZE * 4];
            PackCursorMasks(yin_cursor, CURSOR_SIZE, CURSOR_SIZE, abAndCursor, abXorCursor);
            hYin = CreateCursor(hInstance, YIN_HOT_X, YIN_HOT_Y, CURSOR_SIZE, CURSOR_SIZE,
                                abAndCursor, abXorCursor);

            GetCursorPos(&ptLast);
            SetTimer(hWnd, IDT_CURSOR, TRAP_DELAY, (TIMERPROC)NULL);
            return 0;
        }

        case WM_PAINT:
        {
            PAINTSTRUCT ps;
            HDC hDC = BeginPaint(hWnd, &ps);
            RECT rc;
            GetClientRect(hWnd, &rc);
            DrawIconEx(hDC,
                       (rc.right  - ICON_SIZE) / 2,
                       (rc.bottom - ICON_SIZE) / 2,
                       hYang, ICON_SIZE, ICON_SIZE, 0, NULL, DI_NORMAL);
            EndPaint(hWnd, &ps);
            return 0;
        }

        case WM_MOUSEMOVE:
        {
            POINT ptNow;
            GetCursorPos(&ptNow);
            if (ptNow.x != ptLast.x || ptNow.y != ptLast.y)
            {
                ptLast = ptNow;
                bTrapped = FALSE;
                SetTimer(hWnd, IDT_CURSOR, TRAP_DELAY, (TIMERPROC)NULL);
            }
            break;
        }

        case WM_TIMER:
        {
            POINT ptNow;
            GetCursorPos(&ptNow);
            if (ptNow.x == ptLast.x && ptNow.y == ptLast.y)
            {
                RECT rc;
                GetClientRect(hWnd, &rc);
                POINT ptTarget = {
                    (rc.right  - ICON_SIZE) / 2 + YIN_HOT_X,
                    (rc.bottom - ICON_SIZE) / 2 + YIN_HOT_Y
                };
                ClientToScreen(hWnd, &ptTarget);
                SetCursorPos(ptTarget.x, ptTarget.y);
                // Ignore the mouse-move notification generated by this repositioning.
                GetCursorPos(&ptLast);
                SetCursor(hYin);
                bTrapped = TRUE;
                KillTimer(hWnd, IDT_CURSOR);
            }
            else
            {
                // Movement outside this window does not generate WM_MOUSEMOVE here.
                ptLast = ptNow;
            }
            return 0;
        }

        case WM_SETCURSOR:
            if (bTrapped)
            {
                SetCursor(hYin);
                return TRUE;
            }
            break;

        case WM_DESTROY:
            KillTimer(hWnd, IDT_CURSOR);
            DestroyCursor(hYin);
            DestroyIcon(hYang);
            PostQuitMessage(0);
            return 0;
    }

    return DefWindowProc(hWnd, uMsg, wParam, lParam);
}

알파 혼합 커서 만들기

다음 단계에 따라 런타임에 알파 혼합 커서 또는 아이콘을 만듭니다.

  • top-down DIB(biHeight, , 음수)에 대해 biCompression=BI_RGB를 채우고 CreateDIBSection을 호출하여 색 비트맵을 얻습니다. 동일한 헤더 biBitCount=1 를 다시 사용하여 1bpp AND 마스크를 만듭니다.
  • 커서 이미지를 색 DIB 섹션에 그리고 각 픽셀의 알파 바이트를 설정합니다.
  • AND 마스크를 채웁니다: alpha > 0인 곳에서는 각 비트를 0(불투명)으로 설정하고, alpha == 0인 곳에서는 1(투명)으로 설정합니다.
  • 를 DIB 섹션으로, hbmMask를 AND 마스크로 하여 hbmColor 구조체를 채웁니다.
  • CreateIconIndirect를 호출하여 커서 또는 아이콘을 만듭니다.

다음 코드에서는 알파 혼합 커서를 만드는 방법을 보여 줍니다. DrawRGBCircles는 서로 겹치는 R/G/B 반투명 원 세 개를 픽셀 버퍼에 직접 기록합니다. 즉, AC_SRC_OVER 공식을 사용해 원들을 해석적으로 합성하고 rgbReserved에 알파와 함께 프리멀티플라이드 RGB 채널을 저장합니다. 시스템 커서 크기 설정과 일치하도록 GetSystemMetrics 에서 커서 크기를 읽습니다. 대신 아이콘을 만들려면 TRUE을(를) fIcon(으)로 변경하세요.

#define WIDTHBYTES(bits) ((DWORD)(((bits) + 31) & ~31) / 8)

// Fill pixels (top-down, cx-cy) with three overlapping R/G/B circles, each alpha=128.
// Composites with AC_SRC_OVER and preserves premultiplied RGB channels.
static void DrawRGBCircles(RGBQUAD *pixels, int cx, int cy)
{
    // r = cx/3: each center is r px from its nearest edge, so the circle touches exactly.
    const int r = cx / 3;
    struct { int x, y; RGBQUAD color; } circles[3] = {
        //                  B    G    R    A
        { cx / 2,  r,      {  0,   0, 255, 128 } },  // red,   top-center
        { r,       cy - r, {  0, 255,   0, 128 } },  // green, bottom-left
        { cx - r,  cy - r, {255,   0,   0, 128 } },  // blue,  bottom-right
    };

    for (int y = 0; y < cy; y++) {
        for (int x = 0; x < cx; x++) {
            DWORD R = 0, G = 0, B = 0, A = 0;
            for (int i = 0; i < 3; i++) {
                int dx = x - circles[i].x, dy = y - circles[i].y;
                if (dx*dx + dy*dy > r*r) continue;
                DWORD a = circles[i].color.rgbReserved, inv = 255 - a;
                // AC_SRC_OVER (premultiplied): dst = src + dst * (1 - src_a/255)
                R = circles[i].color.rgbRed   * a / 255 + R * inv / 255;
                G = circles[i].color.rgbGreen * a / 255 + G * inv / 255;
                B = circles[i].color.rgbBlue  * a / 255 + B * inv / 255;
                A = a                                    + A * inv / 255;
            }
            RGBQUAD *p = &pixels[y * cx + x];
            p->rgbReserved = (BYTE)A;
            p->rgbRed   = (BYTE)R;
            p->rgbGreen = (BYTE)G;
            p->rgbBlue  = (BYTE)B;
        }
    }
}

HCURSOR CreateAlphaCursor(void)
{
    int cx = GetSystemMetrics(SM_CXCURSOR);
    int cy = GetSystemMetrics(SM_CYCURSOR);

    // Extra RGBQUAD at the end for the 1bpp colour table (index 1 = white).
    BYTE bmiBuffer[sizeof(BITMAPINFO) + sizeof(RGBQUAD)] = {0};
    BITMAPINFO *pBmi = (BITMAPINFO *)bmiBuffer;
    pBmi->bmiHeader.biSize        = sizeof(pBmi->bmiHeader);
    pBmi->bmiHeader.biWidth       = cx;
    pBmi->bmiHeader.biHeight      = -cy;   // negative = top-down
    pBmi->bmiHeader.biPlanes      = 1;
    pBmi->bmiHeader.biCompression = BI_RGB;

    // Create a top-down 32bpp DIB section for the color (XOR) image.
    pBmi->bmiHeader.biBitCount = 32;
    RGBQUAD *lpBits;
    HBITMAP hBitmap = CreateDIBSection(NULL, pBmi, DIB_RGB_COLORS, (void **)&lpBits, NULL, 0);

    DrawRGBCircles(lpBits, cx, cy);

    // Reuse the same header for the 1bpp AND mask; switch biBitCount and set the colour table.
    pBmi->bmiHeader.biBitCount = 1;
    pBmi->bmiColors[0].rgbRed = pBmi->bmiColors[0].rgbGreen = pBmi->bmiColors[0].rgbBlue = 0;
    pBmi->bmiColors[1].rgbRed = pBmi->bmiColors[1].rgbGreen = pBmi->bmiColors[1].rgbBlue = 255;
    BYTE *andBits;
    HBITMAP hMaskBitmap = CreateDIBSection(NULL, pBmi, DIB_RGB_COLORS, (void **)&andBits, NULL, 0);

    // Set AND mask: bit=1 (transparent) wherever no circle was drawn.
    int maskStride = WIDTHBYTES(cx * 1);
    for (int y = 0; y < cy; y++) {
        for (int x = 0; x < cx; x++) {
            if (lpBits[y * cx + x].rgbReserved == 0)
                andBits[y * maskStride + x / 8] |= (BYTE)(0x80 >> (x % 8));
        }
    }

    ICONINFO ii = {0};
    ii.fIcon     = FALSE;
    ii.xHotspot  = cx / 2;
    ii.yHotspot  = cy / 2;
    ii.hbmMask   = hMaskBitmap;
    ii.hbmColor  = hBitmap;

    HCURSOR hCursor = (HCURSOR)CreateIconIndirect(&ii);

    DeleteObject(hBitmap);
    DeleteObject(hMaskBitmap);

    return hCursor;
}

닫기 전에 DestroyCursor 함수를 사용하여 CreateCursor 또는 CreateIconIndirect를 사용하여 만든 커서를 반드시 삭제해야 합니다. 다른 함수에서 만든 커서를 삭제할 필요는 없습니다.

커서 크기 가져오기

다음 예제에서는 핸들에서 커서 또는 아이콘의 차원을 검색합니다.

BOOL GetCursorDimensions(_In_ HCURSOR hcur, _Out_ SIZE *psiz)
{
    ICONINFO ii;
    BOOL fResult = GetIconInfo(hcur, &ii);
    if (fResult) {
        BITMAP bm;
        fResult = GetObject(ii.hbmMask, sizeof(bm), &bm) == sizeof(bm);
        if (fResult) {
            psiz->cx = bm.bmWidth;
            psiz->cy = ii.hbmColor ? bm.bmHeight : bm.bmHeight / 2;
        }
        DeleteObject(ii.hbmMask);
        if (ii.hbmColor) DeleteObject(ii.hbmColor);
    }
    return fResult;
}

커서 표시

시스템에서 자동으로 클래스 커서(커서가 가리키는 창과 연결된 커서)를 표시합니다. 창 클래스를 등록하는 동안 클래스 커서를 할당할 수 있습니다. 다음 예제에서는 wc 매개 변수로 식별된 WNDCLASS 구조체의 hCursor 멤버에 커서 핸들을 할당하여 이를 보여 줍니다.

WNDCLASS wc = {0};
wc.lpfnWndProc   = MainWndProc;
wc.hInstance     = hInstance;
wc.hIcon         = LoadIcon(NULL, IDI_APPLICATION);
wc.hCursor       = LoadCursor(hInstance, MAKEINTRESOURCE(IDC_MYICON));
wc.hbrBackground = GetStockObject(WHITE_BRUSH);
wc.lpszMenuName  = TEXT("GenericMenu");
wc.lpszClassName = TEXT("GenericWClass");

return RegisterClass(&wc);

창 클래스가 등록되면 애플리케이션의 리소스 정의 파일에서 식별되는 IDC_MYICON 커서가 클래스를 기반으로 하는 모든 창의 기본 커서입니다.

애플리케이션은 SetCursor 함수를 사용하고 다른 커서 핸들을 지정하여 커서 디자인을 변경할 수 있습니다. 그러나 커서가 이동하면 시스템은 새 위치에서 클래스 커서를 다시 그어 옵니다. 클래스 커서가 다시 그려지는 것을 방지하려면 WM_SETCURSOR 메시지를 처리해야 합니다. 커서가 이동하고 마우스 입력이 캡처되지 않을 때마다 시스템은 커서가 이동하는 창으로 이 메시지를 보냅니다.

WM_SETCURSOR처리하는 동안 다양한 조건에 대해 다른 커서를 지정할 수 있습니다. LOWORD(lParam)를 확인하여 클라이언트 영역을 크기 조정 핸들과 같은 비클라이언트 영역과 구분하고, 비클라이언트 히트는 DefWindowProc에 전달하여 시스템이 해당 위치에 적절한 시스템 커서를 설정할 수 있도록 하세요.

case WM_SETCURSOR:
    if (LOWORD(lParam) == HTCLIENT)
    {
        SetCursor(hCurs3);
        return TRUE;
    }
    return DefWindowProc(hWnd, uMsg, wParam, lParam);

TRUE을 반환하면 DefWindowProc가 커서를 클래스 커서로 다시 설정하지 못하게 합니다. 비클라이언트 히트를 DefWindowProc에 전달하면 시스템의 크기 조정 및 이동 커서가 유지됩니다.

SetClassLongPtr 함수를 사용하여 클래스 커서를 바꿀 수 있습니다. 이 함수는 지정된 클래스의 모든 창에 대한 기본 창 설정을 변경합니다. 다음 예제에서는 기존 클래스 커서를 hCurs2 커서로 바꿉니다.

SetClassLongPtr(hWnd, GCLP_HCURSOR, (LONG_PTR)hCurs2);

자세한 내용은 창 클래스 및 마우스 입력 참조하세요.

커서 제한

다음 예제에서는 커서를 애플리케이션의 창으로 제한한 다음 커서를 이전 창으로 복원합니다. 이 예제에서는 GetClipCursor 함수를 사용하여 커서가 이동할 수 있는 영역과 ClipCursor 함수를 기록하여 커서를 제한하고 복원합니다.

RECT rcOldClip;
GetClipCursor(&rcOldClip);

RECT rcClip;
GetWindowRect(hWnd, &rcClip);
ClipCursor(&rcClip);

// ... process input from the confined cursor ...

ClipCursor(&rcOldClip);

시스템에서 한 번에 하나의 커서만 사용할 수 있으므로 커서를 제한하는 애플리케이션은 컨트롤을 다른 창으로 포기하기 전에 커서를 복원해야 합니다.

키보드를 사용하여 커서 이동

시스템에 마우스가 필요하지 않으므로 애플리케이션은 키보드로 마우스 동작을 시뮬레이션할 수 있어야 합니다. 다음 예제에서는 GetCursorPos 및 SetCursorPos 함수를 사용하고 화살표 키의 입력을 처리하여 이를 달성하는 방법을 보여 줍니다.

static int nRepeat = 1;

switch (uMsg)
{
    case WM_KEYDOWN:
    {
        int dx = 0, dy = 0;
        switch (wParam)
        {
            case VK_LEFT:  dx = -nRepeat; break;
            case VK_RIGHT: dx = +nRepeat; break;
            case VK_UP:    dy = -nRepeat; break;
            case VK_DOWN:  dy = +nRepeat; break;
            default: return DefWindowProc(hWnd, uMsg, wParam, lParam);
        }

        POINT pt;
        GetCursorPos(&pt);
        ScreenToClient(hWnd, &pt);
        pt.x += dx;
        pt.y += dy;

        RECT rc;
        GetClientRect(hWnd, &rc);
        if (pt.x < rc.left)         pt.x = rc.left;
        else if (pt.x >= rc.right)  pt.x = rc.right - 1;
        if (pt.y < rc.top)          pt.y = rc.top;
        else if (pt.y >= rc.bottom) pt.y = rc.bottom - 1;

        ClientToScreen(hWnd, &pt);
        SetCursorPos(pt.x, pt.y);
        if (nRepeat < 32) nRepeat++;
        return 0;
    }
    case WM_KEYUP:
        nRepeat = 1;
        return 0;
}