Umíme to s Delphi, 45. díl – vytvořte si vlastní komponentu, 2. část

Před týdnem jsme začali vytvářet své vlastní komponenty. Dnes pokračujeme: nejprve se zaměříme na výběr té „správné“ rodičovské třídy, a pak se vrhneme na vytváření vlastností (properties) nových komponent.
Řešení „domácího úkolu“

V závěru minulého dílu jsme si položili otázku, jak „vylepšit“ komponentu Tiskarny, aby obsahovala hned v počátcích práce ve svém záhlaví jméno jedné z tiskáren a nikoliv svůj název. Jedno z možných řešení spočívá v editaci zdrojového kódu komponenty, a to přidáním jedné řádky do metody CreateWnd:

procedure TTiskarny.CreateWnd;
begin
  inherited CreateWnd;
  Items.Assign(Printer().Printers);
  ItemIndex := 0;
end;    // procedure CreateWnd

Nyní se ve stále viditelném poli seznamu hned od začátku (a již v době návrhu) zobrazuje první tiskárna.

Úvodní analýza a výběr vhodného předka

Před týdnem jsme si řekli, že vývoj každé nové komponenty musí nevyhnutelně začínat důkladným rozborem požadavků, které na tuto komponentu máme. V návaznosti na tuto analýzu pak musíme vybrat třídu, ze které budeme při návrhu komponenty vycházet (jejímž rozšiřováním získáme požadované chování naší komponenty). Pojďme se tedy podívat na vhodné „kandidáty“ na pozici rodiče. Shrneme si jejich vlastnosti a naznačíme, k jakému účelu jsou vhodné a k jakému nikoliv.

