[VB6] Using Structured Queries to conduct a Windows Search by any property
Structured Queries (ICondition) and the SearchFolderItemFactory
IMPORTANT: You need an updated version of oleexp.tlb, v4.61 (released on October 3rd, 2019) or higher to run this project.
So I was looking through some old SDK examples and found a whole new way of automatically conducting very powerful searches through Structured Query objects. This creates a temporary folder of search results you can view in a file browser (like my ucShellBrowse, I'm upgrading that and spun this off as a separate demo) or, as in this demo project, show in an Open File Dialog.
This search is done by Windows, so if you have search indexing on it will be faster, but I don't and it seems very fast anyway. The primary advantage of this search method is the structured query system allows search by various criteria among any PROPERTYKEY. The Demo shows that basics, name (PKEY_ItemNameDisplay), size (PKEY_Size), date modified (PKEY_DateModified), kind (PKEY_Kind), and attributes (PKEY_FileAttributes).
Comparing dates with PKEY_DateModified necessitates using the VT_FILETIME type, so this demo shows how to create a PROPVARIANT from a VB User Type. You can use similar techniques for other PROPVARIANT types not well supported in VB.
One other neat thing in the demo; the file types listed as possible values for 'Kind' vary among Windows versions, so I've created a function to enumerate the ones on the current platform, using IPropertyEnumTypeList.
Requirements
Windows 7 or newer oleexp.tlb 4.61 or higher (Released 03 Oct 2019).
oleexp addons mIID.bas and mPKEY.bas: These modules are included in the main oleexp download; add them to your project (and the demo project, they're referenced but not included).
Code Overview
Here's the main function:
Code:
Private Function ExecFileSearchEx(pLocation As IShellItem, pConditions As ICondition) As IShellItem
Dim pObjects As IObjectCollection
Dim ppia As IShellItemArray
Dim psiSearch As IShellItem
Dim pSearchFact As ISearchFolderItemFactory
Set pSearchFact = New SearchFolderItemFactory
If CreateSearchLibrary(pObjects) = S_OK Then
pObjects.AddObject ByVal ObjPtr(pLocation)
Set ppia = pObjects
pSearchFact.SetScope ppia
pSearchFact.SetDisplayName StrPtr("Search Results")
Dim pCond As ICondition
If (pConditions Is Nothing) = False Then
pSearchFact.SetCondition pConditions
pSearchFact.GetShellItem IID_IShellItem, psiSearch
If (psiSearch Is Nothing) = False Then
Set ExecFileSearchEx = psiSearch
Else
Debug.Print "ExecFileSearchEx->Failed to get IShellItem from search factory"
End If
Set pCond = Nothing
Else
Debug.Print "ExecFileSearchEx->Failed to get ICondition"
End If
Set ppia = Nothing
End If
End Function
The first thing created is the SearchFolderItemFactory object, which creates the results folder for us.
The search scope (starting location) is defined by an IShellItemArray, and getting one of those from a single location is a bit tricky... we create an empty IShellLibrary object (like the Documents, Video libraries you see in Explorer), ask for an object collection from that, and add our item; here's the CreateSearchLibrary function referenced above:
Code:
Private Function CreateSearchLibrary(pObC As IObjectCollection) As Long
Set pObC = Nothing
Dim pLib As IShellLibrary
Set pLib = New ShellLibrary
If (pLib Is Nothing) = False Then
CreateSearchLibrary = pLib.GetFolders(LFF_ALLITEMS, IID_IObjectCollection, pObC)
Else
Debug.Print "CreateSearchLibrary->Failed to create ShellLibrary"
End If
End Function
The most difficult part is creating that ICondition object that's passed to ExecFileSearchEx. You create an ICondition object for each search condition (file name contains, size is less than, etc), then merge them into one ICondition object. Here's what that looks like:
Code:
Private Function SetConditions(ppCondition As ICondition) As Long
'Translates the Search settings into an ICondition object
Set ppCondition = Nothing
SetConditions = -1
Dim pFact As IConditionFactory2
Set pFact = New ConditionFactory
'Conditions demonstrated here:
Dim pKind As ICondition
Dim pSize As ICondition
Dim pName As ICondition
Dim pDate1 As ICondition
Dim pAttrib As ICondition
Dim aCds() As ICondition
ReDim aCds(0)
Dim nCds As Long
If (pFact Is Nothing) = False Then
Dim nCOP As CONDITION_OPERATION
'First, set Name
If Check2.Value = vbChecked Then
Dim sText As String
sText = Text1.Text
If (sText <> "") And (sText <> "*.*") And (sText <> "*") Then 'Only create a condition if it's not 'All Files'
If InStr(sText, "*") Or InStr(sText, "?") Then
nCOP = COP_DOSWILDCARDS
Else
nCOP = COP_VALUE_CONTAINS
End If
pFact.CreateStringLeaf PKEY_ItemNameDisplay, nCOP, StrPtr(sText), 0&, CONDITION_CREATION_DEFAULT, IID_ICondition, pName
Set aCds(nCds) = pName
nCds = nCds + 1
End If
End If
'Exclude folders by looking for the 'D' attribute
If Check4.Value = vbUnchecked Then
pFact.CreateStringLeaf PKEY_FileAttributes, COP_VALUE_NOTCONTAINS, StrPtr("D"), 0&, CONDITION_CREATION_DEFAULT, IID_ICondition, pAttrib
ReDim Preserve aCds(nCds)
Set aCds(nCds) = pAttrib
nCds = nCds + 1
End If
'Set size
If Check1.Value = vbChecked Then
Select Case Combo1.ListIndex
Case 0: nCOP = COP_GREATERTHAN
Case 1: nCOP = COP_EQUAL
Case 2: nCOP = COP_LESSTHAN
End Select
pFact.CreateIntegerLeaf PKEY_Size, nCOP, CLng(Text3.Text) * 1024, CONDITION_CREATION_DEFAULT, IID_ICondition, pSize
ReDim Preserve aCds(nCds)
Set aCds(nCds) = pSize
nCds = nCds + 1
End If
'Kind
If List1.ListIndex > 0 Then
Dim nKind As Long
nKind = List1.ListIndex - 1 'ListIndex starts at 0 but we inserted (any) at the start
pFact.CreateStringLeaf PKEY_Kind, COP_EQUAL, StrPtr(sKindVals(nKind)), 0&, CONDITION_CREATION_DEFAULT, IID_ICondition, pKind
ReDim Preserve aCds(nCds)
Set aCds(nCds) = pKind
nCds = nCds + 1
End If
'DateTime
If Check3.Value = vbChecked Then
Dim stVal As SYSTEMTIME
Dim dtNew As Date
Dim ftVal As FILETIME, ftUtc As FILETIME
Dim vr As Variant
Dim pkDate As oleexp.PROPERTYKEY
dtNew = DTPicker1.Value
Debug.Print "SearchDateTime " & CStr(dtNew)
stVal = DateToSystemTime(dtNew)
SystemTimeToFileTime stVal, ftVal
LocalFileTimeToFileTime ftVal, ftUtc
InitPropVariantFromFileTime ftUtc, vr
If Option1(0).Value = True Then
nCOP = COP_LESSTHAN
Else
nCOP = COP_GREATERTHAN
End If
pFact.CreateLeaf PKEY_DateModified, nCOP, vr, 0&, StrPtr(LOCALE_NAME_USER_DEFAULT), Nothing, Nothing, Nothing, CONDITION_CREATION_DEFAULT, IID_ICondition, pDate1
If (pDate1 Is Nothing) = False Then
Debug.Print "SetConditions->Created DT leaf"
ReDim Preserve aCds(nCds)
Set aCds(nCds) = pDate1
nCds = nCds + 1
Else
Debug.Print "SetConditions->Failed to create DateTime condition"
End If
End If
''You can continue to create as many condition leafs with as many Property Keys as you want.
If UBound(aCds) = 0 Then
'Only one condition, don't need an array
Set ppCondition = aCds(0)
Else
pFact.CreateCompoundFromArray CT_AND_CONDITION, aCds(0), nCds, CONDITION_CREATION_DEFAULT, IID_ICondition, ppCondition
End If
If (ppCondition Is Nothing) = False Then SetConditions = S_OK
Set pFact = Nothing
Else
Debug.Print "SetConditions->Failed to create factory."
End If
End Function
And that's it, all that's left is calling it and displaying the results:
Code:
Private Sub Command1_Click()
Dim psiLoc As IShellItem
Dim pCond As ICondition
oleexp.SHCreateItemFromParsingName StrPtr(Text2.Text), Nothing, IID_IShellItem, psiLoc
If (psiLoc Is Nothing) = False Then
If SetConditions(pCond) = S_OK Then
Set siResultsItem = ExecFileSearchEx(psiLoc, pCond)
If (siResultsItem Is Nothing) = False Then
Dim fod As FileOpenDialog
Set fod = New FileOpenDialog
fod.SetFolder siResultsItem
fod.Show Me.hWnd
'Process a file(s) selected from the results here, or even skip it and enumerate the contents of siResultsItem for the full list
End If
End If
End If
End Sub
That displays them in an Open File dialog, but there's any number of possibilities since it creates an actual location. In the upcoming version of my Shell Browser, it's just handed off to the normal load folder routine, the 'Results Folder' gets added to the directory tree under Desktop.
Last edited by fafalone; Oct 22nd, 2019 at 03:06 AM.
Reason: Emphasized need to update oleexp.tlb... there have been a lot more downloads of this project than of the new version, UPDATE!
Re: [VB6] Using Structured Queries to conduct a Windows Search by any property
It's not in the Demo project but if you wanted to just get a list of all the results, you'd pass siResultsItem to:
Code:
Private Function PrintAllResults(psi As IShellItem)
Dim lpFile As Long
Dim pEnum As IEnumShellItems
Dim siChild As IShellItem
Dim pc As Long
psi.BindToHandler 0&, BHID_EnumItems, IID_IEnumShellItems, pEnum
If (pEnum Is Nothing) = False Then
Do While (pEnum.Next(1&, siChild, pc) = S_OK)
siChild.GetDisplayName SIGDN_FILESYSPATH, lpFile
Debug.Print LPWSTRtoStr(lpFile)
Loop
End If
End Function
Re: [VB6] Using Structured Queries to conduct a Windows Search by any property
Very instructing
I haven't seen this post in 2019.
I tested it and it seems faster to search files on a network folder, instead of searching through all folders myself.
I will replace my search function in my apps using this way.
Re: [VB6] Using Structured Queries to conduct a Windows Search by any property
@xiaoyao, if a file exists in more than 1 directory that's search, there's one entry for each.
C:\dir1\a.txt
C:\dir1\dir2\a.txt
if you search dir1 for *.txt, you'll get
a.txt (Folder=dir1)
a.txt (Folder=dir2)
The default dialog includes that 'Folder' column that tells you which folder that match is in.
@Thierry69, yeah it's good for Networks and other locations besides local, regular file system locations, but what I like most is the ability to search any property.
Re: [VB6] Using Structured Queries to conduct a Windows Search by any property
Could it be shortcut files? I noticed whether it includes the .lnk or not varied against some other search methods I used, returning a different number of results.
Re: [VB6] Using Structured Queries to conduct a Windows Search by any property
I whatched your sample and built my own out of it with a little different GUI. But that's not the problem.
The problem is the condition leafes.
For example I always got ALLFILES and ALLFOLDERS search in the displayname leaf.
Code:
If hr = S_OK Then
ReDim pCondArray(nConds)
If (sSearchPattern = "*.*") Or (sSearchPattern = "*.") Or (sSearchPattern = " ") Then
If InStr(1, sSearchPattern, ";", vbTextCompare) Or InStr(1, sSearchPattern, "?", vbTextCompare) Then
lCondOpFlag = COP_DOSWILDCARDS
End If
Else
lCondOpFlag = COP_VALUE_CONTAINS
End If
pICondFactory.CreateStringLeaf PKEY_ItemNameDisplay, lCondOpFlag, StrPtr(sSearchPattern), StrPtr(vbNullString), CONDITION_CREATION_DEFAULT, IID__ICondition, pCondString
Set pCondArray(nConds) = pCondString
nConds = nConds + 1
Re: [VB6] Using Structured Queries to conduct a Windows Search by any property
These are mutually exclusive:
Code:
If (sSearchPattern = "*.*") Or (sSearchPattern = "*.") Or (sSearchPattern = " ") Then
If InStr(1, sSearchPattern, ";", vbTextCompare) Or InStr(1, sSearchPattern, "?", vbTextCompare) Then
If the search is exactly equal to one of the three in the first line, it *can't* match any on the second line. So you wind up with COP_IMPLICIT (0), which obviously doesn't give you what you want.
Re: [VB6] Using Structured Queries to conduct a Windows Search by any property
Windows Search sometimes fails to handle string case issues. I don't know if you've ever been in this situation.
The folder is in lowercase, and you can't find the file if you use uppercase to search.
Re: [VB6] Using Structured Queries to conduct a Windows Search by any property
I'm looking at a bug with certain special folders in certain versions of Windows 10/11, but other than that it works fine for me. (The bug for me is causing match none instead of match all, because it's replacing a disk path with a special folder GUID but not searching it).