Визуальное и событийно-управляемое программирование
Лабораторная работа № 1.
Теоретические сведения.
Введение
В связи с тем, что сегодня уровень сложности программного обеспечения очень высок, разработка приложений Windows с использованием только какого-либо языка программирования (например, языка C) значительно затрудняется. Программист должен затратить массу времени на решение стандартных задач по созданию пользовательского интерфейса. Реализация технологии связывания и встраивания объектов - OLE - потребует от программиста еще более сложной работы.
Чтобы облегчить работу программиста, практически все современные компиляторы с языка C++ содержат специальные библиотеки классов. Такие библиотеки включают в себя практически весь программный интерфейс Windows и позволяют пользоваться при программировании средствами более высокого уровня, чем обычные вызовы функций. За счет этого значительно упрощается разработка приложений, имеющих сложный интерфейс пользователя, облегчается поддержка технологии OLE и взаимодействие с базами данных.
Современные интегрированные средства разработки приложений Windows позволяют автоматизировать процесс создания приложения. Для этого используются генераторы приложений (AppWizard). Программист отвечает на вопросы генератора приложений и определяет свойства приложения - поддерживает ли оно многооконный режим, технологию OLE, трехмерные органы управления, справочную систему. Генератор приложений, создаст приложение, отвечающее требованиям, и предоставит исходные тексты. Пользуясь им как шаблоном, программист сможет быстро разрабатывать свои приложения.
Подобные средства автоматизированного создания приложений включены в компилятор Microsoft Visual C++ и называются MFC AppWizard. Заполнив несколько диалоговых панелей, можно указать характеристики приложения и получить его тексты, снабженные обширными комментариями. MFC AppWizard позволяет создавать однооконные и многооконные приложения, а также приложения, не имеющие главного окна, -вместо него используется диалоговая панель. Можно также включить поддержку технологии OLE, баз данных, справочной системы, построить библиотеку DLL.
Конечно, MFC AppWizard не всесилен. Прикладную часть приложения программисту придется разрабатывать самостоятельно. Исходный текст приложения, созданный MFC AppWizard, станет только основой, к которой нужно подключить остальное. Но работающий шаблон приложения - это уже половина всей работы. Исходные тексты приложений, автоматически полученные от MFC AppWizard, могут составлять сотни строк текста. Набор его вручную был бы очень утомителен.
Нужно отметить, что MFC AppWizard создает тексты приложений только с использованием библиотеки классов MFC (Microsoft Foundation Class library). Поэтому только изучив MFC, можно пользоваться средствами автоматизированной разработки и создавать свои приложения в кратчайшие сроки.
Ключевые понятия
Объектно-ориентированное программирование и Visual C++
Идеи объектно-ориентированного программирования (ООП) уже оказали и продолжают оказывать решающее влияние на состояние и развитие всех областей современного программирования, языки и системы программирования, операционные системы, базы данных и базы знаний. Одним из результатов этого процесса стал Visual C++.
Среда разработки программ Visual C++ может использоваться для создания консольных приложений., но в основном предназначена для создания Windows-приложений, т.е. программ, созданных теми или иными инструментальными средствами, вместе со всем необходимым для их работы: файлами ресурсов, библиотеками и т.д.
В Visual C++ в основном разрабатываются MFC AppWizard-приложения, использующие библиотеку базовых классов MFC (Microsoft Foundation Class Library) фирмы Microsoft и инструменты AppWizard, ClassWizard, а также редактор ресурсов и текстовый редактор. AppWizard-приложения представляют собой совокупность объектов, которыми является само приложение и все его компоненты: документы, окна, виды документов и т.д. Объект-приложение, обычно носящий имя theApp (the Application, где определенный артикль подчеркивает, что речь идет о конкретном приложении), взаимодействует с другими объектами. Это взаимодействие, как и положено в объектно-ориентированном мире, выражается в сообщениях, посылаемых друг другу объектами. Отсюда ясно, что объяснить, как строятся приложения в Visual C++ и как они работают, не привлекая понятий из области ООП, невозможно.
Основные принципы и понятия ООП: объект, класс и наследование - появились в 1967 году в языке Симула-67. Эти идеи были развиты и четко сформулированы в языке и среде Smalltalk. В версии Smalltalk-80 была высказана еще одна центральная идея: жизнь объектов состоит в том, что они посылают друг другу сообщения.
Классы с наследованием и переопределением методов вошли в большинство объектно-ориентированных языков программирования - в том числе и в C++ (объектно-ориентированное расширение языка C). Идея обмена сообщениями легла в основу операционной системы Windows, где объекты-окна посылают и получают сообщения.
Класс является обобщением понятия типа данных и задает свойства и поведение объектов класса, называемых экземплярами класса. Каждый объект принадлежит некоторому классу. Отношение между объектом и его классом такое же, как между переменной и ее типом. С формальной точки зрения, класс - это объединение данных и обрабатывающих их функций. Данные класса называются также переменными класса, а процедуры - методами класса. В C++ переменная класса data member, метод - member function. Переменные определяют свойства объекта, Говорят также, что значения переменных определяют состояние объекта. Методы определяют поведение объекта.
Если определен класс А, можно определить новый класс В, наследующий свойства и поведение объектов класса А. Это значит, что в классе В определены переменные и методы класса А. Класс В, являясь наследником базового (родительского) по отношению к нему класса А, называется производным (порожденным) по отношению к классу А. В производном классе можно задавать новые свойства и новое поведение, определив новые переменные и новые методы, или даже переопределить существующий метод базового класса. Переопределение метода (или перегрузка (overloading) или перекрытие (overriding)) класса А в производном классе В - это определение в классе В метода с тем же именем, уже являющимся именем какого-то метода класса А. При этом обычно конкретизируется или специализируется реализация переопределенного метода класса А применительно к объектам класса В (они в силу наследования являются частными случаями объектов класса А).
Метод базового класса иногда полезно объявить как виртуальный с атрибутом virtual. Тогда при переопределении метода в производном классе должно сохраняться число параметров метода и их типов, что гарантирует одинаковую форму вызовов виртуальных методов как для производного, так и базового класса. Виртуальность обеспечивает возможность написания полиморфной функции.
Класс В, производный от класса А, в свою очередь может быть базовым по отношению к порожденному от него классу С, становящемуся, таким образом, наследником класса А в следующем поколении, и т.д. Все порожденные классы в любом поколении называются наследниками данного класса, а все, от которых он порожден, - его предками.
Визуальное и событийно-управляемое программирование
Жизнь объектов состоит в обмене сообщениями. Когда один объект посылает сообщение другому, это можно рассматривать, как вызов соответствующего метода этого другого объекта. При получении сообщения вызывается некоторый метод этого объекта, обрабатывающий сообщение.
Рассматривать мир объектов приложения как замкнутый, внутренний вряд ли оправдано. Ситуация меняется, если мир объектов не замкнут и они способны получать сообщения из внешней среды. Эти сообщения могут появляться как результат событий, происходящих во внешнем мире: действий пользователя, операционной системы, других приложений.
Рассмотрим простую и естественную модель событийно-управляемого и визуального программирования, характерную для языка и среды Visual С++. В этой модели у приложения три составляющие: визуальная, системная и обработчик событий. Визуальная составляющая задает образ на экране, с которым будет работать пользователь. Она, как правило, разрабатывается визуальным инструментарием, позволяющим программисту создавать из элементов нужный образ на экране. Эти элементы являются объектами со своими свойствами и поведение.
Визуальная составляющая определяет интерфейс пользователя. Такие элементы интерфейса, как кнопки, окна редактирования, окна списков, называют элементами управления (controls). Эти и другие элементы интерфейса стандартизированы в стандарте пользовательского интерфейса CUA (Common User Access) в рамках общего стандарта SSA (system Application Architecture) фирмы IBM. Поэтому в разных средах разработки (Visual Basic, Visual C++, Delphi и др.) визуальный инструментарий содержит одни и те же элементы интерфейса. Такие же элементы интерфейса содержат разные приложения.
Элементы управления являются объектами, свойства и поведение которых определяется их переменными и методами. Они относятся к интерфейсным объектам.
Пользователь - это возмутитель спокойствия в мире объектов приложения. Он "нажимает" на кнопки, выбирает элементы списков, печатает тексты в окнах редактирования. Каждому его действию соответствует некоторое событие. Системная составляющая приложения, которая включает в себя средства операционной системы и средства среды программирования, определяет тип и параметры события и формирует сообщение объекту, с которым связано событие. Иначе говоря, системная составляющая находит нужный объект и запускает функцию-обработчик сообщения - соответствующий метод этого объекта. Таким образом, пользователь может взаимодействовать с элементами визуальной составляющей, а само взаимодействие обеспечивается системной составляющей.
Обработчики событий и связанных с ними сообщений составляют третий компонент приложения. Когда пользователь действует на элемент управления, происходит событие, распознаваемое системной составляющей, которая вызывает обработчик события. В работе системной составляющей важную роль играют сообщения, связанные с происшедшими событиями. В обработке события (т.е. в методе объекта) программист волен предусмотреть самые разные действия: может изменять свойства других объектов, вызывать методы других объектов, добавлять или удалять объекты визуальной составляющей, даже полностью изменить ее облик.
Программирование на Visual С++ полностью соответствует концепциям визуального и событийно-управляемого программирования. Чтобы создать приложение на Visual С++, нужно сделать две вещи: разработать с помощью визуального инструментария интерфейс пользователя и написать реакции на действия пользователя, т.е. для каждого возможного события - обрабатывающий его метод.
Событийно-управляемое программирование (event-driven programming), называемое также программированием с управлением по событиям, имеет, разумеется, смысл и само по себе, а не только в рамках приведенной модели. Программы, разработанные по его принципам, существовали задолго до появления визуального программирования - например, всем известные текстовые редакторы. Обычно эти программы содержат цикл, в теле которого разбираются варианты реакций на действия пользователя.
Модель Visual С++ особенно интересна программисту. Во-первых, программист может очень быстро создать приложение со стандартными элементами управления, просто определив реакцию приложения на некоторые события. Во-вторых, здесь есть возможность создавать объекты, производные от стандартных, но с отличными свойствами, тем самым создавая уникальные по интерфейсу приложения. Но от программиста требуется глубокое знание системы, а также глубокое понимание особенностей объектно-ориентированного программирования.
Windows-приложения
Для создания прилржений под Windows без использования MFC пользователь должен знать особенности построения Windows-приложений, а также может пользоваться библиотекой API Windows (Application Programming Interface). Мы будем говорить о 32-битовых Windows (95/98 или NT ), поэтому будем раасматривать только Win32 API.
Основным объектом объектно-ориентированной операционной системы Windows является окно (чтобы подчеркнуть его отношение к Windows, говорят Windows-окно). В дополнение к обычным свойствам объекта оно имеет графический образ на экране дисплея в виде окна, с которым взаимодействует пользователь. Переменные Windows-окна определяют такие свойства, как тип, размер, положение на экране и т.д. Поведение этого объекта определяется методом WndProc, называемым обычно функцией окна. В многозадачной и многооконной операционной системе Windows одновременно можно запустить несколько Windows-приложений, с каждым из которых может быть связано несколько окон. В каждом приложении, взаимодействующем с пользователем, есть как минимум одно окно.
События, возникающие в процессе работы компьютера (инициированные пользователем или связанные с посылкой сообщений от одного приложения другому, от одного окна к другому окну того же приложения), приводят к помещению операционной системой сообщения, связанного с событием, в первичную системную очередь сообщений. Windows распределяет сообщения по приложениям, создавая для каждого приложения очередь приложения, куда поступают сообщения от разнообразных источников: мыши, клавиатуры, таймера, других приложений и, что особенно важно, от самой операционной системы. В этой схеме есть исключения, так как некоторые сообщения напрямую выставляются окну, например сообщение WM_DESTROY, уведомляющее окно о том, что оно должно быть закрыто. На самом деле приложение в 32-разрядной операционной системе Windows (95/98 или NT ) может включать несколько потоков, и очередь сообщений создается для каждого потока, если она нужна. Если же поток не получает сообщений, то для него очередь сообщений не создается.
Windows накладывает довольно жесткие ограничения на структуру Windows-приложения: каждое имеет главную процедуру WinMain, одинаково устроенную для всех приложений. WinMain начинает работу с регистрации класса окна приложения, затем создает и рисует на экране главное окно и, возможно, другие окна. Объявление функции WinMain имеет следующий вид:
int WINAPI WinMain ( HINSTANCE hInstance,
HINSTANCE hPrevInstance,
LPSTR lpCmdLine,
int nСmdShow);
hInstance - дескриптор основного приложения,
hPrevInstance - дескриптор предыдущей копии приложения ( в Win32 API этот параметр всегда равен NULL ),
lpCmdLine - указатель на командную строку,
nСmdShow - режим начального отображения главного окна приложения,
Простейшее, ничего не делающее приложение для Windows, может иметь следующий вид:
#include <windows.h>
int WINAPI WinMain ( HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nСmdShow)
{
return (FALSE);
}
Функция WinMain должна выполнить инициализацию приложения, регистрацию класса окна, выод окна, запустить цикл обработки сообщений.
LPSTR szAppClass = "CLASSTEST";
int WINAPI WinMain( HANDLE hInstance, // Instance handle
HANDLE hPrevInstance, // Previous instance handle
LPSTR lpszCommandLine, // Command line string
int nCmdShow ) // ShowWindow flag
{
MSG msg;
if (!InitApp( hInstance)) // зарегистрировать класс окна
return FALSE; // выход в случае ошибки
if (!InitInstance( hInstance, nCmdShow ))
// создать окно
return FALSE; // выход в случае ошибки
while (GetMessage( (LPMSG) &msg, NULL, 0, 0) )
{
TranslateMessage( (LPMSG) &msg );
DispatchMessage( (LPMSG) &msg );
}
return TRUE; //успешный выход
}
Функция InitApp инициализирует данные и регистрирует класс окна.. Обычно функция WinMain возвращает FALSE в случае ошиьки при регистрации класса.
Следующий фрагмент показывает, как может выглядеть функция InitApp:
BOOL InitApp( HANDLE hInstance, szAppClass )
{
WNDCLASS wc;
wc.hCursor = LoadCursor( NULL, IDC_ARROW );
wc.hIcon = NULL;
wc.lpszMenuName = NULL;
wc.lpszClassName = szAppClass;
wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1);
wc.style = CS_VREDRAW | CS_HREDRAW;
wc.lpfnWndProc = WndProc;
if (!RegisterClass( (LPWNDCLASS) &wc) )
return FALSE;
return TRUE;
}
Описание структуры WNDCLASS смотрите в приложении I.
Функция InitInstance может иметь вид:
int InitInstance( HINSTANCE hInstance, int nCmdShow )
{
HWND hwnd;
hwnd = CreateWindow(szAppClass, //имя класса окна
"Заголовок окна", //заголовок окна
WS_OVERLAPPEDWINDOW, //стиль окна
CW_USEDEFAULT, //размеры окна и его положение по
CW_USEDEFAULT, //умолчанию
CW_USEDEFAULT,
CW_USEDEFAULT,
0, //идентификатор родительского окна
0, //идентификатор меню
hInstance, //идентификатор приложения
NULL); //указатель на дополнительные
//параметры
if ( !hwnd )
return FALSE;
ShowWindow(hwnd,nCmdShow); // рисуем окно
UpdateWindow(hWnd);
return TRUE;
}
Описание функций CreateWindow, ShowWindow и UpdateWindow смотрите в приложении I.
После создания объектов-окон, связанных с приложением, запускается цикл обработки очереди сообщений приложения, который иногда называют основным циклом обработки сообщений.
Очередь сообщений приложения формирует Windows. Сначала приложение создает свои объекты. Чтобы они "ожили", пользователь, как правило, должен инициировать некоторые начальные события. Преобразованные в сообщения, события поступают в системную очередь, а оттуда в очередь приложения. После того, как объекты "оживут", источников поступления сообщений в очередь приложения становится больше: это и объекты самого приложения и операционная система, посылающая сообщения этим объектам.
Поскольку в каждый момент активно только одно приложение, то иногда этого достаточно, чтобы понять, какому приложению принадлежит сообщение. Для разделяемых ресурсов (клавиатуры, мыши и др.) это не так: сообщения поступают в системную очередь, где и производится необходимый анализ, позволяющий направить сообщение в очередь соответствующего приложения. Основной цикл обработки сообщений, опуская детали, можно представить так:
while (GetMessage( (LPMSG) &msg, NULL, 0, 0) )
{
DispatchMessage( (LPMSG) &msg );
}
Функция диспетчеризации DispatchMessage классифицирует сообщения и определяет окно, которому нужно направить сообщение, передавая параметры окна системе Windows. Поскольку все окна зарегистрированы в системе, Windows вызывает соответствующую функцию окна WndProc, передав ей сообщение в качестве параметра. Окончательная обработка сообщения выполняется этой функцией. Если очередь сообщений приложения пуста, функция GetMessage передает управление системе Windows, и активным становится другое приложение. Цикл обработки сообщений завершается, когда GetMessage возвращает значение NULL, а это происходит при выборе из очереди сообщения WM_QUIT. На этом завершает свою работу и приложение. Создать Windows-приложение в первую очередь означает разработать функцию WndProc. Она обычно состоит из блоков обработки сообщений, задающих реакцию приложения на происходящие события.
LRESULT CALLBACK WndProc(HWND hwnd, UINT msg, WPARAM wParam, LPARAM lParam)
{
switch (msg)
{
case WM_DESTROY:
PostQuitMessage(0);
return TRUE;
case WM_LBUTTONDOWN:
MessageBox(NULL, "Hello, World","Window",MB_OK);
return TRUE;
default:
return DefWindowProc(hwnd,msg,wParam,lParam);
}
}
Все события, не требующие нестандартной реакции на их появление, обрабатываются функцией DefWindowProc (см. приложение I и приложение II)
Может показаться, что структура Windows-приложения довольно неестественна и что Windows-программист становится жертвой архитектурных решений, принятых при проектировании операционной системы. Однако это не так. Дело в том, что многие архитектурные решения, особенно связанные с организацией пользовательского интерфейса, направлены на реализацию простой, но очень важной идеи. Она, в частности, определяет и структуру Windows-приложения. Суть ее в том, чтобы в паре пользователь - компьютер (точнее, функционирующее на нем программное обеспечение) главной фигурой был первый: он должен всегда чувствовать, что именно он управляет компьютером. Для работы в операционной среде Windows разрабатываются только событийно-управляемые приложения. Это означает, что выполняемые операции инициируются только пользователем, а не приложением. Это-то и позволяет ставить пользователя к рулю управления приложениями, делает его главной фигурой.
На самом деле событийно-управляемые приложения в любой операционной системе имеют структуру, аналогичную структуре Windows-приложений, но в Windows имеется много механизмов, облегчающих разработку событийно-управляемых приложений.