V zásadě existují dvě obecné možnosti, z jaké komponenty vyjít při novém návrhu. Pokud chceme vytvořit „úplně novou“ komponentu, je vhodné použít některou základní třídu (jejichž přehled si uvedeme níže). Potřebujeme-li pouze drobně upravit vlastnosti některé již exitující komponenty (jako tomu bylo v příkladu v předchozím díle seriálu, je vhodnější vyjít z třídy této komponenty.

Při vytváření zcela nové komponenty obvykle vycházíme z některé z tříd uvedených v následující tabulce:

Bázová třída Vhodné využití
TObject Zřejmě již víte, že třída TObject je společným předkem všech tříd hierarchie VCL. Přestože se to obvykle nedělá, je možné odvodit novou komponentu přímo ze třídy TObject. Je logické, že tato třída vnese do vytvářené aplikace nejméně „nepotřebného balastu“, na druhou stranu vytvoření komponenty z TObject stojí nejvíce úsilí (protože TObject toho sama „umí“ nejméně). Při použití TObject jako rodičovské třídy nebude možné pracovat s komponentou v době návrhu aplikace.
TComponent TComponent je vhodným rodičem pro nevizuální komponenty.
TGraphicControl Definuje metodu Paint() a umožňuje kreslit na klientskou plochu svého rodiče. Nemá manipulátor okna, takže nemůže získat zaměření.
TWinControl Na rozdíl od předchozí třídy definuje manipulátor okna, takže může získat zaměření. Je společným předkem všech „okenních“ komponent.
TCustomControl Jakýsi přechod mezi třídami TGraphicControl a TWinControl. Třída TCustomControl obsahuje manipulátor okna a umožňuje uživatelsky definovat metodu Paint(). Bývá často předkem všech vizuálních komponent.

V této tabulce (i v okolním textu) se vyskytuje pojem „okenní komponenta“, případně „ovládací prvek typu okno“, apod. Asi stojí za to vysvětlit, oč jde. Okenní ovládací prvky (komponenty) jsou objekty, které se objevují za běhu programu a se kterými může uživatel provádět nějakou formu interakce. Každá okenní komponenta má svůj handle okna (o manipulátorech handle systému Windows jsme se již v seriálu zmiňovali) a tento handle je přístupný pomocí vlastnosti Handle (do které jej Delphi zapouzdřují). Handle umožňuje systému Windows identifikovat komponentu a manipulovat s ní. Důležité je, že handle umožňuje komponentě obdržet vstupní zaměření (focus) a může být předáván do funkcí Windows API.

Jak je v tabulce uvedeno, všechny okenní komponenty jsou odvozeny ze třídy TWinControl. Jde o většinu základních, nejpoužívanějších komponent, jako např. o tlačítka, seznamy, editační pole... Při vytváření nové komponenty byste ji sice mohli také odvodit přímo od TWinControl, Delphi ovšem přinášejí třídu TCustomControl, která umožňuje snazší uživatelské kreslení obrázků.

Pokud potřebujeme vytvořit komponentu, která má býti vizuální, ovšem nepotřebujeme, aby uměla získat vstupní zaměření, můžeme vytvořit tzv. grafickou komponentu. Grafické komponenty jsou podobné okenním komponentám, ovšem nemají handle okna – a v důsledku toho také spotřebují podstatně méně systémových zdrojů. Příkladem takové komponenty je Label třídy TLabel: nikdy nemůže získat zaměření (focus). Přestože ovšem grafické komponenty nemohou získat vstupní zaměření, mohou reagovat na události od myši. Pokud tedy potřebujete vytvořit grafickou komponentu, je vhodné použít jako bázovou třídu právě TGraphicControl, neboť na rozdíl od třídy TControl poskytuje plátno ke kreslení. Nutné je pouze přetížit metodu Paint.

V případě, že potřebujete vytvořit nevizuální komponentu (jako je např. DataSet, Timer, apod.), můžete ji odvodit přímo od třídy TComponent.

Jak jsme však poznali před týdnem, nejjednodušším způsobem vytvoření nové komponenty je modifikace některé existující. Novou komponentu je možné odvodit (oddědit) od libovolné komponenty poskytované Delphi. Některé ovládací prvky, jako např. seznamy (list box) či tabulky (grid), jsou k dispozici v několika „variantách“ na základní téma. V takových případech obsahuje VCL abstraktní třídu (tedy třídu, od níž nelze přímo vytvářet její objekty), která představuje právě tento „obecný základ“. Tuto třídu poznáte tak, že ve svém názvu obsahují slovo „Custom“, např. „TCustomComboBox, apod.

Tyto abstraktní třídy bývají obvykle relativně vhodným předkem pro vytváření komponent „na dané téma“. Představme si například, že chceme-li vytvořit svůj vlastní (speciální) seznam (ListBox), který nemá mít některé vlastnosti existující komponenty ListBox třídy TListBox. Jak jsme si řekli před týdnem, není vhodné vyjít z třídy TListBox, protože pomocí dědičnosti nemůžeme některé atributy či metody této třídy (ty, které nám nevyhovují), nějak „skrýt“ či „zrušit“. Není ale také vhodné vyjít z úplně základní třídy, například TWinControl, protože by bylo velmi pracné redefinovat veškeré chování, které ListBox „posbíral“ v hierarchii tříd od TWinControl až k TListBox. Optimální samozřejmě bude použití abstraktní třídy TCustomListBox, která implementuje veškeré vlastnosti seznamu (ListBox), nicméně nepublikuje je. Pokud odvodíme svou komponentu z některé takovéto abstraktní třídy, můžeme publikovat pouze ty vlastnosti které chceme mít v nové komponentě přístupné. Ostatní ponecháme chráněné (protected). Důsledkem je, že při umístění nové komponenty na formulář se v Object Inspectoru objevují jen vlastnosti, které tam chceme mít (tedy ty publikované), ostatní viditelné nejsou a Object Inspector je mnohem přehlednější.

Poznámka pro komerčně laděné uživatele: výše uvedený systém publikování vlastností je vhodný v případě, že si komponentu programujeme „sobě na míru“. Pokud chcete výsledek své práce dát (či prodat) ostatním programátorům, asi nemůžete předpokládat, že některá vlastnost nebude nikdy a nikým použita.

Šablony komponent

V souvislosti s úvodní rozvahou a výběrem předka je také nutné uvést pojem šablony komponent. Zkuste si umístit na formulář několik komponent, ošetřete také některé jejich události. Pak všechny tyto komponenty označte a vyberte z hlavní nabídky Delphi volbu Component – Create Component Template. Otevře se dialog (viz následující obrázek), ve kterém vyplňte název šablony, stránku palety a vyberte ikonu. Šablona se následně zobrazí v paletě komponent jako obyčejná komponenta (pouze s vybranou ikonou). Nyní vytvořte novou aplikaci a zkuste na formulář umístit tuto „šablonovou“ komponentu. Nejen že dojde ke správnému umístění všech komponent ze šablony, ale zároveň si všimněte, že jsou ošetřeny všechny události, které jste ošetřili při vytváření šablony. Delphi také vyřeší případné konflikty jmen mezi prvky šablony a prvky vaší aplikace.

Vlastní vytvoření komponenty: vlastnosti (properties)

Doufám, že v předchozích odstavcích jsme dostatečně podrobně rozebrali vybírání bázových tříd a že již dokážete zvolit optimálního předka pro svou komponentu. Nyní již začínáme opravdu vytvářet novou komponentu. Tento proces se skládá z několika dalších kroků a jedním z prvních obvykle bývá vytvoření vlastností komponenty.

Možná si teď říkáte, proč se zabývat vlastnostmi, když k nim přece není co dodat! Ano, máte svým způsobem pravdu. Nicméně je třeba si uvědomit, že na pojem vlastnost můžeme pohlížet z různých pohledů. Dosud jsme pouze pracovali s existujícími vlastnostmi existujících komponent. Jejich hodnoty jsme nastavovali v Object Inspectoru, četli a měnili jsme je také za běhu programu (např. přiřazením btnKonec.Caption := `Konec` jsme nastavili hodnotu vlastnosti Caption tlačítka btnKonec). Ovšem z programátorského hlediska můžeme s pojmem „vlastnost“ pracovat mnohem komplexněji.

To, co si nyní popíšeme, se primárně používá při vytváření nových komponent (proto to také popisujeme na tomto místě). Nicméně vůbec nic nám nebrání používat tento koncept i při běžném programování „obyčejných“ aplikací. Uvidíte, že mnohdy se to přímo nabízí.

V deklaraci třídy (v hodní části modulu) zapisujeme všechny atributy a metody, které třídě náleží. V následující ukázce (po jejímž praktickém využití raději nepátrejte:-)) je deklarována třída TfrmHlavni, která je odvozená (odděděná) od třídy TForm. Třída má 6 atributů (HotKey, MainMenu, Soubo1, Nov1, btnNastavit, Hodnota) a 4 metody (procedury FormCreate, btnKonecClick, NastavHodnotu a funkci PrectiHodnotu).

