简介:这份资源是面向 VC++ 初学者的 DirectDraw 2D 图形编程示例,演示如何在屏幕显示一张图片,适合想了解 DirectX 早期图形接口与底层绘图流程的开发者。工程完整覆盖 DirectDraw 初始化、合作级别设置、双缓冲交换链创建、图片加载与 Blt 绘制、Flip 切换显示以及资源释放等关键环节,代码结构清晰,便于对照学习。压缩包内共 21 个文件,以 h 头文件和 cpp 源文件为主,搭配 bmp 素材、rc 资源脚本、dsw/dsp 工程文件以及 ReadMe 使用说明,整体大小仅 1.31MB,轻量易用。已有 380 人浏览学习,内容聚焦于经典 DirectDraw 7 接口(IDirectDraw7)的调用方式,可帮助读者积累硬件加速 2D 渲染的基础经验,并为后续理解 Direct3D 或现代图形 API 提供铺垫。 "想用VC在屏幕上显示一张图片,很多人第一反应就是GDI的BitBlt,但在全屏游戏和需要高质量动画的场合,DirectDraw才是老牌王者。我用Visual C++ 6.0配DirectX 7 SDK,完整走一遍从初始化DirectDraw到把一张BMP图片显示到屏幕上的全过程,包括可直接编译的代码、原理讲解、常见坑位排查。这篇文章适合刚接触DirectDraw的初学者,也适合想快速捡起老代码干活的人。"
DirectDraw虽然早被Direct2D和GPU加速接口取代,但在老游戏源码、工控上位机、教学项目里仍旧大量存在。我前阵子接手一个老项目,界面逻辑全部跑在DirectDraw上,需求只是"把图片显示在屏幕上",但本质上却要打通一整条DDraw的初始化链路。折腾完之后我把整个流程拆成文章,帮你少走弯路。
1. 先搞清楚DirectDraw到底是怎么回事
1.1 DirectDraw的本质
DirectDraw是DirectX 1.0到7.0时代的2D图形接口,核心工作就是"管理显存表面"。你可以把"表面"理解成一张能放进显存、能被硬件加速操作的画布,这和GDI的DC完全不同。GDI画图走的是系统内存和设备上下文,一帧帧Blt到屏幕上;DirectDraw则是直接操作显卡上的表面缓冲区,再通过翻页(Flip)或块拷贝(Blt/BltFast)把内容显示出来。
对比一下两者,GDI画一帧800x600的图,CPU占用经常拉满;DirectDraw利用显存表面的切换,CPU只需要拷贝一次内容,剩下交给显卡。这也是为什么很多老游戏全屏跑起来丝滑流畅。
在工程层面,DirectDraw的核心API集中在ddraw.h和ddraw.lib里,常用的几个对象是:
- IDirectDraw7:全局对象,负责初始化、设置显示模式、创建表面。
- IDirectDrawSurface7:表面对象,主表面、后备表面、离屏表面都是它。
- IDirectDrawPalette:调色板对象,在8位色深模式时才必须用。
- IDirectDrawClipper:裁剪器,窗口模式下用来限定绘制范围。
1.2 什么场景还在用它
现在写新项目首要考虑的自然是D2D、GDI+或者OpenGL,但下面几类场景你还是不得不碰DirectDraw:
- 老游戏改造:大量2000年前后的国产游戏、外挂工具、游戏辅助代码都是DirectDraw写的。
- 教学实验:很多高校的《计算机图形学》实验题还保留着DirectDraw作业。
- 工控上位机:一些老设备厂商的演示程序,界面还在用DirectDraw做全屏显示。
- 性能敏感的老平台:Windows 2000/XP这些古董环境下,DirectDraw依然是最稳定的全屏2D方案。
明白这些背景之后,下面就直接进入实操。
2. 开工前的环境准备
2.1 VC版本怎么选
标题里明确写了VC,说明项目大概率用的是Visual C++ 6.0。不过我用过VC6、VS2003、VS2010分别编译过DirectDraw代码,老代码在VS2010以上也能编,只是需要手动处理一些头文件的兼容问题。
VC6的优点是与老SDK配合最默契,装上DirectX 7 SDK后几乎不需要额外配置。缺点是对C++标准支持差,写代码时容易踩语法坑。如果你的项目本身就是VC6工程,建议直接用VC6,省得后面一堆链接问题。如果是从零开始学,用VS2010+DirectX SDK(June 2010版本)反而更舒服,编辑器体验好得多。
2.2 DirectX SDK安装与工程配置
下载DirectX 7 SDK(或包含ddraw的DirectX 9 SDK)。安装后确认两个关键路径:
- 头文件目录:dxsdk\Include
- 库文件目录:dxsdk\Lib
在VC6里的配置路径是:菜单Tools -> Options -> Directories,分别在Include files和Library files里加上这两个路径。注意把SDK路径放在VC默认路径之前,否则编译时可能引用到旧版本头文件,造成结构体定义不一致。
如果是VS2010,直接在项目属性 -> VC++ 目录 -> 包含目录/库目录里添加即可。需要链接的库是ddraw.lib,如果你用到DirectInput或DirectSound,还需要相应的库文件。
2.3 运行库那点事
虽然DirectDraw主体是Win32 API调用,但VC编译出来的程序依然依赖VC运行库。VC6生成的文件可能需要msvcrt.dll或mfc42.dll,VS2010生成的可能需要msvcp100.dll。很多人遇到"找不到DLL"的问题,不一定是DDraw代码有问题,而是目标机器缺运行库。
解决办法有三种:一是把用到的DLL拷贝到exe同目录,二是做静态编译(VC6里Project Settings -> C/C++ -> Code Generation -> Use run-time library选Multithreaded),三是在目标机器装对应版本的VC运行库安装包。我的习惯是直接静态编译,省心,兼容性最好。
3. 显示图片的完整流程拆解
3.1 初始化DirectDraw对象
一切从DirectDrawCreateEx开始。这个函数是DirectDraw 7推荐的入口,对比老版本的DirectDrawCreate,它能拿到IDirectDraw7接口,功能和稳定性都更好。
LPDIRECTDRAW7 pDD = NULL; HRESULT hr = DirectDrawCreateEx(NULL, (LPVOID*)&pDD, IID_IDirectDraw7, NULL); if (FAILED(hr)) { // 初始化失败 return FALSE; }第一个参数固定传NULL,表示使用主显示设备。第三个参数必须是IID_IDirectDraw7,否则拿不到7.0版本的接口指针。
拿到对象后,紧接着设置协作级别。协作级别决定程序与Windows桌面以及其他程序的协作方式。要做全屏独占模式,必须用DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN,而且需要传入一个有效的窗口句柄,通常是你的主窗口句柄。
pDD->SetCooperativeLevel(hwnd, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN); pDD->SetDisplayMode(800, 600, 32, 0, 0);3.2 创建主表面和后备表面
显示模式设置好之后,创建主表面。主表面就是屏幕上正在显示的那个表面,在独占模式下,通常还创建后备表面,形成双缓冲结构。
DDSURFACEDESC2 ddsd; ZeroMemory(&ddsd, sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT; ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX; ddsd.dwBackBufferCount = 1; LPDIRECTDRAWSURFACE7 pPrimary = NULL; LPDIRECTDRAWSURFACE7 pBack = NULL; pDD->CreateSurface(&ddsd, &pPrimary, NULL);ddsd.dwBackBufferCount设为1,代表主表面附带一个后备表面,这是双缓冲的最基本结构。DDSCAPS_COMPLEX表示这是一个复合表面,必须配合DDSCAPS_FLIP使用。创建成功后,用GetAttachedSurface获取后备表面。
DDSCAPS2 caps; ZeroMemory(&caps, sizeof(caps)); caps.dwCaps = DDSCAPS_BACKBUFFER; pPrimary->GetAttachedSurface(&caps, &pBack);3.3 从BMP到DirectDraw表面
DirectDraw不直接认识BMP文件,需要先把BMP读成HBITMAP,再通过HDC把像素拷贝到DirectDraw表面。这里用LoadImage加载磁盘BMP文件,然后用GetObject获取位图尺寸。
HBITMAP hBitmap = (HBITMAP)LoadImage(NULL, "test.bmp", IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); BITMAP bmp; GetObject(hBitmap, sizeof(BITMAP), &bmp);接着创建一块离屏表面,尺寸和BMP一致。离屏表面的作用是当图片的中转站,先把BMP数据画上去,再从离屏表面拷贝到后备表面。
DDSURFACEDESC2 ddsdImage; ZeroMemory(&ddsdImage, sizeof(ddsdImage)); ddsdImage.dwSize = sizeof(ddsdImage); ddsdImage.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT; ddsdImage.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; ddsdImage.dwWidth = bmp.bmWidth; ddsdImage.dwHeight = bmp.bmHeight; LPDIRECTDRAWSURFACE7 pImage = NULL; pDD->CreateSurface(&ddsdImage, &pImage, NULL);然后通过GetDC拿到离屏表面的DC,创建一个兼容内存DC,把BMP选进去,用BitBlt完成像素复制。这一步的本质就是GDI的位图绘制,只是绘制目标从窗口DC换成了DirectDraw表面DC。
HDC hdcSurface; pImage->GetDC(&hdcSurface); HDC hdcMem = CreateCompatibleDC(hdcSurface); HGDIOBJ oldBmp = SelectObject(hdcMem, hBitmap); BitBlt(hdcSurface, 0, 0, bmp.bmWidth, bmp.bmHeight, hdcMem, 0, 0, SRCCOPY); SelectObject(hdcMem, oldBmp); DeleteDC(hdcMem); pImage->ReleaseDC(hdcSurface); DeleteObject(hBitmap);3.4 绘制与翻页
图片进入离屏表面后,每次刷新需要两步:第一步把图片从离屏表面拷贝到后备表面,第二步让主表面和后备表面交换(Flip)。
// 清理后备表面为黑色 DDBLTFX fx; ZeroMemory(&fx, sizeof(fx)); fx.dwSize = sizeof(fx); fx.dwFillColor = 0; pBack->Blt(NULL, NULL, NULL, DDBLT_COLORFILL | DDBLT_WAIT, &fx); // 把图片画到指定位置 pBack->BltFast(100, 100, pImage, NULL, DDBLTFAST_WAIT); // 翻转前后台 pPrimary->Flip(NULL, DDFLIP_WAIT);BltFast比Blt更快,因为它不做拉伸缩放和复杂的颜色转换,只是硬件块拷贝。如果只是显示原尺寸图片,我建议直接用BltFast;如果需要缩放,就得改用Blt并重设目标矩形。
4. 可直接编译的完整示例代码
4.1 代码总体结构
整个程序围绕三个模块组织:初始化(InitDirectDraw)、加载图片(LoadBitmapToSurface)、渲染循环(Render/Loop)。我把最简代码整合成了一份,方便直接当作模板使用。
#include <windows.h> #include <ddraw.h> #include <stdio.h> #pragma comment(lib, "ddraw.lib") LPDIRECTDRAW7 g_pDD = NULL; LPDIRECTDRAWSURFACE7 g_pPrimary = NULL; LPDIRECTDRAWSURFACE7 g_pBack = NULL; LPDIRECTDRAWSURFACE7 g_pImage = NULL; BOOL InitDirectDraw(HWND hwnd); BOOL LoadBitmapToSurface(const char* szFile); void RenderFrame(int x, int y); void Cleanup();4.2 关键实现
InitDirectDraw的核心逻辑就是第3节说的初始化三步曲。需要注意顺序:协作级别必须在设置显示模式之前,创建表面必须在设置显示模式之后。
BOOL InitDirectDraw(HWND hwnd) { if (FAILED(DirectDrawCreateEx(NULL, (LPVOID*)&g_pDD, IID_IDirectDraw7, NULL))) return FALSE; if (FAILED(g_pDD->SetCooperativeLevel(hwnd, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN))) return FALSE; if (FAILED(g_pDD->SetDisplayMode(800, 600, 32, 0, 0))) return FALSE; DDSURFACEDESC2 ddsd; ZeroMemory(&ddsd, sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_CAPS | DDSD_BACKBUFFERCOUNT; ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE | DDSCAPS_FLIP | DDSCAPS_COMPLEX; ddsd.dwBackBufferCount = 1; if (FAILED(g_pDD->CreateSurface(&ddsd, &g_pPrimary, NULL))) return FALSE; DDSCAPS2 caps; ZeroMemory(&caps, sizeof(caps)); caps.dwCaps = DDSCAPS_BACKBUFFER; if (FAILED(g_pPrimary->GetAttachedSurface(&caps, &g_pBack))) return FALSE; return TRUE; }LoadBitmapToSurface负责把BMP文件加载成DirectDraw离屏表面。它不关心文件里的颜色格式,统一交给GDI去做像素转换,这样在8位、16位、32位显示模式下都能得到正确的表面数据。
BOOL LoadBitmapToSurface(const char* szFile) { HBITMAP hBitmap = (HBITMAP)LoadImage(NULL, szFile, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE); if (hBitmap == NULL) return FALSE; BITMAP bmp; GetObject(hBitmap, sizeof(BITMAP), &bmp); DDSURFACEDESC2 ddsd; ZeroMemory(&ddsd, sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_CAPS | DDSD_WIDTH | DDSD_HEIGHT; ddsd.ddsCaps.dwCaps = DDSCAPS_OFFSCREENPLAIN; ddsd.dwWidth = bmp.bmWidth; ddsd.dwHeight = bmp.bmHeight; if (FAILED(g_pDD->CreateSurface(&ddsd, &g_pImage, NULL))) { DeleteObject(hBitmap); return FALSE; } HDC hdcSurface; g_pImage->GetDC(&hdcSurface); HDC hdcMem = CreateCompatibleDC(hdcSurface); HGDIOBJ oldBmp = SelectObject(hdcMem, hBitmap); BitBlt(hdcSurface, 0, 0, bmp.bmWidth, bmp.bmHeight, hdcMem, 0, 0, SRCCOPY); SelectObject(hdcMem, oldBmp); DeleteDC(hdcMem); g_pImage->ReleaseDC(hdcSurface); DeleteObject(hBitmap); return TRUE; }RenderFrame对应第3节说的绘制与翻页。这里我把背景清理和图片绘制分开写,一个是便于看黑底效果,一个是方便后续扩展多个图片叠加显示。
void RenderFrame(int x, int y) { DDBLTFX fx; ZeroMemory(&fx, sizeof(fx)); fx.dwSize = sizeof(fx); fx.dwFillColor = 0; g_pBack->Blt(NULL, NULL, NULL, DDBLT_COLORFILL | DDBLT_WAIT, &fx); if (g_pImage) { g_pBack->BltFast(x, y, g_pImage, NULL, DDBLTFAST_WAIT); } g_pPrimary->Flip(NULL, DDFLIP_WAIT); }4.3 窗口模式还是全屏模式
上面的代码是全屏独占模式。如果你想在窗口里显示图片,需要做几处调整:SetCooperativeLevel改为DDSCL_NORMAL,不调用SetDisplayMode,主表面创建用DDSD_CAPS | DDSCAPS_PRIMARYSURFACE,不需要后备表面,也不用Flip,直接用Blt把图片画到主表面。
// 窗口模式初始化 pDD->SetCooperativeLevel(hwnd, DDSCL_NORMAL); DDSURFACEDESC2 ddsd; ZeroMemory(&ddsd, sizeof(ddsd)); ddsd.dwSize = sizeof(ddsd); ddsd.dwFlags = DDSD_CAPS; ddsd.ddsCaps.dwCaps = DDSCAPS_PRIMARYSURFACE; pDD->CreateSurface(&ddsd, &pPrimary, NULL); // 窗口模式下用Blt绘制,目标矩形是窗口客户区的位置 RECT rcDest; GetClientRect(hwnd, &rcDest); pPrimary->Blt(&rcDest, pImage, NULL, DDBLT_WAIT, NULL);窗口模式省去了翻转步骤,但渲染效率明显下降,频繁刷新时可能闪烁。全屏模式才是DirectDraw发挥硬件性能的主场。
5. 实战中遇到的坑与解决方案
5.1 编译链接错误
最常见的是"ddraw.h: No such file or directory",解决方式就是确认DirectX SDK路径配到了编译器Include目录前面。另一个高频错误是链接时找不到DirectDrawCreateEx符号,原因是忘了加#pragma comment(lib, "ddraw.lib"),或者在VC6里没有把ddraw.lib加到Link选项。
还有一个隐蔽问题:VC6自带的头文件里也有一份老版本的ddraw.h,如果SDK头文件目录没有排在系统默认目录之前,编译时可能用了老版本的头文件,导致DirectDrawCreateEx未声明或者IID_IDirectDraw7未定义。遇到这种问题,第一反应就是检查头文件包含顺序。
5.2 表面丢失(DDERR_SURFACELOST)
当显示模式改变、屏幕分辨率切换、或者按Alt+Tab切换窗口后,DirectDraw表面可能丢失(Surface Lost)。这时所有表面操作都会返回DDERR_SURFACELOST,程序不能继续绘制,必须先恢复表面。
if (pPrimary->IsLost() == DDERR_SURFACELOST) { pPrimary->Restore(); // 表面恢复后,离屏表面的内容也会丢失,需要重新加载图片数据 LoadBitmapToSurface("test.bmp"); }这里有个容易疏忽的点:Restore只恢复显存,不恢复表面内容。表面里的图片数据在显存丢失时已经没了,必须重新把图片数据填充回去。
5.3 图片花屏、有条纹
如果把BMP加载到16位色深模式或者8位色深模式,直接BitBlt通常没问题,问题往往出在CreateSurface时没有设置匹配的像素格式。DDSURFACEDESC2的ddpfPixelFormat字段里必须指定表面像素格式,或者干脆用GetDC+BitBlt让GDI去处理转换,这是最保险的做法。
有条纹的另一种可能是图片宽高没有按4字节对齐。DirectDraw要求表面宽度尽量对齐到DWORD,如果你的BMP宽度是奇数像素,建议创建表面时做一下宽度对齐处理,否则颗粒感很严重。我自己写代码时会在创建离屏表面时把宽度向上对齐到4的倍数,然后再把有效区域用Rectangle限制在BMP实际尺寸内。
5.4 常见错误码速查表
| 错误码 | 含义 | 处理办法 |
|---|---|---|
| DDERR_SURFACELOST | 表面丢失 | 调用Restore并重新填充内容 |
| DDERR_EXCLUSIVEMODEALREADYSET | 独占模式已设置 | 检查是否重复设置协作级别 |
| DDERR_INVALIDCAPS | 表面能力标志错误 | 检查ddsCaps标志是否包含必要的DDSCAPS |
| DDERR_NOFLIPHW | 硬件不支持翻转 | 检查是否真的是主表面,或者降低显示模式 |
| DDERR_INVALIDRECT | 矩形无效 | 检查目标矩形是否越界 |
| DDERR_OUTOFVIDEOMEMORY | 显存不足 | 使用系统内存表面,或减小图片尺寸 |
5.5 从Blt切到BltFast后位置偏移
很多初学者第一次用BltFast时,发现图片显示位置和预期不一致。BltFast的目标坐标是相对后备表面的左上角,而Blt的lpDestRect可以指定任意矩形。如果你在BltFast的第二个参数里传了非NULL矩形,那个矩形是源矩形,不是目标矩形。目标位置直接用x、y参数指定,源矩形传NULL代表整张图片。
6. 一些个人经验
这轮代码折腾下来,给我最大的感受是:DirectDraw的核心概念其实非常清晰,它把显存的管理、表面的创建、像素的搬移全部交给了开发者,自由度很高,但每个细节都要自己负责。也正是因为这种自由,代码出了问题就很难排查,尤其是表面丢失、像素格式不匹配这些隐性问题,往往只能在实机上试。
我做这个Demo时,通常还会顺手加一个按键响应,比如按空格键切换图片显示位置,按Esc退出全屏。这样在调试时能快速验证BltFast的坐标是否有效,也方便展示表面丢失后恢复的效果。完整的消息循环代码就不过度展开了,Win32普通窗口的写法即可,只是在WM_KEYDOWN里处理按键,在WM_DESTROY里调用Cleanup释放所有接口。
最后再分享一个硬件兼容性的经验:DirectDraw在Windows XP上表现最稳定,在Windows 10/11上全屏独占模式偶尔出现黑屏或显示异常,这多半是系统对全屏优化的干扰。遇到这种情况,可以右键exe -> 属性 -> 兼容性 -> 勾选"禁用全屏优化",或者把系统设置里的"图形首选项"改为性能模式。如果你的项目只是验证算法,直接改用窗口模式或者改用Direct2D也能绕开这个坑,但那就不是今天这篇的主角了。
本文还有配套的精品资源,点击获取