7. Zpracování zpráv

Jedním z klíčů tradičního programování Windows je zpracování zpráv zasílaných z Windows aplikaci. C++ Builder je většinou zpracuje za nás, ale v případě vytváření komponent je možné, že budeme potřebovat zpracovat zprávu, kterou C++ Builder nezpracovává nebo že vytvoříme svou vlastní zprávu a budeme ji potřebovat zpracovat.
V této kapitole se budeme zabývat:

Seznámení se systémem zpracování zpráv

Všechny objekty C++ Builderu mají mechanismus pro zpracování zpráv. Základní myšlenkou zpracování zpráv je to, že objekt přijme zprávu nějakého typu a zpracuje ji voláním jedné z množiny specifikovaných metod závisejících na přijaté zprávě. Neexistuje-li metoda pro jistou zprávu, je použita implicitní obsluha. Následující diagram ukazuje systém zpracování zpráv:
Knihovna VCL definuje systém zpracování zpráv, který překládá všechny zprávy Windows (včetně uživatelem definovaných zpráv) určené jisté třídě na volání metod. Tento mechanismus nebudeme nikdy potřebovat měnit. Budeme potřebovat pouze vytvářet metody zpracování zpráv.
Zpráva Windows je datová struktura, která obsahuje několik důležitých položek. Nejdůležitější z nich je hodnota, která identifikuje zprávu. Windows definuje mnoho zpráv a soubor MESSAGES.HPP deklaruje identifikátory pro všechny z nich.
Programátoři Windows používají při práci definice Windows, které identifikují zprávy, např. WM_COMMAND nebo WM_PAINT. Tradiční programy Windows obsahují proceduru okna, která slouží jako zpětné volání pro zprávy generované systémem. V této proceduře je obvykle rozsáhlý příkaz switch pro rozlišení všech zpráv okna.
Další důležité informace ve zprávě jsou obsaženy ve dvou položkách parametrů a položce výsledku. Jeden parametr je 16 bitový a druhý 32 bitový. Jak často vidíme v kódu Windows, odkazujeme se na tyto hodnoty jako na wParam (word parametr) a lParam (long parametr). Často každý z těchto parametrů obsahuje více než jednu informaci a na časti parametrů se odkazujeme makry jako LOWORD a HIWORD. Např. voláním HIWORD(lParam) získáme vyšší slovo parametru lParam.
Původně si programátoři Windows museli pamatovat, co každý parametr obsahuje. Později Microsoft tyto parametry pojmenoval, což usnadňuje jejich používání. Např. parametr zprávy WM_KEYDOWN se nyní nazývá nVirtKey, což je více informativní než wParam.
C++ Builder zjednodušuje systém zpracování zpráv v několika směrech: Každá komponenta dědí kompletní systém zpracování zpráv. Tento systém má implicitní zpracování. Definujeme pouze obsluhy pro zprávy, na které chceme reagovat specificky. Můžeme modifikovat pouze malé části zpracování zpráv a pro většinu zpracování použít zděděné metody.
Značnou výhodou tohoto systému zpracování zpráv je, že můžeme bezpečně zasílat kdykoli libovolnou zprávu libovolné komponentě. Jestliže komponenta nemá pro zprávu definovanou obsluhu, pak je použita implicitní obsluha, což obvykle ignoruje zprávu.
C++ Builder registruje metodu nazvanou MainWndProc jako proceduru okna po každý typ komponenty v aplikaci. MainWndProc obsahuje blok zpracování výjimek, předávající záznam zprávy z Windows virtuální metodě nazvané WndProc a zpracovávající libovolné výjimky voláním metody HandleException objektu aplikace. MainWndProc je nevirtuální metoda, která neobsahuje speciální zpracování některých zpráv. Přizpůsobení provádíme až v WndProc, neboť každý typ komponenty může předefinovat tuto metodu podle svých potřeb.
Metody WndProc testují zda zpracovávání nemá ignorovat některé neočekávané zprávy. Např. TWinControl během tažení komponenty ignorují události klávesnice. WndProc předává události klávesnice pouze když komponenta není tažena. Konečně WndProc volá Dispatch, nevirtuální metodu zděděnou od TObject, určující která metoda bude volána k obsluze zprávy.
Dispatch používá položku Msg záznamu zprávy k určení jak zpracovat jistou zprávu. Jestliže komponenta pro zprávu nemá obsluhu pak Dispatch volá DefaultHandler.

