読者です 読者をやめる 読者になる 読者になる

プレビューハンドラで検索されている単語の一覧を取得する

Win32

Windows Vista/7では、IPreviewHandlerを実装することによってエクスプローラの右ペインにファイルのプレビューを表示することが出来る。
ところで、Windows 7では、ファイル検索時に、ファイル一覧では、検索された単語がハイライトされる機能まで実装されているのだが、なぜだか、プレビューハンドラには、この検索条件が一切通知されない。というか、探しても、それを取得するの必要なインターフェイスが見あたらない。

ということで、エクスプローラにユーザーから入力されたキーワード一覧を無理矢理知る方法を考えてみた。

ユーザーの検索クエリを取得する

エクスプローラで検索を行っている状態で、Spyを起動して、ユーザーの検索クエリ内容が取得できないかなぁと見ていたら、ShellTabWindowClassというクラス名のウィンドウが

検索単語 - 検索結果

という状態のテキストを保持していることが分かった。で、闇雲にこのクラス名のウィンドウを探しても良かったのだけれども、このウィンドウは、IPreviewHandlerのウィンドウの先祖(Ancestor)であることが分かったので、Ancestorのウィンドウに限って、このクラスのウィンドウを探すことにする。

// This function tries to get a window of ShellTabWindowClass class.
static HWND getShellTabWindowClass(HWND hWnd /* IPreviewHandler's window handle */)
{
  for(;;)
  {
    if(!hWnd)
      return NULL;
    
    TCHAR className[MAX_PATH];
    if(GetClassName(hWnd, className, MAX_PATH))
    {
      if(_tcscmp(className, _T("ShellTabWindowClass")) == 0)
        return hWnd;
    }
  
    hWnd = GetAncestor(hWnd, GA_PARENT);
  }
}

そうしたら、このウィンドウのキャプションから、" - 検索結果"の部分を見つけ出して、削除すれば、ユーザーの検索クエリ内容になるはず。ただ、"検索結果"の部分は、ローカライズによっていろいろと変更される可能性があるので、最後の " - "を探して、それ以降の文字列を削ることで対処する。
これはたいした作業ではないのだが、ATLに、LastIndexOf的なメソッドがないので微妙に難儀だ。

static CString getCaption(HWND hWnd)
{
  CString ret;
  int len = GetWindowTextLength(hWnd);
  TCHAR* buf = ret.GetBufferSetLength(len);
  GetWindowText(hWnd, buf, len + 1);
  return ret;
}

// こんな関数、本当は作りたくなかった。
static int findReverse(const CString& str, LPCTSTR pattern)
{
  int pos = 0;
  int prevPos = -1;
  int len = _tcslen(pattern);
  for(;;)
  {
    int newPos = str.Find(pattern, pos);
    if(newPos < 0)
      return prevPos;

    prevPos = newPos;
    pos = newPos + len;
  }
}

// From ShellTabWindowClass window, we can obtain user's query string
static CString getQueryString(HWND hWnd)
{
  HWND hWndShellTab = getShellTabWindowClass(hWnd);
  if(!hWndShellTab)
    return _T("");

  CString caption = getCaption(hWndShellTab);
  int sep = findReverse(caption, _T(" - "));
  if(sep > 0)
    caption = caption.Left(sep);
  return caption;
}

クエリの文法解析

これでめでたくクエリが取得できるんだけど、このクエリ、Advanced Query Syntaxという書式に基づいており、また、使える命令がローカライズまでされているので、自前で解析するのは現実的ではない。
と思っていたら、ISearchQueryHelper::GenerateSQLFromUserQueryで簡単にSQLに変換できるらしい。
でも、SQLになっても、やっぱり解析が面倒・・・と思っていたら、プレーンな(制約のない)検索単語に関しては、

CONTAINS(*,"ほげほげ*" AND "ふがふが*")

の様な形でWHERE節に出現することが判明。一応、キーワード中に " とか、 ' を入れたらどうなるか調べてみたが、適当の処理されるようで、少なくとも、簡単にSQL Injectionなどを受けるようにはなってない模様。ということで、"〜*"の部分を抜き出して、"〜"だけを取得すればいいらしい。

// Obtains keyword list
void analyzeQueryKeyword(HWND hWnd, std::vector<wstring>& keywords)
{
  m_keywords.clear();
  
  // try to obtain keyword list from ShellTabWindowClass caption
  CStringW query = CT2W(getQueryString(hWnd));
  if(query.GetLength() > 0)
  {
    CComPtr<ISearchManager> scm;
    TOF(scm.CoCreateInstance(__uuidof(CSearchManager)));
    
    CComQIPtr<ISearchCatalogManager> catMan;
    TOF(scm->GetCatalog(L"SystemIndex", &catMan));
    
    CComQIPtr<ISearchQueryHelper> queryHelper;
    TOF(catMan->GetQueryHelper(&queryHelper));
    
    LPWSTR sql = NULL;
    TOF(queryHelper->GenerateSQLFromUserQuery(query, &sql));
    
    // create a writeable copy of input SQL
    CStringW tmp;
    wchar_t* buf = tmp.GetBufferSetLength(wcslen(sql));
    wcscpy(buf, sql);

    // parse SQL to obtain non-restricted search words
    LPCWSTR contains = L"CONTAINS(*,'";
    wchar_t* p = wcsstr(buf, contains);
    if(p)
    {
      p += wcslen(contains);
      for(;;)
      {
        wchar_t* pw = ++p; // skip "
        while(*p != '\"') p++;
        size_t len = p - pw;
        *p++ = 0; // skip "
        
        if(len > 0)
        {
          wchar_t* lastChar = pw + len - 1;
          if(*lastChar == '*')
            *lastChar = 0;
          
          keywords.push_back(pw);
        }
        
        while(*p != '\"' && *p != '\'') p++;
        if(*p == '\'')
          break;
      }
    }
  }
}