PageRenderTime 109ms CodeModel.GetById 22ms app.highlight 80ms RepoModel.GetById 1ms app.codeStats 0ms

/win32_window_framework.d

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