解析BMP格式图片

本文详细介绍了BMP文件的结构,包括BITMAPFILEHEADER和BITMAPINFOHEADER,以及如何使用C++通过WindowsAPI读取并显示BMP图像。通过创建自定义函数SetImage和ReadBMP,实现了从文件中解析BMP像素数据并显示在窗口上。同时,文章解决了图像反转和颜色反转的问题,确保正确显示图片。
摘要由CSDN通过智能技术生成

介绍

BMP是Bitmap的简称,是一种图像文件的格式格式简单。本篇文章将讲解如何读取bmp格式的文件

BMP结构

我们可以使用软件 010Editor 来帮助我们查看已经文件的结构。如下是我们要解析的bmp图片

(来源于:百度搜索,大小为600x400)

用010Editor打开后是这样子的

 红色框里是该文件的所有结构,蓝色框里是该文件的二进制数据,我们只需要了解这两个地方的内容就行

从中不难看出该BMP由三部分组成:BITMAPFILEHEADER、BITMAPINFOHEADER、BITMAPLINE

BITMAPFILEHEADER:位图文件头,存储着文件文件类型等数据

BITMAPINFOHEADER:位图信息头,存储着图片的宽高,颜色位数等数据

BITMAPLINE:图片像素数据

BITMAPFILEHEADER和BITMAPINFOHEADER结构如下

typedef struct tagBITMAPFILEHEADER {
        WORD    bfType;
        DWORD   bfSize;
        WORD    bfReserved1;
        WORD    bfReserved2;
        DWORD   bfOffBits;
} BITMAPFILEHEADER, FAR *LPBITMAPFILEHEADER, *PBITMAPFILEHEADER;

typedef struct tagBITMAPINFOHEADER{
        DWORD      biSize;
        LONG       biWidth;
        LONG       biHeight;
        WORD       biPlanes;
        WORD       biBitCount;
        DWORD      biCompression;
        DWORD      biSizeImage;
        LONG       biXPelsPerMeter;
        LONG       biYPelsPerMeter;
        DWORD      biClrUsed;
        DWORD      biClrImportant;
} BITMAPINFOHEADER, FAR *LPBITMAPINFOHEADER, *PBITMAPINFOHEADER;

这些结构微软已经帮我们定义好了。我们并不需要了解结构中所有的变量代表什么,只需要知道其中几个就行

BITMAPFILEHEADER:

bfType:文件类型(BMP文件固定为19778,也就是16进制的4D42)

BITMAPINFOHEADER:

biWidth:图像宽度

biHeight:图像高度

biBitCount:图像颜色深度

我们读取bmp只需要如上的数据就行。

BITMAPLINE代表的是图像的像素数据,每个像素一般为一个RGB值,大小由颜色深度决定(一般为24),存储它可以用COLORREF类型也可以用RGBQUAD类型(COLORREF类型就是RGBQUAD类型),这里以COLORREF类型为例子。BMP不支持透明度,而COLORREF支持透明度,所以我们可以将图像中的每个像素看成不透明(透明度为255)的像素并填入COLORREF类型,具体示例如下

//假设pixel为图像中的一个像素(红色),只有R,G,B三位数据
//                 R   G  B
BYTE pixel[3] = { 255, 0, 0 };
//一个RGBAURD(COLORREF)类型的数据有R,G,B,A四位数据
//                R   G  B   A
RGBQUAD rgba = { 0, 0, 0, 128 };
//我们可以将pixel看成是透明度(A)为255的RGBQUAD类型的数据,所以如果要将pixle转化为RGBQUAD类型数据,就可以进行以下操作
rgba.rgbRed = pixel[0];
rgba.rgbGreen = pixel[1];
rgba.rgbBlue = pixel[2];
rgba.rgbReserved = 255;    //透明度设为255,也就是不透明

图片显示

在读取图像前我们需要思考一下如何显示我们读取到的像素。我想一些人首先想到的是 SetPixel函数 。雀食,它可以设置一个点的像素。但它是一个一个地设置像素点,效果如下

可以看出它是逐渐显示出图像的,显示速度取决于图片的大小 ,所以比较浪费时间,我们需要一种可以立刻显示图像的方法。因为是读取像素点并显示,所以不使用BitBlt函数(比较麻烦)。因此我编写了一个可直接传入像素值显示图片的函数,代码如下:

