1.HICON保存为图片

一、前言

HICON是windows系统的一个句柄,具体来说是图标的句柄。

Ombra项目中,我需要获取到应用的图标,而相关的win api都只能获取到图标的HICON句柄。

所以如何将HICON转换、保存为一个icon图标文件就是一件很麻烦的事情,根据我目前搜索的结果来看,并没有一个api可以直接将其保存为图片文件。

并且经过我的搜索发现,这个问题十多年前就有人遇到了,并且也有相应的解决方案:Stack Overflow

具体来说主要有两种方案,第一种是使用OleCreatePictureIndirect函数间接将其转换为图片文件,但很遗憾,虽然这种方法比较简单,但实测会出现问题,比如得到的图片背景变黑,猜测其抹去了原图的一些颜色数据。

所以最终只能采用第二种方案,亲测在大部分情况下都能良好工作。

二、C++实现

原文中使用的是C++代码实现的,具体来说就是得到HICON的图片数据,然后再根据icon图标的格式,自己用这些数据生成一张icon图片。

原文代码如下:

#include <windows.h>
#include <stdio.h>
#include <tchar.h>

//
// ICONS (.ICO type 1) are structured like this:
//
// ICONHEADER (just 1)
// ICONDIR [1...n] (an array, 1 for each image)
// [BITMAPINFOHEADER+COLOR_BITS+MASK_BITS] [1...n] (1 after the other, for each image)
//
// CURSORS (.ICO type 2) are identical in structure, but use
// two monochrome bitmaps (real XOR and AND masks, this time).
//

typedef struct
{
    WORD idReserved; // must be 0
    WORD idType; // 1 = ICON, 2 = CURSOR
    WORD idCount; // number of images (and ICONDIRs)

    // ICONDIR [1...n]
    // ICONIMAGE [1...n]

} ICONHEADER;

//
// An array of ICONDIRs immediately follow the ICONHEADER
//
typedef struct
{
    BYTE bWidth;
    BYTE bHeight;
    BYTE bColorCount;
    BYTE bReserved;
    WORD wPlanes; // for cursors, this field = wXHotSpot
    WORD wBitCount; // for cursors, this field = wYHotSpot
    DWORD dwBytesInRes;
    DWORD dwImageOffset; // file-offset to the start of ICONIMAGE

} ICONDIR;

//
// After the ICONDIRs follow the ICONIMAGE structures -
// consisting of a BITMAPINFOHEADER, (optional) RGBQUAD array, then
// the color and mask bitmap bits (all packed together
//
typedef struct
{
    BITMAPINFOHEADER biHeader; // header for color bitmap (no mask header)
    //RGBQUAD rgbColors[1...n];
    //BYTE bXOR[1]; // DIB bits for color bitmap
    //BYTE bAND[1]; // DIB bits for mask bitmap

} ICONIMAGE;

//
// Write the ICO header to disk
//
static UINT WriteIconHeader(HANDLE hFile, int nImages)
{
    ICONHEADER iconheader;
    DWORD nWritten;

    // Setup the icon header
    iconheader.idReserved = 0; // Must be 0
    iconheader.idType = 1; // Type 1 = ICON (type 2 = CURSOR)
    iconheader.idCount = nImages; // number of ICONDIRs

    // Write the header to disk
    WriteFile( hFile, &iconheader, sizeof(iconheader), &nWritten, 0);

    // following ICONHEADER is a series of ICONDIR structures (idCount of them, in fact)
    return nWritten;
}

//
// Return the number of BYTES the bitmap will take ON DISK
//
static UINT NumBitmapBytes(BITMAP *pBitmap)
{
    int nWidthBytes = pBitmap->bmWidthBytes;

    // bitmap scanlines MUST be a multiple of 4 bytes when stored
    // inside a bitmap resource, so round up if necessary
    if(nWidthBytes & 3)
        nWidthBytes = (nWidthBytes + 4) & ~3;

    return nWidthBytes * pBitmap->bmHeight;
}

