Круговая диаграмма WPF с поддержкой привязки данных

Автор: Колин Эберхардт (Colin Eberhardt)

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

Содержание

  • Введение
  • Этап 1. Наш первый сегмент
  • Этап 2. Создание круговой диаграммы
  • Этап 3. Привязка круговой диаграммы
  • Этап 4. Добавление интерактивности
  • Этап 5. Несогласованность подсказок
  • Этап 6. Легенда
  • Этап 7. Цветовые эффекты
  • Выводы

Введение

Windows Presentation Framework предоставляет разработчикам набор инструментов для разработки визуально богатых и интерактивных пользовательских интерфейсов. Диаграммы являются высокоэффективным способом представления данных пользователям; в частности, круговые диаграммы предоставляют простой механизм сравнения относительных величин разных элементов. WPF не предоставляет библиотеку диаграмм, и при этом отсутствуют "официальные" библиотеки, которые можно было бы заказать в качестве расширения. По этой причине в Интернете имеется множество статей, где описаны простые элементы управления для создания диаграмм.

Для круговых диаграмм в CodeProject имеется библиотека трехмерных круговых диаграмм, в другом месте можно найти круговую диаграмму Silverlight. Однако ни один из этих элементов управления не предоставляет полные преимущества привязки данных. Во всех случаях данные представляются в элементе управления диаграммы в виде массива объектов данных диаграммы (либо программно, либо с помощью XAML). Следовательно, для визуализации данных необходимо скопировать соответствующие свойства объектов данных в "объекты данных диаграммы". При изучении этих библиотек для возможного использования в одном проекте меня осенило, что способ их работы отражает то, как платформы пользовательского интерфейса использовались до появления привязки; элемент управления имеет за собой модель, и задача разработчика состоит в том, чтобы скопировать данные в эту модель, чтобы элемент управления (представление) мог отобразить их. Конечно, обязанностью разработчика также является обеспечение того, чтобы изменения реальных данных копировались в модель, которая поддерживает представление, фактически обеспечивая синхронизацию данных. Благодаря использованию привязки данных перемещение данных между разными несовместимыми моделями должно отойти в прошлое.

В этой статье описывается разработка элемента управления "Круговая диаграмма" для WPF, который использует привязку данных. За круговой диаграммой не стоит своя собственная модель; вместо этого она привязана непосредственно к объектам данных. Значительное преимущество такого подхода состоит в том, что платформа WPF обрабатывает события, имеющие отношение к изменениям в связанных данных, соответствующим образом обновляя представление круговой диаграммы. Попутно будут рассмотрены некоторые другие примечательные области:

  • особенности подсказок и привязки данных;
  • использование FrameworkElement.Tag в качестве механизма передачи данных;
  • разработка пользовательских фигур;
  • наследование свойств зависимостей.

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

Этап 1. Наш первый сегмент

Гибкость API графики WPF делает конструирование сегмента круговой диаграммы относительно простой задачей: сегмент — это Path, состоящий из пары LineSegment и ArcSegment. Круговую диаграмму можно отрисовать, программно добавляя в элемент управления подходящие контуры, однако этот подход недостаточно гибкий. Например, анимация в WPF зависит от наличия свойств зависимостей. В результате, если атрибут нашего объекта не предоставляется в качестве свойства зависимостей, то анимация объекта невозможна. В идеале хотелось бы иметь возможность анимации этих сегментов, плавно увеличивая размер сектора или вращая его вокруг центра круга.

К счастью, можно довольно просто создать свои собственные пользовательские фигуры благодаря статье Томера Шамама (Tomer Shamam) на CodeProject, в которой даются отличные вводные сведения. Следующий фрагмент кода показывает, как задается геометрия нашего сегмента.

