Приложение Navisworks

О функциональности приложения По существу, доступная функциональность COM-оболочки приложения Navisworks ограничивается следующими действиями:

  • открыть проект Navisworks (*.nwd; *.nwf);
  • подключить к проекту данные (аналог команды "Главная - Добавить - ... ");
  • сохранить проект в файл;
  • получить доступ к содержимому проекту (COM-оболочка InwOpState);

Примечание: добиться работы метода AppendFile у меня не получилось - выбрасывало странное исключение про файл.

Наиболее важное доступное действие - это получение доступа к содержимому проекту (COM-оболочка InwOpState), т.к. именно с ней идут все остальные действия с COM API.

О получении приложения Есть 2 варианта взаимодействия с API - для случая внутреннего подключения через .NET API и в случае внешнего взаимодействия. Рассмотрим 2 эти варианта в отдельности

Внешнее взаимодействие

Рассмотрим случай подключения к Navisworks из-под стороннего процесса, создания нового процесса и переходу к COM из-под .NET API

Navisworks, как приложение, описывается отдельной библиотекой типов под названием "NavisworksAutomation". Её необходимо подключить к проекту вручную. Также необходимо подключить Intergated API. ![[Pasted image 20260418201504.png]]

Приложение Navisworks описывается COM-оболочкой NavisworksAutomationAPI18.Document (где 18 - номер версии Navisworks, в данном случае 2021; приведение нужно скорее для знакомства с API, можно использовать и dynamic, прим. автора). У любого приложения Navisworks процесс имеет постоянный префикс Navisworks.Document. По нему можно идентифицировать запущенные процессы, либо создать новый процесс. В табличке ниже приведена информация о соответствии версии NW и ProgId - она фактически "линейная".

Версия NWСоответствующий ProgIdПримечание
Navisworks 2015Navisworks.Document.12
Navisworks 2016Navisworks.Document.13
Navisworks 2017Navisworks.Document.14
Navisworks 2018Navisworks.Document.15
Navisworks 2019Navisworks.Document.16
Navisworks 2020Navisworks.Document.17
Navisworks 2021Navisworks.Document.18
Navisworks 2022Navisworks.Document.1919.1 : 2022.1
Navisworks 2023Navisworks.Document.2020.1 : 2023.1
Navisworks 2024Navisworks.Document.21
Navisworks 2025Navisworks.Document.22
Navisworks 2026Navisworks.Document.23
Navisworks 2027Navisworks.Document.24

Подключение к запущенному Navisworks

Для подключения к приложению Navisworks (если оно запущено) из внешнего процесса необходимо найти среди запущенных процессов начинающиеся на Navisworks.Document и далее использовать его.

Хорошая реализация процесса получения запущенных COM-приложений приведена тут. На всякий случай приведу её листинг ниже:

using Microsoft.Win32;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Runtime.InteropServices.ComTypes;
using System.Text;

public class COM_Interaction
{
    private COM_Interaction() { }

    public static List<string> GetCOMProcesses()
    {
        StringBuilder SB = new StringBuilder();
        List<string> processes = new List<string>();
        foreach (var moniker in EnumRunningObjects())
        {
            List<string> t1 = GetMonikerString(moniker).Split('\t').ToList();
            processes = processes.Concat(t1).ToList();
        }
        return processes;
    }
    private const int S_OK = 0x00000000;

    [DllImport("ole32.dll")]
    private static extern int GetRunningObjectTable(uint reserved, out IRunningObjectTable pprot);

    [DllImport("ole32.dll")]
    private static extern int CreateBindCtx(uint reserved, out IBindCtx ppbc);

    private static void OleCheck(string message, int result)
    {
        if (result != S_OK)
            throw new COMException(message, result);
    }