HWND SetImage(HWND hWnd, COLORREF* colRGBA, int nWidth, int nHeight, int iPosX, int iPosY)
{
    int nSize = nWidth * nHeight;

    WNDCLASS        wnd;

    HWND            hImage = NULL;
    HBITMAP         hCustomBmp = NULL;
    HDC             hLayeredWindowDC = NULL;
    HDC             hCompatibleDC = NULL;

    BITMAPINFO      bmpInfo = { 0 };
    BLENDFUNCTION   blend = { 0 };
    POINT           pSrc = { 0, 0 };
    POINT           ptDst = { iPosX, iPosY };
    SIZE            sizeWnd = { nWidth, nHeight };

    COLORREF*       colBGRA = new COLORREF[nSize];

    if (!colRGBA)
        return NULL;

    for (int index = 0; index < nSize; index++)
    {
        COLORREF col = colRGBA[index];
        colBGRA[index] = (COLORREF)(((BYTE)(col >> 16) | ((WORD)((BYTE)(col >> 8)) << 8)) | (((DWORD)((BYTE)(col)) << 16)) | (((DWORD)((BYTE)(col >> 24)) << 24)));
    }

    wnd.cbClsExtra = 0;
    wnd.cbWndExtra = 0;
    wnd.hbrBackground = (HBRUSH)(GetStockObject(GRAY_BRUSH));
    wnd.hCursor = LoadCursor(NULL, IDC_ARROW);
    wnd.hIcon = LoadCursor(NULL, IDI_APPLICATION);
    wnd.lpfnWndProc = DefWindowProc;
    wnd.lpszClassName = L"LAYERED";
    wnd.lpszMenuName = NULL;
    wnd.style = CS_HREDRAW;
    wnd.hInstance = GetModuleHandle(NULL);
    RegisterClass(&wnd);

    hImage = CreateWindowEx(WS_EX_LAYERED, L"LAYERED", 0,
        WS_POPUP | WS_BORDER, 0, 0, 1, 1, NULL, NULL, GetModuleHandle(NULL), NULL);

    SetParent(hImage, hWnd);

    hLayeredWindowDC = GetDC(hImage);
    hCompatibleDC = CreateCompatibleDC(hLayeredWindowDC);
    blend.BlendOp = AC_SRC_OVER;
    blend.SourceConstantAlpha = 255;
    blend.AlphaFormat = AC_SRC_ALPHA;

    hCustomBmp = CreateCompatibleBitmap(hLayeredWindowDC, nWidth, nHeight);
    SelectObject(hCompatibleDC, hCustomBmp);

    bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    bmpInfo.bmiHeader.biWidth = nWidth;
    bmpInfo.bmiHeader.biHeight = -nHeight;
    bmpInfo.bmiHeader.biPlanes = 1;
    bmpInfo.bmiHeader.biCompression = BI_RGB;
    bmpInfo.bmiHeader.biBitCount = 32;
    SetDIBits(NULL, hCustomBmp, 0, nHeight, colBGRA, &bmpInfo, DIB_RGB_COLORS);

    UpdateLayeredWindow(hImage, hLayeredWindowDC, &ptDst, &sizeWnd, hCompatibleDC, &pSrc, NULL, &blend, ULW_ALPHA);

    DeleteDC(hLayeredWindowDC);

    return hImage;
}

以上代码只需要添加Windows.h头文件,放在任何win32窗口项目里都可以运行。该函数高级在它支持透明度,更加灵活。

hWnd:主窗口的句柄

cPixels:像素点数据

nWidth:图片宽

nHeight:图片高

nPosX:图片(左上角)在窗口中的横坐标

nPosY:图片在窗口中的纵坐标

此函数原理是使用分层窗口通过填充像素显示图片,具体实现原理可以看看我写过的文章 分层窗口的说明与使用。该函数在此处不需要掌握原理,只需会用就行。

至此,我们才正式开始bmp的解析工作

BMP解析

(我是在Windows平台,使用的IDE是vs,其他情况请根据实际进行操作)

我们得先创建一个win32桌面应用程序空项目,创建完之后添加一个main.cpp文件(名字可自己取),创建完后效果如下:

 接下来我们需要写一个简单的窗口,再把上面显示图片的函数 SetImage 放进去,这里不过多赘述,代码如下

#include<windows.h>

