Tvorba komponent pro C++ Builder SimpleEdit

V tomto článku si ukážeme vytvoření komponenty "od základu". Tímto základem bude komponenta TWinControl, která je předkem všech "okenních" komponent, tedy těch, které zapouzdřují okno Windows, tj. mají handle (HWND) proceduru okna a vše, co k oknu patří.
V tomto článku si ukážeme vytvoření komponenty "od základu". Tímto základem bude komponenta TWinControl, která je předkem všech "okenních" komponent, tedy těch, které zapouzdřují okno Windows, tj. mají handle (HWND) proceduru okna a vše, co k oknu patří. Cílem nebude vytvořit nějakou složitou, vysoce sofistikovanou komponentu, ale poznat základní principy vytváření komponent od nižšího základu.

Účel

Jaký může být účel vytváření komponenty tímto způsobem? Myslím, že existují tři hlavní důvody pro vytvoření takové komponenty:
  • chceme ji vytvořit jako společného předka jedné nebo většinou více dalších, sofistikovanějších komponent.
  • vytváříme nějakou speciální komponentu s vlastnostmi zásadně odlišnými od existujících standardních komponent. Může to být například komponenta obsahující více dalších komponent.
  • z důvodů efektivnosti programu, resp. snížení jeho nároků na paměť a systémové zdroje si chceme vytvořit komponentu "na míru", která "umí" pouze to, co budeme potřebovat, a obsahuje minimum dalších "zbytečných" vlastností a funkcí.
Naše komponenta, kterou budeme v tomto článku vytvářet, by mohla spadat do třetí skupiny. Půjde o jednoduchý edit-box, který ovšem bude mít možnost nastavení zarovnání textu kromě standardního levého také vpravo a na střed, což například standardní komponenta TEdit neumí. Dále třeba bude možné nastavit vlastnost "Number", bude tedy akceptovat jako vstupní znaky pouze číslice (což – jestli se nepletu – TEdit též neumí, opravte mě, pokud se mýlím).

Trocha teorie

Když chceme vytvářet komponentu odvozenou od TWinControl, musíme "přepsat" virtuální funkci třídy TWinControl

virtual void __fastcall CreateParams(TCreateParams &Params);

V této funkci určujeme parametry použité při volání funkce CreateWindowsEx, tedy vlastního vytvoření okna, jak napovídá i popis struktury

struct TCreateParams
{
  char *Caption;
  int Style;
  int ExStyle;
  int X;
  int Y;
  int Width;
  int Height;
  HWND WndParent;
  void *Param;
  tagWNDCLASSA WindowClass;
  char WinClassName[64];
} ;
V této přepsané funkci musíme nejdříve zavolat tuto funkci předka, tj. TWinControl, která nám "přednastaví" základní hodnoty parametrů, a poté můžeme modifikovat prvky struktury TCreateParams. Znamená to tedy, že funkce CreateWindowsEx je volána uvnitř VCL, stejně jako zaregistrování třídy okna apod. Na nás je pouze toto přepsání funkce CreateParams. Pokud chceme vytvořit komponentu na základě některého standardního prvku Windows, jako je například edit, list-box, button apod., musíme nejprve zavolat funkci

void __fastcall CreateSubClass(TCreateParams &Params, char * ControlClassName);

která nám vytvoří prvek Windows (window control) příslušné třídy (ControlClassName), což je název jedné ze systémových tříd, již v systému registrovaných.

Jak na to v praxi

Nyní tedy již přistoupíme k tvorbě zmíněné komponenty. Odvodíme ji od TWinControl, nazveme např. TRPJSimpleEdit. Dalším krokem bude přidání několika property a "vypublikování" některých property zděděných od TWinControl. Jak přidat property již umíme z předchozích částí tohoto seriálu, proto zde uvedu pouze výpis části deklarace třídy TRPJSimpleEdit, konkrétně část "published":

__published:
  __property Color;
  __property Font;
  __property ShowHint;
  __property Text;
  __property OnKeyDown;
  __property OnKeyPress;
  __property OnKeyUp;
  // zde můžeme "vypublikovat" podle potřeby
  // další zděděné property a events// nové property
  __property eTextAlign TextAlign
    = { read=FTextAlign, write=SetTextAlign, default=textLeft };
  __property bool LowerCase
    = { read=FLowerCase, write=SetLowerCase, default=false };
  __property bool UpperCase
    = { read=FUpperCase, write=SetUpperCase, default=false };
  __property bool Number
    = { read=FNumber, write=SetNumber, default=false };
  __property bool Password
    = { read=FPassword, write=SetPassword, default=false };
  __property int LimitText
    = { read=FLimitText, write=SetLimitText, default=0 };
  __property bool Multiline
    = { read=FMultiline, write=SetMultiline, default=false };
  __property bool Sizeable
    = { read=FSizeable, write=SetSizeable };
  __property TNotifyEvent OnChange
    = { read=FOnChange, write=FOnChange };

