Tab Control Question

Started by Richard Kelly, April 25, 2026, 04:07:26 AM

Previous topic - Next topic

Richard Kelly

I'm trying to get my form working without DDT and am unsuccessful in adding a listview to a page.

Creating the tab control

DIM pTabPage1 AS CTabPage PTR = NEW CTabPage
pTabPage1->InsertPage(hTab, 0, "Date Types", -1, @TabPage1_WndProc)
DIM pTabPage2 AS CTabPage PTR = NEW CTabPage
pTabPage2->InsertPage(hTab, 1, "Holidays", -1, @TabPage2_WndProc)
DIM pTabPage3 AS CTabPage PTR = NEW CTabPage
pTabPage3->InsertPage(hTab, 2, "Events", -1, @TabPage3_WndProc)
DIM pTabPage4 AS CTabPage PTR = NEW CTabPage
pTabPage4->InsertPage(hTab, 3, "Miscellaneous", -1, @TabPage4_WndProc) 

Adding the listview control to first page

FUNCTION TabPage1_WndProc (BYVAL hwnd AS HWND, BYVAL uMsg AS UINT, BYVAL wParam AS WPARAM, BYVAL lParam AS LPARAM) AS LRESULT

   SELECT CASE uMsg

    CASE WM_CREATE
        DIM pTabPage AS CTabPage PTR = AfxCTabPagePtr(GetParent(hwnd), 0)
        DIM LVColumn AS LVCOLUMN
        DIM hListView AS HWND = pTabPage->AddControl("LISTVIEW", hwnd, IDC_LVPAGE1, "", 9, 408, 688, 20)
        DIM wColumnName AS WSTRING * 260
        LVColumn.mask = LVCF_TEXT
        wColumnName = "Name"
        LVColumn.pszText = @wColumnName
        ShowWindow(hListView,SW_SHOW)

The tab control shows up with all the tabs empty.

hajubu

#1
hi Richard,
I just tried to add a Listview as a  fourth-tabpage to the CW_TabCtrl_01.bas, (v2025+)

by adapting  an extract of the CW_Listview_01.bas to fit the "needs" in the Tab-sample.

Both original ones can be found in SDK-Templates of Jose's Github repos (AfxNova-main).

I do recommend to follow the template - and add your needs.

For an easy use I give you my results here.

b.r.

P.S. The TabCtrl_01b.bas ist the "newer 2026) with J.R. evolution for  Ctab, CListbox, CCombox. (see all #includes)

Hi, Jose  - Thanks - attachment exchanged - for improvement reason -
!!  Listview tabpage should (must)  fit to the "main Window's ClientSize. and shows the scrollbars"

José Roca

#2
Hi hajubu,

Thanks very much for your reply.

Your example works very well, but the reason why the ListView does not show its scrollbars is that the control is not being resized to the actual client area of the tab page. The fixed values: pTabPage->SetWindowPos hListView, NULL, 0, 0, 770, 365, SWP_NOZORDER only work by coincidence if the tab page happens to have exactly that size.

To make the ListView fill the entire tab page (and therefore show scrollbars when needed), you should calculate the real width and height of the page window:

Replace   pTabPage->SetWindowPos hListView, NULL, 0, 0, 770, 365, SWP_NOZORDER with:

' // Get the width and height of the tab page
DIM w AS LONG = pTabPage->Width
DIM h AS LONG = pTabPage->Height
' // Set the listview position and visible size
pTabPage->SetWindowPos hListView, NULL, 0, 0, w, h, SWP_NOZORDER

This ensures that:

* the ListView always fits the tab page

* scrollbars appear correctly

* the layout adapts to any window size or DPI setting

Everything else in your example is excellent — you adapted the ListView code exactly as intended. This is the only adjustment needed to make it behave perfectly.


hajubu

#3
Hi Jose,
Thanks - you are right - my fault - used the extracted snippet from the Listview_01.bas,
which is built  in the "main window"  with  Listview  parameter  of the pwindow.SETClientSize .

