【问题标题】:How to get extra data from MFC Tab Control (TabCtrl)?如何从 MFC 选项卡控件 (TabCtrl) 获取额外数据?
【发布时间】:2018-01-02 09:09:01
【问题描述】:

我创建了一个基于 MFC 对话框的应用程序来研究选项卡控件。在选项卡控件中,可以为每个选项卡设置特定于应用程序的数据。 我正在尝试了解如何设置/检索选项卡控件的各个选项卡的数据。

这是我正在创建的示例应用程序。控件的每个选项卡都应该存储一些 GPU 信息。

据我了解,添加特定于应用程序的数据需要 3 个步骤。

  1. 创建一个用户定义的结构,其第一个成员的类型应为TCITEMHEADER

    struct GPU {
        std::wstring name;
        int busid;
    };
    struct tabData {
        TCITEMHEADER tabItemHeader;
        GPU gpu;
    };
    
  2. 告诉选项卡控件额外的字节,用户定义的结构将采用。这是我在DoDataExchange() 做的。

    int extraBytes = sizeof(tabData) - sizeof(TCITEMHEADER);
    auto status = tabCtrl1.SetItemExtra(extraBytes);
    
  3. 在添加选项卡时设置用户定义的数据。

    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 文件:

// 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();
}


头文件:

// 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();
};


解决方案总结

来自Barmak Shemirani's answer,这是我的代码无法正常工作的 3 个原因。必须阅读他的答案才能更好地理解。

  1. TCIF_PARAM 必须设置在掩码中,同时进行TCM_INSERTITEMTCM_GETITEM
  2. 我使用的是在堆栈上创建的局部变量(tabData td2;对象)。一旦超出范围,对该变量的引用就会变得无效。
  3. 在用于TCM_INSERTITEM 的结构中使用std::wstring。最好使用可以准确确定大小的数据类型(如普通的旧数据类型)。

正如 Barmak Shemirani 在 cmets 中指出的那样,TCITEMHEADER 的文档很少。他的回答提供了详尽的解释。

【问题讨论】:

    标签: visual-c++ mfc ctabctrl


    【解决方案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,不需要TCITEMHEADERSetItemExtra。你甚至可以定义std::vector&lt;GPU&gt;。示例:

    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 中设置额外的项目大小是否正确? OnInitDialogDoDataExchange 之前被调用,因此以下代码中的变量 tab 到那时可能还没有与选项卡控件关联。 tab.SetItemExtra(sizeof(tabData) - sizeof(TCITEMHEADER));
    • CDialogEx::OnInitDialog 将调用DoDataExchange,因此标签控件将在CDialogEx::OnInitDialog 之后准备好(而不是之前)。我在DoDataExchange 中尝试过,也可以。 DoDataExchange 在对话框被销毁时被第二次调用,我猜程序会调用SetItemExtra 并且不会造成任何伤害。
    猜你喜欢
    • 2015-06-14
    • 2013-12-11
    • 1970-01-01
    • 1970-01-01
    • 2011-05-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-09-17
    相关资源
    最近更新 更多