|
| 1 | +#lang scribble/manual |
| 2 | + |
| 3 | +@(require (for-label racket/base |
| 4 | + racket/class |
| 5 | + (prefix-in gui: racket/gui) |
| 6 | + racket/gui/easy |
| 7 | + racket/gui/easy/operator)) |
| 8 | + |
| 9 | +@title{Custom Views} |
| 10 | + |
| 11 | +@(define canvas-list-link |
| 12 | + (link "https://github.com/massung/racket-canvas-list/" (tt "canvas-list<%>"))) |
| 13 | + |
| 14 | +You can create your own views by implementing the @racket[view<%>] |
| 15 | +interface. |
| 16 | + |
| 17 | +As an example, let's wrap Jeffrey Massung's @|canvas-list-link|. I find |
| 18 | +it helps to work backwards from the API you'd like to end up with. In |
| 19 | +this case, that would be: |
| 20 | + |
| 21 | +@racketblock[ |
| 22 | +(canvas-list |
| 23 | + |@entries| |
| 24 | + (λ (item state dc w h) |
| 25 | + (draw-item ...)) |
| 26 | + (λ (item) |
| 27 | + (printf "double-clicked ~s~n" item))) |
| 28 | +] |
| 29 | + |
| 30 | +A @racketid[canvas-list] should take an observable of a list of |
| 31 | +entries, a function that knows how to draw each entry to a |
| 32 | +@racket[gui:dc<%>] and a callback for when the user double-clicks an |
| 33 | +entry. The @racketid[canvas-list] function should probably then look |
| 34 | +something like this: |
| 35 | + |
| 36 | +@racketblock[ |
| 37 | + (define (canvas-list |@entries| draw [action void]) |
| 38 | + (new canvas-list-view% |
| 39 | + [|@entries| |@entries|] |
| 40 | + [draw draw] |
| 41 | + [action action])) |
| 42 | +] |
| 43 | + |
| 44 | +Next, we can define a skeleton implementation of |
| 45 | +@racketid[canvas-list-view%]: |
| 46 | + |
| 47 | +@racketblock[ |
| 48 | + (define canvas-list-view% |
| 49 | + (class* object% (view<%>) |
| 50 | + (init |@entries| draw action) |
| 51 | + (super-new) |
| 52 | + |
| 53 | + (define/public (dependencies) |
| 54 | + (error 'create "not implemented")) |
| 55 | + |
| 56 | + (define/public (create parent) |
| 57 | + (error 'create "not implemented")) |
| 58 | + |
| 59 | + (define/public (update v what val) |
| 60 | + (error 'update "not implemented")) |
| 61 | + |
| 62 | + (define/public (destroy v) |
| 63 | + (error 'destroy "not implemented")))) |
| 64 | +] |
| 65 | + |
| 66 | +Views must communicate what @tech{observables} they depend on to their |
| 67 | +parents. In our case, that's straightforward: |
| 68 | + |
| 69 | +@racketblock[ |
| 70 | + (define canvas-list-view% |
| 71 | + (class* object% (view<%>) |
| 72 | + ... |
| 73 | + |
| 74 | + (define/public (dependencies) |
| 75 | + (list |@entries|)) |
| 76 | + |
| 77 | + ...)) |
| 78 | +] |
| 79 | + |
| 80 | +When a view is rendered, its parent is in charge of calling its |
| 81 | +@method[view<%> create] method. The create method must instantiate a |
| 82 | +GUI object, associate it the passed-in @racketid[parent], perform any |
| 83 | +initialization steps and then return it. In our case: |
| 84 | + |
| 85 | +@racketblock[ |
| 86 | + (define canvas-list-view% |
| 87 | + (class* object% (view<%>) |
| 88 | + ... |
| 89 | + |
| 90 | + (define/public (create parent) |
| 91 | + (new canvas-list% |
| 92 | + [parent parent] |
| 93 | + [items (obs-peek |@entries|)] |
| 94 | + [paint-item-callback (λ (self entry state dc w h) |
| 95 | + (draw entry state dc w h))] |
| 96 | + [action-callback (λ (self item event) |
| 97 | + (action item))])) |
| 98 | + |
| 99 | + ...)) |
| 100 | +] |
| 101 | + |
| 102 | +When the @tech{observables} the view depends on change, its parent |
| 103 | +will call its @method[view<%> update] method with the GUI object that |
| 104 | +the view returned from its @method[view<%> create] method, the |
| 105 | +observable that changed and the observable's value when it changed. |
| 106 | +The view is then in charge of modifying its GUI object appropriately. |
| 107 | + |
| 108 | +@racketblock[ |
| 109 | + (define canvas-list-view% |
| 110 | + (class* object% (view<%>) |
| 111 | + ... |
| 112 | + |
| 113 | + (define/public (update v what val) |
| 114 | + (case/dep what |
| 115 | + [|@entries| (send v set-items val)])) |
| 116 | + |
| 117 | + ...)) |
| 118 | +] |
| 119 | + |
| 120 | +Finally, when a view is no longer visible, its @method[view<%> |
| 121 | +destroy] method is called to dispose of the GUI object and perform any |
| 122 | +teardown actions. In our case, there's nothing to do. We can let |
| 123 | +garbage collection take care of destroying the @racketid[canvas-list%] |
| 124 | +object: |
| 125 | + |
| 126 | +@racketblock[ |
| 127 | + (define canvas-list-view% |
| 128 | + (class* object% (view<%>) |
| 129 | + ... |
| 130 | + |
| 131 | + (define/public (destroy v) |
| 132 | + (void)))) |
| 133 | +] |
| 134 | + |
| 135 | +When the view becomes visible again, its @method[view<%> create] |
| 136 | +method is called again and the whole cycle repeats itself. |
| 137 | + |
| 138 | +That's all there is to it. See the "hn.rkt" example for a program |
| 139 | +that uses a custom view. |
0 commit comments