I did exchange my sample for Richard with the improvements.

b.r. Hans (hajubu)

- Think first -> work better ->  :)

José Roca

#4
One more important detail about the tab page lifecycle.

When the callback receives WM_CREATE, the tab control already exists, but the tab page itself has NOT yet been inserted into the tab control.

Therefore pTabPage->Width and pTabPage->Height are still invalid

Because of this, any attempt to resize the ListView during WM_CREATE will use incorrect dimensions, and the scrollbars will not appear.

The correct Win32 technique is to post a custom message and perform the initialization after the tab page has been inserted and sized:

CASE WM_CREATE
    PostMessageW hwnd, WM_USER + 1, 0, 0
    RETURN 0

Then handle the real initialization here:

CASE WM_USER + 1
    ' Now the tab page has been inserted and has its final size
    DIM pTabPage AS CTabPage PTR = AfxCTabPagePtr(GetParent(hwnd), 0)
    IF pTabPage = NULL THEN EXIT FUNCTION
    DIM hListView AS HWND = pTabPage->AddControl("ListView", hwnd, IDC_LISTVIEW)

    DIM w AS LONG = pTabPage->Width
    DIM h AS LONG = pTabPage->Height
    SetWindowPos hListView, NULL, 0, 0, w, h, SWP_NOZORDER

This ensures that:

* the tab page exists inside the tab control

* the final client size is known

* the ListView fills the page correctly

BTW you're doing very good work. It simply takes time to learn something as extensive as AfxNova. And this example is very tricky.

Richard Kelly

Thank you for that example. I'm still not getting a visible LV on the first tab page.

Creating the page

   DIM hTab AS HWND = pWindow.AddControl("TABCONTROL", hwndMain, IDC_TABCONTROL, "TabControl", 14, 167, 704, 448)
   ' // Anchor the control
   pWindow.AnchorControl(hTab, AFX_ANCHOR_HEIGHT_WIDTH)
   DIM pTabPage1 AS CTabPage PTR = NEW CTabPage
   pTabPage1->InsertPage(hTab, 0, "Date Types", -1, @TabPage1_WndProc)
   pTabPage1->SetBackColor(RGB_LIGHTGRAY)

The tab page does show up with a light gray background.

Creating the LV

FUNCTION TabPage1_WndProc (BYVAL hwnd AS HWND, BYVAL uMsg AS UINT, BYVAL wParam AS WPARAM, BYVAL lParam AS LPARAM) AS LRESULT
   
   SELECT CASE uMsg

    CASE WM_CREATE
        DIM pTabPage AS CTabPage PTR = AfxCTabPagePtr(GetParent(hwnd), 0)
        DIM hListView AS HWND = pTabPage->AddControl("LVSupportedCalendars", hwnd, IDC_LVPAGE1)
        ' // Get the width and height of the tab page
        DIM w AS LONG = pTabPage->Width
        DIM h AS LONG = pTabPage->Height
        ' // Set the listview position and visible size
        pTabPage->SetWindowPos hListView, NULL, 0, 0, w, h, SWP_NOZORDER
        ' // Anchor the ListView
        pTabPage->AnchorControl(IDC_LVPAGE1, AFX_ANCHOR_HEIGHT_WIDTH)
        DIM dwsText AS DWSTRING
        dwsText = "Name"
        ListView_AddColumn(hListView, 0, dwsText, pTabPage->ScaleX(110))
        dwsText = "Date"
        ListView_AddColumn(hListView, 1, dwsText, pTabPage->ScaleX(200))
        dwsText = "Name Column"
        ListView_SetItemText(hListView, 0, 0, dwsText)
        dwsText = "Date Column"
        ListView_SetItemText(hListView, 1, 0, dwsText)
        EXIT FUNCTION
         
   END SELECT
   
   FUNCTION = DefWindowProcW(hWnd, uMsg, wParam, lParam)

END FUNCTION

Do I need a show window somewhere?

