Práce s C++ Builderem využívá myšlenku, že objekt obsahuje
data i kód a že s objektem můžeme manipulovat jak během návrhu, tak i při
běhu aplikace. V tomto smyslu vnímá komponenty jejich uživatel. Při vytváření
nových komponent, pracujeme s objekty způsobem, který koncový uživatel
nikdy nepotřebuje. Před zahájením tvorby komponent, je nutno se dobře seznámit
s principy objektově orientovaného programování popsanými dále.
Základní rozdíl mezi uživatelem komponent a tvůrcem komponent
je ten, že uživatel manipuluje s instancemi tříd a tvůrce vytváří nové
třídy.
Třída je v podstatě typ. Jako programátor, pracujeme
s typy a instancemi, i když tuto terminologii nepoužíváme. Např. vytvoříme
proměnnou typu int. Třídy jsou obvykle mnohem složitější než jednoduché
datové typy, ale pracují stejným způsobem. Přiřazením různých hodnot instancím
stejného typu můžeme provádět různé úlohy. Např. můžeme snadno vytvořit
formulář obsahující dvě tlačítka (OK a Cancel). Každé z nich
je instance třídy
TButton, ale mají přiřazeny různé hodnoty do vlastnosti
Caption
a různě zpracovávají události OnClick.
Účelem definování tříd komponent je poskytnout základ
pro užitečné instance. Tj. cílem je vytvořit objekt, který my nebo jiní
uživatelé mohou používat v různých aplikacích, v různých situacích nebo
alespoň v různých částech stejné aplikace.
Dříve než začneme vytvářet komponenty, pak se musíme
seznámit s následujícími body, které se týkají objektově orientovaného
programování:
Odvozování nové třídy
Jsou dva důvody k odvození nové třídy:
V obou případech, je cílem vytvoření opakovaně použitelných
objektů. Pokud navrhujeme komponentu jako opakovatelně použitou, pak svoji
práci budeme využívat později. Dáme svým třídám použitelné implicitní hodnoty,
ale umožňujeme je také přizpůsobovat.
Změna implicitních
hodnot třídy k zabránění opakování
Ve všech programovacích úlohách se snažíme vyhnout se opakování.
Jestliže potřebujeme několikrát zapsat stejné řádky kódu, pak je můžeme
umístit do funkce nebo dokonce vytvořit knihovnu funkcí, kterou můžeme
používat v mnoha programech. Stejný důvod je i pro komponenty. Jestliže
často měníme stejné vlastnosti nebo provádíme stejné volání metod, je užitečné
vytvořit nový typ komponenty, který tyto věci provede implicitně.
Např. předpokládejme, že pokaždé při vytváření aplikace,
chceme přidat formulář dialogového okna k provedení jisté funkce. Ačkoliv
není obtížné pokaždé znova vytvořit dialogové okno, není to ale nutné.
Můžeme navrhnout okno pouze jednou, nastavit jeho vlastnosti a výsledek
instalovat na Paletu komponent jako znovupoužitelnou komponentu. Toto nejen
redukuje opakování, ale také provádí standardizaci.
Přidání nových možností
ke třídě
Jiným důvodem pro vytváření nového typu komponenty je přidání
možností, které zatím existující komponenta nemá. Lze to provést odvozením
od existujícího typu komponenty (např. vytvořením specializovaného typu
seznamu) nebo od abstraktního základního typu, jako je TComponent
nebo TControl. Novou komponentu vždy odvozujeme od typu, který obsahuje
největší podmnožinu požadovaných služeb. Třídě můžeme přidávat nové vlastnosti,
ale nemůžeme je odebírat, tj. jestliže existující typ komponenty obsahuje
vlastnosti, které nechceme vložit do své komponenty, musíme komponentu
odvodit od předka komponenty.
Např. jestliže chceme přidat nějaké možnosti k seznamu,
můžeme odvodit novou komponentu od TListBox. Nicméně, jestliže chceme
přidat nějaké nové možnosti, ale také odstranit některé existující možnosti
standardního seznamu, musíme odvodit svůj nový seznam od TCustomListBox,
tj. předka TListBox. Zveřejníme možnosti seznamu, které chceme použít
a přidáme své nové možnosti.
Deklarování nové třídy
komponenty
Když se rozhodneme, že je nutno odvodit nový typ komponenty,
musíme také určit od kterého typu komponenty svou komponentu odvodíme.
C++ Builder poskytuje několik abstraktních typů komponent určených pro
tvůrce komponent k odvozování nových typů komponent. K deklarování nového
typu komponenty, přidáme deklaraci typu do hlavičkového souboru jednotky
komponenty.
Následující příklad je deklarace jednoduché grafické
komponenty:
class PACKAGE TPrikladTvaru : public TGraphicControl
{
public:
virtual __fastcall TPrikladTvaru(TComponent
*Owner);
};
Nezapomeňte vložit makro PACKAGE
(definované v SysDefs.h), které umožňuje třídám být importovány
a exportovány.
K dokončení deklarace komponenty vložíme do třídy
deklarace vlastností, položek a metod, ale prázdná deklarace je také přípustná
a poskytuje počáteční bod pro vytváření komponenty.
Předci a potomci
Pro uživatele komponent je komponenta entitou obsahující
vlastnosti, metody a události. Uživatel nemusí znát co z toho komponenta
zdědila a od koho to zdědila. Toto je ale značně důležité pro tvůrce komponenty.
Uživatel komponenty si může být jist, že každá komponenta má vlastnosti
Top
a Left, které určují, kde bude komponenta zobrazena na formuláři,
který ji vlastní. Nemusí znát, že všechny komponenty dědí tyto vlastnosti
od společného předka TControl. Nicméně, když vytváříme komponenty,
musíme znát, která třída od které třídy dědí příslušnou část. Musíme také
znát co naše komponenta dědí a můžeme tak využít zděděné služby bez jejich
znovuvytvoření.
Z definice třídy vidíme, že když odvozujeme třídu, odvozujeme
ji od existující třídy. Třída od které odvozujeme se nazývá bezprostřední
předek naší nové třídy. Předek bezprostředního předka je také předek nové
třídy; jsou to všechno předkové. Nová třída je potomek svých předků.
Všechny vztahy předek-potomek v aplikaci tvoří hierarchii
tříd. Nejdůležitější k zapamatování v hierarchii tříd je to, že každá generace
tříd obsahuje více než její předkové. Tj. třída dědí vše co obsahuje předek
a přidává nová data a metody nebo předefinovává existující metody.
Jestliže nespecifikujeme předka třídy, C++ Builder odvozuje
třídu od implicitního předka TObject. Třída TObject je předkem
všech tříd v Knihovně vizuálních komponent.
Obecné pravidlo pro volbu od které třídy odvozujeme je
jednoduché: Použijeme třídu která obsahuje co nejvíce z toho co chceme
vložit do nové třídy, ale neobsahuje nic z toho co nová třída nemá mít.
Ke třídě můžeme kdykoli přidávat, ale nelze od ní nic odstranit.
Řízení přístupu
C++ Builder poskytuje pět úrovní přístupu k částem tříd.
Řízení přístupu určuje, který kód může přistupovat ke které části třídy.
Specifikací úrovní přístupu, definujeme rozhraní naší komponenty. Pokud
nespecifikujeme jinak, pak položky, metody a vlastnosti přidané do naší
třídy jsou soukromé. Následující tabulka ukazuje úrovně přístupu v pořadí
od nejvíce omezujícího k nejméně omezujícímu:
Skrytí implementačních detailů
Soukromá část deklarace třídy je neviditelná z kódu mimo
třídu, pokud funkce není přítelem třídy. Soukromé části tříd jsou užitečné
pro ukrytí implementačních detailů před uživateli komponent. Jelikož uživatelé
tříd nemohou přistupovat k soukromé části, můžeme změnit vnitřní implementaci
objektu bez vlivu na kód uživatele.
Pokud nespecifikujeme žádné řízení přístupu na datové
složky, metody nebo vlastnosti, pak tato část je soukromá.
Následující příklad skládající se ze dvou částí ukazuje,
jak deklarací datových složek jako soukromých, zabráníme uživateli v přístupu
k informacím. První část je jednotka formuláře skládající se z hlavičkového
souboru a CPP souboru, který přiřazuje hodnotu soukromé datové složce v
obsluze události
OnCreate formuláře. Protože obsluha události je
deklarována ve třídě TSecretForm, jednotka je přeložena bez chyby.
Následuje výpis hlavičkového souboru.
#ifndef HideInfoH
#define HideInfoH
#include <vcl\SysUtils.hpp>
#include <vcl\Controls.hpp>
#include <vcl\Classes.hpp>
#include <vcl\Forms.hpp>
class PACKAGE TSecretForm : public TForm
{
__published:
void __fastcall FormCreate(TObject
*Sender);
private:
int FSecretCode;
// deklarace soukromé datové složky
public:
__fastcall TSecretForm(TComponent*
Owner);
};
extern TSecretForm *SecretForm;
#endif
Toto je odpovídající CPP soubor:
#include <vcl.h>
#pragma hdrstop
#include "hideInfo.h"
#pragma package(smart_init);
#pragma resource "*.dfm"
TSecretForm *SecretForm;
__fastcall TSecretForm::TSecretForm(TComponent*
Owner)
: TForm(Owner)
{
}
void __fastcall TSecretForm::FormCreate(TObject
*Sender)
{
FSecretCode = 42; // toto
je přeloženo správně
}
Druhá část tohoto příkladu je jiná jednotka formuláře,
která se pokouší přiřadit hodnotu datové složce FSecretCode ve formuláři
SecretForm.
Zde je hlavičkový soubor:
#ifndef TestHideH
#define TestHideH
#include <vcl\SysUtils.hpp>
#include <vcl\Controls.hpp>
#include <vcl\Classes.hpp>
#include <vcl\Forms.hpp>
class PACKAGE TTestForm : public TForm
{
__published:
void __fastcall FormCreate(TObject
*Sender);
public:
__fastcall TTestForm(TComponent* Owner);
};
extern TTestForm *TestForm;
endif
Následuje odpovídající CPP soubor. Protože obsluha události
OnCreate
formuláře se pokouší přiřadit hodnotu datové složce soukromé pro formulář
SecrectForm,
překlad končí chybovou zprávou (?TSecretForm::FSecretCode? is not accessible).
#include <vcl.h>
#pragma hdrstop
#include "testHide.h"
#include "hideInfo.h"
#pragma package(smart_init);
#pragma resource "*.dfm"
TTestForm *TestForm;
__fastcall TTestForm::TTestForm(TComponent*
Owner)
: TForm(Owner)
{
}
void __fastcall TTestForm::FormCreate(TObject
*Sender)
{
SecretForm->FSecretCode = 13; //způsobí
při překladu chybu
}
I když program vložením jednotky HideInfo může
používat třídu
TSecrectForm, nemůže přistupovat k datové složce
FSecrectCode
v této třídě.
Definování rozhraní vývojáře
Chráněná část deklarace třídy je neviditelná z kódu mimo
třídu, což je stejné jako u soukromé části. Rozdíl u chráněné části je
ten, že třída odvozená od tohoto typu, může přistupovat k jejím chráněným
částem. Chráněné deklarace můžeme použít k definování rozhraní vývojáře.
Tj. uživatel objektu nemá přístup k chráněným částem, ale vývojář (např.
tvůrce komponent) ano. Můžeme tedy udělat rozhraní přístupné tak, že tvůrci
komponent je mohou v odvozených třídách měnit a tyto detaily nejsou viditelné
pro koncové uživatele.
Definování běhového rozhraní
Veřejná část deklarace třídy je viditelná pro jakýkoli kód,
který má přístup ke třídě jako celku. Tj. veřejné části nemají žádné omezení.
Veřejné části třídy jsou dostupné za běhu programu pro všechen kód a veřejné
části třídy definují rozhraní běhu programu. Rozhraní běhu programu je
užitečné pro prvky, které nezpracováváme v době návrhu, jako jsou vlastnosti,
které závisí na aktuálních informacích o typech za běhu programu nebo které
jsou určeny pouze pro čtení. Metody, které slouží pro uživatele našich
komponent také deklarujeme jako část rozhraní běhu programu. Vlastnosti
určené pouze pro čtení nemůžeme používat během návrhu a uvádíme je ve veřejné
části deklarace.
Následující příklad používá dvě vlastnosti určené pouze
pro čtení deklarované jaké část běhového rozhraní komponenty:
class PACKAGE TPrikladKomponenty : public
TComponent
{
private:
// implementační detaily
jsou soukromé
int FTeplCelsius;
int GetTeplFahrenheit();
public:
...
// vlastnosti jsou veřejné
__property int TeplCelsius
= {read=FTeplCelsius};
__property int TeplFahrenheit
= {read=GetTeplFahrenheit};
};
Toto je metoda GetTeplFahrenheit v CPP souboru:
int TPrikladKomponenty::GetTeplFahrenheit()
{
return FTeplCelsius * (9 / 5) + 32;
}
I když veřejné vlastnosti může uživatel měnit, nejsou
zobrazeny v Inspektoru objektů a tedy nejsou součástí rozhraní pro návrh.
Definování návrhového rozhraní
Zveřejňovaná část deklarace třídy je veřejná, která také
generuje informace o typech za běhu programu. Mimo jiné, informace o typech
za běhu programu zajišťují, že Inspektor objektů může přistupovat k vlastnostem
a událostem. Protože pouze zveřejňovaná část je zobrazována v Inspektoru
objektů, zveřejňovaná část třídy určuje rozhraní třídy pro návrh. Rozhraní
pro návrh zahrnuje vše co uživatel může chtít přizpůsobit během návrhu,
ale nesmí obsahovat vlastnosti, které závisí na informacích o prostředí
běhu programu.
Vlastnosti určené pouze pro čtení nemohou být součástí
návrhového rozhraní, protože vývojář aplikace nemůže do nich přiřazovat
hodnoty přímo. Vlastnosti určené pro čtení tedy musí být veřejné.
Následuje příklad zveřejňované vlastnosti. Protože je
zveřejňovaná, je zobrazena v Inspektoru objektů při návrhu.
class PACKAGE TPrikladKomponenty : public
TComponent
{
private:
int FTeplota;
...
__published:
__property int Teplota
= {read = FTeplota, write = FTeplota};
};
Teplota, vlastnost v tomto příkladě, je dostupná
při návrhu a uživatel komponenty ji může přiřadit hodnotu v Inspektoru
objektů.
Vyřizování metod
Vyřízení metod je termín použitý k popisu, jak naše aplikace
určuje, který kód bude proveden při volání metody. Když zapisujeme kód,
který volá metodu třídy, je to stejné, jako volání jiné funkce. Nicméně
třídy mají dva různé způsoby vyřízení metod. Tyto dva typy vyřízení metod
jsou:
Normální metody
Všechny metody jsou normální (ne virtuální), pokud je speciálně
nedeklarujeme jako virtuální nebo pokud nepřepisujeme virtuální metodu
v základní třídě. Normální metody pracují jako volání normálních funkcí.
Překladač určí adresu metody a připojí metodu během překladu. Základní
výhodou normálních metod je, že jejich vyřízení je velmi rychlé. Protože
překladač může určit adresu metody, metoda je volána přímo.
Další rozdíl u normální metody je ten, že se nemění v
odvozených typech. Tj. když deklarujeme třídu, která obsahuje normální
metodu, potom odvozením nové třídy, potomek třídy sdílí přesně stejnou
metodu na stejné adrese. Normální metody tedy vždy provádějí to samé, bez
ohledu na aktuální typ objektu. Normální metodu nelze předefinovat. Deklarováním
metody v typu potomka se stejným jménem jako má normální metoda ve třídě
předka se nahradí metoda předka.
V následujícím příkladě, objekt typů Odvozena
může volat metodu Normalni, jako svou vlastní metodu. Deklarací
metody v odvozené třídě se stejným jménem a parametry, jako je Normalni
v třídě předka, nahradí metodu předka. V následujícím příkladě, když je
voláno o->JinaNormalni(), pak bude vyřízeno náhradou JinaNormalni
ve
třídě Odvozena.
class Zakladni
{
public:
void Normalni();
void JinaNormalni();
virtual void Virtualni();
};
class Odvozena : public Zakladni
{
public:
void JinaNormalni();
// nahrazuje Zakladni::JinaNormalni()
void Virtualni();
// přepisuje Zakladni::Virtualni()
};
void PrvniFunkce()
{
Odvozana *o;
o = new Odvozana;
o->Normalni();
// Volání Normalni() jako by byla členem Odvozana
// Stejné jako volání o->Zakladni::Normalni()
o->JinaNormalni();
// Volání předefinované JinaNormalni(), ...
// ... nahrazující Zakladni::JinaNormalni()
delete o;
}
void DruhaFunkce(Zakladni *z)
{
z->Virtualni();
z->JinaNormalni();
}
Virtuální metody
Volání virtuálních metod je stejné jako volání jiných metod,
ale mechanismus jejich vyřízení je složitější. Virtuální metody umožňují
předefinování ve třídách potomků, ale stále metodu voláme stejným způsobem.
Adresa volané metody není určena při překladu, ale je hledána až při běhu
aplikace.
V předchozím příkladě, pokud voláme DruhaFunkce
s ukazatelem na objekt Odvozana, pak je volána funkce Odvozana::Virtualni().
Virtuální mechanismus dynamicky zjišťuje typ třídy objektu předaného za
běhu a vyřizuje příslušnou metodu. Ale volání normalní funkce z->JinaNormalni()
bude vždy volání Zakladni::JinaNormalni(), protože adresa JinaNormalni
je
určena již při překladu.
K deklaraci nové virtuální metody, přidáme klíčové slovo
virtual
před deklaraci metody. Klíčové slovo virtual v deklaraci metody
vytváří položku v tabulce virtuálních metod (VTM) třídy. VTM obsahuje adresy
všech virtuálních metod ve třídě. Tabulka je určena za běhu k určení, že
z->Virtualni
bude volat Odvozena::Virtualni a ne Zakladni::Vitrualni.
Když odvozujeme novou třídu od existující třídy, nová
třída získá svou vlastní VTM, která obsahuje všechny položky z VTM svého
předka a položky dalších virtuálních metod deklarovaných v nové třídě.
Potomek třídy může předefinovat některé ze zděděných virtuálních metod.
Předefinování metody znamená její rozšíření nebo změnu, namísto jejího
nahrazení. Třída potomka může opětovně deklarovat a implementovat libovolnou
z metod deklarovaných ve svých předcích.
Přepisování metod
Přepisování metod znamená rozšíření nebo předefinování metody
předka, namísto jejího nahrazení. K přepsání metody ve třídě potomka, opětovně
deklarujeme metodu v odvozené třídě a zajistíme že počet a typy parametrů
jsou stejné.
Následující kód ukazuje deklaraci dvou jednoduchých komponent.
První deklaruje dvě metody, každá je jiného typu vyřízení. Druhá komponenta
odvozená od první, nahrazuje nevirtuální metodu a přepisuje virtuální metodu.
class PACKAGE TPrvniKomponenta : public TComponent
{
void Presun();
// normální metoda
virtual void Zablesk(); // virtuální
metoda
}
class PACKAGE TDruhaKomponenta : public TPrvniKomponenta
{
void Presun(); //deklarací nové
metody skrýváme TPrvniKomponenta::Presun
void Zablesk(); //přepisujeme virtuální
TPrvniKomponenta::Zablesk v předkovi
}
Abstraktní členy tříd
Když metoda je ve třídě předka deklarována jako abstraktní,
pak ji musíme opětovně deklarovat a implementovat v třídě potomka a to
dříve než novou komponentu můžeme použít v aplikaci. C++ Builder nemůže
vytvářet instance tříd obsahující abstraktní složky.
Třídy a ukazatelé
Každá třída (a tedy i každá komponenta) je ve skutečnosti
ukazatel na třídu. Překladač automaticky dereferencuje ukazatel třídy za
nás a my o tom nic nevíme. Toto se ale stává důležité, když předáváme třídy
jako parametry. Při předávání třídy je vhodnější použít parametr volaný
hodnotou než odkazem. Třídy jsou ve skutečnosti ukazateli, které jsou již
odkazem. Předáním třídy odkazem je vlastně předáván odkaz na odkaz.
   |
2. OOP pro tvůrce komponent
|