How to get extra data from MFC Tab Control (TabCtrl)?

1.1k views Asked by At

I have created an MFC dialog based application to study tab control. In a tab control it is possible to set application specific data to each tab. I am trying to understand how to set/retrieve the data for individual tabs of the tab control.

Here is a sample application I am creating. Each tab of the control is supposed to store some GPU info.

Tab Control

As I understand, there are 3 steps to add application specific data.

  1. Create a user defined structure, whose 1st member should be of type TCITEMHEADER.

    struct GPU {
        std::wstring name;
        int busid;
    };
    struct tabData {
        TCITEMHEADER tabItemHeader;
        GPU gpu;
    };
    
  2. Tell the tab control about the extra bytes, the user defined structure is going to take. This I am doing in DoDataExchange().

    int extraBytes = sizeof(tabData) - sizeof(TCITEMHEADER);
    auto status = tabCtrl1.SetItemExtra(extraBytes);
    
  3. Set user defined data while adding tabs.

    static int tabCtr = 0;
    tabData td;
    td.tabItemHeader.pszText = _T("TabX");
    td.tabItemHeader.mask = TCIF_TEXT;
    td.gpu.name = L"AMD NVIDIA";
    td.gpu.busid = 101;
    TabCtrl_InsertItem(tabCtrl1.GetSafeHwnd(), tabCtr, &td);
    

Now to get the data, we simply have to call TabCtrl_GetItem().

tabData td2;
td2.tabItemHeader.pszText = new TCHAR[20];
td2.tabItemHeader.cchTextMax = 20;

td2.tabItemHeader.mask = TCIF_TEXT;

td2.gpu.busid = 0;

TabCtrl_GetItem(tabCtrl1.GetSafeHwnd(), 0, &td2);

But as we can see in the following image. I do get the tab text (pszText member - data Item 1 in image), but not the extra data that I had associated with it previously (Data Items 2 and 3 in image).

Tab Control get Item

Which step am I missing?
Why is the structure associated with application defined data not getting populated?

Additional Info

Here is the complete code for the application.

CPP File:

// tabCtrlStackOverflowDlg.cpp : implementation file
//

#include "stdafx.h"
#include "tabCtrlStackOverflow.h"
#include "tabCtrlStackOverflowDlg.h"
#include "afxdialogex.h"
#include <string>

#ifdef _DEBUG
#define new DEBUG_NEW
#endif


struct GPU {
    std::wstring name;
    int busid;
};

struct tabData
{
    TCITEMHEADER tabItemHeader;
    GPU gpu;
};



CtabCtrlStackOverflowDlg::CtabCtrlStackOverflowDlg(CWnd* pParent /*=NULL*/)
  : CDialogEx(IDD_TABCTRLSTACKOVERFLOW_DIALOG, pParent)
{
  m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME);
}

void CtabCtrlStackOverflowDlg::DoDataExchange(CDataExchange* pDX)
{
    CDialogEx::DoDataExchange(pDX);
    DDX_Control(pDX, IDC_TAB1, tabCtrl1);

    int extraBytes = sizeof(tabData) - sizeof(TCITEMHEADER);

    auto status = tabCtrl1.SetItemExtra(extraBytes);

    wchar_t *t = status ? L"SetItemExtra() success" : L"SetItemExtra() fail";

    GetDlgItem(IDC_STATUSTEXT)->SetWindowTextW(t);
}

BEGIN_MESSAGE_MAP(CtabCtrlStackOverflowDlg, CDialogEx)
  ON_WM_PAINT()
  ON_WM_QUERYDRAGICON()
    ON_BN_CLICKED(IDADDTAB, &CtabCtrlStackOverflowDlg::OnBnClickedAddtab)
    ON_BN_CLICKED(IDC_GETITEM0, &CtabCtrlStackOverflowDlg::OnBnClickedGetitem0)
    ON_BN_CLICKED(IDCLOSE, &CtabCtrlStackOverflowDlg::OnBnClickedClose)
END_MESSAGE_MAP()


// CtabCtrlStackOverflowDlg message handlers

BOOL CtabCtrlStackOverflowDlg::OnInitDialog()
{
  CDialogEx::OnInitDialog();

  // Set the icon for this dialog.  The framework does this automatically
  //  when the application's main window is not a dialog
  SetIcon(m_hIcon, TRUE);         // Set big icon
  SetIcon(m_hIcon, FALSE);        // Set small icon

  // TODO: Add extra initialization here

  return TRUE;  // return TRUE  unless you set the focus to a control
}