//
// Return number of bytes written
//
static UINT WriteIconImageHeader(HANDLE hFile, BITMAP *pbmpColor, BITMAP *pbmpMask)
{
    BITMAPINFOHEADER biHeader;
    DWORD nWritten;
    UINT nImageBytes;

    // calculate how much space the COLOR and MASK bitmaps take
    nImageBytes = NumBitmapBytes(pbmpColor) + NumBitmapBytes(pbmpMask);

    // write the ICONIMAGE to disk (first the BITMAPINFOHEADER)
    ZeroMemory(&biHeader, sizeof(biHeader));

    // Fill in only those fields that are necessary
    biHeader.biSize = sizeof(biHeader);
    biHeader.biWidth = pbmpColor->bmWidth;
    biHeader.biHeight = pbmpColor->bmHeight * 2; // height of color+mono
    biHeader.biPlanes = pbmpColor->bmPlanes;
    biHeader.biBitCount = pbmpColor->bmBitsPixel;
    biHeader.biSizeImage = nImageBytes;

    // write the BITMAPINFOHEADER
    WriteFile(hFile, &biHeader, sizeof(biHeader), &nWritten, 0);

    // write the RGBQUAD color table (for 16 and 256 colour icons)
    if(pbmpColor->bmBitsPixel == 2 || pbmpColor->bmBitsPixel == 8)
    {

    }

    return nWritten;
}

//
// Wrapper around GetIconInfo and GetObject(BITMAP)
//
static BOOL GetIconBitmapInfo(HICON hIcon, ICONINFO *pIconInfo, BITMAP *pbmpColor, BITMAP *pbmpMask)
{
    if(!GetIconInfo(hIcon, pIconInfo))
        return FALSE;

    if(!GetObject(pIconInfo->hbmColor, sizeof(BITMAP), pbmpColor))
        return FALSE;

    if(!GetObject(pIconInfo->hbmMask, sizeof(BITMAP), pbmpMask))
        return FALSE;

    return TRUE;
}

//
// Write one icon directory entry - specify the index of the image
//
static UINT WriteIconDirectoryEntry(HANDLE hFile, int nIdx, HICON hIcon, UINT nImageOffset)
{
    ICONINFO iconInfo;
    ICONDIR iconDir;

    BITMAP bmpColor;
    BITMAP bmpMask;

    DWORD nWritten;
    UINT nColorCount;
    UINT nImageBytes;

    GetIconBitmapInfo(hIcon, &iconInfo, &bmpColor, &bmpMask);

    nImageBytes = NumBitmapBytes(&bmpColor) + NumBitmapBytes(&bmpMask);

    if(bmpColor.bmBitsPixel >= 8)
        nColorCount = 0;
    else
        nColorCount = 1 << (bmpColor.bmBitsPixel * bmpColor.bmPlanes);

    // Create the ICONDIR structure
    iconDir.bWidth = (BYTE)bmpColor.bmWidth;
    iconDir.bHeight = (BYTE)bmpColor.bmHeight;
    iconDir.bColorCount = nColorCount;
    iconDir.bReserved = 0;
    iconDir.wPlanes = bmpColor.bmPlanes;
    iconDir.wBitCount = bmpColor.bmBitsPixel;
    iconDir.dwBytesInRes = sizeof(BITMAPINFOHEADER) + nImageBytes;
    iconDir.dwImageOffset = nImageOffset;

    // Write to disk
    WriteFile(hFile, &iconDir, sizeof(iconDir), &nWritten, 0);

    // Free resources
    DeleteObject(iconInfo.hbmColor);
    DeleteObject(iconInfo.hbmMask);

    return nWritten;
}