HWND SetImage(HWND hWnd, COLORREF* colRGBA, int nWidth, int nHeight, int iPosX, int iPosY)
{
    int nSize = nWidth * nHeight;

    WNDCLASS        wnd;

    HWND            hImage = NULL;
    HBITMAP         hCustomBmp = NULL;
    HDC             hLayeredWindowDC = NULL;
    HDC             hCompatibleDC = NULL;

    BITMAPINFO      bmpInfo = { 0 };
    BLENDFUNCTION   blend = { 0 };
    POINT           pSrc = { 0, 0 };
    POINT           ptDst = { iPosX, iPosY };
    SIZE            sizeWnd = { nWidth, nHeight };

    COLORREF*       colBGRA = new COLORREF[nSize];

    if (!colRGBA)
        return NULL;

    for (int index = 0; index < nSize; index++)
    {
        COLORREF col = colRGBA[index];
        colBGRA[index] = (COLORREF)(((BYTE)(col >> 16) | ((WORD)((BYTE)(col >> 8)) << 8)) | (((DWORD)((BYTE)(col)) << 16)) | (((DWORD)((BYTE)(col >> 24)) << 24)));
    }

    wnd.cbClsExtra = 0;
    wnd.cbWndExtra = 0;
    wnd.hbrBackground = (HBRUSH)(GetStockObject(GRAY_BRUSH));
    wnd.hCursor = LoadCursor(NULL, IDC_ARROW);
    wnd.hIcon = LoadCursor(NULL, IDI_APPLICATION);
    wnd.lpfnWndProc = DefWindowProc;
    wnd.lpszClassName = L"LAYERED";
    wnd.lpszMenuName = NULL;
    wnd.style = CS_HREDRAW;
    wnd.hInstance = GetModuleHandle(NULL);
    RegisterClass(&wnd);

    hImage = CreateWindowEx(WS_EX_LAYERED, L"LAYERED", 0,
        WS_POPUP | WS_BORDER, 0, 0, 1, 1, NULL, NULL, GetModuleHandle(NULL), NULL);

    SetParent(hImage, hWnd);

    hLayeredWindowDC = GetDC(hImage);
    hCompatibleDC = CreateCompatibleDC(hLayeredWindowDC);
    blend.BlendOp = AC_SRC_OVER;
    blend.SourceConstantAlpha = 255;
    blend.AlphaFormat = AC_SRC_ALPHA;

    hCustomBmp = CreateCompatibleBitmap(hLayeredWindowDC, nWidth, nHeight);
    SelectObject(hCompatibleDC, hCustomBmp);

    bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    bmpInfo.bmiHeader.biWidth = nWidth;
    bmpInfo.bmiHeader.biHeight = -nHeight;
    bmpInfo.bmiHeader.biPlanes = 1;
    bmpInfo.bmiHeader.biCompression = BI_RGB;
    bmpInfo.bmiHeader.biBitCount = 32;
    SetDIBits(NULL, hCustomBmp, 0, nHeight, colBGRA, &bmpInfo, DIB_RGB_COLORS);

    UpdateLayeredWindow(hImage, hLayeredWindowDC, &ptDst, &sizeWnd, hCompatibleDC, &pSrc, NULL, &blend, ULW_ALPHA);

    DeleteDC(hLayeredWindowDC);

    return hImage;
}

//自定义窗口过程
LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg,
    WPARAM wParam, LPARAM lParam)
{
    switch (Msg)
    {
    case WM_PAINT:
    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd, &ps);



        EndPaint(hWnd, &ps);
        break;
    }
    case WM_DESTROY:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd, Msg, wParam, lParam);
    }
    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevlnstance,
    LPSTR lpCmdLine, int nShowCmd)
{
    //注册
    WNDCLASS wnd;
    wnd.cbClsExtra = 0;
    wnd.cbWndExtra = 0;
    wnd.hbrBackground = (HBRUSH)(GetStockObject(GRAY_BRUSH));
    wnd.hCursor = LoadCursor(NULL, IDC_ARROW);
    wnd.hIcon = LoadCursor(NULL, IDI_APPLICATION);
    wnd.lpfnWndProc = WndProc;
    wnd.lpszClassName = L"Window";
    wnd.lpszMenuName = NULL;
    wnd.style = CS_HREDRAW;
    wnd.hInstance = hInstance;
    RegisterClass(&wnd);

    //创建
    HWND hWnd = CreateWindow(L"Window", L"BMP",
        WS_OVERLAPPEDWINDOW, 100, 100, 800, 600, NULL, NULL, hInstance, NULL);
    //显示
    ShowWindow(hWnd, nShowCmd);
    //更新
    UpdateWindow(hWnd);

    //循环
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);

    }
}

(PS:若看不懂请先去学习win32基础)

接着我们就要开始解析BMP格式文件了。先梳理一下思路:先读取位图文件头(BITMAPFILEHEADER),再读取位图信息头(BITMAPINFOHEADER),最后再读取图片的像素信息,接下来我们创建一个函数 ReadBMP 专门解析BMP格式文件

为了读取文件我们需要再添加头文件<iostream>

要读取BMP数据就需要一张BMP图片,所以函数就需要传入BMP图片的路径。最终我们需要的是图片的像素值,所以函数需要返回或传出图片的像素值,所以函数就可以初步编写成以下形式


#include <windows.h>
#include <iostream>

HWND SetImage(HWND hWnd, COLORREF* colRGBA, int nWidth, int nHeight, int iPosX, int iPosY)
{
    ...
}

//                    传入图片路径         接收图片像素
BOOL ReadBMP(const char* chPath, COLORREF* colRGBs)
{
    return TRUE;
}

...

首先我们要打开文件,使用fopen_s函数打开文件

BOOL ReadBMP(const char* chPath, COLORREF* colRGBs)
{
    FILE* fp = NULL;
    //打开文件
    fopen_s(&fp, chPath, "rb");
    //打开失败
    if(!fp)
        return FALSE;

    return TRUE;
}

当文件打开成功后,我们首先读取的是位图文件头,使用fread函数读取文件

