This code in the text box's GotFocus method allows the number to be entered correctly: WITH This *** Save the input mask .cOldInputMask = .InputMask *** Remove separators from input m
Trang 1.Value = EVAL( cAlias + '.' + cField )
ENDIF
ELSE
*** Otherwise, save the current work area
*** before switching to the specified table
lnSelect = SELECT()
SELECT ( cAlias )
*** And locate the specified record
LOCATE FOR UPPER( ALLTRIM( EVAL (.cField ) ) ) = UPPER( lcSoFar )
At this point we have either found the desired record in cAlias or we are at the end of the
file All that remains to be done is to reset the highlighted portion of the text box correctly andrefresh the controls in the parent container (if this was specified by setting lRefreshParent =.T.):
*** If we need to refresh the parent container do it here
*** If we have refreshed the controls in the parent container,
*** there are timing issues to overcome
*** Even though SelStart and SelLength have the correct values,
*** the search box does not appear highlighted correctly without this delay
=INKEY( 1, 'H' )
ENDWITH
Notice the INKEY() command here, and take some time to read the comment above if youhaven't already This problem is not specific to our incremental search text box and timingissues like this are not uncommon in Visual FoxPro (We have also run into it when displayingmulti-select list boxes in which the previous selections are highlighted In that case, using
INKEY() in the form's refresh allows the list box to be highlighted correctly.) It is interesting tonote that the INKEY() command is not required in the code above when lRefreshParent = F.
This lends support to the assumption that this is nothing more than a timing issue The shortpause allows Visual FoxPro to catch up
Numeric text box (Example: CH04.VCX::txtNum and txtNumeric)
Visual FoxPro has inherited some serious shortcomings with respect to entering numeric datafrom its FoxPro ancestors It's not too bad when the entire field is selected, and the number isnot formatted with separators However, problems begin to occur when the insertion point is
Trang 2not at the beginning of the displayed value Sometimes the user is trying to type the number 10,but all he can type is 1 and, with confirm set off, the value of the text box becomes 1 and thecursor moves on to the next field We have also seen the opposite problem The user wants toenter 3 but after typing 3 and exiting the control, the number 30 is displayed instead of theintended 3 So what can a Visual FoxPro developer do to help?
There are a few workarounds to this problem You could create a numeric text box toselect the entire field and remove any separators used to format the number This code in the
text box's GotFocus method allows the number to be entered correctly:
WITH This
*** Save the input mask
cOldInputMask = InputMask
*** Remove separators from input mask
InputMask = STRTRAN( cOldInputMask, ',', '' )
*** Perform Visual FoxPro native GotFocus()
TextBox::GotFocus()
*** Select the entire field
SelStart = 0
SelLength = LEN( cOldInputMask )
*** Don't let base class behavior reset SelStart/SelLength
NODEFAULT
ENDWITH
Since we need to change the text box's inputMask to accomplish this, we add a custom property called cOldInputMask to hold the original inputMask assigned to the control We will need this property in the text box's LostFocus method in order to restore the formatting like so:
This.InputMask = This.cOldInputMask
Of course, we already have a text box class that correctly selects the entire field where you
tab into it or mouse-click on it Our base class text box does this when SelectOnEntry = T So all we have to do is base our numeric text box on our base class text box, set SelectOnEntry to true, and put this code in its GotFocus method:
WITH This
*** Save the original input mask
cOldInputMask = InputMask
*** Remove separators from input mask
InputMask = STRTRAN( cOldInputMask, ',', '' )
*** Perform the parent class behavior
DODEFAULT()
ENDWITH
The numeric text box described above may be sufficient for you It's easy to create, doesn'tcontain a lot of code and works around the problems involved in entering numeric data
correctly But wouldn't it be nicer to have a numeric text box that does calculator style entry
from right to left? We have seen several examples of such text boxes and, in our opinion, theyall suffer from the same shortcoming Either the cursor can be seen flashing to the left ascharacters appear from the right or there is no cursor at all Both of these solutions tend tomake things confusing for the user So we set out to create the ultimate Visual FoxPro numeric
Trang 3text box And we very quickly discovered why none currently exists It was HARD! So we
hope you find this useful as it is the result of entirely too many hours and too much blood,sweat, and tears Not only does it do calculator style entry, the cursor is also positioned on thecorrect character When the value in the text box is not selected, you can even delete or insertindividual digits in the middle of the number displayed in the text box
The numeric text box is a simple control to use Just drop it on a form, page or container
and set its ControlSource property That's all! You don't even need to set its InputMask unless
you want the control to be unbound because it is capable of formatting itself when bound Theway most numeric text boxes work is by changing the value into a character string,
manipulating the string and the InputMask and then re-converting the string to a numeric value.
However, our numeric text box is actually an unbound control (even though you can set it up
as if it were bound) and works because its value actually is a character string and is
manipulated as such It uses custom code to update its ControlSource with the numeric
equivalent of the character string which is its value
This example is designed to work either unbound or bound to a field in a table, cursor orview If you need to bind to a form property, the code will need a little modification to account
for it An example of how to do this can be found in the UpdateControlSource method of the spnTime class described later in this chapter.
The following, eight custom properties were added to our custom numeric text box Theyare all used internally by the control and you do not need to do anything with them explicitly
Table 4.2 Custom properties of the numeric text box
Property Description
CcontrolSource Saves the controlSource if this is a bound control before it is unbound in the Init
method Cfield Field name portion of ControlSource if it is bound
CinputMask Stores original inputMask when it is specified, otherwise stores the inputMask
constructed by the control ColdConfirm Original setting of SET( 'CONFIRM' ) saved in GotFocus so it can be restored in
LostFocus.
ColdBell Original setting of SET('BELL') saved in GotFocus so it can be restored in
LostFocus Cpoint Character returned by SET( 'POINT' )
Cseparator Character returned by SET( 'SEPARATOR' )
Ctable Table name portion of ControlSource if it is bound
LchangingFocus Flag set to suppress KEYBOARD '{END}' which is used to position the cursor at
the rightmost position in the text box If we do this when the control is losing focus,
it messes up the tab order NmaxVal Maximum value allowed in the control
The SetUp method, called by the TextBox's Init method, saves the content of the
ControlSource property to the custom cControlSource property before unbinding the control from its ControlSource It also determines, and sets up, the InputMask for the control Even
though this code is executed only once when the text box is instantiated, we have put it in acustom method to avoid coding explicitly in events whenever possible Notice that we use
Trang 4SET( 'POINT' ) and SET( 'SEPARATOR' ) to specify the characters used as the
decimal point and separator instead of hard-coding a specific character This allows the control
to be used just as easily in Europe as it is in the United States without the necessity of
modifying code:
LOCAL laFields[1], lnElement, lnRow, lcIntegerPart, lcDecimalPart, lcMsg
WITH This
*** Save the decimal point and separator characters so we can use this
*** class in either the USA or Europe
cPoint = SET( 'POINT' )
cSeparator = SET( 'SEPARATOR' )
*** Save the controlSource
IF EMPTY( cControlSource )
.cControlSource = ControlSource
ENDIF
Next we parse the table name and field name out of the controlSource It may seem
redundant to store these two properties since they can easily be obtained by executing thissection of code However, because there are various sections of code that refer to one or theother, it's much faster to save them as localized properties when the text box is instantiated
You may wonder then why we have bothered to have a cControlSource property when we
could just as easily have referred to This.cTable + '.' + This.cField We believe this is moreself-documenting and makes the code more readable This is just as important as performanceconsiderations Be nice to the developer who inherits your work You never know when you
may wind up working for her! This code from the text box's Setup method makes its purpose
very clear:
IF ! EMPTY( cControlSource )
*** If This is a bound control, save table and field bound to
*** Parse out the name of the table if ControlSource is prefixed by an alias
IF AT( '.', cControlSource ) > 0
cTable = LEFT( cControlSource, AT( '.', cControlSource ) - 1 )
cField = SUBSTR( cControlSource, AT( '.', cControlSource ) + 1 ) ELSE
The setup routine also saves any specified InputMask to the cInputMask property If this is
a bound control, you do not need to specify an InputMask, although you can do so if you wish.
This section of the code will do it for you by getting the structure of the underlying field It
also sets the control's nMaxVal property, required during data entry to ensure the user cannot
enter a number that is too large, causing a numeric overflow error:
*** Find out how the field should be formatted if no InputMask specified
IF EMPTY(.InputMask)
AFIELDS(laFields, cTable)
lnElement = ASCAN(laFields, UPPER(.cField))
Trang 5IF lnElement > 0
*** If the field is of integer or currency type
*** and no InputMask is specified, set it up for
*** the largest value the field will accommodate
CASE laFields[ lnRow, 2 ] = 'N'
lcIntegerPart = REPLICATE('9', laFields[lnRow, 3] – ;
laFields[lnRow, 4] - 1)
lcDecimalPart = REPLICATE('9', laFields[lnRow, 4])
cInputMask = lcIntegerPart + '.' + lcDecimalPart
nMaxVal = VAL( cInputMask )
OTHERWISE
lcMsg = IIF( INLIST( laFields[ lnRow, 2 ], 'B', 'F' ), ;
'You must specify an input mask for double and float data types', ; 'Invalid data type for this control' ) + ': ' + This.Name
MESSAGEBOX( lcMsg, 16, 'Developer Error!' )
RETURN F.
ENDCASE
ENDIF
ELSE
cInputMask = STRTRAN( InputMask, ',', '' )
nMaxVal = VAL( cInputMask )
ENDIF
ELSE
.cInputMask = STRTRAN( InputMask, ',', '' )
.nMaxVal = VAL( cInputMask )
ENDIF
Now that we have saved the Control Source to our internal cControlSource property, we can safely unbind the control We also set the lChangingFocus flag to true This ensures our
numeric text box will keep the focus if it's the first object in the tab order when SET(
'CONFIRM' ) = 'OFF' This is essential because our text box positions the cursor by using a
KEYBOARD '{END}'. This would immediately set focus to the second object in the tab orderwhen the form is instantiated because we cannot force a SET CONFIRM OFF until our text boxactually has focus:
Trang 6cControlSource evaluates to a numeric value, the first thing we must do is convert this value to
a string We then format the string nicely with separators and position the cursor at the end ofthe string:
WITH This
*** cControlSource is numeric, so convert it to string
IF ! EMPTY ( cControlSource )
IF ! EMPTY ( EVAL( cControlSource ) )
Value = ALLTRIM( PADL ( EVAL( cControlSource ), 32 ) )
The AddSeparators method is used to display the formatted value of the text box The first
step is to calculate the length of the integer and decimal portions of the current string:
LOCAL lcInputMask, lnPointPos, lnIntLen, lnDecLen, lnCnt
*** Reset the InputMask with separators for the current value of the text box lcInputMask = ''
WITH This
*** Find the length of the integer portion of the number
lnPointPos = AT( cPoint, ALLTRIM( Value ) )
*** Find the length of the decimal portion of the number
IF AT( cPoint, cInputMask ) > 0
lnDecLen = LEN( SUBSTR( cInputMask, AT( cPoint, cInputMask ) + 1 ) ) ELSE
lnDecLen = 0
ENDIF
Once we have calculated these lengths, we can reconstruct the inputMask, inserting
commas where appropriate The easy way is to count characters beginning with the rightmostcharacter of the integer portion of the string We can then insert a comma after the format
character if the current character is in the thousands position (lnCnt = 4), the millions position (lnCnt = 7) and so on However, if the text box contains a negative value, this could possibly
result in "-,123,456" being displayed as the formatted value We check for this possibility afterthe commas are inserted:
Trang 7*** Insert the separator at the appropriate interval
*** Make sure that negative numbers are formatted correctly
IF LEFT( ALLTRIM( Value ), 1 ) = '-'
In order for the user to enter data, the control must receive focus This requires that a
number of things be done in the GotFocus method The first is to make sure that SET ( 'CONFIRM' ) = 'ON' and that the bell is silenced, otherwise we will have problems when we
KEYBOARD '{END}' to position the cursor at the end of the field Next we have to strip the
separators out of the InputMask, and finally we want to execute the default SelectOnEntry behavior of our base class text box So the inherited 'Select on Entry' code in the GotFocus
method has to be modified to handle these additional requirements, as follows:
LOCAL lcInputMask, lnChar
*** Reset the InputMask for the current value of the text box
Trang 8Like our incremental search text box, the numeric text box handles the keystroke in the
HandleKey method that is called from InteractiveChange after KeyPress has processed the keystroke The incremental search text box does not require any code in the KeyPress method
because all characters are potentially valid In the numeric text box, however, only a subset of
the keystrokes are valid We need to trap any illegal keystrokes in the control's KeyPress
method and when one is detected, issue a NODEFAULT to suppress the input We do this by
passing the current keystroke to the OK2Continue method If it's an invalid character, this method returns false to the KeyPress method, which issues the required NODEFAULT command:
LPARAMETERS tnKeyCode
LOCAL lcCheckVal, llretVal
llRetVal = T.
WITH This
Since the current character does not become a part of the text box's value until after the
InteractiveChange method has completed, we can prevent multiple decimal points by checking
for them here:
DO CASE
*** Make sure we only allow one decimal point in the entry
CASE CHR( tnKeyCode ) = cPoint && decimal point
IF AT( cPoint, Value ) > 0
The most complex task handled by the OK2Continue method is the check for numeric
overflow We do this by determining what the value will be if we allow the current keystroke
and compare this value to the one stored in the control's nMaxVal property:
*** Guard against numeric overflow!!!!
Trang 9CASE SelStart = 0
lcCheckVal = CHR( tnKeyCode ) + ALLTRIM( Value )
CASE SelStart = LEN( ALLTRIM( Value ) )
lcCheckVal = ALLTRIM( Value ) + CHR( tnKeyCode )
*** Make sure that if the input mask specifies a
*** certain number of decimals, we don't allow more
*** than the number of decimal places specified
ENDIF && tnKeyCode > 47 AND tnKeyCode < 58
ENDIF && SelLength = 0
ENDIF && ! EMPTY( cInputMask )
Like our incremental search text box, a lot of work is done using a little bit of code in our
HandleKey method We can handle the positioning of the cursor and formatting of the value here because InteractiveChange will only fire after KeyPress has succeeded Therefore, handling the keystrokes here requires less code than handling them directly in KeyPress:
LOCAL lcInputMask, lnSelStart, lnEnd
*** Save the cursor's insertion point and length of the value typed in so far lnSelStart = This.SelStart
lnEnd = LEN( This.Value ) - 1
WITH This
*** Get rid of any trailing spaces so we can Right justify the value
Value = ALLTRIM(.Value)
*** We need special handling to remove the decimal point
Trang 10If the character just entered was in the middle of the text box, we leave the cursor where itwas Otherwise we position it explicitly at the end of the value currently being entered:
Nearly there now! If this was originally a bound control, we must update the field
specified by the cControlSource property The Valid method is the appropriate place for this,
on spinner controls We feel there are actually two types of time entry that need to be
considered, and their differences require different controls
First there is the direct entry of an actual time Typically this will be used in a timerecording situation when the user needs to enter, for example, a start and a finish time for atask This is a pure data entry scenario and a text box is the best tool for the job, but there aresome issues that need to be addressed
Second there is the entry of time as an interval or setting Typically this type will be used
in a planning situation when the user needs to enter, for example, the estimated duration for atask In this case, a spinner is well suited to the task since users can easily adjust the value up
or down and can see the impact of their changes
Trang 11A time entry text box (Example: CH04.VCX::txtTime)
The basic assumption here is that a time value will always be stored as a character string in the
form hh:mm We do not expect to handle seconds in this type of direct entry situation Actually
this is not unreasonable, since most time manipulation only requires a precision of hours andminutes This is easiest when the value is already in character form (If you truly need to enterseconds, it would be a simple matter to make this control into a subclass to handle them.) Also
we have decided to work on a 24-hour clock Again this simplifies the interface by removingthe necessity to add the familiar concept of an AM/PM designator
These decisions make the class' user interface simple to build because Visual FoxPro
provides us with both an InputMask and a Format property The former specifies how data
entered into the control should be interpreted, while the latter defines how it should be
displayed In our txtTime class (based on our txtbase class) these properties are defined as
We have some additional code in both the GotFocus and LostFocus methods to save and
restore the current setting of CONFIRM and to force it to ON while the time entry text box hasfocus While not absolutely necessary, we believe it's good practice when limiting entry lengths
to ensure that confirm is on to prevent users from inadvertently typing through the field
All of the remaining code in the class is in the Valid method of the text box and this is
where we need to address the issues alluded to above about how users will use this control Thekey issue is how to handle partial times For example, if a user enters the string: '011' do theyactually mean '01:10' (ten minutes past one in the morning) or '00:11' (eleven minutes pastmidnight)? How about an entry of '09'?
In fact there is no absolute way of knowing All we can do is define and implement somereasonable rulesfor this class as follows:
Table 4.3 Rules for entering a time value
User Enters Interpret as Result
1 A specific hour, no minutes 01:00
11 Hours only, no minutes 11:00
111 Hours and minutes, leading zero omitted 01:11
The code implementing these rules is quite straightforward:
Trang 12LOCAL luHrs, luMins, lcTime, lnLen
*** Note: we have to assume that a user only omits leading or trailing
*** zeroes We cannot guess at the intended result otherwise!!!
*** Assume minutes are correct, hours leading zero was omitted
lcTime = PADL( lcTime, 4, '0' )
CASE lnLen = 2
*** Assume we have just got hours, no minutes
lcTime = PADR( lcTime, 4, '0' )
OTHERWISE
*** A single number must be an hour!
lcTime = "0" + lcTime + "00"
ENDCASE
*** Get the Hours and minutes components
luHrs = LEFT( lcTime, 2 )
luMins = RIGHT( lcTime, 2 )
*** Check that we have not gone over 23:59, or less than 00:00
IF ! BETWEEN( INT(VAL(luMins)), 0, 59) OR ! BETWEEN( INT(VAL(luHrs)), 0, 23) WAIT "Invalid Time Entered" WINDOW NOWAIT
A time entry composite class (Example: CH04.VCX::cntTime)
As noted in the introduction to this section, a spinner control is useful when you need to givethe user the ability to change times, as opposed to entering them directly However, onesignificant difference between using a spinner and a text box to enter time is that a spinnerrequires a numeric value This means if we still want to store our time value as a characterstring, we need to convert from character to numeric and back again For simplicity, this
control is set up to always display a time in hh:mm:ss format and expects that, if bound, it will
be bound to a Character (6) field (The purpose here is to show the basic techniques Modifyingthe control for other scenarios is left as an exercise for the reader.)
The next issue is how to get the time to be meaningful as a numeric value to display
properly Fortunately we can again make use of the Format and InputMask properties to
resolve this dilemma By setting the Spinner's InputMask = 99:99:99, and the Format = "RL"
we can display a six digit numeric value with leading zeroes (The 'L' option only works with
numeric values, so we could not use it in the preceding example.)
The final issue we need to address is how to determine which portion of our six-digitnumber will be changed when the spinner's up/down buttons are clicked The solution is tocreate a composite class that is based on a container with a spinner and a three-button optiongroup The Option group is used to determine which portion of the spinner gets incremented
Trang 13(i.e hours, minutes or seconds) and the Spinner's UpClick and DownClick methods are coded
to act appropriately Here is the class in use:
Figure 4.2 Time Spinner Control
The time spinner's container
The container is a subclass of our standard cntBase class, with a single custom property (cControlSource) and one custom method (SetSpinValue) These handle the requirement to
convert between a character data source for our class and the numeric value required by the
spinner The cControlSource property is populated at design time with the name of the control source for the spinner Code has been added to the Refresh method of the container to call the SetSpinValue method to perform the conversion when the control is bound The Refresh code
The code in SetSpinValue is equally simple, it merely converts the control source's value to
a six-digit number padded with zeroes However, there is one gotcha here – notice the use of
the INT() function in this conversion We must ensure our numeric value is actually an integer
at all times Whether we are dealing with hours, minutes or seconds is based on the positions of
the digits in the numeric value and decimal places would interfere when using the PADx()
The time spinner's option group
This is the simplest part of the class It has no custom code whatsoever other than the change of
its name from the Visual FoxPro default to OptPick The native behavior of an option group is
to record, in the Value property of the group itself, the number of the option button selected.
Since we only need to know which button is selected, we don't need to do anything else
Trang 14The time spinner's spinner
This is, unsurprisingly, where most of the work in the class is done Three properties have been
set – the InputMask and Format (to handle the display issues) and the Increment This has been
set to 0 to suppress the native behavior of the spinner since we need to handle the value change
in a more sophisticated manner
The GotFocus and LostFocus methods are used to turn the cursor off and back on again,
since this control is not intended for direct typing, eliminating the need to show the cursor
The Valid method handles the conversion of the spinner's numeric value back into a
character string and, if the control is bound, handles the REPLACE to update the control source.This code is also quite straightforward:
The tricky bits are handled in the UpClick and DownClick methods While this code may
look a little daunting at first glance, it's really quite straightforward and relies on interpreting
the position of the digits in the numeric value and handling them accordingly The UpClick
method checks the setting of the option group and increments the relevant portion of thenumeric value:
LOCAL lnPick, lnNewVal, lnHrs, lnMins, lnSecs
lnPick = This.Parent.optPick.Value
DO CASE
CASE lnPick = 1 && Hrs
*** Get the next Hours value
lnNewVal = This.Value + 10000
*** If 24 or more, reset to 0 by subtracting
This.Value = IIF( lnNewVal >= 240000, lnNewVal - 240000, lnNewVal )
CASE lnPick = 2 && Mins
*** Get the next value as a character string
lcNewVal = PADL(INT(This.Value) + 100, 6, '0' )
*** Extract hours as a value multiplied by 10000
lnHrs = VAL(LEFT(lcNewVal,2)) * 10000
*** Get the minutes as a character string
lnMins = SUBSTR( lcNewVal, 3, 2)
*** Check the value of this string, and either multiply up by 100 *** or, if above 59, roll it over to 00
lnMins = VAL(IIF( VAL(lnMins) > 59, "00", lnMins )) * 100
*** Extract the seconds portion
lnSecs = VAL(RIGHT(lcNewVal, 2 ))
*** Reconstruct the Numeric Value
This.Value = lnHrs + lnMins + lnSecs
CASE lnPick = 3 && Secs
*** Get the next value as a character string
lcNewVal = PADL(INT(This.Value) + 1, 6, '0' )
*** Extract hours as a value multiplied by 10000
lnHrs = VAL(LEFT(lcNewVal,2)) * 10000
*** Extract minutes as a value multiplied by 100
lnMins = VAL(SUBSTR( lcNewVal, 3, 2)) * 100
*** Get the seconds as a character string
Trang 15lnSecs = RIGHT( lcNewVal, 2)
*** Check the value of this string,
*** If above 59, roll it over to 00
lnSecs = VAL(IIF( VAL(lnSecs) > 59, "00", lnSecs ))
*** Reconstruct the Numeric Value
This.Value = lnHrs + lnMins + lnSecs
ENDCASE
For hours the increment is 10000, for minutes it is 100 and for seconds it is just 1
The control is designed so that if the user tries to increment the hours portion above '23', itrolls over to '00' by simply subtracting the value 240000 from the spinner's new value Both theminutes and seconds are rolled over from '59' to '00', but in this case we need to actually stripout each component of the time to check the relevant portion before re-building the value byadding up the individual parts
A similar approach has been taken in the DownClick method, which decrements the
control's value In this case we need to store the current value and use it to maintain the settings
of the parts of the control that are not being affected Otherwise the principles are the same as
for the UpClick method:
LOCAL lnPick, lcNewVal, lnHrs, lnMins, lnSecs, lcOldVal
*** Get the Current value of the control as a string
lcOldVal = PADL(INT(This.Value), 6, '0' )
lnPick = This.Parent.optPick.Value
DO CASE
CASE lnPick = 1 && Hrs
*** Decrement the hours portion
lnHrs = VAL( LEFT( lcOldVal, 2 ) ) - 1
*** If it is in the desired range, use it, otherwise set to 0
lnHrs = IIF( BETWEEN(lnHrs, 0, 23), lnHrs, 23 ) * 10000
*** Extract the minutes
lnMins = VAL(SUBSTR( lcOldVal, 3, 2)) * 100
*** Extract the seconds
lnSecs = VAL(RIGHT( lcOldVal, 2))
CASE lnPick = 2 && Mins
*** Determine the new, decremented, value
lcNewVal = PADL(INT(This.Value) - 100, 6, '0' )
*** Retrieve the current Hours portion
lnHrs = VAL(LEFT(lcOldVal,2)) * 10000
*** Get the minutes portion from the new value
lnMins = VAL(SUBSTR( lcNewVal, 3, 2))
*** Check for validity with the range, set to 0 if invalid
lnMins = IIF( BETWEEN( lnMins, 0, 59), lnMins, 59 ) * 100
*** Retrieve the current Seconds portion
lnSecs = VAL(RIGHT(lcOldVal, 2 ))
CASE lnPick = 3 && Secs
*** Determine the new, decremented, value
lcNewVal = PADL(INT(This.Value) - 1, 6, '0' )
*** Retrieve the current Hours portion
lnHrs = VAL(LEFT(lcOldVal,2)) * 10000
*** Retrieve the current Minutes portion
lnMins = VAL(SUBSTR( lcOldVal, 3, 2)) * 100
*** Get the Seconds portion from the new value
lnSecs = VAL(RIGHT( lcNewVal, 2))
*** Check for validity with the range, set to 0 if invalid
lnSecs = IIF( BETWEEN(lnSecs, 0, 59), lnSecs, 59 )
Trang 16*** Set the Value to the new, decremented, result
This.Value = lnHrs + lnMins + lnSecs
Conclusion
This time spinner is somewhat restricted in its ability to handle more than the simple
environment that we defined for it While clear to the end user, the mechanism for selectingwhich part of the time needs to be incremented or decremented is a bit cumbersome A moreelegant solution is offered in the final control in this section
The true time spinner (Example: CH04.VCX::spnTime)
This control looks and acts just like the time spinner you see when using the Date/Time PickerActiveX control that ships with Visual Studio However, it has several features that make itmore generally useful than the ActiveX control First, this control does not require that it bebound to a field of the DateTime data type As we have said, the best format for storing user
entered time is in a character field formatted to include the universal ':' time separator The
control can be configured to display and recognize either a 5-character 'hh:mm' format or a full 8-character 'hh:mm:ss' format and update either appropriately This is controlled by a single property, lShowSeconds Finally, because this is a native Visual FoxPro control it does not
suffer from the inherent problems that haunt ActiveX controls with respect to the multipleversions of specific windows DLLs, nor are there any problems associated with registering thecontrol
As with the container-based spinner, this control uses a cControlSource property for binding to an external data source and has associated methods (RefreshSpinner and
UpdateControlSource) to handle the issue of converting between character (source) and
numeric (internal) values and back Unlike the container-based spinner, this control can also
handle being bound to a form property, as well as to a data source The UpdateControlSource method, called from the spinner's Valid method, allows the time spinner, which is really an unbound control, to behave just like one that is bound when its cControlSource property is set:
LOCAL lcTable, lcField, lcValue, lcTemp
lcTable = LEFT( cControlSource, AT( '.', cControlSource ) - 1 )
lcField = SUBSTR( cControlSource, AT( '.', cControlSource ) + 1 ) ELSE
*** Assume the alias is the current selected alias if none is specified *** This is a little dangerous, but if it is a bad assumption, the
*** program will blow up very quickly in development mode giving
*** the developer a very clear indication of what is wrong once he checks *** out the problem in the debugger.
lcTable = ALIAS()
lcField = cControlSource
ENDIF