C# WPF – Собственный ListView с «блэкджеком и …»

от автора


Введение

Признаем все, что «DotNetFramework» — гениальное изобретение Microsoft, которое предоставляет внушительный набор готовых компонентов и позволяет строить ПО по принципу «LEGO». Но не всегда их достаточно, особенно для специфических задач, требующих либо «особенного быстродействия», либо «особенного способа взаимодействия»… И Microsoft даёт возможность создавать свои компоненты. Итак, хочу поделиться опытом создания собственного ListView-компонента (будем называть так вид компонентов, которые выводят для просмотра список каких-либо объектов) — «по-быстрому» (в условиях, когда надо было ещё вчера).

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

Необходим компонент для просмотра списка однородных и неоднородных (т.е. в списке будут содержаться экземпляры объектов разных типов (в том числе и пользовательских)) элементов (более 10000 элементов).
Должны быть возможности «раскрашивания» вида отображения этих элементов.
Ну и обязательно по клику по элементу должно происходить событие, которое будет давать нам ссылку на экземпляр и его индекс в списке объектов.

Предварительный результат

Забегая вперёд, посмотрим, что получилось:

Рисунок 1 – Шаблон с разными размерами отображения


Рисунок 2 – Шаблон с одинаковыми размерами отображения

Рисунок 3 – Клик по элементу
Итак, рассмотрим подход к проектированию структуры компонента и углубимся в код.

«Мат.часть» компонента

В компоненте содержится список из object-ов, который является набором исходных данных.
Делегат на функцию конвертации из пользовательского типа в шаблон для отображения (класс BlockTamplateBase).
Шаблон отображения (класс BlockTamplateBase) содержит информацию об области отображения и функцию, описывающую алгоритм рисования.
Разберём поля ListBlockView – Пример 1.

Пример 1

        /// <summary>         /// Список объектов, которые являются исходными данными (реализуем требование "неоднородности элементов")         /// </summary>         public List<object> SourceData { get; set; }           protected Brush colorChoosenBlockShadow;         /// <summary>         /// Цвет затенения выбранного элемента (по которому "кликнули")         /// </summary>         public Brush ColorChoosenBlockShadow { get { return colorChoosenBlockShadow; } set { colorChoosenBlockShadow = value; this.InvalidateVisual(); } }           public delegate BlockTemplateBase ConvertObjectToBlockTemplateDelegate(object item, int index);         /// <summary>         /// Указатель на функцию, в которой будет описан пользовательский алгоритм для отображения (рисвоания) данных,         /// т.е. управление "раскраской" вида элемента на основе их значений         /// (требование о "раскраске" элементов)         /// (Далее будет подробнее о BlockTemplateBase)         /// </summary>         public ConvertObjectToBlockTemplateDelegate ConvertToBlockTemplate { get; set; }                  /// <summary>         /// Список областей (прямоугольников), в которых отрисованы видымые элементы списка SourceData,         /// начиная с индекса IndexCurrentFirstVisibleBlock         /// (необходимо для реализации клика по элементу)         /// </summary>         protected List<Rect> CurrentVisibleListBlockRect { get; set; }         /// <summary>         /// Индекс первого видимого элемента (необходимо для реализации вертикального скроллнига)         /// </summary>         protected int IndexCurrentFirstVisibleBlock { get; set; }         /// <summary>         /// Индекс текущего выбранного элемента (который будет "затеняться")         /// </summary>         public int IndexCurrentChoosenBlock { get; protected set; } 

Реализация события «Клика по элементу» — Пример 2.
Можно сделать обычное событие, а можно – маршрутизируемое. Для моей задачи и обычного хватает.

Пример 2

        /* Маршрутизируемое событие «Клик по элементу»         public class ClickItemRoutedEventArgs : RoutedEventArgs         {             public int Index { get; protected set; }             public object Item { get; protected set; }             public ClickItemRoutedEventArgs(RoutedEvent routedEvent, object item, int index)                 : base(routedEvent)             {                 Item = item;                 Index = index;             }             public ClickItemRoutedEventArgs()                 : base()             {                 Item = null;                 Index = -1;             }           }         public static readonly RoutedEvent ClickItemEvent = EventManager.RegisterRoutedEvent(“ClickItem”, RoutingStrategy.Bubble, typeof(RoutedEventHandler), typeof(ListBlockView));         public event RoutedEventHandler ClickItem         {             add { base.AddHandler(ClickItemEvent, value); }             remove { base.RemoveHandler(ClickItemEvent, value); }         }         void RaiseClickItem(object item, int index)         {             ClickItemRoutedEventArgs args = new ClickItemRoutedEventArgs(ClickItemEvent, item, index);             RaiseEvent(args);         }          * */           public class ClickDataItemEventArgs : EventArgs         {             public object Item { get; protected set; }             public int Index { get; protected set; }             public ClickDataItemEventArgs() : base() { Item = null; Index = -1; }             public ClickDataItemEventArgs(object item, int index) : base() { Item = item; Index = index; }         }         /// <summary>         /// Событие клика по элементу         /// </summary>         public event EventHandler<ClickDataItemEventArgs> ClickItem;         protected void ClickItemRaiseEvent(object item, int index)         {             if (ClickItem != null)                 ClickItem(this, new ClickDataItemEventArgs(item, index));         } 

