PageRenderTime 172ms CodeModel.GetById 85ms app.highlight 78ms RepoModel.GetById 1ms app.codeStats 1ms

/win32_slider.d

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