StringGrid

  Komponent StringGrid wyświetla fragment arkusz kalkulacyjnego, lecz nie wykonuje on żadnych obliczeń, a jedynie wyświetla siatkę złożoną z komórek, w których można umieszczać dane w postaci łańcuchów znakowych. Zawiera jedną komórkę wyróżnioną, którą za pomocą klawiszy strzałek albo myszy można przemieszczać wewnątrz arkusza. Przykład zastosowania komponentu StringGrid znajduje się w dziale: kursy praktyczne - baza danych.

Menu

  1. Włączanie edycji komórek obiektu StringGrid.

  2. Wprowadzanie tekstu do komórek obiektu StringGrid.

  3. Wyrównywanie tekstu w komórkach obiektu StringGrid.

  4. Zmiana koloru czcionki dla wybranej komórki obiektu StringGrid.

  5. Zmiana stylu czcionki dla wybranej komórki obiektu StringGrid.

  6. Określanie koloru dla wybranej komórki obiektu StringGrid.

  7. Wypełnianie grafiką wybranej komórki obiektu StringGrid.

  8. Przesuwanie wierszy i kolumn.

  9. Nakładanie maski.

  10. Wstawianie obiektu ComboBox do obiektu StringGrid.

  11. Wyróżnianie pojedynczej komórki, czyli rysowanie ramki wokół komórki.

  12. Jak automatycznie dopasować szerokość kolumny w StringGrid.

  13. Zapisywanie i odczytywanie zawartości obiektu StringGrid.

  14. Określanie numeru kolumny i wiersza obiektu StringGrid na który wskazuje kursor Myszy.

  15. Drukowanie zawartości tabeli StringGrid.

  16. Usuwanie wybranego wiersza lub kolumny w tabeli StringGrid.

  17. Zaznaczanie wiersza lub kolumny poprzez kliknięcie na nagłówku tabeli StringGrid.

  18. Sortowanie tabeli StringGrid.

  19. Dodawanie wiersza lub kolumny do tabeli StringGrid.

  20. Kontekstowe wyszukiwanie tekstu w tabeli StringGrid.

  21. Zawijanie tekstu w komórkach tabeli StringGrid.

  22. Sortowanie bąbelkowe tabeli.


Włączanie edycji komórek obiektu StringGrid.

  Po umieszczeniu komponentu StringGrid na formularzu i skompilowaniu programu okaże się, że nie można wprowadzać do siatki tekstu. Dlaczego? Dlatego, że domyślnie funkcja ta jest wyłączona, żeby ją włączyć należy zmienić właściwość obiektu Options | goEditing na true:

Rozmiar: 13468 bajtów


Można również ustawić na true właściwość Options | goAlwaysShowEditor, umożliwi to płynne przemieszczanie się między komórkami przy użyciu tylko strzałek, jeżeli właściwość goAlwaysShowEditor jest ustawiona na false to po wprowadzeniu tekstu do komórki trzeba będzie nacisnąć klawisz Enter, żeby przejść do następnej komórki.
Edycję komórek można również włączać wewnątrz kodu, posługując się np. przyciskiem Button1:

// Plik źródłowy np. Unit1.cpp
//-- Włączenie edycji komórek  <--!!!
void __fastcall TForm1::Button1Click(TObject *Sender)
{
 StringGrid1->Options = StringGrid1->Options << goEditing;
}
//--------------------------------
//-- Wyłączenie edycji komórek  <--!!!
void __fastcall TForm1::Button1Click(TObject *Sender)
{
 StringGrid1->Options = StringGrid1->Options >> goEditing;
}
//--------------------------------

...powrót do menu. 


Wprowadzanie tekstu do komórek obiektu StringGrid.

  Każda komórka obiektu StringGrid posiada własny adres, który jest określany za pomocą dwóch współrzędnych pierwsza określa kolumnę a druga wiersz. Adres komórki podaję się za pośrednictwem właściwości Cells, żeby wprowadzić tekst do komórki należy podać jej adres i przypisać łańcuch znaków:


// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
 StringGrid1->Cells[1][2] = "Cyfrowy Baron";
}
//--------------------------------


W podanym przykładzie tekst "Cyfrowy Baron" zostanie wpisany do komórki znajdującej się na przecięciu kolumny pierwszej i drugiego wiersza. W podobny sposób można pobierać tekst z komórek:

// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
 Edit1->Text = StringGrid1->Cells[1][2];
 String tekst = StringGrid1->Cells[2][3];
}
//--------------------------------


...powrót do menu. 


Wyrównywanie tekstu w komórkach obiektu StringGrid.

  W obiekcie StringGrid tekst wprowadzany do komórek jest zawsze wyrównywany do lewej strony, istnieje jednak sposób umożliwiający wyrównywanie tekstu do środka i do prawej strony. Standardowo StringGrid nie posiada żadnej funkcji, która by to umożliwiała, dlatego trzeba stworzyć taką funkcję od podstaw. Zanim jednak zajmiemy się funkcją, stworzymy najpierw typ wyliczeniowy, który będzie służył do określania strony do której tekst będzie wyrównywany. W tym celu przechodzimy do pliku nagłówkowego i po sekcji include, a jeszcze przed deklaracją klasy TForm1 definiujemy nowy typ wyliczeniowy o nazwie TAlg:

// Plik nagłówkowy np. Unit1.h.
//--------------------------------
#ifndef Unit1H
#define Unit1H
//--------------------------------
#include <Classes.hpp>
#include <Controls.hpp>
#include <StdCtrls.hpp>
#include <Forms.hpp>
#include <Grids.hpp>
//--------------------------------
enum TAlg {algCenter, algRight, algLeft, algAuto};// Typ wyliczeniowy
class TForm1 : public TForm
//--------------------------------


Teraz w sekcji private tego samego pliku nagłówkowego dodajemy deklaracją funkcji o nazwie Alignment (nazwa jest dowolna):

// Plik nagłówkowy np. Unit1.h.
//--------------------------------
private:
  void __fastcall Alignment(TCanvas *canv, String text, TRect &Rect, TAlg align);
//--------------------------------


Następnie w pliku żródłowym tworzymy definicję funkcji Alignment:

// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::Alignment(TCanvas *canv, String text, TRect &Rect, TAlg align)
{
  int i = (Rect.Right - Rect.Left - canv->TextWidth(text))/2;
  int j = (Rect.Bottom - Rect.Top - canv->TextHeight(text))/2;

  if(align != algAuto)
      {
       canv->Brush->Color = canv->Brush->Color;
       canv->FillRect(Rect);
      }

  switch(align){
        case algCenter: canv->TextOut(Rect.Left + i, Rect.Top + j, text); return;
        case algRight : canv->TextOut(Rect.Right - canv->TextWidth(text) - 3, Rect.Top + j, text); return;
        case algLeft  : canv->TextOut(Rect.Left + 2, Rect.Top + j, text); return;
        case algAuto  :{
                             float q;
                               try{
                                 q = text.ToDouble();
                                 Alignment(canv, text, Rect, algRight); return;
                                  }
                               catch(...){return;}
                       }
              }
}
//--------------------------------


W prezentowanej funkcji oprócz wyrównywania do środka, lewej i prawej strony, zaimplementowałem dodatkowo wyrównywanie automatyczne, które służy do automatycznego wyrównywania liczb do prawej strony. Funkcję Alignment należy wywoływać w zdarzeniu OnDrawCell obiektu StringGrid:

// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::StringGrid1DrawCell(TObject *Sender, int ACol,
      int ARow, TRect &Rect, TGridDrawState State)
{
 if(ACol == 1)
     Alignment(StringGrid1->Canvas, StringGrid1->Cells[ACol][ARow], Rect, algLeft);
 if(ACol == 2)
     Alignment(StringGrid1->Canvas, StringGrid1->Cells[ACol][ARow], Rect, algCenter);
 if(ACol == 3)
     Alignment(StringGrid1->Canvas, StringGrid1->Cells[ACol][ARow], Rect, algRight);

 Alignment(StringGrid1->Canvas, StringGrid1->Cells[ACol][ARow], Rect, algAuto);
//--------------------------------


W podanym przykładzie tekst w komórkach pierwszej kolumny będzie wyrównywany zawsze do lewej stony, w komórkach drugiej kolumny do środka, w komórkach trzeciej kolumny do prawej strony, dodatkowo jeśli do którejkolwiek komórki w dowolnej kolumnie zostanie wprowadzona liczba, to zawartość komórki zostanie wyrównana do prawej strony niezależnie od tego jak zostało zdefiniowane wyrównanie dla kolumny.
Tekst można wyrównywać nie tylko dla całych kolumn, lecz również dla pojedynczych komórek:

// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::StringGrid1DrawCell(TObject *Sender, int ACol,
      int ARow, TRect &Rect, TGridDrawState State)
{
 if(ACol == 1 && ARow == 2)
     Alignment(StringGrid1->Canvas, StringGrid1->Cells[ACol][ARow], Rect, algCenter);
}
//--------------------------------


