Tutorial step 6: supporting user configuration

The next logical step is allowing the user to choose between half and third color ramp reduction, and to enable or disable the 2x scaling. This requires two extra routines, configProc and stringProc2.

Since Avisynth does not have a GUI, it cannot use these functions.  Avisynth users will not be able to configure your filter at all if you do not provide a script interface!

The configProc and stringProc2 functions

configProc is simple: it brings up a dialog box. This requires an extra parameter to the usual template:

Function configProc(fa: PFilterActivation; const ff: PFilterFunctions; hWnd: HWND): Integer; cdecl;

When called, configProc should open up a modal dialog box. Return zero for OK, and non-zero for Cancel, but modify configuration parameters on OK only. VirtualDub doesn't care about the return parameter normally, except when a filter is first added to the chain; here VirtualDub calls configProc immediately, and if the user cancels the dialog box, the filter instance is destroyed.

Note: You'll probably need the HINSTANCE of your filter module to open the dialog box. Fortunately, VirtualDub provides this value for you as fa^.filter^.module^.hInstModule, so you won't have to write a DLL entry routine to cache the instance pointer.

stringProc2 is an advancement of stringProc. They both receive the address of a string buffer (i.e. a null-terminated string) to fill. The diference between the two is that stringProc2 receives also in maxlen the number of characters available in the buffer, while stringProc doesn't and you risk overrunning it:

Procedure stringProc(const fa: PFilterActivation; const ff: PFilterFunctions; var buf: PChar); cdecl;
Procedure stringProc2(const fa: PFilterActivation; const ff: PFilterFunctions; var buf: PChar; maxlen: Integer); cdecl;

The purpose of stringProc is to give a quick overview of a filter instance's settings on the filter list. For instance:

320x240 640x480 resize (bilinear)

The buf parameter points right after the name, and the usual course of action is to supply the buffer to StrCopy or StrLCopy. By convention, filters should add the space and specify the parameters in parentheses.

Starting in VirtualDub version 1.4.11 you have to provide the function stringProc2. If you want to keep compatability with previous versions of VirtualDub then you have to provide also the function stringProc.

Adding user configuration

To create a config dialog box, create a new form in Delphi and set the form property BorderStyle to bsDialog, the property FormStyle to fsStayOnTop and the property Position to poDesktopCenter. Add the OK and Cancel buttons, and add a checkbox for the selection of double width and a radiogroup for choosing between the division by 2 or by 3 (half or third color ramp reduction).

In the config form unit file

Next, we'll need to update TFilterData to hold the new fields. The declaration of TFilterData needs also to move from our main delphi project file to the form Unit file, to avoid having both files referencing each other under the uses clause. In a more complex filter involving several files the declaration of TFilterData, other types to be used throughout the project and general variables can be declared in an independent Unit shared by the several files of the project.

PFilterData = ^TFilterData;
TFilterData = packed record
 grn_tab: PArrayPixel32;
 blu_tab: PArrayPixel32;
 fExpand: Boolean;
 fThird: Boolean;
 end;

When creating more complex dialog boxes, you might find the need to store temporary items, such as GDI brush handles; the easiest way to do this is simply to stick them in your instance data structure.

We need a variable to hold the address of the filter data while the user is configurating the filter. One way to do it is to declare it as a field in the form declaration.

TConfigForm = class(TForm)
 (...)
private
 FilterData: PFilterData;
 (...)
end;

Next we need to set up two event handlers. One to initialise the controls in the form that should run when the configuration form is created. And another to update the filter data when the user presses OK.

procedure TConfigForm.FormCreate(Sender: TObject);
begin {TConfigForm.FormCreate}
if FilterData^.fThird = False then RadioGroupDivision.ItemIndex := 0
                              else RadioGroupDivision.ItemIndex := 1;
if FilterData^.fExpand = False then CheckBoxDblWdth.State := cbUnchecked
                               else CheckBoxDblWdth.State := cbChecked;
end; {TConfigForm.FormCreate}

procedure TConfigForm.ButtonOkClick(Sender: TObject);
begin {TConfigForm.ButtonOkClick}
if RadioGroupDivision.ItemIndex = 0 then FilterData^.fThird := False
                                    else FilterData^.fThird := True;
if CheckBoxDblWdth.State = cbUnchecked then FilterData^.fExpand := False
                                       else FilterData^.fExpand := True;
end; {TConfigForm.ButtonOkClick}

Finally, the CreateParented method from the class TForm is overriden in the TConfigForm subclass for reasons that will be explained next.

constructor TConfigForm.CreateParented(ParentWindow: HWnd; aFilterData: PFilterData);
begin {TConfigForm.CreateParented}
inherited CreateParented(ParentWindow);
FilterData := aFilterData;
end; {TConfigForm.CreateParented}

Our config form unit file should now look like this.

Note: You can delete the declaration of the variable for the form that Delphi includes by default, because it will be crated dinamicaly by the CreateParented constructor.

In the main project file

