February 6th, 2026
like1 reaction

How can I prevent the user from changing the widths of ListView columns in version 5 of the common controls?, part 2

Last time, we had figured out how to prevent the version 5 ListView Win32 common control from resizing columns, but we noticed that the cursor still changes to a resize cursor that doesn’t work. How can we avoid misleading the user?

I had a few ideas but decided that the easiest way would be to subclass the header control and override its WM_SET­CURSOR message with one that just sets the arrow cursor.

LRESULT CALLBACK AlwaysArrowSubclassProc(
    HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
    [[maybe_unused]] UINT_PTR uIdSubclass,
    [[maybe_unused]] DWORD_PTR dwRefData)
{
    switch (uMsg) {
    case WM_SETCURSOR:
        SetCursor(LoadCursor(nullptr, IDC_ARROW));
        return 1;
    }
    return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}

    case WM_CREATE: // or WM_INITDIALOG if this is a dialog procedure
        ⟦ ... ⟧

        SetWindowSubclass(ListView_GetHeader(hwndLV),
                          AlwaysArrowSubclassProc, 1, 0);

        ⟦ ... ⟧
        return 0;

    case WM_DESTROY:
        RemoveWindowSubclass(ListView_GetHeader(hwndLV),
                             AlwaysArrowSubclassProc, 1);

        ⟦ ... ⟧
        return 0;

Alternatively, we could have the subclass procedure be self-destroying.

LRESULT CALLBACK AlwaysArrowSubclassProc(
    HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
    UINT_PTR uIdSubclass, [[maybe_unused]] DWORD_PTR dwRefData)

{
    switch (uMsg) {
    case WM_NCDESTROY:                                                   
        RemoveWindowSubclass(hWnd, AlwaysArrowSubclassProc, uIdSubclass);
        break;                                                           
    case WM_SETCURSOR:
        SetCursor(LoadCursor(nullptr, IDC_ARROW));
        return 1;
    }
    return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}

    case WM_CREATE: // or WM_INITDIALOG if this is a dialog procedure
        ⟦ ... ⟧

        SetWindowSubclass(ListView_GetHeader(hwndLV),
                          AlwaysArrowSubclassProc, 1, 0);

        ⟦ ... ⟧
        return 0;

    case WM_DESTROY:
        // RemoveWindowSubclass(ListView_GetHeader(hwndLV),
        //                      AlwaysArrowSubclassProc, 1);

        ⟦ ... ⟧
        return 0;

We could generalize this subclass procedure so it always sets the cursor to one specified in its dwRefData.

LRESULT CALLBACK FixedCursorSubclassProc(
    HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
    UINT_PTR uIdSubclass, [[maybe_unused]] DWORD_PTR dwRefData)

{
    switch (uMsg) {
    case WM_NCDESTROY:
        RemoveWindowSubclass(hWnd, FixedCursorSubclassProc, uIdSubclass);
        break;
    case WM_SETCURSOR:
        SetCursor((HCURSOR)dwRefData);
        return 1;
    }
    return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}

    case WM_CREATE: // or WM_INITDIALOG if this is a dialog procedure
        ⟦ ... ⟧

        SetWindowSubclass(ListView_GetHeader(hwndLV),
                          FixedCursorSubclassProc, 1,
                          (DWORD_PTR)LoadCursor(nullptr, IDC_ARROW));

        ⟦ ... ⟧
        return 0;

And then I realized that I don’t need to set the cursor at all. The default window procedure sets the cursor to the class cursor. We just have to call it.

LRESULT CALLBACK ClassCursorSubclassProc(
    HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam,
    UINT_PTR uIdSubclass, [[maybe_unused]] DWORD_PTR dwRefData)

{
    switch (uMsg) {
    case WM_NCDESTROY:
        RemoveWindowSubclass(hWnd, ClassCursorSubclassProc, uIdSubclass);
        break;
    case WM_SETCURSOR:
        return DefWindowProc(hWnd, uMsg, wParam, lParam);
    }
    return DefSubclassProc(hWnd, uMsg, wParam, lParam);
}

    case WM_CREATE: // or WM_INITDIALOG if this is a dialog procedure
        ⟦ ... ⟧

        SetWindowSubclass(ListView_GetHeader(hwndLV),
                          ClassCursorSubclassProc, 1, 0);

        ⟦ ... ⟧
        return 0;

Recall that all of this work is just to work around the lack of support for the HDS_NO­SIZING style in the version 5 common controls library. If you are using version 6 (and really, you should be), then just set the HDS_NO­SIZING style onto the ListView’s header control, and you’re all done.

Topics
Code

Author

Raymond has been involved in the evolution of Windows for more than 30 years. In 2003, he began a Web site known as The Old New Thing which has grown in popularity far beyond his wildest imagination, a development which still gives him the heebie-jeebies. The Web site spawned a book, coincidentally also titled The Old New Thing (Addison Wesley 2007). He occasionally appears on the Windows Dev Docs Twitter account to tell stories which convey no useful information.

0 comments