6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

DelphiAdvent Calendar 2023

Day 14

[Delphi] FMX TListBox で Drag & Drop でアイテムの位置を変えたい

Last updated at Posted at 2023-12-15

はじめに

ListBox の Item を Drag & Drop して順番を変えたいよー!

Windows / macOS の場合

TListBox.AllowDrag プロパティ を True にするだけ!!

すると次のように Drag & Drop できようになります。

お気づきになっただろうか…?

1度選択したアイテムじゃ無いとドラッグできない!
MouseDown と同時にドラッグしてもドラッグできない!

MouseDown と同時にドラッグすると選択 Item が変わるとか誰得なの~
しかも、Andorid / iOS だと AllowDrag が True でもドラッグできないの!

MouseDown と同時にドラッグできるようにする

MouseDown して一定以上ドラッグするとドラッグになり、一定を超えなければ通常の選択動作になるようにしたい。

これを実現する TDraggableItemListBox を作成します。

1. IFMXDragDropSrvice があるかどうか取得する

IFMXDragDropService は、Windows / macOS で提供されているサービスでこれが使える場合、AllowDrag が効きます。
なので、最初にこれが取得できるかを見て、使える場合 FDraggable を True にします。

constructor TDraggableItemListBox.Create(AOwner: TComponent);
begin
  inherited;

  AllowDrag := True;
  FMoveThreashold := MOVE_THREASHOLD; // 閾値

  // AllowDrag が効くか
  FDraggable :=
    TPlatformServices.Current.SupportsPlatformService(IFMXDragDropService);
end;

2. MouseDown でのドラッグ動作を禁止する

前述の1度選択したものをドラッグできる仕組みは TCustomListBox.MouseDown で実装されています。
この動作をしてほしくないので MouseDown でのドラッグ操作を禁止します。
具体的には MouseDown を override して inherited を呼ぶ時に AllowDrag に False を指定します。
これで MouseDown ではドラッグ動作が発生しなくなりました。

procedure TDraggableListBox.MouseDown(AButton: TMouseButton; AShift: TShiftState; AX, AY: Single);
begin
  // AllowDrag が有効な場合
  if FDraggable then
  begin
    AllowDrag := False;
    try
      inherited;
    finally
      AllowDrag := True;
    end;
  end
  else
    inherited;

  // Android / iOS でドラッグ中を示すフラグを降ろす
  FMobileDragging := False;
end;

3. MouseMove でドラッグさせる

MouseMove で一定の閾値を超えて動いた時、Drag & Drop を動作させます。
そのために、TCustomListBox.MouseMove を override し、以下のように書き換えます。

procedure TDraggableListBox.MouseMove(AShift: TShiftState; AX, AY: Single);
begin
  inherited;

  // LButton が押されている &
  // 押されている Item がある &
  // モバイルでドラッグ中 or 閾値以上動いたか
  if
    Pressed and
    (ItemDown <> nil) and
    (
      (FMobileDragging) or
      (PressedPosition.Distance(PointF(AX, AY)) > FMoveThreashold)
    )
  then
  begin
    // MouseDown されている Item を示す ItemDown プロパティをローカル変数に入れる
    //(入れなくても良い)
    var Item := ItemDown;

    // 以前の Index を覚えて置く
    var OldIndex := Item.Index;

    if FDraggable then
    begin
      // AllowDrag が有効な場合 (Windows / macOS)

      // アニメーション中なら止める
      AniCalculations.MouseLeave;

      // Drag & Drop 開始
      try
        var Screenshot := Item.MakeScreenshot;
        try
          // IRoot の BeginInternalDrag メソッドを呼ぶとドラッグ動作が始まる
          Root.BeginInternalDrag(Selected, Screenshot);
        finally
          Screenshot.Free;
        end;
      finally
        // BeginInternalDrag の中で既に MouseUp されてしまっているため
        // MouseDown - MouseUp のペアが正しくなるように、ここで強制的に呼ぶ
        MouseUp(TMouseButton.mbLeft, AShift, AX, AY);
      end;
    end
    else
    begin
      // AllowDrag が無効な場合 (Android / iOS)

      // 現在位置に Item があるか?
      // あった場合、ItemDown と同じか?
      var Target := ItemByPoint(AX, AY);
      if (Item = Target) or (Target = nil) then
        Exit;

      // ドラッグ中フラグを立てて連続的に MouseMove に入るようにする
      FMobileDragging := True;

      // 現在位置の Item と MouseDown 時の Item を入れ替える
      var TargetSelected := Target.IsSelected;
      Content.Exchange(Item, Target);
      SelectionController.SetSelected(Item, True);
      SelectionController.SetSelected(Target, TargetSelected);
    end;

    // 古い Index と新しい Index が別の物だったらイベントを呼ぶ
    var NewIndex :=  Item.Index;
    if (OldIndex <> NewIndex) and Assigned(FOnDropItem) then
      FOnDropItem(Self, Item, OldIndex, NewIndex);
  end;
end;
  • TControl.Pressed は、現在マウスの左ボタンが押されているかどうかを返すプロパティ
  • TControl.PressedPosition は、MouseDown 時の位置が入っているプロパティ
  • ItemDownは MouseDown した時に Mouse の下にあった Item を表すプロパティ (範囲外なら nil です)
  • MakeScreenshot は、コントロールのスクリーンショットを TBitmap として返すメソッド

windows / macOS の解説

ここで重要なのは IRoot.BeginInternalDrag です。

このメソッドを呼ぶとデフォルトのドラッグ動作が始まり、ドロップ(MouseUp)するまで返ってきません。
デフォルトのドラッグ動作はコントロールによって違いますが、TListBox の場合はアイテムをドラッグして順番を並べ替える動作です。

つまるところ、MouseDown で呼んでいた IRoot.BeginInternalDrag を MouseMove に移動させただけです。

これで Windows / macOS では目的の動作が実装できました。

Android / iOS の解説

Android / iOS の場合は、MouseMove 時に指の下にある Item と MouseDown 時に指の下にあった Item を随時入れ替えることで Drag を実現しています。

実行動画

MouseDown してそのままドラッグできてる~!

ソース

全ソースコードは↓の Gist にあります。
https://gist.github.com/freeonterminate/b57c2f6f9f7f62782ed24c53e6cb34be

最後に

大量のアイテムを並べ替える必要がある場合 or モバイルでも並べ替えたい場合に有効です。

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?