Search code examples
c++windowsunicodetreeviewcommon-controls

Problems with Windows TreeView Common Control Notifications and Multi-Byte Character Set


I ran into a strange problem while using the TreeView Common Control in a Windows application written in C++ and by means of the Windows API (no MFC or other!): The two important notifications TVN_ITEMCHANGED and TVN_ITEMCHANGING (available since Windows Vista) are only sent if Version 6.0 of ComCtl32.dll is loaded (by convincing the linker via a manifest to do so) AND if Unicode character set is employed. Using Multi-Byte character set causes the two notifications mentioned above to disappear. Using Unicode and version 5.82 of ComCtl32.dll yields the same result. I employ Windows 7 x64 and Visual Studio 2010, by the way.

Below, you find a "minimal" (> 180 lines of code :/) working example. Building under Visual Studio 2010 using Unicode character set (Configuration Properties > General > Character Set) makes the program work as expected, but using Multi-Byte character set makes TVN_ITEMCHANGED and TVN_ITEMCHANGING disappear. Other notifications arrive anyway.

Did I overlook something or did I run into a bug in the Common Controls implementation? I sincerely hope it is my former guess, and I very much appreciate your answers and ideas on this matter!

Best regards, D. Feldmann

#include <Windows.h>
#include <CommCtrl.h>

#include <iostream>

#pragma comment(linker, "\"/manifestdependency:type='win32'\
    name='Microsoft.Windows.Common-Controls'\
    version='6.0.0.0'\
    processorArchitecture='*'\
    publicKeyToken='6595b64144ccf1df'\
    language='*'\"")

HINSTANCE g_hInst = 0;
HWND hwndTV_ = 0;

bool setupTreeView(HWND hwnd)
{
    RECT rc = {0};
    GetClientRect(hwnd, &rc);

    DWORD style = WS_CHILD | WS_VISIBLE | WS_OVERLAPPED | TVS_HASBUTTONS | TVS_HASLINES | TVS_LINESATROOT;
    hwndTV_ = CreateWindowEx(WS_EX_CLIENTEDGE, WC_TREEVIEW, 0, style,
        10, 10, (rc.right - rc.left) - 20, (rc.bottom - rc.top) - 20,
        hwnd, (HMENU) 0xDF, g_hInst, 0);

    if (! hwndTV_)
        return false;

    style |= TVS_CHECKBOXES;
    SetWindowLong(hwndTV_, GWL_STYLE, style);

    HIMAGELIST hil = ImageList_Create(24, 24, ILC_COLOR | ILC_COLOR32, 2, 0);
    const int img1 = ImageList_AddIcon(hil, LoadIcon(0, IDI_QUESTION));
    const int img2 = ImageList_AddIcon(hil, LoadIcon(0, IDI_INFORMATION));
    SendMessage(hwndTV_, TVM_SETIMAGELIST, TVSIL_NORMAL, (LPARAM) hil);

    TVINSERTSTRUCT tvis = {0};
    tvis.hParent =  TVI_ROOT;
    tvis.hInsertAfter = TVI_ROOT;
    tvis.item.mask = TVIF_TEXT | TVIF_STATE | TVIF_IMAGE | TVIF_SELECTEDIMAGE;
    tvis.item.cchTextMax = 5;
    tvis.item.pszText = TEXT("root\0");
    tvis.item.state = (2 << 12) | TVIS_EXPANDED;
    tvis.item.stateMask = TVIS_STATEIMAGEMASK | TVIS_EXPANDED;
    tvis.item.iImage = img1;
    tvis.item.iSelectedImage = img2;
    HTREEITEM hRoot = (HTREEITEM) SendMessage(hwndTV_, TVM_INSERTITEM, 0, (LPARAM) &tvis);

    tvis.hParent = hRoot;
    tvis.hInsertAfter = TVI_LAST;
    tvis.item.cchTextMax = 7;
    tvis.item.pszText = TEXT("item 1\0");
    SendMessage(hwndTV_, TVM_INSERTITEM, 0, (LPARAM) &tvis);

    tvis.item.pszText = TEXT("item 2\0");
    SendMessage(hwndTV_, TVM_INSERTITEM, 0, (LPARAM) &tvis);

    return true;
}

LRESULT CALLBACK WindowProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
    LRESULT res = 0u;
    bool handled = true;
    switch (msg)
    {
        case WM_CLOSE:
            PostQuitMessage(0);
            break;

        case WM_CREATE:
            setupTreeView(hwnd);
            break;

        case WM_NOTIFY:
        {
            NMHDR* const nmhdr = (NMHDR*)lParam;
            switch (nmhdr->code)
            {
                case NM_CUSTOMDRAW:
                    std::cout << "NM_CUSTOMDRAW\n";
                    break;

                case NM_CLICK:
                    std::cout << "NM_CLICK\n";
                    break;

                case TVN_ITEMCHANGING:
                    std::cout << "!!! TVN_ITEMCHANGING !!!\n";
                    break;

                case TVN_ITEMCHANGED:
                    std::cout << "!!! TVN_ITEMCHANGED !!!\n";
                    break;

                case TVN_SELCHANGED:
                    std::cout << "TVN_SELCHANGED\n";
                    break;

                case TVN_SELCHANGING:
                    std::cout << "TVN_SELCHANGING\n";
                    break;

                case TVN_ITEMEXPANDED:
                    std::cout << "TVN_ITEMEXPANDED\n";
                    break;

                case TVN_ITEMEXPANDING:
                    std::cout << "TVN_ITEMEXPANDING\n";
                    break;

                default:
                    break;
            }   // switch (
            break;
        }

        default:
            handled = false;
            break;
    }   // switch (msg

    if (! handled)
        res = DefWindowProc(hwnd, msg, wParam, lParam);
    return res;
}