k tomu ještě deklaraci vlastního typu eTextAlign

enum eTextAlign { textLeft, textCenter, textRight };

Výchozí hodnoty nastavíme v konstruktoru třídy:

__fastcall TRPJSimpleEdit::TRPJSimpleEdit(TComponent* Owner)
: TWinControl(Owner)
{
  TextAlign = textLeft;
  Width = 75;
  Height = 20;
  Color = (TColor)GetSysColor(COLOR_WINDOW);
  Password = false;
  LowerCase = false;
  UpperCase = false;
  Multiline = false;
}

Nyní vysvětlení významu použití hodnoty default v deklaraci třídy a nastavení výchozích hodnot v konstruktoru: Pozor! Samotným uvedením hodnoty pomocí default v deklaraci nenastavíme výchozí hodnotu property. Význam použití default je v tom, že pokud v ObjectInspectoru nastavíme hodnotu rovnající se hodnotě uvedené pomocí default, tato hodnota se neuvede v definičním souboru formuláře (.dfm), a tedy nebude ani ve zdrojích (resources) výsledného programu, čímž ušetříme nějaký ten bajt a milisekundu. Naopak výchozí hodnota (která se nám objeví v ObjectInspectoru při přidání komponenty) se nastaví právě uvedeným způsobem v konstruktoru.

Na tomto základě nyní můžeme vytvořit, resp. přepsat virtuální funkci CreateParams, která v případě našeho editu může vypadat třeba takto:

void __fastcall TRPJSimpleEdit::CreateParams(TCreateParams &Params)
{
  TWinControl::CreateParams(Params);
  CreateSubClass(Params, "EDIT");
  Params.Width = Width;
  Params.Height = Height;
  Params.ExStyle = WS_EX_CLIENTEDGE;
  Params.Style |= (ES_AUTOHSCROLL  | ES_AUTOVSCROLL);
  switch ( FTextAlign )
  {
    case textLeft:
      Params.Style |= ES_LEFT;
      break;
    case textCenter:
      Params.Style |= ES_CENTER;
      break;
    case textRight:
      Params.Style |= ES_RIGHT;
      break;
  }
  if ( FLowerCase )
    Params.Style |= ES_LOWERCASE;
  if ( FUpperCase )
    Params.Style |= ES_UPPERCASE;
  if ( FNumber )
    Params.Style |= ES_NUMBER;
  if ( FMultiline )
    Params.Style |= ES_MULTILINE;
  if ( FPassword )
    Params.Style |= ES_PASSWORD;
  if ( FSizeable )
    Params.Style |= WS_SIZEBOX;
}

Jak vidíte, nastavení výsledného stylu edit-boxu spočívá v použití kombinace jednotlivých stylů, jejichž popis lze nalézt v dokumentaci Windows SDK. Těchto stylů ( ES_xxxx) je samozřejmě více a bylo by možné vytvořit stejným způsobem další property, které je budou realizovat. Ale tato ukázková komponenta slouží pouze k "výukovým účelům" a její dotažení do stadia vhodného k praktickému použití je již více méně rutinní záležitostí, samozřejmě na základě toho, co jsme se již naučili a dále naučíme.

Vraťme se k novým property, konkrétně k aplikaci jejich změn pomocí funkcí "Setxxxxx". To jsou funkce, které jsou volány při změně hodnoty property „zvenku“ a jejich „úkolem“ je uložit požadovanou hodnotu do proměnných Fxxxxxx. Když změníme hodnotu některé z těch property, které ovlivňují styl okna, je potřeba tuto změnu stylu aplikovat. Bylo by možné použít funkce SetClassLongPtr nebo SetWindowLongPtr (resp. jejich starší ekvivalenty SetClassLong a SetWindowLong), které umožňují změnit styl okna "za běhu". V některých případech se však tato změna stylu za běhu neprojeví bezprostředně tak, jak bychom potřebovali. Proto se preferuje způsob vyvolat znovu vytvoření okna. Celý proces zrušení a opětovného vytvoření okna za nás "zařídí" VCL, my pouze zavoláme funkci

void __fastcall RecreateWnd(void);

která toto znovuvytvoření provede, a to i v případě, že jsme v návrhovém režimu formuláře. Mimo jiné je znovu volána funkce CreateParams, kde se již odrazí nové hodnoty property. Jako příklad si uvedeme třeba nastavení vlastnosti TextAlign

