Введение

Настоящая статья предназначена для тех, кто собирается автоматизировать процесс подготовки шаблонов конфигураций продаваемого оборудования. Как отмечалось, не все покупатели точно знают, что именно хотят приобрести, да и не во всех может параметрах ряда товаров разобраться обыватель (например, бытовая техника или компьютеры), зачастую требуется консультация эксперта.

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

Уяснив структуру проектируемого приложения, мы можем приступать к написанию кода.

Что нам понадобится?

Конечно, можно обойтись и Access'ом 97 или 2000, однако советую перевести Access- базу нашего магазина в Microsoft SQL Server. Причина здесь очевидна — колоссальная прибавка производительности Microsoft SQL Server'а по сравнению с Microsoft Access'ом. Но как быть? Ведь база уже подготовлена с помощью Access. Рассмотрим подробно процесс создания SQL-версии базы данных.

Итак, для того чтобы скопировать таблицы из базы данных формата Microsoft Access 2000 в Microsoft SQL Server 2000, необходимо запустить средство транспортировки данных DTS (Data Transformation Service), а для этого нужно запустить: Programs->Microsoft SQL Server->Import and Export Data.

Перед вами появится окно мастера импорта и экспорта данных, в котором необходимо последовательно задать источник и приемник данных. В качестве источника данных выберем исходный файл с Access'овской базой данных.

