4

Hi I try this code in my Userform to check if Data entered in textbox1 is a number and if is not show a msgbox to user and select text in textbox1, but below code doesn't select text in textbox1 when Userform is vBModeless

Private Sub TextBox1_Change()
    If Not IsNumeric(TextBox1) Then
        MsgBox " only number"
        TextBox1.SetFocus
        TextBox1.SelStart = 0
        TextBox1.SelLength = Len(TextBox1.Text)
    End If
End Sub

is any solution?

Tim Williams
  • 154,628
  • 8
  • 97
  • 125
Avax
  • 73
  • 1
  • 8
  • 3
    When you say _code doesn't work_, how does it not work exactly? Does it produce an error, does the MsgBox not show, or something else? – Martin Nov 20 '18 at 12:27
  • 1
    @MartinParkin you right I should give more details, Code works, msgbox shows but text in textbox not selected. – Avax Nov 21 '18 at 05:26
  • Have you tried verifying the input in the `TextBox1_Exit()` sub instead of in the `Change`? You should also be able to set `Cancel` to true to prevent them leaving until the input's valid. – Mistella Nov 26 '18 at 20:04
  • Another option would be using the `Key_Down` event (or maybe `Key_Press`?) to check if the unicode is for a number, and if not, "cancel" the keypress. – Mistella Nov 26 '18 at 20:08
  • I still encourage any valid approach that actually includes text selection after `MsgBox` execution, though OP has been accepted already by Avax. I agree with @Mistella that the key to a solution should be found in the control's chain of events. – T.M. Nov 27 '18 at 07:53
  • @Commonsense found an exemplary answer by a) explicitly losing `TextBox1` 's focus by transferring it to another child control or b) switching the `Enabled` property. I'd like to motivate to many upvotes for his really instructive explanations. – T.M. Nov 27 '18 at 10:58

3 Answers3

7

The root of the problem isn't a selection, since it there and works as expected:

Private Sub TextBox1_Change()
    If Not IsNumeric(TextBox1) Then
        MsgBox " only number"
        TextBox1.SetFocus
        TextBox1.SelStart = 0
        TextBox1.SelLength = Len(TextBox1.Text)
        Debug.Print TextBox1.SelText
    End If
End Sub

I think the fundamental problem here is that MSForms controls aren't real windows, but "windowless" entity without window handle (of course, there's exceptions like listbox, tabstrip, multipage), which easily can be tested via hidden method:

'Which obviously returns a zero.
Debug.Print TextBox1.[_GethWnd]

In other hand there's the Window's message-passing model where each control is a window (hence Windows OS) with a proper window handle and with ability to send and recive messages like WM_SETFOCUS/WM_KILLFOCUS and act appropriately. And back to MSForms - the UserForm manages all the interaction between outer world and child controls internally.

Let's start by declaring WIN API function GetFocus:

Public Declare Function GetFocus Lib "user32.dll" () As Long

And let's add some of Debug.Print's to see what is happening:

Private Sub TextBox1_Change()
    If Not IsNumeric(TextBox1) Then
        Debug.Print "--"
        Debug.Print GetFocus
        MsgBox " only number"
        Debug.Print GetFocus
        TextBox1.SetFocus
        Debug.Print GetFocus
        Debug.Print "--"
        TextBox1.SelStart = 0
        TextBox1.SelLength = Len(TextBox1.Text)
    End If
End Sub

Which yields this sequence:

--
 <userform hwnd>
 <outer hwnd>
 <outer hwnd>
--

As you can see - the SetFocus has no effect, because the Userform has no idea that focus is lost (hence there's no Exit event either). To overcome this problem you should explicitly lose your focus by transferring focus to another child control or by switching Enabled (or even Visible) property:

Private Sub TextBox1_Change()
    If Not IsNumeric(TextBox1) Then
        Debug.Print "--"
        Debug.Print GetFocus
        TextBox1.Enabled = False
        'or use CommandButton1.SetFocus or something
        MsgBox " only number"
        TextBox1.Enabled = True
        Debug.Print GetFocus
        TextBox1.SetFocus
        Debug.Print GetFocus
        Debug.Print "--"
        TextBox1.SelStart = 0
        TextBox1.SelLength = Len(TextBox1.Text)
    End If
End Sub

Which yields a desired appearance and a proper sequence:

--
 <userform hwnd>
 <outer hwnd>
 <userform hwnd>
--

As a conclusion, the cause is internal and external focus states got out of sync, which stems from a slightly different managment model between MSForms and WinForms/WinAPI plus a non-modal regime of work, that mixes them both, giving an opportunity to lose focus to something non-MSForms.

CommonSense
  • 4,232
  • 2
  • 14
  • 38
  • Conclusive steps, instructive description, simple solution - an exemplary answer ... or even two answers: 1) switching the `.Enabled` property, 2) losing focus via any other control accepting focus (BTW found this 2nd approach this morning, too). Hopefully for many upvotes :-) – T.M. Nov 27 '18 at 10:38
  • I loved the instructive and conclusive way you got to a solution and am happy to award this bounty to you. I did learn from your answer :-) @CommonSense – T.M. Dec 01 '18 at 20:00