Změna zpracování zpráv

Před změnou zpracování zpráv naší komponenty se ujistíme, že to skutečně musíme udělat. C++ Builder překládá mnoho zpráv Windows na události, které jak tvůrce komponenty, tak i uživatel komponenty může obsloužit. Lépe než měnit chování zpracování zpráv je měnit chování zpracování událostí. Ke změně zpracování zprávy přepíšeme metodu zpracovávající zprávu. Můžeme také zabránit komponentě ve zpracování zprávy při jistých situacích zachycením zprávy.
Pro změnu způsobu zpracování jisté zprávy komponentou přepíšeme metodu zpracování zprávy pro tuto zprávu. Jestliže komponenta nemá obsluhu pro jistou zprávu, musíme deklarovat novou metodu obsluhy zprávy. K přepsání metody zpracování zpráv, deklarujeme novou metodu v chránění části naši komponenty a to se stejným jménem jako má metoda kterou přepisujeme a mapujeme metodu na zprávu pomocí tří maker. Tato makra mají tvar:
BEGIN_MESSAGE_MAP
  MESSAGE_HANDLER(parametr1, parametr2, parametr3)
END_MESSAGE_MAP
Parametr1 je index zprávy definovaný Windows, parametr2 je typ struktury zprávy a parametr3 je jméno metody zprávy. Mezi BEGIN_MESSAGE_MAP a END_MESSAGE_MAP můžeme vložit několik maker MESSAGE_HANDLER.
Např. k přepsání obsluhy zprávy WM_PAINT v komponentě, opětovně deklarujeme metodu WMPaint a třemi makry mapujeme metodu na zprávu WM_PAINT:
class PACKAGE TMojeKomponenta : public TComponent
{
protected:
  void __fastcall WMPaint(TWMPaint *Message);
BEGIN_MESSAGE_MAP
  MESSAGE_HANDLER(WM_PAINT, TWMPaint, WMPaint)
END_MESSAGE_MAP(TComponent)
};
Pouze uvnitř metody zpracování zprávy má naše komponenta přístup ke všem parametrům záznamu zprávy. Jelikož zpráva je vždy parametr volaný odkazem, obsluha může změnit v případě potřeby hodnoty parametrů. Často měníme pouze parametr návratové hodnoty zprávy: hodnotu vrácenou voláním SendMessage, která zasílá zprávu.
Protože se typ parametru Message metody zpracování zprávy mění s typem zpracovávané zprávy, je nutné se podívat do dokumentace Windows na jména a význam jednotlivých parametrů. Jestliže se z nějakého důvodu potřebujeme odkazovat na parametr zprávy jeho starým stylem jmen (wParam, lParam apod.), můžeme přetypovat Message na obecný typ TMessage, který používá tato jména parametrů.
V jistých situacích, můžeme chtít, aby naše komponenta ignorovala jisté zprávy. Tj. můžeme chtít zabránit komponentě ve zpracování zprávy svou obsluhou. Zachycení zprávy provedeme přepsáním virtuální metody WndProc. Metoda WndProc filtruje zprávy před jejich předáním metodě Dispatch, která určuje metodu zpracující zprávu. Přepsáním WndProc, můžeme změnit filtr zpráv před jejich zpracováním. Přepsání WndProc provádíme takto:
void __fastcall TMujOvladac::WndProc(TMessage* Message)
{
  // test na určení, zda pokračovat ve zpracování
  TWinControl->WndProc(Message);
}
Následuje část metody WndProc pro TControl jak je implementována ve VCL v Object Pascalu. TControl definuje rozsah zpráv myši, které jsou filtrovány, když uživatel provádí tažení. Přepsání WndProc tomu pomáhá dvěma způsoby: Můžeme filtrovat interval zpráv namísto specifikování obsluhy pro každou z nich a můžeme zabránit zpracování zpráv v celku a obsluhy nejsou nikdy volány.
procedure TControl.WndProc(var Message: TMessage);
begin
  ...
  if (Message.Msg>=WM_MOUSEFIRST)and(Message.Msg<=WM_MOUSELAST) then
    if Dragging then    {speciální zpracování tažení}
      DragMouseMsg(TWMouse(Message))
    else
      ...      {normální zpracování}
    end;
  ...      {jinak normální proces}
end;

Vytváření nových obsluh zpráv

