Search code examples
listviewdelphidelphi-2009

How to find the next ListView item that is not selected?


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;

Solution

  • You are calling ListView_FindItem() using the LVFI_PARAM flag:

    LVFI_PARAM

    Searches for a match between this structure's lParam member and the lParam member of an item's LVITEM 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.