65

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.

code4life
  • 15,053
  • 7
  • 47
  • 80

9 Answers9

80

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.

Dirk Vollmar
  • 161,833
  • 52
  • 243
  • 303
  • 1
    Do we also need to check HKCU? Because that's where my registry keys are located. Thanks! – Reza S Nov 07 '11 at 22:21
  • 11
    I'm sure it does not need to be said and is somewhat superfluous as a comment, however, the reason that there is no version between 12.0 and 14.0 is the same reason there is not a similar numbered floor in high rise buildings. – Hamp Dec 07 '12 at 16:32
  • 1
    Note that if you use the Common key (to check the common root for all office apps), that won't work in version 15.0. – Scott Whitlock Mar 05 '14 at 15:54
  • Is this still accurate? I've looked at a lot of Windows 8 systems running Word 2013 that don't have the InstallRoot key recently. Has Word been changed to put the key somewhere else? – Hecksa Sep 08 '14 at 12:31
  • 5
    @Hecksa: Is this a Click-To-Run installation? They might handle this differently. Please also note that on 64-bit systems you have to look in `HKEY_LOCAL_MACHINE\SOFTWARE\WOW6432Node\Microsoft\Office` when using regedit. – Dirk Vollmar Sep 26 '14 at 09:35
  • @0xA3: It's not click to run - your point on 64-bit systems is interesting as I certainly didn't check that location in regedit. I have, however, found the key in the expected (32bit) location on the majority of 64 bit machines anyway, which is a little confusing. I suspect the location isn't as simple as 64 bit in one place, 32 in another? I'm currently working pretty heavily on this issue as it's causing some of our user's some serious pain - I'll try to keep this updated if I find anything that can clarify the answer for the future. – Hecksa Sep 26 '14 at 10:22
  • 2
    How would you check if its a 32bit version of Office or 64 bit version? – Benjamin Jones Oct 08 '15 at 02:01
  • This does not seem to be consistently populated - I have two machines with Excel 2016 installed, one had the `InstallRoot::Path` key described above, the other didn't. BobL2112's suggestion below to use `HKEY_CLASSES_ROOT\Word.Application\CurVer` is simpler and less fragile. – mpeac Dec 06 '16 at 03:36
  • 1
    @mpeac: Did you read the comment on 64-bit vs. 32-bit registry keys? – Dirk Vollmar Dec 06 '16 at 13:10
  • 1
    Word 2019 also show version 16.0. So the question is how to distinguish 2016 from 2019 version. – Dmitri Kouminov Oct 23 '18 at 15:01
  • @DmitriKouminov: Thanks for the hint. Hopefully my update helps. – Dirk Vollmar Oct 24 '18 at 13:10
  • @dirk-vollmar : Be careful, with build version on Office 2019; I'm using Office 2016 Plus version 16.0.10827.20181 which not contains Office 2019 new features. – antonio Nov 05 '18 at 05:36
24

How about HKEY_CLASSES_ROOT\Word.Application\CurVer?

BobL2112
  • 241
  • 2
  • 2
  • 1
    This is my favorite as it avoids the Wow64 issue, and also supports the case where multiple office versions are installed and if so I believe CurVer is set to the the last version launched. – JohnZaj Nov 23 '15 at 22:03
  • Wish I saw this earlier, works a treat in Inno Setup as well. I'm using it like this: RegQueryStringValue(HKEY_CLASSES_ROOT, 'Word.Application\CurVer', '', myString) – SlowLearner Oct 21 '16 at 05:05
  • This is a good answer, but for clarity, it only gets the internal version, e.g. `Word.Application.15`, etc. It does not return file or registry paths. You still have to get that separately. – Chris Oct 18 '17 at 18:13
  • I had problems with this method, when many versions of office / office application was installed, It seems to store the first version installed, not the latest – Lord Darth Vader Oct 15 '18 at 08:44
  • Word 2019 also show Word.Application.16. So the question is how to distinguish 2016 from 2019 version. – Dmitri Kouminov Oct 23 '18 at 15:00
17

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.

Hugh W
  • 219
  • 1
  • 6
  • 16
Jason
  • 7,950
  • 9
  • 53
  • 65
8
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;
    }



}
}
zee
  • 405
  • 1
  • 7
  • 13
  • 2
    This looks like the same proposed solution linked to by Longball27, except updated to handle Office 2010. There are two problems with this. The Microsoft.Office.Interop.Word dll may not be installed even if Word is installed, for example if Word was installed before .Net was installed or if a partial install of Office was done instead of a complete install. This is perhaps most typical for partial installs of only Outlook - the corresponding Microsoft.Office.Interop.Outlook dll is typically not installed until the user downloads and installs the "Primary Interop Assembly" for Outlook. – RenniePet Feb 18 '12 at 17:39
  • 4
    The other (very minor) problem is that the "default" clause on the "switch" statement is saying "Too Old!" for unknown versions. A more likely cause of that code being executed is that the version of Word (or whatever) is too new. This was in fact illustrated by the update made to this program to handle Office 2010 - before that update was applied the previous version of this program was saying that Office 2010 was too old! – RenniePet Feb 18 '12 at 17:45
  • getting error Retrieving the COM class factory for component with CLSID {00024500-0000-0000-C000-000000000046} failed due to the following error: 800702e4 The requested operation requires elevation. (Exception from HRESULT: 0x800702E4). – Neo Oct 01 '14 at 08:39
  • Does this code fails if Office not installed in machine ? http://stackoverflow.com/questions/7123196/how-to-check-programatically-if-ms-excel-exists-on-a-pc – Kiquenet Mar 25 '15 at 12:03
6

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?

Community
  • 1
  • 1
John Bosco
  • 183
  • 1
  • 10
  • 1
    This worked for me *very* well and eliminated issues I had regarding x86 vs 64bit and across slightly different installation setups. – Hanny Mar 07 '17 at 16:13
  • 1
    Note: when you install 2 different versions (e.g. Excel 2016 and Excel 2007), this always seems to point to the last recent installation (i.e. it does not point to the version that has last been started. – TmTron Jan 11 '18 at 13:11
5

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.

vaska11
  • 179
  • 1
  • 10
2

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.

Chris
  • 3,181
  • 1
  • 21
  • 39
bovender
  • 1,546
  • 13
  • 28
1
        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;
            }
        }
0

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
Orsiris de Jong
  • 1,678
  • 1
  • 16
  • 32