Přestože C++ Builder poskytuje obsluhy pro mnoho zpráv Windows, můžeme se dostat do situace, kdy budeme potřebovat vytvořit novou obsluhu zpráv a to když definujeme svou vlastní zprávu. Práce s uživatelskými zprávami má dva aspekty: definování své vlastní zprávy a deklarování nové metody zpracování zprávy.
Několik standardních komponent definuje zprávy pro interní použití. Smyslem pro definování zpráv je vysílání informací nepodporovaných standardními zprávami Windows a oznámení změny stavu. Definování zprávy je dvou krokový proces: deklarujeme identifikátor zprávy a deklarujeme typ záznamu zprávy.
Identifikátor zprávy je celočíselná konstanta. Windows rezervuje zprávy do 1024 pro svoje vlastní použití a tak když deklarujeme svou vlastní zprávu musíme začít nad touto úrovní. Konstanta WM_APP reprezentuje počáteční číslo pro uživatelem definované zprávy. Když definujeme identifikátory zpráv, musíme začít od WM_APP. Musíme si být vědomi, že některé standardní ovladače Windows používají zprávy v rozsahu uživatelských definic. Jsou to seznamy, kombinovaná okna, editační okna a tlačítka. Jestliže odvozujeme komponentu od některé z nich a chceme definovat novou zprávu pro ní, je potřeba se podívat do souboru MESSAGES.HPP a zjistit, které zprávy Windows jsou skutečně definované pro tyto ovladače.
Následující kód ukazuje dvě uživatelem definované zprávy:
#define WM_MOJEPRVNIZPRAVA (WM_APP + 400)
#define WM_MOJEDRUHAZPRAVA (WM_APP + 401)
Jestliže chceme dát smysluplná jména parametrům naší zprávy, je potřeba deklarovat typ struktury zprávy pro tuto zprávu. Struktura zprávy je typ předávaného parametru metodě zpracování zprávy. Jestliže nepoužíváme parametr zprávy nebo jestliže chceme použít starý způsob zápisu parametrů (wParam, lParam apod.), můžeme použít implicitní záznam zprávy TMessage. Při deklaraci typu struktury zprávy používáme tyto konvence: Jméno typu záznamu vždy začíná T a následuje jméno zprávy. Jméno první položky v záznamu je Msg a je typu TMsgParam. Definujeme další dvě slabiky, které odpovídají wParam (další dvě slabiky jsou nepoužity). Definujeme další čtyři slabiky, které odpovídají lParam. Nakonec přidáme položku nazvanou Result, která je typu Longint.
Následuje záznam zpráv pro všechny zprávy myši, TWMKey:
struct TWMKey
{
  Cardinal Msg;       // první parametr je identifikátor zprávy
  Word CharCode;      // toto je wParam
  Word Unused;
  Longint KeyData;    // toto je lParam
  Longint Result;     // toto je datová složka Result
};
Jsou dvě situace, které vyžadují deklarování nové metody zpracování zprávy: naše komponenta vyžaduje zpracování zprávy Windows, která není zpracovávána standardními komponentami a definování své vlastní zprávy pro použití v našich komponentách.
Deklaraci metody zpracování zprávy provedeme takto: Deklarujeme metodu v chráněné části deklarace třídy komponenty. Ujistíme se, že metoda vrací void. Nazveme metodu po zpracovávané zprávě, ale bez znaků podtržení. Předáváme ukazatel nazvaný Message, typu struktury zprávy. Mapujeme metodu na zprávu použitím maker. Zapíšeme kód pro specifické zpracování v komponentě. Voláme zděděnou obsluhu zprávy.
Následuje deklarace obsluhy zprávy pro uživatelem definovanou zprávu nazvanou CM_CHANGECOLOR:
#define CM_CHANGECOLOR (WM_APP + 400)
class PACKAGE TMojeKomponenta : public TControl
{
protected:
  void __fastcall CMChangeColor(TMessage &Message);
BEGIN_MESSAGE_MAP
  MESSAGE_HANDLER(CM_CHANGECOLOR, TMessage, CMChangeColor)
END_MESSAGE_MAP(TComtrol)

void __fastcall TMojeKomponenta::CMChangeColor(TMessage &Message)
{
  Color = Message.LParam;
  TControl::CMChangeColor(Message);
}


