[译]JWinSearch:基于搜索的任务和选项卡切换
By robot-v1.0
本文链接 https://www.kyfws.com/applications/jwinsearch-search-based-task-and-tab-switching-zh/
版权声明 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
- 20 分钟阅读 - 9841 个词 阅读量 0[译]JWinSearch:基于搜索的任务和选项卡切换
原文地址:https://www.codeproject.com/Articles/20701/JWinSearch-search-based-task-and-tab-switching
原文作者:Jorge Vasquez
译文由本站 robot-v1.0 翻译
前言
An article describing the various steps and technologies needed to write JWinSearch.
一篇文章,描述编写JWinSearch所需的各种步骤和技术.
- 下载带有源代码的演示项目(VS2005)-41.4 KB(Download demo project with source code (VS2005) - 41.4 KB)
- 下载压缩的MSI安装程序-1.42 MB(Download zipped MSI installer - 1.42 MB) –(-)(由于嵌入式VS运行时DLL而较大)((large due to embedded VS runtime DLLs)) 您还需要从以下位置下载Gecko-SDK:(You’ll also need to download the Gecko-SDK from the) 官方网站(official page) 为了构建Firefox扩展.(in order to build the Firefox extension.)
用法(Usage)
按Windows + W,在应用程序名称,选项卡名称或选项卡URL中键入几个字母,然后按Enter切换到所需的窗口/页面.(Press Windows + W, type a few letters from the application name, tab name, or tab URL, and press Enter to switch to the window / page you wish.)
介绍(Introduction)
受启发(Inspired by the) 博客条目(blog entry) 上(on) 编码恐怖(Coding Horror) ,我决定编写这个具有一些简单目标的实用程序:(, I decided to write this little utility with some simple goals:)
- 按标题搜索窗口(Search windows by title)
- 在Firefox和IE上按标题或URL搜索浏览器选项卡(Search browser tabs by title or URL on Firefox and IE)
- 使用简单的按键激活浏览器选项卡或窗口(Activate either a browser tab or a window using simple keystrokes)
- 保留很少的资源使用情况配置文件(Keep a very little resource usage profile) 最初是从一个简单的项目开始的,但很快就变成了一项耗时的任务,我需要在空闲时间做一些事情.这主要是由于我对两种浏览器的API都不熟悉,尤其是Firefox的API.(What started as a simple project rapidly spawned into a time consuming task for something I was to do on my free time. This was mostly due to my lack of familiarity with both browsers' APIs, especially Firefox’s.)
背景(Background)
在本文中,我将介绍以下基础知识:(In this article, I’ll cover the basics of:)
- 热键和控件子类化(Hotkeys and control subclassing)
- Win32窗口消息传递,枚举和控制(Win32 window messaging, enumerating, and control)
- 任务切换(Task switching)
- IE标签枚举和激活(IE tab enumerating and activation)
- Firefox扩展(Firefox extensions)
目录(Table of contents)
隐藏的窗口和热键(Hidden windows and hotkeys)
为了简化操作,该应用程序仅使用一个隐藏窗口在后台静默运行,以接收热键和控制消息. WIN-W热键已通过以下方式注册:(In order to keep things simple, the application silently runs on the background using just a hidden window to receive hotkey and control messages. The WIN-W hotkey is registered with a call like:)
#define IDH_ALT_WIN 55 // Could be any number
RegisterHotKey(jws_hw_hwnd, IDH_ALT_WIN, MOD_WIN, LOBYTE(VkKeyScan(L'w'))
隐藏的窗口收到热键消息后,将打开或关闭非常简单的对话框:(Once the hidden window receives the hotkey message, it will either open or close the very simple dialog:)
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);
}
您肯定会注意到,在显示主对话框之前,我保存了前一个前景窗口.这样做是为了让我以后可以还原它,但前提是在不选择新任务的情况下关闭对话框的情况下.(You will certainly notice that, before displaying the main dialog, I save the previous foreground window. This is done so that I can restore it later, but only if the dialog is dismissed without selecting a new task.)
在主对话框启动期间,将枚举窗口,并子类化两个主窗口控件,以捕获所有键盘命令,以便应用程序按预期执行:(During the main dialog startup, the windows are enumerated, and the two main window controls are subclassed in order to trap all keyboard commands so that the application performs as expected:)
- 键入过滤窗口列表(Typing filters the window list)
- Enter键激活列表中的第一个应用程序(Enter key activates the first application on the list)
- ESC键随时关闭对话框(ESC key closes the dialog anytime)
- TAB从编辑转到列表(TAB goes from the edit to the list) 在Windows XP和更高版本的Win32 OS上,控件子类化非常容易:(Control subclassing is quite easy on Windows XP and later Win32 OSs:)
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);
}
// ...
}
枚举窗口(Enumerating windows)
枚举顶级窗口应该非常简单:应该只调用一个(Enumerating top-level windows is supposed to be very simple: one should just call) EnumWindows()
,表示通过传递枚举顶级窗口的意图(, indicating the intent to enumerate top-level windows by passing) NULL
作为父窗口.(as the parent window.)
棘手的部分在于从窗口(图标和标题)收集信息,并排除不感兴趣的窗口.(The tricky part lies on gathering information from the window (icon and title) and on excluding windows which are not of interest.)
在(On the) EnumWindows()
回调,我:(callback, I:)
- 排除不可见的窗口((Exclude non-visible windows ()
WS_VISIBLE
)()) - 获取标题((Get the title ()
GetWindowText()
)()) - 排除标题为空的窗口(Exclude windows with an empty title)
- 使用工具栏扩展样式排除窗口((Exclude windows with the toolbar extended style ()
WS_EX_TOOLWINDOW
)()) - 使用获取图标(Get the icon using the)
WM_GETICON
信息(message) - 如果失败,请使用以下命令获取窗口类图标(If that fails, get the window class icon with)
GetClassLongPtr()
- 如果失败,请加载默认图标(If that fails too, load a default icon) 枚举窗口后,我只对检索到的窗口列表进行排序,以便始终按窗口名称对显示进行排序.(After enumerating the windows, I just sort the retrieved window list so that displays will always be ordered by window name.)
下面的代码是大致的实现方式:(The code below is approximately the way to do it:)
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;
}
上面的代码片段中最后一个有用的行将一个窗口添加到窗口列表全局变量中.对话框关闭后,将清除此全局窗口列表.一旦文本在行编辑中发生更改,它便可以快速重建列表视图的内容,从而仅显示与用户搜索查询匹配的窗口.(The last useful line on the code snippet above adds a window to the window list global variable. This global window list is cleared once the dialog is closed. It is used to rapidly reconstruct the list view’s content once the text changes inside the line edit, thereby showing just the windows matching the user’s search query.)
类型(The type) JWS_WINDOW_DATA
只是存储窗口显示数据(图标和标题),窗口句柄以及其他与选项卡有关的信息的结构:(is just a struct which stores window display data (icon and title), window handle, and other information concerning tabs:)
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();
}
};
的(The) =
与IE相关的数据需要上述运算符和上面的复制构造函数,我将在本文稍后解释.该结构具有比较运算符,因此(operator and the copy constructor above are needed for IE-related data, which I’ll explain later on this article. The struct has a comparison operator so that the) std::vector
如上所示,可以排序.在(can be sorted, as seen above. On the) comp_name()
功能,我在标签上添加了父名称,以便它们始终显示在浏览器主窗口之后.(function, I prepend the parent name on tabs so that they show, always, after the browser main window.)
填充列表视图(Populating the list view)
首先,我需要在对话框初始化时设置列表视图.此步骤包括创建一列,创建并填充图像列表,并将其分配给列表视图.请注意以下代码上的一些与大小相关的宏.它们允许在编译时轻松选择图标大小;只是定义(First of all, I needed to setup the list view upon dialog initialization. This step consists of creating a column, creating and populating an image list, and assigning it to the list view. Note some size-related macros on the code below. They allow for compile-time easy icon size selection; just define) JWS_BIG_ICONS
使用32x32图标,而不是我使用的默认16x16图标.(to use 32x32 icons instead of the default 16x16 ones I used.)
初始化是在(This initialization is done on the) WM_INIT
处理程序:(handler:)
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;
}
初始化时,每次用户在编辑面板上键入内容时,(On initialization and each time the user types something on the edit panel, the) jws_display_window_list()
调用函数,以便它可以过滤哪些窗口和选项卡匹配用户搜索条件.列表视图已清除并重新填充此功能.(function is called so that it can filter which windows and tabs match the user search criteria. The list view is cleared and refilled in this function.)
注意缩进浏览器标签是多么容易.记住,我对窗口进行了排序,以确保选项卡位于浏览器主窗口之后.(Note how easy it is to indent the browser tabs. Remember, I sorted the windows in a way that guarantees the tabs are positioned right after the browser main window.)
尽管我设置了一个列并隐藏了它的列,但它仍然存在.如果我不打电话(Although I set one column up and hide its columns, it is still there. If I don’t call) ListView_SetColumnWidth(LVSCW_AUTOSIZE)
,事情真的被截断了.(, things get really truncated.)
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);
}
请注意(Please note that the) LVITEM::lParam
字段用于保存对项目数据的引用.这意味着,您无法在填充列表视图之后对矢量进行排序或添加项目.(field is used to hold a reference to the item’s data. This means, you can’t sort or add items to the vector after filling the list view.)
激活另一个任务(Activating another task)
激活项目后,我必须将窗口置于最前面.根据当前的窗口状态,这可能非常棘手.自从XP启动以来,就具有此功能,可以避免其他窗口将焦点从任何事物移开.为了规避它,我必须通过发出一个调用来附加到目标线程的输入队列.(Upon item activation, I have to bring the window to front. This can be very tricky, depending on the current window state. Since XP was launched, there is this feature which avoids other windows from taking focus away from anything. To circumvent it, I had to attach to the destination thread’s input queue, by issuing a call to) AttachThreadInput()
.(.)
最后,如果所选窗口已图标化,则需要将其恢复为正常大小.这很简单(Last, if the selected window is iconified, one needs to restore it to the normal size. This is done with a simple) WM_SYSCOMMAND(SC_RESTORE)
信息.(message.)
为了激活一个窗口,我这样做:(In order to activate a window, I do:)
BringWindowToTop()
SetWindowPos(HWND_TOP)
SetForegroundWindow()
- 如果需要的话,(If needed,)
AttachThreadInput()
和(and)SetForegroundWindow()
再次(again) - 如果是标志性的,(If iconic,)
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;
}
浏览器整合(Browser integration)
到现在为止,您应该对这个简单的实用程序有一个很好的想法,它涉及到简单的顶层窗口切换.使JWinSearch在同类应用程序中脱颖而出的部分是其将浏览器选项卡视为应用程序的能力.(By now, you should have a pretty good idea of this simple utility regarding simple top-level window switching. The piece which makes JWinSearch stand out from similar applications is its ability to consider browser tabs as applications.)
对于两个受支持的浏览器,必须执行两个任务:(There are two tasks which have to be performed for each of the two supported browsers:)
- 枚举标签(Enumerate tabs)
- 激活标签(Activate a tab) 我很快发现,Firefox几乎没有外部API,因此我不得不编写一个扩展.对于本项目,这意味着同时使用JavaScript和本机代码.(I soon discovered, Firefox has almost no external API, so I had to write an extension. For this project, this meant using both JavaScript and native code.)
当涉及到IE时,我很快发现没有可识别制表符的外部API.此外,屏幕顶部的标签是单个控件,它们共享一个(When it came to IE’s ground, I soon discovered there is no tab-aware external API. Moreover, the tabs on the top of the screen are a single control, they share a single) HWND
.多亏了(. Thanks to a) 发布(post) 通过(by) 丹`莫里斯(Dan Morris) ,枚举IE的标签非常简单,但无法激活它们.那是我执行可怕骇客的地方,使用许多选项卡时您会轻易注意到:我模拟了浏览器窗口中的CTRL + TAB键按下.(, it was quite simple to enumerate IE’s tabs, but not to activate them. That’s the place where I performed a horrible hack you’ll easily notice when using lots of tabs: I simulate the CTRL+TAB key press on the browser window.)
当我描述如何枚举窗口时,我介绍了(When I described how to enumerate windows, I presented the) jws_enum_windows_cb()
打回来.将窗口插入后(callback. After inserting the window on the) JWS_WINDOW_DATA
向量,获得窗口类,如果类名称与IE或Firefox匹配,则为每个浏览器调用一个专用函数:(vector, I get the window class and, if the class name matches IE’s or Firefox’s, I call a dedicated function for each browser:)
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标签页(Enumerating IE tabs)
正如Dan Morris先生指出的那样,只需一点COM代码即可轻松枚举IE选项卡:(As Mr. Dan Morris pointed out, enumerating IE tabs can be easily done with a little bit of COM code:)
-
创建一个(Create a)
ShellWindows
目的.每个窗口都是浏览器(object. Each window is either a browser)标签(TAB)或Windows资源管理器窗口(or a Windows Explorer window) -
枚举其所有窗口,并将其投射到(Enumerate all its windows, casting them to a)
IWebBrowser2
接口(interface) -
得到(Get the)主浏览器的(main browser’s)通过调用窗口句柄(window handle by calling)
IWebBrowser2::get_HWND()
-
与枚举的主浏览器窗口比较(Compare to the enumerated main browser window) 该算法是(This algorithm is the main part of the)
jws_enum_ie_tabs
下面.其他两个部分是:(below. The other two parts are:) -
获取用于确定选项卡是否处于活动状态的窗口句柄.该句柄和浏览器对象保存在窗口的(任务)列表中.(Getting a window handle used to determine if the tab is active. This handle and the browser object are saved inside the window’s (task) list.)
-
获取标签的名称和URI.这些都是直接致电(Getting a tab’s name and URI. Those are straightforward calls to)
IWebBrowser2
,如下所示.(, as seen below.)
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));
}
}
当解释(When explaining the) JWS_WINDOW_DATA
上面,为简单起见,我省略了与选项卡相关的字段和方法.现在,该回顾一下这些内容了.您会看到(above, I omitted the tabs-related fields and methods for the sake of simplicity. Now, it’s time to go back an introduce those. You’ll see that the) ie::browser
成员字段未使用.希望有人能找到一种巧妙的方式来切换活动的IE选项卡.(member field is not used. It is there in the hope that someone finds a clever way of switching the active IE tab.)
由于这个COM指针,我需要编写一个显式的复制构造函数和赋值运算符.由于指针位于联合体内,因此我无法使用(I needed to write an explicit copy constructor and assignment operator because of this COM pointer. Since the pointer is inside a union, I couldn’t use) _com_ptr_t
,这将简化(, which would have simplified the) JWS_WINDOW_DATA
编码很多.(code a lot.)
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的选项卡(Activating IE’s tabs)
以下可怕的骇客,请谨慎操作(*** Horrible hack below, proceed with caution ***)
由于找不到其他方法来切换IE的活动选项卡,因此我采用了CTRL + TAB按键的方式对其进行了暴力破解.至少可以说这很麻烦:它取决于时间安排和其他不确定性因素.这就是下面的代码充满等待条件,超时和(argh)调用的原因(Since I couldn’t find any other way of switching IE’s active tab, I brute-forced it, emulating the CTRL+TAB key press. This is very cumbersome to say the least: it depends on timings and other non-deterministic factors. That’s the reason the code below is full of wait-conditions, timeouts, and (argh) calls to) Sleep()
.(.)
不过,该算法非常简单:它包括发出CTRL + TAB组合键,直到所需的选项卡成为窗口上的活动选项卡.该代码是不言自明的:(The algorithm is pretty simple, though: it consists of emitting CTRL+TAB until the desired tab becomes the active tab on the window. The code is self-explanatory:)
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 extension)
由于Firefox几乎没有像IE这样的外部界面,因此与其选项卡进行交互的唯一有用的方法是编写扩展程序.它比IE的解决方案要复杂一些,但是结果要好得多,没有任何丑陋的破解方法.(Since Firefox has almost no external interface like IE, the only useful approach to interacting with its tabs was writing an extension. It was a little bit more complicated than IE’s solution, but the result was much better, without any kind of ugly hack.)
二元成分(Binary component)
除了与(Besides communicating with)JWin搜索(JWinSearch),我编写的Firefox扩展只有两个简单的功能:(, the Firefox extension I wrote has just two simple functions:)
- 枚举标签,以及(enumerating tabs, and)
- 激活一个新标签.(activating a new tab.) 与沟通(Communicating with)**JWin搜索(JWinSearch)**可以通过多种方式来完成,包括使用COM,但是最简单的方法是使用扩展内的隐藏窗口并交换消息.为此,我需要编写一个本机XPCOM组件.关于这一点,有很多教程和信息.(could be done in a number of ways, including using COM, but the simplest one is by using a hidden window inside the extension and exchange messages. To do so, I needed to write a native XPCOM component. There are lots of tutorials and information regarding this on the) Mozilla开发人员中心XPCOM页面(Mozilla Developer Center XPCOM page) .(.)
二进制组件只是一个通信模块,它:(The binary component is just a communication module that:)
- 创建一个隐藏窗口以与JWinSearch进行通信.(Creates a hidden window to communicate with JWinSearch.)
- 使用(Uses the)
nsIObserverService
通知JavaScript部分传入标签的枚举和激活请求的扩展.(to notify the JavaScript part of the extension of incoming tab enumeration and activation requests.) - 用途(Uses)
WM_COPYDATA
将选项卡列表发送回的消息(messages to transmit the tab list back to)JWin搜索(JWinSearch).(.) 首先,二进制组件(First of all, the binary component)JFFEnumerator
必须找出在其中创建它的主窗口.由于XUL + JavaScript扩展端保证将为每个Firefox浏览器窗口创建一个新实例(我将在下面解释),因此我使用了全局(has to find out the main window inside which it has been created. Since the XUL + JavaScript extension side guarantees a new instance will be created for every Firefox browser window (I’ll explain that below), I use a global)std::map<hwnd,jffenumerator />
.这是可行的,因为Firefox是多窗口但单进程的浏览器.如果您以某种方式使用多个Firefox进程,则该扩展名将无法正常工作.但是,这是一种罕见的情况.(. This works because Firefox is a multi-window but single-process browser. If you use multiple Firefox processes somehow, the extension won’t work correctly. This is a rare situation, though.)
如果一个人打开多个Firefox窗口,则Mozilla开发人员连接上显示的代码将不起作用.拿一个(The code presented at the Mozilla developer connection does not work if one opens more than one Firefox window. Take a) 看它(look at it) 自己,您将轻松发现原因.(yourself, and you’ll easily discover why.)
我的解决方案如下所示:(My solution looks like this:)
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>
他们俩(The two)**主题(topics)**以上是通过(above are the “signals” emitted through the) nsiObserverService
实例.第一个表示"枚举",另一个表示"选择选项卡".(instance. The first one means “enumerate”, the other means “select tab”.)
当隐藏窗口接收到消息时,它仅通知JavaScript代码.请记住,这些通知是同步的,即(When the hidden window receives messages, it just notifies the JavaScript code. Please keep in mind, these notifications are synchronous, i.e., the) nsiObserverService::observe()
在JavaScript函数自身返回之前,方法不会返回.(method doesn’t return until the JavaScript function has returned itself.)
隐藏的窗户(The hidden window’s) WindowProc
处理以下消息:(handles these messages:)
JFFE_WM_ENUMERATE
:要求扩展名枚举.这是通过扩展的JavaScript端完成的,(: asks the extension to enumerate. This is done by the JavaScript side of the extension, which calls)JFFEnumerator::ReportTab()
对于遇到的每个标签,并返回.从窗口过程返回之前,它会发布一个(for each tab encountered, and returns. Before it returns from the window procedure, it posts a)JFFE_WM_REPLY_ENUMERATE
以避免死锁.(to itself in order to avoid dead-locks.)JFFE_WM_REPLY_ENUMERATE
:内部用于避免死锁.此消息只是发送一个(: internally used to avoid a dead-lock. This message simply sends a)WM_COPYDATA
将带有选项卡列表的消息返回到JWinSearch.(message with the tab list back to JWinSearch.)JFFE_WM_ACTIVATE
:要求扩展程序激活选项卡.这是通过扩展的JavaScript端完成的.(: asks the extension to activate a tab. This is done by the JavaScript side of the extension.)
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 component)
JavaScript/XUL组件惊人地类似于DHTML + JavaScript页面.基本上,XUL部分就像HTML页面,而JavaScript类是DOM帮助器.在(The JavaScript / XUL component is surprisingly similar to a DHTML + JavaScript page. Basically, the XUL part is like an HTML page and the JavaScript class is a DOM-helper. In) XUL(XUL) ,我描述了一个覆盖图,它很像一个钩页代码段.此覆盖层只会为每个浏览器窗口创建我的JavaScript类的实例:(, I describe an overlay, which is much like a hooked page snippet. This overlay will just create an instance of my JavaScript class for every browser window:)
<?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组件只是一个事件处理程序,用于处理来自上述二进制组件的通知以及窗口卸载事件.为了枚举标签,我使用了(The JavaScript component is just an event handler for notifications from the binary component described above, and for the window unload event. To enumerate the tabs, I used the sample code provided by the) Mozilla开发人员中心(Mozilla Developer Center) .要设置活动标签,我只需在(. To set the active tab, I just set the corresponding property on the) tabContainer
DOM对象.(DOM object.)
所有这些都可以用C ++代码完成,但是由于缺少接口扁平化,因此难度将增加几个数量级,这仅在像JS这样的脚本语言中才有意义.(All this could be done in C++ code, but it would be orders of magnitude harder because of the lack of interface flattening, which only makes sense in a script language like 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;
}
}
}
为了安装扩展,只需在以下位置创建注册表项即可(In order to install the extension, it suffices to create a registry entry on)HKLU \ Software \ Mozilla \ Firefox \ Extension(HKLU\Software\Mozilla\Firefox\Extension).为了完成扩展,我还需要创建目录结构和RDF清单.请看看(. To finalize the extension, I also needed to create the directory structure and the RDF manifest. Please take a look at) 本文(this article) 有关此主题的详细说明.(for detailed instructions on this subject.)
与Firefox扩展通讯(Communicating with the Firefox extension)
在 - 的里面(Inside the) jws_enum_windows_cb()
功能,(function, the) jws_enum_ff_tabs
将与该类一起调用Windows(will be called for windows with the class) "MozillaUIWindowClass"
.由于所有实际工作都在扩展内部完成,因此此功能比IE选项卡枚举所需的功能简单得多.它只会:(. Since all actual work is done inside the extension, this function is much simpler than the one needed for IE tab enumeration. It will just:)
- 查找浏览器隐藏窗口,该窗口的名称带有浏览器主窗口.这是避免创建不是Windows XP子级的隐藏窗口的好方法(Find the browser hidden window, which has the browser main window in its name. This is a nice way to avoid creating a hidden window which is not a child of)
HWND_MESSAGE
.(.) - 使用同步向它发送消息(Sends a message to it, using the synchronous)
SendMessage()
API.(API.) - 运行事件循环以等待(Runs an event-loop to wait for the)
WM_COPYDATA
消息,它本身由主对话框的对话框过程处理.这个事件循环与通常的有所不同(message, which is itself handled by the main dialog’s dialog procedure. This event-loop is a little bit different from the usual)GetMessage()
基于它,因为它将仅运行预定的时间,并且在(based one, in that it will only run for a predefined amount of time, and blocks on)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 );
}
当…的时候(When the) WM_COPYDATA
收到消息,(message is received, the) on_ff_answer()
将被调用以执行以下步骤:(will be called to perform the following steps:)
- 验证从当前进程外部接收到的数据.(Validate received data from outside the current process.)
- 将每个选项卡添加到任务列表,以保存其隐藏的窗口句柄和选项卡索引.(Add each tab to the task list, which saves its hidden window handle and tab index.)
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扩展二进制组件以激活正确的选项卡.同样,所有工作都在扩展内部完成;我只需要通知它.(On task selection, a simple message is sent to the Firefox extension binary component to activate the correct tab. Again, all the work is done inside the extension; I just needed to notify it.)
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);
// ...
}
历史(History)
- 2007年10月1日-版本1.(October 1, 2007 – Version 1.)
许可
本文以及所有相关的源代码和文件均已获得The Code Project Open License (CPOL)的许可。
C++ C VC8.0 Win2003 Windows WinXP Visual-Studio IE VS2005 Dev 新闻 翻译