CTab_Ctrl class 是否需要额外的 属性 来绘制其 "window"?

Does the CTab_Ctrl class require an additional property to draw in its "window"?

我有一个“默认外观”对话框,如下所示:

我正在尝试修改选项卡并在第一个选项卡中插入 RichEditCtrl。

    InitCommonControlsEx;
    CWnd* pTab = GetDlgItem(IDC_TAB1);
    if (pTab) {
        CRect rect;

        m_TabCtrl = (CTabCtrl*)pTab;
        m_TabCtrl->GetClientRect(&rect);
        
        m_TabCtrl->InsertItem(0, "Stats");
        m_TabCtrl->InsertItem(1, "Settings");
        BOOL getRect = m_TabCtrl->GetItemRect(0, &rect);

        if (!m_richEditCtrl.Create(WS_VISIBLE | ES_READONLY | ES_MULTILINE | ES_AUTOHSCROLL | WS_HSCROLL | ES_AUTOVSCROLL | WS_VSCROLL, rect, m_TabCtrl, 0))
            return FALSE;

        m_font.CreateFont(-11, 0, 0, 0, FW_REGULAR, 0, 0, 0, BALTIC_CHARSET, 0, 0, 0, 0, "Courier New");
        m_richEditCtrl.SetFont(&m_font);
    }

我之前修改的示例只使用了 RichTextCtrl 并在“占位符”文本框内“创建”了它。它工作得很好,但我想将 RichTextCtrl 推到一个选项卡中,并创建另一个选项卡来显示一些数据。问题是我现在只得到 2 个空白标签。我知道父对话框设置“Clip Children”和“Clip Siblings”可能很重要,但我不确定是否需要哪个。我也知道我的 RichEditCtrl 仍然存在,因为我还在向它发送数据,但它肯定没有显示。

我的程序的这一部分甚至不是那么紧急,此时我只是想让它在主体上运行...

Tab Controls 造成错觉,即分隔线和显示区域是同一控件的一部分。事实并非如此。选项卡控件实际上只是标签,加上占位符显示区域。使显示区域的内容生效是应用程序的责任。

一般来说,实现一个功能齐全的选项卡控件需要以下步骤:

  1. 创建选项卡控件和标签。
  2. 创建显示区域的 child 控件。通常将包含单个“页面”的控件放置在对话框中。
  3. 订阅 TCN_SELCHANGE 消息,并动态更新控件的可见性,即隐藏所有不属于当前“页面”的控件并显示所有包含的控件。将“页面”的所有控件放置在对话框中,只需切换对话框的可见性,就可以更轻松地完成此操作。

这是对选项卡控件工作原理的粗略概述。根据您的代码,您需要更改一些内容。具体需要注意以下几点:

  1. IDC_TAB1引用的选项卡控件需要有WS_CLIPCHILDREN样式,这样显示区域就不会覆盖child控件。
  2. m_richEditCtrl 需要用 WS_CHILD 样式创建。
  3. 使用 CTabCtrl::AdjustRect 计算显示区域的大小,并使用它来调整 m_richEditCtrl 的大小以填充整个显示区域(如果这是您想要的)。

进行这些更改后,您应该会看到一个选项卡控件,其显示区域由 Rich Edit 控件填充。在选项卡之间切换不会改变显示区域的内容。这是您需要根据应用程序的要求实施的内容。


以下代码示例基于名为 MfcTabCtrl 的 wizard-generated dialog-based 应用程序。生成的对话框资源 (IDD_MFCTABCTRL_DIALOG) 已删除所有内容,只留下一个空白对话框模板。

同样,主对话框实现的大部分功能都被剥离,只留下重要部分。这是 MfcTabCtrlDlg.h header:

#pragma once

#include "afxdialogex.h"

// Control identifiers
UINT constexpr IDC_TAB{ 100 };
UINT constexpr IDC_RICH_EDIT{ 101 };

class CMfcTabCtrlDlg : public CDialogEx
{
public:
    CMfcTabCtrlDlg(CWnd* pParent = nullptr);

protected:
    afx_msg void OnSize(UINT nType, int cx, int cy);
    afx_msg void OnTabChanged(NMHDR* pNMHDR, LRESULT* pResult);

