转载自:https://www.fawdlstty.com/archives/396.html,如有侵权,请联系我进行删除。

1 GDI与GDI+的区别

GDI是Graphics Device Interface的缩写,含义是图形设备接口,它的主要任务是负责系统与绘图程序之间的信息交换,处理所有Windows程序的图形输出。

GDI+是在GDI基础上提供的一层更高级的图像绘制抽象接口,语义更明确调用更方便。

它们都支持向图片对象或者窗口上输出图形。在窗口上绘图时它们都使用窗口提供的HDC句柄实现绘制;在图片对象绘制图像时,GDI+支持直接传入图片对象实现对图片的绘制,GDI需要先创建一个与图片兼容的HDC,再将HDC与被绘制图片进行绑定,然后才能在图片上进行绘制。

它们在用法上相似,区别主要有以下几个方面:

  • GDI不支持透明图片处理(AlphaBlend只能混合颜色,透明得由第三方库支持)
  • GDI不支持反锯齿(对于图片绘制线条、图像或拉伸等处理时,可能出现白色锯齿形状图像,影响美观)
  • GDI对于图片颜色处理具有很大优势,GDI+相比之下处理速度很慢
  • GDI是以C的接口形式提供接口,GDI+是以C艹和托管类的方式提供接口
  • 使用GDI+的程序在初始化后、程序关闭前需调用GDI+初始化、释放的代码
  • 从层次结构上来说,GDI+更好用

没有绝对的好与坏,可以根据需求来决定使用GDI或GDI+。

2 GDI+的使用

使用GDI+需要首先包含头文件

#include <Gdiplus.h>
#pragma comment(lib, "gdiplus.lib")
using namespace Gdiplus;

在第一次使用GDI+对象前,需使用以下代码进行初始化

ULONG_PTR gdiplusToken; // 这个变量需要保存下来
GdiplusStartupInput gdiplusStartupInput;
GdiplusStartup(&gdiplusToken, &gdiplusStartupInput, NULL);

在程序退出之前,需调用以下代码进行对象释放

GdiplusShutdown(gdiplusToken);

首先是在窗口上绘制一个图像,有两种实现思路。

一种是拿到窗口绘图句柄就直接绘制,这种方式绘制的图像在窗口移动到屏幕边缘再移回来,那么移出屏幕部分就会消失,或者把窗口最小化再还原,那么绘制的部分也会被消失。实现代码类似如下:

// 根据是否需要裁剪非客户区或者如何裁剪,选择合适的函数获取HDC
HDC hdc = GetDC (hWnd); 
// 在此处编写绘图代码

// 对于获取的窗口DC,使用ReleaseDC释放;对于自己生成的DC,使用DeleteDC释放
ReleaseDC (hWnd, hdc);

另一种是在WM_PAINT事件里面绘制,这种绘制方式不会有以上那样的问题,也是个人比较推荐的方式。实现代码类似如下:

LRESULT CALLBACK WndProc (HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam) {
    switch (uMsg) {
    case WM_PAINT:
        PAINTSTRUCT ps = {0};
        HDC hdc = BeginPaint (hWnd, &ps);
        // 在此处编写绘图代码
        EndPaint (hWnd, &ps);
        break;
    default:
        return DefWindowProc (hWnd, uMsg, wParam, lParam);
        break;
    }
    return 0;
}

// 需要刷新窗口时调用以下代码
RECT rect;
GetWindowRect (hWnd, &rect);
InvalidateRect (hWnd, &rect, TRUE);

对于直接绘制到界面上的代码,很可能出现闪屏的问题,另外多次的直接向界面绘制图像,效率也不高,推荐做法是使用双缓冲。

具体实现思路是创建一个窗口大小的图片,首先向图片进行绘制,绘制完成后再将图片绘制到屏幕HDC上,可以提高效率,且避免闪屏的问题。

GDI双缓冲绘制示例代码如下:

// 创建内存兼容 DC
HDC hTmpDc = CreateCompatibleDC (hdc);
// 创建内存兼容位图
HBITMAP hTmpBmp = CreateCompatibleBitmap (hdc, width, height);
// 选定绘图对象
SelectObject (hTmpDc, hTmpBmp);

// 在此处编写绘图代码,绘制到 hTmpDc 设备

// 将图片绘制到屏幕上
BitBlt (hdc, 0, 0, width, height, hTmpDc, 0, 0, SRCCOPY);
// 释放内存兼容位图及内存兼容DC
DeleteObject (hTmpBmp);
DeleteDC (hTmpDc);

GDI+双缓冲绘制示例代码如下:

// 创建临时位图
Gdiplus::Bitmap tmpBmp (width, height, PixelFormat32bppARGB);
// 创建绘制临时位图所需 Graphics 对象
Gdiplus::Graphics tmpG (&tmpBmp);

// 在此处编写绘图代码,绘制到 tmpG 对象

// 创建窗口DC所需 Graphics 对象
Gdiplus::Graphics g (hdc);
// 将图片绘制到屏幕上
g.DrawImage (&tmpBmp, Gdiplus::Rect (0, 0, width, height),
    0, 0, width, height, Gdiplus::UnitPixel);

从代码形式上看,GDI+简洁太多了。

3 GDI+的问题

GDI+里面最坑的东西。对于像素点需要一个一个去计算的实现代码,用GetPixel、SetPixel简直慢的一比。网上对此也有很多实现思路,比如用GDI来代替实现什么的。

但我还是推荐使用物理内存访问来实现:

// 图片对象
Gdiplus::Bitmap bmp (width, height, PixelFormat32bppARGB);
// 图片数据对象
Gdiplus::BitmapData bmpData;
// 锁定内存区域
bmp.LockBits (&Gdiplus::Rect (0, 0, width, height),
    Gdiplus::ImageLockModeRead, PixelFormat32bppARGB, &bmpData);

// 此时可以使用指针来读取图片内存区域。bmpData.Scan0 指向的就是图片数据区
// 图片像素大小受像素格式影响,此处一个像素就占4个字节
// 在这个图片里面读取数据,然后在另一个图片里面写入数据

// 解锁内存区域
bmp.UnlockBits (&bmpData);

接下来附一则常用代码:从资源加载 Image 对象的代码的实现

Gdiplus::Image *load_image_from_resource (LPCTSTR lpResName, LPCTSTR lpResType) {
    HMODULE hModule = GetModuleHandle (NULL);

    //搜索资源
    HRSRC hRsrc = FindResource (hModule, lpResName, lpResType);
    if (NULL == hRsrc)
        return nullptr;

    //获取资源大小
    DWORD dwSize = SizeofResource (hModule, hRsrc);

    //加载资源
    HGLOBAL hGlobal = LoadResource (hModule, hRsrc);
    if (NULL == hGlobal) {
        FreeResource (hGlobal);
        return nullptr;
    }

    //锁定资源
    LockResource (hGlobal);

    // 创建资源流
    LPSTREAM pStream;
    if (S_OK != CreateStreamOnHGlobal (hGlobal, true, &pStream)) {
        GlobalUnlock (hGlobal);
        FreeResource (hGlobal);
        return nullptr;
    }

    // 从资源流创建 Image 对象
    Gdiplus::Image *img = Gdiplus::Image::FromStream (pStream);

    GlobalUnlock (hGlobal);
    FreeResource (hGlobal);

    return img;
}