Wiki

Хранитель экрана на Qt

Andi Peredri Цель этой статьи - показать, что разработка хранителя экрана с помощью Qt является простой задачей, которая под силу даже начинающим программистам. Также в статье рассматриваются вопросы интеграции приложений с помощью desktop-файлов, средств командной строки и идентификаторов окон X Window System на примере программы QStars.

KDesktop

Запуском хранителя экрана и функцией блокирования в KDE занимается программа kdesktop. Для этого она выполняет следующие действия:
  1. отслеживает простой системы (отсутствие ввода с клавиатуры и событий от мыши);
  2. запускает выбранный пользователем хранитель экрана;
  3. на период его работы обеспечивает автоматическое выключение курсора мыши;
  4. при необходимости блокирует экран;
  5. обеспечивает запрос и проверку пользовательского пароля;
  6. завершает работу хранителя экрана.
При этом от разработчика хранителя экрана требуется:
  1. обеспечить отображение графической демонстрации в окне с заданным идентификатором;
  2. обеспечить выбор режима работы хранителя экрана средствами командной строки;
  3. реализовать пользовательский графический интерфейс для настройки параметров работы;
  4. добавить свой хранитель экрана в список установленных в KDE Control Center.
Ниже эти шаги рассмотрены более подробно.

Хранитель экрана QStars

Хранитель экрана QStars имитирует полет космических кораблей в межзвездном пространстве. Для его разработки использовалась библиотека Qt 3.2, тестирование проводилось на базе KDE 2.2. Исходный код QStars можно загрузить здесь: qstars-0.1.tar.gz.

QStars Screenshot

Для реализации хранителя экрана мы используем два класса:
  1. класс Saver является главным окном приложения:
        class Saver : public QWidget
        {
        public:
            Saver(WId window);
        protected:
            void timerEvent(QTimerEvent*);	// Обработчик таймера.
        private:
            QPtrList stars;	// Список всех звезд.
        };
  2. класс Star обеспечивает отрисовку и перемещение звезды:
        class Star
        {
        public:
            Star(int x, int y, int c);
            void paint(QPainter *p);
            void drawPixel(QPainter *p, int x, int y);
        private:
            int sx;		// X-координата.
            int sy;		// Y-координата.
            int color;	// Яркость. Соответствует RGB-цвету: (color, color, color).
            int counter;	// Счетчик. Определяет необходимость перемещения звезды.
        };

Класс Saver

Конструктор Saver::Saver для своей работы использует существующее окно X Window System с указанным идентификатором WId. WId - платформо-зависимый идентификатор окна, который в qwindowdefs.h для X11 определен, как unsigned long. Для инициализации окна используется функция create():
    void QWidget::create( WId window = 0, bool initializeWindow = TRUE, 
					  bool destroyOldWindow = TRUE );
Аргументы по умолчанию сообщают, что нужно инициализировать новое окно и уничтожить старое. Если идентификатор окна будет равен 0, Qt создаст новое окно для виджета. Вот исходный код конструктора класса:
    static int w = 600;	// Размеры окна по умолчанию.
    static int h = 420;
    static int border = 200;	// Область за левой границей окна.
    static QImage background;	// Фоновое изображение с космическим кораблем.
 
    Saver::Saver(WId window) : QWidget()
    {
        if(window)	// Необходимо использовать существующее окно?
        {
	    create(window);		// Инициализируем окно.
	    w = width();
	    h = height();
	}
	else setFixedSize(w,h);	// Устанавливаем размер 600x420.
 
	QPixmap pixmap(w, h);	// Формируем фоновое изображение.
	QPainter p(&pixmap);
	p.fillRect(pixmap.rect(), black);
 
    	srand(time(0));		// Инициализируем генератор случайных чисел.
	QSettings settings;	// Загружаем сохраненную конфигурацию.
 
	// Инициализируем скорость перемещения звезд. 
	// Диапазон возможных значений: от 0 до 3.
	unsigned int speed = settings.readNumEntry("/QStars/Speed", 2);
 
	// Нужно показывать космический корабль?
	bool showships = settings.readBoolEntry("/QStars/ShowShips", true);
 
	// Показываем космический корабль, если размеры окна это позволяют.
	if(showships && w > 300 && h > 200)
	{
	    // Выбираем случайным образом один из кораблей ...
	    QString file = "ship" + QString::number(rand()%5+1) + ".png";
	    QPixmap ship = QPixmap::fromMimeSource(file);
 
	    // ... и размещаем его в центре фонового изображения.
	    int px = (w - ship.width()) / 2;
	    int py = (h - ship.height()) / 2;
	    p.drawPixmap(px, py, ship);
	}
	// Устанавливаем фоновое изображение для окна.
	background = pixmap;
	setErasePixmap(pixmap);
 
	// Создаем необходимое количество звезд ...
	int numstars = (w+border) * h / 300;
        for(int i=0; i < numstars; i++)
	{
	    // ... и равномерно их распределяем по всей области окна.
	    int x = rand() % (w+border) - border;
	    int y = rand() % h;
 
	    // Яркость звезд находится в диапазоне от 30 до 255 единиц.
	    // Ярких звезд в процентном отношении должно быть меньше.
    	    int color = (rand()%16) * (rand()%16) + 30;
	    stars.append(new Star(x, y, color));
        }
	// Запускаем таймер с интервалом от 20 до 50 мс.
	startTimer(50/(speed+2));
    }
