如何显示动画GIF?

问题描述

我有一个动画 GIF,我正在尝试显示它,但我无法让它工作。它不会运行并在行 0xC0000005: Access violation reading location 0x80000B00. 处抛出异常 SetTimer(hWnd,ID_TIMER,((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10,(TIMERPROC)NULL);

LRESULT CALLBACK WndProc(HWND hWnd,UINT message,WParaM wParam,LParaM lParam) {
    Image* m_pImage = new Image(L"spinner.gif");
    UINT m_iCurrentFrame = 0;
    UINT m_FrameCount = 0;
    PropertyItem* m_pItem = 0;

    switch(message) {
        ...
        case WM_TIMER: {
            //Because there will be a new delay value
            KillTimer(hWnd,ID_TIMER);
            //Change Active frame
            GUID Guid = FrameDimensionTime;
            m_pImage->SelectActiveFrame(&Guid,m_iCurrentFrame);
            //New timer
            SetTimer(hWnd,NULL);
            //Again move to the next
            m_iCurrentFrame = (++m_iCurrentFrame) % m_FrameCount;
            InvalidateRect(hWnd,NULL,FALSE);
            break;
        }
        case WM_PAINT:{
            HDC hdc;
            PAINTSTRUCT ps;
            hdc = BeginPaint(hWnd,&ps);

            Graphics graphics(hdc); 
            
            //First of all we should get the number of frame dimensions
            //Images considered by GDI+ as:
            //frames[animation_frame_index][how_many_animation];
            UINT count = m_pImage->GetFrameDimensionsCount();

            //Now we should get the identifiers for the frame dimensions 
            GUID* m_pDimensionIDs = new GUID[count];
            m_pImage->GetFrameDimensionsList(m_pDimensionIDs,count);

            //For gif image,we only care about animation set#0
            WCHAR strGuid[40];
            StringFromGUID2(m_pDimensionIDs[0],strGuid,40);
            m_FrameCount = m_pImage->GetFrameCount(&m_pDimensionIDs[0]);

            //PropertyTagFrameDelay is a pre-defined identifier 
            //to present frame-delays by GDI+
            UINT TotalBuffer = m_pImage->GetPropertyItemSize(PropertyTagFrameDelay);
            m_pItem = (PropertyItem*)malloc(TotalBuffer);
            m_pImage->GetPropertyItem(PropertyTagFrameDelay,TotalBuffer,m_pItem);

            //Set Current Frame at #0
            m_iCurrentFrame = 0;
            GUID Guid = FrameDimensionTime;
            m_pImage->SelectActiveFrame(&Guid,m_iCurrentFrame);

            //Use Timer
            //NOTE HERE: frame-delay values should be multiply by 10
            SetTimer(hWnd,(TIMERPROC)NULL);
          
            //Move to the next frame
            ++m_iCurrentFrame;
            InvalidateRect(hWnd,FALSE);

            //Finally simply draw
            graphics.DrawImage(m_pImage,120,100,100);

            EndPaint(hWnd,&ps);
            break;
        }
        ...
    }
    return DefWindowProc(hWnd,message,wParam,lParam);
}

我发现的大多数示例都是 WCF 或函数折旧。

解决方法

解决方案:

您可以将 m_pItem 设置为全局。除了此错误之外,您还有一些其他错误。例如,m_iCurrentFrameWM_PAINT 中始终初始化为 0。

而且我们只需要加载一次 GIF 图片文件,不需要把它放在 WM_PAINT 中。

GIF 循环播放,

#include <memory>
#include <vector>
#include <algorithm>
#include <windows.h>
#include <objidl.h>
#include <GdiPlus.h>
#include <gdiplusimaging.h>
#include <tchar.h>
using namespace Gdiplus;
#pragma comment (lib,"Gdiplus.lib")

#define TIMER_ID 101

LRESULT CALLBACK WndProc(HWND hWnd,UINT message,WPARAM wParam,LPARAM lParam);

int WINAPI _tWinMain(HINSTANCE hInstance,HINSTANCE hPrevInstance,LPTSTR lpCmdLine,int nCmdShow)
{
    ULONG_PTR m_gdiplusToken;
    GdiplusStartupInput gdiplusStartupInput;
    GdiplusStartup(&m_gdiplusToken,&gdiplusStartupInput,NULL);

    Image gif(_T("spinner.gif"));

    MSG msg = { 0 };
    WNDCLASS wc = { 0 };
    wc.lpfnWndProc = WndProc;
    wc.hInstance = hInstance;
    wc.hbrBackground = NULL; // <= Do not provide a background brush.
    wc.lpszClassName = L"anim_gif_player";
    if (!RegisterClass(&wc))
        return -1;

    if (!CreateWindow(wc.lpszClassName,L"Animated GIF player",WS_OVERLAPPEDWINDOW | WS_VISIBLE,1080,800,hInstance,&gif))
        return -2;

    while (GetMessage(&msg,NULL,0) > 0)
        DispatchMessage(&msg);

    return 0;
}

std::vector<unsigned int> LoadGifFrameInfo(Image* image)
{
    // I think animated gifs will always only have 1 frame dimension...
    // the "dimension" being the frame count,but I could be wrong about this
    int count = image->GetFrameDimensionsCount();
    if (count != 1)
        return std::vector<unsigned int>();

    GUID guid;
    if (image->GetFrameDimensionsList(&guid,1) != 0)
        return std::vector<unsigned int>();
    int frame_count = image->GetFrameCount(&guid);

    auto sz = image->GetPropertyItemSize(PropertyTagFrameDelay);
    if (sz == 0)
        return std::vector<unsigned int>();

    // copy the frame delay property into the buffer backing an std::vector
    // of bytes and then get a pointer to its value,which will be an array of 
    // unsigned ints
    std::vector<unsigned char> buffer(sz);
    PropertyItem* property_item = reinterpret_cast<PropertyItem*>(&buffer[0]);
    image->GetPropertyItem(PropertyTagFrameDelay,sz,property_item);
    unsigned int* frame_delay_array = (unsigned int*)property_item[0].value;

    // copy the delay values into an std::vector while converting to milliseconds.
    std::vector<unsigned int> frame_delays(frame_count);
    std::transform(frame_delay_array,frame_delay_array + frame_count,frame_delays.begin(),[](unsigned int n) {return n * 10; }
    );

    return frame_delays;
}

void GenerateFrame(Bitmap* bmp,Image* gif)
{
    Graphics dest(bmp);

    SolidBrush white(Color::White);
    dest.FillRectangle(&white,bmp->GetWidth(),bmp->GetHeight());

    if (gif)
        dest.DrawImage(gif,0);
}

std::unique_ptr<Bitmap> CreateBackBuffer(HWND hWnd)
{
    RECT r;
    GetClientRect(hWnd,&r);
    return std::make_unique<Bitmap>(r.right - r.left,r.bottom - r.top);
}

LRESULT CALLBACK WndProc(HWND hWnd,LPARAM lParam)
{
    static Image* animated_gif;
    static std::unique_ptr<Bitmap> back_buffer;
    static std::vector<unsigned int> frame_delays;
    static int current_frame;

    switch (message) {
    case WM_CREATE: {
        animated_gif = reinterpret_cast<Image*>(
            reinterpret_cast<CREATESTRUCT*>(lParam)->lpCreateParams
            );

        if (!animated_gif || animated_gif->GetLastStatus() != 0) {
            MessageBox(hWnd,_T("Unable to load animated gif"),_T("error"),MB_ICONERROR);
            return 0;
        }

        // Create a bitmap the size of the window's clent area
        back_buffer = CreateBackBuffer(hWnd);

        // get the frame delays and thereby test that this is really an animated gif
        frame_delays = LoadGifFrameInfo(animated_gif);
        if (frame_delays.empty()) {
            MessageBox(hWnd,_T("Invalid gif or not an animated gif"),MB_ICONERROR);
            return 0;
        }

        current_frame = 0;
        animated_gif->SelectActiveFrame(&FrameDimensionTime,current_frame);

        GenerateFrame(back_buffer.get(),animated_gif);

        SetTimer(hWnd,TIMER_ID,frame_delays[0],nullptr);
        InvalidateRect(hWnd,nullptr,FALSE);
    }
                  break;

    case WM_TIMER: {
        KillTimer(hWnd,TIMER_ID);
        current_frame = (current_frame + 1) % frame_delays.size();
        animated_gif->SelectActiveFrame(&FrameDimensionTime,current_frame);
        GenerateFrame(back_buffer.get(),animated_gif);
        SetTimer(hWnd,frame_delays[current_frame],FALSE);
    } break;

    case WM_PAINT: {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd,&ps);
        Graphics g(hdc);
        g.DrawImage(back_buffer.get(),0);
        EndPaint(hWnd,&ps);
    } break;

    case WM_SIZE: {
        back_buffer = CreateBackBuffer(hWnd);
        GenerateFrame(back_buffer.get(),animated_gif);
    } break;

    case WM_CLOSE:
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hWnd,message,wParam,lParam);
    }
    return 0;
}
,

