niedziela, 2 maja 2010

Delphi i TStringGrid cz.1

Kolejny raz pisząc projekt natrafiłem na seryjnie powtarzające się problemy z StringGridem. Najpopularniejsze braki występujące w komponencie to:

1. Wstawianie kolumny/wiersza
2. Kasowanie kolumny/wiersza
3. Wykrywanie zdarzenia zmiany rozmiaru kolumny/wiersza

Założenia

Dostępne w sieci przykłady nie spełniły moich oczekiwań - skupiają się głównie na pisaniu nowego komponentu udostępniającego jedne z powyższych funkcjonalności. Ja nie lubię specjalnie zaśmiecać palety komponentów, stąd też staram się ograniczać i używać tego co mam.

Poniższe rozwiązania nie wymagają instalowania / tworzenia komponentu a jedynie kilku zmian w kodzie modułu z formą.

Przygotowania

Zanim przystąpimy do omawiania powyższych podpunktów, musimy zastosować technikę zwaną "podrabianiem klasy" (ang. Class Spoofing) - bardzo rzadko stosowana, acz bardzo użyteczna tachnika pozwalająca nam uzyskać dostęp do pól i metod z sekcji protected. Standardowa notacja kodu używana w Delphi nie przewiduje specjalnych oznaczeń, jednak ja konsekwentnie używam przedrostka CS. Należy zadeklarować następujący typ który posłuży nam w dalszej części artykułu:

type // class spoofing
TCS_StringGrid = class(TStringGrid);



1. Wstawianie kolumny/wiersza

W razie potrzeby należy samodzielnie dodać kod zabezpieczający przed wyjściem z zakresu parametrów AColumn i ARow.

procedure TfMain.InsertColumn(AStringGrid: TStringGrid; AColumn: integer);
Begin
with TCS_StringGrid(AStringGrid) do
begin
ColCount := ColCount + 1;
MoveColumn(ColCount - 1, AColumn);
Cols[AColumn].Clear;
end;
end;

procedure TfMain.InsertRow(AStringGrid: TStringGrid; ARow: integer);
begin
with TCS_StringGrid(AStringGrid) do
begin
RowCount := RowCount + 1;
MoveRow(RowCount - 1, ARow);
Rows[ARow].Clear;
end;
end;


2. Kasowanie kolumny/wiersza

procedure TfMain.DeleteColumn(AStringGrid: TStringGrid; AColumn: integer);
begin
with TCS_StringGrid(AStringGrid) do
begin
if ColCount - FixedCols = 1 then
Exit;
DeleteColumn(AColumn);
end;
end;

procedure TfMain.DeleteRow(AStringGrid: TStringGrid; ARow: integer);
begin
with TCS_StringGrid(AStringGrid) do
begin
if RowCount - FixedRows = 1 then
Exit;
DeleteRow(ARow);
end;
end;


Powyższe metody mogą zostać zaimplementowane w klasie formy, lub można w ramach treningu stworzyć klasę pomocniczą dla TStringGrida.

WAŻNE

Jeśli implementowaliśmy zdarzenie przesuwania kolumn/wierszy OnRowMove/OnColumnMoved, to powyższe implementacje mogą być dość problematyczne, ponieważ zostanie wywołany event przesunięcia, pomimo, że takowe "logicznie" nie nastąpiło. Należy się przed tym zabezpieczyć poprzez napisanie metod zawieszających i odwieszających te zdarzenia, oraz wrappery na funkcje dodające/kasujące wiersze/kolumny.

procedure TfMain.LockColRowMoveEvent;
begin
StringGrid1.OnRowMoved := nil;
StringGrid1.OnColumnMoved := nil;
end;

procedure TfMain.UnlockColRowMoveEvent;
begin
StringGrid1.OnRowMoved := StringGridRowMoved;
StringGrid1.OnColumnMoved := StringGridColumnMoved;
end;


a tu mamy wrappery na metody dodawania/kasowania, z których należy korzystać zamiast z InsertColumn, InsertRow, DeleteRow i DeleteColumn w przypadku wystąpienia zdarzeń przesuwania kolumn i wierszy dla danego StringGrida:

procedure TfMain.AddCol(ACol: integer);
begin
LockColRowMoveEvent;

InsertColumn(StringGrid1, ACol);

UnlockColRowMoveEvent;
end;

procedure TfMain.AddRow(ARow: integer);
begin
LockColRowMoveEvent;

InsertRow(StringGrid1, ARow);

UnlockColRowMoveEvent;
end;

procedure TfMain.DelCol(ACol: integer);
begin
LockColRowMoveEvent;

DeleteColumn(StringGrid1, ACol);

UnlockColRowMoveEvent;
end;

procedure TfMain.DelRow(ARow: integer);
begin
LockColRowMoveEvent;

DeleteRow(StringGrid1, ARow);

UnlockColRowMoveEvent;
end;


3. Wykrywanie zdarzenia zmiany rozmiaru kolumny/wiersza

Dla czego? Najodpowiedniejszym wyjaśnieniem jest synchronizacja z innym StringGridem np. znajdującym się nad/pod lub obok.

W przykładzie zostanie pokazana technika wykrywania zdarzenia dla kolumn, w razie potrzeby można dopisać analogiczne metody dla wierszy.

Pierwszym krokiem na drodze do wykrycia zdarzenia jest stworzenie klasy pomocniczej, której metody są używane w dalszym kodzie:

type
TStringGridHelper = class helper for TStringGrid
public
function RightCol: integer;
function BottomRow: integer;
end;

{...}

function TStringGridHelper.BottomRow: integer;
begin
Result := TopRow + VisibleRowCount - 1
end;

function TStringGridHelper.RightCol: integer;
begin
Result := LeftCol + VisibleColCount - 1;
end;


Następnym krokiem jest zadeklarowanie nowej klasy

TStringGridColResize = class(TStringGrid)
protected
procedure ColWidthsChanged; override;
end;


której implementacja wygląda następująco (kiedy chcemy synchronizować z innym StringGridem):

procedure TStringGridColResize.ColWidthsChanged;

procedure AlignColSize(ASource, ADest: TStringGrid);
var
i, RightCol: LongInt;
begin
if ASource.ColCount - ASource.FixedCols <> ADest.ColCount -
ADest.FixedCols then
Exit;

RightCol := ASource.RightCol + 1;
if RightCol = ASource.ColCount then
Dec(RightCol);

for i := ASource.LeftCol to RightCol do
ADest.ColWidths[i + (ADest.FixedCols - ASource.FixedCols)] :=
ASource.ColWidths[i];
end;

begin
if Self = fMain.StringGrid1 then
AlignColSize(fMain.StringGrid1, fMain.StringGrid2)
else if Self = fMain.StringGrid2 then
AlignColSize(fMain.StringGrid2, fMain.StringGrid1);

inherited;
end;


Jak widać nie wykrywamy która konkretnie kolumna podlega zmianie rozmiaru, a jedynie wykrywamy zdarzenie bez danych precyzujących, jak co i o ile.

Można oczywiście wykryć której kolumny rozmiar został zmieniony, poprzez przechowywanie rozmiarów kolumn w osobnej tablicy i porównanie ich z nowymi rozmiarami.

Technika jest wydajna nawet w przypadku dużej liczby kolumn, gdyż sprawdzamy rozmiary kolumn jedynie tych widocznych.

Teraz w zdarzeniu stworzenia formy należy podmienić tablicę VMT dla StringGridów, które mają być podatne na wykrycie zdarzenia zmiany rozmiarów kolumn.

procedure TfMain.FormCreate(Sender: TObject);
begin
// patch dla VMT - potrzebne zdarzenie ColResize
PPointer(StringGrid1)^ := TStringGridColResize;
PPointer(StringGrid2)^ := TStringGridColResize;
end;


Podsumowanie

Zaprezentowane techniki mogą się wydać dość uciążliwe jednak do większości zastosowań są bardzo użyteczne i nie sprawią problemów i co ważniejsze nie trzeba tworzyć/instalować dodatkowych komponentów.

W przypadku intensywnej eksploracji warto się pokusić o stworzenie własnego komponentu :).

[ŚCIĄGNIJ PRZYKŁADOWY PROGRAM]

Pozdrawiam Maciej "HNB" Izak