Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / desktop / MFC

CSplitButton with Images

5.00/5 (22 votes)
19 May 2021CPOL3 min read 14.6K   856  
How to enhance the MFC CSplitButton control to support images
I was looking for a way to add a button which will have pull down menu options, each of them having a PNG image next to the option's text.

Introduction

The CSplitButton is an MFC control which performs a default behavior when a user clicks the main part of the button and displays a drop-down menu when a user clicks the drop-down arrow of the button.

That works great however there aren't any examples for using images for the main and drop-down menu options.

Background

I was working on a commercial project where I wanted to add a "Save" button which will allow various types of files to be saved, so pressing "Save" will save as the default format (Excel) in my case, while it’s also possible to select other formats via a drop-down menu options, which will be CSV or the default (Excel).
I couldn’t find any source code snippet which does that.

Image 1

The SGSplitImageButton Class

I created a new SGSplitImageButton class which is derived from CSplitButton.

C++
class SGSplitImageButton : public CSplitButton
{
    DECLARE_DYNAMIC(SGSplitImageButton)

public:
    SGSplitImageButton();
    virtual ~SGSplitImageButton();
    CMenu *menu;

    void InsertMenu(CString title, UINT imgId, UINT menuID);
    void SetDropDown();

protected:
    DECLARE_MESSAGE_MAP()
public:
    afx_msg int OnCreate(LPCREATESTRUCT lpCreateStruct);
};

InsertMenu - allows inserting pull-down menu options which can have not only text but also PNG images.

To add an image, the image needs to be defined in the .rc file as follows:

<IMAGE ID> "PNG" <path to image file>

For example:

IDB_SAVESPLIT_XLS       PNG                     "res\\save_excel.png"

Then when the InsertMenu() function is called, the imgId would be IDB_SAVESPLIT_XLS.

For example:

C++
m_SaveWorksheet.InsertMenu(_T("Save Excel"), IDB_SAVESPLIT_XLS, WM_SAVE_XSL);

SetDropDown - calls CSplitButton's SetDropDownMenu() function.

The InsertMenu Function

The following function inserts the pull-down menu, along with the associate images. We are using CPngImage which is an internal class of MFC.

C++
void SGSplitImageButton::InsertMenu(CString title, UINT imgId, UINT menuID)
{
    CPngImage btnImg;
    btnImg.Load(imgId, nullptr);
    menu->AppendMenu(MF_STRING, menuID, title);
    menu->SetMenuItemBitmaps(menuID, MF_BYCOMMAND, &btnImg, &btnImg);
    btnImg.Detach();
}

Initializing Our Button

We initialize our button as follows:

C++
CPngImage btnImg;
btnImg.Load(IDB_MAINIMAGE, nullptr);
m_MySplit.SendMessageW(BM_SETIMAGE, (WPARAM)IMAGE_BITMAP, (LPARAM)btnImg.GetSafeHandle());

m_MySplit.InsertMenu(_T("Menu Option 1"), IDB_MENU1, WM_MENUOPTION1);
m_MySplit.InsertMenu(_T("Menu Option 2"), IDB_MENU2, WM_MENUOPTION2);
m_MySplit.InsertMenu(_T("Menu Option 3"), IDB_MENU3, WM_MENUOPTION3);

m_MySplit.SetDropDown();

We use four images in this example:

  • IDB_MAINIMAGE will appear on the button next to its text.
  • IDB_MENU1 will appear on the first menu option next to its text.
  • IDB_MENU2 will appear on the second menu option next to its text.
  • IDB_MENU3 will appear on the third menu option next to its text.

Receiving Messages Per Menu Option

We would also want to be notified when a menu option is selected by the user. To do so, we define private messages per menu option.

We use WM_USER + 1, 2, etc. You can learn more about WM_USER here.

For example:

C++
#define WM_SAVE_XSL             WM_USER + 5
#define WM_SAVE_CSV             WM_USER + 6

or:

C++
#define WM_MENUOPTION1          WM_USER + 1
#define WM_MENUOPTION2          WM_USER + 2
#define WM_MENUOPTION3          WM_USER + 3

Now, let's see how and when we use these custom messages.

First, let's check the BEGIN_MESSAGE_MAP macro.

