I want to search the ListView for the next unselected item, but only with windows APIs.
I tried with the ListView_FindItem
macro but it's not working. The result is always -1:
function TNewListView.NextUnselected(I: Integer): Integer;
var FindInfo: TLVFindInfo;
ItemInfo: TLVItem;
begin
if not HandleAllocated then Exit(-1)
else begin
FillChar(ItemInfo, SizeOf(ItemInfo), 0);
ItemInfo.mask:= LVIF_STATE;
ItemInfo.state:= 0;
ItemInfo.stateMask:= LVIS_SELECTED;
FillChar(FindInfo, SizeOf(FindInfo), 0);
FindInfo.flags:= LVFI_PARAM;
FindInfo.lParam:= LPARAM(@ItemInfo);
Result:= ListView_FindItem(Handle, I, FindInfo);
end;
You are calling ListView_FindItem()
using the LVFI_PARAM
flag:
LVFI_PARAM
Searches for a match between this structure's
lParam
member and thelParam
member of an item'sLVITEM
structure.
That tells the ListView to compare the specified TLVFindInfo.lParam
value as-is to the lParam
of each list item until it finds a match.
If you are using the TListView
in non-virtual mode (OwnerData=False
), a list item's lParam
value holds its corresponding TListItem
object pointer.
If you are using the TListView
in virtual mode (OwnerData=True
), a list item's lParam
value is always 0.
ListView_FindItem()
(and the underlying LVM_FINDITEM
message) can search for a list item by either its Caption
(full or partial), its lParam
1, or its position, but nothing else.
1: For example, the TListItems.IndexOf()
method uses ListView_FindItem()
to return the index of a specified TListItem
object using an lParam
search (which only works in non-virtual mode, where the lParam
of each item is a TListItem
object pointer).
You are trying to perform an lParam
search as well, but you are using the WRONG lParam
value to search for! You are setting the TLVFindInfo.lParam
value to a pointer to a local TLVItem
variable, so the LVFI_PARAM
comparisons will never find a matching list item. That is why you are always getting a result of -1.
ListView_FindItem()
is essentially doing the following logic in your example:
function ListView_FindItem(hWnd: HWND; iStart: Integer; const plvfi: TLVFindInfo): Integer;
var
lvi: TLVItem;
begin
for Result := iStart+1 to ListView_GetItemCount(hWnd)-1 do
begin
FillChar(lvi, SizeOf(lvi), 0);
lvi.iIndex := Result;
lvi.mask = LVIF_PARAM;
ListView_GetItem(hWnd, lvi);
if lvi.lParam = plvfi.lParam then // <-- NEVER FINDS A MATCH!
Exit;
end;
Result := -1;
end;
As you can see, the contents of your local TLVItem
variable are NEVER USED at all, so it doesn't matter what you set the TLVItem
fields to.
You are expecting ListView_FindItem()
to essentially do the following logic instead, WHICH IS NOT HOW IT WORKS, AND IS NOT DOCUMENTED TO WORK THIS WAY:
function ListView_FindItem(hWnd: HWND; iStart: Integer; const plvfi: TLVFindInfo): Integer;
var
lvi: TLVItem;
begin
for Result := iStart+1 to ListView_GetItemCount(hWnd)-1 do
begin
FillChar(lvi, SizeOf(lvi), 0);
lvi.iIndex := Result;
lvi.mask = LVIF_STATE;
lvi.stateMask := PLVItem(plvfi.lParam)^.stateMask;
ListView_GetItem(hWnd, lvi);
if lvi.state = PLVItem(plvfi.lParam)^.state then // <-- BUZZ, WRONG!
Exit;
end;
Result := -1;
end;
So, you simply cannot search for an item by state using ListView_FindItem()
/LVM_FINDITEM
, they do not support that kind of search.
You might be tempted to use ListView_GetNextItem()
/LVM_GETNEXTITEM
instead:
Searches for a list-view item that has the specified properties and bears the specified relationship to a specified item.
But, they can only be used to search for a list item that has specified characteristics enabled (such as having LVNI_SELECTED
enabled). They cannot be used to find an item that has an ABSENCE of specified characteristics (such as having LVNI_SELECTED
disabled).
So, to do what you want, you will just have to manually iterate through the list items, using ListView_GetItem()
or ListView_GetItemState()
to retrieve each item's current state, until you find what you are looking for.
For example:
function TNewListView.NextUnselected(StartIndex: Integer): Integer;
begin
if HandleAllocated then
begin
for Result := StartIndex+1 to ListView_GetItemCount(Handle)-1 do
begin
if (ListView_GetItemState(Handle, Result, LVIS_SELECTED) and LVIS_SELECTED) = 0 then
Exit;
end;
// if you want to implement wrap-around searching, uncomment this...
{
for Result := 0 to StartIndex-1 do
begin
if (ListView_GetItemState(Handle, Result, LVIS_SELECTED) and LVIS_SELECTED) = 0 then
Exit;
end;
}
end;
Result := -1;
end;
Or:
function TNewListView.NextUnselected(StartIndex: Integer): Integer;
function IsNotSelected(Index: Integer): Boolean;
var
ItemInfo: TLVItem;
begin
FillChar(ItemInfo, SizeOf(ItemInfo), 0);
ItemInfo.iItem := Index;
ItemInfo.mask := LVIF_STATE;
ItemInfo.stateMask := LVIS_SELECTED;
ListView_GetItem(Handle, ItemInfo);
Result := (ItemInfo.state and LVIS_SELECTED) = 0;
end;
begin
if HandleAllocated then
begin
for Result := StartIndex+1 to ListView_GetItemCount(Handle)-1 do
begin
if IsNotSelected(Result) then
Exit;
end;
// if you want to implement wrap-around searching, uncomment this...
{
for Result := 0 to StartIndex-1 do
begin
if IsNotSelected(Result) then
Exit;
end;
}
end;
Result := -1;
end;
Both approaches work for what you are attempting.