【问题标题】:How to display animated GIF?如何显示动画 GIF?
【发布时间】:2021-02-26 03:02:52
【问题描述】:

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

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, ID_TIMER, ((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10, 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, ID_TIMER, ((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10, (TIMERPROC)NULL);
          
            //Move to the next frame
            ++m_iCurrentFrame;
            InvalidateRect(hWnd, NULL, FALSE);

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

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

我发现的大部分例子是 WCF 或者函数被贬值了。

【问题讨论】:

  • 可能是因为您在WinProc 中一遍又一遍地加载 gif,而且您从未删除过指针,因此造成了巨大的内存泄漏。
  • PropertyItem* m_pItem = 0; 然后SetTimer(hWnd, ID_TIMER, ((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10, NULL); 你正在使用空指针。
  • 您正在解除对无效指针的引用,导致尝试读取地址 0x80000B00 的内存。调试器将为您提供了解正在发生的事情所需的所有详细信息。假设基本熟悉 C。
  • 现在出现0xC0000005: Access violation reading location 0x880027EB.的错误。
  • 可能和公式((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10有关。

标签: c++ winapi visual-studio-2019 gdi+


【解决方案1】:

解决方案:

您可以将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,
        0, 0, 1080, 800, 0, 0, hInstance, &gif))
        return -2;

    while (GetMessage(&msg, NULL, 0, 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, 0, 0, bmp->GetWidth(), bmp->GetHeight());

    if (gif)
        dest.DrawImage(gif, 0, 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, UINT message, WPARAM wParam, 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"), _T("error"), 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, TIMER_ID, frame_delays[current_frame], nullptr);
        InvalidateRect(hWnd, nullptr, FALSE);
    } break;

    case WM_PAINT: {
        PAINTSTRUCT ps;
        HDC hdc = BeginPaint(hWnd, &ps);
        Graphics g(hdc);
        g.DrawImage(back_buffer.get(), 0, 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;
}

【讨论】:

  • SetTimer 函数中仍有0xC0000005: Access violation reading location 0x8C002288 的错误。大概和((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10这个公式有关。
  • @Papilion 你试过我的代码了吗?你可以创建一个空项目,复制我的代码再次测试。
  • 是的,我就是这么做的。
  • 它对你有用吗?我正在使用 Visual Studio 2019 企业版。
  • @Papilion 是的,它对我有用。请尝试使用绝对路径加载gif图片,例如:Image* m_pImage = new Image(L"C:\\Users\\xyz\\Desktop\\spinner.gif");
【解决方案2】:

根据阅读评论部分,看起来 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, ((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10, NULL);
        m_iCurrentFrame = (++m_iCurrentFrame) % m_FrameCount;
        //Invalidate(FALSE);
        InvalidateRect(hwnd, NULL, 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, m_iCurrentFrame);
        SetTimer(hwnd, 120, ((UINT*)m_pItem[0].value)[m_iCurrentFrame] * 10, NULL);
        ++m_iCurrentFrame;
        InvalidateRect(hwnd, NULL, 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, UINT message, WPARAM wParam, 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, message, wParam, lParam);
}

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

项目示例可以从this链接找到,感谢SeynalKim

【讨论】:

  • 如果我使用 PNG 或 JPEG 而不是 GIF 会发生什么?我以为它会正确显示?
  • @Papilion 循环播放PNG图片。
  • @Papilion - 是的,它会正确显示。如您所见,仅当isPlayable 设置为TRUE 时才会设置计时器。为了设置它TRUEm_FrameCount 必须大于1。由于 PNG 和 JPEG 仅包含单帧,因此计时器将不起作用或 PNG、JPEG 不会循环显示。看看if(isPlayable) {.....}里面发生了什么。
  • @StriveSun-MSFT 不,不会。只有当isPlayableTRUE 时,计时器才会起作用。
  • @Polar 是的,如果添加条件语句判断是否为框架,确实只会绘制一次。
猜你喜欢
  • 2011-02-25
  • 2019-01-04
  • 1970-01-01
  • 2016-08-23
  • 2012-09-15
  • 2016-06-06
  • 1970-01-01
相关资源
最近更新 更多