BOOL ReadBMP(const char* chPath, COLORREF* colRGBs)
{
    FILE* fp = NULL;
    BITMAPFILEHEADER bmpFileHeader = {};        //声明存放位图文件头的变量

    //打开文件
    fopen_s(&fp, chPath, "rb");
    //打开失败
    if (!fp)
        return FALSE;
    //读取位图文件头
    fread(&bmpFileHeader, sizeof(bmpFileHeader), 1, fp);



    return TRUE;
}

我们还得做一些保护措施,如果传入的图片不是BMP,那么接下来所做的操作都将毫无意义,还会有一大堆错误,所以在这里判断一下文件类型是否为BMP,通过位图文件头里的bfType判断,若其等于19778,则为BMP文件,否则就不是BMP文件

BOOL ReadBMP(const char* chPath, COLORREF* colRGBs)
{
    FILE* fp = NULL;
    BITMAPFILEHEADER bmpFileHeader = {};        //声明存放位图文件头的变量

    //打开文件
    fopen_s(&fp, chPath, "rb");
    //打开失败
    if (!fp)
        return FALSE;
    //读取位图文件头
    fread(&bmpFileHeader, sizeof(bmpFileHeader), 1, fp);
    //判断是否为BMP文件
    if (bmpFileHeader.bfType != 19778)
        return FALSE;   //不是就返回FALSE


    return TRUE;
}

当我们确定是BMP文件之后,就可以读取位图信息头了

BOOL ReadBMP(const char* chPath, COLORREF* colRGBs)
{
    FILE* fp = NULL;
    BITMAPFILEHEADER bmpFileHeader = {};        //声明存放位图文件头的变量
    BITMAPINFOHEADER bmpInfoHeader = {};

    //打开文件
    fopen_s(&fp, chPath, "rb");
    //打开失败
    if (!fp)
        return FALSE;
    //读取位图文件头
    fread(&bmpFileHeader, sizeof(bmpFileHeader), 1, fp);
    //判断是否为BMP文件
    if (bmpFileHeader.bfType != 19778)
        return FALSE;   //不是就返回FALSE

    //读取位图信息头
    fread(&bmpInfoHeader, 1, sizeof(bmpInfoHeader), fp);

    return TRUE;
}

接下来就可以通过位图信息头获取一些关于图片的重要信息了

BOOL ReadBMP(const char* chPath, COLORREF* colRGBs)
{
    FILE* fp = NULL;
    BITMAPFILEHEADER bmpFileHeader = {};        //声明存放位图文件头的变量
    BITMAPINFOHEADER bmpInfoHeader = {};

    //打开文件
    fopen_s(&fp, chPath, "rb");
    //打开失败
    if (!fp)
        return FALSE;
    //读取位图文件头
    fread(&bmpFileHeader, sizeof(bmpFileHeader), 1, fp);
    //判断是否为BMP文件
    if (bmpFileHeader.bfType != 19778)
        return FALSE;   //不是就返回FALSE

    //读取位图信息头
    fread(&bmpInfoHeader, 1, sizeof(bmpInfoHeader), fp);
    //获取图片信息
    LONG picWidth = bmpInfoHeader.biWidth;  //获取图片宽
    LONG picHeight = bmpInfoHeader.biHeight;  //获取图片高
    LONG picSize = picWidth * picHeight;//获取图片大小

    

    return TRUE;
}

然后初始化要传出的变量用于存放读取的图片像素

...
LONG picWidth = bmpInfoHeader.biWidth;  //获取图片宽
LONG picHeight = bmpInfoHeader.biHeight;  //获取图片高
LONG picSize = picWidth * picHeight;//获取图片大小

//初始化要传出的变量
ZeroMemory(colRGBs, sizeof(RGBQUAD) * picSize);



return TRUE;
...

之后我们就要读取图片的内容了,通过循环嵌套一行一行地读取像素值

图片的像素数据每个像素占3字节大小,而COLORREF,RGBQUAD等数据类型都占4个字节,为了更方便读取也为了更容易理解这个过程,我们可以创建一个数据类型,使其与图片里的每个像素值类型相符,具体如下

...
#include <iostream>
//创建一个只有3字节大小的数据类型便于读取图像像素值
typedef struct _tagRGBTriplet {
    BYTE rgbRed;        //R
    BYTE rgbGreen;      //G
    BYTE rgbBlue;       //B
}RGBTRIPLET, *LPRGBTRIPLET;

HWND SetImage(HWND hWnd, COLORREF* colRGBA, int nWidth, int nHeight, int iPosX, int iPosY)
...

为了方便赋值,我们需要一个RGBA宏,定义如下:

#define         RGBA(r,g,b,a)                   (COLORREF)(((BYTE)(r)|((WORD)((BYTE)(g)) << 8))|(((DWORD)((BYTE)(b)) << 16))|(((DWORD)((BYTE)(a)) << 24)))

接着回到ReadBMP函数里读取像素值信息

