如何向 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 文档
备注:内容仅供参考。