private void DrawGeometry(StreamGeometryContext context)
{          
    Point startPoint = new Point(CentreX, CentreY);
 
    Point innerArcStartPoint =
 Utils.ComputeCartesianCoordinate(RotationAngle, InnerRadius);
innerArcStartPoint.Offset(CentreX, CentreY);
 
    Point innerArcEndPoint =
Utils.ComputeCartesianCoordinate(RotationAngle + WedgeAngle, InnerRadius);
    innerArcEndPoint.Offset(CentreX, CentreY);
 
    Point outerArcStartPoint =
Utils.ComputeCartesianCoordinate(RotationAngle, Radius);
outerArcStartPoint.Offset(CentreX, CentreY);
 
    Point outerArcEndPoint =
Utils.ComputeCartesianCoordinate(RotationAngle + WedgeAngle, Radius);
outerArcEndPoint.Offset(CentreX, CentreY);
 
    bool largeArc = WedgeAngle>180.0;
              
    Size outerArcSize = new Size(Radius, Radius);
    Size innerArcSize = new Size(InnerRadius, InnerRadius);
 
    context.BeginFigure(innerArcStartPoint, true, true);
context.LineTo(outerArcStartPoint, true, true);
context.ArcTo(outerArcEndPoint, outerArcSize, 0, largeArc,
SweepDirection.Clockwise, true, true);
context.LineTo(innerArcEndPoint, true, true);
context.ArcTo(innerArcStartPoint, innerArcSize, 0, largeArc,
SweepDirection.Counterclockwise, true, true);
}

Метод ComputeCartesianCoordinate является статическим вспомогательным методом преобразования между полярными и декартовыми координатами. Все переменные, CentreX, CentreY, Radius и другие, являются свойствами зависимостей. Это почти все, что нужно для определения пользовательской фигуры. Сегмент ArcSegment WPF и сигнатура метода Geometry.ArcTo(), возможно, немного трудны для понимания. К счастью, Чарльз Петцолд (Charles Petzold) опубликовал очень хорошую статью, описывающую математику ArcSegment с множеством иллюстрированных примеров.

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

<Window x:Class="WPFPieChart.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:b="clr-namespace:ScottLogic.Shapes"
    Title="Pie Pieces" Height="200" Width="200"> 
   <Grid>
        <b:PiePiece CentreX="50" CentreY="80" RotationAngle="45" WedgeAngle="45"
                    Radius="80" InnerRadius="20" Fill="Beige" Stroke="Black"/>
        <b:PiePiece CentreX="50" CentreY="80" RotationAngle="95" WedgeAngle="15"
                    Radius="90" InnerRadius="40" Fill="Chocolate" Stroke="Black"/>
        <b:PiePiece CentreX="30" CentreY="70" RotationAngle="125" WedgeAngle="40"
                    Radius="80" InnerRadius="0" Fill="DodgerBlue" Stroke="Black"/>
   </Grid>      
</Window>

Этап 2. Создание круговой диаграммы

Теперь у нас есть фигура сегмента, и нам следует собрать эти сегменты в круговую диаграмму в пользовательском элементе управления. Я рассматривал идею использования класса ItemsControl или одного из его подклассов в качестве основы этого элемента управления, предполагая, что это подходящий макет элемента управления и шаблон элемента. Класс ItemsControl определенно зарекомендовал себя как очень гибкий: одним из наиболее ярких примеров является дизайн солнечной системы Беатрис Коста (Beatriz Costa). Однако при попытке ввести полярные преобразования этому подходу пришел конец. К сожалению, невозможно создать подкласс абстрактного базового класса Transform, поскольку в нем есть внутренние методы. Если кто-нибудь сможет продемонстрировать, как изменить класс ItemsControl, чтобы элементы, которые он содержит, отрисовывались по кругу для создания круговой диаграммы, я буду узнать, как это сделать.

В моем решении сегменты собираются в круговую диаграмму в пользовательском элементе управления PiePlotter. Поскольку этот элемент управления использует привязку данных, мы можем просто воспользоваться свойством DataContext, чтобы найти данные, которые следует вычертить. В отличие от сетки (ListView), круговая диаграмма может вычерчивать только один атрибут наших данных. Например, может потребоваться вычертить атрибуты портфеля ценных бумаг в виде круговой диаграммы. Позиции (т. е. элементы) в нашем портфеле могут включать такие атрибуты, как количество, рыночная капитализация, стоимость и т. п. С помощью круговой диаграммы мы можем изобразить только один из этих атрибутов за один раз. По этой причине элемент управления PiePlotter имеет свойство зависимостей PlottedProperty, которое указывает, какой атрибут данных следует отображать. Это позволяет нам извлекать данные, связанные с этим атрибутом, из каждого элемента, как показано ниже.

private double GetPlottedPropertyValue(object item)
{
PropertyDescriptorCollection filterPropDesc = TypeDescriptor.GetProperties(item);
    object itemValue = filterPropDesc[PlottedProperty].GetValue(item);
    return (double)itemValue;
}

