Reputation: 1163
How can I:
listHeight - viewPortLength
. Here it can be assumed that the listView
is a simple plain textual list view (Not the big/small pictures version(s)).
While there are a lot of answers pointing to using the EnsureVisible
function, all this does is jump around randomly to ensure the item indicated is visible. If a certain 'scroll position' is desired, and one has no idea beforehand what the visible item(s) should be (because the underlying data might change), then this method is of little use.
I've also found suggestions of using the win32-api, specifically using setScrollInfo
and setScrollWindowEx
. Unfortunately, setScrollInfo
only really ephemerally modifies the scroll bar itself and leaves the window content unchanged, and is only really useful for those implementing their own special scroll behaviours. setScrollWindowEx
should in theory work via adjusting viewports and blitting in new content, but when used with this object said function will cheerily report that it was succesful without doing anything and reporting it has redrawn a rectangle of size [0, 0]
at position [0, 0]
in [w,h],[x,y]
notation. In other words, the internal state of a ListView isn't very accessible.
Upvotes: 0
Views: 1810
Reputation: 1163
I found a partial solution to this problem. Suppose ListView
is being extended/subclassed. First, import a function from user32.dll
; GetScrollInfo
, as follows:
Declare Function GetScrollInfo Lib "user32" Alias "GetScrollInfo" (ByVal hWnd As IntPtr, _
ByVal n As Integer, ByRef lpScrollInfo As SCROLLINFO) As Integer
Structure SCROLLINFO
Dim cbSize As Integer
Dim fMask As Integer
Dim nMin As Integer
Dim nMax As Integer
Dim nPage As Integer
Dim nPos As Integer
Dim nTrackPos As Integer
End Structure
Private Const SB_HORZ As Integer = 0
Private Const SB_VERT As Integer = 1
This can be used to get the scroll position of the window as a Y-offset. The scroll position is defined in fantasy-scroll-units, which are definable however the callee wants them to be, as long as they're 32-bit integers. The 'scroll action' need not be euclidean either, but the units are used to size the scroll bar; e.g. the size of the draggable element or the 'page size' is defined as the size of a window in 'scroll units' divided by the size of the total scroll bar, with a pre-set hardcoded minimum size, but how much each click or drag or mousewheel movement scrolls is coded by the element using the scroll bar.
That leads to the definition of this SCROLLINFO
structure. It has seven members;
nMin
and nMax
. nMin <= nPos <= nMax
is automatically clamped by the scrollBar
. When the scrollbar is fully at the top, then nPos = nMin
. Fully down at the bottom, nPos = nMax
.cbSize
is the size of this structure. Typically cbSize = 28
when using the win32 api.fMask
can be used to retrieve only part of the values. To retrieve all, fMask = 7
. Values of:
0x01
: Retrieves the nMin
, nMax
0x02
: Retrieves nPage
0x04
: Retrieves nPos
and nTrackPos
nPage
denotes the (relative) size of the scroll knob: nPage / (nMax - nMin)
would be the height relative to the size of the bar for a y-scrollbar.nPos
is the current position of the scrollbar. nTrackPos
is the same, but is updated during a click&drag operation, allowing you to have a form element that moves while you drag the scrollbar (instead of the default; when you release it), like most text editors do.The first trick is to figure out exactly how the imaginary units map to pixels. Fortunately for a ListView
this appears rather straightforward: The following definition gets 'close enough';
Public Function getScrollPos() As Integer
Dim si As SCROLLINFO = Me.getScrollInfo()
Dim x As Integer = GetScrollInfo(Me.Handle, SB_VERT, si)
' The Windows Scrollbar magicunit is the same as pixels, off by exactly one window height.
' Adding nPage gets you a pixel based location.
Return si.nPos + si.nPage
End Function
Next, we have to use this information and set the Y-value of the listView
. This is the hard part; there's no ready-to-use method for this task in basic windows forms. However, each item does have a function to get its Y position. But, the naïve method of simply using Me.items(Y/17)
(Note; 17 is the default height) does not work at all, because the Me.Items
member variable is not sorted with the display.
Public Sub setScrollPos(ByVal y As Integer)
Dim si As SCROLLINFO = Me.getScrollInfo()
Dim x As Integer = GetScrollInfo(Me.Handle, SB_VERT, si)
If (y > si.nMax) Then
y = Math.Max(si.nMax, 0)
End If
Dim itemHeight As Integer = Me.Items(0).GetBounds(ItemBoundsPortion.Entire).Height
Dim iClosest = 0
Dim iTmp = 0
Dim delta = Integer.MaxValue
' "Stupidly clever" solution by simply trying all items out and finding out which one is closest to the Y value.
' Implemented as there is no "sorted index" exposed by ListViewItemCollection. (No idea why...)
If Me.Items.Count > 0 Then
For i As Integer = 0 To Me.Items.Count - 1 Step 1
iTmp = Math.Abs(Me.Items.Item(i).Position.Y - y)
If iTmp < delta Then
delta = iTmp
iClosest = i
End If
Next 'i
End If
Me.TopItem = Me.Items.Item(iClosest)
Me.Invalidate()
End Sub
Here, loop over each item, find out which one has the best fitting y-coordinate for our measurement, then set that as the top. The solution is 'stupid' in the sense that it does a calculation that the form needs to do anyway in order to render itself correctly: it needs to know the order its list elements are rendered in. It's also not quite correct: if the top intended visible line is (part of) a "group separator", then our setScrollPos
function will instead find the nearest ListViewItem
and display that up top. It also cannot handle any sub-line movement and can be thus off by (up to) half a line height, by rounding to a whole line.
Upvotes: 1