Why is my stretchblt images losing colours?

334 Views Asked by At

I am having some difficulties in correctly populating a CListCtrl with thumbnails of monitor displays.

On the right of my CDialog I have a static control and I render the image on a white canvas like this:

void CCenterCursorOnScreenDlg::OnDrawItem(int nIDCtl, LPDRAWITEMSTRUCT lpDrawItemStruct)
{
    if (nIDCtl == IDC_STATIC_MONITOR && !m_imgPreview.IsNull())
    {
        // Set the mode
        SetStretchBltMode(lpDrawItemStruct->hDC, HALFTONE);

        // Wipe the canvas
        FillRect(lpDrawItemStruct->hDC, &lpDrawItemStruct->rcItem, static_cast<HBRUSH>(GetStockObject(WHITE_BRUSH)));

        // Get canvas rectangle
        const CRect rectCanvas(lpDrawItemStruct->rcItem);

        // Calculate ratio factors
        const float nRatioImage = m_imgPreview.GetWidth() / static_cast<float>(m_imgPreview.GetHeight());
        const float nRatioCanvas = rectCanvas.Width() / static_cast<float>(rectCanvas.Height());

        // Calculate new rectangle size
        // Account for portrait images (negative values)
        CRect rectDraw = rectCanvas;
        if (nRatioImage > nRatioCanvas)
            rectDraw.SetRect(0, 0, rectDraw.right, static_cast<int>(rectDraw.right / nRatioImage));
        else if (nRatioImage < nRatioCanvas)
            rectDraw.SetRect(0, 0, static_cast<int>((rectDraw.bottom * nRatioImage)), rectDraw.bottom);

        // Add a margin
        rectDraw.DeflateRect(5, 5);

        // Move to center
        const CSize ptOffset = rectCanvas.CenterPoint() - rectDraw.CenterPoint();
        rectDraw.OffsetRect(ptOffset);

        // Add a black frame
        FrameRect(lpDrawItemStruct->hDC, &lpDrawItemStruct->rcItem, static_cast<HBRUSH>(GetStockObject(BLACK_BRUSH)));

        // Draw
        m_imgPreview.Draw(lpDrawItemStruct->hDC, rectDraw);

        return;
    }

    CDialogEx::OnDrawItem(nIDCtl, lpDrawItemStruct);
}

The above works beautifully:

enter image description here

But I have problems with the CListCtrl versions of the images. For instance, I am losing the colouring as you can see.

My CImageList is created like this:

m_ImageListThumb.Create(THUMBNAIL_WIDTH, THUMBNAIL_HEIGHT, ILC_COLOR32, 0, 1);
m_ListThumbnail.SetImageList(&m_ImageListThumb, LVSIL_NORMAL);

I then create all the thumbnails by calling DrawThumbnails() in OnInitDialog:

void CCenterCursorOnScreenDlg::DrawThumbnails()
{
    int monitorIndex = 0;

    m_ListThumbnail.SetRedraw(FALSE);

    for (auto& strMonitor : m_monitors.strMonitorNames)
    {
        CImage img;
        CreateMonitorThumbnail(monitorIndex, img, true);

        CBitmap* pImage = new CBitmap();
        pImage->Attach((HBITMAP)img);
        m_ImageListThumb.Add(pImage, nullptr);

        CString strMonitorDesc = m_monitors.strMonitorNames.at(monitorIndex);

        strMonitorDesc.AppendFormat(L" (Screen %d)", monitorIndex + 1);

        m_ListThumbnail.InsertItem(monitorIndex, strMonitorDesc, monitorIndex);


        monitorIndex++;

        delete pImage;
    }

    m_ListThumbnail.SetRedraw(TRUE);
}

The CreateMonitorThumbnail function:

BOOL CCenterCursorOnScreenDlg::CreateMonitorThumbnail(const int iMonitorIndex, CImage &rImage, bool bSmall)
{
    const CRect rcCapture = m_monitors.rcMonitors.at(iMonitorIndex);

    // destroy the currently contained bitmap to create a new one
    rImage.Destroy();

    auto nWidth = rcCapture.Width();
    auto nHeight = rcCapture.Height();
    if (bSmall)
    {
        nWidth = THUMBNAIL_WIDTH;
        nHeight = THUMBNAIL_HEIGHT;
    }

    // create bitmap and attach it to this object 
    if (!rImage.Create(nWidth, nHeight, 32, 0))
    {
        AfxMessageBox(L"Cannot create image!", MB_ICONERROR);
        return FALSE;
    }


    // create virtual screen DC
    CDC dcScreen;
    dcScreen.CreateDC(_T("DISPLAY"), nullptr, nullptr, nullptr);

    // copy the contents from the virtual screen DC 

    BOOL bRet = FALSE;
    if (bSmall)
    {
        CRect rt(0, 0, nWidth, nHeight);

        //::FillRect(rImage.GetDC(), rt, static_cast<HBRUSH>(GetStockObject(WHITE_BRUSH)));
        bRet = ::StretchBlt(rImage.GetDC(), 0, 0, 
            nWidth, 
            nHeight, 
            dcScreen.m_hDC, 
            rcCapture.left, 
            rcCapture.top, 
            rcCapture.Width(), 
            rcCapture.Height(), SRCCOPY | CAPTUREBLT);

    }
    else
    {
        bRet = ::BitBlt(rImage.GetDC(), 0, 0, 
            rcCapture.Width(), 
            rcCapture.Height(),
            dcScreen.m_hDC, 
            rcCapture.left, 
            rcCapture.top, SRCCOPY | CAPTUREBLT);
    }

    // do cleanup and return
    dcScreen.DeleteDC();
    rImage.ReleaseDC();

    return bRet;
}