José Roca

1.- The name of the ListView class is "ListView" or "SYSLISTVIEW32", not "LVSupportedCalendars".

2.- You're using ListView_SetItemText twice, but yopu aren't adding any item.

3.- As you're using the ListView_xxx macros, make sure that you use #define UNICODE

4.- Haven't you read my previous post?

      CASE WM_CREATE
         PostMessageW hwnd, WM_USER + 1, 0 , 0
         RETURN 0

      CASE WM_USER + 1
        DIM pTabPage AS CTabPage PTR = AfxCTabPagePtr(GetParent(hwnd), 0)
        DIM hListView AS HWND = pTabPage->AddControl("ListView", hwnd, IDC_LVPAGE1)

        ' // Get the width and height of the tab page
        DIM w AS LONG = pTabPage->Width
        DIM h AS LONG = pTabPage->Height
        ' // Set the listview position and visible size
        pTabPage->SetWindowPos hListView, NULL, 0, 0, w, h, SWP_NOZORDER
        ' // Anchor the ListView
        pTabPage->AnchorControl(IDC_LVPAGE1, AFX_ANCHOR_HEIGHT_WIDTH)
        DIM dwsText AS DWSTRING
        dwsText = "Name"
        ListView_AddColumn(hListView, 0, dwsText, pTabPage->ScaleX(110))
        dwsText = "Date"
        ListView_AddColumn(hListView, 1, dwsText, pTabPage->ScaleX(200))

        dwsText = "Name Column"
        ListView_AddItem(hListView, 0, 0, dwsText)
        dwsText = "Date Column"
        ListView_SetItemText(hListView, 0, 1, dwsText)

        EXIT FUNCTION

Richard Kelly

#7
I'm sorry to waste your time. I overlooked that part of your post. Here is the updated code that works. I switched over to using your CListView class.

========================================================================================
' Tab page 1 window procedure
' ========================================================================================
FUNCTION TabPage1_WndProc (BYVAL hwnd AS HWND, BYVAL uMsg AS UINT, BYVAL wParam AS WPARAM, BYVAL lParam AS LPARAM) AS LRESULT
   
   SELECT CASE uMsg
       
    CASE WM_CREATE
        PostMessageW hwnd, WM_USER + 1, 0, 0
       
    CASE WM_USER + 1
    ' Now the tab page has been inserted and has its final size

        DIM pTabPage AS CTabPage PTR = AfxCTabPagePtr(GetParent(hwnd), 0)
        DIM hListView AS HWND = pTabPage->AddControl("LISTVIEW", hwnd, IDC_LVPAGE1)
        ' // Get the width and height of the tab page
        DIM w AS LONG = pTabPage->Width
        DIM h AS LONG = pTabPage->Height
        ' // Set the listview position and visible size
        pTabPage->SetWindowPos hListView, NULL, 0, 0, w, h, SWP_NOZORDER
        ' // Anchor the ListView
        pTabPage->AnchorControl(IDC_LVPAGE1, AFX_ANCHOR_HEIGHT_WIDTH)
        DIM lvc AS LVCOLUMNW
        lvc.mask = LVCF_FMT OR LVCF_WIDTH OR LVCF_TEXT
        lvc.fmt = LVCFMT_LEFT
        lvc.cx = 110
        DIM wszText AS WSTRING * 260 = "Name"
        lvc.pszText = @wszText
        CListView.InsertColumn(hListView, 0, @lvc)
        lvc.cx = 200
        wszText = "Date"
        lvc.pszText = @wszText
        CListView.InsertColumn(hListView, 1, @lvc)
               
        EXIT FUNCTION
         
   END SELECT
   
   FUNCTION = DefWindowProcW(hWnd, uMsg, wParam, lParam)

END FUNCTION

Now that I can get my form built, I have a subroutine that adds all the data values. What technique would you recommend so that gets run the first time after the form is completely built? I have not yet put in any code to detect when my datepicker changes values for subsequent data refreshes.