  1. Začneme s vývojem další komponenty. C++ Builder poskytuje několik typů abstraktních komponent, které můžeme použít jako základ pro přizpůsobování komponent. V tomto bodě si ukážeme jak vytvořit malý jednoměsíční kalendář na základě komponenty mřížky TCustomGrid. Vytvoření kalendáře provedeme v sedmi krocích: vytvoření a registrace komponenty, zveřejnění zděděných vlastností, změnu inicializačních hodnot, změna velikosti buněk, vyplnění buněk, navigaci měsíců a roků a navigaci dní.

  2. Vytvořená komponenta se bude podobat komponentě TCalendar ze stránky Samples Palety komponent.
    Použijeme ruční postup vytváření a registrace komponenty s těmito specifikami: jednotku komponenty nazveme CalSamp, odvodíme nový typ komponenty nazvaný TSampleCalendar od TCustomGrid a registrujeme TSampleCalendar na stránce Samples Palety komponent. Výsledkem této práce je (musíme přidat i hlavičkový soubor Grids.hpp):
    #ifndef CALSAMPH
    #define CALSAMPH
    #include <vcl\sysutils.hpp>
    #include <vcl\controls.hpp>
    #include <vcl\classes.hpp>
    #include <vcl\forms.hpp>
    #include <vcl\grids.hpp>
    class PACKAGE TSampleCalendar : public TCustomGrid
    {
    private:
    protected:
    public:
    __published:
    };
    #endif
    Soubor CPP vypadá takto:
    #include <vcl\vcl.h>
    #pragma hdrstop
    #include "CALSAMP.h"
    #pragma package(smart_init);
    static inline TSampleCalendar *ValidCtrCheck()
    {
      return new TSampleCalendar(NULL);
    }
    namespace Calsamp
    {
      void __fastcall PACKAGE Register()
      {
        TComponentClass classes[1] = {__classid(TSampleCalendar)};
        RegisterComponents("Samples", classes, 0);
      }
    }
  3. Abstraktní komponenta mřížky TCustomGrid poskytuje velký počet chráněných vlastností. Můžeme zvolit, které z těchto vlastností chceme zpřístupnit v naši vytvářené komponentě. K zveřejnění zděděných chráněných vlastností, opětovně deklarujeme vlastnosti ve zveřejňované části deklarace naši komponenty. Pro kalendář zveřejníme následující vlastnosti a události:

  4. class PACKAGE TSampleCalendar : public TCustomGrid
    {
    __published:
      __property Align;
      __property BorderStyle;
      __property Color;
      __property Ctl3D;
      __property Font;
      __property GridLineWidth;
      __property ParentColor;
      __property ParentCtl3D;
      __property ParentFont;
      __property OnClick;
      __property OnDblClick;
      __property OnDragDrop;
      __property OnDragOver;
      __property OnEndDrag;
      __property OnKeyDown;
      __property OnKeyPress;
      __property OnKeyUp;
    };
    Existuje ještě několik dalších vlastností, které jsou také zveřejňované, ale které pro kalendář nepotřebujeme. Příkladem je vlastnost Options, která umožňuje uživateli např. volit typ mřížky.
  5. Kalendář je mřížka s pevným počtem řádků a sloupců, i když ne všechny řádky vždy obsahují data. Z tohoto důvodu, jsme nezveřejnili vlastnosti mřížky ColCount a RowCount, neboť je jasné, že uživatel kalendáře nebude chtít zobrazovat nic jiného než sedm dní v týdnu. Nicméně musíme nastavit počáteční hodnoty těchto vlastností tak, aby týden měl vždy sedm dní. Ke změně počátečních hodnot vlastností komponenty, přepíšeme konstruktor a nastavíme požadované hodnoty. Musíme také předefinovat čirou metodu DrawCell. Dostaneme toto:

  6. class PACKAGE TSampleCalendar : public TCustomGrid
    {
    protected:
      virtual void __fastcall DrawCell(long ACol, long ARow,
                          const Windows::TRect &Rect, TGridDrawState AState);
    public:
      __fastcall TSampleCalendar(TComponent* Owner);
    };
    Do souboru CPP zapíšeme konstruktor:
    __fastcall TSampleCalendar::TSampleCalendar(TComponent* Owner)
     : TCustomGrid(Owner)
    {
      ColCount = 7;
      RowCount = 7;
      FixedCols = 0;
      FixedRows = 1;
      ScrollBars = ssNone;
      Options = (Options >> goRangeSelect) << goDrawFocusSelected;
    }
    void __fastcall TSampleCalendar::DrawCell(long ACol, long ARow,
                          const Windows::TRect &Rect, TGridDrawState AState)
    {
    }
    Nyní, kalendář má sedm řádků a sedm sloupců s pevným horním řádkem. Pravděpodobně budeme chtít změnit velikost ovladače a udělat všechny buňky viditelné. Dále si ukážeme jak reagovat na zprávu změny velikosti od Windows k určení velikosti buněk.
  7. Když uživatel nebo aplikace změní velikost okna nebo ovladače, Windows zasílá zprávu nazvanou WM_SIZE změněnému oknu nebo ovladači, které tak může nastavit svůj obraz na novou velikost. Naše komponenta může reagovat na tuto zprávu změnou velikosti buněk a zaplnit tak celou plochu ovladače. K reakci na zprávu WM_SIZE, přidáme do komponenty metodu reagující na zprávu.

  8. V našem případě ovladač kalendáře vyžaduje k reakci na WM_SIZE přidat chráněnou metodu nazvanou WMSize řízenou indexem zprávy WM_SIZE, a zapsat metodu, která vypočítá potřebné rozměry buněk, což umožní aby všechny buňky byly viditelné:
    class PACKAGE TSampleCalendar : public TCustomGrid
    {
    protected:
      void __fastcall WMSize(TWMSize& Message);
      BEGIN_MESSAGE_MAP
        MESSAGE_HANDLER(WM_SIZE, TWMSize, WMSize);
      END_MESSAGE_MAP(TCustomGrid);
    };

    void __fastcall TSampleCalendar::WMSize(TWMSize &Message)
    {
      int GridLines;
      GridLines = 6 * GridLineWidth;
      DefaultColWidth   = (Message.Width - GridLines) / 7;
      DefaultRowHeight  = (Message.Height - GridLines) / 7;
    }
    Nyní, jestliže přidáme kalendář na formulář a změníme jeho velikost, je vždy zobrazen tak, aby jeho buňky úplně zaplnili plochu ovladače. Zatím ale kalendář neobsahuje data.

  9. Ovladač mřížky je zaplňován buňku po buňce. V případě kalendáře to znamená vypočítat datum (je-li) pro každou buňku. Implicitní zaplňování buněk provádíme virtuální chráněnou metodou DrawCell. Knihovna běhu programu obsahuje pole s krátkými jmény dní a my je použijeme v nadpisu každého sloupce:

  10. class PACKAGE TSampleCalendar : public TCustomGrid
    {
    protected:
      virtual void __fastcall DrawCell(Longint ACol, Longint ARow,
                              const TRect &ARect, TGridDrawState AState);
    };

    void __fastcall TSampleCalendar::DrawCell(long ACol, long ARow,
                                 const TRect &ARect, TGridDrawState AState)
    {
      String TheText;
      int TempDay;
      if (ARow == 0) TheText = ShortDayNames[ACol+1];
      else
      {
        TheText = "";
        TempDay = DayNum(ACol, ARow);
        if (TempDay != -1) TheText = IntToStr(TempDay);
      }
      Canvas->TextRect(ARect, ARect.Left + (ARect.Right - ARect.Left ?
        Canvas->TextWidth(TheText)) / 2, ARect.Top + (ARect.Bottom ?
        ARect.Top - Canvas->TextHeight(TheText)) / 2, TheText);
    };

  11. Pro ovladač kalendáře je vhodné, aby uživatel a aplikace měli mechanismus pro nastavování dne, měsíce a roku. C++ Builder ukládá datum a čas v proměnné typu TDateTime. TDateTime je zakódovaná číselná reprezentace datumu a času, která je vhodná pro počítačové zpracování, ale není použitelná pro použití člověkem. Můžeme tedy ukládat datum v zakódovaném tvaru, poskytnout přístup k této hodnotě při běhu aplikace, ale také poskytnout vlastnosti Day, Month a Year, které uživatel komponenty může nastavit při návrhu.