W podanym przykładzie zawartość komórki w pierwszej kolumnie i w drugim wierszu zawsze będzie wyrównywana do środka.
Analogicznie można wyrównywać zawartość całych wierszy:

// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::StringGrid1DrawCell(TObject *Sender, int ACol,
      int ARow, TRect &Rect, TGridDrawState State)
{
 if(ARow == 1)
     Alignment(StringGrid1->Canvas, StringGrid1->Cells[ACol][ARow], Rect, algLeft);
 if(ARow == 2)
     Alignment(StringGrid1->Canvas, StringGrid1->Cells[ACol][ARow], Rect, algCenter);
 if(ARow == 3)
     Alignment(StringGrid1->Canvas, StringGrid1->Cells[ACol][ARow], Rect, algRight);

 Alignment(StringGrid1->Canvas, StringGrid1->Cells[ACol][ARow], Rect, algAuto);
//--------------------------------


Uzupełnienie, sposób wyrównania komórki w StringGrid nadesłany przez fuma:

// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::StringGrid1DrawCell(TObject *Sender, int ACol,
      int ARow, TRect &Rect, TGridDrawState State)
{

 if(ARow == 1 && ACol == 2) // dla pierwszego wiersza i drugiej kolumny
 StringGrid1->Canvas->FillRect(Rect);
 ::DrawText(StringGrid1->Canvas->Handle,
   StringGrid1->Cells[ACol][ARow].c_str(), -1, (LPRECT)&Rect, DT_CENTER | DT_SINGLELINE | DT_VCENTER);
}
//--------------------------------

 

To było wyrównanie do środka dla wyrównania do lewej lub prawej strony należy zmienić odpowiednio parametr DT_CENTER na DT_LEFT lub DT_RIGHT.

...powrót do menu. 


Zmiana koloru czcionki dla wybranej komórki obiektu StringGrid.

  Zamiany koloru czcionki dla wybranej komórki w obiekcie StringGrid dokonuje się poprzez określenie numeru kolumny i wiersza w zdarzeniu OnDrawCell:

// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::StringGrid1DrawCell(TObject *Sender, int ACol,
      int ARow, TRect &Rect, TGridDrawState State)
{
 StringGrid1->Canvas->Brush->Color = StringGrid1->Canvas->Brush->Color;
 StringGrid1->Canvas->FillRect(Rect);


 if(ACol == 2 && ARow == 1)
    {
     StringGrid1->Canvas->Font->Color = clRed;
    }
 StringGrid1->Canvas->TextOut(Rect.Left, Rect.Top, StringGrid1->Cells[ACol][ARow]);
}
//--------------------------------


Tekst w komórce znajdującej się na przecięciu drugiej kolumny i pierwszego wiersza przyjmie kolor czerwony. Można określać kolor czcionki dla całych kolumn:

 if(ACol == 2) // druga kolumna
    StringGrid1->Canvas->Font->Color = clGreen; // kolor zielony


...lub całych wierszy:

 if(ARow == 3) // trzeci wiersz
    StringGrid1->Canvas->Font->Color = (TColor)0x00D200D2; // kolor         


Ważna uwaga!!!
 Zmiany koloru czcionki w zdarzeniu OnDrawCell należy dokonywać przed innymi zadaniami np. przed wyrównywaniem tekstu w komórkach, czyli najpierw ustawiamy właściwości czcionki a dopierwo potem określamy pozostałe działania.
Dzieje się tak dlatego, że w zdarzeniu OnDrawCell rysowany jest arkusz i tekst w poszczególnych komórkach, a rysowanie odbywa się za każdym razem gdy zostaną wprowadzone jakiekolwiek zmiany w tabeli StringGrid.

...powrót do menu. 


Zmiana stylu czcionki dla wybranej komórki obiektu StringGrid.

  Zmiana stylu czcionki dla wybranej komórki, wiersza lub kolumny dokonuje się w podobny sposób do zmiany koloru czcionki. Żeby dla komórki znajdującej się na skrzyżowaniu drugiej kolumny i trzeciego wiersza zmienić czcionkę na pogrubioną wystarczy tylko określić jej styl jako fsBold:


// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::StringGrid1DrawCell(TObject *Sender, int ACol,
      int ARow, TRect &Rect, TGridDrawState State)
{
 StringGrid1->Canvas->Brush->Color = StringGrid1->Canvas->Brush->Color;
 StringGrid1->Canvas->FillRect(Rect);


 if(ACol == 2 && ARow == 1)
   StringGrid1->Canvas->Font->Style = StringGrid1->Canvas->Font->Style << fsBold;

 StringGrid1->Canvas->TextOut(Rect.Left, Rect.Top, StringGrid1->Cells[ACol][ARow]);
}
//--------------------------------


Wszystkie możliwe style:

Żeby wyłączyć wybrany styl, np. przywrócić czcionkę normalną po tym jak ją pogrubiliśmy należy:

// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::StringGrid1DrawCell(TObject *Sender, int ACol,
      int ARow, TRect &Rect, TGridDrawState State)
{
 StringGrid1->Canvas->Brush->Color = StringGrid1->Canvas->Brush->Color;
 StringGrid1->Canvas->FillRect(Rect);


 if(ACol == 2 && ARow == 1)
   StringGrid1->Canvas->Font->Style = StringGrid1->Canvas->Font->Style >> fsBold;

 StringGrid1->Canvas->TextOut(Rect.Left, Rect.Top, StringGrid1->Cells[ACol][ARow]);
}
//--------------------------------


Podobnie jak w przypadku określania koloru czcionki, styl należy określać przed wykonaniem innych zadań, oczywiście to czy styl zostanie określony przed kolorem nie ma tutej znaczenia.

...powrót do menu. 


Określanie koloru dla wybranej komórki obiektu StringGrid.

  Wszystkie komórki obiektu StringGrid są domyślnie białe i klasa TStringGrid nie zawiera w sobie żadnych funkcji, które umożliwaiłyby zmianę koloru poszczególnych komórek. Nie oznacza to jednak, że nie można tego zrobić. Podobnie jak w przypadku zmiany koloru czcionki należy posłużyć się klasą TCanvas. Tego typu operacje zawsze wykonuje się w zdarzeniu OnDrawCell obiektu StringGrid:


// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::StringGrid1DrawCell(TObject *Sender, int ACol,
      int ARow, TRect &Rect, TGridDrawState State)
{
 if(ARow == 2)
 {
  if(ACol == 1) StringGrid1->Canvas->Brush->Color = clYellow;
  if(ACol == 2) StringGrid1->Canvas->Brush->Color = clRed;
  if(ACol == 3) StringGrid1->Canvas->Brush->Color = clBlue;
  if(ACol == 4) StringGrid1->Canvas->Brush->Color = clGreen;
  StringGrid1->Canvas->FillRect(Rect);
  StringGrid1->Canvas->TextOut(Rect.Left, Rect.Top, StringGrid1->Cells[ACol][ARow]);
 }
}
//--------------------------------


W podanym przykładzie zostały wyróżnione dwa elementy kodu:

 StringGrid1->Canvas->FillRect(Rect);


O ile w funkcji Canvas->Brush->Color zostały określone kolory komórek wybranych w warunkach if(ARow...) i if(ACol...), o tyle nie powoduje ona jeszcze wypełnienia komórek konkretnym kolorem, a jedynie określa kolor, którym te komórki zostaję wypełnione za pomocą funkcji FillRect. Jak widać w przykładzie funkcja FillRect pobiera jako parametr obiekt Rect typu TRect, który jest zadeklarowany w nagłówku zdarzenia OnDrawCell, a zawiera on informacje o położeniu i obszarze wybranej komórki. Dokładniej ujmując każda komórka jest określona za pomocą sześciu parametrów: Rect.Left, Rect.Top, Rect.Right, Rect.Bottom, Rect.Width, Rect.Height.
Drugi wyróżniony element:

 StringGrid1->Canvas->TextOut(Rect.Left, Rect.Top, StringGrid1->Cells[ACol][ARow]);


Ten kod jest niezbędny ponieważ wypełniając komórkę kolorem jednocześnie "zamazujemy" wszystko co się w niej znajduje, czyli w tym konkretnym przykładzie tekst. Tak więc funkcja Canvas->TextOut służy właśnie do ponownego uzupełnienia komórek tekstem, który się w nich znajdował przed "zamazaniem". Zamiast tej funkcji można tam wstawić funkcję Alignment przedstawioną w poradzie Wyrównywanie tekstu w komórkach obiektu StringGrid.
Ważne jest żeby zawsze po wykonaniu dowolnej operacji w obrębie komórki ponownie wypełnić ją tekstem.

...powrót do menu. 


