#if defined(Hiro_ListView) namespace hiro { static auto CALLBACK ListView_windowProc(HWND hwnd, UINT msg, WPARAM wparam, LPARAM lparam) -> LRESULT { if(auto object = (mObject*)GetWindowLongPtr(hwnd, GWLP_USERDATA)) { if(auto listView = dynamic_cast(object)) { if(auto self = listView->self()) { if(!listView->enabled(true)) { if(msg == WM_KEYDOWN || msg == WM_KEYUP || msg == WM_SYSKEYDOWN || msg == WM_SYSKEYUP) { //WC_LISTVIEW responds to key messages even when its HWND is disabled //the control should be inactive when disabled; so we intercept the messages here return false; } } return self->windowProc(hwnd, msg, wparam, lparam); } } } return DefWindowProc(hwnd, msg, wparam, lparam); } auto pListView::construct() -> void { hwnd = CreateWindowEx( WS_EX_CLIENTEDGE, WC_LISTVIEW, L"", WS_CHILD | WS_TABSTOP | LVS_REPORT | LVS_SHOWSELALWAYS, 0, 0, 0, 0, _parentHandle(), nullptr, GetModuleHandle(0), 0 ); SetWindowLongPtr(hwnd, GWLP_USERDATA, (LONG_PTR)&reference); windowProc = (WindowProc)GetWindowLongPtr(hwnd, GWLP_WNDPROC); SetWindowLongPtr(hwnd, GWLP_WNDPROC, (LONG_PTR)&ListView_windowProc); ListView_SetExtendedListViewStyle(hwnd, LVS_EX_FULLROWSELECT | LVS_EX_SUBITEMIMAGES); pWidget::_setState(); setBackgroundColor(state().backgroundColor); setBatchable(state().batchable); setCheckable(state().checkable); setGridVisible(state().gridVisible); setHeaderVisible(state().headerVisible); setSortable(state().sortable); _setIcons(); resizeColumns(); } auto pListView::destruct() -> void { if(imageList) { ImageList_Destroy(imageList); imageList = 0; } DestroyWindow(hwnd); } auto pListView::append(sListViewColumn column) -> void { lock(); wchar_t text[] = L""; LVCOLUMN lvColumn; lvColumn.mask = LVCF_FMT | LVCF_TEXT | LVCF_SUBITEM; lvColumn.fmt = LVCFMT_LEFT; lvColumn.iSubItem = column->offset(); lvColumn.pszText = text; ListView_InsertColumn(hwnd, column->offset(), &lvColumn); if(auto self = column->self()) { self->_setState(); } resizeColumns(); unlock(); } auto pListView::append(sListViewItem item) -> void { lock(); wchar_t text[] = L""; LVITEM lvItem; lvItem.mask = LVIF_TEXT; lvItem.iItem = item->offset(); lvItem.iSubItem = 0; lvItem.pszText = text; ListView_InsertItem(hwnd, &lvItem); if(auto self = item->self()) { self->setChecked(item->state.checked); self->setSelected(item->state.selected); } for(auto& cell : item->state.cells) { if(auto self = cell->self()) { self->_setState(); } } unlock(); } auto pListView::checkAll() -> void { for(auto& item : state().items) { item->self()->setChecked(true); } } auto pListView::remove(sListViewColumn column) -> void { lock(); ListView_DeleteColumn(hwnd, column->offset()); unlock(); } auto pListView::remove(sListViewItem item) -> void { lock(); ListView_DeleteItem(hwnd, item->offset()); unlock(); } auto pListView::reset() -> void { lock(); ListView_DeleteAllItems(hwnd); LVCOLUMN lvColumn{LVCF_WIDTH}; while(ListView_GetColumn(hwnd, 0, &lvColumn)) ListView_DeleteColumn(hwnd, 0); _setIcons(); //free icon resources unlock(); } auto pListView::resizeColumns() -> void { lock(); vector widths; signed minimumWidth = 0; signed expandable = 0; for(auto column : range(state().columns)) { signed width = _width(column); widths.append(width); minimumWidth += width; if(state().columns[column]->expandable()) expandable++; } signed maximumWidth = self().geometry().width() - 4; SCROLLBARINFO sbInfo{sizeof(SCROLLBARINFO)}; if(GetScrollBarInfo(hwnd, OBJID_VSCROLL, &sbInfo)) { if(!(sbInfo.rgstate[0] & STATE_SYSTEM_INVISIBLE)) { maximumWidth -= sbInfo.rcScrollBar.right - sbInfo.rcScrollBar.left; } } signed expandWidth = 0; if(expandable && maximumWidth > minimumWidth) { expandWidth = (maximumWidth - minimumWidth) / expandable; } for(auto column : range(state().columns)) { if(auto self = state().columns[column]->self()) { signed width = widths[column]; if(self->state().expandable) width += expandWidth; self->_width = width; self->_setState(); } } unlock(); } auto pListView::selectAll() -> void { lock(); ListView_SetItemState(hwnd, -1, LVIS_SELECTED, LVIS_SELECTED); unlock(); } auto pListView::setBackgroundColor(Color color) -> void { if(!color) color = {255, 255, 255}; ListView_SetBkColor(hwnd, RGB(color.red(), color.green(), color.blue())); } auto pListView::setBatchable(bool batchable) -> void { auto style = GetWindowLong(hwnd, GWL_STYLE); !batchable ? style |= LVS_SINGLESEL : style &=~ LVS_SINGLESEL; SetWindowLong(hwnd, GWL_STYLE, style); } auto pListView::setCheckable(bool checkable) -> void { auto style = ListView_GetExtendedListViewStyle(hwnd); checkable ? style |= LVS_EX_CHECKBOXES : style &=~ LVS_EX_CHECKBOXES; ListView_SetExtendedListViewStyle(hwnd, style); } auto pListView::setForegroundColor(Color color) -> void { } auto pListView::setGridVisible(bool visible) -> void { //rendered via onCustomDraw } auto pListView::setHeaderVisible(bool visible) -> void { auto style = GetWindowLong(hwnd, GWL_STYLE); !visible ? style |= LVS_NOCOLUMNHEADER : style &=~ LVS_NOCOLUMNHEADER; SetWindowLong(hwnd, GWL_STYLE, style); } auto pListView::setSortable(bool sortable) -> void { auto style = GetWindowLong(hwnd, GWL_STYLE); !sortable ? style |= LVS_NOSORTHEADER : style &=~ LVS_NOSORTHEADER; SetWindowLong(hwnd, GWL_STYLE, style); } auto pListView::uncheckAll() -> void { for(auto& item : state().items) { item->self()->setChecked(false); } } auto pListView::unselectAll() -> void { lock(); ListView_SetItemState(hwnd, -1, 0, LVIS_FOCUSED | LVIS_SELECTED); unlock(); } auto pListView::onActivate(LPARAM lparam) -> void { auto nmlistview = (LPNMLISTVIEW)lparam; if(ListView_GetSelectedCount(hwnd) == 0) return; if(!locked()) { //LVN_ITEMACTIVATE is not re-entrant until DispatchMessage() completes //thus, we don't call self().doActivate() here PostMessageOnce(_parentHandle(), AppMessage::ListView_onActivate, 0, (LPARAM)&reference); } } auto pListView::onChange(LPARAM lparam) -> void { auto nmlistview = (LPNMLISTVIEW)lparam; if(!(nmlistview->uChanged & LVIF_STATE)) return; bool modified = false; for(auto& item : state().items) { bool selected = ListView_GetItemState(hwnd, item->offset(), LVIS_SELECTED) & LVIS_SELECTED; if(item->state.selected != selected) { modified = true; item->state.selected = selected; } } if(modified && !locked()) { //state event change messages are sent for every item //so when doing a batch select/deselect; this can generate several messages //we use a delayed AppMessage so that only one callback event is fired off PostMessageOnce(_parentHandle(), AppMessage::ListView_onChange, 0, (LPARAM)&reference); } if(!locked()) { unsigned row = nmlistview->iItem; unsigned mask = nmlistview->uNewState & LVIS_STATEIMAGEMASK; if(mask == 0x1000 || mask == 0x2000) { bool checked = mask == 0x2000; if(auto item = self().item(row)) { if(checked != item->state.checked) { //WC_LISTVIEW sends this message twice item->state.checked = checked; self().doToggle(item); } } } } } auto pListView::onContext(LPARAM lparam) -> void { auto nmitemactivate = (LPNMITEMACTIVATE)lparam; return self().doContext(); } auto pListView::onCustomDraw(LPARAM lparam) -> LRESULT { auto lvcd = (LPNMLVCUSTOMDRAW)lparam; switch(lvcd->nmcd.dwDrawStage) { default: return CDRF_DODEFAULT; case CDDS_PREPAINT: return CDRF_NOTIFYITEMDRAW; case CDDS_ITEMPREPAINT: return CDRF_NOTIFYSUBITEMDRAW | CDRF_NOTIFYPOSTPAINT; case CDDS_ITEMPREPAINT | CDDS_SUBITEM: return CDRF_SKIPDEFAULT; case CDDS_ITEMPOSTPAINT: { HDC hdc = lvcd->nmcd.hdc; HDC hdcSource = CreateCompatibleDC(hdc); unsigned row = lvcd->nmcd.dwItemSpec; for(auto column : range(state().columns)) { RECT rc, rcLabel; ListView_GetSubItemRect(hwnd, row, column, LVIR_BOUNDS, &rc); ListView_GetSubItemRect(hwnd, row, column, LVIR_LABEL, &rcLabel); rc.right = rcLabel.right; //bounds of column 0 returns width of entire item signed iconSize = rc.bottom - rc.top - 1; bool checked = state().items(row)->state.checked; bool selected = state().items(row)->state.selected; HBRUSH brush = CreateSolidBrush(selected ? GetSysColor(COLOR_HIGHLIGHT) : CreateRGB(_backgroundColor(row, column))); FillRect(hdc, &rc, brush); DeleteObject(brush); if(state().checkable && column == 0) { if(auto htheme = OpenThemeData(hwnd, L"BUTTON")) { unsigned state = checked ? CBS_CHECKEDNORMAL : CBS_UNCHECKEDNORMAL; SIZE size; GetThemePartSize(htheme, hdc, BP_CHECKBOX, state, NULL, TS_TRUE, &size); signed center = max(0, (rc.bottom - rc.top - size.cy) / 2); RECT rd{rc.left + center, rc.top + center, rc.left + center + size.cx, rc.top + center + size.cy}; DrawThemeBackground(htheme, hdc, BP_CHECKBOX, state, &rd, NULL); CloseThemeData(htheme); } rc.left += iconSize + 2; } else { rc.left += 2; } auto cell = self().item(row)->cell(column); if(!cell) continue; if(auto icon = cell->state.icon) { icon.scale(iconSize, iconSize); icon.transform(); auto bitmap = CreateBitmap(icon); SelectBitmap(hdcSource, bitmap); BLENDFUNCTION blend{AC_SRC_OVER, 0, (BYTE)(selected ? 128 : 255), AC_SRC_ALPHA}; AlphaBlend(hdc, rc.left, rc.top, iconSize, iconSize, hdcSource, 0, 0, iconSize, iconSize, blend); DeleteObject(bitmap); rc.left += iconSize + 2; } if(auto text = cell->state.text) { auto halign = state().columns(column)->state.horizontalAlignment; auto valign = state().columns(column)->state.verticalAlignment; utf16_t wText(text); SetBkMode(hdc, TRANSPARENT); SetTextColor(hdc, selected ? GetSysColor(COLOR_HIGHLIGHTTEXT) : CreateRGB(_foregroundColor(row, column))); auto style = DT_SINGLELINE | DT_END_ELLIPSIS; style |= halign < 0.333 ? DT_LEFT : halign > 0.666 ? DT_RIGHT : DT_CENTER; style |= valign < 0.333 ? DT_TOP : valign > 0.666 ? DT_BOTTOM : DT_VCENTER; rc.right -= 2; auto font = pFont::create(_font(row, column)); SelectObject(hdc, font); DrawText(hdc, wText, -1, &rc, style); DeleteObject(font); } if(state().gridVisible) { ListView_GetSubItemRect(hwnd, row, column, LVIR_BOUNDS, &rc); rc.top = rc.bottom - 1; FillRect(hdc, &rc, (HBRUSH)GetStockObject(LTGRAY_BRUSH)); ListView_GetSubItemRect(hwnd, row, column, LVIR_LABEL, &rc); rc.left = rc.right - 1; FillRect(hdc, &rc, (HBRUSH)GetStockObject(LTGRAY_BRUSH)); } } DeleteDC(hdcSource); return CDRF_SKIPDEFAULT; } } } auto pListView::onSort(LPARAM lparam) -> void { auto nmlistview = (LPNMLISTVIEW)lparam; self().doSort(self().column(nmlistview->iSubItem)); } auto pListView::_backgroundColor(unsigned _row, unsigned _column) -> Color { if(auto item = self().item(_row)) { if(auto cell = item->cell(_column)) { if(auto color = cell->backgroundColor()) return color; } if(auto color = item->backgroundColor()) return color; } if(auto column = self().column(_column)) { if(auto color = column->backgroundColor()) return color; } if(auto color = self().backgroundColor()) return color; if(state().columns.size() >= 2 && _row % 2) return {240, 240, 240}; return {255, 255, 255}; } auto pListView::_cellWidth(unsigned _row, unsigned _column) -> unsigned { unsigned width = 6; if(state().checkable && _column == 0) width += 16 + 2; if(auto item = self().item(_row)) { if(auto cell = item->cell(_column)) { if(auto& icon = cell->state.icon) { width += 16 + 2; } if(auto& text = cell->state.text) { width += Font::size(_font(_row, _column), text).width(); } } } return width; } auto pListView::_columnWidth(unsigned _column) -> unsigned { unsigned width = 12; if(auto column = self().column(_column)) { if(auto& icon = column->state.icon) { width += 16 + 12; //yes; icon spacing in column headers is excessive } if(auto& text = column->state.text) { width += Font::size(self().font(true), text).width(); } } return width; } auto pListView::_font(unsigned _row, unsigned _column) -> string { if(auto item = self().item(_row)) { if(auto cell = item->cell(_column)) { if(auto font = cell->font()) return font; } if(auto font = item->font()) return font; } if(auto column = self().column(_column)) { if(auto font = column->font()) return font; } if(auto font = self().font(true)) return font; return Font::sans(8); } auto pListView::_foregroundColor(unsigned _row, unsigned _column) -> Color { if(auto item = self().item(_row)) { if(auto cell = item->cell(_column)) { if(auto color = cell->foregroundColor()) return color; } if(auto color = item->foregroundColor()) return color; } if(auto column = self().column(_column)) { if(auto color = column->foregroundColor()) return color; } if(auto color = self().foregroundColor()) return color; return {0, 0, 0}; } auto pListView::_setIcons() -> void { ListView_SetImageList(hwnd, NULL, LVSIL_SMALL); if(imageList) ImageList_Destroy(imageList); imageList = ImageList_Create(16, 16, ILC_COLOR32, 1, 0); ListView_SetImageList(hwnd, imageList, LVSIL_SMALL); for(auto column : range(state().columns)) { auto icon = state().columns(column)->state.icon; if(icon) { icon.scale(16, 16); icon.transform(); } else { icon.allocate(16, 16); icon.fill(0x00ffffff); } auto bitmap = CreateBitmap(icon); ImageList_Add(imageList, bitmap, NULL); DeleteObject(bitmap); } //empty icon used for ListViewItems (drawn manually via onCustomDraw) image icon; icon.allocate(16, 16); icon.fill(0x00ffffff); auto bitmap = CreateBitmap(icon); ImageList_Add(imageList, bitmap, NULL); DeleteObject(bitmap); } auto pListView::_width(unsigned column) -> unsigned { if(auto width = state().columns[column]->width()) return width; unsigned width = 1; if(state().headerVisible) { width = max(width, _columnWidth(column)); } for(auto row : range(state().items)) { width = max(width, _cellWidth(row, column)); } return width; } } #endif