|
-
Aug 21st, 2001, 04:04 AM
#1
Thread Starter
Registered User
Cool Textbox Class
Lots of people always ask how to make a textbox only take numeric data /uppercase /lowercase / decimal place /prevent pasting etc.
Well here is the start of a class to do all the work for you. Full source is included.
I just put it together over the last hour or so, so there are probably a few bugs.
I am posting it for anyone to use and/or comment on for suggested improvements.
[Edit: Deleted attachment as class improved and posted later in this thread]
Last edited by Nucleus; Aug 30th, 2001 at 12:16 AM.
-
Aug 21st, 2001, 04:11 AM
#2
Hyperactive Member
thats a nice class and works great, but why do all that work when you can simply do it through 2 api calls ?? if you would like i will create u a simple project using the API to do exactly what you have done. Just putting my 2 cents.
This Business Is Binary. Your a 1 or a 0. Alive or Dead. (AntiTrust)
-
Aug 21st, 2001, 04:29 AM
#3
PowerPoster
Brings the teacher an apple but sits up the back and chews gum
Absolutely no offence to u Nucleus and i know u are doing for ppl that ask, but geesh i wonder why ppl insist on annoying the hell out of their users by doing such strange stuff as stopping them entering text etc as they are typing. Have they ever tested such ideas on users and if so have they worn protective headwear?
Anyhow, nice job for those ppl.
Regards
Stuart
-
Aug 21st, 2001, 05:19 AM
#4
Beachbum,
An interesting question - do you lock off a text box to allow the user to enter only valid data, or let them enter anything, then fix it up later ("fix it in post" in film-making parlance)?
I guess it all depends on the target audience, but I'd be interersted to hear what people have to say and their justifications for it...
- gaffa
-
Aug 21st, 2001, 05:55 AM
#5
PowerPoster
Hi Gaffa
I believe in (lol sounds biblical) letting users type whatever they want and then having the program do its damndest to fix it up if they have entered invalid data. eg if the user is supposed to type a date as dd/mm/yy and they actually do dd-mm-yy or ddmmyy then why the hell cant the program just work it out instead of annoying the user with an error msg or annoying them by restricting the use of the "-" key or whatever. Just my opinion.
Regards
Stuart
-
Aug 21st, 2001, 07:26 AM
#6
I've always been of the other variety - restrict the entry so they can't screw it up in the first place (well, restrict within reason anyway - don't let the enter alpha when a number is requried and the like.) The key is making the restriction non-intrusive - no message boxes or anything cos they are just a pain in the rear.
- gaffa
-
Aug 21st, 2001, 07:28 AM
#7
Thread Starter
Registered User
Originally posted by AAG
thats a nice class and works great, but why do all that work when you can simply do it through 2 api calls ?? if you would like i will create u a simple project using the API to do exactly what you have done. Just putting my 2 cents.
AAG, I would be interested in seeing the app that provides the same level of functionality with 2 api calls. Can you post it?
Beachbum/Gaffa I guess I used a combination of both approaches. For example if the textbox only accepts uppercase text, then the class will automatically change lowercase data to uppercase. For things like disabling paste I don't think there is a way around just disabling it flat out.
Then there is the middle ground, if a textbox should accept numeric data, should I build intelligence to convert "one" to 1? This would take a lot of extra code. In short where it was relatively efficient to implement intelligence into the class I did it, where it would take reams of extra code I stayed away.
If I was making a very expensive product which didn't require distribution over the net and and can therefore be a little more bloated, I guess the more intelligence you build into the app the better.
I didn't include the ability to deal with dates as the Masked Edit Box is quite capable of doing the job.
BTW Beachbum, I hoped you noticed the messagebox was optional .
-
Aug 21st, 2001, 07:47 AM
#8
-
Aug 21st, 2001, 09:28 AM
#9
Thread Starter
Registered User
BeachBum, I am looking through those sites, there is some good stuff there.
-
Aug 21st, 2001, 12:50 PM
#10
Well, here comes the criticism
I think you want to change this line:
Case Is < 31 ' control characters
..to this one:
Case Is <= 31 ' control characters
..in the KeyPress event in the class. It probably doesn't matter, tho.
The disable pasting code still allows the user to right-click on the textbox and click Paste from there. It also allows Shift+Insert to be used for pasting. You might want to work on that.
You might also want to put the ColourForInvalidEntry property in as an actual property instead of a public event, so you can find out if it was actually set (using a boolean variable). If it wasn't set, it's probably safe to assume that the programmer doesn't want the textbox to change color when something goes wrong. YOu can even add a ChangeColor (or similarly named) property to set whether the textbox would change color. Of course, setting the ColourForInvalidEntry would automatically set the ChangeColor property to true.
Also, about the ColourForInvalidEntry property, it might be a good idea if you let the programmer turn it black if he/she desires to do so. I'm referring to this line in the Validate event in the class:
If ColourForInvalidEntry = 0 Then ColourForInvalidEntry = vbRed
This next thing is just a suggestion. It doesn't really matter either way. Maybe you should put in a property (or public variable) for the text and title in the error message that shows if the contents of the textbox are invalid. If the property or properties don't get set, just use the default, which would be what you have right now.
I looked back therough the code and I found a problem with the backcolor code. If the backcolor of the textbox gets set somehwere else, the code in the class module doesn't realize it. So when it tries to set the backcolor to what it was before it shaded the textbox because of an error, it sets it to what it originally was. This might not make sense, so I'll try again. When the textbox gets focus for the first time (and only on the first time it gets focus), the class module records the color. Then when the user puts something invalid and the Validate event catches it, it changes the backcolor to red (or whatever it's supposed to change it to). Then when the user types his/her first character to fix the problem, it changes the backcolor back to the color it saved. So what happens if the programmer decides to change the backcolor of the control sometime between the very first time the textbox gets focus and the time the user puts in something invalid? The programmer's change gets lost and the textbox gets the backcolor set back to what it originally had.
I suggest you save the backcolor of the textbox right before you change its color. Then you can set it back to that saved value when you change it back.
Well, after seeing how much I just typed, I'm going to stop now...
-
Aug 21st, 2001, 09:26 PM
#11
Thread Starter
Registered User
Bigger and Better
I have taken the comments from Beachbum and Tygur and incorporated them into this new version of the class, as well as adding a few extra properties/features of my own.
Beachbum, I now have a switch to turn key-trapping on or off. If you leave key-trapping off, then the user can enter/paste anything they want and the program automatically fixes up the data. For example if the textbox accepts only numerical data, all non-numerical characters are deleted. If the textbox accepts only uppercase text and the user enters lowercase text, the text is automatically converted to uppercase without user intervention. No need for a message box at all this time, as the code handles all problems automatically. I agree with you that this probably suits the user more, although they may get confused when the app automatically changes something, they might think "why is the app changing that, that is not what I typed in?"
The advantage of doing it as a class, is that I can put it on a server and all developers can use it, plus the code is in one place and easy to maintain.
I'll post the class directly below, so you can see the code this time.
Tygur Well thanks for the long reply! I have fixed the case Is < 32 problem. I thought about the pasting issue and decided that I do not like applications that stop me from pasting in data. So I changed the class to allow users to paste in any data, if the data pasted is invalid, the class automatically fixes the data in the validate event. This way users are not restricted from pasting, but invalid data is filtered out.
Following BB's comments I have added more intelligence to the class so that it automatically fixes invalid data for the user and no longer needs a message box or to change the background colour.
In a class Module called CTxtBxN
VB Code:
Option Explicit
Public WithEvents Textbox As Textbox
Public AllowSpaces As Boolean
Public AllowUppercase As Boolean
Public AllowLowercase As Boolean
Public AllowSymbols As Boolean
Public AllowNumeric As Boolean
Public TrapKeyPresses As Boolean
Public SelectOnFocus As Boolean
Private m_dp As Long
Private m_ApplyMax As Boolean
Private m_ApplyMin As Boolean
Private m_Max As Double
Private m_Min As Double
Private m_NumOnly As Boolean
Public Property Let Max(MaxValue As Double)
m_ApplyMax = True
m_Max = MaxValue
End Property
Public Property Let Min(MinValue As Double)
m_ApplyMin = True
m_Min = MinValue
End Property
Public Property Let MaxDecimalPlaces(dp As Long)
m_dp = Abs(dp)
End Property
Private Sub Textbox_GotFocus()
If SelectOnFocus Then
Textbox.SelStart = 0
Textbox.SelLength = Len(Textbox.Text)
End If
If m_dp Or m_ApplyMin Or m_ApplyMax Then
'can't enforce these properties without restricting to a number
AllowNumeric = True
AllowSpaces = False
AllowUppercase = False
AllowLowercase = False
AllowSymbols = False
End If
m_NumOnly = AllowNumeric And Not AllowSpaces And Not AllowUppercase And Not AllowLowercase And Not AllowSymbols
End Sub
Private Sub Textbox_KeyPress(KeyAscii As Integer)
Dim pos&
If TrapKeyPresses Then
Select Case KeyAscii
Case Is < 32 ' control characters
Case 32 ' space
If Not AllowSpaces Then KeyAscii = 0
Case 46 ' decimal place
If AllowNumeric Then
If m_dp Then
If Textbox.Text Like "*.*" Then KeyAscii = 0
Else
If m_NumOnly Then KeyAscii = 0
End If
End If
Case 48 To 57 ' number
If Not AllowNumeric Then
KeyAscii = 0
Else
If m_dp Then
pos = InStr(1, Textbox.Text, ".")
If pos Then If Len(Textbox.Text) - pos + 1 > m_dp Then KeyAscii = 0
End If
End If
Case 65 To 90 ' uppercase alpha
If Not AllowUppercase Then
If AllowLowercase Then
KeyAscii = KeyAscii + 32
Else
KeyAscii = 0
End If
End If
Case 97 To 122 ' lowercase alpha
If Not AllowLowercase Then
If AllowUppercase Then
KeyAscii = KeyAscii - 32
Else
KeyAscii = 0
End If
End If
Case Else
If Not AllowSymbols Then KeyAscii = 0
End Select
End If
End Sub
Private Sub Textbox_Validate(Cancel As Boolean)
Dim ba() As Byte, i&, s$, dp&, pos&
ba = Textbox.Text
For i = 0 To UBound(ba) Step 2
Select Case ba(i)
Case Is < 32 'control characters
Case 32 ' space
If Not AllowSpaces Then ba(i) = 1
Case 46 ' decimal place
If AllowNumeric Then
If m_dp Then
dp = dp + 1
If dp > 1 Then ba(i) = 1
Else
If m_NumOnly Then ba(i) = 1
End If
End If
Case 48 To 57 'number
If Not AllowNumeric Then ba(i) = 1
Case 65 To 90 'uppercase alpha
If Not AllowUppercase Then
If AllowLowercase Then
ba(i) = ba(i) + 32
Else
ba(i) = 1
End If
End If
Case 97 To 122 'lowercase alpha
If Not AllowLowercase Then
If AllowUppercase Then
ba(i) = ba(i) - 32
Else
ba(i) = 1
End If
End If
Case Else
If Not AllowSymbols Then ba(i) = 1
End Select
Next i
If Len(Textbox.Text) Then Textbox.Text = ba
Textbox.Text = Replace(Textbox.Text, Chr(1), "")
If m_ApplyMax And Len(Textbox.Text) Then If Textbox.Text > m_Max Then Textbox.Text = m_Max
If m_ApplyMin And Len(Textbox.Text) Then If Textbox.Text < m_Min Then Textbox.Text = m_Min
If m_dp Then
pos = InStr(1, Textbox.Text, ".")
If pos Then Textbox.Text = Left$(Textbox.Text, pos + m_dp)
End If
End Sub
Example Usage add 2 textboxes to a form then add this code:
VB Code:
Option Explicit
Dim Amount As CTxtBxN
Dim NameEntry As CTxtBxN
Private Sub Form_Load()
Set Amount = New CTxtBxN
Set NameEntry = New CTxtBxN
Set Amount.Textbox = Text1
'all available properties in addition to a standard textbox
Amount.SelectOnFocus = True
Amount.AllowSpaces = False
Amount.AllowUppercase = False
Amount.AllowLowercase = False
Amount.AllowSymbols = False
Amount.TrapKeyPresses = False
Amount.AllowNumeric = True
Amount.Max = 100
Amount.MaxDecimalPlaces = 2
Amount.Min = 0
Set NameEntry.Textbox = Text2
'in reality only need to turn on those we need
' this case only allow uppercase text
NameEntry.AllowUppercase = True
End Sub
Last edited by Nucleus; Aug 21st, 2001 at 09:58 PM.
-
Aug 21st, 2001, 10:35 PM
#12
Fanatic Member
Someone brought up the API way to change the numbers only thing (I read all that once and didn't want to read it again, so I apologize for not giving a name), but this is how it's done. It's not as good for all cases though, as this way invalidates everything but numbers (it allows backspace and delete, but not periods, which some people may want for decimals). I took these out of a class I had myself which is somewhat like Nucleus's, but is more for giving a textbox some of the props that exist for edit controls, but aren't exposed with VB.
VB Code:
'this takes a reference to a textbox just like Nucleus's class did
'blnClassBound tells whether or not a textbox reference has been set yet
'in my clsTextBoxEx.SourceTextBox property set procedure
'txtSource is the class side textbox var
Private Declare Function SendMessage Lib "user32" Alias "SendMessageA" (ByVal hWnd As Long, ByVal wMsg As Long, ByVal wParam As Long, ByVal lParam As Long) As Long
Private Declare Function GetWindowLong Lib "user32" Alias "GetWindowLongA" (ByVal hWnd As Long, ByVal nIndex As Long) As Long
Private Declare Function SetWindowLong Lib "user32" Alias "SetWindowLongA" (ByVal hWnd As Long, ByVal nIndex As Long, ByVal dwNewLong As Long) As Long
Private Const EM_SETREADONLY As Long = &HCF
Private Const ES_NUMBER As Long = &H2000
Private Const ES_READONLY As Long = &H800
Private Const GWL_STYLE As Long = -16
Public Property Get NumbersOnly() As Boolean
Dim lngStyle As Long
If Not blnClassBound Then Exit Property
lngStyle = GetWindowLong(txtSource.hWnd, GWL_STYLE)
NumbersOnly = CBool(lngStyle And ES_NUMBER)
End Property
Public Property Let NumbersOnly(ByVal blnNewNumbersOnly As Boolean)
Dim lngStyle As Long
If Not blnClassBound Then Exit Property
lngStyle = GetWindowLong(txtSource.hWnd, GWL_STYLE)
lngStyle = IIf(blnNewNumbersOnly, lngStyle Or ES_NUMBER, lngStyle And (Not ES_NUMBER))
SetWindowLong txtSource.hWnd, GWL_STYLE, lngStyle
End Property
'I had this in there before I even knew a textbox had a .Locked property
'but I'll include it anyway. someone may find a use for it
Public Property Get ReadOnly() As Boolean
Dim lngStyle As Long
If Not blnClassBound Then Exit Property
lngStyle = GetWindowLong(txtSource.hWnd, GWL_STYLE)
ReadOnly = CBool(lngStyle And ES_READONLY)
End Property
'SetWindowLong doesn't seem to make a textbox read only if you manually
'change the ES_READONLY flag for some reason, but SendMessage does get it
Public Property Let ReadOnly(ByVal blnNewReadOnly As Boolean)
If Not blnClassBound Then Exit Property
SendMessage txtSource.hWnd, EM_SETREADONLY, blnNewReadOnly, 0
End Property
I had a .GetLine(Index) prop too, but I remember getting a snag somewhere and ditched it. In that case, it was before I even knew of Split() (those were bad times indeed ), but I had an API way. I'm going to work on that again.
I remembered a while later I forgot one of the useful ones I intended to show in the first place...
VB Code:
Private Const EM_GETMODIFY As Long = &HB8
Private Const EM_SETMODIFY As Long = &HB9
Public Property Get Edited() As Boolean
If Not blnClassBound Then Exit Property
Edited = SendMessage(txtSource.hWnd, EM_GETMODIFY, 0, 0)
End Property
Public Property Let Edited(ByVal blnNewEdited As Boolean)
If Not blnClassBound Then Exit Property
SendMessage txtSource.hWnd, EM_SETMODIFY, blnNewEdited, 0
End Property
Private Sub txtSource_Change()
'this makes sure the edited flag is set, as I noticed there were times
'when changing text wouldn't make it apparent to the class for some reason
Me.Edited = True
End Sub
Last edited by Kaverin; Aug 21st, 2001 at 11:00 PM.
I'm baaaack...
VB5 Professional Edition, VC++ 6
Using a 1 gHz Thunderbird, 256 mb RAM, 40 gb HD system with Win98se
I feel special because I finally figured out how to loop midis: Post link
I'm a fanatic too 
-
Aug 21st, 2001, 10:59 PM
#13
Thread Starter
Registered User
Thanks for the Info Kaverin. I did think about using api calls to make the textbox numerical, but it stops the user from entering decimal places and it doesn't stop the user from pasting in data. I originally wanted to use these shortcuts but found they didn't offer the flexibiliy I needed.
Here are some notes I made:
Private Const ES_LOWERCASE = &H10& 'Make Text Box lowercase only (but can paste so add validate routine)
Private Const ES_UPPERCASE = &H8& 'Make text Box uppercase only (but can paste so add validate rountine)
Private Const ES_NUMBER = &H2000& 'Make text box numeric only (but can paste in so add validate routine)
If I used these messages to force the textbox to be uppercase, then I wouldn't be able to give the user the ability to enter lowercase data and have the textbox automaticlly change it to uppercase. This is why I wanted to see the API app AAG was talking about. I don't think you can get the flexibility needed from API calls.
The idea now is that you can basically select what the textbox should allow (any combination of): numerical, uppercase text, lowercase text, symbols, spaces, number of decimal places, max and min. You can also optionally trap errors at the keystroke level, or choose to let the user enter what they like and have the text box automatically fix up the data later.
-
Aug 21st, 2001, 11:26 PM
#14
Fanatic Member
Those aren't messages for SendMessage, those are styles. I tried them out myself, and they work as expected when you use them in Set/GetWindowLong. They even prevent pasting the wrong case (I checked that out too because I didn't know myself). Pasting known lowercase things into a control where the uppercase style was applied forced them to appear as uppercase, and the reverse. I did notice that applying the ES_LOWERCASE style overrides the upper one if by chance you had both set.
I'm baaaack...
VB5 Professional Edition, VC++ 6
Using a 1 gHz Thunderbird, 256 mb RAM, 40 gb HD system with Win98se
I feel special because I finally figured out how to loop midis: Post link
I'm a fanatic too 
-
Aug 21st, 2001, 11:39 PM
#15
Thread Starter
Registered User
Kaverin, I am sure you see what I mean about getting the flexibilty needed to really make it all work well; it just doesn't happen with these API calls. Perhaps AAG was talking about different API functions that work better?
-
Aug 21st, 2001, 11:44 PM
#16
Fanatic Member
Yes, I understand your point about flexibility. I was just clearing up that setting the styles for upper/lowercase does work, and catches pasting. It won't catch pasting when you have the number only style though. I don't know why though. These are the only APIs I'd know of to do such things myself, so I don't know what AAG could have meant if these weren't the ones.
I'm baaaack...
VB5 Professional Edition, VC++ 6
Using a 1 gHz Thunderbird, 256 mb RAM, 40 gb HD system with Win98se
I feel special because I finally figured out how to loop midis: Post link
I'm a fanatic too 
-
Aug 22nd, 2001, 10:57 AM
#17
Thread Starter
Registered User
-
Aug 22nd, 2001, 02:36 PM
#18
Fanatic Member
Here's some other stuff you may want to add to the class. I see people asking about getting/changing lines in text files/boxes, and about the row/column numbers too. These props will do those for you. There's an API method of the property get version of Lines which I'd say is faster because it doesn't use the split method, but I don't know of a way to implement the API in a property let version. I had to do that with Split().
VB Code:
Private Const EM_GETLINE As Long = &HC4
Private Const EM_GETLINECOUNT As Long = &HBA
Private Const EM_GETSEL As Long = &HB0
Private Const EM_LINEFROMCHAR As Long = &HC9
Private Const EM_LINEINDEX As Long = &HBB
Private Const EM_LINELENGTH As Long = &HC1
'here's the API way if anyone wants to know how it was done
Public Property Get LinesAPI(ByVal lngLineNumber As Long) As String
Dim lngLineCount As Long
Dim lngFirstChar As Long
Dim lngLineLength As Long
Dim hWnd As Long
Dim abyteBuffer() As Byte
LinesAPI = ""
If Not blnClassBound Then Exit Property
hWnd = txtSource.hWnd
lngLineCount = SendMessage(hWnd, EM_GETLINECOUNT, 0, 0)
If (lngLineNumber < 0) Or (lngLineNumber > (lngLineCount - 1)) Then Exit Property
lngFirstChar = SendMessage(hWnd, EM_LINEINDEX, lngLineNumber, 0)
lngLineLength = SendMessage(hWnd, EM_LINELENGTH, lngFirstChar, 0)
If lngLineLength = 0 Then Exit Property
ReDim abyteBuffer(0 To (lngLineLength - 1))
abyteBuffer(0) = lngLineLength And &HFF
If lngLineLength > 255 Then abyteBuffer(1) = lngLineLength And &H100
SendMessage hWnd, EM_GETLINE, lngLineNumber, VarPtr(abyteBuffer(0))
LinesAPI = StrConv(abyteBuffer, vbUnicode)
End Property
'this is the Split() way, which takes less work, but may be slower for longer text
Public Property Get Lines(ByVal lngLineNumber As Long) As String
Dim astrLines As Variant 'may be astrLines() As String for VB6 users
Lines = ""
If Not blnClassBound Then Exit Property
If txtSource.Text = "" Then Exit Property
astrLines = Split(txtSource.Text, vbCrLf)
If (lngLineNumber < 0) Or (lngLineNumber > UBound(astrLines)) Then Exit Property
Lines = astrLines(lngLineNumber)
End Property
Public Property Let Lines(ByVal lngLineNumber As Long, ByVal strNewLine As String)
Dim astrLines As Variant 'may be astrLines() As String for VB6 users
If Not blnClassBound Then Exit Property
If txtSource.Text = "" Then
txtSource.Text = strNewLine
Exit Property
End If
astrLines = Split(txtSource.Text, vbCrLf)
If (lngLineNumber < 0) Or (lngLineNumber > UBound(astrLines)) Then Exit Property
astrLines(lngLineNumber) = strNewLine
txtSource.Text = Join(astrLines, vbCrLf)
End Property
'the next two are mainly for multiline textboxes, but work on single line ones too
'the row will always be 1 for a single line textbox
Public Property Get Column() As Long
Dim lngReturn As Long
Dim lngCharIndex As Long
Dim lngRow As Long
If Not blnClassBound Then Exit Property
lngReturn = SendMessage(txtSource.hWnd, EM_GETSEL, 0, 0)
lngCharIndex = (lngReturn / 65536) And &HFFFF
lngReturn = SendMessage(txtSource.hWnd, EM_LINEINDEX, -1, 0)
Column = lngCharIndex - lngReturn + 1
End Property
Public Property Get Row() As Long
Dim lngReturn As Long
Dim lngCharIndex As Long
If Not blnClassBound Then Exit Property
lngReturn = SendMessage(txtSource.hWnd, EM_GETSEL, 0, 0)
lngCharIndex = (lngReturn / 65536) And &HFFFF
Row = SendMessage(txtSource.hWnd, EM_LINEFROMCHAR, lngCharIndex, 0) + 1
End Property
Last edited by Kaverin; Sep 14th, 2001 at 05:38 PM.
I'm baaaack...
VB5 Professional Edition, VC++ 6
Using a 1 gHz Thunderbird, 256 mb RAM, 40 gb HD system with Win98se
I feel special because I finally figured out how to loop midis: Post link
I'm a fanatic too 
-
Aug 22nd, 2001, 07:09 PM
#19
Thread Starter
Registered User
Nice one Kaverin, those props will come in handy.
Posting Permissions
- You may not post new threads
- You may not post replies
- You may not post attachments
- You may not edit your posts
-
Forum Rules
|
Click Here to Expand Forum to Full Width
|