Настройка автосохранения в NC 23+
Автор: Кулик Алексей aka kpblc | Дата: 3 Апрель 2025 · Прокомментировать
На работе возник вопрос "куда наник выполняет автосохранение?" А, и заодно - формат файла по умолчанию поменять бы на Dwg2013. Клиентов энное количество, к каждому не набегаешься.
Да ну, чего там сложного - подумал я. И, как обычно, ошибся
Все проворачиваю на NC23 (на ноуте установлен только он), хотя технология отлично сработала и на 23.1.

Все, что выделено в рамочке, и буду ковырять.
Но предварительно хотелось бы рассмотреть настройки папок автосохранения, резервной копии, файлов истории. По умолчанию там стоят пустые строки. Что сие значит?
Насколько я понимаю, при таких значениях наник пытается сохранить сооответствующие файлы рядом с открытым dwg. Но вот если права на запись в каталог нету, то все эти баки и автосейвы кидаются… В %temp%? Ну да, под Windows – скорее всего именно туда. А вот под Linux место хранения может гулять как угодно. Лично наблюдал, как автосохранение отправлялось в c:\users\root\tmp (путь стырен из ком.строки наника, так что не удивляемся). Хотя логин пользователя далеко не root. Так что лучше бы эти настройки поменять на что-то более вменяемое.
Нашел только переменные по истории файлов, длительности истории и количества версий файла. В остальном никаких системных переменных я не нашел; лисповые getenv / setenv в нанике работают странновато по сравнению с акадом. Так что придется лезьть напрямую в реестр, и править все именно там.
И тут есть проблема: допустим, я поменял каталоги автосохранения & Co. А читает ли наник эти настройки каждый раз, как ему приходится выполнять автосохранение? ИМХО лучше всего не экспериментировать на ровном месте и просто просить пользователя после смены каталогов перезапустить наник.
Для начала создам-ка несколько перечислений – для формата файла, для настроек инкрементального сохранения, и, пожалуй, для “применять выбранный формат”. Перечисления привожу чохом, хотя в проекте, конечно, они разнесены по разным файлам.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 | using System.ComponentModel; namespace NCadSave.Infrastructure.Enums { /// <summary>Формат для сохранения файла</summary> public enum DwgFormatForSave { [Description("Dwg 2018")] Dwg2018 = 1, [Description("Dwg 2013")] Dwg2013 = 2, [Description("Dwg 2010")] Dwg2010 = 3, [Description("Dwg 2007")] Dwg2007 = 4, [Description("Dwg 2004")] Dwg2005, [Description("Dwg 2000")] Dwg2000 = 6, } /// <summary>Режим инкрементального сохранения</summary> public enum IncrementalSaveMode { [Description("Отключено")] Off = 0, [Description("При сохранении")] OnSave = 1, [Description("При автосохранении и сохранении")] OnSaveAndAutoSave = 2, } /// <summary>Использование выбранного формата хранения</summary> public enum UseFormat { [Description("Не использовать")] Off = 0, [Description("Для новых документов")] NewDocuments = 1, [Description("Для всех документов")] AllDocuments = 2, } }[//cc] Тут получилось очень забавно с форматами файлов. Похоже, нумерация идет как в выпадающем списке, и начинается с 1. Только из-за этого и пришлось прописывать значения для перечисления. Остальные, можно сказать, уже по инерции назначил ;) Теперь делаю класс собственно настроек: [cc lang="csharp"]using NCadSave.Infrastructure.Enums; namespace NCadSave.Infrastructure { /// <summary>Настройки, относящиеся к хранению файлов в NCad</summary> public class SaveSettings { /// <summary>Режим инкрементального сохранения</summary> public IncrementalSaveMode IncSaveMode { get; set; } /// <summary>Формат по умолчанию для сохранения файла</summary> public DwgFormatForSave DefaultFormat { get; set; } /// <summary>Применять выбранныйй формат <see cref="DefaultFormat"/> к документам...</summary> public UseFormat UseDefaultFormat { get; set; } /// <summary>Каталог автосохранения</summary> public string AutosaveFolder { get; set; } /// <summary>Каталог с файлами историй</summary> public string HistoryFolder { get; set; } /// <summary>Время автосохранения</summary> public byte AutosaveTimeout { get; set; } /// <summary>Создавать bak</summary> public bool CreateBak { get; set; } /// <summary>Создавать bak оригинала</summary> public bool CreateOriginalBak { get; set; } } } |
Теперь работа с настройками )) Поскольку работа будет с реестром, в конструкторе будет параметр – имя ветки реестра, внутри которой в общем-то все и будет болтаться. Чтение настроек можно сделать как в классе, так и отдельным методом. Сделаю, пожалуй, отдельным методом – и в таком случае у класса будет 3 метода: чтение, запись, сброс до рекомендованных значений. Пока ставлю на них заглушки, а “рекомендованные значения” приколочу гвоздями в коде: прописывать еще и сериализацию откровенно лень.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 | namespace NCadSave.Infrastructure { public class NanoCadSettings { /// <summary>Настройик NCad. В текущих реалиях - только касаемо автосохранения</summary> /// <param name="registryHiveName">Путь к разделу реестр, в конце обязательно должен включать подкаталог Profiles\ProfileName</param> public NanoCadSettings(string registryHiveName) { RegistryHiveName = registryHiveName; } /// <summary>Чтение имеющихся настроек nanoCAD из реестра</summary> /// <returns></returns> /// <exception cref="NotImplementedException"></exception> public SaveSettings GetSaveSettings() { throw new NotImplementedException(); } /// <summary>Сохранение настроек nanoCAD</summary> /// <param name="settings"></param> /// <exception cref="NotImplementedException"></exception> public void KeepSaveSettings(SaveSettings settings) { throw new NotImplementedException (); } /// <summary>Восстановить рекомендованные значения</summary> /// <exception cref="NotImplementedException"></exception> public void ResetToRecommendedSaveSettings() { throw new NotImplementedException (); } public string RegistryHiveName { get; } } } |
Сначала, пожалуй, сделаю чтение. Пока писал, понял, что забыл про каталог страховых копий. В репозитории все есть, так что здесь править не буду
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | /// <summary>Чтение имеющихся настроек nanoCAD из реестра</summary> /// <returns></returns> /// <exception cref="NotImplementedException"></exception> public SaveSettings GetSaveSettings() { SaveSettings result = new SaveSettings(); RegistryKey hkcu = Registry.CurrentUser; using (RegistryKey hive = hkcu.OpenSubKey(_autosaveFolderHive)) { result.AutosaveFolder = hive.GetValue(_autosaveFolderKey).ToString(); result.BackupFolder = hive.GetValue(_backupFolderKey).ToString(); } using (RegistryKey hive = hkcu.OpenSubKey(_historyFolderHive)) { result.HistoryFolder = hive.GetValue(_historyFolderKey).ToString(); } throw new NotImplementedException(); } |
Я ввел приватные переменные, поскольку по тем же путям и по тем же ключам буду записывать данные. Минимум два прохода будет ))
Теперь добавляю чтение формата, режима инкрементального сохранения, режима применения формата. Заодно и каталоги прочитаем
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 | /// <summary>Чтение имеющихся настроек nanoCAD из реестра</summary> /// <returns></returns> /// <exception cref="NotImplementedException"></exception> public SaveSettings GetSaveSettings() { SaveSettings result = new SaveSettings(); RegistryKey hkcu = Registry.CurrentUser; using (RegistryKey hive = hkcu.OpenSubKey(_autosaveFolderHive)) { result.AutosaveFolder = hive.GetValue(_autosaveFolderKey).ToString(); result.BackupFolder = hive.GetValue(_backupFolderKey).ToString(); } using (RegistryKey hive = hkcu.OpenSubKey(_historyFolderHive)) { result.HistoryFolder = hive.GetValue(_historyFolderKey).ToString(); } using (RegistryKey hive = hkcu.OpenSubKey(_saveProjectsHive)) { byte[] format = (byte[])hive.GetValue(_defaultFormatKey); result.DefaultFormat = (DwgFormatForSave)format[4]; byte[] saveMode = (byte[]) hive.GetValue(_incSaveModeKey); result.IncSaveMode = (IncrementalSaveMode)saveMode[4]; byte[] useFormat = (byte[])hive.GetValue(_useSaveFormatKey); result.UseDefaultFormat = (UseFormat)useFormat[4]; } throw new NotImplementedException(); } |
По аналогии – таймаут сохранения, создание баков и прочая, и прочая (текста и так много, так что ограничусь только повторением работы с веткой автосохранения):
1 2 3 4 5 6 7 8 9 10 11 12 13 | using (RegistryKey hive = hkcu.OpenSubKey(_autosaveFolderHive)) { result.AutosaveFolder = hive.GetValue(_autosaveFolderKey).ToString(); result.BackupFolder = hive.GetValue(_backupFolderKey).ToString(); byte[] timeout = (byte[]) hive.GetValue(_timeoutKey); result.AutosaveTimeout = timeout[4]; byte[] createBak = (byte[])hive.GetValue(_createBakKey); result.CreateBak = createBak[4] == 1; byte[] createOrigBak = (byte[])hive.GetValue(_createOrigBakKey); result.CreateOriginalBak = createOrigBak[4] == 1; } |
Теперь запись? Ну, почти. NC в смысле каталогов ведет себя очень забавно. Можно руками вколотить любой путь к папке, к примеру, автосохранения. Но если этого каталога нет, наник начинает вести себя так, будто там пустая строка, и каталог автоматом не создает. Так что помимо сохранения данных каталоги еще и создавать придется.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 | /// <summary>Сохранение настроек nanoCAD</summary> /// <param name="settings"></param> public void KeepSaveSettings(SaveSettings settings) { if (!string.IsNullOrEmpty(settings.AutosaveFolder) && !Directory.Exists(settings.AutosaveFolder)) { Directory.CreateDirectory(settings.AutosaveFolder); } if (!string.IsNullOrEmpty(settings.BackupFolder) && !Directory.Exists(settings.BackupFolder)) { Directory.CreateDirectory(settings.BackupFolder); } if (!string.IsNullOrEmpty(settings.HistoryFolder) && !Directory.Exists(settings.HistoryFolder)) { Directory.CreateDirectory(settings.HistoryFolder); } #region Реестр RegistryKey hkcu = Registry.CurrentUser using (RegistryKey hive = hkcu.OpenSubKey(_autosaveFolderHive, true)) { hive.SetValue(_autosaveFolderKey, settings.AutosaveFolder); hive.SetValue(_backupFolderKey, settings.BackupFolder); byte[] timeout = (byte[])hive.GetValue(_timeoutKey; timeout[4] = settings.AutosaveTimeout; hive.SetValue(_timeoutKey, timeout); byte[] createBak = (byte[])hive.GetValue(_createBakKey); createBak[4] = (byte)(settings.CreateBak ? 1 : 0); hive.SetValue(_createBakKey, createBak); byte[] createOrigBak = (byte[])hive.GetValue(_createOrigBakKey); createOrigBak[4] = (byte)(settings.CreateOriginalBak ? 1 : 0); hive.SetValue(_createOrigBakKey, createOrigBak); } using (RegistryKey hive = hkcu.OpenSubKey(_historyFolderKey, true)) { hive.SetValue(_historyFolderHive, settings.HistoryFolder); } using (RegistryKey hive = hkcu.OpenSubKey(_saveProjectsHive, true)) { byte[] format = (byte[]) hive.GetValue(_defaultFormatKey); format[4]=(byte)settings.DefaultFormat; hive.SetValue(_defaultFormatKey, format); byte[] saveMode = (byte[])hive.GetValue(_incSaveModeKey); saveMode[4] = (byte)settings.IncSaveMode; hive.SetValue(_incSaveModeKey, saveMode); byte[] useFormat = (byte[])hive.GetValue(_useSaveFormatKey); useFormat[4] = (byte)settings.UseDefaultFormat; hive.SetValue(_useSaveFormatKey, useFormat); } #endregion } |
Вроед ничего не упустил. Так что добавляю вариант “приколочено гвоздями”. Чтоб не сходить с ума потом, делаю все же 2 метода: один просто будет возвращать “рекомендованные” значения, второй – сохранять.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 | /// <summary>Рекомендованные значения</summary> /// <returns></returns> public SaveSettings GetRecommendedSaveSettings() { return new SaveSettings() { AutosaveTimeout = 20, AutosaveFolder = Path.Combine(_customFolder, "Autosave"), BackupFolder = Path.Combine(_customFolder, "Backups"), CreateBak = true, CreateOriginalBak = false, DefaultFormat = DwgFormatForSave.Dwg2013, HistoryFolder = Path.Combine("History"), IncSaveMode = IncrementalSaveMode.Off, UseDefaultFormat = UseFormat.AllDocuments }; } /// <summary>Восстановить рекомендованные значения</summary> public void ResetToRecommendedSaveSettings() { SaveSettings settings = GetRecommendedSaveSettings(); KeepSaveSettings(settings); } |
Работу с системными переменными прописывать нет никакого желания, – FILEHISTORY, FILEHISTORYDURATION, FILEHISTORYMAX и так можно будет потом дообработать
Осталось теперь как минимум “приколоченный” вариант воткнуть в наник, да? В принципе да – достаточно прочитать данные и сравнить каталоги, чтоб понять, надо вываливать сообщение пользователю или нет.
С одной стороны, можно сделать команду, с другой – внедрить в инициализатор, с третьей это можно объединить. Основная проблема – это сравнить старые и новые данные. Мне кажется, самое простое – это просто реализовать возможность сравнения напрямую в SaveSettings. Тогда можно будет прочитать имеющиеся настройки, получить “рекомендованные”, сравнить, чего-то сделать…
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 | using NCadSave.Infrastructure.Enums; namespace NCadSave.Infrastructure { /// <summary>Настройки, относящиеся к хранению файлов в NCad</summary> public class SaveSettings : IEquatable<SaveSettings> { /// <summary>Режим инкрементального сохранения</summary> public IncrementalSaveMode IncSaveMode { get; set; } /// <summary>Формат по умолчанию для сохранения файла</summary> public DwgFormatForSave DefaultFormat { get; set; } /// <summary>Применять выбранныйй формат <see cref="DefaultFormat"/> к документам...</summary> public UseFormat UseDefaultFormat { get; set; } /// <summary>Каталог автосохранения</summary> public string AutosaveFolder { get; set; } /// <summary>Каталог резервных копий</summary> public string BackupFolder { get; set; } /// <summary>Каталог с файлами историй</summary> public string HistoryFolder { get; set; } /// <summary>Время автосохранения</summary> public byte AutosaveTimeout { get; set; } /// <summary>Создавать bak</summary> public bool CreateBak { get; set; } /// <summary>Создавать bak оригинала</summary> public bool CreateOriginalBak { get; set; } public bool Equals(SaveSettings? other) { if (ReferenceEquals(null, other)) return false; if (ReferenceEquals (this, other)) return true; return AutosaveFolder.Equals(other.AutosaveFolder, StringComparison.InvariantCultureIgnoreCase) && BackupFolder.Equals(other.BackupFolder, StringComparison.InvariantCultureIgnoreCase) && HistoryFolder.Equals(other.HistoryFolder, StringComparison.InvariantCultureIgnoreCase); } public override bool Equals(object? obj) { if (ReferenceEquals(null, obj)) return false; if (ReferenceEquals(this, obj)) return true; if (obj.GetType() != typeof (SaveSettings)) return false; return Equals((SaveSettings)obj); } public override int GetHashCode() { var hashCode = (HistoryFolder != null ? HistoryFolder.GetHashCode() : 0); hashCode = (hashCode * 397) ^ (BackupFolder != null ? BackupFolder.GetHashCode() : 0); hashCode = (hashCode * 397) ^ (AutosaveFolder != null ? AutosaveFolder.GetHashCode() : 0); return hashCode; } } } |
В условиях Linux, учитывая, что NC работает под Wine, можно сравнивать строки с игнорированием регистра (хотя в Linux, по-моему, каталоги Abc и aBc могут вполне легко сосуществовать рядом). В общем, пока что оставляю как есть. С надеждой, что переделывать не понадобится
Теперь можно и команду прописывать:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 | using HostMgd.ApplicationServices; using NCadSave.Infrastructure; using System.IO; using System.Windows; using Teigha.Runtime; using Application = HostMgd.ApplicationServices.Application; using MessageBox = System.Windows.MessageBox; namespace NCadSave.NCadCommands { public class SetSaveSettingsCmd { [CommandMethod("kpblc-set-savesettings")] public static void SetSaveSettingsCommand() { string regName = Path.Combine(Teigha.DatabaseServices.HostApplicationServices.Current.UserRegistryProductRootKey, "Profiles", Application.GetSystemVariable("cprofile").ToString()); NanoCadSettings settings = new NanoCadSettings(regName); SaveSettings existData = settings.GetSaveSettings(); settings.ResetToRecommendedSaveSettings(); if (existData != settings.GetRecommendedSaveSettings()) { string message = "Были изменены каталоги в настройках. Настоятельно рекомендуется перезапустить nanoCAD!"; MessageBox.Show(message, "Внимание!", MessageBoxButton.OK, MessageBoxImage.Hand); Document doc = Application.DocumentManager.MdiActiveDocument; if (doc != null) { doc.Editor.WriteMessage($"\n{message}"); } } } } } |
Можно теперь и проверить, чего получится. По ходу дела подправить пару опечаток и одну логическую ошибку, и…
Начальные настройки:
После вызова команды kpblc-set-savesettings выводится сообщение:
Ну и в результате
Ну, почти. Каталог с историей какой-то странненький. Правильно, забыл его корректно прописать в рекомендованных значениях:
1 | HistoryFolder = Path.Combine(_customFolder, "History"), |
Похоже, все правильно. В ExtennsionInitialize прописывать ничего не буду )))
Реп: https://github.com/kpblc2000/NCadSaveSettings
В принципе, все кроме собственно команды, можно “вытащить” в отдельную сборку NET Standard и подключать ее по мере надобности как к NET, так и к Framework-проектам.