type
  TfrmHlavni = class(TForm)
    HotKey: THotKey;
    MainMenu: TMainMenu;
    Soubor1: TMenuItem;
    Nov1: TMenuItem;
    btnNastavit: TButton;
    btnKonec: TButton;
    procedure FormCreate(Sender: TObject);
    procedure btnKonecClick(Sender: TObject);
  private
    Hodnota: Integer;
    { Private declarations }
  public
    procedure NastavHodnotu(Hodn: Integer);
    function PrectiHodnotu;
    { Public declarations }
  end;

Při deklaraci atributů třídy však nemusíme postupovat tak přímočaře, jak je to v tomto příkladu (a jak jsme to dosud dělali). Pokud totiž chceme deklarovat vlastnost (property), můžeme to učinit pomocí klíčového slova property. Jaké to má výhody? Co z toho budeme mít?

Vlastnosti (podle definice uvedené v knize Todda Millera Mistrovství v Delphi 3 na straně 524) představují rozhraní k interním datovým polím komponenty. Pokusíme si definici přeložit. Pokud má třída nějaká soukromá datová pole (soukromé atributy) – v předchozím příkladu například atribut Hodnota –, potřebujeme, aby k nim uživatel třídy (tedy člověk, který při programování tuto třídu používá) měl nějaký přístup. Víme, že k soukromým atributům třídy A mohou přistupovat pouze metody třídy A; „zvenčí“ (tedy mimo metody třídy) se k nim nelze dostat. V předchozím příkladu (třída TfrmHlavni) je takovým soukromým atributem celočíselná proměnná Hodnota. Aby bylo možné dostat se k atributu Hodnota, je tedy nutné vytvořit veřejné metody, pomocí kterých se přečte hodnota atributu a nastaví hodnota atributu (v předchozím příkladu metody NastavHodnotu a PrectiHodnotu). Až dosud není na popsaném mechanismu nic překvapivého: pomocí něj se bežně v objektově orientovaném programování zajišťuje tzv. autorizovaný přístup k datům. Znamená to, že každý objekt je zodpovědný za svá data a díky nemožnosti přímého přístupu k atributům a díky existenci „přístupových“ veřejných metod je zajištěno, že do atributu nebude zapsáno nic, co by do něj býti zapsáno nemělo (zajistit by si to měly právě přístupové metody).

