С.Михайлов
Требования к аппаратному обеспечению
|
Среда разработки Borland C++ Builder 3 очень напоминает среду Delphi эдак 2-4. В ней присутствует новый Type Library Editor, но отсутствуют такие удобства, как Code Explorer, Complete Word и Complete Class.
Подробно останавливаться на ее особенностях мы не будем.
Главное отличие в поддержке разработки ActiveX-компонентов между Delphi и С++ Builder заключается в том, что по самой своей сути способен работать с ATL — самой перспективной библиотекой для работы с ActiveX в целом и с компонентами в частности. Это, несомненно, большое достоинство. ATL создавалась как высокопрофессиональное средство и рассчитана на создание компактных и многофункциональных ActiveX-объектов. В подтверждение этих слов можно сказать, что один из примеров, к сожалению, не поставляющийся с С++ Builder, но имеющийся в поставке Visual C++, позволяет создать ActiveX-объект, исполняемый модуль которого имеет объем всего 5 килобайт и не требует никаких дополнительных библиотек.
Inprise подошел к этому вопросу иначе - по умолчанию к исполняемому модулю при компиляции добавляется код VCL-библиотеки. В результате общий объем компонента превышает 400 КБ. Можно провести компиляцию с run-time-библиотеками, но тогда объем станет еще больше. Конечно, в наше время три рубля — не деньги, а мегабайт — не память... Но Microsoft ориентирует разработчиков на использование ActiveX-технологий в Internet, а пересылка 400 КБ по нашим (да и не нашим) каналам связи, перестает быть проблемой только начиная со скорости в 64 кбит/сек. Отличием от остальных средств разработки является то, что С++ Builder — это единственное средство в нашем обзоре, использующее не свои родные библиотеки, а библиотеки, написанные в Delphi.
Полностью аналогично тому, как это делалось в Delphi4, создадим новую Type Library и ActiveX Form – только к названиям файлов проекта добавим символ “c”. Зарегистровав ActiveX в системе (кнопка “Register” в TypeLibrary), отметим, что компиляция проекта заняла намного больше времени, чем та же операция в Delphi4. Впрочем, все к лучшему: такая скорость компиляции отбивает у программиста стремление ловить ошибки с помощью нажатия на на Ctrl-F9 (повторная компиляция без запуска), приучая к аккуратности и продуманности действий :-). Ради справедливости надо отметить, что Delphi 4.0 производила компиляцию быстрее всех в этом обзоре, хотя Visual C++ 6.0 при повторной компиляции справлялся с этим очень быстро. Также очень важно не забывать, что C++ замечает разницу между заглавными и прописными буквами, и нужно постоянно следить за правильностью написания названий методов и переменных (например: SetRoot это не тоже самое, что Setroot).
В интерфейс “IctestActiveFormX” добавим метод Connect (рис.3) и свойство Cur_ID_RT, настроив их также, как мы делали это в Delphi4 — только для параметра Return Value установим в HRESULT (этого требует спецификация COM), для метода Connect добавим 4 параметра: первые четыре типа BSTR (ServerName, DBName, UserName, Password). Больше ничего изменять не нужно — нажмем на “Register”, импортируем наш ActiveX в C++ Builder и создадим тестовое приложение (содержащее наш ActiveX). Иногда случается, что после возвращения из тестового приложения в основной проект компиляция заканчивается сообщением: “Can not open ….ocx”: после выгрузки-загрузки C++ Builder’a эта проблема обычно снимается.
рисунок 3
Вернувшись в проект с ActiveX Library сбросим на форму те же компоненты, что использовались нами в Delphi, и настроим их свойства аналогичным образом.
В модуле “ctestActiveFormImpl1.cpp” найдем заглушку для метода Connect и внесем в нее код, так, чтобы она выглядела следующим образом:
STDMETHODIMP TctestActiveFormXImpl::Connect(BSTR ServerName, BSTR DBName, BSTR UserName, BSTR Password) { try { //Если связь с базой данных еще не установлена, то передать в Database1 // параметры для связи и соединиться с базой. if (Database1->Connected != true) { Database1->Params->Append(“DATABASE NAME=” + ((AnsiString)(DBName))); Database1->Params->Append(“SERVER NAME=” + ((AnsiString)(ServerName))); Database1->Params->Append(“USER NAME=” + ((AnsiString)(UserName))); Database1->Params->Append(“PASSWORD=” + ((AnsiString)(Password))); Database1->Connected = true; } //Передать в Query1 “псевдоним” базы данных и имя сессии, //с которыми установлено соединение. “m_VclCtl” является //глобальной переменной для класса TctestActiveFormX m_VclCtl->Query1->DatabaseName = Database1->DatabaseName; m_VclCtl->Query1->SessionName = Database1->SessionName; SetRoot(); } catch(Exception &e) { return Error(e.Message.c_str(), IID_IctestActiveFormX); } return S_OK; };
Перейдем в файл проекта “ctestActiveFormp.cpp” и опишем там глобальные переменные Database1, Session1 и iCtrlsCount:
TDatabase * Database1 = NULL; TSession * Session1 = NULL; int iCtrlsCount = 0;
Чтобы переменные, объявленные в файле проекта, были видны во всем проекте, продекларируем их в header-файле “ctestActiveFormImpl1.h”.
extern TDatabase * Database1; extern TSession * Session1; extern int iCtrlsCount;
Переменная iCtrlsCount является счетчиком количества созданных экземпляров нашего ActiveX’a – этот счетчик увеличивается на единицу в конструкторе и уменьшается на единицу в деструкторе. Таким образом, при создании и уничтожении каждого нового ActiveX’a, мы можем знать наверняка, является ли он первым (при создании) или последним (при удалении), и, в зависимости от этого, создавать (уничтожать) единые для всего проекта компоненты Database1 и Session1 (кстати, такой подход неприемлем для многопоточных сред). В Delphi, в отличие от C++ Builder’a, имеются разделы initialization и finalization. Записанный в них код выполняется при загрузке и выгрузке приложения, поэтому не возникает необходимости использовать подобный трюк со счетчиком. В C++ имеется аналогичный механизм (функция WinMain), но, по неведомым нам соображениям, к моменту вызова этой функции одна из .DLL (DBnmpNTw.dll — это библиотека из DBLIB), отвечающих за работу с базами данных, была уже выгружена и при обращении к базе данных происходил сбой.
Опишем в header-файле конструктор и деструктор для класса TctestActiveFormXImpl (они должны быть описаны в разделе public, например, перед функцией “void InitializeControl()”):
public: //Конструктор TctestActiveFormXImpl() { ++iCtrlsCount; //Увеличим счетчик //Если Database1 не существует, то создать и инициализировать ее if(Database1 == NULL) { Database1 = new TDatabase(NULL); Database1->Connected = FALSE; Database1->Params->Clear(); Database1->DriverName = “MSSQL”; Database1->DatabaseName = “cTestDB”; Database1->SessionName = “ctestAXSession”; } //Найти сессию с именем “ctestAXSession” и, если такой нет, открыть ее Session1 = Sessions->FindSession(Database1->SessionName); if (Session1 == NULL) { Session1 = Sessions->OpenSession(Database1->SessionName); } } //Деструктор ~TctestActiveFormXImpl() { —iCtrlsCount; //Уменьшить счетчик //Проверить, является ли последним уничтожаемый компонент if (iCtrlsCount == 0) {//Если Database1 существует, уничтожить ее if (Database1 != NULL) { Database1->Connected = false; delete Database1; Database1 = NULL; } if (Session1 != NULL) { //Если Session1 существует, уничтожить ее Session1->Active = false; delete Session1; Session1 = NULL; } } }
Теперь, когда мы обеспечили единственность Database1 и Session1 для всех экземпляров нашего ActiveX-компонента, перейдем к реализации его методов. Опишем в header-файле для класса TctestActiveFormX следующие функции:
protected: void __fastcall CreateSubNodes(TTreeNode * tnNode); public: void __fastcall SetRoot(); TTreeNode* __fastcall TctestActiveFormX::ExpandParentNode(int iID_RT);
Реализацию этих функций поместим в файл “ctestActiveFormImpl1.cpp”:
void __fastcall TctestActiveFormX::SetRoot() { TQuery *sqlRoot; TTreeNode *tnRoot; try { sqlRoot = new TQuery(NULL); sqlRoot->DatabaseName = Query1->DatabaseName; sqlRoot->SessionName = Query1->SessionName; sqlRoot->SQL->Text = “SELECT Name FROM RT WHERE ID_RT = 0”; sqlRoot->Open(); } catch (Exception &e) { throw Exception(“Ошибка при выполнении SQL-запроса “ “(считывание Root из таблицы RT”); } if (sqlRoot->RecordCount == 0) { throw Exception(“В таблице RT отсутствует корень (Root)”); } tnRoot = TreeView1->Items->AddFirst(NULL,”Root”); tnRoot->HasChildren = true; tnRoot->Data = Pointer(0); tnRoot->SelectedIndex = 1; tnRoot->Expand(False); sqlRoot->Free(); return; }; void __fastcall TctestActiveFormX::CreateSubNodes(TTreeNode * tnNode) { TTreeNode *tnNewNode; Query1->Close(); Query1->ParamByName(“Parent”)->AsInteger = Integer(tnNode->Data); Query1->Open(); while (!Query1->Eof) { tnNewNode = TreeView1->Items->AddChild(tnNode, Query1->FieldByName(“Name”)->AsString); tnNewNode->HasChildren = (Query1->FieldByName(“ChildrenCount”) ->AsInteger > 0); tnNewNode->Data = Pointer(Query1->FieldByName(“ID_RT”)->AsInteger); tnNewNode->SelectedIndex = 1; Query1->Next(); } return; } TTreeNode* __fastcall TctestActiveFormX::ExpandParentNode(int iID_RT) { TQuery *sqlFindID_RT; TTreeNode *TempResult; int i, iNodeCount,iParentID; TempResult = NULL; sqlFindID_RT = new TQuery(NULL); sqlFindID_RT->DatabaseName = Query1->DatabaseName; sqlFindID_RT->SessionName = Query1->SessionName; sqlFindID_RT->SQL->Text = “SELECT Parent FROM RT WHERE ID_RT = “ + ((AnsiString)(iID_RT)); sqlFindID_RT->Open(); iParentID = sqlFindID_RT->RecordCount == 0 ? -1 : sqlFindID_RT ->FieldByName(“Parent”)->AsInteger; sqlFindID_RT->Free(); if (iParentID == -1) return TempResult; if (iParentID == 0) { TempResult = TreeView1->Items->Item[0]; TempResult->Expand(false); return TempResult; } TempResult = ExpandParentNode(iParentID); if (TempResult == NULL) return TempResult; iNodeCount = TempResult->Count - 1; for (i = 0; i <= iNodeCount; i++) if (TempResult->Item[i]->Data == Pointer(iParentID)) { TempResult = TempResult->Item[i]; break; } if (TempResult != NULL) TempResult->Expand(False); return TempResult; }
Этот код мало отличается от написанного ранее для Delphi4, поскольку и в Delphi и в C++ Builder используются одинаковые библиотеки компонентов.
Добавим обработку событий OnCollapsed и OnExpanding для компонента TreeView1:
void __fastcall TctestActiveFormX::TreeView1Expanding(TObject *Sender, TTreeNode *Node, bool &AllowExpansion) { if (!Node->Expanded) CreateSubNodes(Node); } void __fastcall TctestActiveFormX::TreeView1Collapsed(TObject *Sender, TTreeNode *Node) { Node->DeleteChildren(); Node->HasChildren = true; }
Опишем реализацию методов get_CurID_RT и set_CurID_RT для свойства CurID_RT (для этих методов уже созданы заглушки в файле “ctestActiveFormImpl1.cpp”:
STDMETHODIMP TctestActiveFormXImpl::get_CurID_RT(long* Value) { try { *Value = (m_VclCtl->TreeView1->Selected != NULL) ? long(m_VclCtl->TreeView1 ->Selected->Data) :-1; } catch(Exception &e) { return Error(e.Message.c_str(), IID_IctestActiveFormX); } return S_OK; }; STDMETHODIMP TctestActiveFormXImpl::set_CurID_RT(long Value) { try { TreeNode *tnParentNode = NULL; int iOldValue, i, iNodeCount; if (!m_VclCtl->Query1->Active) return S_OK; if (Value < 0) return S_OK; iOldValue = (m_VclCtl->TreeView1->Selected == NULL) ? –1 : Integer(m_VclCtl->TreeView1->Selected->Data); if (Value == iOldValue) return S_OK; tnParentNode = m_VclCtl->ExpandParentNode(Value); iNodeCount = tnParentNode->Count - 1; for (i = 0; i <= iNodeCount; ++i) if (tnParentNode->Item[i]->Data == Pointer(Value)) { tnParentNode->Item[i]->Selected = true; break; } } catch(Exception &e) { return Error(e.Message.c_str(), IID_IctestActiveFormX); } return S_OK; };
Перейдем в тестовый проект (форма должна выглядеть так же, как было в Delphi: ActiveX-компонент, три кнопки и элемент редактирования), назовем кнопки “Connect”, “GetCurID_RT” и “SetCurID_RT” и опишем для них обработчики событий OnClick:
void __fastcall TForm1::ConnectClick(TObject *Sender) { //L означает конвертацию строки ctestActiveFormXProxy1->Connect(L”Название сервера”, L”Название базы данных”,L”Имя пользователя”,L”Пароль”); } void __fastcall TForm1::GetCurID_RTClick(TObject *Sender) { Edit1->Text = IntToStr(ctestActiveFormXProxy1->CurID_RT); } void __fastcall TForm1::SetCurID_RTClick(TObject *Sender) { ctestActiveFormXProxy1->CurID_RT = StrToInt(Edit1->Text); }
Поскольку выделить ActiveX, щелкнув на нем левой кнопкой мыши (как это делается с другими компонентами), невозможно, приходится выбирать его в списке компонентов Object Inspector’a, где он можно найти под именем “ctestActiveFormXProxy1” (хорошо, что мы знаем это имя :-)). Подобный глюк наблюдался иногда и в Delphi3 (в Delphi4 исправлен) – связан, вероятнее всего, с неправильной передачей фокуса в ActiveX form (в частности, с несрабатыванием события на получение фокуса в форме ActiveX).
Наверное уже очевидно, каким образом перевести код Delphi в код C++ Builder’a?! Вернувшись в основной проект, сбросим на форму PopupMenu, добавим ему элементы “Добавить” и “Удалить”, подключим его к TreeView1, опишем функции AddNode и DeleteNode (типа void) и вызовем их в обработчиках событий для соответствующих элементов PopupMenu, после чего опишем обработчики событий OnPopup для PopupMenu и onEdited для компонента TreeView1. Код всего этого будет выглядеть так:
в header-файле для класса TctestActiveFormX: public: void __fastcall AddNode(); void __fastcall DeleteNode(); в файле “ctestActiveFormImpl1.cpp”: void __fastcall TctestActiveFormX::miAddNodeClick(TObject *Sender) { AddNode(); } //—————————————————————————————————————- void __fastcall TctestActiveFormX::miDeleteClick(TObject *Sender) { DeleteNode(); } //—————————————————————————————————————- void __fastcall TctestActiveFormX::AddNode() { Integer iParent; TTreeNode *tnNewNode, *tnParentNode; TQuery *sqlAddNewNode; String sSQL; tnParentNode = TreeView1->Selected; if (tnParentNode == NULL) return; iParent = Integer(tnParentNode->Data); sqlAddNewNode = new TQuery(NULL); sqlAddNewNode->DatabaseName = Query1->DatabaseName; sqlAddNewNode->SessionName = Query1->SessionName; sSQL = String(“declare @NewID_RT int “ ”select @NewID_RT = max(ID_RT) + 1 from RT “)+ “insert into RT values(“ + iParent + “, @NewID_RT, 0, ‘Новая запись №’” + “ + Convert(varchar(15), @NewID_RT)) select @NewID_RT”; sqlAddNewNode->SQL->Text = sSQL; sqlAddNewNode->Open(); tnNewNode = TreeView1->Items->AddChild(tnParentNode, “Новая запись №” + sqlAddNewNode->Fields[0]->AsString); tnNewNode->Data = Pointer(sqlAddNewNode->Fields[0]->AsInteger); tnNewNode->SelectedIndex = 1; sqlAddNewNode->Free(); tnParentNode->HasChildren = true; tnParentNode->Expand(false); tnNewNode->Selected = true; } //—————————————————————————————————————- void __fastcall TctestActiveFormX::DeleteNode() { int iID_RT; TTreeNode *tnDeletingNode; TQuery *sqlDeleteNode; tnDeletingNode = TreeView1->Selected; if (tnDeletingNode == NULL) return; if (tnDeletingNode->Parent == NULL) return; iID_RT = Integer(tnDeletingNode->Data); sqlDeleteNode = new TQuery(NULL); sqlDeleteNode->DatabaseName = Query1->DatabaseName; sqlDeleteNode->SessionName = Query1->SessionName; sqlDeleteNode->SQL->Clear(); sqlDeleteNode->SQL->Append(“DELETE FROM RT WHERE ID_RT = “); sqlDeleteNode->SQL->Append(IntToStr(iID_RT)); sqlDeleteNode->ExecSQL(); sqlDeleteNode->Free(); tnDeletingNode->Delete(); Query1->Close(); Query1->Open(); } //—————————————————————————————————————- void __fastcall TctestActiveFormX::PopupMenu1Popup(TObject *Sender) { if (TreeView1->Selected == NULL) { miAddNode->Enabled = false; miDelete->Enabled = false; } else { miAddNode->Enabled = true; miDelete->Enabled = true; TreeView1->Selected = TreeView1->Selected; } } //—————————————————————————————————————- void __fastcall TctestActiveFormX::TreeView1Edited(TObject *Sender, TTreeNode *Node, AnsiString &S) { TQuery *sqlUpdateNodeName; String sSQL; sqlUpdateNodeName = new TQuery(NULL); sqlUpdateNodeName->DatabaseName = Query1->DatabaseName; sqlUpdateNodeName->SessionName = Query1->SessionName; sSQL = “UPDATE RT SET Name = ‘“ + S + “‘ WHERE ID_RT = “ + Integer(Node->Data); sqlUpdateNodeName->SQL->Text = sSQL; sqlUpdateNodeName->ExecSQL(); sqlUpdateNodeName->Free(); Query1->Close(); Query1->Open(); }
Комментарии к вышеприведенному коду вы можете прочитать в разделе этого обзора, посвященном Delphi.
Многоуровневое приложение
Что касается DCOM, то все так же, как в Delphi, только компоненты находятся не на закладке Midas, а на закладке DataAccess, а вместо DCOMConnection используется компонент MidasConnection, а Wizard для создания Remote Data Module вызывается с закладки New после выбора File/New.
Основной код для DCOM-клиента представлен ниже:
void __fastcall TClient::ConnectClick(TObject *Sender) { MIDASConnection1->Connected = false; // Должна работать нижеследующая строчка, но она дает link-ошибку // MIDASConnection1->ComputerName = DCOM_Server_ComputerName->Text; // Поэтому имя компьютера мы задали в Object Inspector во время проектирования //формы //*********** MIDASConnection1->Connected = true; MIDASConnection1->AppServer.Exec(Function(“Connect”) << ServerName->Text <<DBName->Text << UserName->Text << Password->Text); ClientDataSet1->Active = true; } //—————————————————————————————————————- void __fastcall TClient::UpdateClick(TObject *Sender) { ClientDataSet1->ApplyUpdates(-1); } //—————————————————————————————————————- void __fastcall TClient::RefreshClick(TObject *Sender) { ClientDataSet1->Refresh(); } //—————————————————————————————————————- void __fastcall TClient::ClientDataSet1ReconcileError( TClientDataSet *DataSet, EReconcileError *E, TUpdateKind UpdateKind, TReconcileAction &Action) { if (MessageDlg(String(“Отказ при попытке записи в базу данных:”) + “ “ + E->Message + “ “ + “Перечитать эту запись (записи) из базы?”, mtWarning,TMsgDlgButtons() << mbYes << mbNo,0) == mrYes ) Action = raRefresh; else Action = raAbort; }
Не совсем понятно, почему не работает вызов “MIDASConnection1->ComputerName = DCOM_Server_ComputerName->Text;” – такой код проходит компиляцию, но на link-этапе выдается сообщение об ошибке (вероятно, не находится реализация метода). После замены этой строки на ее реализацию из исходников препятствие было успешно преодолено, но, все же, хотелось бы понять его природу… Скорее всего, злую шутку сыграл код, приведенный ниже, позаимствованный из файла <winbase.h>.
#ifdef UNICODE #define SetComputerName SetComputerNameW #else #define SetComputerName SetComputerNameA #endif
Непонятно одно: как умудрились пропустить такой просчет программисты Inprise? Ведь мы смогли повторить эту ошибку на совершенно новом проекте с абсолютно пустой формой (в форме находился только компонент MidasConnection) без единой лишней строки кода.
Ниже приводится основной код для DCOM-сервера:
STDMETHODIMP TcDCOM_ServerImpl::get_Query1(IProvider** Value) { try { _di_IProvider IProv = m_DataModule->Query1->Provider; IProv->AddRef(); *Value = IProv; } catch(Exception &e) { return Error(e.Message.c_str(), IID_IcDCOM_Server); } return S_OK; }; STDMETHODIMP TcDCOM_ServerImpl::Connect(BSTR ServerName, BSTR DBName, BSTR UserName, BSTR Password) { try { if (m_DataModule->Database1->Connected) m_DataModule->Database1->Connected = false; m_DataModule->Database1->Params->Clear(); m_DataModule->Database1->DriverName = “MSSQL”; m_DataModule->Database1->DatabaseName = String(“TestDCOM_DB_”) + iDBCounter; ++iDBCounter; m_DataModule->Query1->DatabaseName = m_DataModule->Database1 ->DatabaseName; m_DataModule->Database1->LoginPrompt = false; m_DataModule->Database1->Params->Append(String(“DATABASE NAME=”) + DBName); m_DataModule->Database1->Params->Append(String(“SERVER NAME=”) + ServerName); m_DataModule->Database1->Params->Append(String(“USER NAME=”) + UserName); m_DataModule->Database1->Params->Append(String(“PASSWORD=”) + Password); m_DataModule->Database1->Connected = true; } catch(Exception &e) { return Error(e.Message.c_str(), IID_IcDCOM_Server); } return S_OK; }; //—————————————————————————————————————- STDMETHODIMP TcDCOM_ServerImpl::Refresh() { try { m_DataModule->Query1->Close(); m_DataModule->Query1->Open(); } catch(Exception &e) { return Error(e.Message.c_str(), IID_IcDCOM_Server); } return S_OK; };
Здесь хотелось бы отметить одно неудобство, с которым нам пришлось столкнуться в ходе работы: если при создании ActiveX’a было достаточно нетрудно догадаться , что “m_VclCtl” является указателем на реализацию ActiveX form’ы, и именно этот указатель необходим для вызова методов компонента Query1 из метода Connеct – пришлось всего-лишь внимательно просмотреть header-файл, то найти указатель “m_DataModule ” на реализацию TDataModule (при создании DCOM-сервера) было намного труднее – пришлось выуживать его из исходников C++ Builder’a… Возможно, что эти проблемы возникли у нас от недостаточно глубокого изучения документации, но, пожалуй, при столь простом (в целом) создании DCOM-сервера достаточно обидно спотыкаться о такие мелочи, как получасовое гуляние по исходникам.
ActiveX и содержащее его тестовое приложение, написанные на Delphi и C++ Builder’e и скомпилированные c отключенным режимом “Build with run-time packages”, заняли почти одинаковое количество места на диске (ActiveX - 650 Kb, тестовое приложение - 250 Kb). Поскольку для работы с базой данных они подключают одно и то же BDE, то можно предположить, что и объем занимаемой оперативной памяти для них будет примерно равным (как и быстродействие). DCOM-тестирования для Delphi и C++ Builder’a, в основном, дали сходные результаты – только файлы проекта в Delphi заняли в 7 раз меньше места, чем на C++ Builder’e. Несколько непонятно, почему файлы по сути одного и того же проекта в Delphi заняли около 1000 Kb, а в C++ Builder’e – целых 8400 Kb?
Нельзя не отметить, что при работе на C++ Builder’e у нас возникло сильное подозрение, что это та же самая, немного измененная Delphi. Но не Delphi4, а Delphi2-3 — нет ни Complete Class’a, ни Complete Word’a (хотя уже и в Microsoft C++ он есть), но зато все те же глюки Delphi3: шутки с фокусом в ActiveX’ах (уже исправленные в Delphi4), локирование своих собственных файлов с последующими сообщениями вроде “can not open file …ocx” или “internal link error…“ и пр. C++ Builder 3 имеет и свои собственные глюки: очень интересный эффект обнаруживается при отладке приложения. Если остановиться на точке прерывания, и навести курсор на “—iCtrlsCount” (декремент переменной iCtrlsCount), то появившееся значение (например, 10) будет уменьшаться на единицу с каждым новым наездом мыши! Cлишком впечатлительного программиста такая интерактивность может довести до сумашествия…
Создается впечатление, что приоритетным продуктом Inprise в области средств разработки приложений является Delphi, а C++ Builder отстает на шаг, и пока не в состоянии догнать Delphi.
Из всего этого вывод: Delphi и C++ Builder – близнецы-братья, кто более для программиста ценен ? Если вы привыкли писать на C++ и желаете получить среду быстрой разработки с большим набором готовых компонентов, то милости просим в C++ Builder. Если же вы отдаете предпочтение Object Pascal, то Delphi — наиболее подходящая для вас среда.