Umíme to s Delphi: 103. díl – vylepšujeme drag and drop

Dnešní článek navazuje na povídání o drag and drop a obsahuje popis některých dalších možností a vylepšení této operace. Nezůstaneme u souborů a u textu: také si ukážeme, jak zajistit přetahování grafických prvků. V neposlední řadě se také dozvíte poslední a definitivní informace o umístění offline verze tohoto seriálu, kterou si již můžete kdykoliv stáhnout na svůj počítač.

Offline verze seriálu je na Živě!

Než se dostaneme k samotné náplni dnešního dílu, kterou bude další přísun informací o operaci drag and drop, dovolte mi opět jednu odbočku k offline verzi seriálu. Jak jsme již informovali v předchozích článcích a v diskusích, offline verze už je na světě. Poslední dobrá zpráva hovoří o tom, že si ji můžete opět stahovat přímo ze serveru Živě: najdete ji v sekci Soubory. Věřím, že vytvořená offline verze pro vás bude zajímavým a vítaným zlepšením komfortu.

Zpět k tématu článku – ať žije drag and drop

V minulém článku jsme krok za krokem prošli všechny důležité činnosti, které souvisejí s praktickou realizací operace drag and drop a také s její implementací v Delphi. Jak jsem slíbil v závěru předchozího dílu, dnes se k operaci drag and drop ještě krátce vrátíme.

Ukážeme si totiž některé další „vylepšení“ této operace, neboť minule jsme vytvořili skutečně jen zcela základní a nepříliš sofistikovanou verzi. Proto si dnes ukážeme několik možných dalších „featur“.

V dnešním článku ovšem vyjdeme z aplikace vytvořené před týdnem. Pokud jste ji s námi minule nenaprogramovali, učiňte tak prosím teď, protože v dalším textu článku budu předpokládat, že máte aplikaci z předchozího článku k dispozici. Pouze připomenu, že se jedná o aplikaci obsahující komponenty DirectoryListBox a FileListBox (v nichž uživatel najde požadovaný adresář a textový soubor) a komponentu Memo (do níž uživatel vybraný soubor přetáhne a následně dojde k jeho otevření – načtení). Možný vzhled aplikace si můžete připomenout na následujícím obrázku:

Tato aplikace umožňuje tedy přetahovat (realizovat operaci drag and drop) jen u textových souborů a umožňuje je přetahovat pouze z jedné komponenty (FileListBox) do jedné komponenty (Memo). Nyní budeme tuto jednoduchou aplikaci postupně vylepšovat a rozšiřovat a ukážeme si tak další možnosti operace drag and drop.

Vylepšení číslo 1 – manuální začátek tažení

Ve stávající verzi aplikace začíná proces tažení automaticky. Je to proto, že jsme nastavili hodnotu vlastnosti DragMode komponenty FileListBox na dmAutomatic. Tím jsme zajistili, že tažení začne v okamžiku, kdy uživatel stiskne tlačítko myši a kurzor je nad komponentou FileListBox. Hodnota dmManual by naproti tomu znamenala, že tažení začne až ve chvíli, kdy ošetříme událost OnMouseDown a v ní tažení nastartujeme pomocí metody BeginDrag.

Protože hodnota dmManual je přednastavená, nemusíme ji explicitně nastavovat. Stačí ze zdrojového kódu (z obsluhy události FormCreate) odstranit následující řádek kódu:

  FileListBox1.DragMode := dmAutomatic;

To však samozřejmě nestačí. Když si zkusíte nyní aplikaci přeložit a spustit, budete se o přetahování souborů z FileListBox pokoušet marně. Operace drag and drop prostě nikdy nezačne.

Takže kromě odstranění výše uvedené řádky musíme udělat ještě jednu věc – zajistit start operace tažení. To provedeme v rámci obsluhy OnMouseDown komponenty FileListBox. Abychom si ukázali, k čemu může být ruční zahájení tažení dobré, použijeme obsluhu této události třeba k otestování, zda uživatel stiskl pravé tlačítko myši. Pouze v tom případě zahájíme tažení (takže levým tlačítkem nebude možné přetáhnout zhola nic):