int run(HINSTANCE hInst)
{
    g_hInst = hInst;

    WNDCLASSEX wndCls = {0};
    wndCls.cbSize = sizeof(WNDCLASSEX);
    wndCls.cbClsExtra = 0;
    wndCls.cbWndExtra = 0;
    wndCls.hbrBackground = GetSysColorBrush(COLOR_3DFACE);
    wndCls.hCursor = LoadCursor(0, IDC_HAND);
    wndCls.hIcon = LoadIcon(0, IDI_WINLOGO);
    wndCls.hIconSm = LoadIcon(0, IDI_WINLOGO);
    wndCls.hInstance = g_hInst;
    wndCls.lpfnWndProc = WindowProc;
    wndCls.lpszClassName = TEXT("TestWindowClass");
    wndCls.lpszMenuName = 0;
    wndCls.style = CS_HREDRAW | CS_VREDRAW | CS_OWNDC;
    RegisterClassEx(&wndCls);

    INITCOMMONCONTROLSEX cc = {0};
    cc.dwSize = sizeof(INITCOMMONCONTROLSEX);
    cc.dwICC = ICC_TREEVIEW_CLASSES;
    InitCommonControlsEx(&cc);

    HWND hwnd = CreateWindowEx(0, TEXT("TestWindowClass"), TEXT("Test TreeView"),
        WS_VISIBLE | WS_OVERLAPPED | WS_SYSMENU | WS_CAPTION, CW_USEDEFAULT,
        CW_USEDEFAULT, 400, 300, NULL, NULL, hInst, 0);

    MSG msg = {0};
    for (BOOL res = TRUE; res != 0; )
    {
        res = GetMessage(&msg, 0, 0, 0);
        if (res != -1)
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }

        if (msg.message == WM_QUIT)
            res = 0;
    }

    HIMAGELIST hil = (HIMAGELIST) SendMessage(hwndTV_, TVM_GETIMAGELIST, (WPARAM) TVSIL_NORMAL, 0);
    if (hil)
        ImageList_Destroy(hil);

    UnregisterClass(TEXT("TestWindowClass"), g_hInst);

    return 0;
}

// Please build using /subsystem:console
int main(int /*argc*/, char** /*argv*/)
{
    return run(GetModuleHandle(0));
}

Solution

  • They don't disappear, you are simply not looking for them in your WindowProc(). Like many APIs, notifications can also have Ansi and Unicode versions, but you are only looking for one or the other depending on whether you are compiling for MBCS or Unicode. It is possible for an Ansi TreeView to receive Unicode notifications, and vice versa. You should be looking for both A and W versions of the notifications and handle them accordingly:

    switch (nmhdr->code)
    {
        ...
    
        case TVN_ITEMCHANGINGA:
            std::cout << "!!! TVN_ITEMCHANGINGA !!!\n";
            break;
    
        case TVN_ITEMCHANGINGW:
            std::cout << "!!! TVN_ITEMCHANGINGW !!!\n";
            break;
    
        case TVN_ITEMCHANGEDA:
            std::cout << "!!! TVN_ITEMCHANGEDA !!!\n";
            break;
    
        case TVN_ITEMCHANGEDW:
            std::cout << "!!! TVN_ITEMCHANGEDA !!!\n";
            break;
    
        case TVN_SELCHANGEDA:
            std::cout << "TVN_SELCHANGEDA\n";
            break;
    
        case TVN_SELCHANGEDW:
            std::cout << "TVN_SELCHANGEDW\n";
            break;
    
        case TVN_SELCHANGINGA:
            std::cout << "TVN_SELCHANGINGA\n";
            break;
    
        case TVN_SELCHANGINGW:
            std::cout << "TVN_SELCHANGINGW\n";
            break;
    
        case TVN_ITEMEXPANDEDA:
            std::cout << "TVN_ITEMEXPANDEDA\n";
            break;
    
        case TVN_ITEMEXPANDEDW:
            std::cout << "TVN_ITEMEXPANDEDW\n";
            break;
    
        case TVN_ITEMEXPANDINGA:
            std::cout << "TVN_ITEMEXPANDINGA\n";
            break;
    
        case TVN_ITEMEXPANDINGW:
            std::cout << "TVN_ITEMEXPANDINGW\n";
            break;
    
        ...
    }