Введение
Признаем все, что «DotNetFramework» — гениальное изобретение Microsoft, которое предоставляет внушительный набор готовых компонентов и позволяет строить ПО по принципу «LEGO». Но не всегда их достаточно, особенно для специфических задач, требующих либо «особенного быстродействия», либо «особенного способа взаимодействия»… И Microsoft даёт возможность создавать свои компоненты. Итак, хочу поделиться опытом создания собственного ListView-компонента (будем называть так вид компонентов, которые выводят для просмотра список каких-либо объектов) — «по-быстрому» (в условиях, когда надо было ещё вчера).
Постановка задачи
Необходим компонент для просмотра списка однородных и неоднородных (т.е. в списке будут содержаться экземпляры объектов разных типов (в том числе и пользовательских)) элементов (более 10000 элементов).
Должны быть возможности «раскрашивания» вида отображения этих элементов.
Ну и обязательно по клику по элементу должно происходить событие, которое будет давать нам ссылку на экземпляр и его индекс в списке объектов.
Предварительный результат
Забегая вперёд, посмотрим, что получилось:
Рисунок 1 – Шаблон с разными размерами отображения
Рисунок 2 – Шаблон с одинаковыми размерами отображения
Рисунок 3 – Клик по элементу
Итак, рассмотрим подход к проектированию структуры компонента и углубимся в код.
«Мат.часть» компонента
В компоненте содержится список из object-ов, который является набором исходных данных.
Делегат на функцию конвертации из пользовательского типа в шаблон для отображения (класс BlockTamplateBase).
Шаблон отображения (класс BlockTamplateBase) содержит информацию об области отображения и функцию, описывающую алгоритм рисования.
Разберём поля ListBlockView – Пример 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.
Можно сделать обычное событие, а можно – маршрутизируемое. Для моей задачи и обычного хватает.
/* Маршрутизируемое событие «Клик по элементу» 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.
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.
/// <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.
/// <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.
/// <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.
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 – алгоритм преобразования из пользовательского типа в шаблон.
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); }
/// <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.
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/
Добавить комментарий