José Roca

#8
The static classes like CListView are to save work, so instead of

DIM lvc AS LVCOLUMNW
        lvc.mask = LVCF_FMT OR LVCF_WIDTH OR LVCF_TEXT
        lvc.fmt = LVCFMT_LEFT
        lvc.cx = 110
        DIM wszText AS WSTRING * 260 = "Name"
        lvc.pszText = @wszText
        CListView.InsertColumn(hListView, 0, @lvc)
        lvc.cx = 200
        wszText = "Date"
        lvc.pszText = @wszText
        CListView.InsertColumn(hListView, 1, @lvc)

You can simply use:

CListView.AddColumn(hListView, 0, "Name", pWindow.ScaleX(110))
CListView.AddColumn(hListView, 1, "Date", pWindow.ScaleX(200))

CListView.AddColumn does this internally:

PRIVATE FUNCTION CListView.AddColumn (BYVAL hListView AS HWND, BYVAL iCol AS LONG, BYREF wszText AS WSTRING, BYVAL nWidth AS LONG, BYVAL nFormat AS LONG = LVCFMT_LEFT) AS LONG
  DIM lvc AS LVCOLUMNW
  lvc.mask = LVCF_FMT OR LVCF_WIDTH OR LVCF_TEXT OR LVCF_SUBITEM
  lvc.fmt = nFormat
  lvc.pszText = @wszText
  lvc.cx = nWidth
  RETURN SendMessageW(hListView, LVM_INSERTCOLUMNW, iCol, CAST(LPARAM, @lvc))
END FUNCTION

The use of pWindow.ScaleX is to make the width the same relative size depending of the DPI being used.

José Roca

#9
QuoteNow that I can get my form built, I have a subroutine that adds all the data values. What technique would you recommend so that gets run the first time after the form is completely built? I have not yet put in any code to detect when my datepicker changes values for subsequent data refreshes.

That depends of each control. You have to read the MSDN documentation: https://learn.microsoft.com/en-us/windows/win32/controls/date-and-time-picker-control-reference

See the notification messages in that link.

For example, to detect that the date time has changed, you have to process DTN_DATETIMECHANGE.

      CASE WM_NOTIFY
        ' // Notification messages
        DIM dtp AS NMDATETIMECHANGE
        CBNMTYPESET(dtp, wParam, lParam)
        IF dtp.nmhdr.idfrom = IDC_DTPICKER THEN
            IF dtp.nmhdr.code = DTN_DATETIMECHANGE THEN
              ' // Get the selected date
              DIM wszDate AS WSTRING * 260
              GetDateFormatW LOCALE_USER_DEFAULT, DATE_LONGDATE, @dtp.st, NULL, wszDate, SIZEOF(wszDate)\2
              SetWindowText(GetDlgItem(hwnd, IDC_LABEL), "Selected date: " & wszDate)
            END IF
        END IF

Full example: https://github.com/JoseRoca/AfxNova/blob/main/Templates/SDK%20Templates/CW_DTPicker_01b.bas

The Windows API always sends notification messages. Some languages, like .NET, intercept these messages and send "events".

Richard Kelly

Thank you Hajubu and Jose for the great assistance and support. I have the first of 4 tabs done and everything works perfectly.


hajubu

HI,
getting the hint from Jose for "the correct Listview  filling in /of the page"

I also adapted the snippet of listview_01b.bas now as a fourth Tabpage inside the TabCtrl01b.bas as my exercise using CListview.inc

Thanks ,  have fun !
Hans (hajubu)



José Roca

The wrappers are all optional. AfxNova is 100% SDK‑compatible, so you can use the wrappers for convenience or stick to straight SDK code if you prefer.

This macro, #define CBNMTYPESET(tp, wp, lp) memcpy @tp, CAST(ANY PTR, lp), SIZEOF(tp), allows to process WM_NOTIFY messages as if Windows were sending you an NM_ structure directly instead of a pointer to it. This avoids the need to use of pointers and casting.