Při programování komponent se nám popsaný mechanismus autorizovaného přístupu k datům samozřejmě také hodí, nicméně je nutné jej trochu rozšířit. Důvodem je potřeba přistupovat k vlastnostem již v době návrhu. Takto deklarovaná třída (jako v předchozím příkladu) by sice měla své vlastnosti (např. Hodnota), ale v době návrhu by se v Object Inspectoru neobjevily. Pokud chceme, aby se některá vlastnost objevila již v době návrhu v Object Inspectoru, je nutné ji publikovat. Publikování znamená uvedení příslušné vlastnosti v sekci Published deklarace třídy, a to pomocí klíčového slova property.

Než si vše prakticky vysvětlíme a ukážeme, podíváme se ještě na vlastnosti z hlediska programátora. Vlastnosti na první pohled vypadají jako proměnné. Do vlastnosti může vývojář přiřazovat hodnotu, může také naopak hodnotu číst. Jedinou věc, kterou je možné dělat s proměnnými, avšak nikoliv s vlastnostmi, je její předávání odkazem. Vlastnosti však přinášejí mnohé „bonusy“ navíc: mohou být nastavovány a čteny již v době návrhu a umožňují vývojáři modifikovat komponentu ještě před spuštěním aplikace. Mohou se objevit v Object Inspectoru, který (kromě nesporného přínosu týkajícího se pohodlí) může také kontrolovat přiřazení hodnot vlastnostem. Díky vlastnostem (jak zanedlouho poznáme) je možné „skrýt“ volání velmi složité funkce za jednoduše vyhlížející přiřazení. Příkladem je nastavení hodnoty Top některé komponenty: v tom případě není pouze změněna hodnota nějakého atributu, ale komponenta je zároveň přemístěna a překreslena, což vývojář ani neutuší, natož, aby se o to musel starat. Vlastnosti také mohou být virtuální, takže jednoduše a jednoznačně vyhlížející vlastnost může mít zcela odlišné implementace v několika různých komponentách.

Řekli jsme si, že vlastnost je rozhraní k některému internímu datovému prvku komponenty. Z hlediska implementace si ji tedy můžete (velmi zjednodušeně) představit podobně jako v předchozím příkladu metody NastavHodnotu a PrectiHodnotu. Pomocí vlastnosti budeme číst a zapisovat hodnotu z/do nějakého soukromého atributu.

Ukážeme si tedy příklad zdrojového kódu, který bude obsahovat deklaraci třídy se třemi soukromými atributy a třemi vlastnostmi, pomocí kterých se s těmito atributy pracuje.

unit UkazkovaKomponenta;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;

type
  TUkazkovaKomponenta = class(TGraphicControl)
  private
    { Private declarations }
    FCislo: Integer;
    FRetezec: String;
    FZnak: Char;
    FDalsiCislo: Integer;
  protected
    { Protected declarations }
  public
    { Public declarations }
  published
    property Cislo: Integer read FCislo write FCislo;
    property Retezec: String read FRetezec write FRetezec;
    property Znak: Char read FZnak write FZnak;
    { Published declarations }
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents(`Nase`, [TUkazkovaKomponenta]);
end;

