.NET-сборки для AutoCAD разных версий, часть 2
Не удалось мне полностью решить вопрос с единым проектом .NET-сборок для разных версий AutoCAD Поэтому пришлось выкручиваться и задачу решать более извращенным методом.
Конструктивная критика с показом альтернативных решений приветствуется
Начну с самого начала: запуск VS (у меня на работе 2010 Rus), создаем библиотеку классов, задаем каталог и имя - все как обычно.
Переименовываем проект (не решение!) - создаваемый кусок будет отвечать за 2009 версию, потом добавим обработку 2013 (клик на имени проекта, [F2], вводим имя kpblcCommon2009).
Имя сборки оставляем kpblcCommon - наша библиотека планируется к использованию в тысяче мест... | |
Меняем используемую версию .NET на 3.5 | |
Вводим сведения о сборке. Хотя бы в минимальном объеме |
Последний из критических шагов - в "Отладке" установить (при необходимости) запускаемую внешнюю программу. Лично я последнее время этим не занимаюсь, предпочитаю вручную запускать нужный мне AutoCAD - все равно загрузку выполняю вручную (на то, чтобы корректно прописать scr-файл и создать программно же аргументы командной строки, меня не хватило). Скрин на это уже показывать не буду
Далее, меняем имя Class1.cs на нечто более внятное. Допустим, в этом классе у нас будет следующий функционал:
- Получение имени текущего профиля
- Строковое представление версии AutoCAD
- Строковое представление версии AutoCAD с учетом разрядности
Ну пускай имя будет как kpblcCommon - общие все-таки вещи собираемся делать. Захочется другое - бога ради, пока нам никто не запрещает такое сделать
Прежде чем писать код, предлагаю вызвать Проводник (или любой другой файловый менеджер) и войти в каталог создаваемого решения. Скопируем kpblcCommon2009.csproj в kpblcCommon2013.csproj:
Сейчас эти файлы абсолютно идентичны. И бог с ними, вносить изменения будем позже.
Вернемся в VS, правый клик на решении, Добавить, Существующий проект:
Указываем kpblcCommon2013.csproj.
И аналогичным образом настраиваем уже этот проект: версия .NET, имя сборки.
Теперь надо - опять же в свойствах сборок - установить каталоги вывода результатов (и для конфигурации Debug, и для конфигурации Release). Выполняется это на вкладке "Построение".
Возможно, что после подгрузки второго проекта в VS будет наблюдаться картина наподобие
Ничего страшного, просто удаляем Class1.cs из решения.
И тут же добавляем kpblcCommon.cs:
Теперь добавляем ссылки на AutoCAD'овские *mgd.dll в каждый проект и не забываем для импортированных библиотек установить CopyLocal в False. Уж это расписывать не буду
Ну все, можно приступать собственно к коду.
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 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.EditorInput; using Autodesk.AutoCAD.Runtime; using AcAp = Autodesk.AutoCAD.ApplicationServices; namespace kpblcCommon { public static partial class kpblcGlobal { /// <summary> /// Получение строкового представления имени текущего профиля /// </summary> /// <returns>Строка - полное наименование текущего профиля</returns> public static string getAcadProfileName() { return (string)AcAp.Application.GetSystemVariable("cprofile"); } /// <summary> /// Строковое представление номера версии AutoCAD. В случае неподдерживаемой версии вернет пустую строку /// </summary> /// <returns>Номер версии AutoCAD</returns> public static string getAcadVersion() { int ver = System.Convert.ToInt32(AcAp.Application.Version.Major) * 10 + System.Convert.ToInt32(AcAp.Application.Version.Minor); string res; switch (ver) { case 170: { res = "2007"; break; } case 171: { res = "2008"; break; } case 172: { res = "2009"; break; } case 180: { res = "2010"; break; } case 181: { res = "2011"; break; } case 182: { res = "2012"; break; } case 190: { res = "2013"; break; } case 191: { res = "2014"; break; } default: { res = ""; break; } } return res; } /// <summary> /// Возвращает версию AutoCAD (2009, 2010 etc) с указанием разрядности /// </summary> /// <returns>string</returns> public static string getAcadVersionWithBit() { int ver = System.Convert.ToInt32(AcAp.Application.Version.Major) * 10 + System.Convert.ToInt32(AcAp.Application.Version.Minor); string xBit; if (System.Text.RegularExpressions.Regex.IsMatch(AcAp.Application.GetSystemVariable("platform").ToString(), "64", System.Text.RegularExpressions.RegexOptions.IgnoreCase)) { xBit = "x64"; } else { xBit = "x32"; } return getAcadVersion() + xBit; } } } |
Класс сделан partial, добавим ему функционала. Например, создадим в решении kpblcCommon2009 новый файл kpblcPaths.cs, который будет возвращать некоторые пути.
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 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.IO; using Microsoft.Win32; using System.Windows.Forms; namespace kpblcCommon { public static partial class kpblcGlobal { /// <summary> /// Получение каталога %AppData% /// </summary> /// <returns></returns> private static string getPathAppData() { return (string)Registry.GetValue("HKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\Shell Folders", "AppData", null) + "\\kpblc\"; } /// <summary> /// Возвращает путь временных файлов /// </summary> /// <returns></returns> public static string getPathTemp() { string sPath = (string)Microsoft.Win32.Registry.GetValue("HKEY_CURRENT_USER\\Environment", "Temp", "c:\\temp") + "\\kpblc\" + "\" + getAcadProfileName(); try { if (!Directory.Exists(sPath)) { Directory.CreateDirectory(sPath); } } catch { MessageBox.Show("Не могу получить временный каталог." + "\nСоздайте каталог c:\\temp и установите для него полный доступ"); } return sPath; } } } |
Для корректной работы этого класса понадобится добавить ссылку на System.Windows.Forms.
Сохраним файл kpblcPaths.cs и закроем его. Теперь добавим этот файл в решение kpblcCommon2013:
Активируем решение kpblcCommon2013 и откроем kpblcPaths в нем. В окне ошибок будут выведены предупреждения о том, что "Имя типа или пространства имена Windows отсутствует...". Мораль - System.Windows.Forms надо подключить и в этот проект.
А теперь добавим кода, зависящего от версии AutoCAD. Как пример - установка текстового стиля для примитива. Как справедливо заметил Александр Ривилис
до версии 2009 включительно был метод DBText.TextStyle, а с 2010 этот метод называется TextStyleId
Вот и нарисуем код, который устанавливает для переданного примитива текстовый стиль. Немного "пофилоним" и будем обрабатывать только объекты многострочного текста. Добавляем в проект kpblcCommon2009 файл kpblcTextObjects2009.cs и внутри него колотим нечто типа:
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 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.Runtime; namespace kpblcCommon { public static class kpblcAcadTextObject { /// <summary> /// Устанавливает для примитива многострочного текста текстовый стиль. Слой примитива должен быть разблокирован и разморожен. Если текстовый стиль не существует, примитив не меняется. /// </summary> /// <param name="ent">указатель на обрабатываемый примитив.</param> /// <param name="StyleName">имя текстового стиля</param> /// <returns>ObjectId установленного стиля.</returns> public static ObjectId setTextStyle(MText ent, string StyleName) { using (Transaction tr = ent.Database.TransactionManager.StartTransaction()) { TextStyleTable txtStyleTable = (TextStyleTable)tr.GetObject(ent.Database.TextStyleTableId, OpenMode.ForRead); if (txtStyleTable.Has(StyleName)) { TextStyleTableRecord txtStyleTableRec = (TextStyleTableRecord)tr.GetObject(txtStyleTable[StyleName], OpenMode.ForRead); ent.UpgradeOpen(); ent.TextStyle = txtStyleTableRec.ObjectId; ent.DowngradeOpen(); } tr.Commit(); } return ent.TextStyle; } /// <summary> /// Устанавливает для примитива многострочного текста текстовый стиль. Слой примитива должен быть разблокирован и разморожен. Если текстовый стиль не существует, примитив не меняется. /// </summary> /// <param name="ent">указатель на обрабатываемый примитив.</param> /// <param name="StyleName">ObjectId текстового стиля</param> /// <returns>ObjectId установленного стиля.</returns> public static ObjectId setTextStyle(MText ent, ObjectId StyleNameId) { using (Transaction tr = ent.Database.TransactionManager.StartTransaction()) { ent.UpgradeOpen(); ent.TextStyle = StyleNameId; ent.DowngradeOpen(); tr.Commit(); } return ent.TextStyle; } } } |
Обратите внимание - пространство имен я поставил такое же, как и в остальных классах. А из имени класса убрал упоминание о версии.
Сохраняем файл. В решение kpblcCommon2013 добавляем аналогичный код, заменяя TextStyle на TextStyleID:
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 | using System; using System.Collections.Generic; using System.Linq; using System.Text; using Autodesk.AutoCAD.ApplicationServices; using Autodesk.AutoCAD.DatabaseServices; using Autodesk.AutoCAD.Runtime; namespace kpblcCommon { public static class kpblcAcadTextObject { /// <summary> /// Устанавливает для примитива многострочного текста текстовый стиль. Слой примитива должен быть разблокирован и разморожен. Если текстовый стиль не существует, примитив не меняется. /// </summary> /// <param name="ent">указатель на обрабатываемый примитив.</param> /// <param name="StyleName">имя текстового стиля</param> /// <returns>ObjectId установленного стиля.</returns> public static ObjectId setTextStyle(MText ent, string StyleName) { using (Transaction tr = ent.Database.TransactionManager.StartTransaction()) { TextStyleTable txtStyleTable = (TextStyleTable)tr.GetObject(ent.Database.TextStyleTableId, OpenMode.ForRead); if (txtStyleTable.Has(StyleName)) { TextStyleTableRecord txtStyleTableRec = (TextStyleTableRecord)tr.GetObject(txtStyleTable[StyleName], OpenMode.ForRead); ent.UpgradeOpen(); ent.TextStyleId = txtStyleTableRec.ObjectId; ent.DowngradeOpen(); } tr.Commit(); } return ent.TextStyleId; } /// <summary> /// Устанавливает для примитива многострочного текста текстовый стиль. Слой примитива должен быть разблокирован и разморожен. Если текстовый стиль не существует, примитив не меняется. /// </summary> /// <param name="ent">указатель на обрабатываемый примитив.</param> /// <param name="StyleName">ObjectId текстового стиля</param> /// <returns>ObjectId установленного стиля.</returns> public static ObjectId setTextStyle(MText ent, ObjectId StyleNameId) { using (Transaction tr = ent.Database.TransactionManager.StartTransaction()) { ent.UpgradeOpen(); ent.TextStyleId = StyleNameId; ent.DowngradeOpen(); tr.Commit(); } return ent.TextStyleId; } } } |
Естественно, что показан только пример. Дальнейшее наращивание ограничивается, кажется, только Вашей фантазией и чувством меры
Теперь допустим, что нам надо создать какое-то решение для 2009 и 2013 версий одновременно. Дальше разговорор пойдет в предположении, что версия .NET и соответствующие ссылки уже настроены.
В ссылки каждого из проектов надо добавить ссылку на соответствующий вариант kpblcCommon:
Сначала добавим ссылки на kpblcCommon2009 и kpblcCommon2013 | |
В проектах kpblcDiff2009 и kpblcDiff2013 устанавливаем ссылку на проекты kpblcCommon2009 и kpblcCommon2013 соответственно |
Теперь внутри классов kpblcDiff мы можем вполне правомерно писать нечто типа
1 2 3 4 5 | kpblcCommon.kpblcGlobal.getAcadProfileName(); kpblcCommon.kpblcAcadTextObject.setTextStyle(objMtext, stringStyleName); kpblcCommon.kpblcAcadTextObject.setTextStyle(objMText, objIDStyleName); |
При этом, учитывая, что код kpblcCommon у нас написан достаточно корректно, код внутри kpblcDiff уже не потребует переработки под каждый чих
Ффух, вроде все.
>Переименовываем проект (не решение!)
А почему бы сразу не задать решению одно имя, а проекту другое?
Не всегда понятно, какой код у тебя к какому файлу относится... Например первый код, как я понимаю, относится к файлу kpblcCommon.cs. Неплохо было бы первой строкой кода, в исходном файле, обозначать имя этого файла, как это зачастую делают C++ программисты. Тогда читая код однозначно понимаешь, что это за файл.
По поводу твоего кода... Он, мягко говоря, не самый хороший: замечаний много, не знаю писать тебе их сюда или не стоит, просто в комментах код будет очень нечитабелен (форматирования ведь нету).
Теперь я понял, что представляет твой вариант (partial файлы) написания исходного кода, о котором ты писал на форуме... Должен сказать, что считаю этот вариант очень плохим и более того - даже вредным: у тебя получается куча дублирующегося кода в разных файлах.
А вот вариант с директивами препроцессора ты зря не показал, потому что это как раз и было бы наиболее правильным вариантом.
Пиши, я ж не спец по .NET. Считай это чуть ли не первым моим экспериментом
P.S. Я повторяю - у меня не получилось влегкую поменять ссылки на *mgd.dll, а также версию .NET. В результате что есть, то есть.
Удали моё предыдушее сообщение - там сбилось форматирование кода. Исправил это и дублирую заново:
1. На мой взгляд, в функции getAcadProfileName(), т.к. не составляет особого труда в коде напрямую проверять значение системной переменной (хотя, пожалуй, это дело вкуса).
2. Функцию getAcadVersion я бы переписал так:
2
3
4
5
6
7
8
9
10
11
12
13
14
Int32 minMajor = 17;
Int32 maxMajor = 19;
Int32 year = 2007; // The start value is the year of the [minMajor].0 core version.
Dictionary<String, String> dict = new Dictionary<String, String>();
for (Int32 major = minMajor; major <= maxMajor; ++major) {
for (Int32 minor = 0; minor < 3; ++minor) {
dict.Add(String.Format("{0}.{1}", major, minor), year.ToString());
++year;
}
}
String result = dict.ContainsKey(coreVersion) ? dict[coreVersion] : String.Empty;
return result;
}
3. Необходимости в функции getAcadVersionWithBit(), так же как и в функции getAcadProfileName() не вижу. Разрядность AutoCAD всегда та же, что и разрядность операционной системы. Поэтому разрядность можно определять так:
Объединять год с разрядностью, можно либо просто конкатенацией строк(что ты и делал), либо так (предпочитаемый мною вариант):
4. Нет никакой необходимости в наличии функций getPathAppData() и getPathTemp(). Искомую информацию можно получить например так:
2
String temp = Environment.ExpandEnvironmentVariables("%temp%");
Кстати, эти способы повсеместно используются в моём коде на старом сайте, странно, что ты этого не видел (если читал исходники).
5. Ты создал два CS-файла: TextObjects2009.cs и TextObjects2013.cs содержимое которых отличается всего лишь несколькими строчками. Т. о. в случае, если тебе потребуется вносить изменения в код - это придётся делать два раза, о чём я уже писал выше. При этом нужно будет стараться не забыть внести все изменения, не пропустив ничего, что порой может быть затруднительно, если изменений было много (есть шанс что-то упустить). Именно поэтому следует использовать директивы препроцессора. Покажу это на примере одного твоего метода (остальные делаются по аналогии):
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
using Db = Autodesk.AutoCAD.DatabaseServices;
...
public static Db.ObjectId setTextStyle(Db.MText ent, String StyleName) {
Db.ObjectId result = Db.ObjectId.Null;
if (ent == null || StyleName.Trim() == String.Empty) return result;
Db.Database db = ent.Database;
using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
Db.TextStyleTable txtStyleTable = (Db.TextStyleTable)tr.GetObject(
db.TextStyleTableId, Db.OpenMode.ForRead);
if (txtStyleTable.Has(StyleName)) {
Db.TextStyleTableRecord txtStyleTableRec = (Db.TextStyleTableRecord)tr
.GetObject(txtStyleTable[StyleName], Db.OpenMode.ForRead);
ent.UpgradeOpen();
result = txtStyleTableRec.ObjectId;
#if newer_than_2009
ent.TextStyleId = result;
#else
ent.TextStyle = result;
#endif
ent.DowngradeOpen();
}
tr.Commit();
}
return result;
}
...
Как видишь, исходный код присутствует в единственном экземпляре, а различия, специфичные для версий AutoCAD локализованы в двух строках кода.
Удалил. Решение с директивами, как мне кажется, поднимает другой вопрос - понадобится а) менять версию .NET и б) менять выходной каталог. Лично я практически гарантирую, что рано или поздно что-нибудь да будет забыто. Именно поэтому я сделал так, как сделал
Насчет всего остального - спасибо, возьму на вооружение
Выкладываю обещанные мною замечания по программному коду:
1. На мой взгляд, нет особой необходимости в функции getAcadProfileName(), т. к. не составляет особого труда в коде напрямую проверять значение системной переменной. Хотя, пожалуй, это дело вкуса...
2. Функцию getAcadVersion я бы переписал так:
2
3
4
5
6
7
8
9
10
11
12
13
14
Int32 minMajor = 17;
Int32 maxMajor = 19;
Int32 year = 2007; // The start value is the year of the [minMajor].0 core version.
Dictionary<String, String> dict = new Dictionary<String, String>();
for (Int32 major = minMajor; major <= maxMajor; ++major) {
for (Int32 minor = 0; minor < 3; ++minor) {
dict.Add(String.Format("{0}.{1}", major, minor), year.ToString());
++year;
}
}
String result = dict.ContainsKey(coreVersion) ? dict[coreVersion] : String.Empty;
return result;
}
В коде я исхожу из предположения, что Autodesk не станет нарушать своё правило, согласно которому назначает нумерацию версий на протяжении уже многих лет. Если в будущем правило будет изменено, то в этот код потребуется внести учитывающие этот момент изменения.
Твой вариант с использованием case гораздо менее гибок, т. к. ты в нём жёстко зашил конкретные значения. Мой вариант вычисляет все значения автоматически. Позднее ты можешь изменить значение maxMajor на другое число, тем самым автоматически расширив диапазон вычисляемых значений.
3. Необходимости в функции getAcadVersionWithBit(), так же как и в функции getAcadProfileName() я не вижу, поскольку разрядность AutoCAD всегда та же, что и разрядность операционной системы. В виду этого разрядность можно определять так:
Объединять год с разрядностью, можно либо просто конкатенацией строк (что ты и делал в своём коде), либо следующим образом (предпочитаемый мною вариант):
4. Нет никакой необходимости в наличии функций getPathAppData() и getPathTemp(). Искомую информацию можно получить например так:
2
String temp = Environment.ExpandEnvironmentVariables("%temp%");
Посмотри перечисление Environment.SpecialFolder - много интересных вариантов найдёшь, а с помощью Environment.ExpandEnvironmentVariables всегда можно развернуть строку, содержащую в своём составе имя системной переменной.
Примечание: Кстати, эти способы повсеместно используются в моём коде на старом сайте, странно, что ты этого не видел, если конечно вообще смотрел мой код...
5. Ты создал два CS-файла: TextObjects2009.cs и TextObjects2013.cs содержимое которых отличается всего лишь несколькими строчками. Т. о. налицо дублирование существенного объема кода и в случае, если тебе потребуется вносить изменения в код - это придётся делать два раза (или столько, сколько ты создашь почти идентичных CS-файлов), о чём я тебе уже писал ранее по Skype.
При этом нужно будет стараться не забыть внести ВСЕ изменения, не пропустив ничего, что порой может быть затруднительно, если изменений было много, а тебя на работе отвлекают, время от времени - есть немалый шанс что-то пропустить.
Именно поэтому следует использовать директивы препроцессора. Покажу это на переделанном мною примере одного твоего метода (остальные делаются по аналогии):
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
using Db = Autodesk.AutoCAD.DatabaseServices;
...
public static Db.ObjectId setTextStyle(Db.MText ent, String StyleName) {
Db.ObjectId result = Db.ObjectId.Null;
if (ent == null || StyleName.Trim() == String.Empty) return result;
Db.Database db = ent.Database;
using (Db.Transaction tr = db.TransactionManager.StartTransaction()) {
Db.TextStyleTable txtStyleTable = (Db.TextStyleTable)tr.GetObject(
db.TextStyleTableId, Db.OpenMode.ForRead);
if (txtStyleTable.Has(StyleName)) {
Db.TextStyleTableRecord txtStyleTableRec = (Db.TextStyleTableRecord)tr
.GetObject(txtStyleTable[StyleName], Db.OpenMode.ForRead);
ent.UpgradeOpen();
result = txtStyleTableRec.ObjectId;
#if newer_than_2009
ent.TextStyleId = result;
#else
ent.TextStyle = result;
#endif
ent.DowngradeOpen();
}
tr.Commit();
}
return result;
}
...
Как видишь, исходный код присутствует в единственном экземпляре, а различия, специфичные для версий AutoCAD локализованы в двух строках кода.
ВНИМАНИЕ! В данном случае я объявил символ посредством строки:
но в данном коде я это делаю лишь для наглядности! Грамотным решением является определение символа (в нашем случае это newer_than_2009) не в коде, а в настройках проекта: поле "Conditional Compilation Symbols" на вкладке "Build". Если потребуется, то там можно объявлять более одного символа, отделяя их друг от друга пробелами.
Т. о. каждый твой проект, настройки которого ориентированы на конкретную версию AutoCAD, по этим символам сам определит какие именно строки кода ему следует компилировать, а какие нет.
В составе одного решения (solution) иметь несколько проектов (project), каждый из которых имеет свои специфичные для конкретной версии AutoCAD настройки (символы, версия .NET, целевой каталог построения, разрядность, подключаемые библиотеки), но при этом общие для всех проектов (project) исходники - это, очень хорошая идея. Она проста в реализации, легка в использовании и достаточно надёжна.
Насчет GetAcadVersion... Это Вы ребята "круто" загнули, ну вариант Крыса с учетом, что версии они могут переименовывать черт знает как - чтоб потом не вспоминать где тут, что - это еще куда не шло (не думая добавил проверку версии) - не красиво, но надежно, но у Андрея со словарями - это по моему перебор. Получить из одного другое - 1 формула на уровне 5-го класса - 1 строка (F# - на С# все то-же самое - просто под рукой нет, чтоб не напутать с операторами), еще строка перевода из строка-число-строка (а можно и в той-же написать).
let Test x=(x/10-17)*3+x%10+2007 - все
>Решение с директивами, как мне кажется, поднимает другой вопрос – понадобится а) менять версию .NET и б) менять выходной каталог. Лично я практически гарантирую, что рано или поздно что-нибудь да будет забыто.
Честно говоря, я не понял каким образом к директивам препроцессора относятся пункты "а)" и "б)"... Директивы препроцессора предназначены для выполнения условной компиляции. Они никак не связаны ни с "а)", ни с "б)". Забыто ничего не будет, т.к. если что-то будет забыто, то тот проект, в котором ты что-то забыл, попросту не скомпилируется. При этом компилятор тебе точно покажет проблемное место. Многократно проверено на практике.
Так, в поисках нужного pdf-файла наткнулся на эту заметку. Сразу, пока помню - вот мои мультики по обозначенной теме:
первый: https://www.youtube.com/watch?v=Xm3vgnsju0s
второй: https://www.youtube.com/watch?v=Iey1REsvYrw
третий: https://www.youtube.com/watch?v=4zjJXHpDYe4