LoadBitmap fails after multiple calls

2019-07-24 04:17发布

问题:

In this function, after about 90 calls (it's called in a loop and the idea is to load in a separate image each time, but I have kept it to one image for simplicity).The global variables now changed to local ones.

   void CDLP_Printer_ControlDlg::DisplayBMPfromSVG(CString& strDsiplayFile)
{
    HBITMAP hbmp_temp = (HBITMAP)::LoadImage(0, strDsiplayFile, IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
    if (!hbmp_temp)
    {
        //hbmp_temp = ::LoadBitmap(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDB_BITMAP1));
        ActionList.AddString(L"Bitmap Load Failure: GetBMPromSVG");
        ActionList.UpdateWindow();
        if (!hbmp_temp)
            return;
    }

    CBitmap bmp_temp;
    bmp_temp.Attach(hbmp_temp);
    mProjectorWindow.m_picControl.ModifyStyle(0xF, SS_BITMAP, SWP_NOSIZE);
    mProjectorWindow.m_picControl.SetBitmap(bmp_temp);

    return;
}

I hope someone can come up with an Idea whats wrong. GetLastError returns "8" which means nothing to me.

回答1:

Detach will destroy the previous handle.

Note that if you call Detach after calling SetBitmap, the picture control's bitmap is destroyed. The result is that the picture control is painted once, but it won't be repainted. For example picture control goes blank if dialog is resized.

EDIT

To destroy the old bitmap, call Detach followed by DestroyObject. Example

HGDIOBJ hbitmap_detach = m_bitmap.Detach();
if (hbitmap_detach)
    DeleteObject(hbitmap_detach); 
m_bitmap.Attach(hbitmap);

If it's a temporary CBitmap then DeleteObject is not necessary, because DeleteObject is called automatically when CBitmap goes out of scope.

Note that if you destroy the bitmap after calling SetBitmap, the picture control's bitmap is destroyed. The result is that the picture control is painted once, but it won't be repainted. For example picture control goes blank if dialog is resized.

It's the same problem if you declare a temporary CBitmap on stack and attach the bitmap handle. That bitmap handle will be destroyed and picture control can't repaint itself.

In addition, Windows XP sometimes makes duplicate bitmap which needs to be destroyed as well. SetBitmap returns a handle to previous bitmap. In Vista+ the returned bitmap is the same one which was save in m_bitmap, we already destroy that with Detach. But in XP we need to destroy this copy if it is a different handle.

void CMyDialog::foo()
{
    HBITMAP save = m_bitmap;
    HBITMAP hbitmap = (HBITMAP)::LoadImage(0, filename,
        IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
    if (hbitmap)
    {
        HGDIOBJ hbitmap_detach = m_bitmap.Detach();
        //Edit ****************************************
        //Delete old handle, otherwise program crashes after 10,000 calls
        if (hbitmap_detach)
            DeleteObject(hbitmap_detach); 
        //*********************************************
        m_bitmap.Attach(hbitmap);

        HBITMAP oldbmp = m_picControl.SetBitmap(m_bitmap);

        //for Windows XP special case where there might be 2 copies:
        if (oldbmp && (oldbmp != save))
            DeleteObject(oldbmp);
    }
}

Also, SetBitmap takes HBITMAP parameter and returns HBITMAP, so you can avoid using CBitmap altogether. The following example works in Vista+

void foo()
{
    HBITMAP temp = (HBITMAP)::LoadImage(0,filename,IMAGE_BITMAP,0,0,LR_LOADFROMFILE);
    if (temp)
    {
        HBITMAP oldbmp = m_picControl.SetBitmap(temp);
        if (oldbmp)
            DeleteObject(oldbmp);
        DeleteObject(temp);
    }
}


回答2:

Your code has several issues, some minor, others are fatal (and the implementation really only appears to work, because the OS is prepared to deal with those common bugs). Here is an annotated listing of the original code:

void CDLP_Printer_ControlDlg::DisplayBMPfromSVG(CString& strDsiplayFile) {
//                                              ^ should be const CString&
    HBITMAP hbmp_temp = (HBITMAP)::LoadImage(0, strDsiplayFile, IMAGE_BITMAP, 0, 0,
                                             LR_LOADFROMFILE);
    if (!hbmp_temp) {
        //hbmp_temp = ::LoadBitmap(AfxGetInstanceHandle(), MAKEINTRESOURCE(IDB_BITMAP1));
        ActionList.AddString(L"Bitmap Load Failure: GetBMPromSVG");
        ActionList.UpdateWindow();
        if (!hbmp_temp)
//      You already know, that the condition is true (unless your commented out code
//      is supposed to run).
            return;
    }

    CBitmap bmp_temp;
    bmp_temp.Attach(hbmp_temp);
//  ^ This should immediately follow the LoadImage call, to benefit from automatic
//    resource management. (What's the point of using MFC when you decide to implement
//    manual resource management on top of it?)
    mProjectorWindow.m_picControl.ModifyStyle(0xF, SS_BITMAP, SWP_NOSIZE);
//                                            ^ Use named constants. No one is going to
//                                              look up the documentation just to find
//                                              out, what you are trying to do.
    mProjectorWindow.m_picControl.SetBitmap(bmp_temp);
//  The GDI object (hbmp_temp) now has two owners, the CBitmap instance bmp_temp, and
//  the picture control. At the same time, you are throwing away the handle previously
//  owned by the control. This is your GDI resource leak.

    return;
//  ^ Superfluous. This is merely confusing readers. Remove it.
}
// This is where things go fatal: The bmp_temp d'tor runs, destroying the GDI resource
// hbmp_temp, that's also owned by the control. This should really blow up in your face
// but the OS knows that developers cannot be trusted anymore, and covers your ass.

The two major issues are:

  • Failure to delete GDI resources owned by your code (the return value of SetBitmap). This eventually causes any attempt to create additional GDI resources to fail.
  • The dangling pointer (HBITMAP) caused by assigning two owners to the same resource (hbmp_temp).
  • There's really another issue here as well. Since you assigned two owners to the bitmap resource, this would lead to a double-delete. It doesn't, because you opted against resource cleanup. (The fact that you don't know what you're doing saved you here. Keep that in mind the next time you celebrate your "can do" attitude.)

Following is a version with fixed-up resource management. This won't make any sense to you, since you don't know MFC (or C++ for that matter) well enough to understand, how either one aids in automatic resource management. Anyhow:

void CDLP_Printer_ControlDlg::DisplayBMPfromSVG(CString& strDsiplayFile) {
    HBITMAP hbmp_temp = (HBITMAP)::LoadImage(0, strDsiplayFile, IMAGE_BITMAP, 0, 0,
                                             LR_LOADFROMFILE);
    // Immediately attach a C++ object, so that resources will get cleaned up
    // regardless how the function is exited.
    CBitmap bmp_temp;
    if (!bmp_temp.Attach(hbmp_temp)) {
        // Log error/load placeholder image
        return;
    }

    mProjectorWindow.m_picControl.ModifyStyle(0xF, SS_BITMAP, SWP_NOSIZE);
    // Swap the owned resource of bmp_temp with that of the control:
    bmp_temp.Attach(mProjectorWindow.m_picControl.SetBitmap(bmp_temp.Detach()));
}

The last line is the critical part. It implements the canonical way to swap raw Windows API resources with resource management wrappers. This is the sequence of operations:

  1. bmp_temp.Detach() releases ownership of the GDI resource.
  2. SetBitmap() passes ownership of the GDI resource to the control, and returns the previous GDI object (if any).
  3. bmp_temp.Attach() acquires ownership of the returned GDI resource. This ensures that the previous resource gets cleaned up, when bmp_temp goes out of scope (at the end of the function).