Tuesday, October 12, 2010

Fun with callbacks and progress form

Some time ago I needed to display a progress form in order to give feedback to the user on current state of the task, therefore the „challenge¯ is pretty simple: a secondary form that will have a label — to display some info on current task — and two progress bars — for visual feedback — 1 progress bar for the „overall progress¯ and the other one for „current progress¯, this means that we have a task which is composed of few steps and each step has it's own progress.
Sooo... the MAIN idea is that the progress form needs to be displayed as modal while also calling a method(procedure) from another unit — this is where callbacks come into play.
What are callbacks?!

A callback is a reference to executable code, or a piece of executable code, that is passed as an argument to other code. This allows a lower-level software layer to call a subroutine (or function) defined in a higher-level layer.
For a better understanding of this article click this text to download the source code.
Let's achieve the same thing in a new project:
- add a button to this form
- add a new form to this application, remove it from auto-create forms(Project->Options->Forms)
- add two labels and two progress bars to the second form
- set the caption of one label to „Overall progress¯ and to the other label „Current progress¯
- name one progress bar „pbOverall¯ and the other „pbCurrent¯
Now let's write some code, first define the callback method as
type
  TProgressCallback = procedure (InProgressOverall, InProgressCurrent: TProgressBar) of Object;
we will pass „pbOverall¯ and „pbCurrent¯ as parameters in the callback method.
It's time to define a generic method that will create a new instance of our progress form in order to display a modal progress
procedure ShowProgress(InCallback: TProgressCallback);
var
  LWindowList: TTaskWindowList;
  LSaveFocusState: TFocusState;
  LProgressForm: TfrmProgressForm;
begin
  // create the instance
  LProgressForm := TfrmProgressForm.Create(NIL);
  try
    // save the focus state
    LSaveFocusState := SaveFocusState;
    // save the focused form
    Screen.SaveFocusedList.Insert(0, Screen.FocusedForm);
    // notify that a form will be displayed as modal
    Application.ModalStarted;
    // disable all other forms
    LWindowList := DisableTaskWindows(0);
    // set the progress form instance as the screen focused form 
    Screen.FocusedForm := LProgressForm;
    // send a active message
    SendMessage(LProgressForm.Handle, CM_ACTIVATE, 0, 0);
    // show the form
    LProgressForm.Show;
    // InCallback is our callback method to which we pass pbOverall and pbCurrent
    // as parameters so we can play with them later
    InCallback(LProgressForm.pbOverall, LProgressForm.pbCurrent);
    // after the callback is executed enable windows
    EnableTaskWindows(LWindowList);
    // restore focus state
    RestoreFocusState(LSaveFocusState);
  finally
    // notify that we're leaving a modal state
    Application.ModalFinished;
    // free and nil the progress form instance
    FreeAndNil(LProgressForm);
  end;
end;
all we have left to do now is to define a (private or public)method in main form with same parameters as the callback method like so:
type
  TForm1 = class(TForm)
    Button1: TButton;
    procedure Button1Click(Sender: TObject);
  private
    // this is our callback method
    procedure ProgressCallback(InProgressOverall, InProgressCurrent: TProgressBar);
  public
    { Public declarations }
  end;
in the implementation section copy-paste this code
procedure TForm1.ProgressCallback(InProgressOverall,
  InProgressCurrent: TProgressBar);
var
  Index: Integer;
  kIndex: Integer;
begin
  MessageDlg('Press OK to start a long task...', mtInformation, [mbOK], 0);
  // 10 steps
  InProgressOverall.Max := 10;
  // 3000 updates per step
  InProgressCurrent.Max := 3000;
  for Index := 1 to InProgressOverall.Max do begin
    for kIndex := 1 to InProgressCurrent.Max do begin
      InProgressCurrent.Position := kIndex;
      // force application to process messages
      Application.ProcessMessages;
    end; // for kIndex := 1 to InProgressCurrent.Max do begin
    InProgressOverall.Position := Index;
    // force application to process messages
    Application.ProcessMessages;
  end; // for Index := 1 to InProgressOverall.Max do begin
  MessageDlg('Task completed!', mtInformation, [mbOK], 0);
end;
on the main form you have a button, in it's OnClick event add the following code:
ShowProgress(Self.ProgressCallback);
Any ideas on how to achieve the same effect in less code or more elegant?! please leave comment.

No comments:

Post a Comment

Blogroll(General programming and Delphi feeds)