С давних времён я задаюсь вопросом о том как можно бы было сделать рисование полосы загрузки по мере выполнения какого-то большого кода — например, при загрузке операционной системы, при запуске всяких программ и игр.
В случае с установщиком все понятно — установщику нужно скопировать определенный набор файлов, делаем это в цикле, а в самом конце каждой итерации обновляем полосу загрузки.
Но что делать, если в программе нужно выполнить кучу неоднородного кода (который нельзя так просто оформить в виде цикла), заставляющего пользователя ждать? Пример с запуском операционной системы очень хороший.
На Free Pascal я могу назвать три решения.
1) В лоб — после каждой строчки кода загрузки вызываем функцию перерисовки. Это можно сделать так:
for I := 0 to LinesCount - 1 do begin
case I of
0: <Line0>;
1: <Line1>;
2: <Line2>;
...
end;
RedrawLoading(I, LinesCount);
end;
Вполне приемлемое решение, кроме того, что при добавлении новой строки нужно не забыть увеличить LinesCount.
2) Использовать нити (threads, так же известные как «потоки») — в одной нити грузим, в другой обновляем экран. Решение мне не нравится тем, что ИМХО с нитями лучше лишний раз не связываться.
3) Написать класс, предназначенный для выполнения одного шага загрузки, имеющий одну абстрактную виртуальную функцию Execute(). Унаследовать от него разные типы шагов: копирование файла, загрузка библиотеки, загрузки картинки, проверка чего-нибудь и т.д. После этого перед загрузкой занести в нужной последовательности такие классы в массив, и в цикле после каждого Execute вызывать RedrawLoading(I, Length(Steps)). Очень громоздкое решение — городить его ради ерунды как-то не очень хочется.
Наиболее симпатичным является первое решение, за свою простоту. Но вручную это делать не хочется — вот было бы здорово, если бы компилятор это сделал сам!
На лиспе в таких случаях проблем нет — просто пишется макрос, который всю грязную работу выполняет за вас. У меня для данной задачи получилось следующее
(defmacro watch ((watch-func) &body body)
(let ((op body))
`(progn
,@(loop for i upto (- (* (list-length body) 2) 1) collect
(if (evenp i)
`(,watch-func ,(/ i 2) ,(list-length body))
(let ((result `(,@(car op)))) (setf op (cdr op)) result)))
(,watch-func ,(list-length body) ,(list-length body)))))
Функция watch-func как бы наблюдает за ходом выполнения кода body, поэтому макрос я назвал watch. Несмотря на то, что я попытался написать красивый код, получилось спагетти. Но поставленную задачу оно выполняет, вот лог нескольких тестов:
(watch (L) A B C D E F)
(PROGN (L 0 6) A (L 1 6) B (L 2 6) C (L 3 6) D (L 4 6) E (L 5 6) F (L 6 6))
(watch (L) A)
(PROGN (L 0 1) A (L 1 1))
(watch (L))
(PROGN (L 0 0))
По хорошему, нужно еще обезопасить L от многократных вычислений, это можно сделать конструкцией once-only.
В Free Pascal нормальной системы макросов нет. Поэтому аналогичную конструкцию я написал в моей утилитке dpp. Т.к. для того, чтобы достаточно точно интегрировать конструкцию в сам язык, нужно парсить программу на паскале, мне пришлось вводить несколько инородный синтаксис:
#(watch (RenderLoading)
(FGui := TGui.Create(FGraph, FWindow);)
(FFontMan := TFontMan.Create(Graph);)
(FScene := TCucuScene.Create(Graph);)
(FStyle := TStyle.Create;)
(FStyle.SetParam('font.primary', TStyleParamFont.Create(TFont.Create(FFontMan, 'data\font\c256.bit', -6)));)
(FStyle.SetParam('color.primary', TStyleParamColor.Create(Vec4f(0.5, 0.5, 0, 1)));)
(FStyle.SetParam('color.background', TStyleParamColor.Create(Vec4f(0.3, 0.0, 0.3, 1)));)
(FGui.Root.Style := FStyle;)
(FConsole := TGuiConsole.Create(FGui);)
(FStart := GetTimer;)
)
3 комментария:
Единственный реальный способ реализовать прогресс-бар - это выполнять всю работу вне GUI-нити, либо же гарантировать возврат в цикл обработки сообщений через фиксированное время (что сделать очень и очень сложно, т.к. даже простейшие операции могут оказаться блокирующими и очень долгими). Только в этом случае удастся добиться отзывчивости от GUI.
>с нитями лучше лишний раз не связываться.
Чем это объясняется?
Организовывать фиксированное время не обязательно, достаточно чтобы каждое действие при загрузке выполнялось за достаточно малое время, — пользователь и не заметит разницы.
Моё решение, конечно, не претендует на всеобщность, но зато достаточно простое, для небольших программ вполне сойдёт.
> Чем это объясняется?
Наверно, ничем обоснованным :) Моё субъективное предпочтение решениям без нитей там, где это возможно. Вставил в пост «ИМХО».
К примеру, в Free Pascal функция Random не thread-safe, её следует использовать в критических секциях, но об этом нигде не написано. Я этого до недавнего времени не знал, и не понятно сколько всего я ещё не знаю, — поэтому лишний раз лучше не рисковать :)
Отправить комментарий