Работа с каталогами поиска AutoCAD

Дата последнего изменения: 22 марта 2013 (исправлены две ошибки).

    Чтобы найти файл в каталогах поиска AutoCAD, можно воспользоваться методом HostApplicationServices.Current.FindFile(...). Но как быть в том случае, если возникла потребность программного управления данными каталогами?

    В AutoCAD, диалоговое окно Options (открываемое командой _options), на вкладке Files первой же веткой содержит дочернюю ветвь 'Support File Search Path'. В ней инкапсулирован перечень каталогов, по которым AutoCAD осуществляет поиск любой информации в том порядке, в котором указаны каталоги в этой ветви. Саму информацию AutoCAD хранит в ветке реестра текущего профиля пользователя AutoCAD в переменной 'ACAD' в виде текстовой строки, разделённой точкой с запятой. 

    Для того, чтобы иметь возможность работать с этой информацией программно, из плагина AutoCAD, т.е. считывать/изменять/добавлять каталоги поиска - я написал класс FilesSearchPaths, реализующий интерфейс IList<string> и имеющий дополнительное свойство FixedPaths. Для тестирования кода написан другой класс - Test.

Суть работы класса FilesSearchPaths:

    При создании экземпляра класса, происходит автоматическое извлечение из реестра информации о каталогах поиска для текущего профиля AutoCAD. Эта информация, представленная в реестре в виде строки, содержащей перечень каталогов, разделённых символом ";" обрабатывается и сохраняется в скрытом поле экземпляра, имеющим тип IList<string>. В этом поле каждый элемент списка представляет собой одну запись каталога поиска. Теперь с этим классом можно работать как с обычным  IList<string>, но нужно помнить три важных момента (реализовано помимо IList<string>):
  1. Если выполненные изменения нужно сохранить, то после этих изменений следует вызвать метод SaveChanges.
  2.  При изменении текущего профиля AutoCAD, происходит реинициализация экземпляра класса FilesSearchPaths (поскольку назначение класса - работа с текущим профилем AutoCAD), а это значит, что все не сохранённые изменения будут утрачены.
  3. При необходимости, можно вызвать принудительную реинициализацию экземпляра класса FilesSearchPaths, при помощи метода Refresh.
  4. Кроме того, класс FilesSearchPaths содержит в своём составе свойство FixedPaths, доступное только для чтения. В этом свойстве содержится перечень каталогов, в которых так же выполняется поиск, но которые отсутствуют в списке каталогов, указанных в диалоговом окне 'Options'. 
P.S.1. Тем, кто не знаком с IList<T>, следует предварительно обязательно познакомиться с ним.
P.S.2. Код можно использовать в версии .Net Framework 3.5 SP1 и выше.
 
