/win32_slider.d

http://github.com/AndrejMitrovic/cairoDSamples · D · 717 lines · 496 code · 108 blank · 113 comment · 35 complexity · 591e8841b58da435fdd9ed9eb97d6372 MD5 · raw file

  1. module win32_slider;
  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. + This is an example of a custom Slider widget implemented with
  10. + CairoD with the help of the Win32 API.
  11. + You can change the orientation of the slider, its colors,
  12. + its maximum value and the extra padding that allows selection
  13. + of the slider within extra bounds.
  14. +
  15. + Currently it doesn't instantiate multiple sliders but this kind
  16. + of example is coming soon.
  17. +
  18. + It relies heavily on Win32 features, such as widgets being
  19. + implemented as windows (windows, buttons, menus, and even the
  20. + desktop window are all windows) which have X and Y points relative
  21. + to their own window, the ability to capture the mouse position
  22. + even when it goes outside a window area and some other features.
  23. +/
  24. import core.memory;
  25. import core.runtime;
  26. import core.thread;
  27. import core.stdc.config;
  28. import std.algorithm;
  29. import std.array;
  30. import std.conv;
  31. import std.exception;
  32. import std.functional;
  33. import std.math;
  34. import std.random;
  35. import std.range;
  36. import std.stdio;
  37. import std.string;
  38. import std.traits;
  39. import std.utf;
  40. pragma(lib, "gdi32.lib");
  41. import win32.windef;
  42. import win32.winuser;
  43. import win32.wingdi;
  44. alias std.algorithm.min min; // conflict resolution
  45. alias std.algorithm.max max; // conflict resolution
  46. import cairo.cairo;
  47. import cairo.win32;
  48. alias cairo.cairo.RGB RGB; // conflict resolution
  49. /* These should be tracked in one place for the entire app,
  50. * otherwise you might end up having multiple widgets with
  51. * their own control/shift key states.
  52. */
  53. __gshared bool shiftState;
  54. __gshared bool controlState;
  55. __gshared bool mouseTrack;
  56. /* Used in painting the slider background/foreground/thumb */
  57. struct SliderColors
  58. {
  59. RGBA thumbActive;
  60. RGBA thumbHover;
  61. RGBA thumbIdle;
  62. RGBA fill;
  63. RGBA back;
  64. RGBA window;
  65. }
  66. /* Two types of sliders */
  67. enum Axis
  68. {
  69. vertical,
  70. horizontal
  71. }
  72. class SliderWindow
  73. {
  74. int cxClient, cyClient; /* width, height */
  75. HWND hwnd;
  76. RGBA thumbColor; // current active thumb color
  77. Axis axis; // slider orientation
  78. SliderColors colors; // initialized colors
  79. int thumbPos;
  80. int size;
  81. int thumbSize;
  82. int offset; // since round caps add additional pixels, we offset the drawing
  83. int step; // used when control key is held
  84. bool isActive;
  85. this(HWND hwnd, Axis axis, int size, int thumbSize, int padding, SliderColors colors)
  86. {
  87. this.hwnd = hwnd;
  88. this.colors = colors;
  89. this.axis = axis;
  90. this.size = size;
  91. this.thumbSize = thumbSize;
  92. offset = padding / 2;
  93. thumbPos = (axis == Axis.horizontal) ? 0 : size - thumbSize;
  94. step = (axis == Axis.horizontal) ? 2 : -2;
  95. }
  96. /* Get the mouse offset based on slider orientation */
  97. short getTrackPos(LPARAM lParam)
  98. {
  99. final switch (axis)
  100. {
  101. case Axis.vertical:
  102. {
  103. return cast(short)HIWORD(lParam);
  104. }
  105. case Axis.horizontal:
  106. {
  107. return cast(short)LOWORD(lParam);
  108. }
  109. }
  110. }
  111. /* Flips x and y based on slider orientation */
  112. void axisFlip(ref int x, ref int y)
  113. {
  114. final switch (axis)
  115. {
  116. case Axis.vertical:
  117. {
  118. break;
  119. }
  120. case Axis.horizontal:
  121. {
  122. std.algorithm.swap(x, y);
  123. break;
  124. }
  125. }
  126. }
  127. /* Update the thumb position based on mouse position */
  128. void mouseTrackPos(LPARAM lParam)
  129. {
  130. auto trackPos = getTrackPos(lParam);
  131. /* steps:
  132. * 1. compensate for offseting the slider drawing
  133. * (we do not draw from Point(0, 0) because round caps add more pixels).
  134. * 2. position the thumb so its center is at the mouse cursor position.
  135. * 3. limit the final value between the minimum and maximum position.
  136. */
  137. thumbPos = (max(0, min(trackPos - offset - (thumbSize / 2), size)));
  138. }
  139. /* Get the neutral value (calculated based on axis orientation.) */
  140. int getValue()
  141. {
  142. final switch (axis)
  143. {
  144. // vertical sliders have a more natural minimum position at the bottom,
  145. // and since the Y axis increases towards the bottom we have to invert
  146. // this value.
  147. case Axis.vertical:
  148. {
  149. return (retro(iota(0, size + 1)))[thumbPos];
  150. }
  151. case Axis.horizontal:
  152. {
  153. return thumbPos;
  154. }
  155. }
  156. }
  157. /* Process window messages for this slider */
  158. LRESULT process(UINT message, WPARAM wParam, LPARAM lParam)
  159. {
  160. HDC hdc;
  161. PAINTSTRUCT ps;
  162. RECT rc;
  163. HDC _buffer;
  164. HBITMAP hBitmap;
  165. HBITMAP hOldBitmap;
  166. switch (message)
  167. {
  168. case WM_SIZE:
  169. {
  170. cxClient = LOWORD(lParam);
  171. cyClient = HIWORD(lParam);
  172. InvalidateRect(hwnd, NULL, FALSE);
  173. return 0;
  174. }
  175. /* We're selected, capture the mouse and track its position,
  176. * focus our window (this unfocuses all other windows).
  177. */
  178. case WM_LBUTTONDOWN:
  179. {
  180. mouseTrackPos(lParam);
  181. SetCapture(hwnd);
  182. mouseTrack = true;
  183. SetFocus(hwnd);
  184. InvalidateRect(hwnd, NULL, FALSE);
  185. return 0;
  186. }
  187. /*
  188. * End any mouse tracking.
  189. */
  190. case WM_LBUTTONUP:
  191. {
  192. if (mouseTrack)
  193. {
  194. ReleaseCapture();
  195. mouseTrack = false;
  196. }
  197. InvalidateRect(hwnd, NULL, FALSE);
  198. return 0;
  199. }
  200. /*
  201. * We're focused, change slider settings.
  202. */
  203. case WM_SETFOCUS:
  204. {
  205. isActive = true;
  206. thumbColor = colors.thumbActive;
  207. InvalidateRect(hwnd, NULL, FALSE);
  208. return 0;
  209. }
  210. /*
  211. * We've lost focus, change slider settings.
  212. */
  213. case WM_KILLFOCUS:
  214. {
  215. isActive = false;
  216. thumbColor = colors.thumbIdle;
  217. InvalidateRect(hwnd, NULL, FALSE);
  218. return 0;
  219. }
  220. /*
  221. * If we're tracking the mouse update the
  222. * slider thumb position.
  223. */
  224. case WM_MOUSEMOVE:
  225. {
  226. if (mouseTrack) // move thumb
  227. {
  228. writeln(getValue());
  229. mouseTrackPos(lParam);
  230. InvalidateRect(hwnd, NULL, FALSE);
  231. }
  232. return 0;
  233. }
  234. /*
  235. * Mouse wheel can control the slider position too.
  236. */
  237. case WM_MOUSEWHEEL:
  238. {
  239. if (isActive)
  240. {
  241. OnMouseWheel(cast(short)HIWORD(wParam));
  242. InvalidateRect(hwnd, NULL, FALSE);
  243. }
  244. return 0;
  245. }
  246. /*
  247. * Various keys such as Up/Down/Left/Right/PageUp/
  248. * PageDown/Tab/ and the Shift state can control
  249. * the slider thumb position.
  250. */
  251. case WM_KEYDOWN:
  252. case WM_KEYUP:
  253. case WM_CHAR:
  254. case WM_DEADCHAR:
  255. case WM_SYSKEYDOWN:
  256. case WM_SYSKEYUP:
  257. case WM_SYSCHAR:
  258. case WM_SYSDEADCHAR:
  259. {
  260. // message: key state, wParam: key ID
  261. keyUpdate(message, wParam);
  262. InvalidateRect(hwnd, NULL, FALSE);
  263. return 0;
  264. }
  265. /* The paint routine recreates the Cairo context and double-buffering
  266. * mechanism on each WM_PAINT message. You could set up context recreation
  267. * only when it's necessary (e.g. on a WM_SIZE message), however this quickly
  268. * gets complicated due to Cairo's stateful API.
  269. */
  270. case WM_PAINT:
  271. {
  272. hdc = BeginPaint(hwnd, &ps);
  273. GetClientRect(hwnd, &rc);
  274. auto left = rc.left;
  275. auto top = rc.top;
  276. auto right = rc.right;
  277. auto bottom = rc.bottom;
  278. auto width = right - left;
  279. auto height = bottom - top;
  280. auto x = left;
  281. auto y = top;
  282. /* Double buffering */
  283. _buffer = CreateCompatibleDC(hdc);
  284. hBitmap = CreateCompatibleBitmap(hdc, width, height);
  285. hOldBitmap = SelectObject(_buffer, hBitmap);
  286. auto surf = new Win32Surface(_buffer);
  287. auto ctx = Context(surf);
  288. drawSlider(ctx);
  289. // Blit the texture to the screen
  290. BitBlt(hdc, 0, 0, width, height, _buffer, x, y, SRCCOPY);
  291. surf.finish();
  292. surf.dispose();
  293. ctx.dispose();
  294. SelectObject(_buffer, hOldBitmap);
  295. DeleteObject(hBitmap);
  296. DeleteDC(_buffer);
  297. EndPaint(hwnd, &ps);
  298. return 0;
  299. }
  300. default:
  301. }
  302. return DefWindowProc(hwnd, message, wParam, lParam);
  303. }
  304. void drawSlider(Context ctx)
  305. {
  306. /* window backround */
  307. ctx.setSourceRGBA(colors.window);
  308. ctx.paint();
  309. ctx.translate(offset, offset);
  310. ctx.setLineWidth(10);
  311. ctx.setLineCap(LineCap.CAIRO_LINE_CAP_ROUND);
  312. /* slider backround */
  313. auto begX = 0;
  314. auto begY = 0;
  315. auto endX = 0;
  316. auto endY = size + thumbSize;
  317. axisFlip(begX, begY);
  318. axisFlip(endX, endY);
  319. ctx.setSourceRGBA(colors.back);
  320. ctx.moveTo(begX, begY);
  321. ctx.lineTo(endX, endY);
  322. ctx.stroke();
  323. /* slider value fill */
  324. begX = 0;
  325. // vertical sliders have a minimum position at the bottom.
  326. begY = (axis == Axis.horizontal) ? 0 : size + thumbSize;
  327. endX = 0;
  328. endY = thumbPos + thumbSize;
  329. axisFlip(begX, begY);
  330. axisFlip(endX, endY);
  331. ctx.setSourceRGBA(colors.fill);
  332. ctx.moveTo(begX, begY);
  333. ctx.lineTo(endX, endY);
  334. ctx.stroke();
  335. /* slider thumb */
  336. begX = 0;
  337. begY = thumbPos;
  338. endX = 0;
  339. endY = thumbPos + thumbSize;
  340. axisFlip(begX, begY);
  341. axisFlip(endX, endY);
  342. ctx.setSourceRGBA(thumbColor);
  343. ctx.moveTo(begX, begY);
  344. ctx.lineTo(endX, endY);
  345. ctx.stroke();
  346. }
  347. /*
  348. * Process various keys.
  349. * This function is continuously called when a key is held,
  350. * but we only update the slider position when a key is pressed
  351. * down (WM_KEYDOWN), and not when it's released.
  352. */
  353. void keyUpdate(UINT keyState, WPARAM wParam)
  354. {
  355. switch (wParam)
  356. {
  357. case VK_LEFT:
  358. {
  359. if (keyState == WM_KEYDOWN)
  360. {
  361. if (controlState)
  362. thumbPos -= step * 2;
  363. else
  364. thumbPos -= step;
  365. }
  366. break;
  367. }
  368. case VK_RIGHT:
  369. {
  370. if (keyState == WM_KEYDOWN)
  371. {
  372. if (controlState)
  373. thumbPos += step * 2;
  374. else
  375. thumbPos += step;
  376. }
  377. break;
  378. }
  379. case VK_SHIFT:
  380. {
  381. shiftState = (keyState == WM_KEYDOWN);
  382. break;
  383. }
  384. case VK_CONTROL:
  385. {
  386. controlState = (keyState == WM_KEYDOWN);
  387. break;
  388. }
  389. case VK_UP:
  390. {
  391. if (keyState == WM_KEYDOWN)
  392. {
  393. if (controlState)
  394. thumbPos += step * 2;
  395. else
  396. thumbPos += step;
  397. }
  398. break;
  399. }
  400. case VK_DOWN:
  401. {
  402. if (keyState == WM_KEYDOWN)
  403. {
  404. if (controlState)
  405. thumbPos -= step * 2;
  406. else
  407. thumbPos -= step;
  408. }
  409. break;
  410. }
  411. case VK_HOME:
  412. {
  413. thumbPos = 0;
  414. break;
  415. }
  416. case VK_END:
  417. {
  418. thumbPos = size;
  419. break;
  420. }
  421. case VK_PRIOR: // page up
  422. {
  423. thumbPos += step * 2;
  424. break;
  425. }
  426. case VK_NEXT: // page down
  427. {
  428. thumbPos -= step * 2;
  429. break;
  430. }
  431. /* this cam be used to switch between different slider windows.
  432. * However this should ideally be handled in a main window, not here.
  433. * We could pass this message back to the main window.
  434. * Currently unimplemented.
  435. */
  436. case VK_TAB:
  437. {
  438. //~ if (shiftState)
  439. //~ SetFocus(slidersRange.back);
  440. //~ else
  441. //~ SetFocus(slidersRange.front);
  442. //~ break;
  443. }
  444. default:
  445. }
  446. // Validate the thumb position
  447. thumbPos = (max(0, min(thumbPos, size)));
  448. }
  449. void OnMouseWheel(sizediff_t nDelta)
  450. {
  451. if (-nDelta/120 > 0)
  452. thumbPos -= step;
  453. else
  454. thumbPos += step;
  455. thumbPos = (max(0, min(thumbPos, size)));
  456. }
  457. }
  458. /* A place to hold Slider objects. Since each window has a unique HWND,
  459. * we can use this hash type to store references to objects and call
  460. * their window processing methods.
  461. */
  462. SliderWindow[HWND] SliderHandles;
  463. /*
  464. * All Slider windows will have this same window procedure registered via
  465. * RegisterClass(), we use it to dispatch to the appropriate class window
  466. * processing method. We could also place this inside the Slider class as
  467. * a static method, however this kind of dispatch function is actually
  468. * useful for dispatching to any number and types of windows.
  469. *
  470. * A similar technique is used in the DFL and DGUI libraries for all of its
  471. * windows and widgets.
  472. */
  473. extern (Windows)
  474. LRESULT winDispatch(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
  475. {
  476. auto slider = hwnd in SliderHandles;
  477. if (slider !is null)
  478. {
  479. return slider.process(message, wParam, lParam);
  480. }
  481. return DefWindowProc(hwnd, message, wParam, lParam);
  482. }
  483. extern (Windows)
  484. LRESULT mainWinProc(HWND hwnd, UINT message, WPARAM wParam, LPARAM lParam)
  485. {
  486. static int size = 100;
  487. static int padding = 20;
  488. static int thumbSize = 20;
  489. static HWND hSlider;
  490. static axis = Axis.vertical; /* vertical or horizontal orientation */
  491. switch (message)
  492. {
  493. case WM_CREATE:
  494. {
  495. hSlider = CreateWindow(sliderClass.toUTF16z, NULL,
  496. WS_CHILDWINDOW | WS_VISIBLE,
  497. 0, 0, 0, 0,
  498. hwnd, cast(HMENU)(0), // child ID
  499. cast(HINSTANCE)GetWindowLongPtr(hwnd, GWL_HINSTANCE), // hInstance
  500. NULL);
  501. SliderColors colors;
  502. with (colors)
  503. {
  504. thumbActive = RGBA(1, 1, 1, 1);
  505. thumbHover = RGBA(1, 1, 0, 1);
  506. thumbIdle = RGBA(1, 0, 0, 1);
  507. fill = RGBA(1, 0, 0, 1);
  508. back = RGBA(1, 0, 0, 0.5);
  509. window = RGBA(0, 0, 0, 0);
  510. }
  511. SliderHandles[hSlider] = new SliderWindow(hSlider, axis, size, thumbSize, padding, colors);
  512. return 0;
  513. }
  514. /* The main window creates the child window and has to set the position and size. */
  515. case WM_SIZE:
  516. {
  517. auto sliderWidth = size + padding + thumbSize;
  518. auto sliderHeight = padding;
  519. if (axis == Axis.vertical)
  520. std.algorithm.swap(sliderWidth, sliderHeight);
  521. MoveWindow(hSlider, 0, 0, sliderWidth, sliderHeight, true);
  522. return 0;
  523. }
  524. /* Focus main window, this kills any active child window focus. */
  525. case WM_LBUTTONDOWN:
  526. {
  527. SetFocus(hwnd);
  528. return 0;
  529. }
  530. case WM_KEYDOWN:
  531. {
  532. if (wParam == VK_ESCAPE)
  533. goto case WM_DESTROY;
  534. return 0;
  535. }
  536. case WM_DESTROY:
  537. {
  538. PostQuitMessage(0);
  539. return 0;
  540. }
  541. default:
  542. }
  543. return DefWindowProc(hwnd, message, wParam, lParam);
  544. }
  545. string sliderClass = "SliderClass";
  546. int myWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow)
  547. {
  548. string appName = "sliders";
  549. HWND hwnd;
  550. MSG msg;
  551. WNDCLASS wndclass;
  552. wndclass.style = CS_HREDRAW | CS_VREDRAW;
  553. wndclass.lpfnWndProc = &mainWinProc;
  554. wndclass.cbClsExtra = 0;
  555. wndclass.cbWndExtra = 0;
  556. wndclass.hInstance = hInstance;
  557. wndclass.hIcon = LoadIcon(NULL, IDI_APPLICATION);
  558. wndclass.hCursor = LoadCursor(NULL, IDC_ARROW);
  559. wndclass.hbrBackground = null; // todo: replace with null, paint bg with cairo
  560. wndclass.lpszMenuName = NULL;
  561. wndclass.lpszClassName = appName.toUTF16z;
  562. if (!RegisterClass(&wndclass))
  563. {
  564. MessageBox(NULL, "This program requires Windows NT!", appName.toUTF16z, MB_ICONERROR);
  565. return 0;
  566. }
  567. /* Separate window class for all widgets, in this case only Sliders. */
  568. wndclass.hbrBackground = null;
  569. wndclass.lpfnWndProc = &winDispatch;
  570. wndclass.cbWndExtra = 0;
  571. wndclass.hIcon = NULL;
  572. wndclass.lpszClassName = sliderClass.toUTF16z;
  573. if (!RegisterClass(&wndclass))
  574. {
  575. MessageBox(NULL, "This program requires Windows NT!", appName.toUTF16z, MB_ICONERROR);
  576. return 0;
  577. }
  578. hwnd = CreateWindow(appName.toUTF16z, "sliders example",
  579. WS_OVERLAPPEDWINDOW,
  580. CW_USEDEFAULT, CW_USEDEFAULT,
  581. CW_USEDEFAULT, CW_USEDEFAULT,
  582. NULL, NULL, hInstance, NULL);
  583. ShowWindow(hwnd, iCmdShow);
  584. UpdateWindow(hwnd);
  585. while (GetMessage(&msg, NULL, 0, 0))
  586. {
  587. TranslateMessage(&msg);
  588. DispatchMessage(&msg);
  589. }
  590. return msg.wParam;
  591. }
  592. extern (Windows)
  593. int WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int iCmdShow)
  594. {
  595. int result;
  596. void exceptionHandler(Throwable e) { throw e; }
  597. try
  598. {
  599. Runtime.initialize(&exceptionHandler);
  600. result = myWinMain(hInstance, hPrevInstance, lpCmdLine, iCmdShow);
  601. Runtime.terminate(&exceptionHandler);
  602. }
  603. catch (Throwable o)
  604. {
  605. MessageBox(null, o.toString().toUTF16z, "Error", MB_OK | MB_ICONEXCLAMATION);
  606. result = 0;
  607. }
  608. return result;
  609. }