Продолжается подписка на наши издания! Вы не забыли подписаться?

Visual C++ 6.0 в составе Microsoft Visual Studio.

Другие средства разработки

Во время создания данного обзора окончательной версии Visual Studio еще не было (она должна появиться непосредственно перед выходом нашего журнала) и нам пришлось использовать “пре-релиз”, любезно предоставленный нам фирмой Microsoft. Так что в окончательной версии могут появиться изменения, хотя судя по размерам продукта, заметить их будет непросто.

Итак, начнем. Первое, что бросается в глаза при установке Microsoft Visual Studio 6.0 (MSVS), так это частота появления на экране слова “MICROSOFT” - к месту и, в основном, не к месту. Грандиозность данного продукта потрясает, особенно с точки зрения его объема – несколько урезанный нами в процессе инсталляции вариант занял более 1 Гб на жестком диске (Справедливости ради надо заметить, что в поставку MSVS входят, кроме Visual C++ 6.0 (VC) и Visual Basic 6.0 (VB), еще Visual j++ 6.0 и Visual Fox Pro 6.0, но эти продукты мы даже не устанавливали). Разумеется, значительную часть от этого отхватил себе Help – чтобы постоянно не обращаться к CD мы поставили значительную часть Help’a на жесткий диск (примерно 500 Мб), оставив на CD только половину. Очень неудобным оказалось то, что исходные тексты примеров (при любом типе установки) все равно остаются на CD и, если машина не оснащена CD-ROM’ом (именно так и оказалось в нашем случае), то при попытке открыть эти самые исходные тексты выдается сообщение об ошибке. Так что, по мнению Microsoft, вы можете не иметь накопителя на гибких дисках (по простому говоря флопа), но накопитель CD-ROM иметь обязаны.

Help (см. рис.1) от Microsoft – это HELP: каждую букву этого слова хочется написать, нет, высечь из камня наподобие лиц американских президентов, как это сделано где-то не то в Неваде, не то в Алабаме. Help поистине огромен (в полном варианте где-то около 1.5 Гб), в нем можно найти десяток-другой вхождений практически любого слова или словосочетания – будь он русским, вероятно, нашлось бы и “мама мыла раму”. Трудно сказать, что это неоспоримое достоинство, особенно если вы пытаетесь найти список документов по какому-то контексту и получаете 1000 вхождений (совсем как в Internet). Вообще Help стал более обширным, но менее читаемым. Собственно HЕLP - это, несколько урезанная версия MSDN (Microsoft Developer Network – сеть для разработчиков от компании Microsoft): кроме собственно справочной информации сюда входит масса дополнительной информации, такой как книги и периодические издания по программированию, изданные под патронажем Microsoft. Вот список литературы представленной в MSDN Library Visual Studio 6.0 release: Developing International Software for Windows 95 and Windows NT, Advanced Microsoft Visual Basic 5, Hardcore Visual Basic, Inside OLE 2nd Edition (эта самое профессиональное издание, описывающее технологии OLE 2 и COM, можно сказать библия, которая к сожалению никогда не продавалась ни в исходном, ни в переведенном виде в нашей стране), Microsoft® Jet Database Engine Programmer’s Guide, Understanding Thin-Client/Server Computing, The Windows Interface Guidelines for Software Design.

msVisualC_Help.GIF (21036 bytes)

В общем HTLP работает корректно, но иногда пытаясь перейти на описание какой-либо функции, вы оказываетесь в совершенно не относящемся к ней месте (за время нашей работы над тестами такие казусы приключились четыре раза в разных местах). Зато оформлен Help красиво – еще бы, ведь он сделан на aaca Internet Explorer’a 4.01, который, кстати, тоже придется установить при инсталляции Visual Studio.

Что нового в Visual C++

Microsoft Visual С++ 6.0 содержит множество новых возможностей, которые предназначены для помощи разработчикам в создании приложений с высоким быстродействием. Приведем некоторые из них:

Внесение изменений в код при отладке и ее продолжение без необходимости прерывания сеанса отладки, перекомпоновки, повторного запуска отладчика и возвращения приложения в состояние, при котором возникла проблема. Эта функция работает в общем, неплохо. Но при ее использовании есть определенный риск (особенно на медленных машинах, например, Pentium 120 c 64 MB RAM) не дождаться окончания компиляции, перекомпоновки и т.д.

Технология IntelliSense увеличивает продуктивность и заметно упрощает программирование благодаря автоматическому перечислению членов, информации о параметрах, комментариев к коду и завершению написа-ния конструкций, что устраняет необходимость в запоминании сложных правил синтаксиса, параметров, свойств и методов объектов. Нечто подобное вы могли увидеть в Delphi и Visual Basic - под названием Complete Word.

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

На 15-30 процентов (в зависимости от типа компиляции) увеличена производительность компилятора.

Настройка линкера Delay Load Imports позволяет приложениям, созданным при помощи Visual С++, подгру-жать библиотеки (DLL) только при необходимости. Это снимает требование загрузки библиотеки до момента её использования.

С Composite Controls вы получаете новейшие для VC возможности создавать ActiveX'ы (в Delphi и Visual Basic ) — простое использование повторно используемых элементов управления ActiveX внутри собственных эле-ментов, что позволяет создавать полностью настраиваемые элементы управления. Однако корректной работы Composite Controls удалось добиться только при их использовании в MFC-приложениях. Если в Composite Controls используется хотя бы один ActiveX, то приложения, не использующие MFC, просто отказываются ра-ботать. По роковой случайности Visual Basic именно такое приложение.

Использование новых общих элементов управления придаст пользовательскому интерфейсу стиль Internet Explorer 4.0. Это можно считать как достоинством, так и недостатком — в зависимости от вашего отношения к интерфейсу Internet Explorer 4.0.

Шаблоны потребителя и провайдера OLE DB позволяют получать доступ ко всем типам данных или обеспе-чить унифицированный доступ к своим донным при минимальном программировании.

Просмотр таблиц, изменение данных и создание SQL-запросов в IDE для любой ODBC- или OLE DB-базы данных. Визуальное проектирование и модификация схем и объектов для Microsoft SQL Server и Oracle 7.3.3+.

К сожалению, по вполне понятным причинам, нам не удалось собственноручно проверить работу пакета с интеграцией кода с мэйнфрейм-системами и сервисами транзакций при помощи Customer Information Control Systems (CICS).

Visual C++ 6.0, установленная на компьютер с процессором P120 (64 Мб оперативной памяти) под Windows NT 4.0 (Service Pack 3), работала не слишком быстро, но, в общем, приемлемо – в основном тормозил Help и загрузка проектов. Можно предположить, что реальной минимальной конфигурацией для работы на Visual C++ 6.0 будет P100 с 36/48 Мб оперативной памяти для Windows95/NT – на более слабых машинах все действия будут выполняться слишком медленно. Программирование на Visual C++, особенно после работы на продуктах Inprise (Delphi и C++ Builder), поначалу кажется ужасно медленным – то, что делалось в одно движение теперь приходилось делать в четыре. Во-первых, Visual C++ — не такое уж быстрое средство “быстрой разработки приложений” – приходится только мечтать о богатом и разнообразном наборе детально проработанных компонентов типа DBGrid, Query, DCOMConnection и ClientResultSet (сравните ради интереса TreeView из Delphi и из Visual C++, и станет понятно, что мы имеем в виду), а во-вторых, мы сознательно старались прижаться как можно теснее к API, поскольку именно прозрачный доступ к функциям API и хорошая их документированность позволяют создавать компактные и высокопроизводительные программы и компоненты. К сожалению библиотека “Microsoft Foundation Class” (MFC) – основная объектно-ориентированная библиотека, входящая в поставка VC, сделана как обертка API и не дает ощутимого преимущества в областях, не связанных с документно-ориентированными графическими интерфейсами. К тому же, в отличие от продуктов Inprise и Sybase, VС не поддерживает собственных визуальных компонентов — вместо этого можно использовать стандартные элементы интерфейса Windows (они универсальны, но невзрачны, а дополнение стандартных элементов новыми свойствами — тяжелая работа) или ActiveX’ами, множество которых поставляется с MSVS.

В наших тестах мы не обнаружили особого выигрыша от использования MFC. Зато в поставку VC входит библиотека ATL (Active Template Library) — это очень компактная библиотека, позволяющая создавать как ActiveX-компоненты, так и удаленные DCOM-серверы.

За все время нашей работы над тестами среда Visual C++ ни разу не вылетела ни по явным, ни по неявным причинам, а библиотеки проектов или исполняемые файлы никогда не оказывались необоснованно заблокированными средой, как это периодически наблюдалось в продуктах других производителей.

Свойственное большинству продуктов Microsoft стремление помочь пользователю даже тогда, когда он этого не хочет, в редакторе кода Visual C++ невероятным образом ограничено и приходится как нельзя кстати – вообще, нам понравился редактор кода: из всех рассматриваемых в наших обзорах средств разработки он, безусловно, лучший. В нем впервые для C++ присутствует и работает Complete Word (что, учитывая сложность языка C++, является большим достижением).

Правда, надо признать, что реализация Complete Word’а совсем сырая, так он зачастую путается с переменными, тип которых описан через typedef, и не показывает функции из template классов.

Не меньшим достижением можно считать существенно возросшую по отношению к предыдущим версиям скорость компиляции проекта – конечно, он работает намного медленнее, чем компилятор Object Pascal в Delphi, но, затраты времени на компиляцию в этих двух средах уже по крайней мере сравнимы, а C++ Builder и Power++, по нашим впечатлениям, отстают от них обоих и довольно значительно.

Удобно сделана отладка проекта, в том числе и в схеме клиент-сервер DCOM (рис. 2).

msVisualC.GIF (28108 bytes)

При отладке COM-объектов (например, нашего ActiveX’a) можно использовать содержащее его приложение, специальный Test Container или просто Visual Basic. Выбор осуществляется в меню Project/Settings на закладке Debug (поле “Executable for debug session”). Разумеется, при отладке можно трассировать выполнение программы, просматривать содержимое переменных и области памяти, а также регистры процессора. Самый простой способом трассировки кода DCOM-сервера заключается в установке точки прерывания в коде клиента на вызове метода сервера, после чего, при остановке выполнения на этом месте, выполняется Step Into (F11), в результате чего загружается вторая копия среды Visual C++ с проектом сервера в режиме отладки. Если связать два проекта в пределах одного WorkSpace, то любое изменение в одном из них будет приводить к перекомпиляции обоих (связать проекты можно в меню Project/Dependencies). Мы использовали связь между DCOM-проектами клиента и сервера, чтобы синхронизировать их и быть уверенным в том, что не забыли перекомпилировать, скажем, сервер, перед запуском только что перекомпилированного клиента. Ошибка в дереве WorkSpace с раздвоением функций, когда на закладке Class View каждая ветка дерева получает своего “двойника”, не приводит к нарушениям в работе, а всего-лишь несколько сбивает с толку не привыкших к этому программистов (со временем привычка появляется J). Наши тесты показали, что Class View плохо справляется с очень большими проектами и в некоторых случаях VC не может открыть Workspace, который она сама только что сохранила. Надеемся, что в продажной версии (или в ближайших “сервис-паках”) эта проблема будет устранена.

В Visual C++ можно создать несколько конфигураций для одного проекта (конфигурацию, с которой вы работаете в данный момент, можно установить в меню Build/Set Active Configuration). Для новых проектов визард VC создает несколько таких конфигураций: Win 32 Debug, Win 32 Debug Unicode, Win 32 Release… (см. рис. 3)

Project Setings.GIF (11286 bytes)

Для отладки, разумеется, используется режим Debug, а для создания окончательной версии в ATL проекте можно выбрать Release MinDependency (весь код объединяется в одной dll) или MinSize (исполняемый файл становится меньше, но появляется необходимость таскать за собой ATL.DLL).

Можно задать свой сценарий компиляции, если в меню Project/Project Settings выбрать закладку Custom Build. В случае, если после стандартной компиляции приходится выполнять еще какое-то дополнительное действие, например запуск bat-файла. Мы использовали эту возможность для компиляции DCOM-проекта: дело в том, что возвращаемые нами массивы являются типами, несовместимыми с Automation, и для их поддержки в VC необходима так называемая DLL посредник/заглушка (Proxy/Stub DLL) или просто заглушка, выполняющая передачу (маршалинг) содержимого параметров функций между машинами. Код заглушки генерируется автоматически при компиляции проекта DCOM сервера, но компилировать заглушку надо вручную. Чтобы не утруждать себя лишней работой (не забывайте, ведь лень — двигатель прогресса), мы и воспользовались Custom Build. Если же забыть про заглушку, то при попытке вызвать объект, содержащий в себе интерфейсы с нестандартным маршалингом, произойдет ошибка 0x80020008 (неверный тип данных). Чтобы этого не происходило, мы создали файл stub.bat в текущем каталоге проекта DCOM-сервера со следующими командами:

nmake -f msDCOM_1_Testps.mk
regsvr32 /c /s msDCOM_1_Testps.dll
rem наш DCOM-сервер называется msDCOM_1_Test
rem этот файл надо вызвать из Custom Build

После этого мы добавили вызов stub.bat в Custom Build, для чего в поле Commands вписали вызов “call stub.bat”, а в Outputs – “$(TargetDir)\msDCOM_1_Testps.dll”.

Логично было бы сделать это автоматически при создание проекта, но, по непонятным соображениям, в Visual C++ это не сделано. После модификации установок проекта, закладка Custom Build стала выглядеть так же, как на рис. 3.

ActiveX-компонент TreeView, работающий с базой данных

Безусловно, в VC хорошо поддерживается технология COM — иначе и быть не могло, ведь спецификация ActiveX родилась именно в Microsoft. Создание компонента ActiveX не представляет каких-либо проблем, для чего также в VC как в Delphi или в C++ Builder’e, используется соответствующий Wizard. Запустить этот Wizard можно из меню File/New с закладки Projects. Здесь есть целых два Wizard’а: “MFC ActiveX ControlWizard” и “ATL COM AppWizard”. ActiveX компонент, созданный с помощью MFC, больше по размеру (или тянет за собой MFC DLL) и не поддерживает dual-интерфейсов, а создать на MFC DCOM-сервер, реализованный как сервис NT, вообще невозможно. По этой причине был выбран “ATL COM AppWizard”. Мы назвали создаваемый проект “Atl02Test” – и да не осудят нас за неброское название. В появившемся после нажатия кнопки “OK” окне мы выбрали опцию Dynamic Link Library(DLL) и нажали Finish.

Созданный нами проект является пустым — необходимо добавить в него COM объект.

Сделать это можно, если выбрать из меню “Insert” пункт “New ATL Object…” или нажать правую кнопку мыши на название проекта (“atl02Test Classes”) в ClassView и в появившемся меню выбрать пункт “New ATL Object…” (см. рис. 4).

msGoToDeclaration.GIF (7860 bytes)

В появившемся Wizard’e мы выбрали Full Control (в категории Controls), после чего назвали объект “atl01”. На закладке Attributes мы включили опции: Support ISupportErrorInfo и Support Connection Points. На закладке Miscellaneous в поле “Add control based on” выбрали SysTreeView32.

После нажатия на кнопку OK все зажужжало, и в дереве WorkSpace появился новый класс с именем Сatl01. В принципе, после компиляции ActiveX компонент TreeView уже готов и зарегистрирован в системе, но никакой функциональной нагрузки он пока не несет — впереди еще не один килобайт кода, который непременно следует добавить.

В первую очередь, поскольку компонент должен работать с базой данных (в нашем случае, с MSКSQLКserver), мы выбрали ODBC, для чего добавили в header файл “stdafx.h” строку #include <SQLEXT.H>.

Для работы с базой в файле atl02Test.cpp мы объявили несколько переменных:

      HIMAGELIST g_hImageList1 = NULL;
      HMENU g_hPopupMenu1 = NULL;
      HDBC hdbc = NULL, hdbc2 = NULL;
      HENV henv = NULL;
      HSTMT hstmt = NULL, hstmt2 = NULL;

И продекларировали часть из них (те, которые там используются) в файле atl01Ctrl.h:

   extern HMENU g_hPopupMenu1;
   extern HDBC hdbc;
   extern HDBC hdbc2;
   extern HENV henv;

А одну из них – в файле atl01Ctrl.cpp:

   extern HIMAGELIST g_hImageList1;

После этого несложного действия мы несколько изменили функцию DllMain (она находится в папке Globals (ClassView)), которая вызывается при загрузке-выгрузке DLL нашего ActiveX’a. На загрузку DLL (DLL_PROCESS_ATTACH) мы создали ImageList, PopupMenu (все компоненты, независимо от того сколько их будет создано в приложении, будут использовать одну и ту же копию этих объектов) и инициализировали переменные для работы с базой данных, а на выгрузку DLL (DLL_PROCESS_DETACH) – освободили занятые ресурсы:

BOOL WINAPI DllMain(HINSTANCE hInstance, DWORD dwReason, 
                  LPVOID /*lpReserved*/)
{
    if (dwReason == DLL_PROCESS_ATTACH)
{
        _Module.Init(ObjectMap, hInstance, &LIBID_ATL02TESTLib);
        DisableThreadLibraryCalls(hInstance);
        // ***************************************
        //
/* Ниже вписан наш код по размещению в памяти некоторых объектов на момент 
загрузки DLL. */
/* Создание ImageList’a для иконок, отображаемых перед ветками в TreeView. 
Загружаются 3 картинки из ресурсов текущего проекта (предварительно надо 
создать их в разделе Bitmap закладки Resource View, причем все три рисунка 
должны располагаться последовательно в одном BMP ресурсе, а высота и ширина
каждого должна быть 16x16 точек). */
        g_hImageList1 = ImageList_LoadBitmap(
                _Module.GetResourceInstance(),
                MAKEINTRESOURCE(IDB_BITMAP1), // ID - BMP ресурса
                16, // высота рисунков в ImageList’е

                3, 

                RGB(255, 255, 255) // белый цвет будет задавать прозрачность
        );
        // Создание всплывающего меню и двух элементов
        g_hPopupMenu1 = CreatePopupMenu();
        AppendMenu(g_hPopupMenu1, MF_STRING, 1, “Add”);
        AppendMenu(g_hPopupMenu1, MF_STRING, 2, “Delete”);
/* Инициализация работы с базой данных 

Разместить ODBC-окружение (environment)*/
        if (SQL_SUCCESS != SQLAllocEnv(&henv))
        {
/* Здесь можно получить настоящее сообщение об ошибке, но мы остановились 
на более простом варианте */
            MessageBox(0, "Ошибка при вызове SQLAllocEnv",
                "SQL-Error", MB_OK || MB_APPLMODAL || MB_ICONERROR
            );
            return FALSE;
        }
/* Разместить ODBC-connections. В некоторых случаях нам требуются два 
одновременно открытых курсора */
        if (SQL_SUCCESS != SQLAllocConnect(henv, &hdbc))
        {
            MessageBox(0, "Ошибка при вызове SQLAllocConnect для hdbc",
                "SQL-Error", MB_OK || MB_APPLMODAL || MB_ICONERROR );
            return FALSE;
        }
        if (SQL_SUCCESS != SQLAllocConnect(henv, &hdbc2))
        {
            MessageBox(0, "Ошибка при вызове SQLAllocConnect для hdbc2",
                "SQL-Error", MB_OK || MB_APPLMODAL || MB_ICONERROR
            );
            return FALSE;
        }
/* В отличие от предыдущих случаев, вызов SQLConnect возвращает 
SQL_SUCCESS_WITH_INFO или SQL_SUCCESS в случае успеха. Вообще-то, хорошо бы 
это проверять */
        RETCODE Ret = SQLConnect(hdbc, (UCHAR FAR *)"Test", SQL_NTS,
            (UCHAR FAR *)"sa", SQL_NTS, (UCHAR FAR *)"", SQL_NTS);
        if ( SQL_SUCCESS != Ret || SQL_SUCCESS_WITH_INFO != Ret )
        {
            MessageBox(0, "Ошибка при вызове SQLConnect для hdbc",
                "SQL-Error", MB_OK || MB_APPLMODAL || MB_ICONERROR);
            return FALSE;
        }
        Ret = SQLConnect(hdbc2, (UCHAR FAR *)"Test", SQL_NTS,
                (UCHAR FAR *)"sa", SQL_NTS, (UCHAR FAR *)"", SQL_NTS
        );
        if (SQL_SUCCESS != Ret || SQL_SUCCESS_WITH_INFO != Ret)
        {
            MessageBox(0, "Ошибка при вызове SQLConnect для hdbc2",
                "SQL-Error", MB_OK || MB_APPLMODAL || MB_ICONERROR);
            return FALSE;
        }
        if (SQL_SUCCESS != SQLAllocStmt(hdbc, &hstmt))
        {
            MessageBox(0, "Ошибка при вызове SQLAllocStmt для hdbc",
                "SQL-Error", MB_OK || MB_APPLMODAL || MB_ICONERROR);
            return FALSE;
        }
        if (SQL_SUCCESS != SQLAllocStmt(hdbc2, &hstmt2))
        {
            MessageBox(0, "Ошибка при вызове SQLAllocStmt для hdbc",
                "SQL-Error", MB_OK || MB_APPLMODAL || MB_ICONERROR);
            return FALSE;
        }
        //
        // ***************************************
    }
    else if (dwReason == DLL_PROCESS_DETACH)
    {
        _Module.Term();
        // ***************************************
//
/* Ниже приведен наш код по освобождению памяти при выгрузки DLL */
        if (g_hImageList1)
        {
            if(ImageList_Destroy(g_hImageList1))
                g_hImageList1 = NULL;
        }
        if (g_hPopupMenu1)
        {
            if(DestroyMenu(g_hPopupMenu1))
                g_hPopupMenu1 = NULL;
        }
/* Вообще-то здесь следует освободить память от  hdbc, hdbc2, henv, 
hstmt и hstmt2, но ведь мы работаем в защищенной ОС, следовательно, 
DLL при выгрузке автоматически освободит память и ресурсы... */
        //
        // ***************************************
    }
    return TRUE;    // ok
}

Примечание: Внесенный нами код заключен в комментарии со звездочками вот так:
// ***************************************
//
добавленный код…
//
// ***************************************

После написания этого кода, мы переместились на функцию OnCreate (в файл atl01Ctrl.h) — кстати, такой переход удобно осуществлять с помощью двойного щелчка мыши на названии функции в ClassView. В этой функции собственно и создается компонент TreeView – мы лишь добавили несколько флагов в m_ctlSysTreeView32.Create. Здесь m_ctlSysTreeView32 – глобальная переменная для создаваемого TreeView:

LRESULT OnCreate(UINT /*uMsg*/, WPARAM /*wParam*/,
    LPARAM /*lParam*/, BOOL& /*bHandled*/)
{
    RECT rc;
    GetWindowRect(&rc);
    rc.right -= rc.left;
    rc.bottom -= rc.top;
    rc.top = rc.left = 0;
    InitCommonControls();
/* Здесь мы установили флаги для Create (справку можно найти в Help на 
CWindow::Create). В общем они означают, что у нашего TreeView будет 
редактироваться текст веток, ветки могут иметь плюсы, coединения 
между ветками отображаются в виде линий, корневая ветка также соединена 
линией, окно самого компонента TreeView является дочерним, видимым, 
выделенная ветка даже при сходе фокуса показывается как выделенная, 
компонент TreeView  “объемным”) */
    m_ctlSysTreeView32.Create(m_hWnd, rc, NULL,
        TVS_EDITLABELS | TVS_HASBUTTONS | TVS_HASLINES
        | TVS_LINESATROOT | WS_CHILD | WS_VISIBLE
        | TVS_SHOWSELALWAYS, WS_EX_CLIENTEDGE, 1);
    // Вызывает нашу функцию инициализации
    InitializeAll();
    return 0;
}

В функции InitializeAll выполняются действия, следующие за созданием окна компонента TreeView (кстати, создавать свои функции, методы, переменные и т.п. удобнее всего из всплывающего меню, которое появляется по щелчку правой кнопки мыши на ветке “Catl1 Ctrl в ClassView (рис.5):

msAddFunction.GIF (8497 bytes)

void Catl01Ctrl::InitializeAll()
{
    // Подключение ImageList к TreeView
    TreeView_SetImageList(m_ctlSysTreeView32.m_hWnd,
        g_hImageList1, TVSIL_NORMAL );
/* Добавить корневую ветку (Root) макрос _T определяет строку как 
Unicode или ANSY в зависимости от настроек Build/Setting для компиляции 
проекта */
   AddNode(NULL, 0, _T(“Root”), TRUE);
      return;
}
/* Функция AddNode - добавляет ветку в компонент TreeView и возвращает 
указатель на нее */
HTREEITEM Catl01Ctrl::AddNode(HTREEITEM hParent, // родительская ветка
    long ID, // 32-х битное число (мы будем хранить в нем номер ветки из БД)
    LPCTSTR Text, // Отображаемый текст
    BOOL HasChildren, // Отображать наличие дочерних веток (+)
    int iImageIndex // Номер рисунка (из ImageList’а)
// Примечание: эта функция изменяет только визуальный компонент
)
{
    TVINSERTSTRUCT is1;
/* В маске устанавливается набор флагов, который указывает параметры, 
которые мы намерены менять. Остальные игнорируются. Мы устанавливаем 
плюс перед веткой, текст, иконку */
    is1.item.mask = TVIF_CHILDREN | TVIF_TEXT | TVIF_PARAM | TVIF_IMAGE;
    is1.hParent = hParent;
    is1.item.iImage = iImageIndex;
    is1.hInsertAfter = TVI_LAST; //  вставлять после последней ветки
    is1.item.pszText = (LPTSTR)Text;
    is1.item.cChildren = HasChildren ? 1 : 0; // (+) если есть дочернии ветки
    is1.item.lParam = ID;
    HTREEITEM hItem = TreeView_InsertItem(m_ctlSysTreeView32.m_hWnd, &is1);
    return hItem;
}

После выполнения инициализации TreeView отобразит корневую ветку с названием “Root” и плюс перед ней, (плюс указывает на наличие дочерних веток). На самом деле, никаких дочерних веток у ветки “Root”, пока она закрыта, нет – они существуют только в базе данных. Чтобы эти ветки появились в дереве, надо обработать событие Expanding, которое срабатывает перед раскрытием ветки (например, после нажатия на плюс или на кнопку “*”).

Раз уж мы собрались обрабатывать событие Expanding, то неплохо бы посмотреть, какие еще события будут полезны для нашего теста? Поскольку дочерние ветки будут создаваться в дереве при попытке раскрыть ветку, то необходимо удалять дочерние ветки из дерева после закрытия ветки – родителя. Событие Collapsed вылавливается в событии Expanded по флагу TVE_COLLAPSE, поскольку и TVN_ITEMEXPANDED и TVN_ITEMEXPANDED срабатывают с одинаковым успехом и на закрытие, и на раскрытие ветки. Разница заключается во флагах, которые можно получить из nmTreeView->action: закрытию ветки соответствует флаг TVE_COLLAPSE, а раскрытию ветки — TVE_EXPAND.

Кроме того, мы обработали событие завершения редактирования текста ветки (TVN_ENDLABELEDIT) и на щелчок правой кнопкой мыши (NM_RCLICK). Последнее мы выбрали для вызова всплывающего меню.

Все описанные выше события относятся к сообщению WM_NOTIFY, для установки обработчиков которого используется макрос NOTIFY_CODE_HANDLER в Message Map’e:

BEGIN_MSG_MAP(Catl01Ctrl)
   MESSAGE_HANDLER(WM_CREATE, OnCreate)
   MESSAGE_HANDLER(WM_SETFOCUS, OnSetFocus)
   CHAIN_MSG_MAP(CComControl<Catl01Ctrl>)
    //********************
    //
    // Установка обработчиков сообщений WM_NOTIFY от TreeView
    NOTIFY_CODE_HANDLER(TVN_ITEMEXPANDING, OnItemExpanding )
    NOTIFY_CODE_HANDLER( TVN_ITEMEXPANDED, OnItemExpanded )
    NOTIFY_CODE_HANDLER( TVN_ENDLABELEDIT, OnEndLabelEdit )
    NOTIFY_CODE_HANDLER( NM_RCLICK, OnRClick )
    //
    //********************
ALT_MSG_MAP(1)
END_MSG_MAP()

Функции-обработчики событий мы описали вручную, наверное потому, что нам так больше понравилось (а может потому, что программисты из Microsoft забыли сделать поддержку для сообщений TVN_*, NM_* и WM_NOTIFY, ограничившись поддержкой только стандартных WM_* сообщений). Вот код этих обработчиков:

/* Обработчики событий мы поместили внутри описания класса в файле  “atl01ctrl.h” */
LRESULT OnItemExpanding(int idCtrl, LPNMHDR pnmh, BOOL& bHandled)
{
   // Обработчик события Expanding (раскрытие ветки)
   LPNMTREEVIEW  nmTreeView = (NMTREEVIEW *) pnmh; 
/* Если установлены флаги TVE_EXPAND (попытка раскрыть ветку) и 
TVE_TOGGLE (ветка была закрыта), то раскрыть ветку, получив информацию о ее
дочерних ветках из базы данных */
   if (nmTreeView->action & TVE_EXPAND && nmTreeView->action & TVE_TOGGLE)
        ExpandFromDB(nmTreeView->itemNew.hItem, nmTreeView->itemNew.lParam);
   return 0;
}
LRESULT OnItemExpanded(int idCtrl, LPNMHDR pnmh, BOOL& bHandled)
{
   // Обработчик события Expanded (закрытие ветки)
   LPNMTREEVIEW  nmTreeView = (NMTREEVIEW *) pnmh;
/* Если установлен флаг TVE_COLLAPSE (попытка закрыть ветку) и не уста-
новлен флаг TVE_COLLAPSERESET (блокировка рекурсивного вхождения), то 
закрыть ветку с удалением ее подветок (TVE_COLLAPSERESET)*/
   if (nmTreeView->action & TVE_COLLAPSE &&
        !(nmTreeView->action & TVE_COLLAPSERESET) )
   {
        TreeView_Expand(m_ctlSysTreeView32.m_hWnd,
            nmTreeView->itemNew.hItem, TVE_COLLAPSERESET | TVE_COLLAPSE
        );
    }
    return 0;
}
LRESULT OnEndLabelEdit(int idCtrl, LPNMHDR pnmh, BOOL& bHandled)
{
/* Обработчик события EndLabelEdit (завершение редактирования текста) */
    NMTVDISPINFO * nmTVDispInfo = (NMTVDISPINFO *) pnmh;
    // Если pszText NULL, значит редактирование было отменено
     if (!nmTVDispInfo->item.pszText) 
        return FALSAE; 
    long iID_RT = nmTVDispInfo->item.lParam;
/* В случае успешной записи данных в БД функция UpdateRecordDB возвратит - 
TRUE что, приведет к принятию изменения текста редактируемой ветки */
    return UpdateRecordDB(iID_RT, nmTVDispInfo->item.pszText);
}
LRESULT OnRClick(int idCtrl, LPNMHDR pnmh, BOOL& bHandled)
{
/* Обработчик события RClick на щелчок правой кнопкой мыши. */
    POINT p;
    GetCursorPos(&p);
    // Получить ветку, на которой щелкнули правой кнопкой мыши
    HTREEITEM hParent = GetTVItemByXY(p);
    // Если щелкнули не на ветке – выйти из функции
    if (hParent)
        // Установить выделение на ветке
        TreeView_SelectItem(m_ctlSysTreeView32.m_hWnd, hParent);
    else 
        return 0;
/* Вызвать всплывающее меню в координатах курсора. Сочетание флагов 
TPM_RIGHTBUTTON | TPM_RETURNCMD приводит к тому что управление не 
возвращается до тех пор пока не будет выбран пункт меню или произойдет 
отказ от меню */
   int id = TrackPopupMenu(g_hPopupMenu1, TPM_LEFTALIGN |
        TPM_RIGHTBUTTON | TPM_RETURNCMD,
        p.x, p.y, 0, m_ctlSysTreeView32.m_hWnd, NULL);
/* id содержит значение ID выбранного пункта меню или 0 если произошел 
отказ. Значения ID для элементов меню было задано при создании и 
добавлении его элементов в функции DllMain (третий параметр функции 
AppendMenu())*/
    if (id == 1)
    {
/* Выбран элемент “Add” – добавить ветку (Предварительно надо добавить 
новую запись в базу данных). */
        long iNewID_RT = AddNodeDB (hParent);
        if (iNewID_RT > 0)
        {
/* Если запись добавлена успешно, и родительская ветка уже раскрыта, то 
просто добавить в дерево ветку с именем по умолчанию */
            if (GetTVItemExpanded (hParent))
            {
                TCHAR sBuffer [255];
                GetDefaultName(iNewID_RT, sBuffer);
                AddNode(hParent, iNewID_RT, sBuffer, FALSE, 0);
            }
            else
            {
/* Если родительская ветка закрыта или еще не имеет плюса, то установить 
плюс и раскрыть ее. */
                SetTVItemHasChildren(hParent); // (+)
                TreeView_Expand(m_ctlSysTreeView32.m_hWnd,
                    hParent, TVE_EXPAND);
            }
        }
        else 
            return 0; // Не удалось добавить запись в DB
/* Найти новую ветку среди других дочерних веток */
        HTREEITEM hNewItem = FindChildTreeItemByData(hParent, iNewID_RT);
        // Если новая ветка найдена – выделить ее 
        if (hNewItem)
            TreeView_SelectItem(m_ctlSysTreeView32.m_hWnd, hNewItem);
    }
    else if (id == 2) // Выбран пункт меню “Delete” (удалить)
        DeleteCurNode();
    return 0;
}

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

/* Раскрывает ветку, предварительно заполняя ее дочерними 

ветками считанными из БД */ 
void Catl01Ctrl::ExpandFromDB(HTREEITEM hItem, long iID_RT)
{
    // Если ветка уже раскрыта, то не раскрывать
    if (GetTVItemExpanded(hItem)) 
        return;
    char sSQL[256];
    // Заполнить строку SQL-запроса
    wsprintf(sSQL, "SELECT ID_RT, Name, (SELECT count(*) FROM RT as SubRT "
        "WHERE SubRT.Parent = RT.ID_RT) as ChildrenCount "
        "FROM RT WHERE Parent = %d and ID_RT <> Parent ORDER BY Name", iID_RT);
    // Выполнить SQL-запрос
    RETCODE rcdSQLExecDirect = SQLExecDirect(hstmt, 
        (UCHAR FAR *)sSQL, strlen(sSQL));
    if (rcdSQLExecDirect == SQL_SUCCESS)
    {
/* Если нет SQL-ошибки, то перебрать все записи в ResaltSet’e и для каждой 
создать ветку, являющуюся дочерней к раскрываемой (hItem) */
        long iID_RT, iChildrenCount, cbName, cbID_RT, cbChildrenCount;
        bool bHasChildren;
        LPTSTR sText;
        char sName [255];
/* Начать бесконечный цикл (выход по Break после достижения конца 
ResultSet’a. Пока ResultSet не закончится, SQLFetch будет возвращать 
SQL_SUCCESS или SQL_SUCCESS_WITH_INFO */
        while (TRUE)
        {
            // Перейти за следующую запись
            RETCODE retcode = SQLFetch(hstmt);
            if(retcode == SQL_SUCCESS || retcode == SQL_SUCCESS_WITH_INFO)
            {
                // Получить ID ветки (ID_RT)
                SQLGetData(hstmt, 1, SQL_C_SLONG, &iID_RT, 0, &cbID_RT);
                // Обнулить sName
                sName[0] = ‘\0’;
                // Получить название ветки
                SQLGetData(hstmt, 2, SQL_C_CHAR, sName, 255, &cbName);
                // Получить наличие детей (если 0 – FALSE, иначе TRUE)
                SQLGetData(hstmt, 3, SQL_C_SLONG, &iChildrenCount, 0,
                    &cbChildrenCount);
                if(lstrlen(sName) > 0)
                    sText = sName;
                else sText = "Не заполнено";
                bHasChildren =  iChildrenCount > 0 ? TRUE : FALSE;
                AddNode(hItem, iID_RT, sText, bHasChildren);
            }
            else break;
        }
        // Закрыть курсор
        SQLCancel(hstmt);
        return;
    }
    else
    {
        MessageBox(0, "Ошибка при попытке чтения из базы данных",
            "SQL-Error", MB_OK || MB_APPLMODAL || MB_ICONERROR);
    }
    return;
}

long Catl01Ctrl::AddNodeDB(HTREEITEM hParent)
{
/* Добавляет новую запись о ветке в базу данных, но не в дерево (TreeView). 
Возвращает новый ID_RT. Имя по умолчанию определено в 
#define sDefName _T("Новая запись № %d") – см. функцию GetDefaultName */
    // Получить ID_RT для ветки hParent
    long iParent = GetTVItemData(hParent);
    if (iParent <= 0) 
        return –1;
    long iNewID_RT = -1, cbNewID_RT;
    char sSQL[256];
/* Получить новое значение поля ID_RT. Здесь аналогично тесту, выполненному 
нами на Sybase Power++, используется отдельная таблица Counters с 
единственным полем, в котором при каждом добавлении ветки увеличивается 
счетчик записей (их ID_RT). Можно использовать любой другой алгоритм, 
например автоинкремент для поля ID_RT в таблице RT. Более подробные описания 
таблиц см. в “Технологии клиент-сервер 98’3” */
???? Transaction ????
    wsprintf(sSQL,"update Counters set CounterValue = CounterValue + 1");
    RETCODE rcdSQLExecDirect = SQLExecDirect(hstmt,
        (UCHAR FAR *)sSQL, strlen(sSQL));
    if (rcdSQLExecDirect != SQL_SUCCESS) 
        return -1;
    wsprintf(sSQL,"select CounterValue from Counters where ID_Counter = 1");
    rcdSQLExecDirect = SQLExecDirect(hstmt, (UCHAR FAR *)sSQL, strlen(sSQL));
    if (rcdSQLExecDirect == SQL_SUCCESS)
    {
        RETCODE retcode = SQLFetch(hstmt);
        if (SQL_SUCCESS == retcode || SQL_SUCCESS_WITH_INFO == retcode)
        {
             // Получить новое значение в переменную iNewID_RT
             SQLGetData(hstmt, 1, SQL_C_SLONG, &iNewID_RT, 0, &cbNewID_RT);
        }
        else 
             return -1;
        SQLCancel(hstmt);
    }
    else 
        return -1;
// Создать имя по умолчанию
    TCHAR sName [255];
    GetDefaultName(iNewID_RT, sName);
    // Добавить запись о новой ветке в базу данных
    wsprintf(sSQL, "insert into RT (Parent, ID_RT, Name, ImageNum) \n"
                  "   values (%d, %d, ‘%s’, 0)", iParent, iNewID_RT, sName);
    rcdSQLExecDirect = SQLExecDirect(hstmt,
        (UCHAR FAR *)sSQL, strlen(sSQL));
    if (rcdSQLExecDirect != SQL_SUCCESS) 
        return -1;
/* Если на всем этом длинном пути не произошло ошибки, то вернуть новый ID 
ветки */
   return iNewID_RT;
}
void Catl01Ctrl::DeleteCurNode()
{
/* Удаляет текущую (выделенную) ветку дерева, в случае успешного удаления 
соответствующей ей записи из базы данных */
    // Получить выделенную ветку;
    HTREEITEM hCurTreeItem = TreeView_GetSelection(m_ctlSysTreeView32.m_hWnd);
    If (!hCurTreeItem) 
        return;
    // Получить ID ветки (ее ID_RT)
    long iID_RT = GetTVItemData(hCurTreeItem);
    if (iID_RT < 0) 
        return;
    // Удалить ветку из БД и если все нормально, то удалить ее из дерева
    if (DeleteCurNodeDB(iID_RT))
        TreeView_DeleteItem(m_ctlSysTreeView32.m_hWnd, hCurTreeItem);
}
long Catl01Ctrl::GetTVItemData(HTREEITEM hTreeItem)
{
    // Возвращает 32-х битное число ассоциированное с hTreeItem
    // в ней хранится ID_RT ветки
    if (!hTreeItem) 

        return –1;
    TVITEM tvItem;
    tvItem.mask = TVIF_PARAM;
    tvItem.hItem = hTreeItem;
    tvItem.lParam = -1;
    TreeView_GetItem(m_ctlSysTreeView32.m_hWnd, (LPTVITEM) &tvItem);
    return tvItem.lParam;
}
HTREEITEM Catl01Ctrl::GetTVItemByXY(POINT xyPoint)
{
    // Возвращает ветку по экранным координатам (абсолютным)
    HTREEITEM hResult = NULL;
    TVHITTESTINFO tvHitTestInfo;
    ::hTreeItemScreenToClient(m_ctlSysTreeView32.m_hWnd, &xyPoint);
    tvHitTestInfo.pt = xyPoint;
    hResult = TreeView_HitTest(m_ctlSysTreeView32.m_hWnd, &tvHitTestInfo);
    return hResult;
}
//Здесь определена заготовка имени по умолчанию для добавляемых веток 
#define sDefName _T("Новый элемент № %d")
LPTSTR Catl01Ctrl::GetDefaultName(long ID_RT, TCHAR sDefaultName [255])
{   // Формирует имя ветки по умолчанию исходя из ее номера
    // sDefaultName является указателем на буфер
    wsprintf(sDefaultName, sDefName, ID_RT);
    return sDefaultName;
}
bool Catl01Ctrl::GetTVItemExpanded(HTREEITEM hTreeItem)
{
    // Узнает, раскрыта ли ветка hTreeItem, и возвращает TRUE, если раскрыта
    if (!hTreeItem) 
        return FALSE;
    TVITEM tvItem;
    tvItem.hItem = hTreeItem;
    tvItem.mask = TVIF_STATE;
    tvItem.stateMask = TVIS_EXPANDED;
    tvItem.state = 0;
    TreeView_GetItem(m_ctlSysTreeView32.m_hWnd, (LPTVITEM) &tvItem);
    return tvItem.state & TVIS_EXPANDED;
}
void Catl01Ctrl::SetTVItemHasChildren(HTREEITEM hTreeItem)
{
    // Устанавливает плюс перед веткой
    if (!hTreeItem) 
         return;
    TVITEM tvItem;
    tvItem.hItem = hTreeItem;
    tvItem.mask = TVIF_CHILDREN;
    tvItem.cChildren = 1;
    TreeView_SetItem(m_ctlSysTreeView32.m_hWnd, (LPTVITEM) &tvItem);
    return;
}
HTREEITEM Catl01Ctrl::FindChildTreeItemByData(HTREEITEM hParent, long iData)
{
/* Ищет дочернюю ветку с известным значением Data (lParam) для указанной 
родительской ветки. */
    if (!hParent || iData < 0) 
         return NULL;
    HTREEITEM hTreeItemResult = 0;
    // Получить первую дочернюю ветку
    HTREEITEM hTreeItem = 
        TreeView_GetChild(m_ctlSysTreeView32.m_hWnd, hParent);
    while (hTreeItem)
    {
        if (GetTVItemData(hTreeItem) == iData)
        { 
            // Ветка с искомым ID_RT найдена
            hTreeItemResult = hTreeItem;
            break;
        }
        //Получить следующую дочернюю ветку
        else
            hTreeItem = TreeView_GetNextSibling(m_ctlSysTreeView32.m_hWnd, 
                                             hTreeItem);
    }
    return hTreeItemResult;
}
bool Catl01Ctrl::DeleteCurNodeDB(long iID_RT)
{
    // Удаляет запись о ветке с номером iID_RT из базы данных
    if (iID_RT < 0)
        return FALSE;
    char sSQL[256];
    wsprintf(sSQL, "delete from RT where ID_RT = %d", iID_RT);
    RETCODE rcdSQLExecDirect = SQLExecDirect(hstmt,
        (UCHAR FAR *)sSQL, strlen(sSQL));
    // Вернуть TRUE, если удаление прошло успешно.
    return (rcdSQLExecDirect == SQL_SUCCESS);
}
bool Catl01Ctrl::UpdateRecordDB(long iID_RT, LPCTSTR sName)
{
    // Обновляет запись с номером iID_RT (меняет текст ветки)
    if (iID_RT < 0)
        return FALSE;
    char sSQL[256];
    wsprintf(sSQL, "update RT set Name = ‘%s’ where ID_RT = %d",
        sName, iID_RT);
    RETCODE rcdSQLExecDirect = SQLExecDirect(hstmt,
        (UCHAR FAR *)sSQL, strlen(sSQL));
    return (rcdSQLExecDirect == SQL_SUCCESS);
}

После описания всех этих функций осталось только создать у нашего ActiveX’a свойство CurID_RT, и компонент будет работать так, как требуется. Для создания свойства проще всего щелкнуть правой кнопкой мыши на ветке Iatl01Ctrl в Class View, и во всплывшем меню выбрать “Add Property”. В появившемся Wizard’e “Add Property to Interface” мы установили Property Type в long и поле Property Name в CurID_RT (остальные настройки оставили неизменными). При нажатии на OK создались заглушки для функций get_CurID_RT(long *pVal) и put_CurID_RT(long newVal). Параметры pVal и newVal были созданы по умолчанию, чтобы изменить их названия соответственно на pID_RT и ID_RT пришлось руками перебить описания этих функций в файлах atl02Test.idl, atl01Ctrl.cpp и atl01Ctrl.h (здесь проявляется явное отставание от Inprise продукты которого оснащены визуальным редактором автоматически обновляющем все файлы).

Вообще, изменение названий обычных функций, а особенно методов и свойств ActiveX компонентов, выполнять неудобно – приходится все это делать ручками, причем изменение описания в cpp-файле приводит к невозможности перейти на объявление в header-файле через соответствующий пункт всплывающего меню. Будем надеяться, что раз уж сделали в Visual C++ удобную навигацию по проекту, так не грех предусмотреть в следующих версиях продукта и удобную возможность редактирования названий и описаний функций.

А пока мы вручную изменили во всех трех местах названия параметров для функций свойства CurID_RT и описали реализацию этого свойства следующим образом:

STDMETHODIMP Catl01Ctrl::get_CurID_RT(long *pID_RT)
{
/* Получает ID_RT выделенной ветки. Если нет выделенной ветки, вернуть 
E_FAIL, иначе вернуть S_OK и ID ветки (ее ID_RT) */
    *pID_RT = -1;
    HTREEITEM hSelItem = TreeView_GetSelection(m_ctlSysTreeView32.m_hWnd);
    if (hSelItem == NULL) 
         return E_FAIL;
    *pID_RT = GetTVItemData(hSelItem);
    return S_OK;
}

STDMETHODIMP Catl01Ctrl::put_CurID_RT(long ID_RT)
{
   // Устанавливает выделение на ветку с указанным ID_RT
   if (ID_RT < 0) return E_FAIL;
   char sSQL[256];
/* Выполнить SQL-запрос с вызовом Stored-процедуры GetALLParent, которая 
должна к этому времени существовать на сервере. В процедуру передается 
ID_RT, для которого она возвращает список веток-предков от 0 (Root) до 
родительской ветки в порядке вложенности. Текст Stored-процедуры 
GetALLParent представлен за описанием функции put_CurID_RT*/
   wsprintf(sSQL, "exec GetALLParent %d", ID_RT);
   RETCODE rcdSQLExecDirect = SQLExecDirect(hstmt2,
        (UCHAR FAR *)sSQL, strlen(sSQL));
   if (rcdSQLExecDirect == SQL_SUCCESS)
   {
        HTREEITEM hItem = 0;
        long iID_RT, cbID_RT;
/* В этом цикле последовательно раскрываются ветки-предки от корня до 
искомой ветки. Поскольку в процессе выполнения цикла hstmt будет 
использоваться в обработке onExpand, мы вынуждены завести hstmt2 для 
списка веток-предков */
        while (TRUE)
        {
RETCODE retcode = SQLFetch(hstmt2);
            if (SQL_SUCCESS == retcode || SQL_SUCCESS_WITH_INFO == retcode)
            {
                // Получить ID_RT ветки из текущей записи
                SQLGetData(hstmt2, 1, SQL_C_SLONG, &iID_RT, 0, &cbID_RT);
/* Если 0, то установить в hItem указатель на корневую ветку, иначе найти 
ветку по ее RT c hItem в качестве ветки-предка */
                hItem = (0 == iID_RT) 
                    ? TreeView_GetRoot(m_ctlSysTreeView32.m_hWnd)
                    : FindChildTreeItemByData(hItem, iID_RT);
/* если у ветки установлен флаг TVIS_EXPANDEDONCE, то ее раскрытие не 
вызывает события Expanding, поэтому снимаем этот флаг */
                TVITEM tvItem;
                tvItem.hItem = hItem;
                tvItem.mask = TVIF_STATE;
                tvItem.stateMask = TVIS_EXPANDEDONCE;
                tvItem.state = 0; 
                TreeView_SetItem(m_ctlSysTreeView32.m_hWnd, &tvItem);
                // Раскрыть ветку
                TreeView_Expand(m_ctlSysTreeView32.m_hWnd, 
                    hItem, TVE_EXPAND);
            }
            else 
                break;
        }
        // Закрыть курсор
        SQLCancel(hstmt2);
        // Найти ветку
        hItem = FindChildTreeItemByData(hItem, ID_RT);
        // Выделить ветку, если она найдена
        if (hItem)
            TreeView_SelectItem(m_ctlSysTreeView32.m_hWnd, hItem);
    }
    return S_OK;
}

Как вы заметили, в функции put_CurID_RT использовался вызов SQL Stored-процедуры “GetALLParent”. Текст этой процедуры приводится ниже:

/*Создать процедуру GetALLParent с параметром @ID типа Integer */
create proc GetALLParent
     @ID int
as
/* Создать временную таблицу #tmp_RT с двумя полями: ID_RT типа Integer 
для хранения ID_RT веток-предков, и pos типа Integer для хранения глубины 
вложенности ветки-предка в древесном списке*/
   create table #tmp_RT(ID_RT int, pos int)
   SET NOCOUNT ON
/* Объявить и инициализировать переменные @ID_RT, @Parent, @count и 
@Fetched*/
   declare @ID_RT int, @Parent int, @count int, @Fetched int
   select @Parent = @ID, @count = 0, @ID_RT = -1, @Fetched = 1
   /* Повторяем цикл пока не не достигнем корневой ветки */
   while @ID_RT <> @Parent AND @Fetched >= 0
   begin
        select @Fetched = -1
/* Получить Parent для текущей ветки и увеличить @count. (Переменная 
@Fetched изменяется только при условии что в выборке есть хотя бы одна 
запись.) */
        select @ID_RT = ID_RT, @Fetched = ID_RT, 
            @Parent = Parent, @count = @count + 1
            from  RT
            where ID_RT = @Parent
/* Вставить запись c полученными ID_RT и счетчиком вложенности во временную таблицу #tmp_RT */
        if(@ID_RT <> @ID)
            insert into #tmp_RT( ID_RT, pos ) values( @ID_RT, @count )
   end
    /* После выхода из цикла получить из временной таблицы список ID_RT для всех  полученных веток-предков, отсортированную в обратном порядке по счетчику вложенности*/
   select #tmp_RT.ID_RT
        from #tmp_RT, RT
where #tmp_RT.ID_RT = RT.ID_RT

        order by pos DESC
/* Уничтожить временную таблицу #tmp_RT хотя это необязательно (временные 
таблицы автоматически уничтожаются при выходе из stored-процедур)*/
   drop table #tmp_RT
   SET NOCOUNT OFF
go 

Ну вот и все с ActiveX’ом. Осталось только создать тестовое приложение (лучше всего сделать это в Visual Basic’e, поставляемом в комплекте Visual Studio – для того, чтобы сохранить Microsoft-чистоту эксперимента). Мы полностью повторили внешний вид тестовых приложений, уже не раз созданных при работе с продуктами Inprise и Sybase – наш ActiveX, поле редактирования и две кнопки: первая GetCurID_RT, а вторая, само собой, SetCurID_RT. По нажатию на GetCurID_RT в поле редактирования записывается полученный номер текущей записи, а по нажатию на SetCurID_RT в ActiveX’e выделяется ветка с номером из поля редактирования. Сверхсложный код обработок нажатия на эти две кнопки приведен ниже (здесь atl01Ctrl1 – название нашего ActiveX, а dfCurID_RT – название поля редактирования):

Private Sub pbGetCurID_RT_Click()
     dfCurID_RT.Text = atl01Ctrl1.CurID_RT
End Sub

Private Sub pbSetCurID_RT_Click()
    atl01Ctrl1.CurID_RT = dfCurID_RT.Text
End Sub

После запуска Visual Basic-приложения (VB) можно понаблюдать за работой компонента (рисунок 6), и заодно посмотреть объемы занимаемой им памяти.

msDCOM_Client.GIF (4664 bytes)

Поскольку вся функциональность находится в ActiveX’e, будем учитывать только его размер (ведь, к тому же, мы рассматриваем Visual C++, а не Visual Basic). Наш ActiveX, скомпилированный в режиме MinDependеncy, занял на жестком диске 61 Кб (он находится в …\ReleaseMinDependency\atl02Test.dll ). Никаких дополнительных библиотек при этом режиме компиляции не требуется (естественно исключая библиотеки доступа к БД, в DCOM тесте мы избавимся и от них), поэтому можно считать это значение окончательным – понятно, что на действительно больших объемах кода разница с аналогичными компонентами, написанными, скажем, на Delphi (там такой ActiveX занял 600 Кб без учета BDE и библиотеки доступа к БД (DB-LIB)) будет не столь убийственна, но не отметить ее нельзя.

К нашему величайшему сожалению, когда мы встроили ActiveX, сделанный на Delphi, в тестовый проект, сделанный на VB, среда VB вылетела при закрытии VB-приложения с сообщением об ошибке 216, повторяющемся в бесконечном цикле. Причем, если запускать полученный Exe не из-под отладки Basic’a, а сам по себе, такой беды не происходит. Честно говоря, разбираться в том, “кто виноват и что делать” не входило в наши планы, поэтому мы создали одинаковые тесты для ActiveX’ов, сконструированных на Visual C++ и Delphi на Delphi4 (об ошибке 216 мы уже говорили в обзоре по Delphi4). Итак: созданный на Delphi4 ActiveX, встроенный в тестовый проект, созданный на той же Delphi, занял 3 Мб в оперативной памяти. Полностью такой же тестовый проект с ActiveX’ом, созданным на Visual C++, занял 1.1 Мб. При этом возник небольшой сбой в Delphi design-time: при изменении размера ActiveX’a его размер менялся как-то странно (на месте оставалась рамка старого размера) – однако расхождение в понимании методов изменения размеров компонентов (alignment, resize или как вам будет угодно) между Visual C++ и Delphi трудно отнести к недостаткам какого-либо одного из этих двух продуктов (Заметим только что в VB размеры этого компонента изменялись правильно).

Многоуровневое приложение

Прежде, чем делать какие-то общие выводы, мы создали DCOM тест для трех уровневой схемы клиент-сервер. Мы не стали изобретать DBGrid (это совсем не так уж просто!) и использовали в качестве DCOM-клиента тестовое приложение, созданное нами ранее для ActiveX’a “atl01Ctrl” (оно хранится в библиотеке “atl02Test”). Само собой разумеется, что некоторые функции этого ActiveX’a пришлось переписать, поскольку всю работу с базой данных мы вынесли на серверный объект, который в связи с этим получил методы AddNode, DeleteNode, UpdateNode, GetSubNodes и GetAllParentes.

Как мы это сделали: сначала мы создали новый WorkSpace и назвали его “Test_1_ALL”, затем скопировали в образовавшийся каталог проект с ActiveX’ом (“atl02Test”) и добавили его в WorkSpace Test_1_ALL — это для DCOM-клиента. После этого мы создали в этом же WorkSpace новый проект с названием “msDCOM_1_Test”, выбрав при создании проекта на закладке “Projects” Wizard c названием “ATL COM App Wizard”. После запуска Wizard’a уcтановили “Server Type” в Service (EXE) и, нажав OK, получили проект для DCOM-сервера. Затем с помощью Wizard’a New ATL Object добавили в проект объект Simple Object из категории Objects, дав ему имя “msDCOM_1_srv” и установив атрибуты так же, как мы делали это при создании ActiveX-теста.

Добавив в header-файл stdafx.h строку “#include <SQLEXT.h>” для работы с SQL-сервером через ODBC, мы описали в файле msDCOM_1_Test.cpp следующие переменные:

    BOOL bOK = FALSE;
    HDBC hdbc = NULL;
    HENV henv = NULL;
    HSTMT hstmt = NULL;

После чего продекларировали их в файле msDCOM_1_srv.cpp, (кроме bOK) поскольку они будут там использоваться:

    extern HDBC hdbc;
    extern HENV henv;
    extern HSTMT hstmt;

Затем, в функции _tWinMain перед строкой “_Module.Start();” добавили свой код инициализации работы с базой данных и, в файл msDCOM_1_srv.h, написали свою inline-функцию SQL_OK (для упрощения проверок возвращаемых значений ODBC функций:

//в файл msDCOM_1_srv.h
inline BOOL SQL_OK(SQLRETURN SqlResult)
    { return SQL_SUCCESS == SqlResult 
    || SQL_SUCCESS_WITH_INFO == SqlResult;}

//в файл msDCOM_1_Test.cpp
extern “C” int WINAPI _tWinMain(HINSTANCE hInstance,
    HINSTANCE /*hPrevInstance*/, LPTSTR lpCmdLine, int /*nShowCmd*/)
{
    lpCmdLine = GetCommandLine();
    _Module.Init(ObjectMap, hInstance, IDS_SERVICENAME,
        &LIBID_MSDCOM_1_TESTLib);
    _Module.m_bService = TRUE;
    TCHAR szTokens[] = _T(“-/”);
    LPCTSTR lpszToken = FindOneOf(lpCmdLine, szTokens);
    while (lpszToken != NULL)
    {
        if (lstrcmpi(lpszToken, _T(“UnregServer”))==0)
            return _Module.UnregisterServer();

        // Register as Local Server
        if (lstrcmpi(lpszToken, _T(“RegServer”))==0)
            return _Module.RegisterServer(TRUE, FALSE);
        // Register as Service
        if (lstrcmpi(lpszToken, _T(“Service”))==0)
            return _Module.RegisterServer(TRUE, TRUE);
        lpszToken = FindOneOf(lpszToken, szTokens);
}
    // Are we Service or Local Server
    CRegKey keyAppID;
    LONG lRes = keyAppID.Open(HKEY_CLASSES_ROOT, _T(“AppID”), KEY_READ);
    if (lRes != ERROR_SUCCESS)
        return lRes;
    CRegKey key;
    lRes = key.Open(keyAppID,
        _T(“{E604058A-4C73-11D2-84E9-004095F005AC}”), KEY_READ);
    if (lRes != ERROR_SUCCESS)
        return lRes;
    TCHAR szValue[_MAX_PATH];
    DWORD dwLen = _MAX_PATH;
    lRes = key.QueryValue(szValue, _T(“LocalService”), &dwLen);
    _Module.m_bService = FALSE;
    if (lRes == ERROR_SUCCESS)
        _Module.m_bService = TRUE;
    // *************************************************
    //
/* Ниже добавлен наш код инициализации базы данных. В принципе, этот код 
мало отличается от аналогичной инициализации, которую мы делали в 
ActiveX-тесте, за исключением обработки ошибок: здесь в случае выдается 
диалог с сообщением – но виден он только на сервере, а клиентского 
приложение само заботится о своем диалоге по возврату от его 
CoCreateInstanceEx(). */
    if (SQL_OK(SQLAllocEnv(&henv)))
    {
        if (SQL_OK(SQLAllocConnect(henv, &hdbc)))
        {
            if (SQL_OK(SQLConnect(hdbc, (UCHAR FAR *)”Test”, SQL_NTS,
                   (UCHAR FAR *)”sa”, SQL_NTS, (UCHAR FAR *)””, SQL_NTS))
            )
            {
                if (SQL_OK(SQLAllocStmt(hdbc, &hstmt)))
                    bOK = TRUE;
            }
        }
    }
    if (!bOK)
        ::MessageBox(NULL, 
            "Ошибка при попытки подключения к базе данных!\n"
            "Возможно:\n"
            "- отсутствует или неправильно задан\n"
            "  ODBC-Data Source с названием ‘Test’\n"
            "  (это наиболее вероятно);\n"
            "- отсутствует MS SQL-сервер;\n"
            "- отсутствуют драйвера ODBC;\n",
            "ошибка при запуске DCOM-сервера",
            MB_SERVICE_NOTIFICATION);
    //
    // *************************************************
    _Module.Start();
    // *************************************************
    //
/* Вообще-то здесь можно было бы выполнить деинициализацию работы с базой 
данных, но по нашим наблюдениям в этом нет необходимости */
    //
    // *************************************************
    // When we get here, the service has been stopped
    return _Module.m_status.dwWin32ExitCode;
}

Кроме этого, надо описать в функции Run проверку на bOK:

void CServiceModule::Run()
{
    _Module.dwThreadID = GetCurrentThreadId();
    HRESULT hr = CoInitialize(NULL);
    _ASSERTE(SUCCEEDED(hr));
    CSecurityDescriptor sd;
    sd.InitializeFromThreadToken();
    hr = CoInitializeSecurity(sd, -1, NULL, NULL,
        RPC_C_AUTHN_LEVEL_PKT, RPC_C_IMP_LEVEL_IMPERSONATE,
        NULL, EOAC_NONE, NULL);
    _ASSERTE(SUCCEEDED(hr));
    hr = _Module.RegisterClassObjects(CLSCTX_LOCAL_SERVER 
              | CLSCTX_REMOTE_SERVER, REGCLS_MULTIPLEUSE);
    _ASSERTE(SUCCEEDED(hr));
    LogEvent(_T(“Service started”));
    if (m_bService)
        SetServiceStatus(SERVICE_RUNNING);

    //*****************
    //
/* Здесь мы делаем проверку bOK, чтобы не запускать сервер при ошибке 
доступа к базе данных */
extern BOOL bOK;
    if( bOK )
    {
    //
    //*****************
        MSG msg;
        while (GetMessage(&msg, 0, 0, 0))
            DispatchMessage(&msg);
    } //*****************

    _Module.RevokeClassObjects();

    CoUninitialize();
}

Для того, чтобы DCOM-сервер все-таки работал, а не просто радовал нас своим существованием, мы добавили в интерфейс ImsDCOM_1_srv методы AddNode, DeleteNode, UpdateNode, GetSubNodes и GetAllParentes (проще всего для этого воспользоваться Wizard’ом “Add Metod” из вплывающего меню на ветке ImsDCOM_1_srv в дереве ClassView – рис. 7.):

msAddMetod.GIF (8871 bytes)

// в файл msDCOM_1_Test.idl
interface ImsDCOM_1_srv : IDispatch
{
    [id(1), helpstring(“возвр. дочерние ветки для Parent”)]
        HRESULT GetSubNodes(
        [in] long Parent,
/* Параметр count возвращает количество записей в передаваемых в трех 
следующих за ним массивах. (Обратите внимание на использование его в 
атрибуте “size_is”) */
        [out] long *count, 
/* Для того чтобы передать массив через параметр удаленной функции 
необходимо воспользоваться атрибутом “size_is”. Описанные параметров 
приведенное ниже позволяют выделя память на вызываемой стороне. 
Это необходимо так как мы заранее не знаем сколько записей нам необходимо 
получить с сервера, а узнавать это можно только сделав отдельный вызов что 
естественно ухудшит производительность всей системы. */
// Массив уникальных номеров записей
        [out, size_is(,*count)] long ** pArrID,
// Массив текстовых назватий записей
        [out, size_is(,*count)] BSTR ** pArrName,
/* Массив переменных указывающих количество дочерних веток для каждой 
возвращаемой ветки */
        [out, size_is(,*count)] long ** pArrChildrenCount);
    [id(2), helpstring("изменяет текстовое название для ID_RT")]
        HRESULT UpdateNode(
        [in] long ID_RT,
        [in] LPCSTR Name);
    [id(3), helpstring("удаляет ветку заданную через ID_RT")]
        HRESULT DeleteNode(
        [in] long ID_RT);
    [id(4), helpstring("добавляет ветку")]
        HRESULT AddNode(
        [in] long Parent, [out] long * pID_RT, [in] LPCSTR Name);
    [id(5), helpstring("возвращает предков для ветки заданной через ID_RT")]
        HRESULT GetAllParentes(
        [in] long ID_RT,
        [out] long *count,
        [out, size_is(,*count)] long ** pArrParent);
};

// В файл msDCOM_1_srv.h
STDMETHOD(GetSubNodes)(long Parent, long *count, long ** pArrID, 
    BSTR ** pArrName,long ** pArrChildrenCount);
STDMETHOD(UpdateNode)(long ID_RT, LPCSTR Name);
STDMETHOD(DeleteNode)(long ID_RT);
STDMETHOD(AddNode)(long Parent, long * pID_RT, LPCSTR Name);
STDMETHOD(GetAllParentes)(long ID_RT, long *count, long ** pArrParent);

// В файл msDCOM_1_srv.cpp
STDMETHODIMP CmsDCOM_1_srv::GetSubNodes(long ID_RT, long * count, 
                        long ** pArrID,BSTR ** pArrName,
                        long ** pArrChildrenCount)
{
    if( pArrID && count && pArrName && pArrChildrenCount && ID_RT >= 0 )
    {
/* Класс MakeSubNodesDynArrays описан ниже. В нем производится вся работа 
по заполнению массивов */
        MakeSubNodesDynArrays mda(ID_RT);
/* В случае сбоя при работе с базой данных возвращается 
mda.m_count = -1. При успешном считывании списка потомков из базы 
mda.m_count > 0. Если ошибок нет, но у ветки ID_RT нет потомков то 
mda.m_count = 0 */
        *count = mda.m_count;
        if (mda.m_count > 0)
        {
            *count = mda.m_count;
            *pArrID = mda.m_ArrID;
            *pArrName = mda.m_ArrName;
            *pArrChildrenCount = mda.m_ArrChildrenCount;
        }
        else if (mda.m_count < 0)
        {
            return E_FAIL;
/* здесь следовало бы вернуть свой код ошибки */
        }
        return S_OK;
    }
    else
        return E_INVALIDARG;
}
STDMETHODIMP CmsDCOM_1_srv::UpdateNode(long ID_RT, LPCSTR Name)
{
/* По полученным номеру ветки (ID_RT) и новому тексту (Name) формирует 
SQL-запрос обновляющий соответствующую запись в базе данных. Если не 
произошло ошибки, возвращает S_OK, иначе E_FAIL. Ошибка может происходить 
из-за ограничений в базе данных или по др. причинам – поэтому хорошо бы 
возвращать информативное сообщение об ошибке, но мы не стали реализовывать 
это в данном тесте*/
    char sSQL[256];
    wsprintf(sSQL, “update RT set Name = ‘%s’ where ID_RT = %d”,

            Name, ID_RT);
    RETCODE rcdSQLExecDirect = SQLExecDirect(hstmt, (UCHAR FAR *)sSQL,
                                         strlen(sSQL));
    if (rcdSQLExecDirect == SQL_SUCCESS) 
        return S_OK;
    else 
        return E_FAIL;
}
STDMETHODIMP CmsDCOM_1_srv::DeleteNode(long ID_RT)
{
    /* Удаляет соответствующую запись из базы данных (c переданным ID_RT) */
    char sSQL[256];
    wsprintf(sSQL, “delete from RT where ID_RT = %d”, ID_RT);
    RETCODE rcdSQLExecDirect = SQLExecDirect(hstmt, (UCHAR FAR *)sSQL,
                                         strlen(sSQL));
    if (rcdSQLExecDirect == SQL_SUCCESS)
        return S_OK;
    else
        return E_FAIL;
}
STDMETHODIMP CmsDCOM_1_srv::AddNode(long Parent, long *pID_RT, LPCSTR Name)
{
/* добавляет новую запись (дочернюю ветку) в базу данных. ID ветки-родителя
передается через переменную Parent, шаблон имени, используемый по умолчанию,
передается через параметр Name. Функция передает номер новой ветки в 
*pID_RT и возвращает S_OK, если все прошло успешно, или –1 в *pID_RT и 
E_FAIL, если произошла ошибка. При неправильно переданных параметрах 
возвращает E_INVALIDARG */
    long lID_RT = -1, cbNewID_RT;
    if (Parent < 0)
       return E_INVALIDARG;
    char sSQL[256];
/* Сгенерировать новое значение ID_RT в таблице Counters – этот способ мы 
уже описывали раньше */
    wsprintf(sSQL, “update Counters set CounterValue = CounterValue + 1”);
    RETCODE rcdSQLExecDirect = SQLExecDirect(hstmt, (UCHAR FAR *)sSQL,
        strlen(sSQL));
    if (rcdSQLExecDirect != SQL_SUCCESS) 
        return E_FAIL;
    /* Получить новое значение ID_RT из Counters */
    wsprintf(sSQL, “select CounterValue \n”
                  “   from Counters where ID_Counter = 1”);
    rcdSQLExecDirect = SQLExecDirect(hstmt, (UCHAR FAR *)sSQL, strlen(sSQL));
    if (rcdSQLExecDirect == SQL_SUCCESS)
    {
        RETCODE retcode = SQLFetch(hstmt);
        if (retcode == SQL_SUCCESS)
        {
            SQLGetData(hstmt, 1, SQL_C_SLONG, &lID_RT, 0, &cbNewID_RT);
        }
        else return E_FAIL;
        SQLCloseCursor (hstmt);
    }
    else return E_FAIL;
    /* Сформировать имя и вставить запись о новой ветке в базу данных */
    TCHAR sName [255];
    wsprintf(sName, Name, lID_RT);
    wsprintf(sSQL,
        “insert into RT (Parent, ID_RT, Name, ImageNum) \n”
        “   values (%d, %d, ‘%s’, 0)”, Parent, lID_RT, sName );
    rcdSQLExecDirect = SQLExecDirect(hstmt, (UCHAR FAR *)sSQL, strlen(sSQL));
    if (rcdSQLExecDirect == SQL_SUCCESS)
{
        *pID_RT = lID_RT;
        return S_OK;
    }
    else
        return E_FAIL;
}
STDMETHODIMP CmsDCOM_1_srv::GetAllParentes(long ID_RT, long *count, long 

     **pArrParent)
{
/* Возвращает список всех веток-предков для данной ветки 
(ID ветки передается через параметр ID_RT. При успешном завершении 
возвращает S_OK, указатель на массив значений типа long в pArrParent и 
количество элементов массива в count. При ошибке возвращает –1 в count и 
E_FAIL. При неверно переданных параметрах возвращает E_INVALIDARG. Алгоритм 
запаковки аналогичен методу GetSubNodes, но передаются не три массива, а 
один. */
    *count = -1;
    if( pArrParent && ID_RT >= 0 )
    {
/* Описание класса MakeParentesDynArray приведено ниже. Он работает 
аналогично классу MakeSubNodesDynArrays: отличаются лишь SQL-запрос и 
количество заполняемых массивов. */
        MakeParentesDynArray mda(ID_RT);
/* В случае сбоя при работе с базой данных возвращается mda.m_count = -1. 
При успешном считывании списка предков из базы mda.m_count > 0. Если ошибок 
нет, но у ветки ID_RT нет предков (mda.m_count = 0), то она является 
корневой (Root) */
        *count = mda.m_count;
        if (mda.m_count > 0)
        {
            *count = mda.m_count;
            *pArrParent = mda.m_ArrParent;
        }
        else if (mda.m_count < 0)
        {
            return E_FAIL;
/* здесь следовало бы вернуть код ошибки */
        }
        return S_OK;
    }
    else
        return E_INVALIDARG;
}
class MakeSubNodesDynArrays
{
/* При создании этого класса выполняется SQL-запрос возвращающий список 
дочерних веток для ветки с переданным значением ID_RT (значение ID_RT 
задается в конструкторе этого класса), затем запускается рекурсивная 
функция, по мере погружения в которую выполняется Fetch (считывание 
следующей записи) и заполнение локальных переменных значениями ID_RT, 
Name и ChildrenCount с увеличением счетчика погружений в рекурсию (т.е. 
счетчика записей) m_count. Локальные переменные для всех экземпляров 
рекурсивной функции хранятся в стеке до их завершения: пользуясь этим на 
выходе из рекурсии мы заполняем список массивы m_ArrID, m_ArrName и 
m_ArrChildrenCount значениями ID_RT, Name и ChildrenCount из этих переменных,
после чего при выходе из функции эти переменные автоматически удаляются из 
стека. Завершение погружения в рекурсию происходит, когда Fetch возвращает 
FALSE (т.е выходит за последнюю запись) – в этот момент мы выделяем память 
для массивов. Это красивое, но не очень простое решение найдено отчасти для
того, чтобы не фрагментировать память при выделении ее на каждый Fetch, 
отчасти для того, чтобы не следить за очисткой памяти (очистку стека 
обеспечивает рекурсия), а, в основном, просто для того, чтобы было не 
скучно.:-) */
public:
    long m_count;
    long * m_ArrID;
    BSTR * m_ArrName;
    long * m_ArrChildrenCount;
    long m_Parent;
/* Конструктор класса – запускает функцию заполнения массивов*/
MakeSubNodesDynArrays(long Parent)
    {
        m_count = 0;
        m_Parent = Parent;
        m_ArrID = NULL;
        m_ArrName = NULL;
        m_ArrChildrenCount = NULL;
        m_CurRow = 0;
        /* hstmt - глобальный, инициализируется при старте приложения */
        m_hstmt = hstmt;
/* Здесь последовательно выполняются три функции класса: BDExec выполняет 
SQL-запрос, FetchNextSubRow – рекурсивная функция, о которой мы уже 
говорили выше, DBClose – закрывает SQL-курсор */
        if (BDExec(Parent))
            if (FetchNextSubRow())
                if (DBClose())
                    return;
        /* Заполняется при любом сбое */
        m_count = -1;
    }
protected:
    long m_CurRow;
    HSTMT m_hstmt;
    BOOL BDExec(long Parent)
    {
/* Формирует и выполняет SQL-запрос возвращающий список дочерних 
веток. Возвращает TRUE, если все в порядке, или FALSE, если 
произошла ошибка */
        char sSQL[256];
        wsprintf(sSQL,
            "SELECT ID_RT, Name, "
            "  (SELECT count(*)  "
            "    FROM RT as SubRT "
            "    WHERE SubRT.Parent = RT.ID_RT) as ChildrenCount "
            " FROM RT "
            " WHERE Parent = %d and ID_RT <> Parent "
            " ORDER BY Name",
            Parent);
        RETCODE rcdSQLExecDirect = SQLExecDirect(hstmt, 
            (UCHAR FAR *)sSQL, strlen(sSQL));
        return SQL_OK(rcdSQLExecDirect);
    }

    long FetchNextSubRow()
    {
/* Рекурсивная функция. Если запись существует, то увеличивает счетчик 
m_count, заполняет локальные переменные lID_RT, sName и lChildrenCount 
(это делается в функции BDFetchNext) и вызывает сама себя, иначе (если 
записей больше нет) выделяет память для массивов m_ArrID, m_ArrName и 
m_ArrChildrenCount. На выходе из рекурсии заполняет массивы значениями из 
стековых переменных. В счетчике m_count находится количество записей */
        long lID_RT;
        BSTR sName;
        long lChildrenCount;
        if(BDFetchNext(&lID_RT, &sName, &lChildrenCount))
        {
            ++m_count;
            FetchNextSubRow();

            // Заполнить по одной записи в массивах
            --m_CurRow;
            m_ArrID[m_CurRow] = lID_RT;
            m_ArrName[m_CurRow] = sName;
            m_ArrChildrenCount[m_CurRow] = lChildrenCount;
        }
        else
        {
/* Если это - последняя запись, то выделить память для массивов
Здесь следовало бы обработать ошибку БД и вернуть FALSE в случае, если это 
не просто конец ResultSet’a, а действительно ошибка.*/
            m_ArrID = (long *)CoTaskMemAlloc(sizeof(long) * m_count);
/* Обратите внимание на то, что память под массивы выделяется с помощью 
функции CoTaskMemAlloc. Это необходимо так как мы собираемся передать эти 
массивы как параметры RPC функции. Кстати CoTaskMemAlloc может выделить 
ноль байт (пустой массив).*/
            m_ArrName = (BSTR *)CoTaskMemAlloc(sizeof(BSTR*) * m_count);
            m_ArrChildrenCount = (long *)CoTaskMemAlloc(sizeof(long) * 
                                                    m_count);
            m_CurRow = m_count; // Устанавливаем текущую позицию массивов
        }
        return TRUE;
    }
    BOOL BDFetchNext(long * pID_RT, BSTR * pName, long * pChildrenCount)
/* BDFetchNext производит считывание записей из ДБ. При успешном завершении
BDFetchNext возвращает TRUE и заполняет параметры иначе возвращает FALSE */
{
        RETCODE retcode = SQLFetch(hstmt);
        if (SQL_OK(retcode))
        {
            // Макрос необходимый для функций преобразования строк
            USES_CONVERSION;
            long cbID_RT = 0, cbName = 0, cbChildrenCount = 0;
            TCHAR szName [255];
            SQLGetData(hstmt, 1, SQL_C_SLONG, pID_RT, 0, &cbID_RT);
            SQLGetData(hstmt, 2, SQL_C_CHAR, szName, 255, &cbName);
            SQLGetData(hstmt, 3, SQL_C_SLONG, pChildrenCount, 

                      0, &cbChildrenCount);
/* Макрос T2W преобразует строку типа (TCAR *) в (WCHAR *) а, функция 
SysAllocString выделяет область памяти под сороку (память выделенную с 
помощью SysAllocString можно передавать в RPC процедуру). */
           *pName = SysAllocString(T2W(szName));
            return TRUE;
        }
        else
        {
            return FALSE;
        }
    }
    BOOL DBClose()
    {
        /* Конечно, SQLCancel – это не изящно...Но работает */
        return SQL_OK(SQLCancel(hstmt));
    }
};
class MakeParentesDynArray
{
/* Работает аналогично MakeSubNodesDynArrays с SQL-запросом, возвращающий 
предков для заданной записи, в массив m_ArrParent типа long */
public:
    long m_count;
    long * m_ArrParent;
    long m_ID_RT;
    MakeParentesDynArray(long ID_RT)
    {
        m_count = 0;
        m_ID_RT = ID_RT;
        m_ArrParent = NULL;
        m_CurRow = 0;
        m_hstmt = hstmt2; // hstmt2 инициализируется глобально
        if (BDExec(ID_RT))
            if (FetchNextSubRow())
                if (DBClose())
                    return;
        m_count = -1; // Устанавливается при сбое
    }
protected:
    long m_CurRow;
    HSTMT m_hstmt;
    BOOL BDExec(long m_ID_RT)
    {
        char sSQL[256];
        wsprintf(sSQL, “exec GetALLParent %d”, m_ID_RT);
        RETCODE rcdSQLExecDirect = SQLExecDirect(hstmt, (UCHAR FAR *)sSQL, 
                                              lstrlen(sSQL));
        return SQL_OK(rcdSQLExecDirect);
    }
    long FetchNextSubRow()
    {
        long lID_RT;
        if(BDFetchNext(&lID_RT))
        {
            ++m_count;
            FetchNextSubRow();
            // Заполнить записи массива
            -—m_CurRow;
            m_ArrParent [m_CurRow] = lID_RT;
        }
        else
        {
            m_ArrParent =
            ((long *)CoTaskMemAlloc(sizeof(long) * m_count);
            m_CurRow = m_count;
        }
        return TRUE;
    }

    BOOL BDFetchNext(long * pID_RT)
    {
        RETCODE retcode = SQLFetch(hstmt);
        if (SQL_OK(retcode))
        {
            long cbID_RT = 0;
            SQLGetData(hstmt, 1,
                SQL_C_SLONG, pID_RT, 0, &cbID_RT);
            return TRUE;
        }
        else
        {
            return FALSE;
        }
    }
    BOOL DBClose()
    {
        return SQL_OK(SQLCancel(hstmt));
    }
};

Мы постарались дать исчерпывающие комментарии к этому коду в тексте функций, но повторим вкратце основные положения:

Не правда ли, слово “указатель” встречалось в этом описании довольно часто?

Такие структуры, как указатель на массив указателей, не всегда бывают понятны не только широкому кругу читателей, но и некоторым Visual Basic’ам, поэтому необходимо компилировать Visual C++ проект с использованием proxy/stub (как мы это описывали в начале этого обзора). Если случайно про это забыть, то при попытке запустить DCOM-сервер клиентское приложение закричит дурным голосом, что произошла ошибка 0x80020008 (что-то вроде – неверный тип данных). После компиляции проекта DCOM-сервера с proxy/stub (и последующей регистрации этой самой proxy/stub) проблема отпадает как бы сама собой, однако предупреждения на описание методов GetSubNodes и GetAllParentes по прежнему будут выдаваться компилятором MIDL не обращайте на них внимания.

На этом наш DCOM-сервер можно считать законченным. Клиентское приложение тоже практически готово, и немного изменить его, чтобы оно вызывало DCOM-сервер и работало с ним – просто минутное дело. Для начала мы удалили из файла stdafx.h строку “#include <SQLEXT.H>” и объявления переменных hdbc2, hdbc, henv, hstmt, hstmt2 из файлов atl02Test.cpp и atl01Ctrl.h – ведь вся работа с базой данных выполняется теперь DCOM-сервером, после чего добавили в файл atl02Test.cpp строки:

/* Вместо этого можно было бы воспользоваться новым ключевым словом 
#import, позволяющим импортировать описание COM классов прямо из файлов 
содержащих библиотеки типов (*.tlb, *.dll и т.п.)… но, видимо по привычке, 
мы воспользовались старым добрым #include'ом */
    #include “..\msDCOM_1_Test\msDCOM_1_Test.h”
    #include “..\msDCOM_1_Test\msDCOM_1_Test_i.c”

И в файл atl01Ctrl.cpp:

    #include “..\msDCOM_1_Test\msDCOM_1_Test.h”

Это позволило нам использовать описания интерфейсов DCOM-сервера.

Очевидно, что может быть заранее неизвестно, на которой именно удаленной машине мы захотим запустить DCOM-сервер. Конечно, для теста можно было бы зашить это имя жестко, описав его прямо в коде, но извечная (и, временами, пагубная) тяга к лучшему, свойственная всему прогрессивному человечеству, подтолкнула нас к созданию DCOM-login диалога, позволяющего при загрузке клиента указать имя удаленного компьютера, на котором предполагается запустить DCOM-сервер. Для создания такого диалога мы добавили в проект DCOM-клиента (atl02Test) новый ATL object типа Dialog из категории Miscellaneouse и назвали его CLoginDlg (не очень удачно, т.к. название класса получилось CCLoginDlg). Добавив на появившуюся форму поле редактирования (в списке компонентов оно называется Edit Box), мы дали ему ID (уникальный идентификатор) “IDC_EDIT_ServerName” — по этому ID мы будем обращаться к полю редактирования из текста программы.

Примечание: MFC позволяет создать переменную “обертку” типа CString или CEdit для более удобной работы с полями редактирования, но наш проект основан на ATL и не поддерживает такой функциональности.

Установить ID проще всего, вызвав всплывающее меню на размещенном на форме компоненте и выбрав Properties (рисунок. 8) – на первой же закладке можно вписать ID.

msDCOM_LoginDlg.GIF (27986 bytes)

Добавив строку: #include “CLoginDlg.h” в файл atl02Test.cpp мы подключили этот диалог к проекту DCOM-клиента.

Единственной целью нашего Login-диалога является получение и передача имени удаленного компьютера в то место, где его очень ждут – перед вызовом CoCreateInstanceEx (именно таким путем мы запускали серверное приложение). Для успешного выполнения поставленной боевой задачи мы описали в классе CCLoginDlg (файл CLoginDlg.h) переменную:

    public:
        LPTSTR m_szServerName;

После чего изменили обработчики: нажатия кнопки “OK” и инициализации диалога следующим образом:

LRESULT OnOK(WORD wNotifyCode, WORD wID, HWND hWndCtl, BOOL& bHandled)
{
    /* Получить имя удаленного компьютера из поля редактирования */
    GetDlgItemText(IDC_EDIT_ServerName, m_szServerName, _MAX_PATH);
    EndDialog(TRUE);
    return 0;
}
LRESULT OnInitDialog(UINT uMsg, WPARAM wParam, LPARAM lParam, BOOL& bHandled)
{
/* Установить имя удаленного компьютера по умолчанию в поле редактирования 
(мы использовали здесь имя той же машины, на которой запускался DCOM-клиент) */
    SetDlgItemText(IDC_EDIT_ServerName, _T(“P120-2”));
    return 1;  // Let the system set the focus
}

Теперь, когда диалог уже был описан, мы удалили всю инициализацию работы с базой данных из DllMain и перенесли создание всплывающего меню и Image-листа в функцию DllGetClassObject (для того чтобы не создавать их при регистрации DLL'и), после чего добавили в нее же создание DCOM-сервера на удаленной машине. В том же файле (atl02Test.cpp ) объявили глобальную переменную ImsDCOM_1_srv (для интерфейса ImsDCOM_1_srv DCOM-сервера, в котором описаны наши методы) и флаг g_bLoading, чтобы создание сервера происходил только один раз при создании первого ActiveX компонента:

// Добавили объявление в файле atl02Test.cpp
ImsDCOM_1_srv * g_pImsDCOM_1_srv = NULL;
BOOL g_bLoading = FALSE;

// Добавили объявление в файле atl01Ctrl.cpp
extern ImsDCOM_1_srv * g_pImsDCOM_1_srv;

// Внесли изменения в файл atl02Test.cpp 
STDAPI DllGetClassObject(REFCLSID rclsid, REFIID riid, LPVOID* ppv)
{
    // ***************************************
    //
    USES_CONVERSION;
    if (g_bLoading)
    {
        /* Создать и заполнить ImageList и PopupMenu */
        g_bLoading = FALSE;
        g_hImageList1 = ImageList_LoadBitmap(_Module.GetResourceInstance(),
                MAKEINTRESOURCE(IDB_BITMAP1),
                16, 3, RGB(255, 255, 255));
        g_hPopupMenu1 = CreatePopupMenu();
        AppendMenu(g_hPopupMenu1, MF_STRING, 1, _T(“Add”));
        AppendMenu(g_hPopupMenu1, MF_STRING, 2, _T(“Delete”));
/* Создать и инициализировать структуру ServerInfo */
        COSERVERINFO ServerInfo;
        memset(&ServerInfo, 0, sizeof(ServerInfo));
        /* Задать имя серверного компьютера с помощью вызова login-диалога */
        TCHAR szServerName[_MAX_PATH + 1] = _T(“P120-2”);
        CCLoginDlg dlgLogin;
        dlgLogin.m_szServerName = szServerName;
/* Если диалог вернул TRUE, то вызвать DCOM-сервер на указанной машине, 
иначе не вызывать */
if(dlgLogin.DoModal())
        {
/* Установить название удаленного компьютера */
            ServerInfo.pwszName = T2W(dlgLogin.m_szServerName);
/* Заполнить структуру MULTI_QI (с помощью CoCreateInstanceEx через массив 
типа MULTI_QI можно вернуть более одного интерфейса, что в некоторых случаях
позволит избежать лишних вызовов по сети, но нам этого не нужно)*/
            MULTI_QI mq[1];
            mq[0].hr = S_OK; // [out] результат вызова сервера
            mq[0].pIID = &IID_ImsDCOM_1_srv; // [in] ID интерфейса
            mq[0].pItf = NULL; // [out] указатель на интерфейс
/* Создать или вызвать ранее созданное серверное приложение на удаленной 
машине */
            HRESULT hResult = CoCreateInstanceEx(
                CLSID_msDCOM_1_srv, // ID DCOM-серверного объекта
                NULL,
/* CLSCTX_ALL означающий что мы хотим подгрузить любой доступный вариант 
сервера (от DLL до удаленного сервера) */
                CLSCTX_ALL,
                &ServerInfo, // адрес структуры COSERVERINFO
                1, // количество запрашиваемых интерфейсов
                mq); // структура MULTI_QI
/* Если создание DCOM объекта на удаленной машине прошел успешно, то 
запомнить интерфейс ImsDCOM_1_srv в глобальной переменной */
            if (mq[0].hr == S_OK)
                g_pImsDCOM_1_srv = (ImsDCOM_1_srv*) mq[0].pItf;
/* На этапе отладки это ATLASSERT гарантирует, выдачу диагностического 
сообщения с возможностью прервать исполнение, продолжить исполнение или 
перейти в режим отладки, в случае невыполнения указанных в скобках условий 
(если в скобках получается FALSE). Эта команда действует только в режиме 
отладочной компиляции! */
            ATLASSERT(
                SUCCEEDED(hResult)
                && S_OK == mq[0].hr
                && g_pImsDCOM_1_srv
                );
/* Здесь выдается сообщение об ошибке, если она произошла, и ее код: это 
будет работать не только под отладкой, но и при реальном выполнении 
клиентского приложения */
            if (!(SUCCEEDED(hResult) 

                 && S_OK == mq[0].hr 

                 && g_pImsDCOM_1_srv)
               )
            {
                TCHAR sError [255];
                wsprintf(
                sError,
                "Ошибка при попытке первого вызова DCOM-сервера \n"
                "Result(CoCreateInstanceEx) = 0x%x, \n"
                "mq[0].hr = 0x%x, \n "
                "g_pImsDCOM_1_srv = 0x%x",
                hResult, mq[0].hr, g_pImsDCOM_1_srv);
                MessageBox(GetActiveWindow(), sError, "DCOM-Error",
                    MB_OK | MB_ICONERROR | MB_APPLMODAL);
            }
        }
    }
    //
    // ***************************************
    return _Module.GetClassObject(rclsid, riid, ppv);
}

Осталось изменить реализации для некоторый функций, где обращения к базе данных заменить на вызовы методов DCOM-сервера, и клиентское приложение будет готово:

long Catl01Ctrl::AddNodeDB(HTREEITEM hParent)
{
/* Добавляет новую ветку и запись в базу данных, но не TreeView и в 
возвращает ID_RT новой ветки. В случае ошибки возвращает –1 */
long lParent = GetTVItemData(hParent);
    long lID_RT = -1;
    /* Вызов метода DCOM-сервера */
    HRESULT hResult = g_pImsDCOM_1_srv->AddNode(lParent, &lID_RT, 

                                            “Новый элемент № %d”);
    if (FAILED(hResult))
    {
        MessageBox(“Ошибка при попытке записи в базу данных”, 

                  “DCOM/SQL-Error”);
        return -1;
    }
    else
        return lID_RT;
}
STDMETHODIMP Catl01Ctrl::put_CurID_RT(long ID_RT)
{
/* Выделяет ветку с переданным ID_RT. Возвращает E_INVALIDARG если 
передано отрицательное значение ID_RT, на остальные ошибки возвращает 
E_FAIL и выдает сообщение. При успешном завершении возвращает S_OK */
    if (ID_RT < 0) 
        return E_INVALIDARG;
    long count;
    long * pArrParent;
/* Вызов метода DCOM-сервера для получения списка ID_RT веток-предков в 
массив pArrParent */
    HRESULT hResult = g_pImsDCOM_1_srv->GetAllParentes(ID_RT,
            &count, &pArrParent);
    if (FAILED(hResult))
    {
        MessageBox("Ошибка при вызове метода GetAllParentes"
                  "DCOM/SQL-Error");
        return E_FAIL;
    }
    if (!(pArrParent && count))
    {
        MessageBox(
            "Возвращен пустой указатель при вызове метода GetAllParentes",
            "DCOM/SQL-Error");
        return E_FAIL;
    }
    HTREEITEM hItem = NULL;
    int i;
/* В цикле перебираются элементы массива со списком веток-предков, каждая 
из которых сначала находится среди дочерних для предыдущей раскрытой, а 
затем раскрывается сама. Исключение делается для корневой ветки – она не
ищется среди дочерних (т.к. не имеет ветки-родителя), а устанавливается 
директивно. */
    for(i = 0; i < count; i++)
    {
        /* Найти ветку, и установить указатель на нее в hItem */
        hItem = (pArrParent[i] == 0)
                 ? TreeView_GetRoot(m_ctlSysTreeView32.m_hWnd)
                : FindChildTreeItemByData(hItem, pArrParent[i]);
        TVITEM tvItem;
        tvItem.hItem = hItem;
        tvItem.mask = TVIF_STATE;
        tvItem.stateMask = TVIS_EXPANDEDONCE;
        tvItem.state = 0; 
        TreeView_SetItem(m_ctlSysTreeView32.m_hWnd, &tvItem);
        // Раскрыть ветку
        TreeView_Expand(m_ctlSysTreeView32.m_hWnd, hItem, TVE_EXPAND);
    }
    // Освобождение памяти
    CoTaskMemFree(pArrParent);
    // Найти ветку, для которой надо установить выделение
    hItem = FindChildTreeItemByData(hItem, ID_RT);
    // Если ветка найдена, то выделить ее
    if (hItem) 
         TreeView_SelectItem(m_ctlSysTreeView32.m_hWnd, hItem);
    return S_OK;
}
void Catl01Ctrl::InitializeAll()
{
    /* Установить Image-лист */
    TreeView_SetImageList(m_ctlSysTreeView32.m_hWnd,g_hImageList1, 

                        TVSIL_NORMAL );
    AddNode(NULL, 0, _T(“Root”), TRUE);
    return;
}
bool Catl01Ctrl::DeleteCurNodeDB(long iID_RT)
{
    /* Вызвать метод DCOM-server’a для удаления ветки */
    HRESULT hResult = g_pImsDCOM_1_srv->DeleteNode(iID_RT);
    if (FAILED(hResult))
    {
        MessageBox(
            “Ошибка при попытке удаления записи из базы данных”,
            “DCOM/SQL-Error”);
        return FALSE;
    }
    else
        return TRUE;
}
bool Catl01Ctrl::UpdateCurNodeDB(long iID_RT, LPCTSTR sName)
{
    /* Вызвать метод DCOM-server’a для изменения имени ветки */
    HRESULT hResult = g_pImsDCOM_1_srv->UpdateNode(iID_RT, sName);
    if (FAILED(hResult))
    {
        MessageBox(
            "Ошибка при попытке изменения записи в базе данных",
            "DCOM/SQL-Error");
        return FALSE;
    }
    else
        return TRUE;
}
void Catl01Ctrl::ExpandFromDB(HTREEITEM hItem, long iID_RT)
{
    // Если ветка уже раскрыта, то не раскрывать
    if (GetTVItemExpanded(hItem)) 
        return;
    long count = 0, lParent;
    long * pArrID = 0;
    BSTR * pArrName = 0;
    long * pArrChildrenCount = 0;
    /* Получить ID_RT ветки, которую предполагается раскрыть */
    lParent = GetTVItemData(hItem);
/* Вызвать метод DCOM-сервера для получения списка атрибутов дочерних веток
– массивов их ID_RT, названий и количества дочерних веток (показатель: 
ставить плюс или нет) */
    HRESULT hResult = g_pImsDCOM_1_srv->GetSubNodes(lParent, &count, &pArrID, 
          &pArrName, &pArrChildrenCount);
    if (FAILED(hResult) || (count <= 0))
    {
        MessageBox(
            "Ошибка при вызове метода GetSubNodes",
            "DCOM/SQL-Error");
        return;
    }
    if (!(pArrID && count && pArrName && pArrChildrenCount))
    {
        MessageBox(
            "Возвращен пустой указатель при вызове метода GetSubNodes",
            "DCOM/SQL-Error");
        return;
    }
    /* Макрос для использования W2T */
    USES_CONVERSION;
    int i;
/* Цикла добавляющий ветки в дерево */
    for(i = 0; i < count; i++)
    {
        AddNode(hItem, pArrID[i], W2T(pArrName[i]),
            (BOOL)pArrChildrenCount[i]);
        // Освобождение памяти
        SysFreeString(pArrName[i]);
    }
    // Освобождение памяти
    CoTaskMemFree(pArrName);
    CoTaskMemFree(pArrID);
    CoTaskMemFree(pArrChildrenCount);
    return;
}

После того, как весь этот код был описан, мы получили работающие сервер и клиент (в качестве клиента используется все тот же тестовое приложение, написанное на VB), которое мы сделали для тестирования ActiveX’a). После компиляции в режиме ReliseaMinDependancy (убедившись, что в Properties для проекта DCOM-сервера добавлены Link’и к библиотекам odbc32.lib odbccp32.lib, причем для всех режимов компиляции). Запустив клиента и указав ему имя локальной машины для поиска сервера, мы запустили DCOM-сервер на том же компьютере, что и клиента: при этом новые клиенты подключались к уже созданному серверу – т.е. все работало, как надо.

Загруженная и работающая пара DCOM-клиент-сервер заняла в оперативной памяти около 2 Мб, каждый новый клиент занимал по 1 Мб (как для теста, написанного на VB, так и для теста, написанного на Delphi4). Клиентский ActiveX занял на диске около 80 Кб плюс msDCOM_1_Testps.dll около 25 Кб, тогда как DCOM-сервер – около 70 Кб.

Для запуска клиента с удаленной машины очень важно не забыть зарегистрировать на ней библиотеку с ActiveX-клиентом (atl02Test.dll) и библиотеку с proxy/stub-заглушкой (msDCOM_1_Testps.dll) – без этих действий клиент работать не будет! Напомним, что msDCOM_1_Testps.dll требуется нам из-за массивов, которые передаются при вызовах GetAllParentes и GetSubNodes. Также надо настроить права доступа к серверу в dcomcnfg.exe на серверной машине (мы подробно описывали это в предыдущих тестах).

Если запускать этот сервер из-под Windows NT, то, вероятнее всего все необходимые библиотеки уже будут установлены в системе – например, библиотека msvcrt.dll почти наверняка лежит в каталоге System32, т.к. большинство продуктов Microsoft ее используют.

Visual C++, несомненно, не самое быстрое средство разработки приложений: затраты времени на создание наших тестовых примеров на этом средстве были в 2-4 раз больше, чем при решении аналогичной задачи на Delphi4 или C++ Builder’e. Однако, однажды написав ActiveX-компонент (или DCOM-клиент-сервер) с использованием ATL можно быть практически полностью уверенным, что все его сбои вызваны исключительно ошибками программиста и не связаны со средой разработки. Что же касается Delphi4 или C++ Builder’a, то нередко просто невозможно понять, в чем причина сбоя. Чтобы не погрешить против правды, отметим, что в исходных текстах Delphi можно найти объяснение для многих загадочных явлений (мы описывали некоторые в предыдущем обзоре), но часть из них остается для нас загадкой и по сей день.

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

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

В результате работы над тестами по поддержке средствами разработки технологий ActiveX и DCOM, мы сделали некоторые выводы, которые вкратце сводятся к следующему:


Итак, можно завершить наше исследование такими словами: из протестированных нами средств Visual C++ 6.0 наилучшим образом подходит для разработки ActiveX-компонентов и DCOM-приложений, особенно, если основным критерием является надежность работы приложений. Когда надежность не является столь критичной, а на первом месте стоит минимизация времени, потраченного на разработку приложений, Delphi4 получает очевидные преимущества.

C++ Builder3 практически по всем показателям проигрывает Delphi4 (принципиальные споры о том, что лучше — Object Pascal или C++, мы оставим в стороне). Но, все же, с его помощью можно куда быстрее получить конечный результат, чем используя Visual C++ 6.0.

Только найдя оптимум между надежностью продукта и затратами на его разработку можно сделать однозначный вывод о выборе того или иного средства – главное не упускать из виду, что скорость разработки может обернуться бесконечным исправлением ошибок, а неоправданный выбор более трудоемкого пути может привести к нереализуемости задачи вообще.


Copyright © 1994-2016 ООО "К-Пресс"