end.

Tato komponenta (UkazkovaKomponenta) má čtyři soukromé atributy: FCislo, FRetezec, FZnak a FDalsiCislo a dále má tři vlastnosti Cislo, Retezec a Znak. Pokud si zkusíte tuto komponentu instalovat do palety (postup viz minulý díl seriálu), zjistíte, že v Object Inspectoru se objevují vlastnosti zděděné od bázové třídy (tedy TGraphicControl) a dále vlastnosti Cislo, Retezec a Znak (viz následující obrázek). Všimněte si, že po nějaké vlastnosti DalsiCislo není nikde ani stopa: o zobrazení v Object Inspectoru rozhodují published vlastnosti, nikoliv soukromé atributy!

Podle konvence se obvykle názvy soukromých atributů souvisejících s nějakou vlastností zapisují s písmenem F na začátku. Není to striktní pravidlo, kompilátor to samozřejmě nevyžaduje, přesto je to vhodné a doporučené.

Asi nadešel vhodný okamžik pro uvedení syntaxe klíčového slova property. Při definování nové vlastnosti postupujeme takto:

property jméno_vlastnosti [indexy]: typ index celočíselná_konstanta specifikátory;

Následující tabulka popisuje jednotlivé prvky tohoto zápisu:

Prvek Význam
jméno_vlastnosti Platný identifikátor (splňující pravidla pro identifikátory v Object Pascalu)
[indexy] Nepovinné, sekvence parametrů deklarace oddělených středníky. Podrobnosti naleznete u popisu vlastností typu pole.
typ Typ vlastnosti.
celočíselná_konstanta Nepovinná, umožňuje více vlastnostem sdílet tutéž přístupovou metodu.
specifikátory Sekvence specifikátorů read, write, stored, default (či nodefault), implements. Každá vlastnost musí mít alespoň specifikátor read nebo write. Podrobnosti naleznete pod tabulkou.

Nejčastější (a nejjednodušší) použití vlastnosti je právě takové, jaké jsme uvedli v předchozím příkladu, tedy definice obsahuje pouze specifikátory read a write, případně jen jeden z nich. Nyní je již asi dostatečně zřejmý jejich význam: specifikátor read říká, „co se má stát, je-li hodnota vlastnosti čtena“, specifikátor write naopak stanovuje, „jaká akce se má vykonat při požadavku na zápis do hodnoty vlastnosti“. Nejjednodušší a nejpřímočařejší je při požadavku na čtení pouhé přečtení hodnoty soukromého atributu a při požadavku na zápis jednoduchá změna hodnoty soukromého atributu. Později si ukážeme mnohem komplexnější možnosti reakce na čtení či zápis.

Ještě předtím se však stručně zastavíme u jednotlivých typů vlastností. Jelikož existuje celá řada datových typů, existuje také celá řada typů vlastností. Některé z nich jsou velmi jednoduché (např. nastavení vlastnosti Top se provede v Object Inspectoru přímou změnou celočíselné hodnoty), změny jiných jsou však poměrně komplikované a vyžadují speciální editor vlastnosti (vzpomeňte si např. na String List Editor při nastavování některé vlastnosti typu TStrings, např. vlastnost Lines komponenty Memo).

Vytváření vlastností jednoduchých typů

Mezi vlastnosti jednoduchých typů řadíme číselné, znakové a řetězcové vlastnosti. V případě těchto vlastností je nastavování jednoduché, žádný specializovaný editor není zapotřebí. Tyto vlastnosti se používají velice často (číselné např. Position, Height, Top, apod.), řetězcové např. Caption, Text, Name...

