【Flutter】アプリのウィンドウにファイルをドラッグアンドドロップする【desktop_drop】

ネコニウム研究所

PCを利用したモノづくりに関連する情報や超個人的なナレッジを掲載するブログ

【Flutter】アプリのウィンドウにファイルをドラッグアンドドロップする【desktop_drop】

2024-6-11 | ,

Flutterのパッケージdesktop_dropを使ってアプリのウィンドウにファイルをドラッグアンドドロップしたい!

概要

今回の記事では、Flutterのパッケージdesktop_dropを使ってアプリのウィンドウにファイルをドラッグアンドドロップする手順を掲載する。

仕様書

環境

  • Android Studio Giraffe | 2023.2.1 Patch 2
  • Flutter 3.19.6
  • desktop_drop 0.4.4

手順書

インストール編とコード編の2部構成です。

インストール編

パッケージdesktop_dropと併せてパッケージcross_fileも必要になるので一緒にインストールする。

ターミナルでコマンドを実行するか

flutter pub add desktop_drop cross_file

pubspec.yamldependencies:に下記のような感じで追加して

dependencies:
  desktop_drop: ^0.4.4
  cross_file: ^0.3.4+1

ターミナルでflutter pub getする。

flutter pub get

コード編

下記の過去の記事に載せたテキストファイルの入出力する例に

ファイルをウィンドウにドラッグアンドドロップすると最初のファイルの内容をTextFieldに表示するようにした例。

import 'dart:io';
import 'package:cross_file/cross_file.dart';
import 'package:file_picker/file_picker.dart';
import 'package:desktop_drop/desktop_drop.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';

void main() {
  runApp(
    MaterialApp(
      home: Scaffold(
        body: const FilePickerSample1()
      )
    )
  );
}

class SaveTextIntent extends Intent {}
class OpenTextIntent extends Intent {}

class FilePickerSample1 extends StatelessWidget {
  FilePickerSample1({super.key});

  TextEditingController textEditingController = TextEditingController();

  List<XFile> droppedFiles = [];
  bool dragging = false;
  Offset? offset;

  void saveText() async {
    String? outputFile = await FilePicker.platform.saveFile(
      dialogTitle: 'Please select an text file:',
      fileName: 'output.txt',
    );

    if (outputFile == null) {
      return;
    }

    final file = File(outputFile);
    await file.create();
    await file.writeAsString(textEditingController.text);
  }

  void loadText({String filePath = ""}) async {
    FilePickerResult? result;

    if (filePath == "") {
      result = await FilePicker.platform.pickFiles(
        type: FileType.custom,
        allowedExtensions: ['txt'],
      );

      if (result != null) {
        filePath = result.files.single.path!;
      } else {
        return;
      }
    }

    File file = File(filePath);
    if (await file.exists()) {
      final data = await file.readAsString();
      textEditingController.text = data;
    }
  }

  @override
  Widget build(BuildContext context) {
    return DropTarget(
      onDragDone: (detail) async {
        droppedFiles.addAll(detail.files);

        debugPrint('onDragDone:');

        if (detail.files.isNotEmpty) {
          loadText(filePath: detail.files[0].path);
        }

        // Debug用
        for (final file in detail.files) {
          debugPrint('${file.path} ${file.name}'
              '  ${await file.lastModified()}'
              '  ${await file.length()}'
              '  ${file.mimeType}');
        }
      },
      onDragUpdated: (details) {
        offset = details.localPosition;
        debugPrint('x:${offset!.dx}, y:${offset!.dy}');
      },
      onDragEntered: (detail) {
        dragging = true;
        offset = detail.localPosition;
        debugPrint('x:${offset!.dx}, y:${offset!.dy}');
      },
      onDragExited: (detail) {
        dragging = false;
        offset = null;
      },
      child: FocusableActionDetector(
        autofocus: true,
        shortcuts: <ShortcutActivator, Intent>{
          LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyS):
              SaveTextIntent(),
          LogicalKeySet(LogicalKeyboardKey.control, LogicalKeyboardKey.keyO):
              OpenTextIntent(),
        },
        actions: {
          SaveTextIntent: CallbackAction<SaveTextIntent>(
            onInvoke: (intent) => saveText(),
          ),
          OpenTextIntent: CallbackAction<OpenTextIntent>(
            onInvoke: (intent) => loadText(),
          ),
        },
        child: SafeArea(
          child: Center(
            child: Column(children: [
              Row(children: [
                IconButton(
                  icon: const Icon(Icons.upload),
                  onPressed: () {
                    loadText();
                  },
                ),
                IconButton(
                  icon: const Icon(Icons.download),
                  onPressed: () {
                    saveText();
                  },
                ),
              ]),
              const SizedBox(height: 8),
              Expanded(
                //width: double.infinity,
                child: TextField(
                  decoration: const InputDecoration(
                    border: OutlineInputBorder(),
                    labelText: 'テキスト',
                    floatingLabelBehavior: FloatingLabelBehavior.always,
                  ),
                  controller: textEditingController,
                  maxLines: null,
                  expands: true,
                  textAlignVertical: TextAlignVertical.top,
                ),
              ),
            ]),
          ),
        ),
      ),
    );
  }
}

複数のファイルをドラッグアンドドロップした場合もTextFieldには先頭のファイルの内容のみ表示されるんだけども、デバッグのためにドラッグアンドドロップされたファイルの情報を全てコンソールに出力してる。

localPositionはファイルがドロップされたウィンドウの座標。ウィンドウの左上が0

まとめ(感想文)

それっぽいデスクトップアプリの動作になってきた感じ。

引用・参考文献

下記の記事を参考にさせていただきました。ありがとうございました。