KnowledgeBase Archive

An Archive of Early Microsoft KnowledgeBase Articles

View on GitHub

Q192208: BUG: UserControl SHIFT+TAB Does Not Follow Tab Sequence

Article: Q192208
Product(s): Microsoft Visual Basic for Windows
Version(s): 6.0
Operating System(s): 
Keyword(s): kbCtrlCreate kbVBp kbVBp600bug kbGrpDSVB kbDSupport
Last Modified: 10-MAR-2002

-------------------------------------------------------------------------------
The information in this article applies to:

- Microsoft Visual Basic Learning Edition for Windows, version 6.0 
- Microsoft Visual Basic Professional Edition for Windows, version 6.0 
- Microsoft Visual Basic Enterprise Edition for Windows, version 6.0 
-------------------------------------------------------------------------------

SYMPTOMS
========

When tabbing through constituent controls contained on a UserControl in reverse
order (SHIFT+TAB), only the first constituent control is part of the reverse tab
sequence. All controls should be part of the reverse tab sequence unless the
TabStop property is set to False. Tabbing forward through the controls works as
expected.

STATUS
======

Microsoft has confirmed this to be a bug in the Microsoft products listed at the
beginning of this article.

MORE INFORMATION
================

Steps to Reproduce Behavior
---------------------------

1. Start a new Standard EXE project in Visual Basic. Form1 is created by
  default.

2. From the File menu, add an ActiveX Control project. UserControl1 is created
  by default.

3. Add three TextBox controls to UserControl1.

4. Close the UserControl1 window and add an instance of UserControl1 to Form1.

5. Add a Command button to Form1.

6. Save and run the Project.

7. First, tab through all the controls using the TAB key. Now tab backwards
  through the controls using the SHIFT+TAB key combination. Observe that the
  only constituent control that receives focus is the first TextBox.

  Bug: Only the first control on the UserControl is included in the reverse
  tabbing sequence.

