opengl-day01 绘制窗口
-
CS_OWNDC: 为窗口分配独立的设备上下文(重要:每个窗口有自己的DC)
-
CS_HREDRAW | CS_VREDRAW: 窗口大小改变时重绘
-
HDC: 设备上下文句柄,Windows的绘图接口 -
使用内存DC进行离屏绘制,避免闪烁
HDC hDc = GetDC(hWnd); // 获取窗口DC HDC hMem = ::CreateCompatibleDC(hDc); // 创建内存DC
-
创建32位ARGB格式的DIB(设备无关位图)
buffer指针指向像素数据,可以直接操作
BITMAPINFO bmpInfo; // ... 设置位图信息 HBITMAP hBmp = CreateDIBSection(hDc, &bmpInfo, DIB_RGB_COLORS, (void**)&buffer, 0, 0);
while (true) {
// 消息处理
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
// 处理消息
}
// 绘制逻辑
memset(buffer, 0, width* height * 4); // 清屏为黑色
// 绘制白色横线
unsigned char* rgba = (unsigned char*)buffer;
int pitch = width * 4;
memset(rgba + pitch * 10, 255, pitch); // 第11行设为白色
// 显示到窗口
BitBlt(hDc, 0, 0, width, height, hMem, 0, 0, SRCCOPY);
}
绘制机制详解
-
双缓冲技术:
-
先在内存DC中绘制
-
然后一次性复制到屏幕DC
-
避免闪烁
-
-
像素格式:
-
biBitCount = 32: 32位色,RGBA格式 -
每个像素4字节:Blue(0), Green(1), Red(2), Alpha(3)
-
由于使用
memset设置255,实际得到的是白色(所有分量都是255)
-
-
绘制坐标:
-
pitch = width * 4: 每行的字节数 -
rgba + pitch * 10: 指向第11行开头(索引从0开始)
-
unsigned char* rgba = (unsigned char*)buffer;
-
buffer原本是void*类型(通用指针) -
转换为
unsigned char*类型 -
赋值给新的指针变量
rgba
为什么要转换?
1. 原始buffer类型
void* buffer = 0; // 通用指针,没有具体类型
-
void*是"无类型指针",可以指向任何数据类型 -
但你不能直接通过
void*访问内存内容:buffer[0] = 255; // 错误!void*不能直接下标访问 *buffer = 0; // 错误!不能解引用void*
2. 转换为unsigned char*的好处
unsigned char* rgba = (unsigned char*)buffer;
-
unsigned char是1字节(8位)的无符号整数 -
转换为
unsigned char*后:-
可以按字节访问内存
-
支持指针算术运算
-
支持下标操作符
-
BitBlt(hDc, 0, 0, width, height, hMem, 0, 0, SRCCOPY);
这行代码是Windows GDI中非常重要的位块传输函数,用于将图像从源设备上下文复制到目标设备上下文。
BOOL BitBlt( HDC hdcDest, // 目标DC int nXDest, // 目标左上角X坐标 int nYDest, // 目标左上角Y坐标 int nWidth, // 传输宽度 int nHeight, // 传输高度 HDC hdcSrc, // 源DC int nXSrc, // 源左上角X坐标 int nYSrc, // 源左上角Y坐标 DWORD dwRop // 光栅操作码 );
| 参数 | 值 | 说明 |
|---|---|---|
hdcDest |
hDc |
目标DC(窗口的屏幕DC) |
nXDest |
0 |
目标位置X坐标(窗口左上角) |
nYDest |
0 |
目标位置Y坐标(窗口左上角) |
nWidth |
width |
传输宽度(窗口客户区宽度) |
nHeight |
height |
传输高度(窗口客户区高度) |
hdcSrc |
hMem |
源DC(内存DC,包含绘制好的位图) |
nXSrc |
0 |
源位置X坐标(从位图左上角开始) |
nYSrc |
0 |
源位置Y坐标(从位图左上角开始) |
dwRop |
SRCCOPY |
光栅操作:直接复制 |
工作原理
1. 内存DC和屏幕DC的关系
// 之前已经创建了: HDC hDc = GetDC(hWnd); // 获取窗口的屏幕DC HDC hMem = ::CreateCompatibleDC(hDc); // 创建与屏幕兼容的内存DC HBITMAP hBmp = CreateDIBSection(...); // 创建位图 SelectObject(hMem, hBmp); // 将位图选入内存DC // 此时: // hMem(内存DC)中有一个位图,可以通过buffer指针直接修改 // hDc(屏幕DC)连接到窗口的显示区域
2. BitBlt执行的过程
内存DC (hMem) BitBlt复制 屏幕DC (hDc)
+--------------+ ----------> +--------------+
| 我们绘制的 | | 用户看到的 |
| 位图数据 | | 窗口内容 |
| (离屏缓冲) | | (屏幕上) |
+--------------+ +--------------+
^ |
| |
+--------------------------------------+
通过buffer指针修改位图数据
3. 具体步骤可视化
// 步骤1:在内存中绘制 unsigned char* rgba = (unsigned char*)buffer; memset(buffer, 0, width * height * 4); // 清空为黑色 memset(rgba + pitch * 10, 255, pitch); // 绘制白色横线 // 步骤2:BitBlt复制到屏幕 BitBlt(hDc, 0, 0, width, height, hMem, 0, 0, SRCCOPY); // 等价于: // 将内存DC中(0,0)开始、大小为(width,height)的区域 // 复制到屏幕DC的(0,0)位置
光栅操作码(Raster Operation Codes)
SRCCOPY 是最常用的操作码,表示直接复制源到目标。
其他常用操作码:
| 操作码 | 值 | 说明 |
|---|---|---|
SRCCOPY |
0x00CC0020 |
直接复制源到目标 |
SRCPAINT |
0x00EE0086 |
源OR目标 |
SRCAND |
0x008800C6 |
源AND目标 |
SRCINVERT |
0x00660046 |
源XOR目标 |
MERGECOPY |
0x00C000CA |
源AND模式 |
MERGEPAINT |
0x00BB0226 |
(NOT 源) OR 目标 |
BLACKNESS |
0x00000042 |
输出黑色 |
WHITENESS |
0x00FF0062 |
输出白色 |
// 不同的复制效果 BitBlt(hDc, 0, 0, width, height, hMem, 0, 0, SRCCOPY); // 正常复制 BitBlt(hDc, 0, 0, width, height, hMem, 0, 0, SRCINVERT); // 异或效果(反色) BitBlt(hDc, 0, 0, width, height, 0, 0, 0, BLACKNESS); // 填充黑色
BitBlt是实现双缓冲的关键:
while (true) {
// 1. 在内存中绘制(用户看不到)
DrawToMemoryBuffer(buffer); // 绘制到buffer
// 2. 一次性复制到屏幕(用户看到完整一帧)
BitBlt(hDc, 0, 0, width, height, hMem, 0, 0, SRCCOPY);
// 优点:避免闪烁,提高绘制性能
}
性能考虑
-
尽量一次性复制:减少BitBlt调用次数
-
只复制需要更新的区域:
// 只更新变化的部分 BitBlt(hDc, dirtyX, dirtyY, dirtyWidth, dirtyHeight, hMem, dirtyX, dirtyY, SRCCOPY); -
避免频繁的DC获取和释放:
// 错误:每次循环都获取释放DC // 正确:在循环外获取,最后释放
相关函数
-
StretchBlt():支持缩放的位块传输 -
TransparentBlt():支持透明色的位块传输 -
AlphaBlend():支持Alpha混合的位块传输
BitBlt是Windows图形编程的基础,理解它对于掌握Windows GDI编程至关重要。 -
#include <Windows.h>
//添加了 CALLBACK 调用约定
LRESULT CALLBACK windowProc(
HWND hWnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam
)
{
switch (uMsg) {
case WM_CREATE: {
break;
}
case WM_SIZE: {
break;
}
case WM_CLOSE:
{
// 询问用户是否真的要关闭
if (MessageBox(hWnd, "确定要关闭窗口吗?", "确认", MB_YESNO | MB_ICONQUESTION) == IDYES) {
DestroyWindow(hWnd);
}
break;
}
case WM_DESTROY:
PostQuitMessage(0);
default:
break;
}
return DefWindowProc(hWnd, uMsg, wParam, lParam); // 修正函数名
}
int WINAPI WinMain(
HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nShowCmd
)
{
//1、注册窗口类
/**
这里的 :: 表示
明确使用全局命名空间中的 WNDCLASSEXA
避免与其他命名空间中可能存在的同名类型冲突
namespace MyLibrary {
struct WNDCLASSEXA { // 自定义的同名类型
int myData;
};
}
void MyFunction() {
// 如果没有::,编译器会报错或选择错误的类型
// WNDCLASSEXA myVar; // 错误:MyLibrary::WNDCLASSEXA 还是 ::WNDCLASSEXA?
::WNDCLASSEXA winClass = { 0 }; // 明确使用全局的Windows API类型
MyLibrary::WNDCLASSEXA myClass; // 使用MyLibrary的类型
}
WNDCLASSEX 结构类似于 WNDCLASS 结构。 有两个区别。 WNDCLASSEX 包括 cbSize 成员,
该成员指定结构的大小,以及 hIconSm 成员,该成员包含与窗口类关联的小图标的句柄。
*/
::WNDCLASSEXA winClass = { 0 };
winClass.lpszClassName = "Raster";
winClass.cbSize = sizeof(::WNDCLASSEX);
winClass.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC | CS_DBLCLKS;
winClass.lpfnWndProc = windowProc;
winClass.hInstance = hInstance;
winClass.hIcon = 0;
winClass.hIconSm = 0;
winClass.hCursor = LoadCursor(NULL, IDC_ARROW); // 设置光标
winClass.hbrBackground = (HBRUSH)(BLACK_BRUSH);
winClass.lpszMenuName = NULL;
winClass.cbClsExtra = 0;
winClass.cbWndExtra = 0;
// 2、注册窗口类
if (!RegisterClassExA(&winClass)) {
MessageBox(NULL, "窗口类注册失败!", "错误", MB_ICONERROR);
return 1;
}
//3、创建窗口 通常是CreateWindow或CreateWindowEx的返回值
HWND hWnd = CreateWindowExA(
NULL,
"Raster",
"Raster",
WS_OVERLAPPEDWINDOW|WS_CLIPCHILDREN|WS_CLIPSIBLINGS, // 标准窗口样式(包含标题栏、边框等)
0,
0,
480,
320,
0,
0,
hInstance,
0
);
if (!hWnd) {
::MessageBoxA(NULL, "窗口创建失败!", "错误", MB_ICONERROR | MB_OK);
return 1;
}
// 4、显示窗口
/*UpdateWindow(hWnd);
ShowWindow(hWnd, SW_SHOW);*/
// 4、显示窗口
::ShowWindow(hWnd, nShowCmd);
::UpdateWindow(hWnd);
// 窗口 客户区的大小
RECT rt = { 0 };
GetClientRect(hWnd, &rt);
int width = rt.right - rt.left;
int height = rt.bottom - rt.top;
void* buffer = 0; // 指向图片中的数据
// DC 是windows 中绘制上下文对象
HDC hDc = GetDC(hWnd);
HDC hMem = ::CreateCompatibleDC(hDc);
// 创建一张图片
BITMAPINFO bmpInfo;
bmpInfo.bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
bmpInfo.bmiHeader.biWidth = width;
// bmpInfo.bmiHeader.biHeight = -height; // 负值表示从上到下的扫描顺序
bmpInfo.bmiHeader.biHeight = height;
bmpInfo.bmiHeader.biPlanes = 1;
bmpInfo.bmiHeader.biBitCount = 32;
bmpInfo.bmiHeader.biCompression = BI_RGB;
bmpInfo.bmiHeader.biSizeImage = 0;
bmpInfo.bmiHeader.biXPelsPerMeter = 0;
bmpInfo.bmiHeader.biYPelsPerMeter = 0;
bmpInfo.bmiHeader.biClrUsed = 0;
bmpInfo.bmiHeader.biClrImportant = 0;
HBITMAP hBmp = CreateDIBSection(hDc, &bmpInfo, DIB_RGB_COLORS, (void**)&buffer, 0, 0);
SelectObject(hMem, hBmp);
memset(buffer, 0, width * height * 4);
MSG msg = { 0 };
/*
// 5、消息循环(正确的版本)
MSG msg = { 0 };
BOOL bRet;
while ((bRet = ::GetMessageA(&msg, NULL, 0, 0)) != 0) {
if (bRet == -1) {
// 错误处理
break;
} else {
::TranslateMessage(&msg);
::DispatchMessageA(&msg);
}
}
// 或者使用 PeekMessage 版本(适合游戏等需要实时处理的程序)
while (true) {
if (::PeekMessageA(&msg, NULL, 0, 0, PM_REMOVE)) {
if (msg.message == WM_QUIT) {
break;
}
::TranslateMessage(&msg);
::DispatchMessageA(&msg);
} else {
// 这里可以添加游戏逻辑或其他处理
// Sleep(1); // 避免CPU占用过高
}
}
return (int)msg.wParam;
}
**/
// 或者使用 PeekMessage 版本(适合游戏等需要实时处理的程序)
while (true) {
if (
msg.message == WM_DESTROY ||
msg.message == WM_CLOSE ||
msg.message == WM_QUIT
) {
break;
};
if (PeekMessage(&msg, NULL, 0, 0, PM_REMOVE)) {
TranslateMessage(&msg);
DispatchMessage(&msg);
}
memset(buffer, 0, width* height * 4);
// 修改一部分
unsigned char* rgba = (unsigned char*)buffer;
int pitch = width * 4;
memset(rgba+pitch*10, 255, pitch);
BitBlt(hDc, 0, 0, width, height, hMem, 0, 0, SRCCOPY);
}
return 0;
}

浙公网安备 33010602011771号