Поиск

Полнотекстовый поиск:
Где искать:
везде
только в названии
только в тексте
Выводить:
описание
слова в тексте
только заголовок

Рекомендуем ознакомиться

'Рассказ'
Рюноске А. Снежок /рассказ 1 .Сетон-Томпсон Э. Чинк и другие рассказы 17.Смит Д. Сто один далматин 18.Усачев А. Умная собачка Соня/ повесть-сказка 19....полностью>>
'Документ'
В своей монографии, написанной в 1967 году, но так и не увидевшей свет при жизни автора - «Самосознание и научное творчество», М.К.Петровым были выск...полностью>>
'Бюллетень'
Национализация газовой отрасли в Боливии, притязания "Газпрома" на европейские активы, а также IPO "Роснефти" – едва ли не централ...полностью>>
'Документ'
Создание многофункциональной системы ГАММА-3 направлено на решение комплексной проблемы автоматизации решения задач исследования и проектирования сло...полностью>>

Visual Basic Основы работы с базами данных

Главная > Документ
Сохрани ссылку в одной из сетей:

1

Смотреть полностью

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-программисты, возможно, найдут в статье пару-тройку неизвестных им приемов.

Описание примеров

В качестве демонстрационного примера мы возьмем серверную часть гипотетической онлайновой системы обработки транзакций со счетами в реальном времени (назовем ее <CountDown>). Предположим, что поток транзакций достаточно велик, и ключевые таблицы базы данных содержат миллионы записей. Работу системы контролируют операторы, которым требуется немедленная статистическая обработка данных. В статье приведен необходимый минимум исходных текстов; подробные описания стандартных свойств и методов ищите в 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.Close
End Sub

Однако, для нашей системы критично время обработки каждой транзакции, поэтому мы не можем каждый раз тратить время на установку соединения. Вместо этого будем использовать глобальную объектную переменную типа ADODB.Connection, которая будет хранить однажды установленное соединение. А код переделаем так:

Sub DoSimpleAction(cn as

ADODB.Connection, strActionSQL as String)
' Выполняем

запрос
cn.Execute strActionSQL
End Sub

Добавим, что в этом случае желательно:

  • помещать объявление переменной в стандартный модуль, поскольку данные класса копируются, а это потенциальный источник <висячих> ссылок и возникновения таинственных <фантомных> соединений;

  • контролировать корректность соединения, поскольку оно могло прерваться в процессе работы. Для этого можно использовать свойство State и событие Disconnect объекта Connection.

Команды (mand)

  • С помощью команд можно получать наборы записей. Команды могут быть следующих типов:

  • Таблица (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.Recordset
Dim cn As New ADODB.Connection
Dim

rstRes As New ADODB.Recordset 
' Подключаемся к

SQL-серверу
cn.Open "Provider=SQLOLEDB.1;Persist Security

Info=False;Data Source=SomeSQLServer"
' Открываем набор

записей
rstRes.CursorLocation = adUseClient
rstRes.Open

strQSL, cn, adOpenDynamic,

adLockOptimistic 
'Отключение
Set

rstRes.ActiveConnection = Nothing
cn.Close 
Set

GetDisconnectedRecordset = rstRes
End 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, так и внешние), грамотное использование которых повысит общую эффективность труда программиста.

Применение DataEnvironment
DataEnvironment - стандартный 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 float
AS
.....
END

и перетащили ее как сказано выше - у объекта DataEnvironment автоматически появляется метод:
SomeProcWithManyParams(param1 As Long, param2 As String, param3 as Double)

Далее для выполнения команды с необходимыми значениями параметров можно (и нужно) пользоваться именно этим методом. Кстати, подобные методы также создаются для команд типа <запрос SQL>, однако в этом случае их аргументы, соответствующие параметрам запроса, придется доопределить вручную.

Результат подобных действий выглядит примерно так, как показано на рис. 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 =

deMain
End 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 p3

push p2

push 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 (это команда "возврат" с удалением из стека &H20 байт. &H20 – это 16, а 16 – это 4*4, то есть 4 параметра по 4 байта каждый. Они все 4 байта, параметры-то). В этом можно быть уверенным у в двух случаях: вы знаете, что у функции 4 параметра или же вы сами написали некий код, завершающийся командой ret 0x0010. Чувствуете, куда клоню? ;)
    Даже если чувствуете, всё равно запомним промежуточный результат (промежуточных результатов будет несколько, и каждый имеет полное право на самостоятельное и независимое существование и использование):
    Можно напрямую использовать функцию CallWindowProc для вызова другой функции. Для этого в качестве первого параметра нужно передать указатель на эту функцию, а в качестве остальных – параметры этой, вызываемой, функции. Жёсткое ограничение: у вызываемой функции должно быть ровно 4 параметра.
Приведём маленький код в подтверждение сказанного.

Option Explicit

Private 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 Long

Private Declare Function _
FreeLibrary Lib "kernel32.dll" (ByVal _
hLibModule As Long) As Long

Private Declare Function GetProcAddress Lib "kernel32.dll"_
(ByVal _
hModule As Long, ByVal lpProcName As String) As Long

Private Declare Function LoadLibrary Lib "kernel32.dll" _
Alias "LoadLibraryA" (ByVal _
lpLibFileName As String) As Long

Private Sub Form_Load()

Dim user As Long

user = LoadLibrary("user32.dll")

CallWindowProc GetProcAddress(user, "MessageBoxA"), _
Me.hwnd, StrPtr(StrConv("Ну что, работает!", vbFromUnicode)), _
StrPtr(StrConv("Заголовок", vbFromUnicode)), 0

FreeLibrary user

End Sub

Пока не обращайте внимания на StrPtr и StrConv, потом всё скажу

И в обход…


    Вспомним те два случая, в которых можно быть уверенными Не случай номер 1 мы повлиять не можем (на самом деле, при известном шаманстве возможно всё, но я не буду соблазнять вас на модификацию самой команды Ret в теле откомпилированной вызываемой функции; да и что если параметров больше 4, а не меньше?), так что вплотную займёмся вариантом номер два.
Да, мы будем писать код. Но мы будем писать машинный код. Не бойтесь, это несложно Машинные коды, соответствующие инструкциям Push, Call и Ret легко посмотреть в соответствующих мануалах от Intel или ещё от кого. И их всего три! Поехали.
    Задача: мы должны создать в памяти (а больше негде) участок готового машинного кода, который бы завершался командой ret 0x0010, и при этом был бы способен вызывать функцию с любым количеством параметров. Тогда мы сможем передать управление на этот участочек с помощью CallWindowProc, участочек вызовет функцию, управление вернётся на участочек, а он вернёт его в CallWindowProc, выпихнув из стека правильное количество параметров (четыре, четыре...). Делов-то! Всё это должно выглядеть так:

push параметрN

push параметр(N-1)

...

push параметр2

push параметр1

call function

ret 0x0010

    Как видно, нам не удастся сделать участочек неизменяемым: он будет зависеть от количества параметров.  Где-то я прочитал, что начало функции должно быть на границе двойного слова... Честно говоря, я не знаю, так ли это, и не знаю, зачем это нужно Но я следую этому правилу, и потому память для участочка выделяю через GlobalAlloc – эта функция выделяет память сразу по искомой границе. Обращение к этой памяти реализовано через GetMem и PutMem (вы ведь знаете про них, правда?). Итак: выделяем участок памяти, заносим в нужные места этого участка команды push (это просто байт &H68), заносим после каждого байта push один параметр (четыре байта), добавляем команду Call (это байт &HE8) и команду Ret 0x0010 (это три байта: C2 10 00).

Что получаем?

Private Const GMEM_FIXED As Long = &H0
Private Const MAX_PARAMS As Long = 10

Public Function CallFunction(ByVal FuncPointer As Long, _
ParamArray p()) As Long
  Dim i As Long
  Dim hGlobal As Long, hGlobalOffset As Long
 
  'Учтём совпадение числа параметров:
  If UBound(p) - LBound(p) + 1 = 4 Then
    CallFunction = CallWindowProc(FuncPointer, _
CLng(p(0)), CLng(p(1)), CLng(p(2)), CLng(p(3)))
  Else
    hGlobal = GlobalAlloc(GMEM_FIXED, 5 * MAX_PARAMS + _
5 + 3 + 1)   'Заполняем всё подряд, ZEROINIT не нуно.
    If hGlobal = 0 Then Err.Raise 7 'insuff. memory
    hGlobalOffset = hGlobal
   
    For i = LBound(p) To UBound(p)
'если параметров нет, то ubound<lbound, и цикл не выполнится вообще
      PutMem2 hGlobalOffset, &H68 'asmPUSH_imm32
      hGlobalOffset = hGlobalOffset + 1
      PutMem4 hGlobalOffset, CLng(p(i))
      hGlobalOffset = hGlobalOffset + 4
    Next
   
    'Добавляем вызов функции
    PutMem2 hGlobalOffset, &HE8 ' asmCALL_rel32
    hGlobalOffset = hGlobalOffset + 1
    PutMem4 hGlobalOffset, FuncPointer - hGlobalOffset - 4
    hGlobalOffset = hGlobalOffset + 4
   
    PutMem4 hGlobalOffset, &H20C2&        'ret 0x0010
   
    CallFunction = CallWindowProc(hGlobal, 0, 0, 0, 0)
   
    GlobalFree hGlobal
  End If
End Function

'Вызывающий код. Я его больше не буду повторять, ладно?
Private Sub Form_Load()
  Dim user As Long
 
  user = LoadLibrary("user32.dll")
  MsgBox CallFunction(GetProcAddress_
(user, "GetWindowTextLengthA"), Me.hwnd)
  FreeLibrary user
End Sub

 

Вот и ещё один промежуточный результат. В принципе, это готовое решение для вызова чего угодно с любым числом параметров. Мы, конечно, пойдём дальше, но в такой форме оно тоже может использоваться, а порой и удобнее всего.

Далеко в обход...

Что может заставить нас идти дальше? Так это же интересно! Кому не интересно, остаётся на только что достигнутом результате, а мы продолжаем.
Что бросается в глаза - так это ParamArray. Вот если бы можно было задать его тип, чтобы не Variant... А так приходится делать CLng, да и вообще, Variant работает медленно. Жёстким волевым решением постановляем: Variant убрать, перейти на Long. Правда, придётся ввести дополнительный параметр, определяющий количество остальных параметров.
А как же удобно получать эти остальные параметры, спросит внимательный читатель, ведь теперь мы не можем обращаться к ним в цикле! Неужели многочисленные If ParamsCount < ... ? Конечно же нет! Вспомним, ведь наша "вызывалка" - это тоже функция! А раз так, она тоже подчиняется StdCall! А раз так, её параметры идут в стеке в обратном порядке! А раз так, мы можем их получить, если будем иметь "точку опоры"! А она у нас есть - это VarPtr первого параметра! Именно это великое число и показывает положение в стеке первого нашего "значимого" параметра. Последовательно прибавляя к нему 4, мы будем получать указатель на второй параметр, на третий...

В объектно-ориентированный обход...

Кто-то ещё в чём-то сомневается? Нет? Правильно. Возможно всё.
Знатоки технологии OLE наверняка уже в задумчивости. Функциональность любого объекта OLE определяется списком интерфейсов, которые реализует этот объект. А интерфейс – это что? Интерфейс – это указатель на таблицу указателей на функции. А вызывать функции по указателю мы уже худо-бедно умеем, разве нет? Следовательно, модифицировать CallFunction в CallInterface мы сможем очень легко. Для этого надо учесть лишь две вещи:
- указатель на интерфейс – это только первый шаг. Нужно найти, куда он указывает (а указывает он на таблицу функций), сместиться по этой таблице на нужное количество ячеек (одна ячейка – один указатель на метод) и посмотреть, что же в этой ячейке. Это и будет указатель на искомый метод.
- у каждого интерфейсного метода есть неявный первый параметр – указатель на сам интерфейс.
Теперь у нас снова достаточно информации Располагаем параметр pInterface непосредственно перед параметром p1, передаём ассемблерной вставочке не ParamsCount, а ParamsCount + 1, и не VarPtr(p1), а VarPtr(pInterface). Ну и указатель на функцию находим описанным выше способом. См. код!

"А зачем это нужно? Ведь VB может работать с объектами и просто так. Просто создаём объект через CreateObject, ставим точку и пишем имя метода – пусть списочка методов и нет, но VB разберётся!" VB, конечно, разберётся, но только если этот объект поддерживает интерфейс IDispatch. А если не поддерживает, то для работы с таким объектом позднее связывание (да, это оно работает, когда вы вызываете методы из ниоткуда) не работает. Для работы с таким объектом нужно иметь его описание – Type Library, библиотеку типов. Иметь её должен только разработчик, после компиляции она не нужна. Если же нет и библиотеки, то вызвать метод объекта можно только таким вот способом.
Ещё один промежуточный вариант. Теперь мы можем вызвать любой метод любого интерфейса, даже не имея его type library. Для этого нужно иметь указатель на интерфейс (это ObjPtr), а также знать номер вызываемого метода. Номера методов можно посмотреть в файлах .h или в документации. Лучше, конечно, иметь tlb, благо создать его не так уж сложно, но об этом я здесь не буду.

Слушайте! Идея пришла только что... А пусть получением параметров занимается сама ассемблерная вставочка! Во-первых, получится гораздо быстрее, во-вторых, можно будет сделать вставочку фиксированного размера (там будет цикл!). Подставляем только кол-во повторений, указатель на параметры и указатель на функцию! Ну-ка... Ну надо же... Нет, идея правда пришла только что!
И давайте, наконец-то, вынесем это дело в класс. Во-первых, у нас теперь есть необходимость в инициализации (вставочка-то теперь постоянного размера), а во-вторых, так мы сможем привязать экземпляр класса к библиотеке. Я не буду приводить полный код здесь, но в аттаче вы его найдёте и во всём разберётесь. А здесь - только обновлённый код CallFunction:

 

