SysTools Logo SysTools


ListView_DeleteAllItems() и Windows 98


Разрабатывая программу ChemBase (Softpedia, GitHub) пришлось столкнуться с ошибкой в реализации удаления элементов из ListView.

Т.к. все программы разрабатываемые SysTools ориентированы на "Windows All" (т.е. должны работать на любой системе Windows 32/64 бита), то проводится их тестирование под системами Windows 98, Windows XP и Windows 10. Если программа работает под каждой из этих систем без проблем, то она будет работать везде начиная с Windows 98 до Windows 10 включительно.

Один из последних тестов для ChemBase заключался в выводе всех 27000+ элементов из базы данных в ListView (заполнялось примерно за 5 секунд). После чего при попытке закрыть программу она зависла. Зависание стабильно повторялось после каждого перезапуска ChemBase и выбора всей базы данных. Если же не снимать задачу принудительно, то программы сама "выходила из комы" примерно через 10 минут (Windows 98 под Microsoft Virtual PC). Вставка логирования ничего не давала, т.к. зависание проходило где-то в ядре системы при уничтожении главного окна программы. Также выяснилось что ошибка воспроизводится и программа перестаёт отвечать, если выполнить ещё один поиск элементов, даже с пустым результатом. Расстановка логирования в этом участке кода позволила точно выявить виновника - вызов функции ListView_DeleteAllItems() удаляющей все элементы из ListView.

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

EnableWindow(wnd, FALSE);
ShowWindow(wnd, SW_HIDE);
SendMessage(wnd, WM_SETREDRAW, FALSE, 0);

ListView_DeleteAllItems(wnd);

SendMessage(wnd, WM_SETREDRAW, TRUE, 0);
ShowWindow(wnd, SW_SHOW);
EnableWindow(wnd, TRUE);

Вторым предположением для проверки стала попытка совсем отказаться от вызова ListView_DeleteAllItems() и удалять элементы вручную:

while (ListView_DeleteItem(wnd, 0));

ListView_DeleteItem() требует указание индекса элемента для удаления и проще всего указывать 0-ой, потому что он всегда присутствует, пока ListView не пуст. К удивлению, такое удаление заняло те же самые 10 минут.

Последним проверенным предположением стало удаление элементов с конца и ручном указании индекса:

for (i = ListView_GetItemCount(wnd); i > 0; i--) {
  ListView_DeleteItem(wnd, i - 1);
}

И только такое удаление отработало всего за 15 секунд, вместо 10 минут.

Вероятно в Windows 98 (а, возможно, и более ранних системах) удаление всех элементов через ListView_DeleteAllItems() было реализовано последовательным вызовом ListView_DeleteItem() для нулевого элемента т.к. он всегда существовал для непустого ListView. Нужно отметить, что ListView_DeleteAllItems() в Windows XP отрабатывает мгновенно - т.е. начиная как минимум с этой операционной системы удаление всех элементов было исправлено.

Объяснить такую задержку при удалении первого элемента довольно просто - по всей видимости при удалении элемента происходит сдвиг всех элементов после и/или перераспределение памяти.

В итоге код удаления всех элементов стал выглядеть примерно так:

/*
  PATCH: Windows 98 can add 27k+ items just fine (around 10 seconds)
  but deleting with ListView_DeleteAllItems() can take up to whole 10 minutes!
  items MUST be deleted from last to first (around 15 seconds) and not vice versa
  or it takes the same 10 minutes (the Microsoft way)
  this bug was fixed at least in Windows XP and newer
*/

void ListView_ManualDeleteAllItems(HWND wnd) {
int i;
  for (i = ListView_GetItemCount(wnd); i > 0; i--) {
    ListView_DeleteItem(wnd, i - 1);
  }
  // just in case
  ListView_DeleteAllItems(wnd);
}

Важно также не забыть сделать вызов этого кода при закрытии или перед уничтожением главного окна, чтобы не дать системе вызвать обработчик по умолчанию. Вернее система его всё равно вызовет при уничтожении ListView, но он к этому времени будет уже пуст.

К недостаткам такого способа удаления можно отнести то, что отрабатывает он с задержкой на любом Windows (Windows 98: 15 секунд, Windows XP: 3 секунды) при том что ListView_DeleteAllItems() выполняется мгновенно на Windows XP и новее. Однако, в сравнении с 10 минутами, такая задержка уже не является существенной.


2017.03.05


[ Статьи ]