Wypełnianie grafiką wybranej komórki obiektu StringGrid.

  Wypełnianie grafiką wybranej komórki obiektu StringGrid jest bardzo podobne do okreslania koloru dla wybranej komórki i jest wykonywane wewnątrz zdarznie OnDrawCell obiektu StringGrid:

// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::StringGrid1DrawCell(TObject *Sender, int ACol,
      int ARow, TRect &Rect, TGridDrawState State)
{
  Graphics::TBitmap *bmp = new Graphics::TBitmap;
  bmp->LoadFromFile("nazwa_pliku.bmp");

  StringGrid1->Canvas->FillRect(Rect); // tutej następuje zamazanie aktualnej zawartości komórek.

  if(ACol == 2 && ARow == 3) // określenie komórki która będzie zawierała grafikę, tutej jest to komórka znajdująca się na przecięciu drugiej kolumny i trzeciego wiersza.
  {
   StringGrid1->Canvas->StretchDraw(Rect, bmp);
   StringGrid1->RowHeights[2] = bmp->Height;
   StringGrid1->ColWidths[2] = bmp->Width;
  }

  StringGrid1->Canvas->Brush->Style = bsClear;
  StringGrid1->Canvas->TextOut(Rect.Left, Rect.Top, StringGrid1->Cells[ACol][ARow]);

  delete bmp; //usunięcie obiektu bmp z pamięci.
}
//--------------------------------


Najpierw zostaje utworzony nowy obiekt bmp typu Bitmap, służy on do wczytania grafiki w formacie bmp. Następnie aktualna zawartość komórek zostaje zamazana, jest to konieczne ponieważ w dalszej części kodu zostaje zmieniony rozmiar komórki i w efekcie powoduje to przesunięcie tekstu znajdującego się w komórce. Można sprawdzić co się będzie działo z tekstem wpisywanym do komórki pomijając kod: StringGrid1->Canvas->FillRect(Rect).
Potem rozmiar komórki zostaje dostosowany do rozmiaru grafiki, służą do tego funkcje RowHeights[numer wiersza] i ColWidths[numer kolumny], przy czym zmienia się rozmiar całego wiersz i całej kolumny i nie ma sposobu na zmianę rozmiaru tylko wybranej komórki. Następnie korzystając z funkcji Canvas->StretchDraw wypełniamy wybraną komórkę grafiką i tutej należy zauważyć, że funkcja StretchDraw dopasowuje rozmiar grafiki do rozmiaru komórki, a to oznacza, że jeżeli nie dopasujemy rozmiarów komórki do rozmiarów grafiki to funkcja StretchDraw dostosuje rozmiar grafiki do rozmiaru komórki, więc ją ściśnie lub rozciągnie.
W kodzie pojawia się funckja: StringGrid1->Canvas->Brush->Style = bsClear, która usuwa tło z tekstu, czyli czyni je przeźroczystym, jeżeli ten kod zostanie pominięty to tekst będzie wyswietlany na białym tle. Następnie występuje funkcja Canvas->TextOut, która - jak to już wyjaśniłem w poprzednich poradach - służy do ponownego umieszczenia tekstu w komórkach.

...powrót do menu. 


Przesuwanie wierszy i kolumn.

  Chcąc w obiekcie StringGrid zamienić miejscami wiersze i kolumny wystarczy ustawić właściwość Otpions | goRowMoving i goColMoving na true, ale żeby przesuwanie działało siatka musi posiadać tak zwane FixedCols i FixedRows - wyróżnione Kolumny i wiersze w ilości co najmniej 1. Przesuwanie odbywa się po uchwyceniu właśnie tych wyróżnionych komórek.

...powrót do menu. 


Nakładanie maski.

  Nakładanie maski to po prostu określanie sposobu w jaki ma być wpisywany tekst, np. gdy chcemy żeby w wybranej przez nas kolumnie w każdej komórce wpisywany był numer telefonu zawsze w tej samej formie, to wystarczy nałożyć na tą kolumnę maskę.
Nakładanie maski odbywa się w zdarzeniu OnGetEditMask obiektu StringGrid:

// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::StringGrid2GetEditMask(TObject *Sender, int ACol,
      int ARow, AnsiString &Value)
{
 if(ACol == 1)
   Value = "(0 0000 00)000 00 00";
}
//--------------------------------


W przykładzie, w każdej komórce kolumny pierwszej, każdy wprowadzony tekst będzie miał zawsze taką samą postać, gdybym chciał wprowadzić np. numer: 01033226613245, to zostanie on automatycznie przetworzony na: (0 1033 22)661 32 45.

...powrót do menu. 


Wstawianie obiektu ComboBox do obiektu StringGrid.

  Używając tabeli StringGrid czasami występuje konieczność wstawiania tekstu do poszczególnych komórek z listy wyboru np. ComboBox:

Rozmiar: 12670 bajtów

 

Sztuka użycia rozwijanej listy wewnątrz obiektu StringGrid opiera się na dynamicznym utworzeniu obiektu ComboBox. W tym celu w sekcji private pliku nagłówkowego (np. Unit1.h) deklarujemy obiekt myEditor wywodzący się z klasy TComboBox i skoro już tutej jesteśmy zadeklarujemy od razu funkcję myEditorChange, która będzie reagowała na wszelkie operacje wykonywane w obiekcie myEditor. Dodatkowo deklarujemy również funkcję MoveEditor, która będzie odpowiedzialna za ustawienie prawidłowego położenia obiektu myEditor wewnątrz tabeli StringGrid, deklarujemy również obiekt Lista typu TStringList, posłuży on do ładowania listy z pliku do obiektu myEditor. Dodatkowo zostanie dodany obiekt typu TPanel na którym będzie umieszczany ComboBox. To połączenie TPanel z TComboBox jest niezbędne do prawidłowego działania kodu:

// Plik nagłówkowy np. Unit1.h.
//--------------------------------
private:
        TComboBox *myEditor;
        TPanel *myPanel;
        void __fastcall myEditorChange(TObject *Sender);
        void __fastcall MoveEditor(void);
        TStringList *Lista;


//--------------------------------


Następnie przechodzimy do pliku źródłowego (np. Unit1.cpp) i wewnątrz kontruktora klasy (np. TForm1) definiujemy obiekty myEditor i Lista.

// Plik źródłowy np. Unit1.cpp
//--------------------------------
TForm1 *Form1;
//--------------------------------
__fastcall TForm1::TForm1(TComponent* Owner)
        : TForm(Owner)
{
 Lista = new TStringList;
 Lista->LoadFromFile(ExtractFilePath(ParamStr(0)) + "lista.txt"); // wczytywanie pliku listy dla ComboBox

 myPanel = new TPanel(this);
 myPanel->Parent = StringGrid1;
 myPanel->Visible = false;
 myPanel->BevelOuter = bvNone;
 myPanel->Ctl3D = false;
 myPanel->ParentBackground = false; // starsze wersje środowiska tego nie mają
 myPanel->ParentColor = false;
 myPanel->Color = clWhite; // można też dać myPanel->Color - StringGrid1->Color, jeżeli kolor komórek jest inny niż biały
 myPanel->Caption = "";

 myEditor = new TComboBox(myPanel->Handle);
 myEditor->Parent = myPanel;
 myEditor->Left = 0;
 myEditor->Top = 0;
 myEditor->Style = csDropDownList; // ComboBox działa jak rozwijana lista
 myEditor->OnChange = myEditorChange;

}
//--------------------------------


Jak to widać w przykładzie do obiektu lista zostaje wczytana zawartość pliku test.dat, ale oczywiście można wczytywać różne pliki i w róznych momentach pracy naszego programu.
Trzeba jeszcze zdefiniować funkcję MoveEditor:

// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::MoveEditor(void)
{
 TRect Rect = StringGrid1->CellRect(StringGrid1->Col, StringGrid1->Row);
 int gWidth = StringGrid1->GridLineWidth;

 myPanel->Visible = false;
 myPanel->Top = Rect.Top + gWidth;
 myPanel->Left = Rect.Left + gWidth;
 myPanel->Height = (Rect.Bottom - Rect.Top) + 1;
 myPanel->Width = (Rect.Right - Rect.Left) + 1;
 myEditor->Width = myPanel->Width;
 myEditor->Height = myPanel->Height;
 myPanel->Visible = true;

}
//--------------------------------


...oraz funkcję myEditorChange:

// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::myEditorChange(TObject *Sender)
{
 StringGrid1->Cells[StringGrid1->Col][StringGrid1->Row]
         = myEditor->Items->Strings[myEditor->ItemIndex];
 myPanel->Visible = false;

}
//--------------------------------


No i na koniec w zdarzeniu OnClick obiektu StringGrid wywołujemy obiekt myEditor. W przykładzie lista będzie się pojawiała w drugiej i trzeciej kolumnie:


// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::StringGrid1Click(TObject *Sender)
{
 if(StringGrid1->Col >= 2 && StringGrid1->Col <= 3) // zakres komórek w których ma się pojawić ComboBox
 {
  myEditor->Items->Clear();
  myEditor->Items = Lista;
  MoveEditor();
  myEditor->DroppedDown = true;
 }

}
//--------------------------------


Na zakończenie wypadało by jeszcze ustawić opcje obiektu StringGrid:

...i tyle właściwie wystarczy, żeby program zadziałał, pozostało już tylko udoskonalić kod, ale zróbcie to sami...

 

...powrót do menu. 


Wyróżnianie pojedynczej komórki, czyli rysowanie ramki wokół komórki.

 

Pisząc o wyróżnianiu komórki, mam na myśli tworzenie prostokątnej ramki wokół wybranej komórki w obiekcie typu TStringGrid, tak jak to widać na rysunku:

Stworzenie takiego efektu jest bardzo proste, wymaga utworzenia jednej funkcji, którą nazwę DrawRectGrid, oraz jednej zmiennej typu bool o nazwie np. GSelected. Funkcję jak i zmienną deklarujemy w pliku nagłówkowym w sekcji private:

// Plik nagłówkowy np. Unit1.h
//--------------------------------
private:
       void __fastcall
DrawRectGrid(TRect Rect);
       bool GSelected;

 

Następnie przechodzimy do pliku źródłowego i definiujemy zadeklarowaną funkcję DrawRectGrid:

// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::DrawRectGrid(TRect Rect)
{
 if(GSelected)
 {
  StringGrid1->Repaint();
  GSelected = false;
 }

 StringGrid1->Canvas->Brush->Style = bsClear;
 StringGrid1->Canvas->Pen->Color = clRed;
 StringGrid1->Canvas->Pen->Width = 2;

 StringGrid1->Canvas->Rectangle(Rect.Left, Rect.Top, Rect.Right, Rect.Bottom);
}
//--------------------------------

 

Przedstawiona funkcja przejmuje jako argument obiekt typu Rect, a następnie ustawia właściwości Canvas obiektu StringGrid1. Właściwość Brush zostaje ustawiona na bsClear, dzięki temu zostanie narysowany prostokąt tylko z ramką zewnętrzną, a środek pozostanie przeźroczysty. Właściowść Pen zostaje ustawiona na czerwony kolor, czyli ramka będzie czerwona, a następnie przyjmuje wartość 2, czyli ramka będzie miała szerokość linii o wymiarze dwóch pikseli, no a potem to już zostaje narysowany prostokąt, którego początek jest określony za pomocą współrzędnych Rect.Left i Rect.Top, a koniec za pomocą Rect.Right i Rect.Bottom.
Wcześniej jednak zmienna GSelected jest sprawdzana i jeżeli jej wartość jest równa true, to następuje wykonanie instrukcji, której zadaniem jest odrysowanie obiektu StringGrid1. Obiekt StringGrid1 zostaje ponownie odrysowany, tylko po to, żeby przed narysowanie nowego prostokąta zamazać poprzedni, a zminna GSelected spełnia rolę przełącznika. Po co? A no po to, że w po usunięciu starego prostokąta nie ma już potrzeby ponownego rysowania siatki. Gdybyśmy umieścili funkcję StringGrid1->Repaint() (o czym za chwilę) bezpośrednio wewnątrz zdarzenia OnDrawCell dla obiektu StringGrid1, to wywołało by to migotanie tegoż obiektu. Dlatego też. zmienna GSelected po odrysowaniu siatki zostaje ustawiona na false, ponownie zostanie ustawiona na true w zdarzeniu OnSelectCell dla obiektu StringGrid i właśnie poniższy kod pokazuje jak wywołać funkcję DrawRectGrid w zdarzeniu OnDrawCell i jak zmienić ustawienia zmiennej GSelected w zdarzeniu OnSelectCell:


// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::StringGrid1DrawCell(TObject *Sender, int ACol,
        int ARow, TRect &Rect, TGridDrawState State)
{
 if(StringGrid1->Row == ARow && StringGrid1->Col == ACol)
    DrawRectGrid(Rect);
}
//--------------------------------
void __fastcall TForm1::StringGrid1SelectCell(TObject *Sender, int ACol,
        int ARow, bool &CanSelect)
{
 GSelected = true;
}
//--------------------------------

 

...powrót do menu. 

Jak automatycznie dostosować szerokość kolumny w StringGrid?

 

    Chodzi o dostosowanie szerokości kolumny do długości tekstu, który znajdzie się w wierszu kolumny. Zrealizowanie tego zadania jest w zasadzie proste i ogranicza się do sprawdzenie długości tekstu poprzez wykorzystanie klasy TCanvas. Zadanie automatycznego dostosowania szerokości kolumny do tekstu, zrealizuję w zdarzeniu OnSetEditText dla obiektu StringGrid, to zdarzenie jest wywoływane przy wstawianiu tekstu do dowolnej komurki tegoż obiektu:


// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::StringGrid1SetEditText(TObject *Sender, int ACol,
        int ARow, const AnsiString Value)
{
 if(!StringGrid1->Cells[ACol][ARow].IsEmpty())
  StringGrid1->ColWidths[ACol] = 5 + StringGrid1->Canvas->TextWidth(StringGrid1->Cells[ACol][ARow]);
}
//--------------------------------

 

Jak widać do szerokości kolumny dodałem wartość '5', ale to po to, żeby przed i po tekście występowało trochę wolnej przestrzeni.

...powrót do menu. 

Zapisywanie i odczytywanie zawartości obiektu StringGrid.

 

    Często w e-mail'ach różni - jak sadzę - początkujący programiści zwracają się do mnie z prośbą o podanie sposobu na zapisywanie i odczytywanie zawartości StringGrid. Niektórzy próbują wykorzystywać do tego celu struktury, co jest dobrym pomysłem, ale nie zawsze odpowiednim, ponieważ struktury mają to do siebie, że w przypadku zapisywania łańcucha znaków, długość tego łańcucha musi być z góry określona, jest to o tyle wadą, że jeśli zapisujemy łańcuch znaków krótszy od zadeklarowanego, to struktura i tak wypełnia się przypadkowymi znakami (użyłem określenia "przypadkowymi", lecz osobiście uważam, że przypadek nie istnieje, to po prostu ciąg zdarzeń) więc niezależnie od tego jak dużą tabelę zapisujemy, w przypadku struktury wielkość pliku zawsze będzie stała, chociaż istnieje pewien bardzo skomplikowany sposób, żeby to zmienić. Niemniej jednak, w tej poradzie pokaże jak zapisać StringGrid do pliku w trybie tekstowym, w pliku tym oprócz samej zawartości tabeli znajdą się również informacje o liczbie komórek i o szerokości kolumn, tyle na początek, chociaż gdyby rozwinąć ten wątek, to można by umieścić w pliku informacje o kolorach komórki, nazwie czcionki, wyrównaniu tekstu w komórkach itp. Tak daleko, jednak w tej poradzie nie pójdę. Innym często zgłaszanym problemem, jest możliwość zapisywania tabeli w formacie Excel'a, no cóż zapisanie w tym formacie wymagałoby poznania sposobu formatowania pliku, ja go nie znam, ale jest pewien sposób, otóż Excel rozpoznaje pliki *.csv, te pliki nie zawierają co prawda  żadnego formatowania, ale mogą posłużyć do prostego przeniesienia zawartości ze StringGrid do Excel'a.
    Zaczniemy od stworzenia prostej funkcji zapisującej zawartość tabeli do pliku, funkcja pobiera wyrazy i zdania z komórek tabeli, dodaje do nich separator służący do rozdzielania komórek w pliku, no i zapisuje to do pliku:


// Plik źródłowy np. Unit1.cpp
//--- FUNKCJA ZAPISUJĄCA TABELĘ DO PLIKU ------------------------------------
void __fastcall TForm1::SaveGridToFile(AnsiString FileName, TStringGrid *Grid,
        bool csv)
{
 TStringList *Lista = new TStringList;
 String txt_1 = "", txt_2 = "";

 if(!csv)
 Lista->Add("\"Col" + (AnsiString)Grid->ColCount + "\";\"ROW" + (AnsiString)Grid->RowCount + "\"");

 for(int i = 0; i < Grid->RowCount; i++)
 {
  for(int j = Grid->ColCount - 1; j >= 0; j--)
  {
   String temp = Grid->Cells[j][i];
   if(temp.SubString(temp.Length(), temp.Length() + 1) == ";")
    temp = Grid->Cells[j][i] + "'";
   if(!csv)
    txt_1 = ";\"" + temp + "\"" + txt_1;
   else
    txt_1 = "\"" + temp + "\";" + txt_1;

   if(!csv && i == 0)
   {
    txt_2 = (AnsiString)Grid->ColWidths[j] + ";" + txt_2;
    if(j == 0)
    Lista->Add(txt_2);
   }
  }
  Lista->Add(txt_1);
  txt_1 = "";
 }

 Lista->SaveToFile(FileName);
 delete Lista;
}
//--------------------------------

 