Now we need the configProc and stringProc functions. The configProc function will create an instance of the form declared in the form unit file, using the method CreateParented, inherited from the TForm class. CreateParented creates and initializes a control as the child of a specified non-VCL container, in this case VirtualDub which handle is given by the parameter hWnd. In this case the CreateParented from TForm is overriden in the TConfigForm subclass to allow the passage of the filter instance data.

Function tutorialConfigProc(fa: PFilterActivation; const ff: PFilterFunctions; hWnd: HWND): Integer; cdecl;
var
  aConfigForm: TConfigForm;
begin {tutorialConfigProc}
aConfigForm := TConfigForm.CreateParented(hWnd, PFilterData(fa^.filter_data));
if aConfigForm.ShowModal = mrOK then Result := 0
                                else Result := 1;
end; {tutorialConfigProc}

Procedure tutorialStringProc2(const fa: PFilterActivation; const ff: PFilterFunctions; var buf: PChar; maxlen: Integer); cdecl;
const
  modes: Array[False..True, False..True] of PChar = (
    (' (normal)', ' (expand)'),
    (' (third)', ' (expand+third)'));
var
  mfd: PFilterData;
begin {tutorialStringProc2}
mfd := PFilterData(fa^.filter_data);
StrLCopy(buf, modes[mfd^.fThird, mfd^.fExpand], maxlen);
end; {tutorialStringProc2}

Procedure tutorialStringProc(const fa: PFilterActivation; const ff: PFilterFunctions; var buf: PChar); cdecl;
begin {tutorialStringProc}
tutorialStringProc2(fa, ff, buf, 60);
end; {tutorialStringProc}

Add the new functions to the FilterDefinition structure:

configProc: tutorialConfigProc;
stringProc: tutorialStringProc;

stringProc2: tutorialStringProc2

We need to rewrite paramProc as well, to switch between 1x and 2x modes.

Function tutorialParamProc(fa: PFilterActivation; const ff: PFilterFunctions): Integer; cdecl;
var mfd: PFilterData;
begin {tutorialParamProc}
mfd := PFilterData(fa^.filter_data);
if (mfd^.fExpand = True) then fa^.dst.w := fa^.dst.w * 2;
fa^.dst.pitch := (fa^.dst.w * 4 + 7) AND -8;
Result := FILTERPARAM_SWAP_BUFFERS;
end; {tutorialParamProc}

Finally, adjust startProc to generate a half lookup table as well, and runProc to allow non-expansion. The decision between halving and thirding color components is folded into the lookup tables, so there's no need to check the flag at runtime.

Function tutorialStartProc(fa: PFilterActivation; const ff: PFilterFunctions): Integer; cdecl;
var
 mfd: PFilterData;
 i: Integer;
begin {tutorialStartProc}
mfd := PFilterData(fa^.filter_data);
new(mfd^.grn_tab);
new(mfd^.blu_tab);
if (mfd^.fThird = True) then
 for i := 0 to 255 do begin
   mfd^.grn_tab[i] := (($200 + i) div 3) shl 8;
   mfd^.blu_tab[i] := i div 3;
   end
else
 for i := 0 to 255 do begin
   mfd^.grn_tab[i] := (($100 + i) div 2) shl 8;
   mfd^.blu_tab[i] := i div 2;
   end;
Result := 0;
end; {tutorialStartProc}

Function tutorialRunProc(const fa: PFilterActivation; const ff: PFilterFunctions): Integer; cdecl;
var
  w, h: TPixDim;
  src, dst: PPixel32;
  old_pixel, new_pixel: TPixel32;
  mfd: PFilterData;

begin {tutorialRunProc}
src := PPixel32(fa^.src^.data);
dst := PPixel32(fa^.dst^.data);
mfd := PFilterData(fa^.filter_data);
h := fa^.src^.h;
repeat
  w := fa^.src^.w;
  if (mfd^.fExpand = True) then
    repeat
      old_pixel := src^;
      new_pixel := (old_pixel AND $FF0000)
                   + mfd^.grn_tab[(old_pixel shr 8) AND $FF]
                   + mfd^.blu_tab[old_pixel AND $FF];
      dst^ := new_pixel;
      Inc(dst);
      dst^ := new_pixel;
      Inc(src);
      Inc(dst);
      Dec(w);
    until w = 0
  else
    repeat
      old_pixel := src^;
      new_pixel := (old_pixel AND $FF0000)
                   + mfd^.grn_tab[(old_pixel shr 8) AND $FF]
                   + mfd^.blu_tab[old_pixel AND $FF];
      dst^ := new_pixel;
      Inc(src);
      Inc(dst);
      Dec(w);
    until w = 0;
  Inc(src, fa^.src^._modulo);
  Inc(dst, fa^.dst^._modulo);
  Dec(h);
until h = 0;
Result := 0;
end; {tutorialRunProc}

Whew! This one involved a bit of coding. Modifying this structure for other filters is easy, though. Check here how the main project file of the filter should look like and here the config form unit file.