PAYATHON 2026

如何向 GDI StretchDIBits 传入一维字节数组并处理 alpha?

数据小许

可以直接传入一维字节数组。StretchDIBits 的 lpBits 参数接收像素字节缓冲区,无需使用 RGBQUAD 数组。当前代码把每个颜色分量存成了一个 int,而 biBitCount = 32 要求每个完整像素占 4 字节。StretchDIBits 文档

StretchDIBits 和 SetDIBitsToDevice 都不会根据第四个字节自动执行逐像素 alpha 混合。需要透明效果时,应使用 AlphaBlend。

为什么绿色数据会画成蓝色条纹?

原代码为每个像素写入了四个整数:

data2[i*4] = 0;
data2[i*4+1] = 255;
data2[i*4+2] = 0;
data2[i*4+3] = 255;

在 Windows 的常见 C/C++ 环境中,int 占 4 字节,整数按小端序存储。这四个整数实际占用 16 字节:

整数值: 0             255           0             255
内存:   00 00 00 00 | FF 00 00 00 | 00 00 00 00 | FF 00 00 00

32 位 BI_RGB DIB 每 4 字节读取一个像素,颜色字节顺序为 B、G、R,第四字节在普通颜色复制中不用于 alpha 混合。上面的内存会被解释成黑色、蓝色、黑色、蓝色四个像素。

原本打算写入绿色分量的整数 255,其最低字节被当成了另一个像素的蓝色分量,因此出现了蓝色条纹。具体条纹分布还取决于图像宽度和扫描行边界。

int[w*h*4] 与 BYTE[w*h*4] 的内存布局不同,强制转换指针不会完成分量转换。

使用 BYTE 数组绘制纯绿色矩形

下面的示例假设 hdc 是有效的目标窗口 DC,使用连续排列的 B、G、R 和第四字节。普通复制时,第四字节可以填 255,但这不代表启用了透明混合。

#include <windows.h>
#include <vector>
#include <cstddef>

// hdc must be a valid destination DC.
const int w = 320;
const int h = 200;

std::vector<BYTE> pixels(static_cast<std::size_t>(w) * h * 4);

for (std::size_t i = 0; i < pixels.size(); i += 4) {
    pixels[i + 0] = 0;    // B
    pixels[i + 1] = 255;  // G
    pixels[i + 2] = 0;    // R
    pixels[i + 3] = 255;  // Unused by SRCCOPY
}

BITMAPINFO bmi{};
bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmi.bmiHeader.biWidth = w;
bmi.bmiHeader.biHeight = -h;
bmi.bmiHeader.biPlanes = 1;
bmi.bmiHeader.biBitCount = 32;
bmi.bmiHeader.biCompression = BI_RGB;

int copied = StretchDIBits(
    hdc,
    0, 0, w, h,
    0, 0, w, h,
    pixels.data(),
    &bmi,
    DIB_RGB_COLORS,
    SRCCOPY
);

// For this unmirrored copy, copied == 0 means no scan lines were copied.

这里的 biHeight 为负数,表示缓冲区第一行就是图像顶部。正数表示 bottom-up DIB,第一行对应图像底部。StretchDIBits 文档

问题开头的三个像素可以改成:

BYTE data[] = {
    255, 0,   0,   255,  // Blue
    0,   255, 0,   255,  // Green
    0,   0,   255, 255   // Red
};

对应宽度为 3、高度为 1、位深为 32。也可以使用 unsigned char 或 std::uint8_t。普通 char 的有符号性由实现决定,直接写入数值 255 时容易遇到范围或窄化转换问题,使用明确的无符号字节类型更合适。

如果原始数据必须保留为 int 数组

如果上游以四个 int 表示一个像素,可以先转换为紧密排列的字节数组。下面假设所有分量都处于 0~255 范围内:

std::size_t componentCount = static_cast<std::size_t>(w) * h * 4;
std::vector<BYTE> pixels(componentCount);

for (std::size_t i = 0; i < componentCount; ++i) {
    pixels[i] = static_cast<BYTE>(data2[i]);
}

然后把 pixels.data() 传给 GDI。不要用 reinterpret_cast<BYTE*>(data2) 代替转换,这样函数仍会读取原来的整数内存布局。

也可以用一个 32 位整数保存一个完整像素。这时数组长度应为 w*h,每个元素都已打包各个通道,不能让每个整数只表示一个分量。

像素数组需要包含位图头或额外偏移吗?

不需要。StretchDIBits 和示例中的 SetDIBitsToDevice 分别接收像素指针与 BITMAPINFO 指针:

  • 像素指针指向第一个像素字节。
  • BITMAPINFO 描述尺寸、位深和压缩方式。
  • 像素缓冲区前不需要添加 BMP 文件头。

未压缩 DIB 的每行按 4 字节对齐,通用行跨度公式为:

stride = ((width * bitsPerPixel + 31) / 32) * 4

