Кудицкий Дмитрий
НЕКОТОРЫЕ АСПЕКТЫ ПЕРЕХОДА С VB5/6 НА VB.NET.
Скачать исходный код к статье
Обсудить в форуме
Приняв решение о переходе на новую платформу хочется сохранить, приобретенные навыки программирования и возможность использовать унаследованный кода. Код, полученный при создании управляемых сборок, может работать в мире COM (служба RCW), а так же вызывать типы из DLL, написанных на неуправляемом С (служба Pinvoke, пространство System.Runtime.InteropServices). Все это платформа .NET делает так хорошо, что Вы вполне можете использовать VB.NET в стиле VB, почти ничего не зная о премудростях новой платформы программирования от Microsoft. Данной теме посвящена первая часть статьи.
В дальнейшем при желании использовать новые объектно-ориентированные возможности, которые были добавлены в язык VB (первые версии C++ назывались – “C с классами” по аналогии можно сказать, что VB.NET это VB c классами) можно воспользоваться многочисленной литературой по данной теме. Я знакомился с темой ООП на VB по книжке “Программирование на VB.NET: учебный курс.” Гэри Корнел, Джонатан Моррисон – “Питер”2002. Основная направленность книги это ООП применительно к VB. Авторы в разделе благодарности указывают, что Дэн Эпплман их друг – это гарантия качества книги. Вторая часть статьи написана мной после прочтения главы 6, вышеназванной книги и несколько расширяет тему делегатов.
Платформа .NET (подразумевается .NET Framework = CLR + FCL при CLR = CLR/CTS) на данный момент не имеет встроенных средств для работы с трехмерной графикой. Вы не найдете пространства имен типа System.Drawing.Drawing3D. Придется использовать DirectX, реализация которого построена на использовании COM интерфейсов. Обратиться к COM из .NET Вам поможет служба RCW, которая играет роль мостика между этими технологиями. Служба RCW возьмет на себя всю черновую работу (управление ссылками AddRef()/Release(), добыча указателей на интерфейсы QueryInterface(), скрытие низкоуровневых интерфейсов – все, что раньше делал VB5/6).
Сейчас мы создадим приложение на VB.NET, использующие Direct3DRM3 для вывода в своем окне вращающегося логотипа “VB.NET” синего цвета (файл проекта см. папку BitBlitWinDXVB, в папке BitBlitWinDX находится близнец написанный на C#, который выводит логотип “C#” зеленого цвета).
- Создайте приложение типа Windows Application.
-
Теперь необходимо создать буфер (промежуточный класс) для обращения к COM серверу. Выполните команду Project/AddReference, на вкладке COM укажите путь к библиотеке типов:
В окне Solution Explorer автоматически должна появиться ссылка DxVBLib, а в каталоге где находится исполняемый файл, после компиляции появится файл Interop.DxVBLib.dll
-
Если открыть файл Interop.DxVBLib.dll в утилите ILDasm.exe, то Вы увидите эквиваленты типов COM, доступных для вызова в .NET.
Найдите DirectX7Class, если создать объект этого класса Вы получите универсальный указатель (указатель на указатель) через который мы и будем действовать – предоставление этого указателя и есть работа службы RCW. Скопируйте код из проекта в папке BitBlitWinDXVB, в свой проект и сравните с кодом из статьи посвященной программированию DirectX на VB5/6.
Примечание. Статья “Быстрое создание графических приложений с использованием Win32 GDI и Direct3D” находится на сайте http://www.vbstreets.ru в разделе посвященном VB5/6.
Надеюсь пояснения излишни – оба кода почти идентичны.
Точно таким же образом можно получить доступ к другим унаследованным от технологии COM объектам – например ADO/DAO. Посмотрите на окно AddReference до и после инсталляции на компьютер VB6 (.NET и Visual Studio .NET были установлены первыми). Появились такие привычные объекты DAO. Зная объектную модель ADO/DAO и применив службу RCW можно писать базы данных на VB.NET как на VB5/6.
Приведя пример приложения DirectX с трехмерной графикой я не планировал приводить пример использующий DirectDraw, но неожиданно при изучении GDI+ оказалось, что новейший API не поддерживает XOR. Действительно вызов популярных блиттеров (BitBlit(); StretchDIBits()) старого GDI ни к чему не привел. Значит полноценная анимация невозможна? Ниже приводятся два приложения решающие эту задачу. Первое GDI+, а второе DirectDraw. Начнем с GDI+ и попробуем решить проблему анимации, что бы было интересней напишем ресурсную DLL на C# (в DLL будет храниться картинка). Написать такую же на VB.NET элементарная задача.
- Создайте на C# приложение типа Class Library
-
С помощью команды Project/Add Existing Item добавьте в проект файл “kylie.jpg”. Теперь самое главное – графический файл в окне Properties должен быть помечен, как Embedded Resource.
- Добавьте следующий код в проект (файл проекта см. папку BitRes):
using System;
using System.Drawing;//не забудьте через команду Project/AddReference
//добавить ссылку на System.Drawing.dll
namespace BitRes
{
public class BitRes
{
public Bitmap MKylie;
public BitRes()
{
MKylie = new Bitmap(GetType(),"kylie.jpg");
}
}
}
-
После компиляции появится файл BitRes.dll. К этой DLL может обращаться код, написанный на VB.NET или C#.
Переходим к созданию приложения которое сможет загружать из BitRes.dll, встроенную в нее картинку.
-
Создайте на VB.NET приложение типа Windows Application (файл проекта см. папку BitBlitVB, в папке BitBlit находится близнец написанный на C#). После Public Class Form1 вставьте объектную переменную Dim BitfromRes As New BitRes(). В событие Form1_Paint() вставьте код:
Private Sub Form1_Paint(ByVal sender As Object, _
ByVal e As System.Windows.Forms.PaintEventArgs) Handles MyBase.Paint
Dim g As Graphics
g = e.Graphics
g.DrawImage(BitfromRes.MKylie, 5, 5)
End Sub
-
Вызовете окно для установок текущего проекта и в поле Root namespace введите – BitRes (посмотрите на строчку namespace BitRes из проекта создания DLL).
После компиляции картинка из DLL, написанной на C# должна появится на форме. Скопируйте код из папки BitBlitVB в Ваше приложение. Обратите внимание на вызовы функций старого GDI с применением атрибутов . Для их вызова необходимо включить пространство System.Runtime.InteropServices (служба Pinvoke). Эти функции рисуют прямоугольники на заголовке формы, но с помощью этого метода не получится вызвать BitBlit(); StretchDIBits() и использовать XOR (вызвать можно, но это ничего не даст). Внимательно изучите код в событии Form1_Load() в цикле FOR. Вся суть в применении функций GetPixel()/SetPixel() – с начало читаются данные о пикселях из объекта bit1(создан на основе файла), если пиксель белый он переписывается в объект bit2 (создан как битовая карта в памяти с форматом пикселей PixelFormat.Format32bppArgb) как прозрачный – был белый стал прозрачный.
Самое главное заключено в строчке bit3 = BitfromRes.Mkylie из события Button1_Click(). Данное присвоение, это ошибка и теперь bit3 указывает не на “свою память”, а на “чужую”. В профессиональном программировании на C++ таким ошибкам уделяется очень большое значение (поверхностное/полное копирование), но эта тема относится к тонкостям ООП (жизненный цикл объекта, удаление ресурсов итп – можно почитать Д.Рихтера “Программирование на платформе .NET”).
Графически ошибка выразится в следующем - после загрузки приложения подвигайте флажком не нажимая кнопку “Сохранить” и после сохранения (более подробно ошибка обсуждается во второй части статьи).
В данной версии анимация сопровождается заметным “дрожанием” из за вызова перерисовки для ограничивающего прямоугольника Invalidate(MyRect). Если Вас сильно раздражает дрожание можно воспользоваться DirectDraw, который обращается к самой видеокарте и ему не указ есть в GDI+ блиттер или нет. Загрузите файл проекта (см. папку BitBlitWinDDVB) сравните работу двух приложений по изменению прически известной артистки. В версии DirectDraw все происходит гладко и красиво.
Переходим ко второй теме статьи – делегатам. В версиях VB5/6 были функции, структуры, перечисления, циклы и даже классы. Все, что не относится к классам не сильно изменилось (привыкнуть, что массив начинается не с “1”, а с “0” не является подвигом). По объектно-ориентированным возможностям языка имеется довольно много литературы, а теме указателей (а делегаты это объектно-ориентированная оболочка вокруг указателей на функции) уделено недостаточно внимания. Хотя средств для объявления указателя в .NET формально НЕТ (каламбур) объяснить, что такое передача по ссылке или по значению или почему модель событий основана на делегатах(они же указатели на функции) довольно трудно, без понятия указатель.
Тема указателя уходит корнями к описанию данных на ассемблере. Все программы предназначены для обработки данных, которые хранятся в некоторой области памяти. Для доступа к этой памяти и загрузки ее содержимого в регистры процессора необходимо знать “дорогу” к данным. Для резервирования в памяти 4х байтов на ассемблере используется директива dd – например MyVar dd 0000000f резервирует четыре байта (сегменты, регистры, организация доступа к памяти, порядок размещения данных не рассматриваются – важен только принцип) и записывает число 15 в память (f = 15). Эквивалентами такой операции в языках высокого уровня являются следующие определения переменных – VB5/6 = Dim MyVar As Long/VB.NET = Dim MyVar As Integer/C++ = long int MyVar;/C# = int MyVar;, запись значения MyVar = 15.
Следующее объявление переменной на ассемблере определяет указатель MyVar1 dw MyVar (dw два байта - короткий указатель без сегмента, современные указатели в 32 разрядных системах имеют размер как правило четыре байта). Эквивалент такого объявления на С++ long int *MyVar1; MyVar1 = & MyVar;. В VB5/6 операции с указателями скрыты от программиста и выполняются не явно. Данный процесс представлен на рисунке.
Указателем называется ячейка памяти в которой хранится адрес переменной в которой хранятся, необходимые нам данные.
Адресом в общем случае называется смещение с начала памяти до нужных нам данных (напомню, что рассматривается только концепция на самом деле все сложнее). Смысл данная концепция приобретает при передаче переменных функциям и выделении памяти для классов. Рассмотрим случай передачи параметров функции. Напишем две простых функции на VB.NET:
Public Function add(ByVal MyVar As Integer) As Integer
MyVar1 = MyVar1 + 1
Return MyVar1
End Function
Public Function add(ByRef MyVar As Integer) As Integer
MyVar1 = MyVar1 + 1
Return MyVar1
End Function
В первом случае параметры объявлены с ключевым словом ByVal, а во втором с ByRef. В первом случае параметр передается по значению во втором по ссылке. В каждой программе имеется область памяти называемая стек. В программах на ассемблере такую область память надо указывать явно, в программах на VB.NET об этом заботиться не надо (в стиральных машинках и сотовых телефонах устанавливаются восьмиразрядные микроконтроллеры - в них тоже есть стек ). Стек это просто область памяти (точнее сказать адрес от которого идет отсчет, например в рисунке приведенном выше стек мог бы начинаться с адреса 133). Стек необходим при вызовах функций, прерываниях и обработке событий (события вызывают, зарегистрированные делегатом функции для обработки). Рассмотрим, что происходит со стеком при вызове функций. К адресу с которого начинается стек например 133 прибавляются ячейки памяти размер которых достаточен для хранения переменных, указанных в параметрах функции (в нашем случае только одна переменная, но может быть и много) и еще одна ячейка достаточного размера, чтобы хранить возвращаемое значения (см. рисунок шаг 1 - 4бйта + 4байта).
После размещения параметров из функций в стеке (шаг 2) они передаются в функцию для обработки (шаг 3). При вызове ByVal передается не само значение а его копия (такой метод называется передача по значению), а в случае ByRef адрес (называется передача по ссылке). В первом случае (ByVal) к копии числа 15 прибавляется 1 и результат 16 записывается в место, зарезервированное в стеке для возврата, так как мы работали с копией, а не числом саму ячейку памяти это не затронет и в ней останется число 15. Во втором случае (ByRef) передается адрес и программа через него получит доступ не к копии, а к реальному значению хранящемуся по адресу 124. Результат как и в первом случае будет 16 (шаг 4), но при этом при выходе из функции ячейка памяти будет тоже содержать число 16. В этом и есть разница между передачей по ссылке и по значению. По умолчанию платформа VB.NET передает параметры по значению, даже в случае объектных ссылок передача идет по значению (если Вы передадите объектную переменную (то есть указатель) по ссылке получится передача указателя на указатель, например “главарь” COM так и делал - QueryInterface(const IID &iid, (void**)ip)).
А как же функции? У функции, как и переменной есть придуманное вами имя (помните пример с ассемблером когда мы объявили переменную MyVar1 dw MyVar – ее значением стал адрес переменной MyVar), в языке C++ имя функции (как и имя массива) эквивалентно адресу с которого она начинает выполняться (только в отличие от переменной это не сегмент данных, а сегмент кода). Значит бывают указатели на функции (в языке C++ так и есть). Платформа .NET все прячет и маскирует в объекты – указатель на функцию она замаскировала в делегат. В делегате (он же адрес функции) можно зарегистрировать функции, а затем вызывать их через механизм событий (адрес какой функции в делегате зарегистрирован - такая в событии и вызовется). Рассмотрим сначала делегаты без событий. Откройте проект находящийся в папке DPointVB. Найдите объявление Public Delegate Function MyDelegat(ByVal i As Integer, ByVal s As Integer) As Integer. Если Вы думаете, что это функция то ошибаетесь, MyDelegat – это определение класса, а (ByVal i As Integer, ByVal s As Integer) As Integer – это конструктор класса (на досуге просмотрите это объявление в утилите ILDasm.exe). Объявления Dim proc As MyDelegat и Dim proc1 As MyDelegat это объекты класса MyDelegat. Объявление Dim procn As MulticastDelegate – это указатель на массив делегатов, так как это массив делегатов в нем можно хранить делегаты. Наш массив будет состоять всего из двух делегатов - procn = System.Delegate.Combine(proc, proc1) (тоже замаскированный конструктор, так как возвращается ссылка на объект). Регистрировать в делегате можно только функции входные и выходные параметры которых соответствуют его конструктору. В программе определены две такие функции:
Public Function add(ByVal i1 As Integer, ByVal s1 As Integer) As Integer
Return i1 + s1
End Function
Public Function subb(ByVal i1 As Integer, ByVal s1 As Integer) As Integer
Return i1 - s1
End Function
Регистрация происходит в событии Form1_Load(): proc = AddressOf add
proc1 = AddressOf subb
procn = System.Delegate.Combine(proc, proc1)
В событиях Click() трех кнопок происходит вызов функций через делегаты. Если Вы посмотрите код близнеца этого приложения из папки DPoint, написанного на C# то увидите, что регистрация происходит через присвоения делегатам имен функций:
proc = new MyDelegat(add);
proc1 = new MyDelegat(sub);
procn = proc + proc1;
Это наследство C++ в котором имя функции соответствовало ее адресу.
Примечание. Объявление указателя на функцию в языке C++ (long int)(*funcPtr)(long int, long int) эквивалентно делегатам Public Delegate Function MyDelegat(ByVal i As Integer, ByVal s As Integer) As Integer - VB.NET и public delegate int MyDelegat(int i, int s); - C#, только указатель это обычный адрес, а делегаты его объектно-ориентированный родственник.
Делегаты в чистом виде используются очень редко (хотя самая главная функция оконного интерфейса LRESULT CALLBACK WindowProc() начинает свою жизнь с присвоения своего адреса оконному классу winclass.lpfnWndProc = WindowProc;). Примеры приведенные выше имеют скорее методическое значение. Основное предназначение делегатов – предоставлять, зарегистрированные функции событиям.
Откройте проект находящийся в папке DEventVB. В этом примере событие из формы Form2, передается форме Form1. Для отправки сообщений в Form2 определено событие -
Public Event DrawPointEvent(ByVal sender As Object, ByVal x As Integer, ByVal y As Integer).
Это событие умеет переправлять координаты от мышки в переменных x и y. Когда мышка двигается по поверхности формы Form2 генерируется событие RaiseEvent DrawPointEvent(Me, e.X, e.Y). Теперь надо научить Form1 принимать это событие. Поместим в событие Form1_Load() следующий код:
AddHandler myForm2.DrawPointEvent, AddressOf DrawPoint
Наличие строчки AddressOf DrawPoint обязывает нас написать функцию вида Private Sub DrawPoint(ByVal sender As Object, ByVal x As Integer, ByVal y As Integer)
Бросается в глаза, что событие и функция имеют одинаковые входные и выходные значения. А где же делегат? Открою страшную тайну. Весь код, для всех примеров из этой статьи был написан сначала на C# (тяжелое наследие 90х когда считалось, что VB это не круто, а С++ это наше все). Если Вы откроите близнеца для этого проекта из папки Devent (с кодом на C#) обратите внимание на следующий код:
Эквивалентом этих двух строк на C#, как Вы догадались в VB.NET будет одна строчка Public Event DrawPointEvent(ByVal sender As Object, ByVal x As Integer, ByVal y As Integer)
Язык VB.NET настоящий друг он прячет о Вас делегата (хотя ILDasm.exe не обманешь).
В языке VB.NET имеются высокоуровневые операторы, наделяющие классы событиями - WithEvents/ReiseEvent, их то же можно с успехом использовать.
Указатели (адреса) основное средство доступа к объектным типам в мире .NET. В языке С++ объекты можно было размещать как в стеке, так и в динамической памяти. Сейчас объектные типы (проще говоря классы) размещаются только в динамической памяти, а обычные переменные и структуры в стеке. Механизм размещения объекта в динамической памяти не доступен программисту. Вам просто выдается указатель с адресом, который ссылается на динамическую память. Вернемся к примеру по изменению прически известной артистки при помощи GDI+. При создании объектов битовых карт, место для них выделяется в динамической памяти.
В событии Button1_Click() Вы получаете указатель bit3 на память достаточную для размещения пустой битовой карты размером 213х265 пикселей формата Format32bppArgb. Присвоение bit3 = BitfromRes.Mkylie лишает Вас этой памяти. Вместо того, чтобы по очереди скопировать в пустую память, с начало голову, а затем прическу (с прозрачными пикселями) копирование происходит на лысую голову и портит ее. С точки зрения С++ в этой программе совершена серьезная ошибка – “дикий” указатель, но платформа .NET справляется с этой задачей методом подсчета поколений (можно почитать Д. Рихтера “Программирование на платформе .NET”.).
Примечание. Возможно Вы заметили отсутствие в коде вызов метода Dispose() для неуправляемых ресурсов какими являются битовые изображения. Привожу дословно текст из книги Д. Рихтера “Программирование на платформе .NET” стр.409 - .”Настоятельно рекомендую в общем случае отказаться от методов Dispose и Close. Сборщик мусора из CLR достаточно хорошо написан, и пусть он делает свою работу сам.”
Простые (структурные) типы не подвержены таким ошибкам, так как они не размещаются в динамической памяти (это основное отличие структурных и объектных типов). При операции присвоения создается копия в стеке, а этот механизм в данном случае гарантия неизменности исходных данных.
Если все о чем шла речь выше Вам не очень понятно, на мой взгляд вовсе не зазорно использовать VB.NET в стиле VB. На свете достаточно задач для которых не нужно ООП, а нужно простое и надежное процедурное программирование без изысков и тонкостей. Хорошо, что есть VB.NET универсальный инструмент для любых задач от простейших до самых сложных.