/win32_window_framework.d

http://github.com/AndrejMitrovic/cairoDSamples · D · 801 lines · 579 code · 152 blank · 70 comment · 19 complexity · 618d2a08c68c96b8412cfba7f6368da6 MD5 · raw file

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