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.
As I understand, there are 3 steps to add application specific data.
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; };
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);
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).
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.
TCIF_PARAM
must be set in mask, while doingTCM_INSERTITEM
, andTCM_GETITEM
.- 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.
- 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.
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 toOnInitDialog
after default procedure is called. This ensuresSetItemExtra
is called only once when control is empty.The structure
GPU
has astd::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
withwchar_t name[100]
so that data is a simple POD structure with fixed size.Alternative method:
If
std::wstring name;
cannot be replaced withwchar_t
buffer, we have to define a separate permanent data, for example usingstd::vector
. Then we use thelParam
value inTCITEM
to point to the vector.This method only needs the standard 4 bytes of
lParam
, it doesn't requireTCITEMHEADER
andSetItemExtra
. You can even definestd::vector<GPU>
. Example: