Date: | July 20, 2004 / year-entry #284 |
Tags: | code |
Orig Link: | https://blogs.msdn.microsoft.com/oldnewthing/20040720-00/?p=38393 |
Comments: | 37 |
Summary: | Sometimes software development is inventing new stuff. But often, it's just putting together the stuff you already have. Today's puzzle is one of the latter type of problem. Given a window handle, you can you determine (1) whether it is an Explorer window, and if so (2) what folder it is viewing, and (3) what item is currently... |
Sometimes software development is inventing new stuff. But often, it's just putting together the stuff you already have. Today's puzzle is one of the latter type of problem. Given a window handle, you can you determine (1) whether it is an Explorer window, and if so (2) what folder it is viewing, and (3) what item is currently focused. This is not an inherently difficult task. You just have to put together lots of small pieces. Start with the ShellWindows object which represents all the open shell windows. You can enumerate through them all with the Item property. This is rather clumsy from C++ because the ShellWindows object was designed for use by a scripting language like JScript or Visual Basic. IShellWindows *psw; if (SUCCEEDED(CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_ALL, IID_IShellWindows, (void**)&psw))) { VARIANT v; V_VT(&v) = VT_I4; IDispatch *pdisp; BOOL fFound = FALSE; for (V_I4(&v) = 0; !fFound && psw->Item(v, &pdisp) == S_OK; V_I4(&v)++) { ... pdisp->Release(); } psw->Release(); } From each item, we can ask it for its window handle and see if it's the one we want. IWebBrowserApp *pwba; if (SUCCEEDED(pdisp->QueryInterface(IID_IWebBrowserApp, (void**)&pwba))) { HWND hwndWBA; if (SUCCEEDED(pwba->get_HWND((LONG_PTR*)&hwndWBA)) && hwndWBA == hwndFind) { fFound = TRUE; ... } pwba->Release(); } Okay, now that we have found the folder via its IWebBrowserApp, we need to get to the top shell browser. This is done by querying for the SID_STopLevelBrowser service and asking for the IShellBrowser interface. IServiceProvider *psp; if (SUCCEEDED(pwba->QueryInterface(IID_IServiceProvider, (void**)&psp))) { IShellBrowser *psb; if (SUCCEEDED(psp->QueryService(SID_STopLevelBrowser, IID_IShellBrowser, (void**)&psb))) { ... psb->Release(); } psp->Release(); } From the IShellBrowser, we can ask for the current shell view via the QueryActiveShellView method. IShellView *psv; if (SUCCEEDED(psb->QueryActiveShellView(&psv))) { ... psv->Release(); } Of course, what we really want is the IFolderView interface, which is the automation object that contains all the real goodies. IFolderView *pfv; if (SUCCEEDED(psv->QueryInterface(IID_IFolderView, (void**)&pfv))) { ... pfv->Release(); } Okay, now we're golden. What do you want to get from the view? How about the location of the IShellFolder being viewed. To do that, we need to use IPersistFolder2::GetCurFolder. The GetFolder method will give us access to the shell folder, from which we ask for IPersistFolder2. (Most of the time you want the IShellFolder interface, since that's where most of the cool stuff hangs out.) IPersistFolder2 *ppf2; if (SUCCEEDED(pfv->GetFolder(IID_IPersistFolder2, (void**)&ppf2))) { LPITEMIDLIST pidlFolder; if (SUCCEEDED(ppf2->GetCurFolder(&pidlFolder))) { ... CoTaskMemFree(pidlFolder); } ppf2->Release(); }
Let's convert that if (!SHGetPathFromIDList(pidlFolder, g_szPath)) { lstrcpyn(g_szPath, TEXT("<not a directory>"), MAX_PATH); } ... What else can we do with what we've got? Oh right, let's see what the currently-focused object is. int iFocus; if (SUCCEEDED(pfv->GetFocusedItem(&iFocus))) { ... } Let's display the name of the focused item. To do that we need the item's pidl and the IShellFolder. (See, I told you the IShellFolder is where the cool stuff is.) The item comes from the Item method (surprisingly enough). LPITEMIDLIST pidlItem; if (SUCCEEDED(pfv->Item(iFocus, &pidlItem))) { ... CoTaskMemFree(pidlItem); } (If we had wanted a list of selected items we could have used the Items method, passing SVGIO_SELECTION.) After we get the item's pidl, we also need the IShellFolder: IShellFolder *psf; if (SUCCEEDED(ppf2->QueryInterface(IID_IShellFolder, (void**)&psf))) { ... psf->Release(); } Then we put the two together to get the item's display name, with the help of the GetDisplayNameOf method. STRRET str; if (SUCCEEDED(psf->GetDisplayNameOf(pidlItem, SHGDN_INFOLDER, &str))) { ... } We can use the helper function StrRetToBuf to convert the kooky STRRET structure into a boring string buffer. (The history of the kooky STRRET structure will have to wait for another day.) StrRetToBuf(&str, pidlItem, g_szItem, MAX_PATH); Okay, let's put this all together. It looks rather ugly because I put everything into one huge function instead of breaking them out into subfunctions. In "real life" I would have broken things up into little helper functions to make things more manageable. Start with the scratch program and add this new function: #include <shlobj.h> #include <exdisp.h> TCHAR g_szPath[MAX_PATH]; TCHAR g_szItem[MAX_PATH]; void CALLBACK RecalcText(HWND hwnd, UINT, UINT_PTR, DWORD) { HWND hwndFind = GetForegroundWindow(); g_szPath[0] = TEXT('\0'); g_szItem[0] = TEXT('\0'); IShellWindows *psw; if (SUCCEEDED(CoCreateInstance(CLSID_ShellWindows, NULL, CLSCTX_ALL, IID_IShellWindows, (void**)&psw))) { VARIANT v; V_VT(&v) = VT_I4; IDispatch *pdisp; BOOL fFound = FALSE; for (V_I4(&v) = 0; !fFound && psw->Item(v, &pdisp) == S_OK; V_I4(&v)++) { IWebBrowserApp *pwba; if (SUCCEEDED(pdisp->QueryInterface(IID_IWebBrowserApp, (void**)&pwba))) { HWND hwndWBA; if (SUCCEEDED(pwba->get_HWND((LONG_PTR*)&hwndWBA)) && hwndWBA == hwndFind) { fFound = TRUE; IServiceProvider *psp; if (SUCCEEDED(pwba->QueryInterface(IID_IServiceProvider, (void**)&psp))) { IShellBrowser *psb; if (SUCCEEDED(psp->QueryService(SID_STopLevelBrowser, IID_IShellBrowser, (void**)&psb))) { IShellView *psv; if (SUCCEEDED(psb->QueryActiveShellView(&psv))) { IFolderView *pfv; if (SUCCEEDED(psv->QueryInterface(IID_IFolderView, (void**)&pfv))) { IPersistFolder2 *ppf2; if (SUCCEEDED(pfv->GetFolder(IID_IPersistFolder2, (void**)&ppf2))) { LPITEMIDLIST pidlFolder; if (SUCCEEDED(ppf2->GetCurFolder(&pidlFolder))) { if (!SHGetPathFromIDList(pidlFolder, g_szPath)) { lstrcpyn(g_szPath, TEXT("<not a directory>"), MAX_PATH); } int iFocus; if (SUCCEEDED(pfv->GetFocusedItem(&iFocus))) { LPITEMIDLIST pidlItem; if (SUCCEEDED(pfv->Item(iFocus, &pidlItem))) { IShellFolder *psf; if (SUCCEEDED(ppf2->QueryInterface(IID_IShellFolder, (void**)&psf))) { STRRET str; if (SUCCEEDED(psf->GetDisplayNameOf(pidlItem, SHGDN_INFOLDER, &str))) { StrRetToBuf(&str, pidlItem, g_szItem, MAX_PATH); } psf->Release(); } CoTaskMemFree(pidlItem); } } CoTaskMemFree(pidlFolder); } ppf2->Release(); } pfv->Release(); } psv->Release(); } psb->Release(); } psp->Release(); } } pwba->Release(); } pdisp->Release(); } psw->Release(); } InvalidateRect(hwnd, NULL, TRUE); } Now all we have to do is call this function periodically and print the results. BOOL OnCreate(HWND hwnd, LPCREATESTRUCT lpcs) { SetTimer(hwnd, 1, 1000, RecalcText); return TRUE; } void PaintContent(HWND hwnd, PAINTSTRUCT *pps) { TextOut(pps->hdc, 0, 0, g_szPath, lstrlen(g_szPath)); TextOut(pps->hdc, 0, 20, g_szItem, lstrlen(g_szItem)); } We're ready to roll. Run this program and set it to the side. Then launch an Explorer window and watch the program track the folder you're in and what item you have focused. Okay, so I hope I made my point: Often, the pieces you need are already there; you just have to figure out how to put them together. Notice that each of the pieces is in itself not very big. You just had to recognize that they could be put together in an interesting way. Exercise: Change this program so it takes the folder and switches it to details view. [Raymond is currently on vacation; this message was pre-recorded.] |
Comments (37)
|
If everything is so nicely scoped, why not use CComPtr<>s / CComQIPtr<>s ? This very same code would be so much more readable (no ugly QueryInterfaces, Releases, uninitialized variables)…
Guess it’s a habit thing.
I try to avoid using any extra libraries in these articles. If you like those libraries, you can translate raw C++ into your library; but it’s harder for others to translate a library into raw C++. (For example, I could’ve used a library to auto-free the pidlFolder, but that would have confused anybody who wasn’t familiar with that library.)
Hideous. That screams "potential memory leaks!"
That code is hilarious. It’s stuff like that which really puts me off learning about COM. Fortunately, I’ve managed to basically ignore COM so far, and if all this .NET stuff becomes popular maybe I can just pretend it never happened. :)
Pleaaase release me, let me gooooooo……
Well, you could easily flatten out that code with a CComPtr class, a CComMemoryHandle class, and a function called GuaranteeSuccess that throws an exception on a bad HRESULT and catch that in the function.
Any reader who wants to make an ATL version of this function is free to do so. It would probably be a lot prettier. But I write in pure C++ in order to avoid arguing over which template library is best.
cant compile : error C2065: ‘StrRetToBuf’ : undeclared identifier
Does Explorer lock windows that external clients are looking at (or indeed <em>all</em> of them while the windows are being enumerated) or is this code vulnerable to race conditions?
I believe the only point you’ve made is that only VB or C# should ever be used to access COM :-)
—
Cedric
I have a burning sensation in my eyes, is that normal??
Never use goto statements? I LOVE goto statements :)
Zach: Ummm, except now you can’t use C++ construction/destruction semantics with yoru code…
Having had to actually code up a program using a raw dispatch interface in C++, I really appreciate that you wrote this example without libraries. When I was trying to design my program I nearly tore my hair out trying to translate the things written on MSDN with the wrapper code into raw function calls that were actually useful to me.
The thing that bugs me is the use of Hungarian notation. ‘psf’ doesn’t say much, ‘shellFolder’ does.
Zachary: Note however that some of your code goto’s over variable initialization – for example if the initial CoCreateInstance fails. This results in the RELEASE trying to use an uninitialized variable.
Zachary, you have to make sure that when you make macros you take care to write them correctly. For instance, this hypothetical code is wrong:
if(–i == 0) // we have no more use for the object
RELEASE(pObj);
else
COM_CALL(pObj->foo(), finish);
First, because the RELEASE() macro has a semi-colon, you end up with two semi-colons and that’ll terminate the if statement before the else. If you remove the extra semi-colon, then the else clause gets attached to the if statement in the macro, and not the if(–i == 0).
I think the usual way of handling this is to use:
#define RELEASE(ptr)
if (ptr) ptr->Release(); else
So that the semi-colon at the end of RELEASE(foo); will close the else.
Or instead of a macro, you use something like a CComPtr, and to fix the nesting, you do like this:
enum EBadResult
{
kBadResult
};
void Succeed( HRESULT hr )
{
if ( !SUCCEEDED(hr) )
{
throw kBadResult;
}
}
… then in your code, you can simply do …
try
{
Object obj;
Succeed( incoming->DoThis( obj.GetPtrPtr() ) );
Succeed( obj->DoThisNow() );
}
catch ( EBadResult )
{
// Do what you need to in order to fail
}
… and you will get automatic destruction of the intermediate objects that you didn’t use. You write a small amount more code, you get similar ease of use as goto, and much MUCH more robust error handling in the language.
Great post. Keep ’em coming with more good stuff for the shell!
Even in pure C++, better to just stick a label at the bottom and have a goto statement if the COM call fails, rather than use deeper nesting if the call succeeds.
To the poster who said it’s things like that make you not want to learn COM, it’s very simple and easy to read as long as you provide an appropriate framework for making it easy to read. For example, consider the following two macros:
#define COM_CALL(pfn, lbl)
if (FAILED(pfn)) goto lbl;
#define RELEASE(ptr)
if (ptr) ptr->Release();
Now the RecalcText function is as follows:
void CALLBACK RecalcText(HWND hwnd, UINT, UINT_PTR, DWORD)
{
HWND hwndFind = GetForegroundWindow();
g_szPath[0] = TEXT(‘