Public Function CallFunction(ByVal FuncName As String,_
ByRef FuncPointer As Long, ByVal ParamsCount As Long,
_ Optional ByVal p1 As Long = 0,_
Optional ByVal p2 As Long = 0, Optional _
ByVal p3 As Long = 0, _
Optional ByVal p4 As Long = 0, Optional _
ByVal p5 As Long = 0, Optional _
ByVal p6 As Long = 0, Optional _
ByVal p7 As Long = 0, Optional ByVal _
p8 As Long = 0, Optional_
ByVal p9 As Long = 0, Optional ByVal _
p10 As Long = 0) As Long
 
  'Сделаем так:
  '   - если FuncPointer = 0, то проиходит вызов FuncName, а через
  '     FuncPointer возвращается указатель на эту функцию.
  '   - если FuncPointer <> 0, то происходит вызов этого самого FuncPointer,
  '     а FuncName игнорируется.
 
  If ParamsCount < 0 Then Err.Raise 5 'invalid call
 
  If FuncPointer = 0 Then
    FuncPointer = GetProcAddress(m_hLib, FuncName)
    If FuncPointer = 0 Then Err.Raise 453 'function not found
  End If
 
  If ParamsCount = 4 Then
    CallFunction = CallWindowProc(FuncPointer, p1, p2, p3, p4)
  Else
    PutMem4 hGlobal + 1, ParamsCount
    PutMem4 hGlobal + 6, VarPtr(p1)
    PutMem4 hGlobal + 22, FuncPointer - (hGlobal + 22) - 4
    CallFunction = CallWindowProc(hGlobal, 0, 0, 0, 0)
  End If
End Function

 