static UINT WriteIconData(HANDLE hFile, HBITMAP hBitmap)
{
    BITMAP bmp;
    int i;
    BYTE * pIconData;

    UINT nBitmapBytes;
    DWORD nWritten;

    GetObject(hBitmap, sizeof(BITMAP), &bmp);

    nBitmapBytes = NumBitmapBytes(&bmp);

    pIconData = (BYTE *)malloc(nBitmapBytes);

    GetBitmapBits(hBitmap, nBitmapBytes, pIconData);

    // bitmaps are stored inverted (vertically) when on disk..
    // so write out each line in turn, starting at the bottom + working
    // towards the top of the bitmap. Also, the bitmaps are stored in packed
    // in memory - scanlines are NOT 32bit aligned, just 1-after-the-other
    for(i = bmp.bmHeight - 1; i >= 0; i--)
    {
        // Write the bitmap scanline
        WriteFile(
            hFile,
            pIconData + (i * bmp.bmWidthBytes), // calculate offset to the line
            bmp.bmWidthBytes, // 1 line of BYTES
            &nWritten,
            0);

        // extend to a 32bit boundary (in the file) if necessary
        if(bmp.bmWidthBytes & 3)
        {
            DWORD padding = 0;
            WriteFile(hFile, &padding, 4 - bmp.bmWidthBytes, &nWritten, 0);
        }
    }

    free(pIconData);

    return nBitmapBytes;
}

//
// Create a .ICO file, using the specified array of HICON images
//
BOOL SaveIcon3(TCHAR *szIconFile, HICON hIcon[], int nNumIcons)
{
    HANDLE hFile;
    int i;
    int * pImageOffset;

    if(hIcon == 0 || nNumIcons < 1)
        return FALSE;

    // Save icon to disk:
    hFile = CreateFile(szIconFile, GENERIC_WRITE, 0, 0, CREATE_ALWAYS, 0, 0);

    if(hFile == INVALID_HANDLE_VALUE)
        return FALSE;

    //
    // Write the iconheader first of all
    //
    WriteIconHeader(hFile, nNumIcons);

    //
    // Leave space for the IconDir entries
    //
    SetFilePointer(hFile, sizeof(ICONDIR) * nNumIcons, 0, FILE_CURRENT);

    pImageOffset = (int *)malloc(nNumIcons * sizeof(int));

    //
    // Now write the actual icon images!
    //
    for(i = 0; i < nNumIcons; i++)
    {
        ICONINFO iconInfo;
        BITMAP bmpColor, bmpMask;

        GetIconBitmapInfo(hIcon[i], &iconInfo, &bmpColor, &bmpMask);

        // record the file-offset of the icon image for when we write the icon directories
        pImageOffset[i] = SetFilePointer(hFile, 0, 0, FILE_CURRENT);

        // bitmapinfoheader + colortable
        WriteIconImageHeader(hFile, &bmpColor, &bmpMask);

        // color and mask bitmaps
        WriteIconData(hFile, iconInfo.hbmColor);
        WriteIconData(hFile, iconInfo.hbmMask);

        DeleteObject(iconInfo.hbmColor);
        DeleteObject(iconInfo.hbmMask);
    }

    //
    // Lastly, skip back and write the icon directories.
    //
    SetFilePointer(hFile, sizeof(ICONHEADER), 0, FILE_BEGIN);

    for(i = 0; i < nNumIcons; i++)
    {
        WriteIconDirectoryEntry(hFile, i, hIcon[i], pImageOffset[i]);
    }

    free(pImageOffset);

    // finished!
    CloseHandle(hFile);

    return TRUE;
}


int saveIcon(TCHAR* filename, TCHAR* iconFile) {
    HICON hIconLarge;
    HICON hIconSmall;
    BOOL ret;


    if ( ExtractIconEx(filename, 0, &hIconLarge, &hIconSmall, 1) == 0 ) {
        return 1;
    }

    ret = SaveIcon3(iconFile, &hIconSmall, 1);
    if ( ret ) {
        return 0;
    }
    return -1;
}

int _tmain(int argc, TCHAR* argv[]) {
    if ( argc < 3 ) {
        printf("Usage: <exe/dll file> <output ico file>");
        return EXIT_FAILURE;
    }
    _tprintf(_T("src = %s\n"), argv[1]);
    _tprintf(_T("dest = %s\n"), argv[2]);
    saveIcon(argv[1], argv[2]);

    return 0;
}

当然,它这写的是一个命令行工具,获取指定exe、dll文件的图标。

如果你只是想要一个HICON句柄转换为图标的函数,那么上面的代码还需要再改改,下面是我简化、更改后得到的代码:

#include <windows.h>
#include <stdio.h>
#include<fstream>
using namespace std;

