1 /** 2 * Native checked list box (`SysListView32` with checkboxes). 3 * 4 * `CheckListBox` wraps the Win32 list-view common control in list mode with the 5 * checkbox extended style: a single-column list where every item carries its own 6 * checkbox that can be toggled independently of the selection. It exposes item 7 * management, per-item checked state, single selection and delegate-based events 8 * for check toggles and selection changes. Because it is a real native control, 9 * it brings MSAA accessibility for free. 10 * 11 * Win32 stores the per-item checkbox in the "state image" bits of the item state 12 * (mask `LVIS_STATEIMAGEMASK`): state-image index 1 means unchecked and index 2 13 * means checked. 14 */ 15 module deft.controls.checklistbox; 16 17 version (Windows): 18 19 import core.sys.windows.windows; 20 import core.sys.windows.commctrl; 21 22 import deft.controls.control; 23 import deft.events; 24 import deft.util.strings; 25 import deft.widget; 26 27 /// A native list of items, each with its own checkbox. 28 class CheckListBox : Control 29 { 30 /// Fired when an item's checkbox is toggled; argument is the item index. 31 Event!(int) onItemChecked; 32 /// Fired when the selected item changes; argument is the new selected index. 33 Event!(int) onSelectionChanged; 34 35 /** 36 * Create a checked list box as a child of `parent`. 37 * 38 * The control is single-select, always shows the selection, takes part in tab 39 * navigation and has a border. Checkboxes are enabled via the 40 * `LVS_EX_CHECKBOXES` extended style. 41 */ 42 this(Widget parent) 43 { 44 super(parent, "SysListView32", 45 LVS_LIST | LVS_SINGLESEL | LVS_SHOWSELALWAYS | WS_TABSTOP | WS_BORDER); 46 47 SendMessageW(handle, LVM_SETEXTENDEDLISTVIEWSTYLE, 0, LVS_EX_CHECKBOXES); 48 49 // Subclass so WM_SETFOCUS can move focus onto the first item when nothing 50 // is selected yet — keyboard users land on a real, navigable item. 51 subclass(); 52 } 53 54 /// Append an item with the given `text`. Returns the new item's index. 55 int addItem(string text) 56 { 57 LVITEMW item; 58 item.mask = LVIF_TEXT; 59 item.iItem = getItemCount(); 60 item.pszText = cast(LPWSTR) text.toWStringz; 61 return cast(int) SendMessageW(handle, LVM_INSERTITEMW, 0, cast(LPARAM)&item); 62 } 63 64 /// Return the number of items. 65 int getItemCount() 66 { 67 return cast(int) SendMessageW(handle, LVM_GETITEMCOUNT, 0, 0); 68 } 69 70 /// Remove all items. 71 void clear() 72 { 73 SendMessageW(handle, LVM_DELETEALLITEMS, 0, 0); 74 } 75 76 /// Return the text of the item at `index`. 77 string getItemText(int index) 78 { 79 // LVM_GETITEMTEXTW returns the number of characters copied; a result that 80 // fills the buffer (cap-1) may have been truncated, so grow and retry. 81 for (int cap = 256;; cap *= 2) 82 { 83 auto buf = new wchar[cap]; 84 LVITEMW item; 85 item.iSubItem = 0; 86 item.pszText = buf.ptr; 87 item.cchTextMax = cap; 88 int got = cast(int) SendMessageW(handle, LVM_GETITEMTEXTW, index, 89 cast(LPARAM)&item); 90 if (got < cap - 1 || cap >= 1 << 16) 91 return fromWString(buf[0 .. got]); 92 } 93 } 94 95 /// Return whether the item at `index` is checked. 96 bool isChecked(int index) 97 { 98 auto st = cast(uint) SendMessageW(handle, LVM_GETITEMSTATE, index, 99 LVIS_STATEIMAGEMASK); 100 return ((st >> 12) == 2); 101 } 102 103 /// Set the checked state of the item at `index`. 104 void setChecked(int index, bool checked) 105 { 106 LVITEMW item; 107 item.stateMask = LVIS_STATEIMAGEMASK; 108 item.state = (checked ? 2 : 1) << 12; 109 SendMessageW(handle, LVM_SETITEMSTATE, index, cast(LPARAM)&item); 110 } 111 112 /// Return the index of the selected item, or -1 if nothing is selected. 113 int getSelectedIndex() 114 { 115 return cast(int) SendMessageW(handle, LVM_GETNEXTITEM, cast(WPARAM)-1, LVNI_SELECTED); 116 } 117 118 /// Select (and focus) the item at `index`. 119 void setSelectedIndex(int index) 120 { 121 LVITEMW item; 122 item.state = LVIS_SELECTED | LVIS_FOCUSED; 123 item.stateMask = LVIS_SELECTED | LVIS_FOCUSED; 124 SendMessageW(handle, LVM_SETITEMSTATE, index, cast(LPARAM)&item); 125 } 126 127 /// Route list-view selection and check-toggle notifications to their events. 128 override bool processNotify(NMHDR* header) 129 { 130 if (header.code == LVN_ITEMCHANGED) 131 { 132 auto nm = cast(NMLISTVIEW*) header; 133 if (nm.uChanged & LVIF_STATE) 134 { 135 // Selection: fire only on a 0 -> selected transition. 136 if ((nm.uNewState & LVIS_SELECTED) && !(nm.uOldState & LVIS_SELECTED)) 137 onSelectionChanged.fire(nm.iItem); 138 139 // Check toggle: compare the old and new state-image indices. Guard 140 // oldImg != 0 so the initial 0 -> state transition on insert (when 141 // the checkbox first appears) does not fire a spurious event. 142 int oldImg = (nm.uOldState & LVIS_STATEIMAGEMASK) >> 12; 143 int newImg = (nm.uNewState & LVIS_STATEIMAGEMASK) >> 12; 144 if (oldImg != 0 && newImg != 0 && oldImg != newImg) 145 onItemChecked.fire(nm.iItem); 146 } 147 return true; 148 } 149 return false; 150 } 151 152 /** 153 * Move focus onto the first item when the control gains focus with nothing 154 * selected, so a keyboard user starts on a navigable item. Always returns 155 * false so default processing still runs. 156 */ 157 override bool processSubclassed(UINT msg, WPARAM wParam, LPARAM lParam, 158 ref LRESULT result) 159 { 160 if (msg == WM_SETFOCUS && getSelectedIndex() < 0 && getItemCount() > 0) 161 setSelectedIndex(0); 162 return false; 163 } 164 165 /// A sensible default size for a checked list. 166 override Size getPreferredSize() 167 { 168 return Size(200, 160); 169 } 170 }