  12. K uložení datumu pro kalendář, potřebujeme soukromou položku k uložení datumu a vlastnosti běhu programu, které poskytují přístup k tomuto datumu. Přidání interního datumu ke kalendáři vyžaduje tři kroky: V prvním deklarujeme soukromou položku k uložení datumu:
    class PACKAGE TSampleCalendar : public TCustomGrid
    {
    private:
      TDateTime FDate;
    };
    V druhém inicializujeme datumovou položku v konstruktoru:
    __fastcall TSampleCalendar::TSampleCalendar(TComponent* Owner)
     : TCustomGrid(Owner)
    {
      ...
      FDate = CurrentDate();
    }
    V posledním deklarujeme vlastnost běhu programu k poskytnutí přístupu k zakódovanému datumu. Potřebujeme metodu pro nastavení datumu, protože nastavení datumu vyžaduje aktualizaci obrazu ovladače na obrazovce:
    class PACKAGE TSampleCalendar : public TCustomGrid
    {
    private:
      void __fastcall SetCalendarDate(TDateTime Value);
    public:
      __property TDateTime CalendarDate={read=FDate,write=SetCalendarDate,nodefault};
    };

    void __fastcall TSampleCalendar::SetCalendarDate(TDateTime Value)
    {
      FDate = Value;
      Refresh();
    }
    Zakódované datum je vhodné pro aplikaci, ale lidé dávají přednost práci s dny, měsíci a roky. Můžeme poskytnout alternativní přístup k těmto prvkům uloženého zakódovaného datumu vytvořením vlastností. Protože každý prvek datumu (den, měsíc a rok) je celé číslo a jelikož nastavení každého z nich vyžaduje dekódování datumu, můžeme se vyhnout opakování kódu sdílením implementačních metod pro všechny tři vlastnosti. Tj. můžeme zapsat dvě metody, první pro čtení prvku a druhou pro jeho zápis, a použít tyto metody k získání a nastavování všech tří vlastností. Deklarujeme tři vlastnosti a každé přiřadíme jedinečný index:
    class PACKAGE TSampleCalendar : public TCustomGrid
    {
    public:
      __property int Day = {read=GetDateElement, write=SetDateElement,
                            index=3, nodefault};
      __property int Month = {read=GetDateElement, write=SetDateElement,
                              index=2, nodefault};
      __property int Year = {read=GetDateElement, write=SetDateElement,
                             index=1, nodefault};
    Dále zapíšeme deklarace a definice přístupových metod, pracujících s hodnotami podle hodnoty indexu:
    class PACKAGE TSampleCalendar : public TCustomGrid
    {
    private:
      int __fastcall GetDateElement(int Index);
      void __fastcall SetDateElement(int Index, int Value);
    }

    int __fastcall TSampleCalendar::GetDateElement(int Index)
    {
      unsigned short AYear, AMonth, ADay;
      int result;
      FDate.DecodeDate(&AYear, &AMonth, &ADay);
      switch (Index) {
        case 1: result = AYear; break;
        case 2: result = AMonth; break;
        case 3: result = ADay; break;
        default: result = -1;
      }
      return result;
    }

    void __fastcall TSampleCalendar::SetDateElement(int Index, int Value)
    {
      unsigned short AYear, AMonth, ADay;
      if (Value > 0) {
        FDate.DecodeDate(&AYear, &AMonth, &ADay);
        switch (Index) {
          case 1: AYear = Value; break;
          case 2: AMonth = Value; break;
          case 3: ADay = Value; break;
          default: return;
        }
      }
      FDate = TDateTime(AYear, AMonth, ADay);
      Refresh();
    }
    Nyní můžeme nastavovat den, měsíc a rok kalendáře během návrhu použitím Inspektora objektů a při běhu aplikace použitím kódu. Zatím ještě ale nemáme přidaný kód pro zápis datumu do buněk.

  13. Přidání čísel do kalendáře vyžaduje několik úvah. Počet dní v měsíci závisí na tom, o který měsíc se jedná a zda daný rok je přestupný. Dále měsíce začínají v různém dni v týdnu, v závislosti na měsíci a roku. V předchozí části je popsáno jak získat aktuální měsíc a rok. Nyní můžeme určit, zda specifikovaný rok je přestupný a počet dní v měsíci. K tomuto použijeme funkci IsLeapYear a pole MonthDays z hlavičkového souboru SysUtils.

