DelphiでbsNoneフォームに移動とサイズ変更機能を追加する

# DelphiでbsNoneフォームに移動とサイズ変更機能を追加する

## 概要

Delphiの`TForm` の `BorderStyle = bsNone`は、タイトルバーやサイズグリップなしの簡易表示を実現するのに最適です。しかし、`bsNone`を使うと通常のフォームのように「移動」や「サイズ変更」ができなくなります。

本記事ではこの原理を説明し、そのうえで、自動で透明なリサイズハンドルを追加する Delphi ユーティリティの実装を紹介します。

---

## BorderStyle = bsNone の特徴

`TForm.BorderStyle` には以下のような設定があります:

| 値 | 概要 |
|----|------|
| `bsNone` | 表示枠なし (タイトルバーやサイズグリップなし) |
| `bsSingle` | 固定サイズ |
| `bsSizeable` | サイズ変更可 |
| `bsToolWindow` | 表示枠つきの簡易表示 |

`bsNone` を利用すると、自由なデザインの実装や、表示スペースの最適化が可能になります。しかし、以下のような制限も生まれます:

- ドラッグしても移動しない
- サイズバーをドラッグしても変形しない

---

## bsNone で移動やサイズ変更を行う方法

これ自体はそこまで難しいことではありません。

```delphi
procedure TForm1.FormMouseDown(...);
begin
  ReleaseCapture;
  SendMessage(Handle, WM_SYSCOMMAND, SC_MOVE + HTCAPTION, 0);
end;
```

や

```delphi
procedure TForm1.FormMouseDown(...);
begin
  ReleaseCapture;
  SendMessage(Handle, WM_SYSCOMMAND, SC_SIZE + 4, 0); // 左上
end;
```

のような命令で移動やサイズ変更は実現できます。

---

## 問題: 他のコンポーネントにイベントが吸われる

Formのクライアント部分でなら、上記のようなコードで問題なく動作します。しかし、PanelやFrameなどのコンテナの上に合わせると、マウスイベントはそのコンポーネントに吸われ、Formへの通知がされません。

それを解決するために、利用者側でコンポーネント毎に同様の処理を書く必要がありました。

---

## 解決策: ResizeEdges.pas

その迷問を解決するのが、本記事で提供する `ResizeEdges.pas` です。

- transparent なリサイズ/移動ハンドルを各方向に追加
- `AttachEdges` と `AdjustEdges` だけで完結
- 配置先は Form だけでなく Panel、Frame などにも対応
- 処理はすべて親 Form に送信
- 利用者が「どの方向を有効にするか」を指定可能