Функция srand() инициализирует генератор случайных чисел. В качестве параметра она принимает число, на основе которого строится последовательность псевдослучайных чисел. Вторая функция, rand(), при каждом вызове возвращает очередное число из данной последовательности. Эта последовательность будет повторена, если вызвать srand() с тем же инициализирующим значением. Поэтому для избежания повторяемости в качестве такого значения используется результат вызова функции time(0) - число секунд, прошедших с 1 января 1970 года. Возвращаемые псевдослучайные числа будут находиться в диапазоне от 0 до 2147483647. В результате операции rand() % N получается случайное число в диапазоне от 0 до N-1. Перед завершением своей работы конструктор вызывает функцию startTimer():     int QObject::startTimer( int interval ); При этом создается таймер, генерирующий события с интервалом interval мс. Если будет задан слишком маленький интервал ( приблизительно < 20 мс ), то Qt не будет успевать генерировать события, и некоторые из них будут пропущены. Для обработки событий таймера необходимо переопределить виртуальную функцию timerEvent():
    void Saver::timerEvent(QTimerEvent*)
    {
	QPainter p(this);
 
        for(Star* star = stars.first(); star; star = stars.next())
	    star->paint(&p);
    }
Задача timerEvent() - сообщить всем звездам, что нужно перерисовать себя в новой позиции. При возобновлении пользовательской активности работа хранителя экрана принудительно завершится. Поэтому в классе Saver нам не нужно переопределять обработчики событий клавиатуры (keyPressEvent()) и мыши (mousePressEvent()).

Класс Star

Это простой класс, который обеспечивает перемещение звезды и ее отображение в новом положении. При этом скорость перемещения звезды зависит от ее яркости: яркие звезды движутся быстрее. Класс поддерживает звезды размером 1x1 и 2x2 (с яркостью 255).
    Star::Star(int x, int y, int c)
    {
        sx = x;
        sy = y;
        color = c;
        counter = (255 - color) >> 4;
    }
 
    void Star::drawPixel(QPainter *p, int x, int y)
    {
	// Не рисуем поверх космического корабля.
        if(background.valid(x, y) && !qGray(background.pixel(x, y)))
	    p->drawPoint(x, y);
    }
 
    void Star::paint(QPainter *p)
    {
        if(sx < -border)	// Если звезда вышла за край экрана ...
        {
	    sx = w;		// ... то переносим ее в крайнее правое положение
	    sy = rand() % h;	// и меняем ее координату Y.
    	    return;
        }
 
        if(counter--) return;	// Не перемещаем звезду, если счетчик отличен от 0.
        counter = (255 - color) >> 4;	// Подбираем опытным путем.
 
        p->setPen(Qt::black);
	if(color==255)		// Звезда с яркостью 255 имеет размер 2x2.
        {			// Затираем след.
	    drawPixel(p, sx+1, sy  );
	    drawPixel(p, sx+1, sy+1);
        }
	else drawPixel(p, sx, sy);
 
        sx--;			// Перемещаем звезду влево на один пиксел ...
	p->setPen(QColor(color, color, color));
        drawPixel(p, sx, sy);	// ... и отображаем ее в новом положении.
	if(color==255) drawPixel(p, sx, sy+1);
    }

Запуск и работа

