Useful Technique: CBT Hooks
Opening VS.NET’s Option dialog programmatically isn’t hard; in fact, it’s a one-liner:
hr = _ConnectionObject->m_pDTE->ExecuteCommand(CComBSTR("Tools.Options"), CComBSTR(""));
Easy right? Right. But (and there is always a ‘but’ in this industry) how do I ensure that the options page displayed belongs to VisEmacs? The default behaviour of the dialog is to display the last page that was active during the current session. Well, what about that unused second parameter to ExecuteCommand?
HRESULT __stdcall ExecuteCommand(
BSTR CommandName,
BSTR CommandArgs
);
Hmm… CommandArgs .. sounds useful! Let’s try passing the name of the page I want to activate:
hr = _ConnectionObject->m_pDTE->ExecuteCommand(CComBSTR("Tools.Options"), CComBSTR("VisEmacs.Debug"));
Feh. No joy, that didn’t work. Heck, let’s fiddle around in the Command Window:
> Tools.Options
Hey, there’s the options dialog! Ok, let’s try adding an argument:
> Tools.Options VisEmacs.Debug
Drat! The Tools.Options command really doesn’t seem to take arguments. We’ll have to try another approach. Fortunately, a method using a CBTHook was suggested by another VisEmacs user (Hi Michael!).
A CBTHook is used by “Computer Based Training” applications, or at least that was the intent. Here, we’re going to watch for window activation events and if the window being activated is the VisualStudio Options dialog, we’re going to leap into action. Watching all the activation events is overkill for what we need to do so we’ll only install the hook for the duration of our command:
void visemacs::do_configure()
{
HRESULT hr;
// change the target option page so that the VisEmacs entry is highlighted
cbt_select_options_page force_visemacs_options_page_active;
hr = _ConnectionObject->m_pDTE->ExecuteCommand(CComBSTR("Tools.Options"), CComBSTR(""));
}
Neat, eh? The class cbt_select_options_page uses the RAII idiom to install and remove the hook around the call to ExecuteCommand . The class is also responsible for the logic to select the options page. Here are the constructor and destructor:
cbt_select_options_page()
{
bActivateOptionsPage = true;
myCBTHook = ::SetWindowsHookEx(WH_CBT, activateOptionsPage, _AtlModule.GetResourceInstance(), 0);
}
~cbt_select_options_page()
{
::UnhookWindowsHookEx(myCBTHook);
myCBTHook = 0;
}
Pretty straightforward. The constructor sets a boolean that acts as a one-shot gate to the hook proc. We don’t want to continuously activate our options page since that will probably interfere with the normal operation of the dialog. Next, the hook proc is inserted into the CBT hook chain. The destructor simply removes our hook and sets the HHOOK variable to 0.
Let’s look at the hook itself:
static LRESULT CALLBACK activateOptionsPage(int nCode, WPARAM wParam, LPARAM lParam)
{
if (nCode == HCBT_ACTIVATE && bActivateOptionsPage)
{
HWND hWnd = reinterpret_cast(wParam);
CBTACTIVATESTRUCT *pActivateWndStruct = reinterpret_cast(lParam);
if (!pActivateWndStruct->fMouse)
{
CWindow dlg(hWnd);
TCHAR caption[256];
dlg.GetWindowText(caption, 255);
if (_tcsicmp(caption, “Options”) == 0)
{
const int VISUALSTUDIO_OPTIONS_TREEVIEW(0×12c1);
CWindowtree(dlg.GetDlgItem(VISUALSTUDIO_OPTIONS_TREEVIEW));
HTREEITEMhItem = FindTreeItemText(tree, _T(“VisEmacs”)); if (hItem)
{
TreeView_SelectItem(tree, hItem);
TreeView_Expand(tree, hItem, TVE_EXPAND);
}
bActivateOptionsPage = false;
}
}
}
return ::CallNextHookEx(myCBTHook, nCode, wParam, lParam);
}
(Pardon the lack of indentation. Wordpress uses tinyMCE as the editor and it’s stripping out the whitespace for some reason.)
So the way this works is that if our hook gets called for an activate event AND our flag is set, we look to see if the windows caption matches that of Visual /Studio’s options dialog. If it does, we then find the “VisEmacs” item in the treeview, select it, and expand it. Finally, we reset bActivateOptionsPage
to false, and then call the next hook. We’re done.
That’s all there is to it. It’s a useful technique, and one that I’m going to keep in my bag-o-tricks.
Filed under: News

Leave a Reply