Příklad tří vlastností jednoduchých typů jsme si uvedli v předchozím programovém výpisu (vlastnosti Cislo, Retezc a Znak). Za zmínku proto stojí již jen fakt, že nenastavíme-li implicitní (default) hodnotu těchto vlastností, budou standardně nastaveny „na nulu“, tedy v případě čísla na číslo 0, v případě znaku na nulový znak #0 a v případě řetězce na prázdný řetězec. Nastavení (jiných) implicitních hodnot si ukážeme vzápětí.

Vytváření vlastností výčtových typů

Vlastnosti výčtových typů jsou také poměrně časté. V Object Inspectoru je poznáte podle rozbalovacího seznamu, který se objeví po klepnutí na šipku v pravé části okénka příslušné vlastnosti. Mezi vlastnosti výčtového typu patří např. Color, FormStyle, BiDiMode, DragCursor, apod.).

Podle konvence se názvy výčtových typů (přesněji řečeno jejich jednotlivých elementů) uvozují dvěma písmeny, které specifikují příslušný výčtový typ, např. pro typ Color jsou hodnoty clBlack, clRed, clBlue, apod.; pro typ FormStyle jsou hodnoty fsNormal, fsChild, apod.

Před vytvořením vlastnosti výčtového typu je nutné tento typ deklarovat. Pak teprve je možné vytvořit příslušný privátní atribut a odpovídající vlastnost. Předvedeme si to raději na příkladu. Na následující obrázku je hned výsledek, tedy Object Inspector a odpovídající vlastnost. Pod obrázkem je zdrojový kód komponenty (s tučně zvýrazněnými klíčovými body), který to všechno způsobil.

unit UkazkovaKomponenta;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;

type
  TVyctovyTyp = (vtHodnota1, vtHodnota2, vtHodnota3, vtHodnota4);

  TUkazkovaKomponenta = class(TGraphicControl)
  private
    { Private declarations }
    FVyctovyTyp: TVyctovyTyp;
  protected
    { Protected declarations }
  public
    { Public declarations }
  published
    property VyctovyTyp: TVyctovyTyp read FVyctovyTyp write FVyctovyTyp;
    { Published declarations }
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents(`Nase`, [TUkazkovaKomponenta]);
end;

end.

Vytvoření vlastností typu množina

Typu množina se používá např. ve vlastnostech Options či BorderIcons. V Object Inspectoru je poznáte podle hranatých závorek, v nichž jsou uvedeny aktuálně platné hodnoty, a také podle znaménka plus (+) vedle názvu vlastnosti. Po klepnutí na toto znaménko se zobrazí seznam možných hodnot spolu s hodnotou True nebo False, která označuje aktuální (ne)přítomnost daného prvku v množině.

Abychom mohli vytvořit vlastnost typu množina, musíme množinu opět nejprve deklarovat. Využijeme přitom výčtového typu TVyctovyTyp, který jsme vytvořili v předchozím odstavci:

unit UkazkovaKomponenta;

interface

uses
  Windows, Messages, SysUtils, Classes, Graphics, Controls, Forms, Dialogs;

type
  TVyctovyTyp = (vtHodnota1, vtHodnota2, vtHodnota3, vtHodnota4);
  TMnozina = set of TVyctovyTyp;

  TUkazkovaKomponenta = class(TGraphicControl)
  private
    { Private declarations }
    FMnozina: TMnozina;
  protected
    { Protected declarations }
  public
    { Public declarations }
  published
    property Mnozina: TMnozina read FMnozina write FMnozina;
    { Published declarations }
  end;

procedure Register;

implementation

procedure Register;
begin
  RegisterComponents(`Nase`, [TUkazkovaKomponenta]);
end;

end.

Odpovídající Object Inspector si můžete pohlédnout na obrázku:

Na závěr

Ani dnešní díl nedokázal pokrýt z problematiky vytváření komponent vše podstatné, proto se těšte za týden na pokračování. Nejprve dokončíme popis vytváření jednotlivých vlastností (přičemž se budeme věnovat poněkud obtížnějším a komplikovanějším typům, například objektu), a hned vzápětí si vysvětlíme, jak vytvářet metody a události dané komponenty.
Váš názor Další článek: Nová Java 2 verze 1.4

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