for (int nColumn = 0; nColumn < picHeight; nColumn++)
{
    for (int nRow = 0; nRow < picWidth; nRow++)
    {
        RGBTRIPLET rgb = { 0, 0, 0 };
        //读取单个像素值
        fread(&rgb, sizeof(RGBTRIPLET), 1, fp);
        //                                                                       透明度为255意为不透明
        colRGBs[nColumn * picWidth + nRow] = RGBA(rgb.rgbRed, rgb.rgbGreen, rgb.rgbBlue, 255);
        //nColunm * picWidth + nRow 就是位于横坐标为nRow, 纵坐标为nCloumn的像素的索引


    }
}

至此,我们的BMP读取函数完成了!

我们在WM_PAINT消息里读取并显示图片看看是什么样的

已知图片的大小为600x400

...
case WM_PAINT:
{
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hWnd, &ps);

    COLORREF* pixels = new COLORREF[600 * 400];
    //读取BMP
    ReadBMP(".\\CSDN-BMP.bmp", pixels);
    //显示图片
    SetImage(hWnd, pixels, 600, 400, 0, 0);

    EndPaint(hWnd, &ps);
    break;
}
...

不出意外的话,图片效果如下

 从中可以看出两个问题:

1.图像反了

2.图像RGB值有问题

我们先解决第一个问题

因为文件中的像素值是从图像左下角第一个像素开始的,所以图片在变量里的样子应该是上下颠倒的

而SetImage函数是从左上角第一个像素开始读取并显示的,所以显示出的图像就是上下颠倒的样子

 

 因此我们只需要将读取出来的像素上下镜像翻转一下,SetImage就可以正常显示图片了就行了

至于怎么翻转,可以先创建一个临时变量用于存放图像一整行的显像素,每读取一行像素后就将它放入存储图像像素值的变量里倒数的指定行数,例如读取了第一行的像素,就将其放入变量的最后一行;读取了第二行的像素,就将其放入变量的倒数第二行······如此循环往复,就能实现垂直镜像翻转

我们先设图片的高度为h,当前读取的像素行数为a,那么第a行的像素值就应该放入变量里的(h-a+1)行,不妨带入一些数据:图片高度为10,当前读取第3行像素,那么该行像素值应该放入变量里的第(10-3+1)=8行,也就是倒数第三行

实际上在代码中我们不直接使用行数,而是使用索引。图像的第一行索引是0,最后一行索引是(图像高度的值-1),因此我们要对上面的结论做出一些改变:

还是设图片高度为h,若当前读取的像素行数索引为b,那么当前读取的行数为(b+1);若最后一行的索引是p,那么当前行数就是(p+1),可得h=p+1。因此索引为b行的像素值应该放入变量中索引为(h-b-1)的一行

有了以上知识就可以编写代码了

...
for (int nColumn = 0; nColumn < picHeight; nColumn++)
{
    for (int nRow = 0; nRow < picWidth; nRow++)
    {
        RGBTRIPLET rgb = { 0, 0, 0 };
        //读取单个像素值
        fread(&rgb, sizeof(RGBTRIPLET), 1, fp);
        //先将一行像素存入临时变量里                      透明度为255意为不透明
        tmpRGBs[nRow] = RGBA(rgb.rgbRed, rgb.rgbGreen, rgb.rgbBlue, 255);
        //colRGBs[nColumn * picWidth + nRow] = RGBA(rgb.rgbRed, rgb.rgbGreen, rgb.rgbBlue, 255);
        //nColunm * picWidth + nRow 就是位于横坐标为nRow, 纵坐标为nCloumn的像素的索引

    }
    //将索引为nColumn的一行像素复制到指定的位置
    CopyMemory(&colRGBs[(picHeight - nColumn - 1) * picWidth], tmpRGBs, sizeof(COLORREF)*picWidth);
    /*
    因为使用的存储像素值的变量并非二维数组,因此无法直接选择行数
    若将一个一维数组想象成一个m*n的矩阵,若左上角第一个位置为(0,0),那么该矩阵(x,y)位置的索引应为 y*n+x
    若不理解请看下面的示例:
    2   1   3   1
    0   2   8   9
    2   1   5   6
    7   6   4   8
    9   0   0   1
    该矩阵是一个5*4的矩阵,(2,2)的位置值为5,索引为 2 * 4 + 2 = 10
    现在应该就好理解 (picHeight - nColumn - 1) * picWidth 了,如果写完整点就是 (picHeight - nColumn - 1) * picWidth + 0
    */
}
...

我想我的注释已经写得很清楚了

不出意外的话现在运行代码后效果如下

至此我们就解决了第一个问题!

现在我们来解决第二个问题:图像RGB值有问题

如果是一些对win32绘图很熟悉的人,可能一眼就看出这是颜色反转后的图像。没错,图像变成这样子都是因为rgb值反了,也就是像素值中R位置上的数据与B位上的数据反了。

我们可以通过010Editor看看bmp数据。

