Search code examples
mfcvisual-studio-2017ribbon

MFC Ribbon: change icon of a button (dynamically)


For CMFCRibbonBar, I want to change the icon of a button depnding on application state.

(I've dug through the code and documentation for a while, but haven't found anything helpful.)


Solution

  • I derived a class CMFCRibbonButtonEx from CMFCRibbonButton to control how the icon is being drawn. In this special case I wanted to alpha-blend a green checkmark over the original icon:

    Header:

    class CMFCRibbonButtonEx : public CMFCRibbonButton
    {
    // Construction
    public:
        CMFCRibbonButtonEx();
        CMFCRibbonButtonEx(UINT nID, LPCTSTR lpszText, int nSmallImageIndex = -1, int nLargeImageIndex = -1, BOOL bAlwaysShowDescription = FALSE);
        CMFCRibbonButtonEx(UINT nID, LPCTSTR lpszText, HICON hIcon, BOOL bAlwaysShowDescription = FALSE, HICON hIconSmall = NULL, BOOL bAutoDestroyIcon = FALSE, BOOL bAlphaBlendIcon = FALSE);
    
    // Overridden
        void SetCheck(BOOL bCheck = TRUE);
        void DrawImage(CDC* pDC, RibbonImageType type, CRect rectImage);
    
    // Attributes
    private:
        BOOL m_bChecked;
    
    // Helper
    private:
        void DrawCheckmark(CDC* pDC, int CheckmarkResourceBitmapID, RECT *r);
        void PremultiplyBitmapAlpha(HDC hDC, HBITMAP hBmp);
    };  
    

    Implementation:

    CMFCRibbonButtonEx::CMFCRibbonButtonEx() : CMFCRibbonButton() { }
    CMFCRibbonButtonEx::CMFCRibbonButtonEx(UINT nID, LPCTSTR lpszText, int nSmallImageIndex, int nLargeImageIndex, BOOL bAlwaysShowDescription)
        : CMFCRibbonButton(nID, lpszText, nSmallImageIndex, nLargeImageIndex, bAlwaysShowDescription) { }
    CMFCRibbonButtonEx::CMFCRibbonButtonEx(UINT nID, LPCTSTR lpszText, HICON hIcon, BOOL bAlwaysShowDescription, HICON hIconSmall, BOOL bAutoDestroyIcon, BOOL bAlphaBlendIcon)
        : CMFCRibbonButton(nID, lpszText, hIcon, bAlwaysShowDescription , hIconSmall, bAutoDestroyIcon, bAlphaBlendIcon) { }
    void CMFCRibbonButtonEx::SetCheck(BOOL bCheck)
    {
        m_bChecked = bCheck;
    }
    void CMFCRibbonButtonEx::DrawImage(CDC* pDC, RibbonImageType type, CRect rectImage)
    {
        CMFCRibbonButton::DrawImage(pDC, type, rectImage);
        if (type == RibbonImageLarge && m_bChecked) 
            DrawCheckmark(pDC, IDB_BIG_ICON_CHECKMARK, &rectImage);
    }
    void CMFCRibbonButtonEx::DrawCheckmark(CDC* pDC, int CheckmarkResourceBitmapID, RECT *r)
    {
        HDC  hdc;
        CDC  *dc;
        CDC dcMem;
        CBitmap cbm;
    
        VERIFY(hdc = pDC->m_hDC);
        VERIFY(dc = pDC);
    
        dcMem.CreateCompatibleDC(dc);
    
        cbm.LoadBitmap(CheckmarkResourceBitmapID);
        PremultiplyBitmapAlpha(dcMem.m_hDC, cbm);
        SelectObject(dcMem.m_hDC, cbm.m_hObject);
    
        BLENDFUNCTION bf;
        bf.BlendOp = AC_SRC_OVER;
        bf.BlendFlags = 0;
        bf.SourceConstantAlpha = 255;
        bf.AlphaFormat = AC_SRC_ALPHA;
        ::AlphaBlend(hdc, r->left, r->top, r->right-r->left, r->bottom-r->top, dcMem, 0, 0, 32, 32, bf);
    
        VERIFY(dcMem.DeleteDC());
    }
    void CMFCRibbonButtonEx::PremultiplyBitmapAlpha(HDC hDC, HBITMAP hBmp)
    {
       BITMAP bm = { 0 };
       GetObject(hBmp, sizeof(bm), &bm);
       BITMAPINFO* bmi = (BITMAPINFO*) _alloca(sizeof(BITMAPINFOHEADER) + (256 * sizeof(RGBQUAD)));
       ::ZeroMemory(bmi, sizeof(BITMAPINFOHEADER) + (256 * sizeof(RGBQUAD)));
       bmi->bmiHeader.biSize = sizeof(BITMAPINFOHEADER);
       BOOL bRes = ::GetDIBits(hDC, hBmp, 0, bm.bmHeight, NULL, bmi, DIB_RGB_COLORS);
       if( !bRes || bmi->bmiHeader.biBitCount != 32 ) return;
       LPBYTE pBitData = (LPBYTE) ::LocalAlloc(LPTR, bm.bmWidth * bm.bmHeight * sizeof(DWORD));
       if( pBitData == NULL ) return;
       LPBYTE pData = pBitData;
       ::GetDIBits(hDC, hBmp, 0, bm.bmHeight, pData, bmi, DIB_RGB_COLORS);
       for( int y = 0; y < bm.bmHeight; y++ ) {
          for( int x = 0; x < bm.bmWidth; x++ ) {
             pData[0] = (BYTE)((DWORD)pData[0] * pData[3] / 255);
             pData[1] = (BYTE)((DWORD)pData[1] * pData[3] / 255);
             pData[2] = (BYTE)((DWORD)pData[2] * pData[3] / 255);
             pData += 4;
          }
       }
       ::SetDIBits(hDC, hBmp, 0, bm.bmHeight, pBitData, bmi, DIB_RGB_COLORS);
       ::LocalFree(pBitData);
    }
    

    This is how I build my 'Filter' submenu with the CMFCRibbonButtonEx items:

    m_pBtnFilter->RemoveAllSubItems();
    
    int i;
    for (i = 0; i < m_csaNames.GetSize(); i++)
    {
        std::auto_ptr<CMFCRibbonButtonEx> apBtn(new CMFCRibbonButtonEx(ID_FILTER_BASE + i, (LPCTSTR)m_csaNames[i], m_tbiIcons.ExtractIcon(m_nIcons[i], true, NULL, true, true));
        apBtn->SetToolTipText(m_csaTooltips[i]);
        m_pBtnFilter->AddSubItem(apBtn.release());
    }
    

    I'm sure you can adapt this to the main menu buttons (and the special way you want the application state have effect on the icons) without problems.