Исходники
Статьи
Языки программирования
.NET Delphi Visual C++ Borland C++ Builder C/С++ и C# Базы Данных MySQL MSSQL Oracle PostgreSQL Interbase VisualFoxPro Веб-Мастеру PHP HTML Perl Java JavaScript Протоколы AJAX Технология Ajax Освоение Ajax Сети Беспроводные сети Локальные сети Сети хранения данных TCP/IP xDSL ATM Операционные системы Windows Linux Wap Книги и учебники
Скрипты
Магазин программиста
|
Организация "горячего" обмена по DDE между Microsoft Excel и приложением .NET.Microsoft Certified Application DeveloperСкачать пример (C# проект). 1. Вступление.В данной статье я хочу поделиться решением, которое наверняка будет многим полезно. Началось с того, что передо мной была поставлена задача организовать чтение данных из книги MS Excel, причем данные из ячеек нужно было считывать только в том случае, если они изменились. При этом были выдвинуты жесткие требования к скорости и оперативности обработки информации в изменившихся ячейках. Перепробовав несколько решений, я остановился на механизме Dynamic Data Exchange, который, несмотря на преклонный возраст (DDE появился еще в Windows 3.0) и относительную громоздкость и трудоемкость в программировании остается наилучшим решением для организации быстрого обмена данными между приложениями. Однако программирование DDE приложения, тем более в среде .NET, которая не приветствует явного использования указателей и использование памяти вне ведома Garbage Collector, может вызвать вопросы, особенно у начинающих программистов. Вот на эти вопросы, которые в свое время возникли у меня, я и постараюсь ответить в данной статье. В конце статьи я приведу готовое решение - компонент ExcelDDEHotConnection, позволяющий организовать подписку на любую ячейку любой открытой книги, и скрывающий в себе все низкоуровневые операции. Данный компонент может быть свободно использован разработчиками в своих приложениях, при этом я не устанавливаю никаких ограничений или авторских прав на исходный код. 2. Описание протокола DDE.Начнем с кратного описания протокола DDE. Материал статьи охватывает только ту часть API протокола, которая необходима для организации "горячего" канала DDE. Протокол DDE подразумевает клиент-серверную архитектуру. Это значит, что одно их приложений выступает в качестве сервера, а второе - клиента. В нашем случае сервером выступает приложение MS Excel, а приложения .NET являются для него клиентами. Обмен данными между приложениями происходит посредством транзакций. Управляет всем процессом специальное расширение ОС Windows, - динамическая библиотека DDEML. По протоколу DDE, сервер в первую очередь должен зарегистрировать себя в библиотеке DDEML. После этого он регистрирует предоставляемые сервисы. Клиентское приложение также сначала регистрирует себя в библиотеке DDEML. После этого клиентское приложение создает канал связи с сервером. Протокол DDE поддерживает три вида обмена данными между клиентом и сервером:
В первом случае клиент явным образом посылает серверу запрос, указывая нужный элемент данных. Сервер, получив подобный запрос, предоставляет клиенту эти данные. В случае организации "теплого канала" сервер, при изменении данных, отправляет клиенту извещение. Клиент, получив это извещение, может послать запрос серверу на получение этих данных, после чего сервер предоставляет данные клиенту. В случае "горячего канала" сервер будет отправлять клиенту данные, не ожидая явного запроса при их изменении. На практике, как правило, используется либо передача данных по явному запросу, либо "горячий канал", причем второй очень удобен при организации быстрого обмена данными между приложениями в режиме реального времени. Именно "горячий" канал и будет рассматриваться в этой статье. В библиотеке DDEML данные адресуются трехступенчатой схемой: сервис (service), раздел (topiс) и элемент данных (data item). Для сервера DDE приложения MS Excel, эта схема выглядит следующим образом:
Для установления связи, приложение должно сначала зарегистрировать себя в библиотеке DDEML и получить свой программный идентификатор. Этот идентификатор необходимо хранить в течение всей работы, так как для каждого приложения, которое регистрируется в библиотеке DDEML, создается своя копия необходимых структур данных. Регистрация в библиотеке DDEML происходит с помощью функции DdeInitialize, которая имеет следующую сигнатуру: UINT WINAPI DdeInitialize( DWORD FAR* pidInst, PFNCALLBACK pfnCallback, DWORD afCmd, DWORD ulRes); Параметры, которые передаются функции при вызове:
В случае успешной регистрации, функция DdeInitialize возвращает нулевое значение. Если при инициализации произошла ошибка, то функция вернет код ошибки. Если приложение больше не собирается работать с библиотекой DDEML, то оно должно вызвать функцию DdeUninitialize, передав ей в качестве параметра программный идентификатор, полученный при регистрации: BOOL WINAPI DdeUninitialize(DWORD idInst); После успешной инициализации, клиентское приложения должно создать канал связи с сервером. Для каждого сервиса и раздела создается свой канал связи. После успешного создания канала, ему присваивается идентификатор, который указывается в дальнейших транзакциях, как клиентом, так и сервером. Адресация происходит посредством строк, однако в транзакциях используются их идентификаторы. Эти идентификаторы присваиваются каждой строке библиотекой DDEML и хранятся в специальной системной таблице идентификации строк. Для создания идентификатора строки, необходимо воспользоваться функцией DdeCreateStringHandle: HSZ WINAPI DdeCreateStringHandle( DWORD idInst, LPCSTR psz, Int iCodePage); Функция получает следующие параметры:
Функция возвращает идентификатор, который библиотека DDEML присвоила данной строке. Для того, чтобы освободить ресурсы, связанные с зарегистрированной строкой, клиентское приложение должно вызвать функцию отмены регистрации данной строки DdeFreeStringHandle: BOOL WINAPI DdeFreeStringHandle( DWORD idInst, HSZ hsz); В качестве параметров функция получает следующее:
Функция возвращает значение true, если операция прошла успешно, и false - если при выполнении функции произошли ошибки. Для того, чтобы получить строку по ее идентификатору, необходимо воспользоваться функцией DdeQueryString: DWORD WINAPI DdeQueryString( DWORD idInst, HSZ hsz, LPSTR psz, DWORD cchMax, Int iCodePage ); В качестве параметров функция получает следующее:
Функция возвращает количество скопированных символов. При этом если фактическая длина строки (в символах) меньше указанной в параметре cchMax, то функция скопирует cchMax символов и вернет это значение. Если cchMax больше фактической длины строки, то функция скопирует всю строку и вернет количество скопированных символов. Если передать через параметр psz нулевое значение, то функция проигнорирует значение параметра cchMax и вернет фактическую длину строки в символах. Размер буфера в байтах для строки зависит от размера символа и определяется параметром iCodePage. Для получения данных из глобальной области памяти по их идентификатору нужно воспользоваться функцией DdeGetData: DWORD WINAPI DdeGetData( HDDEDATA hData, void FAR* pDst, DWORD cbMax, DWORD cbOff ); Функция должна получить на вход следующие параметры:
Функция возвратит количество фактически скопированных байт данных. Если вместо ссылки на буфер через параметр pDst передать нулевое значение, то функция вернет фактический размер порции данных в глобальной области памяти, при этом значение параметров cbMax и cbOff будут проигнорированы. Канал связи DDE создается с помощью функции DdeConnect: HCONV WINAPI DdeConnect( DWORD idInst, HSZ hszService, HSZ hszTopic, CONVCONTEXT FAR* pCC); В качестве параметров функция должна получить следующее:
Функция возвращает идентификатор созданного канала связи. В случае ошибки функция вернет нулевое значение. Полученный идентификатор канала необходимо хранить в течение всего сеанса связи. Когда приложение завершает работу с каналом, оно должно закрыть его, вызвав функцию DdeDisconnect:BOOL WINAPI DdeDisconnect(HCONV hConv); В качестве параметра функция получает идентификатор канала, который нужно закрыть. Функция возвращает true, если канал успешно закрыт и false в случая возникновения ошибок при закрытии канала. После того, как был создан канал связи, можно начинать обмен данными. Обмен происходит посредством транзакций с помощью функции DdeClientTransaction и функции обратного вызова DdeCallbackFunction. Если приложение (независимо от того, клиент или сервер) хочет отправить данные, то оно должно подготовить их, оформить контекст с помощью функций библиотеки DDEML , а потом вызвать функцию DdeClientTransaction. При этом принимающему приложению будет отправлено сообщение, которое осуществит вызов функцию обратного вызова принимающей стороны. Функции обратного вызова представляют особой обработчик с множественным ветвлением, каждая ветвь которого обрабатывает соответствующую ей транзакцию. Если транзакция не поддерживается, то функция обратного вызова должна вернуть нулевое значение, иначе - один из допустимых для обработанной транзакции, кодов возврата. Функция обратного вызова имеет следующий заголовок: HDDEDATA EXPENTRY DdeCallbackFunction( WORD wType, WORD wFmt, HCONV hConv, HSZ hsz1, HSZ hsz2, HDDEDATA hData, DWORD dwData1, DWORD dwData2 ); где:
В свою очередь функция запуска транзакции DdeClientTransaction имеет следующий заголовок: HDDEDATA WINAPI DdeClientTransaction( void FAR* pData, DWORD cbData, HCONV hConv, HSZ hszItem, UINT uFmt, UINT uType, DWORD dwTimeout, DWORD FAR* pdwResult );
Возвращает нулевое значение, если транзакция была выполнена с ошибкой, или ненулевую величину, смысл которой зависит от транзакции, (В нашем случае будет возвращена единица) при нормальном выполнении. 3. Отображение библиотеки DDEML в .NET.Библиотека DDEML представляет собой 32-разрядную библиотеку платформы Win32. Ее функции не могут быть вызваны непосредственно из приложения .NET. Для того, чтобы иметь возможность работать с этой библиотекой, нужно создать ее <отображение> в среде .NET, используя специальные средства. Таким образом, типы и структуры, с которыми работает библиотека будут автоматически преобразовываться средствами .NET в типы .NET и наоборот. Для доступа к функциям используется класс DLLImportAttribute, который описан в пространстве имен System.Runtime.InteropServices. Что касается типов параметров функций, как уже было сказано, среда .NET в большинстве случаев автоматически осуществляет все необходимые преобразования. В таблице 1 показаны подобные преобразования:
Для функции обратного вызова необходимо создать делегат, который имеет соответствующую сигнатуру, и указать его в качестве параметра в функции DdeInitialize: /// <summary> /// Делегат функции обратного вызова DDE /// </summary> internal delegate IntPtr DDECallBackDelegate( uint wType, // Код транзакции uint wFmt, // Формат данных IntPtr hConv, // Идентификатор канала IntPtr hsz1, // Идентификатор строки (в нашем случае, строки раздела) IntPtr hsz2, // Идентификатор строки (в нашем случае, элемента данных) IntPtr hData, // Идентификатор глобальной области данных, где находятся данные uint dwData1, // Дополнительный параметр (В нашей работе не рассматривается) uint dwData2 // Дополнительный параметр (В нашей работе не рассматривается) ); При этом отображение функции DdeInitialize в среде .NET будет выглядеть так: internal class DDEML { [DllImport("user32.dll", EntryPoint="DdeInitialize", CharSet=CharSet.Ansi)] internal static extern uint DdeInitialize( ref uint pidInst, DDECallBackDelegate pfnCallback, uint afCmd, uint ulRes); ... } Ниже, я привожу пример вызова функции DdeInitialize в среде .NET: public class ExcelDDEHotConnection { // Ссылка на делегат-переходник для функции обратного вызова DDE private DDECallBackDelegate _DDECallBack = null; // Обработчик функции обратного вызова private IntPtr DDECallBack( uint uType, uint uFmt, IntPtr hConv, IntPtr hsz1, IntPtr hsz2, IntPtr hData, uint dwData1, uint dwData2) { switch(uType) { // Мы обрабатываем только транзакции с данными case DDEML.XTYP_ADVDATA: // Выполняем обработку транзакции ... // Возвращаем управление return new IntPtr(DDEML.DDE_FACK); } // Все остальные транзакции мы не обрабатываем return IntPtr.Zero; } // Идентификатор приложения private uint idInst = 0; public ExcelDDEHotConnection() { // Создаем делегат-переходник для функции обратного вызова _DDECallBack = new DDECallBackDelegate(DDECallBack); // Регистрация в библиотеке DDEML DDEML.DdeInitialize(ref idInst, _DDECallBack, 0, 0); // выполняем остальные инициализирующие действия ... } ... } Прошу обратить внимание на то, что ссылку на делегат функции обратного вызова мы храним все время работы с DDEML. Если этого не сделать, то сборщик мусора .NET уничтожит этот делегат при очередной сборке мусора, что приведет к тому, что во внутренних структурах библиотеки DDEML ссылка на функцию обратного вызова будет указывать на уничтоженный объект. Это, естественно, вызовет NullPointerException при попытке библиотеки DDEML вызвать функцию обратного вызова. Поэтому вызов функции DdeInitialize следующего вида нежелателен: // Регистрация в библиотеке DDEML DDEML.DdeInitialize(ref idInst, new DDECallBackDelegate(DDECallBack), 0, 0); Забегая вперед, отмечу, что для отображения необходимых функций и констант библиотеки DDEML в компоненте ExcelDDEHotConnection служит класс DDEML. Ниже приведен список остальных функций для работы с DDE: Делегат функции обратного вызова: internal delegate IntPtr DDECallBackDelegate( uint wType, // Код транзакции uint wFmt, // Формат данных IntPtr hConv, // Идентификатор канала IntPtr hsz1, // Идентификатор строки (в нашем случае, строки раздела) IntPtr hsz2, // Идентификатор строки (в нашем случае, элемента данных) IntPtr hData, // Идентификатор глобальной области данных, где находятся данные uint dwData1, // Дополнительный параметр (В нашей работе не рассматривается) uint dwData2 // Дополнительный параметр (В нашей работе не рассматривается) ); Отображение функции DdeInitialize: [DllImport("user32.dll", EntryPoint="DdeInitialize", CharSet=CharSet.Ansi)] internal static extern uint DdeInitialize( ref uint pidInst, DDECallBackDelegate pfnCallback, uint afCmd, uint ulRes); Отображение функции DdeUninitialize: [DllImport("user32.dll", EntryPoint="DdeUninitialize", CharSet=CharSet.Ansi)] internal static extern bool DdeUninitialize(uint idInst); Отображение функции DdeCreateStringHandle: [DllImport("user32.dll", EntryPoint="DdeCreateStringHandle", CharSet=CharSet.Ansi)] internal static extern IntPtr DdeCreateStringHandle( uint idInst, string psz, int iCodePage); Отображение функции DdeFreeStringHandle: [DllImport("user32.dll", EntryPoint="DdeFreeStringHandle", CharSet=CharSet.Ansi)] internal static extern bool DdeFreeStringHandle(uint idInst, IntPtr hsz); Отображение функции DdeConnect: [DllImport("user32.dll", EntryPoint="DdeConnect", CharSet=CharSet.Ansi)] internal static extern IntPtr DdeConnect( uint idInst, IntPtr hszService, IntPtr hszTopic, IntPtr pCC); Отображение функции DdeDisconnect: [DllImport("user32.dll", EntryPoint="DdeDisconnect", CharSet=CharSet.Ansi)] internal static extern bool DdeDisconnect(IntPtr hConv); Отображение функции DdeClientTransaction: [DllImport("user32.dll", EntryPoint="DdeClientTransaction", CharSet=CharSet.Ansi)] internal static extern IntPtr DdeClientTransaction( IntPtr pData, uint cbData, IntPtr hConv, IntPtr hszItem, uint uFmt, uint uType, uint dwTimeout, ref uint pdwResult); Отображение функции DdeGetData: [DllImport("user32.dll", EntryPoint="DdeGetData", CharSet=CharSet.Ansi)] internal static extern uint DdeGetData( IntPtr hData, [Out] byte[] pDst, uint cbMax, uint cbOff); Отображение функции DdeQueryString: [DllImport("user32.dll", EntryPoint="DdeQueryString", CharSet=CharSet.Ansi)] internal static extern uint DdeQueryString( uint idInst, IntPtr hsz, StringBuilder psz, uint cchMax, int iCodePage); 4. Организация горячего канала Excel - приложение DDE.В этой главе вкратце описано, как осуществить корректное подключение и отключение от ячеек Excel. Например, необходимо получить доступ к ячейке, расположенной во втором столбце и первой строке на странице с названием <Лист1> рабочей книги <Книга1>. Для начала необходимо зарегистрироваться в библиотеке DDEML и получить программный идентификатор idInst: // Создаем делегат-переходник для функции обратного вызова _DDECallBack = new DDECallBackDelegate(DDECallBack); // Регистрация в библиотеке DDEML DDEML.DdeInitialize(ref idInst, _DDECallBack, 0, 0); После этого создаем канал связи с нужным разделом. В нашем случае, как было упомянуто выше, название сервиса: <EXCEL>, а название раздела <[Книга1.xls]Лист1>. Необходимо помнить, что расширение файла необходимо указывать, если эта книга открыта из файла. Если осуществляется подключение к созданной, но еще не сохраненной книге, то расширение не указывается. // Формируем название раздела string szTopic = "[Книга1.xls]Лист1"; // Получение идентификатора сервиса IntPtr hszService = DDEML.DdeCreateStringHandle(_idInst, "EXCEL", DDEML.CP_WINANSI); // Получаем идентификатор раздела IntPtr hszTopic = DDEML.DdeCreateStringHandle(_idInst, szTopic, DDEML.CP_WINANSI); // Подключаемся к разделу IntPtr hConv = DDEML.DdeConnect(_idInst, hszService, hszTopic, (IntPtr)null); // Проверяем результат if(hConv!=IntPtr.Zero) { ... } // Освобождаем идентификаторы строк DDEML.DdeFreeStringHandle(_idInst, hszService); DDEML.DdeFreeStringHandle(_idInst, hszTopic); После создания канала информируем Excel о том, чтобы приложение получало содержимое нужной ячейки, как только оно изменится (<горячий канал>). Для этого посылаем Excel транзакцию XTYP_ADVSTART: // Формируем название ячейки string szItem = "R1C2"; // Создаем идентификатор строки IntPtr hszItem = DDEML.DdeCreateStringHandle(_idInst, szItem, DDEML.CP_WINANSI); // Подписываемся на тему uint pwdResult = 0; IntPtr hData = DDEML.DdeClientTransaction((IntPtr)null, 0, hConv, hszItem, DDEML.CF_TEXT, DDEML.XTYP_ADVSTART, 1000, ref pwdResult); if(hData!=IntPtr.Zero) { ... } // Освобождаем идентификатор строки DDEML.DdeFreeStringHandle(_idInst, hszItem); Отключение производим в обратном порядке, сначала информируем сервер о том, что данные из ячейки нам больше не нужны, посылая Excel транзакцию XTYP_ADVSTOP: // Формируем название ячейки string szItem = "R1C2"; // Создаем идентификатор строки IntPtr hszItem = DDEML.DdeCreateStringHandle(_idInst, szItem, DDEML.CP_WINANSI); // Подписываемся на тему uint pwdResult = 0; IntPtr hData = DDEML.DdeClientTransaction((IntPtr)null, 0, hConv, hszItem, DDEML.CF_TEXT, DDEML.XTYP_ADVSTOP, 1000, ref pwdResult); if(hData!=IntPtr.Zero) { ... } // Освобождаем идентификатор строки DDEML.DdeFreeStringHandle(_idInst, hszItem); После завершения транзакции, закрываем канал: // Закрываем канал DDEML.DdeDisconnect(hConv); И завершаем работу с библиотекой DDEML: // Отключаемся от DDEML DDEML.DdeUninitialize(idInst); Необходимо отметить, что для всех трех режимов создается одинаковый канал. При этом для одних ячеек мы можем указывать <горячий> режим, для других - <теплый>, а с третьими работать по явному запросу. Для того, чтобы включить <теплый> канал, необходимо отправить Excel транзакцию, код которой состоит из побитной комбинации кода транзакции XTYP_ADVSTART и флага XTYPF_NODATA. 5. Компонент ExcelDDEConnection.Компонент ExcelDDEConnection представляет готовое решение, позволяющее организовать <горячий> канал DDE между приложением .NET и Excel. Компонент состоит из нескольких классов, главный из которых - ExcelDDEHotConnection. Экземпляр данного класса автоматически инициализируется в библиотеке DDEML при создании и отключается от ее при завершении своего существования. Ниже приведены основные методы и свойства класса ExcelDDEHotConnection:
Коллекция разделов TopicDescriptorCollection. Коллекция разделов представляет собой набор объектов, описывающий разделы. При добавлении раздела в коллекцию, происходит автоматическое создание канала, а при удалении - закрытие канала. Коллекция не допускает дублирование одинаковых разделов. Раздел добавляется в коллекцию только в том случае, если удалось создать канал для этого раздела.
Дескриптор раздела TopicDescriptor. Экземпляр класса описывает раздел данных. Каждый раздел содержит в себе коллекцию элементов данных типа ItemDescriptor, описывающих ячейки. При добавлении ячейки происходит отправка Excel транзакции на подписку на эту ячейку, при удалении - транзакция на завершение работы с ячейкой.
Дескриптор ячейки ItemDescriptor Описывает ячейку Excel.
Аргумент события Data - AdviseEventArgs. Экземпляр класса передается в качестве аргумента в событии Data.
6. Литература и ссылки.
|
Форум Программиста
Новости Обзоры Магазин Программиста Каталог ссылок Поиск Добавить файл Обратная связь Рейтинги
|