«…Желают знать, что будет» или пишем гадальный шар в САПР NanoCAD на C# (MultiCAD .NET API)
Если верить одной старой песне из советского кинофильма, то люди всегда интересуются вопросами будущего в трудной ситуации. Кто-то подбрасывает монетку, кто-то мучает осьминога Пауля, а совсем уж зверски настроенные люди — ощипывают ромашки. Мы с вами поступим куда как гуманней и найдем для САПР NanoCAD, весьма нетрадиционное применение, а именно сделаем свой аналог гадального шара (почти как на картинке ниже).
В статье мы еще раз потренируемся создавать пользовательские примитивы NanoCAD с помощью MultiCAD.NET API, а также прикрутим к нашему объекту взаимодействие с Windows.Forms.
Код сегодня будет только на C#, писать его будем для платной версии (NC 8.5) и для бесплатной (NC 5.1), ну и естественно пользователи Linux смогут его собрать в Mono и запустить под Wine, поэтому милости прошу под кат…
Если вы раньше не сталкивались, со статьями из этого мини-цикла, то можно заглянуть под спойлер. В прошлых статьях мы рассмотрели разные вопросы от того, что такое NanoCAD, до попыток запустить наши проекты под Linux.
- «Лицо без шрама» или первые шаги в Multicad.NET API 7 (для Nanocad 8.1)
- «Как баран на новые ворота» или пользовательские «псевдо-3D» объекты в NanoCAD с помощью MultiCAD.NET API
- «Я слежу за тобой» или как из CADa сделать SCADA (MultiCAD.NET API)
- «Истина в вине» или пробуем программировать NanoCAD под Linux (MultiCAD.NET API)
- «Здравствуй елка — Новый Год!» или программируем NanoCAD с помощью Visual Basic .NET
Как всегда, напомню, что я не программист и поэтому не все мои мысли в данной статье могут быть корректными, а также что с разработчиками NanoCAD я никак не ангажирован . Хотя безусловно считаю нужным — сказать спасибо всему сообществу пользователей и разработчиков NanoCAD за их помощь на форуме.
Пусть вас не удивляет, что мы опять будем использовать САПР для создания объектов, никак не связанных с проектированием. Просто, мне надо было потренироваться «вставлять» Windows.Forms в пользовательские объекты NanoCAD, а поскольку учебных материалов по API для Нанокада — «кот наплакал», то я решил с вами поделиться своим простым и наглядным примером.
Статья будет короткая и можно было бы обойтись без содержания, но я на всякий случай его для удобства навигации оставлю.
Содержание:
Часть I: введение
Часть II: пишем код на C#
Часть III: заключение
Надо сказать, что несмотря, на то, что учебных материалов меньше чем хотелось бы, при написании кода нам будет на что опираться.
В частности, про создание пользовательского примитива уже писали разработчики в своем блоге на Хабре, ну и вопрос с интеллектуальными ручками, тоже ранее ими разбирался. В принципе мы сегодня сильно за рамки этих двух статей не выйдем.
Обычно в начале статьи я пишу, как создать с нуля проект для NanoCAD, но в этот раз из-за наличия класса с оконной формой я решил выложить весь проект на GitHub, поэтому его можно просто скачать, подключить библиотеки и сразу начать экспериментировать.
Но если для вас разработка под Нанокад — в новинку, посмотрите вот этот кусочек прошлой статьи (для NC 8.5 и для NC 5.1).
Я решил на всякий случай не «обижать» разработчиков NanoCAD и не стал прикладывать к проекту необходимые библиотеки из пакета SDK. Данные библиотеки вы сможете найти либо в папке «bin», установленной программы, либо получив пакет SDK. Для NC 8.5 и других версий необходимо зарегистрировать в клубе разработчиков. На всякий случай напомню, что скачать любую доступную версию NC для целей разработки, участники клуба могут совершенно бесплатно. Ну, а для бесплатного NC 5.1 — SDK вроде бы поставляется в комплекте с программой (если ничего не поменялось).
Итак, начнем разбирать код, я не буду прикладывать автоматически созданный код формы, а ограничусь только классом пользовательского примитива (собственно сам шар) и логикой формы.
Для начала спрячем под спойлером полный код для платной версии NanoCAD.
// Version 1.0
//Use Microsoft .NET Framework 4 and MultiCad.NET API 7.0
//Class for demonstrating the capabilities of MultiCad.NET
//Assembly for the Nanocad 8.5 SDK is recommended (however, it is may be possible in the all 8.Х family)
//Link imapimgd, mapimgd.dll and mapibasetypes.dll from SDK
//Link System.Windows.Forms and System.Drawing
//The commands: draws a fortune-teller ball
//This code in the part of non-infringing rights Nanosoft can be used and distributed in any accessible ways.
//For the consequences of the code application, the developer is not responsible.
//More detailed - https://habrahabr.ru/post/347720/
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Drawing;
using Multicad.Runtime;
using Multicad.DatabaseServices;
using Multicad.Geometry;
using Multicad.CustomObjectBase;
using Multicad;
using Multicad.AplicationServices;
namespace Fortuneteller
{
[CustomEntity(typeof(Ball), "2e814ea6-f1f0-469d-9767-269fedb32226", "Ball", "Fortuneteller Ball for NC85 Entity")]
[Serializable]
public class Ball : McCustomBase
{
private Point3d _basePnt = new Point3d(0, 0, 0);
double _radius=300;
string _predText = "...";
public List predictions =
new List()
{"Act now!",
"Do not do this!",
"Maybe",
"I dont know",
"Everything is unclear",
"Yes!",
"No!",
"Take rest"
};
public override void OnDraw(GeometryBuilder dc)
{
dc.Clear();
dc.Color = McDbEntity.ByObject;
dc.DrawCircle(_basePnt, _radius);
dc.DrawCircle(_basePnt, _radius/2.0);
dc.TextHeight = 31;
dc.DrawMText(_basePnt, Vector3d.XAxis, _predText, HorizTextAlign.Center, VertTextAlign.Center, _radius / 2.05);
}
public override void OnTransform(Matrix3d tfm)
{
// To be able to cancel(Undo)
McUndoPoint undo = new McUndoPoint();
undo.Start();
// Get the coordinates of the base point and the rotation vector
this.TryModify();
this._basePnt = this._basePnt.TransformBy(tfm);
undo.Stop();
}
public override hresult OnEdit(Point3d pnt, EditFlags lInsertType)
{
CallForm();
return hresult.s_Ok;
}
private void CallForm()
{
ListEditorForm frm = new ListEditorForm(this);
frm.Lpredictions.Items.AddRange(predictions.ToArray());
frm.ShowDialog();
}
[CommandMethod("DFTBall", CommandFlags.NoCheck | CommandFlags.NoPrefix)]
public void DrawBall ()
{
Ball ball = new Ball();
ball.PlaceObject();
McContext.ShowNotification("Use green grip or shake (move) ball to get prediction");
}
public override bool GetGripPoints(GripPointsInfo info)
{
//frist grip to move
info.AppendGrip(new McSmartGrip(_basePnt+new Vector3d(0, _radius,0), (obj, g, offset) => {
obj.TryModify();
obj._basePnt += offset;
obj.TryModify();
obj.ShakePredict();
}));
//command grip
var ctxGrip = new McSmartGrip(McBaseGrip.GripType.PopupMenu, 2, _basePnt - 1.0 * new Vector3d(_radius, 0, 0),
McBaseGrip.GripAppearance.PopupMenu, 0, "Select menu", Color.Lime);
ctxGrip.GetContextMenu = (obj, items) =>
{
items.Add(new ContextMenuItem("Get prediction", "none", 1));
items.Add(new ContextMenuItem("Edit predictions", "none", 2));
};
ctxGrip.OnCommand = (obj, commandId, grip) =>
{
if (grip.Id == 2)
{
switch (commandId)
{
case 1:
{
ShakePredict();
break;
}
case 2:
{
CallForm();
break;
}
}
}
};
info.AppendGrip(ctxGrip);
return true;
}
public override hresult PlaceObject(PlaceFlags lInsertType)
{
InputJig jig = new InputJig();
// Get the first box point from the jig
InputResult res = jig.GetPoint("Select center point:");
if (res.Result != InputResult.ResultCode.Normal)
return hresult.e_Fail;
_basePnt = res.Point;
// Add the object to the database
DbEntity.AddToCurrentDocument();
return hresult.s_Ok;
}
private void ShakePredict()
{
Random rand = new Random();
int val = rand.Next(0, predictions.Count);
this.TryModify();
_predText = predictions[val];
}
}
}
Теперь разберем ключевые моменты по частям.
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Drawing;
using Multicad.Runtime;
using Multicad.DatabaseServices;
using Multicad.Geometry;
using Multicad.CustomObjectBase;
using Multicad;
using Multicad.AplicationServices;
namespace Fortuneteller
{
[CustomEntity(typeof(Ball), "2e814ea6-f1f0-469d-9767-269fedb32226", "Ball", "Fortuneteller Ball for NC85 Entity")]
[Serializable]
public class Ball : McCustomBase
{
Подключаем пространства имен, создаем класс пользовательского объекта, присвоим ему какой-нибудь случайно сгенерированный GUID, наследуем наш класс от McCustomBase.
private Point3d _basePnt = new Point3d(0, 0, 0);
double _radius=300;
string _predText = "...";
public List predictions =
new List()
{"Act now!",
"Do not do this!",
"Maybe",
"I dont know",
"Everything is unclear",
"Yes!",
"No!",
"Take rest"
};
Задаём основные переменные для нашего гадального шара: точку центра геометрии, радиус, текст в окошке предсказания и список вариантов предсказания.
public override void OnDraw(GeometryBuilder dc)
{
dc.Clear();
dc.Color = McDbEntity.ByObject;
dc.DrawCircle(_basePnt, _radius);
dc.DrawCircle(_basePnt, _radius/2.0);
dc.TextHeight = 31;
dc.DrawMText(_basePnt, Vector3d.XAxis, _predText, HorizTextAlign.Center, VertTextAlign.Center, _radius / 2.05);
}
Метод отвечает за отрисовку объекта. Чертим два круга и объект многострочного текста.
public override void OnTransform(Matrix3d tfm)
{
// To be able to cancel(Undo)
McUndoPoint undo = new McUndoPoint();
undo.Start();
// Get the coordinates of the base point and the rotation vector
this.TryModify();
this._basePnt = this._basePnt.TransformBy(tfm);
undo.Stop();
}
Метод вызывается при изменении объекта, не на 100% понимаю, как он работает, но он будет нужен для того, чтобы корректно перемещать объект.
public override hresult OnEdit(Point3d pnt, EditFlags lInsertType)
{
CallForm();
return hresult.s_Ok;
}
Метод будет вызывать нашу форму (см. картинку в конце статьи) в момент, когда мы сделаем двойной клик по шару.
private void CallForm()
{
ListEditorForm frm = new ListEditorForm(this);
frm.Lpredictions.Items.AddRange(predictions.ToArray());
frm.ShowDialog();
}
Непосредственно вызов формы. Форма нам нужна для того, чтобы добавить или удалить варианты предсказаний, которые выпадают в шаре.
Мы заранее создали класс формы ListEditorForm и теперь при необходимости создаем объект, передав ему ссылку на наш шар (нужно для обратной связи), перед тем как вызвать форму заполняем её ListBox текущим списком предсказаний.
[CommandMethod("DFTBall", CommandFlags.NoCheck | CommandFlags.NoPrefix)]
public void DrawBall()
{
Ball ball = new Ball();
ball.PlaceObject();
McContext.ShowNotification("Use green grip or shake (move) ball to get prediction");
}
Команда, с помощью которой мы и будем создавать наш гадальный шар.
В самом простом случае в консоли NanoCAD надо будет ввести DFTBall и он вызовет наш метод DrawBall (не забудьте при необходимости загрузить библиотеку командой Netload).
public override bool GetGripPoints(GripPointsInfo info)
{
//first grip to move
info.AppendGrip(new McSmartGrip(_basePnt+new Vector3d(0, _radius,0), (obj, g, offset) => {
obj.TryModify();
obj._basePnt += offset;
obj.TryModify();
obj.ShakePredict();
}));
//command grip
var ctxGrip = new McSmartGrip(McBaseGrip.GripType.PopupMenu, 2, _basePnt - 1.0 * new Vector3d(_radius, 0, 0),
McBaseGrip.GripAppearance.PopupMenu, 0, "Select menu", Color.Lime);
ctxGrip.GetContextMenu = (obj, items) =>
{
items.Add(new ContextMenuItem("Get prediction", "none", 1));
items.Add(new ContextMenuItem("Edit predictions", "none", 2));
};
ctxGrip.OnCommand = (obj, commandId, grip) =>
{
if (grip.Id == 2)
{
switch (commandId)
{
case 1:
{
ShakePredict();
break;
}
case 2:
{
CallForm();
break;
}
}
}
};
info.AppendGrip(ctxGrip);
return true;
}
Здесь мы задаем ручки объекта — синюю и зеленую.
Первая — синяя ручка нужна для перемещения объекта. Перетаскивая шар за синюю ручку его можно потрясти и вы увидите, как меняется строка предсказаний.
Вторая — зеленая ручка (секция //command grip), нужна нам для того, чтобы вывести окошко с двумя командами. Первая генерирует новое предсказание, вторая вызывает редактор списка предсказаний.
public override hresult PlaceObject(PlaceFlags lInsertType)
{
InputJig jig = new InputJig();
// Get the first box point from the jig
InputResult res = jig.GetPoint("Select center point:");
if (res.Result != InputResult.ResultCode.Normal)
return hresult.e_Fail;
_basePnt = res.Point;
// Add the object to the database
DbEntity.AddToCurrentDocument();
return hresult.s_Ok;
}
Этот код вызывается для размещения объекта в пространстве модели. Вначале мы создаем объект InputJig, через него запрашиваем точку вставки, изменяем координаты нашей точки геометрического центра шара и добавляем объект в документ.
private void ShakePredict()
{
Random rand = new Random();
int val = rand.Next(0, predictions.Count);
this.TryModify();
_predText = predictions[val];
}
Ну, а тут с помощью простейшего генератора случайных чисел мы возвращаем какое-нибудь предсказание из общего списка.
Мы не будем разбирать подробно логику работы формы, полный код я спрячу под спойлер
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
namespace Fortuneteller
{
public partial class ListEditorForm : Form
{
private Ball ball;
public ListEditorForm()
{
InitializeComponent();
}
public ListEditorForm(Ball ball)
{
this.ball = ball;
InitializeComponent();
}
private void listView1_SelectedIndexChanged(object sender, EventArgs e)
{
}
private void DelBtn_Click(object sender, EventArgs e)
{
if (Lpredictions.SelectedItem !=null)
{
Lpredictions.Items.Remove(Lpredictions.SelectedItem);
}
}
private void AdBtn_Click(object sender, EventArgs e)
{
if (textBox.Text!="" | textBox.Text != " ")
{
Lpredictions.Items.Add(textBox.Text);
}
}
private void SaceBtn_Click(object sender, EventArgs e)
{
ball.predictions = Lpredictions.Items.OfType().ToList();
this.Close();
}
}
}
Пожалуй, единственное, что хоть как-то тут связанно с NanoCAD это обработчик события кнопки «сохранить и закрыть».
private void SaceBtn_Click(object sender, EventArgs e)
{
ball.predictions = Lpredictions.Items.OfType().ToList();
this.Close();
}
Помните мы раньше передавали форме ссылку на наш шар? Теперь мы, обращаясь к нему записываем в список предсказаний все значения нашего ListBox и закрываем форму, после этого шар начнет выдавать обновленные предсказания. Если форму закрыть, нажав на «крестик», то результат не сохранится.
Код для старого — бесплатного Нанокада, сильно отличатся не будет.
// Version 1.0
//Use Microsoft .NET Framework 3.5 and MultiCad.NET API
//Class for demonstrating the capabilities of MultiCad.NET
//Assembly for the Nanocad 5.1 SDK is recommended
//Link mapimgd.dll and hostmgd.dll from SDK
//Link System.Windows.Forms and System.Drawing
//The commands: draws a fortune-teller ball
//This code in the part of non-infringing rights Nanosoft can be used and distributed in any accessible ways.
//For the consequences of the code application, the developer is not responsible.
//More detailed - https://habrahabr.ru/post/347720/
using System;
using System.Collections.Generic;
using System.Windows.Forms;
using System.Drawing;
using Multicad.Runtime;
using Multicad.DatabaseServices;
using Multicad.Geometry;
using Multicad.CustomObjectBase;
using Multicad;
using HostMgd.ApplicationServices;
using HostMgd.EditorInput;
namespace Fortuneteller
{
[CustomEntity(typeof(Ball), "2e814ea6-f1f0-469d-9767-269fedb32195", "Ball", "Fortuneteller Ball for NC51 Entity")]
[Serializable]
public class Ball : McCustomBase
{
private Point3d _basePnt = new Point3d(0, 0, 0);
double _radius=300;
string _predText = "...";
public List predictions =
new List()
{"Act now!",
"Do not do this!",
"Maybe",
"I dont know",
"Everything is unclear",
"Yes!",
"No!",
"Take rest"
};
public override void OnDraw(GeometryBuilder dc)
{
dc.Clear();
dc.Color = McDbEntity.ByObject;
dc.DrawCircle(_basePnt, _radius);
dc.DrawCircle(_basePnt, _radius/2.0);
dc.TextHeight = 31;
dc.DrawMText(_basePnt, Vector3d.XAxis, _predText, HorizTextAlign.Center, VertTextAlign.Center, _radius / 2.05);
}
public override void OnTransform(Matrix3d tfm)
{
// To be able to cancel(Undo)
McUndoPoint undo = new McUndoPoint();
undo.Start();
// Get the coordinates of the base point and the rotation vector
this.TryModify();
this._basePnt = this._basePnt.TransformBy(tfm);
undo.Stop();
}
public override hresult OnEdit(Point3d pnt, EditFlags lInsertType)
{
CallForm();
return hresult.s_Ok;
}
private void CallForm()
{
ListEditorForm frm = new ListEditorForm(this);
frm.Lpredictions.Items.AddRange(predictions.ToArray());
frm.ShowDialog();
}
[CommandMethod("DFTBall", CommandFlags.NoCheck | CommandFlags.NoPrefix)]
public void DrawBall()
{
Ball ball = new Ball();
ball.PlaceObject();
DocumentCollection dm = HostMgd.ApplicationServices.Application.DocumentManager;
Editor ed = dm.MdiActiveDocument.Editor;
ed.WriteMessage("Use green grip or shake (move) ball to get prediction");
}
public override bool GetGripPoints(GripPointsInfo info)
{
//frist grip to move
info.AppendGrip(new McSmartGrip(_basePnt+new Vector3d(0, _radius,0), (obj, g, offset) => {
obj.TryModify();
obj._basePnt += offset;
obj.TryModify();
obj.ShakePredict();
}));
//command grip
var ctxGrip = new McSmartGrip(McBaseGrip.GripType.PopupMenu, 2, _basePnt - 1.0 * new Vector3d(_radius, 0, 0),
McBaseGrip.GripAppearance.PopupMenu, 0, "Select menu", Color.Lime);
ctxGrip.GetContextMenu = (obj, items) =>
{
items.Add(new ContextMenuItem("Get prediction", "none", 1));
items.Add(new ContextMenuItem("Edit predictions", "none", 2));
};
ctxGrip.OnCommand = (obj, commandId, grip) =>
{
if (grip.Id == 2)
{
switch (commandId)
{
case 1:
{
ShakePredict();
break;
}
case 2:
{
CallForm();
break;
}
}
}
};
info.AppendGrip(ctxGrip);
return true;
}
public override hresult PlaceObject(PlaceFlags lInsertType)
{
InputJig jig = new InputJig();
// Get the first box point from the jig
InputResult res = jig.GetPoint("Select center point:");
if (res.Result != InputResult.ResultCode.Normal)
return hresult.e_Fail;
_basePnt = res.Point;
// Add the object to the database
DbEntity.AddToCurrentDocument();
return hresult.s_Ok;
}
private void ShakePredict()
{
Random rand = new Random();
int val = rand.Next(0, predictions.Count);
this.TryModify();
_predText = predictions[val];
}
}
}
Вся разница по сути только в следующем: из-за того, что McContext.ShowNotification («Use green grip or shake (move) ball to get prediction») еще не реализован в старой версии MultiCAD.NET API, мы его заменили на аналог из простого .NET API.
DocumentCollection dm = HostMgd.ApplicationServices.Application.DocumentManager;
Editor ed = dm.MdiActiveDocument.Editor;
ed.WriteMessage("Use green grip or shake (move) ball to get prediction");
Итак, в итоге мы получили шар, который может выдавать предсказания, если его перетаскивать за синюю ручку или по команде спрятанной в зеленой ручке.
Также мы рассмотрели, простейший пример взаимодействия графической формы и объекта, реализовав редактирование переменной содержащей список предсказаний. Напомню, что вызов формы производится по двойному щелчку на объект или через зеленую ручку.
Вот что получили в итоге
Nanocad 8.5
Nanocad 5.1 Free
Понятно, что данный пример — шуточный и практической пользы не имеет, но надеюсь, что он все же кому-нибудь пригодится.
Всем удачного дня!
P.S. На всякий случай предупрежу, что последнее обновление Windows 10 немного ломает x64 версию NanoCAD 8, поэтому весь код тестировался в x86 версиях.