FilesSearchPaths.cs:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Text;
   4:  using Microsoft.Win32;
   5:  using System.Reflection;
   6:  using System.Runtime.InteropServices;
   7:  using System.IO;
   8:  //Acad
   9:  using acad = Autodesk.AutoCAD.ApplicationServices.Application;
  10:  using Autodesk.AutoCAD.ApplicationServices;
  11:  using Autodesk.AutoCAD.DatabaseServices;
  12:  using Autodesk.AutoCAD.EditorInput;
  13:  using Autodesk.AutoCAD.Runtime;
  14:  using Autodesk.AutoCAD.Interop;
  15:   
  16:  namespace AndreyBushman.CadRecovery.Model {
  17:      /// <summary>
  18:      /// Класс инкапсулирует в себе информацию о каталогах, в которых, при необходимости, AutoCAD 
  19:      /// будет выполнять поиск.
  20:      /// </summary>
  21:      public class FilesSearchPaths : IList<String> {
  22:          List<String> paths;
  23:          List<String> paths2;
  24:   
  25:          /// <summary>
  26:          /// Каталоги, по которым выполняется поиск, но на которые пользователь не может повлиять:
  27:          /// - рабочая директория
  28:          /// - директория, в которой находится активный файл
  29:          /// - каталог, в котором находится acad.exe
  30:          /// </summary>
  31:          public String[] FixedPaths { get { return paths2.ToArray(); } }
  32:   
  33:          RegistryKey regKey;
  34:          /// <summary>
  35:          /// Конструктор по умолчанию - производит нужную инициализацию полей
  36:          /// </summary>
  37:          public FilesSearchPaths() {
  38:              Initialize();
  39:              acad.UserConfigurationManager.CurrentProfileChanged += new ProfileEventHandler(
  40:                  UserConfigurationManager_CurrentProfileChanged);
  41:          }
  42:   
  43:          void UserConfigurationManager_CurrentProfileChanged(object sender, ProfileEventArgs e) {
  44:              Initialize();
  45:          }
  46:   
  47:          private void Initialize() {
  48:              string cProfileName = acad.GetSystemVariable("cprofile") as String;
  49:              //Считываем значение ключа продукта (строковое представление ветви реестра соответству-
  50:              //ющей запущенному AutoCAD)
  51:              String productKey = Autodesk.AutoCAD.Runtime.SystemObjects.DynamicLinker.ProductKey;
  52:              String keyName = String.Format(@"{0}\Profiles\{1}\General", productKey, cProfileName);
  53:              regKey = Registry.CurrentUser.OpenSubKey(keyName, true);
  54:              String regValue = regKey.GetValue("Acad", String.Empty) as String;
  55:              regValue = regValue.Substring(0, regValue.Length - 1);
  56:              paths = new List<String>(regValue.Split(';'));
  57:              paths2 = new List<String>();
  58:              paths2.Add(Environment.CurrentDirectory); // Текущий рабочий каталог
  59:              paths2.Add(Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments));
  60:              paths2.Add(((IAcadApplication) acad.AcadApplication).Path);
  61:              if (acad.DocumentManager.MdiActiveDocument != null)
  62:                  paths2.Add(new FileInfo(acad.DocumentManager.MdiActiveDocument.Name).Directory
  63:                      .FullName);
  64:          }
  65:          #region IList<string> Members
  66:          /// <summary>
  67:          /// Получение индекса по значению записи. Если запись не найдена, возвращается -1.
  68:          /// </summary>
  69:          /// <param name="item">Значение записи</param>
  70:          /// <returns>Индекс записи</returns>
  71:          public Int32 IndexOf(String item) {
  72:              return paths.IndexOf(item);
  73:          }
  74:          /// <summary>
  75:          /// Вставить запись в указанную позицию
  76:          /// </summary>
  77:          /// <param name="index">Индекс позиции (нумерация начинается с нуля)</param>
  78:          /// <param name="item">Строковое значение</param>
  79:          public void Insert(Int32 index, String item) {
  80:              paths.Insert(index, item);
  81:          }
  82:          /// <summary>
  83:          /// Удаление записи по индексу
  84:          /// </summary>
  85:          /// <param name="index">Индекс удаляемой записи</param>
  86:          public void RemoveAt(Int32 index) {
  87:              paths.RemoveAt(index);
  88:          }
  89:          /// <summary>
  90:          /// Извлечение записи по индексу
  91:          /// </summary>
  92:          /// <param name="index">Индекс записи</param>
  93:          /// <returns>Запись, хранящаяся под указанным индексом</returns>
  94:          public String this[Int32 index] {
  95:              get {
  96:                  return paths[index];
  97:              }
  98:              set {
  99:                  paths[index] = value;
 100:              }
 101:          }
 102:   
 103:          #endregion
 104:   
 105:          #region ICollection<string> Members
 106:          /// <summary>
 107:          /// Добавление новой записи в конец списка
 108:          /// </summary>
 109:          /// <param name="item">Строковое значение</param>
 110:          public void Add(String item) {
 111:              paths.Add(item);
 112:          }
 113:          /// <summary>
 114:          /// Удаление всех записей
 115:          /// </summary>
 116:          public void Clear() {
 117:              paths.Clear();
 118:          }
 119:          /// <summary>
 120:          /// Проверка на наличие записи в списке
 121:          /// </summary>
 122:          /// <param name="item">Искомое значение</param>
 123:          /// <returns>Результат поиска</returns>
 124:          public Boolean Contains(String item) {
 125:              return paths.Contains(item);
 126:          }
 127:   
 128:          public void CopyTo(String[] array, Int32 arrayIndex) {
 129:              paths.CopyTo(array, arrayIndex);
 130:          }
 131:          /// <summary>
 132:          /// Количество записей
 133:          /// </summary>
 134:          public Int32 Count {
 135:              get { return paths.Count; }
 136:          }
 137:          /// <summary>
 138:          /// Является ли список доступным только для чтения
 139:          /// </summary>
 140:          public Boolean IsReadOnly {
 141:              get { return false; }
 142:          }
 143:          /// <summary>
 144:          /// Удаление записи по указанному строковому значению
 145:          /// </summary>
 146:          /// <param name="item">Искомая строка</param>
 147:          /// <returns>Результат операции.</returns>
 148:          public Boolean Remove(String item) {
 149:              return paths.Remove(item);
 150:          }
 151:   
 152:          #endregion
 153:   
 154:          #region IEnumerable<string> Members
 155:          /// <summary>
 156:          /// Получение нумератора
 157:          /// </summary>
 158:          /// <returns>Нумератор</returns>
 159:          public IEnumerator<String> GetEnumerator() {
 160:              return paths.GetEnumerator();
 161:          }
 162:   
 163:          #endregion
 164:   
 165:          #region IEnumerable Members
 166:          /// <summary>
 167:          /// Получение нумератора
 168:          /// </summary>
 169:          /// <returns>Нумератор</returns>
 170:          System.Collections.IEnumerator System.Collections.IEnumerable.GetEnumerator() {
 171:              return paths.GetEnumerator();
 172:          }
 173:   
 174:          #endregion
 175:          /// <summary>
 176:          /// Сохранение всех произведённых изменений. Если данный метод не вызвать, то не будет 
 177:          /// произведена синхронизация изменений с записью реестра и в результате реестр останется 
 178:          /// не изменённым.
 179:          /// </summary>
 180:          public void SaveChanges() {
 181:              UpdateRegValue(paths);
 182:          }
 183:          /// <summary>
 184:          /// Синхронизация изменений с записью реестра
 185:          /// </summary>
 186:          /// <param name="paths">Список, содержащий обновлённые данные</param>
 187:          private void UpdateRegValue(List<String> paths) {
 188:              StringBuilder newValue = new StringBuilder();
 189:              foreach (String item in paths) {
 190:                  newValue.Append(item);
 191:                  newValue.Append(";");
 192:              }
 193:              acedSetEnv("ACAD", newValue);
 194:          }
 195:   
 196:          /// <summary>
 197:          /// Ветка реестра, содержащая запись "ACAD", в которой инкапсулирована информация о 
 198:          /// каталогах поиска
 199:          /// </summary>
 200:          public RegistryKey RegistryKey {
 201:              get { return regKey; }
 202:          }
 203:   
 204:          /// <summary>
 205:          /// Реинициализация объекта.
 206:          /// </summary>
 207:          public void Refresh() {
 208:              Initialize();
 209:          }
 210:   
 211:          [System.Security.SuppressUnmanagedCodeSecurity]
 212:          [DllImport("acad.exe", CharSet = CharSet.Auto, CallingConvention = CallingConvention
 213:              .Cdecl)]
 214:          private static extern Int32 acedSetEnv(String envName, StringBuilder NewValue);
 215:      }
 216:  }