Реализация «Клика по элементу» как мышкой, так и программная – Пример 3.

Пример 3

        protected override void OnMouseUp(MouseButtonEventArgs e)         {             base.OnMouseUp(e);             //обходим список с областями рендеринга компонента             for (int i = this.CurrentVisibleListBlockRect.Count - 1; i >= 0; i--)             {                 //если курсор в области элемента, то выбираем его                 if (this.CurrentVisibleListBlockRect[i].Contains(e.GetPosition(this)))                 {                     IndexCurrentChoosenBlock = i + IndexCurrentFirstVisibleBlock;                     this.InvalidateVisual();                     this.ClickItemRaiseEvent(this.SourceData[IndexCurrentChoosenBlock], IndexCurrentChoosenBlock);                 }             }         }         /// <summary>         /// Программный выбор элемента по индексу и генерация события клика по нему         /// </summary>         /// <param name="index"> индекс элемента </param>         public void Select(int index)         {             int tempIndex = index;             if (this.SourceData.Count.Equals(0)) return;             if ((index < 0) && (index >= this.SourceData.Count))                 tempIndex = 0;             this.IndexCurrentChoosenBlock = tempIndex;             this.IndexCurrentFirstVisibleBlock = tempIndex;             this.InvalidateVisual();             ClickItemRaiseEvent(this.SourceData[this.IndexCurrentChoosenBlock], this.IndexCurrentChoosenBlock);         } 

А теперь разберём самое интересное – рендеринг компонента. В примере 4.

Пример 4

        /// <summary>         /// Алгоритм отрисовки компонента         /// </summary>         /// <param name="drawingContext">контекст рисования</param>         protected override void OnRender(DrawingContext drawingContext)         {             base.OnRender(drawingContext);             //Текущая фактическая высота области рисования             double currentHeight = this.RenderSize.Height - this.hScrollBar.RenderSize.Height;             //Текущая фактическая ширина области рисования             double currentWidth = this.RenderSize.Width - this.vScrollBar.RenderSize.Width;             //Область рисования (вне этой области рисовать нельзя)             Size clipSize = new Size(currentWidth, currentHeight);             //ограничиваем             drawingContext.PushClip(new RectangleGeometry(new Rect(new Point(0, 0), clipSize)));             if (this.SourceData.Count <= 0) return;             //текущий индекс рисуемого элемента (блока)             int tempIndex = this.IndexCurrentFirstVisibleBlock;             //текущая точка рисования на канвасе компонента             Point currentBlockRenderLocation = new Point(0,0);             //учёт горизонтального скроллинга (если ползунок передвинут) (в случае когда не помещается полностью элемент)             currentBlockRenderLocation.X = - this.hScrollBar.Value;             this.hScrollBar.Maximum = 0;             //очистка Списка областей (прямоугольников), в которых отрисованы видымые элементы списка SourceData             this.CurrentVisibleListBlockRect.Clear();             //рисуем блоки (элементы) пока они видны на экране (канвасе компонента)             while (currentBlockRenderLocation.Y < currentHeight)             {                 if (this.ConvertToBlockTemplate == null) return;                 if (tempIndex >= this.SourceData.Count) return;                                  //преобразоваем элемент пользовательского типа в универсальный шаблон отображения                 //данную функцию описывает пользователь компонента                 BlockTemplateBase currentRenderedBlock = this.ConvertToBlockTemplate(this.SourceData[tempIndex], tempIndex);                                  //рендерим шаблон                 DrawingVisual currentBlockBuffer =  currentRenderedBlock.Render(currentBlockRenderLocation);                 //рисуем его на канвасе компонента                 drawingContext.DrawDrawing(currentBlockBuffer.Drawing);                   //область рисования текущего шаблона                 Rect currentBlockRect = new Rect(currentBlockRenderLocation, currentRenderedBlock.RenderSize);                 //добавляем его в список (пригодится для реализации клика по элементу)                 this.CurrentVisibleListBlockRect.Add(currentBlockRect);                   //подкрашиваем выбранный элемент                 if (this.IndexCurrentChoosenBlock.Equals(tempIndex))                 {                     drawingContext.PushOpacity(0.5);                     drawingContext.DrawRectangle(this.ColorChoosenBlockShadow, null, currentBlockRect);                     drawingContext.Pop();                 }                   //выбираем самую длинную ширину (самы длинный элемент) (для реализации горизонтального скроллинга)                 double deltaWidth = currentRenderedBlock.RenderSize.Width - currentWidth;                 if (deltaWidth > 0)                     if (this.hScrollBar.Maximum <= deltaWidth) { this.hScrollBar.Maximum = deltaWidth; }                   //переходим вниз, на свободное место для рисования                 currentBlockRenderLocation.Y += currentRenderedBlock.RenderSize.Height;                 tempIndex++;             }         } 

Далее рассмотрим возможности «раскраски элементов» на базе шаблонов. Сразу забегу вперёд, сказав, что такой подход обеспечивается возможность описания пользовательского шаблона, так, как необходимо.
Базовый шаблон (BlockTemplateBase) представлен в примере 5.

Пример 5

    /// <summary>     /// Базовый шаблон отображения     /// </summary>     public class BlockTemplateBase     {         /// <summary>         /// Область рисования         /// </summary>         protected Rect RenderRect;         /// <summary>         /// Буфер рисования         /// </summary>         protected DrawingVisual RenderBuffer;         /// <summary>         /// Размеры области рисования         /// </summary>         public Size RenderSize { get { return this.RenderRect.Size; } set { this.RenderRect.Size = value; } }           public BlockTemplateBase() { RenderRect = new Rect(); RenderSize = new Size(); RenderBuffer = new DrawingVisual(); }           /// <summary>         /// Базовый алгоритм рендеринга         /// </summary>         /// <param name="renderLocation"> точка отрисовки </param>         /// <returns> буфер рисования </returns>         public DrawingVisual Render(Point renderLocation)         {             using (DrawingContext dc = RenderBuffer.RenderOpen())             {                 RenderRect.Location = renderLocation;                 dc.PushClip(new RectangleGeometry(RenderRect));                 //вызываем функцию в которой описан алгоритм риования                 if (DataRender != null) DataRender(dc);                 dc.Close();             }             return RenderBuffer;         }           protected delegate void DataRenderDelegate(DrawingContext dc);         //указатель на функцию с алгоритмом рисования         protected DataRenderDelegate DataRender { get; set; }     } 

Простой шаблон (BlockTemplateSimple) для отображения, наследуемый от базового шаблона представлен в примере 6.

Пример 6

    /// <summary>     /// простенький Шаблон рендеринга     /// </summary>     public class BlockTemplateSimple : BlockTemplateBase     {         /// <summary>         /// Текстовая строка         /// </summary>         public string Data { get; set; }         /// <summary>         /// Цвет фона         /// </summary>         public Brush ColorBackground { get; set; }         /// <summary>         /// Цвет границ         /// </summary>         public Brush ColorBorder { get; set; }         /// <summary>         /// Цвет шрифта         /// </summary>         public Brush ColorFont { get; set; }         /// <summary>         /// размер шрифта         /// </summary>         public double FontSize { get; set; }         /// <summary>         /// Название шрифта         /// </summary>         public string FontName { get; set; }           public BlockTemplateSimple()         {             base.DataRender = this.DataRender;             ColorBackground = Brushes.WhiteSmoke;             ColorBorder = Brushes.Gray;             ColorFont = Brushes.Black;             FontSize = 10;             FontName = “Calibri”;         }           /// <summary>         /// Алгоритм рендеринга         /// </summary>         /// <param name=”dc”> контекст рисования </param>         new void DataRender(DrawingContext dc)         {             //рисуем границу             dc.DrawRectangle(ColorBackground, new Pen(ColorBorder, 1.0), this.RenderRect);             //форматируем текст для рисования             FormattedText txt = new FormattedText(Data, CultureInfo.CurrentCulture, FlowDirection.LeftToRight, new Typeface(FontName), FontSize, ColorFont);             txt.MaxTextWidth = this.RenderRect.Width;;             txt.MaxTextHeight = this.RenderRect.Height;             txt.TextAlignment = TextAlignment.Justify;             //рисуем текст             dc.DrawText(txt, this.RenderRect.Location);         }     } 

Теперь рассмотрим примерный вариант использования данного компонента.
Для этого определим 2 тестовых пользовательских класса, как показано в примере 7.

Пример 7

    public class UserClassA     {         public int Value { get; set; }         public string Str1 { get; set; }         public string Str2 { get; set; }         public UserClassA()         { Value = 0; Str1 = "__"; Str2 = "__"; }         public UserClassA(int val, string str1, string str2):this()         { Value = val; Str1 = str1; Str2 = str2; }     }     public class UserClassB     {         public string Str { get; set; }         public UserClassB()         { Str = "__"; }         public UserClassB(string str) : this()         { Str = str; }     } 

В примере 8 инициализируем компонент. В примере 9 – алгоритм преобразования из пользовательского типа в шаблон.

Пример 8

        List<object> DataList { get; set; }         Random r { get; set; }           public MainWindow()         {             InitializeComponent();             DataList = new List<object>();             r = new Random();             this.listBlockView_Test.ConvertToBlockTemplate = this.ConvertToBlockTemplate;             this.listBlockView_Test.ClickItem += new EventHandler<ListBlockView.ListBlockView.ClickDataItemEventArgs>(listBlockView_Test_ClickItem);         } 

Пример 9

        /// <summary>         /// Пользовательский Алгоритм преобразования из совего класса в шаблон рисования         /// </summary>         /// <param name="item">элемент</param>         /// <param name="index">индекс</param>         /// <returns>шаблон</returns>         BlockTemplateBase ConvertToBlockTemplate(object item, int index)         {             BlockTemplateSimple block = new BlockTemplateSimple();             if (item is UserClassA)             {                 UserClassA itemA = (UserClassA)item;                 //Формируем данные для отображения                 block.Data = String.Format("Value= {0}, Str1= {1}, Str2= {2}", itemA.Value.ToString(), itemA.Str1, itemA.Str2);                  //Раскрашиваем (задаём параметры рисования)                 block.FontSize = 14;                 block.FontName = "Calibri";                 block.ColorBackground = Brushes.Yellow;                 block.ColorBorder = Brushes.Red;                 block.ColorFont = Brushes.Blue;             }             else if (item is UserClassB)             {                 UserClassB itemB = (UserClassB)item;                 //Формируем данные для отображения                 block.Data = String.Format("Str= {0}", itemB.Str);                 //Раскрашиваем (задаём параметры рисования)                 block.FontSize = 12;                 block.FontName = "Courier New";                 block.ColorBackground = Brushes.LightGray;                 block.ColorBorder = Brushes.Black;                 block.ColorFont = Brushes.Green;             }             block.RenderSize = new Size(500, 30);             return block;         } 

Реализация обработки клика по элементу показана в примере 10.

Пример 10

        void listBlockView_Test_ClickItem(object sender, ListBlockView.ClickDataItemEventArgs e)         {             string textBoxStr = "";             if (e.Item is UserClassA)             {                 textBoxStr = String.Format("Индекс элемента = {0}{1}Тип элемента: {2}{3}Value= {4}, Str1= {5}, Str2= {6}",                     e.Index.ToString(), '\n'.ToString(),                      "UserClassA", '\n'.ToString(),                     ((UserClassA)e.Item).Value.ToString(), ((UserClassA)e.Item).Str1, ((UserClassA)e.Item).Str2);             }             else if (e.Item is UserClassB)             {                 textBoxStr = String.Format("Индекс элемента = {0}{1}Тип элемента: {2}{3}Str= {4}",                      e.Index.ToString(), '\n'.ToString(),                     "UserClassB", '\n'.ToString(),                     ((UserClassB)e.Item).Str);             }             MessageBox.Show(textBoxStr);         } 

Выводы

Можно было ещё добавить задание общего вида отображения компонента в xaml-коде и много других плюшек – их в WPF много, но не будем забывать, что компонент нужен был «ещё вчера» и «по-быстрому».
Итак, на выходе имеем:

  • Компонент типа ListView с неоднородным содержимым;
  • Возможностью задания алгоритма рендеринга элемента (преобразования пользовательского типа в шаблон отображения);
  • Возможность создания шаблона отображения «по потребностям» самим пользователем компонента по несложным принципам (т.е. не особо сложно и самому создать – никаких ограничений);
  • Виртуальный режим работы компонента (т.е. вывод (рендеринг) идёт «на лету») – это ускоряет работу компонента (нам не надо ничего с исходными данными делать, только отрисовать несколько элементов исходного списка);
  • Возможность быстрой работы на любом количестве исходных данных (столько, сколько вмещается в List).

P.S.

Прошу подкинуть умных мыслей по улучшению в комментариях.
Ссылка на проект: Скачать проект
Примечание: разработка велась в MS Visual Studio 2010 под «.Net Framework 4»

ссылка на оригинал статьи http://habrahabr.ru/post/272595/