Drag and drop - przeciąganie plików

michalkopacz

Mechanizm drag and drop (z ang. przeciągnij i upuść) to bardzo wygodny sposób przenoszenia obiektów w systemie Windows. Obsługa drag and drop polega na chwyceniu obiektu kursorem myszy, a następnie na przeciągnięciu go w pożądane miejsce i upuszczeniu.

1 Interfejs aplikacji
2 Jak to działa?
3 Włączamy obsługę drag and drop
4 Komunikat WM_DROPFILES
5 Obsługa komunikatów przez Delphi
6 Uzyskiwanie informacji o plikach
7 Złóżmy to w całość

Biblioteka VCL wspiera metodę drag and drop w obrębie danej aplikacji, definiując mechanizm drag and drop pomiędzy kontrolkami. Ten sposób obsługi mechanizmu drag and drop jest dokładnie opisany w pomocy Delphi, dlatego też zajmiemy się mniej znanym sposobem obsługi drag and drop. W tym celu sięgniemy do WinAPI i napiszemy program umożliwiający przeciąganie plików do okna aplikacji.

Interfejs aplikacji

Istniej wiele programów pozwalających użytkownikowi otworzyć plik poprzez przeciągniecie go do okna aplikacji. My również dodamy taką opcję do naszego programu, dzięki czemu użytkownik będzie mógł przeciągnąć plik tekstowy, a my wczytamy go do komponentu TMemo. Tworzymy zatem nowy projekt, wybierając z menu File|New|VCL Forms Application ? Delphi for Win32. Dodajemy na formularz komponent TMemo, zmieniamy jego nazwę na DragMemo, właściwośćAlign na alClient oraz kasujemy zawartość właściwości Lines.

Zmieniamy także właściwości Name i Caption formularza odpowiednio na DragForm i Drag and Drop Notebook. Na koniec zapisujemy projekt pod nazwą DragNotebook.bdsproj, a moduł jako UnitDrag.pas.

Jak to działa?

Istnieją dwa sposoby na dodanie obsługi mechanizmu drag nad drop do naszej aplikacji. Pierwszy polega na wykorzystaniu funkcji WinApi i obsłużeniu komunikatu WM_DROPFILES. Druga metoda związana jest z obiektem COM, który udostępnia odpowiednie interfejsy. My skorzystamy z pierwszego, łatwiejszego sposobu.

Należy zauważyć, że obydwa sposoby umożliwiają jedynie przeciąganie plików. W dodatku nasz program może akceptować upuszczanie plików z Eksploratora Windows, jednak my nie możemy przenosić plików do innej aplikacji. Pomimo tego ograniczenia, użycie drag and drop do przeciągania plików do aplikacji jest bardzo użyteczne.

Przeciąganie plików metodą drag and drop umożliwiają nam, zdefiniowane w bibliotece shell32.dll, funkcje DragQueryFile i DragQueryPoint, procedury DragAcceptFiles i DragFinish oraz komunikat WM_DROPFILES. W Delphi komunikat ten jest zdefiniowany w module Messages, a funkcje API są zadeklarowane w module ShellApi. Dodaj zatem moduł ShellApi do listy uses (moduł Messages już tam się znajduje).

Włączamy obsługę drag and drop

Mechanizm drag and drop jest domyślnie wyłączony w aplikacji. Aby go włączyć użyjemy procedury DragAcceptFiles. W zdarzeniu OnCreate formularza umieszczamy taki kod:

DragAcceptFiles(Self.Handle, True);

Procedura DragAcceptFiles przyjmuje dwa parametry. Pierwszy parametr to uchwyt okna, dla którego włączamy lub wyłączamy akceptacje drag and drop. Wartość Self.Handle odpowiada uchwytowi formularza. Drugim parametrem jest wartość logiczna informująca o włączeniu (True) lub wyłączeniu (False) akceptacji mechanizmu drag and drop przez okno, którego uchwyt przypisaliśmy do pierwszego parametru.

Komunikat WM_DROPFILES

Kiedy pliki są upuszczane na formularzu, Windows wysyła do aplikacji komunikat WM_ DROPFILES. Komunikat ten, poprzez parametr Drop, dostarcza nam uchwyt do struktury przechowującej informację o zrzucanych plikach. Napiszemy teraz szkielet procedury obsługującej komunikat WM_ DROPFILES, a następnie dodamy do niej kod, który umieści zawartość pliku w komponencie TMemo.

Dodajemy najpierw do sekcji publicznej formularza deklaracje metody WMDropFiles:

procedure WMDropFiles(var Msg: TWMDropFiles); message WM_DROPFILES;

Następnie w sekcji implementation wpisujemy poniższy kod:

procedure TDragForm.WMDropFiles(var Msg: TWMDropFiles);
var
  hDrop: THandle; 
  FileCount: Integer; 
  FileNameLength: Integer;
  FileName: String;
begin
  hDrop:= Msg.Drop;
  // tu wpisujemy kod przetwarzający strukturę z nazwami plików 
end;