procedure TForm1.FileListBox1MouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  if Button = mbRight then  { otestujeme, bylo-li stisknuto levé tlačítko }
    with Sender as TFileListBox do  { Sender bude vystupovat jako TFileListBox }
      begin
        if ItemAtPos(Point(X, Y), True) >= 0 then  { došlo ke kliknutí tam, kde je nějaká položka? }
          BeginDrag(False);  { pokud ano, zahájíme její operaci drag }
      end;
end;

Tímto způsobem pak lze realizovat mnoho zajímavých funkcí (asi třeba víte, že když v některých aplikacích ve Windows přetáhnete soubor levým tlačítkem myši, dojde k jeho zkopírování, zatímco když jej přetáhnete pravým tlačítkem, dojde k jeho přesunutí apod.).

Vylepšení 2 – tažení se zahájí až po určitém počtu pixelů

Nyní si ukážeme další (velmi jednoduché) vylepšení drag and drop. Minule jsme si řekli, že nastavením vlastnosti DragImmediate globálního objektu Mouse na True říkáme, že operace je inicializována (může začít) ihned po stisknutí tlačítka myši. Naproti tomu nastavíme-li hodnotu této vlastnosti na False, může být tažení zahájeno až poté, co kurzor myši urazí vzdálenost rovnající se tolika pixelům, kolik je nastaveno ve vlastnosti Treshold. Vlastnost Treshold se samozřejmě týká také globálního objektu Mouse.

Proto znovu upravíme obsluhu události OnCreate hlavního formuláře. Ponecháme nastavení DragImmediate na False, ale připíšeme ještě jeden řádek, v němž řekneme, že tažení nemá začít dříve, než kurzor myši urazí 50 pixelů:

  Mouse.DragThreshold := 50;

Když si nyní zkusíte aplikaci přeložit a spustit, zjistíte (třeba podle vzhledu kurzoru myši), že tažení skutečně začne až po 50 pixelech. Do té doby se jedná jen o přesun kurzoru, k inicializaci operace drag and drop nedošlo.

Zároveň si všimněte, že vlastnosti DragImmediate a Treshold (a tedy nastavení globálního objektu Mouse) je jaksi „nadřazeno“ příkazu BeginDrag: přestože zavoláme BeginDrag ihned po prvním stisku tlačítka myši, nastavení DragImmediate a Treshold znamená pozdržení začátku této operace až do definované doby.

procedure TForm1.FormCreate(Sender: TObject);
begin
  Caption := `Textový prohlížeè souborù`;
  Memo1.Clear;
  // pri zmene adresare v DirectoryListBox se automaticky zmeni obsah FileListBox
  DirectoryListBox1.FileList := FileListBox1;
  FileListBox1.Mask := `*.txt`;
  Mouse.DragImmediate := False;
  Mouse.DragThreshold := 50;
end;

Vylepšení 3 – více zdrojových a cílových komponent

Nyní si ukážeme, jak vyřešit požadavek na více zdrojových a cílových komponent, v nichž je možné realizovat operaci drag and drop. Aplikaci tedy vylepšíme ještě o další komponenty, které umožní pracovat i s grafikou.

Přidejte proto do aplikace ještě komponenty ComboBox, Shape a Image. V rozbalovacím seznamu ComboBox budou uvedeny možné tvary komponenty Shape (kterými jsou stRectangle – obdélník, stSquare – čtverec, stRoundRect – obdélník se zaoblenými rohy, stRoundSquare – čtverec se zaoblenými rohy, stCircle – kružnice, stEllipse – elipsa).

Vybráním tvaru v komponentě COmboBox dojde k jeho zobrazení v komponentě Shape. Pokud tvar zobrazovaný komponentou Shape chytíte myší a přetáhnete jej do komponenty Image, dojde uvnitř komponenty Image k vykreslení příslušného obrazce.