typedef struct
{
	WORD idReserved; // must be 0
	WORD idType; // 1 = ICON, 2 = CURSOR
	WORD idCount; // number of images (and ICONDIRs)
	// ICONDIR [1...n]
	// ICONIMAGE [1...n]
} ICONHEADER;

//
// An array of ICONDIRs immediately follow the ICONHEADER
//
typedef struct
{
	BYTE bWidth;
	BYTE bHeight;
	BYTE bColorCount;
	BYTE bReserved;
	WORD wPlanes; // for cursors, this field = wXHotSpot
	WORD wBitCount; // for cursors, this field = wYHotSpot
	DWORD dwBytesInRes;
	DWORD dwImageOffset; // file-offset to the start of ICONIMAGE

} ICONDIR;

//
// Return the number of BYTES the bitmap will take ON DISK
//
static UINT NumBitmapBytes(BITMAP& pBitmap)
{
	int nWidthBytes = pBitmap.bmWidthBytes;
	// bitmap scanlines MUST be a multiple of 4 bytes when stored
	// inside a bitmap resource, so round up if necessary
	int res = nWidthBytes % 4;
	if (res != 0) {
		nWidthBytes = nWidthBytes + 4 - res;
	}

	return nWidthBytes * pBitmap.bmHeight;
}

static UINT WriteIconData(ofstream& f, HBITMAP hBitmap)
{
	BITMAP bmp;
	int i;
	BYTE* pIconData;

	UINT nBitmapBytes;
	DWORD nWritten;

	GetObject(hBitmap, sizeof(BITMAP), &bmp);

	nBitmapBytes = NumBitmapBytes(bmp);

	pIconData = (BYTE*)malloc(nBitmapBytes);

	GetBitmapBits(hBitmap, nBitmapBytes, pIconData);

	// bitmaps are stored inverted (vertically) when on disk..
	// so write out each line in turn, starting at the bottom + working
	// towards the top of the bitmap. Also, the bitmaps are stored in packed
	// in memory - scanlines are NOT 32bit aligned, just 1-after-the-other
	for (i = bmp.bmHeight - 1; i >= 0; i--)
	{
		// Write the bitmap scanline
		f.write(
			(char*)pIconData + (i * bmp.bmWidthBytes), // calculate offset to the line
			bmp.bmWidthBytes);

		// extend to a 32bit boundary (in the file) if necessary
		if (bmp.bmWidthBytes & 3)
		{
			DWORD padding = 0;
			f.write((char*)&padding, 4 - bmp.bmWidthBytes);
		}
	}

	free(pIconData);

	return nBitmapBytes;
}