Należy pamiętać o deklaracji funkcji w pliku nagłówkowym w sekcji private lub public. Przedstawiona funkcja pobiera parametr bool csv, służy od do ustawienia formatu pliku, jeśli ten parametr będzie miał wartość true, to funkcja zapisze plik z formatowaniem typowym dla plików *.csv, w przeciwnym razie plik zostanie zapisany w formacie własnym, oczywiście funkcja nie nadaje plikom żadnego rozszerzenia, rozszerzenie i nazwa pliku muszą zostać przekazane do funkcja poprzez parametr AnsiString FileName. Oprócz samej zawartości tabeli funkcja ta wprowadza do pliku własne znaki, które będą jej potrzebne przy odczytywaniu pliku i przepisywaniu go do StringGrid. Znaki te nie będą oczywiście umieszczane w tabeli, gdy staną się zbędne zostaną usunięte.
    Teraz zajmiemy się funkcją odczytującą dane z pliku, jednakże do przepisania zawartości pliku do tabeli i jej ustawienia, niezbędne nam będą jeszcze dwie inne funkcje, odczytująca liczbę kolumn i wierszy i funkcja przepisująca tekst do komórek i odczytująca szerokość kolumn, te dwie dodatkowe funkcje nie zostaną przypisane do klasy formularza, czyli nie tworzymy dla nich definicji w pliku nagłówkowym, więc muszą być umieszczone w pliku źródłowym przed funkcją odczytującą dane z pliku. Funkcja odczytująca przynależy do klasy formularza dlatego należy umieścić jej deklarację w pliku nagłówkowym.


// Plik źródłowy np. Unit1.cpp
//--- FUNKCJA ODCZYTUJĄCA LICZBĘ KOLUMN I WIERSZY ---------------------------
void SetColRow(AnsiString txt, TStringGrid *Grid)
{
 int t = txt.AnsiPos("\";");
 String txt_1 = txt.SubString(1, t);
 String txt_2 = txt.SubString(t + 2, 10);
 Grid->ColCount = txt_1.SubString(5, txt_1.Length() - 5).ToInt();
 Grid->RowCount = txt_2.SubString(5, txt_2.Length() - 5).ToInt();
}
//--- FUNKCJA PRZEPISUJĄCA TEKST DO KOMÓREK I ODCZYUJĄCA SZEROKOŚĆ KOLUMN ---
AnsiString SetCellText(AnsiString txt, AnsiString Wtxt, int &Size, int Col)
{
 String txt_1 = "", txt_2 = "";
 for(int i = -1; i < Col; i++)
 {
  int q = 0;
  int t = txt.AnsiPos(";\"");
  if(i >= 0)
   q = Wtxt.AnsiPos(";");
  txt_1 = txt.SubString(2, t - 3);
  if(i >= 0)
   txt_2 = Wtxt.SubString(1, q - 1);

  if(t > 0)
   txt = txt.Delete(1, t);
  else
   txt_1 = txt.SubString(2, txt.Length() - 2);

  if(i >= 0)
  {
   if(q > 0)
    Wtxt = Wtxt.Delete(1, q);
   else
    txt_2 = Wtxt.SubString(1, Wtxt.Length() - 1);
  }
 }
 Size = txt_2.ToInt();
 if(txt_1.SubString(txt_1.Length() - 1, txt_1.Length() + 1) == ";'")
  txt_1 = txt_1.Delete(txt_1.Length(), txt_1.Length() + 1);

 return txt_1;
}
//--- FUNKCJA ODCZYTUJĄCA TABELĘ Z PLIKU ------------------------------------
void __fastcall TForm1::LoadGridFromFile(AnsiString FileName, TStringGrid *Grid)
{
 TStringList *Lista = new TStringList;
 Lista->LoadFromFile(FileName);
 SetColRow(Lista->Strings[0], Grid);

 for(int i = 0; i < Grid->RowCount; i++)
 {
  for(int j = 0; j < Grid->ColCount; j++)
  {
   int Size;
   Grid->Cells[j][i] = SetCellText(Lista->Strings[i + 2], Lista->Strings[1], Size, j + 1);
   Grid->ColWidths[j] = Size;
  }
 }
 delete Lista;
}
//--------------------------------

 

Teraz sposób wywołania tychże funkcji:


// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
 SaveGridToFile("Nazwa_pliku.csv", StringGrid1, true);
}
//--------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
 SaveGridToFile("Nazwa_pliku.roz", StringGrid1, false);
}
//--------------------------------
void __fastcall TForm1::Button3Click(TObject *Sender)
{
 LoadGridFromFile("Nazwa_pliku.roz", StringGrid1);
}
//--------------------------------

 

Na zakończenie mała uwaga, otóż funkcja zapisująca potrafi zapisać plik w formacie *.csv, jednak funkcja odczytująca  nie potrafi go odczytać, tak więc funkcja odczytująca rozpoznaje tylko własny format, można oczywiście ją przerobić.

...powrót do menu. 

Określanie numeru kolumny i wiersza obiektu StringGrid na który wskazuje kursor myszy.

    Do realizacji tego zadania można posłużyć się funkcją MouseToCell i zdarzeniem OnMouseMove dla obiektu StringGrid:


// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TInterface1::StringGrid1MouseMove(TObject *Sender,
        TShiftState Shift, int X, int Y)
{
 int Column, Row;
 try
 {
  StringGrid1->MouseToCell(X, Y, Column, Row);
 }
 catch(...)
 {
  Edit1->Text = "";
 }
 Edit1->Text = "Kolumna: " + (AnsiString)Column + ", Wiersz: " + (AnsiString)Row;
}
//--------------------------------

 

...powrót do menu. 

 

Drukowanie zawartości tabeli StringGrid.

 

Obiekt StringGrid nie posiada żadnej funkcji, która umożliwiałaby jego wydrukowanie, ale można sobie taką funkcję stworzyć i ta porada jest właśnie o tym. Zaprezentuję tutaj prostą funkcję drukującą tabelę, funkcja będzie automatycznie skalowała tabelę do długości strony (width), wyskalowaniu ulegnie oczywiście tylko tabela, natomiast za długi tekst, który nie będzie mieścił się w komórce zostanie obcięty i wykropkowany. Zanim zaczniemy pisać kod, umieszczamy na formularzu tabelę StringGrid1, obiekt PrintDialog1 i przycisk Button1. Funkcja nie potrzebuje deklaracji w pliku nagłówkowym, jednak należy dołączyć do projektu bibliotekę #include <Printers.hpp>


// Plik źródłowy np. Unit1.cpp
//--------------------------------
#include <Printers.hpp>
void __fastcall PrintGrid(TStringGrid *Grid)
{
 Printer()->BeginDoc();
 int x = Printer()->PageWidth;
 int y = Printer()->PageHeight;

 float tmp = 0.0;
 for(int i = 0; i < Grid->ColCount; i++)
  tmp = tmp + (float)Grid->ColWidths[i];

 float ratio_x = x/tmp;

 int w, h = 0;
 int ratio_y = Grid->Canvas->TextHeight("TEXT");
 Printer()->Canvas->Brush->Style = bsClear;
 for(int i = 0; i < Grid->RowCount; i++)
 {
  w = 0;
  for(int j = 0; j < Grid->ColCount; j++)
  {
   String tekst = Grid->Cells[j][i];
   lab_1:
   if(Printer()->Canvas->TextWidth(tekst) + ratio_y >= Grid->ColWidths[j] * ratio_x)
   {
    tekst = tekst.SubString(1, tekst.Length() - 4) + "...";
    goto lab_1;
   }
   Printer()->Canvas->Rectangle(
                                 w * ratio_x,
                                 h * ratio_x,
                                 (w + Grid->ColWidths[j]) * ratio_x,
                                 (h + Grid->RowHeights[i] + ratio_y) * ratio_x
                                );
   Printer()->Canvas->TextOut(w * ratio_x + ratio_y, h * ratio_x + ratio_y, tekst);
   w = w + Grid->ColWidths[j];
  }
  h = h + Grid->RowHeights[i] + ratio_y;
  if((h + Grid->RowHeights[i] + (ratio_y * 4)) * ratio_x >= y)
  {
   h = 0;
   Printer()->Canvas->TextOut(0, y - (ratio_y * 3), "strona " + (String)Printer()->PageNumber);
   Printer()->NewPage();
  }
 }
 Printer()->Canvas->TextOut(0, y - (ratio_y * 3), "strona " + (String)Printer()->PageNumber);
 Printer()->EndDoc();
}
//--------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
 if(PrintDialog1->Execute())
  PrintGrid(StringGrid1);
}
}//--------------------------------

 

...powrót do menu. 

 

Usuwanie wybranego wiersza lub kolumny w tabeli StringGrid.

 

