65.9K
CodeProject 正在变化。 阅读更多。
Home

JWinSearch: 基于搜索的任务和选项卡切换

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.75/5 (6投票s)

2007年10月1日

CPOL

12分钟阅读

viewsIcon

40859

downloadIcon

536

一篇介绍编写JWinSearch所需的各种步骤和技术的文章。

您还需要从官方页面下载Gecko-SDK,以便构建Firefox扩展。

用法

按下 Windows + W,输入应用程序名称、标签名称或标签URL的几个字母,然后按Enter键即可切换到您想要的窗口/页面。

Screenshot - jwinsearch.png

引言

Coding Horror 博客条目的启发,我决定编写这个小工具,目标很简单

  • 按标题搜索窗口
  • 按Firefox和IE的标题或URL搜索浏览器标签
  • 通过简单的按键激活浏览器标签或窗口
  • 保持极低的资源占用

起初是一个简单的项目,但很快就变成了一项耗时的任务,因为这是我计划在业余时间完成的事情。这主要是由于我都不熟悉这两个浏览器的API,特别是Firefox的。

背景

在本文中,我将涵盖以下基础知识:

  • 热键和控件子类化
  • Win32窗口消息、枚举和控制
  • 任务切换
  • IE标签枚举和激活
  • Firefox扩展

目录

隐藏窗口和热键

为了保持简单,应用程序在后台静默运行,仅使用一个隐藏窗口来接收热键和控制消息。WIN-W 热键通过类似以下方式注册:

#define IDH_ALT_WIN 55 // Could be any number

