Making a Delphi form sizable without changing BorderStyle

Don’t you just hate it when you sit in front of a 24″ monitor and have to use a form that is the size of a postage stamp?

GExperts has got a nifty feature where it makes forms of the Delphi IDE sizable that aren’t by default.

GExperts-configuration-ide-enhancements-enhance-dialogs

This works only for some forms and I wondered why it didn’t for others. So I tried to add some more (Yes, I am doing this for fun.). The primary candidates were the Search -> Find and Search -> Replace dialogs. They are sizable in Delphi >=7 but not in Delphi 6.
All I had to do was change some component properties …

procedure TManagedForm.MakeSearchFormResizable(Form: TCustomForm);
var
  frm: TForm;
  i: Integer;
  cmp: TComponent;
begin
  // This is only ever called in Delphi6 because the Search form of later
  // versions is already resizable.
  frm := TForm(Form);
  FMinHeight := -frm.Height;
  FMinWidth := frm.Width;
  frm.OnCanResize := FormCanResize;
  for i := 0 to Form.ComponentCount - 1 do begin
    cmp := Form.Components[i];
    if cmp is TPageControl then begin
      TPageControl(cmp).Anchors := [akLeft, akTop, akRight, akBottom];
    end else if cmp is TButton then begin
      TButton(cmp).Anchors := [akRight, akBottom];
    end else if cmp is TCustomComboBox then begin
      TCombobox(cmp).Anchors := [akLeft, akRight, akTop];
    end else if cmp is TGroupBox then begin
      if cmp.Name = 'GroupBox4' then begin
        // stupid developer forgot to give it a meaningful name :-(
        TGroupBox(cmp).Anchors := [akLeft, akRight, akTop];
      end;
    end;
  end;
end;

procedure TManagedForm.MakeReplaceFormResizable(Form: TCustomForm);
var
  frm: TForm;
  i: Integer;
  cmp: TComponent;
begin
  // This is only ever called in Delphi6 because the Replace form of later
  // versions is already resizable.
  frm := TForm(Form);
  FMinHeight := -frm.Height;
  FMinWidth := frm.Width;
  frm.OnCanResize := FormCanResize;
  for i := 0 to Form.ComponentCount - 1 do begin
    cmp := Form.Components[i];
    if cmp is TPageControl then begin
      TPageControl(cmp).Anchors := [akLeft, akTop, akRight, akBottom];
    end else if cmp is TButton then begin
      TButton(cmp).Anchors := [akRight, akBottom];
    end else if cmp is TCustomComboBox then begin
      TCombobox(cmp).Anchors := [akLeft, akRight, akTop];
    end;
  end;
end;

… add an OnCanResize event handler to it that makes sure the form doesn’t get resized too small …

procedure TManagedForm.FormCanResize(Sender: TObject; var NewWidth, NewHeight: Integer;
  var Resize: Boolean);
begin
  if NewWidth < FMinWidth then
    NewWidth := FMinWidth;
  if (FMinHeight < 0) or (NewHeight < FMinHeight) then
    NewHeight := Abs(FMinHeight);
end;

… and change the border style to bsSizable.

  Form.BorderStyle := bsSizable;

At least that’s what I thought, but unfortunately this resulted in the exception “Cannot change visiblity in OnShow or OnHide”:

Cannot change visiblity in OnShow or OnHide

Looking at the VCL sources it seemed to be simply a case of removing fsShowing from the FormState, then changing the BorderStyle and afterwards adding fsShowing to FormState again. And lo and behold: It worked – kind of. I also had to recreate the form handle and assign it to my internal structures because changing the BorderStyle closes the handle.

type
  TCustomFormHack = class(TCustomForm)
  end;

// [...]
    WasShowing := (fsShowing in TCustomFormHack(Form).FFormState);
    if WasShowing then
      Exclude(TCustomFormHack(Form).FFormState, fsShowing);
// some other stuff
    if WasShowing then begin
      Form.BorderStyle := bsSizeable;
      Form.HandleNeeded;
      Handle := Form.Handle;
    end;

Thus emboldened, I went on to the more complex Tools -> Environment Options dialog. This dialog is sizable in Delphi 2005 and up, but not in Delphi 6 and 7. It was a bit more involved to make its controls move sensibly when it was resized:

procedure TManagedForm.MakePasEnvironmentDialogResizable(Form: TCustomForm);
var
  VariableOptionsFrame: TWinControl;

  function IsInVarOptionsFrame(_cmp: TComponent): boolean;
  begin
    Result := Assigned(VariableOptionsFrame) and (_cmp is TControl);
    if Result then
      Result := VariableOptionsFrame.ContainsControl(TControl(_cmp));
  end;

  procedure HandleComponent(_cmp: TComponent);
  var
    i: Integer;
  begin
    if _cmp.ClassNameIs('TVariableOptionsFrame') then begin
      if SameText(_cmp.Name, 'VariableOptionsFrame') then begin
        VariableOptionsFrame := TWinControl(_cmp);
        VariableOptionsFrame.Align := alClient;
      end;
    end else if _cmp is TPageControl then begin
      TPageControl(_cmp).Anchors := [akLeft, akTop, akRight, akBottom];
    end else if _cmp is TPanel then begin
      if SameText(_cmp.Name, 'ToolListPanel') or IsInVarOptionsFrame(_cmp) then
        TPanel(_cmp).Anchors := [akLeft, akTop, akRight, akBottom];
    end else if _cmp is TButton then begin
      TButton(_cmp).Anchors := [akRight, akBottom];
    end else if _cmp is TColorBox then begin
      if SameText(_cmp.Name, 'cbColorPicker') then
        TColorBox(_cmp).Anchors := [akLeft, akBottom];
    end else if _cmp is TCustomComboBox then begin
      if SameText(_cmp.Name, 'ecLibraryPath') or SameText(_cmp.Name, 'ecDLLOutput')
        or SameText(_cmp.Name, 'ecDCPOutput') or SameText(_cmp.Name, 'ecBrowsing') then begin
        TCombobox(_cmp).Anchors := [akLeft, akRight, akTop];
      end;
    end else if _cmp is TGroupBox then begin
      if SameText(_cmp.Name, 'GroupBox10') or SameText(_cmp.Name, 'Repository') then begin
        TGroupBox(_cmp).Anchors := [akLeft, akTop, akRight];
      end else if SameText(_cmp.Name, 'gbColors') then begin
        TGroupBox(_cmp).Anchors := [akLeft, akTop, akBottom];
      end else if IsInVarOptionsFrame(_cmp) then begin
        if SameText(_cmp.Name, 'GroupBox1') then begin
          TGroupBox(_cmp).Anchors := [akLeft, akTop, akBottom];
        end else if SameText(_cmp.Name, 'GroupBox2') then begin
          TGroupBox(_cmp).Anchors := [akLeft, akBottom];
        end;
      end;
    end else if _cmp is TListBox then begin
      if SameText(_cmp.Name, 'lbColors')  then begin
        TListBox(_cmp).Anchors := [akLeft, akTop, akBottom];
      end else if SameText(_cmp.Name, 'PageListBox') then begin
        TListBox(_cmp).Anchors := [akLeft, akTop, akBottom];
        if (TListBox(_cmp).Items.Count = 0) and (FItems.Count <> 0) then
          TListBox(_cmp).Items.Assign(FItems);
      end;
    end else if _cmp.ClassNameIs('TPropEdit') then begin
      if SameText(_cmp.Name, 'ecRepositoryDir') then begin
        TCustomEdit(_cmp).Anchors := [akLeft, akTop, akRight];
      end;
    end else if _cmp.ClassNameIs('THintListView') then begin
      if IsInVarOptionsFrame(_cmp) then
        TWinControl(_cmp).Anchors := [akLeft, akTop, akRight];
    end;
    for i := 0 to _cmp.ComponentCount - 1 do begin
      HandleComponent(_cmp.Components[i]);
    end;
  end;

var
  frm: TForm;
begin
  // This is called in Delphi6 and Delphi 7 because the Environment form of later
  // versions is already resizable.
  frm := TForm(Form);
  FMinHeight := frm.Height;
  FMinWidth := frm.Width;
  frm.OnCanResize := FormCanResize;
  HandleComponent(frm);
end;

It too seemed to work nicely, until I came across this oddity:

Environment-Options-Palette

The Pages list, that normally contains all tabs of the component palette, was empty. It turned out that this was because the form’s handle was closed and recreated (This is a known VCL problem which still exists in some later Delphi versions. I have no idea if it has been fixed by now.). Trying to store the list in a StringList and assign it later did not solve the problem because the ListBox.Items.Objects[] contained pointers to a rather complex data structure that were also lost.

I was out of ideas and turned to the trusty StackOverflow community (of course I tried to Google this question first, but nothing turned up.) who gave me this answer which I turned into this method:

procedure TManagedForm.ForceVisibleToBeSizable(WndHandle: HWND);
begin
  // this is taken from http://stackoverflow.com/a/34255563/49925
  SetWindowLong(WndHandle, GWL_STYLE,
    GetWindowLong(WndHandle, GWL_STYLE) and not WS_POPUP or WS_THICKFRAME);
  SetWindowLong(WndHandle, GWL_EXSTYLE,
    GetWindowLong(WndHandle, GWL_EXSTYLE) and not WS_EX_DLGMODALFRAME);
  InsertMenu(GetSystemMenu(WndHandle, False), 1, MF_STRING or MF_BYPOSITION, SC_SIZE, 'Size');
  SetWindowPos(WndHandle, 0, 0, 0, 0, 0,
    SWP_NOSIZE or SWP_NOMOVE or SWP_NOZORDER or SWP_FRAMECHANGED);
  DrawMenuBar(WndHandle);
end;

So, now all is well. The Environment Options dialog is sizable.

Sizable-Environment Options