Версия для печати

Архив документации на OpenNet.ru / Раздел "Программирование, языки" (Многостраничная версия)

Библиотека программиста на рабочей станции.

UNIX
X Window
Motif

АО "Аналитик"
Москва 1994 год

Пособие подготовлено коллективом в составе :
Доценко А.В., Исаков А.Б., Рябов А.Ю.

Под редакцией Рябова А.Ю. - М., АО "Аналитик", 1994 г.

Оригинал: www.sensi.org/~alec/x/

    Содержит описание основных методов и приемов программирования в системе X Window и пакетах, базирующихся на ней. При подготовке издания использовались материалы из доступных зарубежных и российских монографий. Иллюстрирующие программы написаны авторами и приведены для закрепления навыков программирования в данной системе. Книга предназначена для разработчиков системных и прикладных программных средств в системе UNIX и X Window на рабочих станциях и других персональных компьютерах. издание может быть полезно аспирантам и студентам ВУЗов, специализирующихся по информатике и программированию.


АННОТАЦИЯ

    В данном издании делается попытка дать начальное представление об основных аспектах программирования в системе X Window, работающей под управлением операционной среды UNIX. Книгу можно рассматривать как логическое продолжение ранее изданных брошюр ("Windows 3.0. Справочник для программистов", "Windows 3.0. Пособие по программированию", ("Windows 3.0. Программирование. Дополнительные главы") посвященных программированию в объектно-ориентированной среде MS WINDOWS. В настоящем издании основное внимание уделяется описанию общего устройства другой объектно-ориентированной системе - X Window, ее связи с UNIX, возможностях, предоставляемых программисту и пользователю. Много места уделено описанию и объяснению двух пакетов X Toolkit Intrinsic (Xt) и OSF/Motif, которые базируются на X Window и с успехом используются для созданию графических интерфейсов. Наша книга в основном адресована программистам, т.к. содержит описание и разъяснение функционирования базовых механизмов, применяемых в UNIX, X Window, Xt и Motif как для работы со своими внутренними объектами, так и для связи с пользовательскими приложениями. Книга содержит большое количество справочной информации, которая является необходимой и полезной не только для пользователей, начинающих изучать UNIX и X Window, но и для более опытных программистов. Для эффективного понимания и усвоения материала приводятся многочисленные примеры, иллюстрирующие описываемые функции и механизмы.

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

    Одним из главных достоинств издания является то, что представленная подборка содержит базовые знания, позволяющие Вам успешно начать программировать на рабочих станциях Sun, HP, Apollo, DEC Alpha и др., на которых основной является система UNIX, а графической оболочкой - X Window.

    Надеемся также, то данная книга положит начало созданию целой серии изданий, посвященных различным аспектам программирования в системе X Window.


Введение.

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

    В нашей стране UNIX был не очень распространен, и тому были свои причины. Во-первых, это существовавшая направленность на использование небольшого количества типов ЭВМ. В основном это были ЕС и СМ, на которых функционировали специально, под конкретную архитектуру разработанные, ОС, такие как ОС ЕС (IВМ 360/370), ОС РВ (RSХ-11) и РАФОС (RТ-11). Во-вторых, созданные, наконец, у нас во второй половине 80-х версии UNIX (МОС для ЕС, ИНМОС и ДЕМОС для СМ) несколько запоздали. Аппаратура, на которой предполагалась их эксплуатация, морально устарела и в настоящее время практически не используется. В-третьих, родные просторы заполонили компьютеры IBM PC - маломощные (до недавнего времени) машины, на которых установка такой среды, как UNIX, просто не оправдана.

    Но ситуация меняется. РС выросли и стали не просто игрушкой для ввода и распечатки текстов, а компьютером, на котором можно решать серьезные задачи. Кроме того, появился, пока еще, ручеек, но очень много обещающий, по которому в страну "потекли" более мощные машины, так называемые "рабочие станции", для которых UNIX является основной операционной системой. Нет сомнений в том, что, как и в случае IВМ РС, производство станций будет налажено и в пределах России. Подводя итог, и учитывая так же и то обстоятельство, что для "любимых" народом РС так и не появилось "нормальной" ОС (OS/2 так и осталась чем-то, о чем многие слышали, но ни разу не видели, а новинка - Windows NT еще та "темная" лошадка), следует ожидать повышение (если не взрыв) интереса к UNIX в нашей державе.

    Но, как и все во вселенной, UNIX имеет не только достоинства, но и недостатки, основными из них, до недавнего времени, было неудобство общения со средой пользователя и то, что система плохо поддерживает работу с привычной по миру РС графикой. Лет десять назад появились первые программные разработки, призванные поправить положение. Стандартом стала система X Window. Она позволяет рисовать на экране дисплея графические изображения, поддерживает концепцию окон и унифицирует работу с различными устройствами ввода-вывода. Для того чтобы облегчить программирование с применением X Window и упростить создание пользовательских интерфейсов существует несколько пакетов, из которых наиболее широко распространено X Toolkit Intrinsics ( Xt ) и Motif.

    Именно об этой четверке UNIX+X Window+Xt+Motif и идет речь в предлагаемой читателю книге. Учитывая сложность всех этих программных продуктов, ясно, что в столь небольшом издании мы смогли привести лишь основы их построения и базу, необходимую для программирования с их использованием. Перечень литературы, приведенный в конце книги, позволит заинтересованным читателям получить более детальную информацию.

    Книга рассчитана на программистов, имеющих опыт работы в несложных операционных средах (например, МS DOS) и знающих язык Си. Предполагается также, что читателю известны основы компьютерной архитектуры.

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

Структура издания.

Книга состоит из введения, четырех глав и четырех приложений.

В главе 1 кратко описывается устройство ОС UNIX и основные выполняемые ею операции. Приводится описание средств, применяемых при построении программного обеспечения.

Глава 2 посвящена системе X Window. Рассказывается, что она из себя представляет. Перечислены и объяснены приемы программирования.

В главе 3 объясняется назначение пакета X Toolkit Intrinsics. Рассказывается о том, как использовать его для того, чтобы строить эффективное прикладное обеспечение, работающее в среде X Window под управлением ОС UNIX.

Глава 4 содержит описание основных объектов, предоставляемых программисту пакетом Motif .

Приложение 1 содержит справочник по типам данных, структурам и функциям X Window.

Приложение 2 содержит справочник по типам данных, структурам и функциям X Window.

Приложения 3 и 4 содержат справочник по типам данных, структурам и функциям пакета Motif .

Материал иллюстрируется многочисленными примерами программ.

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

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


Содержание :

  1. Программирование в среде UNIX.
    1. История создания и развития системы UNIX.
    2. Основные понятия системы UNIX.
    3. Начальная загрузка UNIX. Вход пользователя в систему.
    4. Файловая система.
      1. Соединение многих файловых систем в одну ("монтирование").
      2. Работа с каталогами.
      3. Создание и уничтожение файлов. Получение информации о файлах.
      4. Ввод-вывод данных.
    5. Процессы.
    6. Сигналы.
    7. Обмен данными между процессами.
      1. Разделяемые файлы.
      2. Каналы межпроцессорного обмена.
      3. Другие способы обмена данными.
        1. Очереди сообщений.
        2. Семафоры.
        3. Разделяемая память.
    8. Распределение памяти.
    9. Инструментальные средства программирования в системе UNIX.
      1. Получение подсказки. Программа man.
      2. Файлы системы UNIX, используемые при компиляции и компоновке программ.
      3. Компилятор языка Си.
      4. Создание библиотек файлов. Программа ar.
      5. Программа make.
      6. Системы контроля исходного кода.
    10. Проблемы переносимости программного обеспечения.
  2. Основы программирования в системе X Window.
    1. Основы.
      1. Общее устройство X Window.
      2. X окно.
      3. Управление окнами.
      4. Графические возможности X Window.
      5. "Свойства" и атомы.
      6. Первый пример.
      7. События.
      8. Атрибуты окна.
    2. Текст и графика.
      1. Графический контекст.
      2. Характеристики графического контекста.
      3. Вывод текста.
        1. Функции, рисующие текст.
        2. Шрифты.
        3. Загрузка шрифтов.
      4. Рисование графических примитивов.
      5. Изображение областей.
        1. Работа с областями простой формы.
        2. Работа с областями сложной формы.
      6. Очистка и копирование окон или из частей.
      7. Работа со структурой XImage
      8. Работа с цветом.
        1. Структура XVisualInfo.
        2. Создание и использование цветовых палитр.
        3. Именование цветов.
        4. Выделение цветов программами - клиентами.
    3. Работа с внешними устройствами.
      1. Клавиатура.
        1. События, соответствующие сигналам, посылаемым клавиатурой.
        2. Физические и логические коды клавиш.
        3. Символы и ASCII строки.
        4. Пример программы, работающей с клавиатурой.
        5. Задание параметров клавиатуры.
      2. Мышь.
        1. События, порождаемые мышью.
        2. Работа с курсором мыши.
        3. Задание параметров мыши.
      3. "Захват" клавиатуры и/или мыши.
    4. Программы и их ресурсы.
      1. Формат файла ресурсов.
      2. Доступ к ресурсам программ.
    5. Передача данных между программами.
      1. Механизм "свойств".
      2. Общение с менеджером окон.
  3. Программирование с использованием библиотеки X Toolkit Intrnsics (Xt).
    1. Основы Xt.
      1. Что такое объекты Xt.
      2. Инициализация программы. Контекст программы.
      3. Первый пример.
    2. Объекты Xt и взаимодействие с ними.
      1. Классы объектов.
      2. Атрибуты (ресурсы) объектов.
      3. Управление объектами.
      4. Модификация и чтение ресурсов объекта.
      5. "Динамические" ресурсы объектов.
        1. Процедуры обратного вызова (callback).
        2. Использование action-процедур.
        3. Обработчики событий.
    3. Дополнительные возможности Xt.
      1. Ввод данных из файла или из внешнего устройства.
      2. Таймер.
      3. "Рабочие" (work) процедуры.
      4. Управление очередью событий.
      5. Акселераторы.
      6. Процедуры, предназначенные для работы с окнами объектов.
      7. Программы, имеющие много объектов (окон) верхнего уровня.
    4. Xt и ресурсы программ.
      1. Формат файла описания ресурсов.
      2. Создание базы данных ресурсов программы.
      3. Получение ресурсов программы.
      4. Процедуры, преобразующие значения ресурсов от одного типа к другому ("Конверторы").
  4. Множество widget OSF/Motif.
    1. Основные обозначения и файлы-заголовки Motif
    2. Основные классы объектов Motif.
      1. Класс XmPrimitive.
        1. Класс
        2.  
      2. Класс XmManager.
        1.  
      3. Класс XmMenuShell.
      4. Класс XmDialogShell.
      5. Класс XmGadget и его подклассы.
    3. Создание и использование меню.
    4. Диалоги.
    5. Вывод текста.


/index.html#toc">Содержание

Вперед


Глава 1. Программирование в среде UNIX.

    Данная глава содержит информацию, необходимую пользователю, приступающему к программированию в системе UNIX. Изложенный материал состоит из двух частей. Первая представляет собой взгляд на систему "изнутри" и включает темы, посвященные устройству системы, управлению программами, базовым операциями ввода-вывода данных и многое другое. Вторая содержит описание инструментальных средств, применяемых при программировании в UNIX.

    Предполагается, что читатель знаком с языком программирования Си и имеет навыки построения прикладных задач в несложных операционных средах таких, например, как MS DOS. Кроме того, считается, что читатель уже прочел одну из книг, посвященных тому, как обращаться с UNIX [1-5] (а возможно уже и работал в ней ) и ему знакомы такие понятия, как "вход в систему" ( login ), "командный процессор" ("оболочка" ( shell )) и т.д.

    При описании функций, предназначенных для работы с системой, мы иногда опускаем излишние детали, такие, например, как все возможные значения того или иного аргумента, полный перечень возвращаемых значений. Эта информация может быть найдена либо в руководствах по используемой ОС, либо получена с помощью имеющейся в UNIX системы подсказок (см. 1.9.1).

  1. История создания и развития системы UNIX.
  2. Основные понятия системы UNIX.
  3. Начальная загрузка UNIX. Вход пользователя в систему.
  4. Файловая система.
    1. Соединение многих файловых систем в одну ("монтирование").
    2. Работа с каталогами.
    3. Создание и уничтожение файлов. Получение информации о файлах.
    4. Ввод-вывод данных.
  5. Процессы.
  6. Сигналы.
  7. Обмен данными между процессами.
    1. Разделяемые файлы.
    2. Каналы межпроцессорного обмена.
    3. Другие способы обмена данными.
      1. Очереди сообщений.
      2. Семафоры.
      3. Разделяемая память.
  8. Распределение памяти.
  9. Инструментальные средства программирования в системе UNIX.
    1. Получение подсказки. Программа man.
    2. Файлы системы UNIX, используемые при компиляции и компоновке программ.
    3. Компилятор языка Си.
    4. Создание библиотек файлов. Программа ar.
    5. Программа make.
    6. Системы контроля исходного кода.
  10. Проблемы переносимости программного обеспечения.

1.1. История создания и развитая системы UNIX.

    UNIX - это многозадачная, многопользовательская операционная система (ОС) общего назначения. Многозадачность означает, что в среде одновременно могут выполняться несколько программ. Термин многопользовательская "говорит" о том, что в системе одновременно могут работать несколько пользователей.

    Операционная система UNIX была создана в первой половине 70-х годов в подразделении фирмы АТ&T, называемом Веll Теlephone Labs. Первая версия системы была реализована на мини-ЭВМ РDP/7 и предназначалась, в основном, для обработки текстовой информации. Пройдя успешную проверку, система получила дальнейшее развитие. Большая часть кодов была переписана с ассемблера РDP на язык высокого уровня Си, разрабатываемый в то же время в упомянутой фирме. UNIX удачно отражал особенности существовавших в то время операционных систем и был применен в ряде университетов Северной Америки для обучения студентов принципам построения ОС. В начале 80-х годов UNIX был установлен на ряде вычислительных машин, отличных по своей архитектуре от РDР. В середине 80-х система привлекла внимание фирм-производителей компьютерного оборудования и программного обеспечения. Появились первые коммерческие версии UNIX. Они получили широкое распространение во всем мире. Сегодня трудно назвать ЭВМ, для которой не была бы разработана версия этой ОС (в дальнейшем версии системы мы также буден называть ее диалектами).

    Широкое распространение системы UNIX объясняется простотой ее устройства и легкостью переноса на компьютеры разной архитектуры. Последнее есть следствие того, что ОС написана на языке высокого уровня, и машинно-зависимые ее части невелики по объему и четко выделены. Но слаба машинная зависимость имеет и свои недостатки. Основным из них является некоторое снижение производительности UNIX по сравнению с системами, разработанными для конкретной ЭВМ и в большей степени учитывающими ее аппаратные особенности.


1.2. Основные понятия системы UNIX.

    Реализованные в системе UNIX принципиальные решения представляют собой пример удачного сочетания универсальности и простоты. Они основаны на обобщении понятий, связанных с ресурсами вычислительной системы, управлением программами, распределением памяти, вводом-выводом данных. Базовыми понятиями ОС являются процесс, ядро и файл.

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

    Программы используют ядро для удовлетворения своих потребностей и выполнения операций, связанных с обращением к ресурсам компьютера. С точки зрения процессов ядро можно рассматривать как набор резидентных в памяти подпрограмм и структур данных. Для выполнения действий, связанных с использованием ресурсов системы, задача вызывает процедуры ядра. Такого рода вызовы в UNIX называются системными.

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

    Все процессы в системе работают параллельно, используя один центральный процессор по принципу разделения времени. Каждому из них присваивается определенный приоритет. Чем он выше, тем больше "внимания" уделяет ОС задаче.

    Процесс может быть порожден другим процессом при выполнении последним соответствующего системного вызова. При этом в ядре резервируется необходимая память, и вновь созданной задаче присваивается уникальный идентификатор - неотрицательное целое число. Количество одновременно существующих в системе программ ограничено. Оно определяется в момент установки UNIX на ЭВМ (или, как говорят программисты, в момент генерации системы).

    Важным компонентом операционной системы являются файлы. В UNIX это именованный набор данных, хранящихся на внешнем устройстве (например магнитном диске), который доступен для чтения и (или) записи. Единицей информации, хранящейся в файле, является байт, состоящий из 8 битов. Каждый файл кроме имени имеет дополнительные атрибуты, такие как размер, дата создания, права доступа к нему пользователей и некоторые другие (см. Более подробно, например [3,4]). Файлы объединяются в единую файловую систему (см. ниже).

    Работа с файлами и внешними устройствами, такими как диск, принтер, клавиатура, дисплей, в UNIX унифицирована. Каждому из устройств соответствует файл, который в терминах ОС называют "специальным". Когда программа пишет байты в него, то они выводятся ядром на соответствующее устройство. Аналогично, когда информация читается из специального файла, то реально данные принимаются с устройства. Все устройства делятся на два типа: блочно-ориентированные (блочные) и символьно-ориентированные (символьные). Обмен данными с первыми осуществляется порциями длиной более одного байта (обычно 512 байт). Такими являются, например, магнитные диски. Обмен данными с устройствами второго типа осуществляется порциями равными одному байту (символу). Такими являются, например, клавиатура.


1.3. Начальная загрузка UNIX. Вход пользователя в систему.

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

    При включении машины начинает действовать программа, которая "находит" на устройстве файл, содержащий ядро UNIX, и помещает его в память ЭВМ. Поскольку ядро самостоятельно не выполняет каких-либо действий, для начала работы системы создается самый первый процесс. Ему присваивается идентификатор 0. Данный процесс называется swapper и выполняет действия, связанные с распределением вычислительных ресурсов (памяти и машинного времени) между другими программами. swapper это единственный процесс, который создается не другой задачей, а как бы "сам на себе".

    В начале своей работы swapper порождает новый процесс с идентификаторам 1, называемый init. init, в свою очередь, создает группу процессов, обеспечивающих доступ пользователей к системе. Число их определяется количеством терминалов, подключенных к компьютеру. (Терминалом называется устройство, обеспечивающее интерактивное взаимодействие пользователя с машиной, обычно это дисплей и клавиатура).

    Процесс, обеспечивающий вход пользователя в систему (он называется getty) выводит на терминал сообщение:

login:

    Таким образом пользователю предлагается ввести имя, под которым он известен ОС. После ввода, getty порождает процесс login, который отвечает за дальнейшую работу.

    login проверяет наличие пользователя с введенным именем в системном учетном файле и выводит на терминал "просьбу" ввести пароль:

password:

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

    Обычно задача, запускаемая процессом login, представляет собой интерпретатор команд (командный процессор (shell)). Он позволяет управлять операционной системой. shell воспринимает ввод с клавиатуры, анализирует полученные строки, определяет содержащиеся в них инструкции и выполняет предписываемые ими действия. Взаимодействие пользователя с системой подробно описано в многочисленной литературе по UNIX и мы не будем рассматривать этот вопрос в данном издании ( см. например, [1-4] ).

    Таким образом, мы кратко рассмотрели действия, производимые системой при входе в нее пользователя. Терминал, с которого осуществлен "вход", называется контрольным. Он закрепляется за пользователем до тех пор, пока процесс, порожденный при входе в систему, остается активным. Все запускаемые пользователем программы "прикреплены" к терминалу и принадлежат текущему пользователю. Это позволяет операционной системе определять права их доступа к другим процессам, файлам и прочим ресурсам системы UNIX.

    Когда процесс, порожденный при входе, завершается, то терминал освобождается, и процесс init автоматически создает новый процесс getty для освободившегося устройства. Это дает возможность другому пользователю "войти" в систему.


1.4. Файловая система.

    Файловые системы существуют во многих операционных системах и служат для упорядоченного хранения наборов данных (файлов) на внешних запоминающих устройствах. Файловая система UNIX обладает следующими возможностями:

    Физически файловая система располагается на устройствах ввода-вывода с прямым доступом. Обычно это магнитные диски. Каждый из них имеет свой специальный файл, посредством которого производятся операции обмена данными на уровне ядра. Диски в системе UNIX относятся к так называемым блочно-ориентированным устройствам. Это означает, что операции ввода-вывода для них выполняются порциями (блоками). Их размер, как правило, равен или кратен 512 байтам. Таким образом, система рассматривает диск как набор блоков, пронумерованных от 0 до N, где N зависит от размера устройства.

    Файловая система позволяет разделять дисковое пространство между наборами данных, имеющими различные имена, размеры и другие атрибуты. При ее создании на диске первый блок (номер 0) отводится для программы начальной загрузки ОС, даже если диск не будет содержать ядро UNIX. Следующий блок называется суперблоком. Он содержит информацию о размере файловой системы, указатель на список свободных блоков и указатель на список свободных индексных узлов. Индексный узел представляет собой структуру данных, хранящуюся на диске, и содержащую информацию, необходимую для поиска блоков, относящихся к конкретному файлу.

    Следом за суперблоком располагаются списки самих индексных узлов и свободных блоков. Место для них резервируется при создании файловой системы, далее располагаются блоки, занятые данными файлов, и свободные блоки.

    На логическом уровне файловая система UNIX организована, как иерархическая последовательность каталогов (директорий), содержащих сами файлы. Директория самого верхнего уровня называется "корневой" и имеет имя "/" (косая черта). Она создается в момент установки ОС на компьютер. Все остальные файлы и директории входят в корневую директорию или в ее подкаталоги. Указание ее местоположения на диске содержится в суперблоке.

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

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

    Например, если надо указать полное имя файла "prog.c", содержащегося в директории "src", которая, в свою очередь, входит в "корневую" директорию, то полное имя (путь) в системе UNIX будет выглядеть следующим образом:

/src/prog.c

    Если в имя файла входит точка ('.'), то часть имени файла, стоящую после нее, называют "расширением". Файлы, имеющие одинаковое назначение или принадлежащие одному типу, имеют одинаковое расширение. Так, программы на языке Си имеют расширение ".с", файлы, содержащие объектный код, расширение ".о", и т.д.

  1. Соединение многих файловых систем в одну ("монтирование").
  2. Работа с каталогами.
  3. Создание и уничтожение файлов. Получение информации о файлах.
  4. Ввод-вывод данных.

1.4.1. Соединение многих файловых систем в одну ("монтирование").

    ОС UNIX по умолчанию имеет, по крайней мере, одну файловую систему, создаваемую при ее генерации. К ней можно присоединить файловые системы, находящиеся на других устройствах. Для этого необходимо выполнить процедуру, которую называют "монтированием" (mount). "Монтирование" производится при помощи системного вызова mount ( ). Формат его следующий:

#include <sys/types.h>
#include <sys/mount.h>
    int mount (const char *spec, const char *path, int mode);

    (Здесь и далее указываются файлы-заголовки ( .h ), в которых определяются прототипы функций, константы и другие объекты, используемые для программирования. Где находятся эти файлы, а также, что еще необходимо для построения приложений в среде UNIX, подробно описывается ниже).

    Аргумент spec задает имя специального файла, соответствующего устройству, содержащему файловую систему, path - полное имя директории в существующей файловой системе, mode - режим "монтирования". Указание режима необходимо при присоединении файловых систем, отличающихся по своей природе от файловой системы UNIX (например, файловой системы MS DOS). mount ( ) возвращает значение 0 при успешном завершении и - 1, если в процессе выполнения операции произошли ошибки.

    Новая файловая система присоединяется к существующей в точке, указанной параметром path. Другими словами, содержимое директории path заменяется содержимым "корневой" директории новой файловой системы.

    Рассмотрим пример присоединения файловой системы, находящейся на дискете, к директории "/mnt". Предполагается, что устройству соответствует специальный файл с именем "/dev/diskette" (см. прототип функции perror ( ) в [6]).

if (( err = mount ("/dev/diskette", "/mnt", 0 )) = = - 1 )
   perror ("mount: ошибка монтированная");

    Операция обратная "монтированию" - отсоединение ("де-монтирование") производится системным вызовом umount ( ):

#include <sys/types.h>
#include <sys/mount.h>
    int mount (const char *path);

    Ее аргумент задает полное имя директории, к которой файловая система была ранее присоединена. При успешном завершении функция возвращает 0. "Де-монтирование" файловой системы не может быть произведено, если какой-либо процесс производит операции ввода-вывода с файлами, входящими в ее состав.


1.4.2. Работа с каталогами.

    Рассмотрим подробнее информацию, содержащуюся в файле, описывающим директорию. Он состоит из записей, характеризующих содержащиеся в каталоге наборы данных. Для каждого из них указаны имя, возможно некоторые другие атрибуты, и ссылка на элемент в таблице символов узлов. Последнее необходимо для доступа к содержимому файла. С точки зрения прав доступа все пользователи делятся на три категории: владелец файла, пользователи, входящие в одну группу с владельцем, и остальные. Для каждой категории устанавливаются правила на использование файла для чтения, записи и выполнения. Все файлы, описывающие директории, за исключением файла, соответствующего корневой директории, содержат по две записи для имен "." и ". .". Первое из них используется для указания текущей директории, второе - для обозначения директории, являющейся родительской по отношению к текущей. Использование данного имени позволяет "продвигаться" вверх в иерархии файловой системы (см. подробнее, например [3,4].

    Директории создаются системным вызовом mkdir( ) и удаляются rmdir( ). Если директория не является пустой , т.е. содержит некоторое количество файлов, удалить ее нельзя. Формат функций, реализующих эти системные вызовы, следующий:

#include <sys/types.h>
#include <sys/stat.h>

int mkdir (const char *path, mode_t mode);
int rmdir (const char *path);

    Аргументы функций следующие: path - имя директории, mode - целое число, задающее атрибуты создаваемого каталога. Задание их производится при помощи побитовых логических операций со следующими символическими константами, которые определяют установку прав доступа:

S_IRWXU Владелец может читать, писать и выполнять.
S_IRUSR Владелец может читать.
S_IWUSR Владелец может писать.
S_IXUSR Владелец может выполнять.
S_IRWXG Пользователи, входящие в одну группу с владельцем (группа), могут читать, писать и выполнять.
S_IRGRP Группа может читать.
S_IWGRP Группа может писать.
S_IXGRP Группа может выполнять.
S_IRWXO Все остальные пользователи (остальные) могут читать, писать и выполнять.
S_IROTH Остальные могут читать.
S_IWOTH Остальные могут писать.
S_IXOTH Остальные могут выполнять.

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

#include <sys/dir.h>

DIR *opendir (char *dirname);
struct direct *readdir (DIR *dirp);
int closedir (DIR *dirp);

    (Заметим, что процедуры системно-зависимы. Так, например, в диалекте Solaris 2.x (SunSoft), соответствующем стандарту System V Release 4, вместо файла "sys/dir.h" используется "dirent.h", а вместо структуры direct - структура dirent).

    Функция opendir( ) открывает каталог для чтения. Ее параметр dirname - это имя директории. Возвращает процедура указатель на структуру типа DIR, которая затем используется при работе.

    Функция readdir( ) читает содержимое очередного элемента каталога. Процедура возвращает указатель на структуру direct, описывающую файл, либо NULL, если вся информация уже получена. Структура direct определена следующим образом:

struct direct {
    ino_t d_ino; /*Номер индексного узла */
    char d_name [DIRSIZ]; /* Имя файла */
};

Функция closedir( ) закрывает каталог и освобождает необходимые для работы с ним ресурсы.

В следующем примере иллюстрируется приведенный выше материал.

#include <stdio.h>
#include <dirent.h>

vold main( ) {
    DIR *dirp;
    struct direct *directp;
    
    dirp = opendir ( ".");
    while ( (directp = readdir (dirp) ) != NULL)
        (void) printf ( "%s\n", directp->d_name );
    (void) closedir (dirp);
    return 0;
}

Здесь мы открываем текущий каталог и печатаем его содержимое на экране.


1.4.3. Создание и уничтожение файлов. Получение информации о файлах.

    Для создания обычных файлов используется системный вызов creat( ):

#include <sys/types.h>
#include <sys/stat.h>
#include <fcnt1.h>

int creat (char *path, mode_t mode);

    Аргументы функции: path - имя файла, mode - задает атрибуты создаваемого файла (см. описание системного вызова mkdir( ) в предыдущем разделе).

    Специальные файлы, соответствующие устройствам, обычно располагаются в директории "/dev". Создать их можно системным вызовом mknod( ):

#include <sys/types.h>
#include <sys/stat.h>

int mknod (char *path, mode_t, dev_t dev);

Аргумент mode задает права доступа к файлу. deu представляет собой структуру, задающую тип и номер устройства, соответствующего специальному файлу.

Когда файл более не нужен, его можно удалить с помощью функции unlink ( ):

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int unlink (char *path);

где path - имя файла. Последний должен быть закрыт.

Чтобы получить информацию о наборе данных, в UNIX предусмотрена функция stat( ):

#include <sys/types.h>
#include <sys/stat.h>

int stat (char *path, struct stat *fstat);

Она по имени файла path находит соответствующую информацию и помешает ее в структуру, на которую указывает fstat. Среди полей структуры stat особый интерес представляют следующие:

st_mode - тип файла; это комбинация флагов:
S_IFDIR - каталог;
S_IFCHR - символьно-ориентированное устройство;
S_IFBLK - блочно-ориентированное устройство;
S_IFREG - обычный файл;
st_uid - идентификатор владельца файла;
st_gid - идентификатор группы, которой принадлежит владелец файла;
st_size - размер файла;
st_atime - время последнего использования;
st_mtime - время последней модификации.

1.4.4. Ввод-вывод данных.

    Функции ввода-вывода используются в системе UNIX для доступа к содержимому файлов. Они делятся на две основные группы. Первая представляют собой группу системных вызовов и реализует минимальный набор действий, необходимых для чтения (записи) информации. Это как бы нижний уровень. Процедуры, входящие во вторую группу, называются функциями буферизованного или форматированного ввода-вывода. Они используют в своей работе обращения к системным вызовам нижнего уровня для выполнения непосредственного ввода-вывода данных. Функции, описываемые в данном разделе, относятся к первой группе. Процедуры буферизованного ввода-вывода можно найти в [6].

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

#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int open (const char *path, int oflag, mode_t mode);

    Здесь path - имя файла, oflag - режим открытия файла, задаваемый при помощи констант:

O_RDONLY - открыть для чтения;
O_WRONLY - открыть для записи;
O_RDWR - открыть для чтения и записи.

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

O_APPEND - добавить данные в конец файла;
O_CREAT - создать файл;
O_EXCL - возвратить код ошибки, если задан флаг O_CREAT, и файл уже существует;
O_TRUNC - урезать открываемый файл до нулевой длины.

    Аргумент mode - задает атрибуты создаваемого файла (см. описание системного вызова mkdir( ) в разделе 1.4.2).

    При успешном завершении системный вызов open( ) возвращает дескриптор файла. В случае ошибки открытия файла функция возвращает -1.

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

    Ядро системы устанавливает ограничение на количество одновременно открытых одним процессом файлов Их максимальное число может 16 до 64, в зависимости от версии UNIX и конфигурации ядра системы, заданной при генерации.

    После того как файл открыт, данные вводятся из него посредством функции read( ):

#include <unistd.h>

int read (int fd, void *buff, size_t nbytes);

    Здесь fd - дескриптор файла, buff - адрес буфера для приема данных, и nbytes - количество байт, подлежащих вводу из файла. Функция возвращает число считанных байт.

    Данные выводятся из памяти в файл системным вызовом write( ):

#include <unistd.h>

int write (int fd, void *buff, size_t nbytes);

    Функция имеет те же аргументы, что и предыдущая, и возвращает в качестве своего значения количество записанных байт.

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

#include <sys/types.h>
#include <unistd.h>

int lseek (int fd, off_t offset, int whence);

    Аргумент fd задает дескриптор открытого файла, offset - величину смещения в байтах, whence указывает, относительно какой позиции файла задано смещение. Последний аргумент может принимать следующие значения:

SEEK_SET - относительно начала файла;
SEEK_CUR - относительно текущей позиции;
SEEK_END - относительно конца файла.

     После выполнения последней операции ввода-вывода с файлом, связанную с ним структуру данных ядра можно освободить, выполнив операцию закрытия. Для этого вызывается процедура close( ):

#include <unistd.h>

int close (int fd);

    Все открытые файлы автоматически закрываются при завершении процесса.

    Приведенный ниже пример иллюстрирует применение описанных системных вызовов и содержит программу, дописывающую строку в конец файла.

#include <sys/types.h>
#include <sys/stat.h>
#include <unistd.h>
#include <fcntl.h>
#include <strings.h>

char *file_name = "file.txt";
char *string = "This string to add.\n";

int main ( )
{
  int fd;
  
  if ( (fd = open (file_name, O_WRONLY)) < 0)
    perror ("open file error");

  if (lseek (fd, o, SEEK_END) < 0)
    perror ("lseek error");

  if (write (fd, string, strlen (string)) != strlen (string)
    perror ("write error");
    
  close (fd);
}

1.5. Процессы.

    Как упоминалось выше, UNIX является многозадачной ОС, т.е. в ней одновременно могут работать несколько программ-процессов. Ядро системы ответственно за их запуск и последующее сопровождение. Поскольку процессор в ЭВМ, как правило, один, то ядро же обеспечивает выделение каждой программе интервалов машинного времени. Ядро осуществляет корректное переключение между задачами.

    Выполнение программы начинается с вызова системой функции main( ). Полный формат ее следующий:

int main (int argc, char *argv[], char *evnir[]);

    Здесь argv - массив указателей на строки, содержащие параметры, переданные задаче при запуске, argc - число элементов массива argv, envir - массив строк, содержащих переменные среды (environment) и их значения.

    Завершение процесса происходит при возврате из функции main( ) или вызове программой функции exit( ).

    Во время выполнения задача может запустить другой процесс. Для этого в системе UNIX предусмотрена системная функция fork( ):

#include <sys/types.h>
#include <unistd.h>

pid_t fork (void);

    После выполнения процедуры, ядром UNIX создается точная копия процесса, выполнившего системный вызов. При этом, вызывающий процесс называется родительским, а новый - процессом-"потомком". Родительскому процессу функция fork( ) возвращает идентификатор порожденного процесса, а "потомку" возвращается 0. Важным свойством вызова fork( ) является то, что оба процесса продолжают иметь доступ ко всем открытым файлам "родителя".

    После того, как новая задача запущена, она может выполнять код "родителя", но, как правило, подгружается и начинает работать программа, находящаяся в другом выполнимом файле. Чтобы осуществить это, можно использовать системный вызов exec( ), exec1( ) или другой аналогичный. Перечисленные функции отличаются друг от друга способом передачи параметров вызываемой программе. Функция exec( ) имеет прототип:

#include <unistd.h>

int exec ( const char *path, int argc, char *argv[ ] );

    Здесь path - имя выполняемого файла, argv - массив указателей на строки, передаваемые загружаемой программе в качестве параметров, argc - количество строк массива argv. Прототипы и объяснение других процедур, подобных exec( ), можно найти в соответствующих разделах подсказки UNIX (см. подробнее 1.9.1.).

    Процесс-"родитель", когда заканчивает работу, может подождать завершения запущенных им ранее программ. Для этого следует обратиться к системной процедуре wait( ):

#include <sys/types.h>
#include <sys/wait.h>

pid_t wait (int *status);

    Функция возвращает идентификатор процесса, окончившего работу. Код его завершения записывается по адресу, заданному аргументом status.

    Заметим, что в различных диалектах UNIX имеются более развитые варианты функции wait( ). Это wait3( ), waitpid( ), waitid( ) (более подробную информацию можно найти в документации по той системе, на которой работает пользователь).

    В качестве примера рассмотрим подпрограмму, реализующую функцию system( ). Она выполняет переданную ей командную строку при помощи интерпретатора командного языка (командного процессора) sh.

#include <sys/types.h>
#include <sys/wait.h>
#include <unistd.h>

int system (const char *cmd_string)
  {
  pid_t pid;
  int status;
  if (cmd_string = = NULL)
    return (1);
  if ( (pid = fork ( ) ) < 0)
    return (-1);
  if (pid == 0) {   /* процесс-"потомок" */
    execl ("/bin/sh", "sh", "-c", cmd_string, (char*) 0);
    exit (-1);     /* выполняется при ошибке в execl */
  }
  else {     /* процесс-"родитель" ожидает завершения */
             /* выполнения процесса-"потомка" */
  wait (&status);
  }
}

1.6. Сигналы.

    Для взаимодействия процессов между собой и ядром, в системе UNIX существуют сигналы. Посылаются они в следующих случаях, перечисленных ниже.

  1. При нажатии пользователем определенных клавиш на клавиатуре терминала. Например, <Ctrl+С> - завершить процесс и <Ctrl+Z> - приостановить процесс.
  2. При аппаратных сбоях или попытке выполнения процессом ошибочных с точки зрения системы действий, таких, как деление на ноль или обращение к неверному адресу.
  3. Сигналы могут посылаться одним процессом другому при помощи системного вызова kill( ).
  4. При возникновении программных ошибок в системе, например, при переполнении буфера во время выполнении операции буферизованного вывода.

    Когда программа получает сигнал, вызывается либо функция, зарегистрированная как реакция на него, либо выполняется стандартное действие ОС. Большинство нетривиальных программ в системе UNIX используют сигналы. Каждый сигнал имеет целочисленный идентификатор. Некоторые сигналы являются стандартными. Их идентификаторам соответствуют символические константы, определенные в файле-заголовке <signal.h>.

    Количество стандартных сигналов зависит от версии системы. Ниже приведен перечень, используемый в большинстве диалектов ОС.

SIGABRT Сигнал генерируется вызовом функции abort( ) и вызывает аварийное завершение процесса.
SIGALARM Сигнал посылается по истечении времени, установленного функцией alarm( ).
SIGBUS Сигнал возникает при сбое оборудования.
SIGCONT Сигнал позволяет возобновить выполнение процесса, прерванного по сигналу SIGSTOP.
SIGFPE Ошибка при выполнении арифметической операции с действительными числами.
SIGILL Неверная инструкция процессора при выполнении программы.
SIGINT Сигнал возникает при вводе с терминала <Ctrl+С>.
SIGIO Завершение асинхронной операции ввода-вывода.
SIGIOT Ошибочное завершение асинхронной операции ввода-вывода.
SIGKILL Немедленно завершить процесс.
SIGPIPE Ошибки при записи в канал межпроцессного ввода-вывода (pipe) (см. 1.7.2.).
SIGSEGV Ошибка при использовании процессом неверного адреса.
SIGSTOP Приостановить выполнение процесса.
SIGSYS Ошибка при выполнении системного вызова.
SIGTERM Завершить процесс.
SIGTRAP Аппаратная ошибка при выполнении процесса.
SIGTSTP Ввод с клавиатуры <Ctrl+Z> (приостановить процесс).

    Программа, получив сигнал, может обработать его одним из трех способов.

  1. Процесс может проигнорировать сигнал. Это невозможно только для SIGKILL и SIGSTOP. Эти два сигнала позволяют администратору системы UNIX прерывать или приостанавливать процессы в случае необходимости.
  2. Процесс может зарегистрировать собственную функцию обработки сигнала. Рекомендуется, по возможности, обрабатывать сигналы, приводящие к преждевременному завершению программы.
  3. Если не задана функция обработки сигнала, ядро системы выполняет действия, предусмотренные для его обработки по умолчанию. Если пришел сигнал, связанный с программными и аппаратными ошибками, процесс, как правило, завершается с созданием в текущей директории файла с именем "core". В последний помещается содержимое области оперативной памяти, которая была занята задачей в момент прихода сигнала.

    Для задания способа обработки сигнала используется системный вызов signal( ):

#include <signal.h>

int (*signal (int sig, void (*func) (int))) (int);

    Первый аргумент представляет собой идентификатор сигнала, а второй - адрес функции, производящей обработку. При задании второго аргумента могут быть использованы два специальных значения: SIG_IGN - игнорировать сигнал и SIG_DFL - обработать сигнал стандартным способом. Функция всегда возвращает адрес предыдущей функции обработки сигнала.

    Приведен пример работы с сигналами.

#include <stdio.h>
#include <signal.h>

void SignalCtrlC (int sig_no) {
  printf ("Receive signal : %d\n", sig_no);
  signal (SIGINT, SignalCtrlC);
};

void main (int argc, char *argv []) {
  char c = 0;
  signal (SIGINT, SignalCtrlC);
  while (c!='q')
    c = getchar( );
}

    Программа заказывает реакцию на нажатие комбинации <Ctrl+C> (SIGINT). После чего ожидается ввод с терминала символа 'q'. Когда он приходит, процесс завершается. Функция SignalCtrlC( ) реагирует на сигнал SIGINT, просто печатая соответствующее сообщение.


1.7. Обмен данными между процессами.

    Разделение одной большой программы на несколько параллельно выполняющихся процессов является обычной практикой при программировании в системе UNIX. При этом возникает необходимость обмена данными между задачами. Поскольку сигналы не достаточно информативны, ОС предусматривает другие способы передачи информации от одного процесса другому. Рассмотрим некоторые из них.

  1. Разделяемые файлы.
  2. Каналы межпроцессорного обмена.
  3. Другие способы обмена данными.
    1. Очереди сообщений.
    2. Семафоры.
    3. Разделяемая память.

1.7.1. Разделяемые файлы.

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

    Перед выполнением операции записи в файл, процесс должен проверить, не блокирована ли запись, в которую предполагается поместить новые данные. Если область блокирована другим процессом, первый должен ждать ее освобождения. Затем надо блокировать запись и произвести операцию вывода. По завершении действий блокировка отменяется, давая другим процессам возможность записывать свои данные в этот файл. Для выполнения операции чтения блокировку можно не делать.

    Для блокирования записи используется системный вызов lockf( ):

#include <unistd.h>

int lockf (int fd, int function, long size);

    Здесь fd - дескриптор файла. Аргумент function задает выполняемую операцию и может принимать следующие значения:

F_ULOCK - отменить предыдущую блокировку;
F_LOCK - блокировать запись;
F_TLOCK - блокировать запись с проверкой, не блокирована ли она другим процессом;
F_TEST - проверить, не блокирована ли запись другим процессом.

     Начало записи определяется текущим положением указателя в файле. Длина записи задается аргументом size. При неудачной попытке блокирования записи функция возвращает в качестве своего значения (-1).

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


1.7.2. Каналы межпроцессного обмена.

    В ранних версиях системы UNIX основным средством для обмена данными между задачами были каналы межпроцессного обмена (pipes).

    Для создания канала используется системный вызов pipe( ). Его формат следующий:

#include <inistd.h>

int pipe (int fd[2]);

    Аргументом функции является указатель на массив двух целых чисел, в котором функция возвращает два файловых дескриптора. Первый из них предназначен для чтения данных из канала, второй - для записи в него.

    После создания pipe, процесс может при помощи обычного системного вызова write( ) выводить данные в него, а затем вводить их, вызывая соответственно функцию read( ). При выполнении вызова fork( ) дескрипторы канала наследуются процессом-"потомком". Таким образом, оба процесса получают возможность обмениваться данными.

    Ограничением в данном случае является то, что канал должен работать лишь в одну сторону. Либо "родитель" должен писать, а "потомок" читать, либо наоборот. В первом случае для передачи данных от процесса-"родителя" к процессу-"потомку", первый должен закрыть fd[0], а второй fd[1]. Во втором случае выполняются противоположные действия. Если предполагается передавать данные в обе стороны, необходимо создать два канала.

    Приведенный ниже пример иллюстрирует использование pipe. Процесс-"родитель" создает канал и порождает новый процесс. Затем выводит в pipe строку, которую "потомок" читает и выводит на терминал.

#include <stdio.h>
#include <inistd.h>

int main ()
{
  int n, fd [2];
  pid_t pid;
  char line [128];
    if (pipe (fd) == -1)
    perror ("pipe: ошибка создания канала"); 
      if ( (pid = fork ( )) == -1)
    perror ("fork: ошибка создания процесса"); 
      if (pid > 0) {    /*процесс-"родитель" */
    close (fd [0] );
    write (fd [1], "Hello, world!\n", 12);
  }
  else {	        /*процесс-"потомок" */
    memset (line, 0, 120);
    close (fd [1] );
    n = read (fd [0], line, 120);
    puts (line);
  }
      exit (0);
}

    К недостаткам каналов следует отнести то, что они имеют свойство переполняться. Это значит, что если вывод в pipe происходит более интенсивно, чем ввод из него, возникает ошибка, и ядро системы генерирует сигнал SIGPIPE. Существенным ограничением является и то, что взаимодействовать через pipe могут только процессы, находящиеся "в родственных отношениях" ("родитель"-"потомок").


1.7.3. Другие способы обмена данными.

    В современных версиях системы UNIX предпочтительнее пользоваться средствами межпроцессного обмена данными, называемыми IPC (InterProcess Communications). IPC включает в себя сообщения, семафоры и разделяемую память. В дальнейшем будем именовать их объектами IPC.

    Доступ к объекту IPC производится при помощи уникального ключевого значения (целого числа) , присваиваемого каждому из них при создании. Важным свойством объектов IPC является то, что они не входят составной частью ни в один из процессов, а существуют самостоятельно под управлением ядра. Оно обеспечивает их защиту в многопользовательской многозадачной среде, т.е. создавая очередь сообщений, семафоры или разделяемую память, процесс предусматривает режим доступа к данному объекту других программ. Рассмотрим подробнее каждый из объектов IPC.

  1. Очереди сообщений.
  2. Семафоры.
  3. Разделяемая память.

1.7.3.1. Очереди сообщений.

    Очередь сообщений представляет собой механизм передачи порций данных одинаковой длины. Каждая создаваемая очередь имеет уникальный ключ.

    Создание объекта или доступ к нему производится при помощи функции msgget( ):

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgget (key_t key, int flag);

    Первый аргумент задает идентификатор очереди. Второй - flag - комбинация флагов, которая задает права доступа к объекту, а также то, следует ли создавать очередь сообщений или же использовать очередь, уже созданную другим процессом. Возможные флаги следующие:

MSG_R 00400 владелец может читать;
MSG_W 00200 владелец может писать;
(MSG_R>>3) 00040 пользователи, входящие в одну группу с владельцем (группа), могут читать;
(MSG_R>>3) 00020 группа может писать;
(MSG_R>>6) 00004 все остальные пользователи (остальные) могут читать;
(MSG_R>>6) 00002 остальные могут писать;
IPC_PRIVATE   создается объект, который может использоваться лишь данным процессом;
IPC_CREAT   создать объект, если он не существует;
IPC_EXCL   используется в сочетании с IPC_CREAT; если этот флаг выставлен, и объект с заданным ключом существует, то функция возвращает код ошибки.

     (При описании приведены также численные значения констант). Функция возвращает процессу идентификатор, используемый в дальнейшем для работы с объектом IPC. В случае возникновения ошибок msgget( ) возвращает значение -1.

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

struct msg_struct
{
  long msg_type;                 /* тип сообщения */
  char msg_data [MAX_MSG__DATA]; /* данные сообщения */
};

    Тип используется при выборе сообщения из очереди. Следует отметить, что операционная система накладывает ограничения на максимальный размер сообщений, количество сообщений в одной очереди и общее количество сообщений, записанное во все очереди. Соответствующие ограничения задаются константами MSGMAX, MSGNNB, MSGTQL в файле <sys/msg.h>.

    Записать сообщение в очередь можно, используя функцию msgsnd( ):

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgsnd (int msgid, const void *ptr, size_t nbytes, int flag;

    Здесь :

msgid - идентификатор очереди, полученный при вызове msgget( );
ptr - указатель на сообщение, записываемое в очередь;
nbytes - размер сообщения в байтах;
flag - либо 0, либо IPC_NOWAIT; в первом случае, если очередь полна, то msgsnd( ) ждет ее освобождения; во втором случае функция при полной очереди возвращается сразу с кодом -1.

     Получение сообщений из очереди производится вызовом функции msgrcv( ). Формат вызова функции следующий:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgrcv (int msgid, void *ptr, size_t nbytes, long type, int flag);

    Здесь:

msgid - идентификатор очереди, полученный при вызове msgget( );
ptr - указатель на буфер для приема сообщения;
nbytes - размер буфера;
type - тип выбираемого из очереди сообщения; если значение этого параметра равно 0, из очереди выбирается первое по порядку сообщение; если type больше 0, из очереди выбирается первое сообщение, поле msg_type которого (см. определение msg_struct) содержит значение равное значению, хранящемуся в первых четырех байтах буфера приема сообщения; если же type меньше 0, будет выбрано сообщение, имеющее минимальное значение поля msg_type;
flag - либо 0, либо IPC_NOWAIT; в первом случае, если очередь пуста, то msgrcv() ждет прихода события; во втором случае функция при пустой очереди возвращается сразу с кодом -1.

     Для управления состоянием очереди сообщений используется системный вызов msgctl( ). Функция позволяет получить информацию о состоянии очереди, изменить права доступа процессов к ней или удалить объект. Ее прототип следующий:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/msg.h>

int msgctl (int msgid, int cmd, struct msqid_ms *buf);

Здесь:

msgid - идентификатор очереди, полученный при вызове msgget( );
cmd - команда управления очередью; значения параметра определяются константами: IPC_STAT - получить состояние очереди, IPC_SET - установить параметры очереди, и IPC_RMID - удалить очередь сообщений;
buf - адрес структуры данных, используемой при выполнении команд, задаваемых параметром cmd.

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

Оба процесса используют файл заголовок "message.h".

#include <sys/types.h>
#include <sys/ips.h>
#include <sys/msg.h>

#define MSQ_ID   2001     /* уникальный ключ очереди */
#define PERMS    00666    /* права доступа - все могут
                             читать и писать */
#define MSG_TYPE_STRING 1 /* тип сообщения о том, что
                             передана непустая строка */
#define MSG_TYPE_FINISH 2 /* тип сообщения о том, что
                             пора завершать обмен */
                          
#define MAX_STRING 120    /* максимальная длина строки */
          
typedef struct            /* структура сообщения */
{
  int type;
  char string [MAX_STRING];
}
message_t;

    Код программы-клиента:

#include <stdio.h>
#include <string.h>
#include "message.h"

void sys_err (char * msg)
{
  puts (msg);
  exit (1);
}

int main ()
{
  int msqid;      /* идентификатор очереди сообщений */
  message_t msg;  /* сообщение */
  char s [MAX_STRING];
  
  /* создание очереди */
  if ( (msqid = msgget (MSQ_ID, 0)) < 0)
    sys_err ("client:can not get msg queue");

  while (1) {
    scanf ("%s", s); /* ввод строки */
    if (strlen (s) != 1) {
      msg.type = MSG_TYPE_STRING;
      strncpy (msg.string, s, MAX_STRING);
    }
    else
    {
      msg.type = MSG_TYPE_FINISH;
    };
    /* посылка сообщения процессу-серверу */
    if (msgsnd (msqid, &msg, sizeof (message_t), 0) != 0)
      sys_err ("client: message send error");
    if (strlen (s) == 1) /* пустая строка - выход */
        break;
  }
      exit (0);
}

    Код программы-сервера:

#include <stdio.h>
#include <string.h>
#include "message.h"

void sys_err (char * msg)
{
  puts (msg);
  exit (1);
};

int main ()
{
  int msqid;
  message_t msg;
  char s [MAX_STRING];

  /* создание очереди сообщений */
  if ( (msqid = msgget (MSQ_ID, PERMS | IPC_CREAT) ) < 0)
    sys_err ("server: can not create msg queue");

  while (1) {
    /* получение очередного сообщения */
    if (msgrcv (msqid, &msg, sizeof (message_t), 0, 0) < 0)
      sys_err ("server: msg recive error");
      
    if (msg.type == MSG_TYPE_STRING) /* печать строки */
      printf ("%s", msg.string);

    if (msg.type == MSG_TYPE_FINISH) /* выход из цикла */
      break;
  }

  /* удаление очереди сообщений */ 
  if (msgctl (msqid, IPC_RMID, (struct msqid_ds *) 0) < 0)
    sys_err ("server: msq queue remove error");

  exit (0);
}

1.7.3.2. Семафоры.

    Семафоры представляют собой стандартный способ разрешения или запрещения выполнять те или иные действия. Семафор - это обычно целое число. Если оно имеет значение 1 (установлен), то операция запрещена. Если его значение 0, то - разрешена. UNIX позволяет создавать сразу целые массивы семафоров. Они, как и другие объекты IPC, идентифицируются с помощью уникального ключа, задаваемого неотрицательным целым числом.

    Для создания массива семафоров используется системный вызов semget( ):

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semget (key_t key, int nsems, int flag);

    Здесь:

key - уникальный ключ объекта IPC;
nsems - количество семафоров в массиве;
flag - задание режима создания массива семафоров; аналогично параметру flag в msgget( ), только префикс "MSG" заменен "SEM".

    Функция semget( ) возвращает целое число, используемое для доступа к массиву семафоров.

    Устанавливать и получать значения элементов, а также управлять состоянием всего массива семафоров можно при помощи системного вызова semctl( ).

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>

int semctl (int semid, int semnum, int cmd, union semun arg);

    Здесь:

semid - идентификатор массива семафоров, возвращаемый функцией semget( );
semnum - номер изменяемого элемента в массиве;
cmd - выполняемая операция; основные из них следующие:
GETVAL - получить значение семафора;
SETVAL - задать значение семафора;
IPC_RMID - удалить объект;
arg - объединение, используемое для передачи данных, необходимых для выполнения операции и/или получения ее результатов; например, ее поле val служит для задания и получения значения семафора.

    Пример работы с массивами семафоров приведен в следующем разделе.


1.7.3.3. Разделяемая память.

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

    Для получения доступа к сегменту разделяемой памяти используется системный вызов shmget( ). Формат его следующий:

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

int shmget (key_t key, int size, int flag);

    Здесь:

key - уникальный ключ разделяемого сегмента памяти;
size - размер сегмента; в некоторых версиях системы UNIX он ограничен 128 К байтами;
flag - задание режима создания разделяемой памяти; значение этого параметра то же, что и в msgget( ), только префикс "MSG" заменен на "SHM".

    При успешном создании или, если объект с заданным ключом key существует, функция возвращает идентификатор сегмента. В случае ошибки функция возвращает значение -1.

    После создания разделяемой памяти, надо получить ее адрес. Для этого используется вызов shmat( ):

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

void *shmat (int shmid, void *addr, int flag);

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

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

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/shm.h>

void shmdt (void *ptr);
int shmctl (int shmid, int cmd, struct shmid_ds *buf);

    Здесь :

ptr - указатель на сегмент разделяемой памяти, полученный при вызове shmat( );
shmid - идентификатор сегмента, возвращаемый функцией shmget( );
cmd - выполняемая операция, основная из них IPC_RMID - удалить объект;
buf структура, используемая для передачи данных, необходимых для выполнения операции и/или получения ее результатов.

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

    Оба процесса используют файл-заголовок "message.h".

#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <stdio.h>

#define SEM_ID	2001      /* ключ массива семафоров */
#define SHM_ID	2002      /* ключ разделяемой памяти */
#define PERMS	0666      /* права доступа */

/* коды сообщений */

#define MSG_TYPE_EMPTY  0 /* пустое сообщение */
#define MSG_TYPE_STRING 1 /* тип сообщения о том, что
                             передана непустая строка */
#define MSG_TYPE_FINISH 2 /* тип сообщения о том, что
                             пора завершать обмен */
#define MAX_STRING	120

/* структура сообщения, помещаемого в разделяемую память */
typedef struct
{
  int type;
  char string [MAX_STRING];
} message_t;

    Код программы-клиента:

#include <stdio.h>
#include <string.h>
#include "message.h"

void sys_err (char *msg)
{
  puts (msg);
  exit (1);
}

int main ()
{
  int semid;                    /* идентификатор семафора */
  int shmid;                    /* идентификатор разделяемой памяти */
  message_t *msg_p;             /* адрес сообщения в разделяемой
                                   памяти */
  char s[MAX_STRING];

  /* получение доступа к массиву семафоров */
  if ((semid = semget (SEM_ID, 1, 0)) < 0)
    sys_err ("client: can not get semaphore");

  /* получение доступа к сегменту разделяемой памяти */
  if ((shmid = shmget (SHM_ID, sizeof (message_t), 0)) < 0)
    sys_err ("client: can not get shared memory segment");

  /* получение адреса сегмента */
  if ((msg_p = (message_t *) shmat (shmid, 0, 0)) == NULL)
    sys_err ("client: shared memory attach error");

  while (1)
    {
      scanf ("%s", s);
      while (semctl (semid, 0, GETVAL, 0) || msg_p->type != MSG_TYPE_EMPTY)
        /*
         *   если сообщение не обработано или сегмент блокирован - ждать
         *                                                             */
      ;
      semctl (semid, 0, SETVAL, 1);     /* блокировать */
      if (strlen (s) != 1)
        {
          /* записать сообщение "печать строки" */
          msg_p->type = MSG_TYPE_STRING;
          strncpy (msg_p->string, s, MAX_STRING);
        }
      else
        {
          /* записать сообщение "завершение работы" */
          msg_p->type = MSG_TYPE_FINISH;
        };
      semctl (semid, 0, SETVAL, 0);     /* отменить блокировку */
      if (strlen (s) == 1)
        break;
    }
  shmdt (msg_p);                /* отсоединить сегмент разделяемой памяти */
  exit (0);
}

    Код программы-сервера:

#include <stdio.h>
#include <string.h>
#include "message.h"

void sys_err (char *msg)
{
  puts (msg);
  exit (1);
}

int main ()
{
  int semid;                    /* идентификатор семафора */
  int shmid;                    /* идентификатор разделяемой памяти */
  message_t *msg_p;             /* адрес сообщения в разделяемой
                                   памяти */
  char s[MAX_STRING];

  /* создание массива семафоров из одного элемента */
  if ((semid = semget (SEM_ID, 1, PERMS | IPC_CREAT)) < 0)
    sys_err ("server: can not create semaphore");

  /* создание сегмента разделяемой памяти */
  if ((shmid = shmget (SHM_ID, sizeof (message_t), PERMS | IPC_CREAT)) < 0)
    sys_err ("server: can not create shared memory segment");

  /* подключение сегмента к адресному пространству процесса */
  if ((msg_p = (message_t *) shmat (shmid, 0, 0)) == NULL)
    sys_err ("server: shared memory attach error");

  semctl (semid, 0, SETVAL, 0); /* установка семафора */
  msg_p->type = MSG_TYPE_EMPTY;

  while (1)
    {
      if (msg_p->type != MSG_TYPE_EMPTY)
        {
          if (semctl (semid, 0, GETVAL, 0))     /* блокировка - ждать */
            continue;

          semctl (semid, 0, SETVAL, 1); /* установить блокировку */

          /* обработка сообщения */
          if (msg_p->type == MSG_TYPE_STRING)
            printf ("%s\n", msg_p->string);
          if (msg_p->type == MSG_TYPE_FINISH)
            break;

          msg_p->type = MSG_TYPE_EMPTY; /* сообщение обработано */
          semctl (semid, 0, SETVAL, 0); /* снять блокировку */
        }
    }

  /* удаление массива семафоров */
  if (semctl (semid, 0, IPC_RMID, (struct semid_ds *) 0) < 0)
    sys_err ("server: semaphore remove error");

  /* удаление сегмента разделяемой памяти */
  shmdt (msg_p);
  if (shmctl (shmid, IPC_RMID, (struct shmid_ds *) 0) < 0)
    sys_err ("server: shared memory remove error");

  exit (0);
}

1.8. Распределение памяти.

    Для динамического выделения процессу дополнительной памяти, в системе предусмотрены четыре функции.

Форматы вызова перечисленных процедур следующие:

#include <sys/types.h>
#include <stdlib.h>

void *malloc (size_t size);
void *calloc (size_t nobj, size_t size);
void *realloc (void *ptr, size_t newsize);
void free (void *ptr);

    Здесь:

size - в случае malloc( ) - размер выделяемой области памяти в байтах; в случае calloc( ) - размер в байтах элемента массива данных, под который выделяется память;
nobj - число элементов массива данных;
ptr - указатель на ранее выделенную память;
newsize - новый размер области памяти.

    Функции malloc( ), calloc( ) и realloc( ) возвращают указатель на выделенную память или NULL, если свободного сегмента указанного размера найти не удалось. Адрес памяти, возвращаемый процедурами, гарантировано выравнивается на границу максимального по размеру типа данных в системе. Обычно это тип double, размер которого равен восьми байтам.

    В приводимом примере мы заказываем память под матрицу чисел и заполняем ее единицами.

#include <stdlib.h>

void FillMatrix (int **matrix, int nrows, int ncols)
{
  int i, j;

  for (i=0; i <nrows; i++)
    for (j=0; j<nrows; j++)
      matrix [i] [j] = 1;
}

void main( )
{
  int **matrix;

  if ((matrix=(int**)malloc(100*sizeof(int*)) == NULL) {
    puts ("no memory\n");
  exit (1);
  }

  for (i=0; i<100; i++)
    if ( (matrix [i] = (int*)calloc(100, sizeof(int)) == NULL) {
      puts ("no memory\n");
      exit (1);
    }
  }

  FillMatrix (matrix, 100, 100);

  for (i=0; i<100; i++)
    free (void*)matrix[i]);

  free ( (void*)matrix);

  exit (0);
}

    Здесь мы намеренно, в демонстрационных целях, применяли разные функции для выделения памяти.


 1.9. Инструментальные средства программирования в системе UNIX.

    Данный раздел содержит сведения, необходимые пользователю для создания программ в системе UNIX. Здесь описаны соответствующие инструментальные средства ОС.

    Мы не приводим, правда, сведений о, пожалуй, самой необходимой компоненте - текстовом редакторе. Но их можно найти в [2, 3, 4, 16]. В этих книгах достаточно подробно разобраны такие стандартные утилиты как ed, vi, а в [16] перечислены основные приемы работы с очень распространенным в UNIX редактором emacs.

  1. Получение подсказки. Программа man.
  2. Файлы системы UNIX, используемые при компиляции и компоновке программ.
  3. Компилятор языка Си.
  4. Создание библиотек файлов. Программа ar.
  5. Программа make.
  6. Системы контроля исходного кода.

1.9.1. Получение подсказки. Программа man.

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

    Ее формат следующий:

man [ ключи ] имя

где "имя" - это строка, идентифицирующая объект, о котором надо получить подсказку. Опции man приведены ниже.

    Работает программа следующим образом. Она просматривает файлы, находящиеся в поддиректориях каталога "/usr/man" (заметим, что это имя может меняться в разных версиях ОС, так в системе Solaris 2.x фирмы SunSoft просматривается директория "usr/share/man"). Эти поддиректории называются секции (разделы). Их имена образуются следующим образом: "man<идентификатор>", например "man1", "man1m" и т.д. Если при поиске обнаруживается файл с именем, совпадающим с указанным в командной строке, и расширением, совпадающим с идентификатором раздела, то этот файл показывается на экране. Для просмотра используются клавиши:

<Enter>            - на строку вниз;
<Пробел>       - на экран вниз;
<Ctrl+b>, <b> - на экран вверх.

    В разных секциях располагаются файлы, относящиеся к одной группе. Например, "команды пользователя", "команды системного администратора", "системные вызовы" и пр. При этом в них могут встречаться файлы с одинаковыми именами (но не расширениями). Так существует команда администратора mount и системный вызов mount( ). Соответственно есть и два файла в разных разделах с этим именем. Поэтому, программа man при подсказках идентифицирует объекты следующим образом:

имя ( идентификатор_секции )

    Например:

mount(1m) - команда mount из раздела "1m";
mount(2)  - системный вызов mount( ) из раздела "2".

    Приведем теперь основные ключи команды man.

-M
задает путь для поиска поддиректорий, содержащих файлы подсказки; по умолчанию это "/usr/man" ("/usr/share/man");
-a
находятся и показываются на экране по порядку тексты из всех секций, относящихся к указанному имени;
-l
находятся и предъявляются пользователю ссылки на подсказки, относящиеся к   указанному имени, но находящиеся в разных разделах; так, команда

     man - l mount

выводит на экран

    mount(1m) -M /usr/man
    mount(2) -M /usr/man

-s
"идентификатор" задает идентификатор просматриваемой при поиске секции; другие разделы при этом игнорируются; так, команда

    man -sim mount

выдаст подсказку по команде mount, а

    man -s2 mount

по системному вызову mount( ).


1.9.2. Файлы системы UNIX, используемые при компиляции и компоновке программ.

    Основным языком программирования в среде UNIX является язык Си. Поэтому, именно о нем пойдет речь в данном разделе.

    При программировании на Си в файлы исходного кода (имеющие расширение ".с") включаются файлы-заголовки (расширение ".h"). Система имеет довольно много таких файлов, располагающихся в директории "/usr/include" и ее поддиректориях. Они содержат прототипы системных функций, различные структуры и типы данных и именованные константы.

    Когда программа скомпилирована, из полученных объектных файлов (расширение ".o") создается выполнимый файл. Этот процесс называется компоновкой. В порождаемый на этом шаге программный модуль должны включаться коды всех используемых приложением процедур. Системные функции находятся в так называемых библиотечных (архивных) файлах. Они имеют расширение ".a" и располагаются в директории "/usr/lib". Основным из них является файл "libc.a".

    Каждый архивный файл представляет собой совокупность объектных модулей. По принятому в системе соглашению их имена начинаются с префикса "lib".


1.9.3. Компилятор языка Си.

    Си-компилятор является основным средством для создания программ в системе UNIX. Переоценить его значение для системы невозможно. Достаточно сказать, что около 90% ядра и почти 100% утилит и библиотек системы UNIX написаны на языке Си.

    В системе UNIX Си-компилятор, как правило, состоит из трех программ: препроцессора, синтаксического анализатора и генератора кода. Результатом его работы является программа на языке ассемблера, которая затем транслируется в объектный файл, компонуемый с другими модулями загрузчиком. В результате образуется выполняемая программа.

    Как и большинство сложных программ в системе UNIX, компилятор состоит из нескольких выполняемых файлов. Основной из команд, с которой приходится иметь дело пользователю, является команда cc. Ее функцией является проанализировать введенную командную строку и выполнить действия, предписанные указанными после имени команды параметрами. Для выполнения описанной выше последовательности действий требуется вызвать программы:

/lib/cpp - препроцессор;
/lib/cc0 - синтаксический анализатор;
/lib/cc1 - генератор кода;
/bin/as - ассемблер;
/bin/ld - загрузчик.

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

    Следует отметить, что Си-компилятор зависит от версии системы. UNIX может иметь стандартный Си-компилятор, разработанный еще при создании первых версий ОС. Этот компилятор описан в [6] и в честь авторов его обычно называют "Керниган-Ритчи Си". Современные версии системы UNIX имеют в своем составе ANSI Си-компилятор.

    Для получения выполняемого файла программы, исходный текст которой содержится в одном файле "myprog.c", достаточно ввести следующую строку:

cc myprog.c

    Если в тексте программы не обнаруживается синтаксических ошибок и она не использует функций, не входящих в стандартную Си-библиотеку, в текущей директории будет создан файл "a.out", содержащий выполнимый модуль откомпилированной программы. Но для большинства программ нецелесообразно помещать всю программу в один единственный файл. Может также потребоваться использование дополнительных библиотек, добавление в выполняемый файл информации для отладчика, оптимизация выполняемого кода и многое другое. Сообщить компилятору о том, что и как требуется создать из исходных файлов, можно при помощи ключей, задаваемых в командной строке.

    Синтаксис командной строки для Си-компилятора следующий:

cc [ключ [имя файла]] ...

    Программа, написанная на языке Си, составляется из одной или более программных единиц, называемых функциями. Исходные тексты функций, составляющих программу, располагаются в одном или нескольких файлах. Компилятор служит для получения выполняемого модуля из файлов, содержащих исходные тексты программы. Си-компилятор работает только с файлами определенных типов. Тип задается именем файла, а точнее, его расширением. Эти расширения и соответствующие типы файлов следующие:

.c - исходные тексты программ;
.i - выходные файлы препроцессора;
.s - код на языке ассемблера;
.h - включаемые препроцессором файлы-заголовки;
.o - объектные файлы;
.a - архивы объектных файлов (библиотеки).

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

    Общее количество опций довольно велико. Все они описаны в справочном руководстве по системе (UNIX User Manual). Мы же ограничимся описанием наиболее часто, с нашей точки используемых ключей.

    Опция '-c' сообщает компилятору, что входные файлы должны быть откомпилированы или ассемблированы, но не объединены в выполнимую программу. Полученные объектные модули по умолчанию помещаются в файлы с именами, полученными заменой расширений ".c", ".i" или ".s" на ".o". Так команда

cc -c myprog.c

порождает объектный файл "myprog.o".

    По умолчанию компилятор создает выполнимый файл с именем "a.out". Изменить это имя можно с помощью ключа '-o'. Так, в результате работы команды

cc -o myprog myprog.c

из файла "myprog.c" будет создан выполнимый модуль с именем myprog.

    Оптимизировать объектный код можно, указав в командной строке ключ '-O'. Некоторые реализации Си-компилятора поддерживают несколько уровней (степеней) оптимизации генерируемого объектного кода. Для этого используется ключ '-On', где n - число, задающее уровень (например, '-01' или '-02').

    Для отладки программы при помощи символьного отладчика загрузочный модуль (файл) должен содержать соответствующие данные. Сгенерировать объектный код, содержащий их, можно при помощи ключа '-g'.

    Ключ '-D' позволяет задать директиву препроцессора "#define" без изменения исходного текста программы. Наличие в командной строке записи "-Dname=value" эквивалентно указанию в файле с исходным кодом строки:

#define name value

    Если поле value в командной строке отсутствует, константе присваивается значение 1. Задание знаачений в командной строке имеет приоритет перед определением их в тексте программы.

    Одним из важнейших свойств языка а программирования Си является использование включаемых файлов (или файлов-заголовков). Имена их, как правило, заканчиваются расширением ".h". Как мы уже упоминали, стандартные файлы-заголовки в системе UNIX располагаются в директории "/usr/include". Если имя включаемого файла-заголовка в тексте программы указывается в скобках ("<...>"), и имя файла не начинается с символа '/', препроцессор составляет полное имя файла по правилу: "/usr/include" + "имя файла". Если включаемый файл находится в директории, не являющейся поддиректорией "/usr/include", можно указать его местоположение при помощи ключа "-I". Например, если есть необходимость использовать файл-заголовок, находящийся в директории "/work/include", нужно указать его местоположение следующим образом:

cc -I/work/include -c myprog.c

    Следует заметить, что важна последовательность указанных в командной строке директорий при поиске соответствующих файлов. Если файлы с одинаковым именем находятся в разных директориях, эти директории указаны в командной строке с ключом "-I", то компилятор выберет файл из директории, указанной ранее.

    Для указания загрузчику того, что при создании выполняемого файла требуется использовать какой-либо библиотечный файл, используется ключ "-l". Мы уже говорили, что библиотеки объектных модулей хранятся в файлах, имена которых начинаются с "lib" и заканчиваются расширением ".а". Часть имени файла между ними является собственно именем библиотеки. В системе UNIX все стандартные системные библиотеки располагаются в директории "/usr/lib". Стандартная Си-библиотека системы располагается в файле "/usr/lib/libc.a" и подключается загрузчиком автоматически без указания ее в командной строке. Все остальные библиотеки являются дополнительными и требуют явного указания для использования. Например, если в программу надо включить библиотеку математических функций, находящуюся в файле "/lib/libm.a", командная строка должна содержать ссылку на нее в виде "-lm":

cc -o myprog myprog.c -lm

    Если необходимая библиотека находится в директории, отличной от стандартных ("lib" и "/usr/lib"), необходимо указать ее расположение при помощи ключа "-L". Действие ключа "-L" и правила поиска архивных файлов те же, что и при поиске файлов-заголовков с использованием ключа "-I". Например:

cc -L. . /lib -o myprog myprog.c -lmyprog

    Здесь при компоновке программы будет подключаться архив с именем "libmyprog.a", находящийся в директории "../lib".


1.9.4. Создание библиотек файлов. Программа ar.

    Программа ar (от archiver - архиватор) позволяет создавать библиотеки (архивы), состоящие из произвольных файлов. Библиотека - это, в свою очередь, тоже файл, который включает другие файлы и справочную информацию, необходимую для их сопровождения.

    Формат команды, запускающей архиватор, следующий:

ar [команда] имя_библиотеки имена_файлов

    При этом "команда" задает действие над перечисленными в строке файлами. Эти действия следующие:

r
добавить или заменить файлы "имена_файлов" в библиотеке "имя_библиотеки"; если архива нет - он создается;
d
удалить файлы "имена_файлов" из библиотеки "имя_библиотеки";
t
распечатать содержимое библиотеки "имя_библиотеки";
x
достать файлы "имена_файлов" из библиотеки "имя_ библиотеки".

    Команды могут иметь модификаторы:

v
печатать на экране все, что делает программа ar;
u
при использовании с командой "r" заменяются лишь файлы, версии которых в архиве отличаются от новых.

    Например, команда

ar r mylib a.c

    создает библиотеку "mylib" и помещает в нее файл "a.c". На экран ничего не выводится. Команда

ar rv mylib a.out

    добавляет в "mylib" файл "a.out", при этом печатается соответствующее сообщение. Команда

ar t mylib 

    показывает содержимое библиотеки.

    Заметим, что в некоторых версиях утилиты ar порождаются библиотечные файлы, которые не могут непосредственно обрабатываться компоновщиком при создании выполнимых модулей. Предварительно архив должен быть обработан программой ranlib. Например:

ranlib mylib

    После этого библиотека готова для компоновки.


1.9.5. Программа make.

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

    Сложная программная система, как правило, состоит не только из набора c- и h-файлов, но и из файлов, содержащих рабочую и справочную документацию по разрабатываемому программному обеспечению, исходные данные для работы или отладки модулей, и многое другое.

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

    Файлы, составляющие разрабатываемый комплекс, зависят друг от друга. Так объектные модули зависят от соответствующих файлов, содержащих исходные тексты. Последние зависят от файлов-заголовков. При отладке программ легко забыть, какие файлы после изменений компилировались, а какие нет. Попытка же перекомпилировать весь проект - непозволительная роскошь.

    Решить проблему помогает утилита make. Она позволяет задавать связь модулей, умеет делать сравнения времен их модификации и, на этой основе, выполняет реконструкцию (например, перекомпиляцию) системы.

    При использовании интерпретатора make, рекомендуется следующая технология разработки программного комплекса:

редактор -> make -> проверка -> редактор

    Такая технология существенно повышает производительность труда программиста, так как освобождает его от забот по "ручной" сборке программ. make "следит" за тем, чтобы при многократных компиляциях и отладках приложений "не делалось то, что можно не делать".

    Задание для утилиты make представляет собой программу на специальном языке (ее мы будем называть make-программой). make-программа содержит структуру зависимостей файлов и действия над ними, оформленные в виде списка правил. Выполнение действий приводит к требуемой перестройке комплекса. Текст программы может содержать определения переменных, используемых затем в описаниях правил и специальных директив, понимаемых утилитой.

    Правило описывается следующим образом: сначала указывается имя файла, который необходимо создать (цель (target )), затем двоеточие, затем имена файлов, от которых зависит цель (подцели). Эта строка правила называется "строкой зависимостей". После этого идут строки, описывающие действия, которые надо осуществить, если обнаружено, что хотя бы одна подцель модифицировалась позже, чем файл-цель. Действие - это инструкция командному процессору, предписывающая, например, скопировать файлы, вызвать ту или иную программу (например компилятор) или сделать что-либо еще.

    Таким образом, правило make-программы в общем виде выглядит так:

имя_цели . . . : [имя_подцели] 
	[действие]
	.
	.
	[действие]

    Cинтаксис строк, задающих действия, соответствует синтаксису командных строк shell. Первым символом такой строки в make-программе должен быть символ табуляции - это обязательное условие! Если строка слишком длинная, то ее можно разбить на подстроки. В конце каждой из них, кроме последней, ставится символ '\'. Все последовательности символов, начиная от символа "#"' и до конца строки, являются комментарием. Пустые строки и лишние пробелы игнорируются. Интерпретатор make передает строку, задающую действие, на выполнение shell без ведущего символа табуляции.

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

    Если make-программа размещена в файле с именем "Makefile" или "makefile", то при запуске утилиты имя файла с программой можно не указывать в командой строке. В противном случае его надо задать при помощи опции '-f'. Таким образом, вызов

make

    выполняет программу из файла "Makefile" или "makefile", а

make -f mymakeprog

    программу из файла "mymakeprog".

    В командной строке при вызове make можно также указать имя цели, список зависимостей которой надо проверить и, возможно выполнить соответствующие действия. Иначе выполняться будет первое правило make-программы.

    Рассмотрим пример, иллюстрирующий применения make для создания программы, исходный текст которой содержится в файлах: "file1.c" и "file2.c". Текст make-программы выглядит следующим образом:

prog: file1.o file2.o
	cc -o proj file1.o file2.o
file1.o: file1.c
	cc -c file1.c
file2.o: file2.c
	cc -c file2.c

    Первая строка программы определяет, что целью программы является получение файла "prog", зависящего от файлов "file1.o" и "file2.o". Во второй строке указывается действие, по которому получается целевой файл "prog". Остальные строки содержат описание правил получения подцелей "file1.o" и "file2.o".

    Для того чтобы облегчить создание сложных make-программ, утилита позволяет задавать и использовать в них переменные. Создается переменная с помощью конструкции

<идентификатор>=<значение>

    Здесь <идентификатор> - имя переменной, <значение> - это произвольная строка, возможно пустая. В последнем случае переменная считается неопределенной. Чтобы использовать переменную в make-программе, надо поставить ее идентификатор в скобки, перед которыми находится символ '$'. В следующем примере иллюстрируется применение переменных.

PROGNAME = prog
OBJS	= file1.o \
	file2.o
$(PROGNAME) : $(OBJS)
cc -o $(PROGNAME) $(OBJS)

    Переменные можно определять не только в тексте программы, но и в командной строке при запуске утилиты. Например:

make PROGNAME=prog1

    Если в этом случае значение переменной состоит из нескольких слов, то его надо обрамить двойными кавычками.

    make составляет список имен переменных и присваивает им значения в процессе чтения программы. Если значение переменной задается в командной строке при запуске утилиты, то все ее определения в make-программе игнорируются и используется значение, взятое из командной строки. Рекурсия при присвоении значения переменным не допускается.

    Существуют переменные с предопределенными именами, значения которых устанавливаются при выполнении make-программы. К ним относятся: переменная с именем '@', ее значение - имя текущей цели; переменная с именем '?', принимает значение имен тех файлов-подцелей, которые "моложе" файла-цели; переменная '<' - имя текущей обрабатываемой подцели (используется в правилах, описывающих "неявные" зависимости (см. ниже)); '*' - имя текущей цели без расширения. Предопределенные переменные '@', '?' и '<' используются только в списке действий правила и в каждом правиле имеют свои значения. При ссылках на все перечисленные переменные, имена последних скобками не обрамляются.

    Ниже демонстрируется употребление предопределенных переменных:

CC	= cc
LIBS	= -lgen -lm
OBJS	= file1.o file2.o

prog: $(OBJS)
	$(CC) -o $@ $(OBJS) $(LIBS)

    Кроме описанных выше явных зависимостей между файлами, make позволяет определять так называемые "неявные" зависимости. Они "говорят", каким образом файлы с одним расширением создаются из файлов с другим расширением. Так, например, можно сообщить make, как из исходных ( .c ) файлов получить объектные модули ( .o ):

.o.c :
cc -c $<

    В приведенном примере make сравнивает времена модификации файлов, имеющих одинаковые имена и расширения, соответственно ".o" и ".c". Если o-файл "старше", то выполняется перекомпиляция. При этом в указанной в правиле команде перекомпиляции переменная '<', в соответствии с ее смыслом, заменяется на имя c-файла.

    Из примера виден общий синтаксис правил с "неявными" зависимостями:

<расширение1>. <расширение2> :
	[командная строка, задающая действие]
	.
	.
	[командная строка, задающая действие]

    Проверяется зависимость файлов с расширением 1 от файлов с расширением 2.

    Некоторые "неявные" зависимости определяются make по умолчанию. Таковой, в частности, является зависимость ".o" от ".c". Учитывая сказанное, приведенный в начале раздела пример можно переписать так:

prog: file1.o file2.o
	cc -o proj file1.o file2.o

    Здесь указывается лишь из каких объектных модулей и с помощью какой команды производится компоновка программы. Перекомпиляция же осуществляется согласно правилу с "неявной" зависимостью по умолчанию.

    В заключении отметим еще несколько особенностей программы make. Как правило, при выполнении действий соответствующие команды печатаются на экране. Запретить это можно, если поставить перед ней символ '@'.

    Если текущая выполняемая команда возвращает не нулевой код завершения, то make выводит сообщение об ошибке и останавливается. Чтобы этого не происходило, перед командой должен стоять символ '-'.

    В следующем комплексном примере показаны make-программы, используемые для сборки проекта, разбитого на поддиректории. Предполагается, что проект располагается в каталоге "proj". Последний имеет два подкаталога: "lib" с файлами, образующими библиотеку (пусть это "lib1.c", "lib2.c", а имя самой библиотеки "libproj.a") и "main" с файлами, образующими программу (пусть это "main1.c" и "main2.c", а имя самой программы "main").

    make-программа для сборки проекта.

# Makefile проекта

SUBDIRS = lib main

# Создание проекта 
all:
	@for i in $(SUBDIRS); \
	do \
		cd $$i ; \
		make ; \
		cd . . ; \
	done

# "Очистка" проекта - удаление порождаемых ".a" и ".o" файлов
clean:
	@for i in $(SUBDIRS); \
	do \
		cd $$i ; \
		make clean; \
		cd . . ; \
	done

    Она "пробегает" по подкаталогам проекта, вызывая make для выполнения находящихся там программ.

    make-программа для сборки библиотеки.

# Makefile для библиотеки. Располагается в директории 
# proj/lib.

OFILES = lib1.o lib2.o

# Создание библиотеки 

lib : $(OFILES)
	ar rv libproj.a $(OFILES)

# "Очистка" - удаление порождаемых ".a" и ".o" файлов 

clean : 
	rm -rf *.o *.a

    make-программа для сборки программы.

# Makefile для создания программы. Располагается в директории 
# proj/main

LIBDIR = . ./lib
LIB	= proj
OFILES = main1.o main2.o
PROG	= main

# Создание программы
prog : $(OFILES)
	cc -o $(PROG) $(LIBDIR) $(OFILES) -l$(LIB)

#"Очистка" - удаление порождаемых ".o" файлов и программы
clean :
	rm -rf *.o $(PROG)

    В приведенных make-программах файлы ".o" порождаются по соответствующим ".c" файлам в соответствии с "неявными" зависимостями, принятыми make по умолчанию.

    Описанные характеристики make являются общими для большинства версий UNIX. Но отдельные ее диалекты могут иметь дополнительные свойства. Так, make фирмы SunSoft (OC Solaris 2.x) имеет специальную цель .KEEP_STATE. Если она встречается в make-программе, то утилита автоматически создает список зависимостей исходных файлов ( .c ) от файлов-заголовков ( .h ).


1.9.6. Система контроля исходного кода.

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

    Для такого сопровождения проектов, в UNIX разработано несколько систем. Первой из них, по-видимому, является "Система контроля исходного кода (Source Code Control System - SCCS). Но наиболее распространенным и общепринятым в настоящее время является комплекс "Система контроля версий" (Revision Control System - RCS). О ней и пойдет речь в данном разделе.

    Система RCS обеспечивает запись, поиск и хранение версий текстовых файлов. Это позволяет вести учет изменений и контролировать права доступа к ним при одновременной работе нескольких пользователей. Система применяется для объектов, которые часто редактируются, например , варианты статей, писем, текстов программ и т.п.

    Система RCS обеспечивает возможности, перечисленные ниже.

  1. Ведение для файлов учета всех изменений, которые собираются и заносятся автоматически. Для каждой версии указывается автор, дата и время записи, и краткое описание сути внесенных изменений. Наличие этой информации позволяет проследить историю развития файла, не требуя утомительной операции сравнения листингов различных его модификаций.
  2. Хранение, поиск и выдачу версий файла. RCS сохраняет все версии достаточно экономичным (с точки зрения использования дисковой памяти) способом. Различные варианты объекта, находящиеся под контролем RCS, образуют как бы дерево. Начальная модификация файла получает номер 1.1, следующая 1.2 и т.д. Если необходимо, пользователь может явно задать номер и другие атрибуты модификации, вносимой в RCS (см. ниже). Так, можно из версии 1.2 начать две разных ветви 1.2.1.1 и 1.2.2.1. На рис. 1.1 приведен пример истории развития версий некоторого объекта.

pict-1-1.gif (3542 bytes)

Рис. 1.1. Дерево версий файла, находящегося под контролем RCS.

  1. Версии могут получать символьные имена и иметь статус: экспериментальная, стабильная и т.д. Это обеспечивает достаточно простой способ описания необходимой конфигурации собираемых модификаций. Когда из RCS необходимо "достать" какую-либо из модификаций объекта, то для ее указания можно использовать номер, символьное имя, дату создания, имя автора или статус.
  2. Решение проблем, возникающих при попытке одновременного редактирования одной и той же версии файла несколькими пользователями. Если объект изменяется в настоящее время одним лицом, то попытки других получить к файлу доступ блокируются.
  3. Слияние версий. Две разных ветви развития файла можно слить в одну. При этом система проверяет возможное пересечение редакций и, если такая ситуация обнаружена, сообщает об этом пользователю.
  4. Автоматическое указание при "выписывании" файла из RCS для каждой версии имени, номера, времени создания, автора и т.д. Эта информация может заноситься системой в любое место файла, указанное пользователем.

    Рассмотрим основные команды, которые необходимы для использования системы RCS. Для начала работы с системой достаточно знать всего две из них: ci и co. Первая помещает содержимое текстового файла под контроль RCS, а, если он там уже есть, то образует новую версию. Команда co находит и выдает указанную версию, хранящуюся в системе.

    Рассмотрим использование этих команд на следующем примере. Предположим, что имеется файл "f.c", который требуется передать под контроль RCS. В результате выполнения команды:

ci f.c

    в текущей директории будет создан файл "f.c,v", содержимое файла "f.c" переписано в "f.c,v" в качестве исходной версии 1.1, а сам файл "f.c" уничтожен. Кроме того, при выполнении команды, система потребует ввести краткое описание образованной версии. Все последующие вызовы программы ci будут запрашивать краткий комментарий, который должен отражать суть внесенных изменений (заметим, что если в текущем каталоге есть поддиректория с именем RCS, то файл "f.c,v" будет помещен в него) Файлы с суффиксами ",v" называются файлами RCS.

    Для получения рабочей версии файла "f.c" из предыдущего примера, необходимо вызвать команду:

co f.c

    Она найдет последнюю версию в "f.c,v" и запишет ее в файл "f.c". Можно отредактировать данный файл (его мы будем называть "рабочим") и попытаться занести новую версию в "f.c,v":

ci f.c

    Номер новой модификации будет образован из номера предыдущей версии, увеличенного на единицу. При попытке записи может возникнуть сообщение:

ci error: no lock set by <имя>

    Это означает, что во время создания файла RCS был включен режим блокировки при порождении новых версий. В этом случае при выполнение операции чтения из файла RCS, необходимо сообщить системе, что предполагается создание новой версии файла и требуется запретить попытки параллельного его редактирования до окончания работы над версией. В данном случае, в команде чтения файла должен указываться признак блокировки (ключ -l):

co -l f.c

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

    Существует еще одна возможность зарезервировать за собой право создания очередной версии. Это команда:

rcs -l f.c

    Она эквивалентна "co -l f.c', но "рабочий" файл "f.c" не создается. Команда бывает полезна, если при выписывании файла ключ "-l" не был указан (например, по невнимательности). Если в момент выполнения инструкции, файл уже был захвачен кем-либо, будет выдано сообщение о невозможности блокировки. В этом случае единственно, что можно сделать, это попытаться как-нибудь договориться с соответствующим пользователем, не рассчитывая на мудрость системы RCS.

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

rcs -U f.c

    При выключенной блокировке владельцу файла нет необходимости указывать, что создание "рабочего" файла может привести к появлению новой версии (хотя для остальных пользователей это действие является обязательным).

    Включение режима блокировки выполняется с помощью команды:

rcs -L f.c

    Как правило, создание новой версии связано с получением "рабочего" файла, его редактирования и записи новой версии с помощью команды ci, которая уничтожает "рабочий" файл. При использовании ключа -l команда:

co -l f.c

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

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

    Использование обоих ключей приводит к модификации информационных записей в файле (см. ниже).

    Пользователь может помещать в "рабочий" файл специальные строки (маркеры), которые при "выписывании" его, заменяются системой RCS на справочные сообщения. Так, если в файл поместить последовательность символов:

$Header$

    то при получении версии из RCS, в "рабочем" файле она будет заменена на строку вида:

$Header: файл версия дата время автор $

    В процессе работы не возникает необходимости редактирования этой строки, так как система автоматически проводит коррекцию при изменении даты обновления, идентификатора пользователя (автора), номера версии и т.п. Как правило, маркеры размещаются в строках комментария. Если необходимо, чтобы служебная информация попала и в объектный модуль, достаточно завести в исходном тексте программы соответствующую переменную. Например:

static char rcsid[ ] = "$Header$";

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

Полный список маркеров, используемых системой RCS, приведен ниже.

$Author$
Идентификатор пользователя, записавшего данную версию.
$Date$
Дата и время записи версии.
$Header$
Стандартный заголовок, содержащий имя файла RCS, номер версии, дату записи, идентификатор пользователя и статус версии.
$Locker$
Идентификатор пользователя, зарезервировавшего за собой право записи модифицированной версии. Если версия не блокирована, выдается строка нулевой длины .
$Log$
Комментарий, записанный пользователем при занесении версии в RCS. Перед ним помещается строка, содержащая: имя исходного файла RCS, номер версии, идентификатор пользователя и дата записи.
$Revision$
Номер версии.
$Source$
Полное имя исходного файла RCS.
$State$
Статус версии, присвоенный командой ci.

    В заключении приведем более детальное описание команд co и ci. Формат командной строки для co выглядит следующим образом:

co [options] file . . .

    Параметр file командной строки задает имя "рабочего" или файла RCS. Параметр options служит для указания ключей, определяющих режим работы команды.

    При задании имен файлов в команде co возможны три случая, указанные ниже.

  1. Заданы как файл RCS, так и "рабочий" файл. Имя первого имеет вид: "path1/workfile,v", а имя второго задано в форме "path2/workfile", где "path1/" и "path2/" являются полными именами директорий, а "workfile" - имя самого файла.
  2. Задан только файл RCS. Тогда предполагается, что "рабочий" файл находится в текущей директории, а его имя получается из имени файла RCS удалением "path1/" и суффикса ",v".
  3. Задан только "рабочий" файл. В этом случае имя файла RCS получается из имени "рабочего" файла удалением "path2/" и добавкой суффикса ",v".

    Если полное имя файла RCS не задано или приведено не полностью (т.е. не указано точное положение файла в файловой системе), команда пытается найти его в директории, заданной переменной среды RCS. Если она не определена, то файл ищется в директории "./RCS" и, если такая отсутствует, или в ней нет требуемого файла, то поиск осуществляется в текущей директории.

    Выбор версии файла может производится по номеру, времени создания, автору или статусу. При отсутствии указаний о том, какую версию файла требуется получить, выдается последняя версия. Если задано несколько условий поиска, выбирается "старшая" из множества подходящих версий. Указание даты, автора или статуса может использоваться при поиске необходимой версии какой-либо ветви дерева. При этом берется либо указанная ветвь, либо, если она не задана, самая "старшая" ветвь во всем дереве версий. Имя или номер искомой модификации можно указывать при задании ключей: "-l", "-p", "-q" и "-r" (см. ниже).

    Список ключей команды co и правила их использования следующие.

-l [rev]
Зарезервировать право модификации данной версии только за текущим пользователем. Правила задания номера или имени версии приведены при описании ключа "-r"
-p [rev]
Выдавать найденную версию в стандартный файл вывода. "Рабочий" файл не создается.
-q [rev]
Неинтерактивный режим. Диагностические сообщения на дисплей не выдаются.
-ddate
Выдать версию; дата создания задана параметром date. Если искомая версия отсутствует, выдается "старшая" версия из созданных до указанной даты. Дата может задаваться в любом формате, допустимом в системе UNIX.
-r [rev]
Получить версию с номером rev. Если таковая отсутствует, выдается "старшая" из множества версий с меньшими номерами. Номер версии состоит из последовательности полей, разделенных символом '.' (точка). Каждое поле содержит либо номер, либо символьное имя, которое может присваиваться пользователем с помощью ключа "-n" в команде ci.
-sstate
Выдать "старшую" версию, имеющую статус state. Статус файла задается при записи файла в архив командой ci (ключ "-s").
-w [login]
Выдать "старшую" версию, записанную пользователем с именем login. Если имя пользователя опущено, поиск осуществляется среди версий, записанных пользователем, выполнившим команду co.

    Команда ci, как было уже сказано выше, записывает новые версии "рабочих" файлов в файлы системы RCS. Формат командной строки ci следующий:

ci [ options ] file . . .

    Указываемые параметры и правила поиска "рабочих" и файлов RCS те же, что и в командной строке co.

    Для работы с командой ci может быть необходимо включить в список доступа к соответствующему файлу регистрационное имя пользователя. Эта операция выполняется командой rcs ( ключ "-a"). Она производится, если список доступа пуст, пользователь не является администратором системы (суперпользователем) и не является владельцем файла.

    Для того чтобы добавить новую версию, "старшая" из них должна быть заблокирована пользователем. В противном случае может создаваться только новая ветвь версий. Это ограничение не распространяется на владельца файла. Блокировка, установленная кем-то другим, может быть снята, как было сказано ранее, с помощью команды "rcs -l"'.

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

    При попытке записать версию, команда ci требует ввести краткий комментарии, который должен отражать суть внесенных изменений и заканчиваться либо отдельно стоящей '.' либо нажатием <Ctrl+D>. Если файл RCS не существует, ci создает его (по умолчанию этой версии присваивается номер 1.1). При этом заводится пустой список доступа.

    При записи файлов в архив системы RCS используются ключи, перечисленные ниже.

-r [rev]
Присвоить записанной версии файла номер rev, снять блокировку, уничтожить "рабочий" файл.
-f [rev]
Принудительная запись. Новая версия записывается без возможных дополнительных сообщений.
-l [rev]
Выполняет функции, аналогичные функциям ключа "-r", но для сохраняемой версии дополнительно исполняется команда co с ключом "-l".
-q [rev]
Неинтерактивный режим. Диагностических сообщений не выдается .
-mmsg
Использует строку msg в качестве комментария для всех заносимых версий.
-sstate
Устанавливает статус записываемой версии state. По умолчанию версия получает статус "Exp" (экспериментальная).

    Команда rcs используется для создания новых файлов RCS или замены атрибутов существующих файлов. Каждый файл RCS содержит: версии "рабочего" файла, список доступа, сообщения об изменениях, комментарии и набор атрибутов.

    Для использования команды rcs, как и в случае ci, может быть необходимо включить в список доступа соответствующего файла регистрационное имя пользователя.

    Формат командной строки команды rcs:

rcs [ options ] file . . .

    В качестве входных файлов используются файлы RCS. Ключи команды rcs перечислены ниже.

-i
Создать и инициализировать пустой файл RCS. Если не задано полное имя файла RCS, система пытается разместить его в директории "./RCS". Если она отсутствует, файл создается в текущей директории. Если указанный файл RCS уже существует, выдается сообщение об ошибке.
-alogins
Добавить указанные идентификаторы пользователей (logins) в список доступа архивного файла. В качестве разделителя в строке logins используется запятая.
-e [logins]
Исключить имена пользователей из списка доступа заданного архивного файла. Если имена пользователей не указаны, из списка доступа исключаются все имена.
-l [rev]
Блокировать версию rev. Если номер версии (rev) не указан, подразумевается "старшая" версия.
-L
Включить механизм блокировки. При работе в данном режиме владелец файла RCS не освобождается от обязанности блокировать версию для последующей записи ее модификаций. Этот режим необходим, если изменением файла занимается несколько пользователей.
-U
Выключить механизм блокировки. В данном режиме владелец файла может записывать новые версии без предварительного блокирования.

1.10. Проблемы переносимости программного обеспечения.

    В заключении рассмотрим вопрос о стандартизации операционной системы UNIX и о проблемах переносимости прикладного программного обеспечения между различными ее диалектами. Для этого вернемся к истории развития системы UNIX.

    В начале 80-х годов фирма AT&T утратила монополию на разработку и развитие ОС. Работы по созданию своей версии системы начались в Калифорнийском университете в городе Беркли. Диалекты UNIX, разработанные там, получили название BSD 4.* (Berkley Software Distribution версии 4.*). Свою версию системы UNIX для IBM PC разработала и фирма Microsoft. Она получила название Microsoft Xenix. Существуют и другие варианты ОС. Каждый из них обладает своими достоинствами, но их общим недостатком стала проблема переносимости программ из одной системы в другую. Чтобы исправить положение, фирмой AT&T, при поддержке разработчиков других версий системы UNIX, была создана версия, включающая в себя особенности всех созданных UNIX-систем. Она получила название UNIX System V Release 4.

    В то же время разработан ряд стандартов для операционных систем типа UNIX. Рассмотрим из них три основных.

    Первый касается языка программирования Си, который является основным инструментом создания программного обеспечения в среде UNIX. Стандартизация коснулась не только синтаксиса языка. Был определен набор основных функций и интерфейсов к ним. Стандарт был разработан в 1988 г. Американским Национальным Институтом Стандартов и получил название ANSI C. ANSI C получил широкое распространение. Почти все Си-компиляторы, разработанные с того времени, в той или иной степени соответствуют ему.

    Следующие два стандарта касаются более детального определения среды программирования системы UNIX. Первый из них разработан международной инженерной ассоциацией IEEE Std 1003.1-1990 Portable Operating System Interface Part 1. В литературе этот стандарт более известен под сокращенным названием POSIX.1. Второй стандарт разработан группой фирм-разработчиков вычислительной техники и программного обеспечения. Он получил название X/Open Portability Guide 3. Сокращенно этот стандарт называется XPG 3. Оба стандарта подробно описывают все структуры и типы данных, константы и библиотеки функций, определяющие среду операционной системы (Operating System Environment), в которой осуществляется процесс программирования.

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

 

1

2

3

4

5

 
<ctype.h>

*

   

*

*

символьные типы
<dirent.h>  

*

*

*

*

формат директорий
<errno.h>

*

   

*

*

обработка ошибок
<fcntl.h>  

*

*

*

*

управление файлами
<float.h>

*

   

*

*

константы с "плавающей" запятой
<ftw.h>    

*

*

  дерево файловой системы
<grp.h>  

*

*

*

*

формат файла групп
<langinfo.h>    

*

*

  языковые константы
<limits.h>

*

   

*

*

ограничения реализации
<locate.h>

*

   

*

*

локальные категории
<math.h>

*

   

*

*

математические константы
<nl_types.h>    

*

*

  каталог сообщений
<pwd.h>  

*

*

*

*

формат файла паролей
<regex.h>    

*

*

*

регулярные выражения
<search.h>    

*

*

  таблицы поиска
<setjmp.h>

*

   

*

*

нелокальные переходы
<signal.h>

*

   

*

*

сигналы
<stdarg.h>

*

   

*

*

список аргументов
<stddef.h>

*

   

*

*

стандартные типы
<stdio.h>

*

   

*

*

форматный ввод-вывод
<stdlib.h>

*

   

*

*

стандартная библиотека
<string.h>

*

   

*

*

операции со строками
<tar.h>  

*

*

*

  формат архивов tar
<termios.h>  

*

 

*

*

терминальный ввод-вывод
<time.h>

*

   

*

*

время и дата
<ulimit.h>    

*

*

  ограничения пользователя
<unistd.h>  

*

*

*

*

символьные константы
<utime.h>  

*

*

*

*

время для файлов
<sys/ipc.h>    

*

*

*

взаимодействие процессов
<sys/msg.h>    

*

*

  очереди сообщений
<sys/sem.h>    

*

*

  семафоры
<sys/shm.h>    

*

*

*

разделяемая память
<sys/stat.h>  

*

*

*

*

статус файлов
<sys/times.h>  

*

*

*

*

время для процессов
<sys/types.h>  

*

*

*

*

типы данных
<sys/utsname.h>  

*

*

*

  имя системы
<sys/wait.h>  

*

*

*

*

управление процессами

Здесь:

    1. Стандарт ANSI C.
    2. Стандарт POSIX.1.
    3. Стандарт XPG3.
    4. Версия BSD 4.3.
    5. Версия UNIX System V Release 4.


Назад

Содержание

Вперед


2.1.1. Общее устройство X Window.

    Система X Window представляет совокупность программ и библиотек. "Сердцем" ее является отдельный UNIX-процесс, существующий на компьютере, к которому присоединен дисплей. Именно сервер знает особенности конкретной аппаратуры, знает, что надо предпринять, чтобы закрасить пиксел на экране, нарисовать линию или другой графический объект. Он также умеет воспринимать сигналы, приходящие от клавиатуры и мыши.

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

    Состав пакетов и их последовательность определяются специальным протоколом. Его описание выходит за рамки настоящего издания и может быть найдено в документации по X Window системе [15].

    Но чтобы программировать для X, совсем не обязательно знать детали реализации сервера и протокола обмена. Система предоставляет библиотеку процедур, с помощью которых программы осуществляют доступ к услугам X "на высоком уровне". Так для того, чтобы вывести на экран точку, достаточно вызвать процедуру XDrawPoint(), передав ей соответствующие параметры. Последняя выполняет всю черновую работу по подготовке и передачи пакетов данных серверу. Упомянутая библиотека называется "Xlib". Она помещается в файле "lX11.a", который, как правило, находится в каталоге "/usr/lib". Прототипы функций библиотеки, используемые ею структуры данных, типы и прочее определяется в файлах-заголовках из директории "/usr/include/X11".

    На рисунке 2.1 представлена схема общения клиентов и сервера.

pict-2-1.gif (6371 bytes)

Рис.2.1. Общая схема общения программ-клиентов и X-сервера.

    Посылка порций данных, особенно если она осуществляется через сеть, операция достаточно медленная. Чтобы повысить производительность системы, Xlib не отправляет пакеты сразу, а буферизует их в памяти машины, на которой выполняется программа-клиент. Собственно передача выполняется в тот момент, когда клиент вызывает процедуру, ожидающую получения событий от сервера, например XNextEvent(). Программа может явно инициировать отправку пакетов, обратившись к функциям XFlush() или Xsync().


Назад

Содержание

Вперед


2.1.2. X окно.

    Как уже упоминалось ранее, окно - это базовое понятие в X. Оно представляет прямоугольную область на экране, предоставляемую системой программе-клиенту. Последняя использует окно для вывода графической информации. На рисунке 2.2 показан общий вид окна в X Window.

pict-2-2.gif (4636 bytes)

Рис. 2.2. Общий вид окна X Window.

    Из рисунка видно, что окно имеет внутренность и край. Основными атрибутами окна являются ширина и высота внутренности, а также ширина края. Далее мы будем говорить ширина и высота, а слово "внутренность" станем опускать. Упомянутые параметры окна называются его геометрией.

    С каждым окном связывается система координат. Ее начало находится в левом верхнем углу окна. Ось x направлена вправо, а ось y - вниз. Единица измерения по обеим осям - пиксел.

    Окна могут быть двух типов: InputOutput (для ввода - вывода) и InputOnly (только для ввода). Окно первого типа - это обычное окно. Окно второго типа не может использоваться для рисования. У данного окна нет края, оно "прозрачно". Заметим, что окна этого типа используются достаточно редко.

    X Window позволяет программе создавать несколько окон одновременно. Они связаны в иерархию, в которой одни являются "родителями", а другие "потомками". Сам сервер на каждом экране создает одно основное окно, которое является самым верхним "родителем" всех остальных окон. Это окно мы будем называть главным или "корневым".

    Для получения информации о любом окне X существует утилита xwininfo.


Назад

Содержание

Вперед


2.1.3. Управление окнами.

    Окна могут располагаться на экране произвольным образом, перекрывая друг друга. X имеет набор средств, пользуясь которыми программа-клиент может изменять размеры окон и их положение на экране. Особенностью системы является то, что она не имеет встроенной возможности управлять окнами с помощью клавиатуры или мыши. Чтобы это можно было осуществить, нужен специальный клиент, который называется "менеджер окон" (Window manager). Стандартный дистрибутив X содержит такую программу - twm. Возможности этого менеджера ограничены, но, тем не менее, он позволяет осуществлять базовые действия: передвигать окна с помощью мыши, изменять их размер и т.д. Наиболее же развитым оконным менеджером является, по всей видимости, программа mwm (Motif Window Manager), которая поставляется в рамках системы OSF/Motif. Сама эта система описывается в главе 4 настоящей книги.

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


Назад

Содержание

Вперед


2.1.4. Графические возможности X Window.

    Система X Window предназначена для работы на растровых дисплеях. В подобного рода устройствах изображение представляется матрицей светящихся точек - пикселов. Каждый пиксел кодируется определенным числом бит (как правило 2, 4, 8, 16 или 24). Число бит-на-пиксел называют "толщиной" или "глубиной" (deep) дисплея. Биты с одинаковыми номерами во всех пикселах образуют как бы плоскость, параллельную экрану. Ее называют "цветовой плоскостью". X позволяет рисовать в любой цветовой плоскости (или плоскостях), не затрагивая остальные.

    Значение пиксела не задает непосредственно цвет точки на экране. Последний определяется с помощью специального массива данных, называемого палитрой. Цвет есть содержимое ячейки палитры, номер которой равен значению пиксела.

    X имеет большой набор процедур, позволяющих рисовать графические примитивы - точки, линии, дуги, выводить текст и работать с областями произвольной формы.


Назад

Содержание

Вперед


2.1.5. "Свойства" и атомы.

    В X Window встроены средства для обеспечения обмена информацией между программами-клиентами. Для этого используется механизм "свойств" (properties). "Свойство" - это порция данных, связанная с некоторым объектом (например, окном), и которая доступна всем клиентам X.

    Каждое "свойство" имеет имя и уникальный идентификатор - атом (atom). Обычно имена "свойств" записываются большими буквами, например: "MY_SPECIAL_PROPERTY". Атомы используются для доступа к содержимому "свойств" с тем, чтобы уменьшить количество информации, пересылаемой по сети между клиентами и X сервером.

    В X предусмотрен набор процедур, позволяющих перевести имя "свойства" в уникальный атом, и, наоборот, по атому получить необходимые данные.

    Некоторые "свойства" и соответствующие им атомы являются предопределенными и создаются в момент инициализации сервера. Этим атомам соответствуют символические константы, определенные в файлах-заголовках библиотеки Xlib. Эти константы начинаются с префикса "XA_". Детально механизм "свойств" и атомов описан в п. 2.5.1.

    Посмотреть текущий список атомов можно с помощью утилиты xlsatoms.


Назад

Содержание

Вперед


2.1.6. Первый пример.

    Продолжая традиции многих изданий, посвященных программированию на С, мы начинаем с программы, рисующей на экране строку "Hello, world!"'. В этом примере приведены основные шаги, необходимые для работы в X Window. ( xhello.tgz)

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xos.h>
#include <stdio.h>
#include <string.h>

#define WND_X 0
#define WND_Y 0
#define WND_WDT 100
#define WND_HGH 100
#define WND_MIN_WDT 50
#define WND_MIN_HGH 50
#define WND_BORDER_WDT 5

#define WND_TITLE "Hello!"
#define WND_ICON_TITLE "Hello!"
#define PRG_CLASS "Hello!"

/*
* SetWindowManagerHints - процедура передает информацию о
* свойствах программы менеджеру окон. 
*/

static void SetWindowManagerHints ( 
  Display *   prDisplay,  /*Указатель на структуру Display */
  char *      psPrgClass, /*Класс программы */
  char *      argv[],     /*Аргументы программы */
  int         argc,       /*Число аргументов */
  Window      nWnd,       /*Идентификатор окна */
  int         x,          /*Координаты левого верхнего */
  int         y,          /*угла окна */
  int         nWidth,
  int         nHeight,    /*Ширина и высота окна */
  int         nMinWidth,
  int         nMinHeight, /*Минимальные ширина и высота окна */
  char *      psTitle,    /*Заголовок окна */
  char *      psIconTitle,/*Заголовок пиктограммы окна */
  Pixmap      nIconPixmap /*Рисунок пиктограммы */
)
{
  XSizeHints rSizeHints; /*Рекомендации о размерах окна*/

#ifdef X11R3 /*. X11R3 и ниже */
  rSizeHints.flags = PPosition | PSize | PMinSize;
  rSizeHints.x = x;
  rSizeHints.y = y;
  rSizeHints.width = nWidth;
  rSizeHints.height = nHeight;
  rSizeHints.min_width = nMinWidth;
  rSizeHints.min_height = nMinHeight;

  XSetStandardProperties ( prDisplay, nWnd, psTitle,
      psIconTitle, nIconPixmap, argv, argc, &rSizeHints );
#else /* X11R4 и выше */
  XWMHints rWMHints;
  XClassHint rClassHint;
  XTextProperty prWindowName, prIconName;

  if ( !XStringListToTextProperty (&psTitle, 1, &prWindowName ) ||
       !XStringListToTextProperty (&psIconTitle, 1, &prIconName ) ) {
    puts ( "No memory!\n");
    exit ( 1 );
}

rSizeHints.flags = PPosition | PSize | PMinSize;
rSizeHints.min_width = nMinWidth;
rSizeHints.min_height = nMinHeight;
rWMHints.flags = StateHint | IconPixmapHint |
                 InputHint;
rWMHints.initial_state = NormalState;
rWMHints.input = True;
rWMHints.icon_pixmap= nIconPixmap;

rClassHint.res_name = argv[0];
rClassHint.res_class = psPrgClass;

XSetWMProperties ( prDisplay, nWnd, &prWindowName,
    &prIconName, argv, argc, &rSizeHints, &rWMHints,
    &rClassHint );
#endif
}

/*
*main - основная процедура программы
*/

void main ( int argc, char * argv[] )
{
  Display *prDisplay;    /* Указатель на структуру Display */
  int nScreenNum;        /* Номер экрана */
  GC prGC;
  XEvent rEvent;
  Window nWnd;

  /* Устанавливаем связь с сервером */
  if ( ( prDisplay = XOpenDisplay ( NULL ) ) == NULL ) {
    puts ("Can not connect to the X server!\n");
    exit ( 1 );
  }

  /* Получаем номер основного экрана */
  nScreenNum = DefaultScreen ( prDisplay );

  /* Создаем окно */
  nWnd = XCreateSimpleWindow ( prDisplay,
         RootWindow ( prDisplay, nScreenNum ),
         WND_X, WND_Y, WND_WDT, WND_HGH, WND_BORDER_WDT,
         BlackPixel ( prDisplay, nScreenNum ),
         WhitePixel ( prDisplay, nScreenNum ) );

  /* Задаем рекомендации для менеджера окон */
  SetWindowManagerHints ( prDisplay, PRG_CLASS, argv, argc,
      nWnd, WND_X, WND_Y, WND_WDT, WND_HGH, WND_MIN_WDT,
      WND_MIN_HGH, WND_TITLE, WND_ICON_TITLE, 0 );

  /* Выбираем события, обрабатываемые программой */
  XSelectInput ( prDisplay, nWnd, ExposureMask | KeyPressMask );

  /* Показываем окно */
  XMapWindow ( prDisplay, nWnd );

  /* Цикл получения и обработки ошибок */
  while ( 1 ) {
    XNextEvent ( prDisplay, &rEvent );

    switch ( rEvent.type ) {
       case Expose :
         /* Запрос на перерисовку */
         if ( rEvent.xexpose.count != 0 )
           break;

         prGC = XCreateGC ( prDisplay, nWnd, 0 , NULL );

         XSetForeground ( prDisplay, prGC,
              BlackPixel ( prDisplay, 0) );
         XDrawString ( prDisplay, nWnd, prGC, 10, 50,
              "Hello, world!", strlen ( "Hello, world!" ) );
         XFreeGC ( prDisplay, prGC );
         break;

       case KeyPress :
         /* Нажатие клавиши клавиатуры */
         XCloseDisplay ( prDisplay );
         exit ( 0 );
    }
  }
}

    Для сборки программы используется команда:

cc -o hello hello.o -lX11

    Программа использует ряд функций, предоставляемых библиотекой Xlib: XOpenDisplay( ), XCreateSimpleWindow( ) и др. Их прототипы, стандартные структуры данных, макросы и константы описаны в следующих основных файлах-заголовках: "Xlib.h", "Xutil.h", "Xos.h". Эти и другие файлы поставляются вместе с X Window.

    Для обозначения переменных в книге принята нотация, пришедшая из Microsoft Windows. Идентификатор начинается с префикса, описывающего тип переменной, за которым следует ее имя. Ниже перечислены наиболее распространенные префиксы.

c - символ (байт) ,
n - байт, целое,
s - строка,
p - указатель,
r - структура.

    Перейдем к рассмотрению самой программы. Она начинается установлением связи с Х-сервером. Делает это функция XOpenDisplay( ). Ее аргумент определяет сервер, с которым надо связаться. Если в качестве параметра XOpenDisplay( ) получает NULL, то она открывает доступ к серверу, который задается переменной среды (environment) DISPLAY. И значение этой переменной и значение параметра функции имеют следующий формат: host:server.screen, где host - имя компьютера, на котором выполняется сервер, server - номер сервера (обычно это 0), а screen - это номер экрана. Например, запись kiev:0.0 задает компьютер - "kiev", а в качестве номера сервера и экрана используется 0. Заметим, что номер экрана указывать не обязательно.

    Процедура XOpenDisplay() возвращает указатель на структуру типа DISPLAY. Это большой набор данных, содержащий информацию о сервере и экранах. Указатель следует запомнить, т.к. он используется в качестве параметра во многих процедурах Xlib.

    После того, как связь с сервером установлена, программа "Hello" определяет номер экрана. Для этого используется макрос DefaultScreen(), возвращающий номер основного экрана. Переменная nScreenNum может иметь значение от 0 до величины (ScreenCount (prDisplay ) - 1). Макрос ScreenCoun() позволяет получить число экранов, обслуживаемых сервером.

    Следующий шаг - создание окна и показ его на дисплее. Для этого программа обращается к процедуре  XCreateWindow() или XCreateSimpleWindow(). Для простоты мы используем вторую процедуру, параметры которой задают характеристики окна.

PrWind = XCreateSimpleWindow (
            prDisplay, /* указатель на структуру Display,
                          описывающую сервер */
            RootWindow (prDisplay, nScreenNum),
                        /* родительское окно, в данном случае,
                           это основное окно программы */
            WND_X, WND_Y,
                        /* начальные x и y координаты верхнего
                           левого угла окна программы */
            WND_WIDTH, WND_HEIGHT,
                        /* ширина окна и высота окна */
            WND_BORDER_WIDTH, /* ширина края окна */
            BlackPixel ( prDisplay, nScreenNum ),
                       /* цвет переднего плана окна */
            WhitePixel ( prDisplay, nScreenNum )
                       /* цвет фона окна */
         );

    Для задания цветов окна используются макросы BlackPixel() и WhitePixel(). Они возвращают значения пикселов, которые считаются на данном дисплее и экране соответствующими "черному" и "белому" цветам. Функция XCreateSimpleWindow() ( XCreateWindow() ) возвращает значение типа Window. Это целое число, идентифицирующее созданное окно.

    Среди параметров функций, создающих окна, есть те, которые определяют положение окна и его размеры. Эти аргументы принимаются во внимание системой X Window. Исключение составляет случай, когда родительским для создаваемого окна является "корневое" окно экрана. В этом случае решение о положение окна и его размерах принимает менеджер окон. Программа может пытаться повлиять на решение менеджера окон, сообщив ему свои "пожелания" с помощью функций XSetStandardProperties() и XSetWMHints() (для X версии 11.3 и ниже) или XSetWMProperties() (для X версии 11.4 и выше).

    Из листинга 2.1 видно, что программа может сообщить менеджеру следующие параметры:

    Имя окна и имя пиктограммы в X11R3 и ниже передаются как строки, через параметры функции XSetStandardProperties(). В X11R4 и выше строки должны быть в начале преобразованы в "текстовые свойства", описываемые структурами типа XTextProperty. Это выполняется процедурой XStringListToTextProperty().

    Для передачи информации о желаемой геометрии окна используется структура XSizeHints().

    X Window версии 11.4 (и выше) позволяет сообщить менеджеру также следующее:

    После того, как "рекомендации" менеджеру окон переданы, программа выбирает события, на которые она будет реагировать. Для этого вызывается функция XSelectInput(). Ее последний аргумент есть комбинация битовых масок (флагов). В нашем случае - это ExposureMask | KeyPressMask. ExposureMask сообщает X Window, что программа обрабатывает событие Expose. Оно посылается сервером каждый раз, когда окно должно быть перерисовано. KeyPressMask выбирает событие KeyPress - нажатие клавиши клавиатуры. Типы событий и соответствующие им маски и структуры данных описаны в приложении 1.

    Теперь окно программы создано, но не показано на экране. Чтобы это произошло, надо вызвать процедуру XMapWindow(). Заметим, что из-за буферизации событий библиотекой Xlib, окно не будет реально нарисовано, пока программа не обратится к процедуре получения сообщений от сервера ( XNextEvent() ).

    Программы для X построены по принципу управляемости событиями. Поэтому, после того, как окно создано, заданы необходимые параметры для менеджера окон, основная ее работа - это получать сообщения от сервера и откликаться на них. Выполняется это в бесконечном цикле. Очередное событие "вынимается" процедурой XNextEvent(). Само оно есть переменная типа XEvent, который представляет собой объединение (union) структур. Каждое событие (Expose, KeyPress и т.д.) имеет свои данные (и, следовательно, свое поле в объединении XEvent) Более подробно они описаны в приложении 1.

    При получении сообщения Expose, программа перерисовывает окно. Действия начинаются созданием графического контекста - структуры, которая содержит данные, необходимые для вывода информации, в нашем случае - текста:

prGC = XCreateGC (prDisplay, prWnd, 0, NULL);

    После этого рисуется строка "Hello, world!". Более графический контекст не нужен - он уничтожается:

XFreeGC (prDisplay, prGC);

    Окно может получить несколько событий Expose одновременно. Чтобы не перерисовывать себя многократно, программа дожидается прихода последнего из них и только потом осуществляет вывод.

    Приход события KeyPress означает, что программу надо завершить: прекратить связь с сервером

XCloseDisplay (prDisplay);

    и вызвать функцию exit().


Назад

Содержание

Вперед


2.1.7. События.

    Когда пользователь нажимает на кнопку мыши или клавишу клавиатуры, или когда окно программы нуждается в перерисовке, или когда происходят другие изменения в системе, сервер подготавливает соответствующий пакет данных и отправляет его той или иной программе (или программам). Этот пакет данных называется событием.

    Возможных событий достаточно много. Каждое из них имеет свой тип и соответствующую структуру данных. Все они вместе, как было сказано выше, описываются объединением (union) XEvent.

    Как мы видели из примера в предыдущем пункте, программа для каждого из своих окон может выбрать события, которые будут ему передаваться. Делается это с помощью функции XSelectInput(). При вызове этой процедуры требуемые события идентифицируются соответствующими флагами. Так событию ButtonPress (нажатие кнопки мыши) соответствует флаг ButtonPressMask. Когда кнопка отпускается, сервер порождает событие ButtonRelease, которому соответствует флаг - ButtonReleaseMask.

    Некоторые события посылаются окну независимо от того, выбраны они или нет. Это:

MappingNotify - посылается, когда изменяется состояние клавиатуры (соответствие физических и логических кодов (см. п. 2.3.1.2.);

ClientMessage - так идентифицируются события, посылаемые от клиента к клиенту с помощью процедуры XSendEvent( );

SelectionClear, SelectionNotify, SelectionRequest - эти события используются в стандартном механизме общения между программами, работающими в X (описание этих событий выходит за рамки настоящего издания);

ConfigureExpose, NoExpose - эти события могут посылаться, когда клиент пытается копировать содержимое одного окна в другое.

    Программа получает события в своем основном цикле. Для этого можно использовать ряд процедур. Наиболее простая из них XNextEvent (Display *prDisplay, XEvent *prEvent). Она "вынимает" из очереди событие, находящееся в ее "голове", сохраняет информацию о нем в переменной, на которую указывает параметр prEvent, и возвращается. При этом само событие удаляется из очереди. Функция XPeekEvent() также возвращает переданное сервером событие, но не удаляет его из очереди.

    Процедура XPending() возвращает общее число событий в очереди программы.

    Итак, если событие выбрано для окна, то оно будет передано ему на обработку. А если нет? В этом случае событие передается "родителю" окна. Если и тот не желает "обращать внимание" на данное событие, то оно отправляется дальше, вверх по иерархии окон, и так до тех пор, пока либо не будет найдено окно, выбравшее это событие, либо событие не потеряется.

    Задача может влиять на этот процесс продвижения события по иерархии окон. Если программа включает флаг, соответствующий событию, в специальный атрибут окна, то оно, достигнув этого окна, не будет передано родителю, а будет тут же "снято с повестки дня". Этот атрибут - do_not_propagate (см. п. 2.1.8.).


Назад

Содержание

Вперед


2.1.8. Атрибуты окна.

    Окно в X Window достаточно сложное образование. Основные его характеристики перечислялись в п.2.1.2. Теперь мы поговорим о них более подробно.

    Многие атрибуты окна задаются при его создании с помощью процедуры XCreateWindow() (XCreateSimpleWindow()). В последствии параметры ножно изменить, обратившись к процедуре XChangeWindowAttributes().

    Характеристики окна описываются структурой типа XWindowAttributes. Получить их можно с помощью процедуры XGetWindowAttributes().

    Все они делятся на две группы. В первую входят параметры, доступные "на чтение" и "на запись". Вторая группа представляет собой внутренние данные. Программа может прочитать их, но не может менять.

    Сначала перечислим поля структуры XWindowAttributes, которые относятся к "изменяемым" параметрам.

    Фон окна, определяется атрибутами background_pixmap и background_pixel. Первый из них задает картинку (карту пикселов), которая используется для заливки фона окна. При необходимости картина повторяется слева - направо и сверху - вниз. Если параметр background_pixmap равен None (задается по умолчанию), то он игнорируется. Если же при этом поле background_pixel не задано (установлено по умолчанию), то окно считается "прозрачным", в противном случае его фон заливается цветом background_pixel. Атрибуты background_pixmap и background_pixel могут также принимать значение ParentRelative. В этом случае характеристики фона заимствуются у родительского окна.

    Вид края окна определяется полями border_pixmap и border_pixel. Первый атрибут определяет карту пикселов, используемую для заполнения края. Если он равен None, то край заполняется цветом border_pixel. Если же и поле border_pixel не задано, то для изображения края используются соответствующие характеристики "родителя". То же самое происходит, если параметр border_pixmap равен CopyFromParent (взять у "родителя"). Последнее значение есть значение по умолчанию.

    На перерисовку окна после изменения его размеров влияют атрибуты bit_gravity и win_gravity. Когда окно меняет размер, например, увеличивается или уменьшается, то, в принципе, нет необходимости перерисовывать все его содержимое. Часть окна остается неизменной. Правда, эта часть может поменять свое положение: переместиться вправо, влево, вверх или вниз. Поле bit_gravity "говорит" серверу, что делать с оставшейся частью изображения. Возможные значения параметра следующие:

ForgetGravity - содержимое окна перерисовывается (считается значением по умолчанию);

StaticGravity - остающаяся часть не должна менять положение по отношению к главному ("корневому" (root)) окну сервера;

NorthWestGravity - остающаяся часть смещается к левому верхнему углу;

NorthGravity - остающаяся часть смещается к верху окна;

NorthEastGravity - остающаяся часть смещается к правому верхнему углу;

WestGravity - остающаяся часть смещается к левому краю окна;

CenterGravity - остающаяся часть смещается к центру окна;

EastGravity - остающаяся часть смещается к правому краю окна;

SouthWestGravity - остающаяся часть смещается к левому нижнему углу;

SouthGravity - остающаяся часть смещается к нижнему краю окна;

SouthEastGravity - остающаяся часть смещается к правому нижнему углу.

    Параметр win_gravity "говорит" о том, что делать с подокнами окна после изменения размеров последнего. Возможные значения параметра следующие (при перечислении используются следующие обозначения: H - изменение размеров окна по горизонтали, V - изменение размеров по вертикали, (h, v) - смещение подокна на h пикселов по горизонтали и на v пикселов по вертикали):

UnmapGravity - подокна удаляются с экрана; окну посылается событие UnmapNotify, в ответ на которое оно может переместить свои подокна и показать их с помощью процедуры XMapSubWindow( );

StaticGravity - подокна остаются на месте по отношению к главному ("корневому") окну сервера;

NorthWestGravity - устанавливается по умолчанию; соответствует смещению (0, 0);

NorthGravity - смещение (H/2, 0);

NorthEastGravity - смещение (H, 0);

WestGravity - смещение (0, V/2);

CenterGravity - смещение (H/2, V/2);

EastGravity - смещение (H, V/2);

SouthWestGravity - смещение (0, V);

SouthGravity - смещение (H/2, V);

SouthEastGravity - смещение (H, V);

    Автоматическое сохранение содержимого окна, когда его часть перекрывается другими окнами, или, когда окно удаляется с экрана, определяется параметрами backing_store, backing_planes и backing_pixel. Сохраненные данные могут использоваться для восстановления окна, что значительно быстрее, чем его перерисовка программой в ответ на событие Expose. Параметр backing_store имеет следующие возможные значения:

NotUseful - (устанавливается по умолчанию) - серверу не рекомендуется сохранять содержимое окна;

WhenMapped - серверу рекомендуется спасти содержимое невидимых частей окна, когда окно показывается на экране;

Always - серверу рекомендуется сохранить содержимое окна даже, если оно не показано на экране.

    Сохранение изображений требует, как правило, довольно большого расхода памяти. Атрибуты backing_planes и backing_pixel призваны уменьшить этот расход. Первый из указанных параметров "говорит" серверу, какие плоскости изображения надо сохранять; backing_pixel означает, какой цвет использовать при восстановлении изображения в тех плоскостях, которые не сохранялись. По умолчанию backing_planes - маска, состоящая из единиц, а backing_pixel равно 0.

    Иногда при показе окна полезно спасти содержимое экрана под окном. Если окно невелико, и показывается не на долго, то это позволяет экономить время, которое надо будет затратить на перерисовку экрана после того, как окно будет закрыто. Если атрибут save_under равен True, то сервер будет пытаться сохранить изображение под окном. Если же он равен False (по умолчанию), то сервер ничего не предпринимает.

    Когда обрабатывает (или не обрабатывает) событие, последнее может быть передано его родительскому окну. Атрибут do_not_propagate_mask (по умолчанию 0) "говорит" и о том, какие события не должны доходить до "родителей".

    Изменение размеров окна и его положения на экране контролируется атрибутом override_redirect. Если он равен False, то размер окна и его положение меняются с помощью менеджера окон. Если же он равен True, то окно само решает, где ему быть, и какую ширину и высоту иметь.

    Цветовую гамму окна задает параметр colormap. Более подробная информация о цветах и цветовых палитрах приведена в 2.2.8. Значение по умолчанию - CopyFromPatent, которое "говорит", что окно использует палитру своего непосредственного "родителя".

    Теперь рассмотрим "неизменяемые" параметры окна. Строго говоря, атрибуты, о которых пойдет речь, нельзя назвать "неизменяемыми". Некоторые из них могут меняться сервером или менеджером окон. Но для обычных программ-клиентов они действительно являются таковыми.

    Положение окна и его размеры сообщают поля x, y, width и height. Они дают координаты левого верхнего угла, ширину и высоту окна соответственно. Координаты измеряются в пикселах по отношению к родительскому окну.

    Ширина края окна определяется параметром border_width.

    Маска, "говорящая" о том, какие события выбраны для передачи окну породившим его клиентом, содержится в поле флагов your_event_mask. Значение параметра образуется комбинацией флагов, идентифицирующих события (см. приложение 1.).

    Информация о дисплее, на котором показано окно, содержится в структуре Visual, на которую показывает поле visual. Эти данные, как правило, не обрабатываются обычными программами-клиентами (заметим, что для получения информации о дисплее, в системе предусмотрена процедура XGetVisualInfo( ) (см. 2.2.8.1.)).

    Класс окна сообщает поле class. Возможные значения: InputOutput и InputOnly.

    Число цветовых плоскостей дисплея (число бит-на-пиксел) помещается в поле depth.

    На информацию об экране, на котором помещается окно, указывает поле screen. Она, как правило, не используется обычными программами.

    Идентификатор главного ("корневого") окна экрана, на котором помещается окно, находится в поле root.

    Если окно имеет палитру, и она в настоящее время активна, то поле map_installed равно True, в противном случае - False.

    Видно в настоящее время окно на экране или нет, сообщает атрибут map_state.

    Маска всех событий, выбранных всеми программами для данного окна, содержится в атрибуте all_event_mask. Дело в том, что окно обрабатывается не только порождающим его клиентом, но, возможно, и другими приложениями, например, менеджером окон.

    Мы рассказали о том, как получить атрибуты окна, и что они означают. Теперь рассмотрим, как их изменить. Для этого можно использовать несколько процедур X Window, основной из которых является XChangeWindowAttributes( ), имеющая следующий прототип:

int XChangeWindowAttributes (Display *prDisplay, 
         Window nWnd, unsigned long nValueMask,
         XSetWindowAttributes *prWinAttr); 

    Требуемые установки атрибутов передаются через аргумент prWinAttr. Он указывает на переменную типа XSetWindowAttributes. Ее поля те же, что и соответствующие поля XWindowAttributes. Разница заключается лишь в разных именах некоторых из них. Так поле your_event_mask в XWindowAttributes соответствует полю event_mask в XSetWindowAttributes.

    Структура XSetWindowAttributes содержит дополнительное поле cursor. Оно определяет вид курсора мыши, когда последний находится в окне. Если поле равно None (значение по умолчанию), то используется курсор родительского окна, в противном случае значением параметра должен быть идентификатор того или иного курсора. Создание и установка курсоров подробно рассматривается в 2.3.2.2.

    Параметр nValueMask при вызове указанной процедуры представляет комбинацию флагов, "говорящих" о том, какие из полей переменной prWinAttr принимать во внимание. Поля и соответствующие им флаги перечислены в таблице 1.2.3. приложения 1.

    В следующем примере приведен фрагмент кода, в котором изменяются параметры border_pixmap и win_gravity некоторого окна:

.......
Display *prDisplay;
Window prWnd;
XSetWindowAttributes rWndAttr;
unsigned long nValMask;
Pixmap nPixmap;
.......
nValMask = CWBorderPixmap | CWWinGravity;
rWndAttr.border_pixmap = nPixmap;
rWndAttr.win_gravity = StaticSravity;
.......
XChangeWindowAttributes (prDisplay, prWnd, nValMask,
                &rWndAttr);
.......

    Отдельные атрибуты окна можно изменить более просто с помощью специальных процедур. Так функция XSetWindowBackground( ) меняет фон окна, XSetWindowBorder( ) - его край.


2.2.1. Графический контекст.

    Прежде чем начать работу с графикой, программа должна выделить себе специальную структуру данных и получить указатель на нее. Эта структура называется графическим контекстом (Graphic Context (GC)). Указатель на GC используется в качестве одного из параметров при вызове "рисующих" функций X. Графический контекст содержит ряд атрибутов, влияющих на изображение объектов: текста, линий, фигур и др. Выделенный GC должен быть освобожден до завершения работы программы.

    Графический контекст создается процедурой XCreateGC( ), имеющей следующий прототип:

GC XCreateGC (Display *prDisplay, Drawable nDrawable,
              unsigned long nValueMask, XGCValues *prValues);

    Первый аргумент - это указатель на структуру типа Display, который программа получает после вызова XOpenDisplay( ); второй - идентификатор окна (или карты пикселов), в котором программа будет рисовать; третий - битовая маска, определяющая, какие атрибуты GC задаются; последний аргумент - структура типа XGCValues, определяемая следующим образом:

typedef struct {
int	function;
unsigned long	plane_mask;
unsigned long	foreground;
unsigned long	background;
int	line_width;
int	line_style;
int	cap_style;
int	join_style;
int	fill_style;
int	fill_rule;
int	arc_mode;
Pixmap	tile;
Pixmap	stipple;
int	ts_x_origin;
int	ts_y_origin;
Font	font;
int	subwindow_mode;
Bool	graphics_exposures;
int	clip_x_origin;
int	clip_y_origin;
Pixmap	clip_mask;
int	dash_offset;
char	dashes;
} XGCValues;

 

    Значения полей данной структуры будут объяснены ниже. Каждому из них соответствует бит в маске, которая передается в качестве третьего параметра при вызове процедуры XCreateGC( ). Эти биты обозначаются символическими константами, определенными в файле "Xlib.h". Если бит установлен, то значение соответствующего атрибута должно быть взято из переданной XCreateGC( ) структуры XGCValues. Если бит сброшен, то атрибут приникает значение по умолчанию. Соответствие полей структуры XGCValues и флагов приведено в таблице 1.2.1. приложения 1.

    Следующий пример показывает процесс создания графического контекста, в котором устанавливаются два атрибута: цвет фона и цвет переднего плана.

 

. . . . . . .

GC prGC;

XGCValues rValues;

Display prDisplay;

int nScreenNum;

. . . . . . . .

rValues.foreground = BlackPixel (prDisplay, nScreenNum);

rValues.background = WhitePixel (prDisplay, nScreenNum);

. . . . . . . .

prGC = XCreateGC (prDisplay, RootWindow (prDisplay, nScreenNum),

(GCForeground | GCBackground), &rValues);

 

    Вызов XCreateGC( ) не единственный способ создания графического контекста. Так, например, новый контекст может быть получен из уже существующего GC с помощью XCopyGC( ).

    Когда контекст порожден, его атрибуты могут изменяться процедурой XChangeGC( ). Например:

 

rValues.line_width = 10;

XChangeGC (prDisplay, prGC, GCLineWidth, &rValues);

 

    Приведенный фрагмент кода меняет ширину линий, рисуемых с помощью графического контекста.

    Для того, чтобы получить значение полей GC, используется процедура XGetGCValues( ).


2.2.2. Характеристики графического контекста.

    В предыдущем разделе мы говорили, что GC имеет ряд атрибутов, воздействующих на вывод изображений. Для текста это цвет и шрифт, для линий - цвет и толщина и т.д. Как уже упоминалось выше, атрибуты контекста задаются в момент его создания. Потом они могут меняться с помощью функции XChangeGC( ). Кроме того, X поддерживает специальные функции для изменения параметров GC.

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

    Режим рисования (поле function в структуре XGCValues) указывает, каким образом комбинируются при рисовании цвет графики и цвет изображения, на которое накладывается графика. Данное поле задает некоторую логическую функцию. Возможные значения приведены в таблице 1.2.7. приложения 1.

    По умолчанию function равно GXcopy. Устанавливается режим рисования с помощью процедуры XSetFunction( ).

    Изменяемые цветовые плоскости. Каждый пиксел задается с помощью N бит. Биты с одним номером во всех пикселах образуют как-бы плоскости, идущие параллельно экрану. Получить число плоскостей для конкретного дисплея можно с помощью макроса DisplayPlanes( ). Поле plane_mask структуры графического контекста определяет, в каких плоскостях идет рисование при вызове функций X. Если бит поля установлен, то при рисовании соответствующая плоскость изменяется, в противном случае она не затрагивается.

    Цвет переднего плана и фона (поля foreground и background) задают цвета, используемые при рисовании линий текста и других графических элементов. Устанавливаются значения указанных полей функциями XSetForeground( ) и XSetBackground( ) соответственно.

    Атрибуты, влияющие на рисование линий. Шесть параметров определяют вид прямых, дуг и многоугольников, изображаемых с помощью X Window.

  1. Поле line_width задает толщину линии в пикселах. Нулевое значение поля соответствует тому, что линия должна быть толщиной в один пиксел и рисоваться с помощью наиболее быстрого алгоритма для данного устройства вывода.
  2. Поле line_style определяет тип линии. Возможные значения следующие:

    LineSolid - сплошная линия,

    LineOnOffDash - пунктирная линия; промежутки между штрихами не закрашиваются;

    LineDoubleDash - пунктирная линия; промежутки между штрихами закрашиваются цветом фона;

  1. Параметр cap_style определяет вид линии в крайних точках, если ее ширина больше 1 пиксела. На рисунке 2.3 приведены значения параметра и соответствующий вид конца линии.

pict2-3.gif (3920 bytes)

Рис. 2.3. Значения параметра cap_style графического контекста.

  1. Поле join_style определяет, как соединяются линии друг с другом. На рисунке 2.4 показаны соответствующие возможности. Параметр имеет смысл при толщине линии большей 1.

pict2-4.gif (3083 bytes)

Рис. 2.4. Значения параметра join_style графического контекста.

  1. Если линия пунктирная, то поле dashes дает длину пунктира и промежутков в пикселах.
  1. Параметр dash_offset указывает, с какого места начинать рисование 1-й черточки пунктирной линии. Рисунок 2.5 демонстрирует, как значения полей dashes и dash_offset влияют на вид линии.

Рис. 2.5. Вид линии при различных значениях параметров dashes и dash_offset графического контекста.

 

    Для установки параметров линии используется процедура XSetLineAttributes( ).

    Шрифт. Поле font определяет шрифт, используемый для вывода текста. Задать этот параметр можно с помощью процедуры XSetFont( ). Более подробно о шрифтах и работе с ними будет рассказано в 2.2.3.

    Шаблоны, используемые для заполнения рисуемых фигур. Процесс рисования включает в себя два этапа. На первом определяются пикселы, которые должны быть закрашены. После этого цвет выделенных точек изменяется. Так, для линии входящие в нее пикселы определяются по специальному алгоритму, а потом закрашиваются, например, цветом переднего плана.

    Способ закраски определяется полем fill_style. Он устанавливается процедурой XSetFillStyle( ) и воздействует на все функции, рисующие линии, текст и фигуры. Исключение составляет случай, когда выводится линия, для которой значение line_width равно 0. Возможные значения параметра fill_style перечислены ниже.

FillSolid - для закраски используются цвета переднего плана и фона.

FillTiled - для закраски используется карта пикселов, определяемая параметром tile графического контекста; при этом карта как-бы располагается в окне так, что ее левый верхний угол имеет координаты ts_x_origin и ts_y_origin; затем определяется ее пересечение с рисуемой графикой, и пикселы, попавшие в пересечение, закрашиваются; значения полей ts_x_origin, ts_y_origin устанавливаются процедурой XSetTSOrigin( ); карта tile должна иметь ту же "толщину" (число бит-на-пиксел), что и окно, в котором производится рисование.

FillStippled - для закраски используется карта пикселов, задаваемая полем stipple; данная карта должна иметь "толщину" в 1 бит; способ закраски такой же, как и в случае FillTiled с той лишь разницей, что рисуются лишь те пикселы графики, которым соответствует установленный бит в карте stipple; цвет пиксела задается полем foreground.

FillOpaqueStippled - аналогично значению FillStippled, только пикселы, для которых не установлен бит в карте stipple, закрашиваются цветом фона.

    Для задания полей tile и stipple можно использовать карты любого размера. На некоторых устройствах при определенных размерах рисование идет намного быстрее. Для получения таких размеров можно использовать процедуры XQueryBestSize( ), XQueryBestStipple( ), XQueryBestTile( ).

    Режим заполнения многоугольников указывает, как заполнять цветом многоугольники, стороны которых пересекаются. Возможные значения следующие:

EvenOddRule - заполняются точки фигуры, определяемые по следующему правилу: пусть для некоторой линии растра n1, n2, . . . , nk - стороны многоугольника, которые ее пересекают; тогда закрашиваются точки между n1 и n2, n3 и n4, и т.д.

WindingRule - заполняется вся внутренность фигуры.

    Режим заполнения дуг (поле arc_mode). Параметр задается процедурой XSetArcMode( ) и влияет на вид фигур, рисуемых процедурами XFillArc( ) и XFillArcs( ). Возможные значения параметра и соответствующие способы заливки приведены на рисунке 2.6.

Рис. 2.6. Заполнение дуг при различных значениях параметра arc_mode графического контекста.

    Влияние подокон на рисование графических примитивов определяется полем subwindow_mode. Оно устанавливается процедурой XSetSubwindowMode( ) и имеет следующие значения:

ClipByChildren - часть графики, перекрываемая подокнами, не видна;

IncludeInferiors - графика рисуется поверх всех подокон

    Генерация запроса на перерисовку при копировании частей окон (поле graphics_exposures). Когда часть окна копируется куда-либо, то вполне вероятна ситуация, что исходное изображение перекрыто, возможно не полностью, другими окнами или недоступна по другим причинам. В этом случае может быть необходимо сообщить клиенту, в окно которого происходит копирование, что часть нового изображения не может быть получена простым переносом пикселов, а должна быть перерисована. Если поле graphics_exposures равно True, то X посылает при копировании следующее:

    Если поле равно False, то событие не посылается. Устанавливается параметр процедурой XSetGraphicsExposures( ).

    Область отсечения (задается полями clip_mask, clip_x_origin, clip_y_origin). Это битовая карта, "говорящая" о том, какие пикселы выводятся, а какие нет при всех операциях рисования. Если бит карты установлен, то соответствующий пиксел появится в окне, а если бит сброшен, то пиксел будет пропущен. Положение в окне верхнего левого угла области отсечения определяется параметрами clip_x_origin и clip_y_origin (см. рис. 2.7).

 

Рис. 2.7. Параметры области отсечения и ее влияние на рисование графических примитивов.

    Эти параметры устанавливаются процедурой XSetClipOrigin( ). Сама область отсечения задается с помощью процедур XSetClipMask( ), XSetClipRectangles( ) или XSetClipRegion( ).


2.2.3.1. Функции, рисующие текст.

    Для вывода текста используются процедуры XDrawString( ), XDrawImageString( ) и XDrawText( ). Каждая из них имеет две версии. Первая используется для шрифтов, имеющих не более 256 символов. Если же символов больше ("большие" шрифты), то применяется вторая версия. Функции, работающие с "большими" шрифтами, имеют имена XDrawString16( ), XDrawImageString16( ) и XDrawText16( ). Параметры процедур, выводящих текст, задают дисплей, окно, графический контекст, строку, ее положение и т.д. Рисование идет в соответствии с областью отсечения контекста. Буквы или их части, находящиеся за пределами области отсечения, не изображаются. Наиболее часто употребляется процедура XDrawString( ) ( XDrawString16( ) ). Ее параметры дают строку, ее длину и положение в окне. Текст рисуется цветом переднего плана, выбранного в GC.

    Функция XDrawImageString( ) ( XDrawImageString16( ) ) похожа на предыдущую процедуру с той лишь разницей, что фон символов при рисовании закрашивается цветом фона, установленного в GC. XDrawString( ) и XDrawImageString( ) выводят символы, используя шрифт, установленный в GC.

    XDrawText( ) ( XDrawText16( ) ) позволяет рисовать несколько строк сразу, используя при этом разные шрифты. Каждая рисуемая единица задается структурой XTextItem.

    Процедура XDrawText16( ) использует структуру XDrawText16.

    Поле font, в приведенных структурах (XTextItem и XDrawText16) задает шрифт, используемый для рисования. Если значение поля font - None, то применяется шрифт, выбранный в GC.

    Более подробная информация об указанных структурах и функциях находится в приложении 1.


2.2.3.2. Шрифты.

    Как мы уже говорили ранее, текст, как правило, рисуется шрифтом, выбранным в графическом контексте. X версии 11.4 и ниже поддерживает только растровые шрифты, а начиная с версии 11.5 и выше X Window имеет также и векторные шрифты.

    В растровых шрифтах каждому символу соответствует некоторый битовый шаблон, определяющий порядок закраски пикселов при рисовании. Если бит шаблона равен 1, то соответствующий элемент изображения закрашивается цветом переднего плана GC, если же он равен 0, то он закрашивается либо цветом фона, либо вообще не рисуется.

    В векторных шрифтах каждый символ описывается последовательностью линий, которые будучи составлены вместе и дают его изображение. Размеры символов варьируются от шрифта к шрифту. Для их описания используется структура XCharStruct. Основные параметры символа показаны на рис. 2.8.

ascent

ascent

Базовая линия

descent

lbearing lbearing

rbearing rbearing

width width

Рис. 2.8. Основные параметры символа шрифта.

    Сам шрифт описывается структурой XFontStruct (см. приложение 1.).


2.2.3.3. Загрузка шрифтов.

    Перед тем, как выводить текст, используя тот или иной шрифт, последний должен быть загружен в X Window и выбран в графическом контексте.

    Загрузка шрифта осуществляется процедурой XLoadFont( ). Она берет в качестве аргумента имя шрифта, находит его и возвращает программе соответствующий идентификатор. Этот идентификатор передается затем процедуре XSetFont( ), чтобы выбрать шрифт в GC. Заметим, что реально шрифт с данным именем загружается сервером лишь один раз. После этого при обращениях к XLoadFont( ) с тем же именем шрифта, функция возвращает ссылку на шрифт, уже находящийся в памяти компьютера.

    По умолчанию X ищет файл со шрифтом в директории "usr/lib/X11/fonts". Программист может задать дополнительные директории для поиска с помощью процедуры XSetFontPath( ).

    Имя шрифта в X начинается с "-" и состоит из двух частей. Между ними стоит "--". В свою очередь, каждая из частей состоит из полей - слов, разделенных "-".

    В первой части указывается следующее:

  1. изготовитель шрифтам (foundry), например adobe;
  2. семейство шрифта (font family), например courier, helvetica;
  3. жирность шрифта (weight), например bold;
  4. наклон шрифта (slant);
  5. ширина букв шрифта (width).

    Во второй части указывается следующее:

  1. размер шрифта в пикселах (pixels);
  2. размер шрифта в десятых долях "точки" ("точка" равна 1/72 дюйма );
  3. горизонтальное разрешение устройства, для которого разработан шрифт (horizontal resolution in dpi); величина измеряется в числе "точек" на дюйм;
  4. вертикальное разрешение устройства, для которого разработан шрифт (vertical resolution in dpi); величина измеряется в числе "точек" на дюйм;
  5. тип шрифта (spacing); возможные значения параметра следующие:
    m - шрифт с фиксированной шириной символов;
    p - пропорциональный шрифт с переменной шириной символов;
  6. средняя ширина символов шрифта, изморенная в десятых долях пиксела (average width);
  7. множество символов шрифта в кодировке ISO (International Standards Organisation) (character set).

    Ниже приведен пример названия шрифта.

-adobe-courier-bold-o-normal--10-100-75-75-m-60-iso8859-1

    Части имени могут заменяться символом "*" или "?". В этом случае X подбирает шрифт, сличая имена имеющихся шрифтов с предоставленным шаблоном, так, как это делается при поиске файлов в UNIX. Например, шаблону

*charter-medium-i-*-240-*

соответствуют имена

-hit-charter-medium-i-normal-25-240-75-75-p-136-iso8859-1
-hit-charter-medium-i-normal-33-240-100-75-p-136-iso8859-1

    Названия шрифтов, доступных в системе, хранятся в соответствующей базе данных. Получить список имен шрифтов можно с помощью процедуры XlistFonts( ) или XListFontsWithInfo( ). Список шрифтов, возвращаемый этими функциями, должен быть освобожден вызовом XFreeFontNames( ).

    Некоторые шрифты, такие как "fixed" или "9x15", доступны всегда.

    Получить информацию о загруженном шрифте можно с помощью функции XQueryFont( ), которая возвращает заполненную структуру типа XFontInfo( ). Одновременно загрузить шрифт и получить информацию о нем можно с помощью процедуры XLoadQueryFont( ).

    Когда информация о шрифте больше не нужна, ее следует освободить с помощью XFreeFontInfo( ). Когда становится не нужен и сам шрифт, последний надо "сбросить", обратившись к процедуре XUnloadFont( ). Функция XFreeFont( ) объединяет в себе XFreeFontInfo( ) и XUnloadFont( ).

    Следующий фрагмент кода загружает шрифт "Courier", создает GC и выводит с его помощью строку "Hellow, world!".

Display *prDisplay;
GC prGC;
Window nWnd;
XFontStruct *prFontInfo;
. . . . . . . 
/* Загружаем шрифт */
if ( (prFontInfo=
    XLoadQueryFont(prDisplay, "*-courier-*" )) == NULL){
   printf("Font not found!\n");
   exit(1);
}
. . . . . . .
/* Создаем GC и рисуем строку */
prGC=XCreateGC(prDisplay, nWnd, 0, NULL);
XSetForeground (prDisplay, prGC, BlackPixel(prDisplay, 0));
XSetFont (prDisplay, prGC, prFontInfo->fid);
XDrawString (prDisplay, nWnd, prGC, 10, 50, "Hello, world!",
            strlen ("Hello, world!") );
XFreeGC (prDisplay, prGC);
. . . . . . .
/* "Сбрасываем" шрифт */
XUnloadFont (prDisplay, prFontInfo->fid);
. . . . . . .

 


2.2.3. Вывод текста

    Текст был и, видимо, будет важным средством информационного обмена между программами и пользователем. X Window позволяет выводить строки в любой части окна, используя большое количество шрифтов.

  1. Функции, рисующие текст.
  2. Шрифты.
  3. Загрузка шрифтов.


2.2. Текст и графика.

    В данном разделе описываются возможности, которые имеет программист для вывода текста и произвольных графических изображений. Особенностью X является то, что рисовать можно не только в окне, но и в специально подготовленной области памяти. Данная область называется картой пикселов и идентифицируется целым числом, имеющим тип Pixmap. Карта "толщиной" в один бит имеет специальное название - битовая.


2.3.1.1. События, соответствующие сигналам, посылаемым клавиатурой.

    Когда пользователь нажимает клавишу клавиатуры, программа получает событие KeyPress. Сервер также может послать событие KeyRelease, когда клавиша отпускается, но это справедливо не для всех типов компьютеров.

    Оба этих события сопровождаются структурой типа XKeyEvent. Ее поле keycode содержит код нажатой клавиши, а поле state - состояние клавиш-модификаторов и кнопок мыши. Модификаторами называются такие клавиши, как Shift, Ctrl, Caps Look. Кроме этого, X предусматривает наличие дополнительных модификаторов, которые обозначаются Mod1, . . ., Mod5. Каждой нажатой клавише-модификатору и кнопке мыши соответствует флаг в поле state. Клавиши и соответствующие им флаги приведены в таблице 1.2.8. приложения 1.


2.3.1.2. Физические и логические коды клавиш.

    Коды, передаваемые через поле keycode структуры XKeyEvent, однозначно идентифицируют клавиши. Их конкретные значения зависят от типа машины и клавиатуры. Эти коды мы будем называть физическими. Чтобы обеспечить переносимость программ, сервер устанавливает соответствие между физическими кодами клавиш, которые могут меняться от компьютера к компьютеру, и целочисленными константами - логическими кодами (символами). Они имеют предопределенные значения, которые приведены в файле "Keysymdef.h" и начинаются с префикса "XK_". Так букве "a" соответствует символ XK_a, клавише <Return> (<Enter>) - символ XK_Return и т.д.

    Для разных алфавитов X поддерживает разные множества логических кодов. Возможные типы алфавитов перечисляются в файле "Keysym.h".

    Одному коду клавиши может соответствовать несколько символов в зависимости от состояния клавиш - модификаторов. Процедура

KeySym XKeycodeToKeysym (Display* prDisplay, KeyCode nKeycode,
        int nIndex);

позволяет по коду nKeyCode получить соответствующий ему символ с номером nIndex. Если nIndex равен 0, то полученный символ соответствует просто нажатой клавише. Если nIndex равен 1, то возвращается символ, соответствующий ситуации, когда клавиша нажата одновременно с Shift.

    Функция XKeysymToKeycode( ) осуществляет обратное преобразование.

    Программа может получить карту соответствия кодов и символов, обратившись к процедуре XGetKeyboardMapping( ).

    Изменяется соответствие физических и логических кодов процедурой XChangeKeyboardMapping( ). Следующая последовательность операторов ставит клавише <F2> в соответствие символ XK_F3.

........
Keysym	nF2Sym, nF3Sym;
int	nF2Keycode;
Display	*prDisplay;
........
nF2Sym          = XStringToKeysym ("F2");
nF3Sym          = XStringToKeysym ("F3");
nF2Keycode      = XKeysymToKeycode (prDisplay, nF2Sym);
XChangeKeyboardMapping (prDisplay, nF2Keycode, 1,
               &nF3Sym, 1);
........

    Здесь использована процедура XStringToKeysym( ), которая по строке "str" возвращает соответствующий символ XK_str.

    Когда соответствие кодов меняется, всем работающим в настоящее время клиентам посылается событие MappingNotify.

    Клавиши-модификаторы также имеют логические коды. Клавишам Shift сопоставлены символы XK_Shift_L и XK_Shift_R; Caps Look соответствует XK_Caps Look; Control - XK_Control_L; Mod1 - XK_Meta_L и XK_Meta_R. Символы остальных модификаторов (Mod2 - Mod5) не определены. X содержит набор специальных процедур, которые позволяют получить и установить соответствие код-символ (ы) для модификаторов. Эти функции следующие: XGetModifierMapping( ), XInsertModifiermapEntry( ), XDeleteModifiermapEntry( ), XSetModifierMapping( ). Подробное их описание выходит за рамки настоящего издания ( см. подробнее, например [8] ).


2.3.1.3. Символы и ASCII строки.

    X не останавливается на задании соответствия код клавиши - символ (ы), а идет дальше. Система позволяет программе сопоставить любой комбинации модификаторов и клавиш (например, <Shift+Ctrl+A>) ASCII строку (например, "EXIT"). Для некоторых клавиш соответствующие строки задаются сервером по умолчанию. Так символу XK_A соответствует строка "A".

    Макрос XRebindKeysym( ) берет символ, список модификаторов и сопоставляет им строку.

    Процедура XLookupString( ), наоборот, берет событие о нажатии (отпускании) клавиши и возвращает соответствующие ему символ и строку. Последний ее параметр - указатель на структуру типа XComposeStatus. Дело в том, что некоторые клавиатуры имеют специальную клавишу Compose, которая позволяет печатать символы, которым нет соответствия среди клавиш. Так можно, например, набирать буквы разных алфавитов, такие как: 'й', 'ё' и т.д. Специальная таблица указывает, какой символ должен быть создан, если обычная клавиша нажимается одновременно с Compose. Ссылка на эту информацию и возвращается в структуре XComposeStatus.


2.3.1.4. Пример программы, работающей с клавиатурой.

    В данном разделе приводится фрагмент программы, которая распознает функциональные клавиши <F1>-<F5>, и при их нажатии печатает соответствующую строку. Программа также сопоставляет комбинации <Shift+Control+A> строку "EXIT". Эта комбинация используется для завершения программы.

........
Display	*prDisplay;
int	nScreenNum;
GC	prGC;
XEvent	rEvent;
Window	nWnd;
char	sKeyStr[20];
KeySym	nKeySym, naModList[2];
int	n;

/* Устанавливаем связь с сервером, получаем номер экрана . . . */
.........
/* Задаем соответствие символ-строка */
naModList[0] = XK_Control_L;
naModList[1] = XK_Shift_L;
XRebindKeysym (prDisplay, XK_F6, naModList, 2, "EXIT",
     strlen ("EXIT"));
    /* Цикл получения и обработки событий */

white (1) {
   XNextEvent (prDisplay, &rEvent);
   switch (rEvent.type) {
      ......
      case KeyPress :
         /* Очищаем строку */
         memset (sKeyStr, 0, sizeof (sKeyStr));

         /* Получаем строку, соответствующую событию */
         XLookupString (&rEvent.xkey, sKeyStr),
              sizeof (sKeyStr), &nKeySym, NULL);
         if ( !strcmp (sKeyStr, "EXIT"))
         {
            XFreeGC (prDisplay, prGC);
            XCloseDisplay (prDisplay);
            exit (0);
         }

         n = nKeySym == XK_F1 ? 1 :
             nKeySym == XK_F2 ? 2 :
             nKeySym == XK_F3 ? 3 :
             nKeySym == XK_F4 ? 4 :
             nKeySym == XK_F5 ? 5 : 0;

         if (n) {
            sprintf (sKeyStr, "F%d pressed.", n);
            XClearWindow (prDisplay, nWnd);
            XDrawString (prDisplay, nWnd, prGC, 10, 50,
                    sKeyStr, strlen (sKeyStr));
         }
         break;
   }
}
........

2.3.1.5. Задание параметров клавиатуры.

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

typedef struct {
  int key_click_percent;
  int bell_percent;
  unsigned int bell_pitch, bell_duration;
  unsigned long led_mask;
  int global_auto_repeat;
  char auto_repeats[32];
} XKeyboardsState;

    Поле key_click_percent указывает, имеет ли нажатие клавиши звуковое сопровождение; значения поля задаются в %; 0 - звукового сопровождения нет, 100 - громкий звук. Поле bell_percent, bell_pitch и bell_duration указывают, какую силу, частоту и продолжительность имеет предупреждающий сигнал, возникающий при нажатии некоторых клавиш.

    Некоторые клавиатуры используют для клавиш-модификаторов световую подсветку. Поле led_mask есть комбинация флагов, показывающая, для каких клавиш эта подсветка имеет место.

    Когда клавиша нажата и удерживается, то сервер может автоматически имитировать ее повторное нажатие. Поле global_auto_repeat определяет, делает это сервер или нет. Особенностью X является то, что автоматическую генерацию событий о нажатии можно разрешать или запрещать для отдельных клавиш. Массив auto_repeats содержит информацию о том, для каких клавиш автогенерация имеет место, а для каких нет. Каждый бит массива соответствует клавише с определенным физическим кодом. Если бит установлен, то генерация разрешена, если сброшен, то запрещена. Каждый байт N массива содержит биты для клавиш с кодами от 8N до 8N+7.

    Изменить параметры клавиатуры можно с помощью XChangeKeyboardControl().

    Желаемые установки передаются через переменную, которая указывает на .структуру типа XKeyboardControl, определяемую следующим образом:

typedef struct {
  int key_click_percnet;
  int bell_percent;
  int bell_pitch;
  int bell_duration;
  int led;
  int led_mode;
  int key;
  int auto_repeat_mode;
} XKeyboardControl;

    Первые четыре поля совпадают с аналогичными полями структуры XKeyboardState. Поля led и led_mode позволяют сообщить серверу, какие из клавиш-модификаторов должны сопровождаться подсветкой. Если поле led не задано, и led_mode равно LedModeOn, то изменяется состояние всех клавиш, для которых поддерживается световое сопровождение. Если led_mode равно LedModeOff, то состояние клавиш не меняется. Если поле led задано, то это есть комбинация флагов, указывающих, для каких клавиш подсветку включить (led_mode равно LedModeOn) или выключить (led_mode равно LedModeOff).

    Поля key и auto_repeat_mode определяют, для какой клавиши (клавиш) включить (auto_repeat_mode равно AutoRepeatModeOn) или выключить (auto_repeat_mode равно AutoRepeatModeOff) режим автоматического повтора. Если поле key задано, то автоматический повтор включается или выключается только для клавиши с кодом key.


2.3.1. Клавиатура.

    Как и большинство интерактивных программ, задачи, выполняющиеся в X Window, активно используют для ввода информации клавиатуру компьютера. Когда пользователь нажимает или отпускает клавишу, сервер получает соответствующий сигнал, который преобразуется в событие и отправляется в очередь программы, имеющей фокус ввода (input focus).

    Поясним, что такое "фокус ввода". Дело в том, что клавиатура у машины одна, и она разделяется всеми выполняющимися одновременно программами. Но в каждый момент времени поступающий от устройства сигнал доступен лишь одной из них, как правило, той, которой принадлежит активное окно. В этом случае говорят, что программа и ее окно имеют фокус ввода. Последний может переходить от окна к окну и от программы к программе.

    Когда окно получает фокус, соответствующей программе посылается событие FocusIn, при потере - приходит событие FocusOut.

  1. События, соответствующие сигналам, посылаемым клавиатурой.
  2. Физические и логические коды клавиш.
  3. Символы и ASCII строки.
  4. Пример программы, работающей с клавиатурой.
  5. Задание параметров клавиатуры.




2.3.2. Мышь.

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

  1. События, порождаемые мышью.
  2. Работа с курсором мыши.
  3. Задание параметров мыши.

2.3.3. "Захват" клавиатуры и/или мыши.

    Обычно фокус ввода может свободно переходить от окна к окну. Но иногда программе необходимо запретить передачу фокуса. Это называется "захватом" клавиатуры. Для того, чтобы его реализовать, используется процедура XGrabKeyboard( ).

    Функция XGrabKey( ) запрещает передачу фокуса после нажатия определенной комбинации клавиш. Освободить клавиатуру можно, обратившись к процедуре XUngrabKeyboard( ) (XGrabKey( )).

    Рассмотрим поведение системы при обработке событий от мыши. Как правило, если ее кнопка нажата в момент, когда ее курсор находится в неактивном окне, то последнее активизируется, и события от мыши передаются ему. Сказанное означает, что в нормальном состоянии окно получает только те события от мыши, которые соответствуют сигналам, пришедшим тогда, когда ее курсор находится в пределах окна. Но если программа вызывает

int XGrabPointer (Display* prDisplay, Window nGrabWnd,
    Bool nOwnerEvents, unsigned int nEventMask,
    int nPointerMode, int nKeyboardMode, Window nConfineTo,
    Cursor nCursor, Time nTime);

то положение меняется. Теперь все события будут направляться окну с дескриптором nGrabWnd. "Освобождается" мышь вызовом XUngrabPointer( ). Процедура XGrabButton( ) указывает, что курсор должен быть "захвачен" после нажатия определенной кнопки. Обратной к ней является процедура XUngrabButton( ).

    Процедуры, "захватывающие" устройство, - мышь или клавиатуру - имеют ряд аргументов, влияющих на поведение системы.

    Так параметр nConfineTo есть идентификатор окна, за пределы которого не должен выходить курсор мыши, если он "захвачен".

    Если аргумент nOwnerEvents равен Тrue, то события мыши будут передаваться окнам программы. Если nOwnerEvents - False, или курсор находится в окне, не принадлежащем программе, то события мыши передаются окну nGrabWnd.

    Если nOwnerEvents равен False, то параметр nEventMask указывает, какие события следует передавать окну nGrabWnd.

    Обработка событий от клавиатуры или ныши может быть приостановлена, если nPointerMode или nKeyboardMode равен GrabModeSync. В этом случае события буферизуются сервером, пока устройство не будет освобождено с помощью XUngrabKeyboard( ), XUngrabKey( ), XUngrabPointer( ) или XUngrabButton( ).

    Параметр nCursor задает форму курсора во время того, как мышь "захвачена". Аргумент nTime указывает, когда система должна активизировать режим "захвата".


2.3. Работа с внешними устройствами.

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

  1. Клавиатура.
    1. События, соответствующие сигналам, посылаемым клавиатурой.
    2. Физические и логические коды клавиш.
    3. Символы и ASCII строки.
    4. Пример программы, работающей с клавиатурой.
    5. Задание параметров клавиатуры.
  2. Мышь.
    1. События, порождаемые мышью.
    2. Работа с курсором мыши.
    3. Задание параметров мыши.
  3. "Захват" клавиатуры и/или мыши.

2.4.1. Формат файла ресурсов.

    В X файл ресурсов есть обычный текстовый файл, каждая строка которого задает тот или иной параметр (ресурс) программы. (При этом предполагается, что программу "населяют" именованные объекты, связанные в некоторую иерархию). Общий вид строки следующий:

<имя программы>.<подобъект1>.<подобъект2>. . .
                <подобъектN>.<имя ресурса>: <значение ресурса>   

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

myprog.dialogwnd.background: Red

"говорит", что в программе myprog у объекта с именем dialogwnd параметр background (цвет фона) имеет значение Red (красный цвет).

    Вместо имен объектов могут указываться их классы. Обычно класс имеет то же самое имя, что и объект, но начинается с заглавной буквы, например,

Myprog.dialogwnd.Background: Red

    Часть объектов или классов в левой части строки, задающей ресурс, может заменяться символом '*', например, строка

myprog*background: Red

    указывает, что для всех объектов программы myprog ресурс background имеет значение Red.

    Связка с помощью символа '.' имеет больший приоритет, чем связка с помощью '*'. Так, если в файле, задающем ресурсы, есть две строки

myprog*background: Red
myprog.dialogwnd.background: Green

то все объекты программы будут иметь ресурс background равный Red, кроме объекта dialogwnd, для которого этот параметр есть Green.


2.4.2. Доступ к ресурсам программ.

    Пусть ресурсный файл подготовлен. Как получить доступ к его данным во время работы программы? Для этого X предоставляет набор процедур, которые совокупно называются "менеджер ресурсов" (Resource Manager), и специальную программу xrdb, которая позволяет считать любой ресурсный файл и включить его в общую "таблицу" ресурсов сервера. Последняя называется базой данных ресурсов сервера, и представляет собой область памяти, ассоциированную со "свойством" (property) XA_RESOURCE_MANAGER "корневого" окна экрана дисплея.

    Наиболее простой является процедура XGetDefault( ). Она получает имя программы, имя ресурса и определяет значение последнего. При этом она последовательно совершает следующие шаги:

    Если ресурс одновременно встречается в ".Xdefaults" и файле, определяемом XENVIRONMENT, то берется последнее значение.

    В примере, приводимом ниже, используется XGetDefaults( ), чтобы получить строку, которую надо напечатать в окне программы. Предполагается, что имя программы - "hello", а строка - ресурс с именем "helloWorld", т.е. в файле ".Xdefaults" должна быть помещена, например, следующая запись:

. . . . . . .
hello.helloWorld : Hello, World!
. . . . . . .
    . . . . . . .
Display	*prDisplay;
GC		prGC;
Window	        nWnd;
char		*psString;
. . . . . . . .
/* Устанавливаем связь с сервером, получаем номер экрана. . .*/
. . . . . . . .
/* Выбираем события, обрабатываемые программой */ 
XSelectInput (prDisplay, nWnd, ExposureMask | KeyPressMask);

/* Получаем рисуемую строку */ 
psString = XGetDefaults (prDisplay, "hello", "helloWorld");
. . . . . . . .
XDrawString ( prDisplay, nWnd, prGC, 10, 50, psString,
        strlen (psString) );
. . . . . . . .

    Функция XGetDefaults( ) проста в обращении, но не достаточно гибка. Так, например, с ее помощью нельзя прочитать содержимое произвольного файла ресурсов. Рассмотрим другие более развитые возможности.

    Вызов XrmInitialize( ) инициализирует менеджер ресурсов. Обращение к этой функции предшествует вызовам остальных процедур.

XrmParseCommand (XrmDatabase  *prDB, XrmOptionRec  *prOptRec,
       int nOptRecNum, char  *psProgName, int argc, char  **argv;

сканирует строку, с помощью которой вызвана программа, и "достает" из нее ресурсы и их значения, при этом создается специальная структура данных - база данных ресурсов. Ресурсы и их значения помещаются в нее. Указатель на базу данных передается программе через переменную prDB. Параметр psProgName содержит имя программы, argc - число опций в командной строке, argv - сами опции. Аргумент prOptRec определяет, как разбирать командную строку. nOptRecNum задает число элементов массива prOptRec.

    В примере, приводимом ниже, определяется, что в командной строке опция "-bg" задает цвет фона; "-fg" - цвет переднего плана, а опция "-xrm" позволяет задать в командной строке любой ресурс программы.

. . . . . . . .
XrmOptionDescRec rOptRec[ ] = {
     { "-bg", "*background", XrmoptionSepArg, "Red"   },
     { "-fg", "*foreground", XrmoptionSepArg, "White" },
     { "-xrm", NULL,         XrmoptionResArg, NULL    },
};
XrmDatabase rDB;
. . . . . . . .
void main (int argc, char **argv)
{
   . . . . . . . .
   XrmInitialize( );
   XrmParseCommand (&rDB, rOptRec,
		     sizeof (rOptRec) / sizeof (rOptRec[0]),
		     argv[0], argc, argv);
    . . . . . . . .
}

    Процедура XrmGetFileDataBase( ) позволяет считать указанный ресурсный файл и создать по нему в памяти базу данных ресурсов.

XrmGetResources (XrmDatabase  *prDB, char  *psResName,
          char  *psResClass, char  *psResType, XrmValue  *psResVal);

    Считывает ресурс с именем psResName и классом psResClass из базы данных *prDB. После возврата psResType есть указатель на строку, "говорящую" о типе ресурса. На само значение ресурса указывает psResVal.

    Функция XrmPutResource( ) сохраняет ресурс в базе данных. XrmPutFileDatabase( ) записывает базу данных ресурсов в файл.


2.4. Программы и их ресурсы.

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

    Как правило, программисты создают ресурсы, каждый по своему. В X Window сделана попытка унифицировать этот процесс.

    В данном пункте описывается формат файлов, описывающих ресурсы, и процедуры для работы с ними.

  1. Формат файла ресурсов.
  2. Доступ к ресурсам программ.

2.5.1. Механизм "свойств".

    Как мы уже упоминали ранее, "свойство" есть набор данных, ассоциированных с окном. Они хранятся в специальных "таблицах" в памяти компьютера, на котором работает сервер. Каждое "свойство" имеет имя. Разные окна могут иметь "свойства" с одинаковыми именами.

    Поскольку передача имен - строк произвольной длины - от клиента к серверу может увеличить нагрузку на сеть, X идентифицирует "свойства" с помощью целых чисел - атомов. Процедура XInternAtom( ) включает "свойство" с указанным именем в таблицу сервера и возвращает соответствующий атом.

    Данные "свойства" рассматриваются сервером как массив единиц длиной 8, 16 или 32 бита. Их конкретная интерпретация осуществляется программами-клиентами.

    Каждое "свойство" имеет тип, который, в свою очередь, также задается тем или иным "свойством". Например "свойство", соответствующее атому XA_STRING, задает тип - строка.

    Для работы со "свойствами" кроме XInternAtom() используются следующие процедуры: XChangeProperty() - меняет данные "свойства": XGetWindowProperty() - позволяет получить данные "свойства".

    Особую роль играют "свойства", данные которых содержат строки текста. Они так и называются "текстовыми" и имеют тип "TEXT". Таковыми являются, например, имена (заголовки) окно, имена пиктограмм и т. д. Данные текстового "свойства" описываются структурой XTextProperty. Процедура XStringListToTextProperty() переводит список строк в набор данных типа XTextProperty. XTextPropertyToString() выполняет обратное преобразование.


2.5.2. Общение с менеджером окон.

    Менеджер окон - это специальный клиент, в задачи которого входит интерактивное перемещение окон по экрану, изменение их размеров, минимизация (превращение в пиктограмму) и многое другое. Чтобы облегчить менеджеру его нелегкую жизнь, программам рекомендуется при инициализации сообщить о себе определенную информацию. Передается она через предопределенные "свойства", которые известны менеджеру и могут быть им прочитаны. Некоторые из "свойств" (так называемые "стандартные") задавать обязательно. Все остальное определяется по усмотрению программы. Наиболее простой способ задать стандартные "свойства" - обратиться к процедурам XSetStandardProperties( ) (X11R3 и ниже) или XSetWMProperties( ) (X11R4 и выше ).

    Ниже перечисляются "свойства", создаваемые для менеджера окон программами, а также процедуры для работы с ними.

    Имя (заголовок) окна. Идентифицируется атомом XA_WM_NAME и имеет тип XA_STRING (X11R3 и ниже) или "TEXT" (X11R4 и выше). Данные "свойства" - строка (X11R3 и ниже) или структура XTextProperty (X11R4 и выше). Для задания "свойства" используется процедура XStoreName( ) (XSetWMName( )). Получить его можно с помощью XFetchName( ) (XGetWMName( )).

    Имя пиктограммы. Идентифицируется атомом XA_WM_ICONNAME и имеет тип XA_STRING (X11R3 и ниже) или "TEXT" (X11R4 и выше). Данные "свойства" - строка (X11R3 и ниже) или структура XTextProperty (X11R4 и выше). Для задания "свойства" используется процедура XSetIconName( ) (XSetWMIconName( )). Получить его можно с помощью XGetIconName( ) (XGetWMIconName( )).

    Рекомендации (hints) о геометрии окна. Идентифицируется атомом XA_WM_NORMAL_HINTS и имеет тип XA_WM_SIZE_HINTS. Данные "свойства" - структура типа XSizeHints. Для задания "свойства" используется процедура XSetNormalHints( ). В X11R3 и ниже предусматривались также рекомендации о размерах окна в максимальном (zoomed) состоянии.

    Дополнительные параметры окна: способ работы с клавиатурой, вид и положение пиктограммы. Идентифицируется атомом XA_WM_HINTS и имеет тип XA_WM_HINTS. Данные "свойства" - структура типа XWMHints. Для задания "свойства" используется процедура XSetWMHints( ). В X11R4 и выше структура типа XWMHints, передаваемая функции XSetWMHints( ), должна быть подготовлена с помощью XAllocWMHints( ). Получить данные "свойства" можно с помощью XGetWMHints( ).

    Атрибут, характеризующий "временное" окно. Идентифицируется атомом XA_WM_TRANSIENT_FOR и имеет тип XA_STRING. "Свойство" задается для окон, появляющихся на экране для выполнения вспомогательных функций (диалоги, меню). Такие объекты рассматриваются менеджером по особому. Например, он может не добавлять к окну заголовок и рамку. Данные "свойства" - идентификатор окна родительского по отношению к данному. Задается "свойство" с понощью процедуры XSetTransientForHint( ) (см. подробнее, например [8]Ссылка).

    Имена программы и ее класса, идентифицируется атомом XA_WM_CLASS и имеет тип XA_STRING. Данные "свойства" - структура типа XClassHints. Задается "свойство" с помощью процедуры XSetClassHints( ) и может быть получено с помощью XGetClassHints( ).

    Если окно (окна) программы имеют собственную цветовую палитру, то приложение должно соответствующим образом задать для него атрибут colormap. Далее, в X11R3 и ниже при получении окном фокуса ввода, программа сама обязана перенести логическую палитру в аппаратную. В X11R4 и выше последний шаг выполняется менеджером окон. Программа только эаносит идентификатор окна (идентификаторы окон) в список, ассоциированный со "свойством", имя которого WM_COLORMAP_WINDOWS. Делается это процедурой XSetWMColormapWindows( ). Получить список, уже находящийся в "свойстве", можно, обратившись к XGetWMColormapWindows( ).

    Когда окно открыто, пользователь посредством менеджера совершает над ним разные действия. Программе может быть желательно "перехватывать" некоторые из них. Так, например, если окно представляет собой редактор текста, и пользователь пытается его закрыть, то разумно спросить у сидящего за компьютером человека, а не желает ли он предварительно сохранить результаты редакции. Начиная с X11R4 системой предусматривается "свойство" с именем WM_PROTOCOLS. Оно содержит список атомов, и каждый из них идентифицирует "свойство" , связанное с действиями, о которых надо оповещать программу. Эти "свойства" следующие:

WM_TAKE_FOCUS - задается, если программа хочет передавать фокус ввода между своими окнами самостоятельно; в этом случае менеджер не будет управлять фокусом, ввода, а пошлет приложению событие ClientMessage, у которого поле message_type равно атому, соответствующему "свойству" WM_PROTOCOLS, а поле data.l[0] равно атому, соответствующему "свойству" WM_TAKE_FOCUS; в ответ на это событие программа должна сама обратиться к XSetInputFocus( ) для задания окна, имеющего фокус ввода;

WM_SAVE_YOURSELF - задается, если программа хочет перехватить момент своего завершения; менеджер окон посылает приложению событие ClientMessage, у которого поле message_type равно атому, соответствующему "свойству" WM_PROTOCOLS, а поле data.l[0] равно атому, соответствующему "свойству" WM_SAVE_YOURSELF; в ответ программа может сохранить свое текущее состояние;

WM_DELETE_WINDOW - задается, если программа хочет перехватить моменты, когда менеджер окон закрывает принадлежащие ей окна; менеджер окон посылает приложению событие ClientMessage, у которого поле message_type равно атому, соответствующему "свойству" WM_PROTOCOLS, а поле data.l[0] равно атому, соответствующему "свойству" WM_DELETE_WINDOW; далее программа сама решает, оставить окно на экране или удалить его с помощью XDestroyWindow( ).

    "Свойство" WM_PROTOCOLS задается процедурой XSetWMProtocols( ) и может быть получено с помощью XGetWMProtocols( ) (см. приложение 1 Ссылка).

    Приведем фрагмент программы, задающей "свойство" WM_PROTOCOLS и производящей соответствующую обработку событий.

. . . . . . .
Display		*prDisplay;
int		nScreenNum;
GC		prGC;
XEvent		rEvent;
Window		nWnd;
Atom		pnProtocol[2];
Atom		nWMProtocols;

/*
 *Устанавливаем связь с сервером, получаем номер экрана,
 *создаем окно, выбираем события, обрабатываемые программой
 */
. . . . . . .

/* Задаем свойство WM_PROTOCOLS */

pnProtocol [0] = XInternAtom (prDisplay,
		     "WM_TAKE_FOCUS", True);

pnProtocol [1] = XInternAtom (prDisplay,
		     "WM_SAVE_YOURSELF", True);

nWMProtocols = XInternAtom (prDisplay, "WM_PROTOCOLS", True);

XSetWMProtocols (prDisplay, nWnd, pnProtocol, 2);

/* Показываем окно */

XMapWindow (prDisplay, nWnd);

/* Цикл получения и обработки событий */

while (1)  {
     XNextEvent (prDisplay, &rEvent);

switch (rEvent.type)  {
    . . . . . .
    case ClientMessage :
    if (rEvent.xclient.message_type == nWMProtocols)
    {
       if (rEvent.xclient.data.l[0] == pnProtocol[0])
          puts ("Receiving the input focus.\n");
       else
          if (rEvent.xclient.data.l[0] == pnProtocol[1])
          {
	XCloseDisplay (prDisplay);
	exit (0);
          } 
     }
     break;
     . . . . . . .
  }
}
. . . . . . .

    Заказывается реакция на два события: получение фокуса ввода (WM_TAKE_FOCUS) и завершение программы (WM_SAVE_YOURSELF). Когда сервер посылает событие первого типа, задача печатает соответствующее сообщение на устройства вывода. При приходе события второго типа, программа закрывает связь с сервером и завершается.


2.5. Передача данных между программами.

    В данном разделе описываются встроенные в X средства, позволяющие клиентам передавать друг другу информацию. Он основывается на механизме "свойств" (properties).

  1. Механизм "свойств".
  2. Общение с менеджером окон.

Назад

Содержание

Вперед


Глава 2. Основы программирования в системе X Window.

    X Window или просто X - это система для создания графического пользовательского интерфейса на компьютерах, работающих под управлением операционной системы UNIX. X была создана в Массачусетском Технологическом Институте (США). В настоящее время уже выпущена версия 11.5 (X Window System Version 11 Release 5 или X11R5).

    X и сети. Особенностью системы является то, что она поддерживает работу как на отдельной ЭВМ, так и в сети. Это означает, что программа, "живущая" на одном компьютере, может с помощью X Window общаться с пользователем, сидящим за другой машиной. Система обеспечивает вывод графической информации на экран машины, воспринимает сигналы от внешних устройств, таких как клавиатура и мышь, и передает их программам. Заметим, что устройство вывода может иметь несколько экранов. X обеспечивает рисование на любом из них. Все это: экран (или экраны), а также устройства ввода (клавиатура или мышь) называются в терминах X Window - дисплей.

    Окна. X позволяет пользователю общаться со многими программами одновременно. Чтобы вывод из них не смешивался, система создает на экране дисплея "виртуальные" подэкраны - окна. Каждое приложение, как правило, рисует лишь в своем окне или окнах. X предоставляет набор средств для создания окон, их перемещения по экрану и изменения их размеров.

    X и ресурсы программ. Как правило, программы имеют набор конфигурационных параметров - ресурсов. Это может быть цвет окна, тип шрифта, которым рисуется текст, и многое другое. Система стандартизует способ задания ресурсов приложений и содержит ряд процедур для работы с ними. Эта совокупность функций называется "менеджер ресурсов" (X recource manager или сокращенно Xrm). "Хранилище" параметров программы называется базой данных ресурсов.

    "Управляемость событиями" (event-driver architecture). Особенностью X Window является то, что она организует общение между самими программами и между программами и внешней средой путем рассылки событий. Событие - есть единица информации, идентифицирующая происходящие в системе изменения или действия, и содержащая дополнительные сведения о них.

    В данной главе рассказывается о том, как создавать приложения, предназначенные для работы в X Window. Излагаемый материал относится к системе X Window версии 11.1 и выше.


2.1. Основы.

    В данном пункте с самых общих позиций рассматривается устройство системы X Window. Приводится пример простой программы и освещаются некоторые другие вопросы.

  1. Общее устройство X Window.
  2. X окно.
  3. Управление окнами.
  4. Графические возможности X Window.
  5. "Свойства" и атомы.
  6. Первый пример.
  7. События.
  8. Атрибуты окна.

2.1.1. Общее устройство X Window.

    Система X Window представляет совокупность программ и библиотек. "Сердцем" ее является отдельный UNIX-процесс, существующий на компьютере, к которому присоединен дисплей. Именно сервер знает особенности конкретной аппаратуры, знает, что надо предпринять, чтобы закрасить пиксел на экране, нарисовать линию или другой графический объект. Он также умеет воспринимать сигналы, приходящие от клавиатуры и мыши.

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

    Состав пакетов и их последовательность определяются специальным протоколом. Его описание выходит за рамки настоящего издания и может быть найдено в документации по X Window системе [15].

    Но чтобы программировать для X, совсем не обязательно знать детали реализации сервера и протокола обмена. Система предоставляет библиотеку процедур, с помощью которых программы осуществляют доступ к услугам X "на высоком уровне". Так для того, чтобы вывести на экран точку, достаточно вызвать процедуру XDrawPoint(), передав ей соответствующие параметры. Последняя выполняет всю черновую работу по подготовке и передачи пакетов данных серверу. Упомянутая библиотека называется "Xlib". Она помещается в файле "lX11.a", который, как правило, находится в каталоге "/usr/lib". Прототипы функций библиотеки, используемые ею структуры данных, типы и прочее определяется в файлах-заголовках из директории "/usr/include/X11".

    На рисунке 2.1 представлена схема общения клиентов и сервера.

pict-2-1.gif (6371 bytes)

Рис.2.1. Общая схема общения программ-клиентов и X-сервера.

    Посылка порций данных, особенно если она осуществляется через сеть, операция достаточно медленная. Чтобы повысить производительность системы, Xlib не отправляет пакеты сразу, а буферизует их в памяти машины, на которой выполняется программа-клиент. Собственно передача выполняется в тот момент, когда клиент вызывает процедуру, ожидающую получения событий от сервера, например XNextEvent(). Программа может явно инициировать отправку пакетов, обратившись к функциям XFlush() или Xsync().


2.1.2. X окно.

    Как уже упоминалось ранее, окно - это базовое понятие в X. Оно представляет прямоугольную область на экране, предоставляемую системой программе-клиенту. Последняя использует окно для вывода графической информации. На рисунке 2.2 показан общий вид окна в X Window.

pict-2-2.gif (4636 bytes)

Рис. 2.2. Общий вид окна X Window.

    Из рисунка видно, что окно имеет внутренность и край. Основными атрибутами окна являются ширина и высота внутренности, а также ширина края. Далее мы будем говорить ширина и высота, а слово "внутренность" станем опускать. Упомянутые параметры окна называются его геометрией.

    С каждым окном связывается система координат. Ее начало находится в левом верхнем углу окна. Ось x направлена вправо, а ось y - вниз. Единица измерения по обеим осям - пиксел.

    Окна могут быть двух типов: InputOutput (для ввода - вывода) и InputOnly (только для ввода). Окно первого типа - это обычное окно. Окно второго типа не может использоваться для рисования. У данного окна нет края, оно "прозрачно". Заметим, что окна этого типа используются достаточно редко.

    X Window позволяет программе создавать несколько окон одновременно. Они связаны в иерархию, в которой одни являются "родителями", а другие "потомками". Сам сервер на каждом экране создает одно основное окно, которое является самым верхним "родителем" всех остальных окон. Это окно мы будем называть главным или "корневым".

    Для получения информации о любом окне X существует утилита xwininfo.


2.1.3. Управление окнами.

    Окна могут располагаться на экране произвольным образом, перекрывая друг друга. X имеет набор средств, пользуясь которыми программа-клиент может изменять размеры окон и их положение на экране. Особенностью системы является то, что она не имеет встроенной возможности управлять окнами с помощью клавиатуры или мыши. Чтобы это можно было осуществить, нужен специальный клиент, который называется "менеджер окон" (Window manager). Стандартный дистрибутив X содержит такую программу - twm. Возможности этого менеджера ограничены, но, тем не менее, он позволяет осуществлять базовые действия: передвигать окна с помощью мыши, изменять их размер и т.д. Наиболее же развитым оконным менеджером является, по всей видимости, программа mwm (Motif Window Manager), которая поставляется в рамках системы OSF/Motif. Сама эта система описывается в главе 4 настоящей книги.

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


2.1.4. Графические возможности X Window.

    Система X Window предназначена для работы на растровых дисплеях. В подобного рода устройствах изображение представляется матрицей светящихся точек - пикселов. Каждый пиксел кодируется определенным числом бит (как правило 2, 4, 8, 16 или 24). Число бит-на-пиксел называют "толщиной" или "глубиной" (deep) дисплея. Биты с одинаковыми номерами во всех пикселах образуют как бы плоскость, параллельную экрану. Ее называют "цветовой плоскостью". X позволяет рисовать в любой цветовой плоскости (или плоскостях), не затрагивая остальные.

    Значение пиксела не задает непосредственно цвет точки на экране. Последний определяется с помощью специального массива данных, называемого палитрой. Цвет есть содержимое ячейки палитры, номер которой равен значению пиксела.

    X имеет большой набор процедур, позволяющих рисовать графические примитивы - точки, линии, дуги, выводить текст и работать с областями произвольной формы.


2.1.5. "Свойства" и атомы.

    В X Window встроены средства для обеспечения обмена информацией между программами-клиентами. Для этого используется механизм "свойств" (properties). "Свойство" - это порция данных, связанная с некоторым объектом (например, окном), и которая доступна всем клиентам X.

    Каждое "свойство" имеет имя и уникальный идентификатор - атом (atom). Обычно имена "свойств" записываются большими буквами, например: "MY_SPECIAL_PROPERTY". Атомы используются для доступа к содержимому "свойств" с тем, чтобы уменьшить количество информации, пересылаемой по сети между клиентами и X сервером.

    В X предусмотрен набор процедур, позволяющих перевести имя "свойства" в уникальный атом, и, наоборот, по атому получить необходимые данные.

    Некоторые "свойства" и соответствующие им атомы являются предопределенными и создаются в момент инициализации сервера. Этим атомам соответствуют символические константы, определенные в файлах-заголовках библиотеки Xlib. Эти константы начинаются с префикса "XA_". Детально механизм "свойств" и атомов описан в п. 2.5.1.

    Посмотреть текущий список атомов можно с помощью утилиты xlsatoms.


2.1.6. Первый пример.

    Продолжая традиции многих изданий, посвященных программированию на С, мы начинаем с программы, рисующей на экране строку "Hello, world!"'. В этом примере приведены основные шаги, необходимые для работы в X Window. ( xhello.tgz)

#include <X11/Xlib.h>
#include <X11/Xutil.h>
#include <X11/Xos.h>
#include <stdio.h>
#include <string.h>

#define WND_X 0
#define WND_Y 0
#define WND_WDT 100
#define WND_HGH 100
#define WND_MIN_WDT 50
#define WND_MIN_HGH 50
#define WND_BORDER_WDT 5

#define WND_TITLE "Hello!"
#define WND_ICON_TITLE "Hello!"
#define PRG_CLASS "Hello!"

/*
* SetWindowManagerHints - процедура передает информацию о
* свойствах программы менеджеру окон. 
*/

static void SetWindowManagerHints ( 
  Display *   prDisplay,  /*Указатель на структуру Display */
  char *      psPrgClass, /*Класс программы */
  char *      argv[],     /*Аргументы программы */
  int         argc,       /*Число аргументов */
  Window      nWnd,       /*Идентификатор окна */
  int         x,          /*Координаты левого верхнего */
  int         y,          /*угла окна */
  int         nWidth,
  int         nHeight,    /*Ширина и высота окна */
  int         nMinWidth,
  int         nMinHeight, /*Минимальные ширина и высота окна */
  char *      psTitle,    /*Заголовок окна */
  char *      psIconTitle,/*Заголовок пиктограммы окна */
  Pixmap      nIconPixmap /*Рисунок пиктограммы */
)
{
  XSizeHints rSizeHints; /*Рекомендации о размерах окна*/

#ifdef X11R3 /*. X11R3 и ниже */
  rSizeHints.flags = PPosition | PSize | PMinSize;
  rSizeHints.x = x;
  rSizeHints.y = y;
  rSizeHints.width = nWidth;
  rSizeHints.height = nHeight;
  rSizeHints.min_width = nMinWidth;
  rSizeHints.min_height = nMinHeight;

  XSetStandardProperties ( prDisplay, nWnd, psTitle,
      psIconTitle, nIconPixmap, argv, argc, &rSizeHints );
#else /* X11R4 и выше */
  XWMHints rWMHints;
  XClassHint rClassHint;
  XTextProperty prWindowName, prIconName;

  if ( !XStringListToTextProperty (&psTitle, 1, &prWindowName ) ||
       !XStringListToTextProperty (&psIconTitle, 1, &prIconName ) ) {
    puts ( "No memory!\n");
    exit ( 1 );
}

rSizeHints.flags = PPosition | PSize | PMinSize;
rSizeHints.min_width = nMinWidth;
rSizeHints.min_height = nMinHeight;
rWMHints.flags = StateHint | IconPixmapHint |
                 InputHint;
rWMHints.initial_state = NormalState;
rWMHints.input = True;
rWMHints.icon_pixmap= nIconPixmap;

rClassHint.res_name = argv[0];
rClassHint.res_class = psPrgClass;

XSetWMProperties ( prDisplay, nWnd, &prWindowName,
    &prIconName, argv, argc, &rSizeHints, &rWMHints,
    &rClassHint );
#endif
}

/*
*main - основная процедура программы
*/

void main ( int argc, char * argv[] )
{
  Display *prDisplay;    /* Указатель на структуру Display */
  int nScreenNum;        /* Номер экрана */
  GC prGC;
  XEvent rEvent;
  Window nWnd;

  /* Устанавливаем связь с сервером */
  if ( ( prDisplay = XOpenDisplay ( NULL ) ) == NULL ) {
    puts ("Can not connect to the X server!\n");
    exit ( 1 );
  }

  /* Получаем номер основного экрана */
  nScreenNum = DefaultScreen ( prDisplay );

  /* Создаем окно */
  nWnd = XCreateSimpleWindow ( prDisplay,
         RootWindow ( prDisplay, nScreenNum ),
         WND_X, WND_Y, WND_WDT, WND_HGH, WND_BORDER_WDT,
         BlackPixel ( prDisplay, nScreenNum ),
         WhitePixel ( prDisplay, nScreenNum ) );

  /* Задаем рекомендации для менеджера окон */
  SetWindowManagerHints ( prDisplay, PRG_CLASS, argv, argc,
      nWnd, WND_X, WND_Y, WND_WDT, WND_HGH, WND_MIN_WDT,
      WND_MIN_HGH, WND_TITLE, WND_ICON_TITLE, 0 );

  /* Выбираем события, обрабатываемые программой */
  XSelectInput ( prDisplay, nWnd, ExposureMask | KeyPressMask );

  /* Показываем окно */
  XMapWindow ( prDisplay, nWnd );

  /* Цикл получения и обработки ошибок */
  while ( 1 ) {
    XNextEvent ( prDisplay, &rEvent );

    switch ( rEvent.type ) {
       case Expose :
         /* Запрос на перерисовку */
         if ( rEvent.xexpose.count != 0 )
           break;

         prGC = XCreateGC ( prDisplay, nWnd, 0 , NULL );

         XSetForeground ( prDisplay, prGC,
              BlackPixel ( prDisplay, 0) );
         XDrawString ( prDisplay, nWnd, prGC, 10, 50,
              "Hello, world!", strlen ( "Hello, world!" ) );
         XFreeGC ( prDisplay, prGC );
         break;

       case KeyPress :
         /* Нажатие клавиши клавиатуры */
         XCloseDisplay ( prDisplay );
         exit ( 0 );
    }
  }
}

    Для сборки программы используется команда:

cc -o hello hello.o -lX11

    Программа использует ряд функций, предоставляемых библиотекой Xlib: XOpenDisplay( ), XCreateSimpleWindow( ) и др. Их прототипы, стандартные структуры данных, макросы и константы описаны в следующих основных файлах-заголовках: "Xlib.h", "Xutil.h", "Xos.h". Эти и другие файлы поставляются вместе с X Window.

    Для обозначения переменных в книге принята нотация, пришедшая из Microsoft Windows. Идентификатор начинается с префикса, описывающего тип переменной, за которым следует ее имя. Ниже перечислены наиболее распространенные префиксы.

c - символ (байт) ,
n - байт, целое,
s - строка,
p - указатель,
r - структура.

    Перейдем к рассмотрению самой программы. Она начинается установлением связи с Х-сервером. Делает это функция XOpenDisplay( ). Ее аргумент определяет сервер, с которым надо связаться. Если в качестве параметра XOpenDisplay( ) получает NULL, то она открывает доступ к серверу, который задается переменной среды (environment) DISPLAY. И значение этой переменной и значение параметра функции имеют следующий формат: host:server.screen, где host - имя компьютера, на котором выполняется сервер, server - номер сервера (обычно это 0), а screen - это номер экрана. Например, запись kiev:0.0 задает компьютер - "kiev", а в качестве номера сервера и экрана используется 0. Заметим, что номер экрана указывать не обязательно.

    Процедура XOpenDisplay() возвращает указатель на структуру типа DISPLAY. Это большой набор данных, содержащий информацию о сервере и экранах. Указатель следует запомнить, т.к. он используется в качестве параметра во многих процедурах Xlib.

    После того, как связь с сервером установлена, программа "Hello" определяет номер экрана. Для этого используется макрос DefaultScreen(), возвращающий номер основного экрана. Переменная nScreenNum может иметь значение от 0 до величины (ScreenCount (prDisplay ) - 1). Макрос ScreenCoun() позволяет получить число экранов, обслуживаемых сервером.

    Следующий шаг - создание окна и показ его на дисплее. Для этого программа обращается к процедуре  XCreateWindow() или XCreateSimpleWindow(). Для простоты мы используем вторую процедуру, параметры которой задают характеристики окна.

PrWind = XCreateSimpleWindow (
            prDisplay, /* указатель на структуру Display,
                          описывающую сервер */
            RootWindow (prDisplay, nScreenNum),
                        /* родительское окно, в данном случае,
                           это основное окно программы */
            WND_X, WND_Y,
                        /* начальные x и y координаты верхнего
                           левого угла окна программы */
            WND_WIDTH, WND_HEIGHT,
                        /* ширина окна и высота окна */
            WND_BORDER_WIDTH, /* ширина края окна */
            BlackPixel ( prDisplay, nScreenNum ),
                       /* цвет переднего плана окна */
            WhitePixel ( prDisplay, nScreenNum )
                       /* цвет фона окна */
         );

    Для задания цветов окна используются макросы BlackPixel() и WhitePixel(). Они возвращают значения пикселов, которые считаются на данном дисплее и экране соответствующими "черному" и "белому" цветам. Функция XCreateSimpleWindow() ( XCreateWindow() ) возвращает значение типа Window. Это целое число, идентифицирующее созданное окно.

    Среди параметров функций, создающих окна, есть те, которые определяют положение окна и его размеры. Эти аргументы принимаются во внимание системой X Window. Исключение составляет случай, когда родительским для создаваемого окна является "корневое" окно экрана. В этом случае решение о положение окна и его размерах принимает менеджер окон. Программа может пытаться повлиять на решение менеджера окон, сообщив ему свои "пожелания" с помощью функций XSetStandardProperties() и XSetWMHints() (для X версии 11.3 и ниже) или XSetWMProperties() (для X версии 11.4 и выше).

    Из листинга 2.1 видно, что программа может сообщить менеджеру следующие параметры:

    Имя окна и имя пиктограммы в X11R3 и ниже передаются как строки, через параметры функции XSetStandardProperties(). В X11R4 и выше строки должны быть в начале преобразованы в "текстовые свойства", описываемые структурами типа XTextProperty. Это выполняется процедурой XStringListToTextProperty().

    Для передачи информации о желаемой геометрии окна используется структура XSizeHints().

    X Window версии 11.4 (и выше) позволяет сообщить менеджеру также следующее:

    После того, как "рекомендации" менеджеру окон переданы, программа выбирает события, на которые она будет реагировать. Для этого вызывается функция XSelectInput(). Ее последний аргумент есть комбинация битовых масок (флагов). В нашем случае - это ExposureMask | KeyPressMask. ExposureMask сообщает X Window, что программа обрабатывает событие Expose. Оно посылается сервером каждый раз, когда окно должно быть перерисовано. KeyPressMask выбирает событие KeyPress - нажатие клавиши клавиатуры. Типы событий и соответствующие им маски и структуры данных описаны в приложении 1.

    Теперь окно программы создано, но не показано на экране. Чтобы это произошло, надо вызвать процедуру XMapWindow(). Заметим, что из-за буферизации событий библиотекой Xlib, окно не будет реально нарисовано, пока программа не обратится к процедуре получения сообщений от сервера ( XNextEvent() ).

    Программы для X построены по принципу управляемости событиями. Поэтому, после того, как окно создано, заданы необходимые параметры для менеджера окон, основная ее работа - это получать сообщения от сервера и откликаться на них. Выполняется это в бесконечном цикле. Очередное событие "вынимается" процедурой XNextEvent(). Само оно есть переменная типа XEvent, который представляет собой объединение (union) структур. Каждое событие (Expose, KeyPress и т.д.) имеет свои данные (и, следовательно, свое поле в объединении XEvent) Более подробно они описаны в приложении 1.

    При получении сообщения Expose, программа перерисовывает окно. Действия начинаются созданием графического контекста - структуры, которая содержит данные, необходимые для вывода информации, в нашем случае - текста:

prGC = XCreateGC (prDisplay, prWnd, 0, NULL);

    После этого рисуется строка "Hello, world!". Более графический контекст не нужен - он уничтожается:

XFreeGC (prDisplay, prGC);

    Окно может получить несколько событий Expose одновременно. Чтобы не перерисовывать себя многократно, программа дожидается прихода последнего из них и только потом осуществляет вывод.

    Приход события KeyPress означает, что программу надо завершить: прекратить связь с сервером

XCloseDisplay (prDisplay);

    и вызвать функцию exit().


2.1.7. События.

    Когда пользователь нажимает на кнопку мыши или клавишу клавиатуры, или когда окно программы нуждается в перерисовке, или когда происходят другие изменения в системе, сервер подготавливает соответствующий пакет данных и отправляет его той или иной программе (или программам). Этот пакет данных называется событием.

    Возможных событий достаточно много. Каждое из них имеет свой тип и соответствующую структуру данных. Все они вместе, как было сказано выше, описываются объединением (union) XEvent.

    Как мы видели из примера в предыдущем пункте, программа для каждого из своих окон может выбрать события, которые будут ему передаваться. Делается это с помощью функции XSelectInput(). При вызове этой процедуры требуемые события идентифицируются соответствующими флагами. Так событию ButtonPress (нажатие кнопки мыши) соответствует флаг ButtonPressMask. Когда кнопка отпускается, сервер порождает событие ButtonRelease, которому соответствует флаг - ButtonReleaseMask.

    Некоторые события посылаются окну независимо от того, выбраны они или нет. Это:

MappingNotify - посылается, когда изменяется состояние клавиатуры (соответствие физических и логических кодов (см. п. 2.3.1.2.);

ClientMessage - так идентифицируются события, посылаемые от клиента к клиенту с помощью процедуры XSendEvent( );

SelectionClear, SelectionNotify, SelectionRequest - эти события используются в стандартном механизме общения между программами, работающими в X (описание этих событий выходит за рамки настоящего издания);

ConfigureExpose, NoExpose - эти события могут посылаться, когда клиент пытается копировать содержимое одного окна в другое.

    Программа получает события в своем основном цикле. Для этого можно использовать ряд процедур. Наиболее простая из них XNextEvent (Display *prDisplay, XEvent *prEvent). Она "вынимает" из очереди событие, находящееся в ее "голове", сохраняет информацию о нем в переменной, на которую указывает параметр prEvent, и возвращается. При этом само событие удаляется из очереди. Функция XPeekEvent() также возвращает переданное сервером событие, но не удаляет его из очереди.

    Процедура XPending() возвращает общее число событий в очереди программы.

    Итак, если событие выбрано для окна, то оно будет передано ему на обработку. А если нет? В этом случае событие передается "родителю" окна. Если и тот не желает "обращать внимание" на данное событие, то оно отправляется дальше, вверх по иерархии окон, и так до тех пор, пока либо не будет найдено окно, выбравшее это событие, либо событие не потеряется.

    Задача может влиять на этот процесс продвижения события по иерархии окон. Если программа включает флаг, соответствующий событию, в специальный атрибут окна, то оно, достигнув этого окна, не будет передано родителю, а будет тут же "снято с повестки дня". Этот атрибут - do_not_propagate (см. п. 2.1.8.).


2.1.8. Атрибуты окна.

    Окно в X Window достаточно сложное образование. Основные его характеристики перечислялись в п.2.1.2. Теперь мы поговорим о них более подробно.

    Многие атрибуты окна задаются при его создании с помощью процедуры XCreateWindow() (XCreateSimpleWindow()). В последствии параметры ножно изменить, обратившись к процедуре XChangeWindowAttributes().

    Характеристики окна описываются структурой типа XWindowAttributes. Получить их можно с помощью процедуры XGetWindowAttributes().

    Все они делятся на две группы. В первую входят параметры, доступные "на чтение" и "на запись". Вторая группа представляет собой внутренние данные. Программа может прочитать их, но не может менять.

    Сначала перечислим поля структуры XWindowAttributes, которые относятся к "изменяемым" параметрам.

    Фон окна, определяется атрибутами background_pixmap и background_pixel. Первый из них задает картинку (карту пикселов), которая используется для заливки фона окна. При необходимости картина повторяется слева - направо и сверху - вниз. Если параметр background_pixmap равен None (задается по умолчанию), то он игнорируется. Если же при этом поле background_pixel не задано (установлено по умолчанию), то окно считается "прозрачным", в противном случае его фон заливается цветом background_pixel. Атрибуты background_pixmap и background_pixel могут также принимать значение ParentRelative. В этом случае характеристики фона заимствуются у родительского окна.

    Вид края окна определяется полями border_pixmap и border_pixel. Первый атрибут определяет карту пикселов, используемую для заполнения края. Если он равен None, то край заполняется цветом border_pixel. Если же и поле border_pixel не задано, то для изображения края используются соответствующие характеристики "родителя". То же самое происходит, если параметр border_pixmap равен CopyFromParent (взять у "родителя"). Последнее значение есть значение по умолчанию.

    На перерисовку окна после изменения его размеров влияют атрибуты bit_gravity и win_gravity. Когда окно меняет размер, например, увеличивается или уменьшается, то, в принципе, нет необходимости перерисовывать все его содержимое. Часть окна остается неизменной. Правда, эта часть может поменять свое положение: переместиться вправо, влево, вверх или вниз. Поле bit_gravity "говорит" серверу, что делать с оставшейся частью изображения. Возможные значения параметра следующие:

ForgetGravity - содержимое окна перерисовывается (считается значением по умолчанию);

StaticGravity - остающаяся часть не должна менять положение по отношению к главному ("корневому" (root)) окну сервера;

NorthWestGravity - остающаяся часть смещается к левому верхнему углу;

NorthGravity - остающаяся часть смещается к верху окна;

NorthEastGravity - остающаяся часть смещается к правому верхнему углу;

WestGravity - остающаяся часть смещается к левому краю окна;

CenterGravity - остающаяся часть смещается к центру окна;

EastGravity - остающаяся часть смещается к правому краю окна;

SouthWestGravity - остающаяся часть смещается к левому нижнему углу;

SouthGravity - остающаяся часть смещается к нижнему краю окна;

SouthEastGravity - остающаяся часть смещается к правому нижнему углу.

    Параметр win_gravity "говорит" о том, что делать с подокнами окна после изменения размеров последнего. Возможные значения параметра следующие (при перечислении используются следующие обозначения: H - изменение размеров окна по горизонтали, V - изменение размеров по вертикали, (h, v) - смещение подокна на h пикселов по горизонтали и на v пикселов по вертикали):

UnmapGravity - подокна удаляются с экрана; окну посылается событие UnmapNotify, в ответ на которое оно может переместить свои подокна и показать их с помощью процедуры XMapSubWindow( );

StaticGravity - подокна остаются на месте по отношению к главному ("корневому") окну сервера;

NorthWestGravity - устанавливается по умолчанию; соответствует смещению (0, 0);

NorthGravity - смещение (H/2, 0);

NorthEastGravity - смещение (H, 0);

WestGravity - смещение (0, V/2);

CenterGravity - смещение (H/2, V/2);

EastGravity - смещение (H, V/2);

SouthWestGravity - смещение (0, V);

SouthGravity - смещение (H/2, V);

SouthEastGravity - смещение (H, V);

    Автоматическое сохранение содержимого окна, когда его часть перекрывается другими окнами, или, когда окно удаляется с экрана, определяется параметрами backing_store, backing_planes и backing_pixel. Сохраненные данные могут использоваться для восстановления окна, что значительно быстрее, чем его перерисовка программой в ответ на событие Expose. Параметр backing_store имеет следующие возможные значения:

NotUseful - (устанавливается по умолчанию) - серверу не рекомендуется сохранять содержимое окна;

WhenMapped - серверу рекомендуется спасти содержимое невидимых частей окна, когда окно показывается на экране;

Always - серверу рекомендуется сохранить содержимое окна даже, если оно не показано на экране.

    Сохранение изображений требует, как правило, довольно большого расхода памяти. Атрибуты backing_planes и backing_pixel призваны уменьшить этот расход. Первый из указанных параметров "говорит" серверу, какие плоскости изображения надо сохранять; backing_pixel означает, какой цвет использовать при восстановлении изображения в тех плоскостях, которые не сохранялись. По умолчанию backing_planes - маска, состоящая из единиц, а backing_pixel равно 0.

    Иногда при показе окна полезно спасти содержимое экрана под окном. Если окно невелико, и показывается не на долго, то это позволяет экономить время, которое надо будет затратить на перерисовку экрана после того, как окно будет закрыто. Если атрибут save_under равен True, то сервер будет пытаться сохранить изображение под окном. Если же он равен False (по умолчанию), то сервер ничего не предпринимает.

    Когда обрабатывает (или не обрабатывает) событие, последнее может быть передано его родительскому окну. Атрибут do_not_propagate_mask (по умолчанию 0) "говорит" и о том, какие события не должны доходить до "родителей".

    Изменение размеров окна и его положения на экране контролируется атрибутом override_redirect. Если он равен False, то размер окна и его положение меняются с помощью менеджера окон. Если же он равен True, то окно само решает, где ему быть, и какую ширину и высоту иметь.

    Цветовую гамму окна задает параметр colormap. Более подробная информация о цветах и цветовых палитрах приведена в 2.2.8. Значение по умолчанию - CopyFromPatent, которое "говорит", что окно использует палитру своего непосредственного "родителя".

    Теперь рассмотрим "неизменяемые" параметры окна. Строго говоря, атрибуты, о которых пойдет речь, нельзя назвать "неизменяемыми". Некоторые из них могут меняться сервером или менеджером окон. Но для обычных программ-клиентов они действительно являются таковыми.

    Положение окна и его размеры сообщают поля x, y, width и height. Они дают координаты левого верхнего угла, ширину и высоту окна соответственно. Координаты измеряются в пикселах по отношению к родительскому окну.

    Ширина края окна определяется параметром border_width.

    Маска, "говорящая" о том, какие события выбраны для передачи окну породившим его клиентом, содержится в поле флагов your_event_mask. Значение параметра образуется комбинацией флагов, идентифицирующих события (см. приложение 1.).

    Информация о дисплее, на котором показано окно, содержится в структуре Visual, на которую показывает поле visual. Эти данные, как правило, не обрабатываются обычными программами-клиентами (заметим, что для получения информации о дисплее, в системе предусмотрена процедура XGetVisualInfo( ) (см. 2.2.8.1.)).

    Класс окна сообщает поле class. Возможные значения: InputOutput и InputOnly.

    Число цветовых плоскостей дисплея (число бит-на-пиксел) помещается в поле depth.

    На информацию об экране, на котором помещается окно, указывает поле screen. Она, как правило, не используется обычными программами.

    Идентификатор главного ("корневого") окна экрана, на котором помещается окно, находится в поле root.

    Если окно имеет палитру, и она в настоящее время активна, то поле map_installed равно True, в противном случае - False.

    Видно в настоящее время окно на экране или нет, сообщает атрибут map_state.

    Маска всех событий, выбранных всеми программами для данного окна, содержится в атрибуте all_event_mask. Дело в том, что окно обрабатывается не только порождающим его клиентом, но, возможно, и другими приложениями, например, менеджером окон.

    Мы рассказали о том, как получить атрибуты окна, и что они означают. Теперь рассмотрим, как их изменить. Для этого можно использовать несколько процедур X Window, основной из которых является XChangeWindowAttributes( ), имеющая следующий прототип:

int XChangeWindowAttributes (Display *prDisplay, 
         Window nWnd, unsigned long nValueMask,
         XSetWindowAttributes *prWinAttr); 

    Требуемые установки атрибутов передаются через аргумент prWinAttr. Он указывает на переменную типа XSetWindowAttributes. Ее поля те же, что и соответствующие поля XWindowAttributes. Разница заключается лишь в разных именах некоторых из них. Так поле your_event_mask в XWindowAttributes соответствует полю event_mask в XSetWindowAttributes.

    Структура XSetWindowAttributes содержит дополнительное поле cursor. Оно определяет вид курсора мыши, когда последний находится в окне. Если поле равно None (значение по умолчанию), то используется курсор родительского окна, в противном случае значением параметра должен быть идентификатор того или иного курсора. Создание и установка курсоров подробно рассматривается в 2.3.2.2.

    Параметр nValueMask при вызове указанной процедуры представляет комбинацию флагов, "говорящих" о том, какие из полей переменной prWinAttr принимать во внимание. Поля и соответствующие им флаги перечислены в таблице 1.2.3. приложения 1.

    В следующем примере приведен фрагмент кода, в котором изменяются параметры border_pixmap и win_gravity некоторого окна:

.......
Display *prDisplay;
Window prWnd;
XSetWindowAttributes rWndAttr;
unsigned long nValMask;
Pixmap nPixmap;
.......
nValMask = CWBorderPixmap | CWWinGravity;
rWndAttr.border_pixmap = nPixmap;
rWndAttr.win_gravity = StaticSravity;
.......
XChangeWindowAttributes (prDisplay, prWnd, nValMask,
                &rWndAttr);
.......

    Отдельные атрибуты окна можно изменить более просто с помощью специальных процедур. Так функция XSetWindowBackground( ) меняет фон окна, XSetWindowBorder( ) - его край.


2.2. Текст и графика.

    В данном разделе описываются возможности, которые имеет программист для вывода текста и произвольных графических изображений. Особенностью X является то, что рисовать можно не только в окне, но и в специально подготовленной области памяти. Данная область называется картой пикселов и идентифицируется целым числом, имеющим тип Pixmap. Карта "толщиной" в один бит имеет специальное название - битовая.


2.2.1. Графический контекст.

    Прежде чем начать работу с графикой, программа должна выделить себе специальную структуру данных и получить указатель на нее. Эта структура называется графическим контекстом (Graphic Context (GC)). Указатель на GC используется в качестве одного из параметров при вызове "рисующих" функций X. Графический контекст содержит ряд атрибутов, влияющих на изображение объектов: текста, линий, фигур и др. Выделенный GC должен быть освобожден до завершения работы программы.

    Графический контекст создается процедурой XCreateGC( ), имеющей следующий прототип:

GC XCreateGC (Display *prDisplay, Drawable nDrawable,
              unsigned long nValueMask, XGCValues *prValues);

    Первый аргумент - это указатель на структуру типа Display, который программа получает после вызова XOpenDisplay( ); второй - идентификатор окна (или карты пикселов), в котором программа будет рисовать; третий - битовая маска, определяющая, какие атрибуты GC задаются; последний аргумент - структура типа XGCValues, определяемая следующим образом:

typedef struct {
	int	function;
	unsigned long	plane_mask;
	unsigned long	foreground;
	unsigned long	background;
	int	line_width;
	int	line_style;
	int	cap_style;
	int	join_style;
	int	fill_style;
	int	fill_rule;
	int	arc_mode;
	Pixmap	tile;
	Pixmap	stipple;
	int	ts_x_origin;
	int	ts_y_origin;
	Font	font;
	int	subwindow_mode;
	Bool	graphics_exposures;
	int	clip_x_origin;
	int	clip_y_origin;
	Pixmap	clip_mask;
	int	dash_offset;
	char	dashes;
} XGCValues;

    Значения полей данной структуры будут объяснены ниже. Каждому из них соответствует бит в маске, которая передается в качестве третьего параметра при вызове процедуры XCreateGC( ). Эти биты обозначаются символическими константами, определенными в файле "Xlib.h". Если бит установлен, то значение соответствующего атрибута должно быть взято из переданной XCreateGC( ) структуры XGCValues. Если бит сброшен, то атрибут приникает значение по умолчанию. Соответствие полей структуры XGCValues и флагов приведено в таблице 1.2.1. приложения 1.

    Следующий пример показывает процесс создания графического контекста, в котором устанавливаются два атрибута: цвет фона и цвет переднего плана.

. . . . . . .
GC prGC;
XGCValues rValues;
Display prDisplay;
int nScreenNum;
. . . . . . . .
rValues.foreground = BlackPixel (prDisplay, nScreenNum);
rValues.background = WhitePixel (prDisplay, nScreenNum);
. . . . . . . .
prGC = XCreateGC (prDisplay, RootWindow (prDisplay, nScreenNum),
		 (GCForeground | GCBackground), &rValues);

    Вызов XCreateGC( ) не единственный способ создания графического контекста. Так, например, новый контекст может быть получен из уже существующего GC с помощью XCopyGC( ).

    Когда контекст порожден, его атрибуты могут изменяться процедурой XChangeGC( ). Например:

rValues.line_width = 10;
XChangeGC (prDisplay, prGC, GCLineWidth, &rValues);

    Приведенный фрагмент кода меняет ширину линий, рисуемых с помощью графического контекста.

    Для того, чтобы получить значение полей GC, используется процедура XGetGCValues( ).


2.2.2. Характеристики графического контекста.

    В предыдущем разделе мы говорили, что GC имеет ряд атрибутов, воздействующих на вывод изображений. Для текста это цвет и шрифт, для линий - цвет и толщина и т.д. Как уже упоминалось выше, атрибуты контекста задаются в момент его создания. Потом они могут меняться с помощью функции XChangeGC( ). Кроме того, X поддерживает специальные функции для изменения параметров GC.

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

    Режим рисования (поле function в структуре XGCValues) указывает, каким образом комбинируются при рисовании цвет графики и цвет изображения, на которое накладывается графика. Данное поле задает некоторую логическую функцию. Возможные значения приведены в таблице 1.2.7. приложения 1.

    По умолчанию function равно GXcopy. Устанавливается режим рисования с помощью процедуры XSetFunction( ).

    Изменяемые цветовые плоскости. Каждый пиксел задается с помощью N бит. Биты с одним номером во всех пикселах образуют как-бы плоскости, идущие параллельно экрану. Получить число плоскостей для конкретного дисплея можно с помощью макроса DisplayPlanes( ). Поле plane_mask структуры графического контекста определяет, в каких плоскостях идет рисование при вызове функций X. Если бит поля установлен, то при рисовании соответствующая плоскость изменяется, в противном случае она не затрагивается.

    Цвет переднего плана и фона (поля foreground и background) задают цвета, используемые при рисовании линий текста и других графических элементов. Устанавливаются значения указанных полей функциями XSetForeground( ) и XSetBackground( ) соответственно.

    Атрибуты, влияющие на рисование линий. Шесть параметров определяют вид прямых, дуг и многоугольников, изображаемых с помощью X Window.

  1. Поле line_width задает толщину линии в пикселах. Нулевое значение поля соответствует тому, что линия должна быть толщиной в один пиксел и рисоваться с помощью наиболее быстрого алгоритма для данного устройства вывода.
  2. Поле line_style определяет тип линии. Возможные значения следующие:

    LineSolid - сплошная линия,

    LineOnOffDash - пунктирная линия; промежутки между штрихами не закрашиваются;

    LineDoubleDash - пунктирная линия; промежутки между штрихами закрашиваются цветом фона;

  1. Параметр cap_style определяет вид линии в крайних точках, если ее ширина больше 1 пиксела. На рисунке 2.3 приведены значения параметра и соответствующий вид конца линии.

pict2-3.gif (3920 bytes)

Рис. 2.3. Значения параметра cap_style графического контекста.

  1. Поле join_style определяет, как соединяются линии друг с другом. На рисунке 2.4 показаны соответствующие возможности. Параметр имеет смысл при толщине линии большей 1.

pict2-4.gif (3083 bytes)

Рис. 2.4. Значения параметра join_style графического контекста.

  1. Если линия пунктирная, то поле dashes дает длину пунктира и промежутков в пикселах.
  1. Параметр dash_offset указывает, с какого места начинать рисование 1-й черточки пунктирной линии. Рисунок 2.5 демонстрирует, как значения полей dashes и dash_offset влияют на вид линии.

Рис. 2.5. Вид линии при различных значениях параметров dashes и dash_offset графического контекста.

 

    Для установки параметров линии используется процедура XSetLineAttributes( ).

    Шрифт. Поле font определяет шрифт, используемый для вывода текста. Задать этот параметр можно с помощью процедуры XSetFont( ). Более подробно о шрифтах и работе с ними будет рассказано в 2.2.3.

    Шаблоны, используемые для заполнения рисуемых фигур. Процесс рисования включает в себя два этапа. На первом определяются пикселы, которые должны быть закрашены. После этого цвет выделенных точек изменяется. Так, для линии входящие в нее пикселы определяются по специальному алгоритму, а потом закрашиваются, например, цветом переднего плана.

    Способ закраски определяется полем fill_style. Он устанавливается процедурой XSetFillStyle( ) и воздействует на все функции, рисующие линии, текст и фигуры. Исключение составляет случай, когда выводится линия, для которой значение line_width равно 0. Возможные значения параметра fill_style перечислены ниже.

FillSolid - для закраски используются цвета переднего плана и фона.

FillTiled - для закраски используется карта пикселов, определяемая параметром tile графического контекста; при этом карта как-бы располагается в окне так, что ее левый верхний угол имеет координаты ts_x_origin и ts_y_origin; затем определяется ее пересечение с рисуемой графикой, и пикселы, попавшие в пересечение, закрашиваются; значения полей ts_x_origin, ts_y_origin устанавливаются процедурой XSetTSOrigin( ); карта tile должна иметь ту же "толщину" (число бит-на-пиксел), что и окно, в котором производится рисование.

FillStippled - для закраски используется карта пикселов, задаваемая полем stipple; данная карта должна иметь "толщину" в 1 бит; способ закраски такой же, как и в случае FillTiled с той лишь разницей, что рисуются лишь те пикселы графики, которым соответствует установленный бит в карте stipple; цвет пиксела задается полем foreground.

FillOpaqueStippled - аналогично значению FillStippled, только пикселы, для которых не установлен бит в карте stipple, закрашиваются цветом фона.

    Для задания полей tile и stipple можно использовать карты любого размера. На некоторых устройствах при определенных размерах рисование идет намного быстрее. Для получения таких размеров можно использовать процедуры XQueryBestSize( ), XQueryBestStipple( ), XQueryBestTile( ).

    Режим заполнения многоугольников указывает, как заполнять цветом многоугольники, стороны которых пересекаются. Возможные значения следующие:

EvenOddRule - заполняются точки фигуры, определяемые по следующему правилу: пусть для некоторой линии растра n1, n2, . . . , nk - стороны многоугольника, которые ее пересекают; тогда закрашиваются точки между n1 и n2, n3 и n4, и т.д.

WindingRule - заполняется вся внутренность фигуры.

    Режим заполнения дуг (поле arc_mode). Параметр задается процедурой XSetArcMode( ) и влияет на вид фигур, рисуемых процедурами XFillArc( ) и XFillArcs( ). Возможные значения параметра и соответствующие способы заливки приведены на рисунке 2.6.

Рис. 2.6. Заполнение дуг при различных значениях параметра arc_mode графического контекста.

    Влияние подокон на рисование графических примитивов определяется полем subwindow_mode. Оно устанавливается процедурой XSetSubwindowMode( ) и имеет следующие значения:

ClipByChildren - часть графики, перекрываемая подокнами, не видна;

IncludeInferiors - графика рисуется поверх всех подокон

    Генерация запроса на перерисовку при копировании частей окон (поле graphics_exposures). Когда часть окна копируется куда-либо, то вполне вероятна ситуация, что исходное изображение перекрыто, возможно не полностью, другими окнами или недоступна по другим причинам. В этом случае может быть необходимо сообщить клиенту, в окно которого происходит копирование, что часть нового изображения не может быть получена простым переносом пикселов, а должна быть перерисована. Если поле graphics_exposures равно True, то X посылает при копировании следующее:

    Если поле равно False, то событие не посылается. Устанавливается параметр процедурой XSetGraphicsExposures( ).

    Область отсечения (задается полями clip_mask, clip_x_origin, clip_y_origin). Это битовая карта, "говорящая" о том, какие пикселы выводятся, а какие нет при всех операциях рисования. Если бит карты установлен, то соответствующий пиксел появится в окне, а если бит сброшен, то пиксел будет пропущен. Положение в окне верхнего левого угла области отсечения определяется параметрами clip_x_origin и clip_y_origin (см. рис. 2.7).

 

Рис. 2.7. Параметры области отсечения и ее влияние на рисование графических примитивов.

    Эти параметры устанавливаются процедурой XSetClipOrigin( ). Сама область отсечения задается с помощью процедур XSetClipMask( ), XSetClipRectangles( ) или XSetClipRegion( ).


2.2.3. Вывод текста

    Текст был и, видимо, будет важным средством информационного обмена между программами и пользователем. X Window позволяет выводить строки в любой части окна, используя большое количество шрифтов.

  1. Функции, рисующие текст.
  2. Шрифты.
  3. Загрузка шрифтов.

2.2.3.1. Функции, рисующие текст.

    Для вывода текста используются процедуры XDrawString( ), XDrawImageString( ) и XDrawText( ). Каждая из них имеет две версии. Первая используется для шрифтов, имеющих не более 256 символов. Если же символов больше ("большие" шрифты), то применяется вторая версия. Функции, работающие с "большими" шрифтами, имеют имена XDrawString16( ), XDrawImageString16( ) и XDrawText16( ). Параметры процедур, выводящих текст, задают дисплей, окно, графический контекст, строку, ее положение и т.д. Рисование идет в соответствии с областью отсечения контекста. Буквы или их части, находящиеся за пределами области отсечения, не изображаются. Наиболее часто употребляется процедура XDrawString( ) ( XDrawString16( ) ). Ее параметры дают строку, ее длину и положение в окне. Текст рисуется цветом переднего плана, выбранного в GC.

    Функция XDrawImageString( ) ( XDrawImageString16( ) ) похожа на предыдущую процедуру с той лишь разницей, что фон символов при рисовании закрашивается цветом фона, установленного в GC. XDrawString( ) и XDrawImageString( ) выводят символы, используя шрифт, установленный в GC.

    XDrawText( ) ( XDrawText16( ) ) позволяет рисовать несколько строк сразу, используя при этом разные шрифты. Каждая рисуемая единица задается структурой XTextItem.

    Процедура XDrawText16( ) использует структуру XDrawText16.

    Поле font, в приведенных структурах (XTextItem и XDrawText16) задает шрифт, используемый для рисования. Если значение поля font - None, то применяется шрифт, выбранный в GC.

    Более подробная информация об указанных структурах и функциях находится в приложении 1.


2.2.3.2. Шрифты.

    Как мы уже говорили ранее, текст, как правило, рисуется шрифтом, выбранным в графическом контексте. X версии 11.4 и ниже поддерживает только растровые шрифты, а начиная с версии 11.5 и выше X Window имеет также и векторные шрифты.

    В растровых шрифтах каждому символу соответствует некоторый битовый шаблон, определяющий порядок закраски пикселов при рисовании. Если бит шаблона равен 1, то соответствующий элемент изображения закрашивается цветом переднего плана GC, если же он равен 0, то он закрашивается либо цветом фона, либо вообще не рисуется.

    В векторных шрифтах каждый символ описывается последовательностью линий, которые будучи составлены вместе и дают его изображение. Размеры символов варьируются от шрифта к шрифту. Для их описания используется структура XCharStruct. Основные параметры символа показаны на рис. 2.8.

ascent

ascent

Базовая линия

descent

lbearing lbearing

rbearing rbearing

width width

Рис. 2.8. Основные параметры символа шрифта.

    Сам шрифт описывается структурой XFontStruct (см. приложение 1.).


2.2.3.3. Загрузка шрифтов.

    Перед тем, как выводить текст, используя тот или иной шрифт, последний должен быть загружен в X Window и выбран в графическом контексте.

    Загрузка шрифта осуществляется процедурой XLoadFont( ). Она берет в качестве аргумента имя шрифта, находит его и возвращает программе соответствующий идентификатор. Этот идентификатор передается затем процедуре XSetFont( ), чтобы выбрать шрифт в GC. Заметим, что реально шрифт с данным именем загружается сервером лишь один раз. После этого при обращениях к XLoadFont( ) с тем же именем шрифта, функция возвращает ссылку на шрифт, уже находящийся в памяти компьютера.

    По умолчанию X ищет файл со шрифтом в директории "usr/lib/X11/fonts". Программист может задать дополнительные директории для поиска с помощью процедуры XSetFontPath( ).

    Имя шрифта в X начинается с "-" и состоит из двух частей. Между ними стоит "--". В свою очередь, каждая из частей состоит из полей - слов, разделенных "-".

    В первой части указывается следующее:

  1. изготовитель шрифтам (foundry), например adobe;
  2. семейство шрифта (font family), например courier, helvetica;
  3. жирность шрифта (weight), например bold;
  4. наклон шрифта (slant);
  5. ширина букв шрифта (width).

    Во второй части указывается следующее:

  1. размер шрифта в пикселах (pixels);
  2. размер шрифта в десятых долях "точки" ("точка" равна 1/72 дюйма );
  3. горизонтальное разрешение устройства, для которого разработан шрифт (horizontal resolution in dpi); величина измеряется в числе "точек" на дюйм;
  4. вертикальное разрешение устройства, для которого разработан шрифт (vertical resolution in dpi); величина измеряется в числе "точек" на дюйм;
  5. тип шрифта (spacing); возможные значения параметра следующие:
    m - шрифт с фиксированной шириной символов;
    p - пропорциональный шрифт с переменной шириной символов;
  6. средняя ширина символов шрифта, изморенная в десятых долях пиксела (average width);
  7. множество символов шрифта в кодировке ISO (International Standards Organisation) (character set).

    Ниже приведен пример названия шрифта.

-adobe-courier-bold-o-normal--10-100-75-75-m-60-iso8859-1

    Части имени могут заменяться символом "*" или "?". В этом случае X подбирает шрифт, сличая имена имеющихся шрифтов с предоставленным шаблоном, так, как это делается при поиске файлов в UNIX. Например, шаблону

*charter-medium-i-*-240-*

соответствуют имена

-hit-charter-medium-i-normal-25-240-75-75-p-136-iso8859-1
-hit-charter-medium-i-normal-33-240-100-75-p-136-iso8859-1

    Названия шрифтов, доступных в системе, хранятся в соответствующей базе данных. Получить список имен шрифтов можно с помощью процедуры XlistFonts( ) или XListFontsWithInfo( ). Список шрифтов, возвращаемый этими функциями, должен быть освобожден вызовом XFreeFontNames( ).

    Некоторые шрифты, такие как "fixed" или "9x15", доступны всегда.

    Получить информацию о загруженном шрифте можно с помощью функции XQueryFont( ), которая возвращает заполненную структуру типа XFontInfo( ). Одновременно загрузить шрифт и получить информацию о нем можно с помощью процедуры XLoadQueryFont( ).

    Когда информация о шрифте больше не нужна, ее следует освободить с помощью XFreeFontInfo( ). Когда становится не нужен и сам шрифт, последний надо "сбросить", обратившись к процедуре XUnloadFont( ). Функция XFreeFont( ) объединяет в себе XFreeFontInfo( ) и XUnloadFont( ).

    Следующий фрагмент кода загружает шрифт "Courier", создает GC и выводит с его помощью строку "Hellow, world!".

Display *prDisplay;
GC prGC;
Window nWnd;
XFontStruct *prFontInfo;
. . . . . . . 
/* Загружаем шрифт */
if ( (prFontInfo=
    XLoadQueryFont(prDisplay, "*-courier-*" )) == NULL){
   printf("Font not found!\n");
   exit(1);
}
. . . . . . .
/* Создаем GC и рисуем строку */
prGC=XCreateGC(prDisplay, nWnd, 0, NULL);

XSetForeground (prDisplay, prGC, BlackPixel(prDisplay, 0));
XSetFont (prDisplay, prGC, prFontInfo->fid);
XDrawString (prDisplay, nWnd, prGC, 10, 50, "Hello, world!",
            strlen ("Hello, world!") );
XFreeGC (prDisplay, prGC);
. . . . . . .
/* "Сбрасываем" шрифт */
XUnloadFont (prDisplay, prFontInfo->fid);
. . . . . . .


3.1.1. Что такое обьекты Xt.

    Как мы уже упоминали ранее, пакет представляет собой базу для создания управляющих элементов (widget). В смысле Xt widget - это просто структура данных, поля которой включают идентификатор самого элемента, идентификатор его окна, если таковое имеется, и многое другое. Атрибуты такого объекта называются ресурсами. Ресурсами widget могут быть, например, цвет фона его окна, шрифт выводимого текста, цвет границы окна и т.д.

    Каждый объект принадлежит к одному из предопределенных классов (widget class). Класс можно рассматривать как множество экземпляров (объектов), имеющих одинаковые характеристики. Классы Xt образуют иерархию (см. подробнее 3.2.1.).

    Во время работы программа создает сами объекты (экземпляры классов - widget). Они образуют совокупности, каждая из которых также представляет собой некоторую иерархию. Каждая такая иерархия называется деревом объектов (widget tree). Корнем дерева обязательно является widget, принадлежащий к одному из подклассов специального класса - Shell (в дальнейшем, говоря о таких объектах, мы иногда будет употреблять термин "shell-объект" или "shell-widget"). Если среди двух widget A и B дерева объектов первый ближе к корню, чем второй, то A является родительским объектом ("родителем") для B, а B есть подобъект (или "дочерний" объект, иногда также мы будем использовать термин "потомок") для A. Таким образом shell-объект является родительским widget для всех остальных widget данного дерева объектов. Именно он осуществляет взаимодействие программы и менеджера окон.

    Описанная иерархия widget соответствует взаимосвязи их окон, что является свойством X Window. Кроне этого, на объекты накладывается и другая иерархическая структура. Дело в том, что во время функционирования одни объекты могут управлять другими. Например, если объект A имеет два подобъекта B и C, то при изменении размеров A последний может автоматически перестроить B и C. Для того, чтобы это могло осуществиться, между widget устанавливается связь - связь "по управлению". Каждый объект может иметь один или несколько "управляемых" (managed) подобъектов.

    Теперь рассмотрим, как программа, использующая Xt, вэаимодействует с widget и X Window. Предусмотрены три механизма.

    Первый из них - процедуры обратного вызова (callback-процедуры или просто callback). Для любого класса определена совокупность действий, на которые должны реагировать принадлежащие ему объекты. Так, для любого класса предусмотрена реакция на уничтожение widget. Когда действие производится, происходит вызов либо стандартной функции Xt либо одной или нескольких процедур, предоставляемых программой. Такие функции и называются callback-процедурами.

    Второй способ взаимодействия - action-процедуры. Программа может заказать реакцию на то или иное событие (или группу событий), приходящее от X. Если событие происходит, Xt осуществляет поиск и вызов соответствующей функции.

    Третий механизм - обработчики событий (event handler). Этот способ аналогичен предыдущему, только более


3.1.2. Инициализация программы. Контекст программы.

    Программы, работающие в X, должны выполнить ряд стандартных действий, а именно: установить связь с сервером, задать необходимые свойства для менеджера окон, и выполнить массу других шагов. Если используется Xt, то все это делается одной процедурой - XtInitialize( ). Она инициализирует сам пакет, менеджер ресурсов (resourse manager) X Window, и выполняет другие необходимые операции. Для каждого приложения обращение к XtInitialize( ) или аналогичной процедуре (см. ниже) должно предшествовать всем другим вызовам процедур Xt. После завершения, XtInitialize( ) возвращает идентификатор sheel-widget, который может использоваться как корень дерева объектов программы. XtInitialize( ) имеет следующий прототип:

Widget XtInitialize ( char  *psShellName,
	       char  *psApplicationClass,
	       XrmOptionDescRec pOptions [ ], Cardinal nNumOpt,
	       Cardinal  *argc, char  *argv [ ] );

    Первый аргумент процедуры - имя создаваемого функцией shell-объекта. Эта же строка задает и имя программы. Второй аргумент - класс приложения. Как правило, имя класса программы совпадает с именем самой программы , только начинается с заглавной буквы. Менеджер ресурсов использует имя и класс программы для поиска ее параметров в базе данных ресурсов.

    Каждая программа при запуске может задать в командной строке некоторые дополнительные опции. В Xt определены "стандартные" параметры, которые могут быть переданы при запуске программы (см. 3.4.2.). Кроме этого, в командной строке могут быть и нестандартные, свойственные только данному приложению опции. XtInitialize( ) сканирует строку и размещает соответствующие значения ресурсов в базе данных. Если приложение использует нестандартные опции, то следует создать массив структур типа XrmOptionDescRec и передать указатель на него в качестве третьего параметра процедуры XtInitialize( ). При этом четвертый аргумент определяет число элементов массива. Если нестандартные параметры не используются, то в качестве третьего и четвертого параметров следует задавать NULL и 0 соответственно.

    Пятый и шестой аргументы в рассматриваемой процедуре используются для передачи данных в программу из командной строки.

    Обращение к XtInitialize( ) эквивалентно следующей последовательности вызовов (прототипы процедур см. в приложении 2 Ссылка данного издания):

XtToolkitInitialize (. . . . .);	/* Инициализация Xt */
XtOpenDisplay (. . . . .);		/* Связь с сервером */
XtAppCreateShell (. . . . .);	        /* Создание shell-widget */

    При инициализации создается также контекст программы - структура, которая хранит всю необходимую информацию о приложении. Контекст помогает программе быть менее "зависимой" от различных модификаций в операционной системе. XtInitialize( ) автоматически создает некоторый контекст (default context). Но тем не менее, начиная с X11R4, рекомендуется, чтобы каждый экземпляр программы сам создавал свой личный контекст. Для этого инициализация задачи должна делаться функцией XtAppInitialize( ).

    Обращение к XtAppInitialize( ) эквивалентно вызову процедур:

XtToolkitInitialize (. . . . .);	/* Инициализация Xt */
XtOpenDisplay (. . . . .);		/* Связь с сервером */
XtAppCreateShell (. . . . .);	        /* Создание shell-widget */
XtCreateApplicationContext (. . . . .); /* Создание контекста */


3.1.3. Первый пример.

    Приведем в качестве примера простейшую программу, открывающую окно и рисующую в нем строку "Hello, world!".

#include <X11 / Intrinsic.h>
#include <X11 / StringDefs.h>
#include <X11 / Core.h>

void DrawHellowString (Widget prWidget, XtPointer pData,
	XEvent  *prEvent, Boolean  *pbContinue)

{
    Display	*prDisplay = XtDisplay (prWidget);
    Window	nWindow = XtWindow (prWidget);
    GC		prGC;

    if (prEvent->type == Expose)
    {
       prGC = XcreateGC (prDisplay, nWindow, 0, NULL);
       XDrawString (prDisplay, nWindow, prGC, 10, 50,
	"Hello, world!", strlen ("Hello, world!") );
       XFreeGC (prDisplay, prGC);
    }
}

void main (int argc, char  **argv)
{
    Arg		args[2];
    Widget	toplevel, prCoreWidget;

    toplevel	= XtInitialize (argv[0], "Simple", NULL, 0,
			&argc, argv);
    prCoreWidget = XtCreateWidget ("Core", widgetClass,
			toplevel, NULL, 0);

    XtSetArg (args[0], XtNwidth, 100);
    XtSetArg (args[1], XtNheight, 100);
    XtSetValues (prCoreWidget, args, 2);

    XtManageChild (prCoreWidget);
    XtAddEventHandler (prCoreWidget, ExposureMask, False,
		           DrawHelloString, NULL);
    XtRealizeWidget (toplevel);
    XtMainLoop ( );
}

    Для сборки программы используется команда:

cc -o hello hello.o -lXt  -lX11

    Программа использует ряд функций, предоставляемых библиотекой Xt: XtInitialize( ), XtCreateWidget( ) и др. Их прототипы, стандартные структуры данных, макросы и константы описаны в следующем основном файле-заголовке: "Intrinsic.h" В некоторых случаях необходимо также включить файл "StringDefs.h". Если программа создает и использует какой-либо объект (widget), то она должна включить файл <имя класса widget>.h. Например, в приведенной программе есть объект класса Core, поэтому мы используем файл "Core.h" (заметим, что включение указанного файла не обязательно, т.к. файл "Intrinsic.h" его включает).

    Работа начинается с инициализации программы процедурой XtInitialize( ). Если она завершается успешно, то приложение возвращается указатель на созданный shell-объект класса ApplicationShell.

    На втором шаге создаются управляющие элементы программы. В нашем примере процедура XtCreateWidget( ) создает widget класса Core, который и является единственным "потомком" для shell-widget, созданного процедурой XtInitialize( ). (Существуют и другие способы создания widget, на которых мы остановимся более подробно в 3.2.3.).

    Вызов процедуры XtManageChild( ) делает объект prCoreWidget "управляемым" со стороны "родителя".

    После этого мы регистрируем для этого widget обработчик события Expose. Это процедура DrawHellString( ). Она рисует в окне строку "Hello, world!". Регистрация осуществляется процедурой XtAddEventHandler( ).

    После того, как объекты программы подготовлены к работе, соответствующие им окна показываются на экране:

XtRealizeWidget (toplevel);

    Далее начинается цикл получения и отправки событий:

XtMainLoop( );

    Когда программа использует только процедуры библиотеки Xlib, то она должна рассматривать каждое происходящее событие и соответствующий образом реагировать на него. Если окон у задачи несколько, то прежде чем производить какие-то действия, необходимо определить, в каком из них произошло событие. Все это достаточно утомительно. Xt берет работу на себя. XtMainLoop( ) получает очередное событие и определяет окно, для которого оно предназначено. По окну находится соответствующий widget. Для последнего определяются event handler, action-процедура или callback, зарегистрированные, как реакция на произошедшее событие. Если таковые есть, то они вызываются. Указанный выше механизм называется рассылкой событий.

    XtMainLoop( ) автоматически завершает программу по требованию менеджера окон.

    Наш пример может работать во всех версиях Xt. Но начиная с X11R4 вместо XtInitialize( ) и XtMainLoop( ) следует использовать XtAppInitialize( ) и XtAppMainLoop( )


3.1. Основы Xt.

    В данном разделе приводятся основные сведения о библиотеке Xt


3.2.1. Классы объектов.

    Каждый создаваемый программой widget есть представитель того или иного класса. Xt и основывающиеся на нем пакеты, такие как OSF/Motif, OPEN LOOK и др., имеют большое количество таких классов (заметим, что создание своих собственных widget, непредусмотренных в используемых библиотеках, требует и определения соответствующего класса, что, вообще говоря, является достаточно трудоемкой операцией; этот аспект в данном издании не будет освещаться, для получения же более детальной информации по этому вопросу следует смотреть другие аналогичные издания, например [10,12]Ссылка ).

    Каждый класс имеет ряд фиксированных характеристик, являющихся общими для всех его экземпляров (например, список callback-процедур). Значения этих характеристик у самих объектов могут различаться.

    Все классы в Xt составляют определенную иерархию (см. рисунок 3.1).

Рис. 3.1. Иерархия классов объектов Xt.

Рис. 3.1. Иерархия классов объектов Xt.

    Краткое описание каждого класса будет дано ниже. Если класс A ближе к вершине иерархии, чем класс B, то A называется базовым для B, а B называется производным классом (или подклассом) для A. Например, класс Core является базовым по отношению к Composite, а Composite есть подкласс Core.

    Подклассы наследуют характеристики всех своих базовых классов. Это означает, что экземпляр класса имеет характеристики не только своего класса, но и атрибуты всех базовых классов. Например, как видно из рис. 3.1, основным классом в Xt является класс Object, характеристики которого непосредственно наследует класс RectObj. В класс Shell переносятся атрибуты Core и Composite и т. д.

    В программах каждый класс идентифицируется переменной, которая указывает на соответствующую структуру данных. Эту переменную называют указателем на класс. Данные этой структуры заполняются при инициализации Xt.

    Указатель на класс используется в качестве второго аргумента в процедурах создания widget (например, XCreateWidget( ) и др.). Для каждого класса его указатель определен в файле <имя класса>.h. Так, например, для Composite это файл <Composite.h>, а определение переменной выглядит следующим образом:

WidgetClass compositeWidgetClass;

    Более подробную информацию о структуре класса можно найти в аналогичных изданиях.

    Рассмотрим подробнее основные классы widget.

    Object. Абстрактный класс (т.е. класс, не "порождающий собственного объекта), который используется в качестве корня дерева всех объектов.

    RectObj. Абстрактный класс, который используется для определения некоторых общих характеристик, необходимых для функционирования различных типов объектов (например, для объектов, не имеющих окна).

    Core. Корень дерева классов widget, имеющих окна. Этот класс определяет характеристики, общие для всех объектов, например такие, как размер окна widget и его положение на экране.

    Composite. Подкласс класса Core. Widget, относящиеся к данному классу, могут быть родительскими по отношению к другим объектам. Экземпляры класса Composite определяют следующие особенности поведения своих подобъектов:

  1. задает местоположение "дочерних" widget согласно тем или иным ограничениям;
  2. при уничтожении освобождает память, используемую подобъектами; (при уничтожении самого widget класса Composite сначала будут уничтожены все его "потомки");
  3. управляет появлением на экране окон своих "дочерних" widget;
  4. управляет передачей фокуса ввода между подобъектами.

    Constraint. Это подкласс класса Composite. Он представляет собой дальнейшее расширение базового класса. Его экземпляры имеют дополнительные возможности для управления размером и местоположением своих "потомков". Например, подобъекты могут размещаться в специальном порядке - в ряд, в столбец и т. д.

    Shell. Это специальный подкласс класса Composite, предназначенный для взаимодействия с менеджером окон. Widget из этого класса может иметь только одного "потомка".

    OverrideShell. Это важный подкласс класса Shell. Для окна widget данного класса атрибут override_redirect устанавливается в True. Это означает, что менеджер окон его не контролирует. Как правило, окна объектов данного класса не имеют аксессуаров, добавляемых менеджером (заголовка, рамки), и используются в основном для создания pop up меню.

    WMShell. Это специальный подкласс класса Shell, который содержит дополнительные поля, необходимые для взаимодействия с менеджером окон.

    VendorShell. Этот класс предназначен для того, чтобы дать возможность взаимодействия со специальными менеджерами окон.

    TopLevelShell. Widget данного класса, как правило, используются как shell-объекты дерева объектов.

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

    ApplicationShell. Это подкласс класса TopLevelShell. Программа может иметь, как правило, только один экземпляр, принадлежащий классу ApplicationShell


3.2.2. Атрибуты (ресурсы) объектов.

    Каждый из классов widget, а следовательно, и создаваемые на его основе объекты, имеют достаточно большое количество параметров. В терминах Xt они называются "ресурсы". Подробно атрибуты каждого из классов описаны в приложении 3 Ссылка данного издания.

    Ресурсы объектов однозначно идентифицируются символическими константами, начинающимися с букв "XtN". Например, класс Core имеет параметры XtNx, XtNy, XtNwidth, XtNheight, определяющие начальное положение окна на экране и его размеры. Класс Shell имеет атрибут XtNsaveUnder


3.2.3. Управление объектами.

    Для создания любого объекта, кроме shell-widget, можно использовать процедуру

Widget XtCreateWidget (String psName, WidgetClass pClass,
         Widget prParent, ArgList arArgs, Cardinal nNumArgs);

    Здесь первый аргумент - это произвольная строка, содержащая имя создаваемого widget. Оно используется для получения значений ресурсов объекта из базы данных ресурсов, а также для других целей. Второй аргумент задает класс widget. Третий аргумент - это родительский объект, который должен быть уже создан. В качестве последнего может быть указан shell-widget или любой объект, который может иметь подобьекты (принадлежит классу Composite или его подклассам). Четвертый и пятый аргументы XtCreateWidget( ) определяют значения дополнительных ресурсов, задаваемых при создании widget. Тип ArgList определен в файле "Intrinsic.h" следующим образом:

typedef struct {
         String	        name;
         XtArgVal	value;
} Arg, *ArgList;

    Здесь name представляет предопределенную строковую константу, задающую имя соответствующего ресурса (например XtNwidth). value - это значение атрибута. Это поле имеет специальный тип XtArgVal, определение которого является системно-зависимым. Как правило, размер данного поля достаточен для того, чтобы поместить туда целое число или указатель. Но его не достаточно, например, для задания чисел типа double. Общее правило таково: если размер значения ресурса меньше sizeof (XtArgVal), то в value содержится непосредственно само значение, в противном случае поле value есть адрес области памяти, содержащей значение ресурса. Если аргументы arArgs и nNumArgs в XtCreateWidget( ) не используются, то они должны быть равны NULL и 0 соответственно.

    Если ресурс не найден в списке arArgs, то его значение ищется в базе данных ресурсов программы. В случае когда его нет и там, параметр получает значение по умолчанию.

    XtCreateWidget( ) возвращает идентификатор созданного объекта. В следующем примере создается widget класса Core, имеющий ширину и высоту равные 100 и 150 пикселов соответственно.

. . . . . . .
Arg	aReserv [ ] = { { XtNwidth, (XtArgVal) 100 },
			{XtNheight, (XtArgVal) 150 },
		      };
 /* Ресурсы и их значения */ 
Widget prShellParent;	 /* Родительский объект */ 
Widget prCoreWidget;	 /* Идентификатор объекта */  
. . . . . . .

prCoreWidget = XtCreateWidget ("core", widgetClass,
	    prShellParent, aReserv, XtNumber (aReserv));
. . . . . . .

Здесь XtNumber( ) - макрос, определяющий размер массива фиксированной длины.

    Заметим также, что значение ресурса в массиве aReserv должно иметь тип, соответствующий данному ресурсу.

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

. . . . . . .
Arg	aReserv [5]; 	/* Используется для задания ресурсов */

int	i = 0;
Widget prShellParent;	 /* Родительский объект */ 
Widget prCoreWidget;	 /* Идентификатор объекта */  
. . . . . . .

XtSetArg (aReserv [i], XtNwidth, 100);
i++;
XtSetArg (aReserv [i], XtNheight, 150);
i++;
prCoreWidget = XtCreateWidget ("core", widgetClass,
	    prShellParent, aReserv, i);
. . . . . . .

    В X11R4 и выше задавать ресурсы при создании widget можно более удобно. Для этого надо использовать процедуру

XtVaCreateWidget (String psName, WidgetClass prClass,
        Widget prParent, . . .);

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

. . . . . . .
Widget prShellParent;	 /* Родительский объект */ 
Widget prCoreWidget;	 /* Идентификатор объекта */  
. . . . . . .

prCoreWidget = XtCreateWidget ("core", widgetClass,
    prShellParent, XtNwidth, 100, XtNheight, 150, NULL);
. . . . . . .

    Каждый родительский widget (за исключением shell-widget) управляет своими "потомками", т.е. распоряжается их расположением, размером, получением фокуса ввода и т.д. Например, в OSF/Motif некоторые объекты располагают свои "дочерние" widget в ряды и колонки, другие группируют "потомков" в "блоки" и т.д. Для того, чтобы "дочерний" мог управляться своим "родителем", его необходимо включить в соответствующий список. Для этого используется процедура

void XtManageChild (Widget prWidget);

    Здесь единственный аргумент идентифицирует widget, которым будет управлять "родитель".

    Xt предусматривает более удобную процедуру XtCreateManagedWidget( ) (XtVaCreateManagedWidget( )), которая создает widget и после этого автоматически вызывает функцию XtManageChild( ). Аргументы указанной процедуры полностью идентичны аргументам процедуры XtCreateWidget( ) или XtVaCreateWidget( ). Заметим, однако, что иногда более удобно создать сначала группу "дочерних" widget, а затем сразу всех включить в список управляемых (managed) объектов. Для этого можно воспользоваться функцией

void XtManageChildren (WidgetList prWidgetChildren,
        Cardinal nChildrenNumber);

    Здесь первый и второй аргументы задают массив подобъектов и число элементов в этом массиве соответственно.

    Процедура XtRealizeWidget (Widget prWidget) создает окна заданного widget, его управляемых подобъектов и показывает их на экране. Этот процесс называется "реализацией" объекта. Заметим, что нельзя реализовать widget, если это не сделано предварительно с его "родителем".

    Если объект реализован, то вызов функции XtManageChild( ) для любого его подобъекта автоматически реализует последний.

    Целый ряд процедур Xt позволяют получать различную информацию о widget. Среди таких процедур укажем следующие.

    Процедура XtIsRealized (Widget prWidget) позволяет проверить, реализован данный объект или нет.

    Процедура XtDisplay (Widget prWidget) возвращает указатель на X структуру данных типа Display, которая используется данным widget.

    Процедура XtScreen (Widget prWidget) возвращает для заданного объекта указатель на X структуру данных типа Screen.

    Процедура XDestroyWidget (Widget prWidget)


3.2.4. Модификация и чтение ресурсов объекта.

    Для модификации ресурсов уже созданного widget приложение может использовать процедуру

void XtSetValues (Widget prWidget, ArgList prArgs,
Cardinal nNumArgs);

    Здесь prWidget - объект, ресурсы которого устанавливаются, второй и третий аргументы содержат список пар: "имя ресурса/его значение" и число таких пар соответственно. Например, следующий фрагмент кода позволяет установить ширину и высоту widget класса Core:

. . . . . . .
Arg	aReserv[5]; 	/* Используется для задания ресурсов */

int	i = 0;
Widget prShellParent;	 /* Родительский объект */ 
Widget prCoreWidget;	 /* Идентификатор объекта */  
. . . . . . .
prCoreWidget = XtCreateWidget ("core", widgetClass,
    		prShellParent, NULL, 0);

XtSetArg (aReserv[i], XtNwidth, 100);
i++;
XtSetArg (aReserv[i], XtNheight, 150);
i++;
XtSetValues (prCoreWidget, aReserv, i);
. . . . . . .

    X11R4 и выше поддерживает также функцию

void XtVaSetValues (Widget prWidget, . . . );

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

. . . . . . .
Widget prShellParent;	 /* Родительский объект */ 
Widget prCoreWidget;	 /* Идентификатор объекта */  
. . . . . . .
prCoreWidget = XtCreateWidget ("core", widgetClass,
    		prShellParent, NULL, 0);

XtVaSetValues (prCoreWidget, XtNwidth, 100,
	          XtNheight, 150, NULL);
. . . . . . .

    Xt позволяет получить текущее значение ресурса widget, используя процедуру

void XtGetValues (Widget prWidget, ArgList prArgs,
Cardinal nNumArgs);

    Здесь prWidget - это объект, значения ресурсов которого будут получены. Второй аргумент содержит список пар, каждая из которых задает имя ресурса и адрес, по которому будет сохранено его значение. Третий аргумент есть число таких пар. Например, приводимый ниже фрагмент кода позволяет получить текущее значение высоты созданного объекта.

. . . . . . .
Arg	     aReserv[5];
Dimension  nHeight;
Widget	      prWidget;
. . . . . . .
XtSetArg (aReserv[0], XtNheight, &nHeight);
XtGetValues (prWidget, aReserv, 1);
. . . . . . .

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

    Начиная с X11R4 можно использовать процедуру

void XtVaGetValues (Widget prWidget, . . .);

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

	. . . . . . .
	Dimension  nHeight;
	Widget	     prWidget;
	. . . . . . .
	XtVaGetValues (prWidget, XtNheight, &nHeight, NULL);
	. . . . . . .


3.2.5.1. Процедуры обратного вызова (callback).

    С каждым widget сопоставлен набор операций, которые над ним можно производить. Так, любой элемент может быть создан и уничтожен, "нажимаемая" кнопка (push button) может быть "нажата" с помощью мыши или клавиши "пробел" и т. д. Каждому из таких предопределенных действий соответствует ресурс объекта, значение которого - список процедур библиотеки Xt и самой программы, которые автоматически вызывактся, когда совершается предусмотренное действие. Эти процедуры и называют процедурами обратного вызова (callback-процедуры). Рассмотренные нами ресурсы widget имеют тип XtCallbackList.

    Все widget имеют по крайней мере один список callback. Он соответствует операции уничтожения объекта и наследуется от класса Core. Имя ресурса XtNdestroyCallback.

    Программа может добавлять и убирать процедуры в (из) список (списка) callback. Добавить функцию в список можно с помощью процедуры

XtAddCallback (Widget prWidget, String psCallbackName,
         XtCallbackProc pProcedure, XtPointer pUserData);

    Здесь prWidget задает объект, список которого модифицируется, psCallbackName есть имя списка, pProcedure добавляемая функция, pUserData - указывает на данные, передаваемые callback-процедуре при ее вызове.

    Каждая процедура обратного вызова должна иметь прототип:

void CallbackProc (Widget prWidget, XtPointer pUserData,
	XtPointer pCallData);

    Здесь первый аргумент задает widget, в списке которого находится функция. Второй аргумент указывает на данные, передаваемые из программы в процедуру. Третий аргумент указывает на данные, подготовленные для callback самой Xt или пакетом, построенным на ее основе (например, OSF/Motif). Обычно это структура, содержащая дополнительную информацию о действии, приведшем к вызову функции.

    Примеры использования callback-процедур для различных классов объектов приведены в главе 4.

    Для модификации списка процедур обратного вызова можно также использовать функцию

void XtAddCallbacks (Widget prWidget,
	String psCallbackName, XtCallbackList pCallbacks);

которая позволяет задавать несколько callback одновременно для одного и того же списка.

    Здесь первый аргумент задает widget, чей список callback-процедур меняется. Его имя определяется вторым аргументом функции. Третий аргумент задает сами процедуры и передаваемые им параметры. Этот аргумента имеет специальный тип, определяемый следующим образом:

typedef struct {
  XtCallbackProc  callback;	/* Указатель на процедуру */ 
  caddr_t         client_data;	/* Указатель на данные */
}  XtCallbackRec, *XtCallbackList;

    Здесь первое поле задает callback-процедуру , а второе поле используется для передачи ей при вызове некоторых данных. При обращении к XtAddCallbacks( ) последний элемент массива pCallbacks должен быть равен NULL.

    Удалить процедуру из списка callback можно с помощью функции XtRemoveCallback( )


3.2.5.2. Использование action-процедур.

    Описанный в предыдущем пункте механизм использования callback-процедур достаточно прост и удобен, но имеет и существенный недостаток. В частности, указанные процедуры не могут вызываться в ответ на действие, не определенное для данного класса объектов. Например, в OSF/Motif класс XmDrawingArea (область для рисования) поддерживает список callback- процедур, вызываемый, когда widget получает фокус ввода. Однако, этого явно недостаточно, чтобы реализовать более сложное взаимодействие с пользователем.

    Наряду с callback-процедурами, Xt Intrinsics поддерживает еще один механизм, позволяющий пользователю связывать код программы с widget. Указанный механизм состоит в использовании процедур - "ответов на действия" (action-процедур).

    Как уже отмечалось ранее, action-процедура - это функция, которая вызывается в ответ на определенные события, порождаемые сервером. Они, в свою очередь, могут быть связаны с теми или иными действиями пользователя - нажатием на кнопку мыши, нажатие клавиш клавиатуры и т. д.

    Процесс создания и подключения таких процедур состоит из следующих шагов, описываемых ниже.

  1. Сначала создается специальная "таблица действий" (action table), связывающая символические имена "действий" (action) с указателями на соответствующие action-процедуры.
  2. Регистрация созданной "таблицы". Для этого можно использовать процедуру XtAppAddActions( ) или XtAddActions( ).
  3. Создание "таблицы соответствия" (мы также будем употреблять термин "таблица трансляции", английский эквивалент - translation table). Каждый ее элемент есть пара "последовательность событий / имя (или имена) "действия". "Таблица" размещается в файле ресурсов программы или непосредственно в ее коде. В последнем случае перед использованием "таблица" должна быть переведена процедурой XtParseTranslationTable( ) во внутренне представление Xt и зарегистрирована для объекта с помощью XtAugmentTranslations( ) или XtOverrideTranslations( ). Подробно формат "таблицы соответствия" описан ниже.

    С учетом представленных шагов вызов action-процедуры, когда в widget происходит какое-то событие (события) , производится следующим образом:

    Рассмотрим более подробно, что представляют из себя "таблица действий" и "таблица трансляции".

    Первая из них - это массив структур типа XtActionRec, который определяется следующим образом:

typedef struct {
   String	     string;
   XtActionProc	     proc;
} XtActionRec,      *XtActionRecList;

    Здесь первое поле есть символическое имя "действия". Указатель на соответствующую процедуру задается полем proc. Например, "таблица действий", содержащая процедуру KeyboardPrint( ), вызываемую, когда происходит "действие" с именем "KeyboardPrint", может быть определена следующим образом:

static void KeyboardPrint( );
static XtActionRec paAct[ ] = {
	{ "KeyboardPrint", KeyboardPrint },
};

    Сделаем ряд замечаний.

    Если используется множество управляющих элементов OSF/Motif, то следует помнить, что каждый класс widget указанного множества предусматривает набор стандартных action- процедур, которые могут использоваться всеми экземплярами объектов данного класса. Функции же, регистрируемые с помощью XtAppAddActions( ) (XtAddActions( )), могут быть использованы произвольным widget любого класса данной программы.

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

    Имена "действия" и реализующей его процедуры в принципе могут не иметь ничего общего. Но для удобства принято, чтобы они совпадали.

    Каждая action-процедура должна иметь следующий прототип:

void ActionProc (Widget prW, XEvent *prEvent,
	String *psParams, Cardinal *nNumParams);

    Здесь первый аргумент - это widget, для которого зарегистрирована данная процедура. prEvent - указатель на событие, инициировавшее ее вызов. Если процедура вызывается в ответ на последовательность событий, то в функцию передается первое из них. Третий и четвертый аргументы задают соответственно список параметров, описанных в "таблице трансляции" (см. ниже), и их число.

    Как уже указывалось ранее, после регистрации action-процедуры, например, с помощью XtAppAddActions( ), последнюю могут использовать произвольные widget данной программы. У каждого объекта есть ресурс XtNtranslations, значение которого - "таблица трансляции", представленная во внутреннем формате Xt.

    В ресурсном файле или в исходном коде программы эта "таблица" представляется как строка, содержащая пары "последовательность событий / имя (или имена) действия", разделенные символом '\n'. В начале строки может стоять директива "#replace", "#augment" или "#override". Первая "говорит", что "таблица" должна заменить уже имеющуюся у widget "таблицу". Вторая и третья означают, что новая "таблица" должна быть объединена с "таблицей", уже имеющейся у widget. При этом, если указана директива "#augment", то, если в "таблицах" имеются записи, соответствующие одному и тому же "действию", то приоритет имеет старая запись. Если указана директива "#override", наоборот - новая.

    Каждая пара "последовательность событий / имя (или имена) действия" (в дальнейшем мы будем использовать также термин "трансляция") имеет следующий вид:

[модификатор, . . , модификатор]<событие>, . .<событие>[ (число 
повторений события)] [детализация] : имя action([аргументы]) 
[имя action([ аргументы)  . . . . ] 

Здесь элементы, заключенные в индексные скобки[ ] , являются необязательными.

    В поле "модификатор" могут использоваться стандартные модификаторы, поддерживаемые Xt Intrinsics, например: Ctrl, Shift, Lock и др. Список используемых в "таблице трансляции" модификаторов приводится ниже.

МодификаторОписание
CtrlКлавиша Control
ShiftКлавиша Shift
LockКлавиша Caps Lock
MetaКлавиша Meta
HyperКлавиша Hyper
SuperКлавиша Super
AltКлавиша Alt
Mod1, . . . , Mod5Дополнительные клавиши-модификаторы
Button1, . . . , Button5Кнопки мыши

    В поле <событие> должно стоять имя события X Window. Эти имена приведены в приложении 1Ссылка.

    Если модификаторов в "трансляции" нет, то указанные в записи "действия" и соответствующие им процедуры будут вызываться в ответ на определенные в угловых скобках события или последовательности событий независимо от того, в каком состоянии находятся клавиши-модификаторы и кнопки мыши, например:

<KeyPress> : KeyboardPrint( )

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

    Для конкретизации различных событий предусмотрено специальное поле "детализация". Так для событий от клавиатуры в этом поле указывается символ клавиши, определенный в файле "keysymdef.h" (префикс "XK_" опускается). Например, следующая запись

<KeyPress>a : KeyboardPrint( )

используется для определения события, соответствующего нажатию клавиши "a". Для событий ButtonPress, ButtonRelease, MotionNotify "детализация" есть номер нажатой кнопки.

    Для событий EnterNotify, LeaveNotify, FocusIn, FocusOut в поле "детализация" может быть использован любой из следующих символов:

Normalдля событий с обычным режимом получения и передачи фокуса ввода;
Grab для событий, когда устройство "захвачено";
Ungrabдля событий, возникающих при отмене "захвата".

Для событий ClientMassage "детализация" - тип сообщения (число).

    Для того, чтобы облегчить задание "таблицы трансляций", Xt предусматривает специальную аббревиатуру для некоторых событий.

АббревиатураСмысл
CtrlСобытие KeyPress, когда нажата клавиша "Control".
MetaСобытие KeyPress, когда нажата клавиша "Meta".
ShiftСобытие KeyPress, когда нажата клавиша "Shift".
Btn1Down, . . ., Btn5DownСобытие ButtonPress для 1, . . ., 5-й кнопки мыши.
Btn1Up, . . ., Btn5UpСобытие ButtonRelease для 1, . . ., 5-й кнопки мыши.
BtnMotionСобытие MotionNotify.
Btn1Motion, . . ., Btn5MotionСобытие MotionNotify когда нажата 1, . . ., 5-й кнопка мыши.

Например, запись:

<Btn2Down> : StartWindowing( )

означает, что в ответ на нажатие второй кнопки мыши, вызывается "действие" StartWindowing( ).

    Xt поддерживает набор специальных символов, употребляемых с модификаторами для расширения возможностей использования последних .

СимволОписание
None Ни одна клавиша-модификатор не должна быть нажата.
! Событие должно сопровождаться только указанными модификаторами и никакими другими.
: Позволяет различать события от нажатия клавиш в зависимости от регистра.
~ Модификатор, непосредственно следующий за данным символом, не должен быть в нажатом состоянии.

Например, запись

!Alt<Key>a : KeyboardPrint( )

означает, что соответствующая процедура будет вызвана только при нажатии комбинации <Alt+a>. Применение символа "None" эквивалентно использованию в "трансляции" символа "!" без модификаторов, например, следующие записи эквивалентны:

None<Key>a : KeyboardPrint( )

и

!<Key>a : KeyboardPrint( )

Это означает, что "действие" будет реализовано, когда клавиша "a" нажата, а все модификаторы находятся в не нажатом состоянии.

    Использование в "трансляции" символа ":" позволяет различать события, соответствующие нажатию клавиш на разных регистрах: верхнем или нижнем (т.е. при нажатой или освобожденной клавише Shift или Caps Lock). Дело в том, что, например, следующие записи эквиваленты по выполняемому действию:

<Key>b : KeyboardPrint( )

и

<Key>B : KeyboardPrint( )

т.е. соответствуют вызову процедуры при нажатии клавиши "b" или комбинации <Shift+b>. Использование символа ":" позволяет различить эти две ситуации, в частности, следующие записи будут уже не идентичны:

: <Key>b : KeyboardPrint( )

и

: <Key>B : KeyboardPrint( )

первая определяет вызов соответствующей процедуры при нажатии клавиши "b"; вторая же "трансляция" соответствует вызову функции при нажатии комбинации < Shift+b>.

    Символ "~" используется для отрицания модификаторам т.е. следующий за символом модификатор не должен быть нажат. Например запись:

Button2<Key> : KeyboardPrint( )

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

~Button2<Key> : KeyboardPrint( )

"говорит", что соответствующая функция будет вызвана, когда нажимается произвольная клавиша, и, если при этом не нажата вторая кнопка мыши. Заметим, что использование символа "~" относится только к модификатору, непосредственно следующему за указанным символом, и не распространяется на остальные модификаторы. Например, "трансляция"

~Ctrl Alt<Key>a : KeyboardPrint( )

указывает, что соответствующая функция будет вызвана при нажатии комбинации <Alt+a>, если при этом не нажата клавиша Control.

    Модификаторы, расположенные в начале "трансляции", относятся ко всем событиям, указанным в ней. Например, следующие записи эквивалентны:

Shift<Btn2Down>, <Btn2Up> : KeyboardPrint( )

и

Shift<Btn2Down>, Shift<Btn2Up> : KeyboardPrint( )

    Для определения в "трансляциях" событий от кнопок мыши используются модификаторы Button1, Button2, Button3, Button4 и Button5, например, следующая запись

Button2<Key>a : KeyboardPrint( )

означает, что соответствующая процедура будет вызвана, когда нажимается вторая кнопка мыши и клавиша "a".

    Каждая "трансляция" может определить одно или несколько событий, разделяемых запятыми. Например, для определения последовательности событий, соответствующих нажатию и отпусканию первой кнопки мыши, можно использовать следующую строку:

<Btn1Down>, <Btn1Up> : KeyboardPrint( )

    Для задания нескольких нажатий и отпусканий используется поле "число повторений события". Так, например, "трансляция"

<Btn2Up> : KeyboardPrint( )		Вероятна опечатка

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

<Btn2Down>, <Btn2Up>, <Btn2Down>, <Btn2Up> :
KeyboardPrint( )

    Для задания большего числа повторений можно использовать символ "+", например, следующая запись

<Btn3Up>(2+) : KeyboardPrint( )

означает, что соответствующая функция будет вызвана при 2-х и более нажатиях на третью кнопку мыши (максимальное число, которое может быть указано в поле "повторений" - 9 ). По умолчанию временной интервал, во время которого считаются нажатия кнопки мыши, составляет 200 миллисекунд. Если между очередным нажатием кнопки и следующим проходит меньше 200 миллисекунд, то система рассматривает это как одно двойное нажатие (double click) соответствующей кнопки мыши. Для изменения указанного интервала используется ресурс объекта XtNmultiClickTime. Он может быть установлен функцией XtSetMultiClickTime( ).

    Поле "аргументы" в "трансляции" используется для задания третьего и четвертого аргументов action-процедуры. Например, если "трансляция" была определена следующим образом:

<Key>a : KeyboardPrint(1, JUSTAS, ALEX)

то при вызове соответствующей функции аргумент psParams будет указывать на список, содержащий строки "1", "JUSTAS" и "ALEX", а аргумент nNumParams будет равен 3.

    Ниже приведены примеры задания "таблицы соответствия" в файле ресурсов

justas*mywidget.translations : #augment \
  	<Btn2Down>              : Arm( )\n\
        <Btn2Down>, <Btn2Up>    : Activate( ) Disarm( )\n\
        <Btn2Down>(2+)          : MultiArm( )\n\
        <Btn2Up>(2+)            : MultiActivate( )\n\
        <LeaveWindow>           : Leave( )

и коде программы:

static  char *psTable : "#augment \
        <Btn2Down>              : Arm( )\n\
        <Btn2Down>, <Btn2Up>    : Activate( ) Disarm( )\n\
        <Btn2Down>(2+)          : MultiArm( )\n\
        <Btn2Up>(2+)            : MultiActivate( )\n\
        <LeaveWindow>           : Leave( )";

    Как мы уже упоминали ранее, если "таблица" задана в коде программы, то перед использованием она должна быть переведена во внутренний формат Xt процедурой XtParseTranslationTable( ). Регистрируется "таблица" с помощью функций

XtAugmentTranslations (Widget prWidget,
	           XtTranslations pTranslationsTable);

или

XtOverrideTranslations (Widget prWidget,
	         XtTranslations pTranslationsTable);

обе они регистрируют "таблицу трансляции" для объекта, заданного widget, объединяя "таблицу" pTranslationsTable, с "таблицей", уже имеющейся у widget. При этом вторая процедура замещает существующие "трансляции" на новые, а первая оставляет их без изменения.

    Следующий пример показывает, как добавить action-процедуру к объекту.

. . . . . . . .
Widget prWidget;
static    XtActionRec actions [ ] = ("PressMe", KeyboardPrint);
static    char  UserTranslations [ ] =
	"Alt <Key>a : PressMe"; 
. . . . . . . .
XtAugmentTranslations (prWidget,
         XtParseTranslationTable (UserTranslations));
. . . . . . . .

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

<Key>             : InputSymbol( ) \n\
<Key>Return       : EndInput( )

то процедура, соответствующая "действию" InputSymbol


3.2.5.3. Обработчики событий.

    В Xt предусмотрен еще один механизм использования процедур для выполнения определенных действий при наступлении тех или иных событий. Это механизм обработчиков событий (event handler). Он позволяет определять отдельное событие (или их группу), при наступлении которого будет вызвана указанная программой процедура-обработчик. Обработчики событий менее гибки, чем action-процедуры, но зато значительно быстрее. Последнее происходит потому, что при вызове event handler не требуется поиск процедур по соответствующим "таблицам". В этом пункте будут рассмотрены отдельные аспекты работы с event handler.

    Для того чтобы программа могла использовать данную процедуру как обработчик событий, ее необходимо зарегистрировать специальным образом. Делается это с помощью процедуры XtAddEventHandler( ) или XtInsertEventHandler( ). Первая из них имеет следующий прототип:

void XtAddEventHandler (Widget prWidget,
	EventMask nEventMask, Boolean nNonMaskable,
	XtEventHandler pHandler, XtPointer pUserData);

    Здесь prWidget - идентифицирует объект, к которому будет добавлена процедура- обработчик pHandler. nEventMask - комбинация флагов, задающая события, в ответ на которые будет вызываться регистрируемая процедура (события и соответствующие им флаги приведены в приложении 1)Ссылка. Третий аргумент в процедуре XtAddEventHandler( ) - это логическая переменная равная True, если обработчик будет вызываться для событий, которые всегда посылаются программе. Это MappingNotify, ClientMessage, SelectionClear, SelectionNotify, SelectionRequest (заметим, что последние три события связаны с механизмом общения между программами через системный буфер (clipboard), описание которого выходит за рамки настоящего издания). Наконец, последний аргумент процедуры - pUserData - это указатель на данные, передаваемые в обработчик при его вызове. Если таковых нет, то аргумент должен быть равен NULL.

    Заметим, что для одного и того же события в списке обработчиков может быть зарегистрировано несколько функций. XtAddEventHandler( ) добавляет функцию pHandler в конец этого списка.

    Процедура, используемая как обработчик событий, должна иметь следующий прототип:

void HandlerProc (Widget prWidget, XtPointer pUserData,
	          XEvent  *prEvent, Boolean *pnContinue);

    Здесь первый аргумент указывает на объект, для которого заказана реакция на событие. Второй аргумент указывает на данные, передаваемые в обработчик из программы. Он равен аргументу pUserData, задаваемому при вызове функции XtAddEventHandler( ). Третий аргумент - указатель на событие, инициировавшее обращение к event handler. И, наконец, последний аргумент указывает, следует ли системе вызывать следующие зарегистрированные для данного события обработчики или нет. Перед вызовом процедуры система делает этот параметр равным True. Если программа не желает, чтобы событие обрабатывалось дальше, значение переменной, на которую указывает pnContinue, должно быть изменено на False.

    Функция XtInsertEventHandler( ) также заносит процедуру в список обработчиков того или иного события, но она позволяет поместить ее в произвольное место списка.

    XtInsertEventHandler( ) имеет следующий прототип:

void XtInsertEventHandler (Widget prWidget,
        EventMask nEventMask, Boolean nNonMaskable,
        XtEventHandler pHandler, XtPointer pUserData,
        XtListPosition nPosition);

    Первые пять аргументов полностью идентичны описанным выше для процедуры XtAddEventHandler( ). Последний аргумент - указывает, в какое место списка обработчиков будет помещена функция pHandler. Аргумент может принимать одно из двух предопределенных значений: XtListHead, которое означает, что функция будет помещена в "голову" списка и, следовательно, будет вызываться раньше любого event handler, зарегистрированного для данного объекта и события. Второе значение - XtListTail, которое указывает, что функция будет помещена в конец списка и будет вызываться после всех зарегистрированных процедур.

    Как мы упоминали ранее, для одного и того же события можно зарегистрировать несколько event handler, но при этом каждая функция с одинаковыми параметрами pUserData может встретиться в списке только один раз. Однако, если та же самая функция регистрируется с помощью XtInsertEventHandler( ), то она будет добавлена в список, но передвинута либо в конец списка либо в его начало.

    Вызов процедур XtAddEventHandler( ) и XtInsertEventHandler( ) может осуществляться до и после реализации соответствующего widget.

    Ниже приводится фрагмент кода, в котором с помощью функции XtAddEventHandler( ) регистрируется функция-обработчик, которая будет вызываться при изменении фокуса ввода для данного widget. Ее можно использовать для выполнения определенных действий при наступлении соответствующих событий.

. . . . . . .
void CheckFocus (Widget prWidget, XtPointer pUserData,
	XEvent  *prEvent)
{
   . . . . . . . .
   if (event->type == FocusIn)
   {
       /* Выполняются действия при получении фокуса ввода */
       . . . . . . .
   }
   else
   {
       /* Выполняются действия при потере фокуса ввода */
        . . . . . . 
   }
}
. . . . . . . .
main (int argc, char  *argv)
{
    . . . . . . . .
    XtAddEventHandler (prCoreWidget, FocusChangeMask, False,
		           (XtEventHandler) CheckFocus, NULL);
    . . . . . . . .
}

    Когда процедура-обработчик становится не нужной, ее можно удалить из списка. Для этого можно использовать процедуру

void XtRemoveEventHandler (Widget prWidget,
	EventMask nEventMask, Boolean nNonMaskable,
	XtEventHandler pHandler, XtPointer pUserData);

    Она имеет те же самые аргументы, что и процедура XtAddEventHandler( ). Если параметры в вызовах этих процедур не совпадают, то удаления указанного обработчика не произойдет. Так происходит, например, если параметр pUserData при вызове XtRemoveEventHandler( ) не совпадает с заданным при обращении к XtAddEventHandler( )


3.2.5. "Динамические" ресурсы обьектов.

    Описанные нами ресурсы widget в большинстве своем задают "статику" объекта: размер, цвет и т. д. Но дело в том, что управляющий элемент предназначен для того, чтобы дать возможность пользователю производить определенные действия (например, вводить текст, выбирать элементы из списков данных, рисовать график и т.д.). Кроме того, widget должен соответствующим образом реагировать на запросы о перерисовке и прочая и прочая и прочая. Следовательно, управляющие элементы должны иметь возможность вызывать процедуры, осуществляющие эти действия. При этом ясно, что процедуры, поставляемые самой системой можно использовать только для выполнения некоторого набора стандартных действий. Для возможности же реализации разнообразных специфичных операций приложению необходимо предоставить механизмы, позволяющие устанавливать связь между управляющими элементами и процедурами самой программы, которые выполняют необходимые функции. В Xt предусмотрены следующие три способа (механизма) осуществления такой связи и передачи управления в процедуры пользователя:


3.2. Объекты Xt и взаимодействие с ними.

    Как указывалось выше, Xt предоставляет набор средств для создания объектов, которые используются программами для общения с пользователем, а в общем случае и с остальным внешним миром. В данном разделе рассматриваются классы widget, процедуры для работы с ними, а также способы общения объектов Xt


3.3. Дополнительные возможности Xt.

3.3.1. Ввод данных из файла или из внешнего устройства.

    В Xt предусмотрен механизм для работы с файлами (внешними устройствами) в асинхронном режиме. Приложение может зарегистрировать процедуру, которая будет вызываться по мере готовности данных или при возникновении ошибок чтения/записи. Для регистрации используется процедура:

XtInputId   XtAppAddInput (XtAppContext prAppContext,
		int nSource, XtPointer  pCondition,
		XtInputCallbackProc  pProcedure,
		XtPointer pUserData);

    Здесь первый аргумент задает контекст программы, второй аргумент - это дескриптор файла (устройства), из которого будут читаться данные (до обращения к описываемой процедуре указанный файл должен быть открыт). Третий аргумент задает режим работы с файлом (устройством). Возможные значения этого аргумента перечислены ниже.

ЗначениеОписание
XtInputReadMaskфайл используется только для чтения данных
XtInputWriteMaskфайл используется для записи данных
XtInputExceptMaskпроцедура регистрируется для обработки ошибок
XtInputNoneMaskввода-вывода нет, регистрируемая функция не вызывается

Заметим, что указанные константы не объединяются с помощью оператора OR ( | ).

    Четвертый аргумент задает регистрируемую процедуру. Пятый аргумент используется для передачи данных в pProcedure при ее вызове.

    XtAppAddInput( ) возвращает число, позволяющее идентифицировать зарегистрированную функцию и условия ее работы.

    Процедура чтения/записи данных и обработки ошибок должна иметь следующий прототип:

void InputDataProc (XtPointer pData,
	   int  *nSource, XtInputId  *nIdentificator);

    Здесь первый аргумент определяет данные, являющиеся последним параметром в вызове процедуры XtAppAddInput( ). Второй аргумент задает дескриптор файла (устройства), а третий аргумент есть идентификатор, возвращенный процедурой XtAppAddInput( ).

    Когда обмен данными закончен, зарегистрированную функцию надо удалить с помощью

XtRemoveInput (XtInputId nIdentificator).

    Процедура XtAppAddInput( ) полностью аналогична описанной выше процедуре XtAppAddInput( ) за исключением того, что в ней отсутствует аргумент, указывающий на контекст программы.

    Ниже приводится фрагмент кода, показывающий пример использования описанной процедуры XtAppAddInput( ).

. . . . . .
GetFileInputData(XtPointer pUserData, int  *nSource,
		  XtInputId  *identifier)
{
   char anBuffer [BUFSIZ];
   int nCountBytes;
   char  *p = (char*) pUserData;

   if ( (nCountBytes = read (*nSource, anBuffer, BUFSIZ) ) == -1)
       perror("GetFileInputData");
   else
      printf("%s : %d bytes\n", p, nCountBytes);
}
 
main (int argc, char  **argv)
{
    XtAppContext  prAppContext;
    Widget	 prTopLevelWidget;
    int		 nId;
    char	*pS = "Read data from TESTFILE";

    prTopLevelWidget = XtVaAppInitialize(&prAppContext,
			 "XFileInput", NULL, 0,
			 &argc, argv, NULL, NULL);

   if (nId = open("TESTFILE", O_RDONLY, S_IREAD) )<0
   {
      fprintf(stderr, "xfileinput:I/O error\n");
      exit (1);
   }

   XtAppAddInput(prAppContext, nId, XtInputReadMask,
		   GetFileInputData, (XtPointer) pS);
   XtRealizeWidget(prTopLevelWidget);
   XtAppMainLoop(prAppContext);
}

    Здесь программа открывает файл с именем "TESTFILE" и читает из него данные при помощи процедуры GetFileInputData( ). Мы использовали последний аргумент функции XtAppAddInput( ) для передачи строки, которая должна печататься, если чтение прошло успешно. Заметим, что, вообще говоря, функции read( ) и open( )


3.3.2. Таймер.

    Xt предоставляет приложению возможность выполнять определенные действия через заданные промежутки времени. Например, периодически отображать на экране текущее время в заданном окне и т.д. Для указанных целей используется специальный механизм Xt - таймер. Он обеспечивает вызов через заданный интервал времени специальной функции, заданной программой. Таймер создается, например процедурой

XtIntervalId XtAppAddTimeOut (XtAppContext prAppContext,
     unsigned long nInterval, XtTimerCallbackProc pProcedure,
     XtPointer pUserData);

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

    Процедура возвращает значение типа XtIntervalId, которое однозначно идентифицирует зарегистрированный таймер и которое затем может быть использовано в качестве аргумента в процедуре XtRemoveTimeOut (XtIntervalId), предназначенной для уничтожения таймера.

    Регистрируемая функция, должна иметь следующий прототип:

void TimerProc (XtPointer pUserData, XtIntervalId *Id);

    Здесь pUserData - это указатель на данные, определенные четвертым параметром в вызове процедуры XtAppAddTimeOut( ). Второй аргумент - это указатель на идентификатор таймера.

    Отметим, что таймер автоматически уничтожается при вызове соответствующей процедуры. Поэтому, если необходимо, чтобы таймер функционировал н периодически проявлял себя каждые nInterval миллисекунд, в вызываемой функции он должен пересоздаваться с помощью XtAppAddTimeOut( ).

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

    Ниже приведен фрагмент кода, содержащий пример использования таймера.

void Count (Widget prWidget, XtIntervalId  *Id)
{
     static int n = 0;

     if (n<10)  {
        XtAppAddTimeOut(XtWidgetToApplicationContext (prWidget),
			1000, Count, prWidget);

        n++;
        printf ("%d seconds.\n", n);
  }
  else
    exit(0);
}

void main (int argc, char  **argv)
{
    XtAppContext  prAppContext;
    Widget	  prTopLevelWidget;
    int		  nId;

    prTopLevelWidget = XtVaAppInitialize (&prAppContext,
			"Timer", NULL, 0,
		          &argc, argv, NULL, NULL);
    XtAppAddTimeOut(prAppContext, 1000, Count,
		          prTopLevelWidget);
    XtRealizeWidget(prTopLevelWidget);
    XtAppMainLoop(prAppContext);
}

    В данном случае таймер работает, как секундомер, отсчитывая десять секунд, после чего программа завершается.

    Мы использовали в приведенном фрагменте кода удобную процедуру XtWidgetToApplicationContext( )


3.3.3. "Рабочие" (work) процедуры.

    "Рабочая" процедура (work-процедура) - это специальная функция, определяемая программой и вызываемая Xt, когда очередь событий пуста. Такие функции используются, как правило, для выполнения различных действий и вычислений в течении очень короткого времени. Как и таймер, каждая work-процедура должна быть зарегистрирована. Для этого используется, например, функция

XtWorkProcId XtAppAddWorkProc (XtAppContext prAppContext,
                               XtWorkProc pProcedure,
                               XtPointer pUserData);

    Здесь, как обычно, prAppContext - контекст приложения, pProcedure определяет вызываемую функцию, а pUserData задает передаваемые последней данные. Функция возвращает дескриптор "рабочей" процедуры, который, затем можно использовать для ее удаления.

    Каждая work-процедура должна иметь следующий прототип:

Boolean WorkProc (XtPointer pUserData); 

    Здесь pUserData совпадает с третьим аргументом функции XtAppAddWorkProc( ).

    Если work-процедура возвращает True, то Xt автоматически удаляет ее после того, как она завершит свою работу. В следующий раз, когда очередь событий приложения будет пуста, work-процедура не будет вызвана. Если же возвращаемое значение есть False, то процедура будет вызываться каждый раз, когда в очереди событий ничего нет. Удалить "рабочую" процедуру можно и явно, используя функцию XtRemoveWorkProc (XtWorkProcId nId), задавая в качестве аргумента nId идентификатор функции, возвращенный процедурой XtAppAddWorkProc( ).

    Приложение может зарегистрировать несколько work-процедур.

    Для регистрации "рабочей" процедуры можно использовать также и процедуру XtAddWorkProc( ), которая отличается от описанной выше процедуры XtAppAddWorkProc( ), только отсутствием аргумента prAppContext.


3.3.4. Управление очередью событий.

    Как уже отмечалось выше в 3.1.3., каждое приложение вызывает процедуру XtAppMainLoop( ) или XtMainLoop( ) для организации процесса получения и рассылки событий. Данные процедуры, в свою очередь, вызывают две процедуры XtAppNextEvent( ) (XtNextEvent( )) и XtDispatchEvent( ). Другими словами работа XtAppMainLoop( ) эквивалентна следующей последовательности операторов:

. . . . . . .
XtAppContext prAppContext
. . . . . . .
for ( ; ; )  {
    XEvent rEvent;
    XtAppNextEvent (prAppContext, &rEvent);
    XtDispatchEvent (&rEvent);
}
. . . . . . .

    Процедура XtAppNextEvent( ) имеет прототип:

void XtAppNextEvent (XtAppContext prAppContext,
                     XEvent *prEvent);

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

    Работает XtAppNextEvent( ) так: если в очереди первым идет событие о готовности данных в файле (внешнем устройстве), или там находится событие, инициирующее вызов соответствующей таймер-процедуры, то вызывается процедура (процедуры), зарегистрированные с помощью процедур XtAppAddInput( ) (XtAddInput( )) или XtAppAddTimeOut( ) (XtAddTimeOut( )). Если никаких событий в очереди нет, то вызываются work-процедуры, зарегистрированные процедурой XtAppAddWorkProc( ). Если же в очереди есть событие от сервера, то XtAppNextEvent( ) удаляет его из очереди и переносит в структуру, на которую указывает ее второй аргумент.

    Процедура XtDispatchEvent( ) имеет прототип:

void XtDispatchEvent (XEvent *prEvent); 

    Она анализирует событие и вызывает соответствующую callback-процедуру, action-процедуру или обработчик событий. Если таковых нет, то событие игнорируется. Если для события зарегистрировано несколько event handler или action-процедур, то порядок их выполнения не определен.

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

void MyMainLoop (XtAppContext prAppContext)
{
    XEvent prEvent;
    . . . . . . .
    for ( ; ; )
    {
        XtAppNextEvent (prAppContext, &prEvent);
        . . . . . . .
    /* Предобработка события */
    . . . . . . .
    XtDispatchEvent (&prEvent);
    . . . . . . .
    /* Постобработка события */
    . . . . . . .
    }
}

    Xt предусматривает целый набор процедур для работы с очередью событий. Например, функция XtAppPeekEvent( ) позволяет получить информацию о следующем событии без удаления его из очереди. Процедура имеет следующий прототип:

Boolean XtAppPeekEvent (XtAppContext prAppContext,
                        XEvent  *prEvent);

    Здесь prAppContext - это контекст приложения; второй аргумент используется для хранения информации о следующем событии в очереди. Функция возвращает True, если событие в очереди есть X событие, в противном случае (т.е., если это событие, инициирующее вызов соответствующей таймер-процедуры, или какое-либо другое событие) возвращается False. Событие из очереди не удаляется.

    Процедура XtAppProcessEvent( ) позволяет, задавая маску событий, выбирать из очереди нужные события и обрабатывать их. Функция имеет следующий прототип:

void XtAppProcessEvent (XtAppContext prAppContext,
                        XtInputMask nMask);

    Здесь первый аргумент задает контекст приложения. Второй аргумент определяет маску событий, при этом последняя может состоять из одной или нескольких констант, разделенных оператором OR ( | ). Указанные константы следующие:

XtIMXEvent - X событие
XtIMTimer - событие, инициирующее вызов таймер-процедуры
XtIMAlternateInput - событие о готовности данных
XtIMAll - все события.

    Аналогичные описанным выше процедурам функции XtPeekEvent( ) и XtProcessEvent( ) отличаются только отсутствием аргумента prAppContext. Более подробную информацию о приведенных и других функциях, позволяющих работать с событиями в очереди, можно найти например в [9, 10].


3.3.5. Акселераторы.

    Акселераторы в Xt похожи на action-процедуры с той лишь разницей, что событие (или последовательность событий), происходящее в одном widget, инициирует вызов соответствующей action-процедуры для другого объекта.

    Каждый widget в Xt (кроме объектов классов Object и RectObj) имеет ресурс XtNaccelerators, наследуемый от класса Core. Данный ресурс - это "таблица" акселераторов, которая по формату аналогична "таблице трансляции". Она содержит описание событий и соответствующих "действий". Задается данный ресурс так же, как и ресурс XtNtranslations. Ниже приводится пример его определения в ресурсном файле:

justas*XmDialogShell*mywidget.acceleratos :\n\
                          <KeyPress> : Set( ) Cancel( )

    Если значение ресурса XtNaccelerators задано для объекта W, то программа должна указать widget, в котором будут происходить события, вызывающие выполнение соответствующих action-процедур, указанных в "таблице" акселераторов. Для этого можно использовать процедуру XtInstallAccelerators( ) или XtInstallAllAccelerators( ). Первая из указанных функций имеет следующий прототип:

void XtInstallAccelerators (Widget prDestination,
                            Widget prSource);

    Здесь prDestination - это widget, в котором будут происходить события, которые приводят к выполнению action-процедур, заданных в "таблице" акселераторов для объекта, определяемого вторым аргументом.

    Приведенная процедура может вызываться до и после реализации widget, указанных в качестве ее параметров.

    Когда создается объект, Xt переводит "таблицу" акселераторов во внутренний формат и размещает ее в соответствующей структуре widget. При вызове функции XtInstallAccelerators( ) "таблица" акселераторов объединяется с "таблицей соответствия" или замещает последнюю. Все зависит от директивы "#replace", "#override" или "#augment", указанной при описании "таблицы" акселераторов. По умолчанию действительным считается режим "#augment". Процедура XtRemovelAccelerators( ) имеет обратное действие - она восстанавливает "таблицу соответствия" для объекта.

    Процедура XtInstallAllAccelerators( ) имеет такой же прототип, что и описанная выше функция XtInstallAccelerators( ), и используется для задания акселераторов самого объекта и его "потомков".

    Порядок шагов при вызове акселератора описан ниже. Когда в объекте prDestination происходит какое-то событие, то для widget pSource делается следующее:

    В заключении отметим несколько особенностей, связанных с применением акселераторов.

  1. При определении "таблицы" акселераторов в файле ресурсов, не допускается применение сокращенной аббревиатуры события, кроме "Key" (в случае KeyPress). Необходимо использовать только полное описание события (см. приложение 1 ).
  2. Не разрешены символы None, Meta, Hyper, Super, Alt.
  3. Параметры, передаваемые в action-процедуры, должны быть заключены в двойные кавычки.
  4. Для конвертирования "таблицы" акселераторов во внутреннее представление Xt, используется процедура XtParseAcceleratorTable (String sTable), где строка sTable определяет "таблицу" акселераторов.
  5. Для задания всех акселераторов приложения можно использовать процедуру XtInstallAllAccelerators (prTopLevel, prTopLevel), где prTopLevel widget есть родительский для всех остальных объектов программы.

3.3.6. Процедуры, предназначенные для работы с окнами объектов.

    Xt поддерживает несколько способов работы с окнами widget. Так программа, используя процедуры, предоставляемые Xt, может показать окно, установить для него тот или иной режим работы, и закрыть окно, когда последнее становится не нужным. В основном эти процедуры используются для создания "всплывающих" (pop up) меню и диалогов. Рассмотрим кратко эти способы.

    Первый способ. В Xt есть три предопределенные процедуры для показа окна объекта и одновременного задания режима обработки событий (Xt grab mode). Может быть сохранен нормальный процесс рассылки событий между объектами приложения, но может быть сделано и так, что Xt будет направлять события, возникающие в результате действий пользователя, в widget независимо от того, в каком объекте они происходили. Режим обработки событий называется в Xt "режимом захвата" (grab mode).

    "Захват" может быть двух типов: исключительным (exclusive) и неисключительным (nonexclusive). Разница между ними проявляется, когда программа создает, например, многоуровневое меню. Рассмотрим следующий пример. Пусть мы открыли первое подменю, затем второе, а после него третье. Теперь пользователь нажал на кнопку мыши. Если режим "захвата" exclusive, соответствующее событие будет передано widget, представляющему третье подменю. Если режим "захвата" nonexclusive, то оно будет послано тому подменю, в котором находится курсор мыши. Если же курсор не попал ни в одно подменю, то событие опять таки получит последний (третий) widget.

    Процедуры, отображающие объект и задающие режим обработки событий, следующие: XtCallbackNone( ), XtCallbackExclusive( ), XtCallbackNonexclusive( ). Все они имеют стандартный набор параметров, определенный в Xt для процедур обратного вызова. Идентификатор объекта, окно которого должно быть показано на экране, передается через аргумент pUserData.

    Первая процедура используется для показана окна widget и не меняет способ обработки событий.

    Вторая и третья процедуры используются для отображения окна widget и одновременного задания режимов exclusive и nonexclusive соответственно.

    Ниже приводится фрагмент кода, содержащий пример использования одной из перечисленных выше процедур. Здесь мы используем объект PushButton ("нажимаемая кнопка") из библиотеки OSF/Motif (см. главу 4).

. . . . . . . . . . . . .
main (int argc, char  **argv)
{
     Widget prShellWidget;    /* Показываемый shell-объект */
     Widget prPushButton;     /* "Нажимаемая кнопка"       */
     . . . . . . .
     prShellWidget = XtCreatePopupShell ( . . . );
     prPushButton = XtCreateWidget ( . . .);
     . . . . . . .
     XtAddCallBack (prPushButton, XmNactivateCallback,
            XtCallbackExclusive, prShellWidget);
     . . . . . . .
}
. . . . . . 

    Цель данного кода - показать "всплывающее" (pop up) окно при "нажатии" на кнопку. Для этого мы сначала создаем "всплывающий" shell-объект с помощью процедуры XtCreatePopupShell( ) и заносим стандартную процедуру XtCallbackExclusive( ) в список callback кнопки prPushButton. Этот список соответствует ресурсу XmNactivateCallback (см. Приложение 3). Занесенные в него процедуры вызываются в ответ на "нажатия" на объект с помощью мыши или клавиши клавиатуры. Последний параметр функции XtAddCallBack( ) позволяет нам сообщить стандартной процедуре XtCallbackExclusive( ), окно какого widget должно быть показано на экране.

    Для закрытия окон widget, используется callback-процедура XtCallbackPopdown( ). При ее вызове параметр pUserData должен указывать на структуру типа XtPopdownId, определяемую следующим образом:

typedef struct  {
      Widget shell_widget;
      Widget enable_widget;
} XtPopdownIdRec,  *XtPopdownId; 

    Здесь первое поле определяет shell-объект, который будет закрыт, второе поле задает объект, которой инициировал его показ.

    Ниже приводится фрагмент, содержащий пример использования описанной процедуры.

. . . . . . .
Widget       prShellWidget, prPushButton;
XtPopdownIdRec   rRec;
. . . . . . .
main (int argc, char  **argv)
{
     . . . . . . .
     prShellWidget = XtCreatePopupShell ( . . . );
     prWidget         = XtCreateManageWidget ( . . . );
     . . . . . . .
     rRec.shell_widget    = prShellWidget;
     rRec.enable_widget = prPushButton;
     . . . . . . .
     XtAddCallback (prPushButton, XmNactivateCallback,
          XtCallbackPopdown, &rRec);
     . . . . . . .
}
. . . . . . .

    Второй способ. Xt предусматривает две специальных action-процедуры XtMenuPopup( ) и XtMenuPopdown( ), которые применяются для отображения и закрытия окон объектов соответственно. Имена процедур могут быть использованы в "таблицах трансляции" программы. Функция XtMenuPopup( ) имеет следующий прототип:

void XtMenuPopup (String shellname); 

    Здесь аргумент процедуры используется для задания имени отображаемого shell-объекта. Данная action-процедура может быть определена в "таблице трансляции" для следующих событий: ButtonPress, KeyPress, EnterWindow.

    Процедура XtMenuPopdown( ) аналогична описанной выше функции только используется для закрытия указанного shell-объекта.

    Третий способ. Xt предусматривает для непосредственного отображения на экране и закрытия окон widget также и отдельные процедуры XtPopup( ), XtPopupStringLoaded( ) и XtPopdown( ).

    Первая процедура используется для непосредственного размещения на экране окна widget и имеет следующий прототип:

void XtPopup (Widget prShellWidget,
              XtGrabKind grabmode); 

    Здесь первый аргумент задает shell-объект. Второй аргумент определяет режим обработки событий. Возможные его значения следующие:

XtGrabNone - нормальный режим работы;
XtGrabExclusive - исключительный (exclusive) "режим захвата";
XtGrabNonexclusive - неисключительный (nonexclusive) "режим захвата".

    Вторая функция - XtPopupStringLoaded( ) - используется также для отображения окон и отличается от XtPopup( ) в частности тем, что автоматически задает режим обработки событий exclusive. Функция имеет следующий прототип:

void XtPopupStringLoaded (Widget prShellWidget); 

    Здесь аргумент аналогичен первому аргументу предыдущей процедуры.

    Третья процедура используется для закрытия окна объекта и имеет прототип, аналогичный предыдущей функции.

    Заметим, что, как правило, каждое множество widget (например, OSf/Motif, OPEN LOOK и др.) имеет свои удобные процедуры для создания меню и диалогов, поэтому мы не будем более подробно останавливаться на данном вопросе.


3.3.7. Программы, имеющие много объектов (окон) верхнего уровня.

    Процедура инициализации XtAppInitialize( ) или аналогичная ей возвращает указатель на объект класса ApplicationShell, который непосредственно контактирует с менеджером окон и является "началом" дерева widget. Но приложение может иметь не одно, а несколько объектов "верхнего уровня". Для их создания можно использовать процедуры XtAppCreateShell( ) (XtVaAppCreateShell( ), XtCreateApplicationShell( )). Создаваемые widget принадлежат, как правило, классу TopLevelShell.

    В приводимом ниже примере приложение создает для своих нужд три окна верхнего уровня:

#include <X11/Intrinsic.h>
#include <X11/StringDefs.h>
#include <X11/Shell.h>

void main (int argc, char  **argv)
{
  Widget             topLevel1, topLevel2, topLevel3,
                CoreWidget1, coreWidget2, coreWidget3;
  XtAppContext prAppContext;

  topLevel1      = XtVaAppInitialize (&prAppContext, "Justas1",
           NULL, 0, &argc, argv, NULL, NULL);
  topLevel2      = XtAppCreateShell ("justas2",  "Justas2",
           topLevelShellWidgetClass,
           XtDisplay (topLevel1), NULL, 0);
  topLevel3      = XtAppCreateShell ("justas3", "Justas3",
           topLevelShellWidgetCalss,
           XtDisplay (topLevel1), NULL, 0);

  coreWidget1 = XtCreateManageWidget ("Core", widgetClass,
           topLevel1, NULL, 0);
  coreWidget2 = XtCreateManageWidget ("Core", widgetClass,
           topLevel2, NULL, 0);
  coreWidget3 = XtCreateManageWidget ("Core", widgetClass,
           topLevel3, NULL, 0);

  XtVaSetValues (coreWidget1,
         XtNwidth, 100, XtNheight, 100, NULL);
  XtVaSetValues (coreWidget2,
         XtNwidth, 200, XtNheight, 200, NULL);
  XtVaSetValues (coreWidget3,
         XtNwidth, 400, XtNheight, 400, NULL);

  XtRealizeWidget (topLevel1);
  XtRealizeWidget (topLevel2);
  XtRealizeWidget (topLevel3);
  XtAppMainLoop (prAppContext);
}

    Заметим, что существует еще один способ создания в программе нескольких объектов "верхнего уровня". В частности, можно поступать следующим образом: сначала создается shell-объект класса ApplicationShell, который никогда не показывается на экране, все остальные shell-объекты "верхнего уровня" создаются, как "потомки" данного "родителя" с помощью процедуры XtCreatePopupShell( ).


3.3. Дополнительные возможности Xt.

  1. Ввод данных из файла или из внешнего устройства.
  2. Таймер.
  3. "Рабочие" (work) процедуры.
  4. Управление очередью событий.
  5. Акселераторы.
  6. Процедуры, предназначенные для работы с окнами объектов.
  7. Программы, имеющие много объектов (окон) верхнего уровня.

3.4.1. Формат файла описания ресурсов.

    Подробно формат файла описания ресурсов (или просто файла ресурсов) был приведен в п. 2.4.1 главы 2. Но когда программист работает с Xt, то возникает вопрос, как конкретно определяется значение того или иного параметра widget. Делается это следующим образом. Пусть объект с именем "WnameN" и класса "WclassN" имеет "родителей" с именами "WnameN-1", . . . , "Wname1" и принадлежащих классу "WclassN-1", . . . , "Wclass1" соответственно, причем "Wname1" - корень дерева widget программы. Тогда, чтобы задать значение его атрибута XtNA в ресурсном файле должна быть строка:

<имя программы>. "Wname1".  . . .  . "WnameN".A : <значение> 

    Например, пусть в программе с именем "justas" объект "dialogBox" принадлежит классу TopLevelShell и имеет два родительских widget "core" класса Core и "appShell" класса ApplicationShell. Тогда значение его атрибута XtNheight (высота окна) равное 100 можно задать так:

justas.appShell.core.dialogBox.height : 100 

    Вместо имени любого объекта иерархии можно указать его класс. В этом случае значение ресурса будет распространяться на все объекты, у которых в иерархии на соответствующем месте стоит widget указанного класса . Так, например, если структура иерархии объектов программы такая, как указано на рисунке 3.2, то строки

justas.appShell.core1.dialogBox.height : 100
justas.appShell.core2.dialogBox.height : 200 

означают, что widget с именем "dialogBox" и "родителем" "core1" имеет высоту 100. А widget с именем "dialogBox" и родительским widget "core2" имеет высоту 200. Строка же

justas.appShell.Core.dialogBox.height : 200 

"говорит", что для обоих объектов высота равна 200. В строке, задающей ресурс, можно употреблять символ `*' (см. 2.4.1 главы 2).

Рис. 3.2. Иерархия объектов программы.

Рис. 3.2. Иерархия объектов программы.


3.4.2. Создание базы данных ресурсов программы.

    Загрузка ресурсов производится инициализационными процедурами, например XtAppInitialize( ), которые конструируют базу данных из различных ресурсных файлов, опций командной строки и других источников. Ниже описывается последовательность шагов Xt, выполняемая при поиске и загрузке ресурсов. Если встречаются две одинаковые спецификации какого-то ресурса widget, то будет использована первая встретившаяся (данная последовательность шагов принята в версии X11R5, в X11R4 поиск и загрузка ресурсов выполняется в обратном порядке).

    Первый шаг. Загрузка ресурсов из командной строки. Xt поддерживает стандартное множество опций, комбинации которых могут указываться в командной строке при запуске программы. Они перечислены в таблице, приведенной ниже.

Опция Имя ресурса Тип Описание и примеры
-bg
-background
*background String цвет фона
-bg blue
-background white
-bd
-bordercolor
*borderColorString цвет границы
-bg black
-bordercolor red
-bw
-borderwidth
.borderwidthInteger ширина края окна в пикселах
-bw2
-display .display String дисплей для связи с сервером
-display odessa: 0
-fg
-foreground
*foreground String цвет переднего плана
-fg red
-foreground black
-fn
-font
*font String имя шрифта
-fn 9x15
-font 9x15
-geometry .geometry String размер и положение окна
-geometry = 80x24
-iconic .iconic None если значение опции "on", то окно программы будет показано в минимизированном виде.
-name .name String имя программы
-name Justas
-reverse
-rv
*reverseVideoNone если значение ресурса "on", то выводимое изображение появится в инверсном виде
+rv *reverseVideoNone изображение будет нормальным
-title .title String заголовок окна программы
-title Justas
-xrm значение аргумента String ресурс и его значение задаются аргументом опции
-xrm "*.height: 100"

    В данной таблице точка, предшествующая имени ресурса, означает, что опции могут быть использованы для установки ресурса только в widget, принадлежащих классу TopLevelShell или его подклассу. В свою очередь, "*" означает, что ресурс может быть установлен для произвольного widget приложения.

    Инициализационные процедуры понимают также любую уникальную аббревиатуру предопределенных опций, например:

justas  -back  green

    Xt предоставляет также возможность передавать в командной строке нестандартные опции. Для того, чтобы определить их и получить соответствующие значения, используются третий и четвертый аргументы процедуры XtAppInitialize( ) (XtVaAppInitialize( ), XtInitialize( )).

    Ниже приведен фрагмент кода и пример командной строки, иллюстрирующие описанный выше механизм получения значений ресурсов.

. . . . . . .
static XrmOptionDescRec arCommandOptions [ ] = {
   { "-delay", "*delay",  XrmoptionSepArg, NULL },
   { "-debug", "*debug", XrmoptionNoArg, "True" },
} ;
. . . . . . .
main (int argc, char  **argv)
{
    Widget prWidget;
    . . . . . . .
    prWidget = XtInitialize (argv [0], "Justas", arCommandOptions,
        XtNumber (arCommandOptions), &argc, argv);
    . . . . . . .
}

    Пример командной строки:

justas  -debug  -delay  15

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

    Второй шаг. Загружается файл, на который указывает переменная среды XENVIRONMENT (если таковая задана). Данная переменная содержит полный путь к файлу, включая и его имя. Если же переменная XENVIRONMENT не установлена, то менеджер ресурсов будет пытаться подгрузить файл ".Xdefaults-<hostname>", находящийся в "домашней" (home) директории пользователя. Здесь <hostname> - это имя компьютера, на котором происходит запуск программы.

    Третий шаг. Если "корневое" окно (root window) экрана имеет ресурсы, загруженные в "свойство" RESOURSE_MANAGER (RESOURSE_MANAGER property) программой xrdb, то они также добавляются в базу данных ресурсов приложения. Если же "корневое" окно не имеет такого "свойства", то подгружаются данные, определенные в файле ".Xdefaults", находящемся в "домашней" (home) директории пользователя.

    Четвертый шаг. Если задана переменная среды XAPPLRESDIR, то менеджер ресурсов будет пытаться загрузить ресурсный файл с именем "<$XAPPLRESDIR> / <classname>", где <classname> имя класса программы. Если файла нет, Xt пытается прочитать другой файл с именем "<$APPLRESDIR> / <$LANG> / <classname>" ( здесь LANG - переменная среды, задающая язык системы). Если переменная XAPPLRESDIR не установлена, то соответствующие файлы ищутся в "домашней" (home) директории. Отметим, что в системе могут быть предусмотрены и некоторые другие переменные среды, позволяющие задать путь к файлу ресурсов (например, XUSERFILESEARCHPATH и др.), но для целей данного издания приведенной информации достаточно.

    Пятый шаг. Xt ищет следующий файл:

"/usr/lib/X11/app-derfaults/<classname>"

    Если этот файл существует, он загружается в базу данных ресурсов .

    Шестой шаг. Рассматриваются параметры, переданные с помощью пятого и шестого аргументов инициализационных процедур. Данная возможность может быть использована в случае, если значение какого-либо ресурса осталось неопределенным. Передаваемые через параметры процедур значения ресурсов представляются в виде массива указателей на строки. Каждая строка имеет вид: "<описание ресурса> : <значение ресурса>". Здесь <описание ресурса> такое же, как и в ресурсном файле. Ниже приведен пример этого способа определения параметров программы.

#include <X11/Intrinsic.h>
#include <X11/StringDefs.h>

extern void DrawHellowString( );

void main (int argc, char  **argv)
{
    Widget    topLevel, fallback;
     XtAppContext  prAppContext;

     static String Fallback [ ] = {
         "fallback*background : blue",
         "fallback.Core.width   : 200",
         "fallback.Core.height  : 200",
         NULL,
     } ;

     topLevel = XtVaAppInitialize (&prAppContext, "Fallback",
        NULL, 0, &argc, argv, Fallback, NULL);

     fallback = XtCreateManagedWidget ("Core", widgetClass,
        topLevel, NULL, 0);

     XtAddEventHandler (fallback, ExposureMask, False,
                    DrawHellowString, NULL);
     XtRealizeWidget (topLevel);

     XtAppmainLoop (prAppContext);
}

void DrawHellowString (Widget prWidget, XtPointer pData,
            XEvent  *prEvent, Boolean  *pbContinue)
{
    Display *prDisplay = XtDisplay (prWidget);
    Window  nWindow   = XtWindow (prWidget);
    GC      prGC;

    if (prEvent->type == Expose)
    {
        prGC = XCreateGC (prDisplay, nWindow, 0, NULL);
        XDrawString (prDisplay, nWindow, prGC, 10, 50,
         "Hellow, world!", strlen ("Hellow, world!") );
        XFreeGC (prDisplay, prGC);
     }
}  



3.4.4. Процедуры, преобразующие значения ресурсов от одного типа к другому ("конвертеры").

    Значения параметров в файлах ресурсов представляются в виде строк. Перед использованием в программе, их надо преобразовать к требуемому типу. Так, например, значение атрибута XtNwidth задается строкой "100", но должно быть превращено в целое число. Для этого Xt и программы регистрируют специальные процедуры, преобразующие значения из одной формы представления в другую (т.е. из одного типа в другой). Их называют "конвертеры" (converter) (также мы будем употреблять термины "процедура-конвертор" и "преобразователь").

    Механизм использования "конвертеров" следующий. Когда программа получает ресурс с помощью XtGetApplicationResources( ) или создает widget, обращаясь к XtCreateWidget( ) или аналогичной процедуре, Xt достает значение ресурса из базы данных. Это значение представлено в виде строки. Xt знает тип атрибута (обозначим его T) и ищет зарегистрированный "конвертер" "строка -> T". Если таковой есть, то он вызывается и значение ресурса переводится в требуемую форму. Если же "конвертера" нет, параметр получает значение по умолчанию.

    Мы описали работу "преобразователя" "строка -> произвольный тип". Однако можно создать и зарегистрировать "конвертер" "произвольный тип -> произвольный тип" и использовать его для своих нужд.

    Ниже, в таблице, приведены преобразования типов данных, поддерживаемые самой Xt.

Откуда Куда Описание
XtRString XtRAcceleratorTable преобразует строку, задающую "таблицу" акселераторов во внутреннюю форму Xt
XtRString XtRAtom преобразует строку, содержащую имя "свойства", в соответствующий атом
XtRString XtRBoolean конвертирует строки "True", "False", "yes", "no", "on", "off" в соответствующее логическое значение
XtRString XtRBool аналогично предыдущему
XtRString XtRCursor преобразует стандартное название курсора для X WINDOW в соответствующий идентификатор
XtRString XtRDimension конвертирует ширину и высоту к типу Dimension
XtRString XtRDisplay преобразует имя дисплея и возвращает указатель на структуру типа Display
XtRString XtRFile преобразует имя файла и возвращает его дескриптор
XtRString XtRFloat конвертирует строку, задающую число, в формат float
XtRString XtRFont преобразует имя шрифта и возвращает его идентификатор
XtRString XtRFontStruct преобразует имя шрифта и возвращает указатель на структуру типа XFontStruct
XtRString XtRInitialState конвертирует строки "Normal" или "Iconic" в символы NormalState или IconicState соответственно
XtRString XtRInt конвертирует строку, содержащую число, в формат int
XtRString XtRPixel преобразует строку содержащую имя цвета (например, black или #FF0000), в значение пиксела
XtRString XtRPosition преобразует значения x и y координат к типу Position
XtRString XtRShort конвертирует строку, содержащую число, в форму short
XtRString XtRTranslationTable преобразует строку, задающую "таблицу трансляции", во внутреннюю форму Xt
XtRString XtRUnsignedChar конвертирует строку, содержащую число, в форму unsigned char
XtRString XtRVisual преобразует строку, задающую тип палитры, и возвращает указатель на структуру типа Visual
XtRPixel XtRColor конвертирует значение пиксела в указатель на структуру типа XColor
XtRInt XtRBool конвертирует целое в логическое
XtRInt XtRColor конвертирует целое в XColor
XtRInt XtRDimension конвертирует целое в Dimension
XtRInt XtRFloat конвертирует целое в плавающее
XtRInt XtRFont конвертирует целое в Font
XtRInt XtRPixel конвертирует целое в значение пиксела
XtRInt XtRPixmap конвертирует целое в Pixmap
XtRInt XtRPosition конвертирует целое в Position
XtRInt XtRShort конвертирует целое в short
XtRInt XtRUnsignedChar конвертирует целое в беззнаковое целое

    Процедура-конвертор должна иметь следующий прототип:

void ConverterProc (XrmValue  *prArgs, Cardinal  *nArgs,
        XrmValue  *prFromVal, XrmValue  *prToVal);

    Здесь аргументы prArgs, prFromVal, prToVal - это указатели на структуры типа XrmValue (см. приложение 1) .

    Процедура-конвертор должна преобразовывать данные из структуры prFromVal и результат помещать в структуру prToVal. Любые дополнительные данные, требующиеся при конвертации, передаются через аргументы prArgs и nArgs.

    До того, как менеджер ресурсов сможет использовать указанную процедуру-конвертор, последняя должна быть зарегистрирована. Для этого можно использовать одну из функций XtAddConverter( ), XtAppAddConverter( ) или XtSetTypeConverter( ). В коде программы "конвертор" должен быть зарегистрирован после инициализации Xt, но до обращения к XtGetApplicationResources( ).

    Процедура XtAddConverter( ) имеет следующий прототип:

void XtAddConverter (String prFromType, String psToType,
        XtConverter pConverter, XtConvertArgList prConvArgs,
        Cardinal nNumArgs);

    Первый и второй аргументы процедуры - это строки, задающие соответственно типы, из которого и в который надо преобразовывать. Это должны быть стандартные имена типов данных, определенные в файле "StringDefs.h", или имена, определенные в файле-заголовке программы. Третий аргумент задает непосредственно саму процедуру-конвертор. Четвертый и пятый аргументы задают дополнительные данные, которые передаются "конвертору" и используются им. При этом prConvArgs - указатель на массив структур типа XtConvertArgRec, определяемых следующим образом:

typedef struct  {
    XtAdddressMode  address_mode;
    XtPointer       address_id;
}  XtConvertArgRec,  *XtConvertArgList;

Здесь address_mode определяет, как следует интерпретировать поле address_id. Возможные значения для поля address_mode даются перечисляемым типом XtAdddressMode:

typedef enum {
    XtAddress,          /*адрес                             */
    XtBaseOffset,       /* смещение                         */
    XtImmediate,        /* константа                        */
    XtResourceString,   /* строчное имя ресурса             */
    XtResourceQuark,    /* внутренняя форма задания ресурса */
    XtWidgetBaseOffset, /* смещение от "родителя"           */
    XtProcedureArg,     /* вызов процедуры                  */
} XtAddressMode;

Здесь

XtAddress указывает, что параметр address_id интерпретируется как адрес данных;
XtBaseOffset address_id интерпретируется как смещение относительно базового адреса widget;
XtImmediate address_id - константа;
XtResourceString address_id интерпретируется как имя ресурса, которое будет преобразовано в смещение относительно базового адреса widget;
XtResourceQuark address_id интерпретируется как имя ресурса, которое будет преобразовано во внутреннюю форму XtResourceString;
XtWidgetBaseOffset аналогично XtBaseOffset, за исключением того, что смещение рассматривается относительно "родителя", если последний не принадлежит подклассу класса Core;
XtProcedureArg address_id интерпретируется как указатель на процедуру типа XtConvertArgProc, которая будет вызываться при завершении конвертации (см. определение данного типа в приложении 2 данного издания).

    Процедура XtAppAddConverter( ) аналогична описанной выше процедуре, за исключением того, что добавлен еще один аргумент - контекст приложения. Процедура XtSetTypeConverter( ) также аналогична описанной процедуре, давая при этом дополнительные возможности по более эффективному конвертированию значений ресурсов (более детальную информацию по указанным процедурам можно найти в [9]).

    Ниже приведен пример процедуры-конвертора, позволяющий преобразовывать строку в длинное целое.

Void CnvStringToLong (XrmValue  *prArgs, Cardinal  *pnNrgs,
                    XrmValue  *prFromVal, XrmValue  *prToVal)
{
    static long nResult;      /* result variable */

    if (*pnArgs! = 0)
       XtWarning ("String to Long conversion needs no extra arguments!");

    if (sscanf ( (char *) prFromVal->addr, "%ld", &nResult) = = 1)
    {
        prToVal->size = sizeof (long);
        prToVal->addr = (XtPointer) &nResult;
    }
     else
         XtStringConversionWarning ( (char *) prFromVal->addr,
                  XtRLong);
}

    Данная процедура-конвертор использует функцию XtWarning( ) для печати предупреждающего сообщения, если число аргументов args больше нуля, для конвертирования строковой переменной в переменную типа long используется процедура sscanf( ). Результат работы данной процедуры сохраняется в структуре prToVal. Процедура XtStringConversionWarning( ) предназначена для выдачи предупреждающего сообщения при неуспешном завершении sscanf( ). Процедура имеет два аргумента типа String; первый - строка, задающая данные конвертируемого типа, второй - название типа данных, к которому не смогли быть преобразованы данные, определенные первым аргументом.


3.4. Xt и ресурсы программ.

    В главе 2 рассказывалось, как X Window работает с конфигурационными параметрами программ (ресурсами). Они описываются в специальных файлах - файлах ресурсов. Для работы с ними предусмотрен целый ряд процедур, которые имеют обобщающее название менеджер ресурсов.

    Xt использует менеджер ресурсов X Window для создания базы данных ресурсов программы. Библиотека предоставляет удобный интерфейс для получения значений параметров, хранящихся в этой базе.

  1. Формат файла описания ресурсов.
  2. Создание базы данных ресурсов программы.
  3. Получение ресурсов программы.
  4. Процедуры, преобразующие значения ресурсов от одного типа к другому ("Конверторы").

Назад

Содержание

Вперед


Глава 3. Программирование с использованием библиотеки X Toolkit Intrinsincs (Xt).

    Чтобы облегчить программирование в системе X Window было создано несколько пакетов. Стандартом де-факто в настоящее время стала библиотека X Toolkit Intrinsincs (Xt), которая входит в комплект стандартной поставки системы. Xt упрощает инициализацию программ и создание окон. Кроме того, библиотека содержит средства для создания объектов (управляющих элементов), используемых программами при общении с пользователями. В терминах Xt управляющий элемент называется widget.

    В настоящий момент на основе пакета реализованы различные наборы (множества) управляющих элементов (объектов), такие как OSF/Motif, Athena, OPEN LOOK и др. Эти наборы в совокупности с самой Xt, применяются в качестве удобного инструмента для создания интерфейсов. Они берут на себя ту рутинную работу, которую необходимо выполнить программисту при написании собственного приложения с использованием только процедур X Window.

    Функции Xt находятся в файле "/usr/lib/libXt.a".



4.1. Основные обозначения и файлы-заголовки Motif.

    Motif поддерживает все классы Xt и, следовательно, ресурсы этих классов, но для записи имени и класса ресурса объекта используются константы, начинающиеся соответственно с префиксов XmN и XmC. Например,

    XmNbackground - имя ресурса, задающего цвет фона,

а

    XmCBackground - имя класса этого ресурса.

    Для записи типа ресурса употребляются константы с префиксом XmR (вместо XtR, принятого в Xt).

    Все процедуры, имеющиеся в библиотеке, также начинаются с букв Xm. Например: XmUpdateDisplay( ), XmStringCreate( ). Общие для пакета константы, структуры и прототипы функций находятся в файле-заголовке "Xm.h". Обычно он


4.2.1. Класс XmPrimitive.

    Данный класс включает семь основных подклассов объектов. Один из них, в свою очередь, является базовым еще для четырех подклассов.

    Файл-заголовок - "Primitive.h". Имя класса - XmPrimitive, указатель класса - xmPrimitiveWidgetClass


4.2. Основные классы объектов в Motif.

    Motif расширяет множество классов объектов, предоставляемое Xt. В частности, Motif поддерживает достаточно большой набор классов, позволяющих создавать меню, скролл - бары, "нажимаемые" кнопки, редактирующие элементы и т.д.

    Кроме этого, Motif предоставляет специальный класс объектов, которые называются gadget. Они также могут использоваться для создания интерфейсных элементов, но обладают той особенностью, что не имеют собственных окон. Для рисования, gadget пользуются окном своего "родителя".

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

    Некоторые из классов Motif не используются для создания экземпляров widget (такие классы в терминологии объектно-ориентированного программирования называются "абстрактными"). Они содержат в себе самые общие атрибуты и методы, необходимые для функционирования различных типов widget.

    Иерархия классов объектов Motif и Xt показана на рисунке 4.1.(классы Xt показаны в темных прямоугольниках).

Иерархия классов объектов

Рис. 4.1. Иерархия классов объектов Xt и Motif.

    Заметим, что упомянутый ранее специальный класс объектов - XmGadget является подклассом класса RectObj. Последний, в свою очередь, есть абстрактный класс, инкапсулирующий в себе базовые механизмы управления геометрическими размерами объектов. Класс XmGadget объединяет несколько подклассов, о которых более подробно рассказано в 4.2.5.

    Motif имеет два основных подкласса объектов: XmPrimitive и XmManager. Оба они являются абстрактными. Первый из них наследуется от класса Core и используется как базовый для классов объектов, не "владеющих" другими объектами. Примерами являются "нажимаемые" кнопки, списки и т. д. Класс XmManager наследует свои характеристики от класса Constraint и применяется для создания классов объектов, которые могут иметь и управлять "дочерними" объектами.

    Ниже будет кратко описан каждый класс в иерархии Motif. При этом указывается файл-заголовок, соответствующий классу, имя класса, указатель на данные класса, процедура, применяемая для создания экземпляров класса. Приводятся наиболее часто используемые ресурсы. Причем при описании


Глава 4. Множество widget OSF/Motif

    Open Software Foundation Motif (OSF/Motif) представляет собой пакет, включающийx менеджер окон, набор утилит для выполнения различных вспомогательных операций, а также библиотеку объектов, построенных на основе X Toolkit Intrinsics. На сегодняшний день выпущено несколько версий Motif, начиная с 1.0 и далее 1.1, 1.2, 1.2.1, 1.2.2 и выше. В дальнейшем, если не оговорено особо, описываются процедуры и объекты Motif версии 1.1.

    Основная цель данной главы - дать начальное представление об объектах, определенных в OSF/Motif, а также о некоторых дополнительных возможностях, предоставляемых данной библиотекой. Для эффективного понимания излагаемого материала приводятся простые примеры. Более подробную информацию об излагаемом