Версия для печати


Сам загружу, сам покажу
http://www.delphikingdom.com/asp/viewitem.asp?catalogID=1439

George Judkin
дата публикации 30-10-2011 16:12

Потребовалось мне в одном проекте строить в run-time формы на основании информации из базы данных. База данных, обработка по одной записи, да еще и создание новых компонент — очень большая вероятность медленной работы. Значит, согласно правилам хорошего тона, требуется какая-то индикация для отображения процесса. Посмотрел имеющиеся материалы, но, в основном, разработчики предлагают какие-то варианты внешнего индикатора, который крутится в отдельном потоке, развлекая пользователя, в то время как сама программа втихаря что-то делает. А мне в голову пришла другая идея.

Но сначала немного пояснений. У меня ситуация была такова, что не требовалось обеспечивать работоспособность основной программы, пока идет загрузка данных. Более того, основная программа вообще не должна позволять что-либо делать, пока не закончится загрузка (а то пользователь чего-то такого понажимает, а мне потом заниматься синхронизацией того, что он понажимал, с процессом загрузки). Так и пришла в голову мысль сделать модальное окно, которое бы само и данные грузило, и прогресс отображало (да еще бы и не давало пользователю наделать глупостей).

Пробуем

Программирования и без того было много, так что решил сделать как можно проще. Набросал быстренько форму, примерно вот таким образом:


То есть, TLabel для текста, TProgressBar для индикации процесса и TButton для отмены загрузки, если пользователь устанет ждать. Расставил необходимые свойства (в частности, TButton.ModalResult выставил в mrCancel, чтобы не писать обработчик нажатия кнопки). Пора переходить к реализации собственно загрузчика, а... А где его реализовывать-то? ShowModal — штука подлая: получит управление и не отдаст, пока форму не закроешь. Значит нужно искать какое-то событие. Вариант с вставкой загрузки в OnCreate получил отлуп сразу: даже пробовать не хочу, что получится, если пытаться использовать форму, которая еще до конца не создана. Остаются варианты типа OnActivate или OnShow. Более предпочтительным показался OnActivate, после чего был написан примерно такой код:

procedure TfrmLoader.AMDoLoad(var Msg : TMessage);
var
  i : Integer;
begin
  Progress.Max:=100; // здесь получаю количество элементов
  for i:=1 to Progress.Max do
    begin
    Sleep(50); // здесь собственно обработка
    Progress.StepIt;
    Application.ProcessMessages;
    if ModalResult <> mrNone then Exit;
    end;
  ModalResult:=mrOk;
end;

Для упрощения я выбросил конкретику по определению количества шагов и обработке на каждом шаге.

Все получилось хорошо. Все работало, грузилось и отображалось. Но не работала отмена. В общем, и не удивительно, что она не работала. Если заглянуть в модуль Forms.pas и посмотреть на реализацию метода TCustomForm.ShowModal, то сразу видно, что сначала посылается сообщение CM_ACTIVATE, а только после того, как оно будет обработано, будет установлено нулевое значение ModalResult и начнется цикл петли сообщений. И OnShow здесь не помощник, так как показ формы происходит еще раньше.

Но у нас имеется замечательный механизм — сообщения. Надо только знать, чем отличается SendMessage от PostMessage и не перепутать. От попытки использовать не по назначению какое-либо из уже существующих сообщений я отказался. Мы имеем право создавать и использовать свои собственные сообщения уровня приложения, вот и будем этим пользоваться. В итоге код получился примерно такой:

const
  AM_DOLOAD = WM_USER+1;

type
  TfrmLoader = class(TForm)
    fldText: TLabel;
    Progress: TProgressBar;
    btnCancel: TButton;
    procedure FormActivate(Sender : TObject);
  private
    FLoadStarted : Boolean; // см. реализацию FormActivate
    procedure AMDoLoad(var Msg : TMessage); message AM_DOLOAD;
  end;

implementation

{$R *.dfm}

procedure TfrmLoader.AMDoLoad(var Msg : TMessage);
var
  i : Integer;
begin
  Progress.Max:=100;
  for i:=1 to Progress.Max do
    begin
    Sleep(50);
    Progress.StepIt;
    Application.ProcessMessages;
    if ModalResult <> mrNone then Exit;
    end;
  ModalResult:=mrOk;
end;

procedure TfrmLoader.FormActivate(Sender : TObject);
begin
  if FLoadStarted then Exit; // на всякий случай, чтобы избежать повтора
  FLoadStarted:=true;
  PostMessage(Handle,AM_DOLOAD,0,0);
end;

Собственно, все. Содержательная часть работы перекочевала в метод AMDoLoad. После вызова ShowModal для формы в обработчике OnActivate мы сначала проверяем, не запускался ли уже раньше цикл загрузки (вот такой я маньяк-перестраховщик), если нет, то посылаем сами себе сообщение AM_DOLOAD и сразу выходим, не дожидаясь пока обработается сообщение. А сообщение и не будет пока обрабатываться. Сначала отработает активация, потом начнется цикл петли сообщений, наше сообщение будет извлечено из очереди, и начнется загрузка. Если не нажимать кнопку, то после выполнения загрузки ShowModal закончит работу, вернув нам значение mrOK. Если нажать кнопку, то — mrCancel.

