Введение Настоящая статья предназначена для тех, кто собирается автоматизировать процесс подготовки шаблонов конфигураций продаваемого оборудования. Как отмечалось, не все покупатели точно знают, что именно хотят приобрести, да и не во всех может параметрах ряда товаров разобраться обыватель (например, бытовая техника или компьютеры), зачастую требуется консультация эксперта. Сегодня в продолжение нашей серии мы попытаемся написать программу-конфигуратор, с помощью которой будут создаваться и редактироваться типовые конфигурации продаваемого оборудования (суть задачи не меняется от вида товара — будь то компьютеры или автомобили). Уяснив структуру проектируемого приложения, мы можем приступать к написанию кода. Что нам понадобится? - Microsoft Access 97 или 2000, но лучше Microsoft SQL Server 7.0 или 2000
- Borland C++ Builder 5.0
Конечно, можно обойтись и 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-псевдонима не обойтись. Для этого: - запустите программу — конфигуратор источников данных (Data Sources ODBC) — Start->Settings->Control Panel->Administrative Tools->Data Sources ODBC;
- перейдите во вкладку System DSN и создайте новый источник данных, нажав на Add...;
- в появившемся списке драйверов выберите драйвер баз данных SQL Server и нажмите на Finish;
- в строке Data Source Name задайте имя базы данных, в нашем случае IShop (это то имя, по которому мы в дальнейшем будем обращаться к ней);
- в самой нижней строке задайте имя SQL-сервера, далее введите учетную запись и пароль доступа к серверу и нажмите OK.
Вы увидите появившуюся строку в списке источников данных в вашей системе. Разберемся, каким образом следует сконфигурировать разрабатываемое приложение в зависимости от того, какой псевдоним к базе данных будет использоваться. Речь идет о свойствах объекта Tdatabase. В нашем случае следует заполнить его свойства следующими значениями: - AliasName = 'IShop' — имя псевдонима (созданного с помощью Database Desktop);
- DatabaseName = 'IShop' —название базы данных;
- LoginPrompt = False — запрашивать ли пароль при подключении к базе данных.
$BREAK$ Вся остальная информация о нашей базе данных уже содержится в псевдониме и поэтому может не указываться. Если же по каким-то причинам использование 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 — выпадающий список выбора, связанный с определенным полем заданной таблицы заданной базы данных. Это означает, что изменение текущего значения в таком поле приводит к изменению позиции указателя в связанной с ним таблице данных и наоборот. Согласитесь, что такой компонент избавляет нас от необходимости писать громоздкие функции-обработчики; надо всего лишь заполнить его свойства, а именно: - ListSource — компонент типа TdataSet, с которым связан компонент типа TTable (связанный с заданной таблицей данных);
- ListField — поле таблицы, формирующее список.
Итак... Прежде всего необходимо подготовить форму приложения, причем отметим сразу, что оно будет состоять из двух частей: - программа обработки исходного прайс-листа (предоставляемого торговым отделом гипотетического магазина) в форме таблицы (например, *.xls файла);
- программа-конфигуратор типовых шаблонов состава оборудования.
Для этого удобнее всего воспользоваться компонентом 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); } } //--------------------------- $BREAK$ И наконец, функция сохранения текущей конфигурации, по сути, будет записывать состояние указателей во всех таблицах данных в управляющую таблицу _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-код, по сути, является лишь средством визуализации содержимого базы данных, позволяющим клиентам приобретать или заказывать товары или услуги. Вся «подводная часть» айсберга манипуляций с базой данных (автоматизация обновления, редактирования, реструктуризации и т.д.) ложится именно на вспомогательные средства. Разработка таких средств требует, как правило, гораздо больше времени и сил, чем разработка самих магазинов, хотя в последнее время все чаще и чаще под последними профессионалы подразумевают именно симбиоз собственно магазинов, и средств их поддержки, профилактики, наполнения, систематизации и т.д. Полный архив исходных текстов и проекта программы к настоящей статье лежит здесь. Автор выражает благодарность Архангельскому А.Я. за материалы, предоставленные для подготовки настоящей статьи. |