    private static IEnumerable<IMoniker> EnumRunningObjects()
    {
        IRunningObjectTable objTbl;
        OleCheck("GetRunningObjectTable failed", GetRunningObjectTable(0, out objTbl));
        IEnumMoniker enumMoniker;
        IMoniker[] monikers = new IMoniker[1];
        objTbl.EnumRunning(out enumMoniker);
        enumMoniker.Reset();
        while (enumMoniker.Next(1, monikers, IntPtr.Zero) == S_OK)
        {
            yield return monikers[0];
        }
    }

    private static bool TryGetCLSIDFromDisplayName(string displayName, out string clsid)
    {
        var bBracket = displayName.IndexOf("{");
        var eBracket = displayName.IndexOf("}");
        if (bBracket > 0 && eBracket > 0 && eBracket > bBracket)
        {
            clsid = displayName.Substring(bBracket, eBracket - bBracket + 1);
            return true;
        }
        else
        {
            clsid = string.Empty;
            return false;
        }
    }

    private static string ReadSubKeyValue(string keyName, RegistryKey key)
    {
        var subKey = key.OpenSubKey(keyName);
        if (subKey != null)
        {
            using (subKey)
            {
                var value = subKey.GetValue("");
                return value == null ? string.Empty : value.ToString();
            }
        }
        return string.Empty;
    }

    private static string GetMonikerString(IMoniker moniker)
    {
        IBindCtx ctx;
        OleCheck("CreateBindCtx failed", CreateBindCtx(0, out ctx));
        var sb = new StringBuilder();
        string displayName;
        moniker.GetDisplayName(ctx, null, out displayName);
        sb.Append(displayName);
        sb.Append('\t');
        string clsid;
        if (TryGetCLSIDFromDisplayName(displayName, out clsid))
        {
            var regClass = Registry.ClassesRoot.OpenSubKey("\\CLSID\\" + clsid);
            if (regClass != null)
            {
                using (regClass)
                {
                    sb.Append(regClass.GetValue(""));
                    sb.Append('\t');
                    sb.Append(ReadSubKeyValue("ProgID", regClass));
                    sb.Append('\t');
                    sb.Append(ReadSubKeyValue("LocalServer32", regClass));
                }
            }
        }
        return sb.ToString();
    }
}

Пример работы с листингом выше может выглядеть так (для метода GetNWInstance):

using System.Runtime.InteropServices;

[DllImport("ole32.dll")]
public static extern int GetActiveObjectExt(ref Guid rclsid, IntPtr reserved, [MarshalAs(UnmanagedType.Interface)] out object ppunk);

public static object? GetNWInstance()
{
    foreach (string comProcId in COM_Interaction.GetCOMProcesses())
    {
        if (comProcId.Contains("Navisworks.Document."))
        {
            var type = Type.GetTypeFromProgID(comProcId);
            var guid = type.GUID;

            object obj;
            int result = GetActiveObjectExt(ref guid, IntPtr.Zero, out obj);

            return obj;
        }
    }
    return null;
}

Примечание: в листинге выше используется обращение к GetActiveObjectExt, это универсальная конструкция для .NET5+ и .NET Framework.

Запуск нового экземпляра Navisworks

Если на данном ПК установлен любой Navisworks, то можно запустить его экземпляр для заданной версии ProgID (см. табличку в начале раздела).

string nwProgID = "Navisworks.Document.18"; // for NW 2021
var type = System.Type.GetTypeFromProgID(nwProgID);
object nwApp = System.Activator.CreateInstance(type);

Подключение к COM из-под .NET API

Для этого в .NET API имеется специальный класс Autodesk.Navisworks.Api.ComApi.ComApiBridge (из Autodesk.Navisworks.ComApi.dll), а само COM API доступно через пространство имён Autodesk.Navisworks.Api.Interop. Тогда переход к COM-оболочки модели будет выглядеть как:

using ComBridge = Autodesk.Navisworks.Api.ComApi.ComApiBridge;
using COMApi = Autodesk.Navisworks.Api.Interop.ComApi;

COMApi.InwOpState oState = ComBridge.State;