Visual Basic - Основы работы с базами данных Умение обращаться с файлами данных - чуть ли не одна из самых важных ступений в обучении программированию на Visual Basic! Здесь я постараюсь выложить только самые основные приёмы работы с объектом Data.Прежде чем начать использовать базу данных её нужно сначала создать ;)! 1.Создайте стандартный проект. 2.Кликните на ToolBox'e на кнопку Data и натяните на форму так, чтобы полоска была не очень широкой и в самом низу формы. 3.Теперь создайте на форме четыре кнопки и назовите их (по порядку создания): cmdAdd, cmdDelete, cmdUpdate, cmdExit. Свойствам Caption дайте эти же имена, только без "cmd". 4.Создайте на форме три текстовых поля одинаковой длины. 5.Теперь надо связать объект ^ Data с какой-нибудь базой данных. С какой? С той которая есть у всех владелцев VBasic'a - biblio.mdb! Активируйте объект Data и в свойствах, в DatabaseName выберите этот файл. Теперь там же, но в поле RecordSource надо выбрать раздел "Authors". Этим мы выберем только нужную часть базы данных. 6.Выделите первое текстовое поле и в свойствах, в DataSource выберите единственную в списке, созданную нами базу данных ^ Data1, т.е. тем самым мы связываем это текстовое поле с объектом Data1. А в поле свойств DataField выберите "Au_ID". Теперь задача этого поля отображать идентификационный номер каждого автора! То же самое проделайте со следующими двумя другими полями, но в DataField вместо "Au_ID" выберите "Author" и "Year Born". Поздравляю! Если Вы ещё этого никогда не делали, то знайте - Вы написали первую в своей жизни простенькую программку для отображения содержания базы данных!!! А теперь научимся редактировать её. Итак, главное редактирование - это занесение записей и удаление их. 7.Щёлкните два раза на кнопку Add и введите: Private Sub cmdAdd_Click() Data1.Recordset.AddNew 'Все поля, которые могут быть отредактированы будут очищены 'и подготовлены, поле Year Born будет установленно на 0, 'т.к. оно должно иметь какое-то значение 'поле Au_ID не будет очищено вообще, а изменено на самую 'последнюю позицию End Sub 8.Следующая кнопка сохраняет внесённые изменения в базе данных. Сделайте двойной клик по кнопке Update и введите: Private Sub cmdUpdate_Click() 'сохраняем ... Data1.UpdateRecord 'без следующей строки после сохраниения в полях 'автоматически появились бы самые первые записи. 'поэтому закладке присваеваем идентификатор последней 'изменённой записи и в полях остануться ваши записи Data1.Recordset.Bookmark = Data1.Recordset.LastModified End Sub 9.Теперь самое лёгкое - удаление записей!!! Ведь правильно говорят - "Ломать - не строить!" :-))). Делаем двойной щелчок по кнопке Delete и вводим: Private Sub cmdDelete_Click() 'чтобы выполнить последовательность инструкций над 'одиночным объектом Data1, не перечисляя его каждый 'раз, используем инструкцию With With Data1.Recordset .Delete .MoveNext If .EOF Then .MoveLast End With End Sub 10.И теперь для кнопки Exit просто введите: Private Sub cmdExit_Click() End End Sub Всё! А тепeрь Вы создали почти настоящую программу с базой данных!!! 10.Запустите проект. Нажмите на правую кнопку >. Содержание полей сменилось! Теперь нажмите на правую кнопку >|. В полях появились самые последние записи! То же самое будет происходить при нажатии на левые кнопки |11.Теперь нажмите кнопку "Add". 12.Первое поле НЕ испраляйте, во второе ввидите, например, ваше имя, а в третье год рождения. 13.Теперь сохраните эту запись, нажав кнопку "Update". 14.Знайте, ваша запись теперь сохранена на самом последнем месте! Можете снова "прогуляться" по записям и вернутся наконец с помощью правой кнопки >|. Ваша запись на месте. 15.Теперь нажмите на кнопку Delete... запись исчезла! НО! В этом случае мы НЕ можем удалять записи сделанные не нами. Ну, вот и готово! А теперь я расскажу немного о создании некотрых удобств с работой с базой данных. Например, представим себе, что Вы, просматривая нашу базу данных, заметили имя автора где-то в середине и через некоторое время должны вернуться к этой записи... Не пролистывать же нам снова окло тысячи имён!!! Для этого можно создавать так называеимые закладки! 16.создайте на форме ещё две кнопки с именами cmdMBM и cmdSMB со следующими надписями: Make the Bookmark и Show the Bookmark. 17.Теперь в разделе формы ^ General Declarations объявите переменную, которая будет содержать идентификатор актуальной записи: 'переменная должна быть объявлена, как Variant поскольку 'значения закладки могут быть как числовые, так и строковые Dim BM As Variant 18.Теперь делаем двойной щелчок по кнопке cmdMBM и вводим: Private Sub cmdMBM_Click() 'получаем идентификатор актуальной записи BM = Data1.Recordset.Bookmark End Sub 19.Теперь делаем двойной щелчок по кнопке cmdSBM и вводим: Private Sub cmdSBM_Click() 'присваеваем закладке идентификатор нужной записи Data1.Recordset.Bookmark = BM End Sub 20.Снова запустите проект и прощёлкайте несколько записей. 21.Нажмите кнопку "Make the Bookmark". Так, теперь пролистайте ещё дальше или вернитесь, вобщем уйдите от этой записи. 22.Щёлкните на кнопку "Show the Bookmark". В полях появились записи, на которые Вы и поставили закладку! А теперь я расскажу немного о поисках записей в базе данных. Представьте себе, что Вам нужно найти конкретное имя автора из тысяч других... WOW!!! :-)) Представили? Ну, так вот листать - вроде совсем не интересно... Нужно воспользовать одним из способов нахождения : FindFirst, FindLast, FindNext или FindPrevious. В следующей таблице представлены ихзначения: Метод Пояснение FindFirst Ищет первую запись в БД FindLast Ищет последнюю запись в БД FindNext Ищет каждую следующую запись в БД FindPrevious Ищет предыдущую запись в БД 23.Создайте кнопку cmdFind c надписью "Find" и впишите: Private Sub cmdFind_Click() 'у пользователя запрашивается имя автора а потом происходит 'поиск его в БД Data1.Recordset.FindFirst "Author = '" _ & Trim(InputBox("Введите имя автора")) & "'" 'если запись не найдена появляется следующая надпись: If Data1.Recordset.NoMatch Then MsgBox "Имя не найдено" End Sub 24.Теперь запустите проект. Выищите какое-нибудь имя из БД и нажмите кнопку Find. 25.Введите имя и нажмите ОК. Имя появилось! Чтобы искалось имя с конца БД слово FindFirst надо поменять на FindLast. А если Вы хотите проверить БД на наличие одного имени несколько раз надо применять слово FindNext и каждый раз вызывать поиск! ^ Эффективная работа с БД в VB 6.0 В статье рассматриваются способы повышения эффективности при разработке клиент-серверных СУБД-приложений в среде VisualBasic 6.0. Наиболее подробно освещены следующие аспекты проблемы: Эффективное использование стандартного для VB программного интерфейса доступа к данным (ADO); Повышение эффективности процесса разработки СУБД-приложений с использованием стандартных средств, а также продуктов третьих фирм. ^ На кого рассчитана статьяСтатья рассчитана на широкий круг читателей, меющих дело с разработкой клиент-серверных СУБД-приложений. рограммисты, уверенно владеющие VB, но не знакомые с этой областью его применения, найдут описание эффективных подходов к разработке. Читатели, не знакомые с VB, смогут составить представление о плюсах и минусах этого языка как средства разработки СУБД-приложений. Наконец, опытные VB-программисты, возможно, найдут в статье пару-тройку неизвестных им приемов.^ Описание примеровВ качестве демонстрационного примера мы возьмем серверную часть гипотетической онлайновой системы обработки транзакций со счетами в реальном времени (назовем ее ). Предположим, что поток транзакций достаточно велик, и ключевые таблицы базы данных содержат миллионы записей. Работу системы контролируют операторы, которым требуется немедленная статистическая обработка данных. В статье приведен необходимый минимум исходных текстов; подробные описания стандартных свойств и методов ищите в MSDN.^ Необходимый инструментарийЧтобы испытать рассматриваемые приемы на практике, потребуется Microsoft Visual Basic 6.0 с установленным SP5, а также Microsoft SQL Server 2000 (там, где речь идет о серверной части).^ Программный интерфейс доступа к данным Краткая история развития Самый естественный способ доступа к данным в VB - COM-объекты. Изначально Microsoft предлагала для VB два вида технологий доступа к данным: ориентированные на ядро JET (библиотека DAO, Data Access Objects); ориентированные на источники данных ODBC (библиотека RDO, Remote Data objects). Первая категория вообще не имеет отношения к клиент-серверной архитектуре и рассматривать ее мы не будем. Что касается RDO, то тут были проблемы, связанные как с устаревающим интерфейсом ODBC, так и с реализацией самой библиотеки RDO, как надстройки над ODBC.Проблемы сняло появление библиотеки ADO (ActiveX Data Objects), основанной на новой универсальной технологии доступа к данным OLE DB. Эта библиотека обладает множеством новых возможностей (таких как асинхронная работа, поддержка иерархических наборов записей и пр.) и практически лишена проблем своих предшественниц. оследняя версия - ADO 2.6, в которой добавлена поддержка XML.^ Библиотека ADO Архитектура библиотеки, назначение объектовОсновное назначение библиотеки ADO - операции с DML (Data Manipulation Language, язык обработки данных). Это определяет архитектуру библиотеки, ориентированную на получение наборов данных и их последующую обработку. Библиотека содержит следующие основные объекты: Connection - соединение с базой даннных; Command - команда DML; Recordset - набор записей; Stream - поток двоичных или текстовых данных; Record - запись. Рис.1. Объектная модель ADO Эти объекты предоставляют исчерпывающие возможности для манипуляций с данными; более того, библиотека позволяет выполнять одно и то же действие множеством различных способов, которые могут значительно отличаться по эффективности. Ниже приведены некоторые рекомендации, которые оптимизировать некоторые типичные операции.^ Соединения (ADODB.Connection)Любое действие с данными производится в контексте какого-либо активного соединения. Изучение примеров в MSDN может навести на мысль, что для каждого действия необходимо создать соединение - примерно так: Sub DoSimpleAction(strActionSQL As String)Dim cn As New ADODB.Connection ' одключаемся к SQL-серверуcn.Open "Provider=SQLOLEDB.1;Persist Security Info=False;Data Source=SomeSQLServer"' Выполняем запросcn.Execute strActionSQL 'Отключениеcn.CloseEnd Sub Однако, для нашей системы критично время обработки каждой транзакции, поэтому мы не можем каждый раз тратить время на установку соединения. Вместо этого будем использовать глобальную объектную переменную типа ADODB.Connection, которая будет хранить однажды установленное соединение. А код переделаем так: Sub DoSimpleAction(cn as ADODB.Connection, strActionSQL as String)' Выполняем запросcn.Execute strActionSQLEnd Sub Добавим, что в этом случае желательно: помещать объявление переменной в стандартный модуль, поскольку данные класса копируются, а это потенциальный источник ссылок и возникновения таинственных соединений; контролировать корректность соединения, поскольку оно могло прерваться в процессе работы. Для этого можно использовать свойство State и событие Disconnect объекта Connection. Команды (ADODB.Command) С помощью команд можно получать наборы записей. Команды могут быть следующих типов: Таблица (adCmdTable) - выбирает все записи из таблицы или представления; Хранимая процедура (adCmdStoredProc) - вызов хранимой процедуры, которая возвращает данные; Текст (adCmdText) - произвольный SQL-запрос, возвращающий данные; Файл (adCmdFile) - получение данных из файла; Неуказанный (adCmdUnspecified). Общие рекомендации по использованию команд таковы: Не используйте команд типа или . Вместо этого лучше определить для каждой операции с данными хранимую процедуру. Выгоды следующие: Если запросы разбросаны по тексту программы - искать и исправлять ошибки будет значительно сложнее. Если же вы уверены, что каждая операция выполняется только соответствующей процедурой - времени на отладку уйдет куда меньше. Для полной уверенности следует убрать права на доступ непосредственно к таблицам, назначив вместо этого права на запуск процедур. Возможность изменения логики программы без перекомпиляции. Во многих случаях эта стратегия позволит изменять алгоритмы, или даже структуру данных, перекомпилировав лишь необходимые хранимые процедуры. Не используйте представления. С точки зрения производительности, лучше применять функции, возвращающие наборы записей. Синтаксис их использования тот же, зато в ваших руках оказывается вся мощь T-SQL для оптимизации запросов. Например, в нашей системе требуется немедленное отображение статистики транзакций на рабочих местах, причем необходима довольно сложная обработка информации (скажем, выбрать транзакции по определенному критерию, затем выбрать из них характерные пары последовательных транзакций, вывести по ним статистику, и рассортировать счета по результатам). Решить такую задачу с помощью представлений весьма непросто, решение будет очень сложным для понимания, кроме того производительность такого решения будет низкой. С помощью ункций все намного проще.^ Наборы записей (ADODB.Recordset)Команды возвращают данные в виде наборов записей. Наборы бывают: Динамические (adOpenDynamic) - видны все изменения, вносимые в данные другими пользователями и доступны все операции, поддерживаемые провайдером данных; Ключевые (adOpenKeyset) - аналогичен динамическому, но не видны записи, добавляемые другими пользователями; Статические (adOpenStatic) - данных; изменения, вносимые другими пользователями не видны; Однонаправленные (adOpenForwardOnly) - аналогично статическому, но возможна только прокрутка вперед. Наборы записей также различаются типом блокировки (оптимистическая, пессимистическая, пакетная, только для чтения) и расположением курсора (клиентский или серверный).Очевидно, стоит использовать минимальный подходящий набор возможностей (так, для отчета достаточно статического набора записей, если вам нужен однократный проход данных - используйте однонаправленный набор и т.д). Кроме того - обратите внимание на следующие возможности ADO:Пакетное обновление (при поддержке провайдером) - дает возможность локально внести изменения в несколько записей набора, и потом передать их на сервер все вместе, . Для этого - вместо метода Update используйте метод UpdateBatch, установив при этом пакетный тип блокировки (adLockBatchOptimistic). Это снижает нагрузку на канал передачи данных, и уменьшает риск сбоя на ненадежном канале.Отсоединенные наборы записей. Получив набор записей с клиентским курсором, можно разорвать соединение с базой данных, освобождая занятые им ресурсы. При этом набор записей остается доступным, и соединение в любой момент можно восстановить для обновления набора или сохранения внесенных зменений. Делается это следующим образом: Function GetDisconnectedRecordset(strSQL As String) As ADODB.RecordsetDim cn As New ADODB.ConnectionDim rstRes As New ADODB.Recordset ' Подключаемся к SQL-серверуcn.Open "Provider=SQLOLEDB.1;Persist Security Info=False;Data Source=SomeSQLServer"' Открываем набор записейrstRes.CursorLocation = adUseClientrstRes.Open strQSL, cn, adOpenDynamic, adLockOptimistic 'ОтключениеSet rstRes.ActiveConnection = Nothingcn.Close Set GetDisconnectedRecordset = rstResEnd Function ^ Взаимодействие с ODBC Среди провайдеров данных для OLE DB имеется провайдер для драйверов ODBC (Microsoft OLE DB Provider for ODBC drivers), позволяющий подключаться к любым источникам данных ODBC (рис. 2). Следующий совет может показаться тривиальным, но все-таки: не используйте провайдер данных для ODBC для доступа к СУБД, если для нее существует провайдер данных. Как любой посредник, ODBC утяжеляет каждое обращение к данным; кроме того, потребуется программно создать сточник данных и следить за его корректностью. Без всего этого можно обойтись, используя с ADO для спользуемой СУБД провайдер данных. Помните, что основное назначение провайдера для драйверов ODBC - обеспечить доступ к тем СУБД, для которых еще не существует OLE DB провайдера.Рис. 2. Выбор OLE DB провайдера для создания подключения к СУБД^ Применение расширений ADO для работы со схемой данных - ADOX Библиотека ADO не содержит средств для работы со схемой данных. Для анализа или изменения схемы данных лучше всего воспользоваться библиотекой расширений ADO - ADOX (ActiveX Data Objects Extensions for Data Definition Language). Она содержит такие основные объекты как: Catalog - база данных; Table - реляционная таблица; View - представление; Column - столбец; Procedure - хранимая процедура; Index - индекс; Key - первичный, внешний, или альтернативный ключ таблицы; User - пользователь; Group - группа.Рис. 3. Объектная модель ADOXЭти объекты имеют все необходимые методы и свойства для полноценного управления схемой данных. ^ Оптимизация процесса разработки Сейчас мы рассмотрим некоторые дополнительные средства разработки (как входящие в состав VB, так и внешние), грамотное использование которых повысит общую эффективность труда программиста.^ Применение DataEnvironmentDataEnvironment - стандартный ActiveX-дизайнер, входящий в состав VB. При правильном спользовании он позволяет радикально упростить процесс создания СУБД-приложения, однако многие VB-программисты, даже меющие опыт работы с базами данных, не вполне представляют себе его возможности. Для начала - добавим его к проекту (контекстное меню проекта в окне Project Explorer, см. рис. 4). Рис. 4. Добавление DataEnvironment к проекту В папке Designers проекта появится элемент с названием DataEnvironment1 (которое можно впоследствии зменить), а в программе - одноименный глобальный объект. режде, чем мы воспользуемся новым элементом - нужно создать все необходимые для работы приложения соединения. Теперь рассмотрим некоторые способы его применения:^ Используйте конструктор DataEnvironment для создания команд. Если вы прислушались к совету собрать все операции с данными в хранимых процедурах - у вас может возникнуть вопрос: большое приложение имеет десятки и сотни подобных команд; неужели придется вручную описывать их все на VB? Тут пригодится DataEnvironment. Чтобы создать команду для вызова хранимой процедуры - просто перетащите процедуру мышью з окна Data View в окно конструктора DataEnvironment. Для каждой созданной подобным образом команды создается метод объекта DataEnvironment, аргументы которого соответствуют параметрам хранимой процедуры. Поясним на примере: если вы объявили процедеру, как:CREATE PROC SomeProcWithManyParams@param1 int,@param2 varchar(255),@param3 floatAS.....ENDи перетащили ее как сказано выше - у объекта DataEnvironment автоматически появляется метод:SomeProcWithManyParams(param1 As Long, param2 As String, param3 as Double)Далее для выполнения команды с необходимыми значениями параметров можно (и нужно) пользоваться именно этим методом. Кстати, подобные методы также создаются для команд типа , однако в этом случае их аргументы, соответствующие параметрам запроса, придется доопределить вручную.Результат подобных действий выглядит примерно так, как показано на рис. 5.Рис. 5. Набор команд, созданный в конструкторе DataEnvironment Используйте конструктор DataEnvironment для создания иерархических команд. Иерархические команды в сочетании со стандартным элементом MSHFlexGrid позволяют отображать иерархические данные в весьма удобной форме. DataEnvironment позволяет очень легко создавать иерархические команды, устанавливая связи по значениям одного или нескольких полей. Воспользуйтесь вкладкой окна свойств команды, где можно указать родительскую команду и перечислить поля для связи. Хотя так можно установить только простейшую связь, все равно описанный способ полезен - ведь этот случай является наиболее типичным. ^ Используйте окно кода DataEnvironment для обработки событий, возникающих при работе с данными. Каждая команда, созданная в DataEnvironment является экземпляром класса Command; соответственно обработчики всех событий этого класса можно писать в окне кода DataEnvironment. Кроме этого, каждая команда, возвращающая набор данных, порождает дополнительный объект класса Recordset, имя которого получается добавлением префикса к имени команды. Эти объекты плюс экземпляр класса Connection, который соответствует соединению с базой данных, дают нам полный контроль над событиями, связанными с получением и изменением данных, а также над навигацией по ним. А значит - всю логику работы клиентской части приложения с данными мы можем описать именно здесь, в одном месте программы, облегчая ее понимание и поиск ошибок. Например, здесь можно связать событие перемещения по записям родительского набора записей (master) c обновлением дочернего набора записей (detail). Этот механизм будет автоматически срабатывать в любом участке программы, повлекшим навигацию, что избавляет нас от дублирования кода, которое могло бы возникнуть, свяжи мы подобную логику, например, с визуальными элементами управления.^ Будьте внимательны при обновлении отображаемых наборов записей с помощью DataEnvironment. Существует одна проблема, которая, хоть и документирована, вызывает головную боль у тех, кто документацию не читает. Суть в следующем - если вы обновите набор записей объекта DataEnvironment, который является сточником данных для визуального элемента управления, необходимо заново выполнить привязку этого элемента управления к данным, иначе обновление в нем не отразится. Пример: ' Обновляем набор записейdeMain.rsAccounts.Requery' Необходимо привязать грид заново для отображения новых данныхWith grdAccounts.DataMember = "Accounts"Set .DataSource = deMainEnd With ^ Применение CASE-средств (Visual Modeler, Rational Rose)Разработку сложных приложений с развитой логикой и богатой объектной моделью на VB может существенно упростить и ускорить применение объектно-ориентированных CASE-средств. Конкретные преимущества такие: Автоматизация процесса проектирования (создания объектных моделей); Автоматическая генерация заготовок исходного кода по объектной модели; Автоматический реинжиниринг кода (создание объектной модели по коду); Автоматизация процесса создания документации. Все это в полной мере применимо к разработке сложных СУБД-приложений. Мы не будем излагать основы работы с CASE-средствами, и ограничимся сравнением возможностей двух подобных инструментов, интегрированных с VB, субъективно оценив их возможности по пятибалльной шкале: Visual Modeler Rational Rose Моделирование 3 5 Генерация кода 4 5 Реинжиниринг 3 5 Документирование 2 5 ^ Применение профайлеров (SQL-Profiler, Rational Quantify)Создание эффективных приложений невозможно без поиска и анализа в коде. Анализ УБД-приложений имеет свою специфику: как правило, время выполнения разделено между программой и СУБД. Допустим, тестирование выявило в системе проблему - недостаточную производительность при большом числе клиентов. Где искать - в коде на VB или в хранимых процедурах? Решить проблему помогут специальные средства, предназначенные для локализации проблем производительности - профайлеры.^ Rational Quantify. Этот инструмент позволяет получить детальную информацию о работе VB-приложения. В процессе контрольного выполнения собирается статистика о числе вызовов, среднем и суммарном времени выполнения каждой процедуры и даже строки программы. Если окажется, что производительность тормозят определенные хранимые процедуры - надо оптимизировать их. Если же относительная доля временных затрат на подобные вызовы невелика, дальнейший анализ статистики, собранной Rational Quantify подскажет, где искать в VB-коде. SQL-Profiler. Это средство позволяет анализировать процесс взаимодействия любого приложения с MS SQL Server. Допустим, вы работаете с чужой программой, и у вас есть подозрение, что она работает с СУБД неоптимально, однако непонятны причины и условия, в которых это происходит. В этом случае пригодится SQL-profiler. Он позволяет получить подробную информацию о каждом обращении приложения к СУБД, вплоть до трассировки выполнения хранимых процедур. Если где-то делается неэффективный запрос, то он будет виден в протоколе работы как на ладони, причем в контексте предыдущих последующих вызовов, что в большинстве случаев позволяет обнаружить причину программы. ^ Краткое резюме На этом мы остановимся. К сожалению, ограничения журнального формата не позволяют затронуть в этой статье такие интересные темы как: Разработка собственных источников данных; Создание эффективного интерфейса пользователя; Реализация OLAP средствами VB. Возможно, они будут освещены в следующей статье.В заключение: редакция требует от автора сказать свое слово в споре сторонников и противников VB как средства разработки СУБД-приложений. Что ж, попробуем перечислить сильные и слабые стороны VB в этом отношении, и указать область его применения. Итак, к основным плюсам VB можно отнести: Наличие мощного объектного API-доступа к данным; Наличие визуального конструктора объектов доступа к данным; Мощные средства создания интерфейса пользователя; Простоту отладки приложений; Интеграцию с CASE-средствами. Среди минусов я бы выделил два: Обманчивая простота VB. VB действительно позволяет новичку сделать нечто, похожее на работающую программу. Однако необходимо понимать, что на самом деле VB - достаточно сложный инструмент, для адекватного применения которого требуется серьезный опыт. Иначе разочарования неизбежны ;) Наличие некоторого количества неприятных багов в самых, казалось бы, ровных местах. Хотя они, по большей части, документированы, однако, не все из них имеют простые обходные пути, что создает дополнительный барьер на пути написания сложных приложений: Резюмируя: VB дает возможность чрезвычайно быстро разрабатывать и тестировать эффективные УБД-приложения, относящиеся как к front-end, так и к back-end. Единственное необходимое условие - мастерство программиста.^ Вызов функций по указателю Скачать код к статье Вы читали мою первую статью на эту тему? Надеюсь, что нет. Она мне страшно не нравилась уже сразу после опубликования. К тому же, я получил немало откликов на тему "мало информации", которые, конечно, считаю справедливыми. Я многое упустил из виду, исходя из мысли "так, это понятно, объяснять не нужно". Настало время всё-таки написать что-то относительно более полное. К тому же, название той статьи было "Ассемблер в VB", что не совсем (совсем не) соответствовало её содержанию. Теперь название правильное При написании я предполагал только одно: вы знаете, что такое указатель. Введение Как известно, прямой работы с указателями в VB нет. Однако есть функции для почти прямой работы с ними – частично скрытые (VarPtr, ObjPtr, StrPtr) и скрытые посильнее (см. статью про GetMem и PutMem). А вот вызова функций по указателю нет вообще, в то время как это мощнейший инструмент, очень удобный и простой. И привнести его в VB – деяние приятное и полезное. Многое из описанного в этой статье является весьма и весьма спорным с точки зрения переносимости, может, непереносимым вообще. Но возгласы "эй, а это будет работать в следующей версии VB?" не имеют силы: VB 7 уже вышел. VB 7 – это совсем не VB 6, это другой язык. Так что мы знаем, что VB 6 останется именно таким, какой он есть сейчас. И поэтому мы можем использовать даже самые непереносимые и несовместимые методики. При написании этой статьи я придерживался определённой хронологии и описывал решения в том порядке, в котором к ним приходил. Копирайт: сама идея вызова через CallWindowProc с использованием некоей ассемблерной вставки принадлежит не мне. Но реализация именно в таких вот видах и именно с такими вставками – моя. Взгляд изнутри Прежде всего, необходимо разобраться, что такое вызов функции на самом низком уровне, с точки зрения процессора. Как известно, у процессора есть набор инструкций, заложенных в него проектировщиками, которые он и исполняет. "Вызов" (функции) – это тоже просто одна из команд процессора. Их на самом деле несколько, но мы не будет углубляться в изучение ассемблера (во-первых, это выходит за рамки статьи, а во-вторых, я сам в этом не очень силён . Запомним пока, что есть такая команда. Теперь стек... Что такое стек? Это часть памяти (обыкновенной, оперативной), которая резервируется при запуске программы (нужный размер этой стековой памяти записывается компилятором прямо в exe при создании оного). Особенность работы со стеком описывается буквосочетанием LIFO (Last In, First Out; последним пришёл, первым вышел). Образно стек можно представить как трубу с одним открытым концом (а другой запаян). Можно положить что-то внутрь (при этом оно окажется на самом верху), если есть место, конечно. Можно убрать что-то (только самое верхнее, снизу нельзя). Можно посмотреть, что уже наличествует (смотреть можно и сверху, и глубже – представьте, что вся труба стеклянная). Принцип стека используется в программах (во всех программах) по двум причинам. Во-первых, память резервируется сразу при запуске программы, а значит, не будет расходов на её выделение в процессе работы (это обеспечивает высокую скорость). Во-вторых, это самый простой и удобный способ сохранения предыдущего состояния, чтобы сделать что-то другое и потом вернуться. Когда что-то помещается в верхушку стека, всё остальное остаётся неизменным, и потому после удаления верхнего элемента всё как-то само собой возвращается в исходное состояние, совершенно без лишних движений. Думаю, теперь можно переходить к описанию того, как на самом деле происходит вызов функции. Вызов функции – это помещение параметров в стек и выполнение процессорной команды call. Вот и всё. Существует несколько соглашений вызова (они определяют, как именно помещаются параметры в стек). Мы будем рассматривать только одно соглашение – StdCall. Во-первых, именно его используют все функции Windows, во-вторых, именно его использует VB. В соответствии с этим соглашением, параметры помещаются в стек в обратном порядке (справа налево), а функция сама удаляет их из стека по завершении работы. Допустим, у нас есть функция Function Sum (ByVal p1 As Long, ByVal p2 As Long, ByVal p3 As Long) As Long которая возвращает сумму своих аргументов. Когда мы вызываем её, выполняется такой код: push p3push p2push p1после чего происходит вызов функции Sum. Как вы догадались, команда процессора push означает "поместить в верхушку стека". Как нетрудно видеть, первый аргумент окажется в самой верхушке стека, ведь он помещён туда последним. Функция Sum всё это знает, и свои аргументы оттуда берёт (она пока не удаляет их! она пользуется тем, что труба стеклянная). Закончив работу, функция Sum должна вернуть управление туда, откуда её вызвали (иначе программа остановится… вернее, она рухнет, но это детали…). Но КУДА должна вернуть управление функция Sum? Я написал "после чего происходит вызов функции Sum", но я не написал, как именно он происходит. Очень просто - выполняется единственная команда процессора Call. Эта команда передаёт управление функции Sum, но перед этим помещает в верхушку стека адрес, по которому должна вернуться эта самая Sum! В результате непосредственно перед передачей управления в функцию Sum стек приобретает вид: Адрес возвратаПараметр p1Параметр p2Параметр p3[всё, что глубже, помещено не нами и нас не интересует...]Закончив работу, функция Sum вернёт управление по тому адресу, который лежит в самой верхушке стека. Непосредственно перед этим она удалит (на сей раз именно удалит, а не просмотрит) из стека и этот адрес, и все свои параметры. Ну вот, теперь, кажется, мы знаем достаточно Ну что, поехали… Напрямую... А зачем нам это всё? Ну писали бы на ассемблере, а из VB-то мы не можем вызвать конкретные команды процессора... Да, не могли бы, если бы не одна единственная API-функция CallWindowProc (собственно, ей нужно ставить памятник, она действительно ОДНА...). Эта функция предназначена для вызова обработчика оконных сообщений, но суть её работы сводится к простой упаковке параметров в стек и передаче управления! Иными словами, она не проверяет, что именно её заставляют вызвать (да и не смогла бы проверить при всём желании). А раз она обычная API-функция, то мы можем её Declare. Ну и всё, можете вызывать что хотите, спасибо за внимание... Нет, на самом деле проблемы только начинаются. Проблема первая – количество параметров. Нетрудно видеть, что у CallWindowProc параметров ровно пять, из них один – адрес функции (или указатель на функцию; что-то давно не упоминалось мною это славное слово). Значит, вызываемая функция должна иметь ровно 4 параметра. Ведь у нас соглашение StdCall, помните? А оно требует, чтобы функция удаляла свои параметры из стека сама. Если функция была откомпилирована для работы с 3 параметрами, она удалит из стека ровно 3 параметра, и вы даже не можете себе представить, насколько ей безразлично, сколько их было туда помещено на самом деле. Результат – нарушение структуры стека (вызывающая сторона в недоумении, она-то уверена, что удалены 4 параметра, а тут...) и немедленный crash. Так что использовать функцию CallWindowProc напрямую можно лишь в одном случае: если вы уверены, что вызываемый код завершается командой процессора ret 0x0010 (это команда "возврат" с удалением из стека &H10 байт. &H10 – это 16, а 16 – это 4*4, то есть 4 параметра по 4 байта каждый. Они все 4 байта, параметры-то). В этом можно быть уверенным у в двух случаях: вы знаете, что у функции 4 параметра или же вы сами написали некий код, завершающийся командой ret 0x0010. Чувствуете, куда клоню? ;) Даже если чувствуете, всё равно запомним промежуточный результат (промежуточных результатов будет несколько, и каждый имеет полное право на самостоятельное и независимое существование и использование): Можно напрямую использовать функцию CallWindowProc для вызова другой функции. Для этого в качестве первого параметра нужно передать указатель на эту функцию, а в качестве остальных – параметры этой, вызываемой, функции. Жёсткое ограничение: у вызываемой функции должно быть ровно 4 параметра. Приведём маленький код в подтверждение сказанного. Option ExplicitPrivate Declare Function CallWindowProc Lib "user32.dll" _Alias "CallWindowProcA" (ByVal lpPrevWndFunc _As Long, ByVal hwnd _As Long, ByVal msg As Long, ByVal wParam _ As Long, ByVal lParam As _Long) As LongPrivate Declare Function _FreeLibrary Lib "kernel32.dll" (ByVal _hLibModule As Long) As LongPrivate Declare Function GetProcAddress Lib "kernel32.dll"_ (ByVal _hModule As Long, ByVal lpProcName As String) As LongPrivate Declare Function LoadLibrary Lib "kernel32.dll" _Alias "LoadLibraryA" (ByVal _lpLibFileName As String) As LongPrivate Sub Form_Load() Dim user As Longuser = LoadLibrary("user32.dll") CallWindowProc GetProcAddress(user, "MessageBoxA"), _Me.hwnd, StrPtr(StrConv("Ну что, работает!", vbFromUnicode)), _StrPtr(StrConv("Заголовок", vbFromUnicode)), 0 FreeLibrary userEnd SubПока не обращайте внимания на StrPtr и StrConv, потом всё скажу ^ И в обход… Вспомним те два случая, в которых можно быть уверенными Не случай номер 1 мы повлиять не можем (на самом деле, при известном шаманстве возможно всё, но я не буду соблазнять вас на модификацию самой команды Ret в теле откомпилированной вызываемой функции; да и что если параметров больше 4, а не меньше?), так что вплотную займёмся вариантом номер два. Да, мы будем писать код. Но мы будем писать машинный код. Не бойтесь, это несложно Машинные коды, соответствующие инструкциям Push, Call и Ret легко посмотреть в соответствующих мануалах от Intel или ещё от кого. И их всего три! Поехали. Задача: мы должны создать в памяти (а больше негде) участок готового машинного кода, который бы завершался командой ret 0x0010, и при этом был бы способен вызывать функцию с любым количеством параметров. Тогда мы сможем передать управление на этот участочек с помощью CallWindowProc, участочек вызовет функцию, управление вернётся на участочек, а он вернёт его в CallWindowProc, выпихнув из стека правильное количество параметров (четыре, четыре...). Делов-то! Всё это должно выглядеть так: push параметрNpush параметр(N-1)...push параметр2push параметр1call functionret 0x0010 Как видно, нам не удастся с