Do zmiennej hDrop typu THandle przypisujemy zawartość parametru Drop komunikatu. Tworzymy także trzy pomocnicze zmienne FileCount, FileName i FileNameLength, które wykorzystamy do przechowywania informacji o liczbie plików, poszcze-ólnych nazwach (dokładnie ścieżek dostępu wraz z nazwą) i długości tej nazwy. Jak zatem uzyskać ilość i nazwy przeciąganych plików? Do wykonania tych dwóch zadań służy funkcja DragQueryFile. Przyjrzyjmy się jej dokładniej.

Obsługa komunikatów przez Delphi

Komunikat WM_DROPFILES możemy obsłużyć w naszej aplikacji na dwa sposoby. Pierwszy sposób polega na wykorzystaniu metody obsługującej komunikat (ang. message handler) WM_DROPFILES, co zostało pokazane w poprzednim podpunkcie. Możemy również skorzystać ze zdarzenia OnMessage klasy TApplication przypisując mu procedurę obsługi w kodzie programu (alternatywnie oprogramować zdarzenie OnMessage komponentu TApplicationEvents).

Do konstruktora formularza wpisujemy kod:

procedure TDragForm.FormCreate(Sender: TObject);
begin
  Application.OnMessage:=ApplicationMessage;
  //włączamy akceptacje drag and drop
  DragAcceptFiles(Self.Handle, True);
end;

Napiszmy teraz metodę ApplicationMessage. Powinna ona pełnić jedynie funkcję dystrybutora
rozdzielającego obsługę przychodzących zdarzeń pomiędzy metodami w zależności od numeru komunikatu:

procedure TDragForm.ApplicationMessage(var Msg : TMsg; var Handled : Boolean);
begin
  case (Msg.message) of
    WM_DROPFILES: WMDropFiles(Msg);
  end;
end;

Oczywiście w tym przypadku zastosowanie polecenia switch/case jest przesadzone, swobodnie wystarczyłby
prosty warunek sprawdzający, czy nadchodzący komunikat jest tym, który nas interesuje, ale taka forma w
naturalny sposób umożliwia obsługę kolejnych komunikatów.

Pozostało nam napisanie medoty WMDropFiles. W sekcji implementation wpisujemy poniższy kod:

procedure TDragForm.WMDropFiles(var Msg: TMsg);
var
  hDrop: THandle; 
  FileCount: Integer; 
  FileNameLength: Integer;
  FileName: String;
begin
  hDrop:= Msg.wParam;
  // tu wpisujemy kod przetwarzający strukturę z nazwami plików 
end;

Przedstawianie alternatywnych sposobów obsługi komunikatów w artykule o drag and drop może wydawać się czynnością nie na miejscu. Opisałam jednak to drugie podejście aby ułatwić początkującym zrozumienie innych artykułów na ten temat, których autorzy skorzystali ze zdarzenia OnMessage. Natomiast w dalszej części artykułu będziemy korzystać z pierwszego sposobu obsługi komunikatów przez Delphi.

Uzyskiwanie informacji o plikach

Funkcja DragQueryFile ma następującą postać:

DragQueryFile(Drop: THandle; FileIndex: Integer; FileName: PChar; cb: Integer): Integer; 

Drop przyjmuje uchwyt do struktury plików, czyli wartość parametru Drop komunikatu WM_DROPFILES. Do parametru FileIndex przypisujemy indeks pliku do którego chcemy się odwołać. Trzeci parametr FileName wskazuje na bufor, do którego zostaje przypisana nazwa pliku o indeksie zawartym w parametrze FileIndex. Ostatni parametr, cb, zawiera rozmiar, w znakach, bufora przypisanego do parametru FileName. W zależności od wartości przekazanych parametrów funkcja zwraca jedną z podanych trzech wartości:

1. liczbę opuszczonych plików

Aby uzyskać tę informacje należy przypisać do parametrów następujące wartości:
*FileIndex: $FFFFFFFF;
*FileName: nil;
*cb: 0;

Poniższy kod przypisuje do zmiennej FileCount ilość plików:

FileCount:= DragQueryFile(hDrop, $FFFFFFFF, nil, 0);

2. rozmiar bufora, który przechowuje nazwę (wraz z pełną ścieżką) pliku wynaczonego przez wartość parametru FileIndex

Aby uzyskać tę informacje należy przypisać do parametrów następujące wartości:
*FileIndex: indeks pliku, dla którego zostanie zwrócony rozmiar bufora;
*FileName: nil;
*cb: 0;

Zobaczmy fragment kodu, który zwraca rozmiar bufora, podając ilość jego znaków:

FileNameLength:=DragQueryFile(hDrop, 0, nil, 0);

3. nazwę pliku wraz ze ścieżką dostępu

Aby uzyskać tę informacje należy przypisać do parametrów następujące wartości:
*FileIndex: indeks pliku, dla którego zostanie zwrócona nazwa pliku;
*FileName: bufor do którego przypiszemy nazwę pliku ;
*cb: rozmiar bufora;

Aby uzyskać nazwę pliku należy wpisać poniższy kod:

SetLength(FileName, FileNameLength);
DragQueryFile(hDrop, 0, PChar(FileName), FileNameLength + 1); 

W powyższym fragmencie parametrowi FileIndex przypisaliśmy wartość 0, czyli indeks pierwszego pliku. Z tej wersji funkcji korzystamy wtedy, kiedy utworzyliśmy zmienną FileName typu String. Długi łańcuch zawiera tzw. zerowy ogranicznik, który jest wymagany przez funkcje WinApi. Możesz również spotkać się z przykładami, w których zmienna FileName zadeklarowana jest jako tablica typu Char. W takiej sytuacji nie musisz uzyskiwać rozmiaru bufora poprzez tą wersje funkcji DragQueryDrop, która to umożliwia, wystarczy, że skorzystasz z funkcji sizeof, lub wpiszesz dowolnie dużą liczbę (tak aby zmieściła się ilość znaków parametru FileName), jednak ten sposób wydaje się mało elegancki i może spowodować błąd.

Jeżeli zmienna FileName była typu tablicą typu Char, wpisujemy poniższy kod:

//Najpierw tworzymy tablice
//znaków
var
  FileName: array[0..MAX_PATH] of  Char;

//między begin i end
DragQueryFile(hDrop, 0, PChar(FileName),sizeof(FileName));
//albo
DragQueryFile(hDrop, 0, PChar(FileName), 500);

Jeżeli chcesz odczytać nazwy wszystkich plików musisz utworzyć pętle, która może wyglądać np tak:

var
  I: Integer;

...

for i:= 0 to FileCount ? 1 do
begin
  FileNameLength:= DragQueryFile(hDrop, i, nil, 0);
  SetLength(FileName, FileNameLength);
  DragQueryFile(hDrop, i, PChar(FileName), FileNameLength + 1);
  //teraz możemy skorzystać ze   
  //zmiennej FileName
end;

Złóżmy to w całość

Najwyższy czas wykorzystać zdobyte wiadomości i dokończyć tworzenie naszego notatnika. Poniżej znajduje się kod modułu UnitDrag.pas, który został dla ułatwienia analizy, opatrzony komentarzami.

unit UnitDrag;

interface

uses
  Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
  Dialogs, StdCtrls, ShellAPI;

type
  TDragForm = class(TForm)
    DragMemo: TMemo;
    procedure FormClose(Sender: TObject; var Action: TCloseAction);
    procedure FormCreate(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
    //deklaracja medoty obsługującej komunikat WM_DROPFILES
    procedure WMDropFiles(var Msg: TWMDropFiles); message WM_DROPFILES;
  end;

var
  DragForm: TDragForm;

implementation

{$R *.dfm}

procedure TDragForm.WMDropFiles(var Msg: TWMDropFiles);
var
  hDrop: THandle; //uchwyt do struktury plików
  FileCount: Integer; //ilość opuszczanych plików
  FileNameLength: Integer;   //długość nazwy pliku
  FileName: String;  //nazwa pliku
begin
  //przypisujemy zawartość parametru Drop komunikatu
  hDrop:= Msg.Drop;
  //ilość opuszczanych plików
  //blok try...finally powoduje, że uchwyt zostanie zawsze zniszczony
  try
    //odczytujemy ilość opuszczanych plików
    FileCount:= DragQueryFile(hDrop, $FFFFFFFF, nil, 0);
    //możemy maksymalni otworzyć tylko jeden plik
    if FileCount > 1 then
      ShowMessage('Można otworzyć tylko jeden plik!' )
    else
    begin
      //ustalamy długość nazwy pliku
      FileNameLength:= DragQueryFile(hDrop, 0, nil, 0);
      //SetLength przypisuje do zmiennej FileName rozmiar bufora
      //(ilość znaków nazwy plików) i znak #0 (ogranicznik zerowy)
      SetLength(FileName, FileNameLength);
      //odczytujemy nazwę pliku
      DragQueryFile(hDrop, 0, PChar(FileName), FileNameLength + 1);
      //badamy, czy plik jest plikiem tekstowym
      if ExtractFileExt(FileName) <> '.txt' then
        ShowMessage('Dany plik nie jest plikiem tekstowym!')
      else
        //wczytujemy do kontrolki zawartośc pliku
        DragMemo.Lines.LoadFromFile(FileName);
    end;
  finally
    //niszczymy uchwyt
    DragFinish(hDrop);
  end;
end;

procedure TDragForm.FormClose(Sender: TObject; var Action: TCloseAction);
begin
  //wyłączamy akceptacje drag and drop
  DragAcceptFiles(Self.Handle, False);
end;

procedure TDragForm.FormCreate(Sender: TObject);
begin
  //włączamy akceptacje drag and drop
  DragAcceptFiles(Self.Handle, True);
end;

end.

1 komentarz

Dziwne, twoja funkcja działa mi pięknie na TPanel użyłem jej aby pobierać ścieżkę do upuszczonego pliku niestety co dziwne jeśli program na Win7 jest odpalony z prawami admina to nie działa, upuszczanie plików nie daj żadnej reakcji.