//
// Create a .ICO file, using the specified array of HICON images
//
BOOL SaveIcon3(const char* szIconFile, HICON hIcon)
{
	ofstream of(szIconFile, ios::binary);
	//
	// Write the iconheader first of all
	//
	ICONHEADER iconheader;
	iconheader.idReserved = 0; // Must be 0
	iconheader.idType = 1; // Type 1 = ICON (type 2 = CURSOR)
	iconheader.idCount = 1; // number of ICONDIRs
	// Write the header to disk
	of.write((char*)&iconheader, sizeof(iconheader));
	// Leave space for the IconDir entries
	of.seekp(sizeof(ICONDIR) * 1, ios::cur);
	// Now write the actual icon images!
	ICONINFO iconInfo;
	BITMAP bmpColor, bmpMask;
	GetIconInfo(hIcon, &iconInfo);
	GetObject(iconInfo.hbmColor, sizeof(BITMAP), &bmpColor);
	GetObject(iconInfo.hbmMask, sizeof(BITMAP), &bmpMask);

	// record the file-offset of the icon image for when we write the icon directories
	int pImageOffset = of.tellp();
	// bitmapinfoheader + colortable
	BITMAPINFOHEADER biHeader{};
	UINT nImageBytes;
	// calculate how much space the COLOR and MASK bitmaps take
	nImageBytes = NumBitmapBytes(bmpColor) + NumBitmapBytes(bmpMask);
	printf("%d",nImageBytes);
	// Fill in only those fields that are necessary
	biHeader.biSize = sizeof(biHeader);
	biHeader.biWidth = bmpColor.bmWidth;
	biHeader.biHeight = bmpColor.bmHeight * 2; // height of color+mono
	biHeader.biPlanes = bmpColor.bmPlanes;
	biHeader.biBitCount = bmpColor.bmBitsPixel;
	biHeader.biSizeImage = nImageBytes;
	// write the BITMAPINFOHEADER
	of.write((char*)&biHeader, sizeof(biHeader));


	// color and mask bitmaps
	WriteIconData(of, iconInfo.hbmColor);
	WriteIconData(of, iconInfo.hbmMask);


	DeleteObject(iconInfo.hbmColor);
	DeleteObject(iconInfo.hbmMask);

	// Lastly, skip back and write the icon directories.
	of.seekp(sizeof(ICONHEADER), ios::beg);


	ICONDIR iconDir;

	UINT nColorCount;

	if (bmpColor.bmBitsPixel >= 8) {
		nColorCount = 0;
	}
	else {
		nColorCount = 1 << (bmpColor.bmBitsPixel * bmpColor.bmPlanes);
	}

	// Create the ICONDIR structure
	iconDir.bWidth = (BYTE)bmpColor.bmWidth;
	iconDir.bHeight = (BYTE)bmpColor.bmHeight;
	iconDir.bColorCount = nColorCount;
	iconDir.bReserved = 0;
	iconDir.wPlanes = bmpColor.bmPlanes;
	iconDir.wBitCount = bmpColor.bmBitsPixel;
	iconDir.dwBytesInRes = sizeof(BITMAPINFOHEADER) + nImageBytes;
	iconDir.dwImageOffset = pImageOffset;
	// Write to disk
	of.write((char*)&iconDir, sizeof(iconDir));

	return TRUE;
}

int main() {
	wchar_t path[255] = L"D:\\Install\\test.exe";
	WORD pindex{};
	HICON hIcon = ExtractAssociatedIconW(NULL, path, &pindex);
	SaveIcon3("test.ico", hIcon);
}

三、rust实现

上面之所以会有C++简化版,是因为我这个项目底层使用的rust语言,想要将C++代码改为rust代码是一件很繁琐的事情,为了尽量减少工作量,所以第一步就是尽量减少C++的代码量、逻辑。

rust用的windows crate实现的,如果你对rust使用win api不熟悉,那么可以参考文章:Windows编程

代码实现如下:

fn hicon_to_file(hicon: HICON, save_path: &str) {
    unsafe {
        //根据HICON获取bmp图像数据
        let mut info = ICONINFO::default();
        let _ = GetIconInfo(hicon, &mut info);
        let mut bmp_color = BITMAP::default();
        let mut bmp_mask = BITMAP::default();

        GetObjectW(
            info.hbmColor,
            std::mem::size_of::<BITMAP>() as i32,
            Some(&mut bmp_color as *mut BITMAP as *mut std::ffi::c_void),
        );

        GetObjectW(
            info.hbmMask,
            std::mem::size_of::<BITMAP>() as i32,
            Some(&mut bmp_mask as *mut BITMAP as *mut std::ffi::c_void),
        );

        //得到BMP图片数据
        let n_image_bytes = (num_bitmap_bytes(&bmp_color) + num_bitmap_bytes(&bmp_mask)) as u32;
        println!("{}", n_image_bytes);
        let mut bi_header = BITMAPINFOHEADER::default();
        bi_header.biSize = std::mem::size_of::<BITMAPINFOHEADER>() as u32;
        bi_header.biWidth = bmp_color.bmWidth;
        bi_header.biHeight = bmp_color.bmHeight * 2;
        bi_header.biPlanes = bmp_color.bmPlanes;
        bi_header.biBitCount = bmp_color.bmBitsPixel;
        bi_header.biSizeImage = n_image_bytes;
        //得到ICONDIR数据
        let mut icon_dir = ICONDIR::default();
        icon_dir.b_width = bmp_color.bmWidth as u8;
        icon_dir.b_height = bmp_color.bmHeight as u8;
        icon_dir.b_color_count = 0;
        icon_dir.w_planes = bmp_color.bmPlanes;
        icon_dir.w_bit_count = bmp_color.bmBitsPixel;
        icon_dir.dw_bytes_in_res = size_of::<BITMAPINFOHEADER>() as u32 + n_image_bytes;
        icon_dir.dw_image_offset = (size_of::<ICONHEADER>() + size_of::<ICONDIR>()) as u32;

        //写入icon头
        let mut icon_header = ICONHEADER::default();
        icon_header.id_reserved = 0;
        icon_header.id_type = 1; //type 1=ICON
        icon_header.id_count = 1; //number of icon dir
        let icon_header: &[u8] = std::slice::from_raw_parts(
            &icon_header as *const _ as *const u8,
            size_of::<ICONHEADER>(),
        );

        let mut icon_data = Vec::new();
        icon_header.iter().for_each(|n| icon_data.push(*n));

        // f.write(icon_header).unwrap();
        //写入icon dir
        let icon_dir: &[u8] =
            std::slice::from_raw_parts(&icon_dir as *const _ as *const u8, size_of::<ICONDIR>());
        icon_dir.iter().for_each(|n| icon_data.push(*n));

        //f.write(icon_dir).unwrap();

        //写入bmp头
        let bi_header: &[u8] = std::slice::from_raw_parts(
            &bi_header as *const _ as *const u8,
            size_of::<BITMAPINFOHEADER>(),
        );
        bi_header.iter().for_each(|n| icon_data.push(*n));

        //f.write(bi_header).unwrap();
        //写入图片数据
        write_icon_data(&mut icon_data, info.hbmColor);
        write_icon_data(&mut icon_data, info.hbmMask);

        DeleteObject(info.hbmColor);
        DeleteObject(info.hbmMask);

        std::fs::write(save_path, icon_data).unwrap();
    }
}

//
// Return the number of BYTES the bitmap will take ON DISK
//
fn num_bitmap_bytes(p_bitmap: &BITMAP) -> i32 {
    let mut n_width_bytes = p_bitmap.bmWidthBytes;

    // bitmap scanlines MUST be a multiple of 4 bytes when stored
    // inside a bitmap resource, so round up if necessary
    let res = n_width_bytes % 4;
    if res != 0 {
        n_width_bytes = n_width_bytes + 4 - res;
    }

    return n_width_bytes * p_bitmap.bmHeight;
}
fn write_icon_data(icon_data: &mut Vec<u8>, hbitmap: HBITMAP) {
    unsafe {
        let mut bmp = BITMAP::default();

        GetObjectW(
            hbitmap,
            std::mem::size_of::<BITMAP>() as i32,
            Some(&mut bmp as *mut BITMAP as *mut std::ffi::c_void),
        );

        let n_bitmap_bytes = num_bitmap_bytes(&bmp);
        let mut data = Vec::new();
        data.resize(n_bitmap_bytes as usize, 0);
        GetBitmapBits(
            hbitmap,
            n_bitmap_bytes,
            data.as_mut_ptr() as *mut std::ffi::c_void,
        );

        for i in (0..bmp.bmHeight).rev() {
            // Write the bitmap scanline
            let line_data: &[u8] = std::slice::from_raw_parts(
                (data.as_ptr() as *const _ as *const u8)
                    .wrapping_add((i * bmp.bmWidthBytes) as usize),
                bmp.bmWidthBytes as usize,
            );
            line_data.iter().for_each(|n| icon_data.push(*n));
            // extend to a 32bit boundary (in the file) if necessary
            if bmp.bmWidthBytes % 4 != 0 {
                let padding: u32 = 0;
                let padding: &[u8] =
                    std::slice::from_raw_parts(&padding as *const _ as *const u8, 4);
                padding.iter().for_each(|n| icon_data.push(*n));
            }
        }
    }
}

当你得到了一个HICON,那么就直接调用函数hicon_to_file即可,它的第二个参数为保存该图片的完整路径文件名。

作者:余识
全部文章:0
会员文章:0
总阅读量:0
c/c++pythonrustJavaScriptwindowslinux