БлогNot. Редактируемый список чего-то в C++ Builder

Редактируемый список чего-то в C++ Builder

Решим типовую задачу поддержки редактируемого списка (таблицы) на основе табличной компоненты TStringGrid из библиотеки VCL. Допустим, нам нужен список со столбцами "Фамилия" (строка до 30 символов длиной) и "Показатель" (целое или вещественное число до 8 символов длиной).

Для работы с данными добавим на форму компоненту StringGrid1 с вкладки Additional, установим ей в Инспекторе Объектов следующие свойства:

RowCount=1
FixedRows=FixedCols=0
ColCount=2
Options->goEditing=true
Anchors= [akLeft,akTop,akBottom]
Width=390
Height=480
ScrollBars=ssVertical

Справа от таблицы расположим Panel1 и изменим ему свойства:

Caption = (пусто)
Anchors= [akTop,akRight,akBottom]
Width=250
Height=480

У самой формы можно поставить

Caption = Таблица
ClientWidth=640
ClientHeight=480
Constraints->MinWidth=640
Constraints->MinHeight=480
Position=poScreenCenter

Визуально "подровняем" таблицу и панель инструментов, добавим на панель кнопки "Добавить" и "Удалить", выйдет примерно вот что (верх окна):

Вид окна приложения
Вид окна приложения

В Unit1.cpp (например, перед конструктором формы) опишем глобальные данные:

const int COLUMNS=2; //Число столбцов таблицы
String ColumnHeaders[COLUMNS] = { "Фамилия", "Показатель" }; //Заголовки столбцов
String SortModeStr[COLUMNS] = { "v", "^" }; //Метки режима сортировки по столбцу
int SortMode = 0; //Флажок режима сортировки - не выбрано
int MaxLen[COLUMNS] = { 30, 8 }; //Максимальные длины текста в столбцах

На событие OnResize формы выполним код:

Panel1->ClientWidth = 250;
StringGrid1->ClientWidth = Form1->ClientWidth - Panel1->ClientWidth;
StringGrid1->Left = 0; Panel1->Left = StringGrid1->ClientWidth+1;
Panel1->Top = 0; StringGrid1->Top = 0;
Panel1->ClientHeight = Form1->ClientHeight;
StringGrid1->ClientHeight = Form1->ClientHeight;
StringGrid1->ClientWidth = Form1->ClientWidth - Panel1->ClientWidth;
StringGrid1->ColWidths[1]=100;
StringGrid1->ColWidths[0]= StringGrid1->ClientWidth - StringGrid1->ColWidths[1] - 24;
 //Место справа оставим на возможную вертикальную прокрутку
for (int i=0;i<Form1->ComponentCount; i++) {
 //Перебираем все компоненты формы
 TComponent* tmp = Form1->Components[i];
 String CName = String(tmp->ClassName()); //Получить имя типа компоненты
 if (CName == "TButton") { //Если кнопка
  TButton *bt1 =static_cast<TButton*>(FindComponent(tmp->Name));
  bt1->Left = (Panel1->ClientWidth - bt1->ClientWidth) / 2;
 }
 else if (CName == "TEdit") { //...или поле ввода -
  //центрируем их на панели Panel1
  TEdit *bt1 =static_cast<TEdit*>(FindComponent(tmp->Name));
  bt1->Left = (Panel1->ClientWidth - bt1->ClientWidth) / 2;
 }
}

Он поможет нам контролировать размеры компонентов при изменении размеров окна (между таблицей и панелью не будет пустого места). Всё остальное сделают якоря, которыми мы привязали компоненты к краям формы.

Ну и, заодно, столбцы таблицы примут удобные размеры.

Для простоты, и для столбцов таблицы, и для относительных размеров таблицы и панели мы один размер делаем фиксированным, а другой "резиновым". В реальной программе может понадобиться несколько более сложный пересчёт размеров. А лучше, вообще-то, делать в среде, где возможна полностью логическая вёрстка макета формы (Visual C++ или QT).

