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++;
}
}


Далі розглянемо можливості «розмальовки елементів» на базі шаблонів. Відразу забіжу вперед, сказавши, що такий підхід забезпечується можливість опису шаблону, так, як треба.br/>
Базовий шаблон (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»

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.