Хранитель экрана должен поддерживать четыре режима работы, которые указываются с помощью следующих аргументов командной строки:
  1. -demo - демонстрационный режим (режим по умолчанию). Хранитель экрана для своей работы создает окно фиксированного размера.
  2. -root - режим работы в корневом окне. Именно в этом режиме kdesktop запускает хранитель экрана для блокировки рабочей сессии пользователя. Идентификатор корневого окна хранитель экрана должен определить самостоятельно.
  3. -window-id %w - режим работы в окне с указанным идентификатором %w. Используется в программе KDE Control Center для предварительного просмотра хранителя экрана, выбранного пользователем.
  4. -setup - конфигурационный режим. Используется в KDE Control Center для вызова диалога настройки параметров работы хранителя экрана.
Разбором аргументов командной строки занимается функция main(). Также эта функция определяет идентификатор корневого окна, выводит справочную информацию о программе, обеспечивает настройку и сохранение параметров работы хранителя экрана, создает и показывает главное окно приложения. Ниже представлен исходный код функции main():
    int main(int argc, char** argv)
    {
        QApplication app(argc, argv);
 
        QString mode = "-demo";		// Режим по умолчанию.
	if(app.argc() > 1) mode = app.argv()[1];
 
        WId window = 0;			// Идентификатор окна X Window System.
 
	if(mode == "-window-id" && app.argc() > 2)
	    // Используем идентификатор существующего окна.
	    window = QString(app.argv()[2]).toULong();
	else if(mode == "-root")
	    // Используем идентификатор корневого окна.
	    window = DefaultRootWindow(qt_xdisplay());
 
        if(mode == "-help")	// Выводим сведения о программе.
	{
	    qWarning("Usage: " +QString(app.argv()[0])+ " [options]\n"
		"QStars Screensaver\n"
		"Options:\n"
		"  -help            Show help about options.\n"
		"  -setup           Setup screen saver.\n"
		"  -window-id wid   Run in the specified XWindow.\n"
		"  -root            Run in the root XWindow.\n"
		"  -demo            Start screen saver in demo mode. [default]\n");
	}
	else if(mode == "-setup")	// Показываем диалог настройки.
	{
	    // Читаем сохраненную конфигурацию.
	    QSettings settings;
	    unsigned int speed = settings.readNumEntry("/QStars/Speed", 2);
	    bool showships = settings.readBoolEntry("/QStars/ShowShips", true);
 
	    // Создаем диалог настройки.
	    QDialog dialog;
	    dialog.setCaption("QStars Setup");
	    QVBoxLayout vbox(&dialog,5);
 
	    QVGroupBox box1("Setup", &dialog);
	    QLabel label1("Speed:", &box1);
 
	    // Ползунок для задания скорости движения.
	    QSlider slider(0, 3, 1, speed, Qt::Horizontal, &box1);
	    slider.setTickmarks(QSlider::Left);
 
	    QCheckBox check("Show Ships", &box1);
    	    check.setChecked(showships);
    	    vbox.addWidget(&box1);
	    QVGroupBox box2("About", &dialog);
	    QLabel label2("QStars Screen Saver Version 0.1\n"
		"Copyright (C) 2004 Andi Peredri <andi@ukr.net>\n\n"
		"Homepage: \thttp://qt.osdn.org.ua\n"
		"Graphics: \tXShipWars Project\n"
		"License:  \tGNU General Public License", &box2);
            vbox.addWidget(&box2);
 
	    QHBoxLayout hbox(&vbox);
	    hbox.addStretch();
	    QPushButton button1("OK", &dialog);
    	    button1.setFocus();
	    dialog.connect(&button1, SIGNAL(clicked()), &dialog, SLOT(accept()));
	    hbox.addWidget(&button1);
	    QPushButton button2("Cancel", &dialog);
	    dialog.connect(&button2, SIGNAL(clicked()), &dialog, SLOT(reject()));
	    hbox.addWidget(&button2);
 
	    // Пользователь подтвердил произведенные настройки?
	    if(dialog.exec() == QDialog::Accepted)
	    {
		// ... тогда сохраняем конфигурацию.
		settings.writeEntry("/QStars/ShowShips", check.isChecked());
		settings.writeEntry("/QStars/Speed", slider.value());
	    }
	    // ... выход ...
	}
	else if(mode == "-window-id" || mode == "-demo" || mode == "-root")
	{
	    // Показываем хранитель экрана в одном из режимов.
	    Saver saver(window);
    	    saver.show();
	    app.setMainWidget(&saver);
	    return app.exec();
        }
	else
	{
	    // Недопустимый аргумент командной строки.
	    qWarning("Unknown option: " + mode +
		"\nUse -help to get a list of available command line options.\n");
	}
        return 0;
    }
Если при запуске программы указать параметр -setup, QStars откроет диалог настройки параметров работы (скорость перемещения звезд и необходимость отображения космического корабля):

QStars Setup

Если хранитель экрана запустить с параметром -root, то для нахождения идентификатора корневого окна будет использован макрос DefaultRootWindow(). В качестве параметра макрос принимает указатель на дисплей, который может быть получен с помощью функции qt_xdisplay():     Q_EXPORT Display *qt_xdisplay();    // Функция определена в qwindowdefs.h Изначально макрос DefaultRootWindow() определен в файле X11/Xlib.h. Но существующая его реализация не поддерживает работу с виртуальным корневым окном (Virtual Root Window), которое используется kdesktop для запуска хранителя экрана. Поэтому для обнаружения виртуального корневого окна нам необходимо использовать макрос DefaultRootWindow() из файла vroot.h. Файл vroot.h можно найти в пакете kdelibs-dev под именем /usr/include/kde/kscreensaver_vroot.h. Он не требует для своей компиляции KDE-библиотек, поэтому включен в состав программы QStars. Если вы используете в качестве настольной среды KDE, то, запустив QStars из командной строки с параметром -root, не увидите никаких визуальных проявлений, так как у вас запущен процесс kdesktop. Попробуйте временно приостановить его.

QStars.desktop

Перед тем, как добавить информацию о разрабатываемом хранителе экрана в KDE Control Center, для него необходимо создать файл описания (desktop entry file). В этом файле указываются: тип элемента (приложение, ярлык, устройство или каталог), команда запуска, поддерживаемые MIME-типы и еще около 25 других параметров, которые более подробно рассмотрены в документе Desktop Entry Standard. Ниже представлен файл описания QStars.desktop для хранителя экрана QStars:
    [Desktop Entry]
    Type=Application
    Name=QStars
    Exec=qstars
    Icon=kscreensaver
    Actions=Setup;InWindow;Root
 
    [Desktop Action Setup]
    Name=Setup...
    Exec=qstars -setup
    Icon=kscreensaver
 
    [Desktop Action InWindow]
    Name=Display in specified window
    Exec=qstars -window-id %w
    NoDisplay=true
 
    [Desktop Action Root]
    Name=Display in root window
    Exec=qstars -root
    NoDisplay=true
Файл начинается с обязательной секции [Desktop Entry]. Кратко поясним используемые параметры:
  1. Type - тип элемента, обязательный параметр.
  2. Name - видимое имя приложения или операции, обязательный параметр.
  3. Exec - команда запуска, можно указать аргументы.
  4. Icon - пиктограмма, используемая для приложения или операции.
  5. NoDisplay - информирует о том, что данная операция должна быть скрыта от пользователя.
  6. Actions - поддерживаемые операции, для каждой такой операции в desktop-файле должна быть определена своя секция [Desktop Action ...]
Чтобы сообщить KDE Control Center о нашем хранителе экрана, нужно поместить desktop-файл в один из следующих каталогов:
  1. ~/.kde/share/applnk/System/ScreenSavers - личный каталог пользователя.
  2. /usr/share/applnk/System/ScreenSavers - общесистемный каталог, хранитель экрана станет доступен всем пользователям системы.
Для этого каталог установки указываем в файле проекта qstars.pro:
    share.path  = /usr/share/applnk/System/ScreenSavers
    share.files = QStars.desktop
    INSTALLS   += share
После выполнения команды make install в списке установленных хранителей экрана появится программа QStars. В окне предварительного просмотра будет запущен хранитель экрана с параметром вида -window-id 37749288. В ответ на нажатие кнопки Setup KDE Control Center запустит QStars с параметром -setup и предложит пользователю произвести настройки. После их сохранения хранитель экрана будет автоматически перезапущен в окне предварительного просмотра.

Screensavers Window

Заключение

Таким образом, для разработки хранителя экрана с помощью Qt нам потребовалось написать чуть более 200 строк. Используя QStars в качестве шаблона, вы можете легко и быстро создать свой собственный хранитель экрана. Простота интеграции с KDE во многом обязана модульности KDE Control Center. Для этого нам пришлось создать desktop-файл, обеспечить поддержку командной строки и использовать идентификаторы окон X Window System. Эта техника может быть также с успехом использована для интеграции Gtk/Gnome и Qt/KDE приложений.