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.
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"
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.
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 -> :)
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.
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?
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
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.
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.
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".
Thank you Hajubu and Jose for the great assistance and support. I have the first of 4 tabs done and everything works perfectly.
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)
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.
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
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