Vytvoříme jen velmi jednoduchou verzi aplikace, která sice bude umožňovat vložit v komponentě Image obrázek na zvolené místo (tedy na aktuální souřadnice kurzoru), ale nebude umožňovat změnu jejich velikosti. Věřím, že toto vylepšení byste již dokázali bez problémů realizovat sami.

Po přidání výše uvedených komponent do aplikace je nutné provést následující úpravy ve zdrojovém kódu:

1. někde, například v obsluze události OnCreate hlavního formuláře (a nebo třeba v návrhové fázi pomocí Object Inspectoru) je třeba zadat příslušné položky (tedy názvy tvarů) do komponenty ComboBox. Můžeme si klidně dovolit napsat názvy česky (není nutné používat přímo hodnoty vlastnosti Shape). Kromě toho je dobré nastavit rozbalovacímu seznamu styl na DropDownList, aby uživatel nemohl zapisovat nové (tedy neexistující) tvary přímo do editačního políčka, které je součástí ComboBoxu.

V našem případě provedeme příslušnou operaci uvnitř události OnCreate hlavního formuláře. Zde je výsledek:

  ComboBox1.Clear;
  ComboBox1.Style := csDropDownList;
  ComboBox1.Items.Add(`Obdélník`);
  ComboBox1.Items.Add(`Čtverec`);
  ComboBox1.Items.Add(`Zaoblený obdélník`);
  ComboBox1.Items.Add(`Zaoblený čtverec`);
  ComboBox1.Items.Add(`Elipsa`);
  ComboBox1.Items.Add(`Kružnice`);
  ComboBox1.ItemIndex := 0;

Jak jsem uvedl výše, není důležité, jak jednotlivé tvary pojmenujeme, ale je životně důležité uvést je právě v tomto pořadí, nijak jinak. Důvod si prozradíme vzápětí.

2. Ošetřit zahájení tažení v komponentě Shape. Tento úkol bude jednodušší než v případě zahajování tažení u komponenty FileListBox, protože nemusíme vlastně nic testovat. Napíšeme pro jednoduchou obsluhu události:

procedure TForm1.Shape1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  with Sender as TShape do BeginDrag(False);
end;

Jinak řečeno – pomocí BeginDrag zahájíme operaci, pouze na zdrojovou komponentu budeme pohlížet jako na TShape (což je logické).

3. Další krok spočívá v tom, že jakmile se tažený objekt ocitne nad komponentou Image, musíme prohlásit, zda jsme ochotni jej v této komponentě přijmout. Kladně odpovídáme pouze v případě, že je tímto objektem „cosi“ z komponenty TShape (prozatím). Napíšeme tedy jednoduchou obsluhu události OnDragOver komponenty Image, v níž nastavíme výstupní parametr Accept podle toho, je-li taženým objektem TShape:

procedure TForm1.Image1DragOver(Sender, Source: TObject; X, Y: Integer;
  State: TDragState; var Accept: Boolean);
begin
  // jakmile je objekt z Shape tazen nad Image, musime oznamit, ze jej prijimame
  Accept := Source is TShape;
end;

4. Předposlední, a možná nejzajímavější krok nás čeká v rámci ukončení operace tažení. Pokud se uživatel rozhodne nad komponentou Image upustit tažený objekt (který ale samozřejmě musí být typu TShape), musíme v komponentě Image na příslušnou pozici kurzoru nakreslit požadovaný obrazec. Protože neexistuje žádné spojení mezi komponentou Image a Shape, nezbývá nám, než vykreslit požadovaný tvar „ručně“. K tomu použijeme známé metody třídy TCanvas (připomeňme, že Image má vlastnost Canvas – plátno): Rectangle, RoundRect a Ellipse.

Konkrétní tvar této obsluhy samozřejmě záleží na nás, můžeme si zde poměrně bez omezení vyhrát a jistě bychom nalezli nejednu možnost inspirace – například přidat do aplikace nějaký ColorDialog a umožnit tak výběr barvy vykreslovaného tvaru apod. Uvedeme však nejjednodušší verzi celých manévrů, která spočívá jen a pouze ve vykreslení „taženého“ tvaru:

procedure TForm1.Image1DragDrop(Sender, Source: TObject; X, Y: Integer);
begin
  // pokud byl nad Image upusten objekt z Shape, zobrazime prislusny tvar
  if Source is TShape then
    case Shape1.Shape of
      stRectangle  : Image1.Canvas.Rectangle(X, Y, X+90, Y+50);
      stSquare      : Image1.Canvas.Rectangle(X, Y, X+70, Y+70);
      stRoundRect  : Image1.Canvas.RoundRect(X, Y, X+90, Y+50, 20, 20);
      stRoundSquare : Image1.Canvas.RoundRect(X, Y, X+70, Y+70, 20, 20);
      stCircle      : Image1.Canvas.Ellipse(X, Y, X+30, Y+30);
      stEllipse    : Image1.Canvas.Ellipse(X, Y, X+90, Y+50);
    end;
end;

Jen ve stručnosti poznamenám, že obdélník a čtverec kreslíme pomocí funkce Rectangle, zaoblený obdélník a zaoblený čtverec pomocí funkce RoundRect a na kružnici i elipsu můžeme povolat funkci Ellipse.

5. Poslední krok bude spočívat v aktualizaci komponenty Shape (ve vykreslení požadovaného tvaru) podle toho, jaký tvar uživatel zrovna vybral v komponentě ComboBox. Na tomto místě je nutné vysvětlit, proč jsem při psaní obsluhy FormCreate tak hlasitě vykřikoval, že je nutné „nastrkat“ položky do ComboBoxu právě v jednom specifickém pořadí. Důvod leží v následujícím zdrojovém kódu:

procedure TForm1.ComboBox1Change(Sender: TObject);
begin
  Shape1.Shape := TShapeType(ComboBox1.ItemIndex);
end;

Obsluha OnChange komponenty ComboBox je nyní totiž extrémně krátká a elegantní. Využíváme přitom přetypování celého čísla (hodnoty ItemIndex, tedy indexu zvolené položky v ComboBoxu) na typ TShapeType. Když se podíváte do zdrojových kódů Delphi (konkrétně soubor ExtCtrls.pas), zjistíte, že typ TShapeType je definován jednoduše jako výčtový typ šesti možných hodnot:

  TShapeType = (stRectangle, stSquare, stRoundRect, stRoundSquare,
    stEllipse, stCircle);

Vnitřní reprezentace výčtového typu je přitom taková, že jednotlivým hodnotám jsou vzestupně přiřazena celá čísla počínaje nulou. Z uvedené definice je tedy zřejmé, že po přetypování čísla 0 na typ TShapeType získáme stRectangle, po přetypování čísla 1 získáme stSquare, po přetypování číslo 2 získáme stRoundRect, a tak dále.

Proto pokud vložíme do ComboBoxu hodnoty ve „správném“ pořadí (podle této definice), můžeme hodnotu ItemIndex jednoduše použít pro zjištění požadovaného tvaru. Tento postup je možná trochu „nečistý“ (jistě znáte i výstižnější označení, ale ta bych nerad na stránkách Živě používal) – a zvláště pro začátečníky stojí za úvahu, zda jeho použitím neznepřehledníme zdrojový kód více, než kolik činí úspora – ale zato jsme si ušetřili několik řádků kódu, které bychom jinak museli zapsat. (I když jsem momentálně mnohem více řádků vyplýtval na to, abych celý problém objasnil.)

Tím je naše vylepšení hotovo. Když nyní aplikaci přeložíte a spustíte, budete moci přetahovat komponentu Shape do komponenty Image a po přetáhnutí se v Image vykreslí na dané pozici aktuální tvar Shape.