void __fastcall TRPJSimpleEdit::SetTextAlign(eTextAlign value)
{
  if (FTextAlign != value)
  {
    FTextAlign = value;
    RecreateWnd();
  }
}

Nyní si ještě ukážeme, jak přidat vlastní událost (event) a ve vhodném okamžiku ji vyvolat. V našem případě edit-boxu je asi zřejmé, že budeme chtít dát k dispozici událost, reagující na změnu obsahu edit-boxu. Stejným způsobem, jako když vytváříme běžnou property, vytvoříme property nazvanou OnChange, která bude typu TNotifyEvent.

typedef void __fastcall (__closure *TNotifyEvent)(System::TObject* Sender);

což je typ události, která nemá kromě "odesílatele" žádné další parametry. Stejného typu je také událost OnChange třídy TEdit, jejíž ekvivalent vytváříme.

Čeká nás tedy úkol zachytit v kódu komponenty zprávu oznamující tuto změnu textu edit-boxu. K tomu slouží notifikační zpráva EN_CHANGE, která je v běžném programu pro Windows posílána vlastníku edit-boxu prostřednictvím zprávy WM_COMMAND. Zkušenější programátor by tedy vůbec tuto událost v komponentě nepotřeboval, prostě by si v kódu formuláře vytvořil handler zprávy WM_COMMAND a dále by postupoval obdobně, jak budeme postupovat my. Nejdříve si musíme říci, že v systému zpráv knihovny VCL jsou zprávy jako WM_COMMAND a další posílány zpět komponentě jako zpráva CN_COMMAND s naprosto stejnými parametry jako původní zpráva WM_COMMAND, kterou dostane vlastník edit-boxu. Stejně je tomu s dalšími zprávami typu CN_XXXXXX, což jsou vlastně uživatelsky definované zprávy. Zachycení zprávy CN_COMMAND lze provést v proceduře okna (tedy přepsané virtuální funkci WndProc), jak jsme to realizovali v minulém díle v komponentě "hyperlink". Zde si ukážeme další způsob, a to pomocí mapy zpráv a maker BEGIN_MESSAGE_MAP a END_MESSAGE_MAP. Pokud použijeme k vygenerování ClassExplorer, vybereme příkaz "New Method.." a zobrazený dialog vyplníme třeba takto:

Podívejme se, co nám "wizard" vygeneruje do kódu komponenty. Toto bychom samozřejmě mohli přidat ručně. K ručnímu přidání jenom upřesním, že další handlery budeme přidávat mezi makra BEGIN_MESSAGE_MAP a END_MESSAGE_MAP, která již jsou vytvořena. Přidáme tak pouze makro VCL_MESSAGE_HANDLER. V deklaraci třídy tedy je:

protected:
  BEGIN_MESSAGE_MAP
    VCL_MESSAGE_HANDLER(CN_COMMAND, TMessage, OnCN_COMMAND)
  END_MESSAGE_MAP(TWinControl)
  void __fastcall OnCN_COMMAND(TMessage& Message);

ve vlastním kódu (.cpp) pak bude

void __fastcall TRPJSimpleEdit::OnCN_COMMAND(TMessage& Message)
{
  TWinControl::Dispatch(&Message);
}

Pokud chceme takto zachycenou zprávu předat dále k výchozímu zpracování, musíme zavolat metodu

virtual void __fastcall Dispatch(void *Message);

Je to tedy obdoba volání TWinControl::WndProc(Message), které jsme použili v minulém díle, kde jsme přepsali proceduru okna.

V našem případě chceme zachytávat zprávu EN_CHANGE, jejíž identifikátor je HIWORD parametru wParam, tedy v případě struktury TMessage je to prvek WParamHi. Podíváme se nyní na celý kód handleru a vysvětlíme si podrobnosti:

void __fastcall TRPJSimpleEdit::OnCN_COMMAND(TMessage& Message)
{
  TWinControl::Dispatch(&Message);
  if ( Message.WParamHi == EN_CHANGE )
  {
    if ( FOnChange != NULL )
      FOnChange(this);
  }
}

Vyvolání události provedeme zavoláním FOnChange(this), čímž naplníme parametr Sender třídou komponenty. Testováním na "nenulovou" hodnotu FOnChange zjistíme, zde programátor má vytvořen ve svém kódu handler této události. Kdybychom totiž vyvolali tuto událost "do prázdna", způsobili bychom chybu programu.

V příštím pokračování si tuto komponentu ještě rozšíříme a naučíme se, jak vytvořit a nastavit k vlastní komponentě ikonku (přesněji bitmapu), která ji „reprezentuje“ na paletě komponent.

Zde si můžete nahrát zdrojový kód „dnešní“ komponenty simple_edit.zip

Váš názor Další článek: Excite@Home vyhlašuje bankrot

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