I've just spent some
time trying to solve the following problem: When the uninstaller for a piece of
software is run outside of the Programs
and Features view, how do you tell the Programs and Features view that its
underlying data has changed and it should refresh itself?
It was pretty easy to find out how to enumerate the windows. It
turns out all Windows Explorer and Internet Explorer windows
implement a COM interface called IWebBrowserApp in an object that
MSDN documents as the "InternetExplorer" object. A COM
server called Shell.Application exposes a list of
all currently-open windows in a collection called Windows().
The next step,
though, is to figure out what kind of
window each of these is. That is not as straightforward. Some of the InternetExplorer objects might actually be web browsers. Some might be folder
views. Looking through the various properties at my disposal, identifying the
ones that are showing the Programs and Features view has no obvious solution.
The closest seems to be the LocationName property, but the
problem with that property is that it is localized – that is to say, if you run
the same code on e.g. a Japanese Windows installation, you’ll get back the
string “プログラムと機能” instead of “Programs and Features”.
Having this feature depend on the way the string gets translated into foreign
languages is not going to be robust.
I made a post on an
MSDN community forum asking if anyone had any advice. I got a fairly prompt,
but terse, reply from a helpful commenter, suggesting that I compare the PIDL
of the window with the canonical PIDL for “Programs and Features”. He listed a
few COM interfaces – IShellFolder, IFolderView, IPersistFolder2 – that expose this
type of functionality. The missing piece, though, is how to get from a
reference to an InternetExplorer object into the
Shell world and its PIDL-based interfaces.
The way to do it is
to take advantage of the fact that the InternetExplorer object also implements IServiceProvider. IServiceProvider can then be queried
for the service SID_STopLevelBrowser, which will return
the IShellBrowser controlling the InternetExplorer. (I'm not certain, but it might be possible for an InternetExplorer to be hosted by something other than an IShellBrowser, in which case the call would fail. That doesn't apply to my
scenario, though -- I just need to make my code resilient to the possibility.)
Once you have an IShellBrowser,
you call QueryActiveShellView to get an IShellView reference. The
object underlying that reference may also implement IFolderView, and if it does, that in turn gives you access to an IPersistFolder2 object via the GetFolder method. IPersistFolder2, finally, returns the view's PIDL via GetCurFolder. It’s a bit of a chain, but none of the steps along the way are
hard.
As for what to do with
the PIDL... The helpful forum commenter suggested parsing “FOLDERID_ControlPanelFolder\8\FOLDERID_ChangeRemovePrograms”, but didn’t give any indication of how to parse it. I tried all sorts of options, but I couldn't find
any combination of input string and function that would do it. Finally, I
researched whether the raw bytes of a PIDL would be stable, and it seems for
things like Control Panel entries, yes, they will be, across sessions, reboots,
machines, and so on. So, I used the above technique to get the PIDL for a
Programs and Features window on my PC, and then broke it into its components
and stashed those in my source code. Then, I reworked the code to compare the PIDL with what I'd stashed.
On my development PC, it worked, but that wasn’t a surprise – I’d simply copied
the PIDL from an existing window, and then moments later compared my copy with
the original. They were the same – what did that prove? So, I copied my test
app to a Windows 7 VM and ran it, and ... it didn't work?
Okay, so the Control Panel has two different views. The default
view on a fresh installation is a fancy new categorized view, compared to the
way the Control Panel worked up to XP. However, you can change the view to make
it explicitly a flat, old-fashioned view. Turns out the PIDL for the
"Programs and Features" view encodes either that intermediary
category view ("Programs") or a placeholder for the flat view
("All Control Panel Items"). I contemplated encoding both forms of
PIDL and doing exact matches, but in the end I implemented it by checking that
the PIDL contains the component for "Control Panel" at some point,
and the PIDL for "Programs and Features" at some point, and that the
two appear in that relative order, ignoring all other ITEMIDs in the PIDL.
Hope this helps someone else trying to figure out some shell
stuff, and thanks to Sheng Jiang on the forum for the pointers! :-)
No comments:
Post a Comment