But again, it is optional. Therefore, instead of:

      CASE WM_NOTIFY
        ' // Notification messages
        DIM dtp AS NMDATETIMECHANGE
        CBNMTYPESET(dtp, wParam, lParam)
        IF dtp.nmhdr.idfrom = IDC_DTPICKER THEN
            IF dtp.nmhdr.code = DTN_DATETIMECHANGE THEN
              ' // Get the selected date
              DIM wszDate AS WSTRING * 260
              GetDateFormatW LOCALE_USER_DEFAULT, DATE_LONGDATE, @dtp.st, NULL, wszDate, SIZEOF(wszDate)\2
              SetWindowText(GetDlgItem(hwnd, IDC_LABEL), "Selected date: " & wszDate)
            END IF
        END IF

you can use plain SDK code.

CASE WM_NOTIFY
    ' lParam points to a NMDATETIMECHANGE structure
    DIM pDtp AS NMDATETIMECHANGE PTR
    pDtp = CAST(NMDATETIMECHANGE PTR, lParam)
    IF pDtp->nmhdr.idfrom = IDC_DTPICKER THEN
        IF pDtp->nmhdr.code = DTN_DATETIMECHANGE THEN
            DIM wszDate AS WSTRING * 260
            GetDateFormatW( _
                LOCALE_USER_DEFAULT, _
                DATE_LONGDATE, _
                @pDtp->st, _
                NULL, _
                wszDate, _
                SIZEOF(wszDate)\2)
            SetWindowText(GetDlgItem(hwnd, IDC_LABEL), _
                "Selected date: " & wszDate)
        END IF
    END IF

In short, AfxNova simplifies SDK usage, but it never forces you to use the wrappers.


Richard Kelly

I didn't know that Jose. I do want to use AFXNova as much as possible and I'll switch over my code. I did notice that the datepicker with the drop down calendar sends two DTN_DATETIMECHANGE notifications which I verified via Mr Google. I have a global SYSTIME defined that gets updated when I do my form update on date or time changes and found that if I check the DATEPICKER value and if it's different than my global, then do my form update I can avoid the form update on the second notification as shown below.

