如何从设备相关的 HBITMAP 构造 GDI+ 位图对象

How to construct a GDI+ Bitmap object from a Device-Dependent HBITMAP

我想在以下情况下对文件使用 GDI+ 方法 Image::Save() to save a DDB:

HBITMAP hBitmap = CreateCompatibleBitmap(hDC, 200, 200) ;

...

//hBitmap is a DDB so I need to pass an HPALETTE
Gdiplus::Bitmap(hBitmap,  ???HPALETTE???  ).Save(L"file.png", ...) ;

问题是当位图不是与设备无关的位图时,Bitmap 构造函数要求 HPALETTE

我从哪里获得必要的 HPALETTE?


跟进:
其中一个答案建议将 NULL 作为 HPALETTE 参数传递。
这是一个这样做的工作示例。结果是失去所有颜色的纯黑白图像。

#include <windows.h>
#include <gdiplus.h>

int main(){
    using namespace Gdiplus ;

    GdiplusStartupInput gdiplusStartupInput ;
    ULONG_PTR gdiplusToken ;
    GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL) ;

    CLSID pngEncoder = {0x557cf406, 0x1a04, 0x11d3, {0x9a, 0x73, 0x00, 0x00, 0xf8, 0x1e, 0xf3, 0x2e} } ;

    HDC dcHndl = CreateCompatibleDC(NULL) ;

    HBITMAP hBitmap = CreateCompatibleBitmap(dcHndl, 200, 200) ;

    SelectObject(dcHndl, hBitmap) ;

    BitBlt(dcHndl, 0,0, 200,200, GetDC(NULL), 0,0, SRCCOPY|CAPTUREBLT) ;

    Bitmap(hBitmap, NULL).Save(L"file.png", &pngEncoder) ;
}

可以传NULL。下面的示例代码。

int main()
{
    GdiplusStartupInput gdiplusStartupInput;
    ULONG_PTR gdiplusToken;
    GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);
    GUID encoder = {};

    GetGdiplusEncoderClsid(L"image/png", &encoder); // 

    HDC hdc = GetDC(NULL);

    HBITMAP hBitmap = CreateCompatibleBitmap(hdc, 200, 200);

    Bitmap bmp(hBitmap, NULL);

    bmp.Save(L"File.png", &encoder);

    return 0;
}

作为CreateCompatibleBitmap remarks sate if you are dealing with color bitmaps we can also assume that hDC is a nonmemory device context (because memory device context will only create monochrome bitmaps) and the color palette used by this bitmap is the same color palette used by this device context. You can query it using GetCurrentObject method. However remarks to Bitmap.Bitmap(HBITMAP, HPALETTE) constructor状态:

Do not pass to the GDI+ Bitmap::Bitmap constructor a GDI bitmap or a GDI palette that is currently (or was previously) selected into a device context.

因此您不能直接使用当前设备上下文调色板,而是需要创建它的副本。

/// <returns>
/// Handle to palette currently selected into device context without granting ownership.
/// </returns>
_Check_return_ ::HPALETTE
Fetch_CurrentPalette(_In_ ::HDC const h_dc)
{
    assert(h_dc);
    ::HGDIOBJ const h_palette_object{::GetCurrentObject(h_dc, OBJ_PAL)}; // not owned
    assert(h_palette_object);
    assert(OBJ_PAL == ::GetObjectType(h_palette_object));
    //  Perform unchecked conversion of generic GDI object descriptor to GDI palette descriptor.
    ::HPALETTE h_current_palette{}; // not owned
    {
        static_assert(sizeof(h_palette_object) == sizeof(h_current_palette), "wat");
        ::memcpy
        (
            ::std::addressof(h_current_palette)
        ,   ::std::addressof(h_palette_object)
        ,   sizeof(h_current_palette)
        );
    }
    return(h_current_palette);
}

/// <returns>
/// Handle to palette copy with granting ownership.
/// </returns>
_Check_return_ ::HPALETTE
Make_PaletteCopy(_In_ ::HPALETTE const h_palette)
{
    assert(h_palette);
    ::UINT const first_entry_index{};
    ::UINT entries_count{};
    ::LPPALETTEENTRY p_entries{};
    //  Figure out how many entries palette contains.
    entries_count = ::GetPaletteEntries(h_palette, first_entry_index, entries_count, p_entries);
    assert(1 < entries_count);
    assert(entries_count <= ::std::numeric_limits< decltype(LOGPALETTE::palNumEntries) >::max());
    //  This buffer will hold palette description which contains first PALETTEENTRY as last field.
    //  followed by the rest of PALETTEENTRY items.
    ::std::unique_ptr< ::std::uint8_t[] > const p_buffer
    {
        new ::std::uint8_t[sizeof(::LOGPALETTE) + (sizeof(::PALETTEENTRY) * (entries_count - 1u))]
    };
    //  Perform unchecked conversion of buffer pointer to palette description pointer.
    ::LOGPALETTE * p_description{};
    {
        ::std::uint8_t * const p_buffer_bytes{p_buffer.get()};
        static_assert(sizeof(p_buffer_bytes) == sizeof(p_description), "wat");
        ::memcpy
        (
            ::std::addressof(p_description)
        ,   ::std::addressof(p_buffer_bytes)
        ,   sizeof(p_description)
        );
    }
    //  Copy palette entries into buffer.
    p_entries = static_cast< ::LPPALETTEENTRY >(p_description->palPalEntry);
    ::UINT const copied_entries_count
    {
        ::GetPaletteEntries(h_palette, first_entry_index, entries_count, p_entries)
    };
    assert(copied_entries_count == entries_count);
    //  Create palette copy.
    p_description->palVersion = 0x300; // magic
    p_description->palNumEntries = static_cast< ::WORD >(copied_entries_count);
    ::HPALETTE const h_copied_palette{::CreatePalette(p_description)}; // owned
    assert(h_copied_palette);
    return(h_copied_palette);
}