Tabela typu TStringGrid nie posiada funkcji umożliwiającej usuwanie wierszy lub kolumn, można oczywiście usunąć wybrany wiersz stosując taki kod: StringGrid1->RowCount--; lub kolumnę: StringGrid1->ColCount--; ale spowoduje to tylko usunięcie ostatniego wiersza lub kolumny w tabeli, poza tym jeśli np. komórki w usuwanym wierszu zawierają jakiś tekst i zaraz po usunięciu ponownie dodamy wiersz to ten sam tekst się w nich pojawi, ponieważ usunięcie wiersza nie jest jednoznaczne z wyczyszczeniem jego zawartości, wynika to z konstrukcji klasy TStringGrid i jej klas "pomocniczych".

Opracowałem dwie funkcje, pierwsza DeleteRow usuwa wiersz wybrany w tabeli, druga DeleteCol usuwa kolumnę wybraną w tabeli, w celu wybrania wiersza lub kolumny do usunięcia wystarczy zaznaczyć dowolną komórkę w tabeli nie będącej komórką nagłówka, ponieważ nagłówek w tabeli typu TStringGrid tego nie obsługuje, oczywiście można to zmienić, ale o tym w innej poradzie.

Funkcje oprócz samego usunięcia wiersza lub kolumny powodują również przesunięcie zawartości poszczególnych komórek znajdujących się pod komórką usuwaną do komórki wyżej, dzięki takiemu rozwiązaniu skasowana zostaje tylko wybrany przez mas wiersz lub kolumna z zawartością komórek, nie wpływa to jednak w żaden sposób na zawartość komórek znajdujących się nad i pod usuwanym wierszem, jak również na zawartość komórek znajdujących się z lewej lub prawej strony usuwanej kolumny:

// Plik źródłowy np. Unit1.cpp
//--------------------------------
// USUWANIE WIERSZA Z TABELI
void __fastcall DeleteRow(TStringGrid *Grid, int ARow)
{
 if(ARow < Grid->FixedRows)
 {
  Application->MessageBox("Nie wybrano wiersza do usunięcia", "Usuwanie wiersza", MB_OK | MB_ICONSTOP);
  return;
 }
 if(Grid->RowCount == Grid->FixedRows + 1)
 {
  int id = Application->MessageBox("Conajmniej jeden wiersz (nie licząc nagłówka) musi pozostać w tabeli. "

           "Czy chcesz wyczyścić zawartość komórek w tym wierszu?", "Usuwanie wiersza", MB_YESNO | MB_ICONQUESTION);
  if(id == ID_YES)
   for(int y = 0; y < Grid->ColCount; y++)
    Grid->Cells[y][ARow] = "";
  return;
 }

 int id = Application->MessageBox("Czy na pewno chcesz usunąć wybrany wiersz?\nOperacji nie będzie można cofnąć!",
          "Usuwanie wiersza", MB_YESNO | MB_ICONQUESTION);
 if(id == ID_YES)
 {
  for(int i = ARow; i <= Grid->RowCount - 1; i++)
  {
   for(int y = 0; y < Grid->ColCount; y++)
   {
    Grid->Cells[y][i] = Grid->Cells[y][i + 1];
    Grid->Cells[y][i + 1] = "";
   }
  }
  Grid->RowCount--;
 }
 Grid->SetFocus();
}
//--------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
 DeleteRow(StringGrid1, StringGrid1->Row);
}
//--------------------------------

// USUWANIE KOLUMNY Z TABELI
void __fastcall DeleteCol(TStringGrid *Grid, int ACol)
{
 if(ACol < Grid->FixedCols)
 {
  Application->MessageBox("Nie wybrano kolumny do usunięcia", "Usuwanie kolumny",
  MB_OK | MB_ICONSTOP);
  return;
 }
 if(Grid->ColCount == Grid->FixedCols + 1)
 {
  int id = Application->MessageBox("Conajmniej jedna kolumna (nie licząc nagłówka) musi pozostać w tabeli. "
           "Czy chcesz wyczyścić zawartość komórek w tej kolumnie?", "Usuwanie kolumny", MB_YESNO | MB_ICONQUESTION);
  if(id == ID_YES)
   for(int y = 0; y < Grid->RowCount; y++)
    Grid->Cells[ACol][y] = "";
  return;
 }

 int id = Application->MessageBox("Czy na pewno chcesz usunąć wybraną kolumnę?\nOperacji nie będzie można cofnąć!",
         "Usuwanie kolumny", MB_YESNO | MB_ICONQUESTION);
 if(id == ID_YES)
 {
  for(int i = ACol; i <= Grid->ColCount - 1; i++)
  {
   for(int y = 0; y < Grid->RowCount; y++)
   {
    Grid->Cells[i][y] = Grid->Cells[i + 1][y];
    Grid->ColWidths[i] = Grid->ColWidths[i + 1];
    Grid->Cells[i + 1][y] = "";
   }
  }
  Grid->ColCount--;
 }
 Grid->SetFocus();
}
//--------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
 DeleteCol(StringGrid1, StringGrid1->Col);
}
}//--------------------------------

 

...powrót do menu. 


Zaznaczanie wiersza lub kolumny poprzez kliknięcie w nagłówku tabeli.

    Klikając na nagłówku tabeli obiektu typu TStringGrid nic nie osiągniemy, nie spowoduje to zaznaczenia wiersza lub kolumny, jak również żaden wiersz czy kolumna nie zostaną w ten sposób wybrane, dlatego stworzyłem pewną bardzo prostą funkcję SelectHeaderGrid, która to zmieni. Do obsługi funkcji należy utworzyć dwa zdarzenia dla obiektu StringGrid, OnMouseDown, w którym program sprawdza na który wiersz lub kolumnę kliknięto oraz zdarzenie OnDrawCell obsługujące funkcję SelectHeaderGrid:

// Plik źródłowy np. Unit1.cpp
//--------------------------------
enum TSelectGrid {sgColumn, sgRow, sgNone};
int mCol = -1, mRow = -1;
void __fastcall SelectHeaderGrid(TStringGrid *Grid, TSelectGrid sGrid, TRect &dRect)
{
 Grid->Canvas->Brush->Color = clHotLight;
 if(sGrid != sgNone)
 {
  Grid->Canvas->FillRect(dRect);
  Grid->Canvas->Font->Color = clWhite;
  Grid->Canvas->TextOut(dRect.Left + 2, dRect.Top + 2, Grid->Cells[mCol][mRow]);
 }
 else
 {
  TRect Rcol = Grid->CellRect(Grid->Col, Grid->FixedRows - 1);
  TRect Rrow = Grid->CellRect(Grid->FixedCols - 1, Grid->Row);
  Grid->Canvas->FillRect(Rcol);
  Grid->Canvas->FillRect(Rrow);
  Grid->Canvas->Font->Color = clWhite;
  Grid->Canvas->TextOut(Rcol.Left + 2, Rcol.Top + 2, Grid->Cells[Grid->Col][Grid->FixedRows - 1]);
  Grid->Canvas->TextOut(Rrow.Left + 2, Rrow.Top + 2, Grid->Cells[Grid->FixedCols - 1][Grid->Row]);
 }

 Grid->Canvas->Pen->Width = 2;
 Grid->Canvas->Pen->Color = clRed;

 switch(sGrid)
 {
  case sgColumn:
  {
   int rowheight = Grid->BoundsRect.Bottom;
   if(mCol > 0 || mRow > 0) Grid->Col = mCol;

   Grid->Canvas->Polyline(OPENARRAY (TPoint, (
     Point(dRect.Left - 1, dRect.Bottom),
     Point(dRect.Left - 1, rowheight),
     Point(dRect.Right, rowheight),
     Point(dRect.Right, dRect.Bottom)
    )));
   return;
  }
  case sgRow:
  {
   int colwidth = Grid->BoundsRect.Right;
   if(mCol > 0 || mRow > 0) Grid->Row = mRow;

   Grid->Canvas->Polyline(OPENARRAY (TPoint, (
     Point(dRect.Right, dRect.Top - 1),
     Point(colwidth, dRect.Top - 1),
     Point(colwidth, dRect.Bottom),
     Point(dRect.Right, dRect.Bottom)
    )));
   return;
  }
 }
}
//--------------------------------
void __fastcall TForm1::StringGrid1MouseDown(TObject *Sender,
        TMouseButton Button, TShiftState Shift, int X, int Y) // zdarzenie OnMouseDown
{
 TGridCoord mCoord = StringGrid1->MouseCoord(X, Y);

 if((StringGrid1->FixedCols > 0 && mCoord.Y == StringGrid1->FixedCols - 1 &&
    mCoord.X >= 0 && mCoord.Y >= 0) || (StringGrid1->FixedRows > 0 &&

    mCoord.X == StringGrid1->FixedRows - 1 &&
    mCoord.X >= 0 && mCoord.Y >= 0))
 {
  mCol = mCoord.X;
  mRow = mCoord.Y;
 }

 else
 {
  mCol = -1;
  mRow = -1;
 }
 StringGrid1->Repaint();
}
//--------------------------------
void __fastcall TForm1::StringGrid1DrawCell(TObject *Sender, int ACol,
        int ARow, TRect &Rect, TGridDrawState State) // zdarzenie OnDrawCell
{
 if(ARow == mRow && ACol == mCol && mRow == StringGrid1->FixedRows - 1)
  SelectHeaderGrid(StringGrid1, sgColumn, Rect);

 if(ARow == mRow && ACol == mCol && mCol == StringGrid1->FixedCols - 1)
  SelectHeaderGrid(StringGrid1, sgRow, Rect);

 

 if(mCol == -1 && mRow == -1)
  SelectHeaderGrid(StringGrid1, sgNone, Rect);

 if(StringGrid1->Row == ARow && StringGrid1->Col == ACol)
 {
  StringGrid1->Canvas->Brush->Style = bsClear;
  StringGrid1->Canvas->Pen->Color = clHotLight;
  StringGrid1->Canvas->Pen->Width = 2;
  StringGrid1->Canvas->Rectangle(Rect.Left, Rect.Top, Rect.Right + 1, Rect.Bottom + 1);
 }
}
//--------------------------------


