2. OOP pro tvůrce komponent

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:
 
Úroveň Význam Používá se pro
private Přístupné pouze v kódu v jednotce, kde třída je definována. Skrytí implementačních detailů 
protected Přístupné v kódu v jednotkách, ve kterých třída a její potomci jsou definovány. Definování rozhraní tvůrce komponenty 
public Přístupné ve všem kódu. Definování rozhraní pro běh programu 
__published Přístupné ve všem kódu a z Inspektora objektů. Definování rozhraní pro návrh 
__automated Přístupné ve všem kódu. Je generována informace o typu Automatizace. Pouze pro automatizaci OLE

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