Jacek Matulewski http://www.phys.uni.torun.pl/~jacek/ Tworzenie aplikacji Windows Mechanizm drag n drop Ćwiczenia Toruń, 12 grudnia 2002 Najnowsza wersja tego dokumentu znajduje się pod adresem http://www.phys.uni.torun.pl/~jacek/dydaktyka/rad/rad4_dnd.pdf Źródła opisanych w tym dokumencie programów znajdują się pod adresem http://www.phys.uni.torun.pl/~jacek/dydaktyka/rad/rad4_dnd.zip 1
I. Spis treści I. Spis treści... 2 II. Przenoszenie elementów w obrębie aplikacji... 3 1. Przenoszenie między listami... 3 2. Ukłon w stronę elegancji programowania... 5 3. Przenoszenie wielu elementów... 7 III. Obsługa pliku rzuconego na formę z zewnętrznej aplikacji... 9 2
II. Przenoszenie elementów w obrębie aplikacji Mechanizm Drag & Drop ( d&d ) jest bardzo charakterystycznym elementem Windows, pozwalającym na elastyczne wykorzystanie myszy (lub innego urządzenia wskazującego) do złapania elementu znajdującego się na ekranie, przeniesienie go w inne miejsce i wrzucenie go do nowego pojemnika lub zrzucenie na inny element graficzny. Obiekty VCL Borlanda w pełni wspierają mechanizm drag & drop w obrębie aplikacji, jednak do komunikacji pomiędzy aplikacjami (np. przenoszenie plików) niezbędne jest odwołanie się do mechanizmu komunikatów i WinAPI. 1. Przenoszenie między listami Przygotujmy formę zawierającą dwa ListBoxy: W listboxach umieszczamy elementy ręcznie (własność Items) lub pętlą umieszczoną w konstruktorze formy: fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) for(int i=0;i<10;i++) ListBox1->Items->Add((AnsiString)"Lista 1 : "+i); ListBox2->Items->Add((AnsiString)"Lista 2 : "+i); procedure TForm1.FormCreate(Sender: TObject); var i :Integer; for i:=0 to 9 do ListBox1.Items.Add('Lista 1 : '+IntToStr(i)); ListBox2.Items.Add('Lista 2 : '+IntToStr(i)); 3
Operacja przeniesienia i upuszczenia składa się z trzech etapów: 1) Rozpoczęcie przenoszenia. W zależności od ustawienia własności DragMode może się to odbywać automatycznie po złapaniu elementu lub poprzez wywołanie metody BeginDrag (ręczne wywołanie pozwala np. na filtrowanie elementów, które mogą być przenoszone). 2) Pozostałe elementy aplikacji mogą akceptować lub odrzucać możliwość upuszczenia przenoszonego elementu umożliwia to zdarzenie OnDragOver, które jest wskaźnikiem do funkcji zawierającym referencję do zmiennej logicznej (domyślnie Accept). Jeżeli w metodzie zdarzeniowej Accept zostanie ustalone na False, przenoszony obiekt nie będzie mógł być zrzucony na ten obiekt, którego dotyczy zdarzenie, co manifestuje się zmianą kształtu kursora myszy. 3) Ostatnim etapem jest reakcja na upuszczenie przenoszonego elementu na obiekt. W takiej sytuacji uruchamiana jest metoda związana ze zdarzeniem OnDragDrop. Ad. 1. Rozpoczęcie przenoszenia Po naciśnięciu lewego klawisza myszy na ListBox1 rozpoczynamy operację przenoszenia wyróżnionego elementu 1 (możliwość wyboru większej ilości elementów powinna być, póki co, zablokowana). Wykorzystamy zdarzenie OnMouseDown: void fastcall TForm1::ListBox1MouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y) if (Button==mbLeft && ListBox1->ItemIndex>=0) ListBox1->BeginDrag(false); procedure TForm1.ListBox1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); if (Button=mbLeft) and (ListBox1.ItemIndex>=0) then ListBox1.BeginDrag(false); Jak widać metoda wykorzystuje explicite nazwy obiektów nie jest to korzystne, ponieważ utrudnia zastosowanie tej metody do innych obiektów lub do kopiowania w drugą stronę z ListBox2 do ListBox1. W kolejnym paragrafie do identyfikacji pojemnika źródła i pojemnika celu pomiędzy którymi przenoszone są elementy wykorzystamy argumenty metod zdarzeniowych. Mimo to metoda ta bardzo dobrze ilustruje istotę tego etapu mechanizmu drag & drop. Istota metody jest bardzo prosta: jeżeli naciśnięto lewy klawisz myszy na ListBox1 i zaznaczony jest jakiś element (jeżeli żaden element nie jest zaznaczony własność ListBox1.ItemIndex miałaby wartość 1) to wywoływana jest metoda BeginDrag(). Argument tej funkcji decyduje, czy przenoszenie ma rozpocząć się natychmiast, czy dopiero po przesunięciu myszy. Po rozpoczęciu przeciągania elementu kursor myszy zmieni się na crdrag. Ad. 2. Akceptacja Jeżeli przenoszony element przesuniemy nad inny obiekt, wywoływana jest jego metoda zdarzeniowa związana ze zdarzeniem OnDragOver. Napiszmy odpowiednią metodę dla ListBox2: void fastcall TForm1::ListBox2DragOver(TObject *Sender, TObject *Source, int X, int Y, TDragState State, bool &Accept) Accept=false; if (Source==ListBox1 && ListBox1->ItemIndex>=0) Accept=true; 1 W warunku wywołania metody BeginDrag sprawdzamy czy ItemIndex jest większy od 1 (taka jest wartość tej własności, gdy żaden element nie jest zaznaczony). Warunek ten jest spełniony również wtedy gdy przy zaznaczonym elemencie klikniemy na listę poza umieszczonymi w niej elementami. Aby wykluczyć taką sytuację można również sprawdzać czy myszka jest nad jakimś elementem umieszczonym w liście korzystając z metody ItemAtPos(). 4
procedure TForm1.ListBox2DragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean); Accept:=false; if (Source=ListBox1) and (ListBox1.ItemIndex>=0) then Accept:=true; Metoda przyjmuje proste kryterium jeżeli przenoszony element pochodzi od ListBox1 oraz w ListBox1 jest zaznaczony jakiś element to element zostanie przyjęty. W pozostałych przypadkach zostanie odrzucony (wówczas kursor myszy ustalany jest na crnodrop). Ad. 3. Upuszczenie W momencie upuszczenia elementu wywoływana jest metoda zdarzeniowa OnDragDrop, w której użytkownik może zaprogramować dowolne działanie aplikacji my po prostu dodamy odpowiedni element na końcu ListBox2 i usuniemy go z ListBox1: void fastcall TForm1::ListBox2DragDrop(TObject *Sender, TObject *Source, int X, int Y) ListBox2->Items->Add(ListBox1->Items->Strings[ListBox1->ItemIndex]); ListBox1->Items->Delete(ListBox1->ItemIndex); procedure TForm1.ListBox2DragDrop(Sender, Source: TObject; X, Y: Integer); ListBox2.Items.Add(ListBox1.Items.Strings[ListBox1.ItemIndex]); ListBox1->Items->Delete(ListBox1->ItemIndex); Odczytujemy numer elementu z własności ListBox1.ItemIndex i dodajemy jego łańcuch do ListBox2 2, a następnie usuwamy z ListBox1. W tym etapie nie powinniśmy już sprawdzać czy dany obiekt może być upuszczony, to powinna robić funkcja związana ze zdarzeniem OnDragOver. Inaczej moglibyśmy mieć sytuację, w której pojawia się akceptujący kursor myszy, a elementu nie można byłoby przenieść. Na razie możliwości przenoszenia są dość skromne można przenosić tylko po jednym elemencie z lewej listy na prawą. W kolejnych paragrafach uelastycznimy metody w taki sposób, żeby można je było związać z dowolnym obiektem formy. Wszelkie informacje będziemy przekazywać poprzez argumenty metod związanych z operacją drag & drop bez korzystania explicite z nazw obiektów pomiędzy którymi przenosimy wybrany element. Wbrew pozorom to uprości, a nie skomplikuje kod metod. Poza tym umożliwimy przenoszenie większej ilości elementów jednocześnie. 2. Ukłon w stronę elegancji programowania 2 Przykład można nieco zmodyfikować tak, żeby element był dodawany do listy w miejscu upuszczenia. Wystarczy zamiast ListBox2.Items.Add() wykorzystać metody ListBox2.Items.Insert() oraz ListBox2.ItemAtPos(): TPoint Point=X,Y; ListBox2->Items->Insert(ListBox2->ItemAtPos(Point,false), ListBox1->Items->Strings[ListBox1->ItemIndex]); 5
Pierwszym argumentem wszystkich metod zdarzeniowych VCL jest wskaźnik do obiektu-nadawcy 3. W większości przypadków jest obiekt macierzysty metody. Wskaźnik jest do typu TObject, z którego dziedziczą wszystkie obiekty VCL można więc zrzutować go np. na typ klasy TListBox i wówczas odczytać jego własności i skorzystać z metod (np. ItemAtPos). Dla ułatwienia, żeby nie korzystać stale z rzutowania zdefiniujemy lokalną zmienną typu wskaźnika do klasy TListBox i przypiszemy jej adres przechowywany w zmiennej Sender: TListBox* SenderLB=(TListBox*)Sender; SenderLB:=Sender as TListBox; (z deklaracją var SenderLB :TListBox;) Nowy wskaźnik możemy wykorzystać tak, jakbyśmy korzystali z nazwy obiektu explicite, która w C++ Builderze oznacza w końcu też tylko wskaźnik do obiektu 4. Pozostała część metody, poza zamianą ListBox1 na SenderLB pozostaje niezmieniona: void fastcall TForm1::ListBox1MouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y) TListBox* SenderLB=(TListBox*)Sender; if (Button==mbLeft && SenderLB->ItemIndex>=0) SenderLB->BeginDrag(false); procedure TForm1.ListBox1MouseDown(Sender: TObject; Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var SenderLB :TListBox; SenderLB:=Sender as TListBox; if (Button=mbLeft) and (SenderLB.ItemIndex>=0) then SenderLB.BeginDrag(false); W tej chwili można sensownie przypisać tę metodę również do zdarzenia OnMouseDown drugiego listboxa (za pomocą Object Inspectora). Aby to miało większy sens musimy jednak zmodyfikować funkcję związaną z OnDragOver. Możemy albo zezwolić na upuszczanie zawsze lub na przykład na przenoszenie pomiędzy listami nie pozwalając na upuszczanie elementu na listę, z której jest podnoszony. W drugim przypadku wystarczy jedynie sprawdzić czy Sender i Source (nadawca komunikatu ukrytego za zdarzeniem i źródło przenoszonego elementu) nie przechowują adresu tego samego elementu: void fastcall TForm1::ListBox2DragOver(TObject *Sender, TObject *Source, int X, int Y, TDragState State, bool &Accept) Accept=false; 3 Nazwa tego argumentu Sender związana jest z ukrytego za mechanizmem zdarzeń mechanizmu komunikatów Windows. Nadawca, to obiekt, który jest źródłem komunikatu. 4 Niektórzy studenci dziwią się, że wskaźnik do TObject przechowuje te same informacje co TListBox, a przecież deklaracja tej klasy jest znacznie uboższa. Wskaźnik nie przechowuje żadnych informacji poza adresem obiektu, który dla wszystkich typów zmiennych i obiektów jest tego samego rozmiaru. Twierdzenie to nie dotyczy wskaźników własności i metod, które poza swoim bezpośrednim adresem muszą też przechowywać adres obiektu są więc dwukrotnie większe. 6
if (Source!=Sender) Accept=true; procedure TForm1.ListBox2DragOver(Sender, Source: TObject; X, Y: Integer; State: TDragState; var Accept: Boolean); Accept:=false; if (Source <> Sender) then Accept:=true; Teraz należy tę metodę podpiąć pod zdarzenie pierwszej listy ListBox1->OnDragOver. Można wówczas przenosić i upuszczać elementy również z listy prawej do lewej, ale nic się w takim przypadku nie dzieje, ponieważ nie obsługiwane jest jeszcze zdarzenie ListBox1->OnDragDrop. Za pomocą Object Inspectora zwiążmy również to zdarzenie i zmodyfikujmy metodę ListBox2DragDrop tak, żeby nie zawierała nazw obiektów. Teraz musimy zrzutować oba argumenty (Sender i Source), żeby móc skorzystać z metod dodającej i usuwającej element z listy: void fastcall TForm1::ListBox2DragDrop(TObject *Sender, TObject *Source, int X, int Y) TListBox* SenderLB=(TListBox*)Sender; TListBox* SourceLB=(TListBox*)Source; SenderLB->Items->Add(SourceLB->Items->Strings[SourceLB->ItemIndex]); SourceLB->Items->Delete(SourceLB->ItemIndex); procedure TForm1.ListBox2DragDrop(Sender, Source: TObject; X, Y: Integer); var SenderLB,SourceLB :TListBox; SenderLB:=Sender as TListBox; SourceLB:=Source as TListBox; SenderLB.Items.Add(SourceLB.Items.Strings[SourceLB.ItemIndex]); SourceLB.Items.Delete(SourceLB.ItemIndex); W następnym paragrafie zmodyfikujemy to pozwalając ponadto na przenoszenie wielu elementów jednocześnie. 3. Przenoszenie wielu elementów Aby umożliwić zaznaczanie wielu elementów należy włączyć własność MultiSelect w obu listach. Przy zaznaczonej większej liczbie elementów własność ItemIndex przestaje być użyteczna. Zaznaczone elementy będące odczytywane z tablicy Selected, która ma tę samą długość co Items i przechowuje wartości logiczne (true jeżeli odpowiedni element jest zaznaczony i false jeżeli nie): void fastcall TForm1::ListBox1MouseDown(TObject *Sender, TMouseButton Button, TShiftState Shift, int X, int Y) if (Button == mbleft) ((TListBox*)Sender)->BeginDrag(false); procedure TForm1.ListBox1MouseDown(Sender: TObject; 7
Button: TMouseButton; Shift: TShiftState; X, Y: Integer); var ItemPos :Integer; if (Button = mbleft) then (Sender as TListBox).BeginDrag(false); Zrezygnowałem z deklaracji wskaźnika SenderLB przy jednorazowym jego wykorzystaniu Metoda ListBox2DragOver pozostaje niezmieniona. W metodzie ListBox2DragDrop należy wykonać pętle po elementach i dla zaznaczonych wykonać operację przenoszenia. Dokładniej rzecz biorąc wykonane będą dwie oddzielne pętle pierwsza będzie przenosić elementy, a następna (wykonywana od ostatniego elementu do pierwszego) będzie je usuwać: void fastcall TForm1::ListBox2DragDrop(TObject *Sender, TObject *Source, int X, int Y) TListBox* SenderLB=(TListBox*)Sender; TListBox* SourceLB=(TListBox*)Source; for(int ItemPos=0;ItemPos<SourceLB->Items->Count;ItemPos++) if(sourcelb->selected[itempos]) SenderLB->Items->Add(SourceLB->Items->Strings[ItemPos]); for(int ItemPos=SourceLB->Items->Count-1;ItemPos>=0;ItemPos--) if(sourcelb->selected[itempos]) SourceLB->Items->Delete(ItemPos); procedure TForm1.ListBox2DragDrop(Sender, Source: TObject; X, Y: Integer); var ItemPos :Integer; SenderLB,SourceLB :TListBox; SenderLB:=Sender as TListBox; SourceLB:=Source as TListBox; for ItemPos:=0 to SourceLB.Items.Count-1 do if (SourceLB.Selected[ItemPos]) then SenderLB.Items.Add(SourceLB.Items.Strings[ItemPos]); for ItemPos:=SourceLB.Items.Count-1 downto 0 do if (SourceLB.Selected[ItemPos]) then SourceLB.Items.Delete(ItemPos); W tym momencie nasza aplikacja uzyskuje pełną funkcjonalność mechanizmu drag & drop. Żeby przenosić wiele elementów wystarczy je zaznaczyć korzystając z klawisza Shift lub Ctrl i, nadal trzymając ten klawisz, aby nie zmienić zaznaczenia, przeciągnąć je na drugą listę. 8
III. Obsługa pliku rzuconego na formę z zewnętrznej aplikacji Obsługa mechanizmu drag & drop pomiędzy aplikacjami wymaga obsłużenia odpowiednich komunikatów Windows. Najczęściej stosowane jest przenoszenie plików i taki przykład przedstawiony jest poniżej. Upuszczenie pliku (zabranego np. z Exploratora Windows) na formę powoduje wysłanie do aplikacji komunikatu WM_DROPFILES 5, który spowoduje wywołanie zdarzenia Application->OnMessage. Najprościej będzie więc umieścić właściwy kod w metodzie związanej z tym zdarzeniem. Ponieważ w starszych wersjach C++ Buildera/Delphi nie było na palecie komponentów VCL wystawionego obiektu TApplicationEvents, który ułatwia dostęp do zdarzeń globalnego obiektu Application, w konstruktorze lub w metodzie związanej ze zdarzeniem OnCreate formy musimy przypisać samodzielnie metodę o odpowiedniech argumentach i zwracanej wartości: void fastcall TForm1::ApplicationMessage(tagMSG& Msg,bool& Handled) procedure ApplicationMessage(var Msg :TMsg;var Handled :Boolean); Funkcja nie zwraca żadnego wyniku a przyjmuje przez referencję strukturę tagmsg (w Delphi jest to struktura o nazwie TMsg), która informuje o numerze komunikatu, czasie nadania, pozycji kursora myszy i dwie dodatkowe liczby przekazujące dodatkowe informacje dotyczące sytuacji, która spowodowała komunikat (opis predefiniowanych stałych i sens dodatkowych parametrów znaleźć można w Win32 SDK). Ponieważ nasza aplikacja będzie jedynie przyjmować pliki, więc musimy oprogramować jedynie dwa etapy drag & drop: akceptacja i upuszczenie. Akceptujemy wszystkie pliki bez względu na ich pochodzenie, więc w konstruktorze wywołujemy funkcję WinAPI DragAcceptFiles, której pierwszym argumentem jest uchwyt okna/formy, której akceptacja ma dotyczyć, a drugim wartość logiczna informująca o włączeniu (true) lub wyłączeniu (false) akceptacji. Konstruktor TForm1 powinien więc zawierać dwa polecenia: fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) Application->OnMessage=ApplicationMessage; DragAcceptFiles(Handle,True); procedure TForm1.FormCreate(Sender: TObject); Application.OnMessage:=ApplicationMessage; DragAcceptFiles(Handle,True); Została zasadnicza część zareagowanie na nadejście komunikatu o upuszczeniu pliku (WM_DROPFILES). W tym celu napiszmy 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: void fastcall TForm1::ApplicationMessage(tagMSG& Msg,bool& Handled) switch(msg.message) 5 Zobacz oddzielny skrypt poświęcony obsłudze komunikatów w Delphi/C++ Builder. 9
case WM_DROPFILES: WMDropFiles(Msg); procedure TForm1.ApplicationMessage(var Msg :TMsg;var Handled :Boolean); case (Msg.message) of WM_DROPFILES: WMDropFiles(Msg); 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. Po nadejściu komunikatu WM_DROPFILES wywoływana jest metoda WMDropFiles przyjmująca jako argument referencję do komunikatu (tagmsg to struktura opisana w Win32 SDK). W przypadku tego komunikatu parametr wparam przechowuje uchwyt do struktury przechowującej informację o zrzucanych plikach. Ten uchwyt będzie argumentem funkcji WinAPI DragQueryFile udostępniającej informacje o plikach oraz funkcji DragFinish kończącej operację drag & drop. W metodzie WMDropFiles, korzystając z uchwytu przechowywanego w wparam, pobieramy liczbę zrzuconych plików. Informację taką dostarcza funkcja DragQueryFile jeżeli jej drugi argument ustawiony jest na 0xFFFFFFFF (maksymalna wartość ośmiobitowej liczby naturalnej). Wówczas w pętli po ilości plików pobieramy nazwy i ścieżki do poszczególnych plików korzystając z tej samej funkcji (drugi argument musi być tym razem kolejnym numerem pliku). Nazwa pliku zwracana jest w trzecim argumencie (wskaźnik do łańcucha). Mając nazwę pliku możemy wykonać dowolną operację: wczytać do TMemo, wykonać operację na pliku lub, jak jest w tym przykładzie, po prostu dopisać nazwę pliku do listy. void fastcall TForm1::WMDropFiles(tagMSG& Msg) HDROP hdrop=(hdrop)msg.wparam; //liczba zrzuconych plików int TotalNumberOfFiles=DragQueryFile(hDrop,0xFFFFFFFF,NULL,0); ListBox1->Items->Clear(); //Czyszczenie listy przy kazdym upuszczeniu ListBox1->Items->Add( "Ilosc plikow zrzuconych na forme: "+(AnsiString)TotalNumberOfFiles); for(int i=0;i<totalnumberoffiles;i++) char nazwa_pliku[max_path]; DragQueryFile(hDrop,i,nazwa_pliku,sizeof(nazwa_pliku)); //tu okreslamy wlasciwe dzialanie zwiazane z upuszczeniem pliku //(mamy do dyspozycji nazwe pliku wraz ze sciezka w nazwa_pliku) ListBox1->Items->Add(nazwa_pliku); DragFinish(hDrop); procedure TForm1.WMDropFiles(var Msg :TMsg); var hdrop :THandle; 10
TotalNumberOfFiles,i :Integer; nazwa_pliku :array[0..max_path] of Char; hdrop:=msg.wparam; //liczba zrzuconych plików TotalNumberOfFiles:=DragQueryFile(hDrop,$FFFFFFFF,nil,0); ListBox1.Items.Clear(); //Czyszczenie listy przy kazdym upuszczeniu plikow ListBox1.Items.Add( 'Ilosc plikow zrzuconych na forme: '+IntToStr(TotalNumberOfFiles)); for i:=0 to TotalNumberOfFiles-1 do DragQueryFile(hDrop,i,nazwa_pliku,sizeof(nazwa_pliku)); //tu okreslamy wlasciwe dzialanie zwiazane z upuszczeniem pliku //(mamy do dyspozycji nazwe pliku wraz ze sciezka w nazwa_pliku) ListBox1.Items.Add(nazwa_pliku); DragFinish(hDrop); Metoda kończy się wywołaniem funkcji DragFinish, która kończy operację drag & drop (kursor otrzymuje swoją standardową postać). Dla porządku wyłączmy akceptacje dla upuszczania plików przed zamknięciem formy: void fastcall TForm1::FormClose(TObject *Sender, TCloseAction &Action) DragAcceptFiles(Handle,False); procedure TForm1.FormClose(Sender: TObject; var Action: TCloseAction); DragAcceptFiles(Handle,False); Zadanie Napisać aplikację, która po upuszczeniu plików będzie tworzyła dla każdego pliku z rozszerzeniem txt obiekt TMemo i wczytywała do niego zawartość pliku. Można to zrealizować w postaci aplikacji wielodokumentowej (MDI, zob. własność TForm.FormStyle). Zadanie Pominąć zdarzenie TApplication.OnMessage i napisać metodę WMDropFiles połączoną z przychodzącym komunikatem na poziomie okna Form1 (zob. skrypt nt. obsługi komunikatów w Delphi i C++ Builder). 11