Задача, вроде как, решена. Но есть в этом что-то... не то. Ведь вещь общезначимая, так что хотелось бы сделать так, чтобы потом легко можно было использовать и в других проектах. А заодно и сказать несколько слов о том, как надо делать, а как не надо.

Обобщаем

Есть сейчас такое поветрие использовать оставшееся до завершения процесса время в качестве параметра для отображения хода выполнения процесса. Не знаю, кому как, а меня это сильно раздражает. И ожидание полтора часа, пока загрузится файл, до завершения загрузки которого осталось 3 секунды. И страшное сообщение о том, что копирование файла (сравнительного небольшого) займет 20 минут (а на деле — две). В общем, если нам операционная система не гарантирует точное время выполнения процесса, то использование в данном случае времени — это просто обман пользователя. Уж лучше тогда положить анимированную картинку и вообще ничего не обещать.

Поэтому я обычно стараюсь использовать конкретные перечислимые параметры (количество обрабатываемых файлов или записей в БД, количество загружаемых модулей и т.п.). Тут, правда, придется дополнительно затратить время на подсчет количества элементов, которые нужно обработать. Но, по-моему, это незначительные затраты, которые с лихвой окупаются улучшением юзабилити программы. Поэтому предлагается вот такой модуль.

unit Unit2;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, Buttons, ComCtrls;

const
  AM_STARTLOADING = WM_USER+1;

type
  TfrmLoader = class(TForm)
    fldText : TLabel;
    ProgressBar : TProgressBar;
    btnCancel : TBitBtn;
    procedure FormActivate(Sender : TObject);
  private
    FUsing : Boolean;
    procedure AMStartLoading(var Msg : TMessage); message AM_STARTLOADING;
  protected
    function CalcStepCount : Integer; virtual;
    function ProceedStep(StepNum : Integer) : Boolean; virtual;
  public
    function LoadData : Boolean;
  end;

implementation

{$R *.dfm}

const
  msgInfIsPreparing = 'Идет подготовка данных...';
  msgInfIsLoading   = 'Идет загрузка данных...';

procedure TfrmLoader.AMStartLoading(var Msg : TMessage);
var
  i : Integer;
begin
  fldText.Caption:=msgInfIsPreparing;
  Application.ProcessMessages;
  ProgressBar.Max:=CalcStepCount;
  fldText.Caption:=msgInfIsLoading;
  Application.ProcessMessages;
  for i:=1 to ProgressBar.Max do
    begin
    if not ProceedStep(i) then ModalResult:=mrCancel;
    if ModalResult <> mrNone then Exit;
    ProgressBar.StepIt;
    Application.ProcessMessages;
    end;
  ModalResult:=mrOK;
end;

function TfrmLoader.CalcStepCount : Integer;
begin
  // do calculating...
  Result:=20;
end;

function TfrmLoader.ProceedStep(StepNum : Integer) : Boolean;
begin
  // proceeding step...
  Result:=true;
end;

function TfrmLoader.LoadData : Boolean;
begin
  Result:=(ShowModal = mrOK);
end;

procedure TfrmLoader.FormActivate(Sender : TObject);
begin
  if FUsing then Exit;
  FUsing:=true;
  PostMessage(Handle,AM_STARTLOADING,0,0);
end;

end.

Здесь я явно выделил два метода — CalcStepCount и ProceedStep — в которых нужно вычислить количество шагов загрузки, и выполнить каждый шаг. Методы виртуальные, чтобы при необходимости можно было изменить реализацию в классах-наследниках. Именно реализацию этих двух методов необходимо будет прописать для данного класса. Все остальное — обще-универсальное. Можно доработать, а можно и так оставить.

Если реализация protected-методов, задающих специфику формы, прописана, то использование очень простое: создаем экземпляр формы и вызываем public-метод LoadData. По возвращаемому значению судим об успешности завершения процесса. Все. Потом только уничтожить форму надо не забыть.

У меня эта форма вообще прописалась в репозитории. Как мне кажется, она ничуть не хуже, чем уже имеющиеся там About box или Dual list box.

Хвастаемся

Как водится, к материалу прилагается демонстрационная программа. Как водится, простенькая и не представляющая особой ценности. Указываем файл, а программа для него строит частотную характеристику — гистограмму, в которой каждому из возможных значений байта поставлен в соответствие один столбец таким образом, что высота столбца пропорциональна тому, сколько раз встречается этот байт в файле.

Здесь надо сказать, наверное, только одно. Для обработки файла используется буфер размером в полмегабайта. Количество шагов, таким образом, будет определяться тем, на сколько частей разобьется интересующий нас файл, если размер каждой части равен полмегабайта. Отсюда и соображения по тому, на каких файлах гонять программу. Лучше всего, когда количество шагов порядка сотни. Соответственно, используйте файлы размером порядка 50 мегабайт. Если файл маленький, то вы просто ничего не успеете увидеть, а если большой, то придется долго ждать завершения загрузки.



Специально для Королевства Delphi


К материалу прилагаются файлы: