Creating a table using Win32 API - winapi

I've been searching the net for different things about the win32 API, but it seems all information on it is rather sparse.
I am looking to create a simple window that shows a list of items, however I want to display different columns of data for each item, in a table-style format, where the user could perhaps be allowed to resize the different column widths.
If at all possible, I'd like to also be able to change the background colors of different rows, in the code, between just a general white, red, yellow, or green.
And the user would also be allowed to right click different rows, and be able to call a function on them, or copy the data to the clipboard (but that part is a different story).
Now, I've found list-viewer objects(?) that can be placed in the window, buttons, and right-click menus... but I cannot figure out how to do a table, using the Win32 API. I haven't even really read up on background colors for anything other than the window itself, either.
Is there a different, better framework I should use for this, or are there some functions or items that I've been missing? All help or guidance on the idea would be appreciated...
I'm using MSVC++ to do... everything that I'm working on.

Using the windows API and the standard control ListView you can do a table using the style LVS_REPORT
documentation link - unfortunatelly with no code :( -
About List-View Controls
I've found this good article Windows Programmierung: List View
the explanation is in german language but a google translation together with the code should be enough to understand it. From the article, to create the window:
#include "commctrl.h"
InitCommonControls();
hwndList = CreateWindow(WC_LISTVIEW, "",
WS_VISIBLE|WS_BORDER|WS_CHILD | LVS_REPORT | LVS_EDITLABELS,
10, 10, 300, 100,
hWnd, (HMENU)ID_LIST, hInst, 0);
then it is explained how to create the columns in the method
int CreateColumn(HWND hwndLV, int iCol, char *Text, int iWidth)
how to insert an item (one column)
int CreateItem(HWND hwndList, char *Text)
or insert item with two columns
int Create2ColItem(HWND hwndList, char *Text1, char *Text2)
etc...

Windows provides a fairly basic collection of built-in controls, listed here.
If you want something more sophisticated your options are:
Code it yourself. You have to paint it yourself, handle all the user-interaction, the scrolling, etc. This is a big job.
Find an existing implementation.
Abandon VC++ and use WinForms or WPF.
If you're stuck with VC++, The Grid Control and The Ultimate Grid are MFC-based.
If you're not using MFC there's BABYGRID or The Win32 SDK Data Grid.
If none of them suit, you'll have more luck searching for "grid" than "table".

For Listview examples, nothing beats the clarity of the Classic Sample!
In the meantime, Google Translate along with Unicode + tiny modifications to the rescue for #Alejadro's German link for the Listview- there's no direct translation on offer from search results as the page doesn't contain the appropriate meta tag. Snipped a little for brevity:
Subsequent changes of styles
The style of a ListView can be changed after creation. For this the functions GetWindowLong and SetWindowLong are used. About masks different styles can be defined.
Mask.................................Masked Styles:
LVS_TYPEMASK..............LVS_ICON, LVS_LIST, LVS_REPORT and LVS_SMALLICON
LVS_ALIGNMASK.............LVS_ALIGNLEFT and LVS_ALIGNTOP
LVS_TYPESTYLEMASK...LVS_ALIGNLEFT and LVS_ALIGNTOP but also VS_NOCOLUMNHEADER and LVS_NOSORTHEADER
For the following sequence, dwView contains the style to use, such as LVS_REPORT or LVS_ICON.
DWORD dwStyle = GetWindowLong(hwndLV, GWL_STYLE); // get current style
if ((dwStyle & LVS_TYPEMASK)! = dwView) // only on change
SetWindowLong(hwndLV, GWL_STYLE, (dwStyle & ~ LVS_TYPEMASK) | dwView); }
Control of the ListView control
Generating a list
A list view is created with the CreateWindow function. The window class uses the constant WC_LISTVIEW. To do this, the common control header file must be included.
#include "commctrl.h"
InitCommonControls();
hwndList = CreateWindow(WC_LISTVIEW, "",
WS_VISIBLE | WS_BORDER | WS_CHILD | LVS_REPORT | LVS_EDITLABELS,
10, 10, 300, 100,
hWnd, (HMENU) ID_LIST, hInst, 0);
In the dialog, it is simply defined in the resource.
If there are unresolved externals, you should check whether the library for the Common Controls (comctl32.lib) is included.
Columns of the ListView
Before something can be inserted in a REPORT, the columns must be defined. A column is described by the structure LVCOLUMN. The following routine creates a column.
int CreateColumn(HWND hwndLV, int iCol, char * text, intwidth)
{
LVCOLUMN lvc;
lvc.mask = LVCF_FMT | LVCF_WIDTH | LVCF_TEXT | LVCF_SUBITEM;
lvc.fmt = LVCFMT_LEFT;
lvc.cx = iWidth;
lvc.pszText = text;
lvc.iSubItem = iCol;
return ListView_InsertColumn(hwndLV, iCol, & lvc);
}
The columns can be modified by messages to the ListView or by calling macros that will ultimately execute a SendMessage.
Message Macro call Function
LVM_INSERTCOLUMN ListView_InsertColumn(HWND, int, LVCOLUMN * ) Insert column
LVM_DELETECOLUMN ListView_DeleteColumn(HWND, int) Delete column
LVM_GETCOLUMN ListView_GetColumn(HWND, int, LVCOLUMN * ) Get properties of the column
LVM_SETCOLUMN ListView_SetColumn(HWND, int, LVCOLUMN * ) Change properties of the column
LVM_GETCOLUMNWIDTH ListView_GetColumnWidth(HWND, int) Determine column width
LVM_SETCOLUMNWIDTH ListView_SetColumnWidth(HWND, int, int) Set column width
Insert a line
An element of the ListView is described by the structure LVITEMW (see below). Each element can be represented as an ICON, SMALLICON, LIST element or as the left column of a REPORT line.
int CreateItem(HWND hwndList, wchar_t * text)
{
LVITEMW lvi = {0};
lvi.mask = LVIF_TEXT;
lvi.pszText = text;
return ListView_InsertItem(hwndList, & lvi);
}
The mask field determines which elements of the LVITEMW structure are really used. Since it often makes sense to keep a pointer to the memory object that holds the data behind the object, the lParam field is useful. In order for this to be used, LVIF_TEXT | LVIF_PARAM must be set as a mask.
The constants of mask and the fields that enable them:
LVIF_IMAGE iImage
LVIF_INDENT iIndent
LVIF_PARAM lParam
LVIF_STATE state
LVIF_TEXT pszText
The further columns of a report
The element itself is always left in the report view and is selectable. To fill more columns, a text is added to the item.
int Create2ColItem(HWND hwndList, wchar_t * Text1, wchar_t * Text2)
{
LVITEMW lvi = {0};
int Ret;
// Initialize LVITEMW members that are common to all items.
lvi.mask = LVIF_TEXT;
lvi.pszText = Text1;
Ret = ListView_InsertItem(hwndList, & lvi);
if (Ret >= 0)
{
ListView_SetItemText(hwndList, Ret, 1, Text2);
}
return Ret;
}
The above Create2ColItem is best demonstrated by something along the line of the following statements:
LVHwnd = Your_Create_LV_Routine();
if (LVHwnd)
{
CreateColumn(LVHwnd, 0, ptrColHeaderString1, iColSize1);
CreateColumn(LVHwnd, 1, ptrColHeaderString2, iColSize2);
Create2ColItem(LVHwnd, ptrItemText1, ptrItemText2);
}
The structure LVITEMW
The structure LVITEMW (in CommCtrl.h) describes an element of the ListView. The most important elements are briefly described here. First the definition:
typedef struct tagLVITEMW
{
UINT mask;
int iItem;
int iSubItem;
UINT state;
UINT stateMask;
LPWSTR pszText;
int cchTextMax;
int iImage;
LPARAM lParam;
#if (_WIN32_IE >= 0x0300) //historical note for IE3 users!
int iIndent;
#endif
#if (NTDDI_VERSION >= NTDDI_WINXP)
int iGroupId;
UINT cColumns; // tile view columns
PUINT puColumns;
#endif
#if (NTDDI_VERSION >= NTDDI_VISTA)
int* piColFmt;
int iGroup; // readonly. only valid for owner data.
#endif
} LVITEMW, *LPLVITEMW;
The LVM_GETITEMW and LVM_SETITEMW messages change the attributes of an element. As a parameter, you get a pointer to a LVITEMW structure next to the HWND of the ListView, which must be filled in advance.
The structural elements in detail:
Mask:
Specifies which elements are used. A combination of the following flags is possible:
LVIF_IMAGE iImage
LVIF_INDENT iIndent
LVIF_PARAM lParam
LVIF_STATE state
LVIF_TEXT pszText
iItem
Index (0-based) of the item to which the structure relates.
iSubItem
Index (1-based) of the subitem to which the structure relates. 0 if the structure refers to an item instead of a subitem.
pszText
points to a null-terminated string. If the value is LPWSTR_TEXTCALLBACK, it is a callback item. If this changes, pszText must be set to LPSTR_TEXTCALLBACK and the ListView informed by LVM_SETITEMW or LVM_SETITEMTEXT.
pszText must not be set to LPWSTR_TEXTCALLBACK if the ListView has the style LVS_SORTASCENDING or LVS_SORTDESCENDING.
cchTextMax
Size of the buffer when the text is read.
iImage
Index of the icon of this element from the image list.
lParam
32-bit value that is specific to this element.
Actions with elements
LVM_INSERTITEM Insertion of an element
LVM_DELETEITEM Delete an element
LVM_DELETEALLITEMS Delete all elements
LVM_GETITEMW Read properties of the element
LVM_GETITEMTEXT Read text of the element
LVM_SETITEMW change
LVM_SETITEMTEXT Change to the text
Before inserting multiple items, an LVM_SETITEMCOUNT message will be sent to the ListView indicating how many items will ultimately be contained. This allows the ListView to optimize its memory allocation and release. How many elements a ListView contains can be determined with LVM_GETITEMCOUNT.
Editing selected elements
int Pos = -1;
LVITEMW Item;
Pos = ListView_GetNextItem(hwndList, Pos, LVNI_SELECTED);
while (Pos> = 0)
{
Item.iItem = Pos;
Item.iSubItem = 0;
ListView_GetItem(hwndList, & Item);
TuWasMitElement((Element Type * ) Item.lParam);
Pos = ListView_GetNextItem(hwndList, Pos, LVNI_SELECTED);
}
Events
The ListView sends WM_NOTIFY messages to the parent window. The code can take the following values:
Message............ Description
LVN_BEGINDRAG.............Start a drag-and-drop action
LVN_BEGINRDRAG..........Start a drag-and-drop action with the right mouse button
LVN_BEGINLABELEDIT....Start editing a label
LVN_ENDLABELEDIT.......End edit of a label
LVN_DELETEITEM..........Reports that the item is deleted
LVN_DELETEALLITEMS..Reports that all items are deleted
LVN_COLUMNCLICK......Indicates that the user clicked in the header of a report display
LVN_GETDISPINFO.......The control requests information about the presentation from the parent window
LVN_SETDISPINFO.......The information of the parent window for the item must be renewed
LVN_INSERTITEM..........Indicates the insertion of an item
LVN_ITEMCHANGED.....Indicates that an item has been changed
LVN_ITEMCHANGING....Indicates the intended change of an item
LVN_KEYDOWN.............Key was pressed
Editing the labels
The list view must have been created using the LVS_EDITLABELS style. Then a label can already be clicked on and inputs are accepted. However, the input is discarded immediately afterwards. To allow changes in the label, you just need to catch the WM_NOTIFY and return TRUE. In order to access the entered text in between, access is made to the text of the item. The example shows the input in a message box.
case WM_NOTIFY:
switch (((LPNMHDR) lParam) -> code)
{
case LVN_ENDLABELEDIT:
pItem = (NMLVDISPINFO) lParam;
MessageBox (hWnd, pItem-> item.pszText, "entry", MB_OK);
return TRUE;
If editing was aborted, the pszText element will be 0.
If you want to prevent editing, the message LVN_BEGINLABELEDIT is caught and returned TRUE. Here, too, the item can be accessed in the same way via lParam and thus, for example, a certain item group can be excluded.
Click on the column header in the ListView
case WM_NOTIFY:
switch (((LPNMHDR) lParam) -> code)
{
case LVN_COLUMNCLICK:
ColumnNr = ((LPNMLISTVIEW) lParam) -> iSubItem;
.....
Selection Event
The LVN_ITEMACTIVATE event is sent when the user activates an item. As with the other ListView events, it achieves the window function as part of a WM_NOTIFY message.
case WM_NOTIFY:
switch (((LPNMHDR) lParam) -> code)
{
case LVN_ITEMACTIVATE:
HWND hwndFrom = (HWND) ((LPNMHDR) lParam) -> hwndFrom;MarkedItemIndex =
ListView_GetNextItem(hwndFrom, -1, LVNI_SELECTED);
.....
The LVM_GETSELECTEDCOUNT message can be used to determine how many items have been activated. The LVM_GETNEXTITEM message is sent with the LVNI_SELECTED attribute and all items have been edited.

Related

Are there any Win32 functions I can use to get the count / itemdata from a CComboBoxEx control?

My parent dialog has a CComboBoxEx control (which is mapped to a derived class called CDatesComboBoxEx).
In one part of the application this dialog displays a popup modal dialog. And, inside the modal dialog, it needs access to information from the dates combo.
What I decided to do (which works fine) is pass the address of my combo in the constructor of the popup dialog. So I can now do things like:
m_pComboDates->GetCount()
m_pComboDates->GetItemDataPtr(i)
I was wondering if there was any way to use native Win32 code here instead?
We can get access to the parents handle (GetParent()->GetSafeHWnd()).
We know the ID of the control on the parent dialog (IDC_COMBOBOXEX_OCLM_WEEK_OF_MEETING).
So is it possible to somehow directly get the count and item data?
I know that there are these macros:
ComboBox_GetCount
ComboBox_GetItemData
But:
Can these be macros be used with CComboBoxEx control? And ...
How do we get the HWND on the combo given the context I previously described?
Actually, I think I missunderstood the purpose of those "macros". I can get the combo handle like this:
HWND hDatesCombo = ::GetDlgItem(
GetParent()->GetSafeHwnd(), IDC_COMBOBOXEX_OCLM_WEEK_OF_MEETING);
But, ComboBox_GetCount does not return a value. Nor the others. So I am somewhat confused.
Based on the answer, this bit is now fine:
HWND hDatesCombo = ::GetDlgItem(GetParent()->GetSafeHwnd(), IDC_COMBOBOXEX_OCLM_WEEK_OF_MEETING);
int iNumDates = static_cast<int>(::SendMessage(hDatesCombo, CB_GETCOUNT, 0, 0));
And inside my for loop I am doing this:
LRESULT itemData = ::SendMessage(hDatesCombo, CB_GETITEMDATA, static_cast<WPARAM>(i), 0);
auto* pEntry = static_cast<CChristianLifeMinistryEntry*>((LPVOID)itemData);
That is the only way I can find to cast it. If I try static_cast<LPVOID> it won't work either.
I was wondering if there was any way to use native Win32 code here instead?
Yes, there is. The SendMessage function (and its returned value) is what you need …
Once you have the HWND of your combo-box, you can send it the CB_GETCOUNT message to ask it how many items it contains:
HWND hDatesCombo = ::GetDlgItem(GetParent()->GetSafeHwnd(), IDC_COMBOBOXEX_OCLM_WEEK_OF_MEETING);
LRESULT nItems = ::SendMessage(hDatesCombo, CB_GETCOUNT, 0, 0);
And, to get the item data associated with a particular entry, send the CB_GETITEMDATA message, with the (zero-based) index of the item in question as the wParam argument:
//...
LRESULT *ItemData = new LRESULT[static_cast<size_t>(nItems)];
for (int i = 0; i < nItems; ++i) {
ItemData[i] = ::SendMessage(hDatesCombo, CB_GETITEMDATA, static_cast<WPARAM>(i), 0);
}
//...
delete[] ItemData; // When you're done with the data list
Of course, if your item data are pointers (such as if you have an owner-drawn combo with1 the CBS_HASSTRINGS style), you would need to modify the second code snippet accordingly, adding relevant the reinterpret_cast operations where necessary. (Note that both the LRESULT and WPARAM types are defined as being suitable for storing pointers.)
1 The linked M/S documentation page is a bit fuzzy on whether this applies to owner-drawn combos with or without the CBS_HASSTRINGS style.

How do you process the WM_COMPAREITEM message for a Combo Box?

I primarily come from a .NET background, but there is one small task that I have to accomplish for a Windows Control, specifically a ComboBox.
I understand that the CBS_SORT message is needed for the ComboBox to sort items alphabetically. This works just fine for me when I add strings to the combobox using the CB_ADDSTRING flag.
I'm trying to make the ComboBox use a reverse (or other custom) sorting order.
If I use CreateWindow to make my combobox, is there a way I can take the HWND it returns and hook up a custom method that processes the WM_COMPAREITEM message?
This is the article from Microsoft that talked about processing the WM_COMPAREITEM message. https://msdn.microsoft.com/en-us/library/windows/desktop/bb775791(v=vs.85).aspx
Edit:
I suppose one workaround is to use the CB_INSERTSTRING flag for custom sorting, but I'd really prefer to implement something similar to a CompareTo in other languages if I can.
Edit:
It's worth mentioning that class which calls CreateWindow (w/o using the CBS_HASSTRINGS flag) has a message handler for the WM_COMMAND message. It switches in the HIWORD to handle notifications like CBN_SELCHANGE.
I looked at the documentation for Combo box and noticed a slight difference between CBN_SELCHANGE and WM_COMPAREITEM.
The CBN_SELCHANGE notification code is sent when the user changes the current selection in the list box of a combo box. The user can change the selection by clicking in the list box or by using the arrow keys. The parent window of the combo box receives this notification in the form of a WM_COMMAND message with CBN_SELCHANGE in the high-order word of the wParam parameter.
And here's what it says about WM_COMPAREITEM
The system sends the WM_COMPAREITEM message to determine the relative position of a new item in the sorted list of an owner-drawn combo box or list box. Whenever the application adds a new item, the system sends this message to the owner of a combo box or list box created with the CBS_SORT or LBS_SORT style.
As far as I can tell, the DWORD Style that is passed into the CreateWindow method is WS_VISIBLE | WS_CHILD | WS_VSCROLL | CBS_AUTOHSCROLL | CBS_DROPDOWNLIST | CBS_SORT
Perhaps the problem is that the combobox that is created is not an "owner-drawn" combobox?
If that is the case, I would need to find a way to do that, but only handle the WM_COMPAREITEM case and not any other custom painting code.
Update
I've created the ComboBox without the CBS_HASSTRINGS and with the CB_OWNERDRAWFIXED (Which then lets me process the WM_COMPAREITEM message).
Here's the message handling code
LRESULT MyComboBoxParent::OnCompareItem(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
//umsg is always wm_compareitem
COMPAREITEMSTRUCT* pCompareItemStruct = (COMPAREITEMSTRUCT*)lParam;
ATLASSERT(pCompareItemStruct->CtlType == ODT_COMBOBOX); //this assert works
int iItemId1 = pCompareItemStruct->itemID1;
int iItemId2 = pCompareItemStruct->itemID2;
LPCTSTR item1 = (LPCTSTR)pCompareItemStruct ->itemData1;
ATLASSERT(item1 != NULL);
LPCTSTR item2 = (LPCTSTR)pCompareItemStruct->itemData2;
ATLASSERT(item2 != NULL);
int iStringCompareResult = _tcscmp(item1, item2);
//should reverse the sorting order
int iReturn = -1 * iStringCompareResult;
return iReturn;
}
//and the existing code snippet that adds the values to the combobox
::SendMessage(m_hwndCombo,CB_ADDSTRING,0,(LPARAM) ((wchar_t*)bstr_tDisplayedValue))
The data coming in from the COMPAREITEMSTRUCT is really strange.
I add the following values to the combo box in order.
aaaa
bbbb
this triggers wm_compareitem
iItemId1 is 0
iItemId2 is -1
item1 is 0x00000000 (null ptr)
item2 is L"bbbb"
(this causes the string compare to fail, so I just force return of -1)
cccc
iItemId1 is 0
iItemId2 is -1
item1 is 0x00000000 (null ptr)
item2 is L"cccc"
zzzz
yyyy
xxxx
z,y and x all follow the same pattern. I may have found one other guy on the internet with the same problem here. But hard to say for sure.

How to compute the actual height of the text in a static control

My simple Win32 DialogBox contains two static text controls (IDC_STATIC_TITLE and IDC_STATIC_SECONDARY), here's what it looks like in the resource editor:
At run time, the text first string is updated dynamically. Also, the font of the that text string is replaced such that it's bigger than the IDC_STATIC_SECONDARY string below it. The resulting text string might span a single line, two lines, or more.
I want the other static control holding the secondary text to be placed directly underneath the title string at run time. However, my resulting attempt to re-position this control in the WM_INITDIALOG callback isn't working very well. The second string is overlapping the first. I thought I could use DrawText with DT_CALCRECT to compute the height of the primary text string and then move the secondary text string based on the result. My code is coming up a bit short as seen here:
DrawText returns a RECT with coordinates {top=42 bottom=74 left=19 right=461} Subtracting bottom from top is "32". That seems a little short. I suspect I'm not invoking the API correctly and/or an issue with the different mappings between logical and pixel units.
Here's the relevant ATL code. The CMainWindow class just inherits from ATL's CDialogImpl class.
CMainWindow::CMainWindow():
_titleFont(NULL),
_secondaryFont(NULL)
{
LOGFONT logfont = {};
logfont.lfHeight = 30;
_titleFont = CreateFontIndirect(&logfont);
}
LRESULT CMainWindow::OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
CString strTitle;
RECT rectDrawText = {}, rectTitle={}, rectSecondary={};
CWindow wndTitle = GetDlgItem(IDC_STATIC_TITLE);
CWindow wndSecondary = GetDlgItem(IDC_STATIC_SECONDARY);
this->GetDlgItemText(IDC_STATIC_TITLE, strTitle);
wndTitle.SetFont(_titleFont); // font created with Create
wndTitle.GetWindowRect(&rectTitle);
wndSecondary.GetWindowRect(&rectSecondary);
ScreenToClient(&rectTitle);
ScreenToClient(&rectSecondary);
rectDrawText = rectTitle;
DrawText(wndTitle.GetDC(), strTitle, strTitle.GetLength(), &rectDrawText, DT_CALCRECT|DT_WORDBREAK); // compute the actual size of the text
UINT height = rectSecondary.bottom - rectSecondary.top; // get the original height of the secondary text control
rectSecondary.top = rectDrawText.bottom; // position it to be directly below the bottom of the title control
rectSecondary.bottom = rectSecondary.top + height; // add the height back
wndSecondary.MoveWindow(&rectSecondary);
return 0;
}
What am I doing wrong?
Despite what its name may make it sound like, wndTitle.GetDC() doesn't return some pointer/reference that's part of the CWindow and that's the same every call. Instead, it retrieves a brand new device context for the window each time. (It's basically a thin wrapper for the GetDC() Windows API call, right down to returning an HDC instead of the MFC equivalent.)
This device context, despite being associated with the window, is loaded with default parameters, including the default font (which IIRC is that old "System" font from the 16-bit days (most of this screenshot)).
So what you need to do is:
Call wndTitle.GetDC() to get the HDC.
Call SelectObject() to select the correct window font in (you can use WM_GETFONT to get this; not sure if MFC has a wrapper function for it), saving the return value, the previous font, for step 4
Call DrawText()
Call SelectObject() to select the previous font back in
Call wndTitle.ReleaseDC() to state that you are finished using the HDC
More details are on the MSDN page for CWindow::GetDC().

How to redirect a WM_KEYDOWN message to another control in MFC?

I'm on a roll today with MFC! :D
I have a text box and a list view control.
When the user presses the VK_UP and VK_DOWN keys in the text box, I would like this to happen:
Do some stuff.
Have the list view control also process the message (to highlight the previous/next item).
I want the list view to wrap around the current selection, if the key press is the first in its sequence.
Do some more stuff.
I tried subclassing my edit box in my dialog:
class MyEditBox : public CWnd
{
bool allowWrap;
afx_msg void OnKeyUp(UINT, UINT, UINT) { this->allowWrap = true; }
afx_msg void OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags)
{
CListCtrl &listView = static_cast<CListView *>(
this->GetParent()->GetDlgItem(IDC_LIST_VIEW))->GetListCtrl();
if (nChar == VK_UP || nChar == VK_DOWN)
{
int iSelBefore = listView.GetNextItem(-1, LVNI_SELECTED);
this->GetParent()->GetDlgItem(IDC_LIST_VIEW)
->OnKeyDown(nChar, nRepCnt, nFlags); //Oops! Protected member :(
int iSelAfter = listView.GetNextItem(-1, LVNI_SELECTED);
if (iSelBefore == iSelAfter && // Did the selection reach an end?
this->allowWrap) // If so, can we wrap it around?
{
int i = a == 0 ? listView.GetItemCount() - 1 : 0;
listView.SetItemState(i, LVIS_SELECTED | LVIS_FOCUSED,
LVIS_SELECTED | LVIS_FOCUSED);
}
}
this->allowWrap = false;
}
}
but OnKeyDown() is a protected member, so I can't just call it on another control.
Is there a better way to solve this than manually sending the command with SendMessage? Should I change my design, e.g. subclass something else, etc.?
Your intention is to select previous or next item in list control, right? Then you should call the method to do that directly instaed of asking the CListCtrl to "process" your message.
You may call CListCtrl::SetSelectionMark and CListCtrl::SetItemState to select next or previous keystroke. Example:
cListCtrl.SetSelectionMark(nIndex);
cListCtrl.SetItemState(nIndex, LVIS_SELECTED | LVIS_FOCUSED, 0xFF);
You can handle Key Down, Key Up as well as Page Down, Page Up, End, Home or any any key from edit box. You need to do calculation, though.
Or you can just SendMessage. There is no need to call OnKeyDown directly. Let the framework call it for you when you send the message.
I am seeing also other ways to solve this:
Derive a class from CListCtrl called MyListCtrl and choose one of two things:
1.1 Declare MyEditBox as a friend and now you can call the protected methods on MyEditBox
1.2 Add public methods CallOnKeyDown(...) and CallOnKeyup(...) to it that only do what is needed.
And when creating the control, instance a MyListCtrl instead of a CListCtrl. Also replace the listView variable you have shown here to be a MyListCtrl and use the methods you have now available
Use PreTranslateMessage(...). I think this "hammer" solution is worse than sending a message.