Zároveň si zkuste, že tvar z komponenty Shape nejde přetáhnout do komponenty Memo a stejně tak soubor z komponenty FileListBox nejde přetáhnout do komponenty Image. To je dáno nastavením podmínek, za nichž se přiřadí do parametru Accept (v událostech OnDragOver komponent Memo a Image) hodnota True. A samozřejmě i v „upouštěcích“ událostech, ztedy v OnEndDrag komponent Memo (resp. Image) testujeme, zda je upouštěným objektem zrovna soubor (resp. tvar).

V rámci tohoto vylepšení jsme si také ukázali, jak v okamžiku upuštění taženého objektu můžeme využít parametry X a Y, které máme v obsluze události OnEndDrag k dispozici. V předchozím kódu je využíváme k tomu, abychom nakreslili tvar do Image na požadovanou pozici.

Možný vzhled aplikace vidíte na následujícím obrázku:

A mohli bychom pokračovat (ale nebudeme:-))

Jistě bychom vymysleli ještě řadu dalších vylepšení, kterými bychom aplikaci ozvláštnili. Napadá mě třeba možnost zobrazovat obrázky (ze souborů) také v komponentě Image. Toto vylepšení by nebylo obtížné – jen bychom museli rozšířit podmínky v komponentě Image tak, aby nepřijímala pouze tvary ze Shape, ale i soubory z FileListBox. Jen by bylo nutné otestovat, že tažený soubor opravdu obsahuje obrázek a ne třeba text. Ale myslím, že podstata operace drag and drop je nyní již dostatečně vysvětlena a že jste se dozvěděli vše, co k jednoduchému programování této operace potřebujete znát.

Zdrojový kód

Uvedeme si opět kompletní zdrojový kód celé aplikace – tedy stav, do něhož jsme kód dnes uvedli. Jen připomínám, že jsme z kódu neodstranili testování na stisk pravého tlačítka myši (při přetahování souborů z FileListBox do Memo), takže při zkoušení aplikace se neulekněte, že snad přetahování nefunguje.

unit Unit1;

interface

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

type
  TForm1 = class(TForm)
    FileListBox1: TFileListBox;
    DirectoryListBox1: TDirectoryListBox;
    Memo1: TMemo;
    Label1: TLabel;
    Shape1: TShape;
    ComboBox1: TComboBox;
    Image1: TImage;
    procedure FormCreate(Sender: TObject);
    procedure Memo1DragOver(Sender, Source: TObject; X, Y: Integer;
      State: TDragState; var Accept: Boolean);
    procedure Memo1DragDrop(Sender, Source: TObject; X, Y: Integer);
    procedure FileListBox1EndDrag(Sender, Target: TObject; X, Y: Integer);
    procedure FileListBox1MouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure Shape1MouseDown(Sender: TObject; Button: TMouseButton;
      Shift: TShiftState; X, Y: Integer);
    procedure Image1DragOver(Sender, Source: TObject; X, Y: Integer;
      State: TDragState; var Accept: Boolean);
    procedure Image1DragDrop(Sender, Source: TObject; X, Y: Integer);
    procedure ComboBox1Change(Sender: TObject);
  private
    { Private declarations }
  public
    { Public declarations }
  end;

var
  Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.FormCreate(Sender: TObject);
begin
  Caption := `Textový prohlížeč souborů`;
  Memo1.Clear;
  // pri zmene adresare v DirectoryListBox se automaticky zmeni obsah FileListBox
  DirectoryListBox1.FileList := FileListBox1;
  FileListBox1.Mask := `*.txt`;
  Mouse.DragImmediate := False;
  Mouse.DragThreshold := 50;

  ComboBox1.Clear;
  ComboBox1.Style := csDropDownList;
  ComboBox1.Items.Add(`Obdélník`);
  ComboBox1.Items.Add(`Čtverec`);
  ComboBox1.Items.Add(`Zaoblený obdélník`);
  ComboBox1.Items.Add(`Zaoblený čtverec`);
  ComboBox1.Items.Add(`Elipsa`);
  ComboBox1.Items.Add(`Kružnice`);
  ComboBox1.ItemIndex := 0;