Чтобы отрисовывать данные в виде круговой диаграммы, сначала мы получаем объект CollectionView из свойства DataContext. Следующий этап состоит в подсчете значений для отрисовываемого атрибута, чтобы их можно было разместить в круговой диаграмме в 360°. Наконец, мы проходим по коллекции, определяя угол каждого сегмента, аккумулируя совокупность всех сегментов, поскольку мы собираем круговую диаграмму по направлению часовой стрелки.

private void ConstructPiePieces()
{
    CollectionView myCollectionView = (CollectionView)
        CollectionViewSource.GetDefaultView(this.DataContext);
    if (myCollectionView == null)
        return;
 
    double halfWidth = this.Width / 2;
    double innerRadius = halfWidth * HoleSize;           
 
    // compute the total for the property which is being plotted
    double total = 0;
    foreach (Object item in myCollectionView)
    {
        total += GetPlottedPropertyValue(item);
    }
   
    // add the pie pieces
canvas.Children.Clear();                       
    double accumulativeAngle=0;
    foreach (Object item in myCollectionView)
    {
        double wedgeAngle = GetPlottedPropertyValue(item) * 360 / total;
 
        PiePiece piece = new PiePiece()
        {
            Radius = halfWidth,
            InnerRadius = innerRadius,
            CentreX = halfWidth,
            CentreY = halfWidth,
            WedgeAngle = wedgeAngle,
            RotationAngle = accumulativeAngle,
            Fill = Brushes.Green
        };
 
canvas.Children.Insert(0, piece);
 
        accumulativeAngle += wedgeAngle;
    }
}

Теперь элемент управления PiePlotter можно вставить в окно, имеющее подходящее свойство DataContext, со свойством PlottedProperty, указывающим, какой атрибут данных следует отображать, как показано ниже.

<Window x:Class="WPFPieChart.Window1"
    xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
    xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
    xmlns:c="clr-namespace:ScottLogic.Controls.PieChart"
    Title="Pie Chart Databinding" Height="300" Width="300">
   <Grid>
      <c:PiePlotter PlottedProperty="Benchmark" Width="250" Height="250"/>
   </Grid>
</Window>

Этап 3. Привязка круговой диаграммы

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

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

Связанные объекты должны реализовывать интерфейс INotifyPropertyChanged, чтобы уведомлять представление об изменении и о том, что его следует отразить в пользовательском интерфейсе. Поскольку наш элемент управления связан с коллекцией, мы должны пройти по всей коллекции, добавляя прослушиватель событий для каждого из связанных элементов. Это выполняется в обработчике для события FrameworkElement.DataContextChanged, как показано ниже.

void DataContextChangedHandler(object sender,
              DependencyPropertyChangedEventArgs e)
{
    CollectionView myCollectionView = (CollectionView)
CollectionViewSource.GetDefaultView(this.DataContext);
 
    foreach (object item in myCollectionView)
    {
        if (item is INotifyPropertyChanged)
        {
INotifyPropertyChanged observable = (INotifyPropertyChanged)item;
observable.PropertyChanged +=
               new PropertyChangedEventHandler(ItemPropertyChanged);
        }
    }
}
 
private void ItemPropertyChanged(object sender, PropertyChangedEventArgs e)
{
    // if the property which this pie chart
    // represents has changed, re-construct the pie
    if (e.PropertyName.Equals(PlottedProperty))
    {
        ConstructPiePieces();
    }
}

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

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

// handle the events that occur when the bound collection changes
if (this.DataContext is INotifyCollectionChanged)
{
    INotifyCollectionChanged observable =
(INotifyCollectionChanged)this.DataContext;
observable.CollectionChanged +=
        new NotifyCollectionChangedEventHandler(BoundCollectionChanged);
}

Этап 4. Добавление интерактивности

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

Этого можно довольно просто добиться путем обработки события CollectionView.CurrentChanged с анимацией соответствующего сегмента.

void CollectionViewCurrentChanged(object sender, EventArgs e)
{
    CollectionView collectionView = (CollectionView)sender;
 
    PiePiece piece = piePieces[collectionView.CurrentPosition];
 
    DoubleAnimation a = new DoubleAnimation();
    a.To = 10;
    a.Duration = new Duration(TimeSpan.FromMilliseconds(200));
 
piece.BeginAnimation(PiePiece.PushOutProperty, a);           
}

