Камчатский государственный технический университет Кафедра систем управления
Ю.В. Марапулец
ОПЕРАЦИОННЫЕ СИСТЕМЫ Метод...
101 downloads
308 Views
775KB Size
Report
This content was uploaded by our users and we assume good faith they have the permission to share this book. If you own the copyright to this book and it is wrongfully on our website, we offer a simple DMCA procedure to remove your content from our site. Start by pressing the button below!
Report copyright / DMCA form
Камчатский государственный технический университет Кафедра систем управления
Ю.В. Марапулец
ОПЕРАЦИОННЫЕ СИСТЕМЫ Методические указания по выполнению лабораторных работ № 1–8 по курсу "Операционные системы" для студентов специальности 220400 "Программное обеспечение вычислительной техники и автоматизированных систем"
Петропавловск-Камчатский 2004
УДК 681.306 ББК 32.973-018.2 М25
Рецензент: Г.А. Пюкке, кандидат технических наук, заведующий кафедрой систем управления КамчатГТУ
Марапулец Ю.В. М25
Операционные системы. Методические указания по выполнению лабораторных работ №1–8 для студентов специальности 220400 "Программное обеспечение вычислительной техники и автоматизированных систем". – Петропавловск-Камчатский: КамчатГТУ, 2004. – 86 с.
Методические указания предназначены для выполнения лабораторных работ по курсу "Операционные системы". В тексте приводятся задания для каждой лабораторной работы, краткие теоретические сведения, требования по оформлению отчета. Рекомендовано к изданию решением учебно-методического совета КамчатГТУ (протокол № 6 от 20 февраля 2004 г.).
УДК 681.306 ББК 32.973-018.2
© КамчатГТУ, 2004 © Марапулец Ю.В., 2004 2
ВВЕДЕНИЕ Лабораторные работы по курсу "Операционные системы" выполняются в среде разработчика Visual C++ 6 на языке программирования С++. Программы должны устойчиво работать в операционных системах Windows 95–98, ME, NT4, 2000, XP (лабораторная работа № 5 выполняется только в операционных системах Windows NT, 2000, XP).
ПОРЯДОК ВЫПОЛНЕНИЯ ЛАБОРАТОРНЫХ РАБОТ 1. Подготовка и допуск к работе. 1.1. К выполнению лабораторной работы допускаются студенты, которые подготовились к работе и имеют не более двух незащищенных работ. 1.2. Перед работой каждый студент должен: – предъявить преподавателю полностью предыдущей работе; – ответить на вопросы преподавателя.
оформленный
отчет
о
1.3. К работе не допускаются студенты, которые не выполнили одно из вышеперечисленных требований. 1.4.Лабораторные работы, которые студент пропустил, выполняются в конце семестра; допуск к работе производится в порядке, указанном в п.1.1. 2. Требования по содержанию отчета по лабораторным работам приведены в конце каждой лабораторной работы.
3
ЛАБОРАТОРНАЯ РАБОТА №1 "Многопоточное приложение" Цель работы: Изучение принципов разработки программы, позволяющей использовать несколько потоков (На примере программы Threads). Задание к лабораторной работе: 1. Запустить программу Threads. Результат работы программы представлен на рис.1.1. В результате исполнения создаются четыре вторичных потока, каждый из которых рисует в дочернем окне прямоугольники, задавая их размеры и цвет случайным образом. В верхней части окна находится список, хранящий информацию обо всех четырех потоках. Выделив какой-нибудь элемент списка и выбрав определенную команду меню Thread, можно приостановить любой из потоков, возобновить его выполнение или изменить приоритет. С помощью меню Options можно также активизировать исключающий семафор, который позволит в каждый момент времени выполняться только одному потоку.
Рис.1.1. Окно программы Threads'
4
2. Рассмотреть исходный код программы. Первоначально рассмотреть функции инициализации. Данный класс функций регистрируют два класса окон, один из которых предназначен для главного окна, а другой - для дочерних окон, в которых потоки выполняют свои графические операции. Кроме того, создается таймер, позволяющий с пятисекундным интервалом обновлять в списке информацию о каждом из потоков. Функция CreateWindows создает и позиционирует все окна, в том числе и список, в котором содержится информация о каждом потоке. Четыре потока создаются в обработчике сообщения WM_CREATE. /* WIN MAIN - вызов функции инициализации и запуск цикла обработки сообщений */ int WINAPI WinMain ( HINSTANCE hinstThis, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int iCmdShow ) { MSG msg; hInst = hinstThis; // запись в глобальную переменную if (! InitializeApp ( )) { //выход из программы, если приложение не было инициализировано return( 0 ) ; } ShowWindow ( hwndParent, iCmdShow ); UpdateWindow( hwndParent ); // получение сообщений из очереди while ( GetMessage( &msg, NULL, 0, 0 ) ) { TranslateMessage( &msg ) ; DispatchMessage ( &msg ) ; } return( msg.wParam ); } Обратить внимание на отсутствие функции PeekMessage. Это - наглядное свидетельство того, что в приложении реализована приоритетная многозадачностью. (Многозадачность данного типа подразумевает, что система прерывает выполнение потока, предоставляя другим потокам возможность получить доступ к ресурсам центрального процессора. При кооперативной многозадачности система ожидает, пока поток не вернет ей управление над процессором.) Потоки имеют возможность непрерывно производить экранные операции, не монополизируя процессор. В это же время могут выполняться и другие программы. Помимо регистрации класса приложения и выполнения стандартных действий по инициализации, функция InitializeApp задает приоритеты потоков и запускает все потоки в режиме ожидания. 5
/*INITIALIZE APP - регистрация двух классов и создание окон. */ BOOL InitializeApp ( void ) { … // Пометить исходное состояние каждого потока как SUSPENDED // сразу при их создании. for( iCount = 0; iCount < 4; iCount++ ) { iState[iCount] = SUSPENDED; } // Задать первичному потоку более высокий приоритет, // что позволит ускорить ввод/вывод команд пользователем. SetThreadPriority( GetCurrentThread(), THREAD_PRIORITY_ABOVE_NORMAL ); // Создать все окна. return( CreateWindows( ) ); } Вызов функции SetThreadPriority приводит к увеличению приоритета первичного потока. Если все вторичные потоки будут иметь такой же приоритет, как и первичный, то реакция на выбор команд меню будет очень медленной. Убедиться в этом, запустив программу и повысив приоритет вторичных потоков. Функция CreateWindow создает не только основное окно, но и список, а также набор дочерних окон для потоков. /* CREATE WINDOWS - создать главное окно, окно списка и четыре дочерних окна */ BOOL CreateWindows ( void ) { char szAppName[MAX_BUFFER] ; char szTitle[MAX_BUFFER] ; char szThread[MAX_BUFFER]; HMENU hMenu; int iCount; // загрузка соответствующих строк LoadString( hInst, IDS_APPNAME, szAppName, sizeof(szAppName)); LoadString( hInst, IDS_TITLE, szTitle, sizeof(szTitle)); LoadString( hInst, IDS_THREAD, szThread, sizeof(szThread)); // создать родительское окно hMenu = LoadMenu( hInst, MAKEINTRESOURCE(MENU_MAIN) ); hwndParent = CreateWindow( szAppName, szTitle, WS_OVERLAPPEDWINDOW | WS_CLIPCHILDREN, CW_USEDEFAULT, CW_USEDEFAULT, 6
CW_USEDEFAULT, CW_USEDEFAULT, NULL, hMenu, hinst, NULL ) ; if( ! hwndParent) { return( FALSE ) ; } // создать окно списка hwndList = CreateWindow( "LISTBOX", NULL, WS_BORDER | WS_CHILD | WS_VISIBLE | LBS_STANDARD | LBS_NOINTEGRALHEIGHT, 0, 0, 0, 0, hwndParent, (HMENU)1, hinst, NULL ) ; if( ! hwndList ) { return( FALSE ) ; } // создать четыре дочерних окна for( iCount = 0; iCount < 4; iCount++ ) { hwndChild[iCount] = CreateWindow( "ThreadClass", NULL, WS_BORDER | WS_CHILD | WS_VISIBLE | WS_CLIPCHILDREN, 0, 0, 0, 0, hwndParent, NULL, hInst, NULL ); if(! hwndChild ) return( FALSE ); } return( TRUE ); } 3. Рассмотреть функции обработки сообщений. Большинство функций обработки сообщений очень просты. Функция Main_OnTimer вызывает функцию, которая очищает список, генерирует четыре новые информационные строки и выводит их в окне списка. Функция Main_OnSize приостанавливает все вторичные потоки, пока программа изменяет положение дочерних окон в соответствии с новыми размерами родительского окна. В противном случае работающие потоки будут замедлять выполнение операции отображения. Функция Main_OnCreate создает потоки, а также исключающий семафор. /* MAIN_WNDPROC - обработка всех сообщений главного окна */ LRESULT WINAPI Main_WndProc( HWND hWnd, // адрес сообщения UINT uMessage, // тип сообщения WPARAM wParam, // содержимое сообщения LPARAM lParam ) // дополнительное содержимое { switch( uMessage ) 7
{ HANDLE_MSG( hWnd, WM_CREATE, Main_OnCreate ); // Создание окна и потоков HANDLE_MSG( hWnd, WM_SIZE, Main_OnSize ); // Согласование положения дочерних окон при изменении размеров // главного окна HANDLE_MSG( hWnd, WM_TIMER, Main_OnTimer ) ; // Обновление списка через каждые пять секунд HANDLE_MSG( hWnd, WM_INITMENU, Main_OnInitMenu ) ; // Если переменная bUseMutex равна TRUE, пометить пункт меню // Use Mutex. HANDLE_MSG( hWnd, WM_COMMAND, Main_OnCommand ); // Обработка команд меню HANDLE_MSG( hWnd, WM_DESTROY, Main_OnDestroy ); // Очистка экрана и выход из программы default: return( DefWindowProc(hWnd, uMessage, wParam, lParam) ); } return( 0L ) ; } В программе Threads используется макрокоманда обработки сообщений HANDLE_MSG, поэтому компилятор может выдать ряд предупреждений типа "Unreferenced formal parameter" (Неопознанный формальный параметр). Во избежание этого в программу следует включить директиву argsused: #ifdef _BORLANDC_ #pragma argsused #endif Функция Main_OnCreate завершает процесс инициализации потоков и создает исключающий семафор: /* MAIN_ONCREATE - создать четыре потока и установить таймер */ BOOL Main_OnCreate( HWND hWnd, LPCREATESTRUCT lpCreateStruct ) { UINT uRet; int iCount; // создание четырех потоков, приостановленных в исходном состоянии for( iCount = 0; iCount < 4; iCount++ ) { iRectCount[iCount] = 0; dwThreadData[iCount] = iCount; hThread[iCount] = CreateThread( NULL, 0, 8
(LPTHREAD_START_ROUTINE) StartThread, (LPVOID) ( & ( dwThreadData[iCount] ) ), CREATE_SUSPENDED, (LPDWORD) ( & ( dwThreadID(iCount] ) ) ); if( ! hThread[iCount] ) // Был ли поток создан? { return( FALSE ) ; } } // создание таймера с пятисекундным периодом срабатывания; // использование таймера для обновления списка uRet = SetTimer( hWnd, TIMER, 5000, NULL ); if( ! uRet ) { // создать таймер не удалось return( FALSE ) ; } // создание исключающего семафора hDrawMutex = CreateMutex( NULL, FALSE, NULL ); if( ! hDrawMutex ) { //не удалось создать исключающий семафор KillTimer( hWnd, TIMER ); // остановка таймера return( FALSE ) ; } // запуск потоков с приоритетом ниже стандартного for( iCount = 0; iCount < 4; iCount++ ) { SetThreadPriority( hThread[iCount], THREAD_PRIORITY_BELOW_NORMAL ); iState[iCount] = ACTIVE; ResumeThread( hThread[iCount] ); } // Теперь запущены все четыре потока! return( TRUE ) ; } Функция Main_OnSize не только изменяет размер окна приложения, но и корректирует размеры и положение всех дочерних окон. /* MAIN_ONSIZE - Позиционирует окно списка и четыре дочерних окна. */ void Main_OnSize( HWND hWnd, UINT uState, int cxClient, int cyClient ) { char* szText =-"No Thread Data"; int iCount; // Приостанавливает активные потоки, пока не будет завершено // изменение размеров и обновление соответствующих окон. 9
// Эта пауза значительно ускоряет процесс обновления содержимого экрана. for( iCount = 0; iCount < 4; iCount++ ) { if ( iState[iCount] == ACTIVE ) SuspendThread ( hThread[iCount] ); } // Размещает список в верхней четверти главного окна. MoveWindow( hwndList, 0, 0, cxClient, cyClient / 4, TRUE ); // Размещает четыре дочерних окна в трех нижних четвертях // главного окна. // Левая граница первого окна имеет координату 'х', равную 0. MoveWindow ( hwndChild[0], 0, cyClient /4-1, cxClient /4 + 1, cyClient, TRUE ); for( iCount = 1; iCount < 4; iCount++ ) { MoveWindow( hwndChild[iCount], (iCount * cxClient) / 4, cyClient /4-1, cxClient / 4 +1, cyClient, TRUE ); } // Вводит в список строковые значения, заданные по // умолчанию, и выполняет инициализацию. // Начальное число прямоугольников равно 0. for( iCount = 0; iCount < 4; iCount++ ) { iRectCount[iCount] = 0; ListBox_AddString( hwndList, szText ); } ListBox_SetCurSel(hwndList, 0); // Возобновляет выполнение потоков, которые были приостановлены // на время обновления окна. for( iCount = 0; iCount < 4; iCount++ ) { if( iState[iCount] == ACTIVE) { ResumeThread( hThread[iCount] ); } } return; } Обновление окна списка по сообщениям таймера - решение далеко не идеальное. Гораздо целесообразнее, чтобы этот процесс инициировался теми операциями, которые приводят к изменению исходных данных. Однако применение таймера можно считать простейшим способом выполнения поставленной задачи. 10
/*MAIN_ONTIMER - Использует сообщение таймера для обновления списка. */ void Main_OnTimer( HWND hWnd, UINT uTimerID ) { // Обновляет данные, представленные в списке UpdateListBox() ; return; } Функция Main_OnlnitMenu просто выполняет установку или снятие отметки команды меню Use Mutex. /* MAIN_ONINITMENU - Устанавливает или снимает отметку команды меню Use Mutex на основании значения переменной bUseMutex. */ void Main_OnInitMenu( HWND hWnd, HMENU hMenu ) { CheckMenuItem ( hMenu, IDM_USEMUTEX, MF_BYCOMMAND | (UINT)( bUseMutex ? MF_CHECKED : MF_UNCHECKED ) ) ; return; } Функция Main_OnCommand объединяет все сообщения, для которых нет специальных обработчиков. (Обработчики сообщений группируются в функции Main_WndProc, которая с помощью макрокоманды HANDLE_MSG перенаправляет их на обработку соответствующим функциям.) /* MAIN_ONCOMMAND - Реагирует на команды пользователя. */ void Main_OnCommand ( HWND hWnd, int iCmd, HWND hwndCtl, UINT uCode ) { switch ( iCmd ) { case IDM_ABOUT: // вывод окна About MakeAbout ( hWnd ) ; break; case IDM_EXIT: // выход из программы DestroyWindow( hWnd ) ; break; case IDM_SUSPEND: // изменение приоритета или состояния потока case IDM_RESUME: case IDM_INCREASE: case IDM_DECREASE: DoThread( iCmd ); // модификация параметров потока; case IDM_USEMUTEX: // включение или отключение // исключающего семафора ClearChildWindows( ); // установление белого цвета 11
// для окон всех потоков bUseMutex = !bUseMutex; // переключение параметров // исключающего семафора default: break; } return; } 4. Рассмотреть функции изменения параметров. Функция DoThread в ответ на соответствующие команды меню изменяет параметры потока, выбранного в списке. Эта функция может повысить или понизить приоритет потока, а также приостановить или возобновить его выполнение. Текущее состояние каждого потока записывается в массив iState. В массиве hThreads сохраняются дескрипторы каждого из четырех вторичных потоков. /*DO THREAD - Изменяет приоритет потока или его состояние в ответ на команды меню. */ void DoThread( int iCmd ) { int iThread; int iPriority; // Определяет, какой из потоков выбран. iThread = ListBox_GetCurSel ( hwndList ) ; switch ( iCmd ) { case IDM_SUSPEND: // Если поток не остановлен, останавливает его. if( iStatefiThread] != SUSPENDED ) { Suspend/Thread ( hThread[iThread] ); iState[iThread] = SUSPENDED; } break; case IDM_RESUME: // Если поток не активен, активизирует его. if( iState[iThread] != ACTIVE ) { ResumeThread( hThread(iThread] ); iState[iThread] = ACTIVE; } break; case IDM_INCREASE: // Повышает приоритет потока, если только он 12
// уже не является самым высоким iPriority = GetThreadPriority( hThread[iThread] ); switch( iPriority ) { case THREAD_PRIORITY_LOWEST: SetThreadPriority( hThread[iThread], THREAD_PRIORITY_BELOW_NORMAL ) ; break; case THREAD_PRIORITY_BELOW_NORMAL: SetThreadPriority( hThread[iThread], THREAD_PRIORITY_NORMAL ) ; break; case THREAD_PRIORITY_NORMAL: SetThreadPriority( hThread[iThread], THREAD_PRIORITY_ABOVE_NORMAL ) ; break; case THREAD_PRIORITY_ABOVE_NORMAL: SetThreadPriority( hThread[iThread], THREAD_PRIORITY_HIGHEST ) ; break; default: break; } break; case IDM_DECREASE: // Понижает приоритет потока, если только он //не является самым низким iPriority = GetThreadPriority( hThread[iThread] ); switch( iPriority ) { case THREAD_PRIORITY_BELOW_NORMAL: SetThreadPriorityt hThread[iThread], THREAD_PRIORITY_LOWEST ) ; break; case THREAD_PRIORITY_NORMAL: SetThreadPriority (hThread[iThread], THREAD_PRIORITY_BELOW_NORMAL ) ; break; case THREAD_PRIORITY_ABOVE_NORMAL: SetThreadPriority( hThread[iThread], THREAD_PRIORITY_NORMAL ) ; break; case THREAD_PRIORITY_HIGHEST: SetThreadPriority( hThread[iThread], 13
THREAD_PRIORITY_ABOVE_NORMAL ) ; break; default: break; } break; default: break; } return; } 5. Рассмотреть функции для выполнения операций с потоками. Создав вторичные потоки, функция Main_OnCreate при каждом вызове функции CreateThread передает ей указатель на функцию StartThread. Последняя становится стартовой функцией для всех потоков. Она начинает и завершает их выполнение. Если флаг bUseMutex имеет значение TRUE, потоки ожидают разрешения исключающего семафора, который обеспечивает одновременное выполнение графических операций только одним потоком. /*START THREAD - Эта процедура вызывается на начальном этапе выполнения каждого потока. */ LONG StartThread ( LPVOID lpThreadData ) { DWORD *pdwThreadID; // указатель на переменную типа DWORD // для записи идентификатора потока DWORD dwWait; // результирующее значение // функции WaitForSingleObject // получение идентификатора потока pdwThreadID = lpThreadData; // Выполнение графических операций до тех пор, пока // значение флага bTerminate не станет равным TRUE. while( ! bTerminate ) { if (bUseMutex) // Используется ли исключающий семафор? { // Выполнение действий при получении разрешения // от исключающего семафора. dwWait = WaitForSingleObject( hDrawMutex, INFINITE ); if( dwWait == 0 ) { DrawProc( *pdwThreadID ); // рисование // прямоугольников 14
ReleaseMutex( hDrawMutex ); // разрешить выполнение // других потоков } } else { // Исключающий семафор не используется, // разрешить выполнение потока. DrawProc( *pdwThreadID ) ; } } // Этот оператор неявно вызывает команду ExitThread. return ( 0L ) ; } Функция DrawProc предназначена для рисования прямоугольников. Однако во избежание перегрузки системы из-за передачи большого количества мелких сообщений между программой и подсистемой Win32 обращение к GDI не всегда происходит сразу же. Поэтому графические команды становятся в очередь и периодически выполняются все одновременно. Такие задержки несколько преуменьшают влияние приоритетов потоков в программе Threads. /*DRAW PROC - Рисует хаотически расположенные прямоугольники. */ void DrawProc ( DWORD dwID ) { if (bUseMutex) { iTotal = 50; // Если выполняется только один поток, } // разрешить ему рисовать большее число фигур. else { iTotal = 1; } // сброс генератора случайных чисел srand( iRandSeed++ ); // получение размеров окна bError = GetClientRect( hwndChild[dwID], &rcClient ); if( ! bError ) return; cxClient = rcClient.right - rcClient.left; cyClierit = rcClient. bottom - rcClient.top; //не рисовать, если не заданы размеры окна if( ( ! cxClient ) || ( ! cyClient ) ) { 15
return; } // получить контекст устройства для рисования hDC = GetDC( hwndCbild[dwID] ); if( hDC ) { // нарисовать группу случайно расположенных фигур for( iCount = 0; iCount < iTotal; iCount++ ) { iRectCount[dwID]++; // задать координаты xStart = (int)( rand() % cxClient ); xStop = (int)( rand() % cxClient ); yStart = (int) ( rand() % cyClient ); yStop = (int)( rand() % cyClient ); // задать цвет iRed = rand() & 255; iGreen = rand() &, 255; iBlue = rand() & 255; // создать сплошную кисть hBrush = CreateSolidBrush( // предотвратить появление // полутоновых цветов GetNearestColor( hDC, RGB( iRed, iGreen, iBIue ) ) ); hbrOld = SelectBrush( hDC, hBrush); // нарисовать прямоугольник Rectangle( hDC, min( xStart, xStop ), max ( xStart, xStop ), min( yStart, yStop ), max( yStart, yStop ) ); // удалить кисть DeleteBrush( SelectBrush(hDC, hbrOld) ); } // Если выполняется только один поток, // очистить дочернее окно до начала выполнения // другого потока. if( bUseMutex ) { SelectBrush( hDC, GetStockBrush(WHITE_BRUSH) ); PatBlt( hDC, (int)rcClient.left, (int)rcClient.top, (int)rcClient.right, (int)rcClient.bottom, PATCOPY ) ; } // освободить контекст устройства ReleaseDC( hwndChild[dwID], hDC ); } return; } 16
1. 2. 3. 4.
Содержание отчета: Цель работы; Исходный текст программы; Результаты работы программы (главное окно в ОС Windows); Выводы по проделанной работе с указанием достоинств и недостатков предложенного исходного кода.
ЛАБОРАТОРНАЯ РАБОТА №2 "Выделение памяти" Цель работы: Изучение принципов разработки программы, позволяющей резервировать и закреплять память (На примере программы List). Задание к лабораторной работе: 1. Запустить программу List. Результат работы программы представлен на рис.2.1. В результате исполнения создается список, резервируется и закрепляется виртуальная память, когда пользователь вводит новые элементы списка. Список, заполняющий рабочую область окна, содержит перечень всех введенных элементов. Меню List позволяет добавлять и удалять элементы списка. 2. Рассмотреть исходный текст программы. В файле заголовков определены две константы, которые управляют размером и структурой списка. Для каждого элемента списка выделено 1024 символа. Поскольку размер системной страницы (4 Кб) кратен размеру элемента списка (1 Кб), ни один элемент не пересечет границу между страницами. Следовательно, для чтения любого элемента не потребуется загрузка с диска более одной страницы. В программе List максимальный размер массива составляет 500 элементов. Среди глобальных переменных, определенных в программе, имеется два массива: iListLookup и bInUse. int iListLookup[MAX_ITEMS]; // согласует индекс элемента списка // с его положением в массиве BOOL bInUse[MAX_ITEMS]; // отмечает используемые элементы списка Первый массив связывает строки списка с элементами динамического массива. Если элемент массива iListLookup[4] равен 7, строка с номером 5 из списка будет записана в элементе динамического массива с индексом 7. (Как в списке, так и в динамическом массиве нумерация элементов начинается с 0.) 17
Второй массив содержит значения типа Boolean для каждого элемента динамического массива. Когда программа добавляет или удаляет строки, она устанавливает для соответствующего элемента массива bInUse значение TRUE, если позиция занята, и FALSE, если она пуста. При добавлении строки программа ищет пустой элемент в массиве bInUse, а при удалении определяет позицию элемента массива, обращаясь к массиву iListLookup.
Рис 2.1 Окно, позволяющее добавлять новые элементы в список При запуске программы осуществляется вызов функции CreateList, которая резервирует память и инициализирует вспомогательные структуры данных. Команда VirtualAlloc резервирует блок адресов размером 1 Мб. Подобно остальным зарезервированным страницам, адреса должны быть помечены флагом PAGE_NOACCESS, пока они не будут закреплены. BOOL CreateList ( void ) { int i; // зарезервировать 1 Мб адресного пространства pBase = VirtualAlloc( NULL, // начальный адрес (произвольный) MAX_ITEMS * ITEM_SIZE, // один мегабайт MEM_RESERVE, // зарезервировать; //не закреплять PAGE_NOACCESS ); // нет доступа if( pBase == NULL ) { ShowErrorMsg( __LINE__ ) ; return( FALSE ) ; 18
} // инициализация служебных массивов for ( i = 0; i < MAX_ITEMS; i++ ) { bInUse[i] = FALSE; // ни один из элементов // не используется iListLookup[i] = 0; } bListEmpty = TRUE; // обновить глобальные флаги bListFull = FALSE; return ( TRUE ) ; } 3. Добавить элемент списка. При выборе пункта Add Item в меню программы List функция AddItem выполняет следующие действия. 1. Находит первый свободный элемент в массиве (iIndex). 2. Предлагает пользователю ввести новую строку в диалоговом окне. 3. Копирует новую строку в блок памяти, выделенный при выполнении команды CreateList. 4. Обновляет список и несколько глобальных переменных. Первый свободный элемент массива может занимать незакрепленную страницу памяти. В этом случае команда lstrcpy, которая пытается записать новую строку, генерирует исключение. Обработчик исключений ожидает, когда поступит сигнал EXCEPTION_ACCESS_VIOLATIUN, и реагирует на него вызовом функции VirtualAlloc, которая закрепляет страницу из предварительно зарезервированного интервала. void AddItem ( void ) { char szText[ITEM_SIZE]; int iLen; int iIndex; int iPos; int iCount; BOOL bDone;
// текст для одного элемента // длина строки // положение в массиве // положение в списке //счетчик элементов списка // TRUE, если найден свободный // элемент массива // определение положения первого свободного элемента bDone = FALSE; iIndex = 0; while( ( ! bDone ) && ( iIndex < MAX_ITEMS ) ) { if( ! bInUse[iIndex] ) bDone = TRUE;
// используется ли данный элемент? // обнаружен свободный элемент 19
else iIndex++;
// переход к следующему элементу
} // предложить пользователю ввести новую строку iLen = GetItemText(szText); if( ! iLen ) return; В блоке try новый текст копируется в свободную часть массива. Если соответствующая страница памяти не закреплена, функция lstrcpy порождает исключение. Фильтр исключений закрепляет страницу, и выполнение функции продолжается. try {
// записать текст в элемент массива lstrcpy( &( pBase[iIndex * ITEM_SIZE) ), szText );
} except( CommitMemFilter( GetExceptionCode(), iIndex ) ) { // вся работа выполняется фильтром исключений } // пометить данный элемент как занятый bInUse[iIndex] = TRUE; bListEmpty = FALSE; Далее программа добавляет новый текст в список. Строка вставляется в позицию, которая задана переменной iPos. При этом обновляется элемент iListLookup [ipos], который указывает, где в массиве записана новая строка (iIndex). iCount = ListBox_GetCount( hwndList ) ; iPos = ListBox_InsertString( hwndList, iCount, szText ); iCount++; ListBox_SetCurSel( hwndList, iPos ); iListLookup[iPos] = iIndex; if (iCount == MAX_ITEMS) // заполнена ли последняя позиция? { bListFull= TRUE; } return; } Функция CommitMemFilter представляет собой фильтр обработки исключений для функции AddItem. При наличии страничной ошибки функция 20
CommitMemFilter пытается закрепить страницу и, если ее старания приводят к успеху, продолжает выполнение команды lstrcopy. Если функция CommitMemFilter не справляется с задачей, поиск соответствующего обработчика исключения продолжается. LONG CommitMemFilter ( DWORD dwExceptCode, // код, идентифицирующий исключение int iIndex ) // элемент массива, в котором произошла ошибка { LPVOID lpvResult; // Если исключение не является страничной ошибкой, // отказаться обрабатывать его и поручить системе // поиск соответствующего обработчика исключений. if( dwExceptCode != EXCEPTION_ACCESS_VIOLATION ) { return( EXCEPTION_CONTINUE_SEARCH ) ; } // Попытка закрепить страницу. lpvResult = VirtualAlloc( &( pBase(iIndex * ITEM_SIZE] ), // нижняя граница закрепляемой области ITEM_SIZE, // размер закрепляемой области MEM_COMMIT, // новый флаг состояния PAGE_READWRITE ); // уровень защиты if( ! lpvResult ) // произошла ли ошибка выделения памяти? { // Если мы не можем закрепить страницу, то не сможем обработать исключение return( EXCEPTION_CONTINUE_SEARCH ); } // Недостающая страница теперь находится на своем месте. // Система должна вернуться назад и повторить попытку. return( EXCEPTION_CONTINUE_EXECUTION ) ; } 4. Удалить элемент списка. Функция DeleteItem удаляет элемент из виртуального массива, она проверяет, не осталось ли других элементов в этой странице памяти. Если все четыре элемента, которые находятся на странице, пусты, функция отменяет закрепление страницы, передавая дополнительные 4 Кб памяти в распоряжение системы. Виртуальные адреса страницы остаются зарезервированными. Независимо от того, освобождает команда DeleteItem страницу или нет, она удаляет строку из списка и обновляет глобальные переменные состояния. 21
Выполнению этих операций способствуют две дополнительные функции. Функция GetPageBaseEntry получает в качестве аргумента индекс массива и возвращает индекс первого элемента соответствующего страничного блока. Иными словами, эта функция производит округление до ближайшего меньшего значения, кратного четырем. Функция AdjustLookupTable удаляет запись, выделенную в списке, и смещает все последующие элементы для заполнения образовавшейся пустоты. void DeleteItem ( void ) { int iCurSel; // позиция текущей выбранной строки в списке int iPlace; // позиция текущей выбранной строки в массиве элементов int iStart; // позиция первого элемента в той странице памяти, // где находится выбранный элемент int i; // переменная цикла BOOL bFree; // TRUE, если все 4 элемента, содержащиеся в данной // странице памяти, не используются BOOL bTest; // для проверки результатов // определяет смещение в памяти текущей выбранной записи iCurSel = ListBox_GetCurSel( hwndList ) ; iPLace = iListLookup [iCurSel] ; // обнуляет удаленный элемент FillMemory( & (pBase [iPlace * ITEM_SIZE] ) , ITEM_SIZE, 0 ); // помечает этот элемент как свободный bInUse[iPlace] = FALSE; На данном этапе определяется номер первого элемента в текущей странице памяти. Если все четыре элемента, которые находятся на данной странице, свободны, закрепление страницы отменяется. iStart = GetPageBaseEntry( iPlace ) ; bFree = TRUE; for( i = 0; i < 4; i++ ) // проверка четырех записей { if( bInUse[i + iStart] ) // используется? { bFree = FALSE; // страница занята } } Если не используется вся страница памяти, она освобождается. if( bFree ) {
// свободна ли страница? // ДА; освободить ее 22
bTest = VirtualFree( &( pBase[iStart * ITEM_SIZE] ), ITEM_SIZE, MEM_DECOMMIT ) ; if( ! bTest ) { ShowErrorMsg( __LINE__ ); ExitProcess( (UINT)GetLastError() ); } }
Далее обновляеся список и массив связей и проверяется, остались ли еще элементы в списке. ListBox_DeleteString( hwndList, iCurSel ); AdjustLookupTable( iCurSel ); bListEmpty =TRUE; i = 0; while( ( i < MAX_ITEMS ) && ( bListEmpty ) ) { // если элемент используется, значит, список не пустой bListEmpty = !bInUse[i++] ; } // изменить положение маркера выделения в списке if(! bListEmpty ) { if( iCurSel ) // удален ли первый элемент списка? { // нет; выбрать элемент над удаленной // записью ListBox_SetCurSel( hwndList, iCurSel-1 ); } else // удаленная запись была самой верхней // в списке; { // выбрать новую верхнюю запись ListBox_SetCurSel ( hwndList, iCurSel ); } } return; }
Когда программа удаляет все элементы списка, вызывается функция DeleteList, которая освобождает память, прежде занятую записями. 23
void DeleteList() { // отменить закрепление памяти и освободить адресное пространство // Для операции MEM_DECOMMIT необходимо указать размер области if( ! VirtualFree( (void*) pBase, MAX_ITEMS*ITEM_SIZE, МЕМ_DECOMMIT)) ShowErrorMsg ( __LINE__ ) ; // освободить память, начиная с базового адреса; // указывать размер не требуется if( ! VirtualFree( (void*) pBase, 0, MEM_RELEASE ) ) ShowErrorMsg( __LINE__ ); return; } По индексу в списке функция GetPageBaseEntry определяет первый элемент соответствующей страницы памяти, округляя индекс до ближайшего значения, кратного четырем и меньшего или равного iPlace. int GetPageBaseEntry ( int iPlace ) { while( iPlace % 4 ) { iPlace--; } return( iPlace ) ; } После удаления записи из списка необходимо обновить массив, который связывает элементы списка со значениями смещения в памяти. Параметр iStart задает позицию строки, удаленной из списка. void AdjustLookupTable ( int iStart ) { int i; // Этот цикл начинается с позиции, из которой только что // была удалена запись. Все последующие элементы смещаются // таким образом, чтобы заполнить образовавшееся свободное место. for( i = iStart; i < MAX_ITEMS - 1; i++ ) { iListLookup[i] = iListLookup[i + 1]; } iListLookup[MAX_ITEMS - 1] = 0; 24
return; } Остальные функции в программе List служат для вызова диалогового окна; в котором пользователь вводит текст, отображения окна About и вывода сообщений об ошибках. 1. 2. 3. 4.
Содержание отчета: Цель работы; Исходный текст программы; Результаты работы программы (главное окно в ОС Windows); Выводы по проделанной работе с указанием достоинств и недостатков предложенного исходного кода.
ЛАБОРАТОРНАЯ РАБОТА №3 "Реестр ОС Windows" Цель работы: Изучение принципов разработки программы, позволяющей производить запись и считывание данных с реестра ОС Windows (на примере программы Reg_Ops). Задание к лабораторной работе: 1. Перед запуском программы Reg_Ops открыть с помощью проводника Windows, менеджера файлов или любой другой программы просмотра файл Special.REG. Вызванная при этом утилита RegEdit создаст несколько дополнительных записей в разделе HKEY_LOCAL_MACHINE\SOFTWARE\ системного реестра. Содержимое текстового файла Special.REG представлено ниже. REGEDIT4 [HKEY_LOCAL_MACHINE\SOFTWARE\Registry Demo] [HKEY_LOCAL_MACHINE\SOFTWARE\Registry Demo\Charlie Brown] ''Password"="@ABCDGCGJ" "Status"=dword:000003e9 [HKEY_LOCAL_MACHINE\SOFTWARE\Registry Demo\Henry Ford] "Password"="B@@EC" 25
"Status"=dword:000003ea [HKEY_LOCAL_MACHINE\SOFTWARE\Registry Demo\Billy Sol Estes] "Password"="ABDABBCGE" "Status"=dword:000003eb [HKEY_LOCAL_MACHINE\SOFTWARE\Registry Demo\Thomas Alva Edison] "Password"="AA@@F" "Status"=dword:000003ec [HKEY_LOCAL_MACHINE\SOFTWARE\Registry Demo\Blaise Pascal] "Password"="ACBBDEGE" "Status"=dword:000003ed [HKEY_LOCAL_MACHINE\SOFTWARE\Not Very Secret] "Secret Key"="Rumplestiltskin" Программа Reg_Ops использует приведенные записи реестра в качестве исходных данных для демонстрируемых операций. 2. Рассмотреть исходный текст программы Reg_Ops. Программа начинается с вызова метода OnInitDialog, который использует функцию RegCreateKey для открытия дескриптора личного раздела приложения в корневом разделе HKEY_LOCAL_MACHINE. BOOL CReg_OpsDlg::OnInitDialog () { CDialog::OnInitDialog() ; ... // начинается с открытия разделов для записи данных в реестр RegCreateKey ( HKEY_LOCAL_MACHINE, "SOFTWARE \ Registry demo", &m_hRegKey ); ResetButtons() ; // инициализация поля со списком InitializeListBox() ; return TRUE; // возвращается значение TRUE, если фокус не // устанавливается на элементе управления } Поскольку в списке содержатся имена пользователей, флаги состояния и ключи для проверки паролей, программа записывает эту информацию в раздел HKEY_LOCAL_MACHINE, а не в раздел HKEY_USERS. Если нужно зарегистрировать информацию о привилегиях конкретного пользователя, ее следует записать в раздел HKEY_CURRENT_USER. Такая информация будет 26
доступной сразу после входа данного пользователя в систему. Функция RegCreateKey открывает раздел, если он уже существует, или создает его, если раздела еще нет, и возвращает дескриптор в переменную m_hRegKey соответствующего раздела. Поскольку эта переменная является переменнойчленом (тип HKEY), она доступна и для других функций приложения. Если предполагается, что доступ к разделу будет происходить часто и этот раздел будет одновременно использоваться различными приложениями, удобно один раз открыть дескриптор раздела и использовать его в дальнейшем для доступа к подразделам. В других случаях, когда доступ к разделу будет происходить лишь изредка, можно не торопиться и открывать этот раздел только при необходимости. 3. Рассмотреть режим чтения данных из разделов реестра. С помощью переменной m_hRegKey программа инициализирует содержимое списка, читая записи из подразделов реестра (см. файл Reg_OpsDlg.CPP). BOOL CReg_OpsDlg::InitializeListBox() { DWORD dwName, dwSubkeys, dwIndex = 0; long lResult; TCHAR szBuff[MAX_PATH+l] ; CString csBuff; EnableButtons ( FALSE ); lResult = RegQueryInfoKey( m_hRegKey, NULL, NULL, NULL, &dwSubkeys, NULL, NULL, NULL, NULL, NULL, NULL, NULL ) ; if( lResult == ERROR_SUCCESS ) dwIndex = dwSubkeys; else ReportError( lResult ); Прежде чем приступить к чтению содержимого подразделов используется API-функция RegQueryInfoKey, которая определяет, сколько подразделов содержит раздел HKEY_LOCAL_MACHINE\SOFTWARE\Registry Demo. Функция RegQueryInfoKey имеет несколько аргументов, но всем ненужным аргументам можно просто присвоить значение NULL, как это сделано в приведенном выше фрагменте. Такой подход дает возможность запросить только одно значение, не заботясь о параметрах, которые не интересуют. Если для инициализации реестра будут использованы данные, которые содержатся в файле Special.REG, функция RegQueryInfoKey сообщит о наличии пяти подразделов. Теперь, зная, сколько подразделов содержится в выбранном разделе, для переменной dwName следует задать размер, равный размеру массива szBuff. Поскольку величина szBuff не должна изменяться, эта операция будет выполнена только один раз, и значение dwName сохранится неизменным. 27
dwName = sizeof( szBuff ); do { lResult = RegEnumKey( m_hRegKey, --dwIndex, (LPTSTR)szBuff, dwName ); Так как нумерация подразделов начинается с нуля, значение переменной dwIndex перед его использованием следует декрементировать, т.е. необходимо запросить пять подразделов с индексами 4, 3, 2, 1 и 0. Посредством функции RegEnumKey можно осуществить циклический запрос имен подразделов. Каждый раз при вызове функции RegEnumKey со следующим значением номера dwIndex в переменной lpName возвращается имя другой подстроки. Когда список подстрок заканчивается, функция RegEnumKey выдает код ошибки ERROR_NO_MORE_ITEMS, свидетельствующей о том, что подразделов больше нет. Порядок, в котором эти элементы возвращаются, не имеет значения. До тех пор пока не задан индекс, для которого подраздела с данным номером нет, функция будет возвращать значение ERROR_SUCCESS, а в массиве lpName будет храниться имя соответствующего подраздела. При получении любого результирующего значения, кроме ERROR_SUCCESS и ERROR_NO_MORE_ITEMS, должно выводиться сообщение об ошибке. if(lResult != ERROR_SUCCESS && lResult != ERROR_NO_MORE_ITEMS ) ReportError ( lResult ); else if( lResult == ERROR_SUCCESS ) { HKEY hKeyItem; ULONG ulSize, ulType; int nIndex; m_csNameEntry = szBuff; Если был получен результат ERROR_SUCCESS, строку из переменной szBuff необходимо передать в переменную m_csNameEntry, являющуюся членом класса CString. Переменная szBuff сразу же будет использоваться повторно, но содержащееся в ней значение не должно потеряться. Далее снова вызвать функцию RegCreateKey. На этот раз для идентификации открываемого подраздела используется дескриптор корневого раздела m_hRegKey и строковое значение, которое содержится в переменной szBuff. В результате получен дескриптор hKeyItem, идентифицирующий открытый подраздел. 28
RegCreateKey( m_hRegKey, szBuff, ShKeyItem ); Так как заранее известно, что этот раздел существует (об этом свидетельствует результат выполнения функции RegEnumKey), функция RegCreateKey просто открывает раздел и возвращает его дескриптор, который может использоваться в запросах для определения значений параметров, содержащихся в открытом разделе. ulSize = sizeof( m_dwNameStatus ); lResult = RegQueryValueEx ( hKeyltem, "Status", NULL, &ulType, (LPBYTE) &m_dwNameStatus, &ulSize ); Указанная выше функция RegQueryValueEx запрашивает значение, содержащееся в именованном параметре Status. Вместе с этим значением переменная-член ulType возвращает также идентификатор соответствующего типа данных в виде длинного целого без знака. Информация о размере данных будет получена посредством переменной-члена ulSize. Независимо от того, что собой представляют запрашиваемые данные - значение типа DWORD, строковое значение, двоичный массив или данные любого другого типа, адрес получающей их переменной всегда преобразуется в формат указателя на байт. Затем, чтобы убедиться в корректности выполнения функции, произвести проверку значения lResult. В случае некорректного ее завершения на экране появляется сообщение об ошибке. if( lResult != ERROR_SUCCESS ) ReportError( lResult ); К этому моменту уже получен из подраздела код состояния, а также имя самого подраздела. Прежде чем цикл будет продолжен для следующего подраздела, эти значения необходимо задействовать путем размещения их соответственно в качестве строкового элемента списка ComboListbox и индекса данного элемента. nIndex = m_cbNamesList.AddString( m_csNameEntry ); m_cbNamesList.SetItemData( nIndex, m dwNameStatus ); } Затем цикл будет выполнен для всех остальных подразделов. } while( lResult != ERROR_NO_MORE_ITEMS ); return TRUE; }
29
Подпрограмма ConfirmPassword демонстрирует процедуру чтения параметра реестра в виде строкового значения. В этой подпрограмме имя, прочитанное из списка, используется для открытия дескриптора раздела, а затем для чтения пароля. BOOL CReg_OpsDlg::ConfirmPassword() { HKEY hKeyItem; ULONG ulSize, ulType; long lResult; TCHAR szBuff[MAX_PATH+1]; CString csBuff, csPassword; RegCreateKey ( m_hRegKey, m_csNameEntry, &hKeyItem ); ulSize = sizeof( szBuff ); lResult = RegQueryValueEx( hKeyItem, "Password", NULL, &ulType, (LPBYTE) szBuff, &ulSize ); if(lResult != ERROR_SUCCESS ) { ReportError( lResult ) ; return FALSE; } CEnterPassword *pDlg == new CEnterPassword(); if( pDlg->DoModal() == IDOK ) csPassword = theApp.Encrypt( pDlg->m_csPassword ); else return FALSE; return( csPassword == szBuff ); } В эту функцию также включена операция проверки ошибочных результатов. Затем пароль, полученный из диалогового окна класса CEnterPassword, перед проверкой правильности введенных данных передается подпрограмме Encrypt класса CReg_OpsApp. 4. Рассмотреть режим записи данных в разделы реестра. Открыть раздел с помощью функции RegCreateKey. Для этого, сначала вызвать функцию RegOpenKey. Если указанный раздел уже существует, то подготовить его для записи новой информации, в противном случае - создать его. После открытия или создания соответствующего раздела производится вызов функции RegSetValueEx, которая записывает информацию в реестр. Подпрограмма AddToRegistry содержит два примера использования функции RegSetValueEx для записи данных в реестр. Первый раз функция RegSetValueEx применяется для записи строкового значения с указанием типа REG_SZ: 30
BOOL CReg_OpsDlg::AddToRegistry() { HKEY hKeyNew; RegCreateKey( m_hRegKey, m_csNameEntry, ShKeyNew ) ; RegSetValueEx( hKeyNew, "Password", NULL, REG_SZ, (LPBYTE)(LPCTSTR)m_csPassword, m_csPassword.GetLength()+1 ); Поскольку указывается тип REG_SZ, к аргументу, задающему размер значения, необходимо добавить единицу - в таком случае будет включен завершающий нулевой символ, который не учитывается в значении, возвращаемом функцией GetLength. Ранее указывалось, что функции для работы с реестром не распознают классов библиотеки MFC, в том числе и класс CString, однако в представленном примере в качестве аргументов использованы две ссылки на этот класс. Причем в первом случае значение приведено к типу LPCSTR, определяющему его как массив символов, во втором же случае передается целочисленное значение, возвращаемое функцией-членом. Во второй операции с функцией RegSetValueEx необходимо записать значение типа DWORD: RegSetValueEx( hKeyNew, "Status", NULL, REG_DWORD, (LPBYTE)&m_dwNameStatus, sizeof( m_dwNameStatus) ); return TRUE; } Записываемое значение передается с помощью адреса, после которого указывается размер данных (в байтах). В обеих функциях RegSetValueEx использован аргумент типа LPBYTE (указатель на байт) и что данные всегда должны передаваться по адресу. 5. Рассмотреть подпрограмму системных сообщений об ошибках. В данном случае для интерпретации сообщений об ошибках, возвращаемых функциями RegQueryInfoKey, RegEnumKey и RegQueryValueEx, применяется функция ReportError. При вызове функции ReportError ей в качестве аргумента передается код ошибки, возвращаемый любой другой функцией, которая осуществляет обращение к реестру. В функции ReportError вызывается функция FormatMessage с флагом FORMAT_MESSAGE_FROM_SYSTEM, которая возвращает строку с разъяснением соответствующей системной ошибки. Флаг FORMAT_MESSAGE_ALLOCATE_BUFFER позволяет использовать допустимый указатель буфера, реально не выделяя его. Функция FormatMessage сама управляет выделением памяти, руководствуясь размером сообщения. При использовании флага FORMAT_MESSAGE_ALLOCATE_BUFFER после завершения работы необходимо вызвать функцию LocalFree и освободить память, выделенную для размещения буфера. 31
void CReg_OpsDlg::ReportError( long lError ) { #ifdef _DEBUG LPVOID lpMsgBuf; FormatMessage ( FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, NULL, lError, MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // язык, заданный по умолчанию (LPTSTR) SipMsgBuf, 0, NULL ) ; MessageBox( (char *)lpMsgBuf ); LocalFree( lpMsgBuf ) ; #endif } Если вместо аргумента lError указать функцию GetLastError, то представленную выше подпрограмму можно будет использовать для интерпретации других ошибок, для которых код ошибки не был явно возвращен. Функцию ReportError не рекомендуется включать в окончательную версию приложения. Возвращаемые ею сообщения об ошибках не несут полезной для пользователей информации, так как предназначены для программистов - помогают им при отладке программы. Поэтому в подпрограмме содержится конструкция #ifdef _DEBUG / #endif. 6. Запустить программу Reg_Ops. Диалоговое окно программы, содержащее поле со списком и набор кнопок состояния, представлено на рисунке 3.1. Программа позволяет выбрать одно из имен, содержащихся в списке (при этом будет запрошен пароль), либо ввести новое имя в поле, назначить статус и задать пароль для данной записи. Введенное имя будет присвоено новому подразделу реестра, а код состояния и пароль станут именованными параметрами этого подраздела.
Рис.3.1
Окно программы Reg_Ops, демонстрирующей выполнения операций с реестром
принципы
Разобраться, какие действия выполняются каждой из кнопок состояния (за исключением кнопки Computer Dweeb). Использование кнопки System Guru (системный гуру) подразумевает, что пользователь обладает 32
определенными знаниями о реестре (не слишком глубокими) - только при таком условии будет присвоен соответствующий статус. Подпрограмма шифрования пароля принимает строку и вырабатывает код проверки, который может использоваться впоследствии для сравнения с новым введенным значением без сохранения пароля. Такой механизм довольно прост и не обеспечивает стопроцентной надежности, однако для целей программы вполне пригоден. Рассмотреть реализацию этого механизма. Ниже представлен список имен, содержащихся в файле Special.REG, и для каждого из них указан пароль. Charlie Brown Henry Ford Billy Sol Estes Thomas Alva Edison Blaise Pascal 1. 2. 3. 4.
Countess Menlo fefifofum edsel anhydrous
Содержание отчета: Цель работы; Исходный текст программы; Результаты работы программы (диалоговое окно в ОС Windows); Выводы по проделанной работе с указанием достоинств и недостатков предложенного исходного кода.
ЛАБОРАТОРНАЯ РАБОТА №4 "Буфер обмена ОС Windows" Цель работы: Изучение принципов работы с буфером обмена. (На примере программы ClipBoard). Задание к лабораторной работе: 1. Запустить программу ClipBoard. Данная программа демонстрирует запись в буфер обмена и чтение из него данных трех различных типов: текста, растрового изображения и метафайла. Меню программы содержит два основных подменю, Data To Clipboard и Data from Clipboard, каждое из которых предлагает весьма "прозаический" набор команд, а именно Write и Retrieve. Команда Write Bitmap осуществляет простейшую операцию захвата экрана (по крайней мере, фрагмента экрана размером 640х480) в виде 33
растрового изображения и записи этого изображения в буфер обмена. Команда Write Text копирует в буфер обмена простую строку текста. 2. Рассмотреть механизм чтения и записи текста. Текстовые операции являются простейшими операциями, которые выполняются с буфером обмена. Запись текста. Поскольку программа ClipBoard является одновременно и источником, и приемником данных, первый шаг при работе с ней заключается в передаче текстовой информации в буфер обмена. В качестве записываемого текста выбрана фиксированная строка "The quick brown fox jumps over the lazy red dog". Механизм обработки текста реализован в виде функции, которая вызывается с двумя параметрами: дескриптором приложения hwnd и указателем строки текста lpText. BOOL TextToClipboard( HWND hwnd, LPSTR lpText ) { int i, wLen; GLOBALHANDLE hGMem; LPSTR lpGMem; В функции TextToClipboard используются четыре локальных переменных, но пояснений требуют только две последние из них. Переменная hGMem содержит глобальный дескриптор блока памяти, который пока не был выделен. Вторая переменная, lpGMem, служит указателем блока памяти. После инициализации переменной wLen и присвоения ей значения длины текстовой строки дескриптор hGMem становится идентификатором глобального блока памяти, выделенного для хранения копии текста. Однако обратите внимание, что значение переменной wLen на единицу больше длины строки, поскольку в строку входит еще и завершающий пустой символ. В буфер обмена текст всегда записывается в формате ASCIIZ (или ANSIZ): wLen = strlen( lpText ); hGMem = GlobalAlloc( GHND, (DWORD) wLen + 1 ); lpGMem = GlobalLock( hGMem ); Наконец, переменной lpGMem присваивается указатель блока памяти, полученный в результате выполнения функции GlobalLock. Но не забывайте, что спецификация GHND объявляет этот блок как перемещаемый. Функция GlobalLock временно предотвращает перемещение блока памяти операционной системой. Вторая особенность спецификации GHND заключается в том, что выделенный блок памяти освобождается от своего текущего содержимого и заполняется нулями. Таким образом, следующая операция - это копирование в выделенный блок памяти локальной строки, заданной указателем lpText:
34
for( i=0; i<wLen; i++ ) *lpGMem = *lpText++; GlobalUnlock( hGMem ); return( TransferToClipboard( hwnd, hGMem, CF_TEXT ) ); } После копирования текста вызывается функция GlobalUnlock, которая отменяет блокировку дескриптора hGMem, разрешая перемещение и повторное выделение блока памяти. Если бы блок памяти был перемещен до копирования текста, указатель lpGMem стал бы недействительным. В завершение процесса передачи данных вызывается рассмотренная выше функция TransferToClipboard с указанием дескриптора блока hGMem, флага CF_TEXT и дескриптора окна приложения. Чтение текста. Чтение текста из буфера обмена осуществляется столь же просто, как и запись, однако отдельная процедура при этом не вызывается, а действия выполняются в ответ на сообщение WM_PAINT. Это позволяет приложению при необходимости обновлять окно. Операции чтения начинаются с открытия буфера обмена и вызова APIфункции GetClipboardData, которая возвращает дескриптор блока памяти, принадлежащего буферу: OpenClipboard( hwnd); hTextMem = GetClipboardData( CF_TEXT ); lpText = GlobalLock( hTextMem ); Как и при записи данных в буфер обмена, функция GlobalLock блокирует область памяти и ее адрес сохраняется в переменной lpText. Но на этот раз содержимое строки копируется из области памяти (переменная lpText) в локальную переменную TextStr не в цикле, а напрямую, с помощью функции lstrcpy: lstrcpy( TextStr, lpText ); GlobalUnlock( hTextMem ); CloseClipboard(); Наконец, функция GlobalUnlock снимает блокировку области памяти, а функция CloseClipboard завершает сеанс работы с буфером обмена. Важно помнить, что область памяти никогда не должна оставаться заблокированной и вызов функции GlobalLock всегда должен сопровождаться вызовом GlobalUnlock.
35
3. Рассмотреть механизм чтения и записи растровых изображений. В программе ClipBoard содержится пример передачи посредством буфера обмена растрового изображения. Эта часть программы начинается с захвата текущего содержимого экрана, которое передается в буфер обмена в виде растрового изображения. На рис. 4.1 показаны окно программы ClipBoard после выполнения операции захвата экрана.
Рис. 4.1. Захват растрового изображения Запись растрового изображения. Процесс передачи изображения в буфер обмена начинается с создания в памяти совместимого контекста устройства hdcMem. Затем происходит создание и выбор (также в памяти) совместимого растрового изображения. hdc = GetDC( hwnd ) ; hdcMem = CreateCompatibleDC( hdc ); hBitmap = CreateCompatibleBitmap( hdc, 640, 480 ); SelectObject( hdcMem, hBitmap ) ; Далее происходит копирование изображения из источника (в нашем случае - с экрана) в контекст памяти. Затем вызывается функция TransferToClipboard, которая, собственно, и осуществляет запись в буфер. StretchBlt( hdcMem, 0, 0, 639, 479, 36
hdc, 0, 0, 639, 479, SRCCOPY ); TransferToClipboard( hwnd, hBitmap, CE_BITMAP ); DeleteDC( hdcMem ); Наконец, контекст устройства удаляется из памяти, а право на владение растровым изображением передается буферу обмена. Чтение растрового изображения. Аналогичным образом происходит и чтение растрового изображения из буфера обмена. Процесс начинается с открытия буфера и чтения из него дескриптора растрового изображения: OpenClipboard( hwnd ); hBitmap = GetClipboardData ( CF_BITMAP ) ; hdcMem = CreateCompatibleDC ( hdc ); SelectObject( hdcMem, hBitmap ); Опять-таки, необходим совместимый контекст устройства. Затем функция SelectObject загружает в него изображение. Параллельно с описанными выполняются еще несколько задач. Вопервых, в контексте памяти необходимо задать режим отображения, совместимый с контекстом дисплея. Во-вторых, перед тем как изображение будет скопировано, нужно определить его размер. Размер изображения можно получить путем копирования его заголовка в локальную переменную bm: SetMapMode(hdcMem,.GetMapMode( hdc ) ); GetObject( hBitmap, sizeof(BITMAP), (LPSTR) &bm ); Далее изображение с помощью функции BitBlt копируется из контекста памяти в контекст устройства: BitBlt( hdc, 0, 0, bm.bmWtdth, bm.bmHeight, hdcMem, 0, 0, SRCCOPY ); ReleaseDC( hwnd, hdc ); DeleteDC( hdcMem ); CloseClipboard(); Осталось только очистить буфер обмена перед его закрытием, и задача будет выполнена. 4. Рассмотреть механизм чтения и записи метафайлов. Запись метафайла. В текстовом и декартовых режимах отображения масштаб является фиксированным. Изотропный и анизотропный режимы требуют наличия специальной информации, сопровождающей команды метафайла. 37
Для передачи метафайла посредством буфера обмена применяется структура METAFILEPICT, в которой записываются режим отображения, информация о размерах, а также сам сценарий метафайла. Структура METAFILEPICT определена в файле WinGDI.H: typedef struct tagMETAFILEPICT { LONG mm; LONG xExt; LONG yExt; HMETAFILE hMF; } METAFILEPICT, FAR *LPMETAFILEPICT; Поле mm содержит идентификатор режима отображения. Поле hMF представляет собой дескриптор набора команд метафайла. Остальные два поля, xExt и yExt, могут содержать два разных типа информации в зависимости от режима отображения. Для текстового и декартовых режимов в полях xExt и yExt задаются размеры содержащегося в метафайле изображения по горизонтали и по вертикали, в единицах, которые соответствуют режиму отображения. При использовании изотропного и анизотропного режимов отображения поля xExt и yExt содержат необязательные, рекомендуемые размеры, выраженные в единицах MM_HIMETRIC. Если рекомендуемые размеры не указываются, эти поля могут содержать нулевые значения. Если же поля xExt и yExt имеют отрицательные значения, значит, они представляют соотношения размеров, а не абсолютные размеры. Функция DrawMetafile создает метафайл в памяти и после создания метафайла следующий шаг заключается в формировании структуры METAFILEPICT в памяти и получении с помощью функции GlobalLock ее указателя lpMFP: hGMem = GlobalAlloc( GHND, (DWORD) sizeof( METAFILEPICT ) ) ; lpMFP = (LPMETAFILEPICT) GlobalLock( hGMem ) ; Теперь можно задать соответствующий режим отображения, размеры изображения и назначить дескриптор метафайла. lpMFP->mm = MM_ISOTROPIC; lpMFP->xExt = 200; // рекомендуемые размеры // lpMFP->yExt = 200; // в единицах MM_HIMETRIC // lpMFP->hMF = hMetaFile;
38
Далее остается только разблокировать память и передать дескриптор метафайла в буфер обмена. GlobalUnlock( hGMem ) ; TransferToClipboard( hwnd, hGMem, CF_METAFILEPICT ); Чтение метафайла. Чтение метафайла начинается с открытия буфера обмена, запроса дескриптора блока памяти, в котором содержится метафайл, и последующей блокировки этой области. OpenClipboard( hwnd ); hGMem = GetClipboardData( CF_METAFILEPICT ); lpMFP = (LPMETAFILEPICT) GlobalLock( hGMem ); Теперь переменная lpMFP содержит указатель блока памяти, в котором находится метафайл, или, точнее, структуры METAFILEPICT, которая, в свою очередь, хранит указатель собственно метафайла. Но прежде чем метафайл будет воспроизведен, необходимо решить ряд дополнительных задач, начав с сохранения текущего контекста устройства. SaveDC( hdc ); CreateMapMode( hdc, lpMFP, cxWnd, cyWnd ) ; Далее информация, которая содержится в структуре METAFILEPICT, передается на обработку функции CreateMapMode. Это делается потому, что расшифровка информации о режиме отображения и о размерах - задача довольно сложная. Функция CreateMapMode вызывается с четырьмя параметрами, которые задают дескриптор контекста устройства для приложения, указатель структуры METAFILEPICT и размеры окна приложения. BOOL CreateMapMode( HDC hdc, LPMETAFILEPICT lpMFP, int cxWnd, int cyWnd ) { long lMapScale; int nHRes, nVRes, nHSize, nVSize; SetMapModet hdc, lpMFP->mm ); if( lpMFP->mm!=MM_ISOTROPIC&&lpMFP->mm!= MM_ANISOTROPIC ) return( TRUE ) ; Сначала функция CreateMapMode устанавливает режим отображения, заданный для метафайла. Если задано какое-либо другое значение, не 39
MM_ISOTROPIC или MM_ANISOTROPIC, функция просто завершает свою работу, не выполняя никаких действий. Если режим отображения является изотропным или анизотропным, функция CreateMapMode должна осуществить дополнительные операции. Вопервых, вызывается функция GetDeviceCaps, которая возвращает горизонтальный и вертикальный размеры экрана, а также разрешение. nHRes = GetDeviceCaps( hdc, HORZRES ) ; nVRes = GetDeviceCaps( hdc, VERTRES ) ; nHSize = GetDeviceCaps( hdc, HORZSIZE ); nVSize = GetDeviceCaps( hdc, VERTSIZE ) ; Последующие действия зависят от значений полей xExt и yExt, которые могут быть положительными, отрицательными или нулевыми. Если размеры полей заданы посредством положительных значений, они воспринимаются как рекомендуемые размеры изображения в единицах MM_HIMETRIC. В этом случае вызывается функция SetViewportExtEx, которая соответствующим образом задает размеры области просмотра. if( lpMFP->xExt > 0 ) SetViewportExtEx( hdc,(int)((long) lpMFP->xExt * nHRes / nHSize / 100 ), (int)((long) lpMFP->yExt * nHRes / nHSize / 100 ), NULL ); Если указаны отрицательные значения, содержимое полей интерпретируется как масштабный коэффициент. Поэтому сначала производится расчет масштаба по отношению к контексту устройства. else if( lpMFP->xExt < 0 ) { lMapScale = min( ( 100L * (long) cxWnd * nHSize / nHRes / -lpMFP->xExt ), (100L * (long) cyWnd * nVSize / nVRes /-lpMFP->yExt ) ); Вычисляются два масштаба: по оси Х и по оси Y. Однако в качестве значения lMapScale берется меньшая из двух величин, чтобы результирующее изображение точно помещалось на экране. Теперь функция SetViewportExtEx вызывается еще раз - для согласования размеров изображения и области просмотра. SetViewportExtEx( hdc,(int)((long) -lpMFP->xExt * lMapScale * nHRes / nHSize / 100 ),(int)((long) -lpMFP->yExt * lMapScale * nVRes / nVSize / 100 ), NULL ); } 40
Кроме того, размеры (или масштабные коэффициенты) могут быть не заданы. В этом случае размеры области просмотра будут просто совпадать с размерами окна. else SetViewportExtEx( hdc, cxWnd, cyWnd, NULL ) ; Когда результат функции CreateMapMode известен, оставшаяся часть задачи выполняется очень просто. Нужно только вызвать функцию PlayMetaFile, точно таким же образом, как было показано ранее. PlayMetaFile( hdc, lpMFP->hMF ); RestoreDC( hdc, -1 ) ; GlobalUnlock( hGMem ) ; CloseClipboard() ; После воспроизведения метафайла вызывается функция RestoreDC с аргументом -1 (чтобы восстановить исходный контекст устройства), разблокируется память метафайла и закрывается буфер обмена. 1. 2. 3. 4.
Содержание отчета: Цель работы; Исходный текст программы; Результаты работы программы (диалоговое окно в ОС Windows); Выводы по проделанной работе с указанием достоинств и недостатков предложенного исходного кода.
ЛАБОРАТОРНАЯ РАБОТА №5 "Безопасность Windows " Цель работы: Изучение принципов обеспечения безопасности приложений в ОС Windows NT (На примере программы File_User). Задание к лабораторной работе: 1. Запустить программу File_User. Данная программа, работающая только в среде Windows NT, демонстрирует, каким образом можно получить текущий дескриптор безопасности файла и обновить его путем добавления записи (АСЕ) AccessAllowed (доступ разрешен) или AccessDenied (доступ запрещен) для заданного пользователя. Для упрощения примера создано консольное 41
приложение Win32, работающее в режиме командной строки. Программа ожидает, когда в командной строке будет задано имя файла, имя пользователя и параметр + (доступ разрешен) или - (доступ запрещен). 2. Рассмотреть исходный код программы. Предложенный код позволяет понять, как осуществляется установка или изменение атрибутов безопасности на уровне объектов в операционной системе Windows NT. Поэкспериментировать с программой, изменив ее таким образом чтобы запретить доступ к данному объекту со стороны всех пользователей или, наоборот, чтобы разрешить всеобщий доступ к нему. Изменить владельца файла. Настроить программу для работы с другими объектами, для этого модифицировать DACL. Содержание отчета: 1. Цель работы; 2. Исходный текст модифицированной программы (согласно задания); 3. Выводы по проделанной работе с указанием достоинств и недостатков предложенного исходного кода.
ЛАБОРАТОРНАЯ РАБОТА №6 "Обработка исключений" Цель работы: Изучение принципов обработки исключений в программе. (На примере программы Exceptions). Задание к лабораторной работе: 1. Запустить программу Exceptions. Данная программа содержит ряд примеров обработки исключений различного типа. Пример, который необходимо запустить, можно выбрать из меню программы. Simple Handler - Демонстрирует простейшую структуру блоков try/catch. Nested Handlers - Демонстрирует вложенную структуру блоков try/catch. Failed Catch - Отключает возможность перехвата ошибки доступа к памяти; может потребовать последующей перезагрузки компьютера. Resource Exception - Ошибка, обусловленная загрузкой несуществующего устройства. User Exception - Генерирует пользовательское исключение.
42
Для удобства в каждом примере в окне приложения отображается последовательность текстовых сообщений, сопровождающих процесс обработки исключения. 2. Рассмотреть механизм простейшего обработчика исключений. Блок try/catch перехватывает исключение при вызове процедуры ForceException, которая порождает исключение доступа к памяти. На рис. 6.1 показано окно программы, сообщения в котором позволяют отслеживать ход выполнения процедуры.
Рис. 6.1. Окно, предназначенное для наблюдения за работой простейшего обработчика исключений В примере исключения перехватываются внутри блока catch (...) и не приводят к системной ошибке. void CExceptionsView::OnExceptionsSimplehandler() { CDC *pDC = GetDC(); CSize cSize = pDC->GetTextExtent( "A", 1 ); // получить вертикальный размер текста int vSize = cSize.cy, iLine = 0; CString csText; Каждый из обработчиков начинается с команды получения контекста устройства, а затем вызывает функцию GetTextExtent, предназначенную для определения размера строки текста по вертикали. Это значение используется для разделения строк при отображении сообщений. Кроме того, вызываются 43
функции Invalidate и OnPaint, посредством которых удаляется текст, оставшийся от предыдущих примеров. Invalidated; OnPaint (); // удаляют весь предшествующий текст csText = "Starting process"; pDC->TextOut( 10, vSize *iLine++, csText ); try { csText = "In try block"; pDC->TextOut( 50, v3ize*iLine++, csText ); csText = "About to cause exception"; pDC->TextOut( 50, vSize*iLine++, csText ); ForceException (); // показана ранее Несмотря на то, что исключение происходит в отдельной процедуре, выполнение блока try приостанавливается и управление передается блоку catch (...). Поэтому следующая строка текста не будет отображаться в окне программы (см. рис. 3.1). Выполнение программы возобновится в блоке catch, где имеется другое сообщение. csText = "This line will not display because execution has passed " "to the catch block"; pDC->TextOut( 50, vSize*iLine++, csText ); } catch (...) // перехватывать все исключения!!! { csText = "In catch all block"; pDC->TextOut( 50, vSize*iLine++, csText ); } csText = "End of process"; pDC->TextOut( 10, vSize*iLine++, csText ); } 3. Рассмотреть механизм обработчика вложенного исключения. Второй пример демонстрирует использование вложенных исключений путем размещения одного блока try/catch внутри другого, внешнего блока. Кроме того, внутренний блок содержит набор команд catch, которые оперируют различными классами, производными от CException. Наконец, оператор throw во внутреннем блоке catch передает перехваченное исключение внешнему блоку catch. void CExceptionsView::OnExceptibnsNestedhandlers() { 44
CDC *pDC = GetDC() ; CSize cSize = pDC->GetTextExtent( "A", 1 ); int vSize = cSize.cy, iLine = 0; CString csText; Invalidate(); OnPaint(); // удаление всего предшествующего текста csText = "Starting process"; pDC->TextOut( 50, vSize*iLine++, csText ); try { csText = "In outer try block"; pDC->TextOut ( 50, vSize*iLine++, csText ); try { csText = "In inner try block"; pDC->TextOut( 90, vSize*iLine+.+, csText ); csText = "About to cause exception ..."; pDC->TextOut( 90, vSize*iLine++, csText ); ForceException (); В этот момент команда ForceException готова породить исключение и передать его внутреннему блоку catch, предотвращая выполнение следующих операторов. csText = "This line will not display because execution " "has passed to the catch block"; pDC->TextOut( 50, vSize*iLine++, csText ); } Далее следует набор блоков catch, каждый из которых предназначен для перехвата исключения, тип которого определяется классом, производным от CException. Но тип исключения, задействованного в данном примере (ошибка доступа к памяти), не относится ни к одному из этих классов. Поэтому реальный перехват не произойдет до тех пор, пока не будет найден подходящий обработчик. Все операторы catch имеют практически одно и то же тело (различаются лишь выводимой на экран строкой), поэтому достаточно рассмотреть одну структуру: catch( CMemoryException *e ) { TCHAR szCause[255]; 45
// нехватка памяти
csText = "CMemoryException cause: "; e->GetErrorMessage( szCause, 255 ); csText += szCause; pDC->TextOut( 90, vSize*iLine++, csText ); } catch ( CFileException *e ) { ... } catch( CArchiveException *e )
// ошибка при работе с. файлом
// ошибка, допущенная при // операции архивации/сериализации
{ ... } catch( CNotSupportedException *e ) // запрос сервиса, { // который не поддерживается ... } catch( CResourceException *e ) // ошибка, допущенная при // выделении ресурса { ... } Поскольку в рассматриваемой программе поддержка баз данных не реализована, классы CDaoException и CDBException не распознаются и закомментированы в исходном тексте. /*
// поддержка баз данных не реализована, // поэтому исключения CDaoException и // CDBEkception в этом примере не распознаются catch ( CDaoException *e ) //исключения, связанные //с базами данных (DAO-классы) { ... } // исключения, связанные catch( CDBException *e ) // с базами данных (ODBC-классы) { ... } 46
*/ catch ( COleException *e ) // OLE-исключения { ... } catch(COleDispatchException *e ) // исключения диспетчеризации // (OLE-автоматизации) { ... } catch( CUserException *e ) // выводит окно сообщения, { //а затем генерирует исключение CException ... } Вслед за этим следует блок catch (CException *e). В противном случае компилятор воспринял бы предыдущие блоки как ошибочные. catch ( CException *e ) // должен следовать после предыдущих блоков, // иначе компилятор выдаст код ошибки { TCHAR szCause[255] ; csText = "CException cause: "; e->GetErrorMessage( szCause, 255 ); csText += szCause; pDC->TextOut( 90, vSize*iLine++, csText ); } И поскольку нам известно, что ни один из классов CException на самом деле не может обработать данное исключение, в программу включен блок catch (...), который перехватывает все исключения, но выдает не очень полную информацию о том, что произошло. // перехват всех исключений!!! catch(...) { csText = "In inner catch all block: "; pDC->TextOut( 90, vSize*iLine++, csText ); csText = "Throwing exception to outer catch block: "; pDC->TextOut( 90, vSize*iLine++, csText ); throw; // передает исключение внешнему блоку catch, // приведенному ниже } Исключение, произошедшее во внутреннем блоке try, перехватывается внутренним блоком catch и передается внешнему блоку catch. Оператор throw предотвращает выполнение фрагмента программы, который следует после 47
блоков try/catch. Аналогичная обработка происходит автоматически во внутреннем блоке try при генерации исходного исключения. Единственное различие между ними заключается в том, что при входе программы во внутренний блок catch внешний блок try исполняться не будет. Это дает дополнительную возможность проконтролировать ход выполнения приложения и позволяет при необходимости предотвратить выполнение последующих операторов программы. csText = "This line will not display because execution has been " "thrown to the outer catch block"; pDC->TextOut( 50, vSize*iLine++, csText ); } // перехват всех сообщений!!! catch(...) { csText = "In outer catch all block, catching thrown exception"; pDC->TextOut( 50, vSize*iLine++, csText ); } csText = "End of process"; pDC->TextOut (10, vSize*iLine++, csText ); } На рис. 6.2 представлен отчет, который генерируется во время выполнения модуля Nested Handlers.
Рис. 6.2. Отчет, генерируемый в процессе выполнения примера Nested Handlers 4. Рассмотреть пример неудачной обработки исключения. Модуль Failed Catch демонстрирует, что происходит в том случае, когда блоки try/catch пытаются защитить выполнение определенного фрагмента программы, однако не могут правильно перехватить исключение (программа наталкивается на 48
исключение, которое не перехватывается этими блоками). Код такого примера практически идентичен приведенному выше. void CExceptionsView::OnExceptionsFailedcatch() { CDC *pDC = GetDC() ; CSize cSize = pDC->GetTextExtent( "A", 1 ); int vSize = cSize.cy, iLine =0, *p = 0x00000000; CString csText; Invalidate () ; OnPaint(); // удаление всего предшествующего текста csText = "Starting process"; pDC->TextOut( 10, vSize*iLine++, csText ); try { csText = "In try block"; pDC->TextOut( 50, vSize*iLine++, csText ); csText = "About to cause exception ..."; pDC->TextOut( 90, vSize*iLine++, csText ); Поскольку эта процедура точно будет выполнена некорректно, то кроме вывода текста она также отображает модальное диалоговое окно сообщения, содержащее соответствующее предупреждение (рис. 6.3).
Рис. 6.3. Окно-предупреждение, генерируемое модулем Failed Catch Ниже представлен исходный код, позволяющий выводить данное предупреждение. MessageBox( "WARNING!\r\nThis exception may require a reboot\r\n" "for full recovery!" ); *p = 999; } // класс CException не может перехватить catch( CException *e ) //код ошибки доступа к памяти { TCHAR szCause[255]; 49
csText = "In catch block"; pDC->TextOut( 50, vSize*iLine++, csText ); csText = _T("CException cause: "); e->GetErrorMessage( szCause, 255 ); csText += szCause; } Следует отметить, что выполнение модуля Failed Catch может привести к возникновению ошибки в системной памяти, что потребует перезагрузки системы. Но в любом случае будет получено предупреждающее сообщение. Фрагмент, содержащий обработчик catch (...), закомментирован, поскольку это единственная подпрограмма обработки, способная перехватывать ошибки доступа к памяти, а целью данного примера является демонстрация событий, которые произойдут в том случае, если соответствующее исключение перехвачено не будет. Если необходимо предотвратить ошибку следует убрать знаки комментария в блоке catch(...). /*
// если сделать блок catch(...) доступным, // ошибка будет перехвачена и не попадет в систему catch (...) // перехватывает все // исключения, в том числе // ошибки доступа к памяти! { csText = "In catch all block"; pDC->TextOut( 50, vSize*iLine++, csText ); }
*/ csText = "End of process"; pDC->TextOut( 10, vSize*iLine++, csText ); } 5. Рассмотреть механизм исключения "Загрузка несуществующего ресурса". Не все исключения генерируются системой автоматически. Во многих случаях исключение пользователю необходимо породить самостоятельно. Такая потребность возникает, когда обнаруживается неправильный результат выполнения функции или нужно перенаправить поток выполнения в обход последующих операторов. Эту задачу проще всего решить путем генерации исключения. Блок catch перехватит его, выдаст сообщение или осуществит иного рода обработку. Так, если в процессе перевода большой строковой таблицы, предназначенной для применения в многоязыковой версии приложения, потеряется одна или несколько строк (а такое вполне возможно), исходный код программы все равно будет скомпилирован, поскольку идентификатор ресурса сохранится. Однако в программе будут содержаться операторы, которые 50
выводят несуществующие записи строковой таблицы. Эта ошибка сама по себе не порождает исключения. Просто в случае отсутствия искомой записи оператор LoadString возвращает строку нулевой длины. Программа Resource Exception генерирует исключение при наличии этой ошибки. Следующий пример начинается с внутреннего и внешнего блоков try и демонстрирует применение оператора throw. Во внутреннем блоке try делается попытка загрузить строковый ресурс. void CExceptionsView::OnExceptionsResourceexception() { CDC *pDC = GetDC() ; CSize cSize = pDC->GetTextExtent( "A", 1 ); int vSize = cSize.cy, iLine = 0; CString csText, csMsg; Invalidate(); OnPaint(); // удаляют весь предшествующий текст csText = "Starting process"; pDC->TextOut( 10, vSize*iLine++, csText ); try { csText = "In outer try block"; pDC->TextOut( 50, vSize*iLine++, csText ); try { csText = "In inner try block"; pDC->TextOut( 90; vSize*iLine++, csText ); csText = "About to cause exception ..."; pDC->TextOut( 90, vSize*iLine++, csText ); if( ! csText.LoadString ( IDS_NON_STRING ) ) AfxThrowResourceException() ; pDC->TextOut( 90, vSize*iLine++, csText ); } При успешном выполнении функции LoadString программа отобразила бы загруженную строку. Но в строковой таблице нет соответствующей записи, хотя файл Resource.H содержит определение идентификатора этого ресурса. Поэтому вызывается функция AfxThrowResourceException, порождающая исключение, которое приводит к выполнению следующих блоков catch. catch( CResourceException *e ) // ошибка выделения ресурса { TCHAR szCause[255]; csText = "CResourceException cause: "; e->GetErrorMessage( szCause, 255 ); 51
csText += szCause; pDC->TextOut( 90, vSize*iLine++, csText ); } В этом блоке исключение перехватывается классом CResourceException, который передает сообщение о недоступности запрашиваемого ресурса "A required resource was unavailable" (рис. 6.4). catch( CException *e )// данный блок должен следовать после // предыдущих блоков, // иначе компилятор выдаст код ошибки { TCHAR szCause[255]; csText = "CException cause: "; e->GetErrorMessage( szCause, 255 ); csText += szCause; pDC->TextOut( 90, vSize*iLine++, csText ); } // перехват всех исключений!!!. catch(...) { csText = "In inner catch all block: "; pDC->TextOut( 90, vSize*iLine++, csText ); csText = "Throwing exception to outer catch block: "; pDC->TextOut( 90, vSize*iLine++, csText ); throw; // передает исключение внешнему блоку catch }
Рис. 6.4. Окно, содержащее сообщения, которые возникают в процессе порождения исключения, связанного с ресурсом Следует обратить внимание на тот факт, что блок catch(. ..) содержит оператор throw, а обработчик CResourceException - нет. Поскольку исключение 52
перехватывается (и обрабатывается) обработчиком CResourceException, оператор throw не выполняется. Поэтому внешний блок try продолжает выполнение и отображает следующее сообщение. csText = "Still in the outer try block. This line displays because"; pDC->TextOut( 50, vSize*iLine++, csText ); csText = "execution was not thrown to the outer catch block"; pDC->TextOut( 50, vSize*iLine++, csText ); } catch (...) // перехватывает все исключения!!! { csText = "In outer catch all block, catching thrown exception"; pDC->TextOut( 50, vSize*iLine++, csText ); } csText = "End of process"; pDC->TextOut( 10, vSize*iLine++, csText ); } Приложение может по-разному реагировать на исключения путем использования различных обработчиков. Кроме того, каждый обработчик может выполнять различные операции в зависимости от условия возникновения исключения. 6. Рассмотреть пример обработки пользовательского исключения. Пользовательские исключения всегда должны генерироваться явным образом. Система не имеет возможности распознавать пользовательские ошибки, поскольку они обычно происходят в контексте приложения. Таким образом, пользовательские исключения всегда следует определять самостоятельно. Исключения должны происходить не обязательно в той же процедуре, где расположены блоки try/catch. Рассмотреть следующий пример: BOOL CExceptionsView::UserException() // предположим, что произошла ошибка { AfxMessageBox( "Drat! The XDR Veng operation failed!" ); AfxThrowUSerException() ; return TRUE; } В данном фрагменте кода функция UserException отображает окно сообщения (рис. 6.5), в котором приведена информация об ошибке, а затем функция AfxThrowUserException генерирует исключение. Функция UserException вызывается из следующей подпрограммы, содержащей блоки try/catch: void CExceptionsView::OnExceptionsUserexception() { CDC *pDC = GetDC(); 53
CSize cSize = pDC->GetTextExtent( "A", 1 ); int vSize = cSize.cy, iLine = 0; CString csText, csMsg; Invalidate(); OnPaint(); // удаление всего предшествующего текста csText = "Starting process"; pDC->TextOut( 10, vSize*iLine++, csText ); try { csText = "In try block, about to call UserException()"; pDC->TextOut( 50, vSize*iLine++, csText ); if ( UserException() ) { csText = "In try block, the XDR Veng operation succeeded (impossible) "; pDC->TextOut( 50, vSize*iLine++, csText ); } csText = "Continuing try block"; pDC->TextOut( 50, vSize*iLine++, csText ); }
Рис. 6.5. Окно, содержащее сообщения, которые возникают в процессе генерации пользовательского исключения Поскольку предполагается, что функция UserException должна породить исключение, сообщение об успешном выполнении операции никогда не появится. Если за этим процессом проследить пошагово, то станет ясно, что функция UserException не возвращает результирующего значения, поскольку выполнение передается непосредственно обработчикам catch. catch( CUserException *e ) 54
{ TCHAR szCause[255]; csText = "In CUserException catch block"; pDC->TextOut( 50, vSize*iLine++, csText ); csText = "CUserException cause: "; e->GetErrorMessage( szCause, 255 ); csText += szCause; pDC->TextOut( 90, vSize*iLine++, csText ); } В обработчике CUserException причина исключения идентифицируется как '"unknown" (неизвестная). В конце концов, класс CUserException не может же знать, почему было сгенерировано исключение. Но с помощью пользовательских производных классов можно обеспечить механизм, выявляющий причину возникновения исключений различных типов. catch( CException *e ) { TCHAR szCause[255] ; csText = "In CException catch block (unexpected)"; pDC->TextOut ( 50, vSize*iLine++, csText ) ; csText = "CException cause: "; e->GetErrorMessage( szCause, 255 ); csText += szCause; pDC->TextOut ( 90, vSize*iLine++, csText ); } csText = "End of process"; pDC->TextOut( 10, vSize*iLine++, csText ); return; } 1. 2. 3. 4.
Содержание отчета: Цель работы; Исходный текст подпрограмм; Результаты работы подпрограмм (главные окна в ОС Windows); Выводы по проделанной работе с указанием достоинств и недостатков предложенного исходного кода. ЛАБОРАТОРНАЯ РАБОТА №7 "Динамический обмен данными"
55
Цель работы: Изучение принципов разработки программы, позволяющей использовать динамический обмен данными (На примере программы DDE_Demo). Задание к лабораторной работе: 1. Запустить программу DDE_Demo. Результат работы программы представлен на рис.7.1. В результате исполнения создаются пять экземпляров приложения, взаимодействующих друг с другом. Каждый из экземпляров можно свернуть или даже убрать с экрана (сделать невидимым), но все они останутся активными вне зависимости от состояния и размеров их окон. Каждый экземпляр программы DDE_Demo взаимодействует со всеми остальными экземплярами и поддерживает локальный элемент данных, который обозначается как Local Stock и выделяется красным цветом. В то же время каждый экземпляр сообщает о значении данного элемента во всех остальных экземплярах, выделяя эту информацию черным цветом.
Рис. 7.1. Пять экземпляров приложения, взаимодействующих посредством DDE 2. Рассмотреть исходный код программы. В функции WinMain происходит инициализация приложения путем вызова функции InitApplication из файла Template.I. Далее вызывается DDEML-функция DdeInitialize, которая устанавливает функцию обратного вызова для управления графиком DDEсообщений: int WINAPI WinMain( HINSTANCE hInstance, HINSTAKCE hPrevInstance, 56
LPSTR lpCmdLine, INT nCmdShow ) { … if( DdeInitialize( &idInst, (PFNCALLBACK) DdeCallback, APPCMD_FILTERINITS | CBF_SKIP_CONNECT_CONFIRMS | CBF_FAIL_SELFCONNECTIONS | CBF_FAIL_POKES, 0 ) ) return(FALSE); // не продолжать при наличии ошибки Одновременно с заданием функции обратного вызова происходит и установка нескольких флагов, которые играют роль фильтров, ограничивающих типы обрабатываемых сообщений. APPCMD_FILTERINITS - Фильтруются все приложения за исключением тех, имена которых совпадают с нашим собственным именем сервиса. CBF SKIP_CONNECT_CONFIRMS - При установлении подключения подтверждающие сообщения не посылаются. CBF_FAIL_SELFCONNECTIONS - Не допускается подключение приложения к самому себе. CBF FAIL_POKES - Не допускаются транзакции XTYP_POKE. Если по каким-либо причинам не удается установить функцию обратного вызова, приложение возвращает значение FALSE и прекращает свою работу (но это событие маловероятно). Далее производится вызов функции CreateWindow. В случае возникновения ошибки перед выходом из приложения принимаются меры по вызову функции DdeUninitialize: hinst = hInstance; hwnd = CreateWindow( szAppName, szAppTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT, NULL, NULL, hInstance, NULL ); if( ! hwnd ) { DdeUninitialize ( idInst ); return ( FALSE ) ; } На следующем шаге происходит поиск аргумента командной строки, который будет использоваться для идентификации отдельных экземпляров приложения. Поскольку исходному экземпляру не должен передаваться аргумент командной строки, по умолчанию ему назначается номер 1. 57
if( strlen( lpCmdLine ) ) iInst = atoi( lpCmdLine ) ; else iInst = 1; Данные номера не имеют ничего общего с DDEML и служат лишь в качестве меток. DDEML имеет иное средство идентификации экземпляров приложений - дескрипторы, передаваемые операционной системой. Для получения локальных данных предназначен генератор псевдослучайных чисел. Обычно подобные генераторы должны инициализироваться различными начальными значениями, иначе они будут всегда генерировать одну и ту же последовательность чисел. Для этой цели, как правило, применяется Borlandфункция randomize, которая использует в качестве инициализирующего числа текущее значение системного таймера. Но поскольку в компиляторе Microsoft эта функция отсутствует, воспользоваться ее аналогом. #ifdef __BORLANDC__ randomize (); #еlsе srand( (unsigned)time( NULL ) ); #endif Далее первый экземпляр должен запустить остальные четыре экземпляра приложения с помощью функции WinExec, предоставив ей соответствующие аргументы командной строки для каждого экземпляра. switch( iInst ) { // первый экземпляр запускает остальные case 1: xLoc = 195; yLoc = 125; WinExec( "DDE_DEMO 2", SW_SHOW ); WinExec ( "DDE_DEMO 3", SW_SHOW ); WinExec( "DDE_DEMO 4", SW_SHOW ); WinExec( "DDE_DEMO 5", SW_SHOW ); break; case 2: xLoc = 0; yLoc =0; break; case 3: xLoc = 390; yLoc = 0; break; case 4: xLoc = 390; yLoc =250; 58
break; case 5: xLoc = 0; yLoc = 250; break; } Кроме того, каждому экземпляру передаются координаты xLoc и yLoc, что позволяет разместить окна на экране удобным для просмотра образом. 3. Подготовить приложения к подключению. Вызвать функции ShowWindow и UpdateWindow. Перед началом традиционного цикла передачи сообщений необходимо выполнить несколько действий, обязательных при работе с DDEML. hszAppName = DdeCreateStringHandle( idInst, szAppTitle, 0 ); Сначала необходимо создать дескриптор строки с именем приложения. Аргумент idInst, который был получен ранее с помощью функции DdeInitialize, идентифицирует экземпляры приложения. Последний аргумент указывает кодовую страницу (по умолчанию - CP_WINANSI). В случае использования Unicode-версии DDEML следует указать кодовую страницу CP_WINUNICODE. Далее функция RegisterClipboardFormat возвращает дескриптор формата данных, после чего с помощью функции DdeConnectList делается попытка установить соединение с остальными DDE-приложениями. hFormat = RegisterClipboardFormat( szAppTitle ); hConvList = DdeConnectList( idInst, hszAppName, hszAppName, hConvList, NULL ) ; Если ни одно из DDE-приложений не имеет имени и темы, совпадающих с заданными значениями, список диалогов hConvList остается пустым до тех пор, пока не появится приложение, с которым можно установить соединение. После этого происходит вызов функции DdeNameService для регистрации текущего экземпляра приложения. Эта функция посылает всем остальным DDE-при-ложениям широковещательное сообщение о том, что текущий экземпляр приложения доступен, но пока не установил подключения. DdeNameService( idInst, hszAppMame, 0, DMS_REGISTER ); После установки DDE-соединений начинается обычный цикл сообщений, который будет продолжаться вплоть до прекращения работы данного экземпляра приложения. while( GetMessage( &msg, NULL, 0, 0 ) ) { 59
TranslateMessage( smsg ); DispatchMessage( smsg ) ; } Когда приложение будет готово завершить свою работу, желательно выполнить набор обычных в таких случаях операций очистки, в том числе вызвать функцию DestroyWindow,а затем - функцию UnregisterClass. DestroyWindow( hwnd ) ; UnregisterClass( szAppTitle, hInstance ); 4. Рассмотреть реакцию приложений на сообщения. Первая выполняемая транзакция - это XTYP_CONNECT. Поскольку уже были заданы параметры фильтрации (посредством функции DdeInitialize), любые поступающие запросы на подключение предназначены текущему DDE-приложению и могут быть приняты путем возвращения значения TRUE. case XTYP_CONNECT: return( TRUE ) ; Кроме того, поскольку транзакции XTYP_WILDCONNECT не обрабатываются, попытки установить "обобщенные" подключения автоматически завершатся неудачно. При получении запроса XTYP_ADVSTART выполняется операция проверки, позволяющая обеим программам (клиенту и серверу) убедиться в том, что соединение установлено именно с тем приложением и именно по той теме, которые предполагались. case XTYP_ADVSTART: return( (UINT) wFmt == hFormat && hszItem == hszAppName ); Если какой-либо из аргументов не соответствует требованиям, возвращается значение FALSE и диалог прекращается. Транзакция XTYP_REGISTER уведомляет о появлении нового приложения; в ответ обновляется список подключений hConvList и передается сообщение XTYP_ADVSTART. case XTYP_REGISTER: hConvList = DdeConnectList( idInst, hszItem, hszAppName, hConvList, NULL ); PostTransaction( NULL, 0, XTYP_ADVSTART ); UpdateWindow( hwnd ) ; return ( TRUE ) ;
60
При появлении нового DDE-приложения на обновление окна текущего экземпляра затрачивается дополнительное время. Эта операция может быть отложена до поступления дополнительной информации (например, при передаче данных). Кроме того, одно из приложений может прервать диалог. Если это произойдет, достаточно простой команды обновления окна InvalidateRect. case XTYP_DISCONNECT: InvalidateRect( hwnd, NULL, TRUE ); break; Следующие два типа транзакций, XTYP_ADVREQ и XTYP_REQUEST, ожидают ответа в виде информационного сообщения. Во многих DDEприложениях эти транзакции подразумевают передачу данных в текстовом формате (с разделителями), совместимом с форматом CF_TEXT. case XTYP_ADVREQ: case XTYP_REQUEST: return ( DdeCreateDataHandle ( idInst, (PBYTE) &DataOut, sizeoft DataOut ), 0, hszAppName, hFormat, 0 ) ) ; Функция DdeCreateDataHandle используется для получения дескриптора блока данных. Передаваемые данные имеют тип UINT (32-разрядное целое беззнаковое число), причем номер экземпляра исходного приложения, iInst, находится в старшем байте старшего слова, а остальные данные записаны в младшем слове. Формирование этих данных осуществляется процедурой WndProc и будет описано немного позже. Когда приложение выступает в роли клиента, а не сервера, в ответ на транзакцию XTYP_ADVDATA происходит обратный процесс. С помощью функций DdeGetData и DdeSetUserHandle выполняется преобразование полученных данных в приемлемый формат. case XTYP_ADVDATA: if( DdeGetData( hData, (PBYTE) &DataIn, sizeof(DataIn), 0 ) ) DdeSetUserHandle( hConv, QID_SYNC, DataIn ); InvalidateRect( hwnd, NULL, TRUE ); return ( DDE_FACK ) ; Функция DdeGetData копирует данные в локальный буфер DataIn, после чего вызывается функция DdeSetUserHandle для связывания локального буфера с дескриптором диалога hConv. Этот процесс упрощает выполнение асинхронных транзакций (они будут рассмотрены позже) и запускается в подпрограмме WndProc при вызове функции DdeQueryConvInfo в ответ на 61
сообщение WM_PAINT, когда в цикле опрашиваются все участники текущего диалога. Такая транзакция может инициироваться по разным причинам и при различных обстоятельствах - например, по периодическим сигналам таймера или в ответ на изменение определенных значений. Используемый тип обработки связан с особенностями демонстрационной программы. Остается последний тип транзакций, XTYP_EXECUTE (другое приложение или другой экземпляр просит текущий экземпляр приложения выполнить определенные действия). Но прежде чем определить, какие действия запрашиваются, необходимо получить доступ к данным с помощью функции DdeAccessData, которая возвращает локальный указатель строковых данных (локальный по отношению к данному экземпляру приложения и к данной процедуре). case XTYP_EXECUTE: pszExec = DdeAccessData( hData, sdwSize ); if( pszExec ) { Необязательный параметр dwSize содержит информацию о длине возвращаемой строки. Если нет необходимости знать это значение, аргументу dwSize можно присвоить значение NULL. Если параметр pszExec не равен NULL (т.е. указывает на строку), следующий этап заключается в определении запрашиваемой команды. if( ! stricmp( "PAUSE", pszExec ) ) PauseAutomatic( hwnd ); else if( ! stricmpt "RESUME", pszExec ) ) ResumeAutomatic( hwnd ); Здесь представлены только две возможные команды, которые описываются ключевыми словами PAUSE (пауза) и RESUME (продолжить) и вызывают соответствующие подпрограммы. Поскольку операторы switch/case могут принимать только целочисленные аргументы, приходится выполнять серию операторов if/else для проверки строк. При большом количестве операций проверки это может привести к усложнению структуры программы. В качестве альтернативного варианта предлагается воспользоваться циклической проверкой совпадения строковых аргументов с фиксированными записями в таблице строк с последующим использованием найденного номера строки в операторе switch/case. 5. Рассмотреть технологию DDE в процедуре WndProc, которая управляет работой программы DDE_Demo. При создании каждого экземпляра приложения 62
в ответ на сообщение WM_CREATE происходит инициализация таймера, после чего генерируется сообщение WM_SIZE, позволяющее установить размеры и положение окна данного экземпляра. Подпрограмма обработки сообщения WM_TIMER - это первое место, где происходит вызов DDE-функций. Она начинается с обновления переменной LocalStock: case WM_TIMER: if( random( 2 ) ) LocalStock += random( 100 ); else LocalStock -= min( (UINT) random) 100 ), LocalStock ); DataOut = ( iInst << 24 ) + LocalStock; Далее происходит обновление переменной DataOut, которая задает пользовательский формат данных, предназначенный для передачи. Значение переменной образуется путем размещения номера экземпляра приложения iInst в старшем байте старшего слова, а переменной LocalStock - в младшем слове, Когда данные готовы, выполняется функция DdePostAdvise, уведомляющая другие приложения о наличии данных. Передаваемое сообщение принимается другими участниками диалога с помощью функции DdeCallback в виде транзакции XTYP_ADVDATA. DdePostAdvise( idInst, hszAppName, hszAppName ); SetRect( &rc, 0, 0, cxText, cyText ); InvalidateRect( hwnd, &rc, TRUE ) ; UpdateWindow( hwnd ) ; break; После выдачи DDE-транзакции экземпляр приложения передает инструкции для обновления своего собственного окна, чтобы отобразить в нем новые данные. Транзакцию подключения можно также выполнить в ответ на сообщение WM_PAINT. case WM_PAINT: ... // подсчет числа участников if( hConvList ) { hConv = DdeQueryNextServer( hConvList, 0 ); while ( hConv ) { ciData.cb = sizeof(CONVINFO); DdeQueryConvInfo( hConv, QID_SYNC, &ciData ); 63
Получив дескриптор диалога, с помощью функции DdeQueryNextServer можно запросить самую последнюю версию имеющихся данных. Закончив чтение данных, снова вызвать функцию DdeQueryNextServer, на этот раз используя в качестве аргумента дескриптор текущего диалога. В результате возвращается дескриптор следующего диалога в списке. Цикл продолжается до тех пор, пока список не завершится. hConv = DdeQueryNextServer( hConvList, hConv ); } } Перед закрытием приложения всегда выдается сообщение WM_CLOSE, ответом на которое обычно является вызов функции PostQuitMessage, завершающей работу программы. Далее необходимо закрыть DDE-соединения, а также об уничтожить локальный таймер. case WM_CLOSE: KillTimer( hwnd, TRUE ) ; DdeDisconnectList( hConvList ); DdeNameService( idlnst, 0, 0, DNS_UNREGISTER ) ; DdeFreeStringHandle( idInst, hszAppName ); DdeUninitialize( idInst ); PostQuitMessage( FALSE ); break; Последовательность действий здесь такова: прежде всего нужно разорвать связь, затем отменить регистрацию имени сервиса и освободить строковый дескриптор, а затем отменить инициализацию DDE-подключения. После выполнения всех этих операций приложение готово к завершению работы. 6. Рассмотреть дополнительные операции в процедуре WndProc. Наряду с операциями, непосредственно связанными с DDE, в процедуре WndProc происходит также обработка сообщений меню. В программе имеется два подменю, Local и Global. В каждом из них содержится по две команды, Resume и Pause. Команды меню Local инструктируют локальный экземпляр приложения о необходимости либо возобновить работу в автоматическом режиме, либо приостановить ее вообще. Команды меню Global передают управление соответствующим локальным командам, однако перед этим осуществляют широковещательную передачу сообщений другим участникам диалога, инструктируя их о необходимости либо возобновить работу, либо приостановить ее. Эта задача выполняется с помощью функции Post-Transaction: 64
case WM_COMMAND: switch( LOWORDt wParam ) ) { case IDG_RESUME: PostTransaction( "RESUME", 7, XTYP_EXECUTE ); // не прекращая выполнения, переходим к следующей операции case IDL_RESUME: ResumeAutomatic( hwnd. ); break; case IDG_PAUSE: PostTransaction( "PAUSE", 6, XTYP_EXECUTE ); //не прекращая выполнения, переходим к следующей операции case IDL_PAUSE: PauseAutomatic( hwnd ) ; break; } break; Локальная функция PostTransaction вызывается с тремя параметрами. pScr - Указатель на командную строку. cbData - Длина командной строки. xtyp - Тип транзакции. VOID PostTransaction( PBYTE pSrc, DWORD cbData, UIMT xtyp ) { HCONV hConv; DWORD dwResult; int iCheck =0; Переменная hConv содержит дескриптор диалога. Целочисленное значение iCheck служит для проверки того, были ли установлены новые подключения, и, если это так, чтобы дать возможность локальному экземпляру обновить свое окно. Цикл проверки подключений в целом идентичен циклу, выполнявшемуся при получении сообщения WM_PAINT, но между ними имеются отличия. В данной ситуации вызывается API-функция DdeClientTransaction, в качестве аргументов которой указываются команда, длина команды, имя приложения (идентификатор), формат данных, тип транзакции, а также инструкция TIMEOUT_ASYNC, свидетельствующая о том, что эта транзакция является асинхронной. Для синхронной транзакции вместо последнего аргумента следует указать время ожидания, выраженное в миллисекундах. Далее в переменную dwResult записывается результат транзакции. Для разрыва диалога вызывается функция DdeAbandonTransaction. 65
if( DdeClientTransaction ( pSrc, cbData, hConv, hszAppName, hFormat, xtyp, TIMEOUT_ASYNC, SdwResult ) ) DdeAbandonTransaction( idInst, hConv, dwResult ); 1. 2. 3. 4.
Содержание отчета: Цель работы; Исходный текст программы; Результаты работы программы (главное окно в ОС Windows); Выводы по проделанной работе с указанием достоинств и недостатков предложенного исходного кода.
ЛАБОРАТОРНАЯ РАБОТА №8 "Обработка мультимедийной информации" Цель работы: Изучение принципов разработки программы, позволяющей обрабатывать звук. (На примере программы ShowWave). Задание к лабораторной работе: 1. Запустить программу ShowWave. Программа ShowWave имитирует утилиту Sound Recorder (Звукозапись), которая входит в стандартный комплект поставки Windows. Она осуществляет запись и чтение аудиофайлов, воспроизводит, записывает и микширует звуки из различных файлов, а также позволяет устанавливать уровень громкости. Вы можете запускать программу ShowWave без звуковой карты для открытия, закрытия, микширования, просмотра и сохранения звуковых файлов, но в этом случае программа будет "немой и глухой" и не сможет ничего записывать и воспроизводить. Программа ShowWave состоит из нескольких модулей (перечислены в том порядке, в котором они будут рассмотрены): • модуль Mci.C посылает команды аудиоустройству (звуковой карте); • модуль Mmio.C осуществляет чтение и запись аудиоданных; • модуль WinMain.C содержит процедуру WinMain и формирует окно About; • модуль ShowWave.С контролирует работу элементов управления диалогового окна; • модуль GraphWin.C контролирует работу пользовательского элемента управления, который реализует графические операции программы.
66
Первые два модуля, Mci.C и Mmio.C, содержат общие процедуры для выполнения основных операций с аудиофайлами, например, их записи и воспроизведения. Модуль WinMain.C создает и регистрирует окно программы и управляет диалоговым окном About. Четвертый модуль, ShowWave.C, в ответ на команды пользователя вызывает соответствующие функции. Модуль GraphWin.C управляет графическим представлением звукового сигнала, которое отображается в центре окна программы (рис. 8.1). В лабораторной работе рассмотрим только модули, обеспечивающие обработку звука - Mci и Mmio.
Рис. 8.1. Окно программы ShowWave 2. Рассмотреть модуль МСI. В модуле МСI сосредоточены все обращения к функции mciSendCommand и сформированы отдельные процедуры для каждого командного сообщения. Все процедуры являются короткими и в основном следуют такой базовой схеме: • инициализация блока параметров; • передача команды; • проверка ошибок; • возвращение результата. Все восемь процедур, содержащиеся в модуле, описаны в начале Mci.C: ОТКРЫТЫЕ ФУНКЦИИ OpenDevice открытие аудиоустройства CloseDevice закрытие аудиоустройства SetTimeFormat выбор формата времени BeginPlay начало воспроизведения StopPlay конец воспроизведения BeginRecord начало записи SaveRecord сохранение записи ЗАКРЫТАЯ ФУНКЦИЯ ReportMciError вывод сообщения об ошибке Открытие и закрытие устройства. Операция открытия устройства аналогична операции открытия файла: она объявляет о вашем намерении произвести обмен информацией с определенным устройством, а также 67
заставляет систему создать внутренние структуры, которые необходимы для поддержки такого взаимодействия. Система передает вам идентификатор устройства, который, подобно дескриптору файла, указывает вашего партнера в процессе обмена информацией. Когда взаимодействие завершается, вы закрываете устройство, система освобождает связанные с ним ресурсы памяти и идентификатор устройства становится недействительным. Все мультимедийные устройства реагируют на сообщения MCI_OPEN и MCI_CLOSE. Кроме того, все драйверы должны реагировать еще на три сообщения: MCI_GETDEVCAPS, MCI_STATUS и MCI_INFO, каждое из которых запрашивает информацию об устройстве. Каждая команда MCI_OPEN сопровождается структурой MCI_OPEN_PARMS, которая определена в файле Mmsystem.H: /* блок параметров для командного сообщения MCI_OPEN */ typedef struct tagMCI_OPEN_PARMS { DWORD dwCallback; // дескриптор окна MCIDEVICEID wDeviceID; // идентификатор устройства LPCTSTR lpstrDeviceType; // тип открываемого устройства LPCTSTR lpstrElementName; // входной элемент устройства LPCTSTR lpstrAlias; // необязательный псевдоним } MCI_OPEN_PARMS; Во всех параметрических структурах присутствует поле dwCallback. Оно используется совместно с флагом MCI_NOTIFY. Любой вызов функции mciSendCommand, запрашивающий уведомление, должен содержать дескриптор окна в младшем слове поля dwCallback. Таким образом, по завершении операции система может отправить сообщение MM_MCINOTIFY заданному окну. При рассмотрении модуля ShowWave.C вы увидите, как следует реагировать на подобное уведомление. При открытии устройства поле wDeviceID должно оставаться пустым; подсистема WinMM назначает идентификатор открываемому устройству и записывает этот идентификатор в данное поле. После открытия любого устройства целесообразно сразу сохранить его идентификатор. В поле lpcstrDeviceType указывается тип устройства. Он берется из системного реестра, в котором содержатся записи, подобные приведенным ниже: AVIVideo : REG_SZ : mciavi32.dll WaveAudio : REG_SZ : mciwave.dll Sequencer : REG_SZ : mciseq.dll CDAudio : REG_SZ : mcicda.dll Для воспроизведения WAV-файлов программе Show Wave необходимо устройство типа WaveAudio. 68
Поле lpstrElementName назначает источник данных для комбинированного устройства. Windows различает простые и комбинированные устройства. Для простого устройства не нужно указывать имя файла, а для комбинированного устройства - нужно. Например, программа не может самостоятельно выбрать компакт-диск, а воспроизводит всегда тот диск, который находится в дисководе. Таким образом, проигрыватель компактдисков является простым устройством. С другой стороны, проигрыватель аудиофайлов может воспроизводить любой указанный ему файл. Поэтому устройство waveaudio является комбинированным. Элемент устройства представляет собой входную или выходную среду, которую программа связывает с устройством. Чаще всего это файл, поэтому в поле lpstrElementName обычно содержится имя файла. Последнее поле, lpstrAlias, позволяет задать псевдоним для имени открытого устройства. Псевдонимы можно применять только в командных строках MCI. Все поля блока параметров заполнять не обязательно. Так, можно задать только имя элемента и позволить системе самостоятельно выбрать соответствующее устройство по расширению имени файла - скажем, проигрыватель WAV-файлов или синтезатор для MID-файлов. Если вы хотите получить информацию о любом устройстве, можно открыть его, указав его тип без задания элемента. Флаги функции mciSendCommand сообщают системе, какие поля следует прочитать. Например: /*------------------------------------------------------------OPEN DEVICE Открытие устройства воспроизведения аудиофайлов --------------------------------------------------------------*/ BOOL OpenDevice( HWND hWnd, LPSTR lpszFileName, MCIDEVICEID *lpmciDevice ) { DWORD dwRet; MCI_OPEN_PARMS mciOpenParms; /* открытие комбинированного устройства */ mciOpenParms.lpstrDeviceType = "waveaudio"; mciOpenParms.lpstrElementName = lpszFileName; dwRet = mciSendCommand( 0, // идентификатор устройства MCI_OPEN, // команда MCI_OPEN_TYPE | MCI_OPEN_ELEMENT, // флаги (DWORD)(LPVOID) &mciOpenParms ); // блок параметров if( dwRet != 0 ) { ReportMCIError( hWnd, dwRet ); return( FALSE ); } 69
/* задание возвращаемых значений */ *lpmciDevice = mciOpenParms.wDeviceID; return( TRUE ); } Первый параметр функции mciSendCommand может иметь только нулевое значение, так как устройство не открыто и ему не присвоен идентификатор. В третьем параметре содержится комбинация двух флагов. Первый из них, MCI_OPEN_TYPE, дает системе команду прочитать поле lpstrDeviceType из блока параметров, поскольку в этом поле записана соответствующая строка. Второй флаг, MCI_OPEN_ ELEMENT, дает команду прочитать поле lpstrElementName. Поскольку мы не указали флаг MCI_OPEN_ALIAS, система будет игнорировать любое значение, содержащееся в поле lpstrAlias. Функция OpenDevice возвращает значения TRUE или FALSE, свидетельствующие об успешности ее выполнения. Кроме того, при успешном завершении функция возвращает в третьем параметре идентификатор устройства. Идентификатор устройства необходим для выполнения последующих операций, например для закрытия устройства. /*-------------------------------------------------------CLOSE DEVICE Закрытие мультимедийного устройства ------------------------------------------------------------*/ void CloseDevice( HWND hWnd, MCIDEVICEID mciDevice ) { DWORD dwRet; dwRet = mciSendCommand( mciDevice, MCI_CLOSE, MCI_WAIT, (DWORD) NULL ); if( dwRet != 0 ) { ReportMCIError( hWnd, dwRet ) ; } return; } Функция CloseDevice принимает идентификатор устройства. Никаких других данных ей не нужно; команда MCI_CLOSE даже не использует блок параметров. Задание формата времени. Когда программа ShowWave приказывает устройству waveaudio воспроизвести звук, она всегда задает позицию в файле, с которой следует начать воспроизведение. С помощью полосы прокрутки, которая имеется в основном окне программы, пользователь может 70
перемещаться по файлу, т.е. начинать его воспроизведение с любой позиции. Программа ShowWave и драйвер устройства должны "договориться" о единицах измерения позиции в файле. Такими единицами измерения могут быть байты, выборки и миллисекунды. С интуитивной точки зрения в качестве единиц измерения целесообразно использовать миллисекунды. Как говорилось выше, оцифровка производится с постоянной частотой, поэтому каждой миллисекунде оцифрованного звука соответствует одно и то же количество выборок. Например, в случае звука, оцифрованного с частотой 22,5 кГц, за одну миллисекунду происходит приблизительно 22 выборки. Вследствие этого в программе ShowWave выбран формат MM_FORMAT_MILLISECONDS. Выбор формата означает передачу сообщения MCI_SET с блоком параметров MCI_SET_PARMS. /* блок параметров для сообщения MCI_SET */ typedef struct tagMCI_SET_PARMS { DWORD dwCallback; // окно для получения сообщений MM_MCINOTIFY' DWORD dwTimeForinat; // константа формата // времени DWORD dwAudio; //выходной аудиоканал } MCI_SET_PARMS; Поле dwTimeFormat может принимать значения MM_FORMAT_BYTES, MM_FORMAT_SAMPLES или MM_FORMAT_MILLISECONDS. В программе ShowWave стереозвук не воспроизводится, поэтому поле dwAudio мы игнорируем. /*----------------------------------------------------------------------SET TIME FORMAT Задание формата времени. Мы применяем формат миллисекунд (а не байтов или выборок). ------------------------------------------------------------------------*/ BOOL SetTimeFormat( HWND hWnd, MCIDEVICEID mciDevice ) { DWORD dwRet; MCI_SET_PARMS mciSetParms; /* задание формата времени (миллисекунды) */ mciSetParms.dwTimeFormat = MCI_FORMAT_MILLISECONDS; dwRet = mciSendCommand( mciDevice, MCI_SET, MCI_SET_TIME_FORMAT,(DWORD)(LPVOID)&mciSetParms) ; if( dwRet != 0 ) { 71
ReportMCIError ( hWnd, dwRet ) ; return ( FALSE ) ; } return( TRUE );
// успешное выполнение
} Флаг MCI_SET_TIME_FORMAT указывает системе на необходимость прочитать значение в поле dwTimeFormat структуры mciSetParms. Воспроизведение звука. Командное сообщение MCI_PLAY инициирует воспроизведение аудиофайла. Блок параметров для этой команды называется MCI_PLAY_PARAMS. /* блок параметров для сообщения MCI PLAY */ typedef struct tagMCI_PLAY_PARMS { DWORD dwCallback; // окно для получения сообщений MM_MCINOTIFY DWORD dwFrom; // начальная позиция DWORD dwTo; // конечная позиция } MCI_PLAY_PARMS; По умолчанию команда Play начинает воспроизведение с текущей позиции в файле и продолжает его до конца, однако флаги dwFrom и dwTo, если они установлены, заставляют подсистему WinMM начать и закончить воспроизведение в других позициях. Начальную и конечную позиции можно выразить в байтах, выборках или миллисекундах, но вы должны заранее "предупредить" драйвер устройства о том, какие единицы измерения будут использоваться. По умолчанию драйверы работают с миллисекундами. /*-------------------------------------------------BEGIN PLAYBACK ------------------------------------------------------*/ BOOL BeginPlay(HWND hWnd, MCIDEVICEID mciDevice, DWORD dwFrom) { DWORD dwRet; MCI_PLAY_PARMS mciPlayParms; /* установка формата времени (миллисекунды) */ if( ! SetTimeFormat( hWnd, mciDevice ) ) { return( FALSE ) ; } // По завершении воспроизведения в окно обратного // вызова передается уведомляющее сообщение MM_MCINOTIFY. // При этом оконная процедура закрывает устройство. mciPlayParms.dwCallback = (DWORD)(LPVOID) hWnd; mciPlayParms.dwFrom = dwFrom; 72
dwRet = mciSendCommand( mciDevice, MCI_PLAY, MCI_FROM | MCI_NOTIFY, (DWORD)(LPVOID) &mciPlayParms ); if (dwRet != 0) { ReportMCIError( hWnd, dwRet ); return( FALSE ); } return( TRUE ); // успешное завершение } Флаг MCI_FROM сигнализирует о присутствии определенного значения в поле dwFrom. Флаг MCI_NOTIFY приказывает системе отправить уведомление в конце воспроизведения. Аудиофайлы могут быть достаточно большими, поэтому мы позволяем подсистеме WinMM воспроизводить запись в фоновом режиме, пока программа ShowWave выполняет следующую процедуру. Когда подсистема WinMM обнаруживает, что достигнут конец WAV-файла, она отправляет сообщение MM_MCINOTIFY окну, указанному в поле dwCallback. (При рассмотрении процедуры ShowWave_WndProc обратите внимание на обработчик сообщения). Уведомление не поступает до тех пор, пока устройство воспроизведения не достигнет конца аудиофайла или позиции, заданной параметром dwTo. В аргументе wParam уведомляющее сообщение содержит код завершения, который говорит о том, выполнена ли операция нормально, или же она прервана либо подавлена другой командой, адресованной к данному устройству, или же не выполнена вообще из-за ошибки устройства. В младшем слове аргумента lParam содержится идентификатор устройства. Остановка воспроизведения. Команда MCI_STOP прерывает выполнение текущей операции. Если пользователь, начав воспроизведение длинного аудиофайла, затем принимает решение не прослушивать его до конца, он может щелкнуть на кнопке Stop. При этом передается сообщение MCI_STOP, которое отменяет воспроизведение. Подобно команде MCI_CLOSE, в сообщении MCI_STOP не используется блок параметров. /*------------------------------------------------STOP PLAY Прекращение воспроизведения ---------------------------------------------------------*/ void StopPlay( HWND hWnd, MCIDEVICEID mciDevice ) { DWORD dwRet; dwRet = mciSendCommand(mciDevice, MCI_STOP, MCI_WAIT, (DWORD)NULL ); if( dwRet != 0 ) 73
{ ReportMCIError( hWnd, dwRet ); } return; } Если в сообщении MCI_STOP передать команду MCI_NOTIFY вместо MCI_WAIT, оконная процедура получит два уведомления. Первое из них, MCI_NOTIFY_ABORTED, говорит о том, что воспроизведение завершилось, не достигнув конечной точки файла. Второе уведомление, MCI_NOTIFY_SUCCESSFUL, сообщает об успешном выполнении команды Stop. Запись звука. На звуковых картах обычно есть разъем для подключения микрофона, что позволяет записывать звук прямо на жесткий диск. Сообщение MCI_RECORD заставляет устройство WaveAudio принимать входной сигнал от микрофона. /* блок параметров для сообщения MCI_RECORD */ typedef struct tagMCI_RECORD_PARMS { DWORD dwCallback; // окно для получения сообщений MM_MCINOTIFY DWORD dwFrom; // начальная позиция DWORD dwTo; // конечная позиция } MCI_RECORD_PARMS; Поля dwFrom и dwTo указывают область в существующем файле, куда должна быть записана информация. В новом файле имеет значение только поле dwTo - новые записи должны всегда начинаться с нулевой позиции. Без флага MCI_TO и значения dwTo запись продолжается до тех пор, пока не будет заполнен весь диск или пока драйвер устройства не получит команду Stop. (Для создания нового файла следует в качестве параметра имени файла в сообщении MCI_OPEN указать пустую строку - " "). /*----------------------------------BEGIN RECORD ------------------------------------------*/ BOOL BeginRecord ( HWND hWnd, MCIDEVICEID mciDevice, DWORD dwTo ) { DWORD dwRet; MCI_RECORD_PARMS mciRecordParms; /* установка формата времени (миллисекунды) */ if ( ! SetTimeFormat ( hWnd, mciDevice ) ) { return( FALSE ) ; 74
} // Осуществить запись в течение заданного времени // (в миллисекундах) . По завершении записи в окно // обратного вызова поступает уведомление MM_MCINOTIFY. // При этом оконная процедура сохраняет запись //и закрывает устройство. mciRecordParms.dwCallback = (DWORD)(LPVOID) hWnd; mciRecordParms.dwTo = dwTo; dwRet = mciSendCommand( mciDevice, MCI_RECORD, MCI_TO | MCI_NOTIFY, (DWORD)(LPVOID) &mciRecordParms ) ; if( dwRet != 0 ) { ReportMCIError( hWnd, dwRet ); return ( FALSE ) ; } return( TRUE ); // успешное завершение } Сохранение записанного звука. Команда MCI_SAVE дает драйверу устройства инструкцию сохранить текущий звуковой фрагмент на диск. Если выполнить запись звука, а затем закрыть приложение, не послав команды MCI_SAVE, все записанные данные будут потеряны. /* блок параметров для сообщения MCI SAVE */ typedef struct tagMCI_SAVE_PARMS { DWORD dwCallback; // окно для получения сообщений MM_MCINOTIFY LPCTSTR lpfilename; // имя файла на диске } MCI_SAVE_PARMS; Строка в поле lpfilename содержит имя результирующего файла. /*------------------------------------SAVE RECORD Сохранение звукозаписи ----------------------------------------*/ BOOL SaveRecord( HWND hWnd, MCIDEVICEID mciDevice, LPSTR lpszFileName ) { DWORD dwRet; MCI_SAVE_PARMS mciSave Farms ; // Сохранение записанных данных в указанный файл. // Перед продолжением работы программа ожидает // завершения операции записи. mciSaveParms.lpfilename = lpszFileName; 75
dwRet = mciSendCommand( mciDevice, MCI_SAVE, MCI_SAVE_FILE | MCI_WAIT, (DWORD)(LPVOID) &mciSaveParms ) ; if( dwRet != 0 ) { ReportMCIError( hWnd, dwRet ) ; return( FALSE ) ; } return ( TRUE ); //успешное завершение } Обработка ошибок. Последняя функция в модуле MCI осуществляет обработку ошибок, возникших при выполнении предыдущих функций. Она отображает окно сообщения, в котором содержится информация о том, что случилось. Процедура обработки ошибок использует две строки. Первая из них представляет собой название программы, которое будет отображаться в заголовке окна. Эта строка загружается из ресурса строковой таблицы. Вторая это сообщение об ошибке, получаемое непосредственно от MCI. Функция mciSendCommand возвращает код ошибки, который программа ShowWave записывает в переменную dwRet. Если результирующее значение не равно 0, значит, произошла ошибка, и программа ShowWave вызывает функцию ReportMCIError. Функция mciGetErrorString возвращает строку, соответствующую коду ошибки, который содержится в переменной dwRet. В файле Mmsystem.H определено приблизительно 90 кодов различных ошибок. Некоторые из них, например MCIERR_INVALID_DEVICE_ID, могут произойти в любой момент; другие, такие как MCIERR_CANNOT_LOAD_DRIVER, возникают только при выполнении определенных команд (в данном случае команды Open). Некоторые ошибки характерны только для определенных устройств. В частности, ошибка MCIERR_WAVES_OUTPUTSINUSE свидетельствует о том, что все аудиоустройства в настоящий момент заняты. /*------------------------------------REPORT MCI ERROR Сообщить пользователю о возникшей MCI-ошибке ---------------------------------------------------------------*/ static void ReportMClError( HWND hWnd, DWORD dwError ) { HINSTANCE hInstance; char szErrStr[MAXERRORLENGTH]; char szCaption[MAX_RSRC_STRING_LEN]; hInstance = GetWindowInstance( hWnd ); LoadString( hInstance, IDS_CAPTION, szCaption, sizeof(szCaption) ); mciGetErrorString( dwError, szErrStr, sizeof(szErrStr) ); 76
MessageBox( hWnd, szErrStr, szCaption, MB_ICONEXCLAMATION | MB_OK ); return; } 3. Рассмотреть модуль ММIO. Если бы программа ShowWave только записывала и воспроизводила звуки, модуль ММIO не использовался бы. Но нам необходимо также манипулировать данными непосредственно в аудиофайлах. В частности, для графического отображения звукового сигнала необходимо читать выборки из WAV-файла. Кроме того, поскольку пользователь имеет возможность модифицировать звукозаписи, находящиеся в памяти, иногда программа ShowWave должна сохранять данные в виде нового файла. Модуль ММIO содержит одну функцию для чтения данных, одну - для записи и одну - для обработки ошибок, связанных с файлами. Чтение WAV-файла. Функция ReadWaveData загружает все данные из WAV-файла в память. Она выполняет следующие операции: • открытие файла; • поиск WAVE-блока; • поиск вложенного fmt-блока и проверка того, в подходящем ли формате записан звук; • поиск вложенного data-блока и загрузка его в память; • закрытие файла. /*--------------------------------------------------READ WAVE DATA Чтение звукозаписи из RIFF-файла в память. Возвращает TRUE при успешном заполнении буфера; в противном случае возвращается FALSE. Если функция возвращает TRUE, значит, в последних трех параметрах содержится информация о новом буфере. ----------------------------------------------------------------*/ BOOL ReadWaveData( HWND hWnd, LPSTR lpszFileName, LPSTR *lplpWaveData, // указатель на буфер DWORD *lpdwWaveDataSize, // размер буфера DWORD *lpdwSamplesPerSec ) // частота оцифровки { HMMIO hmmio; // дескриптор файла MMCKINFO mmckinfoWave; // описание блока WAVE MMCKINFO mmckinfoFrnt; // описание блока fmt MMCKINFO mmckinfoData; // описание блока data PCMWAVEFORMAT pcmWaveFormat; // содержимое блока fmt LONG lFmtSize; // размер блока fmt LONG lDataSize; // размер блока data LPSTR lpData; // указатель на буфер данных 77
/* открытие файла для чтения */ hmmio = mmioOpen( lpszFileName, NULL, MMIO_ALLOCBUF | MMIO_READ ); if (hmmio == NULL) { ReportError( hWnd, IDS_CANTOPEHFILE ) ; return( FALSE ) ; } Команда mmioOpen принимает три входных параметра: имя файла, структуру с дополнительными параметрами и флаги для обозначения операций. Дополнительные параметры нужны только для изменения размера буфера, для открытия файла в памяти и для указания пользовательской процедуры чтения файла. Поскольку функция ReadWaveData не выполняет ни одну из этих операций, второй параметр равен NULL. Флаг MMIO_ALLOCBUF включает буферизацию ввода/вывода. Другой флаг, MMIO_READ, задает открытие файла только для чтения. Функция mmioWrite вернет сообщение об ошибке при записи в файл, открытый с установленным флагом MMIO_READ. /* выделяет из файла блок WAVE */ mmckinfoWave.fccType = mmioFOURCC('W,'А','V,'Е'); if (mmioDescend(hmmio,&mmckinfoWave,NULL,MMIO_FINDRIFF)!=0) { ReportError( hWnd, IDS_NOTWAVEFILE ); mmioClose ( hmmio, 0 ) ; return( FALSE ) ; } /* поиск вложенного блока формата */ mmckinfoFmt.ckid = mmioFOURCC('f,'m','t',' '); if( mmioDescend( hmmio, &mmckinfoFmt, &mmckinfoWave, MMIO_FINDCHUNK ) != 0) { ReportError( hWnd, IDS_CORRUPTEDFILE ); mmioClose ( hmmio, 0 ); return( FALSE ) ; } После открытия файла нам необходимо найти нужные данные и проверить их правильность. Первая команда mmioDescend ищет метку RIFF, за которой следует код WAVE. При успешном выполнении поиска вторая команда ищет вложенный блок формата звукозаписи. 78
Для поиска первого фрагмента заполняется только одно поле информационной структуры: fccType. Поле ckid (идентификатор блока) должно содержать значение RIFF, но флаг MMIO_FINDRIFF и так указывает цель этого поиска. Команда mmioDescend распознает также еще три флага: MMIO_FINDCHUNK, MMIO_FINDRIFF и MMIO_FINDLIST. Флаг MMIO_FINDCHUNK указывает, что поиск необходимо осуществлять по содержимому поля ckid, а остальные флаги говорят о том, что поле fccType соответствует блоку RIFF или LIST. Функция mmioDescend принимает четыре параметра: дескриптор файла, описание искомого блока, описание его родительского блока и несколько флагов. Блоки RIFF не имеют родительских блоков, поэтому третье поле мы оставляем пустым (значение NULL). Блок формата всегда является вложенным в другой родительский блок. Вложенные блоки могут быть только у блоков RIFF и LIST. Для поиска вложенного блока формата мы записываем значение fmt в информационную структуру искомого блока и значение WAVE в информационную структуру родительского блока. По достижении конца текущего блока WAVE функция mmioDescend прекратит поиск метки fmt. Это может случиться только в том случае, если файл поврежден и его нельзя использовать, поскольку интерпретировать блок WAVE без спецификаций его формата невозможно. Вторая команда mmioDescend оставляет указатель файла в начале вложенного блока формата. Затем мы загружаем информацию о формате в память для проверки. /* чтение вложенного блока формата */ lFmtSize = (LONG)sizeof( pcmWaveFormat ); if( mmioRead( hmmio, (LPSTR)&pcmWaveFormat, lFmtSize ) != lFmtSize ) { ReportError( hWnd, IDS_CANTREADFORMAT ) ; mmioClose ( hmmio, 0 ); return( FALSE ) ; } /* выход из вложенного блока в родительский блок */ if( mmioAscend( hmmio, SmrnckinfoFrnt, 0 ) != 0 ) { ReportError( hWnd, IDS_CANTREADFORMAT ); mmioClose ( hmmio, 0 ) ; return( FALSE ) ; } /* проверка того, действительно ли файл представляет собой WAV-файл в 8-разрядном РСМ-формате (моно) */ if((pcmWaveFormat.wf.wFormatTag!=WAVE_FORMAT_PCM)|| ( pcmWaveFormat .wf.nChannels != 1 ) || ( pcmWaveFormat.wBitsPerSample != 8 )) 79
{ ReportError ( hWnd, IDS_UNSUPPORTEDFORMAT ); mmioClose( hmmio, 0 ); return( FALSE ) ; } Функция mmioRead требует задания дескриптора файла, указателя буфера памяти, а также количества байтов. Поле lFmtSize содержит информацию о количестве байтов в структуре PCMWAVEFORMAT, которое будет загружено из дискового файла функцией mmioRead. Функция mmioAscend перемещает указатель текущей позиции файла в точку за последним байтом блока формата, подготавливая программу к выполнению последующей операции. Для простоты анализа программы Show Wave мы ограничились только 8битовыми монозаписями. Чтобы изменить эти параметры, нужно добавить несколько дополнительных переменных и изменить подпрограмму, управляющую работой полосы прокрутки. Итак, мы проверили формат данных. Теперь можно загрузить данные в память. Программа осуществляет поиск блока data, определяет его размер, выделяет для него соответствующий буфер в памяти и читает данные в буфер. /* поиск блока данных */ mmckinfoData.ckid = mmioFOURCC('d','а','t','а'); if( mmioDescend ( hmmio, &mmckinfoData, &mmckinfoWave, MMIO_FINDCHUNK ) != 0 ) { ReportError( hWnd, IDS_CORRUPTEDFILE ); mmioClose ( hmmio, 0 ) ; return( FALSE ) ; } /* определение размера блока данных */ lDataSize = (LONG)mmckinfoData.cksize; if( lDataSize == 0 ) { ReportError( hWnd, IDS_NOWAVEDATA ) ; mmioClose( hmmio, 0 ); return( FALSE ); } /* выделение и блокирование памяти */ lpData = GlobalAllocPtr( GMEM_MOVEABLE, lDataSize ); if( ! lpData ) { ReportError( hWnd, IDS_OUTOFMEMORY ); mmioClose( hmmio, 0 ) ; return( FALSE ) ; 80
} /* чтение блока данных */ if( mmioRead( hmmio, (LPSTR)lpData, IDataSize ) != lDataSize ) { ReportError( hWnd, IDS_CANTREADDATA ); GlobalFreePtr( lpData ); mmioClose( hmmio, 0 ) ; return( FALSE ) ; } Поиск блока data осуществляется аналогично поиску блока fmt. Функция mmioDescend записывает в переменную mmckinfoData информацию о размере блока данных. Параметр lDataSize сообщает, какой объем памяти должен быть выделен и сколько байтов необходимо прочитать из файла. Для завершения процедуры ReadWaveData нужно закрыть файл и вернуть три значения: указатель нового объекта в памяти, количество байтов данных в этом объекте, а также частоту оцифровки: /* закрыть файл */ mmioClose( hmmio, 0 ); /* задание возвращаемых значений */ *lplpWaveData = lpData; *lpdwWaveDataSize = (DWORD)lDataSize; *lpdwSamplesPerSec = pcmWaveFormat.wf.nSamplesPerSec; return( TRUE ) ; } Запись WAV-файла. Функция WriteWaveData переносит звукозапись из буфера в памяти в файл на диске. Когда пользователь изменяет существующие аудиоданные или записывает новые, функция WriteWaveData сохраняет результат. Она выполняет следующие операции: • открывает файл; • создает блок RIFF формата WAVE; • создает вложенный блок format и заполняет его поля размера и данных; • создает вложенный блок data и заполняет его поля размера и данных; • перемещает указатель текущей позиции в конец файла/что приводит к записи полного размера старшего блока; • закрывает файл. /*-----------------------------------------------WRITE WAVE DATA Запись аудиоданных из буфера в памяти в файл на диске --------------------------------------------------------------------------*/ BOOL WriteWaveData( HWND hWnd, // основное окно LPSTR lpszFileName, // файл назначения 81
LPSTR lpWaveData, // буфер исходных данных DWORD dwWaveDataSize, // объем данных в буфере DWORD dwSamplesPerSec ) // частота оцифровки { HMMIO hmmio; // дескриптор файла MMCKINFO mmckinfoWave; // описание блока WAVE MMCKINFO mmckinfoFmt; // описание блока fmt MMCKINFO mmckinfoData; // описание блока data PCMWAVEFORMAT pcmWaveFormat; // содержимое блока fmt LONG lFmtSize; // размер блока fmt LONG lDataSize; // размер блока data /* открыть файл для записи */ hmmio = mmioOpen( lpszFileName, NULL, MMIO_ALLOCBUF | MMIO_WRITE | MMIO_CREATE ); if( hmmio == NULL ) { ReportError( hWnd, IDS_CANTOPENFILE ); return( FALSE ) ; } /* создать блок RIFF формата WAVE */ mmckinfoWave.fccType = mmioFOURCC( 'W','A','V,'E' ); if( mmioCreateChunk(hmmio, SmmckinfoWave, MMIO CREATERIFF)!= 0 ) { ReportError( hWpd, IDS_CANTWRITEWAVE ); mmioClose( hmmio, 0); return( FALSE ) ; } Флаги команды mmioOpen сообщают системе о том, что мы хотим буферизовать операции с файлом, осуществлять запись, а не чтение, и что необходимо создать файл, если он еще не существует. Функция mmioCreateChunk предполагает задание трех параметров: дескриптора файла, структуры с описанием нового блока и необязательного флага, определяющего тип блока. Структура MMCKINFO имеет поле, называющееся dwDataOffset. Функция mmioCreateChunk записывает в него значение, указывающее позицию внутри файла, в которой начинается область данных нового блока. Функция mmioCreateChunk не может вставлять новые блоки в середину файла. Если указатель текущей позиции находится не в конце файла, запись произойдет поверх старых данных. Сформировав основной блок RIFF, на следующем этапе создаем и инициализируем вложенный блок format: 82
/* сохранение размера вложенного блока format */ lFmtSize = (LONG)sizeoft pcmWaveFormat ); // Создание вложенного блока format. // Поскольку размер этого блока нам известен, // указываем его в структуре MMCKINFO, // чтобы функции не пришлось снова определять // размер блока при выходе из него. mmckinfoFmt.ckid = mmioFOURCC( 'f', 'm', 't', ' ' ); mmckinfoFmt.cksize = lFmtSize; if (mmioCreateChunk( hmmio, &mmckinfoFmt, 0 ) != 0) { ReportError( hWnd, IDS_CANTWRITEFORMAT ) ; mmioClose( hmmio, 0 ) ; return ( FALSE ) ; } /* инициализация структуры PCMWAVEFORMAT */ . pcmWaveFormat.wf.wFormatTag = WAVE_FORMAT_PCM; pcmWaveFormat.wf.nChannels = 1; pcmWaveFormat.wf.nSamplesPerSec = dwSamplesPerSec; pcmWaveFormat.wf.nAvgBytesPerSec = dwSamplesPerSec; pcmWaveFormat.wf.nBlockAlign =1; pcmWaveFormat.wBitsPerSample = 8; /* запись вложенного блока format */ if( mmioWrite( hmmio, (LPSTR)&pcmWaveFormat, lFmtSize ) != lFmtSize ) { ReportError( hWnd, IDS_CANTWRITEFORMAT ); mmioClose( hmmio, 0 ); return ( FALSE ) ; } /* выход из вложенного блока format */ if( mmioAscend( hmmio, smmckinfoFmt, 0 ) != 0 ) { ReportError( hWnd, IDS_CANTWRITEFORMAT ); mmioClose ( hmmio, 0 ) ; return( FALSE ); } He забывайте, что каждый блок содержит метку, размер и некоторые данные. Функция mmioCreateChunk оставляет место для записи размера, но если значение поля cksize равно 0, это место остается пустым до тех пор, пока следующая команда mmioAscend не закроет новый блок. Обычно функция mmioAscend должна рассчитывать объем данных, возвращаться в поле размера и заполнять его вычисленным значением, а затем снова перемещаться к концу 83
области данных. Задавая размер данных, мы предотвращаем ненужные перемещения по файлу, устраняя лишние обращения к диску. По умолчанию в глобальной переменной dwSamplesPerSecond содержится значение 22050 (22,05 кГц), которое изменяется при загрузке нового файла с помощью функции ReadWaveData. (При выборе команды New из меню File происходит сброс этого значения.) Поскольку программа ShowWave ограничивается работой с монозаписями, характеризующимися разрядностью выборки 8 битов, каждая выборка всегда содержит 1 байт данных. Поэтому мы можем записать одно и то же значение в поля nSamplesPerSecond и nAvgBytesPerSecond переменной pcmWaveForm. Поле nBlockAlign указывает, сколько байтов заполняется одной выборкой. Размер выборки должен быть округлен к значению ближайшего байта. Например, 12-разрядные выборки будут записываться в двух байтах. Четыре бита в каждом блоке окажутся лишними, но при загрузке их в память заполнение дополнительных разрядов ускоряет доступ к данным. Центральный процессор всегда читает информацию из памяти не отдельными битами, а целыми байтами и словами. У вас может возникнуть вопрос, зачем нужно вызывать функцию mmioAscend, ведь при выполнении операции записи указатель текущей позиции уже переместился в конец блока формата. Это делается, опять-таки, для ускорения доступа к данным. Область данных блока всегда должна содержать четное число байтов, поэтому данные должны быть размещены по границам слов (2 байта). Если данные содержат нечетное число байтов, завершающая команда mmioAscend добавляет недостающий байт. Если количество байтов данных четное, команда mmioAscend не выполняет никаких действий. После функции mmioCreateChunk всегда желательно вызывать команду mmioAscend. Записав блок format, мы должны повторить те же самые операции для создания блока data: /* запись размера вложенного блока data */ lDataSize = (LONG)dwWaveDataSize; /*создание вложенного блока data, содержащего выборки*/ mmckinfoData.ckid = mmioFOURCC( 'd', 'a', 't', 'a' ); mmckinfoFmt.cksize = lDataSize; if( mmioCreateChunk ( hmmio, &mmckinfoData, 0 ) != 0 ) { ReportError( hWnd, IDS_CANTWRITEDATA); mmioClose ( hmmio, 0 ) ; return( FALSE ) ; } /* запись вложенного блока data */ if( mmioWrite( hmmio, lpWaveData, lDataSize ) != lDataSize ) { ReportError( hWnd, IDS_CANTWRITEDATA ); 84
mmioClose ( hmmio, 0 ) ; return( FALSE ) ; } /* выход из вложенного блока data */ if( mmioAscend( hmmio, &mmckinfoData, 0 ) != 0 ) { ReportError( hWnd, IDS_CANTWRITEDATA ); mmioClose( hmmio, 0 ); return( FALSE ) ; } Команда mmioAscend осуществляет переход в конец вложенного блока data, но при этом мы остаемся в пределах основного блока RIFF. Следовательно, функция mmioCreateChunk вызывалась три раза, a mmioAscend - только дважды. /* при выходе из блока WAVE происходит запись его размера */ if( mmioAscend( hmmio, &mmckinfoWave, 0 ) != 0) { ReportError( hWnd, IDS_CANTWRITEWAVE ); mmioClose( hmmio, 0 ) ; return( FALSE ); } /* закрыть файл */ mmioClose ( hmmio, 0 ) ; return( TRUE ) ; } Перед созданием каждого вложенного блока мы записываем значение размера в поле cksize, поэтому подсистема WinMM знает размер блока с самого начала. Но для первого, родительского, блока мы задали только формат WAVE. Заключительная команда mmioAscend завершает создание первого блока. Она вычисляет размер блока и записывает его сразу после метки RIFF в начале файла. Обработка ошибок. Команда mciGetErrorString оперирует только теми ошибками, которые возвращаются функцией mciSendCommand, а для процедур ввода/вывода нет эквивалентной функции генерации сообщений об ошибках. Мы ввели собственные сообщения в файл ShowWave.RC и написали функцию ReportError, которая отображает эти сообщения. /*--------------------------------------REPORT ERROR Выдает сообщение об ошибке -------------------------------------------*/ static void ReportError( HWND hWnd, int iErrorID ) 85
{ HINSTANCE hInstance; char szErrStr[MAX_RSRC_STRING_LEN] ; char szCaption [MAX_RSRC_STRING_LEN] ; hInstance = GetWindowInstance( hWnd ) ; LoadString( hInstance, iErrorID, szErrStr, sizeof (szErrStr) ); LoadString( hInstance, IDS_CAPTION, szCaption, sizeof(szCaption) ); MessageBox( hWnd, szErrStr, szCaption, MB_ICONEXCLAMATION | MB_OK ); return; } 1. 2. 3. 4.
Содержание отчета: Цель работы; Исходный текст программы; Результаты работы программы (главное окно в ОС Windows); Выводы по проделанной работе с указанием достоинств и недостатков предложенного исходного кода.
86
Марапулец Юрий Валентинович ОПЕРАЦИОННЫЕ СИСТЕМЫ Методические указания по выполнению лабораторных работ № 1–8 по курсу «Операционные системы» для студентов специальности 220400 «Программное обеспечение вычислительной техники и автоматизированных систем» В авторской редакции Технический редактор Бабух Е.Е. Компьютерный набор Марапулец Ю.В. Верстка Марапулец Ю.В., Бабух Е.Е. Оригинал-макет Лылова А.А. Лицензия ИД № 02187 от 30.06.2000 г. Подписано в печать 10.03.2004 г. Формат 61*86/16. Печать офсетная. Гарнитура Times New Roman Авт. л. 6,53. Уч.-изд. л. 6,68. Усл. печ. л. 5,58 Тираж 102 экз. Заказ № 215 Редакционно-издательский отдел Камчатского государственного технического университета Отпечатано полиграфическим учестком РИО КамчатГТУ 683003, г. Петропавловск-Камчатский, ул. Ключевская, 35
87