  14. Když již máme informace o přestupných rocích a dnech v měsíci, můžeme vypočítat, kde v mřížce je konkrétní datum. Výpočet je založen na dni v týdnu, kdy měsíc začíná. Protože potřebujeme ofset měsíce pro každou buňku, je praktičtější jej vypočítat pouze, když měníme měsíc nebo rok. Tuto hodnotu můžeme uložit v položce třídy a aktualizovat ji při změně datumu. Zaplnění dnů do příslušných buněk provedeme takto: Přidáme položku ofsetu měsíce a metodu aktualizující hodnotu položky v objektu:
    class PACKAGE TSampleCalendar : public TCustomGrid
    {
    private:
      int FMonthOffset;
    protected:
      virtual void __fastcall UpdateCalendar();
    };

    void __fastcall TSampleCalendar::UpdateCalendar()
    {
      unsigned short AYear, AMonth, ADay;
      TDateTime FirstDate;
      if ((int)FDate != 0) {
        FDate.DecodeDate(&AYear, &AMonth, &ADay);
        FirstDate = TDateTime(AYear, AMonth, 1);
        FMonthOffset = 2- FirstDate.DayOfWeek();
      }
      Refresh();
    }
    Přidáme příkazy do konstruktoru a metod SetCalendarDate a SetDateElement, které volají novou aktualizační metodu při změně data.
    __fastcall TSampleCalendar::TSampleCalendar(TComponent* Owner)
     : TCustomGrid(Owner)
    {
      ...
      UpdateCalendar();
    }
    void __fastcall TSampleCalendar::SetCalendarDate(TDateTime Value)
    {
      FDate = Value;
      UpdateCalendar();
    }
    void __fastcall TSampleCalendar::SetDateElement(int Index, int Value)
    {
      ...
      FDate = TDateTime(AYear, AMonth, ADay);
      UpdateCalendar();
    }
    Přidáme ke kalendáři metodu, která vrací číslo dne, když předáme souřadnice řádku a sloupce buňky:
    int __fastcall TSampleCalendar::DayNum(int ACol, int ARow)
    {
      int result = FMonthOffset + ACol + (ARow - 1) * 7;
      if ((result < 1) || (result > MonthDays[IsLeapYear(Year)][Month])) result = -1;
      return result;
    }
    Nesmíme zapomenou přidat deklaraci DayNum do deklarace třídy komponenty.
    Nyní, když již víme ve které buňce které datum je, můžeme doplnit DrawCell k plnění buňky datem:
    void __fastcall TSampleCalendar::DrawCell(long ACol, long ARow,
                               const TRect &ARect, TGridDrawState AState)
    {
      String  TheText;
      int TempDay;
      if (ARow == 0) TheText = ShortDayNames[ACol+1];
      else {
        TheText = "";
        TempDay = DayNum(ACol, ARow);
        if (TempDay != -1) TheText = IntToStr(TempDay);
      }
      Canvas->TextRect(ARect, ARect.Left + (ARect.Right - ARect.Left ?
        Canvas->TextWidth(TheText)) / 2, ARect.Top + (ARect.Bottom ?
        ARect.Top - Canvas->TextHeight(TheText)) / 2, TheText);
    };
    Jestliže nyní opětovně instalujeme komponentu a umístíme ji na formulář, vidíme správné informace pro současný měsíc.

  15. Když již máme čísla v buňkách kalendáře, je vhodné přesunout vybranou buňku na buňku se současným datumem. Je tedy potřeba nastavit vlastnosti Row a Col, když vytváříme kalendář a když změníme datum. K nastavení výběru na tento den, změníme metodu UpdateCalendar tak, aby nastavila obě vlastnosti před voláním Refresh:

  16. void __fastcall TSampleCalendar::UpdateCalendar()
    {
      unsigned short AYear, AMonth, ADay;
      TDateTime FirstDate;
      if ((int)FDate != 0) {
        FDate.DecodeDate(&AYear, &AMonth, &ADay);
        FirstDate = TDateTime(AYear, AMonth, 1);
        FMonthOffset = 1- FirstDate.DayOfWeek();
        Row = (ADay - FMonthOffset) / 7 + 1;
        Col = (ADay - FMonthOffset) % 7;
      }
      Refresh();
    }
  17. Vlastnosti jsou užitečné pro manipulace s komponentami, obzvláště během návrhu. Jsou ale typy manipulací, které často ovlivňují více než jednu vlastnost, a je tedy užitečné pro ně vytvořit metodu. Příkladem takového manipulace je služba kalendáře ?následující měsíc?. Zpracování měsíce v rámci měsíců a případná inkrementace roku je jednoduchá, ale velmi výhodná pro vývojáře používající komponentu. Jedinou nevýhodou zaobalení manipulací do metody je, že metody jsou přístupné pouze za běhu aplikace.

