• Non ci sono risultati.

Wrapping up OCaml: Designing a GUI Library

18.12 Listeners and Notifiers

repaint = (fun (g:Gctx.gctx) ->

Gctx.draw_string g lbl.contents);

size = (fun () ->

Gctx.text_size lbl.contents);

handle = (fun (g:Gctx.gctx) (e:Gctxt.event) -> ()) },

{set_label = (fun (r:string) -> lbl.contents <- r);

get_label = (fun () -> lbl.contents)})

Now when we call thelabel function, it returns a pair consisting of a widget and a label controller that can be used to change the string displayed by the label.

More generally, any stateful widget will have its own kind of controller that can be used to modify the widgets state.

18.12 Listeners and Notifiers

Different widgets may need to react to different kinds of events. For example, a button needs to “listen” for mouseclick events and then run some appropriate code; a scrollbar might have to “listen” for mouse drag events (mouse motion with the button down); a textbox widget might have to “listen” for key press events. Moreover, the way that a button (or other widget) responds to a certain event might change depending on application context. In general, whether or not a widget should “listen” for a particular event might also depend on application-specific state.

How can we easily capture the wide variety of possible ways that a widget might want to interact with user-generated events? How can we cdmodularly add or remove code for processing events? Our solution (which is based on Java’s Swing library) is to introduce a new kind of widget called a notifier. The idea is that a notifier widget “eavesdrops on” or “listens to” the events flowing through a part of the widget hierarchy. It manages a list of event listeners, which are a bit like

handlefunctions except that they don’t really participate in routing events through the widget hierarchy—they simply listen for a particular kind of event, react to it, and then either propagate the event or stop it from being further processed.

Figure 18.7 shows pictorially, how we might add anotifierwidget to the “Hello World” example.

Notifiers, like the stateful version oflabelwidgets discussed above, maintain

!"#$%&%'#(

3456789(:*!!(7866(

;-'+%'(

<=*"'(

;-'+%'(

!*;%!(

<=*"'(

#=*1%( ;-'+%'(

!*;%!(

>"+?%$($'%%(

Hello! World!

@&($<%(#1'%%&(

&-./%'( !6(AA(!7(AA(!B(AA(CD((

E#%'(1!"1F#G(

?%&%'*.&?(

%H%&$(%(

Figure 18.7: A notifier widget maintains in its local state a list of “listeners” that get a chance to process events that flow through the widget tree. Which listeners are associated with the notifier can be changed using a notifier controller.

some local state—in this case a list ofevent_listenerobjects. Thus, each notifier widget comes equipped with a notifier_controller that can be used to modify the list of listeners. For simplicity in the code below, thenotifier_controlleronly allows newevent_listenerobjects to be added to thenotifier; it is easy to extend this idea to allowevent_listeners to be removed as well.

These design criteria lead us to create a type forevent_listenerfunctions that looks like this:

type event_listener =

Gctx.gctx -> Gctx.event -> unit

An event_listener is just a function that, like a handle method of a widget, takes aGctx.gctxandGctx.eventand processes the event.

Once we have defined the type ofevent_listeners, it is straightforward to de-fine the behavior of thenotifierwidget and itsnotifier_controller:

(*

A notifier_controller is associated with a notifier widget.

It allows the program to add event listeners to the notifier.

*)

type notifier_controller = {

add_event_listener: event_listener -> unit }

(*

A notifier widget is a widget "wrapper" that doesn't take up any extra screen space -- it extends an existing widget with the ability to react to events. It maintains a list of

"listeners" that eavesdrop on the events propagated through the notifier widget.

When an event comes in to the notifier, it is passed to each listener in turn and then propagated to the child widget.

*)

let notifier (w: t) : widget * notifier_controller = let listeners = { contents = [] } in

{

repaint = w.repaint;

handle =

(fun (g: Gctx.gctx) (e: Gctx.event) ->

List.iter (fun h -> h g e) listeners.contents;

w.handle g e);

size = w.size },

{

add_event_listener =

fun (newl: event_listener) ->

listeners.contents <- newl::listeners.contents }

With this infrastructure in place, it is easy to define specialized event listen-ers that react to particular kinds of events. For example, it is useful to define a

mouseclick_listener, parameterized by an action to perform when the mouse is clicked:

(* Performs an action upon receiving a mouse click. *) let mouseclick_listener

(action: unit -> unit) : event_listener = fun (g:Gctx.gctx) (e: Gctx.event) ->

if Gctx.event_type e = Gctx.MouseDown then action ()

18.13 Buttons (at last!)

Our GUI library finally has enough functionality to implement a traditional button widget: A button is just a label widget wrapped in a notifier. The resulting widget has both a label_controller and a notifier_controller, which can be used to change the state of the button.

(*

A button has a string, which can be controlled by the corresponding label_controller, and a notifier_controller, which can be used to add listeners (e.g. a mouseclick_listener) that will perform an action when the button is pressed.

*)

let button (s: string) : widget * label_controller

* notifier_controller = let (w, lc) = label s in

let (w', nc) = notifier w in (w', lc, nc)

To add the ability to react to a mouse click event to the button, which is typically the desired behavior, we can simply use the notifier_controller’s

add_event_listenerfunction to add amouseclick_listener. For example, to cre-ate a button that prints"Hello, world!" to the console each time it is clicked, we could write the following code:

let (hw_button, _, hw_nc) = button "Print It!"

let print_hw () : unit =

print_endline "Hello, world!"

;; hw_nc.add_event_listener (mouseclick_listener print_hw)

Thehw_buttonwidget could then be added to a larger widget tree using layout widgets, or, simply run as the entire “application”. The latter would be accom-plished by doing:

;; Eventloop.run hw_button