На событие OnCreate формы выполним:

for (int c=0; c<COLUMNS; c++) {
 StringGrid1->Cells[c][0] =  ColumnHeaders[c];
}

то есть расставим столбцам заголовки. Как сделать их "красивыми" или хотя бы отличающимися от того, как выглядит содержимое? Чтобы изменить отрисовку некоторых ячеек, используйте OnDrawCell таблицы, например, центрируем заголовки на желтом фоне:

if (ARow==0) {
 StringGrid1->Canvas->Brush->Color = clYellow;
 StringGrid1->Canvas->FillRect(Rect);
 DrawText(StringGrid1->Canvas->Handle,StringGrid1->Cells[ACol][0].w_str(),-1,
   &Rect,DT_CENTER|DT_VCENTER|DT_SINGLELINE);       
}

Если у Вас старая версия Builder, не понимающая wchar_t, вместо w_str придётся написать имя метода c_str.

Мы разрешили редактирование таблицы, но не хотим, чтобы пользователь мог менять заголовки. Решить проблему поможет событие OnSelectCell таблицы. Выполним в этом обработчике:

if (ARow==0) StringGrid1->Options >> goEditing;
else StringGrid1->Options << goEditing;

- то есть, мы временно выключаем редактирование таблицы при выборе ячейки в первой строке.

Реализуем код для кнопки "Добавить", добавляющий пустую запись после выбранной в таблице строки (в том числе, заголовочной):

int Row = StringGrid1->Row, r, c;
StringGrid1->RowCount++;
for (r=StringGrid1->RowCount-1; r>Row+1; r--)
for (c=0; c<COLUMNS; c++) {
 StringGrid1->Cells[c][r] = StringGrid1->Cells[c][r-1];
 StringGrid1->Cells[c][r-1]="";
}
if (StringGrid1->Row+1 < StringGrid1->RowCount) StringGrid1->Row++;
StringGrid1->SetFocus();

Последний оператор поможет программе сразу перейти на добавленную строчку.

Настало время реализовать функционал удаления текущей записи (по нажатию кнопки "Удалить"):

int Row = StringGrid1->Row, r, c;
if (Row>0) {
 for (r=Row+1; r<StringGrid1->RowCount; r++)
 for (c=0; c<COLUMNS; c++) {
  StringGrid1->Cells[c][r-1]=StringGrid1->Cells[c][r];
 }
 for (c=0; c<COLUMNS; c++) StringGrid1->Cells[c][StringGrid1->RowCount-1]="";
 StringGrid1->RowCount--;
 if(StringGrid1->Row>StringGrid1->RowCount-1) StringGrid1->Row=StringGrid1->RowCount-1;
}
else StringGrid1->SetFocus();

Задействуем контроль длины строк в событии OnSelEditText таблицы:

if (StringGrid1->Cells[ACol][ARow].Length() > MaxLen[ACol]) {
 StringGrid1->Cells[ACol][ARow] = StringGrid1->Cells[ACol][ARow].SubString(1,MaxLen[ACol]);
}

Ограничить диапазоны вводимых символов можно в обработчике события OnKeyPress, примерно так, как сделано в предыдущей заметке. Отмечу, что увлекаться "текущим" контролем пользователя в процессе ввода им данных не нужно - трудоёмко писать, а юзера только раздражает. Лучше дать ему ввести, что он хочет, и потом проверить по завершении ввода.

Сделаем в нашей таблице сортировку по событию двойного щелчка на заголовке столбца. Соответственно, нам придётся написать такой вот обработчик события OnDblClick таблицы:

int Col = StringGrid1->Selection.Left;
int Row = StringGrid1->Selection.Top;
if (Row==0) {
 SortMode = (SortMode==1?2:1);
 StringGrid1->Cells[Col][0]= ColumnHeaders[Col]+" "+ SortModeStr[SortMode-1];
 for (int c=0; c<COLUMNS; c++) if(c!=Col)
 StringGrid1->Cells[c][0]= ColumnHeaders[c];
 if (StringGrid1->RowCount>2) {
  SortGrid(StringGrid1, Col, SortMode);
  StringGrid1->Refresh();
 }
}

Первые 2 строчки также показывают, как получить координаты текущей строки и столбца в таблице, если обработчик события не получает их параметрами.

Остальной код нужен, чтобы показать юзеру режим сортировки дополнительными значками "v" или "^" в строке заголовка.

Непосредственно сортировкой занимается вспомогательная функция SortGrid, код которой приводится ниже.

void SortGrid(TStringGrid *SG, int Column, int Mode) {
 int MinRowId, RowId1, RowId2, ColId;
 String Temp1, Temp2;
 for (RowId1=1; RowId1<SG->RowCount - 1;RowId1++) {
   Temp1 = SG->Cells[Column][RowId1];
   MinRowId = RowId1;
   for (RowId2=RowId1 + 1;RowId2<SG->RowCount;RowId2++) {
     Temp2 = SG->Cells[Column][RowId2];
     if (Mode==1 && Temp2 < Temp1 || Mode==2 && Temp2 > Temp1) {
       Temp1 = SG->Cells[Column][RowId2];
       MinRowId = RowId2;
     }
   }
   for (ColId= 0; ColId<SG->ColCount; ColId++) {
     Temp2 = SG->Cells[ColId][RowId1];
     SG->Cells[ColId][RowId1] = SG->Cells[ColId][MinRowId];
     SG->Cells[ColId][MinRowId] = Temp2;
    }
  }
}

Можно сделать более "продвинутым" методом, а не "пузырьком"... мы использовали простейший.

Займёмся поиском по таблице с переходом к найденной строке. Для этого добавим на форму поле ввода Edit1, в котором будем писать искомый фрагмент. В свойствах поля выставим

Text = (пусто)
MaxLength = 30

Запускать процесс поиска проще всего по нажатию кнопки "Найти", добавим её на форму и реализуем обработку её события OnClick:

int r,c,Row=StringGrid1->Row,found=0;
if (StringGrid1->RowCount>0) {
 String FindStr = LowCase (Edit1->Text);
 for (r=Row+1; r<StringGrid1->RowCount; r++)
 for (c=0; c<StringGrid1->ColCount; c++) {
  if (LowCase(StringGrid1->Cells[c][r]).Pos(FindStr)>0) {
   StringGrid1->Row=r;
   StringGrid1->Col=c;
   StringGrid1->SetFocus();
   found=1;
   goto out_label;
  }
 }
 out_label:
 if (!found) ShowMessage ("Не найдено!");
}
else ShowMessage ("Таблица пуста, поиск невозможен!");

Здесь поиск начинается со строки, следующей за текущей, и останавливается, если найдено совпадение содержимого ячейки с искомой строкой (в том числе, частичное) или достигнут конец таблицы. Чтобы не зависеть от регистра символов и используемой кодировки, сравниваются строки, приведённые к одному регистру собственной функцией LowCase, вот её простейшая (по коду, а не скорости выполнения) реализация:

String LowCase (String s) {
 String Upper = "ABCDEFGHIJKLMNOPQRSTUVWXYZАБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ";
 String Lower = "abcdefghijklmnopqrstuvwxyzабвгдеёжзийклмнопрстуфхцчшщъыьэюя";
 String s2="",subs;
 int pos;
 for (int i=1; i<=s.Length(); i++) {
  subs = s.SubString(i,1);
  pos = Upper.Pos(subs);
  s2+=pos>0?Lower.SubString(pos,1):subs;
 }
 return s2;
}

Очистка всей таблицы (кроме заголовка) - конечно, тоже нужное дело, повесим её на кнопку и снабдим окном-предупреждением:

if (MessageDlg("Вы уверены, что хотите очистить всю таблицу?",
 mtConfirmation	, TMsgDlgButtons() << mbOK << mbCancel, 0)==mrOk) {
 ClearTable(StringGrid1);
}

Как видно из кода, непосредственно очисткой займётся служебная функция ClearTable, почему так, будет ясно дальше.

void ClearTable (TStringGrid *StringGrid1) {
 for (int i=1;i<StringGrid1->RowCount; i++) StringGrid1->Rows[i]->Clear();
 StringGrid1->RowCount=1;
 StringGrid1->Row=0;
 StringGrid1->SetFocus();
}

Наконец, неплохо бы добавить сохранение содержимого таблицы в файл и возможность загрузки его из файла. Для простоты примем, что нас интересуют только текстовые файлы, в окно формы добавим стандартные диалоги сохранения и открытия файла (TSaveDialog и TOpenDialog, вкладка Dialogs), а всё остальное сделаем программно по нажатию новой кнопки "Сохранить":

SaveDialog1->DefaultExt="txt";
SaveDialog1->Filter="ASCII files (*.txt)|*.txt";
if(SaveDialog1->Execute()){
 if (ExtractFileExt(SaveDialog1->FileName).UpperCase()==".TXT") {
  FILE *fp=fopen(SaveDialog1->FileName.c_str(),"wt");
  if (fp){
   if (StringGrid1->RowCount>1)  
   for (int i=1;i<StringGrid1->RowCount;i++) {
    fprintf(fp,"%s",StringGrid1->Rows[i]->CommaText.c_str());
    if (i<StringGrid1->RowCount-1) fprintf(fp,"\n");
   }
   fclose(fp);
  }
  else ShowMessage ("Не могу открыть файл на запись, проверьте допустимость имени и права");
 }
 else {
  ShowMessage ("Поддерживается сохранение только текстовых файлов!");
 }
}

Не забудьте для работы кода подключить в модуле заголовки стандартной библиотеки ввода-вывода:

#include <stdio.h>

Логику функции для загрузки файла нам особо менять не придётся, главное, чтоб файл был не испорчен каким-то внешним вмешательством :) Естественно, на форму добавлена ещё одна кнопка - "Загрузить".

const int maxlen = 128;
String str; str.SetLength(maxlen+1);
OpenDialog1->DefaultExt="txt";
OpenDialog1->Filter="ASCII files (*.txt)|*.txt";
if(OpenDialog1->Execute()){
 if (ExtractFileExt(OpenDialog1->FileName).UpperCase()==".TXT") {
  FILE *fp=fopen(OpenDialog1->FileName.c_str(),"rt");
  if (fp){
   ClearTable(StringGrid1);
   int i=1;
   while (1) {
    fgets(str.c_str(),maxlen,fp);
    if (str.Length()>0) {
     StringGrid1->RowCount++;
     StringGrid1->Rows[i++]->CommaText = str;
    }
    if (feof(fp)) break;
   }
   fclose(fp);
  }
  else ShowMessage ("Не могу открыть файл на чтение, проверьте допустимость имени и права");
 }
 else {
  ShowMessage ("Поддерживается чтение только текстовых файлов!");
 }
}

Тестовый файл для открытия/сохранения возьмём таким, чтоб он содержал "неудобные" символы - кавычки, запятые, пробелы и т.п. Вот как выглядит сохранённый файл:

"1,2","2,3"
" ","""кавы"""
"кук пробельчеги","223,1"

Лишних пустых строк (в начале или конце) в файле быть не должно.

 Скачать как проект C++ Builder в архиве .zip (7 Кб)

Проект я проверил в старом Builder 6 под Windows 7, больше ничего не было под рукой :) В новом Builder XE заметных отличий не будет, может, перед строковыми константами придётся "L" поставить.

30.09.2014, 17:57 [16298 просмотров]


теги: c++ программирование список builder

К этой статье пока нет комментариев, Ваш будет первым