DragonFire's Blog

Библиотека Cimanu и язык программирования c#
Копирование изображения FrameworkElement

Поиск простого решения

Иногда появляется необходимость сохранить изображение произвольного FrameworkElement-а в виде изображения. Например, Вы отобразили пользователю красивый график с множеством разнообразных трехмерных линий и хотите автоматически отправить его в виде изображения по электронной почте, либо вставить в MS Word/Excel.

Но данная задача решается не так просто, как бы это могло показаться. Когда я решал подобную задачу я столкнулся с целым рядом проблем:

  • Что делать если элемент не представлен в визуальном дереве?
  • Каким образом представлять фон элемента, если тот является прозрачным?
  • Как правильно привести размеры элемента в соответствие с требуемыми, не исказив его содержимое?
  • Почему при удалении элемента из визуального дерева метод RenderTargetBitmap.Render?
  • И многое другое...

Поэтому, решив все эти проблемы, делюсь найденным решением в виде готового класса.

[more]

Класс FrameworkElementRender

[code:c#]    // Класс, позволяющий представить любой FrameworkElement в виде изображения
    public static class FrameworkElementRender
    {
        // Возвращает изображение FrameworkElement
        public static BitmapSource Render(FrameworkElement renderTarget, Size availableSize, Brush backgroundBrush)
        {
            if (renderTarget == null)
                return null;
 
            //Перерисовываем FrameworkElement
            renderTarget.Measure(availableSize);
            renderTarget.Arrange(new Rect(renderTarget.DesiredSize));
  
            //Рисуем фон и изображение элемента
            DrawingVisual dw = new DrawingVisual();
            using (DrawingContext dc = dw.RenderOpen())
            {
                dc.DrawRectangle(backgroundBrush, null, new Rect(renderTarget.DesiredSize));
                dc.DrawRectangle(new VisualBrush(renderTarget), null, new Rect(renderTarget.DesiredSize));
            }
 
            //Создаем изображение
            RenderTargetBitmap rtb = new RenderTargetBitmap(
                (Int32)renderTarget.DesiredSize.Width,
                (Int32)renderTarget.DesiredSize.Height,
                96.0,
                96.0,
                PixelFormats.Default);
            rtb.Render(dw);
  
            return rtb;
        }
 
        // Возвращает изображение FrameworkElement
        // Задает по умалчанию кисть заднего фона - белой
        public static BitmapSource Render(FrameworkElement renderTarget, Size availableSize)
        {
            return Render(renderTarget, availableSize, Brushes.White);
        }
 
        // Возвращает изображение FrameworkElement
        // Задает по умалчанию кисть заднего фона - белой
        // Размер - тот, который именно FrameworkElement в данный момент
        public static BitmapSource Render(FrameworkElement renderTarget)
        {
            if (Double.IsNaN(renderTarget.ActualWidth) || (Double.IsNaN(renderTarget.ActualHeight)))
                throw new ArgumentException("Требуемый FrameworkElement не имеет размеров, либо не представлен в визуальном дереве!");
            return Render(renderTarget, new Size(renderTarget.ActualWidth, renderTarget.ActualHeight));
        }
    }[/code]

 

BitmapSource и буфер обмена

Дополнительным плюсом данного решения является то, что теперь можно легко помещать полученное изображение в буфер обмена:

[code:c#]Clipboard.SetImage(FrameworkElementRender.Render(myUserControl));[/code]

 

Благодарности

Огромное спасибо пользователю vit_as форума rsdn.ru за помощь в нахождении решения.

 

 

Региональный формат чисел и WPF

Проблема с региональным форматом чисел в WPF

При отладке приложения, обнаружил интересную вещь: в система задана русская культура, т.е. CurrentCulture возвращает ru-RU. Системный разделитель соответственно — запятая. Записываю в TextBox, (в нем использую стандартные методы Double.Parse и биндинг) число "1,1" — и приложение, вместо записи в Double поле класса 1,1 записывает 11.

Копнул поглубже и обнаружил, что CurrentUICulture возвращает en-US независимо от настроек в самой Windows. А в en-US запятая — разделитель порядка, типа "1,000,000" = 1 миллион...

[more]

Hard code в коде класса FrameworkElement

Эта проблема возникает изза хардкоддинга класса FrameworkElement, который берет значение культуры из xml:lang (которое по умалчанию - пустая строка). Поэтому, используется жестко закодированная культура "en-US".

Подробно об этом написано в статье, посвященной FrameworkElement.Language Property.

Решение

Решение состоит в прямом присваивании свойству FrameworkElement.Language текущей культуры:

[code:c#]

protected override void OnStartup(StartupEventArgs e)
{

FrameworkElement.LanguageProperty.OverrideMetadata(
typeof(FrameworkElement),
new FrameworkPropertyMetadata(
XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)
)
);

TextElement.LanguageProperty.OverrideMetadata(
typeof(TextElement),
new FrameworkPropertyMetadata(
XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag)
)
);

base.OnStartup(e);
}
[/code]
За помощь в поиске решения, благодарю пользователя форума rsdn, Vladek.

Полезные ссылки на msdn

FrameworkElement.Language Property

Обработка xml:lang в XAML

 

Скрипт для очистки временных файлов Visual Studio

Зачем нужно удалять временные файлы?

Ответ на этот вопрос простой - зачем хранить ту информацию, которую можно восстановить по исходным кодам? Тем более, если эта информация не несет в себе никакой практической ценности.

Более конкретно - при использовании системы управления версиями Subversion, в репозитории необходимо хранить только полезную информацию, а именно исходные коды, документацию и т.д. Хранить отладочную информацию и временные файлы, тем более историю их изменений, совершенно бессмысленно.

Поэтому перед тем, как зафиксировать очередную правку в хранилище, необходимо удалить все ненужные файлы, которые создает Visual Studio в процессе сборок проекта.

[more]

Временные файлы Visual Studio

  • Файлы *.pdb в папке Bin - используются для хранения отладочной информации.
  • Файлы *.vshost.* в папке Bin - используются для сбора отладочной информации средой разработки.
  • Папки obj в каталогах всех проектов - содержат множество временных файлов, генерируемых студией при каждом построении проекта.

Скрипт для очистки

Исходя из вышесказанного, следующий скрипт будет удалять отладочные файлы и папки obj, содержащие временые файла. Вызов этого скрипта перед каждой фиксацией очередной правки значительно снижает объемы ненужной информации в репозитории.

Clear.bat:

[code:c#]@ECHO OFF

echo Deleting Files
del .\*.~* /S
del .\*.pdb /S
del .\*.vshost.* /S

echo Deleting Folders
setlocal
set rdir=obj
set fpath=%~dps0
call :func "%fpath:~0,-1%"
goto end
:func
for /f "delims=" %%i in ('dir %1 /a:d /b') do IF /I %%i==%rdir% ( rmdir /s /q %1\%%i && echo Deleted Folder - %1\%%i ) ELSE ( call :func %1\"%%i" )
exit /b
:end[/code]

Скрипт необходимо поместить в корень дерева проектов.

Полезные ссылки

Первоисточник, в котором дан похожий скрипт. Скрипт содержит баг - при наличии пробелов в названии папки, он перестает работать. В скрипте, приведенном в этом посте, этот баг исправлен, а сам скрипт адаптирован для удаления временных файлов Visual Studio.

Библиотека Cimanu.MapInfo (Версия 0.2)

Вступление

Данная статья является продолжением первой части, в которой я описывал библиотеку Cimanu.MapInfo версии 0.1. С тех пор, прошло много времени, и я спешу представить новую версию, включающую исправления найденных багов, а также несколько новых расширений (функций), о которых и пойдет речь ниже.

Я не буду приводить полное описание библиотеки, так что для тех, кто знакомится с ней впервые, настоятельно рекомендую прочитать первый пост по теме.

Диаграмма классов

Актуальное видение проекта отражает диаграмма классов, созданная в UMLet. На ней можно отследить все связи между классами, а также составить полное представление о библиотеке в целом.

  • Зеленым цветом отмечены классы, прошедшие комплексное тестирование и не содержащие ошибок.
  • Желтым цветом помечены классы, которые могут содержать какие-либо баги, хотя они тоже проходили тестирование.
  • Розовым цветом помечено ядро MapInfo, к которому библиотека обращается с запросами.

Стрелки между классами содержат поясняющие заголовки, которые помогают лучше понять смысл ассоциации.

[more]

Скачать диаграмму можно в двух форматах:

  1. Родной uxf (открывать через UMLet) - Диаграмма классов.uxf (59,30 kb)
  2. Конвертированный в pdf (без надписей и немного кривой) - Диаграмма классов.pdf (18,29 kb)

Найденные и исправленные баги

  • Баг в классе CoordSys, когда тот пытался обратиться к функции CoordSysName$, в ответ на что, получал исключение.
  • Баг в функции GetMapObject() класса Row
  • Баг в функциях GetX(...) и GetY(...) классов Region и Polyline

Мелкие изменения

  1. В классе Mapper добавлена функция SetZoomEntire(), позволяющая отобразить карту целиком в видимой области.
  2. В классе Controller добавлена статическая функция IsLaunch(), возвращающую true, при наличии запущенного сервера MapInfo.
  3. В классе Controller добавлена функция SetProgressBar(...) позволяющая включить или выключить стандартный ProgressBar MapInfo.
  4. Обновлен класс Map. Теперь он может добавлять хранилища граффических объектов к существующим таблицам, с указанием необходимой СК.
    Конструктор: public Map(Table Table, CoordSys CoordSys)
    Метод: public void AddTable(Table Table, CoordSys CoordSys)
  5. Расширен класс Session, который теперь позволяет получить информацию, а также установить текущую кисть, перо, символ или шрифт.

Работа с данными

Добавлены классы для работы с типамиданных MapInfo - класс ColumnType и перечисление ColumnTypes. Обновлены классы Table, Row, Column. Появилась возможность добавлять столбцы или строки в открытую таблицу.

Пример:

[code:c#]TestTable = new Table(@"C:\table.tab", new String[] { "ID", "Имя" }, new ColumnType[] { new ColumnType(ColumnTypes.Integer), new ColumnType(ColumnTypes.Char, 50) });
TestTable.GetColumn("Имя").Rename("Name");
TestTable.AddColumn("c1", new ColumnType(ColumnTypes.Float));
TestTable.AddRow(1, "один", 0.1);
Map.AddTable(TestTable, Map.Mapper.CoordSys);[/code]

Расширение графической модели

Добавлен новый абстрактный класс DrawningTool, являющийся предком всех графических инструментов.

Расширена модель работы с символами. Теперь за создание любых символов по строке MapInfo отвечает абстрактный класс-фабрика Symbol, а реальные символы делятся на три вида:

  • StandartSymbol - стандартный символ MapInfo;
  • FontSymbol - символ из заданного шрифта;
  • CustomSymbol - символ из заданного графического файла, хранящегося на жестком диске.

Добавлена поддержка текста с помощью классов Font и Text. Обновлен класс Canvas, поддерживающий рисование текста на слое.

Новые полезные классы

Класс Selector

Добавлен статический класс Selector для работы с запросами. С помощью метода Select можно получить объект Table, являющийся результатом искомого запроса. А с помощью методов Avg, Min, Max и Sum получить информацию по данным цифровых столбцов.

[code:c#]Double sumData = Selector.Sum(myTable,"DataColumn");[/code]

Класс Finder

Добавлен статический класс Finder, с помощью которого можно осуществлять поиск значения в заданном столбце таблицы. Все найденные значения помечаются на карте символом, которые устанавливается методом SetSymbol().

[code:c#]Symbol s = Cimanu.MapInfo.Dialog.ShowSymbolDialog();[/code]

Класс Dialog

Позволяет обращатся к стандартным диалогам MapInfo, таким как выбор кисти, пера, символа или шрифта; а также к диалогу выбора системы координат. Каждый такой метод возвращает объект искомого класса.

[code:c#]Finder.SetSymbol(s);
Finder.Find(TestTable, "Name", "один");[/code]

Оптимизация

Оптимизирована работа класса MapObject. А именно алгоритм поиска не занятого имени переменного, для создаваемого графического объекта. При больших объемах работы с объектами скорость нового алгоритма превышает скорость старого на два порядка.

Что осталось в планах

В первом посте я писал о своих кланах, относительно дальнейшего развития. Часть из этих кланов удалось воплотить в жизнь Улыбаюсь А вот что осталось:

  • Доработка механизм различных запросов к MapInfo на языке, аналогичном SQL
  • Поддержка меток в слоях
  • Создание механизма получения системной информации об установленной версии MapInfo
  • Исправление багов Улыбаюсь

Заключение

Итак, работа над библиотекой продолжается, со временем будут появляться новые версии. Буду рад ответить на любые вопросы и учесть любые пожелания / замечания. Именно благодаря таким письмам, было произведено много работы по расширению библиотеки.

С наилучшими пожеланиями, Степанов Евгений.

Скачать библиотеку версии 0.2

Cimanu.MapInfo.0.2.rar (164,52 kb)

Binding и проверка данных в WPF

Вступление

При написании бизнес системы всегда возникает необходимость получения данных от пользователя и передаче их объектам логики. Естественно, перед передачей, данные необходимо проверить, и в случае ошибки, дать знать об этом пользователю.

WPF значительно облегчает задачу передачи и контроля данных. В этом посте я покажу, как написать полностью интерактивное приложение сбора данных и их обработки. Ссылки на исходные код и полезные статьи приведены в конце поста.

Постановка задачи

В качестве примера рассмотрим простенькую задачку. Имеется хранилище некоторого объема (VolumeRepository). В хранилище находится жидкость (VolumeLiquid). Необходмо вычислить свободный объем этого хранилища (VolumeAdd), при этом полностью контролируя вводимые данные.

Image Hosting

[more]

Бизнес - логика

Единственный класс SimpleClass, содержащий три поля типа Double, которые по умолчанию инициализируются нулями:

  • VolumeRepository (входной объем хранилища);
  • VolumeLiquid (входной объем жидкости);
  • VolumeAdd(расчетный свободный объем).

А также один метод Calculate, осуществляющий простой расчет по формуле: VolumeAdd = VolumeRepository - VolumeLiquid.

Разметка окна

Поля в первом столбце служат для ввода исходных данных и вывода результата, а во втором столбце - для контрольного вывода текущих значений VolumeRepository и VolumeLiquid из логики. В обработчике кнопки вызывается единственный метод logic.Calculate().

Привязка данных

Создадим привязку интерфейса к логике. Привязку будем осуществлять декларативно (в XAML разметке) с помощью класса Binding. Для поля ввода объема хранилища устанавливаем Mode="TwoWay" и UpdateSourceTrigger="PropertyChanged" для моментальной записи вводимых значений в поле VolumeRepository логики:

[code:xml]       <TextBox Name="textBox1" Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" HorizontalAlignment="Center" MinWidth="100">
            <TextBox.Text>
                <Binding Path="VolumeRepository" Mode="TwoWay" UpdateSourceTrigger="PropertyChanged">
                    <Binding.ValidationRules>
                        <local:ConvertToTypeRule RuleType="System.Double" />
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>
        </TextBox>
        <TextBox Text="{Binding Path=VolumeRepository, Mode=OneWay}" Grid.Row="1" Grid.Column="2"
            VerticalAlignment="Center" HorizontalAlignment="Center" MinWidth="100" IsEnabled="False"/>[/code]

Для поля, в которое будет осуществлятся контрольный вывод информации из VolumeRepository установим Mode=OneWay. Аналогично осуществляем привязку для остальных полей.

Далее, при загрузке формы создаем бизнес-объект логики и помещаем его в DataContext Grid-a:

[code:c#]public partial class Window1 : Window
    {
        public SimpleClass logic;       
        public Window1()
        {
            InitializeComponent();
            logic = new SimpleClass();
            this.DataContext = logic;
        }[/code]

Уведомление интерфейса об изменении полей бизнес-логики

Следующим шагом должно стать уведомление интерфейса приложения, об изменениях данных в логике. Если этого не сделать, после нажатия на кнопку расчета (выполнения метода Calculate), WPF слой, отвечающий за binding, ничего не будет знать про обновление значения VolumeAdd и результат не будет отображен.

Для реализации уведомлений используем интерфейс INotifyPropertyChanged. Единственное событие этого интерфейса необходимо генерировать при каждой установке свойства:

[code:c#]        public Double VolumeAdd
        {
            . . .
            set { _VolumeAdd = value;
                NotifyPropertyChanged("VolumeAdd");  }
        }
        public event PropertyChangedEventHandler PropertyChanged;
        private void NotifyPropertyChanged(String info)
        {
            if (PropertyChanged != null)
            {
                PropertyChanged(this, new PropertyChangedEventArgs(info));
            }
        }[/code]

Вызов такого события необходимо генерировать и для остальных свойств, передавая их имена в качестве параметра.

Проверка вводимых данных

Для начала необходимо решить кто будет отвечать за проверку вводимых данных. По этому вопросу возникает много споров, но я придерживаюсь мнения, что контролировать правильность типа данных должен отдельный класс, а правильность вводимых велечин - сама логика. Это объясняется тем, что в логику должны поступать данные, которые она может принять, но никто, кроме нее не знает, какие именно значениями считать правильными, а какие нет.

В нашей программе рассмотрим разные типы ограничений на входные данные, а именно:

  1. Ограничение по типу. Вводимые данные должны быть числами типа Double. Причем, десятичный разделитель должен быть такой, какой установлен в системе, чтобы не смущать пользователей программы.
  2. Ограничение пределами. Вводимые данные ограничены некоторыми пределами. Нижний предел - 0 (Объем должен быть положительной величиной). Верхний предел - некое максимальное значение, к примеру 100 (допустим 100 кубометров).
  3. Ограничение физической модели. Объем жидкости, находящейся в хранилище не должен превышать объема самого хранилища.

Частым предметом спора является утверждение, что неверные входные данные необходимо отвергать на этапе записи. Такие ограничения можно реализовать с помощью наложения ограничений на свойства, но в таком случае, все свойства в логике должны быть свойствами зависимостей, а сама логика - наследником DependencyObject, что несколько необычно. Также такой вариант является недружественным к пользователю. Например, если пользователь сначала введет объем воды, система не примет это значение, так как объем хранилища изначально равен нулю. Более удобно хранить все поступающие значения, а потом оценивать их правильность. Такой подход открывает много возможностей, например сериализацию логики для сохранения в файле (БД и т.д.), не заставляя пользователя вводить сразу все необходимые корректные данные.

Сначала остановимся на проверке типа вводимых данных.

Ограничение по типу

Наша программа ожидает числа типа Double. Система WPF имеет встроенный механизм проверки вводимых данных. Для начала нужно создать класс-наследник ValidationRule и переопределить единственный метод Validate. Реализуем самую простейшую проверку:

[code:c#]public class ConvertToTypeRule : ValidationRule
    {
        public override ValidationResult Validate(object value, CultureInfo culture)
        {
            Double d;
            if (!Double.TryParse(value.ToString(), NumberStyles.Number, culture, out d))
            {
                return new ValidationResult(false, "Недопустимые символы в строке.");
            }
            return new ValidationResult(true, null);
        }

    }[/code]

После этого нужно привязать получившийся класс к TextBox-ам:

[code:xml]           <TextBox.Text>
                <Binding Path="VolumeRepository" Mode="TwoWay" ValidatesOnDataErrors="True" UpdateSourceTrigger="PropertyChanged" NotifyOnValidationError="True">
                    <Binding.ValidationRules>
                        <local:ConvertToTypeRule/>
                    </Binding.ValidationRules>
                </Binding>
            </TextBox.Text>[/code]

Проблема с CultureInfo

Если теперь попытаться ввести данные в TextBox, то не всегда разделителем дробной части становиться системный разделитель, хотя такая ситуации весьма вероятна. Для предотвращения возникших проблем, явно укажем всем элементам WPF на текущую культуру, добавив несколько строк в App.xaml.cs:

[code:c#]        protected override void OnStartup(StartupEventArgs e)
        {
            FrameworkElement.LanguageProperty.OverrideMetadata(
                typeof(FrameworkElement),
                new FrameworkPropertyMetadata(
                    XmlLanguage.GetLanguage(CultureInfo.CurrentCulture.IetfLanguageTag))
                );
            base.OnStartup(e);
        }[/code]

Обобщение класса ConvertToTypeRule

Ранее мы создали класс, который проверяет соотвеnствие вводимых данных типу Double. Но такой подход, как правило, не используется. Вместо этого создается обобщенный тип, способный проверять соответствие информации любому заданному типу:

[code:c#]public class ConvertToTypeRule : ValidationRule
    {
        private String _RuleType = "Object";

        public String RuleType
        {
            get { return _RuleType; }
            set { _RuleType = value; }
        }

        public override ValidationResult Validate(object value, CultureInfo culture)
        {
            Type t = Type.GetType(RuleType);
            MethodInfo miParse = t.GetMethod("Parse", new Type[] { typeof(String), typeof(IFormatProvider) });
            if ((t != null) && (miParse != null))
            {
                try
                {
                    Object obj = miParse.Invoke(null, BindingFlags.Default | BindingFlags.Static | BindingFlags.InvokeMethod | BindingFlags.Public,
                        null, new object[] { value.ToString(), culture }, culture);
                }
                catch
                {
                    return new ValidationResult(false, "Недопустимые символы в строке.");
                }
            }
            else
            {               
                throw new Exception("Неопознанный тип данных");
            }
            return new ValidationResult(true, null);
        }
    }[/code]

Такой класс гораздо более гибок, теперь его можно привязывать к различным TextBox-ам указывая только целевой тип для проверки:

[code:xml]<local:ConvertToTypeRule RuleType="System.Double" />[/code]

Изменение стиля отображения ошибок

Если теперь запустить код и ввести в TextBox символы, рамка вокруг него станет красной. И все. Никакой информации о возникшей ошибке пользователь не получит. Поэтому переопределим шаблон TextBox-а и его стиль, так чтобы:

  1. При наведении мыши на TextBox-е выводилась всплывающая подсказка, сообщающая об ошибке ввода.
  2. При редактировании значения задний фон TextBox-а окрашивался в зеленый цвет в случае верной информации и красной при ошибочных значениях.
  3. Шаблон можно было быстро переопределять в Blend-е, например налету изменять цвета фона. Для этого вынесем шаблон и стиль в отдельный ресурсный словарь, при подключении которого заданный стиль будет автоматически применятся ко всем элементам TextBox.

[code:xml]    Цвет заднего фона при вводе верных данных
    <SolidColorBrush x:Key="tbEditBackgroundBrush" Color="#FFEEF6ED"/>
    Цвет заднего фона при вводе ошибочных данных
    <SolidColorBrush x:Key="tbErrorBackgroundBrush" Color="#FFF9B6B3"/>

    Шаблон TextBox
    <ControlTemplate x:Key="TextBoxTemplate" TargetType="{x:Type TextBox}">
        . . .
        <ControlTemplate.Triggers>
           . . .
            <Trigger Property="Validation.HasError" Value="True">
                <Setter Property="ToolTip"
                        Value="{Binding RelativeSource={RelativeSource Self},
                        Path=(Validation.Errors)[0].ErrorContent}" />
                <Setter Property="Background" TargetName="Bd" Value="{StaticResource tbErrorBackgroundBrush}"></Setter>
            </Trigger>

        </ControlTemplate.Triggers>
    </ControlTemplate>

    Стиль TextBox (применяется автоматически ко всем элементам)
    <Style TargetType="{x:Type TextBox}">
        Очищаем шаблон ошибки - эту функциональность мы реализовали в TextBoxTemplate
        <Setter Property="Validation.ErrorTemplate" Value="{x:Null}"></Setter>
        <Setter Property="Control.Template" Value="{StaticResource TextBoxTemplate}"></Setter>
    </Style>
</ResourceDictionary>[/code]

Ограничение пределами и моделью

Следующим этапом, необходимо реализовать проверку попадания входных значений в интервалы [0;100]. Тут нам на помощь приходит интерфейс IDataErrorInfo. Интерфейс содержит всего одно свойство Error (которое WPF не использует) и строковый индексатор. При вводе данных или обновлении целевого свойства WPF обращается к этому строковому индексатору, передовая ему имя свойства, чтобы проверить корректность значения. Индексатор должен выполнить проверку и, в случае провала, вернуть строку, содержащую ошибку. Если проверка прошла успешно, индексатор возвращает null.

Для данной задачи реализация этого интерфейса может выглядеть так:

[code:c#]        public string this[string propertyName]
        {
            get
            {
                Boolean validateAll = (propertyName == "");
                StringBuilder errorsText = new StringBuilder();

                if ((validateAll) || (propertyName == "VolumeLiquid"))
                {
                    if ((VolumeLiquid < 0) || (VolumeLiquid > 100))
                        errorsText.Append("Неверные пределы!");
                }
                if ((validateAll) || (propertyName == "VolumeRepository"))
                {
                    if ((VolumeRepository < 0) || (VolumeRepository > 100))
                        errorsText.Append("Неверные пределы!");
                }
                if ((validateAll) || (propertyName == "VolumeLiquid"))
                {
                    if (VolumeLiquid > VolumeRepository)
                        errorsText.Append("Неверные пределы!");
                }

                if (errorsText.ToString() == "")
                    return null;
                else
                    return errorsText.ToString();
            }
        }
        public string Error { get { return this[""]; } }[/code]

В данном примере подразумевается, что передача пустой строки в индексатор дает команду проверить все свойства на наличие ошибок. Таким образом свойство Error в любом момент времени возвращает состояние объекта. Если свойство пусто - объект стабилен.

Для включения мониторинга корректности значений необходимо установить двум TextBox-ам свойство ValidatesOnDataErrors в значение "True".

Проблема зависимых величин

Но такой код будет иметь один недостаток. Рассмотрим такую последовательность действий:

  1. Вводим объем хранилища, равный 20.
  2. Вводим объем жидкости, равный 30 (при этом поле окрашивается красным, сигнализируя о том, что превышен допустимый предел значения).
  3. Увеличиваем объем хранилища с 20 до 40.

После этого поле "объем жидкости", по прежнему сигнализирует об ошибке, хотя значение уже не является неверным. Для решения этой проблемы слегка модифицируем установщик свойства VolumeRepository:

[code:c#]        public Double VolumeRepository
        {
            . . .
            set { _VolumeRepository = value;
                NotifyPropertyChanged("VolumeRepository");
                NotifyPropertyChanged("VolumeLiquid"); }
        }[/code]

Вызов NotifyPropertyChanged для VolumeLiquid дает команду WPF пересчитать корректность объема жидкости при изменении объема хранилища.

Подведем итог - при реализации зависимостей между величинами необходимо заставлять WPF делать перерасчет всех зависимых значений, при изменении одного из них.

Возможность расчета или "активная кнопка"

В приложении осталась одна уязвимость - пользователь может кликнуть на кнопке расчета, когда исходные данные находятся в некорректном состоянии. Чтобы предотвратить такую возможность воспользуемся событием Validation.Error. Для включения отправки этого события установим свойство NotifyOnValidationError в значение "True" у TextBox-ов. Событие Error является всплывающим, поэтому создадим единый обработчик и прикрепим его к Grid-у, содержащему все TextBox-ы.

[code:c#]        private void Grid_Error(object sender, ValidationErrorEventArgs e)
        {
            if (e.Action == ValidationErrorEventAction.Added)
                button1.IsEnabled = false;
            else
            {
                button1.IsEnabled = true;
                foreach (TextBox t in Grid1.Children.OfType<TextBox>())
                    if (Validation.GetHasError(t)) button1.IsEnabled = false;
            }
        }[/code]

[code:xml]<Grid Name="Grid1" Validation.Error="Grid_Error">[/code]

Свойство Action у аргумента события сообщает, когда возникло это событие - при удалении какой-либо ошибки, или при добавлении таковой.

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

Тут присутствует одна странность - выше было описано свойство logic.Error, возвращающее null, если объемт находится в корректном состоянии. Возникает вопрос - зачем писать дополнительную логику перебора, когда можно проверить это свойство? Все дело в том, что событие Validation.Error возникает ДО того, как новые данные запишутся в объект, т.е. свойство Error вернет результат проверки предыдущего состояния.

Атрибуты проверки свойств

Программа, созданная к этому моменту вполне работоспособна, но в ней все же присутствует еще одна проблема. Что, если у бизнес-объекта не три свойства, а 300 и каждое из них необходимо проверить? Тогда тело индексатора может раздуться до размеров реализации самой логики. А если еще большинство проверок для разных свойств однотипны, то возникает много дублируемого кода.

Самым простым способом наложения ограничений на свойства, при этом, не "раздувая" тело индексатора и не дублируя код, является реализация атрибутов проверки. Создадим единый базовый интерфейс:

[code:c#]public interface ILimitInspection
    {      
        String Inspection(Object Sender, Object Value);
    }[/code]

Единственный метод интерфейса принимает объект, свойство которого подлежит проверки, и значение, подлежащее проверке. Реализуем этот интерфейс для ограничения значения свойства пределами. Ничего сложно в реализации такого объекта нет, главное сделать его максимально обобщенным:

[code:c#]    [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
    public class LimitDoubleAttribute : Attribute, ILimitInspection
    {
        private Double _MinValue = Double.MinValue;
        private Double _MaxValue = Double.MaxValue;
        private Boolean _IncludeMin = false;
        private Boolean _IncludeMax = false;

        public Double MinValue { get { return _MinValue; } }
        public Double MaxValue { get { return _MaxValue; } }
        public Boolean IncludeMin { get { return _IncludeMin; } }
        public Boolean IncludeMax { get { return _IncludeMax; } }

        public LimitDoubleAttribute(Double MinValue)
        {
            _MinValue = MinValue;
        }
        public LimitDoubleAttribute(Double MinValue, Double MaxValue)
        {
            _MinValue = MinValue;
            _MaxValue = MaxValue;
            if (MinValue > MaxValue)
                throw new Exception("MinValue > MaxValue!");
        }
        public LimitDoubleAttribute(Double MinValue, Double MaxValue, Boolean IncludeMin, Boolean IncludeMax)
            : this(MinValue, MaxValue)
        {
            _IncludeMin = IncludeMin;
            _IncludeMax = IncludeMax;
        }

        public String Inspection(Object Sender, Object Value)
        {
            Double value = (Double)(Value);
            if (((IncludeMin) && (value < MinValue)) || ((!IncludeMin) && (value <= MinValue)))
                return "Величина меньше минимальной.";
            if (((IncludeMax) && (value > MaxValue)) || ((!IncludeMax) && (value >= MaxValue)))
                return "Величина больше максимальной.";
            return "";
        }
    }[/code]

Свойства MinValue и MaxValue устанавливают нижний и верхний пределы, а IncludeMin и IncludeMax указывают, стоит ли включать края отрезка в допустимую зону. Применим этот атрибут к свойствам:

[code:c#]        [LimitDouble(0, 100)]
        public Double VolumeRepository
        . . .
        [LimitDouble(0, 100)]       
        public Double VolumeLiquid[/code]

Осталось переписать строковый индексатор, чтобы он проверял значения исходя из установленных атрибутов. Тут нужно заметить, что атрибутов может быть несколько, поэтому необходимо перебирать их все. Про оптимизацию такого алгоритма перебора я напишу чуть ниже.

[code:c#]                PropertyInfo[] propInfos = typeof(SimpleClass).GetProperties();
                foreach (PropertyInfo propInfo in propInfos)
                {
                    if ((validateAll) || (propInfo.Name == propertyName))
                    {
                        Attribute[] attrs = Attribute.GetCustomAttributes(propInfo);
                        foreach (Attribute attr in attrs)
                        {
                            if (attr is ILimitInspection)
                            {
                                Object Value = propInfo.GetValue(this, null);
                                String err = (attr as ILimitInspection).Inspection(this, Value);
                                if (err != "")
                                {
                                    errorsText.AppendLine(err);
                                }
                            }
                        }
                    }
                }[/code]

В завершение перепишем возвращаемое значение, чтобы оно удаляло лишнюю строку, которая добавляется вызовом последнего AppendLine:

[code:c#]else
{ errorsText.Remove(errorsText.Length - 2, 2);
     return errorsText.ToString();  }[/code]

LimitPropertyAttribute

К сожалению, каким бы общим не был LimitDoubleAttribut, мы не можем использовать его для проверки зависимостей между свойствами (а именно это нам и нужно, чтобы реализовать условие VolumeLiquid<VolumeRepository).

Создадим новый атрибут LimitPropertyAttribute. Свойства MinProperty и MaxProperty принимают названия свойств, значения которых должны служить пределами. Если передать пустую строку в качестве одного из имен свойств - пределов, то этот предел будет игнорироваться.

Как Вы успели заметить, при реализации первого атрибута мы не использовали объект Sender, указанный в интерфейсе. Теперь он нам понадобиться, чтобы рефлексно добираться до свойств, служащих границей.

[code:c#]    [AttributeUsage(AttributeTargets.Property, Inherited = false, AllowMultiple = true)]
    public class LimitPropertyAttribute : Attribute, ILimitInspection
    {
        private String _MinProperty = "";
        private String _MaxProperty = "";
        private PropertyInfo MinPropertyInfo;
        private PropertyInfo MaxPropertyInfo;

        public String MinProperty { get { return _MinProperty; } }
        public String MaxProperty { get { return _MaxProperty; } }

        public LimitPropertyAttribute(String MinProperty, String MaxProperty)
        {
            _MinProperty = MinProperty;
            _MaxProperty = MaxProperty;
        }

        public String Inspection(Object Sender, Object Value)
        {
            if (MinProperty != "")
            {
                if (MinPropertyInfo == null)
                {
                    MinPropertyInfo = Sender.GetType().GetProperty(MinProperty);
                }
                Object minValue = MinPropertyInfo.GetValue(Sender, null);
                if ((minValue as IComparable).CompareTo(Value) > 0)
                    return "Величина меньше минимальной.";
            }
            if (MaxProperty != "")
            {
                if (MaxPropertyInfo == null)
                {
                    MaxPropertyInfo = Sender.GetType().GetProperty(MaxProperty);
                }
                Object maxValue = MaxPropertyInfo.GetValue(Sender, null);
                if ((maxValue as IComparable).CompareTo(Value) < 0)
                    return "Величина больше максимальной.";
            }
            return "";
        }
    }[/code]

При первом вызове проверки (метода Inspection) мы получаем и кэшируем объекты PropertyInfo, что повышает производительность при будущих проверках. Теперь можно подключить атрибут к свойству VolumeLiquid:

[code:c#]        [LimitDouble(0, 100)]
        [LimitProperty("", "VolumeRepository")]
        public Double VolumeLiquid[/code]

Кэширование

Пора задуматься о производительности. Рефлексный перебор всех свойств и атрибутов занимает слишком много времени. Гораздо практичнее составить список свойств, к которым применены атрибуты, поддерживающие интерфейс ILimitInspection, заранее. Например, при создании объекта. Для этого опишем еще один класс:

[code:c#]    public class PropertyCache
    {
        private PropertyInfo _Property;
        private List<Attribute> _Attributes;

        public PropertyInfo Property { get { return _Property; } }
        public List<Attribute> Attributes { get { return _Attributes; } }

        public PropertyCache(PropertyInfo Property)
        {
            _Property = Property;
            _Attributes = new List<Attribute>();
            Attribute[] AttributesArray = Attribute.GetCustomAttributes(Property);
            foreach (Attribute a in AttributesArray)
                if (a is ILimitInspection)
                {
                    _Attributes.Add(a);
                }
        }
    }[/code]

Такой класс хранит в себе информацию о конкретном свойстве и обо всех атрибутах, примененных к нему и поддерживающих интерфейс ILimitInspection. Эту информацию класс собирает один раз на этапе создания.

В классе бизнес - логики добавим одно поле, содержащее список из вышеприведенных классов:

[code:c#]private List<PropertyCache> PropertysCache;[/code]

Этот список будем заполнять в конструкторе:

[code:c#]        public SimpleClass()
        {
            PropertysCache = new List<PropertyCache>();
            PropertyInfo[] PropertyInfos = typeof(SimpleClass).GetProperties();
            PropertyCache p;
            foreach (PropertyInfo pInfo in PropertyInfos)
            {
                p = new PropertyCache(pInfo);
                if (p.Attributes.Count > 0)
                    PropertysCache.Add(p);
            }
        }[/code]

В список попадают только те свойства, для которых определены какие-либо граничные условия. Теперь на этапе проверки корректности необходимо использовать не рефлексию, а кэшированный список:

[code:c#]                foreach (PropertyCache p in PropertysCache)
                {
                    if ((validateAll) || (p.Property.Name == propertyName))
                    {
                        foreach (Attribute a in p.Attributes)
                        {
                            Object Value = p.Property.GetValue(this, null);
                            String err = (a as ILimitInspection).Inspection(this, Value);
                            if (err != "")
                            {
                                errorsText.AppendLine(err);
                            }
                        }
                    }
                }[/code]

Заключение

Если еще глубже задуматься о производительности, можно придти к выводу. что слабым местом остаются вызовы PropertyInfo.GetValue. Достаточно отказаться от них и использовать fastproperty обертки, но это "уже совсем другая история"...

Если у кого-нибудь возникнут вопросы или комментарии - пишите :)

С наилучшими пожеланиями, Степанов Евгений.

Полезные ссылки

Практическое руководство. Реализация проверки привязки

Класс Validation

Исходные коды

WpfDataBinding.rar (43,09 kb)

Горячие клавиши Visual Studio 2008

Немного о MS Visual Studio

MS Visual Studio - это удобный и функциональный инструмент для разработки приложений на языке c#. В отличие от таких инструментов как CodeGear RAD Studio, студия от microsoft является менее заторможенной (да, такое бывает Улыбаюсь) и содержит инструменты для работы с самыми новыми технологиями.

Но, смотря за работой коллег в Visual Studio, мне порой кажется, что люди могли бы с тем же успехом использовать блокнот для написания исходного кода. А, между тем, среда предоставляет огромный спектор функций именно для удобного создания, редактирования и просмотра иходного кода. Одним из таких иструментов являются горячие клавиши, о которых и пойдет речь далее.

Горячие клавиши Visual Studio 2008

Далее перечисленные лишь некоторые полезные команды, которые быстро запоминаються и могут часто использоваться в работе над кодом. Полный список и описание команд можно найти по ссылкам в конце поста.

[more]

Редактирование кода

Ctrl+Enter – новая строка выше курсора

Ctrl+L – вырезает строку, на которой находиться курсор

Ctrl+Del и Ctrl+Back Space – удаление слов целиком

Shift+Alt+↑↓→← - выделение вертикальных/горизонтальных блоков текста

Ctrl+U – перевести выделенный текст в строчный регистр

Ctrl+Shift+U – перевести выделенный текст в ЗАГЛАВНЫЙ регистр

F2изменение имени выделенного объекта (Refactor→Rename)

Автоформатирование кода

Ctrl+K+F – автоформатирование выделенного блока

Ctrl+K+D – автоформатирование всего документа

Быстрый поиск

Ctrl+I – быстрый поиск набираемого текста вниз

Ctrl+Shift+I – быстрый поиск набираемого текста вверх

Ctrl+F3 – быстрый поиск выделенного текста вниз

Закладки

Ctrl+K+K – установить/стереть закладку

Навигация

Ctrl+← и Ctrl+→ - перемещение по словам

Ctrl+ и Ctrl+↑ - перемещение экрана по строкам без переноса курсора

Ctrl+Page UP – курсор вверх экрана

Ctrl+Page Down – курсор вниз экрана

Ctrl+Tab – перемещение по вкладкам и окнам

Сниппеты

Ctrl+K+X – показать доступные сниппеты

Tab – создание кода выбранного сниппета и навигация по его составным частям

Esc – завершение редактирования

Подсказки

Ctrl+Space – открыть набор доступных элементов

Ctrl+Shift+Space – подсказка по параметрам функции

Ctrl+K+I – подсказка по функции

Структура кода

Ctrl+M+O – сворачивает код классов в структуру

Ctrl+M+L – сворачивает/разворачивает код

Комментарии

Ctrl+K+C – закомментировать выделенный блок

Ctrl+K+Uраскомментировать выделенный блок

/// - вставка комментариев к методу

Многооконное редактирование

Многооконным редактированием называется одномременное редактирование файлов (или файла) в двух и более окнах. Такая функция полезна для счастливых обладателей широкоформатных (и нетолько) мониторов с большой диаганалью (22 дюйма и выше). Далее перечислены три наиболее полезные из этих функций:

Window -> Split - разделяет текущее окно на две части по горизонтали. В обеих частях отображается один и тот же файл с кодом.

Window –> New window - создает новое окно с тем же файлом кода.

Window ->  New vertical tab group - переносит окно в новую вертикальную вкладку, что позволяет одномермено редактировать 2 различных файла паралельно.

Полезные ссылки

Секреты Visual Studio - Советы пользователям Microsoft Visual Studio от Сары Форд (и не только)

Библиотека Cimanu.MapInfo (Версия 0.1)

Кратко про MapInfo и интегрированную картографию

В настоящее время существует множество картограффических пакетов, позволяющих работать с огромными объемами географических и других данных. Одним из таких пакетов и является MapInfo.

Пакет MapInfo не представлял бы интереса, если бы его нельзя было использовать в сторонних (собственных) программах. Использование механизма MapInfo для работы с картами в сторонних программах на различных языках программирования называется интегрированной картографией. О ней и пойдет речь далее.

Для работы с MapInfo "извне" используется специальный язык MapBasic, внешне напоминающий VB . Также существует готовая функциональная библиотека MapX, но она стоит немалых денег.

Введение

Месяц назад мне пришлось делать поддержку карт MapInfo для существующей программы, активно использующей растровые изображения в качестве карт. Перерыв весь интернет, насткнулся на несколько русскоязычных интересных ресурсов, ссылки на которые приведены в конце поста. С помощью нескольких замечательных людей, мне удалось быстро реализовать поддержку MapInfo.

По ходу написания, я вынес весь код взаимодействия на MapBasic в отдельную сборку, которую и представляю сейчас.

Библиотека MapInfo

  • Платформа: .NET Framework 2.0 (и выше)
  • Язык: C#
  • Среда разработки: Visual Studio 2008 Professional
  • Версия: 0.1 (Alpha)
  • Физическое расположение в двух файлах:
    • Cimanu.MapInfo.CallBack.dll
    • Cimanu.MapInfo.dll
  • Документация: XML, неполная
  • Для работы необходима установленная среда MapInfo Professional 9.x.x

[more]

Что может библиотека

  • Создавать сервер-приложение MapInfo или подключатся к запущенному процессу.
  • Работать с картами в окне программы, или в среде MapInfo.
  • Открывать карты из файлов, управлять уже открытыми картами:
    • Получать/устанавливать параметры карты: еденицы измерения, масштаб, область видимости;
    • Управлять количеством карт;
    • Закрывать, обновлять, помещать поверх других и изменять размер окон с картами;
    • Добавлять к карте новые слои.
  • Получать/устанавливать параметры текущей сессии.
  • Работать с таблицами:
    • Открывать, закрывать таблицы;
    • Читать данные любые из таблиц, в т.ч. информацию о столбцах;
    • Рисовать граффические объекты в таблицах и косметическом слое карт:
    • Точки, в т.ч. специальные символы;
    • Линейные объекты: линии, дуги, полилинии;
    • Площадные объекты: круги, эллипсы, прямоугольники, произвольные области.
  • Получать обратные вызовы (CallBack) от MapInfo.
  • Создавать/изменять/управлять пользовательскими панелями инструментов и меню.
  • Обращатmся непосредственно к MapInfo на языке MapBasic и получать типизированные данные: Int32, Double, String.
  • Получение информации по слоям карты

Что в планах

  • Механизм различных запросов к MapInfo на языке, аналогичном SQL:
    • Выборки из таблиц
    • Получение суммарной информации по таблицам
  • Механизм поиска объектов на карте
  • Поддержка текста
  • Поддержка меток в слоях
  • Исправление багов Улыбаюсь

Быстрый старт

Чтобы начать работу с MapInfo через библиотеку достаточно добвить Cimanu.MapInfo в References проекта и объявить пространство имен:

[code:c#]using Cimanu.MapInfo; [/code]

Главным классом в сборке является Controller. Чтобы подключиться к MapInfo достаточно создать экземпляр данного класса с требуемыми параметрами. Подробнее опишу чуть ниже.

Примечание: Объект Controller должен быть ОДИН на всю программу. Его можно передавать из формы в форму, или сделать Одиночкой - по желанию.

Демо-приложение

Так как значительная часть кода не документирована, я написал простое демо-приложение на WinForms, в котором постарался охватить все аспекты работы с Cimanu.MapInfo. Приложение состоит из четырех частей:

  1. Форма для создания (или подключения к...) сервера и установки первичных параметров приложения.
  2. Форма для отображения информации о сессии и текущей активной карте. также на этой форме находятся управляющие кнопки.
  3. Форма, содержащяя карту и пользовательские элементы управления.
  4. Форма для отображения любых данных из открытых таблиц.

Работа программы с новом сервером (рис 1) и существующем сервером (рис 2) выглядет так:

          

Демо-приложение и библиотеку, а также одну карту MapInfo, прикладываю к этому посту.

Создание сервер-приложения MapInfo или подключение к запущенному процессу, а также работа с картами

Для выполнение одной из этих операций достаточно создать объект класса Controller. Конструкторы:

  • Controller = new Controller(ServerType.ActiveServer); - подключаемся к запущенному процессу. Если такового не имеется выбрасывается исключение и просьба запустить MapInfo.

  • Controller = new Controller(ServerType.NewServer); - создается новое приложение. Запуск приложения занимает 4-10 секунд в зависимости от мощности машины. В будущем планируется автоматическое отображение заставки, поясняющей что идет работа по запуску MapInfo.

Image Hosting

Далее необходимо открыть карту. Делается это с помощью команды Controller.NewMap(FileName) или Controller.NewMap(FileName, Handle). Такие команды заставляют MapInfo открыть таблицу FileName (полный путь к таблице) и создать для этой таблицы окно карты. Если указан параметр Handle, то карта откроется на панели (окне) с этим Handle, иначе в приложении MapInfo.

Чтобы открыть таблицу, но не создавать карту для нее можно воспользоваться командой Controller.OpenTable(FileName).

Доступ к активной карте

MapInfo одновременно может работать с несколькими картами. Чтобы получить коллекцию таких карт достаточно вызвать метод GetMaps() у объекта Controller. Текущую активную карту можно получить из свойства ActiveMap.

Для получения данных о карте, необходимо обратится к свойству Mapper. Например Mapper.Scale вернет текущий масштаб карты.

Бывают случаи, когда необходимо работать с какой-то определенной картой, в то время, как пользователь может преключатся между несколькими, открывать или закрывать их. Решением является создание переменной для определенной карты и работа именно с этой переменной, а не с объектом Controller:

[code:c#]Map = Controller.NewMap(FileName, panel.Handle);[/code]

Работа с данными

Хранением данных в MapInfo занимаются таблицы. Чтобы начать работу с таблицей, необходимо ее открыть. Список всех открытых таблиц можно вывести в DataGridView например так:

[code:c#]foreach (Table t in Controller.GetTables())
{
     dg.Rows.Add(t.Name, t.RowCount, t.ColCount, t.IsMappable, t.CoordSys.Identification, t.ReadOnly);               
}[/code]

Для чтения данных из таблицы в библиотеке предусмотренны два механизма:

  1. Для чтения определенной записи прекрасно подойдет метод GetRow() объекта Table.
  2. Для вывода большого объема информации из таблицы лучше использовать класс Reader, свойство объекта Table.

Пользовательский интерфейс

Когда карта открыта внутри приложения, необходимо иметь доступ к инструментам управления. Этим занимаются классы Tool и Menu. Например выбрать элемент "Рука" для перемещения по карте можно вызвать команду Controller.Tool.ActiveToolItem(StandartTool.Grabber).

При всем многообразии стандартных инструментов MapInfo бывают случаи, когда необходимо написать собственный обработчик некоего события. Такой инструмент с собственным обработчиком событий можно создать таким образом:

[code:c#]Controller.Tool.AddToolItem("MyPanel", "MyTool", "Мой инструмент", Cimanu.MapInfo.Cursor.Arrow, Cimanu.MapInfo.Icon.MI_ICON_ARROW, Cimanu.MapInfo.DrawMode.Point, true);
Controller.Tool["MyTool"].Click += new EventHandler<ToolClickEventArgs>(MyTool_Click);
void MyTool_Click(object sender, ToolClickEventArgs e)
{
     //Do Something
}[/code]

Процедуры вида MyTool_Click называют обратными вызовами. Подробнее - следущий раздел.

Обратные вызовы

Когда MapInfo необходимо уведомить программу о каком-либо событии, они посылает ему асинхронное (относительно программы) сообщение. Модуль Cimanu.MapInfo.CallBack занимается приемом таких вызовов, синхронизацией их и передачей вверх в Cimanu.MapInfo.

Таким образом процедура MyTool_Click из предыдущего раздела является потокобезопасной, т.е. в ней можно беспрепятственно обращаться к форме и ее элементам.

Порой бывает необходимо отследить какие-либо изменения в открытой карте (перемещение, масштаб и т.д.). Для таких случаев прекрасно подходит подписка на событие UpdateMapStatus:

[code:c#]Controller.UpdateMapStatus += new EventHandler<EventArgs>(Controller_UpdateMapStatus)
public void Controller_UpdateMapStatus(object sender, EventArgs e)
{                       
      //Do Something
}[/code]

Рисование граффических объектов

Рисовать объекты можно двумя способами:

  1. Создавать граффический объект и вставлять его в таблицу. Данный механизм в библиотеке недостаточно проработан, его я касаться не буду.
  2. Создавать объекты непосредственно для открытой карты. Об этом далее.

Чтобы нарисовать объект на карте, надо выбрать слой для рисования. Это может быть косметический слой (такие объекты не будут сохраняться после закрытия карты, если конечно не сохранить принудительно косметический слой в таблицу). А может быть какая-нибудь таблица, которая отражена на карте.

В любом случае для отраженной таблицы существует понятие "Слой (Layer)". Именно у слоя существует объект Canvas с помощью которого можно рисовать примитивы. Пример:

[code:c#]Map.CosmeticLayer.Canvas.Brush = new Cimanu.MapInfo.Brush(53, Color.Red, Color.Transparent);
Map.CosmeticLayer.Canvas.Pen = new Cimanu.MapInfo.Pen(2, 7, Color.Blue);
Map.CosmeticLayer.Canvas.DrawCircle(10, 10, 5);[/code]

Обращение к MapInfo посредством языка MapBasic

Это очевидная возможность, на которую стоит обратить внимание. Можно не только послать команду MapInfo, но и получить результат. Класс OutServer, доступ к которому осуществляется посредством поля контроллера, отвечает за обработку параметров и возвращение необходимого результата.

К примеру мы можем напрямую обратиться к MapInfo и узнать масштаб определенной карты:

[code:c#]Double Scale = Controller.OutServer.EvalDouble("MapperInfo({0},1)", MyMap.Window.ID);[/code]

В этой строке символ {0} заменится на значение MyMap.Window.ID независимо от языка, установленного в системе. Таким образом проблема точек и запятых при отделеннии дробной части от целой решается на самом низком уровне библиотеки как для входных параметров, так и для выходных результатов.

Заключение

Как я уже упоминал в своем первом посте, значительную часть библиотеки я дописывал во внерабочее время, и, следовательно, практически не отлаживал в "полевых условиях".

Если библиотека окажется востребованной, я продолжу над ней работу, буду исправлять баги (которые наверняка проявятся) и добавлять новые функции (которые наверняка понадобяться :)). А также вести этот блог, отвечать на вопросы и отвещать неясные моменты :)

Жду ваших писем, с наилучшими пожеланиями, Степанов Евгений.

Полезные ссылки

Оффициальный сайт MapInfo

Статья про использование интегрированной картографии в Delphi

Формум, посвященный работе с MapInfo и MapBasic

Форум картографов MapInfo

Тема на одном из форумов, посвященная MapBasic

Скачать библиотеку

Cimanu.MapInfo.rar (153,79 kb)

Об этом блоге или первая запись

Приветствую тебя, Читатель, на моем небольшом блоге, посвященном информационным технологиям.

В своем блоге я буду отмечать различные аспекты работы с языком программирования C# и средой разработки Visual Studio. С# является одним из самых эффективных языков программированя. На данный момент он обретает все большую популярность.

Работая над программой, часто возникают ситуации, когда для решения какой-то глобальной задачи приходиться писать большое количество дополнительного кода. С недавних пор, такой код я стал выносить в отдельные библиотеки, которые изменяю и дорабатываю во внерабочее время. Например, в результате напряженной работы :) была получена библиотека для взаимодействия с пакетом MapInfo. Как Вы успели заметить, в блоге фигурирует название "Библиотека Cimanu". Оно возникло от имени пространства имен (Cimanu) в которое я помещаю все такие решения.

Надеюсь, кто-либо из Вас подчерпнет здесь интересную информацию, c уважением,

Евгений Степанов.