    // Convenience implementation to calculate the display area
    RECT GetDisplayArea();

    virtual BOOL OnInitDialog();
    DECLARE_MESSAGE_MAP()

private:
    CTabCtrl m_TabCtrl{};
    CRichEditCtrl m_richEditCtrl{};
};

实现文件MfcTabCtrlDlg.cpp也不是很广泛:

#include "MfcTabCtrlDlg.h"

CMfcTabCtrlDlg::CMfcTabCtrlDlg(CWnd* pParent /*=nullptr*/)
    : CDialogEx(IDD_MFCTABCTRL_DIALOG, pParent)
{
}

void CMfcTabCtrlDlg::OnSize(UINT nType, int cx, int cy)
{
    CDialogEx::OnSize(nType, cx, cy);

    // Resize tab control only after it has been created
    if (IsWindow(m_TabCtrl)) {
        m_TabCtrl.MoveWindow(0, 0, cx, cy);
        // Determine display area
        auto const disp_area{GetDisplayArea()};
        // Resize child control(s) to cover entire display area
        if (!IsRectEmpty(&disp_area) && IsWindow(m_richEditCtrl)) {
            m_richEditCtrl.MoveWindow(&disp_area);
        }
    };
}

void CMfcTabCtrlDlg::OnTabChanged(NMHDR* /*pNMHDR*/, LRESULT* pResult)
{
    auto const cur_sel{ m_TabCtrl.GetCurSel() };
    switch (cur_sel) {
    // First tab selected
    case 0:
        m_richEditCtrl.ShowWindow(SW_SHOW);
        break;
    // Second tab selected
    case 1:
        m_richEditCtrl.ShowWindow(SW_HIDE);
        break;
    }

    // Allow other subscribers to handle this message
    *pResult = FALSE;
}

// Returns the display area in client coordinates relative to the dialog.
// Returns an empty rectangle on failure.
RECT CMfcTabCtrlDlg::GetDisplayArea()
{
    RECT disp_area{};

    if (IsWindow(m_TabCtrl)) {
        m_TabCtrl.GetWindowRect(&disp_area);
        m_TabCtrl.AdjustRect(FALSE, &disp_area);
        this->ScreenToClient(&disp_area);
    }
    
    return disp_area;
}

// The message map registers only required messages
BEGIN_MESSAGE_MAP(CMfcTabCtrlDlg, CDialogEx)
    ON_WM_SIZE()
    ON_NOTIFY(TCN_SELCHANGE, IDC_TAB, &CMfcTabCtrlDlg::OnTabChanged)
END_MESSAGE_MAP()


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

    // Set up tab control to cover entire client area
    RECT client{};
    GetClientRect(&client);
    m_TabCtrl.Create(WS_VISIBLE | WS_CHILD | WS_CLIPCHILDREN, client, this, IDC_TAB);
    m_TabCtrl.InsertItem(0, L"Stats");
    m_TabCtrl.InsertItem(1, L"Settings");

    // Set up rich edit control.
    // The WS_BORDER style is set strictly to make it visible.
    auto const disp_area{ GetDisplayArea() };
    m_richEditCtrl.Create(WS_BORDER | WS_VISIBLE | WS_CHILD,
                          disp_area, &m_TabCtrl, IDC_RICH_EDIT);

    return TRUE;  // Let the system manage focus for this dialog
}

结果是一个对话框,其中包含一个带有两个标签的选项卡控件。包含的丰富编辑控件的可见性在 TCN_SELCHANGE 通知处理程序中切换,仅在选择第一个选项卡时显示。更复杂的 GUI 也会根据此处当前选择的选项卡更新所有控件的可见性。

请注意,选项卡控件显示区域内的控件在对话框的生命周期内永远不会被销毁。即使在选项卡之间切换时,通常也希望保留用户数据。如有必要,还可以在切换选项卡时销毁和(重新)创建部分或全部 child 控件。