1 /** 2 * Base class for native common controls. 3 * 4 * `Control` creates a child window of a given Win32 class (`"Button"`, 5 * `"SysListView32"`, …) and provides the operations common to all controls: 6 * text get/set, font assignment, a preferred size, parent message routing 7 * (`WM_COMMAND` / `WM_NOTIFY`), and opt-in subclassing for controls that need 8 * to intercept their own messages. 9 */ 10 module deft.controls.control; 11 12 version (Windows): 13 14 import core.sys.windows.windows; 15 import core.sys.windows.commctrl : DefSubclassProc, SetWindowSubclass, SUBCLASSPROC; 16 17 import deft.util.strings; 18 import deft.widget; 19 import deft.platform.win32.init : hInstance; 20 import deft.platform.win32.wndproc : lookupWidget; 21 22 private __gshared int g_nextControlId = 1000; 23 24 private int nextControlId() 25 { 26 return g_nextControlId++; 27 } 28 29 /// The system default GUI font, used for all controls. Cached stock object. 30 HFONT defaultFont() 31 { 32 return cast(HFONT) GetStockObject(DEFAULT_GUI_FONT); 33 } 34 35 /// Base class for all Win32 common-control wrappers. 36 class Control : Widget 37 { 38 private int controlId_; 39 private bool subclassed_; 40 41 /// The control's command identifier (the `hMenu` child id at creation). 42 int controlId() const @safe pure nothrow @nogc 43 { 44 return controlId_; 45 } 46 47 /** 48 * Create the control as a child window. 49 * 50 * `className` is a Win32 window class name (for example `"Button"`). 51 * `WS_CHILD | WS_VISIBLE` are added to the supplied `style`. The control is 52 * registered with its parent and given the system default GUI font. 53 */ 54 this(Widget parent, string className, DWORD style, DWORD exStyle = 0) 55 { 56 this.parent_ = parent; 57 controlId_ = nextControlId(); 58 59 HWND parentHandle = parent !is null ? parent.handle : null; 60 61 // WS_GROUP makes each control start its own keyboard navigation group, so 62 // arrow keys stay within a control rather than bleeding to the next one. 63 // Radio buttons that continue a group clear it again (see RadioButton). 64 handle_ = CreateWindowExW( 65 exStyle, 66 className.toWStringz, 67 ""w.ptr, 68 WS_CHILD | WS_VISIBLE | WS_GROUP | style, 69 0, 0, 0, 0, 70 parentHandle, 71 cast(HMENU) cast(size_t) controlId_, 72 hInstance(), 73 null); 74 75 registerHandle(); 76 77 if (parent !is null) 78 parent.addChild(this); 79 80 setFont(defaultFont()); 81 } 82 83 /// Set the control's text. 84 void setText(string text) 85 { 86 if (handle) 87 SetWindowTextW(handle, text.toWStringz); 88 } 89 90 /// Get the control's text. 91 string getText() 92 { 93 if (!handle) 94 return ""; 95 96 int len = GetWindowTextLengthW(handle); 97 if (len <= 0) 98 return ""; 99 100 auto buf = new wchar[len + 1]; 101 int got = GetWindowTextW(handle, buf.ptr, cast(int) buf.length); 102 return fromWString(buf[0 .. got]); 103 } 104 105 /// Assign a font to the control and request a repaint. 106 void setFont(HFONT font) 107 { 108 if (handle) 109 SendMessageW(handle, WM_SETFONT, cast(WPARAM) font, cast(LPARAM) TRUE); 110 } 111 112 /// A reasonable default preferred size; override per control type. 113 override Size getPreferredSize() 114 { 115 return Size(80, 24); 116 } 117 118 /** 119 * Handle a `WM_COMMAND` notification routed from the parent. 120 * 121 * `notificationCode` is the high word of the command's `wParam`. Return 122 * `true` if the notification was handled. The default does nothing. 123 */ 124 bool processCommand(ushort notificationCode) 125 { 126 return false; 127 } 128 129 /** 130 * Handle a `WM_NOTIFY` notification routed from the parent. Return `true` 131 * if it was handled. The default does nothing. 132 */ 133 bool processNotify(NMHDR* header) 134 { 135 return false; 136 } 137 138 /** 139 * Install a subclass window procedure so the control can intercept its own 140 * messages (for example, swallowing the Enter key in a text field). 141 * Idempotent. Subclasses override `processSubclassed` to do the work. 142 */ 143 void subclass() 144 { 145 if (handle && !subclassed_) 146 { 147 SetWindowSubclass(handle, &controlSubclassProc, 1, 148 cast(DWORD_PTR) cast(void*) this); 149 subclassed_ = true; 150 } 151 } 152 153 /** 154 * Intercept a message while subclassed. Set `result` and return `true` to 155 * consume the message; return `false` to let default processing continue. 156 */ 157 bool processSubclassed(UINT msg, WPARAM wParam, LPARAM lParam, ref LRESULT result) 158 { 159 return false; 160 } 161 } 162 163 /** 164 * Route a parent's `WM_COMMAND` to the originating control. Returns `true` if a 165 * control handled it. 166 */ 167 bool routeCommand(WPARAM wParam, LPARAM lParam) 168 { 169 auto controlHwnd = cast(HWND) lParam; 170 if (controlHwnd is null) 171 return false; // menu or accelerator command, not a control 172 173 if (auto widget = lookupWidget(controlHwnd)) 174 if (auto control = cast(Control) widget) 175 return control.processCommand(HIWORD(cast(DWORD) wParam)); 176 177 return false; 178 } 179 180 /** 181 * Route a parent's `WM_NOTIFY` to the originating control. Returns `true` if a 182 * control handled it. 183 */ 184 bool routeNotify(LPARAM lParam) 185 { 186 auto header = cast(NMHDR*) lParam; 187 if (header is null) 188 return false; 189 190 if (auto widget = lookupWidget(header.hwndFrom)) 191 if (auto control = cast(Control) widget) 192 return control.processNotify(header); 193 194 return false; 195 } 196 197 /// The shared subclass procedure; dispatches to `Control.processSubclassed`. 198 private extern (Windows) LRESULT controlSubclassProc(HWND hwnd, UINT msg, 199 WPARAM wParam, LPARAM lParam, UINT_PTR idSubclass, DWORD_PTR refData) nothrow 200 { 201 try 202 { 203 auto control = cast(Control) cast(void*) refData; 204 if (control !is null) 205 { 206 LRESULT result; 207 if (control.processSubclassed(msg, wParam, lParam, result)) 208 return result; 209 } 210 } 211 catch (Throwable) 212 { 213 // Never propagate a D throwable through the Win32 dispatcher. 214 } 215 216 return DefSubclassProc(hwnd, msg, wParam, lParam); 217 }