8. Return to design mode. On the Project menu, choose Add Class Module to add a
  new class module. Change the class name to be "clsTabManager" (without the
  quotation marks) and add the following code to this module:

  Option Explicit

  '=============================================================================
  ' This class was created to work around a bug in UserControls.  The bug is
  ' documented in Microsoft KB article
  '
  ' Q192208: BUG: UserControl SHIFT+TAB Does Not Follow Tab Sequence
  '
  ' To use this class, you need to have the following code in your user control
  '
  ' Private m_TabManager As clsTabManager
  '
  ' Private Sub UserControl_EnterFocus()
  '    m_TabManager.ProcessEnterFocus
  ' End Sub
  '
  ' Private Sub UserControl_ExitFocus()
  '    m_TabManager.ProcessExitFocus
  ' End Sub
  '
  ' Private Sub UserControl_Terminate()
  '    Set m_TabManager = Nothing
  ' End Sub
  '
  ' Private Sub UserControl_Initialize()
  '    If m_TabManager Is Nothing Then
  '       Set m_TabManager = New clsTabManager
  '       m_TabManager.ClassConstruct UserControl.Controls
  '    End If
  ' End Sub
  '=============================================================================

  Private Declare Function SetFocusAPI Lib "user32" Alias "SetFocus" _
      (ByVal hWnd As Long) As Long
  Private Declare Function GetKeyState Lib "user32" (ByVal nVirtKey As Long) _
      As Integer
  Private Declare Function GetForegroundWindow Lib "user32" () As Long

  ' All the controls that can get focus are stored in this array in tab order.
  Private m_WindowArray() As Control

  ' Number of controls
  Private m_NumberOfWindows As Integer

  ' Store the last foreground window to work around the bug documented in
  ' Microsoft KB article Q253782.
  Private m_hWndLastForeGround As Long

  ' Determine if ClassConstruct was called.
  Dim m_ClassIsConstructed As Boolean

  '==============================================================================
  ' Description: Construct the array of tabbable controls.
  ' Parameters : CtlCollection - controls collection from the user control.
  ' Return Val : True if the class is ready to be used
  ' Notes      : This function needs to be called before you use the class and
  '               whenever the Tab order of the controls is changed.
  '==============================================================================
  Public Function ClassConstruct(ByRef CtlCollection As Object) As Boolean
     Reset
     If Not (CtlCollection Is Nothing) Then
        SetupWindowArray CtlCollection
      
        m_ClassIsConstructed = True
        ClassConstruct = True
        m_hWndLastForeGround = GetForegroundWindow
     Else
        m_ClassIsConstructed = False
        ClassConstruct = False
     End If
  End Function

  '=============================================================================
  ' Description: Set focus to the last control in the tab order and should be
  '               called from the EnterFocus event of the UserControl.
  ' Parameters : None.
  ' Return Val : True if the focus was changed, false if it wasn't.
  '=============================================================================
  Public Function ProcessEnterFocus() As Boolean
     Dim LastCtl As Control
     ProcessEnterFocus = False

     If IsUserShiftTabbing() Then
        Set LastCtl = GetLastFocusableTabStop()
        If (Not (LastCtl Is Nothing)) And _
              m_hWndLastForeGround = GetForegroundWindow Then
           SetFocusAPI LastCtl.hWnd
           ProcessEnterFocus = True
        Else
           m_hWndLastForeGround = GetForegroundWindow
        End If
     End If
  End Function

  '=============================================================================
  ' Description: Record the current foreground window and should be
  '               called from the ExitFocus event of the UserControl.
  ' Parameters : None.
  ' Return Val : True.
  '=============================================================================
  Public Function ProcessExitFocus() As Boolean
     m_hWndLastForeGround = GetForegroundWindow
     ProcessExitFocus = True
  End Function

  '=============================================================================
  ' Description: Return the the last control that can get focus.
  ' Parameters : None.
  ' Return Val : The last control that can get focus.  If there is none, Nothing
  '               will be returned.
  '=============================================================================
  Public Function GetLastFocusableTabStop() As Control
     Dim i As Integer
    
     Set GetLastFocusableTabStop = Nothing
    
     For i = m_NumberOfWindows - 1 To 0 Step -1
        If CanWindowGetFocus(m_WindowArray(i)) Then
           Set GetLastFocusableTabStop = m_WindowArray(i)
           Exit For
        End If
     Next i
  End Function

  '=============================================================================
  ' Description: Determine if the given control can get focus or not.  This will
  '               take into account the Tabstop, Visible and enabled property
  '               and only return True if all are set to True.
  ' Parameters : Ctl - The control to check if allowed to get focus.
  ' Return Val : True if the given window can get focus, False if it cannot
  '=============================================================================
  Private Function CanWindowGetFocus(Ctl As Control)
     On Error GoTo PropertiesNotSupported
    
     If Ctl.TabStop And Ctl.Visible And Ctl.Enabled Then
        CanWindowGetFocus = True
     Else
        CanWindowGetFocus = False
     End If
    
     Exit Function

  PropertiesNotSupported:
     ' If no Tabstop, visible or enabled properties, it can't get focus
     CanWindowGetFocus = False
  End Function

  '=============================================================================
  ' Description: Walk the controls collection of the given container and fill
  '               the array of windows that are tabable controls.  We have to
  '               use an On Error trap since we need to check things via the
  '               Hwnd and not all controls in the controls collection will
  '               have an hWnd.  All items that have an hWnd will be in this
  '               array when we are done.
  ' Parameters : CtlCollection - Controls collection from the user control.
  ' Return Val : None.
  '=============================================================================
  Private Sub SetupWindowArray(ByRef CtlCollection As Object)
     Dim Ctl As Control
     Dim CtlHwnd As Long
     Dim i As Integer
     Dim NbrValidWindows As Long
     Dim NbrCtls As Long
    
     Dim TmpArray() As Control
     NbrCtls = CtlCollection.Count
     ReDim TmpArray(NbrCtls) As Control
    
     NbrValidWindows = 0
     m_NumberOfWindows = 0
    
     On Error GoTo NoHwnd
    
     For Each Ctl In CtlCollection
        ' This may trigger an Error, but the error trap should
        ' catch it and return control to next statement.
        CtlHwnd = Ctl.hWnd
      
        If CtlHwnd <> 0 Then
           ' Order the controls by tabindex.  VB Makes sure that Tab
           ' Indexes always start at 0 and go up consecutively by 1
           Set TmpArray(Ctl.TabIndex) = Ctl
           NbrValidWindows = NbrValidWindows + 1
        Else
           Set TmpArray(Ctl.TabIndex) = Nothing
        End If
     Next Ctl

     ' Now copy the array to the windows array.
     ReDim m_WindowArray(NbrValidWindows) As Control
    
     For i = 0 To NbrCtls - 1
        If Not (TmpArray(i) Is Nothing) Then
           Set m_WindowArray(m_NumberOfWindows) = TmpArray(i)
           m_NumberOfWindows = m_NumberOfWindows + 1
        End If
     Next i
    
     Exit Sub
    
  NoHwnd:
     CtlHwnd = 0
     Resume Next
  End Sub

  '=============================================================================
  ' Description: Check to see if user is shift-tabbing
  ' Parameters : None.
  ' Return Val : True if the user is shift-tabbing, False if not
  '=============================================================================
  Private Function IsUserShiftTabbing() As Boolean
     IsUserShiftTabbing = False

     If GetKeyState(vbKeyTab) < 0 And GetKeyState(vbKeyShift) < 0 Then
        IsUserShiftTabbing = True
     End If
  End Function

  '=============================================================================
  ' Description: Reset all of the data for this class.
  ' Parameters : None.
  ' Return Val : None.
  '=============================================================================
  Private Sub Reset()
     m_ClassIsConstructed = False
     m_NumberOfWindows = 0
    
     Erase m_WindowArray
  End Sub

  Private Sub Class_Initialize()
     Reset
  End Sub

  Private Sub Class_Terminate()
     Reset
  End Sub