FUNCTION WndProc (BYVAL hWnd AS HWND, BYVAL uMsg AS UINT, BYVAL wParam AS WPARAM, BYVAL lParam AS LPARAM) AS LRESULT

   SELECT CASE uMsg
           
      CASE WM_USER + 99
         ' // Update Form for date and time
         UpdateForm()
      CASE WM_CREATE
         RETURN 0
      CASE WM_COMMAND
         SELECT CASE LOWORD(wParam)
            CASE IDCANCEL
               ' // If ESC key pressed, close the application sending an WM_CLOSE message
               IF HIWORD(wParam) = BN_CLICKED THEN
                  SendMessageW hwnd, WM_CLOSE, 0, 0
                  RETURN 0
               END IF
          END SELECT
         
      CASE WM_NOTIFY
         DIM uFromSystemDate AS SYSTEMTIME
         DIM pTabPage AS CTabPage PTR    ' // Tab page object reference
         DIM ptnmhdr AS NMHDR PTR        ' // Information about a notification message
         ptnmhdr = CAST(NMHDR PTR, lParam)
         SELECT CASE ptnmhdr->idFrom
            CASE IDC_TABCONTROL
               SELECT CASE ptnmhdr->code
                  CASE TCN_SELCHANGE
                     ' // Show the selected page
                     pTabPage = AfxCTabPagePtr(ptnmhdr->hwndFrom, -1)
                     IF pTabPage THEN ..ShowWindow pTabPage->hTabPage, SW_SHOW
                  CASE TCN_SELCHANGING
                     ' // Hide the current page
                     pTabPage = AfxCTabPagePtr(ptnmhdr->hwndFrom, -1)
                     IF pTabPage THEN ..ShowWindow pTabPage->hTabPage, SW_HIDE
               END SELECT
            CASE IDC_DATEPICKER
               SELECT CASE ptnmhdr->code
                   CASE DTN_DATETIMECHANGE
                       ' // Date Picker sends two DTN_DATETIMECHANGE messages.
                       '//  Check if our global SYSTIME variable is different before updating for date and time
                       CDtPicker.GetSystemtime(ptnmhdr->hwndFrom, uFromSystemDate)
                       IF (uSystemDate.wYear <> uFromSystemDate.wYear) OR (uSystemDate.wMonth <> uFromSystemDate.wMonth) OR (uSystemDate.wDay <> uFromSystemDate.wDay) THEN
                          PostMessageW hWnd, WM_USER + 99, 0, 0
                       END IF
               END SELECT
            CASE IDC_TIMEPICKER
               SELECT CASE ptnmhdr->code
                   CASE DTN_DATETIMECHANGE
                      PostMessageW hWnd, WM_USER + 99, 0, 0
               END SELECT
         END SELECT
         
      CASE WM_SIZE
         ' // Get the tab control handle
         DIM hTab AS HWND = GetDlgItem(hwnd, IDC_TABCONTROL)
         ' // Get a pointer to the tab page
         DIM pTabPage AS CTabPage PTR = AfxCTabPagePtr(hTab, -1)
         ' // Resize the tab pages
         IF pTabPage THEN pTabPage->ResizePages
         RETURN 0

      CASE WM_DESTROY
        ' // Destroy the tab pages
        DIM hTab AS HWND = GetDlgItem(hwnd, IDC_TABCONTROL)
        AfxDestroyAllTabPages(hTab)
        PostQuitMessage(0)
        EXIT FUNCTION
       
   END SELECT

   ' // Default processing of Windows messages
   FUNCTION = DefWindowProcW(hWnd, uMsg, wParam, lParam)

END FUNCTION

Richard Kelly

I updated my notify code to use your macro as shown below and it works perfectly. Thank you for the tip.

      CASE WM_NOTIFY
         DIM uFromSystemDate AS SYSTEMTIME
         DIM pTabPage AS CTabPage PTR    ' // Tab page object reference
         DIM dtp AS NMDATETIMECHANGE
         CBNMTYPESET(dtp, wParam, lParam) ' // Information about a notification message
         SELECT CASE dtp.nmhdr.idFrom
            CASE IDC_TABCONTROL
               SELECT CASE dtp.nmhdr.code
                  CASE TCN_SELCHANGE
                     ' // Show the selected page
                     pTabPage = AfxCTabPagePtr(dtp.nmhdr.hwndFrom, -1)
                     IF pTabPage THEN ..ShowWindow pTabPage->hTabPage, SW_SHOW
                  CASE TCN_SELCHANGING
                     ' // Hide the current page
                     pTabPage = AfxCTabPagePtr(dtp.nmhdr.hwndFrom, -1)
                     IF pTabPage THEN ..ShowWindow pTabPage->hTabPage, SW_HIDE
               END SELECT
            CASE IDC_DATEPICKER
               SELECT CASE dtp.nmhdr.code
                   CASE DTN_DATETIMECHANGE
                       ' // Date Picker sends two DTN_DATETIMECHANGE messages.
                       '//  Check if our global SYSTIME variable is different before updating for date and time
                       CDtPicker.GetSystemtime(dtp.nmhdr.hwndFrom, uFromSystemDate)
                       IF (uSystemDate.wYear <> uFromSystemDate.wYear) OR (uSystemDate.wMonth <> uFromSystemDate.wMonth) OR (uSystemDate.wDay <> uFromSystemDate.wDay) THEN
                          PostMessageW hWnd, WM_USER + 99, 0, 0
                       END IF
               END SELECT
            CASE IDC_TIMEPICKER
               SELECT CASE dtp.nmhdr.code
                   CASE DTN_DATETIMECHANGE
                      PostMessageW hWnd, WM_USER + 99, 0, 0
               END SELECT
         END SELECT