А в качестве приемника данных выберем Microsoft OLE DB Provider for SQL Server, введем пароль системного администратора, а также имя SQL-базы данных нашего магазина (можно либо создать базу данных заблаговременно, либо выбрать в списке элемент «<new>» и в появившемся окне впечатать имя создаваемой базы данных — в нашем случае IShop и продолжим процесс импорта нажатием на кнопку Next.

После этого следует еще раз нажать кнопку Next, выбирая из предлагаемых операций простое копирование таблиц, в появившемся списке таблиц проставить галочки слева от тех таблиц, которые должны быть скопированы (в нашем случае можно воспользоваться кнопкой Select All, выделив все таблицы Access'овской базы) и запустить процесс импорта данных (Run Immediately), запуская таким образом процесс копирования данных немедленно. После этого начнется процесс копирования данных.

По окончании процесса копирования можно запустить Enterprise Manager (Programs->Microsoft SQL Server-> Enterprise Manager) и убедиться в том, что таблицы попали в нужную базу данных.

Вроде бы все готово и можно приступать к процессу создания приложения — лиента к SQL-базе данных. Однако перед этим следует выполнить еще несколько действий. Во-первых, после перегона данных из Access'а в SQL Server следует внимательно просмотреть все типы полученных там полей данных и проверить их правильность. Если какие-либо типы данных не соответствуют требуемым, можно выполнить преобразование вручную, если же данные в ходе этого могут быть искажены (о чем вас предупредит Enterprise Manager), можно повторить процесс перегона данных, в ходе которого воспользоваться кнопкой Advanced в программе — трансформаторе данных, где задать правила перетипизации данных. Далее необходимо указать ключевое поле Set Primary Key и включить свойство счетчика Identity, выставив в нем значение Yes.

В результате каждая таблица данных в режиме редактирования типов полей Design должна выглядеть так, как показано на рисунке.

И наконец, рекомендуется создать еще один псевдоним к нашей базе данных — для упрощения процесса создания приложения с помощью Borland C++Builder. Он называется алиасом (от англ. Alias) и служит для связи реальной базы данных с так называемым BDE (Borland Database Engine).

Связь с базами данных в C++Builder

Основой работы C++Builder с базами данных является Borland Database Engine (BDE) — процессор баз данных фирмы Borland. BDE служит посредником между приложением и базами данных. Он предоставляет пользователю единый интерфейс для работы, освобождающий пользователя от конкретной реализации базы данных. Благодаря этому отпадает необходимость менять приложение при смене реализации базы данных. Приложение C++ Builder никогда не обращается к базе данных непосредственно, а только к BDE.

Приложение C++Builder, когда ему нужно связаться с базой данных, обращается к BDE и обычно сообщает псевдоним базы данных и необходимую таблицу в ней. BDE реализован в виде динамически подключаемых библиотек DLL, которые, как и любые другие библиотеки, снабжены API (Application Program Interface — интерфейс прикладных программ), названным IDAPI (Integrated Database Application Program Interface). Это список процедур и функций для работы с базами данных, которым и пользуются приложения, создаваемые с помощью Borland C++Builder.

BDE по псевдониму находит подходящий для указанной базы данных драйвер. Драйвер — это вспомогательная программа, «понимающая», как общаться с базами данных определенного типа. Если в BDE имеется собственный драйвер соответствующей СУБД, то BDE связывается через него с базой данных и с нужной таблицей в ней, отрабатывает запрос пользователя и возвращает в приложение результаты обработки. BDE поддерживает естественный доступ к таким базам данных, как Microsoft Access, FoxPro, dBase и Paradox.

Если же собственного драйвера нужной СУБД в BDE нет, то можно воспользоваться ODBC (в предыдущей статье описано, как это делается для базы данных MS Access). ODBC (Open Database Connectivity) — DLL, аналогичная по функциям BDE, но разработанная компанией Microsoft. Поскольку Microsoft включила поддержку ODBC в свои офисные продукты и для ODBC созданы драйверы практически к любым СУБД, компания Borland включила в BDE драйверы, позволяющие использовать ODBC-псевдонимы.

В отличие от BDE работа через ODBC осуществляется гораздо медленнее, поэтому рекомендуется по возможности пользоваться именно BDE-алиасами и BDE-драйверами к базе данных. Однако, если это невозможно (по причине отсутствия таковых), можно воспользоваться и ODBC-алиасами и драйверами соответственно.

Теперь рассмотрим процесс создания BDE-алиаса к нашей базе данных во всех подробностях.

Для начала следует запустить программу-конфигуратор Database Desktop, входящую в состав пакета Borland C++Builder. Для этого выполним команду: Programs->Borland C++Builder->Database Desktop. Далее уже в Database Desktop выполним команду меню Tools->Alias Manager, в появившемся окне конструктора псевдонимов нажмите на кнопку «New» для создания нового псевдонима и заполните поля соответствующими значениями.

Теперь можно проверить правильность только что созданного псевдонима (кнопка Connect Now). При выходе из программы Database Desktop попросит сохранить созданный алиас и при этом обновит файл конфигурации всех алиасов системы.

Как видите, создавать ODBC-алиас при разработке приложений клиентов к базам данных с помощью Borland C++Builder'а в общем случае вовсе не обязательно. Однако не следует забывать, что в нашем случае клиентом к базе данных служит не только Windows-приложение, которое мы собираемся разрабатывать, но и Web-приложение (рассмотренное в деталях в предыдущей статье). Поэтому нам без создания ODBC-псевдонима не обойтись.

Для этого:

Вы увидите появившуюся строку в списке источников данных в вашей системе.

Разберемся, каким образом следует сконфигурировать разрабатываемое приложение в зависимости от того, какой псевдоним к базе данных будет использоваться. Речь идет о свойствах объекта Tdatabase. В нашем случае следует заполнить его свойства следующими значениями:

Вся остальная информация о нашей базе данных уже содержится в псевдониме и поэтому может не указываться. Если же по каким-то причинам использование BDE-псевдонима невозможно, то свойства объекта TDatabase в нашем случае будут выглядеть следующим образом:

DatabaseName = 'ISHop'
LoginPrompt = False
Params.Strings = (
    [ODBC]
    DRIVER=MSSQL — драйвер базы данных
    UID=sa — учетная запись
    DATABASE=ISHop — название базы данных
    APP=ISHManager — название приложения
    SERVER= Aquarius — название сервера
    USER NAME=sa — учетная запись пользователя
    PASSWORD=123 — пароль пользователя
)

Что же необходимо изменить в ASP-коде магазина для того, чтобы все работало под управлением SQL-сервера. По сути, все изменения относятся к способу подключения к базе данных, а именно:

вместо строки: db.Open "DSN=IShop;UID=sa;PWD=;"

будем использовать строку: db.Open "DSN=ISHop; UID=sa;PWD=;database=ISHop"

Прежде чем приступать...

...Хочется дать один совет относительно способов проектирования приложений-клиентов к базам данных с помощью Borland C++ Builder. Речь идет о двух компонентах, без существования которых не обошлось бы ни одно СУБД-приложение, а именно о TTable и TQuery.

Понимание разницы между ними очень важно для дальнейшей работы. Оба эти компонента предоставляют доступ к базе данных. Компонент TTable предоставляет более удобный интерфейс к таблицам базы данных, однако в отличие от компонента TQuery усложняет разработку сетевых многопользовательских приложений. Дело в том, что компонент TTable в подключенном к таблице базы данных состоянии (Active = true) не дает двум или более пользователям одновременно редактировать таблицу в базе данных. Эту проблему можно решить, открывая и закрывая элемент типа TTable всякий раз, когда происходит обращение к таблице. Однако при этом, естественно, указатель активной записи в таблице теряет свое положение. Поэтому, открывая таблицу в последующем, придется снова находить нужную (последнюю) запись. Конечно, это не составляет труда, но тем не менее может серьезно замедлить работу многопользовательского приложения. В нашем случае многопользовательность — не самое главное, однако мы будем использовать как компонент TTable, так и TQuery.

Используемые компоненты

Теперь рассмотрим компоненты, используемые для визуализации данных и одновременной связи с ними. Такие компоненты расположены в палитре Data Controls Borland C++Builder, из которой нам понадобится лишь один — ТDBLookupComboBox — выпадающий список выбора, связанный с определенным полем заданной таблицы заданной базы данных. Это означает, что изменение текущего значения в таком поле приводит к изменению позиции указателя в связанной с ним таблице данных и наоборот. Согласитесь, что такой компонент избавляет нас от необходимости писать громоздкие функции-обработчики; надо всего лишь заполнить его свойства, а именно:

Итак...

Прежде всего необходимо подготовить форму приложения, причем отметим сразу, что оно будет состоять из двух частей:

Для этого удобнее всего воспользоваться компонентом TPageControl со вкладки Win32 палитры компонентов Borland C++Builder'а версии 5.0.

Создадим главное окно нашего приложения, поле типа TlistBox, в котором будут отображаться ключевые поля анализируемой таблицы (исходные данные) и поле с названиями соответствующих таблиц (также типа TListBox). Эти поля будут служить для установления соответствия названий ключевых полей исходного прайса таблицам базы данных, в которые программа будет вставлять значения.

Далее создадим компоненты типа TQuery для построения запросов к базам данных и определим процедуру инициализации формы:

//--------------------------
void __fastcall TMainForm::FormCreate(TObject *Sender)
{
 AnsiString Str;

 Height = 386;
 
 ErrStr1 = "";
 ErrStr2 = "";
 ErrStr3 = "";

// Подключение базы данных
 Database1->Connected = true;

//Подключение таблиц данных
 ConfTB->Active = true;
 MBTB->Active = true;
 CPTB->Active = true;
 SVTB->Active = true;
 IDEHTB->Active = true;
 SCSITB->Active = true;
 SndTB->Active = true;
 KBDTB->Active = true;
 MouseTB->Active = true;
 CaseTB->Active = true;
 MonTB->Active = true;
 SpeakerTB->Active = true;
 FaxTB->Active = true;
 RAMTB->Active = true;
 FDDTB->Active = true;
 CDTB->Active = true;
 PadTB->Active = true;
 // Функция загрузки текущего шаблона
 OpenConfiguration(); 
 // Функция чтения ключевых полей 
 // исходного прайс-листа
 ReadKF();

 Str = AnsiString(KeyFields->Items->Count);
 Label1->Caption = Label1->Caption + Str;
 Str = AnsiString(Map->Items->Count);
 Label5->Caption = Label5->Caption + Str;
 KeyFields->ItemIndex = 0;
 Map->ItemIndex = 0;
 KeyFieldsClick(Sender);
 MapClick(Sender);
}
//--------------------------

Кроме того, процедуру, вызываемую при закрытии формы:

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

void __fastcall TMainForm::FormDestroy(TObject *Sender)
{
 SaveConfiguration(); // Сохранение текущей конфигурации
 ConfTB->Active = false;
 MBTB->Active = false;
 CPTB->Active = false;
 SVTB->Active = false;
 IDEHTB->Active = false;
 SCSITB->Active = false;
 SndTB->Active = false;
 KBDTB->Active = false;
 MouseTB->Active = false;
 CaseTB->Active = false;
 MonTB->Active = false;
 SpeakerTB->Active = false;
 FaxTB->Active = false;
 RAMTB->Active = false;
 FDDTB->Active = false;
 CDTB->Active = false;
 PadTB->Active = false;
 Query1->Close();
 Query2->Close();
 Query3->Close();
 Query4->Close();
 Database1->Connected = false;
}
//---------------------------

Создадим две вкладки и назовем их «Прайс-процессор» и «Конфигуратор ПК» соответственно. Определим процедуру переключения вкладок следующим образом:

//---------------------------
void __fastcall TMainForm::PageControlChange(TObject *Sender)
{
 if (PageControl->ActivePage == TabSheet1) Height = 386;
 else Height = 518;
}
//---------------------------

Таким образом, высота окна нашей формы будет изменяться в зависимости от того, какая из двух вкладок активна. Далее определим связанные с источниками данных списки выбора комплектующих (для этого воспользуемся компонентами DBLookupComboBox из палитры DataAccess) и функцию инициализации состояния списков выбора комплектующих:

//---------------------------
void TMainForm::OpenConfiguration()
{
 Configuration->DataField = "PCType";
 Configuration->KeyValue = 
	ConfTB->FieldByName("ID")->AsInteger;

 MB->DataField = "Title";
 MB->KeyValue = ConfTB->FieldByName("MainBoardID")->AsInteger;
// Текущее значение выпадающего 
// списка позиций совпадает с 
// заданной позицией указателя в 
// соответствующей таблице
 MBPrc->Text = 
	AnsiString(ConfTB->FieldByName("MainBoardNum")
	->AsInteger); 

// Занесем в поле типа TEdit значение 
// количества экземпляров текущей позиции
// ...
// И далее для всех таблиц данных
// ...

// Функция подсчета интегральной 
// стоимости набора
 CalcTotalPrice(); 
}
//---------------------------

Прежде чем приступить к написанию функции подсчета интегральной стоимости всего набора, подготовим поля для указания стоимости (TDBEdit) и количества экземпляров (TEdit) каждого типа комплектующих. Теперь напишем функцию подсчета стоимости всего набора:

//---------------------------
void TMainForm::CalcTotalPrice()
{
 AnsiString Str, dig = "";
 float CurVal, TotalVal = 0.0;
 int CurNum;

 Str = MBPrc->Text;
// Проверка правильности ввода
 try { CurNum = Str.ToInt(); } 
 catch (Exception &E) {
  Str = "Допустимы только целые численные значения!\n"
  Str = Str + Str + "—- не верный формат целого числа.";
  Application->MessageBox(Str.c_str(), "Ошибка!", MB_ICONERROR|MB_OK);
  return;
}

Str = MBNum->Field->AsString;
CurVal = Str.ToDouble();
// Подсчет общей стоимости
TotalVal = TotalVal + CurVal * CurNum; 

// ...
// И далее для всех таблиц данных
// ...

// Отсечем ненужные знаки после 
// запятой (после второй цифры)
 Str = AnsiString (TotalVal);

 for (int i = 1; i <= Str.Length(); i++) {
   if (Str[i] == ',') {
   dig = dig + Str[i];
   if (Str.Length() >= i+1) dig = dig + Str[i+1];
   if (Str.Length() >= i+2) dig = dig + Str[i+2];
   break;
  }
  dig = dig + Str[i];
 }

// И выведем полученное значение суммарной 
// стоимости в соответствующее поле
 Total->Text = dig; 
}
//---------------------------

Теперь настало время разобраться с ключевыми полями нашего прайса. Допустим, прайс-лист ежедневно поставляется в формате Excel-файла, причем все позиции в нем представлены в одной таблице в форме списка (это наиболее типичный случай). Для автоматизации его разбивки на соответствующие разделы необходимо выявить признаки, свойственные наименованиям этих разделов. Назовем эти разделы ключевыми полями и сымпортируем Excel-файл в SQL Server (это делается точно так же, как и в случае с Access'ом) в таблицу с именем Src. Далее от нас потребуется, «проходя» последовательно по всем записям этой таблицы и выявляя ключевые поля, вставлять записи в соответствующие названиям ключевых полей таблицы. Остается одно: определить то свойство, которое позволит нам отличить наименование раздела (например, «Процессоры» или «Материнские платы» от наименования позиций). Предположим, что в нашем случае такая разница заключается в отсутствии значения в столбцах «Цена» у наименования позиции.

//---------------------------
void TMainForm::ReadKF()
{
 AnsiString sSQL, CurField, p1, p2, p3, p4, p5, descr;
 int i;

// Выборка всех записей таблицы-исходного прайс-листа
 sSQL = "SELECT * FROM Src"; 

 Query1->Close();
 Query1->SQL->Clear();
 Query1->SQL->Add(sSQL);
 Query1->Open(); // Выполнение запроса

 KeyFields->Items->Clear();

 while (!Query1->Eof) {
  CurField = 
    Query1->FieldByName("Title")-
			>AsString; 
// Поле «Title» текущей записи
  p1 = Query1->FieldByName("Price1")->AsString;
  p2 = Query1->FieldByName("Price2")->AsString;
  p3 = Query1->FieldByName("Price3")->AsString;
  descr = Query1->FieldByName("Description")->AsString;
// Если значение цен в столбцах отсутствует, значит это ключевое поле
// В противном случае продолжаем...
  if (p1 != "" || p2 != "" 
		|| p3 != "" || descr != "") {
   Query1->Next();
   continue;
  }

 int k;
 k = CurField.Length();

// Отсечем возможные порядковые номера раздела 
 if (CurField[1] == '1' || CurField[1] == '2' || CurField[1] == '3' ||
     CurField[1] == '4' || CurField[1] == '5' || CurField[1] == '6' ||
     CurField[1] == '7' || CurField[1] == '8' || CurField[1] == '9' ||
     CurField[1] == '0' || CurField[1] == '.') {

  for (i = 1; i <= k;  i++)
   if (CurField[i] == '1' || CurField[i] == '2' || CurField[i] == '3' ||
       CurField[i] == '4' || CurField[i] == '5' || CurField[i] == '6' ||
       CurField[i] == '7' || CurField[i] == '8' || CurField[i] == '9' ||
       CurField[i] == '0' || CurField[i] == '.') {
   CurField = CurField.SubString(i+1, CurField.Length());
   i = 0;
   k = CurField.Length();
   }
   else break;

 }

  if (CurField[1] == ' ') CurField = CurField.SubString(2, CurField.Length());

// Все изменения будем делать непосредственно в таблице Src
  Query1->Edit();
  Query1->FieldByName("Title")->AsString = CurField;
  Query1->Post();

// И добавим полученное значение текущего 
// распознанного ключевого поля к списку ключевых полей

  KeyFields->Items->Add(CurField);
  Query1->Next();
 }
 Query1->First();

 sSQL = "SELECT * FROM _Components ORDER BY CategoryTableName ASC";
// Выберем из управляющей таблицы с наименованиями 
// таблиц данных все записи,
// расположенные в алфавитном порядке по 
// наименованиям таблиц данных

 Query4->Close();
 Query4->SQL->Clear();
 Query4->SQL->Add(sSQL);
 Query4->Open(); // Выполним запрос

// И загрузим результат запроса в список выбора
 Map->Items->Clear();
 while (!Query4->Eof) {
   Map->Items->Add(Query4-
		>FieldByName("CategoryTableName")->AsString);
   Query4->Next();
 }
 Query4->First();
}
//---------------------------

Поскольку функция обновления всех таблиц данных производится оператором регулярно, то перед каждым добавлением новых значений все таблицы данных необходимо очистить от старых значений. Для этого:

//---------------------------
void __fastcall TMainForm::DropAllClick(TObject *Sender)
{
 AnsiString sSQL;

 if (Application->MessageBox("Данная 
	операция очистит содержимое ВСЕХ таблиц базы. 
	Выполнить?", "Внимание!", 
	MB_ICONQUESTION|MB_YESNO) == IDYES) {

  sSQL = "SELECT * FROM _Components"; 
// Из всех таблиц, чье название содержится в столбце
// "CategoryTableName" управляющей таблицы _Components,
// удалим все данные

  Query3->Close();
  Query3->SQL->Clear();
  Query3->SQL->Add(sSQL);
  Query3->Open();

  while (!Query3->Eof) {
   sSQL = "DELETE FROM " + 
	Query3->FieldByName("CategoryTableName")->AsString;
   Query2->Close();
   Query2->SQL->Clear();
   Query2->SQL->Add(sSQL);
   Query2->ExecSQL();
   Query3->Next();
  }
  Application->MessageBox("Содержимое 
		таблиц данных уничтожено!", 
		"Готово!", 0);
 }
}
//---------------------------

После того как содержимое всех таблиц данных уничтожено, можно вставить в них новые значения:

//---------------------------
void __fastcall TMainForm::UpdateAllClick(TObject *Sender)
{
 AnsiString gSQL, CItem, NItem, CTitle;
 int i;

// Выборка всех записей таблицы — 
// исходного прайс-листа
 gSQL = "SELECT * FROM Src"; 

 Query2->Close();
 Query2->SQL->Clear();
 Query2->SQL->Add(gSQL);
 Query2->Open(); // Выполнение запроса

 i = 0;

 while (i < KeyFields->Items->Count) {
 // Для каждого ключевого поля

  while (!Query2->Eof) {
   CItem = KeyFields->Items->Strings[i];
   //Начиная со следующей за ключвым полем записи 
   //(до следующего ключевого поля)
   if (i < KeyFields->Items->Count — 1) 
		NItem = KeyFields->Items->Strings[i+1];
   else NItem = "NItem";
   CTitle = Query2->FieldByName("Title")->AsString;
  // Если текущее поле совпадает с текущим 
  // ключевым полем
   if (CTitle == CItem) {
    // То вставим в таблицу с названием, 
   // совпадающим с названием текущего ключевого поля,
   // Строку"Выберите позицию" 
    InsRecord(CItem, "Выберите позицию", NULL, NULL, NULL, NULL);

   // И далее в цикле все значения до 
  // следующего ключевого поля  
    while (CTitle != NItem && !Query2->Eof) {
       if (CTitle == CItem) {
        Query2->Next();
        CTitle = Query2->FieldByName("Title")->AsString;
        continue;
       }
       CTitle = Replace(CTitle);
       InsRecord(CItem, CTitle, Query2->FieldByName("Price1")->AsFloat,
                   Query2->FieldByName("Price2")->AsFloat,
                   Query2->FieldByName("Price3")->AsFloat,
    Query2->FieldByName("Description")->AsString);
       Query2->Next();
       CTitle = Query2->FieldByName("Title")->AsString;
    }
    i++;
    Query2->First();
    break;
   }
   else Query2->Next();
  }
 }

 Application->MessageBox("Содержимое таблиц 	
	данных обновлено!", "Готово!", 0);
}
//---------------------------

Теперь разберемся с функциями InsRecord и Replace. Функция InsRecord, по сути, генерирует SQL-строку для ввода значений в заданную таблицу базы данных:

//---------------------------
void TMainForm::InsRecord(AnsiString TName,      
 	  // Название таблицы
      AnsiString Title,                           
	  // Наименование позиции
      float Price1, float Price2, float Price3,   
	  // Значения цен
      AnsiString Description)      
	  // Строка с дополнительным описанием
{
 AnsiString Str, tmp, dig;

 Str = "INSERT INTO " + TName; 
 Str = Str + "(Title, Price1, Price2, Price3, Description)";
 Str = Str + " VALUES";

Текущее значение не должно содержать символов кавычек
 for (int j = 1; j<= Title.Length(); j++) if (Title[j] == '\'') Title[j] = ' ';

 Str = Str + "('"  + Title;
 Str = Str + "', ";

// Прежде чем вставить численные значения, 
// заменим в строках символы «,» на символы «.»

 dig = "";
 tmp = AnsiString (Price1);
 for (int j = 1; j <= tmp.Length(); j++) {
  if (tmp[j] == ',') {
   tmp[j] = '.';
   dig = dig + tmp[j];
   if (tmp.Length() >= j+1) dig = dig + tmp[j+1];
   if (tmp.Length() >= j+2) dig = dig + tmp[j+2];
   break;
  }
  dig = dig + tmp[j];
 }
 Str = Str + dig;
 Str = Str + ", ";

 dig = "";
 tmp = AnsiString (Price2);
 for (int j = 1; j <= tmp.Length(); j++) {
  if (tmp[j] == ',') {
   tmp[j] = '.';
   dig = dig + tmp[j];
   if (tmp.Length() >= j+1) dig = dig + tmp[j+1];
   if (tmp.Length() >= j+2) dig = dig + tmp[j+2];
   break;
  }
  dig = dig + tmp[j];
 }
 Str = Str + dig;
 Str = Str + ", ";

 dig = "";
 tmp = AnsiString (Price3);
 for (int j = 1; j <= tmp.Length(); j++) {
  if (tmp[j] == ',') {
   tmp[j] = '.';
   dig = dig + tmp[j];
   if (tmp.Length() >= j+1) dig = dig + tmp[j+1];
   if (tmp.Length() >= j+2) dig = dig + tmp[j+2];
   break;
  }
  dig = dig + tmp[j];
 }
 Str = Str + dig;
 Str = Str + ", '";

 Str = Str + Description + "')";

 Query3->Close();
 Query3->SQL->Clear();
 Query3->SQL->Add(Str);
 Query3->ExecSQL();
// И выполним SQL-запрос
}
//---------------------------

Заметьте, что в случаях, когда результат выполнения запроса не формирует данные, вместо Query3->Open() применяется Query3->ExecSQL().

Функция же Replace попросту подменяет в строке один символ другим:

//---------------------------
AnsiString TMainForm::Replace(AnsiString S)
{
 for (int i = 1; i <= S.Length(); i++)
  if (S[i] == '#') S[i] = ' ';
 return S;
}
//---------------------------

Еще одна полезная функция (без которой не представляет себе жизни бухгалтерия) предоставляет возможность умножения всех цен на задаваемый коэффициент (очень часто используется для «накрутки» цен). Давайте реализуем эту возможность:

//---------------------------
void __fastcall TMainForm::MulByFactorBtnClick(TObject *Sender)
{
 AnsiString Str, sSQL;
 double factor;

// Для начала проверим, ввел ли пользователь корректное значение

 try {
  factor = Factor->Text.ToDouble();
 }
 catch(Exception &E) {
 // Если нет, то выдадим предупреждение 
 // и прекратим дальнейшие вычисления
  Str = "Допустимы только численные 
    значения множителя!\n" 
		+ Factor->Text 
		+ " —  неверный формат числа 
		с плавающей точкой.";
  Application->MessageBox(Str.c_str(), "Ошибка!", MB_ICONERROR|MB_OK);
  return;
 }

// Уверен ли пользователь?

 Str = "Данная операция умножит значения 
    ВСЕХ столбцов \n с ценами таблиц данных базы на ";
 Str = Str + Factor->Text + ". Выполнить?";

 if (Application->MessageBox(Str.c_str(), 
		"Внимание!", MB_ICONQUESTION|MB_YESNO) == IDYES) {

  // Если да, то прочитаем значение фактора 
  // умножения в строке текста
  // И заменим в ней символ ',' на '.'

  Str = Factor->Text;
  for (int i = 1; i <= Str.Length(); i++) if (Str[i] == ',') Str[i] = '.';

  sSQL = "SELECT * FROM _Components";
  // Далее во всех таблицах с данными (чьи имена 
  // хранятся в таблице "_Components")
  Query3->Close();
  Query3->SQL->Clear();
  Query3->SQL->Add(sSQL);
  Query3->Open();

 // Обновим значения цен
  while (!Query3->Eof) {
   sSQL = "UPDATE " + 
		Query3->FieldByName("CategoryTableName")->AsString 
		+ " SET ";
   sSQL = sSQL + "Price1 = Price1 * " + Str + ", ";
   sSQL = sSQL + "Price2 = Price2 * " + Str + ", ";
   sSQL = sSQL + "Price3 = Price3 * " + Str;

   Query2->Close();
   Query2->SQL->Clear();
   Query2->SQL->Add(sSQL);
   Query2->ExecSQL();
   Query3->Next();
  }
  Application->MessageBox("Содержимое 
		таблиц данных пересчитано!", "Готово!", 0);
 }
}
//---------------------------

И наконец, функция сохранения текущей конфигурации, по сути, будет записывать состояние указателей во всех таблицах данных в управляющую таблицу _PCType:

//---------------------------
void TMainForm::SaveConfiguration()
{
 ConfTB->Edit();
 ConfTB->FieldByName("MainBoardID")->AsInteger = MB->KeyValue;
 ConfTB->FieldByName("MainBoardNum")->AsInteger = MBPrc->Text.ToInt();
 ConfTB->FieldByName("CPUID")->AsInteger = CPU->KeyValue;
 ConfTB->FieldByName("CPUNum")->AsInteger = CPPrc->Text.ToInt();
 ConfTB->FieldByName("RAMID")->AsInteger = RAM->KeyValue;
 ConfTB->FieldByName("RAMNum")->AsInteger = RAMPrc->Text.ToInt();
 ConfTB->FieldByName("SVGAID")->AsInteger = SVGA->KeyValue;
 ConfTB->FieldByName("SVGANum")->AsInteger = SVPrc->Text.ToInt();
 ConfTB->FieldByName("IDEHDDID")->AsInteger = IDEH->KeyValue;
 ConfTB->FieldByName("IDEHDDNum")->AsInteger = IDPrc->Text.ToInt();
 ConfTB->FieldByName("SCSIHDDID")->AsInteger = SCSIH->KeyValue;
 ConfTB->FieldByName("SCSIHDDNum")->AsInteger = SCPrc->Text.ToInt();
 ConfTB->FieldByName("SBID")->AsInteger = Sound->KeyValue;
 ConfTB->FieldByName("SBNum")->AsInteger = SBPrc->Text.ToInt();
 ConfTB->FieldByName("KBID")->AsInteger = KBD->KeyValue;
 ConfTB->FieldByName("KBNum")->AsInteger = KBPrc->Text.ToInt();
 ConfTB->FieldByName("MOUSEID")->AsInteger = Mouse->KeyValue;
 ConfTB->FieldByName("MouseNum")->AsInteger = MouPrc->Text.ToInt();
 ConfTB->FieldByName("CASEID")->AsInteger = Case->KeyValue;
 ConfTB->FieldByName("CaseNum")->AsInteger = CSPrc->Text.ToInt();
 ConfTB->FieldByName("MONITORID")->AsInteger = Monitor->KeyValue;
 ConfTB->FieldByName("MONITORNum")->AsInteger = MonPrc->Text.ToInt();
 ConfTB->FieldByName("SpeakerID")->AsInteger = Speakers->KeyValue;
 ConfTB->FieldByName("SpeakerNum")->AsInteger = SPKPrc->Text.ToInt();
 ConfTB->FieldByName("FaxID")->AsInteger = Fax->KeyValue;
 ConfTB->FieldByName("FaxNum")->AsInteger = FaxPrc->Text.ToInt();
 ConfTB->FieldByName("FDDID")->AsInteger = FDD->KeyValue;
 ConfTB->FieldByName("FDDNum")->AsInteger = FDDPrc->Text.ToInt();
 ConfTB->FieldByName("CdromID")->AsInteger = CDROM->KeyValue;
 ConfTB->FieldByName("CDRomNum")->AsInteger = CDPrc->Text.ToInt();
 ConfTB->FieldByName("PadID")->AsInteger = Pad->KeyValue;
 ConfTB->FieldByName("PadNum")->AsInteger = PadPrc->Text.ToInt();
 ConfTB->Post();
}
//---------------------------

Теперь поговорим о событиях. Фактически интегральная стоимость всего набора должна пересчитываться всякий раз, когда выбирается позиция из любого списка выбора или вводится значение в поле — указатель количества единиц. В первом случае для каждого из списков выбора необходимо написать функцию, которая будет отрабатываться всякий раз, как только из соответствующего списка выбора позиции будет выбираться значение:

//---------------------------
void __fastcall TMainForm::FaxCloseUp(TObject *Sender)
{
 CalcTotalPrice();
}
//---------------------------

А во втором случае пересчет интегральной стоимости целесообразно выполнить непосредственно после того, как оператор переместит фокус ввода в другой компонент интерфейса программы (используя для этого сообщение OnExit):

//---------------------------
void __fastcall TMainForm::CPPrcExit(TObject *Sender)
{
 CalcTotalPrice();
}
//---------------------------

Приложение-клиент к базам данных в нашем случае является инструментом подготовки базы данных. Им будут пользоваться операторы. А поскольку операторы могут ошибаться, рекомендуется свести возможность допущения таких ошибок к минимуму. Для этого необходимо спроектировать интерфейс программы весьма тщательным образом, по возможности минимизируя действия, выполняемые оператором при редактировании и пополнении базы данных.

Вот собственно и все. Откомпилируем и запустим полученное приложение, которое будет выглядеть следующим образом: первая вкладка («Прайс процессор») — и вторая вкладка («ПК конфигуратор»).

Заключение

Существует множество Интернет-магазинов. От большинства из них требуется мгновенная реакция на изменение ценовой политики, перечня товаров или услуг. А все это невозможно без надежных средств автоматизации обработки огромного массива данных. Ведь перечень позиций в некоторых крупных Интернет-супермаркетах переваливает за 2-3 тысячи, а количество ежечасных транзакций (обращений) зачастую намного больше. Здесь и от сервера, и от серверной СУБД требуется недюжинное быстродействие (обычным настольным ПК и Access'ом здесь не обойтись). Да и сам ASP-код должен быть написан таким образом, чтобы исключить неоправданные циклы, задержки или лишние обращения к базе данных. ASP-код, по сути, является лишь средством визуализации содержимого базы данных, позволяющим клиентам приобретать или заказывать товары или услуги. Вся «подводная часть» айсберга манипуляций с базой данных (автоматизация обновления, редактирования, реструктуризации и т.д.) ложится именно на вспомогательные средства. Разработка таких средств требует, как правило, гораздо больше времени и сил, чем разработка самих магазинов, хотя в последнее время все чаще и чаще под последними профессионалы подразумевают именно симбиоз собственно магазинов, и средств их поддержки, профилактики, наполнения, систематизации и т.д.

Автор выражает благодарность Архангельскому А.Я. за материалы, предоставленные для подготовки настоящей статьи.