Funkcja będzie działać w pełni prawidłowo, jeżeli opcja goAlwaysShowEditor obiektu StringGrid będzie ustawiona na false.

...powrót do menu. 


Sortowanie tabeli StringGrid.

    W tej poradzie pokaże jak można posortować zawartość tabeli typu  TStringList, rosnąco lub malejąco według wybranej kolumny, tak by wszystkie rekordy w tabeli pozostały ze sobą prawidłowo powiązane, tzn. jeżeli sortowana jest np. kolumna nr. 2 to pozostałe kolumny w tabeli zostają z nią powiązane i zostają również posortowane, więc jeśli np. wiersz nr. 3 z kolumny nr. 2 zostaje przeniesiony na pozycję nr. 1 to wszystkie wiersze  nr. 3 z pozostałych kolumn również zostają przeniesione na pozycję nr. 1. Wydaje mi się, że dość jasno to wyraziłem i już wszyscy wiedzą na czym polega sortowanie tabeli. Obiekt typu TStringGrid nie posiada żadnej funkcji umożliwiającej posortowanie tabeli, dlatego stworzyłem własną SortGrid, a posłużyłem się w niej mechanizmem opisanym w poradzie sortowanie listy według różnych kryteriów. Funkcja będzie sortowała tabelę w oparciu o tekst, więc jeśli komórka zawiera liczbę to i tak zostanie ona potraktowana jako tekst. Takie rozwiązanie sprawia, że np. jeżeli komórki z liczbą zawiera następujące wartości:

 

100

10

23

1000

1

 

to przy potraktowaniu liczb jako tekst zostaną one posortowane w następujący sposób (rosnąco):

 

1000

100

10

1

23

 

jak widać nie odpowiada to wartościom.

 

Funkcję SortGrid można wywołać w dowolnym zdarzeniu np. OnClick dla przycisku typu TButton, jednak ja posunę się nieco dalej i wyposażę tabelę w mechanizm umożliwiający posortowani poprzez kliknięcie na nagłówkach kolumn, przy czym pierwsze kliknięcie będzie sortowało kolumnę rosnąco, drugie malejąco, kolejne rosnąco itd. Dla zobrazowania sortowania w zdarzeniu OnDrawCell dla obiektu StringGrid umieściłem kod rysujący strzałkę wskazującą kierunek sortowania:

// Plik źródłowy np. Unit1.cpp
//--------------------------------
enum TTypeSort {tsIncrease, tsDecrease};
int __fastcall CustomSortIncrease(TStringList *Lista, int idx1, int idx2)
{
 String cTemp1 = Lista->Strings[idx1];
 String cTemp2 = Lista->Strings[idx2];
 return CompareText(cTemp1, cTemp2);
}
int __fastcall CustomSortDecrease(TStringList *Lista, int idx1, int idx2)
{
 String cTemp1 = Lista->Strings[idx1];
 String cTemp2 = Lista->Strings[idx2];
 return -CompareText(cTemp1, cTemp2);
}
//--------------------------------
void __fastcall SortGrid(TStringGrid *Grid, int Col, TTypeSort tsSort)
{
 TStringList *SortList = new TStringList;
 String temp = "";

 for(int i = Grid->FixedRows; i < Grid->RowCount; i++)
 {
  temp = Grid->Cells[Col][i];
  if(temp.IsEmpty()) temp = " ";
  for(int z = Grid->FixedCols; z <= Grid->ColCount; z++)
  {
   if(Grid->Cells[z][i].IsEmpty()) Grid->Cells[z][i] = " ";
   if(z != Col)
    temp = temp + "^" + Grid->Cells[z][i];
  }
  SortList->Add(temp.SubString(0, temp.Length() - 1));
  temp = "";
 }

 switch(tsSort)
 {
  case tsIncrease: SortList->CustomSort(CustomSortIncrease); break;
  case tsDecrease: SortList->CustomSort(CustomSortDecrease); break;
 }
 temp = "";
 for(int i = 0; i < SortList->Count; i++)
 {
  temp = SortList->Strings[i];

  int y = temp.Pos("^");
  Grid->Cells[Col][i + 1] = temp.SubString(1, y - 1).Trim();
  temp = temp.Delete(1, y);
  int z;
  for(z = Grid->FixedCols; z < Grid->ColCount; z++)
  {
   if(z != Col)
   {
    y = temp.Pos("^");
    Grid->Cells[z][i + 1] = temp.SubString(1, y - 1).Trim();
    temp = temp.Delete(1, y);
   }
  }
  z++;
  y = temp.Pos("^");
  Grid->Cells[z][i + 1] = temp.SubString(1, y - 1).Trim();
  temp = temp.Delete(1, y);
 }

 delete SortList;
}
//--------------------------------
TTypeSort tsSwitch = tsIncrease;
int sCol = -1, sRow = -1;
void __fastcall TForm1::StringGrid1MouseDown(TObject *Sender,
        TMouseButton Button, TShiftState Shift, int X, int Y) // zdarzenie OnMouseDown
{
 TGridCoord mCoord = StringGrid1->MouseCoord(X, Y);
 if(mCoord.Y == StringGrid1->FixedCols - 1)
 {
  sCol = mCoord.X;
  StringGrid1->Col = sCol;


  switch(tsSwitch)
  {
   case tsIncrease: SortGrid(StringGrid1, sCol, tsIncrease); tsSwitch = tsDecrease;
                    StringGrid1->Repaint();
                    return;
   case tsDecrease: SortGrid(StringGrid1, sCol, tsDecrease); tsSwitch = tsIncrease;
                    StringGrid1->Repaint();
                    return;
  }
 }
}
//--------------------------------
void __fastcall TForm1::StringGrid1DrawCell(TObject *Sender, int ACol,
        int ARow, TRect &Rect, TGridDrawState State) // zdarzenie OnDrawCell
{
 if(ACol == sCol)
 {
  TRect rRect = StringGrid1->CellRect(sCol, 0);
  StringGrid1->Canvas->Brush->Color = clBlack;
  StringGrid1->Canvas->Pen->Color = clBlack;

  switch(tsSwitch)
  {
   case tsIncrease:
                   StringGrid1->Canvas->Polygon(OPENARRAY (TPoint, (
                                Point(rRect.Right - 10, rRect.Top + 5),
                                Point(rRect.Right - 13, rRect.Top + 10),
                                Point(rRect.Right - 8, rRect.Top + 10)
                                                                   )));
                   break;
   case tsDecrease:
                   StringGrid1->Canvas->Polygon(OPENARRAY (TPoint, (
                                Point(rRect.Right - 13, rRect.Top + 5),
                                Point(rRect.Right - 10, rRect.Top + 10),
                                Point(rRect.Right - 8, rRect.Top + 5)
                                                                   )));
                   break;
  }
 }
}
//--------------------------------

...powrót do menu. 

 

Dodawanie wiersza lub kolumny do tabeli StringGrid.

    To jak dodawać wiersz lub kolumnę do tabeli typu TStringGrid wie chyba każdy kto tego obiektu używał, wystarczy zrobić tak:

 

dodawanie wiersza: StringGrid1->RowCount++;

dodawanie kolumny: StringGrid1->ColCount++;

 

Takie dodawanie ma jednak wadę, kolumna lub wiersz zawsze są dodawane na końcu tabeli, dlatego w tej poradzie zaprezentuję własną funkcję AddColRow wstawiającą kolumnę lub wiersz w wybranej pozycji do tabeli. Przez "w wybranej pozycji" mam na myśli to, że kolumna lub wiersz zostaną wstawione do tabeli zawsze poniżej lub z prawej strony wybranej komórki, więc jest to nie tyle dodawanie wiersza lub kolumny, lecz ich wstawianie. Jeżeli komórki tabeli będą wypełnione jakimiś wartościami to zostaną one przesunięte w dół lub w prawą stronę, tak wby wstawiany wiersz lub kolumna były puste:

// Plik źródłowy np. Unit1.cpp
//--------------------------------
enum TAddColRow {crCol, crRow};
void __fastcall AddColRow(TStringGrid *Grid, TAddColRow crAdd)
{
 switch(crAdd)
 {
  case crRow: Grid->RowCount++;
              for(int i = Grid->RowCount - 1; i >= Grid->Row + 1; i--)
              {
               for(int j = 0; j < Grid->ColCount; j++)
               {
                if(i == Grid->Row + 1) Grid->Cells[j][Grid->Row + 1] = "";
                else Grid->Cells[j][i] = Grid->Cells[j][i - 1];
               }
              }
              return;
  case crCol: Grid->ColCount++;
              for(int i = Grid->ColCount - 1; i >= Grid->Col + 1; i--)
              {
               for(int j = 0; j < Grid->RowCount; j++)
               {
                if(i == Grid->Col + 1)
                {
                 Grid->Cells[Grid->Col + 1][j] = "";
                 Grid->ColWidths[Grid->Col + 1] = Grid->ColWidths[Grid->Col];
                }
                else
                {
                 Grid->Cells[i][j] = Grid->Cells[i - 1][j];
                 Grid->ColWidths[i] = Grid->ColWidths[i - 1];
                }
               }
              }
              return;
 }
}
//--------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
 AddColRow(StringGrid1, crRow); // wstawianie wiersza
}
//--------------------------------
void __fastcall TForm1::Button2Click(TObject *Sender)
{
 AddColRow(StringGrid1, crCol); // wstawianie kolumny
}
//--------------------------------

...powrót do menu. 

Kontekstowe wyszukiwanie tekstu w tabeli StringGrid.

    W tej poradzie zaprezentuję własnego pomysłu funkcję FindInGrid wyszukującą  kontekstowo zadany tekst lub tylko fragment tekstu, czyli frazę, ewentualnie całe zdanie. Funkcja sprawdza po kolei każdą komórkę w tabeli i jeśli napotka tam poszukiwany tekst to zwraca jako wynik numer kolumny i wiersza, dodatkowo funkcja zapamiętuje wynik i po kolejnym wybraniu wyszukiwania rozpoczyna przeszukiwanie od miejsc w którym skończyła:

// Plik źródłowy np. Unit1.cpp
//--------------------------------
struct FindCells
{
 int Row;
 int Col;
};
FindCells FindInGrid(TStringGrid *Grid, String tekst)
{
 FindCells fc;
 fc.Col = -1;
 fc.Row = -1;

 static int row = Grid->FixedRows;
 if(row >= Grid->RowCount - 1)
 row = Grid->FixedRows;

 for(int i = Grid->FixedCols; i < Grid->ColCount; i++)
 {
  for(int j = row; j < Grid->RowCount; j++)
  {
   String find = Grid->Cells[i][j];
   int x = find.Pos(tekst);
   if(x > 0)
   {
    fc.Col = i;
    fc.Row = j;
    row = j + 1;
    return fc;
   }
  }
 }
 row = Grid->FixedRows;
 return fc;
}
//--------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
 FindCells fc;
 fc = FindInGrid(StringGrid1, Edit1->Text);
 if(fc.Col < 0)
 {
  Application->MessageBox(("Nie można odnaleźć „" + Edit1->Text + "”").c_str(),
   "Znajdź", MB_OK | MB_ICONINFORMATION);
  return;
 }
 StringGrid1->Col = fc.Col;
 StringGrid1->Row = fc.Row;
 StringGrid1->SetFocus();
}
//--------------------------------

...powrót do menu. 

Zawijanie tekstu w komórkach tabeli StringGrid.

Zawijanie tekstu w komórkach tabeli należy realizować w zdarzeniu OnDrawCell obiektu StringGrid za pomocą funkcji DrawText. Tekst jest zawijany dopiero po opuszczeniu komórki, wtedy również mienia się rozmiar całego wiersza dostosowując się do wysokości tekstu w komórce.

// Plik źródłowy np. Unit1.cpp
//--------------------------------
void __fastcall TForm1::StringGrid1DrawCell(TObject *Sender, int ACol,
        int ARow, TRect &Rect, TGridDrawState State)
{
 AnsiString CellText = StringGrid1->Cells[ACol][ARow];
 int j = (Rect.Bottom - Rect.Top - StringGrid1->Canvas->TextHeight(CellText))/2;

 if(State.Contains(gdFixed))
 {
  StringGrid1->Canvas->Brush->Color = clBtnFace;
  StringGrid1->Canvas->Font->Color = clWindowText;
  StringGrid1->Canvas->FillRect(Rect);
  Frame3D(StringGrid1->Canvas, Rect, clBtnHighlight, clBtnShadow, 1);
 }
 else
  if(State.Contains(gdSelected))
  {
   StringGrid1->Canvas->Brush->Color = clHighlight;
   StringGrid1->Canvas->Font->Color = clHighlightText;
   StringGrid1->Canvas->FillRect(Rect);
  }
  else
  {
   StringGrid1->Canvas->Brush->Color = StringGrid1->Color;
   StringGrid1->Canvas->Font->Color = StringGrid1->Font->Color;
   StringGrid1->Canvas->FillRect(Rect);
  }

 TRect DrawRect = Rect;
 DrawText(StringGrid1->Canvas->Handle, CellText.c_str(), -1, &DrawRect, DT_WORDBREAK | DT_CALCRECT | DT_LEFT);

 if(StringGrid1->RowHeights[ARow] < (DrawRect.Bottom - DrawRect.Top) + 2)
  StringGrid1->RowHeights[ARow] = (DrawRect.Bottom - DrawRect.Top) + 2;
 else
  if(StringGrid1->RowHeights[ARow] > (DrawRect.Bottom - DrawRect.Top) + 2 && (DrawRect.Bottom - DrawRect.Top) + 2 > StringGrid1->DefaultRowHeight)
   StringGrid1->RowHeights[ARow] = (DrawRect.Bottom - DrawRect.Top) + 2;

 if(StringGrid1->RowHeights[ARow] <= StringGrid1->DefaultRowHeight)
 {
  StringGrid1->Canvas->Brush->Color = StringGrid1->Canvas->Brush->Color;
  StringGrid1->Canvas->FillRect(Rect);
  StringGrid1->Canvas->TextOut(Rect.Left, Rect.Top + j, CellText);
 }
 else
  DrawText(StringGrid1->Canvas->Handle, CellText.c_str(), CellText.Length(), &DrawRect, DT_WORDBREAK | DT_LEFT);
}
//--------------------------------

...powrót do menu. 

Sortowanie bąbelkowe tabeli.
 

W tej poradzie przedstawię prostą funkcję sortującą tabelę według kolumny rosnąco, be rozróżnienia na tekst i  liczby, wykorzystującą sortowanie bąbelkowe. Polega ono na zamienianiu miejscami kolejnych dwóch sąsiadujących ze sobą elementów, tak długo, aż uzyska się zbiór uporządkowany. Sortowanie bąbelkowe jest jedną z wolniejszych metod sortowania, więc raczej nie nadaje się do tabel zawierających kilka tysięcy wierszy.

Funkcja BubbleSort nie rozróżnia zamieszczony w komórkach tekstu na tekst i liczby, tylko traktuje wszystko jako tekst. Wyjaśniłem to dokładniej w poradzie Sortowanie tabeli StringGrid. W odróżnieniu od tamtej porady, ta metoda sortowania sortuje tylko rosnąco, aczkolwiek można to zmienić, zmieniając kierunek działania pętli for.



// Plik źródłowy np. Unit1.cpp
//--------------------------------
void BubbleSort(TStringGrid *Tabela, int Col)
{
 AnsiString temp;
 for(int i = Tabela->FixedRows; i < Tabela->RowCount; i++)
 {
  for(int j = Tabela->FixedRows; j < i; j++)
  {
   if(Tabela->Cells[Col][i] < Tabela->Cells[Col][j])
   {
    temp = Tabela->Rows[i]->GetText();
    Tabela->Rows[i]->SetText(Tabela->Rows[j]->GetText());
    Tabela->Rows[j]->SetText(temp.c_str());
   }
  }
 }
}
//--------------------------------
void __fastcall TForm1::Button1Click(TObject *Sender)
{
 BubbleSort(StringGrid1, 2);
}
//--------------------------------

Jak widać funkcja pobiera dwa argumenty, pierwszy to wskaźnik do tabeli którą chcemy posortować, drugi to numer kolumny według której odbywa się sortowanie.

...powrót do menu.