1 // Written by Christopher E. Miller 2 // See the included license.txt for copyright and license details. 3 4 module dfl.button; 5 6 import core.sys.windows.windows; 7 8 import dfl.exception; 9 import dfl.base; 10 import dfl.control; 11 import dfl.application; 12 import dfl.event; 13 import dfl.drawing; 14 import dfl.internal.dlib; 15 import dfl.internal.utf : callWindowProc; 16 17 private extern (Windows) void _initButton(); 18 19 abstract class ButtonBase : ControlSuperClass { 20 21 @property void textAlign(ContentAlignment calign) { 22 LONG wl = _bstyle() & ~(BS_BOTTOM | BS_CENTER | BS_TOP | BS_RIGHT | BS_LEFT | BS_VCENTER); 23 24 final switch (calign) { 25 case ContentAlignment.TOP_LEFT: 26 wl |= BS_TOP | BS_LEFT; 27 break; 28 29 case ContentAlignment.BOTTOM_CENTER: 30 wl |= BS_BOTTOM | BS_CENTER; 31 break; 32 33 case ContentAlignment.BOTTOM_LEFT: 34 wl |= BS_BOTTOM | BS_LEFT; 35 break; 36 37 case ContentAlignment.BOTTOM_RIGHT: 38 wl |= BS_BOTTOM | BS_RIGHT; 39 break; 40 41 case ContentAlignment.MIDDLE_CENTER: 42 wl |= BS_CENTER | BS_VCENTER; 43 break; 44 45 case ContentAlignment.MIDDLE_LEFT: 46 wl |= BS_VCENTER | BS_LEFT; 47 break; 48 49 case ContentAlignment.MIDDLE_RIGHT: 50 wl |= BS_VCENTER | BS_RIGHT; 51 break; 52 53 case ContentAlignment.TOP_CENTER: 54 wl |= BS_TOP | BS_CENTER; 55 break; 56 57 case ContentAlignment.TOP_RIGHT: 58 wl |= BS_TOP | BS_RIGHT; 59 break; 60 } 61 62 _bstyle(wl); 63 64 _crecreate(); 65 } 66 67 @property ContentAlignment textAlign() { 68 LONG wl = _bstyle(); 69 70 if (wl & BS_VCENTER) { // Middle. 71 if (wl & BS_CENTER) { 72 return ContentAlignment.MIDDLE_CENTER; 73 } 74 if (wl & BS_RIGHT) { 75 return ContentAlignment.MIDDLE_RIGHT; 76 } 77 return ContentAlignment.MIDDLE_LEFT; 78 } else if (wl & BS_BOTTOM) { // Bottom. 79 if (wl & BS_CENTER) { 80 return ContentAlignment.BOTTOM_CENTER; 81 } 82 if (wl & BS_RIGHT) { 83 return ContentAlignment.BOTTOM_RIGHT; 84 } 85 return ContentAlignment.BOTTOM_LEFT; 86 } else { // Top. 87 if (wl & BS_CENTER) { 88 return ContentAlignment.TOP_CENTER; 89 } 90 if (wl & BS_RIGHT) { 91 return ContentAlignment.TOP_RIGHT; 92 } 93 return ContentAlignment.TOP_LEFT; 94 } 95 } 96 97 // Border stuff... 98 99 /+ 100 override void createHandle() { 101 if(isHandleCreated) { 102 return; 103 } 104 105 createClassHandle(BUTTON_CLASSNAME); 106 107 onHandleCreated(EventArgs.empty); 108 } 109 +/ 110 111 protected override void createParams(ref CreateParams cp) { 112 super.createParams(cp); 113 114 cp.className = BUTTON_CLASSNAME; 115 if (isdef) { 116 cp.menu = cast(HMENU) IDOK; 117 if (!(cp.style & WS_DISABLED)) { 118 cp.style |= BS_DEFPUSHBUTTON; 119 } 120 } else if (cp.style & WS_DISABLED) { 121 cp.style &= ~BS_DEFPUSHBUTTON; 122 } 123 } 124 125 protected override void prevWndProc(ref Message msg) { 126 msg.result = callWindowProc(buttonPrevWndProc, msg.hWnd, msg.msg, msg.wParam, msg.lParam); 127 } 128 129 protected override void onReflectedMessage(ref Message m) { 130 super.onReflectedMessage(m); 131 132 switch (m.msg) { 133 case WM_COMMAND: 134 assert(cast(HWND) m.lParam == handle); 135 136 switch (HIWORD(m.wParam)) { 137 case BN_CLICKED: 138 onClick(EventArgs.empty); 139 break; 140 141 default: 142 } 143 break; 144 145 default: 146 } 147 } 148 149 protected override void wndProc(ref Message msg) { 150 switch (msg.msg) { 151 case WM_LBUTTONDOWN: 152 onMouseDown(new MouseEventArgs(MouseButtons.LEFT, 0, 153 cast(short) LOWORD(msg.lParam), cast(short) HIWORD(msg.lParam), 0)); 154 break; 155 156 case WM_LBUTTONUP: 157 onMouseUp(new MouseEventArgs(MouseButtons.LEFT, 1, 158 cast(short) LOWORD(msg.lParam), cast(short) HIWORD(msg.lParam), 0)); 159 break; 160 161 default: 162 super.wndProc(msg); 163 return; 164 } 165 prevWndProc(msg); 166 } 167 168 /+ 169 protected override void onHandleCreated(EventArgs ea) { 170 super.onHandleCreated(ea); 171 172 /+ 173 // Done in createParams() now. 174 if(isdef) { 175 SetWindowLongA(handle, GWL_ID, IDOK); 176 } 177 +/ 178 } 179 +/ 180 181 this() { 182 _initButton(); 183 184 wstyle |= WS_TABSTOP /+ | BS_NOTIFY +/ ; 185 ctrlStyle |= ControlStyles.SELECTABLE; 186 wclassStyle = buttonClassStyle; 187 } 188 189 protected: 190 191 final @property void isDefault(bool byes) { 192 isdef = byes; 193 } 194 195 final @property bool isDefault() { 196 //return (_bstyle() & BS_DEFPUSHBUTTON) == BS_DEFPUSHBUTTON; 197 //return GetDlgCtrlID(m.hWnd) == IDOK; 198 return isdef; 199 } 200 201 protected override bool processMnemonic(dchar charCode) { 202 if (canSelect) { 203 if (isMnemonic(charCode, text)) { 204 select(); 205 //Application.doEvents(); // ? 206 //performClick(); 207 onClick(EventArgs.empty); 208 return true; 209 } 210 } 211 return false; 212 } 213 214 override @property Size defaultSize() { 215 return Size(75, 23); 216 } 217 218 private: 219 bool isdef = false; 220 221 package: 222 final: 223 // Automatically redraws button styles, unlike _style(). 224 // Don't use with regular window styles ? 225 void _bstyle(LONG newStyle) { 226 if (isHandleCreated) //SendMessageA(handle, BM_SETSTYLE, LOWORD(newStyle), MAKELPARAM(TRUE, 0)); 227 { 228 SendMessageA(handle, BM_SETSTYLE, newStyle, MAKELPARAM(TRUE, 0)); 229 } 230 231 wstyle = newStyle; 232 //_style(newStyle); 233 } 234 235 LONG _bstyle() { 236 return _style(); 237 } 238 } 239 240 class Button : ButtonBase, IButtonControl { 241 this() { 242 } 243 244 @property DialogResult dialogResult() { 245 return dresult; 246 } 247 248 @property void dialogResult(DialogResult dr) { 249 dresult = dr; 250 } 251 252 // True if default button. 253 void notifyDefault(bool byes) { 254 isDefault = byes; 255 256 if (byes) { 257 if (enabled) { // Only show thick border if enabled. 258 _bstyle(_bstyle() | BS_DEFPUSHBUTTON); 259 } 260 } else { 261 _bstyle(_bstyle() & ~BS_DEFPUSHBUTTON); 262 } 263 } 264 265 void performClick() { 266 if (!enabled || !visible || !isHandleCreated) { // ? 267 return; // ? 268 } 269 270 // This is actually not so good because it sets focus to the control. 271 //SendMessageA(handle, BM_CLICK, 0, 0); // So that wndProc() gets it. 272 273 onClick(EventArgs.empty); 274 } 275 276 protected override void onClick(EventArgs ea) { 277 super.onClick(ea); 278 279 if (!(Application._compat & DflCompat.FORM_DIALOGRESULT_096)) { 280 if (DialogResult.NONE != this.dialogResult) { 281 auto xx = cast(IDialogResult) topLevelControl; 282 if (xx) { 283 xx.dialogResult = this.dialogResult; 284 } 285 } 286 } 287 } 288 289 protected override void wndProc(ref Message m) { 290 switch (m.msg) { 291 case WM_ENABLE: { 292 // Fixing the thick border of a default button when enabling and disabling it. 293 294 // To-do: check if correct implementation. 295 296 DWORD bst; 297 bst = _bstyle(); 298 if (bst & BS_DEFPUSHBUTTON) { 299 //_bstyle(bst); // Force the border to be updated. Only works when enabling. 300 if (!m.wParam) { 301 _bstyle(bst & ~BS_DEFPUSHBUTTON); 302 } 303 } else if (m.wParam) { 304 //if(GetDlgCtrlID(m.hWnd) == IDOK) 305 if (isdef) { 306 _bstyle(bst | BS_DEFPUSHBUTTON); 307 } 308 } 309 } 310 break; 311 312 default: 313 } 314 315 super.wndProc(m); 316 } 317 318 override @property void text(Dstring txt) { 319 if (txt.length) { 320 assert(!this.image, "Button image with text not supported"); 321 } 322 323 super.text = txt; 324 } 325 326 alias text = Control.text; // Overload. 327 328 final @property Image image() { 329 return _img; 330 } 331 332 final @property void image(Image img) 333 in { 334 if (img) { 335 assert(!this.text.length, "Button image with text not supported"); 336 } 337 } 338 body { 339 _img = null; // In case of exception. 340 LONG imgst = 0; 341 if (img) { 342 switch (img._imgtype(null)) { 343 case 1: 344 imgst = BS_BITMAP; 345 break; 346 347 case 2: 348 imgst = BS_ICON; 349 break; 350 351 default: 352 throw new DflException("Unsupported image format"); 353 } 354 } 355 356 _img = img; 357 _style((_style() & ~(BS_BITMAP | BS_ICON)) | imgst); // Redrawn manually in setImg(). 358 if (img) { 359 if (isHandleCreated) { 360 setImg(imgst); 361 } 362 } 363 //_bstyle((_bstyle() & ~(BS_BITMAP | BS_ICON)) | imgst); 364 } 365 366 private void setImg(LONG bsImageStyle) 367 in { 368 assert(isHandleCreated); 369 } 370 body { 371 WPARAM wparam = 0; 372 LPARAM lparam = 0; 373 374 /+ 375 if(bsImageStyle & BS_BITMAP) { 376 wparam = IMAGE_BITMAP; 377 lparam = cast(LPARAM)(_picbm ? _picbm.handle : (cast(Bitmap)_img).handle); 378 } else if(bsImageStyle & BS_ICON) { 379 wparam = IMAGE_ICON; 380 lparam = cast(LPARAM)((cast(Icon)(_img)).handle); 381 } else 382 { 383 return; 384 } 385 +/ 386 if (!_img) { 387 return; 388 } 389 HGDIOBJ hgo; 390 switch (_img._imgtype(&hgo)) { 391 case 1: 392 wparam = IMAGE_BITMAP; 393 break; 394 395 case 2: 396 wparam = IMAGE_ICON; 397 break; 398 399 default: 400 return; 401 } 402 lparam = cast(LPARAM) hgo; 403 404 //assert(lparam); 405 SendMessageA(handle, BM_SETIMAGE, wparam, lparam); 406 invalidate(); 407 } 408 409 protected override void onHandleCreated(EventArgs ea) { 410 super.onHandleCreated(ea); 411 412 setImg(_bstyle()); 413 } 414 415 protected override void onHandleDestroyed(EventArgs ea) { 416 super.onHandleDestroyed(ea); 417 418 /+ 419 if(_picbm) { 420 _picbm.dispose(); 421 _picbm = null; 422 } 423 +/ 424 } 425 426 private: 427 DialogResult dresult = DialogResult.NONE; 428 Image _img = null; 429 //Bitmap _picbm = null; // If -_img- is a Picture, need to keep a separate Bitmap. 430 } 431 432 class CheckBox : ButtonBase { 433 434 final @property void appearance(Appearance ap) { 435 final switch (ap) { 436 case Appearance.NORMAL: 437 _bstyle(_bstyle() & ~BS_PUSHLIKE); 438 break; 439 440 case Appearance.BUTTON: 441 _bstyle(_bstyle() | BS_PUSHLIKE); 442 break; 443 } 444 445 _crecreate(); 446 } 447 448 final @property Appearance appearance() { 449 if (_bstyle() & BS_PUSHLIKE) { 450 return Appearance.BUTTON; 451 } 452 return Appearance.NORMAL; 453 } 454 455 final @property void autoCheck(bool byes) { 456 if (byes) { 457 _bstyle((_bstyle() & ~BS_CHECKBOX) | BS_AUTOCHECKBOX); 458 } else { 459 _bstyle((_bstyle() & ~BS_AUTOCHECKBOX) | BS_CHECKBOX); 460 } 461 // Enabling/disabling the window before creation messes 462 // up the autocheck style flag, so handle it manually. 463 _autocheck = byes; 464 } 465 466 final @property bool autoCheck() { 467 /+ 468 return (_bstyle() & BS_AUTOCHECKBOX) == BS_AUTOCHECKBOX; 469 +/ 470 return _autocheck; 471 } 472 473 this() { 474 wstyle |= BS_AUTOCHECKBOX | BS_LEFT | BS_VCENTER; // Auto check and MIDDLE_LEFT by default. 475 } 476 477 /+ 478 protected override void onClick(EventArgs ea) { 479 _updateState(); 480 481 super.onClick(ea); 482 } 483 +/ 484 485 final @property void checked(bool byes) { 486 if (byes) { 487 _check = CheckState.CHECKED; 488 } else { 489 _check = CheckState.UNCHECKED; 490 } 491 492 if (isHandleCreated) { 493 SendMessageA(handle, BM_SETCHECK, cast(WPARAM) _check, 0); 494 } 495 } 496 497 // Returns true for indeterminate too. 498 final @property bool checked() { 499 if (isHandleCreated) { 500 _updateState(); 501 } 502 return _check != CheckState.UNCHECKED; 503 } 504 505 final @property void checkState(CheckState st) { 506 _check = st; 507 508 if (isHandleCreated) { 509 SendMessageA(handle, BM_SETCHECK, cast(WPARAM) st, 0); 510 } 511 } 512 513 final @property CheckState checkState() { 514 if (isHandleCreated) { 515 _updateState(); 516 } 517 return _check; 518 } 519 520 protected override void onHandleCreated(EventArgs ea) { 521 super.onHandleCreated(ea); 522 523 if (_autocheck) { 524 _bstyle((_bstyle() & ~BS_CHECKBOX) | BS_AUTOCHECKBOX); 525 } else { 526 _bstyle((_bstyle() & ~BS_AUTOCHECKBOX) | BS_CHECKBOX); 527 } 528 529 SendMessageA(handle, BM_SETCHECK, cast(WPARAM) _check, 0); 530 } 531 532 private: 533 CheckState _check = CheckState.UNCHECKED; // Not always accurate. 534 bool _autocheck = true; 535 536 void _updateState() { 537 _check = cast(CheckState) SendMessageA(handle, BM_GETCHECK, 0, 0); 538 } 539 } 540 541 class RadioButton : ButtonBase { 542 543 final @property void appearance(Appearance ap) { 544 final switch (ap) { 545 case Appearance.NORMAL: 546 _bstyle(_bstyle() & ~BS_PUSHLIKE); 547 break; 548 549 case Appearance.BUTTON: 550 _bstyle(_bstyle() | BS_PUSHLIKE); 551 break; 552 } 553 554 _crecreate(); 555 } 556 557 final @property Appearance appearance() { 558 if (_bstyle() & BS_PUSHLIKE) { 559 return Appearance.BUTTON; 560 } 561 return Appearance.NORMAL; 562 } 563 564 final @property void autoCheck(bool byes) { 565 /+ 566 if(byes) { 567 _bstyle((_bstyle() & ~BS_RADIOBUTTON) | BS_AUTORADIOBUTTON); 568 } else { 569 _bstyle((_bstyle() & ~BS_AUTORADIOBUTTON) | BS_RADIOBUTTON); 570 } 571 // Enabling/disabling the window before creation messes 572 // up the autocheck style flag, so handle it manually. 573 +/ 574 _autocheck = byes; 575 } 576 577 final @property bool autoCheck() { 578 /+ // Also commented out when using BS_AUTORADIOBUTTON. 579 return (_bstyle() & BS_AUTOCHECKBOX) == BS_AUTOCHECKBOX; 580 +/ 581 return _autocheck; 582 } 583 584 this() { 585 wstyle &= ~WS_TABSTOP; 586 //wstyle |= BS_AUTORADIOBUTTON | BS_LEFT | BS_VCENTER; // MIDDLE_LEFT by default. 587 wstyle |= BS_RADIOBUTTON | BS_LEFT | BS_VCENTER; // MIDDLE_LEFT by default. 588 } 589 590 protected override void onClick(EventArgs ea) { 591 if (autoCheck) { 592 if (parent) { // Sanity. 593 foreach (Control ctrl; parent.controls) { 594 if (ctrl is this) { 595 continue; 596 } 597 if ((ctrl._rtype() & (1 | 8)) == (1 | 8)) { // Radio button + auto check. 598 (cast(RadioButton) ctrl).checked = false; 599 } 600 } 601 } 602 checked = true; 603 } 604 605 super.onClick(ea); 606 } 607 608 /+ 609 protected override void onClick(EventArgs ea) { 610 _updateState(); 611 612 super.onClick(ea); 613 } 614 +/ 615 616 final @property void checked(bool byes) { 617 if (byes) { 618 _check = CheckState.CHECKED; 619 } else { 620 _check = CheckState.UNCHECKED; 621 } 622 623 if (isHandleCreated) { 624 SendMessageA(handle, BM_SETCHECK, cast(WPARAM) _check, 0); 625 } 626 } 627 628 // Returns true for indeterminate too. 629 final @property bool checked() { 630 if (isHandleCreated) { 631 _updateState(); 632 } 633 return _check != CheckState.UNCHECKED; 634 } 635 636 final @property void checkState(CheckState st) { 637 _check = st; 638 639 if (isHandleCreated) { 640 SendMessageA(handle, BM_SETCHECK, cast(WPARAM) st, 0); 641 } 642 } 643 644 final @property CheckState checkState() { 645 if (isHandleCreated) { 646 _updateState(); 647 } 648 return _check; 649 } 650 651 void performClick() { 652 //onClick(EventArgs.empty); 653 SendMessageA(handle, BM_CLICK, 0, 0); // So that wndProc() gets it. 654 } 655 656 protected override void onHandleCreated(EventArgs ea) { 657 super.onHandleCreated(ea); 658 659 /+ 660 if(_autocheck) { 661 _bstyle((_bstyle() & ~BS_RADIOBUTTON) | BS_AUTORADIOBUTTON); 662 } else { 663 _bstyle((_bstyle() & ~BS_AUTORADIOBUTTON) | BS_RADIOBUTTON); 664 } 665 +/ 666 667 SendMessageA(handle, BM_SETCHECK, cast(WPARAM) _check, 0); 668 } 669 670 /+ package +/ /+ protected +/ override int _rtype() { 671 if (autoCheck) { 672 return 1 | 8; // Radio button + auto check. 673 } 674 return 1; // Radio button. 675 } 676 677 private: 678 CheckState _check = CheckState.UNCHECKED; // Not always accurate. 679 bool _autocheck = true; 680 681 void _updateState() { 682 _check = cast(CheckState) SendMessageA(handle, BM_GETCHECK, 0, 0); 683 } 684 }