Search code examples
winformsms-accesscomboboxms-access-formsms-forms

Equivalent to Microsoft Forms 2.0 ComboBox .TopIndex property in Windows Forms and Access Forms?


The (deprecated) Microsoft Forms 2.0 controls include a combobox which provides an invaluable property: .TopIndex (documentation here).

It seems that this property is not available with standard comboboxes in forms in Microsoft Access 2019 (according to the documentation), and is not available with standard comboboxes in Windows Forms (.NET) (here, ComboBox inherits from ListControl and does not provide this property, while ListBox also inherits from ListControl, but provides it).

I have a lot of old code which heavily relies on the .TopIndex property, and it's time to move that code to other technologies.

So I'd like to know if I have missed something in the documentation and if there is an equivalent property with another name which I could use to determine which items are visible in the list part of a combobox. I'd like to know this for comboboxes in Access 2019 (I am not that hostile towards this application as many others here are) as well as for comboboxes in Windows Forms.

I am aware that there are a lot of free and commercial controls (including comboboxes) with enhanced functionality for Windows Forms. I will definitely go that way (or write my own) unless I have missed something in the documentation.

However, the situation is completely different when it comes to Access 2019 forms. I could not find a single free third-party ActiveX / COM combobox which I could use on Access forms and which provides this functionality. Theoretically, I probably could write an ActiveX / COM control using .NET and then use it on Access 2019 forms, but this seems quite painful.


Solution

  • As far as .Net WinForm ComboBox goes, you have not missed anything as the functionality of the TopIndex property is not implemented. That said, it is pretty straight forward to extend the base ComboBox control to add this property. The following example control should get you started.

    This control attaches a listener to the native ListBox dropdown and updates the TopIndex property on WM_VSCROLL and the LB_SETCARETINDEX (this captures the initial position on opening) messages. Additionally, the base SelectedIndexChange event is used to capture changes due to keyboard actions (pgUp/pgDn, Arrow up/down). The TopIndex property is retained after the dropdown closes and is reset on opening the dropdown. The control also exposes a TopIndexChanged event.

    Imports System.Runtime.InteropServices
    
    Public Class ComboBoxEx : Inherits ComboBox
      Private listBoxListener As ListBoxNativeWindow
      Public Event TopIndexChanged As EventHandler(Of ComboBoxTopIndexArg)
    
      Private _TopIndex As Int32 = -1
    
      Public Sub New()
        MyBase.New
        listBoxListener = New ListBoxNativeWindow(Me)
      End Sub
    
      Public Property TopIndex As Int32
        Get
          Return _TopIndex
        End Get
        Private Set(value As Int32)
          If value <> _TopIndex Then
            _TopIndex = value
            RaiseEvent TopIndexChanged(Me, New ComboBoxTopIndexArg(value))
          End If
        End Set
      End Property
    
      Protected Overrides Sub OnDropDown(e As EventArgs)
        _TopIndex = -1 ' reset on opening the listbox
        MyBase.OnDropDown(e)
      End Sub
    
      Private Class ListBoxNativeWindow : Inherits NativeWindow
        Private listBoxHandle As IntPtr
        Private TopIndex As Int32
        Private parent As ComboBoxEx
    
        Public Sub New(ByVal parent As ComboBoxEx)
          Me.parent = parent
          WireParent()
          If parent.IsHandleCreated Then
            GetListBoxHandle()
            AssignHandle(listBoxHandle)
          End If
        End Sub
    
        Private Sub WireParent()
          AddHandler parent.HandleCreated, AddressOf Me.OnHandleCreated
          AddHandler parent.HandleDestroyed, AddressOf Me.OnHandleDestroyed
          AddHandler parent.SelectedIndexChanged, AddressOf UpdateTopIndexOnIndexChanged
        End Sub
    
        Private Sub OnHandleCreated(ByVal sender As Object, ByVal e As EventArgs)
          GetListBoxHandle()
          AssignHandle(listBoxHandle)
        End Sub
    
        Private Sub OnHandleDestroyed(ByVal sender As Object, ByVal e As EventArgs)
          ReleaseHandle()
        End Sub
    
        Private Sub UpdateTopIndexOnIndexChanged(sender As Object, e As EventArgs)
          SetParentTopIndex()
        End Sub
    
        Private Sub GetListBoxHandle()
          Const CB_GETCOMBOBOXINFO As Int32 = &H164
          Dim info As New ComboBoxInfo
          info.cbSize = Marshal.SizeOf(info)
          Dim res As Boolean = SendMessage(Me.parent.Handle, CB_GETCOMBOBOXINFO, Nothing, info)
          listBoxHandle = info.hwndList
        End Sub
    
        Protected Overrides Sub WndProc(ByRef m As Message)
          Const WM_VSCROLL As Int32 = &H115
          Const LB_SETCARETINDEX As Int32 = &H19E
    
          MyBase.WndProc(m)
          If m.Msg = WM_VSCROLL OrElse m.Msg = LB_SETCARETINDEX Then
            SetParentTopIndex()
          End If
        End Sub
    
        Private Sub SetParentTopIndex()
          Const LB_GETTOPINDEX As Int32 = &H18E
          parent.TopIndex = SendMessage(listBoxHandle, LB_GETTOPINDEX, IntPtr.Zero, IntPtr.Zero)
        End Sub
      End Class
    
      Public Class ComboBoxTopIndexArg : Inherits EventArgs
        Public Sub New(topIndex As Int32)
          Me.TopIndex = topIndex
        End Sub
    
        Public ReadOnly Property TopIndex As Int32
      End Class
    
    #Region "NativeMethods"
      <StructLayout(LayoutKind.Sequential)>
      Private Structure ComboBoxInfo
        Public cbSize As Int32
        Public rcItem As RECT
        Public rcButton As RECT
        Public stateButton As IntPtr
        Public hwndCombo As IntPtr
        Public hwndEdit As IntPtr
        Public hwndList As IntPtr
      End Structure
    
      <StructLayout(LayoutKind.Sequential)>
      Private Structure RECT
        Public Left, Top, Right, Bottom As Int32
      End Structure
    
      <DllImport("user32.dll")>
      Private Shared Function SendMessage(hWnd As IntPtr, Msg As Int32, wParam As IntPtr, <Out()> ByRef lParam As ComboBoxInfo) As Boolean
      End Function
    
      <DllImport("user32.dll")>
      Private Shared Function SendMessage(hWnd As IntPtr, Msg As Int32, wParam As IntPtr, lParam As IntPtr) As Int32
      End Function
    #End Region
    
    End Class
    

    I leave it to you to wrap this in an ActiveX exposed wrapper for use in Access. Doing so is fairly easy using the templates in the Microsoft InteropForms Toolkit 2.1. Just note that those templates are setup using the "Any CPU" platform and you will need to change that to "x86".