The Tile View in common controls offers a compact and pretty nice way to show some additional context within the list view, without resorting to the full reports mode. It's used a lot in explorer like programs where things like the disk size and file type are shown. But, it's been sitting unimplemented in wine for a long time. I've just spent a few days implementing this by: - Building a small test program in windows to get a feel for how it works - Hacking just enough to get something displaying - Writing a test suite to try and understand the ownership model, getting, and setting, and the various messages passed by windows. - Fixing up the implementation. - Adjusting my program to use I_COLUMNCALLBACK in windows and testing that. - Fixing my implementation to match the behaviour observed. Full disclosure, I originally started this implementation within the ReactOS build system as I had that compiling installed on my Windows box (my Mac is an M1 so doesn't play very nicely with Wine). I've since spun up a Linux VM and continued the work there. All bugs and misunderstandings are my own; I used clean room techniques (read MSDN, write a test program, write tests, compare results, iterate). Interesting things: - I initially wrote layout code for tiling mode as well, but a few rounds of playing with my test program and various layouts and settings (things like `LVS_ALIGNTOP`, `LVS_AUTOARRANGE` and `LVS_ALIGNLEFT`) for both tile mode and icon mode, convinced me that they use same layout logic (or close enough to). - I noticed some differences in Wine's Icon mode layout as compared to Windows 10. - I didn't fix these, one thing at a time. - I noticed and fixed a bug in SetView, where `UpdateItemSize` was called _after_ `Arrange`; that doesn't work. - The Text colours are really odd, if set to black the sub items show up gray, but any other colour and they're the same as the main. - Layout is tricky, in windows it looks like the first item (file name for example) can flow to 2 lines, and subitems are restricted to 1 - But this is really subtle, using GetItemMetrics kind of assumes there's no 'interference' from other items. - If we're on subitem 2, I don't want to have to fetch 0 and 1 to render their sizes before calculating my own, it seems like an `O(N^2)` situation off the bat. - I used a sidechannel from DrawItems, I'm open to other ideas here. - I haven't tested message passing, many of the test failures for listview at the moment look like sequencing errors for messages. I didn't look into it. - The auto mode on Windows 10 looks quite sophisticated, in that it examines many (all) items before deciding the best size. I haven't done anything interesting for auto size mode. - Not really interesting, but the tabs vs spaces situation in this file is a mess. Next Steps ========== - ~~Test with image lists loaded as well~~ - Hope for feedback here (particularly for messages) - Think about auto sizing. - Think about vertical centering within the space. Windows seems to have this as an option, though I haven't discovered how that works just yet. If Merged ========= Now that I have a pretty good grasp of how this component works and lays things out, once this is merged I'll see if I can do Groups as well. Pictures: ======== Windows ListView with Tile mode active. The first entry uses a Callback for the text and the columns layout, while the second is static. {width=300} Wine (with an image list as well) {width=300} And Explorer++ {width=400} Known Issues: ============= - Some highlighting seems a bit janky, I've a quick fix for this, but I think it would be best handled along with: - Hit testing when sub items are touched seems janky too. Test Program =========== ```C #include <windows.h> #include <commctrl.h> HINSTANCE g_hInst; HWND g_hLV; static void AddItems(HWND hLV) { static UINT puColumns[2] = {1,2}; LVITEMW it = {0}; it.mask = LVIF_TEXT | LVIF_COLUMNS; it.cColumns = I_COLUMNSCALLBACK; it.iItem = 0; it.iSubItem = 0; it.pszText = LPSTR_TEXTCALLBACK; ListView_InsertItem(hLV, &it); ListView_SetItemText(hLV, 0, 1, L"Static"); ListView_SetItemText(hLV, 0, 2, L"1 KB"); ListView_SetItemText(hLV, 0, 3, L"2026-02-23 12:00"); it.cColumns = 2; it.puColumns = puColumns; it.iItem = 1; it.iSubItem = 0; it.pszText = L"bravo.txt"; ListView_InsertItem(hLV, &it); ListView_SetItemText(hLV, 1, 1, L"Text"); ListView_SetItemText(hLV, 1, 2, L"12 KB"); ListView_SetItemText(hLV, 1, 3, L"2026-02-22 09:30"); } static void AddColumns(HWND hLV) { LVCOLUMNW col = {0}; col.mask = LVCF_TEXT | LVCF_WIDTH; col.cx = 160; col.pszText = L"Name"; ListView_InsertColumn(hLV, 0, &col); col.cx = 140; col.pszText = L"Type"; ListView_InsertColumn(hLV, 1, &col); col.cx = 100; col.pszText = L"Size"; ListView_InsertColumn(hLV, 2, &col); col.cx = 180; col.pszText = L"Modified"; ListView_InsertColumn(hLV, 3, &col); } static void EnableTileView(HWND hLV) { SIZE size = { 100, 70 }; LVTILEVIEWINFO tileViewInfo = {0}; tileViewInfo.cbSize = sizeof(tileViewInfo); tileViewInfo.dwFlags = LVTVIF_FIXEDSIZE; tileViewInfo.dwMask = LVTVIM_COLUMNS | LVTVIM_TILESIZE; tileViewInfo.cLines = 3; tileViewInfo.sizeTile = size; ListView_SetTileViewInfo(hLV, &tileViewInfo); ListView_SetView(hLV, LV_VIEW_TILE); } static void OnGetDispInfo(NMLVDISPINFO* pnmv) { if (pnmv->item.iItem < 0) { return; } if (pnmv->item.mask & LVIF_TEXT) { // Demonstrates that Windows is passing us a size 20 buffer to write to. pnmv->item.pszText = pnmv->item.cColumns == 20 ? L"Yes" : L"No"; } if (pnmv->item.mask & LVIF_IMAGE) { pnmv->item.iImage = -1; } if (pnmv->item.mask & LVIF_STATE) { pnmv->item.state = 0; } if (pnmv->item.mask & LVIF_COLUMNS) { pnmv->item.cColumns = 2; pnmv->item.puColumns[0] = 1; pnmv->item.puColumns[1] = 2; } } static LRESULT OnNotify(WPARAM wParam, LPARAM lParam) { switch (((LPNMHDR)lParam)->code) { case LVN_GETDISPINFO: OnGetDispInfo((NMLVDISPINFO*) lParam); break; } return TRUE; } static LRESULT CALLBACK WndProc(HWND hWnd, UINT msg, WPARAM wParam, LPARAM lParam) { switch (msg) { case WM_CREATE: { g_hLV = CreateWindowExW(WS_EX_CLIENTEDGE, WC_LISTVIEWW, L"", WS_CHILD | WS_VISIBLE | LVS_REPORT | LVS_ALIGNTOP | LVS_SINGLESEL | LVS_SHOWSELALWAYS | LVS_AUTOARRANGE, 0, 0, 0, 0, hWnd, (HMENU)1, g_hInst, NULL); ListView_SetExtendedListViewStyle(g_hLV, LVS_EX_DOUBLEBUFFER | LVS_EX_INFOTIP); AddColumns(g_hLV); AddItems(g_hLV); EnableTileView(g_hLV); return 0; } case WM_SIZE: if (g_hLV) MoveWindow(g_hLV, 0, 0, LOWORD(lParam), HIWORD(lParam), TRUE); return 0; case WM_DESTROY: PostQuitMessage(0); return 0; case WM_NOTIFY: OnNotify(wParam, lParam); return 0; } return DefWindowProcW(hWnd, msg, wParam, lParam); } int WINAPI wWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPWSTR lpCmdLine, int nCmdShow) { INITCOMMONCONTROLSEX iccx; g_hInst = hInstance; /* Initialize common controls */ iccx.dwSize = sizeof(iccx); iccx.dwICC = ICC_STANDARD_CLASSES | ICC_WIN95_CLASSES; InitCommonControlsEx(&iccx); WNDCLASSW wc = {0}; wc.lpfnWndProc = WndProc; wc.hInstance = hInstance; wc.lpszClassName = L"WineLvTileSmoke"; wc.hCursor = LoadCursor(NULL, IDC_ARROW); RegisterClassW(&wc); HWND hWnd = CreateWindowExW(0, wc.lpszClassName, L"ListView Tile puColumns Smoke Test", WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 700, 450, NULL, NULL, hInstance, NULL); ShowWindow(hWnd, nCmdShow); MSG msg; while (GetMessageW(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessageW(&msg); } return (int)msg.wParam; } ``` -- v18: comctl32: Implement Tile view style https://gitlab.winehq.org/wine/wine/-/merge_requests/10191