6

In my version of Excel A msgbox is always vbModal, it cannot be vbModeless, you can only set its Modal scope property to be as application level or system level

  • At Application level, it stops the application until it is responded
  • At system level it suspends all applications until the user responds to it

In order to do what you intend to do; I have created a Modeless UserForm and use it as a message box

The code becomes

Private Sub TextBox1_Change()

    If Not IsNumeric(TextBox1) Then
        UserForm2.Label1 = "Only Number is Allowed"
        UserForm2.Show

        'At this point TextBox1 has lost focus,
        'to set the focus again you have to setfocus on something else
        'and then again set focus on textbox1 (a way to reinitialize it).
        'I have added a hidden textbox2 and will set focus on it

        TextBox2.Visible = True
        TextBox2.SetFocus
        TextBox2.Visible = False

        TextBox1.SetFocus
        TextBox1.SelStart = 0
        TextBox1.SelLength = Len(TextBox1.Text)

    End If

End Sub

enter image description here

The screenshot is only a test, you can do the formatting etc according to your application.

usmanhaq
  • 1,527
  • 1
  • 6
  • 11
  • My simple hope is a solution that selects the validated text after *actually* executing a **`MsgBox`** statement (VBA.Interaction class element). But I appreciate your approach to replace the causing obstacle. – T.M. Nov 26 '18 at 18:04
  • What I have tested on my PC is; the msgbox is a Modal dialog, it stops execution of the application. For an example, if you put two msgbox statements they will never show up at once, you have to close first to resume the program and the second one will show up. Maybe the only way could be to make a system call somehow to change the vbModal behavior to vbModeless. Maybe if it is possible it can work. – usmanhaq Nov 27 '18 at 00:33
  • @usmanhaq, Thank you for your help I modified your code in my project and its working perfectly ;) – Avax Nov 27 '18 at 05:32
  • You might be interested in some amplifying links to (modeless) UserForms: [Destroy a modeless Userform instance properly](https://stackoverflow.com/questions/47357708/vba-destroy-a-modeless-userform-instance-properly), [The perfect UserForm in VBA](https://www.vitoshacademy.com/vba-the-perfect-userform-in-vba/); cf. also [Apply logic for UserForm dialog](https://rubberduckvba.wordpress.com/2018/05/08/apply-logic-for-userform-dialog/) – T.M. Nov 29 '18 at 19:47
1

I vote for usmanhaq and CommonSense!

just something to add: I've tried to implement similiar thing on one of my projects, I end up avoiding pop up another window. Just use a label to alert.

And after i implement this i find this more userfriendly!

Hope this helps!

userform:

Private Sub TextBox1_Change()
If Not IsNumeric(TextBox1.Value) Then
    Label1.Caption = "NUMBER ONLY!"
    UserForm1.TextBox1.SetFocus
    UserForm1.TextBox1.SelStart = FirstNonDigit(TextBox1.Value) - 1
    UserForm1.TextBox1.SelLength = Len(TextBox1.Text)
Else
     Label1.Caption = ""
End If
End Sub

enter image description here

this function is funded online that would help highlight starting from the first non number

Public Function FirstNonDigit(xStr As String) As Long
    Dim xChar As Integer
    Dim xPos As Integer
    Dim I As Integer
    Application.Volatile
    For I = 1 To Len(xStr)
        xChar = Asc(Mid(xStr, I, 1))
        If xChar <= 47 Or _
           xChar >= 58 Then
            xPos = I
            Exit For
        End If
    Next
    FirstNonDigit = xPos
End Function
li rachel
  • 196
  • 4