::HPALETTE const hPal{Make_PaletteCopy(Fetch_CurrentPalette(hDC))}; // owned
assert(hPal);
::HBITMAP const hBitmap{::CreateCompatibleBitmap(hDC, 200, 200)}; // owned
assert(hBitmap);
{
    ::Gdiplus::Bitmap bmp{hBitmap, hPal};
    assert(::Gdiplus::Status::Ok == bmp.GetLastStatus());
    //  Do something...
}
//  Delete palette and bitmap after GDI+ bitmap object went out of scope.
if(FALSE == ::DeleteObject(hPal))
{
    assert(false);
}
if(FALSE == ::DeleteObject(hBitmap))
{
    assert(false);
}

首先(这与您的主要问题无关):

为屏幕截图创建位图时,不要使用内存 dc,因为它会创建单色位图。这是您获得黑白图像的主要原因(在我的计算机上我只获得黑色图像)。

不要在另一个函数中使用 GetDC(0)。每次调用 GetDC 匹配都有匹配的 ReleaseDC 以避免资源泄漏。

在调用 BitBlt 之后,从 dc 中选择 select hbitmap 是个好习惯,因为你基本上已经完成了 dc 的绘制。

以下代码适用于 Windows 10

int w = 800;
int h = 600;

HDC hdc = GetDC(HWND_DESKTOP);
HDC memdc = CreateCompatibleDC(hdc);
HBITMAP hbitmap = CreateCompatibleBitmap(hdc, w, h);
HBITMAP oldbmp = (HBITMAP)SelectObject(memdc, hbitmap);
BitBlt(memdc, 0, 0, w, h, hdc, 0, 0, SRCCOPY | CAPTUREBLT);
SelectObject(memdc, oldbmp);

Bitmap(hbitmap, NULL).Save(filename, &pngEncoder);

DeleteObject(hbitmap);
DeleteDC(memdc);
ReleaseDC(HWND_DESKTOP, hdc);

回到你关于文档的问题:

Type: HPALETTE
Handle to a GDI palette used to define the bitmap colors if hbm is not a device-independent bitmap (DIB).

此外,

Do not pass to the Bitmap::FromHBITMAP method a GDI bitmap or a GDI palette that is currently (or was previously) selected into a device context.

我发布的代码只遵循一个规则,即 GDI 位图当前未 select 编辑到设备上下文中(但之前已 select 编辑)。

该文档可能适用于 Windows 的旧版本。据我所知,MFC 的 CImage class 并不遵循所有这些规则。新的电脑显示器都是 24 或 32 位的,我不知道你怎么弄到它的调色板。

要严格按照文档进行操作,您可以使用 CreateDIBSectionGetDIBits 将 DDB 转换为 DIB 部分。在 Bitmap::FromHBITMAP 中使用新的 DIB 部分 hbitmap_dib。这将满足所有条件:hbitmap 是 dib,它没有(也没有)select 进入设备上下文。

或者,Gdiplus::Bitmap还有另一种方法Bitmap::FromBITMAPINFO。如果没有调色板,您可以使用此代码代替:

HDC hdc = GetDC(HWND_DESKTOP);
HDC memdc = CreateCompatibleDC(hdc);
HBITMAP hbitmap = CreateCompatibleBitmap(hdc, w, h);
HBITMAP oldbmp = (HBITMAP)SelectObject(memdc, hbitmap);
BitBlt(memdc, 0, 0, 800, 600, hdc, 0, 0, SRCCOPY | CAPTUREBLT);
SelectObject(memdc, oldbmp);

BITMAP bm;
GetObject(hbitmap, sizeof(bm), &bm);
int size = ((bm.bmWidth * bm.bmBitsPixel + 31) / 32) * 4 * bm.bmHeight;
BITMAPINFO info{ sizeof(info), bm.bmWidth, bm.bmHeight, 1, bm.bmBitsPixel, BI_RGB, size };
std::vector<char> bits(size);
GetDIBits(memdc, hbitmap, 0, bm.bmHeight, &bits[0], &info, DIB_RGB_COLORS);

Bitmap *bitmap = Bitmap::FromBITMAPINFO(&info, &bits[0]);
bitmap->Save(filename, &pngEncoder);
delete bitmap;

DeleteObject(hbitmap);
DeleteDC(memdc);
ReleaseDC(HWND_DESKTOP, hdc);