GDI의 SelectObject 함수

요즘은 GDI를 사용할 일이 없겠지만 간혹 사용할 일이 생긴다. 최근 uxtheme를 파고들다보니 다시 접하게 되었는데 GDI 정보가 생각보다 일관성이 없다.

일단 내가 오래전에 봤던 내용 중 기억에 남아있고 하던 코딩 방식은 아래와같았다.

static void TestDC()
{
    HDC hdc = GetDC(NULL);
    HDC memDC = CreateCompatibleDC(hdc);
    HBITMAP memBitmap = CreateCompatibleBitmap(hdc, 100, 100);
    HBITMAP oldBitmap = (HBITMAP)SelectObject(memDC, memBitmap);

    // ...

    SelectObject(memDC, oldBitmap);

    DeleteObject(memBitmap);
    DeleteDC(memDC);
    ReleaseDC(NULL, hdc);
}

DC 객체를 생성하고 처음 SelectObject하면 반환되는 기본 객체는 DC를 지우기 전 다시 SelectObject로 기본 객체를 선택하여 DC를 지워야한다는 것이였다.

GDI 오브젝트 개수를 확인하는 코드를 구글링하니 GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) 이러한 코드가 나왔다(참조 링크). 이걸 참조하여 아래와같이 수정하여 실행해보았다.

static void TestDC()
{
    std::cout << "#0 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";

    HDC hdc = GetDC(NULL);
    std::cout << "#1 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";
    HDC memDC = CreateCompatibleDC(hdc);
    std::cout << "#2 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";
    HBITMAP memBitmap = CreateCompatibleBitmap(hdc, 100, 100);
    std::cout << "#3 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";
    HBITMAP oldBitmap = (HBITMAP)SelectObject(memDC, memBitmap);

    std::cout << "#4 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";
    // ...

    SelectObject(memDC, oldBitmap);
    std::cout << "#5 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";
    DeleteObject(memBitmap);
    std::cout << "#6 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";
    DeleteDC(memDC);
    std::cout << "#7 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";
    ReleaseDC(NULL, hdc);

    std::cout << "#8 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";
}

생각과는 조금은 달랐지만 아래와같이 출력이 되었다.

#0 Objects: 0
#1 Objects: 1
#2 Objects: 2
#3 Objects: 3
#4 Objects: 3
#5 Objects: 3
#6 Objects: 2
#7 Objects: 1
#8 Objects: 0

HDC객체는 기본 팬, 브러시, 비트맵이 내장되지만 HDC 객체가 생성될 때 카운트가 1만 올라가는걸 보면 기본 팬과 브러시, 비트맵은 별도의 GDI 객체로 카운트 되지 않는것으로 보인다. 덕분에 조금은 모호해지는점도 있다. 예를들어 SelectObject(memDC, oldBitmap); 라인만 주석을하면 아래와같이 변한다.

#0 Objects: 0
#1 Objects: 1
#2 Objects: 2
#3 Objects: 3
#4 Objects: 3
#5 Objects: 3
#6 Objects: 3
#7 Objects: 1
#8 Objects: 0

분명 DeleteObject(memBitmap);이후 #6 Objects: 2가 출력되어야할텐데 #6 Objects: 3이 출력된다. COM Object나 sharedptr처럼 객체 카운팅을 하여 SelectObject(memDC, memBitmap);에서 memBitmap의 카운트는 2, DeleteObject(memBitmap);에서는 카운트가 1, DeleteDC(memDC);에서 memDCmemBitmap을 보유하고 있으므로 memDC가 지워질 때 memBitmap 객체를 DeleteObject하여 카운트가 0이되어 함께 지워진거같다. (하지만 이건 그런거 같다는 생각일 뿐. 윈도우가 오픈소스였으면, 최소한 윈도우 9x라도 오픈소스로 풀렸으면 분석이라도 해봤을텐데 아쉽다.)

여기에서 기본 팬, 브러시, 비트맵이 GDI 객체 개수에 카운팅 안되어 모호해지는점이 SelectObject(memDC, memBitmap);이후 oldBitmap은 카운트가 2가되어 SelectObject(memDC, memBitmap);없이 DeleteDC(memDC);하면 DeleteObject(oldBitmap);을 해야하는지, 아니면 DeleteDC(memDC);하면 oldBitmap의 카운트와 상관없이 지우는지 확실히 모르겠다.

이런 이유로 처음 비트맵을 선택할 때 반환되는 oldBitmapmemDC를 지우기 전 SelectObject(memDC, oldBitmap);를 실행하여 정석대로 지워야할거같다.

이런 결론과 별개로 구글링 하다보니 아래와같은 코드가 보였는데…

static void TestDC()
{
    HDC hdc = GetDC(NULL);
    HDC memDC = CreateCompatibleDC(hdc);
    HBITMAP bitmap = CreateCompatibleBitmap(hdc, 100, 100);

    // ...

    DeleteObject(SelectObject(memDC, bitmap));

    DeleteObject(bitmap);
    DeleteDC(memDC);
    ReleaseDC(NULL, hdc);
}

