В проекте написанном на WinForms необходимо было показать форму WPF. Показывается, все замечательно. Но... При попытке что-то набрать в TextBox-е в нем отображаются только пробелы. Все остальные клавиши игнорируются. Ларчик открывался просто. При показе WPF окна надо вызывать не Show, а ShowDialog. Иначе ввод не работает. Вот как так, а?
Архив рубрики: WPF
При показе из WinForms приложения WPF окна в его TextBox-ах не вводится текст
В проекте написанном на WinForms необходимо было показать форму WPF. Показывается, все замечательно. Но... При попытке что-то набрать в TextBox-е в нем отображаются только пробелы. Все остальные клавиши игнорируются. Ларчик открывался просто. При показе WPF окна надо вызывать не Show, а ShowDialog. Иначе ввод не работает. Вот как так, а?
Работа с файлами Excel пользователем через интерфейс, на основе SpreadsheetGear
Возникла задача дать пользователю возможность ввести данные в приложении, но на основе шаблона загруженного из Excel файла. Или, иными словами, грузиться и показывается пользователю Excel файл. Файл заранее подготовлен и данные можно вводить только в определенные места (отличающиеся от файла к файлу). Стоит задача показать такой файл, выполнить с ним некоторые операции, ну и считать из него данные по завершению ввода.
данная статья сборник небольших примеров решения такой задачи с использование компонента SpreadsheetGear.
1. Установка и подключение в проект
Здесь все просто. Качаем бесплатную версию здесь. Запускаем инсталятор, далее, далее, далее, конечно же, не забыв поставить галку, что вы согласны с лицензией. Запускаем VS, создаем проект WPF и в Панели инструментов видим:
Все, можно перетаскивать в разметку. Для любителей все делать самому, можно добавить в проект ссылки на две библиотеки (все картинки кликабельны):
Ну и в XAML, подключаем пространство имен и размещаем компонент WorkbookView в нужном нам месте:
Во всех остальных примерах, я буду использовать приложение с вот такой разметнкой главной формы:
Ну и все примеры, это обработчики соответствующих кнопок.
2. Загрузка файла XLS в компонент
А вот так, для книги в которой листы защищены (нет сетки и заголовков строк и столбцов):
Т.к. мне представляет интерес работа именно с защищенными листами, то все остальные картинки будут именно с ними.
3. Скрываем листы книги, которые нам не нужны
Перед началом работы с элементами книги, необходимо заблокировать ее, а по окончании работы освободить. В остальных примерах я на этом уже не буду останавливаться, но везде будут соответствующие блоки.
При попытке скрыть последний лист, возникает исключение.
4. Работа с именованными блоками
Если в книге есть именованные блоки, то мы можем как внести в них данные, так и считать их. Например, я буду вносить по кнопке почтовый индекс. Блок в котором он находится имеет имя "П000020020001".
Со значениями в компонентах для эмуляции Excel, отдельная песня. Value у диапазона или ячейки имеет тип object. У SpreadsheetGear, чтобы определить тип хранящегося значения есть специальное свойство ValueType. Вот так, например, можно проверить какого типа значение храниться в ячейке:
Если поправить пример с добавлением вот так:
Т.е. в зависимости от типа присвоенного значения, определяется тип ячейки.
Ну и самый интересный эксперимент. Присвоим ячейке дату:
Обращают на себя внимание два факта. Во-первых, дата показывается правильно, а вот тип ячейки определяется как числовой. Печально...
5. Определение ячейки и блока редактируемых пользователем
Речь идет о редактировании ячейки, т.е. пока ячейка не перешла в состояние редактирования события не вызываются. Ну и для решения этой задачи есть два варианта.
В рамках первого способа, мы можем обрабатывать клики мышкой, для этого подписываемся в компоненте на соответствующее событие:
На что следует обратить внимание:
а) Хотя мы и подписывались на MouseUp событие вызовется только при первом переходе ячейки в редактирование. Выделение ячейки событие не вызывает, также как и повторные клики на редактируемой ячейке.
б) Получить блок в который входит ячейка можно двумя способами. Разницы в них не заметил.
в). Ни один из способов получения ячейки не позволяет понять, что этот Range именованный:
Второй способ, на мой взгляд более правильный, это обрабатывать события связанные с ячейкой. В данном случае, нас интересует CellBeginEdit. В данном случае, обработчик будет значительно проще:
6. Добавление строк и копирование части листа
В оригинальном листе есть блок:
Я хочу его скопировать и вставить под оригинальный М1. Сделать это можно достаточно просто:
7. Присвоение имен блокам
Последний на сегодня пример, связанный, кстати, с предыдущим. Блоки подсвеченные синим у меня имен не имеют, я хочу подсвеченным ячейкам внутри М1 эти имена дать, чтобы было удобнее присваивать в них значения.
Вот так выглядит код присвоения имен:
но, т.к. мы их присваивали через раздел, то присвоение по имени будет выглядеть:
На этом все, мое мнение, все это весьма неоднозначно и связано с неоправданными сложностями. Всякие WPF вкусности типа Binding применить не получится. Для узкой и специфичной задачи возможно применение этого компонента имеет смысл, ну а так...
данная статья сборник небольших примеров решения такой задачи с использование компонента SpreadsheetGear.
1. Установка и подключение в проект
Здесь все просто. Качаем бесплатную версию здесь. Запускаем инсталятор, далее, далее, далее, конечно же, не забыв поставить галку, что вы согласны с лицензией. Запускаем VS, создаем проект WPF и в Панели инструментов видим:
Все, можно перетаскивать в разметку. Для любителей все делать самому, можно добавить в проект ссылки на две библиотеки (все картинки кликабельны):
Ну и в XAML, подключаем пространство имен и размещаем компонент WorkbookView в нужном нам месте:
<Window x:Class="SpreadsheetGearTestProject.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:sg="clr-namespace:SpreadsheetGear.Windows.Controls;assembly=SpreadsheetGear2012.Windows.WPF"
Title="MainWindow" Height="350" Width="525">
<Grid>
<sg:WorkbookView />
</Grid>
</Window>
Можно запускать.Во всех остальных примерах, я буду использовать приложение с вот такой разметнкой главной формы:
Ну и все примеры, это обработчики соответствующих кнопок.
2. Загрузка файла XLS в компонент
private void Load_Click(object sender, RoutedEventArgs e)
{
// Настраиваем культуру, в рамках которой будет читаться файл,
// можем, например, использовать не установленную в системе, а задать требуемую нам
SpreadsheetGear.IWorkbookSet workbookSet =
SpreadsheetGear.Factory.GetWorkbookSet(System.Globalization.CultureInfo.CurrentCulture);
// Загружаем книгу
SpreadsheetGear.IWorkbook workbook = workbookSet.Workbooks.Open("DOC1152016_505.xls");
// Передаем книгу в компонент
wvExcel.ActiveWorkbook = workbook;
}
Вот так это выглядит для обычной книги:А вот так, для книги в которой листы защищены (нет сетки и заголовков строк и столбцов):
Т.к. мне представляет интерес работа именно с защищенными листами, то все остальные картинки будут именно с ними.
3. Скрываем листы книги, которые нам не нужны
Перед началом работы с элементами книги, необходимо заблокировать ее, а по окончании работы освободить. В остальных примерах я на этом уже не буду останавливаться, но везде будут соответствующие блоки.
wvExcel.GetLock();
try
{
wvExcel.ActiveWorkbook.ActiveSheet.Visible = SpreadsheetGear.SheetVisibility.Hidden;
}
finally
{
wvExcel.ReleaseLock();
}
После выполнения этого кода, будет скрыт активный лист и активным станет соседний лист:При попытке скрыть последний лист, возникает исключение.
4. Работа с именованными блоками
Если в книге есть именованные блоки, то мы можем как внести в них данные, так и считать их. Например, я буду вносить по кнопке почтовый индекс. Блок в котором он находится имеет имя "П000020020001".
private void AddPostCode_Click(object sender, RoutedEventArgs e)
{
wvExcel.GetLock();
try
{
var name = wvExcel.ActiveWorkbook.Names.Cast<IName>().FirstOrDefault(n => n.Name == "П000020020001");
if (name != null)
{
var range = name.RefersToRange;
range.Value = "248021";
// Т.к. ячейка может быть не на текущем активном листе,
// то для удобства проверки, перейдем на него
wvExcel.ActiveSheet = range.Worksheet;
}
}
finally
{
wvExcel.ReleaseLock();
}
}
Вот так работает:Со значениями в компонентах для эмуляции Excel, отдельная песня. Value у диапазона или ячейки имеет тип object. У SpreadsheetGear, чтобы определить тип хранящегося значения есть специальное свойство ValueType. Вот так, например, можно проверить какого типа значение храниться в ячейке:
private void CheckType_Click(object sender, RoutedEventArgs e)
{
wvExcel.GetLock();
try
{
var name = wvExcel.ActiveWorkbook.Names.Cast<IName>().FirstOrDefault(n => n.Name == "П000020020001");
if (name != null)
{
var range = name.RefersToRange;
if (range.ValueType == SpreadsheetGear.ValueType.Number)
{
MessageBox.Show(string.Format("В ячейке числовой тип значение {0}", (double)range.Value));
}
else if (range.ValueType == SpreadsheetGear.ValueType.Logical)
{
MessageBox.Show(string.Format("В ячейке логический тип значение {0}", (bool)range.Value));
}
else if (range.ValueType == SpreadsheetGear.ValueType.Empty)
{
MessageBox.Show("Ячейка пустая");
}
else
{
MessageBox.Show(string.Format("В ячейке строковый тип значение {0}", range.Value));
}
}
}
finally
{
wvExcel.ReleaseLock();
}
}
Если загрузить данные, добавить индекс и попробовать получить тип, то мы увидим:Если поправить пример с добавлением вот так:
range.Value = 248021;
То при той же последовательности действий результат будет вот такой:Т.е. в зависимости от типа присвоенного значения, определяется тип ячейки.
Ну и самый интересный эксперимент. Присвоим ячейке дату:
range.Value = DateTime.Now.Date;
Результат будет следующий:Обращают на себя внимание два факта. Во-первых, дата показывается правильно, а вот тип ячейки определяется как числовой. Печально...
5. Определение ячейки и блока редактируемых пользователем
Речь идет о редактировании ячейки, т.е. пока ячейка не перешла в состояние редактирования события не вызываются. Ну и для решения этой задачи есть два варианта.
В рамках первого способа, мы можем обрабатывать клики мышкой, для этого подписываемся в компоненте на соответствующее событие:
<sg:WorkbookViewGrid.Row="1" x:Name="wvExcel"MouseLeftButtonUp="wvExcel_MouseLeftButtonUp" />
Ну и пишем соответствующий обработчик:private void wvExcel_MouseLeftButtonUp(object sender, MouseButtonEventArgs e)
{
wvExcel.GetLock();
try
{
// Перевоим координаты клика мышкой
// в строку и столбец активного листа
System.Windows.Point point = e.GetPosition(wvExcel);
double x = point.X;
double y = point.Y;
double row, col;
wvExcel.LocationToRange(x, y, out row, out col,
SpreadsheetGear.Windows.Controls.RangeLocationFlags.Headers);
// Проверяем что кликнули именно в пределах листа и есть столбец и строка
if (row >= 0.0 && col >= 0.0)
{
// Получаем ячейку
var range = wvExcel.ActiveWorksheet.Cells[(int)row, (int)col];
// Получаем блок в который входит эта ячейка (если входит)
var block = range.CurrentRegion; // var block = range.MergeArea; - можно и вот так
MessageBox.Show(string.Format(
"Редактируем ячейку ({0}) значение в ней '{1}'.{6}Ячейка входит в блок ({2}) объединяющий строк: {3} и столбцов {4}.{6}Значение в блоке '{5}'.",
range.Address,
range.Value,
block.Address,
block.RowCount,
block.ColumnCount,
((block.Value as object[,]) ?? new object[,] { { null } })[0, 0],
Environment.NewLine
));
}
}
finally
{
wvExcel.ReleaseLock();
}
}
Загрузив наш лист и кликнув по ячейке с индексом, мы получим:На что следует обратить внимание:
а) Хотя мы и подписывались на MouseUp событие вызовется только при первом переходе ячейки в редактирование. Выделение ячейки событие не вызывает, также как и повторные клики на редактируемой ячейке.
б) Получить блок в который входит ячейка можно двумя способами. Разницы в них не заметил.
в). Ни один из способов получения ячейки не позволяет понять, что этот Range именованный:
Второй способ, на мой взгляд более правильный, это обрабатывать события связанные с ячейкой. В данном случае, нас интересует CellBeginEdit. В данном случае, обработчик будет значительно проще:
private void wvExcel_CellBeginEdit(object sender, SpreadsheetGear.Windows.Controls.CellBeginEditEventArgs e)
{
wvExcel.GetLock();
try
{
// Получаем ячейку
var range = wvExcel.ActiveCell;
// Получаем блок в который входит эта ячейка (если входит)
var block = range.CurrentRegion; // var block = range.MergeArea; - можно и вот так
MessageBox.Show(string.Format(
"Редактируем ячейку ({0}) значение в ней '{1}'.{6}Ячейка входит в блок ({2}) объединяющий строк: {3} и столбцов {4}.{6}Значение в блоке '{5}'.",
range.Address,
range.Value,
block.Address,
block.RowCount,
block.ColumnCount,
((block.Value as object[,]) ?? new object[,] { { null } })[0, 0],
Environment.NewLine
));
}
finally
{
wvExcel.ReleaseLock();
}
}
Что самое интересное, выбирая вроде-как активную ячейку, мы на самом деле получаем весь блок и нам доступно его имя:6. Добавление строк и копирование части листа
В оригинальном листе есть блок:
Я хочу его скопировать и вставить под оригинальный М1. Сделать это можно достаточно просто:
private void CloneM1_Click(object sender, RoutedEventArgs e)
{
wvExcel.GetLock();
try
{
var name = wvExcel.ActiveWorkbook.Names.Cast<IName>().FirstOrDefault(n => n.Name == "М1");
if (name != null)
{
var range = name.RefersToRange;
// Снимаем защиту с листа
range.Worksheet.Unprotect("DOC1152016_505.xls");
//// Добавляем строки для копирования в них нашего М1 (формат с какой и по какую строки)
string address = string.Format("{0}:{1}", range.Row + range.RowCount + 1, range.Row + 2 * range.RowCount);
range.Worksheet.Cells[address].Insert();
// Устанавливаем высоту строк аналогично высоте строк в M1
for (int i = 0; i < range.RowCount; i++)
{
range.Worksheet.Cells[range.Row + range.RowCount + i, 0].RowHeight =
range.Worksheet.Cells[range.Row + i, 0].RowHeight;
}
// Копируем в подготовленные блок М1
range.Copy(range.Worksheet.Cells[range.Row + range.RowCount, range.Column]);
// Востанавливаем защиту
range.Worksheet.Protect("DOC1152016_505.xls");
// Т.к. ячейка может быть не на текущем активном листе,
// то для удобства проверки, перейдем на него
wvExcel.ActiveSheet = range.Worksheet;
}
}
finally
{
wvExcel.ReleaseLock();
}
}
Обратите внимание, что т.к. я работаю с защищенными листами, перед копированием, мне приходится снимать защиту. Ну и результат:7. Присвоение имен блокам
Последний на сегодня пример, связанный, кстати, с предыдущим. Блоки подсвеченные синим у меня имен не имеют, я хочу подсвеченным ячейкам внутри М1 эти имена дать, чтобы было удобнее присваивать в них значения.
Вот так выглядит код присвоения имен:
private void NameM1Child_Click(object sender, RoutedEventArgs e)
{
wvExcel.GetLock();
try
{
var name = wvExcel.ActiveWorkbook.Names.Cast<IName>().FirstOrDefault(n => n.Name == "М1");
if (name != null)
{
var range = name.RefersToRange;
var worksheet = range.Worksheet;
// Ищем все дочерние объединения с учетом цвета фона
var background = SpreadsheetGear.Color.FromArgb(255, 51, 204, 204);
List<IRange> childRanges = new List<IRange>();
for (int column = 0; column < range.ColumnCount; column++)
{
for (int row = 0; row < range.RowCount; row++)
{
var cell = worksheet.Cells[range.Row + row, range.Column + column];
if (cell.Interior.Color == background)
{
var block = cell.MergeArea;
if (!childRanges.Any(r => r.Row == block.Row && r.Column == block.Column))
{
childRanges.Add(block);
}
}
}
}
// Даем им имена
for (int i = 0; i < childRanges.Count; i++)
{
var newName = worksheet.Names.Add(string.Format("П0_{0}", i), string.Format("='{0}'!{1}", childRanges[i].Worksheet.Name, childRanges[i].Address), ReferenceStyle.A1);
}
}
}
finally
{
wvExcel.ReleaseLock();
}
}
Несмотря на то, что мы присваивали имена вида П0_0 и П0_1:но, т.к. мы их присваивали через раздел, то присвоение по имени будет выглядеть:
private void SetM1Child_Click(object sender, RoutedEventArgs e)
{
wvExcel.GetLock();
try
{
var name = wvExcel.ActiveWorkbook.Names.Cast<IName>().FirstOrDefault(n => n.Name == "'Раздел 1'!П0_0");
if (name != null)
{
var range = name.RefersToRange;
// Снимаем защиту с листа
range.Worksheet.Unprotect("DOC1152016_505.xls");
range.Value = 100000;
// Востанавливаем защиту
range.Worksheet.Protect("DOC1152016_505.xls");
}
}
finally
{
wvExcel.ReleaseLock();
}
}
Чтобы обращаться только по имени, без имени раздела, нужно добавление имени сделать следующим образом:var newName = wvExcel.ActiveWorkbook.Names.Add(string.Format("П0_{0}", i), string.Format("='{0}'!{1}", childRanges[i].Worksheet.Name, childRanges[i].Address), ReferenceStyle.A1);
Тогда поиск ячейки будет иметь вид:var name = wvExcel.ActiveWorkbook.Names.Cast<IName>().FirstOrDefault(n => n.Name == "П0_0");
И в первом и во втором случае, если начала присвоить имена, а потом по имени присвоить значение, то результат будет одинаковый:На этом все, мое мнение, все это весьма неоднозначно и связано с неоправданными сложностями. Всякие WPF вкусности типа Binding применить не получится. Для узкой и специфичной задачи возможно применение этого компонента имеет смысл, ну а так...
Построение графов при помощи NodeXL
В связи с производственной необходимостью, возникла потребность в компоненте для построения графов. Сходу были найдены вот эти три проекта:
http://graphx.codeplex.com/
http://graphsharp.codeplex.com/
http://nodexl.codeplex.com/
Т.к. по картинкам мне больше понравился третий, то его и пробовал. Он оказался неплох. Поэтому под катом рассказ о том, как при помощи NodeXL строить графики в WPF приложениях.
На всякий пожарный ссылка на скачивание. Качаем, распакуем, присоединяем dll-ки в проект:
Все, можно начинать использовать.
Для показа графа используется контрол NodeXLControl из пространства имен Smrf.NodeXL.Visualization.Wpf. Можно его как добавить через XAML, так и создать из кода и поместить в какой-нибудь контейнер. Дополнительных настроек не требуется.
Для данной статьи, я создал пустой WPF проект и на главную форму поместил вот такую разметку:
Ну а теперь, собственно построение графа. Информация о графе собрана в свойстве с говорящим именем Gpaph. Ну а вершины, соответственно, в свойстве Vertices графа. Чтобы постоянно не писать весь путь с имени контрола, можно это свойство скопировать в переменную и работать с ней:
Интерфейс у компонента достаточно понятный, например, добавление вершин осуществляется методом Add возвращающем ссылку на свежесозданную вершину:
А вот дальше начинается проблема из-за желания разработчика сделать все максимально универсально. Настройка свойств вершины идет через метод SetValue первым параметром в который необходимо передавать строку с именем настраиваемого свойства. Сильно радует наличие предопределенного класса ReservedMetadataKeys с перечнем этих строковых значений в виде readonly полей. Т.е. настройка вершины имеет вид:
Ну и еще пара вершин оформленных по другому:
Мы можем задавать расположение вершин принудительно. У каждой вершины есть свойство Location, вот только проблема, это свойство из библиотеки WinForms. Нет, мы можем подключить бибилотеку System.Drawing.dll и написать что-нибудь вида:
third.Location = new System.Drawing.PointF(60, 60);
И оно даже заработает:
Но в большинстве случаев, мы можем просто при построении графа довериться авторасположению вершин:
nxGraph.DrawGraph(true);
Выглядит:
Только нужно не забывать, что при выборе такого способа все установленные вручную Location будут игнорироваться.
Хорошо, вершины разместили, переходим к ребрам. Список всех ребер лежит в свойстве Edges уже упоминавшегося свойства Graph:
Работа с ребрами очень похожа на работу с вершинами, только при создании ребра мы передаем две вершины которые ребро будет соединять и признак направленности ребра:
Компонент представляет возможность группировать вершины, но смотрится это не фонтан, т.к. все связи к сгруппированным вершинам отображаются. Но давайте покажу.
Поэтому предлагаю перейти к событиям и на них покажу работу с группами.
Событий у вершин, групп и т.д. нет. Все события собраны в контроле. Например, чтобы обрабатывать клик на первой или второй вершине со сворачиванием группы мне придется подписаться на событие:
Вот так выглядит граф после сворачивания двух вершин:
На сегодня все. Может в следующий раз покажу какой-нибудь пример приближенный к реальности на основе этого контрола.
http://graphx.codeplex.com/
http://graphsharp.codeplex.com/
http://nodexl.codeplex.com/
Т.к. по картинкам мне больше понравился третий, то его и пробовал. Он оказался неплох. Поэтому под катом рассказ о том, как при помощи NodeXL строить графики в WPF приложениях.
На всякий пожарный ссылка на скачивание. Качаем, распакуем, присоединяем dll-ки в проект:
Все, можно начинать использовать.
Для показа графа используется контрол NodeXLControl из пространства имен Smrf.NodeXL.Visualization.Wpf. Можно его как добавить через XAML, так и создать из кода и поместить в какой-нибудь контейнер. Дополнительных настроек не требуется.
Для данной статьи, я создал пустой WPF проект и на главную форму поместил вот такую разметку:
<Window x:Class="WpfApplication9.MainWindow"
xmlns:node="clr-namespace:Smrf.NodeXL.Visualization.Wpf;assembly=Smrf.NodeXL.Control.Wpf"
Title="MainWindow" Height="350" Width="525">
<Grid>
<node:NodeXLControl x:Name="nxGraph" />
</Grid>
</Window>
Ну а теперь, собственно построение графа. Информация о графе собрана в свойстве с говорящим именем Gpaph. Ну а вершины, соответственно, в свойстве Vertices графа. Чтобы постоянно не писать весь путь с имени контрола, можно это свойство скопировать в переменную и работать с ней:
IVertexCollection oVertices = nxGraph.Graph.Vertices;
Интерфейс у компонента достаточно понятный, например, добавление вершин осуществляется методом Add возвращающем ссылку на свежесозданную вершину:
IVertex first = oVertices.Add();
// Цвет вершины
first.SetValue(ReservedMetadataKeys.PerVertexLabelFillColor, Color.FromArgb(255, 255, 255, 11));
// Тип отображения
first.SetValue(ReservedMetadataKeys.PerVertexShape, VertexShape.Label);
// Текст вершины
first.SetValue(ReservedMetadataKeys.PerVertexLabel, "Первая");
Да, вы правильно поняли, что второй параметр метода SetValue типа object и во многих случаях придется угадывать какого типа он должен быть реально по замыслу разработчиков.Ну и еще пара вершин оформленных по другому:
IVertex second = oVertices.Add();
second.SetValue(ReservedMetadataKeys.PerColor, Color.FromArgb(255, 255, 0, 255));
// Задаем радиус вершины
second.SetValue(ReservedMetadataKeys.PerVertexRadius, 20F);
// И говорим что вершина - шарик
second.SetValue(ReservedMetadataKeys.PerVertexShape, VertexShape.Sphere);
IVertex third = oVertices.Add();
// Здесь мы тоже говорим, что сфера
third.SetValue(ReservedMetadataKeys.PerVertexShape, VertexShape.Sphere);
// Но задаем надпись
third.SetValue(ReservedMetadataKeys.PerVertexLabel, "Label");
Ок, давайте запустим наше приложение. Но перед этим вызовем метод отрисовки графа:nxGraph.DrawGraph();
Вот так это выглядит:Мы можем задавать расположение вершин принудительно. У каждой вершины есть свойство Location, вот только проблема, это свойство из библиотеки WinForms. Нет, мы можем подключить бибилотеку System.Drawing.dll и написать что-нибудь вида:
third.Location = new System.Drawing.PointF(60, 60);
И оно даже заработает:
Но в большинстве случаев, мы можем просто при построении графа довериться авторасположению вершин:
nxGraph.DrawGraph(true);
Выглядит:
Только нужно не забывать, что при выборе такого способа все установленные вручную Location будут игнорироваться.
Хорошо, вершины разместили, переходим к ребрам. Список всех ребер лежит в свойстве Edges уже упоминавшегося свойства Graph:
IEdgeCollection oEdges = nodeXLControl.Graph.Edges;
Работа с ребрами очень похожа на работу с вершинами, только при создании ребра мы передаем две вершины которые ребро будет соединять и признак направленности ребра:
IEdge oEdge1 = oEdges.Add(first, second, true);
Ну а свойства уже по привычной схеме:// Цвет
oEdge1.SetValue(ReservedMetadataKeys.PerColor, Color.FromArgb(255, 55, 125, 98));
// Толщина
oEdge1.SetValue(ReservedMetadataKeys.PerEdgeWidth, 3F);
// Подпись
oEdge1.SetValue(ReservedMetadataKeys.PerEdgeLabel, "Из первой во вторую");
Аналогично для остальных вершин:// Первую и третью вершину соеденит ненаправленное ребро
IEdge oEdge2 = oEdges.Add(first, third, false);
// Цвет
oEdge1.SetValue(ReservedMetadataKeys.PerColor, Color.FromArgb(255, 55, 125, 98));
// Вторую с третьей - направленное
IEdge oEdge3 = oEdges.Add(second, third, true);
// Линия будет штрих-пунктирная
oEdge3.SetValue(ReservedMetadataKeys.PerEdgeStyle, EdgeStyle.DashDotDot);
Смотрится симпотично:Компонент представляет возможность группировать вершины, но смотрится это не фонтан, т.к. все связи к сгруппированным вершинам отображаются. Но давайте покажу.
// Создаем группу, по умолчанию свернутую (второй параметр)
GroupInfo oGroup = new GroupInfo("GroupFirstAndSecond", true, "Тут две вершины");
// Вершины объединенные группой
oGroup.Vertices.AddFirst(first);
oGroup.Vertices.AddLast(second);
// Добавляем группу в граф
nxGraph.Graph.SetValue(ReservedMetadataKeys.GroupInfo, new GroupInfo[] { oGroup });
Обратили внимание на признак свернутости? Так вот, он игнорируется... Если сейчас запустить, то мы увидим все тоже самое, что и на предыдущей картинке.Поэтому предлагаю перейти к событиям и на них покажу работу с группами.
Событий у вершин, групп и т.д. нет. Все события собраны в контроле. Например, чтобы обрабатывать клик на первой или второй вершине со сворачиванием группы мне придется подписаться на событие:
nxGraph.VertexClick += nxGraph_VertexClick;
И в обработчике добавить проверку:void nxGraph_VertexClick(object sender, VertexEventArgs vertexEventArgs)
{
if (vertexEventArgs.Vertex.ID < 3) // Первая или вторая вершина
{
Dispatcher.BeginInvoke((Action)(() => nxGraph.CollapseGroup("GroupFirstAndSecond", true)));
}
if (vertexEventArgs.Vertex.ID == 3)
{
nxGraph.ExpandGroup("GroupFirstAndSecond", true);
}
}
Вызов через Dispatcher вынужденная мера, т.к. после сворачивания вершины из графа скрываются и обработка клика падает.Вот так выглядит граф после сворачивания двух вершин:
На сегодня все. Может в следующий раз покажу какой-нибудь пример приближенный к реальности на основе этого контрола.
Создание объектов из XAML
Возник на форумах MSDN вопрос, как привязать команду к контекстному меню. У автора небольшое недопонимание Binding-а, но не суть. Предлагаю посмотреть, как можно создавать объекты их XAML и использовать их, например, как источники в Binding.
Создаем новое WPF приложение и добавляем в него нашу команду:
public void Execute(object parameter)
{
MessageBox.Show("Привет");
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
}
На форму приложения подключаем пространство имен:
xmlns:local="clr-namespace:WpfApplication2"
И в ресурсах создаем наш объект на основе SimpleCommand:
<Window.Resources>
<ObjectDataProviderObjectType="{x:Type local:SimpleCommand}" x:Key="comm" />
Ну а теперь добавляем в ресурсы наше контекстное меню:
</ContextMenu>
Я указал наш созданный класс в качестве источника, Path не указываю, т.к. сам объект помещенный в Source нас и интересует. Но можно и классически, объект источника поместить в DataContext:
<MenuItem Header="Hello" Command="{Binding }" DataContext="{StaticResource comm}" />
Для демо, я кинул на форму TextBox и указал у него наше контекстное меню:
<TextBox ContextMenu="{StaticResource Hello}" />
Запускаем:
Ну и два усовершенствования. По-первых, при создании объектов их XAML мы можем передавать в конструктор параметры. Переписываем нашу команду:
private string _message;
public SimpleCommand(string p_message)
{
_message = p_message;
}
public void Execute(object parameter)
{
MessageBox.Show(_message);
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
}
Подключаем пространство имен System:
xmlns:system="clr-namespace:System;assembly=mscorlib"
И создаем не один объект, а два:
<system:String>Hello</system:String>
</ObjectDataProvider.ConstructorParameters>
</ObjectDataProvider>
<ObjectDataProvider ObjectType="{x:Type local:SimpleCommand}" x:Key="commBye">
<ObjectDataProvider.ConstructorParameters>
<system:String>Bye</system:String>
</ObjectDataProvider.ConstructorParameters>
</ObjectDataProvider>
Правим контекстное меню (я Key поменял, надо и в TextBox тоже поправить):
<MenuItem Header="Bye" Command="{Binding }" DataContext="{StaticResource commBye}" />
</ContextMenu>
Во-вторых, если бы я решал эту задачу, то я бы добавил в проект еще один класс:
public ICommand Hello { get; set; }
public ICommand Bye { get; set; }
public ContextMenuViewModel()
{
Hello = new SimpleCommand("Hello");
Bye = new SimpleCommand("Bye");
}
}
Ну и окно переписал бы так:
<MenuItem Header="Hello" Command="{Binding Hello}" />
<MenuItem Header="Bye" Command="{Binding Bye}" />
</ContextMenu>
Все.
Создаем новое WPF приложение и добавляем в него нашу команду:
public class SimpleCommand : ICommand
{public void Execute(object parameter)
{
MessageBox.Show("Привет");
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
}
На форму приложения подключаем пространство имен:
xmlns:local="clr-namespace:WpfApplication2"
И в ресурсах создаем наш объект на основе SimpleCommand:
<Window.Resources>
<ObjectDataProviderObjectType="{x:Type local:SimpleCommand}" x:Key="comm" />
Ну а теперь добавляем в ресурсы наше контекстное меню:
<ContextMenu x:Key="Hello">
<MenuItem Header="Hello" Command="{Binding Source={StaticResource comm}}" /></ContextMenu>
Я указал наш созданный класс в качестве источника, Path не указываю, т.к. сам объект помещенный в Source нас и интересует. Но можно и классически, объект источника поместить в DataContext:
<MenuItem Header="Hello" Command="{Binding }" DataContext="{StaticResource comm}" />
Для демо, я кинул на форму TextBox и указал у него наше контекстное меню:
<TextBox ContextMenu="{StaticResource Hello}" />
Запускаем:
Ну и два усовершенствования. По-первых, при создании объектов их XAML мы можем передавать в конструктор параметры. Переписываем нашу команду:
public class SimpleCommand : ICommand
{private string _message;
public SimpleCommand(string p_message)
{
_message = p_message;
}
public void Execute(object parameter)
{
MessageBox.Show(_message);
}
public bool CanExecute(object parameter)
{
return true;
}
public event EventHandler CanExecuteChanged;
}
Подключаем пространство имен System:
xmlns:system="clr-namespace:System;assembly=mscorlib"
И создаем не один объект, а два:
<ObjectDataProvider ObjectType="{x:Type local:SimpleCommand}" x:Key="commHello">
<ObjectDataProvider.ConstructorParameters><system:String>Hello</system:String>
</ObjectDataProvider.ConstructorParameters>
</ObjectDataProvider>
<ObjectDataProvider ObjectType="{x:Type local:SimpleCommand}" x:Key="commBye">
<ObjectDataProvider.ConstructorParameters>
<system:String>Bye</system:String>
</ObjectDataProvider.ConstructorParameters>
</ObjectDataProvider>
Правим контекстное меню (я Key поменял, надо и в TextBox тоже поправить):
<ContextMenu x:Key="Menu">
<MenuItem Header="Hello" Command="{Binding }" DataContext="{StaticResource commHello}" /><MenuItem Header="Bye" Command="{Binding }" DataContext="{StaticResource commBye}" />
</ContextMenu>
Во-вторых, если бы я решал эту задачу, то я бы добавил в проект еще один класс:
class ContextMenuViewModel
{public ICommand Hello { get; set; }
public ICommand Bye { get; set; }
public ContextMenuViewModel()
{
Hello = new SimpleCommand("Hello");
Bye = new SimpleCommand("Bye");
}
}
Ну и окно переписал бы так:
<ObjectDataProvider ObjectType="{x:Type local:ContextMenuViewModel}" x:Key="contextMenuViewModel" />
<ContextMenu x:Key="Menu" DataContext="{StaticResource contextMenuViewModel}"><MenuItem Header="Hello" Command="{Binding Hello}" />
<MenuItem Header="Bye" Command="{Binding Bye}" />
</ContextMenu>
Все.
Ввод данных и их проверка на уровне Binding
Очередной вопрос на MSDN. Стоит задача при редактировании записи сначала проверить данные, а только потом применить изменения к объекту модели. Я всю конструкцию MVVM воспроизводить не буду и покажу на примере в котором будет только один объект модели, а все остальное будет в лоб. Начнем.
Создаем пустой проект и добавляем в него два класса. Первый класс для модели:
public string LastName
{
get { return (string)GetValue(LastNameProperty); }
set { SetValue(LastNameProperty, value); }
}
// Using a DependencyProperty as the backing store for LastName. This enables animation, styling, binding, etc...
public static readonly DependencyProperty LastNameProperty =
DependencyProperty.Register("LastName", typeof(string), typeof(Person), new PropertyMetadata(""));
public string FirstName
{
get { return (string)GetValue(FirstNameProperty); }
set { SetValue(FirstNameProperty, value); }
}
// Using a DependencyProperty as the backing store for FirstName. This enables animation, styling, binding, etc...
public static readonly DependencyProperty FirstNameProperty =
DependencyProperty.Register("FirstName", typeof(string), typeof(Person), new PropertyMetadata(""));
}
Для проверки добавляем класс чекающий строку на пустоту:
1. Не забываем на форме подключить пространство имен где описан класс валидатора:
xmlns:local="clr-namespace:WpfApplication3"
2. Для того, чтобы Binding не срабатывал сам, а только по кнопке, прописываем в нем:
UpdateSourceTrigger="Explicit"
3. Перед принудительным применением Binding проверяем, а все ли нормально:
if (beLastName.ValidateWithoutUpdate() && beFirstName.ValidateWithoutUpdate())
4. Работает вот так. Запускаем:
Вводим слева значение и переводим фокус ввода, значение отображается справа:
Теперь вводим значение в правый TextBox и меняем фокус ввода:
Как видим, значение слева не обновилось. Нажимаем Принять:
Значение обновилось. Теперь значение меняем на некорректное (пустую строку) и нажимаем принять:
Видим подсветку ошибки и то, что в модель пустое значение не скопировалось.
Создаем пустой проект и добавляем в него два класса. Первый класс для модели:
class Person : DependencyObject
{public string LastName
{
get { return (string)GetValue(LastNameProperty); }
set { SetValue(LastNameProperty, value); }
}
// Using a DependencyProperty as the backing store for LastName. This enables animation, styling, binding, etc...
public static readonly DependencyProperty LastNameProperty =
DependencyProperty.Register("LastName", typeof(string), typeof(Person), new PropertyMetadata(""));
public string FirstName
{
get { return (string)GetValue(FirstNameProperty); }
set { SetValue(FirstNameProperty, value); }
}
// Using a DependencyProperty as the backing store for FirstName. This enables animation, styling, binding, etc...
public static readonly DependencyProperty FirstNameProperty =
DependencyProperty.Register("FirstName", typeof(string), typeof(Person), new PropertyMetadata(""));
}
Для проверки добавляем класс чекающий строку на пустоту:
class NotEmptyValidation : ValidationRuleРазметка формы:
{
public override ValidationResult Validate(object value, System.Globalization.CultureInfo cultureInfo)
{
var result = new ValidationResult(false, "Не допустима пустая строка");
if (value is string && !string.IsNullOrWhiteSpace(value.ToString()))
{
result = new ValidationResult(true, null);
}
return result;
}
}
<Window x:Class="WpfApplication3.MainWindow"Код формы:
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:WpfApplication3"
Title="MainWindow" Height="350" Width="525">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition Height="auto" />
<RowDefinition />
</Grid.RowDefinitions>
<TextBox Text="{Binding Person.LastName}" />
<TextBox Text="{Binding Person.FirstName}" Grid.Row="1" />
<TextBox Grid.Column="2" x:Name="tbLastName">
<TextBox.Text>
<Binding Path="Person.LastName" UpdateSourceTrigger="Explicit">
<Binding.ValidationRules>
<local:NotEmptyValidation />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<TextBox Grid.Row="1" Grid.Column="2" x:Name="tbFirstName">
<TextBox.Text>
<Binding Path="Person.FirstName" UpdateSourceTrigger="Explicit">
<Binding.ValidationRules>
<local:NotEmptyValidation />
</Binding.ValidationRules>
</Binding>
</TextBox.Text>
</TextBox>
<Button Content="Принять" Click="Button_Click" Grid.Column="2" Grid.Row="2" />
</Grid>
</Window>
public partial class MainWindow : WindowНесколько комментариев:
{
public MainWindow()
{
InitializeComponent();
Loaded += MainWindow_Loaded;
}
public Person Person
{
get { return (Person)GetValue(PersonProperty); }
set { SetValue(PersonProperty, value); }
}
// Using a DependencyProperty as the backing store for Person. This enables animation, styling, binding, etc...
public static readonly DependencyProperty PersonProperty =
DependencyProperty.Register("Person", typeof(Person), typeof(MainWindow), new PropertyMetadata(null));
void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
Person = new Person() { LastName = "Иванов" };
this.DataContext = this;
}
private void Button_Click(object sender, RoutedEventArgs e)
{
BindingExpression beLastName = tbLastName.GetBindingExpression(TextBox.TextProperty);
BindingExpression beFirstName = tbFirstName.GetBindingExpression(TextBox.TextProperty);
if (beLastName.ValidateWithoutUpdate() && beFirstName.ValidateWithoutUpdate())
{
beLastName.UpdateSource();
beFirstName.UpdateSource();
// Вот здесь можно закрывать View, не забыв уведомить ViewModel
}
}
}
1. Не забываем на форме подключить пространство имен где описан класс валидатора:
xmlns:local="clr-namespace:WpfApplication3"
2. Для того, чтобы Binding не срабатывал сам, а только по кнопке, прописываем в нем:
UpdateSourceTrigger="Explicit"
3. Перед принудительным применением Binding проверяем, а все ли нормально:
if (beLastName.ValidateWithoutUpdate() && beFirstName.ValidateWithoutUpdate())
4. Работает вот так. Запускаем:
Вводим слева значение и переводим фокус ввода, значение отображается справа:
Теперь вводим значение в правый TextBox и меняем фокус ввода:
Как видим, значение слева не обновилось. Нажимаем Принять:
Значение обновилось. Теперь значение меняем на некорректное (пустую строку) и нажимаем принять:
Видим подсветку ошибки и то, что в модель пустое значение не скопировалось.