Обратите внимание, что для навигации по сегментам на основе индекса в представлении коллекции во время построения диаграммы заполняется список элементов piePiece. Кроме того, для облегчения навигации в другом направлении (от сегмента к элементу коллекции) индекс элемента хранится в свойстве Tag элемента FrameworkElement. Это позволяет добавить обработчик событий в каждый сегмент, чтобы при его щелчке выбирался элемент в связанной коллекции.

void PiePieceMouseUp(object sender, MouseButtonEventArgs e)
{
    CollectionView collectionView = (CollectionView)
CollectionViewSource.GetDefaultView(this.DataContext);
 
    PiePiece piece = sender as PiePiece;
 
    // select the item which this pie piece represents
    int index = (int)piece.Tag;
collectionView.MoveCurrentToPosition(index);
}

Этап 5. Несогласованность подсказок

Подсказки используются для предоставления дополнительных контекстных сведений. Однако простое добавление подсказки в каждый сегмент не приведет к нужному результату. Свойство DataContext для сегмента будет наследоваться от PiePlotter и будет коллекцией. Элемент ToolTip, связанный с тем же свойством DataContext, будет отображать сведения, соответствующие выбранному в текущий момент элементу, который не всегда совпадает с сегментом, на который пользователь наводит указатель мыши, чтобы получить подсказку. Чтобы устранить эту проблему, обрабатывается событие FrameworkElement.ToolTipOpening, что позволяет изменить DataContext перед отрисовкой элемента Tooltip.

void PiePieceToolTipOpening(object sender, ToolTipEventArgs e)
{
    PiePiece piece = (PiePiece)sender;
 
    CollectionView collectionView = (CollectionView)
            CollectionViewSource.GetDefaultView(this.DataContext);
             
    // select the item which this pie piece represents
    int index = (int)piece.Tag;
  
    ToolTip tip = (ToolTip)piece.ToolTip;
    tip.DataContext = collectionView.GetItemAt(index);
}

При этом для улучшения результата используется свойство Tag сегмента.

Теперь свойство ContentTemplate элемента Tooltip можно изменить, чтобы предоставить сводку данных, которые представляет данный сегмент. К сожалению, привязка данных в элементе Tooltip не так проста, как в других элементах управления. Элемент Tooltip отображается в новом окне; следовательно, эти элементы не появляются в логическом дереве родительского окна и по этой причине не наследуют свойства. Этому вопросу посвящено много записей в блогах, описывающих, как выполнять привязку данных в подсказках и как сделать рабочими привязки ElementName. Основная идея состоит в том, что необходимо вручную получать DataContext для элемента Tooltip. К счастью, этого легко добиться, используя привязку RelativeSource для связи двух DataContext вместе. RelativeSource — это мощная концепция, которая может использоваться многими интересными и неожиданными способами (подробнее об этом позже). Шаблон данных DataTemplate элемента Tooltip, приведенный далее, иллюстрирует, как можно найти процентное значение, представляемое сегментом (являющееся свойством зависимости сегмента), установив свойство DataContext элемента TextBlock равным свойству PlacementTarget нашего элемента Tooltip, то есть сегменту, к которому "присоединен" элемент Tooltip. Свойство Text элемента TextBlock затем привязывается к свойству Percentage с помощью подходящего преобразователя значений.

<DataTemplate>
    <!-- bind the stack panel datacontext to the tooltip data context -->
    <StackPanel Orientation="Horizontal"
            DataContext="{Binding Path=DataContext, RelativeSource={RelativeSource
AncestorType={x:Type ToolTip}}}">
       
        <!-- navigate to the pie piece (which is the placement
             target of the tooltip) and obtain the percentage -->
        <TextBlock FontSize="30" FontWeight="Bold" Margin="0,0,5,0"                       
                DataContext="{Binding Path=PlacementTarget,
RelativeSource={RelativeSource AncestorType={x:Type ToolTip}}}"
                Text="{Binding Path=Percentage, Converter={StaticResource
