How to detect installed version of MS-Office?

C#Excel.Net 3.5Ms Office

C# Problem Overview


Does anyone know what would be the best way to detect which version of Office is installed? Plus, if there are multiple versions of Office installed, I'd like to know what versions they are. A bonus would be if I can detect the specific version(s) of Excel that is(/are) installed.

C# Solutions


Solution 1 - C#

One way to check for the installed Office version would be to check the InstallRoot registry keys for the Office applications of interest.

For example, if you would like to check whether Word 2007 is installed you should check for the presence of the following Registry key:

HKLM\Software\Microsoft\Office\12.0\Word\InstallRoot::Path

This entry contains the path to the executable.

Replace 12.0 (for Office 2007) with the corresponding version number:

Office 97   -  7.0
Office 98   -  8.0
Office 2000 -  9.0
Office XP   - 10.0
Office 2003 - 11.0
Office 2007 - 12.0
Office 2010 - 14.0 (sic!)
Office 2013 - 15.0
Office 2016 - 16.0
Office 2019 - 16.0 (sic!)

The other applications have similar keys:

HKLM\Software\Microsoft\Office\12.0\Excel\InstallRoot::Path
HKLM\Software\Microsoft\Office\12.0\PowerPoint\InstallRoot::Path

Or you can check the common root path of all applications:

HKLM\Software\Microsoft\Office\12.0\Common\InstallRoot::Path

Another option, without using specific Registry keys would be to query the MSI database using the MSIEnumProducts API as described here.

As an aside, parallel installations of different Office versions are not officially supported by Microsoft. They do somewhat work, but you might get undesired effects and inconsistencies.

Update: Office 2019 and Office 365

As of Office 2019, MSI-based setup are no longer available, Click-To-Run is the only way to deploy Office now. Together with this change towards the regularly updated Office 365, also the major/minor version numbers of Office are no longer updated (at least for the time being). That means that – even for Office 2019 – the value used in Registry keys and the value returned by Application.Version (e.g. in Word) still is 16.0.

For the time being, there is no documented way to distinguish the Office 2016 from Office 2019. A clue might be the file version of the winword.exe; however, this version is also incremented for patched Office 2016 versions (see the comment by @antonio below).

If you need to distinguish somehow between Office versions, e.g. to make sure that a certain feature is present or that a minimum version of Office is installed, probably the best way it to look at the file version of one of the main Office applications:

// Using the file path to winword.exe
// Retrieve the path e.g. from the InstallRoot Registry key
var fileVersionInfo = FileVersionInfo.GetVersionInfo(@"C:\Program Files (x86)\Microsoft Office\root\Office16\WINWORD.EXE");
var version = new Version(fileVersionInfo.FileVersion);

// On a running instance using the `Process` class
var process = Process.GetProcessesByName("winword").First();
string fileVersionInfo = process.MainModule.FileVersionInfo.FileVersion;
var version = Version(fileVersionInfo);

The file version of Office 2019 is 16.0.10730.20102, so if you see anything greater than that you are dealing with Office 2019 or a current Office 365 version.

Solution 2 - C#

How about HKEY_CLASSES_ROOT\Word.Application\CurVer?

Solution 3 - C#

If you've installed 32-bit Office on a 64-bit machine, you may need to check for the presence of "SOFTWARE\Wow6432Node\Microsoft\Office\12.0", substituting the 12.0 with the appropriate version. This is certainly the case for Office 2007 installed on 64-bit Windows 7.

Note that Office 2010 (== 14.0) is the first Office for which a 64-bit version exists.

Solution 4 - C#

namespace Software_Info_v1._0
{
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.Office.Interop;

public class MS_Office
{
    public string GetOfficeVersion()
    {
        string sVersion = string.Empty;
        Microsoft.Office.Interop.Word.Application appVersion = new Microsoft.Office.Interop.Word.Application();
        appVersion.Visible = false;
        switch (appVersion.Version.ToString())
        {
            case "7.0":
                sVersion = "95";
                break;
            case "8.0":
                sVersion = "97";
                break;
            case "9.0":
                sVersion = "2000";
                break;
            case "10.0":
                sVersion = "2002";
                break;
            case "11.0":
                sVersion = "2003";
                break;
            case "12.0":
                sVersion = "2007";
                break;
            case "14.0":
                sVersion = "2010";
                break;
            default:
                sVersion = "Too Old!";
                break;
        }
        Console.WriteLine("MS office version: " + sVersion);
        return null;
    }



}
}

Solution 5 - C#

Despite the fact that this question has been answered long time ago, I found some interesting facts to add that are related to the answers above.

As Dirk mentioned, there seems to be a weird fashion of version control from MS, starting from Office 365 / 2019. You cannot distinguish among the three(2016, 2019, O365), by seeing at the executable paths anymore. And just like he reputed himself, looking at the builds of the executable, as a mean of telling which is what, isn't quite effective either.