Ideally I want to have exactly the same kind of visual image as on the right, but obviously resized down. How do I fix this?


I simplified the converting from CImage to CBitmap but it made no difference:

void CCenterCursorOnScreenDlg::DrawThumbnails()
{
    int monitorIndex = 0;

    // Stop redrawing the CListCtrl
    m_ListThumbnail.SetRedraw(FALSE);

    // Loop monitor info
    for (auto& strMonitor : m_monitors.strMonitorNames)
    {
        // Create the thumbnail image
        CImage monitorThumbnail;
        CreateMonitorThumbnail(monitorIndex, monitorThumbnail, true);

        // Convert it to a CBitmap
        CBitmap* pMonitorThumbnailBitmap = CBitmap::FromHandle(monitorThumbnail);

        // Add the CBitmap to the CImageList
        m_ImageListThumb.Add(pMonitorThumbnailBitmap, nullptr);

        // Build the caption description
        CString strMonitorDesc = m_monitors.strMonitorNames.at(monitorIndex);
        strMonitorDesc.AppendFormat(L" (Screen %d)", monitorIndex + 1);

        // Add the item to the CListCtrl
        m_ListThumbnail.InsertItem(monitorIndex, strMonitorDesc, monitorIndex);

        monitorIndex++;
    }

    // Start redrawiung the CListCtrl again
    m_ListThumbnail.SetRedraw(TRUE);
}

If I change my code to pass false for the last parameter, so that it uses the original captured images without scaling down:

enter image description here

The colours are god there, so it is when I do:

if (bSmall)
{
    CRect rt(0, 0, nWidth, nHeight);

    //::FillRect(rImage.GetDC(), rt, static_cast<HBRUSH>(GetStockObject(WHITE_BRUSH)));
    bRet = ::StretchBlt(rImage.GetDC(), 0, 0, 
        nWidth, 
        nHeight, 
        dcScreen.m_hDC, 
        rcCapture.left, 
        rcCapture.top, 
        rcCapture.Width(), 
        rcCapture.Height(), SRCCOPY | CAPTUREBLT);

}

that it messes up.

1

There are 1 best solutions below

0
On

My issue did not have anything to do with OnDrawItem. I simply included that to indicate how the image on the right was being rendered. I thought it may helped as background information. But it has probably confused the question and I may take it out in the long run!

Based on the comments I was reminded about SetStretchBltMode which was missing from CreateMonitorThumbnail. So, I now have this function:

BOOL CCenterCursorOnScreenDlg::CreateMonitorThumbnail(const int iMonitorIndex, CImage &rImage, bool bResizeAsThumbnail)
{
    const CRect rcCapture = m_monitors.rcMonitors.at(iMonitorIndex);

    // Destroy the currently contained bitmap to create a new one
    rImage.Destroy();

    // Massage the dimensions as we want a thumbnail
    auto nWidth = rcCapture.Width();
    auto nHeight = rcCapture.Height();
    if (bResizeAsThumbnail)
    {
        nWidth = m_iThumbnailWidth;

        auto dRatio = rcCapture.Width() / nWidth;
        //nHeight = m_iThumbnailHeight;
        nHeight = static_cast<int>(rcCapture.Height() / dRatio);

        if (nHeight > m_iThumbnailHeight)
        {
            AfxMessageBox(L"Need to investigate!");
        }
    }

    // Create bitmap and attach it to this object 
    if (!rImage.Create(nWidth, nHeight, 32, 0))
    {
        AfxMessageBox(L"Cannot create image!", MB_ICONERROR);
        return FALSE;
    }


    // Create virtual screen DC
    CDC dcScreen;
    dcScreen.CreateDC(L"DISPLAY", nullptr, nullptr, nullptr);

    // Copy (or resize) the contents from the virtual screen DC 

    BOOL bRet = FALSE;
    auto dcImage = rImage.GetDC();
    if (bResizeAsThumbnail)
    {
        // Set the mode first!
        SetStretchBltMode(dcImage, COLORONCOLOR);

        CPen penBlack;
        penBlack.CreatePen(PS_SOLID, 3, RGB(0, 0, 0));
        ::Rectangle(dcImage, 0, 0, m_iThumbnailWidth, m_iThumbnailHeight);

        int iTop = (m_iThumbnailHeight - nHeight) / 2;

        // Copy (and resize)
        bRet = ::StretchBlt(dcImage, 0, iTop,
            nWidth, 
            nHeight, 
            dcScreen.m_hDC, 
            rcCapture.left, 
            rcCapture.top, 
            rcCapture.Width(), 
            rcCapture.Height(), SRCCOPY | CAPTUREBLT);
    }
    else
    {
        // Copy
        bRet = ::BitBlt(dcImage, 0, 0, 
            rcCapture.Width(), 
            rcCapture.Height(),
            dcScreen.m_hDC, 
            rcCapture.left, 
            rcCapture.top, SRCCOPY | CAPTUREBLT);
    }

    // Do cleanup and return
    dcScreen.DeleteDC();
    rImage.ReleaseDC();

    return bRet;
}

That was the key to getting the thumbnail showing with the right colours:

enter image description here