이 코드가 이 글을 쓰게 된 원인이다. ReleaseDC(NULL, hdc);이후 GDI 객체 개수를 출력하면 0이 아닌 1이된다.

위에서 생각했던것과 달리 DeleteObject(SelectObject(memDC, bitmap));을 실행하면 SelectObject(memDC, bitmap)에서 기본 비트맵이 리턴되어 DeleteObject함수에 의해 삭제되었고 DeleteObject(bitmap); 그리고 DeleteDC(memDC); 를 실행하면 bitmapmemDC이 삭제되었어야하는데 GDI 객체 개수는 1이된다.

MSDN에서 SelectObject의 설명을 보면 “애플리케이션은 새 개체로 그리기를 완료한 후 항상 새 개체를 원래의 기본 개체로 바꿔야 합니다.”라고 설명 단락에 명시되어있긴하다. 단지 MSDN에서 중요한 코드를 생략하는 경우가 의외로 많은데 예를들어 펜 또는 브러시 색 설정 문서가 그중 하나이다. 뭐가 그렇게 힘든지 모르겠지만 EndPaint 함수호출이 없다. BeginPaint함수의 설명을 보면 “BeginPaint에 대한 각 호출에는 EndPaint 함수에 대한 해당 호출이 있어야 합니다.”이라는 기계번역이 명시되어있지만 코드 셈플은 EndPaint를 호출하지 않으니 자칫하면 혼란을 일으키기도한다.

보다보면 이런식으로 코드의 일부만 적어서 코드의 이전과 이후는 어떻게 해야하는지 혼란스러운 경우가 종종 있다. (예를들면 객체를 어떻게 생성하는가. IDXGIDevice 인터페이스 설명을 보면 인터페이스를 쿼리하는 방법으로 g_pd3dDevice->QueryInterface(__uuidof(IDXGIDevice), (void **)&pDXGIDevice); 이 코드가 전부다. g_pd3dDevice가 어느 d3d 디바이스 객체인지 d3d 9 device도 가능한지 d3d 11, 12 등등 어느 인터페이스에서 가능한지, 추가정보에 관련된 링크도 없다.)

이러한 이유와 구글링으로 답이 안나올때는 Qt 소스코드를 참조하는 경우가 많은데 Qt는 테스트 코드에서 아래와같은 코드가 튀어나왔다. (파일명은 qtbase/tests/auto/gui/image/qpixmap/tst_qpixmap.cpp)

void tst_QPixmap::fromWinHBITMAP()
{
    QFETCH(int, red);
    QFETCH(int, green);
    QFETCH(int, blue);

    HDC display_dc = GetDC(0);
    HDC bitmap_dc = CreateCompatibleDC(display_dc);
    HBITMAP bitmap = CreateCompatibleBitmap(display_dc, 100, 100);
    SelectObject(bitmap_dc, bitmap);

    SelectObject(bitmap_dc, GetStockObject(NULL_PEN));
    HGDIOBJ old_brush = SelectObject(bitmap_dc, CreateSolidBrush(RGB(red, green, blue)));
    Rectangle(bitmap_dc, 0, 0, 100, 100);

    QPixmap pixmap = qt_pixmapFromWinHBITMAP(bitmap);
    QCOMPARE(pixmap.width(), 100);
    QCOMPARE(pixmap.height(), 100);

    QImage image = pixmap.toImage();
    QRgb pixel = image.pixel(0, 0);
    QCOMPARE(qRed(pixel), red);
    QCOMPARE(qGreen(pixel), green);
    QCOMPARE(qBlue(pixel), blue);

    DeleteObject(SelectObject(bitmap_dc, old_brush));
    DeleteObject(SelectObject(bitmap_dc, bitmap));
    DeleteDC(bitmap_dc);
    ReleaseDC(0, display_dc);
}

CreateCompatibleBitmap 함수에서 생성한 bitmap 객체를 SelectObject하여 리턴되는 기본 비트맵은 사용하지않고 DeleteObject(SelectObject(bitmap_dc, bitmap));를 실행해버린다.

확인을 위해 아래와같이 코드를 작성하고 실행을 해보았다.

static void TestDC3()
{
    std::cout << "#0 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";

    HDC hdc = GetDC(NULL);
    HDC memDC = CreateCompatibleDC(hdc);
    HBITMAP memBitmap = CreateCompatibleBitmap(hdc, 100, 100);

    std::cout << "#1 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";

    HBITMAP nullBitmap1 = reinterpret_cast<HBITMAP>(SelectObject(memDC, memBitmap));
    HBITMAP nullBitmap2 = reinterpret_cast<HBITMAP>(SelectObject(memDC, memBitmap));

    std::cout << "- memBitmap: " << (void*)memBitmap << "\n";
    std::cout << "- nullBitmap1: " << (void*)nullBitmap1 << "\n";
    std::cout << "- nullBitmap2: " << (void*)nullBitmap2 << "\n";

    DeleteObject(nullBitmap2);

    std::cout << "#2 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";
    DeleteDC(memDC);
    std::cout << "#3 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";
    ReleaseDC(NULL, hdc);

    std::cout << "#4 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";
}