Test.cs:

   1:  using System;
   2:  using System.Collections.Generic;
   3:  using System.Linq;
   4:  using System.Text;
   5:  using Microsoft.Win32;
   6:  using System.Reflection;
   7:  using System.Runtime.InteropServices;
   8:  //Acad
   9:  using acad = Autodesk.AutoCAD.ApplicationServices.Application;
  10:  using Autodesk.AutoCAD.ApplicationServices;
  11:  using Autodesk.AutoCAD.DatabaseServices;
  12:  using Autodesk.AutoCAD.EditorInput;
  13:  using Autodesk.AutoCAD.Runtime;
  14:   
  15:  namespace AcadCommonLibrary
  16:  {
  17:      /// <summary>
  18:      /// Тестовый класс, предназначенный для демонстрации работы класса FilesSearchPaths
  19:      /// </summary>
  20:      public class Test
  21:      {
  22:          [CommandMethod("hwd", "www", CommandFlags.Modal)]
  23:          public void StartTest()
  24:          {
  25:              Editor ed = acad.DocumentManager.MdiActiveDocument.Editor;
  26:              ed.WriteMessage("Считываем из реестра перечень каталогов, в которых AutoCAD должен выполнять поиск:\n\n");
  27:              FilesSearchPaths x = new FilesSearchPaths();
  28:              int n = 1;
  29:              foreach (string item in x)
  30:              {
  31:                  ed.WriteMessage("\t" + n.ToString() + ".\t" + item + "\n");
  32:                  ++n;
  33:              }
  34:              ed.WriteMessage("\n" + new string('*', 30) + "\n\n");
  35:              //Тестовые каталоги
  36:              string dir1 = @"c:\000";
  37:              string dir2 = @"d:\www";
  38:              string dir3 = @"C:\ObjectARX 2009";
  39:              string dir4 = x[3];
  40:              ed.WriteMessage("Теперь произведём разного рода модификации имеющихся данных:\n");
  41:              ed.WriteMessage(string.Format(@"1. В конец списка добавляем путь '{0}'", dir1) + "\n");
  42:              x.Add(dir1);
  43:              ed.WriteMessage(string.Format(@"2. Под вторым индексом в списке размещаем значение '{0}', сдвигая все последующие записи на одну позицию ниже.", dir2) + "\n");
  44:              x.Insert(2, dir2);
  45:              ed.WriteMessage(string.Format(@"3. Вычисляем индекс записи '{0}'...", dir3) + "\n");
  46:              int i = x.IndexOf(dir3);
  47:              ed.WriteMessage(string.Format("\tЗапись имеет индекс: {0}\n", i == -1 ? "-1, это значит, что запись отсутствует" : i.ToString()));
  48:              ed.WriteMessage(string.Format(@"4. Проверяем, содержится ли в перечне каталогов такой: '{0}'...", dir4) + "\n");
  49:              bool exists = x.Contains(dir4);
  50:              ed.WriteMessage(string.Format("Результат поиска: {0}\n", exists ? "запись существует" : "запись отсутствует"));
  51:              ed.WriteMessage("\nУдаляем первый каталог из списка и сохраняем все выполненные изменения в реестр.\n");
  52:              x.Remove(x[0]);
  53:              x.SaveChanges();
  54:              ed.WriteMessage("\n" + new string('*', 30) + "\n\n");
  55:              ed.WriteMessage(@"Теперь считываем напрямую ИЗ РЕЕСТРА значение переменной 'ACAD', содержащей перечень каталогов, в которых AutoCAD должен выполнять поиск:" + "\n\n");
  56:              n = 1;
  57:              foreach (string item in (Registry.GetValue(x.RegistryKey.Name, "ACAD", string.Empty) as string).Split(';'))
  58:              {
  59:                  if (item != string.Empty)
  60:                  {
  61:                      ed.WriteMessage("\t" + n.ToString() + ".\t" + item + "\n");
  62:                      ++n;
  63:                  }
  64:              }
  65:              ed.WriteMessage("\n" + (new string('*', 20)) + "\n");
  66:              ed.WriteMessage(string.Format("\n Теперь считываем значения каталогов, в которых так же производится поиск, но которые не прописаны в диалоговом окне 'Options':\n"));
  67:              foreach (var item in x.FixedPaths.Distinct())
  68:              {
  69:                  ed.WriteMessage(string.Format("\n'{0}'", item));
  70:              }
  71:          }
  72:      }
  73:  }

