-
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í.
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);
}
}
-
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:
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.
-
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:
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.
-
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.
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.
-
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:
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);
};
-
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.
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.
-
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.
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.
-
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:
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();
}
-
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.
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.
-
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.
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.