이것또한 #2 Objects: 2가 출력되지않고 아래와같이 #2 Objects: 3이 출력되었다. 그래도 결과적으로 #3 Objects: 1이 출력되어 객체는 잘 지워진거처럼 보인다.

#0 Objects: 0
#1 Objects: 3
- memBitmap: 000000003305128F
- nullBitmap1: 000000000085000F
- nullBitmap2: 000000003305128F
#2 Objects: 3
#3 Objects: 1
#4 Objects: 0

단지 CreateCompatibleBitmap 함수로 비트맵을 두개 생성하여 각각 SelectObject한 후 비트맵 객체를 다시 SelectObject하면 기본 비트맵 포인터가 반환되지 않았다. 특수한 상황이 아니면 그냥 정석대로 코딩하는게 좋을 듯 하다.

마지막으로 tst_qpixmap 코드에서 눈에띄는 SelectObject(memDC, GetStockObject(NULL_PEN)); 코드를 아래와 같이 코드를 추가하여 확인해보았다.

static void TestDC4()
{
    std::cout << "#0 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";

    HDC hdc = GetDC(NULL);
    HDC memDC = CreateCompatibleDC(hdc);
    HBITMAP memBitmap = CreateCompatibleBitmap(hdc, 100, 100);

    HPEN pen = CreatePen(PS_SOLID, 1, RGB(255, 255, 255));
    HGDIOBJ blackPen = GetStockObject(BLACK_PEN);
    HGDIOBJ nullPen1 = SelectObject(memDC, GetStockObject(NULL_PEN));
    HGDIOBJ nullPen2 = SelectObject(memDC, pen);
    HGDIOBJ nullPen3 = SelectObject(memDC, nullPen2);

    std::cout << "- pen: " << (void*)pen << "\n";
    std::cout << "- blackPen: " << (void*)blackPen << "\n";
    std::cout << "- nullPen1: " << (void*)nullPen1 << "\n";
    std::cout << "- nullPen2: " << (void*)nullPen2 << "\n";
    std::cout << "- nullPen3: " << (void*)nullPen3 << "\n";

    std::cout << "#1 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";

    HBITMAP nullBitmap1 = reinterpret_cast<HBITMAP>(SelectObject(memDC, memBitmap));
    HBITMAP nullBitmap2 = reinterpret_cast<HBITMAP>(SelectObject(memDC, memBitmap));

    std::cout << "- memBitmap: " << (void*)memBitmap << "\n";
    std::cout << "- nullBitmap1: " << (void*)nullBitmap1 << "\n";
    std::cout << "- nullBitmap2: " << (void*)nullBitmap2 << "\n";

    DeleteObject(nullPen3);
    DeleteObject(nullBitmap2);

    std::cout << "#2 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";
    DeleteDC(memDC);
    std::cout << "#3 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";
    ReleaseDC(NULL, hdc);

    std::cout << "#4 Objects: " << GetGuiResources(GetCurrentProcess(), GR_GDIOBJECTS) << "\n";
}

결과는 아래와같다.

#0 Objects: 0
- pen: 0000000059303382
- blackPen: 0000000000B00017
- nullPen1: 0000000000B00017
- nullPen2: 0000000000B00016
- nullPen3: 0000000059303382
#1 Objects: 4
- memBitmap: FFFFFFFFB6053985
- nullBitmap1: 000000000085000F
- nullBitmap2: FFFFFFFFB6053985
#2 Objects: 3
#3 Objects: 1
#4 Objects: 0

MSDN에서 GetStockObject의 내용을 보면 “It is not necessary (but it is not harmful) to delete stock objects by calling DeleteObject.”라고 되어있으며 구글 번역기를 돌리면 “DeleteObject를 호출하여 스톡 객체를 삭제할 필요는 없습니다(하지만 해롭지는 않습니다).”로 번역된다. 어디에서 본 바로는 스톡 객체를 삭제하면 사용자 코드가 아닌 다른 코드에서 사용자가 삭제한 스톡 객체를 사용하는 문제가 발생 할 수 있기때문에 스톡 객체는 삭제하지 마라고 봤는데… MSDN에서 본게 아니였나…

더 다양하게 확인이 되어야겠지만 DC 객체의 기본 팬은 BLACK_PEN과 같은 스톡 객체이며 스톡 객체로 SelectObject하면 기본 객체로 변경할 필요가 없어보인다. 단지 비트맵과 마찬가지로 DC 객체를 지울 때 사용자가 생성한 팬으로 처음 SelectObject할 때 반환된 객체로 SelectObject 후 객체를 지워야 하는것으로 보인다.

아… 복잡하다 복잡해…

답글 남기기

이메일 주소는 공개되지 않습니다. 필수 필드는 *로 표시됩니다

*
*

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.