1 module window_framework; 2 3 /+ 4 + Copyright Andrej Mitrovic 2011. 5 + Distributed under the Boost Software License, Version 1.0. 6 + (See accompanying file LICENSE_1_0.txt or copy at 7 + http://www.boost.org/LICENSE_1_0.txt) 8 +/ 9 10 /+ 11 + Shows one way to implement Widgets on top of WinAPI's 12 + windowing framework. Makes use of Johannes Phauf's 13 + updated std.signals module (renamed to signals.d to 14 + avoid clashes). 15 + 16 + Demonstrates simple MenuBar and Menu widgets, as well 17 + as mouse-enter and mouse-leave behavior. 18 +/ 19 20 /* 21 * Major todo: We need to decouple assigning parents from construction, 22 * otherwise we can't reparent or new() a widget and append 23 * it to a container widget. So maybe only appending would create 24 * the backbuffer. 25 * 26 * Major todo: Fork the Widget project and separate initialization 27 * routines. 28 */ 29 30 import core.memory; 31 import core.runtime; 32 import core.thread; 33 import core.stdc.config; 34 35 import std.algorithm; 36 import std.array; 37 import std.conv; 38 import std.exception; 39 import std.functional; 40 import std.math; 41 import std.random; 42 import std.range; 43 import std.stdio; 44 // import std.signals; // outdated 45 import util.signals; // new 46 import std.string; 47 import std.traits; 48 import std.utf; 49 50 pragma(lib, "gdi32.lib"); 51 52 import windows.windef; 53 import windows.winuser; 54 import windows.wingdi; 55 56 alias std.algorithm.min min; 57 alias std.algorithm.max max; 58 59 import cairo.c.cairo; 60 import cairo.cairo; 61 import cairo.win32; 62 63 import util.rounded_rectangle; 64 65 struct StateContext 66 { 67 // maybe add a scale() call, althought that wouldn't work good on specific shapes. 68 Context ctx; 69 70 this(Context ctx) 71 { 72 this.ctx = ctx; 73 ctx.save(); 74 } 75 76 ~this() 77 { 78 ctx.restore(); 79 } 80 81 alias ctx this; 82 } 83 84 class PaintBuffer 85 { 86 /* Each window with paintbuffer consumes 3 GDI objects (hBuffer, hBitmap, and hdc of the window). */ 87 this(HDC localHdc, int cxClient, int cyClient) 88 { 89 width = cxClient; 90 height = cyClient; 91 92 hBuffer = CreateCompatibleDC(localHdc); 93 hBitmap = CreateCompatibleBitmap(localHdc, cxClient, cyClient); 94 hOldBitmap = SelectObject(hBuffer, hBitmap); 95 96 surf = new Win32Surface(hBuffer); 97 ctx = Context(surf); 98 initialized = true; 99 } 100 101 ~this() 102 { 103 if (initialized) 104 { 105 initialized = false; 106 clear(); 107 } 108 } 109 110 void clear() 111 { 112 //~ surf.dispose(); 113 //~ ctx.dispose(); 114 //~ surf.finish(); // @BUG@: runtime exceptions 115 116 SelectObject(hBuffer, hOldBitmap); 117 DeleteObject(hBitmap); 118 DeleteDC(hBuffer); 119 120 initialized = false; 121 } 122 123 bool initialized; 124 int width, height; 125 HDC hBuffer; 126 HBITMAP hBitmap; 127 HBITMAP hOldBitmap; 128 Context ctx; 129 Surface surf; 130 } 131 132 HANDLE makeWindow(HWND hwnd, int childID = 1, string classname = WidgetClass, string description = null) 133 { 134 return CreateWindow(classname.toUTF16z, description.toUTF16z, 135 WS_CHILDWINDOW | WS_VISIBLE | WS_CLIPCHILDREN, // WS_CLIPCHILDREN is necessary 136 0, 0, 0, 0, // Size!int and Position are set by MoveWindow 137 hwnd, cast(HANDLE)childID, // child ID 138 cast(HINSTANCE)GetWindowLongPtr(hwnd, GWL_HINSTANCE), // hInstance 139 null); 140 } 141 142 abstract class Widget 143 { 144 TRACKMOUSEEVENT mouseTrack; 145 Widget parent; 146 HWND hwnd; 147 PaintBuffer paintBuffer; 148 PAINTSTRUCT ps; 149 150 int xOffset; 151 int yOffset; 152 int width, height; 153 154 bool needsRedraw = true; 155 bool isHidden; 156 bool mouseEntered; 157 158 Signal!() MouseLDown; 159 alias MouseLDown MouseClick; 160 161 Signal!(int, int) MouseMove; 162 Signal!() MouseEnter; 163 Signal!() MouseLeave; 164 165 this(Widget parent, int xOffset = 0, int yOffset = 0, int width = 0, int height = 0) 166 { 167 this.parent = parent; 168 this.xOffset = xOffset; 169 this.yOffset = yOffset; 170 this.width = width; 171 this.height = height; 172 173 assert(parent !is null); 174 hwnd = makeWindow(parent.hwnd); 175 WidgetHandles[hwnd] = this; 176 177 this.hwnd = hwnd; 178 MoveWindow(hwnd, 0, 0, size.width, size.height, true); 179 180 mouseTrack = TRACKMOUSEEVENT(TRACKMOUSEEVENT.sizeof, TME_LEAVE, hwnd, 0); 181 } 182 183 // children of the main window use this as the main window isn't a Widget type yet (to be done) 184 this(HWND hParentWindow, int xOffset = 0, int yOffset = 0, int width = 0, int height = 0) 185 { 186 this.parent = null; 187 this.xOffset = xOffset; 188 this.yOffset = yOffset; 189 this.width = width; 190 this.height = height; 191 192 hwnd = makeWindow(hParentWindow); 193 WidgetHandles[hwnd] = this; 194 195 this.hwnd = hwnd; 196 MoveWindow(hwnd, 0, 0, size.width, size.height, true); 197 } 198 199 @property Size!int size() 200 { 201 return Size!int(width, height); 202 } 203 204 @property void size(Size!int newsize) 205 { 206 width = newsize.width; 207 height = newsize.height; 208 209 auto localHdc = GetDC(hwnd); 210 211 if (paintBuffer !is null) 212 { 213 paintBuffer.clear(); 214 } 215 216 paintBuffer = new PaintBuffer(localHdc, width, height); 217 ReleaseDC(hwnd, localHdc); 218 219 MoveWindow(hwnd, xOffset, yOffset, width, height, true); 220 221 needsRedraw = true; 222 blit(); 223 } 224 225 void moveTo(int newXOffset, int newYOffset) 226 { 227 xOffset = newXOffset; 228 yOffset = newYOffset; 229 MoveWindow(hwnd, xOffset, yOffset, width, height, true); 230 } 231 232 void show() 233 { 234 if (isHidden) 235 { 236 ShowWindow(hwnd, true); 237 isHidden = false; 238 } 239 } 240 241 void hide() 242 { 243 if (!isHidden) 244 { 245 ShowWindow(hwnd, false); 246 isHidden = true; 247 } 248 } 249 250 LRESULT process(UINT message, WPARAM wParam, LPARAM lParam) 251 { 252 switch (message) 253 { 254 case WM_ERASEBKGND: 255 { 256 return 1; 257 } 258 259 case WM_PAINT: 260 { 261 OnPaint(hwnd, message, wParam, lParam); 262 return 0; 263 } 264 265 case WM_SIZE: 266 { 267 width = LOWORD(lParam); 268 height = HIWORD(lParam); 269 270 size(Size!int(width, height)); 271 return 0; 272 } 273 274 case WM_LBUTTONDOWN: 275 { 276 MouseLDown.emit(); 277 return 0; 278 } 279 280 case WM_MOUSELEAVE: 281 { 282 MouseLeave.emit(); 283 mouseEntered = false; 284 return 0; 285 } 286 287 case WM_MOUSEMOVE: 288 { 289 TrackMouseEvent(&mouseTrack); 290 291 // @BUG@ WinAPI bug, calling ShowWindow in succession can create 292 // an infinite loop due to an odd WM_MOUSEMOVE call to the window 293 // which issued the ShowWindow call to other windows. 294 static LPARAM oldPosition; 295 if (lParam != oldPosition) 296 { 297 if (!mouseEntered) 298 { 299 MouseEnter.emit(); 300 mouseEntered = true; 301 } 302 303 oldPosition = lParam; 304 auto xMousePos = cast(short)LOWORD(lParam); 305 auto yMousePos = cast(short)HIWORD(lParam); 306 307 MouseMove.emit(xMousePos, yMousePos); 308 } 309 310 return 0; 311 } 312 313 //~ case WM_TIMER: 314 //~ { 315 //~ blit(); 316 //~ return 0; 317 //~ } 318 319 case WM_DESTROY: 320 { 321 // @BUG@ 322 // Not doing this here causes exceptions being thrown from within cairo 323 // when calling surface.dispose(). I'm not sure why yet. 324 paintBuffer.clear(); 325 return 0; 326 } 327 328 default: 329 } 330 331 return DefWindowProc(hwnd, message, wParam, lParam); 332 } 333 334 void redraw() 335 { 336 needsRedraw = true; 337 blit(); 338 } 339 340 void blit() 341 { 342 InvalidateRect(hwnd, null, true); 343 } 344 345 void OnPaint(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 346 { 347 auto ctx = &paintBuffer.ctx; 348 auto hBuffer = paintBuffer.hBuffer; 349 auto hdc = BeginPaint(hwnd, &ps); 350 auto boundRect = ps.rcPaint; 351 352 if (needsRedraw) 353 { 354 draw(StateContext(*ctx)); 355 needsRedraw = false; 356 } 357 358 with (boundRect) 359 { 360 BitBlt(hdc, left, top, right - left, bottom - top, hBuffer, left, top, SRCCOPY); 361 } 362 363 EndPaint(hwnd, &ps); 364 } 365 366 void draw(StateContext ctx) { } 367 } 368 369 enum Alignment 370 { 371 left, 372 right, 373 center, 374 top, 375 bottom, 376 } 377 378 class Button : Widget 379 { 380 string name; 381 string fontName; 382 int fontSize; 383 Alignment alignment; 384 bool selected = true; 385 386 this(Widget parent, string name, string fontName, int fontSize, 387 Alignment alignment = Alignment.center, int xOffset = 0, int yOffset = 0, int width = 0, int height = 0) 388 { 389 auto textWidth = name.length * fontSize; 390 width = textWidth; 391 height = fontSize * 2; 392 this.name = name; 393 this.fontName = fontName; 394 this.fontSize = fontSize; 395 this.alignment = alignment; 396 397 super(parent, xOffset, yOffset, width, height); 398 } 399 400 override void draw(StateContext ctx) 401 { 402 ctx.setSourceRGB(1, 1, 0); 403 ctx.paint(); 404 405 ctx.setSourceRGB(0, 0, 0); 406 ctx.selectFontFace(fontName, FontSlant.CAIRO_FONT_SLANT_NORMAL, FontWeight.CAIRO_FONT_WEIGHT_NORMAL); 407 ctx.setFontSize(fontSize); 408 409 final switch (alignment) 410 { 411 case Alignment.left: 412 { 413 ctx.moveTo(0, fontSize); 414 break; 415 } 416 417 case Alignment.right: 418 { 419 break; 420 } 421 422 case Alignment.center: 423 { 424 // todo 425 auto centerPos = (width - (name.length * 6)) / 2; 426 //~ ctx.moveTo(centerPos, fontSize); 427 ctx.moveTo(0, fontSize); 428 break; 429 } 430 431 case Alignment.top: 432 { 433 break; 434 } 435 436 case Alignment.bottom: 437 { 438 break; 439 } 440 } 441 442 ctx.showText(name); 443 } 444 } 445 446 class MenuItem : Widget 447 { 448 string name; 449 string fontName; 450 int fontSize; 451 bool selected; 452 453 this(Widget parent, string name, string fontName, int fontSize, 454 int xOffset = 0, int yOffset = 0, int width = 0, int height = 0) 455 { 456 auto textWidth = name.length * fontSize; 457 width = textWidth; 458 459 height = fontSize * 2; 460 this.name = name; 461 this.fontName = fontName; 462 this.fontSize = fontSize; 463 464 super(parent, xOffset, yOffset, width, height); 465 466 this.MouseEnter.connect({ selected = true; redraw(); }); 467 this.MouseLeave.connect({ selected = false; redraw(); }); 468 } 469 470 override void draw(StateContext ctx) 471 { 472 if (selected) 473 ctx.setSourceRGB(1, 0.9, 0); 474 else 475 ctx.setSourceRGB(1, 1, 0); 476 477 ctx.paint(); 478 479 ctx.setSourceRGB(0, 0, 0); 480 ctx.selectFontFace(fontName, FontSlant.CAIRO_FONT_SLANT_NORMAL, FontWeight.CAIRO_FONT_WEIGHT_NORMAL); 481 ctx.setFontSize(fontSize); 482 ctx.moveTo(0, fontSize); 483 ctx.showText(name); 484 } 485 } 486 487 class Menu : Widget 488 { 489 MenuItem[] menuItems; 490 size_t lastYOffset; 491 492 // todo: main window will have to be a widget 493 //~ this(Widget parent, MenuType menuType, int xOffset = 0, int yOffset = 0, int width = 0, int height = 0) 494 //~ { 495 //~ super(parent, xOffset, yOffset, width, height); 496 //~ this.menuType = menuType; 497 //~ } 498 499 this(HWND hParentWindow, int xOffset = 0, int yOffset = 0, int width = 0, int height = 0) 500 { 501 super(hParentWindow, xOffset, yOffset, width, height); 502 } 503 504 void append(MenuItem menuItem) 505 { 506 width = max(width, menuItem.name.length * menuItem.fontSize); 507 menuItems ~= menuItem; 508 menuItem.moveTo(0, lastYOffset); 509 510 this.size = Size!int(width, menuItems.length * menuItem.height); 511 lastYOffset += menuItem.height; 512 } 513 514 MenuItem append(string name, string fontName, int fontSize) 515 { 516 auto textWidth = name.length * fontSize; 517 width = max(width, textWidth); 518 519 auto menuItem = new MenuItem(this, name, fontName, fontSize, 0); 520 menuItem.moveTo(0, lastYOffset); 521 menuItems ~= menuItem; 522 523 this.size = Size!int(width, menuItems.length * menuItem.height); 524 lastYOffset += menuItem.height; 525 526 return menuItem; 527 } 528 529 override void draw(StateContext ctx) 530 { 531 // todo: draw bg and separators 532 ctx.setSourceRGB(0.8, 0.8, 0.8); 533 ctx.paint(); 534 } 535 } 536 537 class MenuBar : Widget 538 { 539 Menu[] menus; 540 Button[] buttons; 541 size_t lastXOffset; 542 Menu activeMenu; 543 bool isMenuOpened; 544 545 // todo: main window will have to be a widget 546 //~ this(Widget parent, int xOffset = 0, int yOffset = 0, int width = 0, int height = 0) 547 //~ { 548 //~ super(parent, xOffset, yOffset, width, height); 549 //~ } 550 551 this(HWND hParentWindow, int xOffset = 0, int yOffset = 0, int width = 0, int height = 0) 552 { 553 super(hParentWindow, xOffset, yOffset, width, height); 554 } 555 556 void showMenu(size_t index) 557 { 558 assert(index < menus.length); 559 560 if (activeMenu !is null) 561 activeMenu.hide(); 562 563 activeMenu = menus[index]; 564 565 if (isMenuOpened) 566 activeMenu.show(); 567 else 568 activeMenu.hide(); 569 } 570 571 void append(Menu menu, string name) 572 { 573 static size_t menuIndex; 574 enum fontSize = 10; 575 enum fontName = "Arial"; 576 immutable yOffset = 2 * fontSize; 577 578 auto button = new Button(this, name, fontName, fontSize); 579 this.size = Size!int(width + button.width, yOffset); 580 581 int frameIndex = menuIndex++; 582 583 buttons ~= button; 584 button.moveTo(lastXOffset, 0); 585 586 button.MouseLDown.connect({ this.isMenuOpened ^= 1; this.showMenu(frameIndex); }); 587 button.MouseEnter.connect({ this.showMenu(frameIndex); }); 588 589 menus ~= menu; 590 menu.hide(); 591 menu.moveTo(lastXOffset, yOffset); 592 593 lastXOffset += button.width; 594 } 595 596 override void draw(StateContext ctx) 597 { 598 ctx.setSourceRGB(0, 1, 1); 599 ctx.paint(); 600 } 601 } 602 603 /* A place to hold Widget objects. Since each window has a unique HWND, 604 * we can use this hash type to store references to Widgets and call 605 * their window processing methods. 606 */ 607 Widget[HWND] WidgetHandles; 608 609 /* 610 * All Widget windows have this window procedure registered via RegisterClass(), 611 * we use it to dispatch to the appropriate Widget window processing method. 612 * 613 * A similar technique is used in the DFL and DGUI libraries for all of its 614 * windows and widgets. 615 */ 616 extern (Windows) 617 LRESULT winDispatch(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 618 { 619 auto widget = hwnd in WidgetHandles; 620 621 if (widget !is null) 622 { 623 return widget.process(message, wParam, lParam); 624 } 625 626 return DefWindowProc(hwnd, message, wParam, lParam); 627 } 628 629 extern (Windows) 630 LRESULT mainWinProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam) 631 { 632 static PaintBuffer paintBuffer; 633 static int width, height; 634 635 void draw(StateContext ctx) 636 { 637 ctx.setSourceRGB(1, 1, 1); 638 ctx.paint(); 639 } 640 641 switch (message) 642 { 643 case WM_CREATE: 644 { 645 auto hDesk = GetDesktopWindow(); 646 RECT rc; 647 GetClientRect(hDesk, &rc); 648 649 auto localHdc = GetDC(hwnd); 650 paintBuffer = new PaintBuffer(localHdc, rc.right, rc.bottom); 651 652 auto menuBar = new MenuBar(hwnd); 653 654 // todo 655 //~ auto fontSettings = FontSettings("Arial", 10); 656 657 auto fileMenu = new Menu(hwnd); 658 659 auto item = fileMenu.append("item1", "Arial", 10); 660 item.MouseLDown.connect( { writeln("Clicked."); } ); 661 662 menuBar.append(fileMenu, "File"); 663 664 return 0; 665 } 666 667 case WM_LBUTTONDOWN: 668 { 669 SetFocus(hwnd); 670 return 0; 671 } 672 673 case WM_SIZE: 674 { 675 width = LOWORD(lParam); 676 height = HIWORD(lParam); 677 return 0; 678 } 679 680 case WM_PAINT: 681 { 682 auto ctx = paintBuffer.ctx; 683 auto hBuffer = paintBuffer.hBuffer; 684 PAINTSTRUCT ps; 685 auto hdc = BeginPaint(hwnd, &ps); 686 auto boundRect = ps.rcPaint; 687 688 draw(StateContext(paintBuffer.ctx)); 689 690 with (boundRect) 691 { 692 BitBlt(hdc, left, top, right - left, bottom - top, paintBuffer.hBuffer, left, top, SRCCOPY); 693 } 694 695 EndPaint(hwnd, &ps); 696 return 0; 697 } 698 699 case WM_TIMER: 700 { 701 InvalidateRect(hwnd, null, true); 702 return 0; 703 } 704 705 case WM_MOUSEWHEEL: 706 { 707 return 0; 708 } 709 710 case WM_DESTROY: 711 { 712 PostQuitMessage(0); 713 return 0; 714 } 715 716 default: 717 } 718 719 return DefWindowProc(hwnd, message, wParam, lParam); 720 } 721 722 string WidgetClass = "WidgetClass"; 723 724 int myWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow) 725 { 726 string appName = "Window Framework"; 727 728 HWND hwnd; 729 MSG msg; 730 WNDCLASS wndclass; 731 732 /* One class for the main window */ 733 wndclass.lpfnWndProc = &mainWinProc; 734 wndclass.cbClsExtra = 0; 735 wndclass.cbWndExtra = 0; 736 wndclass.hInstance = hInstance; 737 wndclass.hIcon = LoadIcon(null, IDI_APPLICATION); 738 wndclass.hCursor = LoadCursor(null, IDC_ARROW); 739 wndclass.hbrBackground = null; 740 wndclass.lpszMenuName = null; 741 wndclass.lpszClassName = appName.toUTF16z; 742 743 if (!RegisterClass(&wndclass)) 744 { 745 MessageBox(null, "This program requires Windows NT!", appName.toUTF16z, MB_ICONERROR); 746 return 0; 747 } 748 749 /* Separate window class for Widgets. */ 750 wndclass.hbrBackground = null; 751 wndclass.lpfnWndProc = &winDispatch; 752 wndclass.cbWndExtra = 0; 753 wndclass.hIcon = null; 754 wndclass.lpszClassName = WidgetClass.toUTF16z; 755 756 if (!RegisterClass(&wndclass)) 757 { 758 MessageBox(null, "This program requires Windows NT!", appName.toUTF16z, MB_ICONERROR); 759 return 0; 760 } 761 762 hwnd = CreateWindow(appName.toUTF16z, "step sequencer", 763 WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, // WS_CLIPCHILDREN is necessary 764 cast(int)(1680 / 3.3), 1050 / 3, 765 400, 400, 766 null, null, hInstance, null); 767 768 ShowWindow(hwnd, iCmdShow); 769 UpdateWindow(hwnd); 770 771 while (GetMessage(&msg, null, 0, 0)) 772 { 773 TranslateMessage(&msg); 774 DispatchMessage(&msg); 775 } 776 777 return msg.wParam; 778 } 779 780 extern (Windows) 781 int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow) 782 { 783 int result; 784 785 786 try 787 { 788 Runtime.initialize(); 789 myWinMain(hInstance, hPrevInstance, lpCmdLine, iCmdShow); 790 Runtime.terminate(); 791 } 792 catch (Throwable o) 793 { 794 MessageBox(null, o.toString().toUTF16z, "Error", MB_OK | MB_ICONEXCLAMATION); 795 result = -1; 796 } 797 798 return result; 799 }