File Associations in Windows

A Guide to doing it the Right Way


Consider this document public domain or the closest equivalent in your place of residence. Even if I'm disclaiming all my rights to this text, I would still like to be attributed as the original author, out of courtesy.

The C++ source code snippets given in this document are adapted from Aegisub which is licensed under a permissive BSD license, but these adapted snippets are given to the public domain as the rest of this document is. Feel free to use them in your own software with or without attribution.

// You paid me nothing, I owe you nothing. Act responsibly.
#include <std_disclaimer.h>


Introduction

Something central to the user experience in Windows' shell has always been how applications manage file type associations. File type associations have
existed since Windows 3.0, possibly even earlier (I can't confirm that right now) but it was especially with Windows 95 they became truly visible to a large portion of the users, through the more pervasive file management interface the Explorer shell introduced.

However, Windows 95 and the later operating systems in the family had a problem, and we are still fighting the bad software practices that problem caused: All users were administrators, and all users, and thus the software they ran, could change the global file associations. Furthermore, the system
used for associations wasn't very mature or flexible at the time, and importantly couldn't keep track of multiple applications being able to handle the same file type, which caused lots of contention over some types, and gave rise to a horror people are still talking about, the associations thieves.

Especially media players and photo applications have been known to ruthlessly take any and all file type associations when installed, and possibly even every time they started. Some even went as far as installing background programs that ensured they had the associations all the time, and reset it if anything changed it.

Fortunately, with Windows 2000, XP and especially Vista, Microsoft introduced new ways for applications, and more importantly, for the users, to manage
file type associations. Microsoft also introduced User Account Control, which while hated by many, gives a big advantage: You can be an unprivileged user
and an administrator user at the same time, only taking the administrator privileges when really needed. Daily managing of file type associations does
not need administrator privileges, and can be done per user, and that is what I want to describe in this document.

This guide mainly applies to applications made for Windows XP and later, if you want to run on earlier versions of Windows not everything will apply and
not all techniques will work. Some of the best techniques only work on Windows Vista and later, but I'm providing workarounds to give almost the
same features on Windows XP and Server 2003. I don't have access to a Windows 2000 system, so I can't test how much applies to that version.

I am only going to touch associating file types with programs run with regular command lines. I will not touch protocol handler registration or DDE command verbs. If you need any of those you should read the documentation Microsoft provides on MSDN. While it is a bit fragmented, it is useful enough. (But for your own sake, try to avoid the DDE stuff! It became obsolete when preemptive multitasking was introduced in Windows. Disregard that Microsoft uses it for most of the Office Suite.)


A note about Portable applications

Portable applications have become popular along with the portable USB flash memory sticks. Why not take an application and make sure it can keep all of its configuration on the same little portable device, instead of storing it per computer, per user? Then you can bring your favourite software and its configuration along with you.

But there is one thing a Portable application should never do: It should never touch file type associations! File type associations are still a per-computer setting, even if stored in the user profile, and a Portable application has no business touching per-computer settings.

If your application supports a Portable installation mode natively, ensure that the application knows that at runtime, and that it must never touch file type associations when running with a Portable configuration.


Some terminology

First we should agree on what different parts of the file type association system are called.

Registry
The registry in Windows NT is split across multiple files, each which covers separate sub-trees. Parts of the registry gives configuration information for the running system and doesn't exist on disk. The registry as we will discuss it has two parts: The system (global) trees and the user tree. The system trees live in files in the %SYSTEMROOT%\System32\config\ directory; as an unprivileged user you don't even have read access to that directory (on Vista at least.) The user tree lives in %USERPROFILE%\ntuser.dat, which is locked for all access as long as the user is logged on.

Registry key
This is what you may think of as a "directory" in the registry. When the registry was first introduced in Windows 3 with OLE, the registry consisted only of keys, each key having an optional string value, and optional subkeys. It was later extended such that a key can also contain values, which can have different data types too. In the Windows NT security model, only registry keys have access control lists, values do not have ACLs.

Registry value
A value in the registry has three main components: A name, a type, and a data contents. If the name of a value is specified as the empty string or as NULL, the Default value for the key will be accessed. The Default value for a key is always of a string type and appears as (Default) in the Regedit program.

HKEY_LOCAL_MACHINE or HKLM
The registry root key containing macine-global configuration data. All users have read access to this tree, but you need power user privileges to get write access to it. During daily use of an application, you should never need to write data to HKLM, you should only write to HKLM when installing an application or maintaining an installation.

HKEY_CURRENT_USER or HKCU
The registry root key containing user-specific configuration data. This is where you as an application should store the user's preferences. The user always has full access to HKCU. There is also a HKEY_USERS root key, accessing that is not reliable and you shouldn't try to do that in general.

HKEY_CLASSES_ROOT or HKCR
This is a virtual root key, it is a merger of two sub keys of respectively HKLM and HKCU. You should consider HKCR to be read only, even if you seem to be able to write to it. You should always write to the location you mean to update: HKEY_LOCAL_MACHINE\SOFTWARE\Classes or HKEY_CURRENT_USER\Software\Classes. Note that the HKCU Classes tree takes precedence over the HKLM Classes tree, meaning that user preferences override machine configuration.

File extension
Take a file name, find the last period/dot (U+002E) in the name, copy the string from and including that dot to the end of the file name. That's the extension of a file name.

File type
Pretty much the same as a file extension, except now you're assigning some meaning to it. As soon as you define that ".jpg" is a file extension of JPEG format images, you have a file type.

File type association
A collection of registry values that together specify that a given file type can be opened by a given application either by executing a command line or by performing a DDE request.

ProgID
A description of how a file type association can be handled. A ProgID has a name, a user readable name, an icon, and usually one or more verbs giving actions a user can perform on a file having a type associated to the ProgID.
While the original intention was that one program registered one ProgID and used it for all file types it could handle, today it's more common to have one ProgID for every type you can handle.

Verb
A command a user can invoke on a file from Explorer. There are a bunch of standard verbs, and you can define your own as well. Standard verbs are "open", "edit", "print", "explore" and "properties". The two last have predefined functionality, and the three first have system-provided user readable names.

Perceived types
A perceived type is related to file types, but covers broad groups of file types. A registered file extension can (and should) have a perceived type associated with it, to provide some default behaviour for the type regardless of what application may otherwise be associated with that type at the time. Some  examples of perceived types are: "image", "text", "audio", "video", and "system".


SPAD and Default Programs

SPAD (Set Program Access and Defaults) was introduced in Windows XP SP1 due to the antitrust cases against Microsoft. This is a control panel that lets the user configure the system defaults for web browsing, e-mail client, media playback and Java runtime environment. Unfortunately, it doesn't do anything else and configures per-system settings.

SPAD was removed again in Windows Vista, and replaced with the far better and much more useful Default Programs control panel. The Default Programs control panel contains everyhing SPAD had, but also takes over the file type associations UI that was previously part of Explorer, making managing file type associations far easier.

I'm not going to discuss SPAD either, because it is so limited in what it offers the user, but I will be discussing Default Programs plenty, and how to implement your own UI that can emulate what Default Programs offers, just inside your own application.

Windows XP does offer a bit of the functionality Default Programs has, but it's harder to find and not useful for general management of associations.


Registration during installation

Two thirds of managing file type associations right is doing the right thing at install time. When installing you want to do two things:

1. Register your application with the system
This entails registering your ProgID's in HKLM Classes and configuring yourself for handling perceived types and appearing in Default Programs.

2. Claim any file types that aren't already globally claimed
If you discover that there are some file extensions that aren't yet claimed by another ProgID in HKLM Classes, you can safely claim that extension atinstall time without negatively interfering with the user's preferences:
If the user has an association set up in HKCU that one takes precedence, and you already tested that there isn't one set up in HKLM.

So what do you do?

Setting up a ProgID

The first thing you do is deciding on the name of the ProgID! Let's go with Microsoft's recommendation and use this scheme:
Vendor.Application.Type.Version

For example:
Flobware.Hornjor.JOR.1

Skip the Vendor part if you want, it might not make sense if for example you are an open source application and there is no "vendor" to speak of. (Don't pretend that you are a vendor called "OpenSource" or such.)

Okay, so create a key under HKLM\SOFTWARE\Classes with the name of your ProgID, and make the Default value for that key the human readable name for your ProgID, what will appear in Explorer as the file type name for files with extensions registered to your ProgID.

[HKLM\SOFTWARE\Classes\Flobware.Hornjor.JOR.1]
"" = "Jammed Orange ranting"

Note that since I'm using .reg file format, the backslashes need escaping with backslashes. Double quotes will also need escaping with backslashes, as seen later.

You probably also want an icon for your file type, let's pick the second icon resource in our executable:

[HKLM\SOFTWARE\Classes\Flobware.Hornjor.JOR.1\DefaultIcon]
"" = "C:\\Program Files\\Flobware\\Hornjor 1.0\\jor.exe,1"

You can also control how much of an a ProgID the user can edit from the GUI by setting the EditFlags DWORD value in your ProgID key. Read about those on MSDN.

Adding some verbs

Verbs are added below the "shell" subkey of your ProgID key. Command verbs are added to a "command" key below the verb key below the "shell" key below the ProgID key. DDE verbs would be added under a "dde" key under the verb key, but I'm not going to discuss those here.

Addition 2010-05-03: Raymond Chen has posted an example of using the IDropTarget COM interface to accept file open requests in a third, possibly more flexible, manner.

Command verbs are very simple: They simply have a Default value being the command line you want to have executed when the verb is invoked:

[HKLM\SOFTWARE\Classes\Flobware.Hornjor.JOR.1\shell\open\command]
"" = "\"C:\\Program Files\\Flobware\\Hornjor 1.0\\jor.exe\" \"%L\""

Two things to take note of: First, I'm enclosing the path to the executable in double quotes. Just make it explicit from the beginning instead of letting CreateProcess() guess where the executable file name ends and the parameter list begins. Second, I'm using %L and not %1. You can use %1 but then you may get a short file name instead of a full, long file name. This doesn't seem to be documented though, but I'm quite sure it works everywhere. Remember that long filenames can contain spaces and should be enclosed in quotes on the command line.

If you want a custom verb that need a user readable name, it gets a slight bit more complex, but only slight!

[HKLM\SOFTWARE\Classes\Flobware.Hornjor.JOR.1\shell\horn]
"" = "Horn ranting"
[HKLM\SOFTWARE\Classes\Flobware.Hornjor.JOR.1\shell\horn\command]
"" = "\"C:\\Program Files\\Flobware\\Hornjor 1.0\\horn.exe\" \"%L\""

Now go create as many ProgID's as you think you will need. If you have a few "preferred" file types and maybe a whole lot more you, for example, can read but not write and as such they maybe aren't "preferred", consider having a ProgID for each preferred format, and one for each major logical group of other file types.

Registering ProgIDs with file extensions

File extensions are also stored in the Classes tree, under the name of the file extension. The default value of a HKLM\SOFTWARE\Classes is the system default ProgID for handling files with that extension. This is the value that was so contested during the early years, but has become less important today. It still has some importance however, and during installation you claim it if nothing else has done so already.

So, let's check if the default value exists, and write our own if it doesn't:

[HKLM\SOFTWARE\Classes\.jor]
"" = "Flobware.Hornjor.JOR.1"

If there's nothing under HKCU that causes the ".jor" file extension to be associated with something else, you will now be the happy owner of that file type on the user's system! If there was already something registered for the file extension, don't despair, because we aren't done yet.

We should also register as being able to handle the type even if someone else has claimed it for theirs. We'll add our ProgID as an OpenWithProgID:

[HKLM\SOFTWARE\Classes\.jor\OpenWithProgIDs]
"Flobware.Hornjor.JOR.1" = ""

Note that here you add a value with the name of your ProgID, and the data in the value doesn't matter, just leave it as an empty string or even use a REG_NONE type value if you fancy. When you've done that, whenever the user right-clicks a file with that extension in Explorer and looks in the Open With submenu, they will see your application. Perfect!

Open With -> Select Default Program...

No wait, we aren't entirely done with the Open With thing yet. We need to register the application itself to appear in the general Open With dialogue box, so the user can also select it from there for any kind of type, or set it as the default application for a type from there.

Applications for Open With are registered in the "Applications" subkey of the Classes key. For whatever reason, you use your executable file name for key under there, and set it up with verbs as you would a ProgID.

[HKLM\SOFTWARE\Classes\Applications\jor.exe]
"FriendlyAppName" = "Hornjor"
[HKLM\SOFTWARE\Classes\Applications\jor.exe\shell\open]
"FriendlyAppName" = "Hornjor"
[HKLM\SOFTWARE\Classes\Applications\jor.exe\shell\open\command]
"" = "\"C:\\Program Files\\Flobware\\Hornjor 1.0\\jor.exe\" \"%L\""

Why two "FriendlyAppName" values? The first isn't actually used. Microsoft documents the first as being correct, but practice shows that it's the second that's correct. It also makes more sense for the second to be correct, since you're really picking a verb to perform on the file type, not an application to use for the "open" verb. (Or, the Open With dialogue box isn't restricted to the "open" verb, and it needs different "FriendlyAppName"s for each verb.)

However, this isn't entirely enough, we also need to specify what file extensions we are prepared to handle. This can be more than those you would usually register your ProgID for, just throw in anything you can possibly take. Do that in the SupportedTypes subkey of your application key:

[HKLM\SOFTWARE\Classes\Applications\jor.exe\SupportedTypes]
".jor" = ""
".horn" = ""
".txt" = ""
".bmp" = ""

Again, these are empty values, the data don't matter for them.

Default Programs

Almost done, but we still need to register for Windows Vista's Default Programs control panel to pick us up. Doing this registration is also handy if you want to have your own in-program association handling UI for earlier versions of Windows.

For this we will need a "Capabilities" key somewhere inder HKLM. If you're already storing some settings for your application under HKLM\SOFTWARE, that's a perfect location to store your Capabilities data.

Your Capabilities key should contain at least an Application Description, which apparently is intended to be a marketing blob telling not much of use, so if you have a marketing department, tell them to write a short paragraph for that now, so it's ready for when your installer goes final. The Capabilities key can also have subkeys describing what file extensions with associated ProgID's, URL types, and other things, your application can be made default for. Again, we'll focus solely on file extensions.

[HKLM\SOFTWARE\Flobware\Hornjor 1.0\Capabilities]
"ApplicationDescription" = "Your complete solution for all combinations of Horn technology with ranting Jammed Oranges"
[HKLM\SOFTWARE\Flobware\Hornjor 1.0\Capabilities\FileAssociations]
".horn" = "Flobware.Hornjor.HORN.1"
".jor" = "Flobware.Hornjor.JOR.1"
".txt" = "Flobware.Hornjor.Text.1"
".bmp" = "Flobware.Hornjor.Image.1"

Since there's no single standard location to collect all those Capabilities keys, there is instead a central location they are referenced. When in doubt, add another layer of indirection.

[HKLM\SOFTWARE\RegisteredApplications]
"Flobware Hornjor 1.0" = "SOFTWARE\Flobware\Hornjor 1.0\Capabilities"

Here, the value name is a human readable name for your application, that will appear in the Default Programs GUI (unless something else overrides it), and and value data is a string referencing a subkey of HKLM, where your Capabilities key is.

Registering Perceived Types

The last thing before we're done installing the application is handling perceived types. Perceived types are overly general groups of types, grouping them by what the user is expected to see the type as, rather than what it actually is. You can also think of it as an extremely simplified and limited kind of inheritance scheme or class mixin, but it's probably better if you don't.

There's a bunch of predefined perceived types, and it's possible to define your own if you really feel the need, I'll leave that to MSDN. The most important ones are "image", "text", "audio", "video" and "system".

We'll first register some one of our file extensions to have a perceived type, and then add ourselves to handle various preceived types.

For giving a file extension a perceived type, we'll revisit the extension's key under Classes. It's quite simple:

[HKLM\SOFTWARE\Classes\.horn]
"PerceivedType" = "text"

Now anything that's registered to handle the "text" perceived type will automagically appear in the Open With menu and the Select Default Program dialogue box for the ".horn" file extension. It's officially a text file!

Since our application also seems to handle text files and image files in general, let's claim to handle those perceived types as well so we can get the same Open With treatment as Notepad gets for text files and Windows Media Player gets for video and audio files.

Claiming to handle a perceived type happens in the "SystemFileAssociations" key under Classes. Registering for a perceived type is just like setting up a ProgID and file extension both under the same key.

[HKLM\SOFTWARE\Classes\SystemFileAssociations\text\OpenWithList\jor.exe]

Yes, no values under that key, it just has to be there. The name of the key derives from the name we set for the Open With/Select Default Program dialogue box earlier, under HKLM\SOFTWARE\Classes\Applications.

We can also add a few verbs here, that will appear unless something else overrides them:

[HKLM\SOFTWARE\Classes\SystemFileAssociations\text\shell\horn]
"" = "Horn ranting"
[HKLM\SOFTWARE\Classes\SystemFileAssociations\text\shell\horn\command]
"" = "\"C:\\Program Files\\Flobware\\Hornjor 1.0\\horn.exe\" \"%L\""

That's it!

Yes, that's finally it, you're done doing the during-installation set-up of file type associations. You've now told Windows and your users all you can about all the fancy file types you handle, without stepping over anyone's toes or stealing associations from anyone else.


Uninstalling

Now that we know what to do during installation, let's quickly go over what you need to do in case the user wants to uninstall your program. The main thing to remember is to only remove things that directly relate to you, and leave anything that may have been changed alone.

Delete your ProgIDs. When your application is gone, they are worth nothing.

Delete yourself from OpenWithProgIDs in all extentions you added yourself.

Delete yourself from HKLM\SOFTWARE\Classes\Applications.

Delete your Capabilities key and remove yourself from HKLM\SOFTWARE\RegisteredApplications.

Remove anything you added under SystemFileAssociations, but be careful you don't remove something that was changed by someone else, especially do check any verbs that could be generic in some way. Don't step on someone else!

And what you should leave alone: Leave whatever the ProgID for any extensions you may or may not have taken during installation. It doesn't hurt with stale ProgIDs being referenced there and you just risk stepping on someone else. Leave any PerceivedTypes you set for any extensions, they are still valid.


After installation

Let's start with a simple use case you may want to implement in your application, and how to handle it on modern versions of Windows, i.e. Vista and later. We'll leave in a stub for earlier versions too.

The user wants to customise what file types your application takes, and you provide a button in your Options GUI to take the user to somewhere to
configure that.

Here's an example in C++ of what that button could do:

    // This is your value name under HKLM\Software\RegisteredApplications
    #define APP_REGISTRATION_NAME L"Flobware Hornjor 1.0"

    HRESULT res;
    IApplicationAssociationRegistrationUI *aarui;
    res = CoCreateInstance(
        CLSID_ApplicationAssociationRegistrationUI, 0,
        CLSCTX_INPROC_SERVER,
        IID_IApplicationAssociationRegistrationUI, (LPVOID*)&aarui);

    if (SUCCEEDED(res) && aarui != 0)
    {
        // We can call the Vista-style UI for registrations
        aarui->LaunchAdvancedAssociationUI(APP_REGISTRATION_NAME);
        aarui->Release();
    }
    else
    {
        ShowOldAssociationsGUI();
    }

That's easy, isn't it? The user clicks the button, the user gets the standard Windows UI for managing file type associations for an application. If just this had been introduced ealier... later I'll present a C++ class that does the most important things on older systems.


Now, what if you want to check on start-up whether your application actually is the default for all file types and offer the user to take over, it's also quite easy on Windows Vista and later systems. It takes some more code than just showing the application association registration UI, mostly because you first need to ask the user!

    if (!DoesUserWantToBeAskedAboutTakingDefaults()) return;

    HRESULT res;
    IApplicationAssociationRegistration *aar;
    res = CoCreateInstance(
        CLSID_ApplicationAssociationRegistration, 0,
        CLSCTX_INPROC_SERVER,
        IID_IApplicationAssociationRegistration, (LPVOID*)&aar);

    if (SUCCEEDED(res) && aar != 0)
    {
        // We can use the system-provided associations checker class
        BOOL is_default = FALSE;
        res = aar->QueryAppIsDefaultAll(AL_EFFECTIVE, APP_REGISTRATION_NAME, &is_default);
        if (SUCCEEDED(res) && is_default)
        {
            if (AskUserToTakeDefaults()) // Good boys use a TaskDialog for this!
            {
                aar->SetAppAsDefaultAll(APP_REGISTRATION_NAME);
            }
        }
        aar->Release();
    }
    else
    {
        DoOldDefaultsCheck();
    }

Remember to provide a way for the user to disable the check for being default application, and remember that regardless of whether the user clicks Yes or No to the "take defaults?" question, you should still store whether the user wants to be asked again. The IApplicationAssociationRegistration interface has many more functions, be sure to look up the documentation on it.

Before proceeding to handling the case of old Windows version with our own own code, let's take a quick look at where Explorer (or rather the conglomerate of COM classes that makes up the Windows Shell) resolves file type associations.


Reverse engineering default programs for a file type

Let's start by taking a look at MSDN. Somewhat strangely, it seems to be the page discussing perceived types that mentions the priority order of the association locations. See near the bottom of the linked article.

Here's my own short discussion of each of these:

1. "User customised"
This is a "secret" location managed by the Shell under HKCU, updated when the user selects a new default program for a file type through some system-provided UI. Our class that will imitate the Vista ApplicationAssociationRegistration class to a degree will be messing with this, despite it being undocumented.
2. "ProgID"
This is the regular file extension -> ProgID mapping found in the HKCR merger of HKLM\SOFTWARE\Classes and HKCU\Software\Classes.
3. "SystemFileAssociations"
HKCR contains another sub-tree with this name, which essentially acts as a fallback. Apparently the idea is to provide a "safe haven" for shell extensions to play nicer with each other and not unregister each other because of thoughtless coding.
4. "PerceivedType"
Also living in the SystemFileAssociations, this is yet another fallback. This is a good place to register yourself if you do in fact handle a lot of files of generic types. But if you can't do anything useful with anything but GIF images, don't register yourself as handling the "image" perceived type, because you don't really handle it, do you?
5. "Base Class"
—a.k.a the HKCR\* file extension registration. This is a wonderful place to register yourself if you're a shell extension that to attempt to trigger in anything and everything, "Hey could that random file perhaps be a disguised ARJ archive?! Better check!" This is only for actual files, by the way.
6. "AllFilesystemObjects"
Anything that isn't just an imaginary object of the Shell namespace, essentially the same as the "Base Class" above, but file folders included this time. This is HKCR\AllFilesystemObjects.

Regular applications are supposed to have no business with any of these during regular operation, though you may want to touch 1. and 2. at times. Locations 3., 5., and 6. are mostly intended for shell extensions and things that look and feel like shell extensions, while the others are for more regular file creation and viewing software.

As to why it's safe to touch the location in 1., despite it being undocumented, the argument will be: Windows Vista and newer provides a well-documented programmatic interface to manipulate the user defaults. Use that interface when it's available. When it's not available, you're on an older version of Windows, and you know that all newer versions will support the well-documented interface. The undocumented interface won't magically disappear from old and already-published versions of Windows, and it won't be redefined in those either. Therefore it's safe to use it, as long as the new programmatic interface is not available.

The undocumented "user defaults" location on Windows NT 5.x is HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts.

This key stores mostly a whole Classes mini-subtree containing all kinds of user preferences and MRU lists. Stuff that changes during daily use. Do note that the format of this key did change slightly from Windows XP to Windows Vista! Just to prove that it should indeed be considered undocumented territory.

Under the Explorer\FileExts key you will find a bunch of keys, each for one file extension. A FileExt subkey can have a value specifying an override ProgID or Application. The primary thing to take note of is that if a FileExt subkey has a value named "Progid" or "Application", then the ProgID seen in HKCR for the extension should be ignored. There should never be both a "Progid" and an "Application" value present, but if both are, the "Progid" takes precedence.

Here's an example of something you would not see:

[HKCU\Software\Microsoft\Windows\CurrentVersion\Explorer\FileExts\.txt]
"Application" = "WORDPAD.EXE"
"Progid" = "Flubware.Hornjor.Text.1"

Currently, double-clicking a .txt file would invoke jor.exe on it, per the Flubware.Hornjor.Text.1 ProgID in HKLM Classes. If the "Progid" value was removed from the Explorer\FileExts\.txt key, then .txt files would be opening with WordPad instead. If you also remove the "Application" value, they would be back to opening with Notepad, or whatever is reflected in HKCR.

Let's take advantage of how HKCU Classes override HKLM Classes and do as little mocking around in Explorer's undocumented keys as we can.

We'll use the Explorer\FileExts subkeys to help check whether we have file types, but write our own defaults to HKCU Classes file extension keys, and then just delete any user preferences set from Explorer.

If you look inside the Explorer\FileExts subkeys on Windows XP, you will also note some OpenWithList and OpenWithProgids subkeys of those, they are user-customised MRU lists of what applications the user has been opening that file type with. We don't need to touch those.


Aside: Shell New

One of those little niceties if you have an application that creates documents is having a Shell New registration. Providing one via template files is easy and you can be sure users will be thankful. It might not be something you ever really hear of, but do it anyway!

Create a document file in your application that would constiture a "blank" document. Include that file in your installer package and install it to the CSIDL_COMMON_TEMPLATES directory (ask SHGetSpecialFolderPath().) The file should be named something relevant to your application, and have the proper file extension!

Now ensure that HKCR\.ext\ProgID\ShellNew has a value named "FileName" with data as the path to your installed template file:

[HKLM\SOFTWARE\Classes\.jor\Flubware.Hornjor.JOR.1\ShellNew]
"FileName" = "C:\\Documents and Settings\\All Users\\Templates\\Hornjor.jor"

You can also try to do simpler, and not install any template, and instead put a value named "NullFile" with no data, and you create zero byte files. Just make sure your application code supports that case.

On uninstall, remember to delete the template file and remove the registry entries for it.


Managing default applications in Windows 5 with C++

Let the rest of this file be commented C++ source code.


#include <windows.h>
#include <string>
#include <vector>


class OldRegistrationChecker {
public:
    struct FileType {
        std::wstring ext;
        std::wstring progid;
        std::wstring typedesc;
        FileType(const std::wstring &_ext, const std::wstring &_progid, const std::wstring &_typedesc)
            : ext(_ext)
            , progid(_progid)
            , typedesc(_typedesc)
        { }
    };

private:
    std::vector<FileType> types;
    const std::wstring exefile;

    HKEY hkey_user_classes;
    HKEY hkey_explorer_fileexts;

public:
    // @capskey is the path to your "Capabilities" key under HKLM
    // @exefile is the base name of your exe file
    OldRegistrationChecker(const std::wstring &capskey, const std::wstring &exefile);
    ~OldRegistrationChecker();

    size_t GetTypeCount() const { return types.size(); }
    const FileType& GetType(size_t index) const { return types.at(index); }

    bool HasType(const FileType &type) const;
    bool HasAllTypes() const;
    void TakeType(const FileType &type) const;
    void TakeAllTypes() const;
};


OldRegistrationChecker::OldRegistrationChecker(const std::wstring &capskey, const std::wstring &_exefile)
: exefile(_exefile)
{
    // Read the registration data written for Vista+ systems into HKLM
    // We can just as well re-use the data the installer already writes!
    HKEY hkey_caps = 0;
    HKEY hkey_machine_classes = 0;

    if (RegOpenKeyExW(HKEY_LOCAL_MACHINE, (capskey + L"\\FileAssociations").c_str(), 0, KEY_READ, &hkey_caps) == ERROR_SUCCESS &&
        RegOpenKeyExW(HKEY_LOCAL_MACHINE, L"SOFTWARE\\Classes", 0, KEY_READ, &hkey_machine_classes) == ERROR_SUCCESS)
    {
        int enum_index = 0;
        const size_t alloc_value_size = 256;
        wchar_t *value_ext = new wchar_t[alloc_value_size];
        wchar_t *value_pid = new wchar_t[alloc_value_size];
        wchar_t *value_dsc = new wchar_t[alloc_value_size];

        LRESULT enumres = ERROR_SUCCESS;
        while (enumres == ERROR_SUCCESS)
        {
            DWORD value_ext_size = (DWORD)alloc_value_size;
            // Find next listed file extension
            enumres = RegEnumValueW(hkey_caps, enum_index, value_ext, &value_ext_size, 0, 0, 0, 0);

            if (enumres != ERROR_SUCCESS) break;

            DWORD value_pid_size = (DWORD)alloc_value_size*2;
            DWORD value_type = REG_NONE;
            // Grab the ProgID for that extension
            if (RegQueryValueExW(hkey_caps, value_ext, 0, &value_type, (LPBYTE)value_pid, &value_pid_size) == ERROR_SUCCESS &&
                value_type == REG_SZ)
            {
                DWORD value_dsc_size = (DWORD)alloc_value_size*2;
                // And find the human-readable name for that ProgID
                if (RegQueryValueW(hkey_machine_classes, value_pid, value_dsc, (PLONG)&value_dsc_size) == ERROR_SUCCESS)
                {
                    // And dump it all into our table
                    types.push_back(FileType(
                        std::wstring(value_ext, value_ext_size),
                        std::wstring(value_pid, value_pid_size/2),
                        std::wstring(value_dsc, value_dsc_size/2)));
                }
            }

            enum_index += 1;
        }

        delete[] value_ext;
        delete[] value_pid;
        delete[] value_dsc;
    }

    if (hkey_caps)
        RegCloseKey(hkey_caps);
    if (hkey_machine_classes)
        RegCloseKey(hkey_machine_classes);

    if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Classes", 0, KEY_READ|KEY_WRITE, &hkey_user_classes) != ERROR_SUCCESS)
        hkey_user_classes = 0;
    if (RegOpenKeyExW(HKEY_CURRENT_USER, L"Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\FileExts", 0, KEY_READ|KEY_WRITE, &hkey_explorer_fileexts) != ERROR_SUCCESS)
        hkey_explorer_fileexts = 0;
}


OldRegistrationChecker::~OldRegistrationChecker()
{
    if (hkey_user_classes)
        RegCloseKey(hkey_user_classes);
    if (hkey_explorer_fileexts)
        RegCloseKey(hkey_explorer_fileexts);
}


bool OldRegistrationChecker::HasAllTypes() const
{
    bool res = true;
    for (size_t i = 0; i < types.size(); ++i)
    {
        res &= HasType(types[i]);
    }
    return res;
}


void OldRegistrationChecker::TakeAllTypes() const
{
    for (size_t i = 0; i < types.size(); ++i)
    {
        TakeType(types[i]);
    }
}


bool OldRegistrationChecker::HasType(const FileType &type) const
{
    // Checks to perform:
    // 1. Check hkey_explorer_fileexts\<ext> for value "Progid":
    //    * If the value data is our <progid> then we have the type
    //    * If the value data is something else, we don't have the type
    //    * If the value doesn't exist, continue
    // 2. Check hkey_explorer_fileexts\<ext> for value "Application":
    //    * If the value data is <exename> then we have the type
    //    * If the value data is something else, we don't have the type
    //    * If the value doesn't exist, continue
    // 3. Check the default value of HKEY_CLASSES_ROOT\<ext>:
    //    * If it is our <progid> then we have the type
    //    * If it is something else, we don't have the type
    //    * If the value doesn't exist, we don't have the type

    const DWORD alloc_size = 260;
    wchar_t *data = new wchar_t[alloc_size];
    DWORD data_type, data_size;

    HKEY hkey_fileext;
    if (RegOpenKeyExW(hkey_explorer_fileexts, type.ext.c_str(), 0, KEY_READ, &hkey_fileext) == ERROR_SUCCESS)
    {
        // Step 1
        data_type = REG_NONE;
        data_size = alloc_size*sizeof(wchar_t);
        if (RegQueryValueExW(hkey_fileext, L"Progid", 0, &data_type, (LPBYTE)data, &data_size) == ERROR_SUCCESS &&
            data_type == REG_SZ)
        {
            std::wstring cur_progid(data, data_size/2);

            RegCloseKey(hkey_fileext);
            delete[] data;

            return type.progid.compare(cur_progid) == 0;
        }

        // Step 2
        data_type = REG_NONE;
        data_size = alloc_size*sizeof(wchar_t);
        if (RegQueryValueExW(hkey_fileext, L"Application", 0, &data_type, (LPBYTE)data, &data_size) == ERROR_SUCCESS &&
            data_type == REG_SZ)
        {
            std::wstring cur_app(data, data_size/2);

            RegCloseKey(hkey_fileext);
            delete[] data;

            return cur_app.compare(exename) == 0;
        }

        RegCloseKey(hkey_fileext);
    }

    // Step 3
    bool res = false;
    data_size = alloc_size*sizeof(wchar_t);
    if (RegQueryValueW(HKEY_CLASSES_ROOT, type.ext.c_str(), data, (PLONG)&data_size) == ERROR_SUCCESS)
    {
        std::wstring cur_progid(data, data_size/2);
        res = type.progid.compare(cur_progid) == 0;
    }

    delete[] data;
    return res;
}


void OldRegistrationChecker::TakeType(const FileType &type) const
{
    // Making sure we have the type:
    // 1. From hkey_explorer_fileexts\<ext> delete any "Application" value
    // 2. From hkey_explorer_fileexts\<ext> delete any "Progid" value
    // 3. Read HKCR\<ext> default value:
    //    * If it is our <progid>, we are done
    //    * If there is no value, continue to step 4
    //    * If the value is something else, write a value with that as name to
    //      hkey_user_classes\<ext>\Progids with blank data and continue
    // 4. Set default value of hkey_user_classes\<ext> to our <progid>
    // First two removes any customisation the user has done in Explorer, last two
    // override anything set in HKLM\SOFTWARE\Classes\<ext>.

    HKEY hkey_fileext;
    if (RegOpenKeyExW(hkey_explorer_fileexts, type.ext.c_str(), 0, KEY_READ|KEY_WRITE, &hkey_fileext) == ERROR_SUCCESS)
    {
        // Step 1 and 2
        RegDeleteValueW(hkey_fileext, L"Application");
        RegDeleteValueW(hkey_fileext, L"Progid");
        RegCloseKey(hkey_fileext);
    }

    // Step 3
    DWORD old_progid_size = 260*2;
    wchar_t *old_progid = new wchar_t[old_progid_size];
    if (RegQueryValueW(HKEY_CLASSES_ROOT, type.ext.c_str(), old_progid, (PLONG)&old_progid_size) == ERROR_SUCCESS)
    {
        std::wstring old_progid_str(old_progid, old_progid_size/2);
        // Check if existing progid matches ours
        if (type.progid.compare(old_progid_str) == 0)
        {
            // It does, we're done
            delete[] old_progid;
            return;
        }
        else if (old_progid_str.size() != 0)
        {
            // Not our progid and not blank, make sure it's present for Open With
            HKEY hkey_user_ext = 0;
            HKEY hkey_progids = 0;
            if (RegOpenKeyExW(hkey_user_classes, type.ext.c_str(), 0, KEY_WRITE, &hkey_user_ext) == ERROR_SUCCESS &&
                RegOpenKeyExW(hkey_user_ext, L"OpenWithProgids", 0, KEY_WRITE, &hkey_progids) == ERROR_SUCCESS)
            {
                RegSetValueExW(hkey_progids, old_progid_str.c_str(), 0, REG_NONE, 0, 0);
            }

            if (hkey_progids)
                RegCloseKey(hkey_progids);
            if (hkey_user_ext)
                RegCloseKey(hkey_user_ext);
        }
    }
    delete[] old_progid;

    // Step 4
    RegSetValueW(hkey_user_classes, type.ext.c_str(), REG_SZ, type.progid.c_str(), (type.progid.size()+1)*sizeof(wchar_t));
}


Comments