end;

procedure TForm1.Memo1DragOver(Sender, Source: TObject; X, Y: Integer;
  State: TDragState; var Accept: Boolean);
begin
  // jakmile je objekt z FileListBoxu tazen nad Memo, musime oznamit, ze jej prijimame
  Accept := Source is TFileListBox;
end;

procedure TForm1.Memo1DragDrop(Sender, Source: TObject; X, Y: Integer);
begin
  // pokud byl nad Memo upusten objekt z FileListBox, nacteme a zobrazime prislusny soubor
  if Source is TFileListBox then
    Memo1.Lines.LoadFromFile(FileListBox1.FileName);

end;

procedure TForm1.FileListBox1EndDrag(Sender, Target: TObject; X,
  Y: Integer);
begin
  // pro demonstraci vypiseme hlasku o pripadnem neuspesnem dokonceni operace
  if Mouse.IsDragging then          // pokud vubec zacala operace drag&drop
    if (Target <> nil) then
      ShowMessage(`Soubor byl úspěšně přetažen do komponenty.`)
    else
      ShowMessage(`Soubor se nepodařilo přetáhnout. `);

end;

procedure TForm1.FileListBox1MouseDown(Sender: TObject;
  Button: TMouseButton; Shift: TShiftState; X, Y: Integer);
begin
  if Button = mbRight then  { otestujeme, bylo-li stisknuto levé tlačítko }
    with Sender as TFileListBox do  { Sender bude vystupovat jako TFileListBox }
      begin
        if ItemAtPos(Point(X, Y), True) >= 0 then  { došlo ke kliknutí tam, kde je nějaká položka? }
          BeginDrag(False);  { pokud ano, zahájíme její operaci drag }
      end;
end;

procedure TForm1.Shape1MouseDown(Sender: TObject; Button: TMouseButton;
  Shift: TShiftState; X, Y: Integer);
begin
  with Sender as TShape do BeginDrag(False);
end;

procedure TForm1.Image1DragOver(Sender, Source: TObject; X, Y: Integer;
  State: TDragState; var Accept: Boolean);
begin
  // jakmile je objekt z Shape tazen nad Image, musime oznamit, ze jej prijimame
  Accept := Source is TShape;
end;

procedure TForm1.Image1DragDrop(Sender, Source: TObject; X, Y: Integer);
begin
  // pokud byl nad Image upusten objekt z Shape, zobrazime prislusny tvar
  if Source is TShape then
    case Shape1.Shape of
      stRectangle  : Image1.Canvas.Rectangle(X, Y, X + 90, Y + 50);
      stSquare      : Image1.Canvas.Rectangle(X, Y, X + 70, Y + 70);
      stRoundRect  : Image1.Canvas.RoundRect(X, Y, X + 90, Y + 50, 20, 20);
      stRoundSquare : Image1.Canvas.RoundRect(X, Y, X + 70, Y + 70, 20, 20);
      stCircle      : Image1.Canvas.Ellipse(X,Y,X + 30,Y+30);
      stEllipse    : Image1.Canvas.Ellipse(X,Y,X+90,Y+50);
    end;
end;

procedure TForm1.ComboBox1Change(Sender: TObject);
begin
  Shape1.Shape := TShapeType(ComboBox1.ItemIndex);
end;

end.

Na závěr

Tolik k operaci drag and drop. V dnešním článku a v předchozích dvou dílech jsme ji rozebrali dostatečně podrobně a věřím, že nyní již dokážete zajistit tažení a puštění skoro čehokoliv:-) Drag and drop je vítaným doplňkem, přičemž jeho implementace v Delphi je více než snadná. Tato operace by měla být podporována všude, kde to má alespoň trochu smysl – pro uživatele přináší podstatné zvýšení komfortu ovládání.

Váš názor Další článek: Říjnová kumulativní záplata pro Internet Explorer

Témata článku: , , , , , , , , , , , , , , , , , , , , , , , , ,