1001 Things You Wanted To Know About Visual FoxPro phần 3 pot

33 422 1
1001 Things You Wanted To Know About Visual FoxPro phần 3 pot

Đang tải... (xem toàn văn)

Tài liệu hạn chế xem trước, để xem đầy đủ mời bạn chọn Tải xuống

Thông tin tài liệu

92 1001 Things You Always Wanted to Know About Visual FoxPro .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 ) IF FOUND() .Value = EVAL( .cAlias + '.' + .cField ) ENDIF *** Restore the original work area SELECT ( lnSelect ) ENDIF ENDIF 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 and refresh 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 .lRefreshParent .RefreshParent() ENDIF *** Highlight the portion of the value after the insertion point .SelStart = lnSelStart lnSelLength = LEN( .Value ) - lnSelStart IF lnSelLength > 0 .SelLength = lnSelLength ENDIF *** 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 you haven't already. This problem is not specific to our incremental search text box and timing issues like this are not uncommon in Visual FoxPro. (We have also run into it when displaying multi-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 to note 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 short pause 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 data from its FoxPro ancestors. It's not too bad when the entire field is selected, and the number is not formatted with separators. However, problems begin to occur when the insertion point is Chapter 4: Basic Controls 93 not 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 the cursor moves on to the next field. We have also seen the opposite problem. The user wants to enter 3 but after typing 3 and exiting the control, the number 30 is displayed instead of the intended 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 to select 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't contain 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, they all suffer from the same shortcoming. Either the cursor can be seen flashing to the left as characters appear from the right or there is no cursor at all. Both of these solutions tend to make things confusing for the user. So we set out to create the ultimate Visual FoxPro numeric 94 1001 Things You Always Wanted to Know About Visual FoxPro text 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 the correct character. When the value in the text box is not selected, you can even delete or insert individual 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. The way 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 or view. 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. They are 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 a custom method to avoid coding explicitly in events whenever possible. Notice that we use Chapter 4: Basic Controls 95 SET( '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 this section of code. However, because there are various sections of code that refer to one or the other, 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 more self-documenting and makes the code more readable. This is just as important as performance considerations. 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 .cField = .cControlSource *** No alias in ControlSource *** assume the table is the one in the currently selected work area .ctable = ALIAS() ENDIF 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)) 96 1001 Things You Always Wanted to Know About Visual FoxPro IF 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 DO CASE CASE laFields[ lnRow, 2 ] = 'I' .cInputMask = "9999999999" .nMaxVal = 2147483647 CASE laFields[ lnRow, 2 ] = 'Y' .cInputMask = "999999999999999.9999" .nMaxVal = 922337203685477.5807 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 order when the form is instantiated because we cannot force a SET CONFIRM OFF until our text box actually has focus: .ControlSource = '' *** This keeps us from KEYBOARDing an '{END}' and moving to the next control *** if this is the first one in the tab order .lChangingFocus = .T. .FormatValue() ENDWITH The FormatValue method performs the same function that the native Visual FoxPro refresh method does for bound controls. It updates the control's value from its ControlSource. Actually, in this case, it updates the control's value from its cControlSource. Since Chapter 4: Basic Controls 97 cControlSource 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 of the string: WITH This *** cControlSource is numeric, so convert it to string IF ! EMPTY ( .cControlSource ) IF ! EMPTY ( EVAL( .cControlSource ) ) .Value = ALLTRIM( PADL ( EVAL( .cControlSource ), 32 ) ) ELSE .Value = ' ' ENDIF *** And format it nicely with separators .AddSeparators() ELSE .Value = ' ' .InputMask = '#' ENDIF *** Position the cursor at the right end of the textbox IF .lChangingFocus .lChangingFocus = .F. ELSE KEYBOARD '{END}' ENDIF ENDWITH 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 ) ) IF lnPointPos = 0 lnIntLen = LEN( .Value ) ELSE lnIntLen = LEN( LEFT(.Value, lnPointPos - 1 ) ) ENDIF *** 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 rightmost character 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 after the commas are inserted: 98 1001 Things You Always Wanted to Know About Visual FoxPro *** Insert the separator at the appropriate interval lcInputMask = '' FOR lnCnt = lnIntLen TO 1 STEP -1 IF INLIST( lnCnt, 4, 7, 10, 13, 16, 19, 21, 24 ) lcInputMask = lcInputMask + "#" + .cSeparator ELSE lcInputMask = lcInputMask + "#" ENDIF ENDFOR *** Make sure that negative numbers are formatted correctly IF LEFT( ALLTRIM( .Value ), 1 ) = '-' IF LEN( lcInputMask ) > 3 IF LEFT( lcInputMask, 2 ) = '#,' lcInputMask = '#' + SUBSTR( lcInputMask, 3 ) ENDIF ENDIF ENDIF We finish up by adding a placeholder for the decimal point and any placeholders that are needed to represent the decimal portion of the number: IF lnPointPos > 0 *** Allow for the decimal point in the input mask lcInputMask = lcInputMask + '#' *** Add to the input mask if there is a decimal portion IF lnDecLen > 0 lcInputMask = lcInputMask + REPLICATE( '#', lnDecLen ) ENDIF ENDIF .InputMask = lcInputMask ENDWITH 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: This.cOldConfirm = SET('CONFIRM') This.cOldBell = SET( 'BELL' ) SET CONFIRM ON SET BELL OFF This.SetInputMask() DODEFAULT() Note that the SetInputMask method is also called from the HandleKey method to adjust the InputMask as the user enters data. Here it is: LOCAL lcInputMask, lnChar *** Reset the InputMask for the current value of the text box Chapter 4: Basic Controls 99 lcInputMask = '' FOR lnChar = 1 to LEN( This.Value ) lcInputMask = lcInputMask + '#' ENDFOR lcInputMask = lcInputMask + '#' This.InputMask = lcInputMask Like 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 llRetVal = .F. ENDIF Likewise, we will not allow a minus sign to be typed in unless it is the first character in the string: *** Make sure we only have a minus sign at the beginning of the number CASE tnKeyCode = 45 IF .SelStart > 0 llRetVal = .F. ENDIF 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!!!! OTHERWISE IF ! EMPTY( .cInputMask ) IF .SelLength = 0 IF tnKeyCode > 47 AND tnKeyCode < 58 DO CASE 100 1001 Things You Always Wanted to Know About Visual FoxPro CASE .SelStart = 0 lcCheckVal = CHR( tnKeyCode ) + ALLTRIM( .Value ) CASE .SelStart = LEN( ALLTRIM( .Value ) ) lcCheckVal = ALLTRIM( .Value ) + CHR( tnKeyCode ) OTHERWISE lcCheckVal = LEFT( .Value, .SelStart ) + CHR( tnKeyCode ) + ; ALLTRIM( SUBSTR( .Value, .SelStart + 1 ) ) ENDCASE IF ABS( VAL( lcCheckVal ) ) > .nMaxVal llRetVal = .F. ENDIF *** 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 IF AT( '.', lcCheckVal ) > 0 IF AT( '.', .cInputMask ) > 0 IF LEN( JUSTEXT( lcCheckVal ) ) > LEN( JUSTEXT( .cInputMask ) ) llretVal = .F. ENDIF ENDIF ENDIF ENDIF && tnKeyCode > 47 AND tnKeyCode < 58 ENDIF && .SelLength = 0 ENDIF && ! EMPTY( .cInputMask ) ENDCASE ENDWITH RETURN llRetVal This code may look rather ugly, but in fact it executes extremely quickly because the nested IF structure ensures that various checks are performed sequentially and that if any one fails, the rest are never processed at all. 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 IF LASTKEY() = 127 && backspace IF .Value = .cPoint .Value = ' ' .InputMask = '#' ENDIF ENDIF .SetInputMask() Chapter 4: Basic Controls 101 If the character just entered was in the middle of the text box, we leave the cursor where it was. Otherwise we position it explicitly at the end of the value currently being entered: IF lnSelStart >= lnEnd KEYBOARD '{END}' ELSE .SelStart = lnSelStart ENDIF ENDWITH 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, so we use it: WITH This IF ! EMPTY( .cControlSource ) REPLACE ( .cField ) WITH VAL( .Value ) IN ( .cTable ) ENDIF ENDWITH Finally, we need a little bit of code in the text box's LostFocus method to reset CONFIRM to its original value and to format the displayed value with the appropriate separators: WITH This *** Set flag so we don't keyboard an end and mess up the tab order .lChangingFocus = .T. .Refresh() IF .cOldConfirm = 'OFF' SET CONFIRM OFF ENDIF ENDWITH Handling time One of the perennial problems when constructing a user interface is how to handle the entry of time. Many applications require this support and we have seen varied approaches, often based 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 time recording situation when the user needs to enter, for example, a start and a finish time for a task. This is a pure data entry scenario and a text box is the best tool for the job, but there are some 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 a task. 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. [...]... need to address is how to determine which portion of our six-digit number will be changed when the spinner's up/down buttons are clicked The solution is to create a composite class that is based on a container with a spinner and a three-button option group The Option group is used to determine which portion of the spinner gets incremented 104 1001 Things You Always Wanted to Know About Visual FoxPro. .. base class pages and add our custom pages at run time Here it is: 124 1001 Things You Always Wanted to Know About Visual FoxPro LOCAL lnPageCount, lnCnt *** Make sure we can find the prg that defines the custom pages SET PROC TO CH04 ADDITIVE *** Remove base class pages and add our custom pages WITH This lnPageCount = PageCount PageCount = 0 ActivePage = 0 FOR lnCnt = 1 TO lnPageCount *** Add new one... has a PageHeight property, so we have to use that to expand the edit box: Top Left Height Width = = = = 0 0 Parent.Parent.PageHeight Parent.Parent.PageWidth Then we must make sure the edit box appears on top of all the other controls on the page Calling its ZOrder method with a parameter of 0 will do this: 116 1001 Things You Always Wanted to Know About Visual FoxPro zOrder(0) That takes care of the... news – the only way we found to use our custom pages was by adding them to our custom page frame at run time The page frame does not have any accessible property to store to the class on which to base its pages and always instantiates the number of required pages from the Visual FoxPro Page base class This means that custom pages are virtually useless unless you intend to use delayed instantiation... our text box classes, we like to be able to select all contents when the edit box gets focus, so we check if SelectOnEntry = T and select all text if required In addition, we position the cursor at the end of the selected text using a KEYBOARD '{CTRL+END}', if our lPositionAtEnd property is set: 114 1001 Things You Always Wanted to Know About Visual FoxPro WITH This IF SelectOnEntry SelStart = 0 SelLength... the control is not bound However, setting the value programmatically to anything other than T or F or to any numeric value other than 0 or 1 gives a strange result, as Figure 4.7 shows: 122 1001 Things You Always Wanted to Know About Visual FoxPro Figure 4.7 Check box programmatic behavior Notice that the conventional check box appears to be both checked and disabled at the same time when an invalid value... already in character form (If you truly need to enter seconds, 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 removing the 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...102 1001 Things You Always Wanted to Know About Visual FoxPro A 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,... LEN( lcMemoFld ) TO 1 STEP -1 IF ASC( SUBSTR( lcMemoFld, lnChar, 1 ) ) > 32 EXIT ENDIF ENDFOR IF lnChar > 1 118 1001 Things You Always Wanted to Know About Visual FoxPro lcMemoFld = LEFT( lcMemoFld, lnChar ) ENDIF Value = lcMemoFld ENDWITH Calendar combo (Example: CH04.VCX::cntCalendar) This class is actually a composite designed to mimic the behavior of a drop-down list for entering dates We have included... OleCalendar.Refresh() ENDWITH We also need some code to mimic the native behavior of drop-down lists, which can be opened using F4 or + We trap these keys in the Keypress method of the text box so we can make the calendar visible when they are detected: 120 1001 Things You Always Wanted to Know About Visual FoxPro *** Check for F4 and ALT+DNARROW IF ( nKeyCode = -3 ) OR ( nKeyCode = 160 ) WITH This.Parent . these solutions tend to make things confusing for the user. So we set out to create the ultimate Visual FoxPro numeric 94 1001 Things You Always Wanted to Know About Visual FoxPro text box. And. spinner and a three-button option group. The Option group is used to determine which portion of the spinner gets incremented 104 1001 Things You Always Wanted to Know About Visual FoxPro (i.e. hours,. in "-,1 23, 456" being displayed as the formatted value. We check for this possibility after the commas are inserted: 98 1001 Things You Always Wanted to Know About Visual FoxPro ***

Ngày đăng: 05/08/2014, 10:20

Từ khóa liên quan

Tài liệu cùng người dùng

  • Đang cập nhật ...

Tài liệu liên quan