The following example incorporates ideas from the previous sections to show how you might approach the task of writing a compound widget. The widget is called CW_DICE, and it simulates a single six-sided die. XDICE is discussed in Using CW_DICE in a Widget Program.
| Note |
cw_dice.pro can be found in the lib subdirectory of the IDL distribution. xdice.pro can be found in the examples/widgets subdirectory of the IDL distribution. You should examine these files for additional details and comments not included here. We present sections of the code here for didactic purposes-there is no need to re-create either of these files yourself.
The CW_DICE compound widget has the following features:
Almost any compound widget will have an associated state. The following is the state of CW_DICE:
The first four items are stored in a per-widget structure kept in one of the child widget's user values. Since the bitmaps never change, it makes sense to keep them in a COMMON block to be accessed freely by all the CW_DICE routines. It also makes sense to use a single random number seed for the entire CW_DICE class rather than one per instance to avoid the situation where multiple dice, having been created at the same time, have the same seed and thus display the same value on each roll.
| Note |
Given the above decisions, it is now possible to write the CW_DICE procedure:
;Value is an optional argument that lets the caller set the initial
;die value to a value between 1 and 6. UVALUE will simply be passed
;on to the root base of CW_DICE. The TUMBLE keywords let the user
;adjust the tumble count and period.
FUNCTION cw_dice, parent, value, UVALUE=uvalue, $
TUMBLE_CNT=tumble_cnt, TUMBLE_PERIOD=tumble_period
;This COMMON block holds the bitmaps and random number generator
;seed.
COMMON CW_DICE_BLK, seed, faces
;Provide defaults for the keywords.
IF ~ KEYWORD_SET(tumble_cnt) THEN tumble_cnt=10
;Guard against a nonsensical request.
IF (tumble_cnt LT 1) THEN tumble_cnt=10
;Default tumble period in seconds.
IF ~ KEYWORD_SET(tumble_period) THEN tumble_period=.05
IF (tumble_period lt 0) THEN tumble_period=.05
IF ~ KEYWORD_SET(uvalue) THEN uvalue=0
;Return to caller if an error occurs.
ON_ERROR, 2
;Generate the die face bitmaps. The actual code for this is
;omitted here because it doesn't add much to the example, but
;it can be found in the CW_DICE.PRO file.
faces=LONARR(192)
;Use RANDOMU to pick the initial value of the die unless the user
;provided one.
IF(N_ELEMENTS(value) EQ 0) THEN value = FIX(6*RANDOMU(seed) + 1)
;Construct a state variable for this instance.
state = { value:value, tumble_cnt:FIX(tumble_cnt), $
tumble_period:tumble_period, remaining:0 }
;Create the base widget, passing the UVALUE through for the
;caller. Notice that we also register an event function and
;GET/SET value routines which will be called by WIDGET_CONTROL
;on our behalf.
base = WIDGET_BASE(parent, UVALUE=uvalue, $
EVENT_FUNC='CW_DICE_EVENT', $
FUNC_GET_VALUE='CW_DICE_GET_VALUE', $
PRO_SET_VALUE='CW_DICE_SET_VALUE')
;Create the die, setting its bitmap to the current value.
die = WIDGET_BUTTON(base, VALUE=faces[*, *, value-1])
;Save the state in the first child's user value. Notice the
;use of the NO_COPY keyword for efficiency.
WIDGET_CONTROL, WIDGET_INFO(base, /CHILD), $
SET_UVALUE=state, /NO_COPY
;The result of a compound widget is always the ID of its topmost
;widget.
RETURN, base
END
The above code makes reference to two routines named CW_DICE_SET_VAL and CW_DICE_GET_VAL. By using the FUNC_GET_VALUE and PRO_SET_VALUE keywords to WIDGET_BASE, WIDGET_CONTROL can call these routines whenever the user makes a WIDGET_CONTROL, SET_VALUE or GET_VALUE request:
;This is the SET_VALUE routine for CW_DICE. The number and type of ;the arguments is defined by WIDGET_CONTROL. Id is the widget ID of ;a CW_DICE, and value is the user's requested value. PRO cw_dice_set_val, id, value COMMON CW_DICE_BLK, seed, faces ;Get the ID of the first child of the CW_DICE widget. This is ;where the state information is stored. stash = WIDGET_INFO(id, /CHILD) ;Get the state structure. WIDGET_CONTROL, stash, GET_UVALUE=state, /NO_COPY ;If the value is outside the range [1,6] then roll the die as if ;the user pressed the button. IF (value LT 1) OR (value GT 6) THEN BEGIN ;CW_DICE_ROLL rolls the dice. It's a separate function because ;our event handler also needs to use it. CW_DICE_ROLL, stash, state ENDIF ELSE BEGIN ;If the value is in the range [1,6] then simply set the die ;to that value without rolling. state.value=value ;Set the new bitmap on the button. We take advantage of the ;fact that stash must be the widget ID of the button widget, ;since the base only has one child. WIDGET_CONTROL, stash, SET_VALUE=faces[*,*, value-1] ENDELSE ;Restore the state in the child UVALUE. WIDGET_CONTROL, stash, SET_UVALUE=state, /NO_COPY END ;This is the GET_VALUE routine for CW_DICE. The number and type of ;the arguments is defined by WIDGET_CONTROL. Id is the widget ID of ;a CW_DICE. The return value of this function must be the current ;value of the compound widget, as defined by that widget. FUNCTION cw_dice_get_val, id ;Get the ID of the first child of the CW_DICE widget. This is ;where the state information is stored. stash = WIDGET_INFO(id, /CHILD) ;Get the state structure. WIDGET_CONTROL, stash, GET_UVALUE=state, /NO_COPY ;Get the current value from the state structure. ret = state.value ;Restore the state in the child UVALUE. WIDGET_CONTROL, stash, SET_UVALUE=state, /NO_COPY RETURN, ret END
CW_DICE_SET_VALUE makes reference to a procedure named CW_DICE_ROLL that does the actual dice rolling. Rolling is implemented as follows:
;Roll the specified die. Dice is the widget ID of the button ;holding the bitmap, and state is the state as extracted from the ;CW_DICE UVALUE by the caller. PRO cw_dice_roll, dice, state COMMON CW_DICE_BLK, seed, faces ;First time. IF (state.remaining EQ 0) THEN BEGIN ;Set the counter for the number of tumbles remaining. state.remaining = state.tumble_cnt ;Determine final value now. state.value = FIX(6*RANDOMU(seed)+1) ENDIF ;Last time. IF (state.remaining EQ 1) THEN BEGIN ;Use the previously-saved final result. value = state.value ;Not the last time. ENDIF ELSE BEGIN ;Generate an intermediate value. value = FIX(6 * RANDOMU(seed) + 1) ;Since this isn't the last tumble, make the next timer request. WIDGET_CONTROL, dice, TIMER=state.tumble_period ENDELSE ;Display the correct bitmap. WIDGET_CONTROL, dice, SET_VALUE=faces[*,*, value-1] ;Decrement tumble counter. state.remaining = state.remaining-1 END
This leads us to the event handler function:
FUNCTION cw_dice_event, event
;The primary use for the HANDLER field of event structures is to
;make finding the root of a compound widget easy.
base = event.handler
;Get the ID of the first child of the CW_DICE widget. This is
;where the state information is stored.
stash = WIDGET_INFO(base, /CHILD)
;Get the state structure.
WIDGET_CONTROL, stash, GET_UVALUE=state, /NO_COPY
;Roll the die and display a new bitmap.
CW_DICE_ROLL, stash, state
;This event handler expects to see button press events generated
;from a user action as well as TIMER events from CW_DICE_ROLL. We
;only want to issue events for the button presses. Even though
;the die still has several tumbles left, we know that the final
;value is in the state now.
IF (TAG_NAMES(event, /STRUCTURE_NAME) NE 'WIDGET_TIMER') THEN $
;Create an event.
ret = { CW_DICE_EVENT, ID:base, TOP:event.top, $
HANDLER:0L, VALUE:state.value} $
ELSE ret = 0
;By not returning an event structure, we cause the event to be
;swallowed by WIDGET_EVENT.
;Restore the state in the child UVALUE.
WIDGET_CONTROL, stash, SET_UVALUE=state, /NO_COPY
RETURN, ret
END
We can use CW_DICE to implement an application named XDICE. XDICE displays two dice as well as a "Roll" button. Pressing either die causes it to roll individually. Pressing the "Roll" button causes both dice to roll together. A text widget at the bottom displays the current value.
| Note |
xdice.pro can be found in the examples/widgets subdirectory of the IDL distribution. You can run the program from the IDL distribution by entering: xdice
at the IDL command prompt. See Running the Example Code if IDL does not run the program as expected. You should examine the files for additional details and comments not included here.
;Providing standard keywords usually found in other widget
;applications is a nice finishing touch. GROUP is easy to support
;since we just pass it to XMANAGER.
PRO xdice, GROUP=group
;Create the top-level base that holds everything else.
base = WIDGET_BASE(/COLUMN, TITLE='Pair O'' Dice')
;A button group compound widget is used to implement the Done and
;Roll buttons. The SPACE keyword simply causes the buttons to be
;spread out from each other.
bgroup = CW_BGROUP(base, ['Done', 'Roll'], /ROW, SPACE=50)
;Create a row base to hold the dice. XPAD moves the first die
;away from the left side of the application and helps center the
;dice.
dice = WIDGET_BASE(base, /ROW, XPAD=20)
;The first die.
d1 = CW_DICE(dice)
;The second die.
d2 = CW_DICE(dice)
;We need the initial dice values to set the label appropriately.
;We could have specified initial values for the calls to CW_DICE
;above, but it seems better to let them be different on each
;invocation.
WIDGET_CONTROL, d1, GET_VALUE=d1v
WIDGET_CONTROL, d2, GET_VALUE=d2v
;Format the initial label text.
str=STRING(FORMAT='("Current Value: ",I1,", ",I1)', d1v, d2v)
;This label is used to textually display the current dice values.
label = WIDGET_LABEL(base, VALUE=str)
;Information that is needed in the event handler.
state = { bgroup:bgroup, d1:d1, d2:d2, label:label }
;Save useful information in the base UVALUE, and realize the
;application.
WIDGET_CONTROL, base, SET_UVALUE=state, /NO_COPY, /REALIZE
;Pass control to XMANAGER.
XMANAGER, 'xdice', base, GROUP=group
END
The following event handler is called by XMANAGER to process events for the XDICE application:
PRO xdice_event, event
;Recover the state.
WIDGET_CONTROL, event.top, GET_UVALUE=state, /NO_COPY
;Either the Done or Roll button was pressed.
IF (event.ID EQ state.bgroup) THEN BEGIN
;The Done button.
IF (event.VALUE EQ 0) THEN BEGIN
WIDGET_CONTROL, /DESTROY, event.TOP ;Destroy the
application.
;Return now to avoid trying to update the widget label we
;just destroyed.
RETURN
;The Roll button.
ENDIF ELSE BEGIN
;Roll the first die by asking for an out of range value.
WIDGET_CONTROL, state.d1, SET_VALUE=-1
;Roll the second die.
WIDGET_CONTROL, state.d2, SET_VALUE=-1
ENDELSE
ENDIF
;Get value of first die.
WIDGET_CONTROL, state.d1, GET_VALUE=d1v
;Get value of second die.
WIDGET_CONTROL, state.d2, GET_VALUE=d2v
;Format the initial label text.
str = STRING(FORMAT='("Current Value: ",I1,", ",I1)', d1v, d2v)
;Update the label.
WIDGET_CONTROL, state.label, SET_VALUE=str
;Restore the state.
WIDGET_CONTROL, event.TOP, SET_UVALUE=state, /NO_COPY
END