我们点击左下方框中的BITMAPLINE,在上方数据框中蓝色的就是图像像素值数据,可以看到第一个RGB值为"00 1B 2B",合起来就是0x2B1B00。我们在打开系统自带的画图软件,看看第一个像素值到底是什么

确实是2B1B00,那为什么颜色会被反转呢?

我们再来看看用来存放读取RGB值的数据类型

typedef struct _tagRGBTriplet {
    BYTE rgbRed;        //R
    BYTE rgbGreen;      //G
    BYTE rgbBlue;       //B
}RGBTRIPLET, *LPRGBTRIPLET;

因为我们用来读取图像RGB值的数据类型是三个BYTE合在一起的,因此它们是按照顺序来读取每个字节,因此它们被拆成00 1B 2B三个数据被错误地放入了R、G、B位,要解决它只需要调换其中R位与B位的位置,因为无论怎么翻转G位始终在中间不变

将此结构改成以下形式就行

typedef struct _tagRGBTriplet {
    BYTE rgbBlue;       //B
    BYTE rgbGreen;      //G
    BYTE rgbRed;        //R
}RGBTRIPLET, *LPRGBTRIPLET;

到目前为止,所以问题都已被解决,不出意外的话运行代码后效果如下

终于可以正常显示图像了! 

现在我们来换一张图片显示看看效果如何

 (原图:https://www.pixiv.net/artworks/106964347,建议直接使用上面的图像)

上图大小为799x745

现在让我们改一下代码

...
case WM_PAINT:
{
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hWnd, &ps);

    COLORREF* pixels = new COLORREF[799 * 745];
    ReadBMP(".\\CSDN-BMP-2.bmp", pixels);
    SetImage(hWnd, pixels, 799, 745, 0, 0);

    EndPaint(hWnd, &ps);
    break;
}
...

不出意外的话,显示的图像会长这样

 为什么我老婆会变成这个样子呢?责任全在系统!!!开个玩笑awa

系统一般以4字节为单位读取数据,因此当图像一行的字节数不是4的倍数时,系统会自动补0使其成为4的倍数,这就是为什么中间会看到一条黑线(黑色的RGB值就为0),所以我们在读取像素时要检查图像一行的字节数是否为4的倍数,如果是,那么读取完一行后就要使用fseek函数跳过系统自动补全的地方

代码如下

...
//初始化要传出的变量
ZeroMemory(colRGBs, sizeof(RGBQUAD) * picSize);
//计算要跳过的字节数
int DataJump = 4 - ((picWidth * (sizeof(RGBQUAD) - 1)) % 4);
/*
sizeof(RGBQUAD) - 1:就是RGB值的大小,为3字节
picWidth * (sizeof(RGBQUAD) - 1):图像一整行的字节数,之后除以4取余数
用4减去此余数就是系统自动补全的字节数,读取完一行后就需要跳过这些字节
若不理解请好好思考
*/

//创建临时变量
COLORREF* tmpRGBs = new COLORREF[picWidth];
ZeroMemory(tmpRGBs, sizeof(RGBQUAD) * picWidth);
for (int nColumn = 0; nColumn < picHeight; nColumn++)
{
    for (int nRow = 0; nRow < picWidth; nRow++)
    {
        RGBTRIPLET rgb = { 0, 0, 0 };
        //读取单个像素值
        fread(&rgb, sizeof(RGBTRIPLET), 1, fp);
        //先将一行像素存入临时变量里                      透明度为255意为不透明
        tmpRGBs[nRow] = RGBA(rgb.rgbRed, rgb.rgbGreen, rgb.rgbBlue, 255);
        //colRGBs[nColumn * picWidth + nRow] = RGBA(rgb.rgbRed, rgb.rgbGreen, rgb.rgbBlue, 255);
        //nColunm * picWidth + nRow 就是位于横坐标为nRow, 纵坐标为nCloumn的像素的索引

    }
    //将索引为nColumn的一行像素复制到指定的位置
    CopyMemory(&colRGBs[(picHeight - nColumn - 1) * picWidth], tmpRGBs, sizeof(COLORREF)*picWidth);
    /*
    因为使用的存储像素值的变量并非二维数组,因此无法直接选择行数
    若将一个一维数组想象成一个m*n的矩阵,若左上角第一个位置为(0,0),那么该矩阵(x,y)位置的索引应为 y*n+x
    若不理解请看下面的示例:
    2   1   3   1
    0   2   8   9
    2   1   5   6
    7   6   4   8
    9   0   0   1
    该矩阵是一个5*4的矩阵,(2,2)的位置值为5,索引为 2 * 4 + 2 = 10
    现在应该就好理解 (picHeight - nColumn - 1) * picWidth 了,如果写完整点就是 (picHeight - nColumn - 1) * picWidth + 0
    */

    //读取完一行后,若图像一行的字节数不是4的倍数
    if (picWidth % 4 != 0)
        fseek(fp, DataJump, SEEK_CUR);  //跳过系统补全的地方
}

return TRUE;
...

现在再次运行代码,效果如下