// If you add a minimize button to your dialog, you will need the code below
//  to draw the icon.  For MFC applications using the document/view model,
//  this is automatically done for you by the framework.

void CtabCtrlStackOverflowDlg::OnPaint()
{
  if (IsIconic())
  {
      CPaintDC dc(this); // device context for painting

      SendMessage(WM_ICONERASEBKGND, reinterpret_cast<WPARAM>(dc.GetSafeHdc()), 0);

      // Center icon in client rectangle
      int cxIcon = GetSystemMetrics(SM_CXICON);
      int cyIcon = GetSystemMetrics(SM_CYICON);
      CRect rect;
      GetClientRect(&rect);
      int x = (rect.Width() - cxIcon + 1) / 2;
      int y = (rect.Height() - cyIcon + 1) / 2;

      // Draw the icon
      dc.DrawIcon(x, y, m_hIcon);
  }
  else
  {
      CDialogEx::OnPaint();
  }
}

// The system calls this function to obtain the cursor to display while the user drags
//  the minimized window.
HCURSOR CtabCtrlStackOverflowDlg::OnQueryDragIcon()
{
  return static_cast<HCURSOR>(m_hIcon);
}



void CtabCtrlStackOverflowDlg::OnBnClickedAddtab()
{
    static int tabCtr = 0;
    tabData td;
    td.tabItemHeader.pszText = _T("TabX");
    td.tabItemHeader.mask = TCIF_TEXT;

    td.gpu.name = L"AMD NVIDIA";
    td.gpu.busid = 101;

    int status = TabCtrl_InsertItem(tabCtrl1.GetSafeHwnd(), tabCtr, &td);

    wchar_t *t = L"";

    if (status == -1)
    {
        t = L"TabCtrl_InsertItem() Fail";
    }
    else
    {
        t = L"TabCtrl_InsertItem() success";
    }

    GetDlgItem(IDC_STATUSTEXT)->SetWindowTextW(t);
    tabCtr++;
}


