Pull to refresh

«…Желают знать, что будет» или пишем гадальный шар в САПР NanoCAD на C# (MultiCAD .NET API)

Reading time 12 min
Views 3.6K
Если верить одной старой песне из советского кинофильма, то люди всегда интересуются вопросами будущего в трудной ситуации. Кто-то подбрасывает монетку, кто-то мучает осьминога Пауля, а совсем уж зверски настроенные люди — ощипывают ромашки. Мы с вами поступим куда как гуманней и найдем для САПР NanoCAD, весьма нетрадиционное применение, а именно сделаем свой аналог гадального шара (почти как на картинке ниже).

В статье мы еще раз потренируемся создавать пользовательские примитивы NanoCAD с помощью MultiCAD.NET API, а также прикрутим к нашему объекту взаимодействие с Windows.Forms.

Код сегодня будет только на C#, писать его будем для платной версии (NC 8.5) и для бесплатной (NC 5.1), ну и естественно пользователи Linux смогут его собрать в Mono и запустить под Wine, поэтому милости прошу под кат…





Если вы раньше не сталкивались, со статьями из этого мини-цикла, то можно заглянуть под спойлер. В прошлых статьях мы рассмотрели разные вопросы от того, что такое NanoCAD, до попыток запустить наши проекты под Linux.


Как всегда, напомню, что я не программист и поэтому не все мои мысли в данной статье могут быть корректными, а также что с разработчиками 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.

Полный код для NC 8.5 на C#
// 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<String> predictions =
             new List<String>()
             {"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<Ball>(_basePnt+new Vector3d(0, _radius,0), (obj, g, offset) => {
                obj.TryModify();
                obj._basePnt += offset;
                obj.TryModify();
                obj.ShakePredict();
            }));
          
            //command grip
            var ctxGrip = new McSmartGrip<Ball>(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<String> predictions =
             new List<String>()
             {"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<Ball>(_basePnt+new Vector3d(0, _radius,0), (obj, g, offset) => {
                obj.TryModify();
                obj._basePnt += offset;
                obj.TryModify();
                obj.ShakePredict();
            }));
          
            //command grip
            var ctxGrip = new McSmartGrip<Ball>(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];

        }

Ну а тут с помощью простейшего генератора случайных чисел мы возвращаем какое-нибудь предсказание из общего списка.

Мы не будем разбирать подробно логику работы формы, полный код я спрячу под спойлер

код класса ListEditorForm

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<String>().ToList();
            this.Close();
        }


    }
}


Пожалуй, единственное, что хоть как-то тут связанно с NanoCAD это обработчик события кнопки «сохранить и закрыть».

        private void SaceBtn_Click(object sender, EventArgs e)
        {
            ball.predictions = Lpredictions.Items.OfType<String>().ToList();
            this.Close();
        }

Помните мы раньше передавали форме ссылку на наш шар? Теперь мы, обращаясь к нему записываем в список предсказаний все значения нашего ListBox и закрываем форму, после этого шар начнет выдавать обновленные предсказания. Если форму закрыть, нажав на «крестик», то результат не сохранится.

Код для старого — бесплатного Нанокада, сильно отличатся не будет.

код для NanoCAD 5.1
// 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<String> predictions =
             new List<String>()
             {"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<Ball>(_basePnt+new Vector3d(0, _radius,0), (obj, g, offset) => {
                obj.TryModify();
                obj._basePnt += offset;
                obj.TryModify();
                obj.ShakePredict();
            }));
          
            //command grip
            var ctxGrip = new McSmartGrip<Ball>(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 версиях.
Tags:
Hubs:
+10
Comments 0
Comments Leave a comment

Articles