终于可以正常显示我的老婆了! qwq

至此,我们已经完成了BMP文件的解析工作并成功显示了它,本文也在此画上一个"完美"的句号。

其实本文中的一些代码并不完美,还请读者试着自己改进与完善

最后附上完整代码(都在一个文件里)


#include <windows.h>
#include <iostream>

#define         RGBA(r,g,b,a)                   (COLORREF)(((BYTE)(r)|((WORD)((BYTE)(g)) << 8))|(((DWORD)((BYTE)(b)) << 16))|(((DWORD)((BYTE)(a)) << 24)))

//创建一个只有3字节大小的数据类型便于读取图像像素值
typedef struct _tagRGBTriplet {
    BYTE rgbBlue;       //B
    BYTE rgbGreen;      //G
    BYTE rgbRed;        //R
}RGBTRIPLET, *LPRGBTRIPLET;

HWND SetImage(HWND hWnd, COLORREF* colRGBA, int nWidth, int nHeight, int iPosX, int iPosY)
{
    int nSize = nWidth * nHeight;

    WNDCLASS        wnd;

    HWND            hImage = NULL;
    HBITMAP         hCustomBmp = NULL;
    HDC             hLayeredWindowDC = NULL;
    HDC             hCompatibleDC = NULL;

    BITMAPINFO      bmpInfo = { 0 };
    BLENDFUNCTION   blend = { 0 };
    POINT           pSrc = { 0, 0 };
    POINT           ptDst = { iPosX, iPosY };
    SIZE            sizeWnd = { nWidth, nHeight };

    COLORREF*       colBGRA = new COLORREF[nSize];

    if (!colRGBA)
        return NULL;

    for (int index = 0; index < nSize; index++)
    {
        COLORREF col = colRGBA[index];
        colBGRA[index] = (COLORREF)(((BYTE)(col >> 16) | ((WORD)((BYTE)(col >> 8)) << 8)) | (((DWORD)((BYTE)(col)) << 16)) | (((DWORD)((BYTE)(col >> 24)) << 24)));
    }

    wnd.cbClsExtra = 0;
    wnd.cbWndExtra = 0;
    wnd.hbrBackground = (HBRUSH)(GetStockObject(GRAY_BRUSH));
    wnd.hCursor = LoadCursor(NULL, IDC_ARROW);
    wnd.hIcon = LoadCursor(NULL, IDI_APPLICATION);
    wnd.lpfnWndProc = DefWindowProc;
    wnd.lpszClassName = L"LAYERED";
    wnd.lpszMenuName = NULL;
    wnd.style = CS_HREDRAW;
    wnd.hInstance = GetModuleHandle(NULL);
    RegisterClass(&wnd);

    hImage = CreateWindowEx(WS_EX_LAYERED, L"LAYERED", 0,
        WS_POPUP | WS_BORDER, 0, 0, 1, 1, NULL, NULL, GetModuleHandle(NULL), NULL);

    SetParent(hImage, hWnd);

    hLayeredWindowDC = GetDC(hImage);
    hCompatibleDC = CreateCompatibleDC(hLayeredWindowDC);
    blend.BlendOp = AC_SRC_OVER;
    blend.SourceConstantAlpha = 255;
    blend.AlphaFormat = AC_SRC_ALPHA;

    hCustomBmp = CreateCompatibleBitmap(hLayeredWindowDC, nWidth, nHeight);
    SelectObject(hCompatibleDC, hCustomBmp);

    bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    bmpInfo.bmiHeader.biWidth = nWidth;
    bmpInfo.bmiHeader.biHeight = -nHeight;
    bmpInfo.bmiHeader.biPlanes = 1;
    bmpInfo.bmiHeader.biCompression = BI_RGB;
    bmpInfo.bmiHeader.biBitCount = 32;
    SetDIBits(NULL, hCustomBmp, 0, nHeight, colBGRA, &bmpInfo, DIB_RGB_COLORS);

    UpdateLayeredWindow(hImage, hLayeredWindowDC, &ptDst, &sizeWnd, hCompatibleDC, &pSrc, NULL, &blend, ULW_ALPHA);

    DeleteDC(hLayeredWindowDC);

    return hImage;
}

