如何从 MFC 选项卡控件 (TabCtrl) 获取额外数据?
Posted
技术标签:
【中文标题】如何从 MFC 选项卡控件 (TabCtrl) 获取额外数据?【英文标题】:How to get extra data from MFC Tab Control (TabCtrl)? 【发布时间】:2018-01-02 09:09:01 【问题描述】:我创建了一个基于 MFC 对话框的应用程序来研究选项卡控件。在选项卡控件中,可以为每个选项卡设置特定于应用程序的数据。 我正在尝试了解如何设置/检索选项卡控件的各个选项卡的数据。
这是我正在创建的示例应用程序。控件的每个选项卡都应该存储一些 GPU 信息。
据我了解,添加特定于应用程序的数据需要 3 个步骤。
创建一个用户定义的结构,其第一个成员的类型应为TCITEMHEADER
。
struct GPU
std::wstring name;
int busid;
;
struct tabData
TCITEMHEADER tabItemHeader;
GPU gpu;
;
告诉选项卡控件额外的字节,用户定义的结构将采用。这是我在DoDataExchange()
做的。
int extraBytes = sizeof(tabData) - sizeof(TCITEMHEADER);
auto status = tabCtrl1.SetItemExtra(extraBytes);
在添加选项卡时设置用户定义的数据。
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);
现在要获取数据,我们只需调用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);
但正如我们在下图中看到的那样。我确实获得了选项卡文本(pszText 成员 - 图像中的数据项 1),但没有获得我之前与之关联的额外数据(图像中的数据项 2 和 3)。
我错过了哪一步? 为什么没有填充与应用程序定义的数据相关的结构?
其他信息
这是应用程序的完整代码。
CPP 文件:
// tabCtrl***Dlg.cpp : implementation file // #include "stdafx.h" #include "tabCtrl***.h" #include "tabCtrl***Dlg.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; ; CtabCtrl***Dlg::CtabCtrl***Dlg(CWnd* pParent /*=NULL*/) : CDialogEx(IDD_TABCTRL***_DIALOG, pParent) m_hIcon = AfxGetApp()->LoadIcon(IDR_MAINFRAME); void CtabCtrl***Dlg::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(CtabCtrl***Dlg, CDialogEx) ON_WM_PAINT() ON_WM_QUERYDRAGICON() ON_BN_CLICKED(IDADDTAB, &CtabCtrl***Dlg::OnBnClickedAddtab) ON_BN_CLICKED(IDC_GETITEM0, &CtabCtrl***Dlg::OnBnClickedGetitem0) ON_BN_CLICKED(IDCLOSE, &CtabCtrl***Dlg::OnBnClickedClose) END_MESSAGE_MAP() // CtabCtrl***Dlg message handlers BOOL CtabCtrl***Dlg::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 CtabCtrl***Dlg::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 CtabCtrl***Dlg::OnQueryDragIcon() return static_cast<HCURSOR>(m_hIcon); void CtabCtrl***Dlg::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 CtabCtrl***Dlg::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 CtabCtrl***Dlg::OnBnClickedClose() CDialog::OnCancel();
头文件:
// tabCtrl***Dlg.h : header file // #pragma once #include "afxcmn.h" // CtabCtrl***Dlg dialog class CtabCtrl***Dlg : public CDialogEx // Construction public: CtabCtrl***Dlg(CWnd* pParent = NULL); // standard constructor // Dialog Data #ifdef AFX_DESIGN_TIME enum IDD = IDD_TABCTRL***_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(); ;
解决方案总结
来自Barmak Shemirani's answer,这是我的代码无法正常工作的 3 个原因。必须阅读他的答案才能更好地理解。
TCIF_PARAM
必须设置在掩码中,同时进行TCM_INSERTITEM
和TCM_GETITEM
。
我使用的是在堆栈上创建的局部变量(tabData td2;对象)。一旦超出范围,对该变量的引用就会变得无效。
在用于TCM_INSERTITEM
的结构中使用std::wstring。最好使用可以准确确定大小的数据类型(如普通的旧数据类型)。
正如 Barmak Shemirani 在 cmets 中指出的那样,TCITEMHEADER
的文档很少。他的回答提供了详尽的解释。
【问题讨论】:
【参考方案1】:与文档冲突
TCITEMHEADER 的文档没有提到使用 TCIF_PARAM
标志。也许这是文档中的错误!
最好在调用默认过程后将SetItemExtra
移动到OnInitDialog
。这确保了SetItemExtra
在控件为空时仅被调用一次。
结构GPU
有一个std::wstring
成员,其数据大小在开始时是未知的。 TCM_INSERTITEM
不能复制这些数据,除非你有一个简单的 POD 结构。
要将数据存储在选项卡中,请将std::wstring
替换为wchar_t name[100]
,这样数据就是一个固定大小的简单POD 结构。
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);
替代方法:
如果std::wstring name;
不能替换为wchar_t
缓冲区,我们必须定义一个单独的永久数据,例如使用std::vector
。然后我们使用TCITEM
中的lParam
值来指向向量。
这个方法只需要标准的4字节lParam
,不需要TCITEMHEADER
和SetItemExtra
。你甚至可以定义std::vector<GPU>
。示例:
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);
【讨论】:
您指出 td 是临时变量,并且 gpu 有一个大小未知的成员。这两个提示帮助我找出正确的解决方案。除了这两个之外,我在插入/获取数据时还必须使用 TCIF_PARAM 标志。我不想使用 TCITEM 结构,而是使用用户定义的结构,如 tabData。如果您可以在答案中加入如何使用 tabData 而不是 TCITEM,并说明在这种情况下 TCIF_PARAM 标志的用法,我会将您的答案标记为已接受。否则您的回答很有帮助,只需稍加补充即可。 你的程序有什么限制?我添加了第二种方法,它使用TCITEMHEADER
存储在选项卡中。但是你不能在第二种方法中使用std::wstring
。如果您能够在应用程序中使用永久数据(例如m_data
),那么避免使用TCITEMHEADER
会更安全、更容易。 TCITEMHEADER
的文档很少,在 TCITEMHEADER
上的网络搜索让我回到了这个问题。
我想使用用户定义的结构,以及TCITEMHEADER
。多亏了你,我的代码现在可以工作了。您指出文档中没有提到 TCIF_PARAM
是对的。
在OnInitDialog
中设置额外的项目大小是否正确? OnInitDialog
在 DoDataExchange
之前被调用,因此以下代码中的变量 tab
到那时可能还没有与选项卡控件关联。 tab.SetItemExtra(sizeof(tabData) - sizeof(TCITEMHEADER));
CDialogEx::OnInitDialog
将调用DoDataExchange
,因此标签控件将在CDialogEx::OnInitDialog
之后准备好(而不是之前)。我在DoDataExchange
中尝试过,也可以。 DoDataExchange
在对话框被销毁时被第二次调用,我猜程序会调用SetItemExtra
并且不会造成任何伤害。以上是关于如何从 MFC 选项卡控件 (TabCtrl) 获取额外数据?的主要内容,如果未能解决你的问题,请参考以下文章