Требования к аппаратному обеспечению
|
При тестировании Power++ корпорации Sybase мы столкнулись с некоторыми сложностями, заставившими нас значительно отклониться от “программы испытаний”. Дело в том, что Power++ не позволяет создавать ActiveX-компоненты (в смысле ActiveX-control, он же OCX) и не поддерживает, без посторонней помощи, технологий DCOM, что заставило нас вместо ActiveX-компонента создать Power++ компонент TreeView. От DCOM-теста пришлось просто отказаться.
Вообще,
Power++ производит впечатление несомненно
оригинального продукта – со всеми вытекающими
отсюда достоинствами и недостатками. Очень
забавно выглядит Drag/Drop компонента с формы на
CodeEditor с появлением при этом “Reference Card” Wisard’a для
встраивания в код вызова методов (своеобразная
замена отсутствующего Complete Word’a). Reference Card можно
вызвать и просто через меню Help.
Относительно неплохой Help позволяет достаточно легко найти содержащуюся в нем информацию – жаль, что она не всегда соответствует действительности. Например, при вызове метода “Expand(hTVItem);” для компонента TreeView срабатывает событие Expand (т.е. метод триггерит), хотя в Help сказано: “This method does not generate an Expand event” (Этод метод не генерирует событие Expand). Но такие казусы случаются и с продуктами других производителей, поэтому не будем слишком заострять внимание на такой мелочи. Повторимся, что Help, в целом, неплохой, обнаружить искомую информацию в нем можно без особого труда (при условии, что она там есть). Этим Sybase выгодно отличается от Inprise (Borland), в продуктах которого Help построен очень системно и, да простят нас за эти слова, “объектно-ориентированно”, но обнаружить там даже имеющуюся информацию не легко. В продуктах Microsoft Help сделан очень профессионально и с вселенским размахом (например, даже поиск слова mother выдал более 40 вхождений), так что найти в нем можно всё, что угодно. Другой вопрос, сколько сил и времени на это уйдёт... Так что Help от Sybase является золотой серединой — небольшой, но легко читаемый и “причесанный”. Отчасти простота поиска в Help’е может объясняться хорошей продуманностью библиотек. Продуманность и изящность - это главная отличительная особенность Power++.
Вообще, заканчивая разговор о Help’e, хотелось бы сослаться на очень показательный пример – да, при работе с Power++ нам действительно пришлось залезть пару раз в исходные тексты, но при использовании Delphi приходится это делать постоянно.
Если от библиотек Power++ мы были в восторге, то работа с API нас сильно утомила. По умолчанию Windows API не подключено – следуя инструкции по подключению (описана в Help на “API functions”) и, добавив после этого в cpp-файл #include “CommCtrl.h”, действительно удалось использовать API-функции для компонента TreeView, однако это не сработало при создании Power++-компонента CtestTreeView — компилятор обнаруживал какие-то несусветные синтаксические ошибки в исходном файле “…\Power21\w32sdk\h\nt\prsht.h”. В связи с этой неприятностью пришлось переопределять в коде все необходимые константы и структуры для вызова API-функции “TreeView_SetItem”, взяв их из исходного файла “CommCtrl.h”, – а другого метода принудительно установить плюс для ветки дерева, не имеющей дочерних ветвей, в Power++ мы не нашли.
Развернутые сообщения об ошибках, появляющиеся прямо в Code Editor’e – это интересная находка, хотя, наверное, лучше было бы выводить их где-нибудь в другом месте, или, хотя бы, дать возможность отключить эту опцию. Дело в том, что при наличии двух-трех таких сообщений код становится трудночитаемым и практически нередактируемым (поскольку сообщения об ошибках редактировать и удалять не разрешается) — сообщения удаляются из кода только после записи проекта. Параллельно сообщения об ошибках выводятся в отдельное окно Error Log.
Особо хочется отметить работу в режиме отладки компонента с запуском содержащего его тестового приложения. Дело в том, что мы не нашли возможности подключать библиотеку с нашим компонентом в run-time режиме (как это делается, например, в Delphi) — по этой причине после каждого изменения кода компонента приходится сначала запускать его проект на выполнение (при этом компонент инсталлируется в Power++), затем переходить в проект с тестовым приложением и, сделав какое-нибудь бессмысленное изменение (иначе Exe-файл не будет переделан), запускать его. Если же мы хотим поработать в режиме отладки компонента, то надо вновь возвращаться в его проект и запускаться оттуда — все это очень неудобно. Сбой точек прерывания при отладке компонента (произвольное перескакивание на другие, даже закоментаренные, строки и т.п.), представленный на рисунке 4, вероятнее всего возникает именно по причине пропуска какого-либо из вышеописанных действий (т.е. когда новый код компонента не соответствует его же старому коду, прилинкованному к Exe-файлу). Вообще, режим отладки явно недоработан.
Рисунок 4.
Мы столкнулись с проблемой поиска места, откуда можно инициализировать окно компонента TreeView. Мыпопытались сделать это в конструкторе, но нас ждало разочарования, так как окно на этот момент еще отсутствовало, а вызов конструктора происходил дважды для одного и того же компонента. Мы попытались найти решение в Help’e, но ответа не нашли (мы долго искали!). Поиск по исходникам натолкнул нас на мысль переопределить virtual-методы MakeWindow и LoadWindow (файл Wwindow.hpp), и на их вызов выполнять инициализацию переменных и создание динамических объектов (query_1 и пр.). Как мы впоследствии узнали из Help’a на “Known problems”, все WWindow::Create-методы вызывают MakeWindow или LoadWindow. Если форма, включающая в себя компонент, является диалогом, то срабатывает LoadWindow, иначе вызывается MakeWindow. В целом, наше решение сработало, за исключением того, что инициализация происходит каждый раз при изменении свойств данного окна. Решение этой проблемы попробуйте найти сами.
Cбои в среде разработки Power++ происходят по самым разным причинам (чаще всего, неочевидным). Например, удавалось вылететь при вызове списка Class в Object Inspector’e, и при выполнении некоторых действий в Watch List’e (по привычке после Delphi). Однако, ради справедливости отметим, что Delphi 3 и C++ Builder имеют такую же привычку, и при работе с ActiveX’ами иногда поражают воображение программиста как своими труднопредсказуемыми сбоями, так и удивительными сообщениями об ошибках. Delphi4, на наш взгляд, несколько более надежна.
При создания Power++ компонента мы старались, насколько это возможно, повторить то же самое, что делалось с аналогичными тестами на других средствах разработки, но полного подобия достичь, очевидно, невозможно из-за нежелания Power++ создавать ActiveX-компоненты.
Сделаем “Power++”-компонент, для чего сначала создадим библиотеку компонентов (рис. 6), после чего следуя инструкциям, изложенным в Help по адресу “Create a component“, выберем File/New/Class/Native Component (рис. 7) и доведем до конца появившийся Wizard. Запустим проект на выполнение (при этом библиотека будет установлена в Power++) и, для отладки компонента, создадим тестовое приложение. Оно подключается к проекту с отлаживаемым компонентом (рис. 8) через Run Options (описано в Help на “Debugging a design-time component library”), однако это имеет смысл только в том случае, когда мы подгружаем свой компонент динамически (описав это в коде тестового проекта). В принципе, в Help сказано, что все приложения по умолчанию используют “run-time”-версии библиотек, при этом их текст не включается в текст исполняемого файла (.exe), что сокращает время линковки и делает более удобной отладку (“Programs prepared with the run-time version of the library have smaller executable files and are faster to link… …By default, all executable files use the run-time version of the Power++ library”). Этот же параметр можно установить в настройках для проекта: если открыть View/Targets и, выбрать Default Options/Exe(Test1), вызвать Properties во всплывающем по правой кнопке мыши меню, то на закладке Executable Size должен быть выделен режим “Use the run-time DLL”. Все это хорошо, и даже прекрасно – но по какой-то причине не работает с компонентами, созданными в Power++, поэтому приходится при внесении изменений в компонент пересоздать полностью сначала его, потом тестовое приложение, а затем снова возвращаться в проект с компонентом для его отладки (из-за такого неудобства мы всего-лишь пару раз пользовались точкоми прерывания при отладке компонента, когда просто нельзя было обойтись без просмотра содержимого переменных, а в основном запускали тестовое приложение из его же проекта).
Рисунок 5. Сообщения об ошибках, появляющиеся в Code Editor.
Для добавления в компонент функций (user functions), свойств, методов и событий при создании компонентов используются Wizard’ы из меню, всплывающего при щелчке правой кнопкой мыши на имени компонента в окне Classes (Insert/Property).
Рисунок 6.
Рисунок 7.
Рисунок 8.
Добавим свойство CurID_RT с типом WLong (во избежание лишних проблем не меняя другие значения по умолчанию, кроме типа данных). На этом работа с автоматизацией в нашем проекте закончилась. Остальной код был введен вручную.
Единственным серьёзным отличием от реализации в других средах разработки стала ликвидация метода Сonnect - по лени.
Рисунок 9. Property Wizard
Мы не стали подробно комментировать код, поскольку, в принципе, все точно так же, как и при работе с уже описанными приложениями. Выполнив двойной щелчок левой кнопки мыши на названии компонента откроем CodeEditor и внесем в него:
public: // add your public instance data here WBool MakeWindow( WWindow * parent, WUInt id, const WChar *className, const WChar *title, const WRect & r, WStyle wstyle, WStyle exStyle, void *data=NULL); WBool LoadWindow( WWindow * parent, const WResourceID & id, WModuleHandle module=_ApplicationModule ); private: // add your private instance data here protected: // add your protected instance data here void onExpand( WObject * source, WTreeViewEventData * event ); void onEndLabelEdit( WObject * source, WTreeViewDispEventData * event ); void onCollapse( WObject * source, WTreeViewEventData * event ); void miAdd_onClick(WObject * source, WEventData * event); void miDelete_onClick(WObject * source, WEventData * event); void InitControls(); void __fastcall set_CurID_RT(long Value); long __fastcall get_CurID_RT(); WTreeViewItemHandle __fastcall ExpandParentNode(long iID_RT); WTreeViewItemHandle __fastcall FindChildByData( WTreeViewItemHandle TVItemParent, void * pData); WBool __fastcall TreeViewCallBack1 (WTreeView * treeView, WTreeViewItemHandle item, void * userData ); //Описание переменных WTreeViewItemHandle tviRoot; //Указатель на корневой (первый) элемент дерева WTransaction * tran; WPopupMenu * ttt; WMenuItem * miDelete, * miAdd;//Указатели на элементы всплывающего меню WQuery * query_1; WImageList * ImageList1; public: // CurID_RT Property WLong GetCurID_RT() const; WBool SetCurID_RT( WLong newValue ); public: CtestTreeView(); public: ~CtestTreeView(); }; // Code added here will be included at the top of the .CPP file // Include definitions for resources. #include “WRes.h” //описание взято из исходника “CommCtrl.h” (подключить этот header-файл в данный //компонент нам не удалось) #define TVIF_CHILDREN 0x0040 #define TV_FIRST 0x1100 // TreeView messages #define TVM_SETITEMA (TV_FIRST + 13) #define TVM_SETITEMW (TV_FIRST + 63) #ifdef UNICODE #define TVM_SETITEM TVM_SETITEMW #else #define TVM_SETITEM TVM_SETITEMA #endif typedef WLong LPARAM; typedef WUInt UINT; typedef HANDLE HTREEITEM; typedef struct _TV_ITEM { UINT mask; WTreeViewItemHandle hItem; UINT state; UINT stateMask; LPSTR pszText; int cchTextMax; int iImage; int iSelectedImage; int cChildren; LPARAM lParam; } TV_ITEM, FAR *LPTV_ITEM; //Конструктор CtestTreeView::CtestTreeView() { } //Деструктор CtestTreeView::~CtestTreeView() { } //На MakeWindow или LoadWindow выполняется инициализация (InitControls()) WBool CtestTreeView::MakeWindow( WWindow * parent, WUInt id, const WChar *className, const WChar *title, const WRect & r, WStyle wstyle, WStyle exStyle, void * data ) { WBool b = WTreeView::MakeWindow(parent, id, className, title, r, wstyle, exStyle, data); InitControls(); return b; } WBool CtestTreeView::LoadWindow( WWindow * parent, const WResourceID & id, WModuleHandle module) { WBool b = WTreeView::LoadWindow(parent, id, module); InitControls(); return b; } void CtestTreeView::InitControls() { //Создание компонента tran (класс WTransaction) и связь его с базой данных ...//через //существующее ODBC-соединение. Очень важно не забыть tran->Connect, т.к. по //умолчанию компонент WTransaction в Desighn-time создается со свойством //AutoConnect = True, но этого свойства в run-time нет. tran = new WTransaction(); tran->SetDBMSName(“ODBC”); tran->SetDataSource(“Test”); tran->SetUserid(“sa”); tran->SetCursorDriver(WTCDNative); tran->Connect(); //Создание компоеннта query_1 (класс WQuery) и подключение к нему //компонента tran query_1 = new WQuery(); query_1->SetTransactionObject(tran); //Создание всплывающего меню ttt (класс WPopupMenu), создание и подключение //элементов меню, подключение меню к компоненту. ttt = new WPopupMenu(“ttt”, true); miAdd = new WMenuItem(0, “Add”, “”, WTextMenuItem); miDelete = new WMenuItem(0,”Delete”, “”, WTextMenuItem); ttt->AddItem(miAdd, 1, true); ttt->AddItem(miDelete, 1, true); SetPopup(ttt); //Создание первой (корневой) ветки tviRoot = Add( “Root”, NULLHTVITEM, 0, -1, 0, NULLHTVITEM, (void *) 0, WTVC_HAS_CHILDREN ); //Установка обработчиков для событий компонента SetEventHandler( WExpandEvent, this, WEventHandlerCast(CtestTreeView, onExpand)); SetEventHandler( WCollapseEvent, this, WEventHandlerCast(CtestTreeView, onCollapse)); SetEventHandler( WEndLabelEditEvent, this, WEventHandlerCast(CtestTreeView, onEndLabelEdit)); miAdd->SetEventHandler( WClickEvent, this, WEventHandlerCast(CtestTreeView, miAdd_onClick)); miDelete->SetEventHandler( WClickEvent, this, WEventHandlerCast(CtestTreeView, miDelete_onClick)); } void CtestTreeView::onExpand( WObject * source, WTreeViewEventData * event ) { WDataValue value; WString sSQL; //Получение ID_RT для выделенной ветки и формирование SQL-запроса для получения //названий, ID_RT дочерних веток и количества дочерних веток у них WLong lUserData = (WLong)(GetUserData( event->item )); query_1->Close(); sSQL.Sprintf( “select ID_RT, Name, \r\n” “ (select count(*) from RT as RT1 \r\n” “ where RT1.Parent = RT.ID_RT \r\n” “ and RT1.ID_RT <> RT.ID_RT) as HasChildren \r\n” “ from RT \r\n” “ where Parent = %d and ID_RT <> Parent \r\n” “ order by Name”, lUserData); query_1->SetSQL( sSQL ); if (!query_1->Open()) { WMessageBox mb; mb.SetText(“Произошла ошибка при попытке чтения из базы данных!”); mb.Prompt(); return; }; //Создание дочерних веток для раскрывающейся (выделенной) ветки в компоненте while (query_1->FetchNext( FALSE, FALSE )) { WLong lID_RT = query_1->GetValue( 1 ).GetSLONG( ); WString sName = query_1->GetValue( 2 ).GetCHAR( ); WTreeViewCChildren HasChildren = query_1->GetValue( 3 ).GetSLONG( ) != 0 ? WTVC_HAS_CHILDREN : WTVC_FIELD_UNUSED; Add( sName, event->item, 0, -1, 0, NULLHTVITEM, (void *)lID_RT, HasChildren ); } return; } void CtestTreeView::onCollapse( WObject * source, WTreeViewEventData * event ) { //На закрытие ветки удалить все дочерние ветки //(Метод Collaps не вызывает события onCollapse) Collapse(event->item, TRUE); return; } void CtestTreeView::onEndLabelEdit(WObject * source, WTreeViewDispEventData * event ) { WQuery sqlUpdateNodeName; WString sSQL; //Создается новый объект WQuery, выполняется изменение имени для //редактируемой ветки sqlUpdateNodeName.SetTransactionObject( query_1->GetTransactionObject()); sSQL.Sprintf(“UPDATE RT SET Name = ‘%s’ WHERE ID_RT = %d “, event->itemText, long(GetUserData(event->itemHandle))); if (!sqlUpdateNodeName.Execute(sSQL)) { WMessageBox mb; mb.SetText(“Произошла ошибка при попытке записи в базу данных!”); mb.Prompt(); return; }; //Обновление данных в query_1 query_1->Close(); query_1->Open(); return; } WBool CtestTreeView::SetCurID_RT(WLong newValue) { WTreeViewItemHandle hTVItem = (WTreeViewItemHandle)NULL; WLong iOldValue; if (!query_1->GetOpened()) return false; if (newValue < 0) return false; hTVItem = GetSelectedItem(); iOldValue = ( hTVItem == NULL) ? -1 : (WLong)(GetUserData(hTVItem)); if (newValue == iOldValue) return false; //Раскрыть все ветки, которые являются предками искомой hTVItem = ExpandParentNode(newValue); //Перейти на ветку с переданным значением newValue = ID_RT // hTVItem является Parent’ом для искомой ветки FindChildByData(hTVItem, (void *)newValue); return false; }; WLong CtestTreeView::GetCurID_RT() const { //Получить ID_RT выделенной ветки, если такая есть, иначе вернуть -1 WLong iID_RT = (GetSelectedItem() != NULL) ? (WLong) (GetUserData(GetSelectedItem())) : -1; return iID_RT; } WTreeViewItemHandle __fastcall CtestTreeView::ExpandParentNode( long iID_RT) { WTreeViewItemHandle TempResult = (WTreeViewItemHandle)NULL; WQuery sqlFindID_RT; WLong iParentID; WString sID_RT; //Создать новый объект WQuery и выполнить запрос //для нахождения ветки-предка для данной ветки sqlFindID_RT.SetTransactionObject( query_1->GetTransactionObject()); sID_RT.Sprintf(“%d”,iID_RT); sqlFindID_RT.SetSQL(“SELECT Parent FROM RT” ” WHERE ID_RT = “ + sID_RT); sqlFindID_RT.Open(); sqlFindID_RT.FetchNext( FALSE, FALSE ); iParentID = sqlFindID_RT.GetRowCount(false) == 0 ? –1 :sqlFindID_RT.GetValue(1).GetSLONG(); if (iParentID == -1) return TempResult; if (iParentID == 0) { //Если найден корень (Root), то раскрыть его и выйти в из функции TempResult = tviRoot; Expand(TempResult); return TempResult; } //Рекурсивный вызов ExpandParentNode TempResult = ExpandParentNode(iParentID); //На выходе из рекурсии каждый находить ветку, для которой ранее //искался Parent, и раскрывать ее if (TempResult == NULL) return TempResult; TempResult = FindChildByData(TempResult, (void *)iParentID); if (TempResult != NULL) Expand(TempResult); return TempResult; } WTreeViewItemHandle __fastcall CtestTreeView::FindChildByData(WTreeViewItemHandle TVItemParent, void * pData) { WTreeViewItemHandle hTChild; //Перебрать все дочерние ветки для TVItemParent и найти ту, //ID_RT которой совпадает с искомым (ID_RT хранится в Data в виде (void *) EnumerateChildren(TVItemParent, this, (WTreeViewCallback)&TreeViewCallBack1, pData); hTChild = GetSelectedItem(); return hTChild; } WBool __fastcall CtestTreeView::TreeViewCallBack1 ( WTreeView * treeView, WTreeViewItemHandle item, void * userData ) { //Эта Call-Back-функция вызывается из EnumerateChildren. // Она повторно выполняется до тех //пор, пока не возвращает false или не заканчивается список дочерних веток if (GetUserData(item) == userData ) { SetSelectedItem(item); return false; } return true; } void CtestTreeView::miAdd_onClick( WObject * source, WEventData * event ) { WString sSQL; WString sName = “Новый элемент №”; WTreeViewItemHandle hTVItem = GetSelectedItem(); WLong lUserData = (WLong)(GetUserData(hTVItem)); WQuery query_temp; query_temp.SetTransactionObject(query_1->GetTransactionObject()); //Отключение AutoCommit позволяет держать транзакцию сколько нужно, //что позволяет выполнить несколько запросов и не беспокоиться, что //другой пользователь в это же время изменяет данные tran->SetAutoCommit(false); //Получение номера следующей записи с помощью специальной таблицы Counters //которая содержит единственное поле,увеличивающееся на 1 при каждой новой //записи в таблицу RT. В принципе, лучше было бы оформить этот запрос как //Stored-procedur’у с именем, например,“Next”, и вызывать ее при каждой //вставке записи. query_temp.SetSQL(“update Counters set CounterValue = CounterValue + 1”); if (!query_temp.Execute()) { WMessageBox mb; mb.SetText(“Произошла ошибка при попытке записи в базу данных!”); mb.Prompt(); return; }; //Получаем новое ID_RT из таблицы Counters query_temp.SetSQL(“select CounterValue \r\n” “from Counters where ID_Counter = 1”); if (!query_temp.Open()) { WMessageBox mb; mb.SetText(“Произошла ошибка при попытке записи в базу данных!”); mb.Prompt(); return; }; query_temp.FetchNext( FALSE, FALSE ); WLong lNew = query_temp.GetValue( 1 ).GetSLONG( ); query_temp.Close(); //Сформировать название для новой записи sName.Sprintf(“%s %d”, sName.GetText(), lNew); //Вставить новую запись в таблицу RT sSQL.Sprintf(“insert into RT (ID_RT, Parent, Name, ImageNum) \r\n” “values (%d, %d, ‘%s’, 0)” ,lNew, lUserData, sName.GetText()); query_temp.SetSQL(sSQL); if (!query_temp.Execute()) { WMessageBox mb; mb.SetText(“Произошла ошибка при попытке записи в базу данных!”); mb.Prompt(); return; }; //Выполнить Commit вручную и разрешить доступ к базе данных //другим пользователям tran->Commit(); tran->SetAutoCommit( true ); //Если ветка-предок, в которую добавляется дочерняя ветка, открыта, то просто //добавить новую ветку, иначе с помощью Windows API добавить плюс к //ветке-предку (т.к. у нее могло не быть дочерних веток на этот момент, //а Power++ не дает возможности прибавить плюс к ранее созданной ветке не //добавляя дочерних веток) и раскрыть ее и выделить новую ветку if (GetExpanded(hTVItem)) { Add( sName.GetText(), hTVItem, 0, -1, 0, NULLHTVITEM, (void *) lNew); } else { TV_ITEM tvi; tvi.mask = TVIF_CHILDREN; tvi.hItem = hTVItem; tvi.cChildren = 1; SendMessage(WMessage( TVM_SETITEM, WUInt(0), WLong(&tvi))); Expand(hTVItem); FindChildByData(hTVItem, (void *)lNew); }; //обновить query_1 query_1->Close(); query_1->Open(); return; } void CtestTreeView::miDelete_onClick( WObject * source, WEventData * event ) { WQuery sqlDelete; WString sSQL; WTreeViewItemHandle hTVItem = GetSelectedItem(); sqlDelete.SetTransactionObject(query_1->GetTransactionObject()); sSQL.Sprintf( “delete from RT where ID_RT = %d”, (WLong)GetUserData(hTVItem)); sqlDelete.SetSQL( sSQL ); if (!sqlDelete.Execute()) { WMessageBox mb; mb.SetText(“Произошла ошибка при попытке удаления записи из ” “базы данных!”); mb.Prompt(); return; }; Delete(hTVItem); query_1->Close(); query_1->Open(); return; }
В код тестового проекта надо добавить обработчики событий Click для кнопок GetCurID_RT и SetCurID_RT, подобно тому, как мы это делали в предыдущих примерах:
WBool Form1::SetCur_ID_Click( WObject * source, WEventData * event ) { ctesttreeview_1->SetCurID_RT(atoi(CurID_RT->GetText())); return FALSE; } WBool Form1::GetCur_ID_Click( WObject * source, WEventData * event ) { WString sTemp; sTemp.Sprintf("%d", ctesttreeview_1->GetCurID_RT()); CurID_RT->SetText(sTemp); return FALSE; }
В начале нашей речи о Power++ мы немного погрешили против правды — все-таки некоторая поддержка ActiveX в нем имеется, а именно, есть возможность создания AсtiveX-server’a. На самом деле это минимальный COM-объект, реализованный в виде DLL. Это означает, что его можно запустить только в локальном процессе или под управлением MTS. Поддержка распределенной технологии в Power++ вообще реализована пока только для Jaguar и MS Transaction Server с помощью компонента ActiveX-server. Возможно, это — политика фирмы.
Работа с базой данных в Power++ реально возможна только через ODBC, что влечет за собой ряд ограничений на сложность SQL-запросов. Наш пример использует существующее ODBC-соединение “Test”, настроенное на работу с MS-SQL Server.
В целом, Sybase Power++ произвел на нас впечатление достаточно “сырого” продукта, который, однако, заметно совершенствуется от версии к версии (разница между 1.0 и 2.1 более чем ощутима). В рассматриваемом нами аспекте (COM-DCOM) Power++ отстает и пока не может конкурировать с Delphi, C++ Builder или Visual C++, но, возможно, впоследствии это отставание будет если не ликвидировано, то по крайней мере сокращено. Несмотря на все недоработки, в Power++ значительно реже приходится прибегать к “магическим” действиям типа “TreeView.Selected := TreeView.Selected”, фокус в импортированных ActiveX’ах работает нормально, даже если эти ActiveX’ы были созданы в Delphi, чего нельзя сказать о намного более продвинутых и бесконечно более загадочных продуктах Inprise. Для работы с базами данных у компании Sybase предусмотренно значительно более удобное, чем у любого из конкурирующих продуктов, средство DataWindow, реализованное в Power++ в виде ActiveX-компонента. С его помощью визуально создаются формы, отчеты или просто таблицы. Оно, разумеется, не всемогуще, но при корпоративном использовании позволяет быстро добиться вполне приемлемых результатов.
Так как работа с API и расширяемость Power++ оставляют желать лучшего, то для системной разработки лучше использовать другие продукты. Однако при внутрикорпоративном использовании хорошо продуманные библиотеки, удобные компоненты типа DataWindow, решающие большинство проблем доступа к корпоративным БД и легкий в использовании Help делают Power++ весьма неплохим выбором — особенно, если вы уже используете Power Builder той же фирмы.