How to manipulate DLGTEMPLATE programmatically?

What?
I have a DLGTEMPLATE loaded from a resource DLL, how can I change the strings assigned to the controls at runtime programmatically?
I want to be able to do this before the dialog is created, such that I can tell that the strings on display came from the resource DLL, and not from calls to SetWindowText when the dialog is initialized.
Google has found examples of creating DLGTEMPLATE in code, or twiddling simple style bits but nothing on editing the strings in memory.
How?
I am doing this by hooking the Dialog/Property Sheet creation API's. Which gives me access to the DLGTEMPLATE before the actual dialog is created and before it has a HWND.
Why?
I want to be able to do runtime localization, and localization testing. I already have this implemented for loading string (including the MFC 7.0 wrapper), menus and accelerator tables, but I am struggling to handle dialog/property sheet creation.
Code examples would be the perfect answer, ideally a class to wrap around the DLGTEMPLATE, if I work out my own solution I will post it.
You can't edit the strings in memory. The DLGTEMPLATE structure is a direct file mapping of the relevent bytes of the resource dll. Thats read only.
You are going to need to process the entire DLGTEMPLATE structure and write out a new one with the altered length strings.
It will frankly be easier to just hook the WM_INITDIALOG and alter the strings by interacting with the controls than building a DLGTEMPLATE writer. Because there arn't a lot of those around. Unless you have an additional requirement to actually save altered dialog resources to disk as raw .res files (or attempt to modify the .dll inplace) Id really recommend you avoid this approach.
You say you are already doing this for accellerator tables and menu strings - if you can guarantee that the patched in strings are going to be shorter, then just make a binary copy of the DLGTEMPLATE struct, and write the non trivial scanning code necessary to find each string so you can patch the copy in place.
There is a file out there somewhere (which I think originated at Microsoft but I am not completely sure) called RESFMT.ZIP which explains this with some code examples. Raymond Chen also does some excellent explanations of this on his blog. Note that the format of DIALOGEX and DIALOG controls are different.
As noted in some other answers you would need to create the structure again from the start. This isn't all bad as you already have the basic information. Adding the controls is where is gets hard.
Basically, allocate a largish block of memory into a WORD *lpIn. Then add the structure up on top of that. adding the basic information for the DIALOG (see DLGTEMPLATE) and the controls is pretty obvious as the information is there in MSDN.
The two biggest problems you will encounter are: Making sure that the various part start on an alignment boundary, and interpreting the values of DIALOG controls, especially when to add a just a string or, a string or ordinal. Each control needs to start on an even boundary.
For the first (borrowed from somewhere I think RESFMT.ZIP):
WORD *AlignDwordPtr (WORD *lpIn)
{
ULONG ul;
ul = (ULONG) lpIn;
ul +=3;
ul >>=2;
ul
What I did was build a series of functions like this one following that allowed me to assemble DIALOGS in memory. (My need was so I could have some common code that didn't need an associated RC file for some very basic messages).
Here is an example...
WORD *AddStringOrOrdinalToWordMem( WORD *lpw, char *sz_Or_Ord )
{
LPWSTR lpwsz;
int BufferSize;
if (sz_Or_Ord == NULL)
{
*lpw++ = 0;
}
else
{
if (HIWORD(sz_Or_Ord) == 0) //MAKEINTRESOURCE macro
{
*lpw++ = 0xFFFF;
*lpw++ = LOWORD(sz_Or_Ord);
}
else
{
if (strlen(sz_Or_Ord))
{
lpwsz = ( LPWSTR ) lpw;
BufferSize = MultiByteToWideChar( CP_ACP, 0, sz_Or_Ord, -1, lpwsz, 0 );
MultiByteToWideChar( CP_ACP, 0, sz_Or_Ord, -1, lpwsz, BufferSize );
lpw = lpw + BufferSize;
}
else
{
*lpw++ = 0;
}
}
}
return( lpw );
}
The header file to the complete module included these functions:
WORD *AddControlToDialogTemplateEx(MTDialogTemplateType *dlgtmp,
char *Title,
WORD Id,
char *WinClass,
DWORD Style,
short x,
short y,
short cx,
short cy,
DWORD ExStyle,
int HelpID);
int DestroyDlgTemplateEx(MTDialogTemplateType *dlgtmp);
MTDialogTemplateType *CreateDlgTemplateEx( char *Name, // We use name just for reference, so it can be NULL
short x,
short y,
short cx,
short cy,
DWORD ExtendedStyle,
DWORD Style,
char *Menu,
char *WinClass,
char *Caption,
char *FontTypeFace,
int FontSize,
int FontWeigth,
int FontItalic,
int Charset,
int HelpID,
int NumberOfControls);
Which allowed me to assemble whole dialogs easily from code.
See the API function ::EnumChildWindows( HWND, WNDENUMPROC, LPARAM )
You can call this in a CFormView::Create or CDialog::OnInitDialog to give yourself a chance to replace the control captions. Don't worry, the old strings don't flicker up before you replace them.
In your dialog resource, set the control captions to a key in some kind of dictionary. If you're compiling /clr you can use a managed string table resource. In your callback, look up the translated string in your dictionary and set the control's caption to the translation. Another benefit of /clr and managed string table is that you can automatically look up the right language by Windows (or you) having already set System::Threading::Thread::CurrentThread->CurrentUICulture.
Something like this
CMyDialog::OnInitDialog()
{
::EnumChildWindows(
this->GetSafeHwnd(),
CMyDialog::UpdateControlText,
(LPARAM)this )
}
BOOL CALLBACK CMyDialog::UpdateControlText( HWND hWnd, LPARAM lParam )
{
CMyDialog* pDialog = (CMyDialog*)lParam;
CWnd* pChildWnd = CWnd::FromHandle( hWnd );
int ctrlId = pChildWnd->GetDlgCtrlID();
if (ctrlId)
{
CString curWindowText;
pChildWnd->GetWindowText( curWindowText );
if (!curWindowText.IsEmpty())
{
CString newWindowText = // some look up
pChildWnd->SetWindowText( newWindowText );
}
}
}
You'll have to locate the string you want to modify in the mem buffer that represents the template. The only way to do that is to traverse the whole template. Which is not easy.
Once you've done that, either insert bytes in the buffer if your new string is longer than the original one. Or shrink the buffer if the new string is shorter.
As Chris wrote, it would be much easier to modify the text in WM_INITDIALOG and try to re-phrase you requirement that says you may not call SetWindowText().
Thanks all, I actually had 24 hours rest on the problem, then went with a global windows hook filtering WM_INITDIALOG which was a much simpler method, worked out just fine, no API hooking required, 2 pages of code down to just a few lines.
Thanks for all the answers.

Resources