```ResizeEdges.pas
{
  ResizeEdges.pas

  ■ 概要:
    透明なサイズ変更および移動用エッジを任意の TWinControl に追加するユーティリティ。
    bsNone のフォームや Align 設定されたパネル・フレームにも対応可能。

  ■ 設計思想:
    - UI に干渉しない「完全透明なハンドル」を提供（TLabel + Transparent）
    - 上下左右のサイズ変更方向は開発者が明示的に指定できる（TResizeDirectionSet）
    - 対象が Panel や Frame であっても GetParentForm を使って正しく Form にメッセージ送信
    - 四隅の斜めリサイズは方向の組み合わせから自動で生成
    - 手動でレイアウト制御する設計を前提とし、Align による自動リサイズとの干渉を避ける

  ■ 使用上の注意:
    - AttachEdges 実行後、親コントロールのサイズを変更した際は AdjustEdges を必ず呼び出すこと
    - Align が alBottom などの場合、対応方向に注意。rdTop などは指定しないこと
    - 上方向のサイズ変更と移動ハンドルは領域が重なるため同時に指定しないことを推奨
    - 移動可能エリアの幅と高さは MoveHandleWidth / MoveHandleHeight により変更可能

  ■ 利用例:
    TResizeEdgeHelper.AttachEdges(MyPanel, 10, [rdLeft, rdRight, rdBottom, rdMove]);
    MyPanel.SetBounds(...);
    TResizeEdgeHelper.AdjustEdges(MyPanel);

  ■ 最終更新:
    2024-05-12 設計確定／方向指定対応版／斜め自動生成対応
}

unit ResizeEdges;

interface

uses
  System.Classes, System.SysUtils, Winapi.Windows, Winapi.Messages,
  Vcl.Controls, Vcl.ExtCtrls, Vcl.Graphics, Vcl.Forms, Vcl.StdCtrls;

type
  TResizeDirection = (rdTop, rdBottom, rdLeft, rdRight, rdMove);
  TResizeDirectionSet = set of TResizeDirection;

  TResizeEdgeHelper = class
  private
    class var FHandleSize: Integer;
    class var FMoveHandleWidth: Integer;
    class var FMoveHandleHeight: Integer;
  public
    class property HandleSize: Integer read FHandleSize write FHandleSize;
    class property MoveHandleWidth: Integer read FMoveHandleWidth write FMoveHandleWidth;
    class property MoveHandleHeight: Integer read FMoveHandleHeight write FMoveHandleHeight;

    class procedure AttachEdges(Target: TWinControl; EdgeSize: Integer = 5;
      Directions: TResizeDirectionSet = [rdTop, rdBottom, rdLeft, rdRight, rdMove]);
    class procedure AdjustEdges(Target: TWinControl);
  end;

implementation

type
  TResizeEdgeLabel = class(TLabel)
  private
    FDirection: Integer;
    procedure EdgeMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
  public
    constructor CreateEdge(AOwner: TComponent; Direction: Integer);
    procedure AdjustPosition;
  end;

{ TResizeEdgeLabel }

constructor TResizeEdgeLabel.CreateEdge(AOwner: TComponent; Direction: Integer);
begin
  inherited Create(AOwner);
  Parent := TWinControl(AOwner);
  FDirection := Direction;

  Caption := '';
  Transparent := True;
  Color := clNone;
  Visible := True;
  Enabled := True;
  Align := alNone;

  AdjustPosition;

  case FDirection of
    1, 2: Cursor := crSizeNS;
    3, 4: Cursor := crSizeWE;
    5: Cursor := crSizeAll;
    6: Cursor := crSizeNWSE;
    7: Cursor := crSizeNESW;
    8: Cursor := crSizeNESW;
    9: Cursor := crSizeNWSE;
  end;

  OnMouseDown := EdgeMouseDown;
  BringToFront;
end;

procedure TResizeEdgeLabel.AdjustPosition;
begin
  if not Assigned(Parent) then Exit;

  with TResizeEdgeHelper do
    case FDirection of
      1: SetBounds(0, 0, Parent.Width, HandleSize);
      2: SetBounds(0, Parent.Height - HandleSize, Parent.Width, HandleSize);
      3: SetBounds(0, 0, HandleSize, Parent.Height);
      4: SetBounds(Parent.Width - HandleSize, 0, HandleSize, Parent.Height);
      5: begin
        Left := HandleSize;
        Top := 0;
        Width := Parent.Width - HandleSize * 2;
        Height := MoveHandleHeight;
      end;
      6: SetBounds(0, 0, HandleSize, HandleSize);
      7: SetBounds(Parent.Width - HandleSize, 0, HandleSize, HandleSize);
      8: SetBounds(0, Parent.Height - HandleSize, HandleSize, HandleSize);
      9: SetBounds(Parent.Width - HandleSize, Parent.Height - HandleSize, HandleSize, HandleSize);
    end;
end;

procedure TResizeEdgeLabel.EdgeMouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
const
  DirToSC: array[1..9] of Integer = (
    SC_SIZE + 3, SC_SIZE + 6, SC_SIZE + 1, SC_SIZE + 2, SC_MOVE + HTCAPTION,
    SC_SIZE + 4, SC_SIZE + 5, SC_SIZE + 7, SC_SIZE + 8
  );
var
  FormHandle: HWND;
begin
  if Button = mbLeft then
  begin
    ReleaseCapture;
    FormHandle := GetParentForm(Self).Handle;
    if FormHandle <> 0 then
      SendMessage(FormHandle, WM_SYSCOMMAND, DirToSC[FDirection], 0);
  end;
end;

{ TResizeEdgeHelper }

class procedure TResizeEdgeHelper.AttachEdges(Target: TWinControl; EdgeSize: Integer;
  Directions: TResizeDirectionSet);
begin
  if rdTop in Directions then
    TResizeEdgeLabel.CreateEdge(Target, 1);
  if rdBottom in Directions then
    TResizeEdgeLabel.CreateEdge(Target, 2);
  if rdLeft in Directions then
    TResizeEdgeLabel.CreateEdge(Target, 3);
  if rdRight in Directions then
    TResizeEdgeLabel.CreateEdge(Target, 4);

  // 斜めリサイズは組み合わせで判断
  if (rdTop in Directions) and (rdLeft in Directions) then
    TResizeEdgeLabel.CreateEdge(Target, 6);
  if (rdTop in Directions) and (rdRight in Directions) then
    TResizeEdgeLabel.CreateEdge(Target, 7);
  if (rdBottom in Directions) and (rdLeft in Directions) then
    TResizeEdgeLabel.CreateEdge(Target, 8);
  if (rdBottom in Directions) and (rdRight in Directions) then
    TResizeEdgeLabel.CreateEdge(Target, 9);

  if rdMove in Directions then
    TResizeEdgeLabel.CreateEdge(Target, 5);
end;

class procedure TResizeEdgeHelper.AdjustEdges(Target: TWinControl);
var
  I: Integer;
begin
  for I := 0 to Target.ControlCount - 1 do
    if Target.Controls[I] is TResizeEdgeLabel then
      TResizeEdgeLabel(Target.Controls[I]).AdjustPosition;
end;

initialization
  TResizeEdgeHelper.FHandleSize := 10;
  TResizeEdgeHelper.FMoveHandleWidth := 80;
  TResizeEdgeHelper.FMoveHandleHeight := 10;

end.

```

### 使用例

```delphi
TResizeEdgeHelper.AttachEdges(MyPanel, 10, [rdLeft, rdRight, rdBottom, rdMove]);
TResizeEdgeHelper.AdjustEdges(MyPanel);
```

---

## 再利用性

- `.pas` としてプロジェクトに追加するだけ
- 必要な方向だけを指定
- Align を利用した設計も可

---

## 本ユニットの作成背景

`ResizeEdges.pas`は OpenAIの ChatGPT（本記事筆者との実際の対話）によって設計・作成されました。

設計者側では:

- 移動やリサイズの自然な振る舞い
- 不要な方向の削除
- Formだけでなくパネル対応

などを重視して、最終的に非常に役立つ部品にまとまりました。

---

## おわりに

「フォームに移動とサイズ変更をつけたいだけ」と思った簡単な判断に、思ったより深い原因と必要な配慮が隠れていました。

この部品を使うことで、bsNone を使用したカスタム UI の設計に自信を持てるようになるはずです。