Занятие первое


Об инструментах
Главное в Windows-программе

Многие из вас хотели бы научиться разрабатывать программы для Windows, но, столкнувшись с трудностями, отступили. И теперь в редакцию приходят письма с просьбами открыть цикл статей, из которых бы читатели могли почерпнуть необходимую информацию о технике создания Windows-приложений. Признаться, автор этих строк и сам подумывал об этом, но никак не мог решиться. Однако почта, касающаяся вопроса Windows-программирования, которая была получена за последнее время, склонила чашу весов в сторону начала публикации. Обратите внимание на один важный момент: здесь вы не найдете тщательного "разжевывания", характерного для книг. Если вы совсем не знакомы с основными принципами функционирования Windows, непременно прочтите какую-нибудь книгу по данной тематике.

Об инструментах

За несколько последних лет программирование Windows-приложений претерпело значительные изменения, в первую очередь они связаны с переходом на 32-разрядные программы. Кроме того, наметилась тенденция все большего использования коммерческих библиотек классов, таких как Microsoft MFC, Borland OWL, Rogue Wave zApps и некоторых других.

Как известно, на заре Windows практически все программы для этой, тогда еще оболочки, создавались с применением набора Microsoft Software Development Kit (SDK). В состав этого набора, который, кстати, продолжает выпускаться и сейчас, входит комплект полезных утилит, предназначенных для создания ресурсов приложений, тестирования программ и просто ускорения процесса разработки. SDK содержит полный набор библиотек для связывания Windows-программ, заголовочные файлы, электронную документацию и большое количество примеров программ на все случаи жизни.

Мы с вами также будем опираться на SDK. И это вовсе не потому, что автор - фанат командной строки, не признающий достижений современного программирования, а потому, что исходные тексты Windows-приложений, разработанных на уровне программного интерфейса Windows (Windows API) без всяких библиотек классов, легко могут быть переработаны в готовые программы с помощью любого компилятора языков Си и Си++. Все, что потребуется, - это знание универсального программного интерфейса API Win32, на котором базируются приложения для Windows 95 и Windows NT.

Пытавшиеся ранее программировать для Windows, наверняка вздрогнули на этом месте, вспоминая, как они мучились с API и как радовались появлению первых библиотек. А теперь им предлагают вернуться назад. Ну что же, если хотите, можете обойтись и без API. Но есть и контрдоводы. Главный из них: вы никогда не создадите программы для Windows, если не знаете API. Это не голословное утверждение. Разумеется, с помощью библиотеки можно создать работающий каркас с приятным внешним видом, но по мере усложнения вашей программы и добавления новых функций, вы все чаще и чаще будете сталкиваться с невозможностью решения проблем на уровне библиотек класса. И тогда вам опять придется прибегать к API. Кроме того, знание API дает понимание глубинных процессов, происходящих в ваших программах. А без этого, согласитесь, считать себя программистом нелепо. В лучшем случае это просто кодировщик.

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

Главное в Windows-программе

Любая программа для операционной системы Windows начинается всегда с одного и того же - вызова функции WinMain. Это точка входа в Windows-приложение, такая же, как функция main для DOS-программ. WinMain описана в документации к SDK следующим образом:

int WINAPI WinMain(
 HINSTANCE hInstance, // дескриптор 
        // текущего экземпляра программы
 HINSTANCE hPrevInstance, // дескриптор 
         // предыдущего экземпляра программы 
 LPSTR lpCmdLine, 
         // указатель на командную строку
 int nCmdShow 
         // состояние показа окна программы
 );

Я бы хотел извиниться перед читателями за использование сбивающего с толку слова дескриптор. Мне не удалось найти сколько-нибудь приличного перевода для слова handle. Предлагаемое некоторыми российскими издателями слово "описатель" совершенно не подходит по смыслу, а использовать словечко "хэндл" из программистского жаргона просто неуместно. Поэтому сойдемся на нейтральном слове "дескриптор", тем более, что чаще всего употребляется именно это слово.

Разберем параметры, передаваемые функцей WinMain. Первый параметр, hInstance, представляет собой дескриптор экземпляра программы. В 16-разрядных версиях Windows этот параметр четко определял связь сегмента данных программы с ее экземпляром, позволяя определить точно, с каким из запущенных экземпляров программы мы работаем. С появлением 32-битовых программ, когда каждому экземпляру программы выделяется свой сегмент данных, не зависящий от других экземпляров этой же программы, параметр hInstance - это линейный базовый адрес, по которому программа загружена в память. Это значение для Windows 95 может быть от 0x0040000 и выше. Почему именно 0x0040000? Потому что, начиная с этого адреса памяти, загрузчик размещает программу при ее запуске. Обычно это значение можно изменить в опциях компоновщика (linker), но, как правило, это не требуется.