Результат работы кода 
Test.cs (тестирование)
:





Метод acedSetEnv взят мною отсюда.

Примечания:
По ряду причин сохранение значения переменной в ветвь реестра приходится делать именно таким способом (т.е. с использованием неуправляемого кода). 
Причины следующие:
1. Если сохранить изменения в реестре иным способом, например через RegistryKey.SetValue, то значение в реестре будет обновлено, однако AutoCAD проигнорирует изменения и при выключении перезапишет значение переменной реестра на старый вариант. 
2. Код acad.UserConfigurationManager.OpenCurrentProfile().OpenSubsection("General").WriteProperty("ACAD", newValue); имеет те же недостатки, что и п.1.
3. Метод Document.SendStringToExecute обрезает слишком длинные строки.

Если нужно просто найти какой-то файл в путях поддержки автокада, то можно воспользоваться таким методом:

   1:  ...
   2:  //AutoCAD namespaces
   3:  using acad = Autodesk.AutoCAD.ApplicationServices.Application;
   4:  ...
   5:  /// <summary>
   6:  /// Метод определяет наличие файла в путях поддержки, прописанный в настройках AutoCAD.
   7:  /// </summary>
   8:  /// <param name="fileName">Имя искомого файла (вместе с указанием расширения)</param>
   9:  /// <returns>Если файл обнаружен - возвращается строковое представление каталога, в котором этот файл находится. Если файл не найден - возвращается null.</returns>
  10:          private string FileExistsInAcadSupportDirectories(string fileName)
  11:          {
  12:              return acad.UserConfigurationManager.OpenCurrentProfile().OpenSubsection("General").ReadProperty("ACAD", string.Empty).ToString().Split(';').Where(n => n != string.Empty).FirstOrDefault(n => File.Exists(Path.Combine(n, fileName)));            
  13:          }

Comments