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

255 views Asked by At

I've got a "default looking" dialog box like the following: enter image description here

And I'm attempting to modify the tabs and insert a RichEditCtrl in the first tab.

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

The sample I'm modifying previously had only used the RichTextCtrl and "created" it inside of a "placeholder" text box. It worked great, but I wanted to shove that RichTextCtrl into a tab, and create another tab to display some data. The problem is that I now just get 2 blank tabs. I know that the parent dialog settings "Clip Children" and "Clip Siblings" may matter, but I'm not sure which if I need, if either. I also know that my RichEditCtrl still exists because I'm still sending data to it, but it's certainly not displaying.

This piece of my program isn't even really that urgent, and I am just trying to get this to work on principal at this point...

1

There are 1 answers

2
IInspectable On BEST ANSWER

Tab Controls create the illusion, that the dividers and the display area were part of the same control. That's not the case. The tab control is really just the labels, plus the placeholder display area. Bringing the display area's contents to live is the responsibility of the application.

In general, the following steps are required to implement a fully functional tab control:

  1. Create the tab control plus labels.
  2. Create the display area's child controls. It is common to place the controls comprising a single "page" in a dialog.
  3. Subscribe to the TCN_SELCHANGE message, and dynamically update the visibility of the controls, i.e. hide all controls that aren't part of the current "page" and show all controls that are. Placing all controls for a "page" inside a dialog makes this easier by only requiring to toggle the visibility of the dialogs.

This is a rough overview of how tab controls work. Given your code, there are some things you need to change. Specifically, the following need to be taken care of:

  1. The tab control referenced by IDC_TAB1 needs to have the WS_CLIPCHILDREN style, so that the display area doesn't cover the child controls.
  2. m_richEditCtrl needs to be created with the WS_CHILD style.
  3. Calculate the size of the display area using CTabCtrl::AdjustRect and use that to size the m_richEditCtrl to fill the entire display area (if that is what you want).

With those changes you should see a tab control whose display area is filled by a Rich Edit control. Switching between tabs doesn't change the contents of the display area just yet. That's something you'll need to implement as required by your application.


The following code sample is based on a wizard-generated dialog-based application named MfcTabCtrl. The generated dialog resource (IDD_MFCTABCTRL_DIALOG) had all content removed, leaving just a blank dialog template.

Likewise, the main dialog implementation had most of its functionality stripped, leaving just the vital parts. This is the 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{};
};

The implementation file MfcTabCtrlDlg.cpp isn't very extensive either:

#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
}

The result is a dialog holding a tab control with two labels. Visibility of the contained rich edit control is toggled in the TCN_SELCHANGE notification handler, showing it only when the first tab is selected. A more complex GUI would update the visibility of all controls based on the currently selected tab here as well.

Note that the controls inside the tab control's display area are never destroyed during the dialog's life time. This is usually desirable to persist user data even when switching between tabs. If necessary it is also possible to destroy and (re-)create some or all of the child controls when switching tabs.