After some researching, I found a feasible solution. The solution lies under the registry subkey Computer\HKEY_CURRENT_USER\Software\Microsoft\Office\16.0\Common\Licensing\LicensingNext.

So, my logic follows below:

Case 1: If the computer has the MSOffice 2016 installed, there is no subkeys under Licensing.

Case 2: if the computer has MSOffice 2019 installed, there is the name of the value (which is one of the Office Product ID). (e.g. Standard2019Volume)

Case 3: if the computer has Office365 installed, there is a value called o365bussinessretail(which is also a product ID) along with some other values.

The possible productIds are provided here.

To distinguish the three, I just opened the key and see if fails. If the open fails, its Office 2016. Then I enumerate LicensingNext and try to see if any name has a prefix o365, if it finds it then its O365. If it does not, then its Office 2019.

Frankly speaking, I did not have enough time to test the logic under varying environment. So please, note that.

Hope this will help whoever's interest.

Solution 6 - C#

Why not check HKLM\\SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\App Paths\\[office.exe], where [office.exe] stands for particular office product exe-filename, e.g. winword.exe, excel.exe etc. There you get path to executable and check version of that file.

How to check version of the file: in C++ / in C#

Any criticism towards such approach?

Solution 7 - C#

I figured out an elegant way to detect the "Microsoft Office Version" that perhaps also works, when you want to detect if you have "Office 2016" or "Office 2019" installed.

I just detected the installation path of "Microsoft Office" then declared a System.Diagnostics.FileVersionInfo from an exe-file of an "Office"-Application (in my examle "Word") and got everything i need from this FileVersionInfo.

Here is the full code of a very small console application as an example:

using System;
using System.IO;
using System.Diagnostics;
using System.Runtime.InteropServices;

namespace Sandbox_Console
{
    public class Program
    {
        static void Main(string[] args)
        {
            string installpath = GetOfficeInstallPath();
            OfficeVersionInfo info = new OfficeVersionInfo(installpath);
            Console.WriteLine("Full Office Version Number: " + info.FullVersionNumber);
            Console.WriteLine("Full Office Name: " + info.FullOfficeVersionName);
            Console.Write("Press any key to end this program...");
            Console.ReadKey();
        }

        public class OfficeVersionInfo
        {
            private string _FullVersionNumber = "";
            private string _FullOfficeVersionName = "";

            public string FullVersionNumber { get { return _FullVersionNumber; } }
            public string FullOfficeVersionName { get { return _FullOfficeVersionName; } }

            public OfficeVersionInfo(string installPath)
            {
                string filepath = installPath + "\\winword.exe"; //For Excel, PowerPoint or others use the exe-files for them.
                if (File.Exists(filepath))
                {
                    FileVersionInfo info = FileVersionInfo.GetVersionInfo(filepath);
                    _FullVersionNumber = info.ProductVersion;
                    _FullOfficeVersionName = info.ProductName;
                }
            }
        }

        public static string GetOfficeInstallPath()
        {
            string result = "";
            try
            {
                Type appType = Type.GetTypeFromProgID("Word.Application"); //Also works with Excel, PowerPoint or others.
                dynamic appInstance = Activator.CreateInstance(appType);
                result = appInstance.Path;
                appInstance.quit();
                Marshal.ReleaseComObject(appInstance);
            } catch (Exception exc)
            {
                Debug.Print(exc.Message);
            }
            return result;
        }  
    }
}

The infos stored in "OfficeVersionInfo" in the code example are the infos you get when you rightclick on the "winword.exe" and then click on "Properties". See this: Details of winword.exe

Solution 8 - C#

> A bonus would be if I can detect the specific version(s) of Excel that is(/are) installed.

I know the question has been asked and answered a long time ago, but this same question has kept me busy until I made this observation:

To get the build number (e.g. 15.0.4569.1506), probe HKLM\SOFTWARE\Microsoft\Office\[VER]\Common\ProductVersion::LastProduct, where [VER] is the major version number (12.0 for Office 2007, 14.0 for Office 2010, 15.0 for Office 2013).

On a 64-bit Windows, you need to insert Wow6432Node between the SOFTWARE and Microsoft crumbs, irrespective of the bitness of the Office installation.

On my machines, this gives the version information of the originally installed version. For Office 2010 for instance, the numbers match the ones listed here, and they differ from the version reported in File > Help, which reflects patches applied by hotfixes.