formatter}, ConverterParameter='\{0:0%\}'}"/>
 
        <StackPanel Orientation="Vertical">                                 
            <TextBlock FontWeight="Bold"  Text="{Binding Path=Class}"/>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Fund"/>                              
                <TextBlock Text=": "/>
                <TextBlock Text="{Binding Path=Fund}"/>
            </StackPanel>
            <StackPanel Orientation="Horizontal">
                <TextBlock Text="Benchmark"/>                              
                <TextBlock Text=": "/>
                <TextBlock Text="{Binding Path=Benchmark}"/>
            </StackPanel>
        </StackPanel>
    </StackPanel>
</DataTemplate>

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

Этап 6. Легенда

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

Класс Legend — это очередной пользовательский элемент управления, то есть другой многократно используемый модуль без тесной связи с элементом управления PiePlotter. Это позволяет добиться высокого уровня гибкости. Макет круговой диаграммы (который включает в себя саму диаграмму и легенду) может управляться с помощью XAML, что позволяет создавать диаграммы с разными визуальными конфигурациями. Оба элемента управления, Legend и PiePlotter, будут совместно использовать множество свойств зависимостей, таких как PlottedProperty. Имеет смысл включить эти два элемента управления в другой пользовательский элемент управления, который задает макет.

<UserControl x:Class="ScottLogic.Controls.PieChart.PieChartLayout" ...>
    <Grid>
        <StackPanel Orientation="Horizontal">
            <c:PiePlotter Margin="10" Height="200" Width="200" HoleSize="0.3"/>
            <c:Legend Margin="10" Height="200" Width="200" />
        </StackPanel>       
    </Grid>
</UserControl>

Общие свойства зависимостей могут быть заданы в элементе управления PieChartLayout, который может передавать их в элементы управления Legend и PiePlotter путем наследования свойств зависимостей. Здесь стоит упомянуть, что свойства зависимостей могут участвовать в наследовании только в том случае, если они являются присоединенными свойствами. Это не очевидно, и может привести к некоторой несогласованности.

Сам по себе элемент управления Legend — это просто ListBox с шаблоном DataTemplate, примененным для обеспечения желаемого внешнего вида. Название элемента управления Legend получается из свойства зависимостей PlottedProperty с помощью привязки RelativeSource.

<TextBlock TextAlignment="Center" Grid.Column="1" FontSize="20" FontWeight="Bold"
        Text="{Binding Path=(c:PieChartLayout.PlottedProperty),
        RelativeSource={RelativeSource AncestorType={x:Type c:Legend}}}"/>

Поскольку это свойство зависимостей является присоединенным свойством, путь привязки имеет немного отличающийся синтаксис по сравнению с другой областью, что приводит к несогласованности!

Ниже показан шаблон данных для списка легенды.

<DataTemplate>
    <Grid HorizontalAlignment="Stretch" Margin="3">
        <Grid.Background>
            <SolidColorBrush Color="#EBEBEB"/>
        </Grid.Background>
        <Grid.ColumnDefinitions>
            <ColumnDefinition Width="20"/>
            <ColumnDefinition Width="120"/>
            <ColumnDefinition/>
        </Grid.ColumnDefinitions>
        <Grid.RowDefinitions>
            <RowDefinition/>
        </Grid.RowDefinitions>
 
        <Rectangle Grid.Column="0" Width="13"
                   Height="13" Tag="{Binding}"
                   Fill="{Binding RelativeSource={RelativeSource Self},
                         Converter={StaticResource colourConverter}}"/>
        
        <TextBlock Grid.Column="1" Margin="3" Text="{Binding Path=Class}"/>
 
        <TextBlock Grid.Column="2" Margin="3" Tag="{Binding}"
                   Text="{Binding RelativeSource={RelativeSource Self},
Converter={StaticResource legendConverter}}"/>
    </Grid>
</DataTemplate>

Заполнение Fill для прямоугольника и Text второго TextBlock выводятся из немного необычной привязки. TextBlock отображает значение свойства, которое отображает круговая диаграмма, в соответствии со свойством PlottedProperty. Другими словами, свойство объекта данных, к которому осуществляется привязка, является переменной.

Было бы неплохо, если бы элемент TextBlock мог указывать значение пути привязки, выведенное из свойства зависимостей PlottedProperty (с помощью привязки RelativeSource, конечно). Однако это невозможно, поскольку только свойства зависимостей могут иметь привязки. Binding.Path является обычным свойством среды CLR. Чтобы обойти эту проблему, я использовал прием, на который меня, в частности, натолкнула запись в блоге Майка Хилберга (Mike Hillberg) о параметризованных шаблонах, когда он использовал свойство Tag элемента Button для передачи URI изображения в шаблон данных.