We map messages (menu options selected), and also the press of the button itself, IDC_SPLIT1).

C++
BEGIN_MESSAGE_MAP(SGSplitImageButtonDlg, CDialogEx)
    ON_WM_SYSCOMMAND()
    ON_WM_PAINT()
    ON_WM_QUERYDRAGICON()
    ON_COMMAND(WM_MENUOPTION1, &OnMenuOption1)    // Menu option 1
    ON_COMMAND(WM_MENUOPTION2, &OnMenuOption2)    // Menu option 2
    ON_COMMAND(WM_MENUOPTION3, &OnMenuOption3)    // Menu option 3
    // Main button pressed
    ON_BN_CLICKED(IDC_SPLIT1, &SGSplitImageButtonDlg::OnBnClickedSplit1)
END_MESSAGE_MAP()

Next, all we need is to create functions to accept the following events:

  1. Button was clicked:
    C++
    OnBnClickedSplit1()
  2. A menu option was selected:
    C++
    OnMenuOption1()
    
    OnMenuOption2()

    and:

    C++
    OnMenuOption3()

Tips for Creating and Using Custom Controls

Generally speaking, when a custom or inherited control is created, we would still want to use the Resource Editor which is available when clicking the dialog from the Resource View.

To make that possible, please do the following:

  1. Create a normal CSplitButton and place it on the dialog.
  2. Give the control a meaningful ID in the Properties tab.

    Image 2

  3. Right click the control and press "Add Variable". Give the control a variable name.

    In our case, the ID is IDC_BUTTON_SAVE and the variable is m_SaveWorkSheet.

    Image 3

  4. Search for the variable name you have given. You will find it in the dialog's header file.

The Resource Editor will add the following line:

C++
CSplitButton m_SaveWorksheet;

and you need to change it to:

C++
SGSplitImageButton m_SaveWorksheet;

or:

C++
SGSplitImageButton m_MySplit;;

(in the attached source code).

You will need to include "SGSplitImageButton.h".

The Resource Editor should have already added the following line to the dialog .cpp file:

C++
DDX_Control(pDX, IDC_BUTTON_SAVE, m_SaveWorksheet);

or:

C++
DDX_Control(pDX, IDC_SPLIT1, m_MySplit);

(in the attached source code).

So now, you have a custom control which can be used like a normal control from the IDE.

Creating a Default Action

In order to create a default action for the button, and change it to the last selected menu, I added the following member function.

C++
void SGSplitImageButton::SetMainImage(int MenuImageNumber)
{
    CPngImage btnImg;
    if (m_ImageIDs.size() > MenuImageNumber)
    {
        btnImg.Load(m_ImageIDs[MenuImageNumber], nullptr);
        this->SendMessageW(BM_SETIMAGE, (WPARAM)IMAGE_BITMAP, (LPARAM)btnImg.GetSafeHandle());
    }
}

and created a vector (m_ImageIDs) which holds the bitmap IDs of all menus.

We need to add:

C++
#include <vector>

to stdafx.h.

Next, we need to update our InsertMenu function and add:

C++
m_ImageIDs.insert(m_ImageIDs.end(), imgId);

to it. Now InsertMenu will look like this:

C++
void SGSplitImageButton::InsertMenu(CString title, UINT imgId, UINT menuID)
{
    CPngImage btnImg;
    btnImg.Load(imgId, nullptr);
    menu->AppendMenu(MF_STRING, menuID, title);
    menu->SetMenuItemBitmaps(menuID, MF_BYCOMMAND, &btnImg, &btnImg);
    m_ImageIDs.insert(m_ImageIDs.end(), imgId);
    btnImg.Detach();
}

Last, we call SetMainImage whenever a menu is selected, changing the button's image to the image of the selected menu:

C++
void SGSplitImageButtonDlg::OnMenuOption1()
{
    m_MySplit.SetMainImage(0);
}

void SGSplitImageButtonDlg::OnMenuOption2()
{
    m_MySplit.SetMainImage(1);
}

void SGSplitImageButtonDlg::OnMenuOption3()
{
    m_MySplit.SetMainImage(2);
}

It should then look like this:

Image 4

History

  • 19th May, 2021: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)