对于这里的 32 位格式,stride = width * 4,每行自然对齐,不需要额外填充。缓冲区大小为 stride * height。图像方向和行布局应与头部描述一致。BITMAPINFOHEADER 文档

仅凭现有代码,无法确定增加数组长度后底部随机像素的具体来源。应检查实际绘图尺寸、循环是否完整初始化了被读取的区域,以及行跨度是否匹配。增加分配长度无法修复错误的像素布局。

为什么改成 BI_BITFIELDS 后不显示?

BI_BITFIELDS 需要提供颜色位掩码。使用 BITMAPINFOHEADER 时,需要在头部之后提供红、绿、蓝三个 DWORD 掩码,不能只修改 biCompression。BITMAPINFOHEADER 文档

当前的标准 32 位布局直接使用 BI_RGB 即可。位掩码不能把四个独立的 int 分量自动压缩为四个字节,也不会使 StretchDIBits 自动支持 alpha 混合。

需要 alpha 混合时如何绘制?

使用 CreateDIBSection 创建 32 位位图,将像素写入它提供的内存,把位图选入内存 DC,再调用 AlphaBlend。CreateDIBSection 文档

启用逐像素 alpha 时,需要设置:

BLENDFUNCTION blend{AC_SRC_OVER, 0, 255, AC_SRC_ALPHA};

其中 255 表示不再通过整体透明度降低源图像的不透明度;AC_SRC_ALPHA 表示使用每个像素的 alpha。源数据必须是 32 位,而且 B、G、R 必须预乘 alpha:BLENDFUNCTION 文档

B' = B * A / 255
G' = G * A / 255
R' = R * A / 255
A' = A

下面的函数接收未预乘的 BGRA 字节数组,并在写入 DIB 时完成预乘。调用前需保证缓冲区至少包含 w*h*4 字节,且尺寸与乘积均在有效范围内。

#include <windows.h>
#include <cstddef>

// Link with Msimg32.lib and Gdi32.lib.
bool DrawBGRAWithAlpha(
    HDC hdc, int x, int y, int w, int h, const BYTE* bgra)
{
    if (!hdc || !bgra || w <= 0 || h <= 0) {
        return false;
    }

    BITMAPINFO bmi{};
    bmi.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
    bmi.bmiHeader.biWidth = w;
    bmi.bmiHeader.biHeight = -h;
    bmi.bmiHeader.biPlanes = 1;
    bmi.bmiHeader.biBitCount = 32;
    bmi.bmiHeader.biCompression = BI_RGB;

    HDC srcDC = CreateCompatibleDC(hdc);
    if (!srcDC) {
        return false;
    }

    void* bits = nullptr;
    HBITMAP bitmap = CreateDIBSection(
        hdc, &bmi, DIB_RGB_COLORS, &bits, nullptr, 0);

    if (!bitmap) {
        DeleteDC(srcDC);
        return false;
    }

    auto* dst = static_cast<BYTE*>(bits);
    const std::size_t count = static_cast<std::size_t>(w) * h;

    for (std::size_t i = 0; i < count; ++i) {
        const std::size_t p = i * 4;
        const unsigned int a = bgra[p + 3];

        dst[p + 0] = static_cast<BYTE>((bgra[p + 0] * a + 127) / 255);
        dst[p + 1] = static_cast<BYTE>((bgra[p + 1] * a + 127) / 255);
        dst[p + 2] = static_cast<BYTE>((bgra[p + 2] * a + 127) / 255);
        dst[p + 3] = static_cast<BYTE>(a);
    }

    HGDIOBJ oldBitmap = SelectObject(srcDC, bitmap);
    if (!oldBitmap || oldBitmap == HGDI_ERROR) {
        DeleteObject(bitmap);
        DeleteDC(srcDC);
        return false;
    }

    BLENDFUNCTION blend{AC_SRC_OVER, 0, 255, AC_SRC_ALPHA};
    BOOL ok = AlphaBlend(
        hdc, x, y, w, h,
        srcDC, 0, 0, w, h,
        blend
    );

    SelectObject(srcDC, oldBitmap);
    DeleteObject(bitmap);
    DeleteDC(srcDC);
    return ok != FALSE;
}

例如,半透明绿色的未预乘输入为 {0, 255, 0, 128},写入 DIB 后应为 {0, 128, 0, 128}。如果输入已经预乘,直接复制即可;重复预乘会让颜色变暗。A = 255 时颜色分量保持不变,因此问题中的不透明像素无需改变颜色值。

绘制透明像素前,应先准备好目标背景;窗口中需要持续显示的内容应在 WM_PAINT 中重绘。频繁绘制时可以复用内存 DC 和 DIB section。如果在 GDI 使用该位图之后再次通过指针修改像素,需要按文档要求同步访问,必要时调用 GdiFlush。CreateDIBSection 文档

备注:内容仅供参考。