Вот и ещё один промежуточный результат. Для вызова нужно явно указать число параметров, а затем и перечислить их. Параметры помещаются в стек ассемблерным циклом, поэтому их количество может быть любым. Если вам не хватит 10, добавляйте ещё (в форме "optional byval p# as long", где # - номер очередного параметра). Ни код функции, ни инициализация класса от этого не изменятся.
Большинство команд заполнения ассемблерной вставки перенесено в инициализацию класса, благодаря чему возросла скорость вызова.
Запомним и этот вариант; а какой где использовать, разберёмся в конце.

Сусанин отдыхает...

Что, можно придумать что-то ещё?! А вы как думали!Согласитесь, вызов функции через CallFunction довольно удобен, а поначалу просто шикарен... Но когда попривыкнешь, начинаешь думать о чём-то ещё более удобном... Вот если бы совместить обычный синтаксис VB с вот этим всем... Ну так в чём же дело?
Помещаем в модуль функцию. Задаём ей список параметров, соответствующий списку параметров вызываемой по указателю функции. В тело функции помещаем единственную команду – возвращение нуля. Берём адрес этой функции (AddressOf). Адрес функции – это место в памяти, где расположена первая команда функции. Так вот пишем в это место совсем другую команду, а именно команду безусловного перехода на нужный адрес – jmp. Команда jmp не меняет содержимого стека. В результате цепочка будет такой:
- мы пишем по адресу VB-функции команду jmp. Мы делаем это 1 раз, потом будем пользоваться сколько угодно
- вызываем эту VB-функцию (обычным синтаксисом VB, как обычную функцию модуля)
- VB думает, что мы правда хотим именно эту функцию, упаковывает параметры в стек (сам, без нас) и передаёт управление на свой модуль
- а там команда jmp! Сразу и без вопросов получается переход на тот адрес, который нам на самом деле нужен. Очень быстрый переход.
- управление переходит в действительно нужную нам функцию. С её точки зрения всё прекрасно: параметры упакованы в стек как нужно, а команда jmp не поменяла ни их, ни адрес возврата. Поэтому при завершении работы управление будет возвращено не в модульную функцию VB, а в то место, откуда была вызвана модульная функция
- VB вообще об этом не подозревает. Он думает, что модульная функция успешно завершилась.

Позвольте, а почему я решил, что в AddressOf функции хватит места для записи команды перехода и адреса перехода? А потому что я скомпилировал проект с пустой функцией с одним параметром, возвращающей ноль, и нашёл её в exe. Код следующий:

xor eax, eax

ret 4

Кто-то ещё сомневается в крутизне компилятора VB? Но не отвлекаемся: указанные две команды занимают ровно 5 байт. Именно столько нам и нужно. Фантастика. Даже лишних растрат памяти на функции-пустышки не будет.
Вот! Наконец-то! Вызов любых функций по указателю синтаксисом VB!
Единственная проблема – работает это только в exe по причинам, которые выходят за рамки этой статьи. Раз так, провести отладку не сможем. Потеря отладки – серьёзный недостаток, и мириться с ним я бы не стал. Но позвольте! Первые варианты прекрасно работают и в exe, и в IDE. Но они существенно медленнее и не позволяют осуществлять вызов синтаксисом VB... Но это не имеет значения, потому что мы объединим их через условную компиляцию. При работе из IDE будет использоваться медленный, но совместимый с IDE способ, а при компиляции в exe будет записан быстрый.
А как же права записи? PutMem4 тут не поможет, нет права на запись в эту часть кода. А вот WriteProcessMemory пофигист куда больший.
А как же синтаксис вызова? Он же разный? Шерстить всю программу? Ну уж нет... Тут пришлось немного постараться, но в результате вызов "совместимым" способом стал с точки зрения внешнего пользователя неотличим от вызова "несовместимым". Для настройки функции на нужный указатель используется SetFunction, а вызов настроенной функции – как всегда вызывали функцию модуля.
Логика тут такая.
Если #ReleaseBuild = False, то:
- модуль содержит коллекцию, хранящую служебную информацию, а также функцию CallFunction.
- вызов SetFunction приводит к занесению в коллекцию адреса реального вызова, ключом для которого является строковое представление адреса функции-пустышки. Переписывание тела пустышки не производится.
- Будучи вызванной, функция-пустышка извлекает из коллекции тот адрес, с которым она ассоциирована, и вызывает CallFunction по этому адресу.
- Код пустышки несколько усложняется, писать его вручную уже не с руки (об этом ниже). Не огорчайтесь, в exe попадёт единственная команда xor eax, eax...
- Да, всё это очень медленно! Но это только для отладки.

Если #ReleaseBuild = True, то:
- Модуль не содержит ничего, кроме пустышек. Пустышки не содержат ничего, кроме команды возвращения нуля.
- Вызов SetFunction приводит к прямой записи по указанному адресу, без всяких проверок (предполагается, что всё уже отлажено. Вы же не будете отлаживать в режиме ReleaseBuild = True? А раз вы всё уже отладили, зачем проверки?).
- Максимальная скорость! Серьёзно.
Вот такая вот логика
Спокойно пишем свой рулезный софт, используем вызов по указателю, всё тестируем, и т.д. и т.п. Убедившись в работоспособности, ставим ReleaseBuild = True и компилируем. Всё, опять-таки, работает, но на гораздо большей скорости.
Самые ленивые, однако, уже отметили, что придётся писать усложнённый код пустышек. Придётся! Не хотите – пожалуйста, с потерей отладки... Ну ладно, ладно В конце концов, можно переложить эту работу на IDE. Для этого нужно всего лишь написать add-in (стандартный шаблон оной есть в окне создания нового проекта). Используя события среды, надстройка будет отслеживать активное окно с кодом. Если это окно является модулем (а пустышки могут быть только там), начинается сабклассинг оного (дело в том, что у кодовых окон нет нужных нам событий). Перехватывается нажатие Enter. Если Enter нажат на строке, содержащей кодовую последовательность

#func MyFunction(paramlist)

то происходит замена этой последовательности на тело соответствующей пустышки! Paramlist – список параметров функции, по тем же правилам, что и всегда. Код пустышки будет сгенерирован с подстановкой VarPtr, ObjPtr и StrPtr в тех местах, где это необходимо. Обратите внимание – используется полное имя функции в форме ИмяМодуля.ИмяФункции. Это связано с тем, что иначе нельзя получить адрес функции из самой этой функции.

Применимость

Применять при вызове функций по указателю! Теперь чуть более подробно. Все параметры должны быть размером 4 байта. Целые числа можно передавать по значению. Целые и нецелые числа можно передавать по указателю (т.е. в качестве параметра нужно указать VarPtr).
Строки нужно передавать либо по значению (StrPtr), либо по ссылке (VarPtr). На самом деле, в обоих случаях будет передан указатель, в первом случае – на строковые данные, во втором – на переменную VB, содержащую указатель на эти строковые данные.
Объекты нужно передавать по значению (ObjPtr) или по ссылке (VarPtr). Правила те же, что и со строкой.
Неявное конвертирование из Unicode в Ansi не происходит! Поэтому если вы вызываете функцию, ожидающую Ansi, вы должны передать ей что-то вроде StrPtr(StrConv("string", vbFromUnicode)). Если же функция ожидает Unicode, передавайте StrPtr("string").
Используйте прямой вызов CallWindowProc, если у вызываемой функции ровно 4 параметра, а больше ничего вы и не вызываете. Ну бывают такие совпадения.
Используйте CallFunction с ParamArray в том случае, когда вы не хотите включать в проект модуль с пустышками, когда для вас не критична скорость, и когда вы не хотите каждый раз указывать число параметров явно.
Используйте CallFunction с явным указанием числа параметров в том случае, когда вы не хотите включать в проект модуль с пустышками, когда скорость гораздо более важна (тогда почему вы пустышки-то не хотите?) и когда вам нужна большая жёсткость при передаче параметров. В предыдущем варианте вы могли передать любое выражение в качестве параметра, а здесь – только Long. Теперь вы не забудете о том, что строки передаются через StrPtr, переменные по ссылке – через VarPtr. Иначе просто не будет компилироваться. В принципе, этот метод предпочтительнее, во избежание ошибок.
Используйте функции-пустышки, когда вы хотите вызывать что угодно по указателю синтаксисом VB или когда для вас критична скорость вызова. Не обращайте внимания на медленный вызов из IDE, он всё равно не попадёт в exe. Надстройка, следящая за вводом кодовой последовательности шаблона, будет генерировать код пустышек, подставляя VarPtr, StrPtr и ObjPtr там, где это нужно. Главное – перед компиляцией не забудьте установить ReleaseBuild = True.

И последнее

Я писал всё это в расчёте на понимающего читателя, который не будет пытаться передать пустышке Variant ByVal, или Double тем же способом. Параметры размером 4 байта все. Для работы с API этого более чем достаточно, а для передачи больших данных используются указатели (передать Variant ByRef – всегда пожалуйста). Генератор кода пустышек будет стараться исправлять ваши возможные ошибки.

Автор: Сергей Гергерт

 

Комментарии

Статья замечательная, однако, есть ряд неучтённых нюансов, которые делают предложенный метод не очень удобным.

Начнём с мелочей.

В качестве инструкции ветвления автор предлагает использовать инструкцию ветвления  - безусловный переход по относительному адресу:

jmp  rel32

Однако, очевидно, что данный метод весьма неудобен по ряду причин. Если вдруг захочется изменить адрес, придётся заново обращаться к SetFunction, а это не очень удобно. Да и зачем это делать зазря , если можно и вовсе обойтись без повторного обращения к SetFunction? Достаточно лишь использовать безусловный переход по адресу, содержащемуся в таком-то месте адресного пространства. Под <таким-то местом адресного пространства> я подразумеваю наибанальнейшую (глобальную) переменную. Ладно, от разговора на языке жестов перейдём к инструкции:

Public vCallAddress as Long, Ia() as Byte

ReDim Ia(1 to 6)

'jmp dword ptr ds:[imm32=VarPtr(vCallAddress)]

Ia(1)=&HFF: Ia(2)=&H35: PutMem4 VarPtr(Ia(3)), VarPtr(vCallAddress)

Массив объявлен условно, лишь для того, чтобы показать, как это выглядит в машинных кодах.

На самом деле, чтобы адекватно заменить инструкцию в примере Сергея необходимо учитывать, что его инструкция занимает пять байт. Она устанавливается из SetFunction в PlaceholderFor1ParamFunction вместо находящихся там после линковки инструкций

xor  eax,eax

ret  4

которые тоже занимают пять байт. А наша инструкция занимает шесть байт. Но это очень просто поправить, достаточно переписать PlaceholderFor1ParamFunction в виде

Public Function PlaceholderFor1ParamFunction(ByVal p As Long) As Long

  #If ReleaseBuild Then

    PlaceholderFor1ParamFunction = 1&

  #Else

    Dim a As Long

    On Error Resume Next

    a = mCol(CStr(ReturnMe(AddressOf
modFuncCall.PlaceholderFor1ParamFunction)))

    On Error GoTo 0

    If a Then PlaceholderFor1ParamFunction = CallFunction(a, 1, p)

  #End If

End Function

тогда при линковке туда будут записаны инструкции

mov  eax,imm32

ret  4

занимающие восемь байт, теперь перезаписываемый фрагмент будет выглядеть

ReDim Ia(1 to 8)

'jmp dword ptr ds:[imm32=VarPtr(vCallAddress)]

Ia(1)=&HFF: Ia(2)=&H35: PutMem4 VarPtr(Ia(3)), VarPtr(vCallAddress)

'nop

'nop

Ia(7)=&H90: Ia(8)=&H90

Теперь, чтобы сделать переадресацию, не нужно больше обращаться к SetFunction, а достаточно всего лишь:

vCallAddress=vNewValue

и можно опять обращаться к PlaceholderFor1ParamFunction.

 

Это были мелкие замечания, так сказать, <по процедурному вопросу>. Теперь хотелось бы поговорить об упущенных нюансах, имеющих, тем не менее, глобальный характер.

Не очень верным является мажорное высказывание автора в адрес CallWindowProc. Да, безусловно, чем-то эта функция хороша, однако, мне не кажется, что это удобный метод переадресации, тем более под IDE.

Какие будут мои аргУменты? Да очень простые! Давайте сначала вспомним, как вообще процессор <понимает> из какого места адресного пространства необходимо исполнять инструкцию. Для указания адреса исполняемой инструкции используется регистр eip. Причём, нет инструкций явных чтения/записи из этого регистра. А в качестве неявно изменяющих содержимое регистра eip используются jmp, jxx и пары call-ret, enter-leave. VB'эшные компилятор и интерпретатор обычно используют jmp, jxx и пару call-ret. Последняя, как раз, нас и интересует. Выполнение инструкции

call operand

сводится.

push eip

jmp  operand

или ещё подробнее к

mov  dword ptr ds:[esp-4],eip

sub  esp,4

mov  eip,operand

После того, как старое значение eip сохраняется в стеке (в регистре esp находится указатель на начало стека), а значение operand'а загружается в eip, процессор <сам по себе> переходит к выполнению инструкции, находящейся по этому адресу.

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

ret  optional imm16

optional imm16 используется для очистки стека, если это необходимо.
Развёрнутая интерпретация этой инструкции выглядит

add  esp,optional imm16+4

mov  eip,dword ptr ds:[esp-optional imm16-4]

Причём, если прочее содержимое стека не меняется, то место по адресу [esp-optional imm16-4] после этого уже не содержит адрес возврата. Теперь после маленького ликбеза <вернёмся к нашим баранам>.

В среде VB из любой процедуры, независимо от того IDE это или откомпилированный файл, возврат ВСЕГДА осуществляется с помощью инструкции

ret  imm16

Таким образом, если внутри нашей процедуры изменить значение <на вершине
стека>, то вместо возврата произойдёт передача управления на тот адрес,
который мы запишем по адресу, содержащемуся в esp. Но как узнать значение регистра esp, как узнать адрес начала стека? Заметьте, сам автор пишет:
<перед передачей управления в функцию : стек приобретает вид:>

Но как будто он ничего при этом не замечает. Заметьте, что первым параметром в стеке является адрес возврата! Тот самый, по которому происходит возврат из функции при выполнении инструкции

ret  imm16

А где у нас находится начало стека при передаче управления, скажем, в такую
функцию:

Private Function Test(byval vP1 as Long)as Long

:

End Function

Ну, разумеется, в откомпилированном виде начало стека будет находиться по
адресу:

vESP = VarPtr(vP1) - 4

А в IDE, спросите вы, где будет начало стека при вызове функции в IDE? Ну, здесь всё очень просто. По ряду причин, которые я не хотел бы здесь обсуждать, вызов процедуры <из под> IDE осуществляется почти так же, как осуществляется в откомпилированном виде вызов функции из чужого ActiveX (кстати, думаю, что самые сметливые уже догадались, почему так происходит в IDE). Что это значит? Да вот что: начало стека и первый параметр разделяет ещё один параметр (указатель на адреса ObjTable). То есть начало стека находится следующим образом:

vESP = VarPtr(vP1) - 8

Теперь достаточно сохранить этот адрес в <какой-нибудь> (специально для этого предназначенной) переменной

Public pRet as Long

CopyMem VarPtr(pRet), vESP, 4

после этого по адресу vESP можно записать новый (необходимый нам) адрес.

И далее, процессор <сам по себе> переходит на адрес, скажем, на следующий переключатель в маш. кодах:

Public RDa() as Byte, pRD as Long

Private Sub InitRD()

 ReDim RDa(1 to 16)

 'восстанавливаем значение регистра esp

 'sub esp,imm8=4

 RDa(1)=&H83: RDa(2)=&HEC: RDa(3)=4

 'заново прописываем адрес возврата, предварительно сохранённый нами в pRet

 'push      dword ptr ds:[imm32=VarPtr(pRet)]

 RDa(4)=&HFF: RDa(5)=&H34: RDa(6)=&H35: PutMem4 VarPtr(RDa(7)), VarPtr(pRet)

 'ну, теперь, наконец, можно перейти по интересующему нас адресу

 'предварительно записанному нами в vCallAddress

 'jmp dword ptr ds:[imm32=VarPtr(vCallAddress)]

 RDa(11)=&HFF: RDa(12)=&H35: PutMem4 VarPtr(RDa(13)), VarPtr(vCallAddress)

 pRD=VarPtr(RDa(1))

End Sub

А тело функции Test необходимо оформить так, чтобы у нас внутри неё вычислялось значение pRet и вместо адреса возврата записывался указатель на наш переключатель. Тело функции должно выглядеть следующим образом:

Private Function Test(byval vP1 as Long)as Long

 #If ReleaseBuild Then

  'вычисляем адрес начала стека

  vESP = VarPtr(vP1) - 4

 #Else

  'вычисляем адрес начала стека

  vESP = VarPtr(vP1) - 8

 #End If

 'сохраняем адрес возврата в переменной pRet

 CopyMem VarPtr(pRet), vESP, 4

 'прописываем в начало стека новый адрес pRD

 PutMem4 vESP, pRD

End Function

И всё! И не нужны никакие CallWindowProc! Всё работает и под IDE, и в откомпилированном виде.

 

Теперь поговорим немного <о Сусанине>. Если по каким-либо причинам переадресация, предложенная мною в откомпилированном виде кажется нежелательной (например, требуется большая скорость и не хочется терять даже десятка полтора тактов процессора на приведенный выше вариант, которые при частом обращении (например, в цикле!) могут сыграть значительную роль).
Тогда можно пойти как тем поправленным вариантом предложенным выше мною, так и авторским. И всё же. Если уж многократное обращение происходит в цикле (а <потерянное> время бывает критичным только в этом случае), тогда можно извратиться несколько больше, чем предложил автор.

Дело в том, что для обращения к какой-либо <собственной> функции используется стандартная инструкция:

call rel32

Если взять авторский вариант, то у нас получится последовательность вызовов

call rel32=Offset PlaceholderFor1ParamFunction

jmp  rel32=Offset <Вызываемая функция>

или в поправленном мною выше варианте

call rel32=Offset PlaceholderFor1ParamFunction

jmp  dword ptr ds:[imm32=VarPtr(vCallAddress)] функция>

Ну, почему бы нам вообще не избавиться от <лишнего> jmp, коли уж так сильно поджимает нас время? И избавимся! Да ещё и будем ходить по условному адресу.
Для этого немного напишем функцию переключателя

#If ReleaseBuild Then

Private Ra() as Byte

Public Function ReDirect(Optional ByVal vP0 As Long, Optional ByVal vP1 As
Long) As Long

 vESP=VarPtr(vP0)-4

 CopyMem VarPtr(pRet), vESP, 4

 ReDim Ra(1 to 7)

 'call      dword ptr ds:[imm32=VarPtr(vCallAddress]

 Ra(1)=&H3E: Ra(2)=&HFF: Ra(3)=&H25: PutMem VarPtr(Ra(4)),
VarPtr(vCallAddress)

 WriteProcessMemory GetCurProcess, pRet-7, VarPtr(Ra(1)), 7, 0&

 PutMem4 vESP, pRD

End Function

#End If

а обращаться к этой функции будем

vCallAddress=vNewAddress

vReturnValue=ReDirect(,vParamValue)

<концовка> подобного обращения (из любого места VB-программы!) в маш. кодах выглядит

push imm8=0

call rel32

что занимает семь байт. Но инструкция

call dwrod ptr ds:[imm32]

На самом деле занимает шесть байт. Что делать? Не вставлять же nop'ы в конце? Ведь это опять получится дополнительная, хотя и пустая (один такт) операция процессора. Есть простое решение указанная выше инструкция по умолчанию адресуется относительно регистра ds, достаточно приписать перед этой инструкцией <уточняющий> префикс &H3E, который будет делать тоже самое, но в явном виде. Что в итоге? В итоге имеем те самые семь байт.

Что же на самом деле делает функция ReDirect? При обращении к этой функции, она вычисляет адрес возврата уже описанным выше способом. Затем меняет предшествующие адресу возврата семь байт. Таким образом, если обращение из этого места происходит либо в цикле, либо просто часто, то в следующий раз обращение по нужному адресу (vCallAddress) будет происходить прямо оттуда!
Кстати говоря, несмотря на то, что функция ReDirect, вроде бы имеет два параметра, при повторных обращениях задержки из-за пустого параметра всё равно не происходит, так как

push imm8=0

мы затираем при первом обращении.

Есть ещё один не менее изящный, на мой взгляд, вариант.

#If ReleaseBuild Then

Private REa() as Byte

Public Function REx(ByVal vP0 As Long, ByVal vP1 As Long) As Long

 vESP=VarPtr(vP0)-4

 CopyMem VarPtr(pRet), vESP, 4

 ReDim REa(1 to 5)

 'pop       eax

 REa(1)=&H67: REa(2)=&H38

 'call      eax

 REa(3)=&H67: REa(4)=&HFF: REa(5)=&HD0

 WriteProcessMemory GetCurProcess, pRet-5, VarPtr(REa(1)), 5, 0&

 PutMem4 vESP, pRD

End Function

#End If

В этом случае обращение к функции REx должно выглядеть следующим образом:

vReturnValue=REx(vCallAddress,vParamValue)

То есть теперь инструкция

call rel32

заменяется инструкциями

pop  eax

call eax

Как мы помним инструкция

call rel32

занимает пять байт, в то время, как наша замена - всего три. Как и выше, <недостающие байты> мы <добираем> за счёт <уточняющих> префиксов. В данном случае это префикс &H67. Его <безболезненно> можно ставить перед любой из наших инструкций. В итоге получаем искомые пять байт. Заметьте, что последний вариант при передаче управления по условному адресу выполняется максимально быстро. Если же, мы знаем, что обращение будет происходить на один и тот же адрес, то максимально быстро будет работать вот такой вариант

#If ReleaseBuild Then

Public vNewAddress as Long

 

Private Function GetPA(ByVal vProcAddress as Long)as Long

 GetPA=vProcAddress

End Function

 

Public Function ReD(ByVal vP1 As Long) As Long

 vESP=VarPtr(vP0)-4

 CopyMem VarPtr(pRet), vESP, 4

 'до обращения в vNewAddress должен содержаться <абсолютный> адрес

 'после вычисления там будет содержаться относительный

 vNewAddress=pRet-GetPA(AddressOf ReD)+vNewAddress

 WriteProcessMemory GetCurProcess, pRet-4, VarPtr(vNewAddress), 4, 0&

 PutMem4 vESP, pRD

End Function

#End If

Теперь обращение к этой функции обычное.

vNewAddress=<адрес вызываемой процедуры>

:

vReturnValue=ReD(vParamValue)

А что происходит в функции ReD? Просто в инструкции

call rel32

перезаписывается само значение rel32.

 

Вот, собственно говоря, и все замечания. В прочих случаях, не требующих столь изощрённых методов оптимизации предлагаю пользоваться переадресацией альтернативной (если честно, то не очень-то поворачиваются пальцы на клавиатуре их сравнивать) CallWindowProc.

Техника программирования сложных окон в Visual Basic

Введение

Mногие из Вас наверняка видели в Windows программах окна нестандартной формы (круглые, треугольные и т.д.) и задавали себе вопрос: как мне сделать такое окно? Если прочитать документацию по Visual Basic, то можно сделать вывод, что стандартные средства языка не предоставляют такой возможности. А что же делать, если очень хочется? Тогда следует вспомнить, что в распоряжении программиста на VB есть еще и Windows API, который должен нам в этом помочь.

Теоретические основы

Для начала давайте разберемся, как это можно сделать теоретически. Из документации Windows видно, что каждое окно в системе описывается множеством параметров, из которых нас с Вами интересует <видимая область окна>. Видимая область окна в системе, создаваемое Visual Basic <по умолчанию> имеет вид прямоугольника, но, в принципе, ничто не мешает изменить форму этой области. Данная область окна описывается с помощью специального объекта, который называется Region. Регион можно представить в виде поверхности, ограниченной координатами, описываемыми угловые точки этой области. Проще говоря, можно описать область любой формы, затем создать из неё, с помощью специальных функций, регион и <прикрепить> его к нужому нам окну.

Существует несколько функций Windows API для создания регионов, основными из которых являются следующие:

  • CombineRgn - Комбинирует два региона между собой

  • CreateEllipticRgn - Создает регион в виде эллипса или окружности

  • CreatePolygonRgn - Создает регион в виде многоугольника

  • CreateRectRgn - Создает прямоугольный регион

  • CreateRoundRectRgn - Создает регион со скругленными краями из прямоугольной области

  • SetWindowRgn - Прикрепляет регион к указанному окну

Я не буду приводить подробное описание этих функций, так как его можно найти в описании Win32 API. Кроме этих функций существуют ещё несколько функций для работы с регионами, но нам они не потребуются.

Создание простых нестандартных окон

Теперь, когда нам известны основные функции, для создания регионов, мы можем применить полученные знания на практике. Загрузите проект pTestRgn и внимательно изучите его код. В этом проете, для изменения формы окна на овальную, используется всего три строки кода и три функции Win32 API. Вначале с помощью CreateEllipticRgn создается регион, затем он прикрепляется к окну и, наконец, завершающая фаза удаление, ставшего ненужным, созданного нами региона. Если же Вы не удалите ненужный Вам больше объект, то Windows, создав регион для Вас будет хранить его в своих <недрах> и ждать дальнейших указаний по его использованию. В общем, нехорошо <захламлять> выделенную память, и настигнет Вас кара небесная, и затянется небо тучами синими, и будет страшный суд над всеми неверующими: Короче код выглядит так:

Private Sub cmbCreateOval_Click()

Dim lRgn As Long

lRgn = CreateEllipticRgn(0, 0, Me.ScaleWidth / Screen.TwipsPerPixelX, _

Me.ScaleHeight / Screen.TwipsPerPixelY)

SetWindowRgn Me.hwnd, lRgn, True

DeleteObject lRgn

End Sub

Так же всё просто, скажете Вы? Да, на первый взгляд всё очень просто, но это только кажется. Тот пример, который Вы только что видели, почти не имеет практического применения в настоящих приложениях Windows. Кому же нужно просто овальное окно, которое к тому же жестко задается на этапе программирования? А вот окно, которое свободно могло бы менять свою форму вполне может потребоваться. Примеры? Пожалуйста, WinAmp, Помощник в Microsoft Office и другие программы. Как же там всё это реализовано? Давайте разберемся с таким применением регионов.

Создание сложных нестандартных окон

Допустим, что у нас есть рисунок в BMP формате, из которого нужно сделать форму, а белый цвет (например) на нём означает <пустоту>. Как же сделать форму? Очень просто, нужно взять все <не белые> пиксели на рисунке, создать из их координат регион и прикрепить его к нужному нам окну. Анализировать пиксели можно GetPixel, эта функция по координатам возвращает его цвет. Давайте теперь напишем такой алгоритм для анализа BMP матрицы. Я думаю, что такой алгоритм Вам известен, и мы не будем его подробно разбирать, отмечу только, что анализ производится построчно и Pixel-и добавляются в регион не по одному, а группами построчно. Такой подход сильно экономит ресурсы процессора, выигрыш в производительности достигает 100%.

Public Function lGetRegion(pic As PictureBox, lBackColor As Long) As Long

Dim lRgn As Long

Dim lSkinRgn As Long

Dim lStart As Long

Dim lX As Long

Dim lY As Long

Dim lHeight As Long

Dim lWidth As Long

'создаем пустой регион, с которого начнем работу

lSkinRgn = CreateRectRgn(0, 0, 0, 0)

With pic

'подсчитаем размеры рисунка в Pixel

lHeight = .Height / Screen.TwipsPerPixelY

lWidth = .Width / Screen.TwipsPerPixelX

For lX = 0 To lHeight - 1

lY = 0

Do While lY < lWidth

'ищем нужный Pixel

Do While lY < lWidth And GetPixel(.hDC, lY, lX) = lBackColor

lY = lY + 1

Loop

If lY < lWidth Then

lStart = lY

Do While lY < lWidth And GetPixel(.hDC, lY, lX) <> lBackColor

lY = lY + 1

Loop

If lY > lWidth Then lY = lWidth

'нужный Pixel найден, добавим его в регион

lRgn = CreateRectRgn(lStart, lX, lY, lX + 1)

CombineRgn lSkinRgn, lSkinRgn, lRgn, RGN_OR

DeleteObject lRgn

End If

Loop

Next

End With

lGetRegion = lSkinRgn

End Function

Итак, для проверки на практике этого алгоритма загрузите пример pTestRgnSkin и внимательно изучите его код. В этом проекте нужный нам рисунок, для удобства, <зашит> в файле ресурсов, кроме того проект запускается процедурой Main, в которой и происходят все преобразования. Вначале загружается форма, затем в PictureBox из ресурсов загружается нужный нам рисунок, далее вызывается функция, которая создает регион и, наконец, завершающий этап - прикрепление региона к нужному нам окну. Для удобства здесь же вызывается функция, помещающая окно <поверх всех>, чтобы оно <не потерялось> у Вас на рабочем столе Windows. Кроме того, для нормальной работы программы необходимо, чтобы для PictureBox свойство AutoRedraw было установленно в True, иначе ничего не получится.

Sub Main()

Dim lRgn As Long

Load frmTestRgnSkin

frmTestRgnSkin.pic.Picture = LoadResPicture(101, vbResBitmap)

lRgn = lGetRegion(frmTestRgnSkin.pic, vbWhite)

SetWindowRgn frmTestRgnSkin.hWnd, lRgn, True

DeleteObject lRgn

frmTestRgnSkin.Show

SetFormPosition frmTestRgnSkin.hWnd, True

End Sub

Теперь можно запускать проект... О, знакомое лицо, скажите Вы, это же <Скрепыш> из Microsoft Office. Да, похож, но не совсем, <Скрепыш> двигается, а этот нет. Что же нужно сделать, чтобы это окно динамически изменяло свою форму по рисунку, отображаемому в данный момент времени в PictureBox?

Динамическое изменение формы окна

Существуют программы в которых необходимо динамически во время работы изменять форму окна (например анимированный персонаж из Microsoft Office). Все это не очень сложно реализовать, нужно в событие PictureBox.Change добавить следующий код:

lRgn = lGetRegion(frmTestRgnSkin.pic, vbWhite)

SetWindowRgn frmTestRgnSkin.hWnd, lRgn, True

DeleteObject lRgn

SetFormPosition frmTestRgnSkin.hWnd, True

В принципе всё готово, осталось только добавить код для изменения картинки на форме, и <Скрепыш> оживёт. В нашем примере изменять рисунок будем в Timer циклически, т.е. анимация будет непрерывна, так проще. Итак, добавим на форму Timer и поместим <в него> небольшой код, отвечающий за изменения рисунка в PictureBox. Рисунков в файле ресурсов десять штук, поэтому I должно изменяться от 101 до 110. Код изменения выглядит так:

Static i As Long

If i < 101 Then i = 101

If i > 110 Then i = 101

frmAnimateForm.pic.Picture = LoadResPicture(i, vbResBitmap)

i = i + 1

Готово, можно запускать проект, и если Вы счастливый обладатель Pentium III или Athlon, то Вам улыбнется удача, так как <Скрепыш> будет двигаться. Но если Ваш процессор Pentium II и ниже, то компьютер не сможет выполнять необходимые расчеты за нужное нам время, так как для плавной анимации необходимо (для нашего случая) показывать порядка 15 кадров в секунду, а точнее каждые 80 милисекунд по кадру и ещё оставлять время для других задач компьютера. Как мы видим наши алгоритмы явно не тянут для таких задач и предназначены для <работ> не требующих таких быстрых изменений формы окна, так как, например на Celeron 333 один кадр формируется около 100 милисекунд. Что же делать?

Оптимизация алгоритма для быстрой анимации

Анализ работы алгоритма показывает, что наибольшие затраты времени приходятся на функцию GetPixel. Это происходит потому, что анализ картинки идет непосредственно на экране. Единственный путь увеличения быстродействия алгоритма, это перенос анализа в память компьютера и использование при этом Win 32 API. Такие алгоритмы существуют, но это тема отдельного разговора, скажу только, что для оптимизации работы алгоритм пишется отдельно для каждой глубины цвета и при применении такой схемы быстродействие увеличивается почти в четыре раза и позволяет делать практически любую анимацию.

Visual Basic и Системный Реестр Windows

Системный Реестр (Registry) Windows - это база данных для хранения системной и пользовательской информации в Windows. Системный Реестр состоит из двух файлов - SYSTEM.DAT, в нем содержится информация о конкретном компьторе, и USER.DAT, содержащий пользовательские установки и данные.
Для работы с Системным Реестром обычно применяют специальный редактор Системного Реестра - REGEDIT.EXE, или специальные функции Win32 API, или некоторые встроенные функции и операторы Visual Basic.
Остановимся на последнем и рассмотрим работу с данными в Системном Реестре с помощью Visual Basic. Для этого Вам придется сделать следующее:

  • Запустите редактор Системного Реестра, он находится в каталоге Windows - C:\Windows\Regedit.exe. При запуске редактора в окне появятся шесть ключей-папок (эти ключи как раз и представляют содержимое файлов SYSTEM.DAT и USER.DAT). Выберите ключ HKEY_CURRENT_USER (этот ключ входит в состав файла USER.DAT) и щелкните на плюсике "+". В раскрывшемся списке выберите подключ - Software - и раскройте его. Найдите во вновь открывшемся списке папку - VB and VBA Program Setting - это и есть раздел Системного Реестра куда с помошью встроенных операторов и функций Visual Basic Вы можете записывать и считывать необходимую Вам информацию.

  • Запустите Visual Basic и откройте новый проект. Поместите на форму четыре командные кнопки и в свойстве Caption" , каждой из них введите соответственно: SaveSetting, GetSetting, GetAllSettings, DeleteSetting. Запомните проект под именем, скажем, MYREG.


А теперь приступим к работе.

Запись значений с помощью оператора SaveSetting

Дважды щелкните на командной кнопке SaveSetting и в раскрывшемся окне введите следующий код:

Private Sub Command1_Click()
SaveSetting App.Title, "PortSettings", "Connect using", "COM1"
End Sub

Запустите программу на выполнение и нажмите на кнопку SaveSetting, потом переключитесь на окно редактора Системного Реестра. Откройте папку VB and VBA Program Setting и там Вы увидите вновь созданную папку MYREG, а в ней подраздел с именем PortSettings. Открыв его, в левом окне редактора, Вы прочтете запись которую только что создали - Connect using   "COM1".

***Если Вы правильно выполнили все что написано, но ничего не увидели, не волнуйтесь. Нажмите клавишу F5 (Refresh) и перед вами появится искомая запись.

Теперь расмотрим по-подробнее, что Вы написали в окне кода. Синтаксис оператора SaveSetting следующий:
SaveSetting VBKeyName, Section, Key, Setting, где:

  • SaveSetting - имя самого оператора;

  • VBKeyName   - строковое значение, которое является именем внутреннего подраздела VB and VBA Program Setting. В нашем случае онo будет носить имя нашей программы - MYREG, т.к. мы использовали свойство объекта App.Title;

  • Section     - строковое значение, которое является именем внутреннего подраздела VBKeyName. В нашем случае оно носит имя PortSettings;

  • Key    - строковое значение, которое представляет имя параметра в созданном подразделе PortSettings.В подразделе может быть много параметров.

  • Setting    - строковое значение, которое Вы хотите присвоить данному параметру. В нашем случае параметру Connect using приваиваем значение COM1;

Чтение значений с помощью функции GetSetting()

***При выполнении нижеследующих действий объявите в разделе Declaration формы переменные CrtMsg As String, MySet As Variant

Чтобы получить значение определенного параметра, нужно использовать функцию GetSetting().Дважды щелкните на командной кнопке GetSetting и в раскрывшемся окне введите следующий код:

Private Sub Command2_Click()
MySet = GetSetting(App.Title, "PortSettings", "Connect using", CrtMsg)
Debug.Print MySet
End Sub

Запустите программу на выполнение и намите на кнопку GetSetting. В окне Immediate Вы увидете введенное ранее (оператором SaveSetting) значение - COM1.
Теперь расмотрим подробнее, что Вы написали в окне кода. Синтаксис оператора GetSetting следующий:
MySet = GetSetting ( VBKeyName, Section, Key [, Default ] )
 

  • MySet   - строка для хранения возвращаемого функцией GetSetting значения;

  • GetSetting - имя самой функции;

  • VBKeyName   - строковое значение, которое является именем внутреннего подраздела VB and VBA Program Setting. Мы используем свойство объекта App.Title, т.к. хотим прочесть информацию из созданного нашей программой раздела ;

  • Section     - строковое значение, которое является именем внутреннего подраздела VBKeyName. В нашем случае он носит имя PortSettings;

  • Key    - строковое значение, которое представляет имя параметра в созданном подразделе PortSettings.В подразделе может быть много параметров и по-этому, мы указываем на желаемое - Connect using;
    Default     - необязательный аргумент, представляющий строковое значение, которое будет возвращено функцией в случае ошибки (если такого параметра нет). Функция возвращает строковое значение, присвоенное аргументу Default. К примеру CrtMsg = "Такого параметра не существует.".

Чтение значений с помощью функции GetAllSettings()

***Предварительно добавте следующий код к имеющемуся коду командной кнопки
SaveSetting:
SaveSetting App.Title, "PortSettings", "Data bits", "8"
SaveSetting App.Title, "PortSettings", "Parity", "None"
SaveSetting App.Title, "PortSettings", "Stop bits", "1"
SaveSetting App.Title, "PortSettings", "Flow control", "None"
Произведите операцию записи данных в Системный Реестр.

Для получения из реестра массива, содержащего все значения параметров из определенного подраздела (например, PortSettings) применяется функция GetAllSettings(). Для этого дважды щелкните на командной кнопке GetAllSettings и в раскрывшемся окне введите следующий код:

Private Sub Command3_Click()
Dim intSettings As Integer
MySet = GetAllSettings(App.Title, "PortSettings")
  
For intSettings = LBound(MySet, 1) To UBound(MySet, 1)
     
Debug.Print MySet(intSettings, 0), MySet(intSettings, 1)
  
Next intSettings
End Sub

Запустите программу на выполнение и нажмите на кнопку GetAllSettings. В окне Immediate Вы увидете введенные ранее (оператором SaveSetting) значения:

Connect using      COM1
Bits per second    2400
Data bits          8
Parity             None
Stop bits          1
Flow control       None

Теперь расмотрим подробнее, что Вы написали в окне кода. Синтаксис оператора GetAllSetting следующий:

MySet = GetAllSettings ( VBKeyName, Section)

  • MySet   - возвращаемый функцией массив значений, он должен быть типа Variant

  • GetSetting - имя самой функции;

  • VBKeyName   - строковое значение, которое является именем внутреннего подраздела VB and VBA Program Setting. Мы используем свойство объекта App.Title, т.к. хотим прочесть информацию из созданного нашей программой раздела ;

  • Section     - строковое значение, которое является именем внутреннего подраздела VBKeyName. В нашем случае он носит имя PortSettings;

Остальные операторы помогают обработать массив значений и получить информацию в удобном виде.

Удаление раздела параметров с помощью оператора DeleteSetting

Вы уже создали целый массив параметров! А что делать если он больше ненужен? Для удаления параметров применяется оператор DeleteSetting. Дважды щелкните на командной кнопке DeleteSetting и в раскрывшемся окне введите следующий код:

Private Sub Command4_Click()
DeleteSetting App.Title,"PortSettings", "Flow control "
End Sub

Запустите программу на выполнение и намите на кнопку DeleteSetting, потом переключитесь на окно редактора Системного Реестра и Вы увидете, что параметр Flow control отсутствует.

***Если Вы правильно выполнили все что написано, но ничего не увидели, не волнуйтесь. Нажмите на клавишу F5 - Refresh.

Теперь расмотрим подробнее, что Вы написали в окне кода. Синтаксис оператора DeleteSetting следующий:

DeleteSetting VBKeyName, Section, Key , где:

  • DeleteSetting - имя самого оператора;

  • VBKeyName   - строковое значение, которое является именем внутреннего подраздела VB and VBA Program Setting. Мы используем свойство объекта App.Title, т.к. хотим удалить информацию из созданного нашей программой  раздела ;

  • Section     - строковое значение, которое является именем внутреннего подраздела VBKeyName. В нашем случае он носит имя PortSettings;

  • Key    - строковое значение, которое представляет имя параметра в подразделе PortSettings.В подразделе может быть много параметров и по-этому, мы указываем на желаемое - Flow control;


Если Вы захотите удалить сразу весь подраздел, а не некоторые параметры, то для этого используйте следующий синтаксис оператора DeleteSetting:

 DeleteSetting VBKeyName, Section ;

Поместив код DeleteSetting App.Title,"PortSettings" в Private Sub Command4_Click(), Вы удалите весь подраздел PortSettings.

А если Вы захотите убрать вообще все Ваши установки из Сиастемного Реестра, то воспользуйтесь следующим кодом:

Private Sub Command4_Click()
DeleteSetting App.Title
End Sub

***Внимание!!! Пользуйтесь оператором DeleteSetting очень осторожно! 

1

Смотреть полностью


Скачать документ

Похожие документы:

  1. Работа с базами данных

    Документ
    База данных — это организованная структура, предназначенная для хранения информации. Внимательный читатель, знающий из первой главы этого пособия о том, что данные и информация — понятия взаимосвязанные, но не тождественные, должен
  2. Задачи: Изучить среду Visual Basic; Подобрать задачи исследовательского характера для визуализации экспериментального процесса; Разработать программы и исследовать информационные процессы

    Исследовательская работа
    Visual Basic; (VB) - это среда разработки программ, которая позво­ляет быстро и легко создавать приложения (прикладные программы) для Windows. В нее включено все, что необходимо для создания, модифика­ции, тестирования, корректирования
  3. «Обработка баз данных при помощи Microsoft Access»

    Документ
    Когда необходимо хранить и использовать большие объемы данных, необходим инструмент для простой и быстрой обработки информации. В семействе Microsoft Office таким инструментом является MS Access.
  4. Базы данных (2)

    Документ
    В основе решения многих задач лежит обработка информации. Для облегчения обработки информации создаются информационные системы (ИС). Автоматизированными называют ИС, в которых применяют технические средства, в частности ЭВМ.
  5. Visual basic for applications (vba)

    Документ
    Visual Basic является мощным программным средством, с помощью которого можно реализовать широкий спектр прак­тических задач. Основное достоинство этого языка програм­мирования состоит в том, что он оптимально сочетает в себе простоту

Другие похожие документы..