Загрузка в listview данных из двумерного массива. Обновление контрола.
Пожалуйста помогите решить такую проблему:
1) Есть контрол ListView с отображением данных в режиме Details
2) Данные в объект подтягиваются из некоторого двумерного массива str[*,*]
3) Данные подтягиваются предположим каждые 2 секунды, за это время массив str[*,*] меняется
Вот код:
{
str = new string[40, 2];
for (int i = 0; i < 40; i++)
{
for (int j = 0; j < 2; j++)
{
str[i, j] = "a" + Convert.ToString(i) + " b" + Convert.ToString(j);
}
}
}
private void timer1_Tick(object sender, EventArgs e)
{
ListViewItem[] itmx = new ListViewItem[str.GetLength(0)];
for (int i = 0; i < str.GetLength(0); i++)
{
itmx = new ListViewItem(i.ToString());
for (int j = 0; j < str.Length / str.GetLength(0); j++)
{
itmx.SubItems.Add(str[i, j]);
}
}
listView1.Items.Clear();
listView1.Items.AddRange(itmx);
}
Что не устраивает:
1) Все это дело мерцает
2) Полосы прокрутки возвращаются в умолчальное состояния, т.е. горизонтальная - до упора влево, а вертикальная - до упора вверх
Необходимо избавиться от этих двух эффектов.
Желаемый результат - получить контрол, который мог-бы отображать динамически изменяющиеся данные, наподобие того как это сделано в том-же uTorrent, что бы все обновлялось, без мерцаний и изменения положения полос прокрутки.
Всем заранее спасибо,
С уважением.
Дык, в этом то и заключается вопрос: каким образом?
Про VirtualMode я почитал... но не совсем понял.... примеров которые удовлетворяли бы моим условиям я тоже не нашел :( поэтому задаю вопрос здесь
Сделал по примеру в MSDN: http://msdn.microsoft.com/ru-ru/library/system.windows.forms.listview.virtualmode.aspx
Отрисовка ListView вызывается по событию timer_tick командой ListView.Refresh()
В результате избавился от перемещения полос прокрутки и исчезновения выделения, но к сожалению осталось мерцание, даже при обновлении один раз в три секунды.
Думаю что это из-за достаточно большого набора данных, если например массив str[*,*] будет размером 42х10.
Подскажите как быть?
Думаю что это из-за достаточно большого набора данных, если например массив str[*,*] будет размером 42х10.
Подскажите как быть?
А вы обновляете все ячейки? Или только те, что изменились?
Получается что все. Т.к. вызывается listview.Refresh()
Подскажите как перерисовать только измененные поля?
Подскажите как перерисовать только измененные поля?
Изменить соответсвующий SubItem в списке.
Естественно, все манипуляции производить между вызовами BeginUpdate и EndUpdate, также стоит установить DoubleBuffered в истину (у формы или контрола).
Естественно, все манипуляции производить между вызовами BeginUpdate и EndUpdate, также стоит установить DoubleBuffered в истину (у формы или контрола).
Т.е. Вы предлагаете:
1) Сначала скопировать полученный массив str[*,*] в str_swap[*,*]
2) Затем отрисовать str_swap[*,*] на listview
3) На следующем шаге сравнить str[*,*] c str_swap[*,*], понять что изменилось
4) Отрисовать изменения
5) Опять скопировать str[*,*] в str_swap[*,*]
Я Вас правильно понимаю?
Ну или сравнивать массив str[*,*] с тем что уже есть в ListView и при обнаружении изменений менять соответствующим Item?
Я думаю, что стоит держать набор (например, массив) каких-то промежуточных объектов (скажем, DataItem), которые будут осведомлены о предыдущем состоянии ячейки и в то же время будут иметь ссылку на соответствующий им SubItem списка. При изменении своего состояния они будут напрямую менять данные в списке.
А Вы не могли бы привести пример кода? что-то я уже битых 3 часа ковыряюсь и пока ничего не придумал :(
Вот пример кода, должно быть все достаточно прозрачно, но есть один хак, не без этого.
using System.Collections.Generic;
using System.Reflection;
using System.Windows.Forms;
namespace WindowsFormsApplication8 {
public partial class Form1 : Form {
private const int COLS = 20;
private const int ROWS = 50;
public Form1() {
InitializeComponent();
listView1.OptimizeForDoubleBuffer();
this.data = GenerateArray(COLS, ROWS);
this.data_items = DataItemHelper.FillListView(listView1, data);
}
private int[,] data;
private DataItem<int>[,] data_items;
private static int[,] GenerateArray(int col_count, int row_count) {
int[,] result = new int[row_count, col_count];
int value = 0;
for (int col = 0; col < col_count; ++col) {
for (int row = 0; row < row_count; ++row) {
result[row, col] = value++;
}
}
return result;
}
private void Form1_Load(object sender, EventArgs e) {
timer1.Enabled = true;
}
private Random random = new Random();
private void timer1_Tick(object sender, EventArgs e) {
int items_to_change = random.Next(20);
for (int i = 0; i < items_to_change; ++i) {
int row = random.Next(ROWS);
int col = random.Next(COLS);
++data[row, col];
}
DataItemHelper.RefreshListView(listView1, data, data_items);
}
}
public static class ListViewHelper {
public static void OptimizeForDoubleBuffer(this ListView list_view) {
// list_view.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
[COLOR=Red] list_view.GetType()
.GetMethod("SetStyle", BindingFlags.Instance | BindingFlags.NonPublic)
.Invoke(list_view, new object[] { ControlStyles.OptimizedDoubleBuffer, true });
[/COLOR] }
}
public class DataItem<T> {
private static readonly EqualityComparer<T> comparer = EqualityComparer<T>.Default;
public DataItem(ListViewItem.ListViewSubItem sub_item, T value) {
this._sub_item = sub_item;
this._value = value;
ValueChanged();
}
private readonly ListViewItem.ListViewSubItem _sub_item;
private T _value;
public T Value {
get {
return _value;
}
set {
if (!comparer.Equals(_value, value)) {
_value = value;
ValueChanged();
}
}
}
private void ValueChanged() {
_sub_item.Text = _value.ToString();
}
}
public static class DataItemHelper {
private const int COL_INDEX = 1;
private const int ROW_INDEX = 0;
public static DataItem<T>[,] FillListView<T>(ListView list, T[,] data) {
int row_count = data.GetUpperBound(ROW_INDEX);
int col_count = data.GetUpperBound(COL_INDEX);
list.BeginUpdate();
try {
list.Clear();
list.Columns.Add(new ColumnHeader() {
Width = 0,
DisplayIndex = 0
});
for (int col = 0; col <= col_count; ++col) {
list.Columns.Add(new ColumnHeader() {
Text = col.ToString(),
Width = 30
});
}
DataItem<T>[,] result = new DataItem<T>[row_count + 1, col_count + 1];
for (int row = 0; row <= row_count; ++row) {
ListViewItem item = list.Items.Add("");
for (int col = 0; col <= col_count; ++col) {
result[row, col] = new DataItem<T>(item.SubItems.Add(""), data[row, col]);
}
}
return result;
} finally {
list.EndUpdate();
}
}
public static void RefreshListView<T>(ListView list, T[,] data, DataItem<T>[,] data_items) {
int row_count = data.GetUpperBound(ROW_INDEX);
int col_count = data.GetUpperBound(COL_INDEX);
if (row_count != data_items.GetUpperBound(ROW_INDEX))
throw new ArgumentException();
if (col_count != data_items.GetUpperBound(COL_INDEX))
throw new ArgumentException();
for (int row = 0; row <= row_count; ++row) {
for (int col = 0; col <= col_count; ++col) {
data_items[row, col].Value = data[row, col];
}
}
}
}
}
Класс DataItem<T> оборачивает данные типа T и при изменении значения Value производит изменение соответствующего ListViewItem.ListViewSubItem-а.
Класс DataItemHelper содержит два метода: FillListView для начального заполнения списка и RefreshListView для обновления представления данных, последний метод как раз изменяет свойство Value у DataItem-ов.
Теперь по поводу хака. Для включения двойной буферизации в ListView необходимо вызвать защищенный метод SetStyle, безопасно можно это сделать просто пронаследовавашись от ListView и вызвав его в конструкторе (см. комментарий в коде). Я сделал небольшой хак, позволяющий избежать наследования, но вызвать этот метод посредством отражения, реализация находится в методе-расширении OptimizeForDoubleBuffer класса ListViewHelper.
Тогда мы получаем сообщение об ошибке вот в этом месте:
throw new ArgumentException();
А сообщение такое:
Значение не попадает в ожидаемый диапазон.
Тоже самое происходит если в массив наоборот добавляется еще одна строка
Значит нужно удалять/добавлять лишние/недостающие строки в списке а не швырять исключение, как в примере. ;)
Я видимо тупой но подскажите как, особенно если взять общий случай если добавилась новая строка не в конец массива, или если удалилась не последняя строка
А, собственно, не важно: из списка удалять последние элементы, если строк стало меньше, или добавлять нужное количество в конец в случае, когда строки наоборот добавились. Представление данных в конце концов все равно будет скорректировано.
Т.е. по сути можно удалит последнюю строку, или добавить новую и пустую, а потом после вызова очередного Refresh все перерисуется как нужно, верно я Вас понял? Т.е. главное что бы количество строк/столбцов совпадало?
Количество можно в методе Refresh контролировать вместо выброса исключений.
А каким образом можно переопределить размер массива data_items?
Я так понимаю что если у нас в массиве data строк больше чем в data_items то добавляем в data_items еще одну, т.е. меняем верхнюю границу массива
Если в data строк меньше, то удаляем одну строку из data_items.
Как это сделать что бы не создавать новый массив?
Никак, массивы в .NET не могут изменять размер. Array.Resize<T>, которая работает только для одномерных массивов занимается элементарным копированием данных из старого в новый.
Блин, ну как же тогда быть??? я никак не могу придумать нормального решения... помогите пожалуйста
Сделал так, но работает только при добавлении нового элемента... на удаление никак не реагирует:
Код формы
{
int SelectItem=0;
DataItemHelper.RefreshListView(ListView1, data, data_items);
if (Torrents_list.SelectedIndices.Count > 0)
{
SelectItem = ListView1.SelectedIndices[0];
}
}
private void button1_Click(object sender, EventArgs e)
{
if (Working == false)
{
backgroundWorker1.RunWorkerAsync();
Working = true;
GetDataTimer.Enabled = true;
}
else
{
backgroundWorker1.CancelAsync();
GetDataTimer.Enabled = false;
RefreshDataTimer.Enabled = false; FirstTime = true;
Working = false;
}
}
private void GetDataTimer_Tick(object sender, EventArgs e)
{
if (swap != null) { data = swap; }
if (data != null && FirstTime == true)
{
FirstTime = false;
data_items=DataItemHelper.FillListView(ListView1, data);
RefreshDataTimer.Enabled = true;
}
}
А в Вашем классе подправил следующий кусок таким образом:
{
int row_count = data.GetUpperBound(ROW_INDEX);
int col_count = data.GetUpperBound(COL_INDEX);
if (row_count != data_items.GetUpperBound(ROW_INDEX))
---------------- FirstTime = true;// throw new ArgumentException(); ------------------------------
if (col_count != data_items.GetUpperBound(COL_INDEX))
throw new ArgumentException();
for (int row = 0; row <= row_count; ++row)
{
for (int col = 0; col <= col_count; ++col)
{
data_items[row, col].Value = data[row, col];
}
}
Переменная FirstTime объявлена как Public