  18. Pro kalendář přidáme následující čtyři metody pro následující a předchozí měsíc i rok:
    void __fastcall TSampleCalendar::NextMonth()
    {
      DecodeDate(IncMonth(CalendarDate, 1), Year, Month, Day);
    }
    void __fastcall TSampleCalendar::PrevMonth()
    {
      DecodeDate(IncMonth(CalendarDate, -1), Year, Month, Day);
    }
    void __fastcall TSampleCalendar::NextYear()
    {
      DecodeDate(IncMonth(CalendarDate, 12), Year, Month, Day);
    }
    void __fastcall TSampleCalendar::PrevYear()
    {
      DecodeDate(IncMonth(CalendarDate, -12), Year, Month, Day);
    }
    Musíme také přidat deklarace nových metod k deklaraci třídy kalendáře. Nyní, když vytváříme aplikaci, která používá komponentu kalendáře, můžeme snadno implementovat procházení přes měsíce nebo roky.
  19. K daném měsíci jsou možné dva způsoby navigace přes dny. První je použití kurzorových kláves a druhý je reakce na kliknutí myši. Standardní komponenta mřížky zpracovává oboje jako kliknutí. Tj. použití kurzorové klávesy je chápáno jako kliknutí na odpovídající buňku.

  20. Zděděné chování mřížky zpracovává přesun výběru v reakci na stisknutí kurzorové klávesy nebo kliknutí, ale jestliže chceme změnit vybraný den, musíme toto implicitní chování modifikovat. K obsluze přesunu v kalendáři, přepíšeme metodu Click mřížky. Když přepisujeme metodu jako je Click, musíme vždy vložit volání zděděné metody, a neztratit tak standardní chování. Následuje přepsaná metoda Click pro mřížku kalendáře. Nesmíme zapomenout přidat deklaraci Click do TSampleCalendar:
    void __fastcall TSampleCalendar::Click()
    {
      TCustomGrid::Click();
      int TempDay = DayNum(Col, Row);
      if (TempDay != -1) Day = TempDay;
    }
    Nyní, když uživatel může změnit datum v kalendáři, musíme zajistit, aby aplikace mohla reagovat na tuto změnu. Do TSampleCalendar přidáme událost OnChange. Musíme deklarovat událost, položku k uložení události a virtuální metodu k volání události:
    class PACKAGE TSampleCalendar : public TCustomGrid
    {
    private:
      TNotifyEvent FOnChange;
    protected:
      virtual void __fastcall Change();
    __published:
      __property TNotifyEvent OnChange = {read=FOnChange, write=FOnChange};
    };
    Dále zapíšeme metodu Change:
    void __fastcall TSampleCalendar::Change()
    {
      if (FOnChange != NULL) FOnChange(this);
    }
    Na konec metod SetCalendarDate a SetDateElement musíme ještě přidat příkaz volání metody Change:
    void __fastcall TSampleCalendar::SetCalendarDate(TDateTime Value)
    {
      FDate = Value;
      UpdateCalendar();
      Change();
    }
    void __fastcall TSampleCalendar::SetDateElement(int Index, int Value)
    {
      ...
      FDate = TDateTime(AYear, AMonth, ADay);
      UpdateCalendar();
      Change();
    }
    Aplikace používající komponentu může nyní reagovat na změny data komponenty připojením obsluhy k události OnChange.
    Když přecházíme po dnech v kalendáři, zjistíme nesprávné chování při výběru prázdné buňky. Kalendář umožňuje přesunutí na prázdnou buňku, ale nemění datum v kalendáři. Nyní zakážeme výběr prázdných buněk. K určení, zda daná buňka je vybíratelná, předefinujeme metodu SelectCell mřížky. SelectCell je funkce, která jako parametry přebírá číslo řádku a sloupce a vrací logickou hodnotu indikující zda specifikovaná buňka je vybíratelná. Metoda SelectCell bude nyní vypadat takto:
    bool __fastcall TSampleCalendar::SelectCell(long ACol, long ARow)
    {
      if (DayNum(ACol, ARow) == -1) return false;
      else return TCustomGrid::SelectCell(ACol, ARow);
    }
    Tím jsme dokončili tvorbu naší komponenty.
7. Zpracování zpráv