Solution 9 - C#

        public string WinWordVersion
        {
            get
            {
                string _version = string.Empty;
                Word.Application WinWord = new Word.Application();   
             
                switch (WinWord.Version.ToString())
                {
                    case "7.0":  _version = "95";
                        break;
                    case "8.0": _version = "97";
                        break;
                    case "9.0": _version = "2000";
                        break;
                    case "10.0": _version = "2002";
                        break;
                    case "11.0":  _version = "2003";
                        break;
                    case "12.0": _version = "2007";
                        break;
                    case "14.0": _version = "2010";
                        break;
                    case "15.0":  _version = "2013";
                        break;
                    case "16.0": _version = "2016";
                        break;
                    default:                            
                        break;
                }

                return WinWord.Caption + " " + _version;
            }
        }

Solution 10 - C#

To whoever it might concern, here's my version that checks for Office 95-2019 & O365, both MSI based and ClickAndRun are supported, on both 32 and 64 bit systems (falls back to 32 bits when 64 bit version is not installed).

Written in Python 3.5 but of course you can always use that logic in order to write your own code in another language:

from winreg import *
from typing import Tuple, Optional, List

# Let's make sure the dictionnary goes from most recent to oldest
KNOWN_VERSIONS = {
    '16.0': '2016/2019/O365',
    '15.0': '2013',
    '14.0': '2010',
    '12.0': '2007',
    '11.0': '2003',
    '10.0': '2002',
    '9.0': '2000',
    '8.0': '97',
    '7.0': '95',
}


def get_value(hive: int, key: str, value: Optional[str], arch: int = 0) -> str:
    """
    Returns a value from a given registry path

    :param hive: registry hive (windows.registry.HKEY_LOCAL_MACHINE...)
    :param key:  which registry key we're searching for
    :param value: which value we query, may be None if unnamed value is searched
    :param arch: which registry architecture we seek (0 = default, windows.registry.KEY_WOW64_64KEY, windows.registry.KEY_WOW64_32KEY)
                 Giving multiple arches here will return first result
    :return: value
    """

    def _get_value(hive: int, key: str, value: Optional[str], arch: int) -> str:
        try:
            open_reg = ConnectRegistry(None, hive)
            open_key = OpenKey(open_reg, key, 0, KEY_READ | arch)
            value, type = QueryValueEx(open_key, value)
            # Return the first match
            return value
        except (FileNotFoundError, TypeError, OSError) as exc:
            raise FileNotFoundError('Registry key [%s] with value [%s] not found. %s' % (key, value, exc))

    # 768 = 0 | KEY_WOW64_64KEY | KEY_WOW64_32KEY (where 0 = default)
    if arch == 768:
        for _arch in [KEY_WOW64_64KEY, KEY_WOW64_32KEY]:
            try:
                return _get_value(hive, key, value, _arch)
            except FileNotFoundError:
                pass
        raise FileNotFoundError
    else:
        return _get_value(hive, key, value, arch)


def get_keys(hive: int, key: str, arch: int = 0, open_reg: HKEYType = None, recursion_level: int = 1,
             filter_on_names: List[str] = None, combine: bool = False) -> dict:
    """
    :param hive: registry hive (windows.registry.HKEY_LOCAL_MACHINE...)
    :param key: which registry key we're searching for
    :param arch: which registry architecture we seek (0 = default, windows.registry.KEY_WOW64_64KEY, windows.registry.KEY_WOW64_32KEY)
    :param open_reg: (handle) handle to already open reg key (for recursive searches), do not give this in your function call
    :param recursion_level: recursivity level
    :param filter_on_names: list of strings we search, if none given, all value names are returned
    :param combine: shall we combine multiple arch results or return first match
    :return: list of strings
    """
    def _get_keys(hive: int, key: str, arch: int, open_reg: HKEYType, recursion_level: int, filter_on_names: List[str]):
        try:
            if not open_reg:
                open_reg = ConnectRegistry(None, hive)
            open_key = OpenKey(open_reg, key, 0, KEY_READ | arch)
            subkey_count, value_count, _ = QueryInfoKey(open_key)

            output = {}
            values = []
            for index in range(value_count):
                name, value, type = EnumValue(open_key, index)
                if isinstance(filter_on_names, list) and name not in filter_on_names:
                    pass
                else:
                    values.append({'name': name, 'value': value, 'type': type})
            if not values == []:
                output[''] = values

            if recursion_level > 0:
                for subkey_index in range(subkey_count):
                    try:
                        subkey_name = EnumKey(open_key, subkey_index)
                        sub_values = get_keys(hive=0, key=key + '\\' + subkey_name, arch=arch,
                                              open_reg=open_reg, recursion_level=recursion_level - 1,
                                              filter_on_names=filter_on_names)
                        output[subkey_name] = sub_values
                    except FileNotFoundError:
                        pass

            return output

        except (FileNotFoundError, TypeError, OSError) as exc:
            raise FileNotFoundError('Cannot query registry key [%s]. %s' % (key, exc))

    # 768 = 0 | KEY_WOW64_64KEY | KEY_WOW64_32KEY (where 0 = default)
    if arch == 768:
        result = {}
        for _arch in [KEY_WOW64_64KEY, KEY_WOW64_32KEY]:
            try:
                if combine:
                    result.update(_get_keys(hive, key, _arch, open_reg, recursion_level, filter_on_names))
                else:
                    return _get_keys(hive, key, _arch, open_reg, recursion_level, filter_on_names)
            except FileNotFoundError:
                pass
        return result
    else:
        return _get_keys(hive, key, arch, open_reg, recursion_level, filter_on_names)