void CtabCtrlStackOverflowDlg::OnBnClickedGetitem0()
{
    tabData td2;
    td2.tabItemHeader.pszText = new TCHAR[20];
    td2.tabItemHeader.cchTextMax = 20;

    td2.tabItemHeader.mask = TCIF_TEXT;

    td2.gpu.busid = 0;

    if (TabCtrl_GetItem(tabCtrl1.GetSafeHwnd(), 0, &td2) == TRUE)
    {
        std::wstring text = td2.tabItemHeader.pszText;
        text += std::wstring(L" ") + td2.gpu.name;
        GetDlgItem(IDC_STATUSTEXT)->SetWindowTextW(text.c_str());
    }
    else
    {
        GetDlgItem(IDC_STATUSTEXT)->SetWindowTextW(_T("TabCtrl_GetItem()
error"));
    }
}


void CtabCtrlStackOverflowDlg::OnBnClickedClose()
{
    CDialog::OnCancel();
}


Header File:

// tabCtrlStackOverflowDlg.h : header file
//

#pragma once
#include "afxcmn.h"


// CtabCtrlStackOverflowDlg dialog
class CtabCtrlStackOverflowDlg : public CDialogEx
{
// Construction
public:
  CtabCtrlStackOverflowDlg(CWnd* pParent = NULL); // standard constructor

// Dialog Data
#ifdef AFX_DESIGN_TIME
  enum { IDD = IDD_TABCTRLSTACKOVERFLOW_DIALOG };
#endif

  protected:
  virtual void DoDataExchange(CDataExchange* pDX);    // DDX/DDV support


// Implementation
protected:
  HICON m_hIcon;

  // Generated message map functions
  virtual BOOL OnInitDialog();
  afx_msg void OnPaint();
  afx_msg HCURSOR OnQueryDragIcon();
  DECLARE_MESSAGE_MAP()
public:
    CTabCtrl tabCtrl1;
    afx_msg void OnBnClickedAddtab();
    afx_msg void OnBnClickedGetitem0();
    afx_msg void OnBnClickedClose();
};


Solution Summary

From Barmak Shemirani's answer here are the 3 reasons my code wasn't working. Must read his answer for better understanding.

  1. TCIF_PARAM must be set in mask, while doing TCM_INSERTITEM, and TCM_GETITEM.
  2. I was using local variables created on stack (tabData td2; object). The reference to this variable was becoming invalid as soon as it was going out of scope.
  3. Using std::wstring in the structure being used for TCM_INSERTITEM. It is better to use data types whose size can be accurately be determined (like plain old data types.).

As Barmak Shemirani points out in comments, the documentation for TCITEMHEADER is scarce. His answer provides a thorough explanation.

1

There are 1 answers

5
Barmak Shemirani On BEST ANSWER

Conflict with documentation

Documentation for TCITEMHEADER does not mention using TCIF_PARAM flag. Maybe that's a mistake in documention!


It's better if SetItemExtra is moved to OnInitDialog after default procedure is called. This ensures SetItemExtra is called only once when control is empty.

The structure GPU has a std::wstring member whose data size is unknown at the start. TCM_INSERTITEM cannot make a copy of this data unless you have a simple POD structure.

To store the data in the tab, replace std::wstring with wchar_t name[100] so that data is a simple POD structure with fixed size.

struct GPU
{
    //std::wstring name;
    wchar_t name[100];
    int busid;
};

struct tabData
{
    TCITEMHEADER tabItemHeader;
    GPU gpu;
};

void CMyDialog::OnBnClickedAddtab()
{
    int index = tab.GetItemCount();
    wchar_t tabname[50];
    wsprintf(tabname, L"Tab %d", index);
    tabData sdata = { 0 };
    sdata.tabItemHeader.mask = TCIF_TEXT | TCIF_PARAM;
    sdata.tabItemHeader.pszText = tabname;
    wsprintf(sdata.gpu.name, L"AMD NVIDIA %d", index);
    sdata.gpu.busid = 101;
    tab.SendMessage(TCM_INSERTITEM, index, (LPARAM)(TCITEMHEADER*)(&sdata));
}

void CMyDialog::OnBnClickedGetitem0()
{
    int index = tab.GetCurSel();
    tabData data = { 0 };
    wchar_t buf[20] = { 0 };
    data.tabItemHeader.pszText = buf;
    data.tabItemHeader.cchTextMax = sizeof(buf)/sizeof(wchar_t);
    data.tabItemHeader.mask = TCIF_TEXT | TCIF_PARAM;
    if(tab.SendMessage(TCM_GETITEM, index, (LPARAM)(TCITEMHEADER*)(&data)))
    {
        CString str;
        str.Format(L"%d %s", data.gpu.busid, data.gpu.name);
        GetDlgItem(IDC_STATIC1)->SetWindowText(str);
    }
}


Alternative method:

If std::wstring name; cannot be replaced with wchar_t buffer, we have to define a separate permanent data, for example using std::vector. Then we use the lParam value in TCITEM to point to the vector.

This method only needs the standard 4 bytes of lParam, it doesn't require TCITEMHEADER and SetItemExtra. You can even define std::vector<GPU>. Example:

std::vector<tabData> m_data;

BOOL CMyDialog::OnInitDialog()
{
    CDialogEx::OnInitDialog();

    tabData data;

    data.gpu.name = L"AMD NVIDIA1";
    data.gpu.busid = 101;
    m_data.push_back(data);

    data.gpu.name = L"AMD NVIDIA2";
    data.gpu.busid = 102;
    m_data.push_back(data);

    return TRUE;
}

void CMyDialog::OnBnClickedAddtab()
{
    static int tabCtr = 0;
    if(tabCtr >= (int)m_data.size())
        return;

    TCITEM item = { 0 };
    item.pszText = _T("TabX");
    item.mask = TCIF_TEXT | TCIF_PARAM;
    item.lParam = (LPARAM)&m_data[tabCtr];
    tab.InsertItem(tabCtr, &item);

    tabCtr++;
}

void CMyDialog::OnBnClickedGetitem0()
{
    TCITEM item = { 0 };
    item.mask = TCIF_TEXT | TCIF_PARAM;
    if(tab.GetItem(tab.GetCurSel(), &item) == TRUE)
    {
        tabData* ptr = (tabData*)item.lParam;
        CString str;
        str.Format(L"%d %s", ptr->gpu.busid, ptr->gpu.name.c_str());
        GetDlgItem(IDC_STATIC1)->SetWindowText(str);
    }
}