Чтобы извлечь значение свойства из элемента, потребуется два элемента данных: во-первых, свойство зависимостей PlottedProperty; во-вторых, сам элемент. Преобразователи значений не являются частью визуального дерева, следовательно, мы не можем выполнить навигацию до элемента управления Legend, чтобы получить значение PlottedProperty. Трюк здесь состоит в том, чтобы передать TextBlock в преобразователь значений с помощью привязки RelativeSource типа Self, что позволяет преобразователю значений вести навигацию по визуальному дереву. Элемент, связанный с ListBoxItem, привязывается к свойству Tag. Далее это свойство можно получить в преобразователе значений. Ниже приводится код, который все это выполняет.

public class LegendConverter : IValueConverter
{
    public object Convert(object value, Type targetType,
        object parameter, CultureInfo culture)
    {
        // the item which we are displaying is bound to the Tag property
        TextBlock label = (TextBlock)value;
        object item = label.Tag;
 
 
        // find the item container
        DependencyObject container = (DependencyObject)
          Helpers.FindElementOfTypeUp((Visual)value, typeof(ListBoxItem));
 
        // locate the items control which it belongs to
        ItemsControl owner = ItemsControl.ItemsControlFromItemContainer(container);
 
        // locate the legend
        Legend legend = (Legend)Helpers.FindElementOfTypeUp(owner, typeof(Legend));
       
        // extract the ‘plottedproperty’ value from the item
PropertyDescriptorCollection filterPropDesc = TypeDescriptor.GetProperties(item);
        object itemValue = filterPropDesc[legend.PlottedProperty].GetValue(item);
 
        return itemValue;
    }
 
    public object ConvertBack(object value, Type targetType,
        object parameter, CultureInfo culture)
    {
        throw new NotImplementedException();
    }
}

Вспомогательные приложения — это набор служебных программ для работы с визуальным деревом, написанных Эндрю Виддеттом (Andrew Whiddett). Также обратите внимание, что процесс навигации состоит из двух этапов: сначала находится родительский объект ListBoxItem, а затем находится ItemsControl с помощью ItemsControlFromItemContainer. Это происходит потому, что элементы-контейнеры в ItemsControl не являются дочерними элементами ItemsControl.

Джош Смит (Josh Smith) описывает решение аналогичного класса проблем в своей статье по добавлению "виртуальных ветвей" в логическое дерево, что устраняет проблему получения значений свойств зависимостей в правиле проверки.

Этап 7. Цветовые эффекты

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

public interface IColorSelector
{
    Brush SelectBrush(object item, int index);
}

С помощью экземпляра IColorSelector элементы управления Legend и PiePlotter могут получить нужный элемент Brush, используемый при отрисовке сектора или цветной области в легенде, которые соответствуют этим элементам. Очень простая реализация, показанная ниже, использует массив кистей, по которым выполняется циклический проход для выбора цвета.

public class IndexedColourSelector : DependencyObject, IColorSelector
{
 
    /// <summary>
    /// An array of brushes
    /// </summary>
    public Brush[] Brushes
    {... }
 
    public Brush SelectBrush(object item, int index)
    {
        if (Brushes == null || Brushes.Length == 0)
        {
            return System.Windows.Media.Brushes.Black;
        }
        return Brushes[index % Brushes.Length];
    }
}

Экземпляр приведенного выше решения может быть использован в круговой диаграмме с помощью XAML.

<Window >
    <Window.Resources>
        <x:ArrayExtension Type="{x:Type Brush}" x:Key="brushes">
            <SolidColorBrush Color="#9F15C3"/>
            <SolidColorBrush Color="#FF8E01"/>
            <SolidColorBrush Color="#339933"/>
            <SolidColorBrush Color="#00AAFF"/>
            <SolidColorBrush Color="#818183"/>
            <SolidColorBrush Color="#000033"/>
        </x:ArrayExtension>       
    </Window.Resources>   
    <Grid>
        <c:PieChartLayout PlottedProperty="Fund" Margin="10">
            <c:PieChartLayout.ColorSelector>
                <c:IndexedColourSelector Brushes="{StaticResource brushes}"/>
            </c:PieChartLayout.ColorSelector>
        </c:PieChartLayout>
    </Grid>
</Window>

Выводы

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

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