BOOL ReadBMP(const char* chPath, COLORREF* colRGBs)
{
    FILE* fp = NULL;
    BITMAPFILEHEADER bmpFileHeader = {};        //声明存放位图文件头的变量
    BITMAPINFOHEADER bmpInfoHeader = {};

    //打开文件
    fopen_s(&fp, chPath, "rb");
    //打开失败
    if (!fp)
        return FALSE;
    //读取位图文件头
    fread(&bmpFileHeader, sizeof(bmpFileHeader), 1, fp);
    //判断是否为BMP文件
    if (bmpFileHeader.bfType != 19778)
        return FALSE;   //不是就返回FALSE

    //读取位图信息头
    fread(&bmpInfoHeader, 1, sizeof(bmpInfoHeader), fp);
    //获取图片信息
    LONG picWidth = bmpInfoHeader.biWidth;  //获取图片宽
    LONG picHeight = bmpInfoHeader.biHeight;  //获取图片高
    LONG picSize = picWidth * picHeight;//获取图片大小

    //初始化要传出的变量
    ZeroMemory(colRGBs, sizeof(RGBQUAD) * picSize);
    //计算要跳过的字节数
    int DataJump = 4 - ((picWidth * (sizeof(RGBQUAD) - 1)) % 4);
    /*
    sizeof(RGBQUAD) - 1:就是RGB值的大小,为3字节
    picWidth * (sizeof(RGBQUAD) - 1):图像一整行的字节数,之后除以4取余数
    用4减去此余数就是系统自动补全的字节数,读取完一行后就需要跳过这些字节
    若不理解请好好思考
    */

    //创建临时变量
    COLORREF* tmpRGBs = new COLORREF[picWidth];
    ZeroMemory(tmpRGBs, sizeof(RGBQUAD) * picWidth);
    for (int nColumn = 0; nColumn < picHeight; nColumn++)
    {
        for (int nRow = 0; nRow < picWidth; nRow++)
        {
            RGBTRIPLET rgb = { 0, 0, 0 };
            //读取单个像素值
            fread(&rgb, sizeof(RGBTRIPLET), 1, fp);
            //先将一行像素存入临时变量里                      透明度为255意为不透明
            tmpRGBs[nRow] = RGBA(rgb.rgbRed, rgb.rgbGreen, rgb.rgbBlue, 255);
            //colRGBs[nColumn * picWidth + nRow] = RGBA(rgb.rgbRed, rgb.rgbGreen, rgb.rgbBlue, 255);
            //nColunm * picWidth + nRow 就是位于横坐标为nRow, 纵坐标为nCloumn的像素的索引

        }
        //将索引为nColumn的一行像素复制到指定的位置
        CopyMemory(&colRGBs[(picHeight - nColumn - 1) * picWidth], tmpRGBs, sizeof(COLORREF)*picWidth);
        /*
        因为使用的存储像素值的变量并非二维数组,因此无法直接选择行数
        若将一个一维数组想象成一个m*n的矩阵,若左上角第一个位置为(0,0),那么该矩阵(x,y)位置的索引应为 y*n+x
        若不理解请看下面的示例:
        2   1   3   1
        0   2   8   9
        2   1   5   6
        7   6   4   8
        9   0   0   1
        该矩阵是一个5*4的矩阵,(2,2)的位置值为5,索引为 2 * 4 + 2 = 10
        现在应该就好理解 (picHeight - nColumn - 1) * picWidth 了,如果写完整点就是 (picHeight - nColumn - 1) * picWidth + 0
        */

        //读取完一行后,若图像一行的字节数不是4的倍数
        if (picWidth % 4 != 0)
            fseek(fp, DataJump, SEEK_CUR);  //跳过系统补全的地方
    }
    
    return TRUE;
}

//自定义窗口过程
LRESULT CALLBACK WndProc(HWND hWnd, UINT Msg,
    WPARAM wParam, LPARAM lParam)
{
    switch (Msg)
    {
    case WM_PAINT:
    {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd, &ps);

        COLORREF* pixels = new COLORREF[799 * 745];
        ReadBMP(".\\CSDN-BMP-2.bmp", pixels);
        SetImage(hWnd, pixels, 799, 745, 0, 0);

        EndPaint(hWnd, &ps);
        break;
    }
    case WM_DESTROY:
        PostQuitMessage(0);
        return 0;
    default:
        return DefWindowProc(hWnd, Msg, wParam, lParam);
    }
    return 0;
}

int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevlnstance,
    LPSTR lpCmdLine, int nShowCmd)
{
    //注册
    WNDCLASS wnd;
    wnd.cbClsExtra = 0;
    wnd.cbWndExtra = 0;
    wnd.hbrBackground = (HBRUSH)(GetStockObject(GRAY_BRUSH));
    wnd.hCursor = LoadCursor(NULL, IDC_ARROW);
    wnd.hIcon = LoadCursor(NULL, IDI_APPLICATION);
    wnd.lpfnWndProc = WndProc;
    wnd.lpszClassName = L"Window";
    wnd.lpszMenuName = NULL;
    wnd.style = CS_HREDRAW;
    wnd.hInstance = hInstance;
    RegisterClass(&wnd);

    //创建
    HWND hWnd = CreateWindow(L"Window", L"BMP",
        WS_OVERLAPPEDWINDOW, 100, 100, 800, 600, NULL, NULL, hInstance, NULL);
    //显示
    ShowWindow(hWnd, nShowCmd);
    //更新
    UpdateWindow(hWnd);

    //循环
    MSG msg;
    while (GetMessage(&msg, NULL, 0, 0))
    {
        TranslateMessage(&msg);
        DispatchMessage(&msg);

    }
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值