2026/9/2 18:58:42

DirectDraw图形编程入门:用VC与DirectDraw7显示一张BMP图片

DirectDraw图形编程入门:用VC与DirectDraw7显示一张BMP图片 简介这是一份面向VC初学者的DirectDraw图形编程示例演示在屏幕窗口中加载并显示BMP图片的完整实现。DirectDraw是DirectX中的经典2D图形加速接口曾广泛用于游戏开发与高性能图形应用示例采用IDirectDraw7接口围绕对象创建、合作级别设置、表面建立、图像拷贝、缓冲区翻转以及资源释放等关键环节组织代码能够让读者直观看到从初始化到最终显像的完整工程脉络。压缩包共21个文件主要包含6个h头文件、4个cpp源文件以及rc资源脚本、ico图标、bmp位图、ReadMe说明文档等整体仅1.31MB便于下载后直接阅读和编译调试。项目基于Visual C对话框与文档视结构搭建主程序、对话框逻辑、全局声明与框架代码分层清晰适合课程设计或自学查漏补缺。已有380人学习下载对于想了解DirectX早期图形编程原理或需要完成类似实验任务的开发者是一份小巧而完整的参考范例。 搞过老一代图形编程的朋友对“DirectDraw”这六个字母应该都不陌生。当年在VC 6.0时代想在窗口里显示一张图片、做个小游戏或者播放一段动画很多人第一反应就是开一个DirectDraw设备创建表面然后往上贴图。现在虽然Direct3D 9、11甚至12已经替代了这套老接口但从学习角度讲DirectDraw依然是理解“显存表面”、“页面翻转”、“离屏表面”这些2D图形核心概念的绝佳教材。这篇笔记就从一个最简单的需求出发——用VC和DirectDraw在屏幕上显示一张图片。我会先讲清楚整套流程的设计思路然后给出可以直接编译运行的完整示例代码再逐段解释关键API的使用最后把我踩过的几个坑和排查方法一并整理出来。无论你是想复习老技术还是刚接触Win32图形编程想了解底层原理这篇文章都很适合。1. 为什么现在还要翻出DirectDraw一套经典流程的价值很多年轻的朋友可能一上来就会问显示一张图片用GDI的BitBlt不行吗用CreateCompatibleDC加StretchBlt几行代码就搞定了为什么非要绕一圈去用DirectDraw这个问题问得挺好。单看“显示图片”这个需求GDI确实更简单。但DirectDraw的价值不在于“简单”而在于它给你展示了另一套图形处理路径——直接面向显存表面的操作思路。用GDI画图系统要经过窗口管理器、GDI渲染队列层层转换而DirectDraw的思路是把你需要的东西提前放到一个“表面”里然后通过硬件快速拷贝或者页面翻转直接显示。这个“表面”的概念后来在D3D9、D3D11里变成Texture、RenderTarget本质上是一脉相承的。1.1 DirectDraw到底是什么DirectDraw是DirectX 1.0到7.0时代的2D图形组件它的核心抽象就是“表面”。表面可以理解成一块显存或者系统内存里的矩形缓冲区里面保存着像素数据。DirectDraw允许你直接读写这个表面也可以在表面之间做快速复制Blt还可以把一个后备表面通过翻转操作直接变成可见内容。这套东西在当时的游戏开发里非常流行像《红警2》早期版本、《仙剑奇侠传》的某些界面底层都能看到DirectDraw的影子。后来微软在Windows Vista引入WDDM驱动模型之后DirectDraw的硬件加速能力基本被移除了现在的DirectDraw调用大都是在软件模拟层上执行。但API接口保持兼容所以老代码依然能跑学习用途依然成立。1.2 一套完整的图片显示流程怎么拆把一个“显示图片”的需求拆开DirectDraw的做法其实就四步创建DirectDraw对象并设置协作级别、显示模式。创建主表面需要的话再创建一个后备表面形成翻页结构。把图片文件加载到一个离屏表面里。在程序主循环里把离屏表面内容贴到后备表面再执行翻转。这种“离屏表面存素材、后备表面做合成、主表面负责显示”的三层设计是DirectDraw的核心思路。今天读起来仍然很有价值因为它天然解决了“闪烁”问题你永远不直接在可见表面上画东西而是先在不可见的表面里画好一整帧再一次性切换显示。1.3 这个示例适合谁如果你满足下面任何一种情况这个示例值得你亲手敲一遍正在学习Win32程序设计想知道除了GDI之外还有什么绘制方式。想读老游戏源码但被DirectDraw API的基本概念卡住。想理解现代图形API里“表面/纹理/交换链”这些概念的源头。DirectDraw的API风格相对直白不像D3D11那样动辄一堆管线状态、Shader绑定。它更接近“向显存画图”的直觉适合作为你从GDI走向现代图形API之间的过渡桥。2. 核心对象和初始化先把DD对象“伺候”好在DirectDraw的世界里几乎所有东西都围绕“接口”展开。那个年代COM接口还非常流行所以你会看到一大堆LPDIRECTDRAW7、LPDIRECTDRAWSURFACE7这样的指针类型。别被这些长名字吓到它们本质上是“一组函数指针的集合”你只需要记住每个接口负责什么就行。2.1 DirectDrawCreateEx与DirectDraw7接口的选择创建DirectDraw对象有老、新两个函数老的DirectDrawCreate创建的是IDirectDraw接口只支持到DirectDraw 5的功能新的DirectDrawCreateEx则可以指定任意版本的接口比如IID_IDirectDraw7。实际开发中我建议直接用DirectDrawCreateEx创建DirectDraw7接口。原因很简单DirectDraw7是这套API的最终版本功能最全而且性能接口的处理也更规范。老接口虽然能用但官方早就不推荐了没必要在自己代码里保留一个落后版本。注意使用DirectDrawCreateEx时记得在工程设置里链接dxguid.lib否则IID_IDirectDraw7这个GUID会报链接错误。用VC 6那会儿把dxguid.lib ddraw.lib加进“Project Settings - Link - Object/library modules”里就行用新版Visual Studio则是在“链接器 - 输入 - 附加依赖项”里配置。2.2 协作级别与显示模式全屏独占还是窗口化创建完DirectDraw对象之后第一步永远是SetCooperativeLevel它告诉系统你的程序准备怎么使用显示设备。设置协作级别之前所有其他调用都可能失败所以这步别省。最经典的模式是DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN也就是全屏独占。这种模式下你可以调用SetDisplayMode任意切换屏幕分辨率、色深并且能够使用页面翻转功能。页面翻转是DirectDraw避免闪烁的利器——你先把内容画到后备表面上然后通过Flip指令把后备表面和主表面交换显示器下一个刷新周期就显示新画面了。如果你想在普通窗口里显示图片协作级别要用DDSCL_NORMAL同时不能调用SetDisplayMode也不能用Flip只能用Blt或者BltFast把内容直接传到主表面。窗口模式下主表面自动与窗口客户区关联但这种模式下主表面没有翻页结构画面闪烁需要自己想办法缓解。下面是一张对比表格方便你快速理解两种模式的区别协作级别显示模式表面结构适用场景DDSCL_EXCLUSIVE|DDSCL_FULLSCREEN可调用SetDisplayMode主表面后备表面支持Flip游戏、全屏演示DDSCL_NORMAL不可调用SetDisplayMode仅主表面可Blt普通窗口显示2.3 表面体系主表面、后备表面、离屏表面DirectDraw表面分很多种你只需要搞清楚三个核心概念主表面直接对应屏幕可见内容用DDSD_CAPS加DDSCAPS_PRIMARYSURFACE标志创建。窗口模式下它映射到窗口客户区全屏模式下它映射整个屏幕。后备表面只在全屏翻页结构中存在主表面创建时设置DDSCAPS_FLIP | DDSCAPS_COMPLEX和DDSCAPS_BACKBUFFERCOUNT1然后用GetAttachedSurface获取。它和主表面构成一个翻转环。离屏表面不与屏幕直接关联的普通表面用DDSCAPS_OFFSCREENPLAIN创建专门用来存放图片、精灵等静态素材。理解这三者的关系你会突然明白DirectDraw所有绘制操作的本质把图片从离屏表面拷贝到后备表面然后翻转再拷贝再翻转。如此往复就形成了连续的画面。3. 实操示例从0到1显示一张BMP理论铺垫够了直接进入正题。下面这个例子是用Win32 DirectDraw7实现的全屏显示图片程序画面中央会显示一张pic.bmp图片按Esc键或关闭窗口退出。代码我尽量精简但每一部分都保留了完整逻辑。3.1 工程配置头文件库文件链接在写代码之前先把环境准备好。Visual Studio下新建一个Win32项目把ddraw.h头文件包含进来并在链接器设置里加上ddraw.lib和dxguid.lib这两个库文件。如果你的开发环境是VC 6.0操作路径是“Project - Settings - Link - Object/library modules”然后在输入框里追加这两个库名。用新版Visual Studio则在项目属性里找“链接器 - 输入 - 附加依赖项”。在较新的Windows SDK里ddraw.h和ddraw.lib仍然存在不需要额外安装DirectX SDK。我实测了一下Windows 10/11自带的SDK就能编译通过系统System32目录下也始终带着ddraw.dll运行完全没问题。这一点对老技术爱好者来说相当友好——不用折腾直接就能跑。3.2 初始化窗口和DD对象在主函数里我们首先创建一个普通的Win32窗口。因为用的是全屏独占模式窗口风格直接设成WS_POPUP没有标题栏和边框显示的时候铺满屏幕。窗口的尺寸倒不需要和屏幕分辨率一致因为SetDisplayMode会把整个显示模式切掉屏幕最终分辨率以SetDisplayMode为准。创建窗口后关键代码登场#include windows.h #include ddraw.h LPDIRECTDRAW7 g_pDD NULL; LPDIRECTDRAWSURFACE7 g_pPrimary NULL; LPDIRECTDRAWSURFACE7 g_pBack NULL; LPDIRECTDRAWSURFACE7 g_pBmp NULL; const int SCR_W 800; const int SCR_H 600; BOOL InitDirectDraw(HWND hwnd) { // 1. 创建DirectDraw7对象 HRESULT hr DirectDrawCreateEx(NULL, (void**)g_pDD, IID_IDirectDraw7, NULL); if (FAILED(hr)) return FALSE; // 2. 设置协作级别为全屏独占 hr g_pDD-SetCooperativeLevel(hwnd, DDSCL_EXCLUSIVE | DDSCL_FULLSCREEN); if (FAILED(hr)) return FALSE; // 3. 设置显示模式 hr g_pDD-SetDisplayMode(SCR_W, SCR_H, 32, 0, 0); if (FAILED(hr)) return FALSE; // 4. 创建主表面并附带一个后备表面 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; hr g_pDD-CreateSurface(ddsd, g_pPrimary, NULL); if (FAILED(hr)) return FALSE; // 5. 获取后备表面 DDSCAPS2 ddscaps; ZeroMemory(ddscaps, sizeof(ddscaps)); ddscaps.dwCaps DDSCAPS_BACKBUFFER; hr g_pPrimary-GetAttachedSurface(ddscaps, g_pBack); if (FAILED(hr)) return FALSE; return TRUE; }这里有两个值得留意的细节。第一DirectDrawCreateEx的第一个参数设为NULL表示使用当前主显示器多显示器环境下这个空值也够用除非你要针对特定显示器做开发。第二创建主表面时dwFlags必须包含DDSD_BACKBUFFERCOUNT并且把后备表面数量设为1否则GetAttachedSurface拿不到后备表面。3.3 加载图片到离屏表面图片加载的方式有很多种这里我用最直接的方法先用LoadImage从文件读入BMP再用GetDC获取离屏表面的HDC最后用GDI的BitBlt把位图内容拷贝到DirectDraw表面。这种做法的好处是代码量小不需要逐像素操作而且对这种一次性加载的静态图片来说性能足够。BOOL LoadBitmapToSurface(LPCTSTR lpszFileName) { // 1. 用LoadImage读取BMP文件 HBITMAP hBitmap (HBITMAP)LoadImage(NULL, lpszFileName, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE | LR_CREATEDIBSECTION); if (hBitmap NULL) return FALSE; // 2. 获取位图尺寸 BITMAP bm; GetObject(hBitmap, sizeof(bm), bm); // 3. 创建与位图等大的离屏表面 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 bm.bmWidth; ddsd.dwHeight bm.bmHeight; HRESULT hr g_pDD-CreateSurface(ddsd, g_pBmp, NULL); if (FAILED(hr)) { DeleteObject(hBitmap); return FALSE; } // 4. 用GDI BitBlt把位图拷到离屏表面 HDC hdcMem CreateCompatibleDC(NULL); HGDIOBJ oldBmp SelectObject(hdcMem, hBitmap); HDC hdcSurface NULL; g_pBmp-GetDC(hdcSurface); BitBlt(hdcSurface, 0, 0, bm.bmWidth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY); g_pBmp-ReleaseDC(hdcSurface); SelectObject(hdcMem, oldBmp); DeleteDC(hdcMem); DeleteObject(hBitmap); return TRUE; }这段代码里最需要留意的是LR_CREATEDIBSECTION标志。如果不加这个标志LoadImage返回的可能是设备相关位图GetObject拿到的BITMAP结构里点距信息会不准确而且BitBlt拷贝时也可能出现颜色错乱。加了LR_CREATEDIBSECTION以后我们会拿到一个独立于设备的DIB段像素格式和尺寸都相对可控拷到表面后颜色表现更稳定。另一个容易被忽略的点是离屏表面的宽高和原图宽高不一定需要完全一致。如果图片尺寸很大可以创建更大的表面然后用BltFast的RECT参数只贴出一部分如果图片尺寸较小就按原尺寸创建。这里直接按位图尺寸创建逻辑最清晰。3.4 主循环贴图与翻页窗口初始化完成、图片也加载到离屏表面之后进入最关键的主循环。这个循环做的事情只有两件把离屏表面的图片内容贴到后备表面然后调用Flip让后备表面内容显示到屏幕上。MSG msg; BOOL bDone FALSE; while (!bDone) { while (PeekMessage(msg, NULL, 0, 0, PM_REMOVE)) { if (msg.message WM_QUIT) { bDone TRUE; break; } TranslateMessage(msg); DispatchMessage(msg); } if (bDone) break; // 清空后备表面为黑色避免上一帧残留 DDBLTFX ddbltfx; ZeroMemory(ddbltfx, sizeof(ddbltfx)); ddbltfx.dwSize sizeof(ddbltfx); ddbltfx.dwFillColor 0; // 黑色 g_pBack-Blt(NULL, NULL, NULL, DDBLT_WAIT | DDBLT_COLORFILL, ddbltfx); // 把图片从离屏表面贴到后备表面中央 int nX (SCR_W - bmWidth) / 2; int nY (SCR_H - bmHeight) / 2; RECT rcDest { nX, nY, nX bmWidth, nY bmHeight }; g_pBack-Blt(rcDest, g_pBmp, NULL, DDBLT_WAIT, NULL); // 翻页后备表面变成可见表面 g_pPrimary-Flip(NULL, DDFLIP_WAIT); }这里我特意在每次贴图之前先对整个后备表面做了一次COLORFILL填充黑色这样做是为了防止图片尺寸小于屏幕时边缘残留上一帧的脏数据。如果你希望图片铺满全屏也可以把Blt的目标矩形直接设为整个屏幕区域但那样会拉伸图片除非图片分辨率刚好等于屏幕分辨率否则别这么做。Flip(NULL, DDFLIP_WAIT)是这套流程里最核心的一步。它把主表面和后备表面互换整个操作在显示器的垂直回扫期间完成所以画面不会出现撕裂和闪烁。DDFLIP_WAIT标志表示等待翻转完成如果画面更新速度要求极高也可以用DDFLIP_DONOTWAIT但初学者老老实实等待就好。这段循环里我假设bmWidth、bmHeight是全局变量LoadBitmapToSurface函数里从BITMAP结构赋值得到。为了代码简洁我在这里直接用变量名表示实际代码里你需要在全局区维护这两个变量。3.5 资源清理的正确顺序程序退出时DirectDraw资源的释放顺序是有讲究的。原则很简单先释放子对象再释放父对象。具体到这个示例里顺序是先释放离屏表面g_pBmp再释放后备表面g_pBack再释放主表面g_pPrimary最后释放DirectDraw对象g_pDD。if (g_pBmp) { g_pBmp-Release(); g_pBmp NULL; } if (g_pBack) { g_pBack-Release(); g_pBack NULL; } if (g_pPrimary) { g_pPrimary-Release(); g_pPrimary NULL; } if (g_pDD) { g_pDD-Release(); g_pDD NULL; }有一个细节很容易踩坑后备表面g_pBack是通过g_pPrimary-GetAttachedSurface拿到的它实际上是主表面附属的引用不是独立创建的新对象。理论上你只Release主表面后后备表面会自动释放但为了保险起见显式地把它也Release掉没有任何问题。不过一定要先释放后备再释放主表面顺序反了可能出现未定义行为严重时进程退出会崩。4. 踩坑实录常见问题与排查技巧写DirectDraw代码不难调起来才是真正费时间的地方。这部分我把这些年帮人排查时遇到的高频问题整理成一个速查表再展开说几个“文档里不会写”的实践心得。4.1 问题速查表现象常见原因排查与解决办法编译时找不到ddraw.h未包含Windows SDK或老DX SDKVS里安装“桌面开发”工作负载Windows SDK自带该头文件链接时IID_IDirectDraw7未定义缺少dxguid.lib在链接器附加依赖项中加入dxguid.lib窗口一闪而过WM_PAINT处理不当或消息循环写错确认主循环里没有把DispatchMessage漏掉窗口过程别直接PostQuitMessage黑屏无内容没有设置协作级别就调用其他API按顺序初始化CreateEx - SetCooperativeLevel - SetDisplayMode - CreateSurface图片显示出来颜色不对显示模式色深和位图色深不一致统一用32位色深或把位图转成与显示模式一致的格式画面剧烈闪烁直接把图贴到了主表面而不是后备表面全屏独占模式下一定要用后备表面Flip别用主表面直接Blt拖动窗口时画面不刷新窗口模式没有处理WM_PAINT窗口模式下需要在WM_PAINT里重新执行贴图逻辑程序退出时崩溃释放顺序错误严格按离屏 - 后备 - 主表面 - DD对象的顺序释放4.2 几个容易忽略的细节先说颜色格式问题。DirectDraw表面里的像素格式取决于SetDisplayMode设置的色深。如果显示模式是16位色表面里的像素可能按RGB565排列如果位图文件本身是24位BMP直接BitBlt过去GDI会自己处理转换。但如果你后续要做像素级操作比如逐像素去色、混合就一定要搞清楚表面的像素格式否则算出来的颜色完全不对。排查方法很简单调用GetSurfaceDesc查看表面描述里的ddpfPixelFormat结构里面会有dwRBitMask、dwGBitMask等掩码信息。再说图片加载失败的隐蔽原因。LoadImage如果找不到文件不会报错崩溃只会在返回NULL很多人第一反应是代码错了其实检查一下工作路径就明白了。在Visual Studio里调试运行工作目录默认是$(ProjectDir)也就是工程文件所在目录。所以pic.bmp要放在工程目录下或者你直接写绝对路径比如D:\\test\\pic.bmp。我建议调试阶段用绝对路径跑通以后再改成相对路径省得被这种小问题耽误时间。还有一点是关于DDBLT_WAIT和DDBLTFAST_WAIT的选择。这个示例里Blt用的是DDBLT_WAIT它自带裁剪功能目标矩形可以超出屏幕边界而不报错。BltFast则要求源矩形和目标矩形都在合理范围内性能更快但约束也多。初学者建议先用Blt等需要优化性能再切到BltFast。最后说说现代的兼容性。Windows Vista之后DirectDraw的硬件加速被禁用所有操作都走软件模拟所以性能肯定没法和当年真硬件加速比。但做教学示例、研究老代码完全够用。我实测过在Windows 10/11上跑全屏800x600的翻转显示CPU占用不高画面稳定不闪。4.3 这套流程还能怎么扩展一个图像显示示例只是个起点顺着这套DirectDraw流程继续往下挖你可以做很多事情把离屏表面换成多个子图实现精灵动画系统就是老游戏里人物行走、子弹飞行的基础。在贴图之前对表面执行Lock和Unlock直接修改像素缓冲区实现淡入淡出、颜色变换等滤镜效果。用Blt的源矩形参数做局部贴图轻松实现地图瓦片渲染。结合GetTickCount控制每帧间隔做一个基本的帧率控制循环。我自己的体会是DirectDraw这套接口虽然技术上已经过时但它的“表面翻转”思想在现代图形API里依然是核心。D3D11的SwapChain本质上就是升级版的主表面加后备表面Vulkan的Present Queue也承担着类似Flip的工作。把DirectDraw的示例写透你再去看现代图形引擎的框架结构会感觉很多概念一下子通了。如果你在本地编译运行这个示例时碰到任何我上面没写到的问题建议先从初始化顺序入手检查——DirectDraw的API调用顺序极其敏感绝大多数“黑屏”、“无效调用”都和初始化顺序有关。那次我帮朋友调一个老项目图片闪烁问题最后查到原因居然是协作级别里漏了DDSCL_EXCLUSIVE标志导致翻转表面根本创建不出来。这种问题看代码很难发现最好的办法就是按我列出的初始化顺序逐条核对一步都不能错。本文还有配套的精品资源点击获取