def get_office_click_and_run_ident():
    # type: () -> Optional[str]
    """
    Try to find the office product via clickandrun productID
    """
    try:
        click_and_run_ident = get_value(HKEY_LOCAL_MACHINE,
                                                 r'Software\Microsoft\Office\ClickToRun\Configuration',
                                                 'ProductReleaseIds',
                                                 arch=KEY_WOW64_64KEY |KEY_WOW64_32KEY,)
    except FileNotFoundError:
        click_and_run_ident = None
    return click_and_run_ident


def _get_used_word_version():
    # type: () -> Optional[int]
    """
    Try do determine which version of Word is used (in case multiple versions are installed)
    """
    try:
        word_ver = get_value(HKEY_CLASSES_ROOT, r'Word.Application\CurVer', None)
    except FileNotFoundError:
        word_ver = None
    try:
        version = int(word_ver.split('.')[2])
    except (IndexError, ValueError, AttributeError):
        version = None
    return version


def _get_installed_office_version():
    # type: () -> Optional[str, bool]
    """
    Try do determine which is the highest current version of Office installed
    """
    for possible_version, _ in KNOWN_VERSIONS.items():
        try:
            office_keys = get_keys(HKEY_LOCAL_MACHINE,
                                               r'SOFTWARE\Microsoft\Office\{}'.format(possible_version),
                                               recursion_level=2,
                                               arch=KEY_WOW64_64KEY |KEY_WOW64_32KEY,
                                               combine=True)

            try:
                is_click_and_run = True if office_keys['ClickToRunStore'] is not None else False
            except:
                is_click_and_run = False

            try:
                is_valid = True if office_keys['Word'] is not None else False
                if is_valid:
                    return possible_version, is_click_and_run
            except KeyError:
                pass
        except FileNotFoundError:
            pass
    return None, None


def get_office_version():
    # type: () -> Tuple[str, Optional[str]]
    """
    It's plain horrible to get the office version installed
    Let's use some tricks, ie detect current Word used
    """

    word_version = _get_used_word_version()
    office_version, is_click_and_run = _get_installed_office_version()

    # Prefer to get used word version instead of installed one
    if word_version is not None:
        office_version = word_version

    version = float(office_version)
    click_and_run_ident = get_office_click_and_run_ident()

    def _get_office_version():
        # type: () -> str
        if version:
            if version < 16:
                try:
                    return KNOWN_VERSIONS['{}.0'.format(version)]
                except KeyError:
                    pass
            # Special hack to determine which of 2016, 2019 or O365 it is
            if version == 16:
                if isinstance(click_and_run_ident, str):
                    if '2016' in click_and_run_ident:
                        return '2016'
                    if '2019' in click_and_run_ident:
                        return '2019'
                    if 'O365' in click_and_run_ident:
                        return 'O365'
                return '2016/2019/O365'
        # Let's return whatever we found out
        return 'Unknown: {}'.format(version, click_and_run_ident)

    if isinstance(click_and_run_ident, str) or is_click_and_run:
        click_and_run_suffix = 'ClickAndRun'
    else:
        click_and_run_suffix = None

    return _get_office_version(), click_and_run_suffix

You can than use the code like the following example:

office_version, click_and_run = get_office_version()
print('Office {} {}'.format(office_version, click_and_run))

Remarks

  • Didn't test with office < 2010 though
  • Python typing is different between the registry functions and the office functions since I wrote the registry ones before finding out that pypy / python2 does not like typing... On those python interpreters you might just remove typing completly
  • Any improvements are highly welcome

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
Questioncode4lifeView Question on Stackoverflow
Solution 1 - C#Dirk VollmarView Answer on Stackoverflow
Solution 2 - C#BobL2112View Answer on Stackoverflow
Solution 3 - C#JasonView Answer on Stackoverflow
Solution 4 - C#zeeView Answer on Stackoverflow
Solution 5 - C#vaska11View Answer on Stackoverflow
Solution 6 - C#John BoscoView Answer on Stackoverflow
Solution 7 - C#GeoMHView Answer on Stackoverflow
Solution 8 - C#bovenderView Answer on Stackoverflow
Solution 9 - C#Rakesh Ravi GView Answer on Stackoverflow
Solution 10 - C#Orsiris de JongView Answer on Stackoverflow