9. Add the following code to UserControl1:

  Option Explicit
  Private m_TabManager As clsTabManager

  Private Sub UserControl_EnterFocus()
     m_TabManager.ProcessEnterFocus
  End Sub

  Private Sub UserControl_ExitFocus()
     m_TabManager.ProcessExitFocus
  End Sub

  Private Sub UserControl_Terminate()
     Set m_TabManager = Nothing
  End Sub

  Private Sub UserControl_Initialize()
     If m_TabManager Is Nothing Then
        Set m_TabManager = New clsTabManager
        m_TabManager.ClassConstruct UserControl.Controls
     End If
  End Sub

10. Save and run the project again. This time the SHIFT+TAB sequence loops
  through all the controls in the UserControl.

Additional query words:

======================================================================
Keywords          : kbCtrlCreate kbVBp kbVBp600bug kbGrpDSVB kbDSupport 
Technology        : kbVBSearch kbAudDeveloper kbZNotKeyword6 kbZNotKeyword2 kbVB600Search kbVB600
Version           : :6.0
Issue type        : kbbug
Solution Type     : kbpending

=============================================================================

THE INFORMATION PROVIDED IN THE MICROSOFT KNOWLEDGE BASE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND. MICROSOFT DISCLAIMS ALL WARRANTIES, EITHER EXPRESS OR IMPLIED, INCLUDING THE WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. IN NO EVENT SHALL MICROSOFT CORPORATION OR ITS SUPPLIERS BE LIABLE FOR ANY DAMAGES WHATSOEVER INCLUDING DIRECT, INDIRECT, INCIDENTAL, CONSEQUENTIAL, LOSS OF BUSINESS PROFITS OR SPECIAL DAMAGES, EVEN IF MICROSOFT CORPORATION OR ITS SUPPLIERS HAVE BEEN ADVISED OF THE POSSIBILITY OF SUCH DAMAGES. SOME STATES DO NOT ALLOW THE EXCLUSION OR LIMITATION OF LIABILITY FOR CONSEQUENTIAL OR INCIDENTAL DAMAGES SO THE FOREGOING LIMITATION MAY NOT APPLY.

Copyright Microsoft Corporation 1986-2002.