根据评论部分的阅读,似乎发生了第 0xC0000005: Access violation reading location 0x80000B00. 行的错误 SetTimer(hWnd,ID_TIMER,((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10,(TIMERPROC)NULL);,因为 GIF 仅包含一帧。

例如,您可以通过仅在超过一帧时调用计时器来避免这种情况;

if(m_FrameCount > 1){
   //Continue calling the timer.
}

这是另一个例子;

/** These are global **/

Image* m_pImage;
GUID* m_pDimensionIDs;
UINT m_FrameCount;
PropertyItem* m_pItem;
UINT m_iCurrentFrame;
UINT_PTR timer_id = 123456; //set your own timer id
BOOL m_bIsPlaying;
BOOL isPlayable;

/** Functions **/
void LoadImage() {
    m_pImage = Image::FromFile(L"C:\\Users\\username\\location_of_file\\spinner.gif");
    UINT count = m_pImage->GetFrameDimensionsCount();
    m_pDimensionIDs = new GUID[count];
    m_pImage->GetFrameDimensionsList(m_pDimensionIDs,count);

    WCHAR strGuid[39];
    (void)StringFromGUID2(m_pDimensionIDs[0],strGuid,39);
    m_FrameCount = m_pImage->GetFrameCount(&m_pDimensionIDs[0]);

    if(m_FrameCount > 1) { //frame of GIF is more than one,all good,we don't want the error of `Access violation reading location`
        isPlayable = TRUE;
        OutputDebugString(_T("Image file contains more than 1 frame,its playable.\n"));
    }

    UINT TotalBuffer = m_pImage->GetPropertyItemSize(PropertyTagFrameDelay);
    m_pItem = (PropertyItem*)malloc(TotalBuffer);
    m_pImage->GetPropertyItem(PropertyTagFrameDelay,TotalBuffer,m_pItem);
}

/** call this function on timer **/
void OnTimer() {
    if(isPlayable) {
        KillTimer(hwnd,timer_id);
        GUID Guid = FrameDimensionTime;
        m_pImage->SelectActiveFrame(&Guid,m_iCurrentFrame);
        SetTimer(hwnd,120,NULL);
        m_iCurrentFrame = (++m_iCurrentFrame) % m_FrameCount;
        //Invalidate(FALSE);
        InvalidateRect(hwnd,FALSE);
    }
}

/** call this to animate the GIF image,should be call before drawing the image usually on WM_PAINT. **/
void AnimateGIF() {
    if(m_bIsPlaying == TRUE) {
        return;
    }
    if(isPlayable) {
        m_iCurrentFrame = 0;
        GUID Guid = FrameDimensionTime;
        m_pImage->SelectActiveFrame(&Guid,NULL);
        ++m_iCurrentFrame;
        InvalidateRect(hwnd,FALSE);
        m_bIsPlaying = TRUE;
    }
}

/** Function to draw the image in Window,must be call on WM_PAINT. **/
void DrawItem(HDC hdc,int xPosition,int yPosition,int width,int height) {
    AnimateGIF(); //This will only works if the image has more than one frame. Otherwise,the single frame of image will be shown.
    Graphics g(hdc);
    g.DrawImage(m_pImage,xPosition,yPosition,width,height);
}


/** Function to destroy objects and arrays,call this function on WM_DESTROY of WinProc. **/
void Desrtroy() {
    if(m_pDimensionIDs) {
        delete[] m_pDimensionIDs;
    }

    if(m_pItem) {
        free(m_pItem);
    }

    if(m_pImage) {
        delete m_pImage;
    }
}

然后在你的WndProc上,你可以调用上面的函数;

LRESULT CALLBACK WndProc(HWND hWnd,LPARAM lParam) {
    switch(message) {
    case WM_CREATE: {
        LoadImage(); //1. Load the image
        break;
    }
    case WM_TIMER: {
        OnTimer();  //2. Call on timer. 
        break;
    }
    case WM_PAINT:
    {
        HDC hdc;
        PAINTSTRUCT ps;
        hdc = BeginPaint(hWnd,&ps);
       
        DrawItem(hdc,160,150,95,95); //3. Draw your image,specify your own x and y Position,width and height.
       
        EndPaint(hWnd,&ps);
        break;
    }
    case WM_DESTROY:
    {
        Desrtroy(); //4. Don't forget to delete the images and free up the arrays.
        PostQuitMessage(0);
        break;
    }
    }
    return DefWindowProc(hWnd,lParam);
}

使用这种方法,您不会遇到相同的错误。如果超过一帧,GIF 将永远播放。否则只会显示单帧。

项目示例可以从 this 链接找到,归功于 SeynalKim