Search code examples
c++winapimfcinno-setupfile-association

Changing file extension associations programatically using MFC?


I can use this code in InitInstance to confirm which executable is associated with a given extension:

TCHAR szRegisteredEXE[_MAX_PATH];
DWORD dwBufferLen = _MAX_PATH;

HRESULT  hRes = AssocQueryString(ASSOCF_NONE, ASSOCSTR_EXECUTABLE, 
                    _T("MeetSchedAssist.MWB"), NULL, szRegisteredEXE, &dwBufferLen);
if (hRes == S_OK)
{
    // TODO
}

It works fine.

My software installs a 32 bit version and a 64 bit version of the executable. So what I would like the code to do is prompt to update the associations if the registered exe is not the active exe.

I know how to get my active exe path and how to confirm if it matches szRegisteredEXE but how to I then deal with updating the file associations (assuming the user says yes to the prompt to associate)?


Solution

  • Step one is to create a Inno Setup installer that is going to manage the tweaking of the registry for us:

    ; SignTool parameters
    #define SignedDesc "$qMeeting Schedule Assistant File Associations Tool$q"
    #define SignedPfx "$q~~~~~$q"
    #define SignedTimeStamp "$qhttp://timestamp.verisign.com/scripts/timestamp.dll$q"
    #define SignedPw "$q~~~~~$q"
    
    #define AppURL "https://www.publictalksoftware.co.uk"
    #define AppPublisher "~~~~~"
    
    #define AppVerText() \
       ParseVersion('..\Meeting Schedule Assistant\Release\Meeting Schedule Assistant.exe', \
           Local[0], Local[1], Local[2], Local[3]), \
       Str(Local[0]) + "." + Str(Local[1]) + "." + Str(Local[2])
    
    [Setup]
    DisableReadyPage=True
    DisableReadyMemo=True
    DisableFinishedPage=True
    UsePreviousSetupType=False
    UsePreviousTasks=False
    UsePreviousLanguage=False
    FlatComponentsList=False
    AlwaysShowComponentsList=False
    ShowComponentSizes=False
    AppName=Meeting Schedule Assistant File Associations Tool
    AppVersion={#AppVerText}
    CreateAppDir=False
    Uninstallable=no
    OutputBaseFilename=MSATweakFileAssociations
    SignTool=SignTool /d {#SignedDesc} /du $q{#AppURL}$q /f {#SignedPfx} /p {#SignedPw} /t {#SignedTimeStamp} /v $f
    AppId={{~~~~~}
    
    [registry]
    Root: HKCR; SubKey: "MeetSchedAssist.MWB\Shell\Open\Command"; ValueType: string; ValueData: """{param:ExePath}"" ""%1""";
    Root: HKCR; SubKey: "MeetSchedAssist.SRR\Shell\Open\Command"; ValueType: string; ValueData: """{param:ExePath}"" ""%1""";
    

    Then, in MFC, we run our tool, like this:

    void COptionsDlg::OnBnClickedMfcbuttonFileAssociations()
    {
        // Try to run the help installer
        CString strSetupExe = _T("~~~~~.exe");
        CString strProgramFolder = theApp.GetProgramPath();
        CString strParams = _T("");
    
        strParams.Format(_T("/SP- /VERYSILENT /ExePath=\"%s\""), (LPCTSTR)theApp.GetProgramEXE());
        if (!theApp.ExecuteProgram(strProgramFolder + strSetupExe, strParams))
        {
            // Problem running the installer
            AfxMessageBox(_T("Unable to change the file associations"), MB_OK | MB_ICONERROR);
            return;
        }
    }
    
    BOOL CMeetingScheduleAssistantApp::ExecuteProgram(CString strProgram, CString strArguments)
    {
        SHELLEXECUTEINFO    se = { 0 };
        MSG                 sMessage;
        DWORD               dwResult;
    
        theApp.SetProgramExecuting(true);
    
        se.cbSize = sizeof(se);
        se.lpFile = strProgram;
        se.lpParameters = strArguments;
        se.nShow = SW_SHOWDEFAULT;
        se.fMask = SEE_MASK_NOCLOSEPROCESS;
        ShellExecuteEx(&se);
    
        if (se.hProcess != nullptr)
        {
            do
            {
                dwResult = ::MsgWaitForMultipleObjects(1, &(se.hProcess), FALSE,
                    INFINITE, QS_ALLINPUT);
                if (dwResult != WAIT_OBJECT_0)
                {
                    while (PeekMessage(&sMessage, nullptr, NULL, NULL, PM_REMOVE))
                    {
                        TranslateMessage(&sMessage);
                        DispatchMessage(&sMessage);
                    }
                }
            } while ((dwResult != WAIT_OBJECT_0) && (dwResult != WAIT_FAILED));
    
            CloseHandle(se.hProcess);
        }
    
        theApp.SetProgramExecuting(false);
    
        if ((DWORD_PTR)(se.hInstApp) < 33)
        {
            // Throw error
            AfxThrowUserException();
            return FALSE;
        }
    
        SHChangeNotify(SHCNE_ASSOCCHANGED, SHCNF_IDLIST, NULL, NULL);
    
        return TRUE;
    }
    

    Voila! It updates the registry.