RegisterHotKey(jws_hw_hwnd, IDH_ALT_WIN, MOD_WIN, LOBYTE(VkKeyScan(L'w'))

一旦隐藏窗口收到热键消息,它将打开或关闭非常简单的对话框。

static LRESULT CALLBACK jws_hw_proc(HWND hwnd, UINT msg, 
                        WPARAM wparam, LPARAM lparam)
{
  switch(msg)
  {
  case WM_HOTKEY:
    if(wparam != IDH_ALT_WIN)
      return FALSE;
    if(jws_hwnd)
    {
      EndDialog(jws_hwnd, 0);
      SetForegroundWindow(jws_previous_fg_win);
      return TRUE;
    }
    jws_previous_fg_win = GetForegroundWindow();
    DialogBox(jws_hinstance, MAKEINTRESOURCE(IDD_JWS_DLG), 
              jws_hw_hwnd, &jws_dlg_proc);
    return TRUE;
  }

  return DefWindowProc(hwnd, msg, wparam, lparam);
}

您一定会注意到,在显示主对话框之前,我会保存之前的活动窗口。这是为了稍后恢复它,但前提是对话框在未选择新任务时关闭。

在主对话框启动期间,将枚举窗口,并且两个主要窗口控件被子类化,以便捕获所有键盘命令,从而使应用程序按预期运行。

  • 键入内容会过滤窗口列表
  • 回车键激活列表中的第一个应用程序
  • ESC键随时关闭对话框
  • TAB键从编辑框切换到列表

在Windows XP及更高版本的Win32操作系统上,控件子类化非常简单。

static INT_PTR on_init_dlg(HWND hwnd)
{
  if(!SetWindowSubclass(GetDlgItem(hwnd, IDC_EDIT), jws_edit_subproc, 0, 0))
    EndDialog(hwnd, 1);
  if(!SetWindowSubclass(GetDlgItem(hwnd, IDC_LIST), jws_list_subproc, 0, 0))
    EndDialog(hwnd, 1);
  //...

}

static LRESULT CALLBACK jws_list_subproc(HWND hwnd, UINT msg, 
               WPARAM wparam, LPARAM lparam, UINT_PTR , DWORD_PTR)
{
  switch(msg)
  {
  case WM_GETDLGCODE:
    return DLGC_WANTALLKEYS;
  case WM_KEYDOWN:
    if(wparam == VK_ESCAPE)
    {
      EndDialog(jws_hwnd, 0);
      SetForegroundWindow(jws_previous_fg_win);
      return 0;
    }
  }

  return DefSubclassProc(hwnd, msg, wparam, lparam);
}

static LRESULT CALLBACK jws_edit_subproc(HWND hwnd, UINT msg, 
               WPARAM wparam, LPARAM lparam, UINT_PTR , DWORD_PTR)
{
  // ...

    else if(wparam == VK_DOWN || wparam == VK_TAB)
    {
      HWND lv_hwnd = GetDlgItem(jws_hwnd, IDC_LIST);
      if(!ListView_GetItemCount(lv_hwnd))
        return 0;
      ListView_SetItemState(lv_hwnd, 0, LVIS_SELECTED | 
           LVIS_FOCUSED, LVIS_SELECTED | LVIS_FOCUSED);
      SetFocus(lv_hwnd);
      return 0;
    }
    // ...

    else if(wparam == VK_RETURN)
    {
      HWND lv_hwnd = GetDlgItem(jws_hwnd, IDC_LIST);
      if(!ListView_GetItemCount(lv_hwnd))
      {
        EndDialog(jws_hwnd, 0);
        SetForegroundWindow(jws_previous_fg_win);
        return 0;
      }
      on_task_selected(0);
    }
  // ...

}

窗口枚举

枚举顶层窗口应该很简单:只需调用 EnumWindows(),通过传递 NULL 作为父窗口来指示枚举顶层窗口的意图。

棘手的部分在于收集窗口的信息(图标和标题)以及排除不感兴趣的窗口。

EnumWindows() 回调中,我:

  1. 排除不可见窗口 (WS_VISIBLE)
  2. 获取标题 (GetWindowText())
  3. 排除标题为空的窗口
  4. 排除具有工具栏扩展样式 (WS_EX_TOOLWINDOW) 的窗口
  5. 使用 WM_GETICON 消息获取图标
  6. 如果失败,则使用 GetClassLongPtr() 获取窗口类图标
  7. 如果仍然失败,则加载默认图标

枚举完窗口后,我只需对检索到的窗口列表进行排序,以便显示始终按窗口名称排序。

下面的代码大致是实现方式:

static INT_PTR on_init_dlg(HWND hwnd)
{
  //...


  EnumWindows(&jws_enum_windows_cb, 0);
  std::sort(jws_windows.begin(), jws_windows.end());
  
  // ...

}

static BOOL CALLBACK jws_enum_windows_cb(HWND hwnd, LPARAM )
{
  wchar_t title[512];

  DWORD dwStyle = GetWindowLongPtr(hwnd, GWL_STYLE);
  if(!(dwStyle & WS_VISIBLE))
    return TRUE;
  GetWindowText(hwnd, title, _countof(title));
  if(!title[0])
    return TRUE;
  LONG exstyle = GetWindowLongPtr(hwnd, GWL_EXSTYLE);
  if(exstyle & WS_EX_TOOLWINDOW)
    return TRUE;
  HICON hicon = (HICON)SendMessage(hwnd, WM_GETICON, JWS_WND_ICON_TYPE, 0);
  if(!hicon)
    hicon = (HICON)(UINT_PTR)GetClassLongPtr(hwnd, JWS_CLS_ICON_TYPE);
  if(!hicon)
    hicon = LoadIcon(NULL, IDI_APPLICATION);
  
  jws_windows.push_back(JWS_WINDOW_DATA(hwnd, hicon, title));
    
  // [Tab enumeration code] ...

  
  return TRUE;
}

上面的代码片段中的最后一行有用代码是将一个窗口添加到全局窗口列表变量中。这个全局窗口列表在对话框关闭时会被清空。它用于在行编辑框中的文本发生变化时,快速重建列表视图的内容,从而只显示匹配用户搜索查询的窗口。

类型 JWS_WINDOW_DATA 只是一个结构,用于存储窗口显示数据(图标和标题)、窗口句柄以及有关标签的其他信息。

struct JWS_WINDOW_DATA
{
  JWS_WINDOW_DATA(const JWS_WINDOW_DATA &other)
  {
    *this = other;
  }

  // Standard windows

  JWS_WINDOW_DATA(HWND _hwnd, HICON hicon, const wchar_t *_name)
    : hwnd(_hwnd), icon(hicon), name(_name), type(JWS_WT_NORMAL) { }

  // [ Contructors for IE and FF ] ...


  ~JWS_WINDOW_DATA()
  {
    // ...

  }

  void operator = (const JWS_WINDOW_DATA &other)
  {
    // ...

  }

  int icon_index;  // inside the listview's image list


  HWND hwnd;
  HICON icon;
  std::wstring name;
  std::wstring parent_name;
  JWS_WINDOW_TYPE type;
  // ...


  std::wstring comp_name(void) const
  {
    if(type == JWS_WT_NORMAL)
      return name;
    return parent_name + name;
  }
  
  bool operator  < (const JWS_WINDOW_DATA &d) const 
  { 
    return comp_name() < d.comp_name(); 
  }
};

上面的 = 运算符和复制构造函数是针对IE相关数据所必需的,我将在本文后面解释。该结构有一个比较运算符,以便 std::vector 可以被排序,如上所示。在 comp_name() 函数中,我将父窗口名称添加到标签前面,以便它们始终显示在浏览器主窗口之后。

填充列表视图

首先,我需要在对话框初始化时设置列表视图。此步骤包括创建一个列、创建和填充一个图像列表,并将其分配给列表视图。请注意下面的代码中的一些与大小相关的宏。它们允许在编译时轻松选择图标大小;只需定义 JWS_BIG_ICONS 即可使用 32x32 的图标,而不是我使用的默认 16x16 的图标。

此初始化在 WM_INIT 处理程序中完成。

static INT_PTR on_init_dlg(HWND hwnd)
{
  // ...

  LVCOLUMN col;
  memset(&col, 0, sizeof(LVCOLUMN));
  ListView_InsertColumn(GetDlgItem(jws_hwnd, IDC_LIST), 0, &col);

  HIMAGELIST image_list = ImageList_Create(JWS_ICON_SIZE, JWS_ICON_SIZE, 
                          ILC_MASK | ILC_COLOR32, 
                          (int)jws_windows.size(), 0);
  JWS_WINDOW_DATA *w, *w_end;
  int index = 0;
  for(w = &jws_windows.front(), w_end = w + 
      jws_windows.size(); w < w_end; w++)
    w->icon_index = ImageList_AddIcon(image_list, w->icon);
  ListView_SetImageList(GetDlgItem(jws_hwnd, IDC_LIST), 
                        image_list, JWS_IMAGE_LIST_SIZE);
  jws_display_window_list();
  // ...

  return TRUE;
}

在初始化时以及每次用户在编辑面板中键入内容时,都会调用 jws_display_window_list() 函数,以便它能够过滤哪些窗口和标签匹配用户的搜索条件。此函数会清除并重新填充列表视图。

注意浏览器标签的缩进有多么容易。请记住,我以一种保证标签位于浏览器主窗口之后的方式对窗口进行了排序。

虽然我设置了一个列并隐藏了其列标题,但它仍然存在。如果我不调用 ListView_SetColumnWidth(LVSCW_AUTOSIZE),内容会变得非常截断。

static void jws_display_window_list()
{
  HWND lv_hwnd = GetDlgItem(jws_hwnd, IDC_LIST);
  const JWS_WINDOW_DATA *base, *w, *w_end;
  wchar_t filter[128];

  GetDlgItemText(jws_hwnd, IDC_EDIT, filter, _countof(filter));

  ListView_DeleteAllItems(lv_hwnd);

  LRESULT ret;

  int n_item = 0;
  for(w = base = &jws_windows.front(), w_end = 
      w + jws_windows.size(); w < w_end; w++)
  {
    if(filter[0] && !stristrW(w->name.c_str(), filter))
      continue;

    LVITEM it;
    memset(&it, 0, sizeof(LVITEM));
    it.iItem = n_item++;
    it.mask = LVIF_IMAGE | LVIF_TEXT | LVIF_PARAM | LVIF_INDENT;
    it.iSubItem = 0;
    it.pszText = (LPWSTR)w->name.c_str();
    it.cchTextMax = (int)w->name.length() + 1;
    it.iImage = w->icon_index;
    it.lParam = (LPARAM)w;
    it.iIndent = w->type == JWS_WT_NORMAL ? 0 : 1;

    ret = SendMessage(lv_hwnd, LVM_INSERTITEM, 0, (LPARAM)&it);
  }
  ListView_SetColumnWidth(lv_hwnd, 0, LVSCW_AUTOSIZE);
}

请注意,LVITEM::lParam 字段用于保存对项目数据的引用。这意味着,在填充列表视图后,您无法对向量进行排序或向其中添加项目。

激活另一个任务

在项目激活时,我必须将窗口带到前面。这可能非常棘手,取决于当前窗口状态。自XP推出以来,有一项功能可以防止其他窗口抢占焦点。为了规避这一点,我不得不附加到目标线程的输入队列,通过调用 AttachThreadInput()

最后,如果选定的窗口被最小化,则需要将其恢复为正常大小。这可以通过简单的 WM_SYSCOMMAND(SC_RESTORE) 消息来完成。

为了激活一个窗口,我执行以下操作:

  1. BringWindowToTop()
  2. SetWindowPos(HWND_TOP)
  3. SetForegroundWindow()
  4. 如果需要,再次调用 AttachThreadInput()SetForegroundWindow()
  5. 如果最小化,则调用 WM_SYSCOMMAND(SC_RESTORE)
static INT_PTR on_task_selected(int item_idx)
{
  JWS_WINDOW_DATA *w;
  HWND fg_hwnd;
  LVITEM item;

  ShowWindow(jws_hwnd, SW_HIDE);
  memset(&item, 0, sizeof(LVITEM));
  item.iItem = item_idx;
  item.mask = LVIF_PARAM ;
  ListView_GetItem(GetDlgItem(jws_hwnd, IDC_LIST), &item);

  w = (JWS_WINDOW_DATA *)item.lParam;
  BringWindowToTop(w->hwnd);
  SetWindowPos(w->hwnd, HWND_TOP, 0, 0, 0, 0, SWP_NOMOVE | SWP_NOSIZE);
  fg_hwnd = GetForegroundWindow();
  if(!SetForegroundWindow(w->hwnd))
  {
    if(!fg_hwnd)
      fg_hwnd = FindWindow(L"Shell_TrayWnd", NULL);
    DWORD pid_fg_win = GetWindowThreadProcessId(fg_hwnd, NULL);
    DWORD pid_win = GetWindowThreadProcessId(w->hwnd, NULL);
    {
      wchar_t title[256];
      GetWindowText(w->hwnd, title, _countof(title));
      title[255] = 0;
    }
    AttachThreadInput(pid_fg_win, pid_win, TRUE);
    SetForegroundWindow(w->hwnd);
    AttachThreadInput(pid_fg_win, pid_win, FALSE);
  }
  if(IsIconic(w->hwnd))
    SendMessage(w->hwnd, WM_SYSCOMMAND, SC_RESTORE, 0);

  ShowWindow(jws_hwnd, SW_HIDE);
  // [ Tab activation code ] ...

  EndDialog(jws_hwnd, 0);
  return TRUE;
}

浏览器集成

到目前为止,您应该对这个简单的应用程序在简单的顶层窗口切换方面有了很好的了解。JWinSearch之所以能从类似应用程序中脱颖而出,是因为它能够将浏览器标签视为应用程序。

对于支持的两种浏览器,都需要执行两项任务:

  • 枚举标签
  • 激活标签

我很快发现,Firefox几乎没有外部API,所以我不得不编写一个扩展。对于这个项目,这意味着同时使用JavaScript和原生代码。

在IE方面,我很快发现它没有标签感知的外部API。而且,屏幕顶部的标签是一个单一的控件,它们共享一个单一的 HWND。多亏了Dan MorrisMSDN论坛上的一个帖子,枚举IE的标签很容易,但激活它们却不行。在那里,我执行了一个糟糕的hack,当您使用大量标签时会很容易注意到:我模拟了浏览器窗口中的CTRL+TAB按键。

当我描述如何枚举窗口时,我介绍了 jws_enum_windows_cb() 回调。在将窗口插入 JWS_WINDOW_DATA 向量后,我获取窗口类,如果类名与IE或Firefox匹配,我将为每个浏览器调用一个专用函数。

static BOOL CALLBACK jws_enum_windows_cb(HWND hwnd, LPARAM )
{
  // ...

  wchar_t className[512];
  if(GetClassName(hwnd, className, _countof(className)))
  {
    if(!wcscmp(className, L"MozillaUIWindowClass"))
      jws_enum_ff_tabs(title, hwnd, hicon);
    else if(!(wcscmp(className, L"IEFrame")))
      jws_enum_ie_tabs(title, hwnd, hicon);
  }
  
  return TRUE;
}

枚举IE标签

正如Dan Morris先生指出的那样,枚举IE标签可以通过一点COM代码轻松完成。

  1. 创建一个 ShellWindows 对象。每个窗口要么是浏览器标签,要么是Windows Explorer窗口。
  2. 枚举其所有窗口,并将它们转换为 IWebBrowser2 接口。
  3. 通过调用 IWebBrowser2::get_HWND() 获取主浏览器的窗口句柄。
  4. 与枚举的主浏览器窗口进行比较。

这个算法是下面 jws_enum_ie_tabs 的主要部分。另外两个部分是:

  • 获取一个窗口句柄,用于确定标签是否处于活动状态。这个句柄和浏览器对象被保存在窗口(任务)列表中。
  • 获取标签的名称和URI。这些是直接调用 IWebBrowser2,如下所示。
static void jws_enum_ie_tabs(const std::wstring &parent_name, 
                             HWND hwnd, HICON hicon)
{
  if(!jws_shellwindows && 
      S_OK != CoCreateInstance(CLSID_ShellWindows, 0, 
              CLSCTX_INPROC_SERVER, IID_IShellWindows, 
              (void **)&jws_shellwindows))
    return;
  VARIANT v;
  V_VT(&v) = VT_I4;
  IDispatchPtr disp(0);
  HRESULT hr;
  for(V_I4(&v) = 0; S_OK == 
     (hr = jws_shellwindows->Item(v, &disp)); V_I4(&v)++)
  {
    // Cast to IWebBrowser2

    IWebBrowser2Ptr browser(0);
    if(S_OK != disp->QueryInterface(IID_IWebBrowser2, (void **)&browser))
      continue;
    // Verify if it is inside the desired window

    HWND browser_hwnd;
    if(S_OK != (browser->get_HWND((SHANDLE_PTR *)&browser_hwnd) ) || 
                browser_hwnd != hwnd)
      continue;
    
    // OK, we got a TAB from this browser


    // Get the tab's HWND, it will be used
    // to determine if the table is active

    IServiceProviderPtr servProv(0);
    IOleWindowPtr oleWnd(0);
    HWND tab_hwnd;
    _bstr_t title, uri;
    BSTR title_b, uri_b;
    if(S_OK != browser->QueryInterface(IID_IServiceProvider, (void **)&servProv))
      continue;
    if(S_OK != servProv->QueryService(SID_SShellBrowser, 
               IID_IOleWindow, (void **)&oleWnd))
      continue;
    if(S_OK != oleWnd->GetWindow(&tab_hwnd))
      continue;

    // Add to the window list

    if(S_OK != browser->get_LocationName(&title_b))
      title = _bstr_t(L"");
    else
      title.Attach(title_b);
    if(S_OK != browser->get_LocationURL(&uri_b))
      uri = _bstr_t(L"");
    else
      uri.Attach(uri_b);

    jws_windows.push_back(JWS_WINDOW_DATA(parent_name, hwnd, 
                     tab_hwnd, hicon, title, uri, browser));
  }
}

当解释上面的 JWS_WINDOW_DATA 时,我为了简洁而省略了与标签相关的字段和方法。现在,是时候回去介绍这些了。您会发现 ie::browser 成员字段没有被使用。它在那里,希望有人能找到一种巧妙的方法来切换活动IE标签。

我需要编写一个显式的复制构造函数和赋值运算符,因为这个COM指针。由于指针位于联合体中,所以我无法使用 _com_ptr_t,而它本可以大大简化 JWS_WINDOW_DATA 的代码。

struct JWS_WINDOW_DATA
{
  JWS_WINDOW_DATA(const JWS_WINDOW_DATA &other)
  {
    *this = other;
  }

  // IE tabs

  JWS_WINDOW_DATA(const std::wstring &_parent_name, HWND ie_hwnd, 
                  HWND ie_tab_hwnd, HICON ie_hicon, const wchar_t *title, 
                  const wchar_t *uri, IWebBrowser2 *browser)
    : hwnd(ie_hwnd), icon(ie_hicon), type(JWS_WT_IE), parent_name(_parent_name)
  {
    if(!*title)
      name = uri;
    else
    {
      name = title;
      name += L" -- ";
      name += uri;
    }
    ie.tab_window = ie_tab_hwnd;
    ie.browser = browser;
    browser->AddRef();
  }
  // ...

    ~JWS_WINDOW_DATA()
  {
    if(type == JWS_WT_IE && ie.browser)
    {
      ie.browser->Release();
      ie.browser = 0;
    }
  }

  // ...

  
  void operator = (const JWS_WINDOW_DATA &other)
  {
    name = other.name;
    hwnd = other.hwnd;
    icon = other.icon;
    type = other.type;
    parent_name = other.parent_name;
    if(type == JWS_WT_IE)
    {
      ie.tab_window = other.ie.tab_window;
      ie.browser = other.ie.browser;
      if(ie.browser)
        ie.browser->AddRef();
    }
    else if(type == JWS_WT_FIREFOX)
    {
      firefox.hid_window = other.firefox.hid_window;
      firefox.tab_index = other.firefox.tab_index;
    }
  }

  // ...

    
  union 
  {
    struct
    {
      HWND hid_window;
      int tab_index;
    } firefox;
    struct
    {
      HWND tab_window;
      IWebBrowser2 *browser;
    } ie;
  } ;
}

激活IE的标签

**下面的hack很糟糕,请谨慎使用**

由于我找不到其他方法来切换IE的活动标签,所以我尝试了暴力破解,模拟了CTRL+TAB按键。至少可以说,这是非常繁琐的:它依赖于时序和其他非确定性因素。这就是为什么下面的代码充满了等待条件、超时和(我讨厌)对 Sleep() 的调用。

算法很简单:它由发出CTRL+TAB按键直到目标标签成为窗口中的活动标签组成。代码是不言自明的。

static INT_PTR on_task_selected(int item_idx)
{
  // ...

  if(IsIconic(w->hwnd))
    SendMessage(w->hwnd, WM_SYSCOMMAND, SC_RESTORE, 0);

  ShowWindow(jws_hwnd, SW_HIDE);

  if(w->type == JWS_WT_FIREFOX)
    PostMessage(w->firefox.hid_window, JFFE_WM_ACTIVATE, 
                w->firefox.tab_index, (LPARAM)w->hwnd);
  else if(w->type == JWS_WT_IE)
    select_ie_tab(w);

  EndDialog(jws_hwnd, 0);
  return TRUE;
}

// Horrible way of selecting IE tab. Couldn't find anything better.

static void select_ie_tab(JWS_WINDOW_DATA *w)
{
  int n_tab_tries, n_tries = JWS_MAX_IE_RAISE_TIME;

  // Wait until IE Window raises

  while(GetForegroundWindow() != w->hwnd && n_tries-- > 0)
    Sleep(1);
  if(n_tries <= 0) // give up

    return;

  // Send CTRL+TAB until the correct tab is active

  INPUT ctrl_tab[4];

  ctrl_tab[0].type = INPUT_KEYBOARD;
  ctrl_tab[0].ki.wVk = VK_CONTROL;
  ctrl_tab[0].ki.wScan = 0x1d;
  ctrl_tab[0].ki.dwFlags = 0;
  ctrl_tab[0].ki.dwExtraInfo = 0;
  ctrl_tab[0].ki.time = 0;

  ctrl_tab[1] = ctrl_tab[0];
  ctrl_tab[1].ki.wVk = VK_TAB;
  ctrl_tab[1].ki.wScan = 0x0f;

  ctrl_tab[2] = ctrl_tab[1];
  ctrl_tab[2].ki.dwFlags |= KEYEVENTF_KEYUP;

  ctrl_tab[3] = ctrl_tab[0];
  ctrl_tab[3].ki.dwFlags |= KEYEVENTF_KEYUP;

  n_tab_tries = 200;
  HWND cur_tab_hwnd = FindWindowEx(w->hwnd, 0, L"TabWindowClass", 0);
  HWND new_tab_hwnd;
  UINT n_keys_sent;
  while(!IsWindowEnabled(w->ie.tab_window) && n_tab_tries-- > 0)
  {
    // If IE goes ou of focus, don't send CTRL+TAB to anyone

    if(GetForegroundWindow() != w->hwnd)
      return;
    // Send the 4 keys

    for(n_keys_sent = 0; 
        n_keys_sent < 4; 
        n_keys_sent += SendInput(4 - n_keys_sent, ctrl_tab + 
                       n_keys_sent, sizeof(INPUT)), Sleep(1));
    // Give IE a chance

    Sleep(JWS_MIN_IE_TAB_SWITCH_TIME);
    // Wait for the TAB to change

    for(n_tries = JWS_MAX_IE_TAB_SWITCH_TIME / 5; 
        cur_tab_hwnd == (new_tab_hwnd = FindWindowEx(w->hwnd, 0, 
                L"TabWindowClass", 0)) && n_tries > 0; 
        n_tries--)
      Sleep(5);
    if(n_tries > 0) // OK, tab switched

      cur_tab_hwnd = new_tab_hwnd;
  }
}

Firefox扩展

由于Firefox几乎没有像IE那样的外部接口,与它的标签交互的唯一有用的方法是编写一个扩展。这比IE的解决方案要复杂一些,但结果要好得多,没有任何丑陋的hack。

二进制组件

除了与JWinSearch通信之外,我编写的Firefox扩展只有两个简单的功能:

  • 枚举标签,和
  • 激活一个新标签。

JWinSearch通信可以通过多种方式完成,包括使用COM,但最简单的方法是使用扩展内的隐藏窗口并交换消息。为此,我需要编写一个原生的XPCOM组件。在Mozilla开发者中心XPCOM页面上有很多关于这个的教程和信息。

二进制组件只是一个通信模块,它:

  • 创建一个隐藏窗口,用于与JWinSearch通信。
  • 使用nsIObserverService通知扩展的JavaScript部分关于传入的标签枚举和激活请求。
  • 使用 WM_COPYDATA 消息将标签列表传回JWinSearch

首先,二进制组件 JFFEnumerator 必须找到它被创建的主窗口。由于XUL + JavaScript扩展端保证为每个Firefox浏览器窗口创建一个新实例(我将在下面解释),我使用一个全局的 std::map<hwnd,jffenumerator />。这之所以可行,是因为Firefox是一个多窗口但单进程的浏览器。如果您以某种方式使用多个Firefox进程,扩展将无法正常工作。不过,这种情况很少见。

Mozilla开发者连接上提供的代码在打开多个Firefox窗口时不起作用。您不妨自己看一下,您很容易就会发现原因。

我的解决方案看起来是这样的:

typedef std::map<hwnd,jffenumerator> EnumMap;
typedef EnumMap::value_type EnumMapPair;

static EnumMap jffe_map;
JFFEnumerator::JFFEnumerator()
: m_hwnd(0),
  m_h_hwnd(0)
{
  EnumThreadWindows(GetCurrentThreadId(), 
        &JFFEnumerator::enum_win_cb, (LPARAM)this);

  if(!m_hwnd)
    return;

  // ...

}

BOOL CALLBACK JFFEnumerator::enum_win_cb(HWND hwnd, LPARAM lp_this)
{
  wchar_t classname[512];
  if(!GetClassName(hwnd, classname, _countof(classname)))
    return TRUE;
  if(wcscmp(classname, L"MozillaUIWindowClass"))
    return TRUE;
  if(jffe_map.find(hwnd) != jffe_map.end())
    return TRUE;
  JFFEnumerator *jffe = (JFFEnumerator *)lp_this;
  jffe->m_hwnd = hwnd;
  sprintf_s(jffe->m_select_topic, "S:%x", hwnd);
  sprintf_s(jffe->m_enumerate_topic, "E:%x", hwnd) ;
  jffe_map.insert(EnumMapPair(hwnd, jffe));

  return FALSE;
}
</hwnd,jffenumerator>

上面的两个主题是通过 nsiObserverService 实例发出的“信号”。第一个表示“枚举”,第二个表示“选择标签”。

当隐藏窗口接收到消息时,它只会通知JavaScript代码。请记住,这些通知是同步的,即 nsiObserverService::observe() 方法在JavaScript函数返回之前不会返回。

隐藏窗口的 WindowProc 处理这些消息:

  • JFFE_WM_ENUMERATE:请求扩展枚举。这是由扩展的JavaScript部分完成的,它为遇到的每个标签调用 JFFEnumerator::ReportTab(),然后返回。在从窗口过程中返回之前,它会向自身发送一个 JFFE_WM_REPLY_ENUMERATE 消息,以避免死锁。
  • JFFE_WM_REPLY_ENUMERATE:内部使用以避免死锁。此消息将一个带有标签列表的 WM_COPYDATA 消息发送回JWinSearch。
  • JFFE_WM_ACTIVATE:请求扩展激活一个标签。这是由扩展的JavaScript部分完成的。
LRESULT CALLBACK JFFEnumerator::hidden_win_proc(HWND hwnd, UINT msg, 
                                                WPARAM wparam, LPARAM lparam)
{
  switch(msg)
  {
  default:
    return DefWindowProc(hwnd, msg, wparam, lparam);
  case JFFE_WM_ENUMERATE:
    {
      EnumMap::iterator it = jffe_map.find((HWND)lparam);
      if(it == jffe_map.end())
        return 1;
      JFFEnumerator *jffe = it->second;
      
      jffe->m_tab_list.clear();
      nsresult rv = jffe->notify(jffe->m_enumerate_topic, L"");
      // Must defer processing with PostMessage because 

      // JWinSearch is waiting for message reply inside SendMessage()

      PostMessage(jffe->m_h_hwnd, JFFE_WM_REPLY_ENUMERATE, 
                  wparam, (LPARAM)jffe);
    }
    return 0;
  case JFFE_WM_REPLY_ENUMERATE:
    {
      JFFEnumerator *jffe = (JFFEnumerator *)lparam;
      COPYDATASTRUCT copy;

      copy.dwData = jffe->m_tab_list.size();
      copy.cbData = (DWORD)(copy.dwData * sizeof(JFFE_TabInfo));
      copy.lpData = &(jffe->m_tab_list[0]);
      
      SendMessage((HWND)wparam, WM_COPYDATA, 
                 (WPARAM)jffe->m_h_hwnd, (LPARAM)©);

      jffe->m_tab_list.clear();
    }
    return 0;
  case JFFE_WM_ACTIVATE:
    {
      EnumMap::iterator it = jffe_map.find((HWND)lparam);
      if(it == jffe_map.end())
        return 1;
      JFFEnumerator *jffe = it->second;

      wchar_t extra_data[20];
      swprintf_s(extra_data, L"%x", (int)wparam);
      jffe->notify(jffe->m_select_topic, extra_data);
    }
    return 0;
  }
}

nsresult JFFEnumerator::notify(const char *topic, 
         const PRUnichar *data)
{
  nsCOMPtr<nsiservicemanager> servMan;
  nsresult rv = NS_GetServiceManager(getter_AddRefs(servMan));
  if (NS_FAILED(rv))
    return rv; 

  nsCOMPtr<nsiobserverservice> observerService;
  rv = servMan->GetServiceByContractID("@mozilla.org/observer-service;1", 
                      NS_GET_IID(nsIObserverService), 
                      getter_AddRefs(observerService));
  if (NS_FAILED(rv))
    return rv;

  rv = observerService->NotifyObservers(this, topic, data);
  
  return rv;
}
</nsiobserverservice></nsiservicemanager>

JavaScript / XUL 组件

JavaScript / XUL 组件出奇地类似于DHTML + JavaScript页面。基本上,XUL部分就像一个HTML页面,而JavaScript类是一个DOM助手。在XUL中,我描述了一个覆盖层,它很像一个被挂钩的页面片段。这个覆盖层将为每个浏览器窗口创建一个我的JavaScript类的实例。

<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>

<overlay xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" 
         id="JWinSearchFFTabEnumeratorOverlay">
  <script id="JWSFF_SRC"  src="jwsff.js"/>
  <script id="JWSFF_START" >
   <![CDATA[var jfftabenumerator = new JFFTabEnumeratorJS;]]>
  </script>
</overlay>

JavaScript组件只是一个事件处理程序,用于处理来自上述二进制组件的通知以及窗口卸载事件。为了枚举标签,我使用了Mozilla开发者中心的示例代码。为了设置活动标签,我只需设置 tabContainer DOM对象的相应属性。

所有这些都可以用C++代码完成,但由于缺乏接口展平,它会比使用脚本语言(如JS)要困难得多。

function JFFTabEnumeratorJS()
{
  this.start();
}

JFFTabEnumeratorJS.prototype = 
{
  ffTabEnumerator:null,
  
  start: function() 
  {
    try 
    {
      const cid = "@jorge.vasquez/fftabenumerator;1";
      var cobj = Components.classes[cid].createInstance();
      ffTabEnumerator = 
        cobj.QueryInterface(Components.interfaces.IJFFEnumerator);

      var sel_topic = ffTabEnumerator.getSelectTopic();
      var enum_topic = ffTabEnumerator.getEnumerateTopic();
     
      var observerService = Components.classes["@mozilla.org" + 
          "/observer-service;1"].getService(
          Components.interfaces.nsIObserverService);
      observerService.addObserver(this, sel_topic, false);
      observerService.addObserver(this, enum_topic, false);
      
      window.addEventListener('unload', 
         function() { this.unload(event); }, false );
    }
    catch (err) 
    {
      alert(err);
      return;
    }
  },
  
  unload: function(event)
  {
      var observerService = Components.classes["@mozilla.org/" + 
          "observer-service;1"].getService(
          Components.interfaces.nsIObserverService);
      observerService.removeObserver(this, sel_topic);
      observerService.removeObserver(this, enum_topic);
      window.removeEventListener('unload', 
          function() { this.unload(event); }, false );
      ffTabEnumerator = null;
  },
  
  observe: function(subject, topic, data) 
  {
    if(topic == ffTabEnumerator.getEnumerateTopic())
    {
      var n_tabs = getBrowser().browsers.length;
      
      for (var i = 0; i < n_tabs; i++) 
      {
        var b = gBrowser.getBrowserAtIndex(i);
        ffTabEnumerator.reportTab(i, b.contentDocument.title, 
                                  b.currentURI.spec);
      }
    }
    else if(topic == ffTabEnumerator.getSelectTopic())
    {
      var tab_index = parseInt(data, 16);    
      if(isNaN(tab_index))
        return;
      gBrowser.mTabContainer.selectedIndex = tab_index;
    }
  }
}

要安装该扩展,只需在HKLU\Software\Mozilla\Firefox\Extension中创建一个注册表项。为了最终化扩展,我还需要创建目录结构和RDF清单。请参阅这篇文章以获取有关此主题的详细说明。

与Firefox扩展通信

jws_enum_windows_cb() 函数中,对于类名为 "MozillaUIWindowClass" 的窗口,将调用 jws_enum_ff_tabs。由于所有实际工作都在扩展内部完成,因此此函数比IE标签枚举所需的函数要简单得多。它只会:

  1. 查找浏览器隐藏窗口,该窗口的名称中包含浏览器主窗口。这是一个很好的方法,可以避免创建不是 HWND_MESSAGE 子窗口的隐藏窗口。
  2. 使用同步的 SendMessage() API向其发送消息。
  3. 运行一个事件循环来等待 WM_COPYDATA 消息,该消息本身由主对话框的对话框过程处理。这个事件循环与普通的基于 GetMessage() 的循环略有不同,它只运行预定的时间,并阻塞在 MsgWaitForMultipleObjects() 上。
static void jws_enum_ff_tabs(std::wstring parent_name, HWND hwnd, HICON hicon)
{
  wchar_t winName[20];
  HWND hid_hwnd;

  // Tell extension to enumerate its tab and report back to us

  swprintf_s(winName, L"W:%x", hwnd);
  hid_hwnd = FindWindow(jffe_hid_win_classname, winName);
  if(!hid_hwnd)
    return;
  LRESULT r = SendMessage(hid_hwnd, JFFE_WM_ENUMERATE, 
                         (WPARAM)jws_hwnd, (LPARAM)hwnd);
  if(r)
    return;

  // Process all message for up
  // to JWS_FF_TIMEOUT or an answer is received

  jws_ff_answered = FALSE;
  jws_ff_icon = hicon;
  jws_ff_hwnd = hwnd;
  jws_ff_parent_name = parent_name;

  DWORD start;
  start = GetTickCount();
  do
  {
    MsgWaitForMultipleObjects(0, 0, 0, JWS_FF_TIMEOUT - 
      (GetTickCount() - start), QS_ALLINPUT | QS_ALLPOSTMESSAGE);
    MSG msg;
    while(PeekMessage(&msg, 0, 0, 0, PM_REMOVE))
    {
      if(msg.message == WM_QUIT)
      {
        PostMessage(jws_hw_hwnd, msg.message, msg.wParam, msg.lParam);
        return;
      }
      if(IsDialogMessage(jws_hwnd, &msg))
        continue;
      TranslateMessage(&msg);
      DispatchMessage(&msg);
    }
  } while(!jws_ff_answered && GetTickCount() - start < JWS_FF_TIMEOUT );
}

当收到 WM_COPYDATA 消息时,将调用 on_ff_answer() 来执行以下步骤:

  1. 验证来自当前进程外部的接收数据。
  2. 将每个标签添加到任务列表中,该列表保存其隐藏窗口句柄和标签索引。
static LRESULT on_ff_answer(HWND sender_hwnd, COPYDATASTRUCT *reply)
{
  JFFE_TabInfo *i, *end;

  // validate reply->dwData

  if(reply->dwData * sizeof(JFFE_TabInfo) != reply->cbData)
    return TRUE;

  for(i = (JFFE_TabInfo *)reply->lpData, 
      end = i + reply->dwData; i < end; i++)
  {
    i->tab_name[JFFE_TAB_NAME_LEN - 1] = 0;
    i->tab_uri[JFFE_TAB_URI_LEN - 1] = 0;
    jws_windows.push_back(JWS_WINDOW_DATA(jws_ff_parent_name, 
                jws_ff_hwnd, sender_hwnd, jws_ff_icon, i));
  }

  jws_ff_answered = TRUE;

  return TRUE;
}

在选择任务时,会向Firefox扩展二进制组件发送一个简单的消息来激活正确的标签。同样,所有工作都在扩展内部完成;我只需要通知它。

static INT_PTR on_task_selected(int item_idx)
{
  JWS_WINDOW_DATA *w;
  // ...

  
  if(w->type == JWS_WT_FIREFOX)
    PostMessage(w->firefox.hid_window, JFFE_WM_ACTIVATE, 
                w->firefox.tab_index, (LPARAM)w->hwnd);
    
  // ...

}

历史记录

  • 2007年10月1日 - 版本1。
© . All rights reserved.