Второй параметр, hPrevInstance, в 16-битовых программах трактуется, как дескриптор предыдущей запущенной копии данной программы. Но поскольку все экземпляры 32-битовых программ никак не связаны друг с другом, этот параметр всегда равен нулю, независимо от того, сколько экземпляров своей программы вы уже запустили. Это лишний раз подтверждает, что все запущенные копии 32-разрядных программ ничего друг о друге не знают.

Третий параметр, lpCmdLine, является указателем на параметры командной строки, переданные вашей программе при запуске. Но это действует с одной поправкой. Дело в том, что, в отличие от командной строки программ DOS, вы не сможете извлечь из lpCmdLine полный путь к запущенной вами программе, и даже ее название. Все, что вы можете получить - это дополнительные параметры, передаваемые вместе с командной строкой. Если же вам нужно знать полную командную строку, то необходимо вызвать функцию Win32 API GetCommandLine().

Последний параметр, nCmdShow, показывает, в каком виде после запуска пребывает окно вашей программы. В Win32 для этого определено 10 констант. Это довольно редко используемый параметр, поэтому не будем утруждаться его разбором.

Итак, теперь вы знакомы с WinMain и, следовательно, готовы для написания пробной программы, которая поможет рассмотреть аспекты использования этой функции. Пусть наша программа покажет при запуске содержимое всех параметров, которые были переданы WinMain. Поскольку мы еще не разбирали процесс создания окна программы, то вместо последнего воспользуемся диалоговой панелью сообщений, которую можно получить вызовом функции API MessageBox(). Вот исходный текст нашей программы:

Файл test1.c
#include 
#define STRICT

int WINAPI WinMain(
 HINSTANCE hInstance,
 HINSTANCE hPrevInstance,
 LPSTR lpCmdLine,
 int nCmdShow )
{
char str[100];
wsprintf(str, 
"hInstance = %#08lx
hPrevInstance = %#08lx

lpCmdLine = %s
nCmdShow = %u",
hInstance, hPrevInstance, lpCmdLine, 
nCmdShow);

MessageBox(NULL, str, 
"WinMain example", MB_OK);

 return 0;
}       

Произведите сборку программы с помощью любого 32-разрядного компилятора. Сгодится Borland C++ версий 4.x и 5.x, Watcom С++, Microsoft Visual C++ 4.x (я использовал Microsoft Visual C++ 4.2). Запустим нашу программу, набрав в командной строке "test1.exe Passed String". Лучше всего, если вы повторите эту операцию два-три раза.

Внимательно посмотрите на параметры, которые показывает наша программа. Оказывается, между ними нет никакой разницы! Это очень важное открытие, подтверждающее сказанные ранее слова о том, что один экземпляр программы не знает о другом. Как и было обещано, система загрузила нашу программу по адресу 0x00400000, т. е. на отметке 4 Мбайт, о чем свидетельствует содержимое параметра hInstance. Обратите внимание, что все экземпляры программы загружены по одному и тому же адресу. Это не чудо, а умение процессоров Intel транслировать виртуальные адреса в физические. На самом деле, внутри физической памяти все запущенные экземпляры могут быть в совершенно разных ее уголках, а могут и вовсе быть сброшены на жесткий диск в процессе подкачки.

Второй параметр, hPrevInstance, равен нулю у всех экземпляров, как уже было сказано. Параметр командной строки, а именно строка "Passed String", передана нашей программе без изменений. И наконец последний параметр, nCmdShow, содержит число 1, которому в API Win32 соответствует флаг SW_SHOWNORMAL. Этот флаг говорит о том, что окно отображается в нормальном режиме.

Следующий обязательный блок для любой Windows-программы - цикл получения сообщений. Как вы, наверное, уже знаете, операционная система непрерывно бомбардирует программы потоком сообщений о тех или иных событиях. Большинство из них для программ бесполезны, и лишь небольшая их часть должна быть перехвачена и обработана. При запуске программы Windows создает очередь сообщений. Для получения сообщений из очереди внутри функции WinMain() должен быть специальный блок исходного текста. Он выглядит примерно так:

MSG msg;
while (GetMessage(&msg, (HWND) NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

Разберем этот исходный текст подробнее. Прежде всего это цикл, созданный на основе оператора while. Разумеется, вы можете использовать операторы for и do-while. Данный цикл на базе while представляет собой лишь общепринятую форму. Цикл будет выполняться до тех пор, пока функция GetMessage() не возвратит FALSE. Это означает, что в очереди сообщений обнаружено сообщение WM_QUIT, которое говорит о том, что программа закончила свою работу и нужно прекратить обработку сообщений. Программист сам может послать такое сообщение, вызвав функцию API PostQuitMessage(), и передать через эту функцию код возврата из программы. Так вызов PostQuitMessage(100) приведет к завершению программы с кодом выхода 100.

Функция GetMessage извлекает сообщение из очереди и сохраняет его в структуре MSG, адрес которой передается как первый параметр функции. Если полученное сообщение не WM_QUIT, то GetMessage возвратит TRUE, и цикл обработки сообщений будет продолжен. Второй параметр функции GetMessage - это дескриптор окна, от которого функция хочет получать сообщения. Если значение равно NULL, это означает, что мы хотим получать сообщения от всех окон, принадлежащих данной программе или потоку. Вообще-то этот параметр чаще всего равен NULL, потому что редко в практике программирования может возникнуть задача слежения за сообщениями конкретного окна.

Предпоследний и последний параметры задают минимальное и максимальное числа, соответствующие минимальному и максимальному принимаемым сообщениям. Если эти параметры равны 0, то принимаются все возможные сообщения.

После того как сообщение получено и упаковано в структуру MSG, за дело берутся две другие функции API: TranslateMessage() и DispatchMessage(). Функция TranslateMessage() занимается тем, что транслирует сообщения нажатия и отпускания клавиш WM_KEYDOWN, WM_KEYUP, WM_SYSKEYDOWN и WM_SYSKEYUP в сообщения WM_CHAR, WM_DEADCHAR, WM_SYSCHAR и WM_SYSDEADCHAR. Такая трансляция приводит к тому, что виртуальные коды клавиш транслируются в коды символов.

DispatchMessage() завершает путь прохождения сообщений, пересылая их в оконную процедуру - сердце Windows-программы, определяющее поведение программы. Оконная процедура отбирает нужные ей сообщения, реагирует на них соответствующим образом, а необработанные сообщения пересылает системному обработчику DefWindowProc(). Обычно на долю именно этой функции выпадает основная работа по выполнению такой рутинной работы, как изменение размера окна, его перемещение и т. д.

Следующим "номером" нашей программы будет методика создания окна. Как известно, большая часть программ в Windows имеют собственные окна. Поэтому программист должен уметь создавать окна и работать с ними. Создание окна в Windows происходит в три этапа. На первом этапе необходимо создать класс окна программы. В системе уже зарегистрировано несколько собственных классов различных окон, например класса "BUTTON", который служит для создания всех кнопок в Windows. Однако создание своего класса - это необходимость. Если, к примеру, вы создадите окно на базе своего класса, и будете проводить над ним манипуляции, то система может гарантировать, что другие окна, не относящиеся к вашему классу, не будут затронуты вашими действиями. Кроме того, если вы создаете свой класс окна, то можете настроить его по своему желанию и в дальнейшем использовать как шаблон для создаваемых окон. С системными классами эксперементировать не стоит. Если вы измените параметры класса "BUTTON", то это немедленно отразится на всех кнопках в Windows, что делать не стоит.

Второй этап создания окна состоит в регистрации созданного класса вызовом функции API RegisterClass(). После регистрации класс можно использовать. С этого момента можно создавать свои окна на базе зарегистрированного класса. Это делается вызовом CreateWindow(). Но это еще не окно на экране, а лишь его образ в памяти. Показ окна на экране дисплея - это третий и последний этап создания окна. Для этого вызывается функция ShowWindow(), показывающая окно на экране, и функция UpdateWindow(), посылающая сообщение WM_PAINT, получив которое программа должна перерисовать свое окно. В принципе UpdateWindow() можно и не вызывать, но тогда Windows не может гарантировать, что в вашем окне будет именно то, что вы хотите.

/*------------------------
Файл test2.c
------------------------*/
#include 
#define STRICT

/* Прототип оконной функции */
LRESULT CALLBACK WndProc
 (HWND, UINT, WPARAM, LPARAM);  
int WINAPI WinMain(
 HINSTANCE hInstance,
 HINSTANCE hPrevInstance,
 LPSTR lpCmdLine,
 int nCmdShow )
{
 
 /* Структура для хранения сообщений */
 MSG msg;
 /* Структура класса окна */
 WNDCLASS wc;
 /* Дескриптор окна */
 HWND hWnd;

 /* создаем класс */

 /* имя класса */
 wc.lpszClassName = (LPCSTR)"OurClass";
 /* оконная процедура класса */
 wc.lpfnWndProc = WndProc;
 /* стиль окна */
 wc.style = CS_VREDRAW | CS_HREDRAW;
 /* дескриптор экземпляра программы */
 wc.hInstance = hInstance;
 /* загрузить пиктограмму для окон класса */
 wc.hIcon = LoadIcon( NULL, IDI_APPLICATION );
 /* загрузить курсор мыши для окон класса */
 wc.hCursor = LoadCursor( NULL, IDC_ARROW );
 /* установить закраску окна */
 wc.hbrBackground = (HBRUSH)( COLOR_WINDOW+1 );
 /* у окон этого класса нет меню */
 wc.lpszMenuName = NULL;
 /* дополнительные данные в структуре класса */
 /* и окна не требуются */
 wc.cbClsExtra = 0;
 wc.cbWndExtra = 0;
/* Регистрируем класс окна */
RegisterClass(&wc);

/* создаем окно на базе нашего класса */
hWnd = CreateWindow(
/* Создается окно нашего класса */
"OurClass",
/* Заголовок создаваемого окна */
"Пример создания окна",
/* Стандартный тип окна */
WS_OVERLAPPEDWINDOW,
/* стандартное горизонтальное размещение окна */
CW_USEDEFAULT,
/* стандартное вертикальное */
CW_USEDEFAULT,
/* стандартная ширина окна */
CW_USEDEFAULT,
/* стандартная высота окна */
CW_USEDEFAULT,
/* дескриптор родительского окна */
NULL,
/* меню у окна отсутствует */
NULL, 
/* дескриптор экземпляра программы */
hInstance,
/* указатель на данные окна исполь-*/
/* зуется при создании окон MDI */
NULL
);

/* показать окно на экране */
ShowWindow(hWnd,
/* показать окно в нормальном режиме */
SW_SHOWNORMAL 
);

/* обновить содержимое окна */
UpdateWindow(hWnd);

/* начать обработку сообщений */
while(GetMessage(&msg, (HWND)NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}

/* отменить зарегистрированный класс, */
/* чтобы освободить память */
UnregisterClass("OurClass", hInstance);

/* вернуть Windows код возврата */
return msg.wParam;      
}

/* Оконная процедура */
LRESULT CALLBACK WndProc(
HWND hWnd,
UINT uMsg,
WPARAM wParam,
LPARAM lParam )
{
/* Не делать ничего, просто передать */
/* управление обработчику по умолчанию */
return DefWindowProc
        (hWnd, uMsg, wParam, lParam);
}

Чтобы помочь читателю разобраться с исходным текстом, приведем некоторые комментарии. Строка wc.style = CS_VREDRAW | CS_HREDRAW обозначает, что при изменении размеров окна по вертикали или горизонтали окно должно быть перерисовано. Еще одна непонятная строка - wc.hbrBackground = (HBRUSH)( COLOR_WINDOW+1 ). Но она поддается расшифровке, если знать, что системное число COLOR_WINDOW - это тот цвет, который выбран вами для фона. Однако массив цветовых настроек устроен так, что приходится прибавлять единицу для того, чтобы найти правильный его элемент. Два загадочных параметра wc.cbClsExtra и wc.cbWndExtra всегда сбивают с толку программистов. А между тем ничего загадочного здесь нет. Через эти параметры программист может запросить дополнительные байты памяти, размещенные в структуре параметров класса (cbClsExtra) или в структуре окна (cbWndExtra). Как правило, это необходимо для передачи каких-либо данных между окнами или экземплярами программы.

Последний параметр lpParam, передаваемый функции CreateWindow(), также может вызвать замешательство. На самом деле, это указатель на структуру CREATESTRUCT, задающую параметры создания окна и предаваемую в качестве параметра при посылке сообщения WM_CREATE. Это сообщение посылается оконной функции в момент вызова функции CreateWindow(). Это дает шанс оконной процедуре поправить параметры или просто выявить параметры создаваемого окна для каких-то собственных нужд.

Наверное читатель заметил непонятный вызов UnregisterClass(), происходящий при завершении программы. Это действительно очень важный момент. Когда вы регистрируете свой класс окна, для него выделяется блок памяти и делается запись в системной области, где хранятся все важные имена. Обычно при завершении программы Windows чистит эти области. Но не стоит надеяться на то, что это случится, мы должны быть приверженцами "дуракоустойчивого" программирования. Это значит, что мы сами освобождаем описанные выше системные области вызовом UnregisterClass().

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

После того как вы сами написали Windows-программу, вы уже догадались, что в природе должен существовать такой инструмент, который способен значительно облегчить работу. Его название Borland C++ Builder - Delphi-подобная среда разработки программ. Он позволяет избавиться от необходимости написания исходного текста для создания окон и всех связанных с этой задачей проблем. С самого начала программист получает готовое приложение с окном, сразу же начиная решать главную задачу, а не вспомогательную. Поэтому в своих дальнейших занятиях мы будем пользоваться пакетом Borland C++ Builder.