РАЗРАБОТКА КРОССПЛАТФОРМЕННОГО МЕНЕДЖЕРА ВИДЖЕТОВ РАБОЧЕГО СТОЛА

МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ

Федеральное государственное бюджетное образовательное учреждение

высшего профессионального образования

«КУБАНСКИЙ ГОСУДАРСТВЕННЫЙ УНИВЕРСИТЕТ»

(ФГБОУ ВПО «КубГУ»)

Кафедра вычислительных технологий

ДОПУСТИТЬ К ЗАЩИТЕ В ГАК

Заведующий кафедрой

д. ф. – м. – н., профессор

___________________А. И. Миков

(подпись)

__________________2014 г.

ВЫПУСКНАЯ КВАЛИФИКАЦИОННАЯ РАБОТА

Бакалавра

рАЗРАБОТКА КРОССПЛАТФОРМЕННОГО МЕНЕДЖЕРА ВИДЖЕТОВ РАБОЧЕГО СТОЛА

Работу выполнил _______________________________ А.А. Карташов

(подпись, дата)

Факультет компьютерных технологий и прикладной математики

Направление 010400.62 Информационные технологии

Научный руководитель

профессор, д. т. н.

заведующий кафедрой _________________________________ А.И.Миков

(подпись, дата)

Нормоконтролер ____________________________________В.В. Кузнецова

(подпись, дата)

Краснодар 2014

РЕФЕРАТ

Дипломная работа 32с., 5 рисунков, 3 источника.

Целью работы является разработка уникального програмного продукта – кроссплатформенного менеджера виджетов рабочего стола с небольшим начальным набором виджетов. Основновное отличие данного программного продукта – абстракция от конкретной операционной системы. Работа состоит из трех частей: анализа, проектирования и разработки. В первой части анализируются доступные программные средства, среди которых выбираются те, которые позволяют реализовать идею с оптимальными затратами ресурсов; проводится анализ требований к интерфейсным частям. Во второй части проводится проектировние менеджера виджетов, модулей виджетов и серверной части, определяются возможные механизмы языка. В третьей части проводится разработка менеджера виджетов, модулей виджетов и серверной части с уже использованными механизмами языка Python и библиотеки Qt. Данный программный комплекс может использоваться любым человеком на персональном компьютере, что позволит пользователю экономить свои ресурсы. В работе использован язык Python с использованием библиотеки Qt.

КЛЮЧЕВЫЕ СЛОВА ДАННОЙ РАБОТЫ:

кроссплатформенность, Python, Qt, виджет.


СОДЕРЖАНИЕ


ВВЕДЕНИЕ

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

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

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

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

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

1 Анализ.

  1. Программные средства.

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

- время на разработку

- время на тестирование

- время на поддержку

- время на расширение функционала

- время на компиляцию

Положительной стороной компилируемых является:

- более высокая скорость работы при прочих равных

- более низкое потребление ресурса ОЗУ при прочих равных

Избежать этих колоссальных затрат времени позволяют языки, исполняемые на виртуальных машинах, такие как Java и Python. Исходные коды, транслируясь в байт-код, выполняются на виртуальной машине (Java Virtual Machine и Python Virtual Machine соответственно), что сводит к минимуму платформо-зависимость: если на целевой ОС есть соответствующая виртуальная машина, то результат работы будет одинаковым даже на разных ОС. Более того, многие операции выполняются в вышеприведенных языках более лаконично, нежели в языках С и С++, что позволяет значительно снизить время на разработку. Недостатком языков, исполняемых на виртуальных машинах (далее - ВМ) являются большие требования к ОЗУ (так как требуется загрузить в память не только программу, но и саму ВМ), и большее время на выполнение операций, связанны с хост-машиной (например, записыва в файл, делается вызов в программе, затем, вызов к хост-ОС из ВМ, вызов хост-ОС к хост-машине).

Язык Python обладает следующими приемуществами перед Java:

  • Функции являются объектами второго рода, что позволяет сделать программу более лаконичной
  • Нестрогая типизация, предоставляющая высокий уровень абстракции
  • Динамичность, что позволяет сделать язык более лаконичным
  • Многие функции используют C-вызовы, что ускоряет работу программы
  • Python установлен «из коробки» в ОС семейства Mac OS и ОС с ядром Linux, что позволит сократить размеры stand-alone дистрибутива программы и облегчит установку для пользователя
  • Является открытым и бесплатным

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

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

- Написанна на С++

- Кроссплатформенность

- Широкий функционал

- Бесплатность

Для работы с Python и Qt используются так называемые «связывания» (англ. «bindings») PyQt, что позволяет использовать гибкость и динамичность Python и скорость работы Qt (C++).

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

  1. Интерфейс и функции виджетов.

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

Функции:

- Размещение виджетов на рабочем столе (т. н. Компоновка)

- Изменение состава виджетов на рабочем столе (добавление/удаление)

- Экспорт и импорт настроек

- Предоставление пользовательского интерфейса для предоставления пользователю вышеперечисленных функций

- Сериализация/десериализация

Рисунок 1. Интерфейс КМВ

Рисунок 2. Уведомление пользователя

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

Функции:

- Вывод текущего времени суток в графическом представлении

- Установка сигнала по времени (т. н. «будильник»)

- Изменение поправки отображаемого времени по запросу пользователя (т. н. «выбор часового пояса»)

- Предоставление пользовательского интерфейса для предоставления пользователю вышеперечисленных функций

- Сериализация/десериализация

Рисунок 3. Интерфейс часов

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

Функции:

- хранения произвольной текстовой информации ограниченной длины (неменее 512 символов)

- Предоставление пользовательского интерфейса для предоставления пользователю вышеперечисленных функций

- сериализация/десериализация

Рисунок 4. Интерфейс заметки

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

Функции:

- Вывод сетки дат текущего месяца в графическом представлении

- Изменение состава событий дат календаря (например, праздников)

- Оповещение о предстоящих событиях

- Предоставление пользовательского интерфейса для предоставления пользователю вышеперечисленных функций

- Сериализация/десериализация

Рисунок 5. Интерфейс календаря и заметка календаря

Серверная часть не ребует интерфейса.

Функции:

- Получение данных пользователя

- Проверка пользователя

- Хранение данных пользователя

- Возвращение данных по требованию


2 Проектирование

2.1 Менеджер виджетов

Перед запуском, менеджер виджетов должен проверить, на какой ОС он запущен, и, если ОС поддерживаемая – продолжить работу. Для обеспечения простого и гибкого добавления виджетов в набор виджетов, все КМВ осуществляет проверку каталога, и, в случае наличия в нем виджетов, оформленных в виде python-пакета, добавить его в список и позволить пользователю добавлять его на рабочий стол. Интерпритируемость и динамичность языка позволяет импортировать новые модули во время работы программы.

Для возможности синхронизации с сервером, необходимо работать с подсистемой сокетов. Пакет “socket” позволяет получить доступ к работе с подсистемой сокетов.

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

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

Создание иконки в трее и передача сообщения реализуются классом библиотеки Qt.

Для взаимодействия виджетов друг с другом, КМВ должен предоставлять соответствующие методы. Блягодаря «договорной» инкапсуляции языка python, взаимодействие с виджетами будет максимальным.

Для сериализации классов python используется, как правило, модуль pickle. Однако, ввиду того, что классы будут наследниками классов библиотеки Qt, требуется создать свой метод сериализации, который позволит восстановить состояние КМВ и виджетов однозначно.

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

2.2 Виджет часов

При отрисовке стрелок на часах необходимо использовать антиалиасинг, чтобы стрелки часов выглядели приятнее пользователю. Библиотека Qt позволяет использовать антиалиасинг при отрисовке виджетов.

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

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

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

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

Для удобства перемещения виджета необходимо реализовать механизм перемещения перетаскиванием.

2.3 Виджет заметки

Виджет должен поддерживать операции работы с буфером обмена. Библиотека Qt позволяет работать с буфером обмена ОС.

Для быстрого удаления данных заметки необходимо реализовать клавишу очистки.

Для удобства перемещения виджета необходимо реализовать механизм перемещения перетаскиванием.

2.4 Виджет календаря

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

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

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

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

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

Для удобства перемещения виджета необходимо реализовать механизм перемещения перетаскиванием.

2.5 Сервер

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

При разработке следует учесть специфическое ограничение python: один интерпритатор – один одновременно выполняющийся на процессоре поток исполнения. Это ограничение влияет исключительно на скрипты python, поэтому не замедлит выполнение приложения, использующие сторонние библиотеки, такие, как Qt, поэтому графическое исполнение виджетов и КМВ не будет ограничено этим ограничением.


3 Разработка

3.1 Менеджер виджетов

Была выбрана следующая файловая структура менеджера виджетов, внутри корневого каталога КМВ:

- Файл исходного кода CWM.pyw. Данный файл содержит логику КМВ, описанную на языке программирования python.

- Файл data.txt. Данный файл содержит персистентные данные локального пользователя о виджетах. Иными словами, если пользователь не подключался к серверу, или подключиться не удалось, менеджер сохранит данные в этот файл.

- Файл login.txt. Данный файл содержит информацию о данных подключения к серверу.

- Файл icon.xpm. Этот файл содержит изображение иконки, отображаемой в системном трее. Файл создан при помощи редактора GIMP.

- Каталог widgets. Данный каталог сожержит пакеты виджетов.

Перед запуском менеджера проверяется ОС, на которой запускается приложение. Для этого был использован модуль platform, функция system, возвращающая имя семейства системы строковым параметром, например, “Windows”.

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

Создается объект QApplication. Этот объект – основа работы с библиотекой Qt и взаимодействия с приложением, написанным на Qt, например, выход из приложения путем вызова метода exit().

Импортируются модули из каталога widgets, содержащие классы виджетов.

Логика КМВ заложена в классе CWM, который наследует класс QWidget.

Создается экземпляр класса CWM. Конструктору передается параметр – экземпляр раннее созданного приложения QApplication для взаимодействия с приложением и корректного завершения приложения.

Для приложения задается правило setQuitOnLastWindowClosed(False), запрещающее завершать приложение, если было закрыто последнее видимое окно сзаголовком, т.к. КМВ и виджеты не имеют заголовков окон, а КМВ не имеет видимого окна, лишь только иконку в системном трее.

Вызывается метод загрузки данных CWM.load().

Запускается приложение на Qt методом exec_().

КМВ оформлен в класс ввиду того, что это облегчит использование в других проектах КМВ, если это потребуется. Для этого нужно будет создать в каталоге КМВ пустой файл __init__.py и в проекте, в котором необходимо использовать его функционал, провести импорт класса: import CWM.

Конструктор класса CWM __init__(self, parent=None, app=None) проводит инициализацию и подготовку к работе, устанавливая значения по умолчанию для пароля, логина, хоста, создает иконку системного трея и меню, составляя подменю «добавить» на основе содержимого каталога widgets, составляет окно заполнения параметров подключения к серверу и скрывает его, проводит соединение соответствующих сигналов со слотами и, в случае успешной инициализации создает сообщение в системном трее о успешном запуске.

Класс CWM обладает следующими слотами:

- ontop. Предназначен для выведения всех виджетов на верхний слой оконного менеджера. Получает сигналы в случае выбора меню «активировать» в меню системного трея КМВ, или при нажатии на иконку системного трея КМВ. Позволяет пользователю увидеть виджеты, если он скрыл их другим окном.

- addWidgetSlt. Предназначен для отлавливания нажатий пунктов подменю в меню добавления нового виджета. Вызывает метод addWidget без параметра данных виджета.

- exit. Предназначен для завершения работы с менеджером виджетов. Получает сигнал при нажатии в меню КМВ пункта «Выход». Сораняет данные на сервере/локально.

- show_connect_diag. Предназначен для отображения диалога ввода данных для подключения к серверу. Получает сигнал при нажатии пункта меню «подключиться».

Класс CWM обладает следующими методами:

- save_login. Сохраняет данные о подключении к серверу в полях класса, обновляет значения «по умолчанию» в диалоге заполняния данных о подключении. Скрывает диалог ввода данных о подключении к серверу.

- addWidget. Метод добавления виджета. Принимает параметром имя виджета и опционально данные виджета, если виджет требуется десериализовать. В случае ошибки создает сообщение в трее о неудаче с иконкой сообщения «внимание». Виджет добавляется в список виджетов CWM.

- trayMessage. Показывает сообщение в системном трее. Принимает параметром заголовок сообщения, текст сообщения и опционально тип иконки (по умолчанию используется тип «Информация»).

- getWidget. Принимает имя виджета параметром и возвращает первый виджет с таким именем в списке виджетов, если такой виджет имелся.

- serialize. Возвращает все данные, необходимые для восстановления текущего состояния КМВ и виджетов, в виде строки. Вызывает метод сериализации каждого отдельного виджета, не являющегося сервисным, разделяя строки, принадлежащие каждому отдельному виджету спец-символом с hex-кодом 4, параметры положения х, у и данные разделяются спец-символом с hex-кодом 5.

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

Менеджер виджетов следит за расположением виджетов, проверяя их расположение и перекрытие другими окнами. Более того, если пользователь использовал на одном из своих ПК монитор с большим разрешением, и координаты виджетов сохранились так, что, когда пользователь перешел за другой ПК, оказались за границами видимого пространства, менеджер виджетов исправит ситуацию и расположит виджеты у границы, за которую они «хотели вылезть», позволяя пользователю не заботится о расположении виджетов на разных ПК с разными мониторами. КМВ позволяет добавлять новые виджеты в коллекцию виджетов просто и быстро: благодаря динамичности языка python достаточно просто поместить новый тип виджета в каталог widgets менеджера виджетов и перезапустить КМВ. При запуске новый тип виджета будет добавлен в подменю «добавить» с соответствующим именем. КМВ выводит пользователю информацию о работе и ошибках в неназойливом и наглядном виде путем отображения сообщений системного трея. Таким образом, пользователь не будет отвлечен от работы, если ему необходимо нахождения фокуса оконного менеджера на его приложении. Файл данных менеджера виджетов можно копировать и носить с собой, если другой ПК пользователя не имеет доступа к серверу. Более того, КМВ является portable программой, что означает, что его достаточно скопировать на носитель и запускать прямо с него, не устанавливая в целевой системе.

3.2 Виджет часов

Редактором GIMP были созданы следующие изображения:

- bg.png – Фоновое изображение виджета

- cb.png – Изображение для кнопки закрытия виджета

- cfg_b.png – Изображение кнопки управления будильником

- db.png – Изображение кнопки смены часового пояса

При помощи аппаратных средств был записан звук механического стрелочного будильника и сохранен в файл analogue_alarm.wav

Виджет часов оформлен в виде класса Clock в пакете clock. Класс реализует общие для всех реализованных виджетов методы:

- exit. Метод, закрывающий виджет без сохранения данных и высвобождеющий ресурсы системы, использованные данным виджетом. Виджет удаляетя из списка виджетов КМВ, скрывается, и вызывает деструктор. Стоит заметить, что в python нет такого понятия, как «деструктор» у класса ввиду автоматической сборки мусора, однако, унаследован класс QWidget, написанный на С++ в библиотеке Qt, которая, в свою очередь, имеет тоже специальный механизм особождения памяти. Поэтому, после закрытия виджета, вызывается метод ксласса destroy(), сообщающий механизму сборки Qt о том, что ресурсы виджета можно освободить.

- mousePressEvent. Отлавливает нажатия на виджет в области, которой можно перетаскивать. Если левая клавиша мыши зажата, то флажок moveing виджета устанавливается в True, что означает, что виджет можно перетаскивать. Флажок moveing утанавливается False после того, как клавиша была отущена.

- mouseMoveEvent. При перемещении курсора мыши и зажатой левой клавишей мыши (флажок moveing установлен в True) перемещает виджет, позволяя пользователю «перетаскивать» в удобное для пользователя места на рабочем столе. Пользователю не удасться переместить виджет за видимые границы экрана.

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

Список слотов класса:

- edit_alarms. Предназначен для отображения окна редактирования будильников. Получает сигнал от кнопки редактирования будильников.

- dt_pick_ok. Предназначен для вызова метода добавления будильника. Получает сигнал от кнопки «создать будильник» в окне редактирования будильника.

- dt_pick_del. Предназначен для удаления будильника. Получает сигнал от кнопки «удалить будильник» в окне редактирования будильника.

- change_delta. Предназначен для отображения окна выбора временного смещения отностительно UTC. Получает сигнал от кнопки редактирования верменного смещения относительно UTC.

- d_pick_ok. Предназначен для смены временного смещения относительно UTC. Получает сигнал от кнопки «задать часовой пояс» в окне редактирования смещения относительно UTC.

Методы класса:

- paintEvent. Метод, отрисовывающий стрелки на часах и, в случае отличия смещения времено от локального, надпись UTC величина_смещения.

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

- add_alarm. Метод добавления будильника в список. В случае, если такой будильник существует, выведет пользователю сообщение, что такой будильник уже существует.

- del_alarm. Метод удаления будильника.

- refresh_al_list. В данный метод был вынесен повторяемый функционал для сокращения количества кода. Метод обновляет список существующих будильников в окне управления будильниками.

- alarm. Метод, воспроизводящий звуковой сигнал analogue_alarm.wav. Содержит платформо-зависимый код. Для воспроизведения звука использует способы библиотеки Qt, но на Linux эти методы не работают, поэтому на ОС семейства Linux использует sox для вывода звука.

Общий стиль виджета соответствует представлению человека о аналоговых часах, что облегчает восприятие времени человеком. Изменение часового пояса производится легко и быстро, а в списке выбора смещения напротив локального смещения указано в скобках, что пользователь в этом часовом поясе. Управление будильниками предельно просто: при нажатии на кнопку управления перед пользователем предстает интерфейс управления будильниками. В списке отображаются время и даты будильников (будильник можно устанавливать на любое время и дату, хоть в следующем году). Если пользователь планирует установить будильник в будущем, то, нажав на пиктограмму календаря в поле редактирования времени будильника, появится всплывающее окно календаря, на котором пользователь может выбрать необходимую дату. Заполнив время, дату (автоматически подставляется сегодня), комментарий будильника, необхожимо нажать кнопку «добавить будильник». Будильник будет добавлен и отобразится в списке, который отсортирован по близости времени будильников. Для удаления достаточно выбрать будильник в списке и нажать кнопку «удалить будильник». Все будильники, время которых находится в прошлом от текущего момента, сработают в ближайшем timerEvent, однако, сигнал будет один, чтобы за счет суммирования звуков звуковой системой ОС не перегрузить акустическую систему ПК и не повредить слух пользователя. Так же, при наступлении времени будильника создается заметка с текстом того, что будильники сработали и описанием будильников. Это оповестит пользователя даже если он отошел от ПК во время срабатывания будильника.

3.3 Виджет заметки

Редактором GIMP были созданы следующие изображения:

- bg.png – Фоновое изображение виджета

- cb.png – Изображение для кнопки закрытия виджета

- clrb.png – Изображение кнопки очистки содержимого

Виджет заметки оформлен в виде класса Note в пакете note. Класс реализует общие для всех реализованных виджетов методы:

- exit. Метод, закрывающий виджет без сохранения данных и высвобождеющий ресурсы системы, использованные данным виджетом. Виджет удаляетя из списка виджетов КМВ, скрывается, и вызывает деструктор. Стоит заметить, что в python нет такого понятия, как «деструктор» у класса ввиду автоматической сборки мусора, однако, унаследован класс QWidget, написанный на С++ в библиотеке Qt, которая, в свою очередь, имеет тоже специальный механизм особождения памяти. Поэтому, после закрытия виджета, вызывается метод ксласса destroy(), сообщающий механизму сборки Qt о том, что ресурсы виджета можно освободить.

- mousePressEvent. Отлавливает нажатия на виджет в области, которой можно перетаскивать. Если левая клавиша мыши зажата, то флажок moveing виджета устанавливается в True, что означает, что виджет можно перетаскивать. Флажок moveing утанавливается False после того, как клавиша была отущена.

- mouseMoveEvent. При перемещении курсора мыши и зажатой левой клавишей мыши (флажок moveing установлен в True) перемещает виджет, позволяя пользователю «перетаскивать» в удобное для пользователя места на рабочем столе. Пользователю не удасться переместить виджет за видимые границы экрана.

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

Класс обладает следующими слотами:

- clear_note. Очищает текст заметки. Получает сигнал в случае нажатия кнопки очистки заметки.

Класс обладает следующими методами:

- set_text. Метод предназначен для сторонних виджетов для задания текста заметки. Данный метод используют виджеты календарь и часы для отображения информации.

Виджет заметки визуально оформлен в виде занкомых всем современным людям небольшими отрывными цветными листочками – стикерами. Такой дизайн создает у пользователя правильное представление о виджете: хранение и отображение небольших объемов информации и заметок. Кнопки виджета заметки выполнены в едином стиле: кнопка очистки имеет пиктограмму ластика, а кнопка закрытия – рукописного диагонального крестика. Поля ввода заметки позволяет использовать буфер обмена ОС, позволяя пользователю использовать вставку и копирование текста (клавиатурные комбинации Ctrl+C и Ctrl+V, а также комбинация «вырезать» Ctrl+X).

3.4 Виджет календаря

Редактором GIMP были созданы следующие изображения:

- bg.png – Фоновое изображение виджета

- cb.png – Изображение для кнопки закрытия виджета

- cfg_b.png – Изображение кнопки управления датами

- aeb.png – Изображение кнопки добавления события

Виджет календаря оформлен в виде класса Calendar в пакете calendar. Класс реализует общие для всех реализованных виджетов методы:

- exit. Метод, закрывающий виджет без сохранения данных и высвобождеющий ресурсы системы, использованные данным виджетом. Виджет удаляетя из списка виджетов КМВ, скрывается, и вызывает деструктор. Стоит заметить, что в python нет такого понятия, как «деструктор» у класса ввиду автоматической сборки мусора, однако, унаследован класс QWidget, написанный на С++ в библиотеке Qt, которая, в свою очередь, имеет тоже специальный механизм особождения памяти. Поэтому, после закрытия виджета, вызывается метод ксласса destroy(), сообщающий механизму сборки Qt о том, что ресурсы виджета можно освободить.

- mousePressEvent. Отлавливает нажатия на виджет в области, которой можно перетаскивать. Если левая клавиша мыши зажата, то флажок moveing виджета устанавливается в True, что означает, что виджет можно перетаскивать. Флажок moveing утанавливается False после того, как клавиша была отущена.

- mouseMoveEvent. При перемещении курсора мыши и зажатой левой клавишей мыши (флажок moveing установлен в True) перемещает виджет, позволяя пользователю «перетаскивать» в удобное для пользователя места на рабочем столе. Пользователю не удасться переместить виджет за видимые границы экрана.

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

Класс обладает следующими слотами:

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

- add_event. Предназначен для проверки данных, введенных пользователем и, в случае верности заполненных полей, добавления события в соответствующих список. Получает сигнал от кнопки «добавить событие» в окне добавления события.

- del_event. Предназначен для удаления события. Получает сигнал от кнопки «удалить событие» в окне редактирования событий.

- show_help. Предназначен для отображения окна справочной информации о составлении событий. Получает сигнал от кнопки «справка» в окне добавления события.

- configure. Предназначен для отображения окна управления событиями. Получает сигнал от кнопки конфигурации.

Класс обладает следующими методами:

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

- on_activate. Метод, вызываемый при двойном нажатии на дате в календаре. Автоматически заполняет год, месяц, день месяца значениями сегодняшнего дня, устанавливает тип события в «событие» и отображает окно добавления события. Этот метод обеспечивает быстрое и удобное добавление события в календарь.

- update_events. Метод, обновляющий отображение событий в календаре. Осуществляет обход собыйтий в списках повторяющихся событий и разовых событий, получая их даты на промежутке от сегодня-30 дней до сегодня+366 дней, заполняет список дат с описаниями событий и выделяет в календаре даты путем изменения параметра шрифта на «жирный подчеркнутый».

- near_events_text. Метод, возвращающий текст о предстоящих событиях. Этот текст используется в первоначальном сообщении календаря, в специальной заметке. Если ближайших событий нет, то возвращает «на ближайшие дни событий нет».

- date_clocked. Метод, вызываемый при нажатии на дату в календаре. Если на эту дату назначено событие, то появится информационный виджет-заметка календаря, содержащая текст о событиях в выделенный день. Если событий в этот день нет, то заметка не отображается/скрывается открытая. Заметка, вызываемая календарем является виджетом заметкой – «миньоном». Виджет-миньон не будет сериализован по завершении работы.

- refresh_ev_list. Метод, содержащий повторяющийся функционал. Вызывается для обновления списка событий в окне редактирования событий.

Также, для осуществления функционала календаря, а также для задания особых условий дат, в пакете Calendar был создан класс события - Event. Данный класс содержит данные о условиях наступления событий и содержит следующие методы:

- match. Метод, возвращающий True, если дата, переданная параметром в этот метод, соответствует условиям наступления события.

- get_dates. Метод, возвращающий список кортежей даты и описания события для текущего события на промежутке времени, переданного в параметрах. Иными словами, метод возвращает все даты, когда наступит событие, на промежутке, переданному в параметры методу. Список дат возвращается потому, что некоторые события могут содержать особые условия, например «третий понедельник», который, соответственно, есть в каждом месяце каждого года, и, таким образом, метод, при параметрах (сегодня - 30, сегодня + 366) вернет 13 дат.

- __eq__. Перегружаемый метод класса Object, возвращает True, если события эквивалентны.

- __ne__. Перегружаемый метод класса Object, возвразает not __eq__

- __str__. Перегружаемый метод класса Object, возвращает строковое представление события. Перегрузка метода была необходима для наглядного отображения события и его условий наступления в списке событий окна редактирования событий.

Виджет календаря оформлен визуально в виде привычного большинству пользователей перекидного календаря. Виджет выполняет функции календаря и программы, уведомляющей о событиях, которые были добавлены пользователем. Окно добавления события предоставляет пользователю широкий выбор условий для создания события, вместе с тем, оставляя простым добавления события: для добавления дня рождения достаточно выбрать тип события «день рождения», заполнить поля года, месяца (числом), дня месяца, описание события и нажать клавишу «добавить»! Обозначенная дата сразу будет отмечена в календаре, а при клике на ней, будет выведена заметка с описанием и количеством лет, исполняющихся имениннику. Для предоставления информации о заполнении полей событий создана справочная информация, окно с которой вызывается путем нажатия клавиши «справка» в окне создания события. При запуске, календарь уведомляет пользователя о предстоящих событиях, позволяя ему быть в курсе предстоящий событий. Удобное добавление события по двойному клику на дате значительно увеличивает скорость добавления события, повышая удобство работы с виджетом. Выделение дат с событиями подчеркнутым жирным шрифтом позволяет информировать пользователя о событиях, но при этом не перегружать интерфейс цветовыми маркерами дней. Кнопка управления событиями позволяет быстро просмотреть списки событий и удалить ненужные события.

3.5 Сервер

Серверная часть легкореализуема и полностью кросс-платформенна ввиду того, что использует только стандартную библиотеку python. Сервер выдает сервисные сообщения в консоль, в которой был запущен. По умолчанию, сервер работает на порту 33333, однако, ввиду интерпритируемости языка python, порт может быть быстро сменен (для избежания конфликтов системы). Поток сервера содержит основной цикл исполнения, называемый mainloop. В этом цикле сервер извлекает соединение из очереди на сокете, получает данные, разбивает данные на код, логин, пароль, данные.

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


ЗАКЛЮЧЕНИЕ

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

Также, был более глубоко изучен язык Python, библиотека Qt и связки языка Python с библиотекой Qt PyQt. Были изучены тонкости работы языка python при использовании библиотеки Qt, закреплены уже известные знания по работе с данными технологиями, особенности кросс-платформенной разработки приложения. Был получен колоссальный опыт в построении пользовательских интерфейсов – UI и работы с пользователями, тестирующими приложение на добровольной основе. Гибкость языка Python и скорость работы и компактность Qt позволили созать приложение суммарым размером в 1.37МиБ с учетом изображений и аудиофайлов, а при работе, несмотря на ресурсоемкость Python, потребляющим всего 19МиБ ОЗУ при трех запущенных виджетах (часы, календарь, заметка), и потребляющим менее 1% ресурсов CPU.


СПИСОК ИСПОЛЬЗОВАННЫХ ИСТОЧНИКОВ

  1. Python: Python v3.2.5 documentation. URL: https://docs.python.org/3.2/index.html (дата обращения 3.06.2014)
  2. PyQt: PyQt Class Reference. URL: http://pyqt.sourceforge.net/Docs/PyQt4/classes.html (дата обращения 5.06.2014)
  3. Qt: Qt4.8 documentation. URL: http://qt-project.org/doc/qt-4.8/ (дата обращения 5.06.2014)

ПРИЛОЖЕНИЕ А

Описание программы

1.Общие сведения

Данная программа написана на языке Python, основные файлы исходного кода программы CWM.pyw, файлы __init__.py в каталогах виджетов. Для запуска необходим интерпритатор python 3, установленные библиотеки Qt и связывания PyQt4.

2. Функциональное назначение

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

3. Описание логической структуры

Код программы написан лаконично и ясно. В коде нет двойственности или «хаков». Код снабжен комментариями в местах, где стороннему разработчику может быть сложно понять его.

Менеджер виджетов, код которого описан в CWM.pyw работает по следующему алгоритму:

  1. Проверка имени семейства хост-ос
  2. Установка локали
  3. Создание экземпляра приложения QApplication
  4. Установка параметров QApplication
  5. Импорт классов виджетов
  6. Инициализация. Создание форм, меню, иконки в системном трее.
  7. Загрузка данных, десериализация.
  8. Запуск основного цикла приложения QApplication.
  9. Ожидание действия в основном цикле.

Действия могут быть следующими:

- Выход с сохранением данных локально и на сервере

- Добавление виджета

- Отображение диалога ввода данных для подключения к серверу

- Перенос виджетов на верхний слой

- Отобразить сообщение в трее

- Вернуть виджет с определенным именем

- Сериализация

Виджет часов опиан в widgets/clock/__init__.py и работает по следующему алгоритму:

  1. Инициализация: восстановление состояния, если переданы данные в конструктор.
  2. Инициализация окон, форм и данных.
  3. Отрисовка основного окна, запуск таймера.
  4. Ожидание действия.

Действия могут быть следующими:

- Событие таймера – отрисовка интерфейса и проверка будильников.

- Добавление будильника.

- Удаление будильника.

- Звуковой сигнал.

- Изменение смещения.

- Закрытие.

- Сериализация.

- Перемещение.

Виджет заметки опиан в widgets/note/__init__.py и работает по следующему алгоритму:

  1. Инициализация: восстановление состояния, если переданы данные в конструктор.
  2. Инициализация окона.
  3. Отрисовка основного окна.
  4. Ожидание действия.

Действия могут быть следующими:

- Очистка поля.

- Работа с буфером обмена.

- Закрытие.

- Сериализация.

- Перемещение.

Виджет календаря опиан в widgets/calendar/__init__.py и работает по следующему алгоритму:

  1. Инициализация: восстановление состояния, если переданы данные в конструктор.
  2. Инициализация окон, форм и данных.
  3. Проверка дат.
  4. Отрисовка основного окна, запуск таймера.
  5. Ожидание действия.

Действия могут быть следующими:

- Событие таймера – отображение вспомогательного виджета заметки для отображения сообщения о предстоящих датах.

- Добавление события.

- Удаление события.

- Обновление списка дат.

- Отображение информационной заметки от дате.

- Закрытие.

- Перемещение.

- Сериализация.

Также следует отметить алгоритм проверки класса события, метода match:

  1. Если задан год и год не в списке годов, вернуть Ложь.
  2. Если задан день года и день года не в списке дней года, вернуть Ложь.
  3. Если задан месяц и месяц не в списке месяцев, вернуть Ложь.
  4. Если задан день месяца и день месяца не в списке дней месяца, вернуть Ложь.
  5. Если задан день недели и день недели не в списке дней недели, вернуть Ложь, иначе:
    1. Если задан номер дня недели и номер дня недели не в списке номеров дней недели, вернуть ложь, иначе если задан последний номер дня недели и день недели последний, вернуть Истина

6 Вернуть Истина

Таким образом, если критейрий не задан, например, не указан год, то он считается всегда верным, т.е., событие «13 марта» будет соответствовать 13.3.[любой год]

4. Используемые технические средства

Любой персональный компьютер, работающий под управлением операционной системы Windows XP, Vista, 7, 8; Mac OS; Linux с графической оболочкой. Установленный интерпритатор python версии 3.2 и выше, утановленные библиотеки Qt версии выше 4.7 и ниже 5.0.

5. Вызов и загрузка.

Для вызова программы нужно два раза щелнуть левой кнопкой мыши на файле CWM.pyw. В системе должны быть установлены значения по умолчанию для pyw файлов – запуск программой python (как правило, алиас устанавливается при установке дистрибудтива интерпритатора python). Если запуска не произошло, возможен запуск из командной строки командой «pythonw CWM.pyw» в ОС семейства Windows, или «python CWM.pyw» в ОС семейства Linux и Mac OS.

6. Входные данные

Входными данными КМВ и виджетов являются конфигурационные файлы и события, передаваемые ОС (счетчики, нажатия клавиш клавиатуры, нажатие клавиш мыши, движение мыши, данные, полученные по сети).

7. Выходные данные.

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


ПРИЛОЖЕНИЕ Б

Пояснительная записка

Введение.

Основаная задача программного комплекса – предоставить функции виджетов пользователю, абстрагировав его от ограничений ОС. Основными документами на разработку являются учебные программы направлений бакалавриата, согласно которым, студент обязан выполнить выпускную квалификационную работу. Тематика выпускной квалификационной работы Карташова Александра, студента КубГУ ФКТиПМ 46 группы - «Разработка кросплатформенного менеджера виджетов рабочего стола»

1.Назначение и область применения.

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

2. Технические характеристики.

Постановка задачи произведена на основе технического задания на выполнение выпускной квалификационной работы. Согласно этому документу данная программа написана на языке Python с использованием библиотеки Qt.

Основные файлы исходного кода программы CWM.pyw, файлы widgets/clock/__init__.py, widgets/note/__init__.py, widgets/calendar/__init__.py.

Менеджер виджетов, код которого описан в CWM.pyw работает по следующему алгоритму:

1. Проверка имени семейства хост-ос

2. Установка локали

3. Создание экземпляра приложения QApplication

4. Установка параметров QApplication

5. Импорт классов виджетов

6. Инициализация. Создание форм, меню, иконки в системном трее.

7. Загрузка данных, десериализация.

8. Запуск основного цикла приложения QApplication.

9. Ожидание действия в основном цикле.

Действия могут быть следующими:

- Выход с сохранением данных локально и на сервере

- Добавление виджета

- Отображение диалога ввода данных для подключения к серверу

- Перенос виджетов на верхний слой

- Отобразить сообщение в трее

- Вернуть виджет с определенным именем

- Сериализация

Виджет часов опиан в widgets/clock/__init__.py и работает по следующему алгоритму:

1. Инициализация: восстановление состояния, если переданы данные в конструктор.

2. Инициализация окон, форм и данных.

3. Отрисовка основного окна, запуск таймера.

4. Ожидание действия.

Действия могут быть следующими:

- Событие таймера – отрисовка интерфейса и проверка будильников.

- Добавление будильника.

- Удаление будильника.

- Звуковой сигнал.

- Изменение смещения.

- Закрытие.

- Сериализация.

- Перемещение.

Виджет заметки опиан в widgets/note/__init__.py и работает по следующему алгоритму:

1. Инициализация: восстановление состояния, если переданы данные в конструктор.

2. Инициализация окона.

3. Отрисовка основного окна.

4. Ожидание действия.

Действия могут быть следующими:

- Очистка поля.

- Работа с буфером обмена.

- Закрытие.

- Сериализация.

- Перемещение.

Виджет календаря опиан в widgets/calendar/__init__.py и работает по следующему алгоритму:

1. Инициализация: восстановление состояния, если переданы данные в конструктор.

2. Инициализация окон, форм и данных.

3. Проверка дат.

4. Отрисовка основного окна, запуск таймера.

5. Ожидание действия.

Действия могут быть следующими:

- Событие таймера – отображение вспомогательного виджета заметки для отображения сообщения о предстоящих датах.

- Добавление события.

- Удаление события.

- Обновление списка дат.

- Отображение информационной заметки от дате.

- Закрытие.

- Перемещение.

- Сериализация.

Также следует отметить алгоритм проверки класса события, метода match:

1. Если задан год и год не в списке годов, вернуть Ложь.

2. Если задан день года и день года не в списке дней года, вернуть Ложь.

3. Если задан месяц и месяц не в списке месяцев, вернуть Ложь.

4. Если задан день месяца и день месяца не в списке дней месяца, вернуть Ложь.

5. Если задан день недели и день недели не в списке дней недели, вернуть Ложь, иначе:

5.1 Если задан номер дня недели и номер дня недели не в списке номеров дней недели, вернуть ложь, иначе если задан последний номер дня недели и день недели последний, вернуть Истина

6 Вернуть Истина

Таким образом, если критейрий не задан, например, не указан год, то он считается всегда верным, т.е., событие «13 марта» будет соответствовать 13.3.[любой год]

3.Технико-экономические показатели.

Суммарно, код программы содержит 1208 строк кода, без учета по оформлению кода согласно стандарту оформления кода PEP8.

Колчество ОЗУ, потребляемое во время работы программы с запущенными 3 виджетами (календарь, часы, заметка), составляет 19МиБ. Процент процессорного времени на ПК с двухядерным ЦП фирмы Intel с тактовой частотой ядер 2.93 МГц составляет менее 1% (ОС округляет показатель до 0%), интерфейс отвечает на действия пользователя без задержек.


Источники, использованные при разработке.

1. Python: Python v3.2.5 documentation. URL: https://docs.python.org/3.2/index.html (дата обращения 3.06.2014)

2. PyQt: PyQt Class Reference. URL: http://pyqt.sourceforge.net/Docs/PyQt4/classes.html (дата обращения 5.06.2014)

3. Qt: Qt4.8 documentation. URL: http://qt-project.org/doc/qt-4.8/ (дата обращения 5.06.2014)

ПРИЛОЖЕНИЕ В

Код программы

CWM.pyw

import sys

import os

from PyQt4 import QtGui

from PyQt4 import QtCore

from platform import system

import locale

class CWM(QtGui.QWidget):

"""CWM"""

def __init__(self, parent=None, app=None):

"""Инициализация"""

QtGui.QWidget.__init__(self, parent)

self.app = app

self.name = 'CWM'

self.widget_list = []

self.login = ''

self.password = ''

self.host = '192.168.0.1'

self.max_size = QtGui.QDesktopWidget().availableGeometry().size()

self.tray_icon = QtGui.QSystemTrayIcon(QtGui.QIcon("icon.xpm"))

self.tray_menu = QtGui.QMenu()

self.add_wgt_menu = QtGui.QMenu('Добавить')

for w in filter(lambda x: not x.startswith('__'), os.listdir("widgets")):

act = QtGui.QAction(w.capitalize(), self)

act.setStatusTip("Виджет %s" % w)

QtCore.QObject.connect(act, QtCore.SIGNAL("triggered()"), self, QtCore.SLOT("addWidgetSlt()"))

self.add_wgt_menu.addAction(act)

self.tray_menu.addMenu(self.add_wgt_menu)

act = QtGui.QAction("Активировать", self)

act.setStatusTip("Список")

QtCore.QObject.connect(act, QtCore.SIGNAL("triggered()"), self, QtCore.SLOT("ontop()"))

self.tray_menu.addAction(act)

act = QtGui.QAction("Залогинится", self)

act.setStatusTip("Указать логин, пароль, хост")

QtCore.QObject.connect(act, QtCore.SIGNAL("triggered()"), self, QtCore.SLOT("show_connect_diag()"))

self.tray_menu.addAction(act)

act = QtGui.QAction("Выход", self)

act.setStatusTip("Завершить работу приложения")

QtCore.QObject.connect(act, QtCore.SIGNAL("triggered()"), self, QtCore.SLOT("exit()"))

self.tray_menu.addAction(act)

self.tray_icon.setContextMenu(self.tray_menu)

QtCore.QObject.connect(self.tray_icon, QtCore.SIGNAL("activated(QSystemTrayIcon::ActivationReason)"), self, QtCore.SLOT("ontop()"))

self.tray_icon.show()

self.setToolTip('Менеджер виджетов')

self.trayMessage('Привет!', 'Виджет менеджер успешно запущен ;)')

# диалог задания логина

self.connect_diag = QtGui.QWidget()

self.connect_diag.setWindowTitle('Логин')

self.connect_diag.setLayout(QtGui.QVBoxLayout())

layout = QtGui.QHBoxLayout()

layout.addWidget(QtGui.QLabel('Хост'))

layout.addSpacing(15)

self.cd_host = QtGui.QLineEdit()

if self.host:

self.cd_host.setText(self.host)

layout.addWidget(self.cd_host)

self.connect_diag.layout().addLayout(layout)

layout = QtGui.QHBoxLayout()

layout.addWidget(QtGui.QLabel('Логин'))

layout.addSpacing(7)

self.cd_login = QtGui.QLineEdit()

if self.login:

self.cd_login.setText(self.login)

layout.addWidget(self.cd_login)

self.connect_diag.layout().addLayout(layout)

layout = QtGui.QHBoxLayout()

layout.addWidget(QtGui.QLabel('Пароль'))

self.cd_password = QtGui.QLineEdit()

if self.password:

self.cd_password.setText(self.password)

layout.addWidget(self.cd_password)

self.connect_diag.layout().addLayout(layout)

btn = QtGui.QPushButton('Сохранить')

self.connect_diag.layout().addWidget(btn)

QtCore.QObject.connect(btn, QtCore.SIGNAL("clicked()"), self.save_login)

def save_login(self):

self.login = self.cd_login.text()

self.password = self.cd_password.text()

self.host = self.cd_host.text()

self.connect_diag.hide()

@QtCore.pyqtSlot()

def ontop(self):

self.activateWindow()

@QtCore.pyqtSlot()

def addWidgetSlt(self):

self.addWidget(QtCore.QObject.sender(self).text())

@QtCore.pyqtSlot()

def exit(self):

if len(self.widget_list):

with open('data.txt', 'w') as f:

f.write(self.serialize())

f.flush()

app.exit()

def addWidget(self, widget_name, widget_data=None):

"""Виджет будет добавлен, если его пакет находится

в каталоге widgets, имя пакета строчными буквами,

имя класса - с прописной буквы строчными:

пример: находится в каталоге widgets/clock,

имя виджета Clock"""

try:

constr = eval(widget_name.capitalize())

wgt = constr(self, data=widget_data)

self.widget_list.append(wgt)

return wgt

except Exception as e:

self.trayMessage('Ошибка', 'Ошибка добавления виджета ' + widget_name,

QtGui.QSystemTrayIcon.Warning)

print(e)

def trayMessage(self, title, text, icon=QtGui.QSystemTrayIcon.Information):

self.tray_icon.showMessage(title, text, icon)

def getWidget(self, widget_name):

"""Возвращает первый в списке запущенный виджет с

именем типа виджета widget_name"""

for w in self.widget_list:

if w.name == widget_name:

return w

def serialize(self):

"""Сериализация. Ввиду того, что это PyQt,

стандартные методы не подходят

(c object'ты не сериализуются)"""

with open('login.txt', 'w') as lf:

lf.write(self.host + '\n')

lf.write(self.login + '\n')

lf.write(self.password + '\n')

return '\x04'.join(['\x05'.join([repr(s.x()), repr(s.y()), s.serialize()]) for s in filter(lambda x: not x.minion, self.widget_list)])

@QtCore.pyqtSlot()

def show_connect_diag(self):

self.connect_diag.show()

def load(self):

"""Десериализация. Считывание файла-конфига"""

try:

with open('login.txt') as lf:

self.host = lf.readline()

self.login = lf.readline()

self.password = lf.readline()

if self.host:

self.cd_host.setText(self.host)

if self.login:

self.cd_login.setText(self.login)

if self.password:

self.cd_password.setText(self.password)

with open('data.txt') as f:

wgt_datas = f.read().split('\x04')

for wgt_data in wgt_datas:

wx, wy, wgt_data = wgt_data.split('\x05')

wgt_name = wgt_data.split('\x03')[0]

wgt = self.addWidget(wgt_name, wgt_data)

wx = eval(wx)

wy = eval(wy)

# проверим, не находится ли виджет за границами экрана

if wx < 0:

wx = 0

if wx > (self.max_size.width() - wgt.width()):

wx = self.max_size.width() - wgt.width()

if wy < 0:

wy = 0

if wy > (self.max_size.height() - wgt.height()):

wy = self.max_size.height() - wgt.height()

wgt.move(wx, wy)

except Exception as e:

print(e)

if __name__ == '__main__':

oslist = ['Windows', 'Linux']

if system() not in oslist:

machine = -1

print("CWM не будет работать на неизвестной ему ОС\n"

"Проверьте переменные окружения, где указано имя ОС,\n"

"если вы их изменяли.\n"

"Поддерживаемые ОС: " + ', '.join(oslist))

exit(1)

locale.setlocale(locale.LC_ALL, '')

app = QtGui.QApplication(sys.argv)

app.setQuitOnLastWindowClosed(False)

# импорт всех пакетов, что находятся в widgets

for widget in filter(lambda x: not x.startswith('__'), os.listdir("widgets")):

exec("from widgets.%s import *" % widget)

cwm = CWM(app=app) # создаем экземпляр менеджера виджетов

cwm.load()

sys.exit(app.exec_())

widgets/clock/__init__.py

"""

Виджет часов

"""

from PyQt4 import QtCore, QtGui

import datetime

from math import cos, sin, pi

from platform import system

class Clock(QtGui.QWidget):

"""Часы"""

def __init__(self, parent, data=None):

"""Инициализация"""

QtGui.QWidget.__init__(self, parent)

self.name = 'Clock'

self.wm = parent

# если пришли данные - восстановим

if data:

data = data.split('\x03')

self.show_notific = eval(data[1])

self.show_tray_notific = eval(data[2])

self.alarms = eval(data[3])

self.delta = eval(data[4])

else:

self.alarms = {}

self.show_notific = True

self.show_tray_notific = True

self.delta = datetime.datetime.now() - datetime.datetime.now().utcnow()

self.ldelta = datetime.datetime.now() - datetime.datetime.now().utcnow()

self.grabpoint = None

self.moveing = None

self.minion = False

# Управление будильниками

self.dt_picker_wdg = QtGui.QWidget()

self.dt_picker_wdg.setWindowTitle('Управление будильником')

self.dt_picker_wdg.resize(400, 500)

self.dt_picker_wdg.setLayout(QtGui.QVBoxLayout())

self.al_list = QtGui.QListView()

self.al_model = QtGui.QStandardItemModel(self.al_list)

self.al_list.setModel(self.al_model)

self.dt_picker_wdg.layout().addWidget(self.al_list)

clndr = QtGui.QCalendarWidget()

clndr.setVerticalHeaderFormat(QtGui.QCalendarWidget.NoVerticalHeader)

clndr.setFirstDayOfWeek(QtCore.Qt.Monday)

self.dt_edit = QtGui.QDateTimeEdit()

self.dt_edit.setCalendarPopup(True)

self.dt_edit.setCalendarWidget(clndr)

self.dt_picker_wdg.layout().addWidget(self.dt_edit)

self.cmnt_edit = QtGui.QLineEdit()

self.cmnt_edit.setText('Введите коментарий')

self.dt_picker_wdg.layout().addWidget(self.cmnt_edit)

dt_pkr_wgt_ok = QtGui.QPushButton()

dt_pkr_wgt_ok.setText('Создать будильник')

self.dt_picker_wdg.layout().addWidget(dt_pkr_wgt_ok)

QtCore.QObject.connect(dt_pkr_wgt_ok, QtCore.SIGNAL("clicked()"), self, QtCore.SLOT('dt_pick_ok()'))

dt_pkr_wgt_del = QtGui.QPushButton()

dt_pkr_wgt_del.setText('Удалить будильник')

self.dt_picker_wdg.layout().addWidget(dt_pkr_wgt_del)

QtCore.QObject.connect(dt_pkr_wgt_del, QtCore.SIGNAL("clicked()"), self, QtCore.SLOT('dt_pick_del()'))

# Выбор часового пояса

self.d_picker_wdg = QtGui.QWidget()

self.d_picker_wdg.setWindowTitle('Выберите часовой пояс')

self.d_picker_wdg.resize(200, 40)

self.d_picker_wdg.setLayout(QtGui.QVBoxLayout())

self.d_edit = QtGui.QComboBox()

for i in range(14, -14, -1):

self.d_edit.addItem('UTC %s%d %s' % ('+' if i >= 0 else '', i, '(Локальное)' if (self.ldelta.seconds//3600 == i) else ''), i)

self.d_picker_wdg.layout().addWidget(self.d_edit)

d_pkr_wgt_ok = QtGui.QPushButton()

d_pkr_wgt_ok.setText('Задать пояс')

self.d_picker_wdg.layout().addWidget(d_pkr_wgt_ok)

QtCore.QObject.connect(d_pkr_wgt_ok, QtCore.SIGNAL("clicked()"), self, QtCore.SLOT('d_pick_ok()'))

self.alarmsnd_path = "widgets/clock/analogue_alarm.wav"

if system() == 'Windows':

self.alarmsnd = QtGui.QSound(self.alarmsnd_path)

self.hpen = QtGui.QPen(QtGui.QColor(30, 30, 30, 255), 6,

QtCore.Qt.SolidLine, QtCore.Qt.RoundCap,

QtCore.Qt.RoundJoin)

self.mpen = QtGui.QPen(QtGui.QColor(50, 50, 50, 255), 4,

QtCore.Qt.SolidLine, QtCore.Qt.RoundCap,

QtCore.Qt.RoundJoin)

self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.FramelessWindowHint)

self.setWindowTitle("Clock widget")

self.resize(240, 240)

self.bg = QtGui.QPixmap("widgets/clock/bg.png")

pal = self.palette()

pal.setBrush(QtGui.QPalette.Normal, QtGui.QPalette.Window, QtGui.QBrush(self.bg))

pal.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, QtGui.QBrush(self.bg))

self.setPalette(pal)

self.setMask(self.bg.mask())

self.cb = QtGui.QPushButton(self)

self.cb.resize(20, 20)

self.cb.setIcon(QtGui.QIcon(QtGui.QPixmap("widgets/clock/cb.png")))

self.cb.move(200, 00)

self.cfg_b = QtGui.QPushButton(self)

self.cfg_b.resize(20, 20)

self.cfg_b.setIcon(QtGui.QIcon(QtGui.QPixmap("widgets/clock/cfg_b.png")))

self.cfg_b.move(200, 20)

self.db = QtGui.QPushButton(self)

self.db.resize(20, 20)

self.db.setIcon(QtGui.QIcon(QtGui.QPixmap("widgets/clock/db.png")))

self.db.move(180, 00)

QtCore.QObject.connect(self.cfg_b, QtCore.SIGNAL("clicked()"), self, QtCore.SLOT('edit_alarms()'))

QtCore.QObject.connect(self.db, QtCore.SIGNAL("clicked()"), self, QtCore.SLOT('change_delta()'))

QtCore.QObject.connect(self.cb, QtCore.SIGNAL("clicked()"), self, QtCore.SLOT("exit()"))

self.show()

self.startTimer(10000)

def paintEvent(self, event):

"""Отрисовка стрелок"""

dnow = datetime.datetime.now()

time = dnow.utcnow() + self.delta

hhangle = -(time.hour + time.minute/60)*pi/6 + pi/2

mhangle = -(time.minute + time.second/60)*pi/30 + pi/2

painter = QtGui.QPainter()

painter.begin(self)

painter.setRenderHint(QtGui.QPainter.Antialiasing, True)

painter.setPen(self.hpen)

if time != dnow:

painter.drawText(93, 160, "UTC %s%d" % ("+" if self.delta.seconds >= 0 else '', self.delta.seconds//3600))

painter.drawLine(110 - cos(hhangle)*5, 110 + sin(hhangle)*5, 110 + cos(hhangle)*45, 110 - sin(hhangle)*45)

painter.setPen(self.mpen)

painter.drawLine(110 - cos(mhangle)*6, 110 + sin(mhangle)*6, 110 + cos(mhangle)*60, 110 - sin(mhangle)*60)

painter.end()

def timerEvent(self, *args, **kwargs):

self.update()

t = datetime.datetime.today()

alarms = []

for comm, dt in self.alarms.items():

if t >= dt:

alarms.append(comm)

for comm in alarms:

self.alarms.pop(comm)

if len(alarms):

text = 'Будильник%s <b>%s</b> сработал%s!' % ('и' if len(alarms) > 1 else '', ', '.join(alarms), 'и' if len(alarms) > 1 else '')

if self.show_notific:

wgt = self.wm.addWidget('Note', '\x03'.join([repr('Note'), repr(text)]))

wgt.activateWindow()

if self.show_tray_notific:

self.wm.trayMessage('Внимание!', text.replace('<b>', '\'').replace('</b>', '\''))

self.alarm()

self.del_alarm(comm)

@QtCore.pyqtSlot()

def edit_alarms(self):

"""Слот при нажатии на 'Управление будильниками'"""

self.refresh_al_list()

self.dt_edit.setDateTime(QtCore.QDateTime(datetime.datetime.now()))

self.dt_picker_wdg.show()

@QtCore.pyqtSlot()

def dt_pick_ok(self):

"""Слот при нажатии 'Добавить будильник' в выборе даты и коммента будильника"""

self.add_alarm(self.dt_edit.dateTime().toPyDateTime(), self.cmnt_edit.text())

self.refresh_al_list()

@QtCore.pyqtSlot()

def dt_pick_del(self):

"""Слот при нажатии 'Удалить будильник' в окне будильников"""

sel = self.al_list.selectedIndexes()

if len(sel) == 1:

al_comm = sel[0].data()

al_comm = al_comm[al_comm.index('(') + 1: -1]

self.del_alarm(al_comm)

self.refresh_al_list()

@QtCore.pyqtSlot()

def exit(self):

"""Слот при нажатии кнопки закрытия"""

self.wm.widget_list.remove(self)

self.hide()

self.destroy()

@QtCore.pyqtSlot()

def change_delta(self):

"""Слот при нажатии кнопки смены часового пояса"""

self.d_picker_wdg.show()

@QtCore.pyqtSlot()

def d_pick_ok(self):

"""Слот при нажатии кнопки ОК смены часового пояса"""

self.d_picker_wdg.hide()

self.delta = datetime.timedelta(hours=self.d_edit.itemData(self.d_edit.currentIndex()))

self.update()

def add_alarm(self, date_time=None, comment='безымянный'):

"""Добавление будильника"""

comment = comment.replace('\x03\x04', '') # удалим спец-символы, если кто-то надоумился их туда вставить

if comment not in self.alarms.keys():

self.alarms[comment] = date_time

else:

msg = QtGui.QMessageBox(self)

msg.setWindowTitle('Ошибка')

msg.setText('Будильник <b>%s</b> уже есть' % comment)

msg.show()

def del_alarm(self, comment='безымянный'):

"""Удаление будильника"""

if comment in self.alarms.keys():

self.alarms.pop(comment)

def refresh_al_list(self):

self.al_model.clear()

dtnow = datetime.datetime.now()

for al_comm, al_date in sorted(self.alarms.items(), key=lambda x: x[1]):

d_fromat = ['%Y', '%d %b.', '%X']

if al_date.year == dtnow.year:

d_fromat.pop(0)

if al_date.month == dtnow.month and al_date.day == dtnow.day:

d_fromat.pop(0)

item = QtGui.QStandardItem(' %s (%s)' % (al_date.strftime(', '.join(d_fromat)), al_comm))

self.al_model.appendRow(item)

def alarm(self):

"""Воспроизведение звука будильника.

Здеь содержится платформо-зависимый код т.к.

со звуковыми подсистемами qt решили не мудрить.

В Linux требуется sox."""

if system() == 'Windows':

self.alarmsnd.play()

elif system() == 'Linux':

QtCore.QProcess.startDetached("play " + self.alarmsnd_path) # требует sox на машине

# следующие 2 метода задают поведение: виджет будет перетаскиваться при перетаскивании мыши

def mousePressEvent(self, event):

"""Записывает координаты нажатия мыши"""

if event.button() == QtCore.Qt.LeftButton:

self.grabpoint = event.pos()

self.moveing = True

# up

def mouseMoveEvent(self, event):

"""Перетаскивает виджет за курсором мыши"""

if (event.buttons() & QtCore.Qt.LeftButton) and self.moveing:

newx = self.x() + event.pos().x() - self.grabpoint.x()

newy = self.y() + event.pos().y() - self.grabpoint.y()

if 0 <= newx <= (self.wm.max_size.width() - self.width()):

self.move(newx, self.y())

if 0 <= newy <= (self.wm.max_size.height() - self.height()):

self.move(self.x(), newy)

def serialize(self):

"""Сериализация. Ввиду того, что это PyQt,

стандартные методы не подходят

(c object'ты не сериализуются)"""

return '\x03'.join([self.name,

repr(self.show_notific),

repr(self.show_tray_notific),

repr(self.alarms),

repr(self.delta)])

widgets/calendar/__init__.py

"""

Виджет календаря

"""

from PyQt4 import QtCore, QtGui

import datetime

from calendar import monthrange

import pickle

class Event():

"""Класс события"""

def __init__(self, evtype, description, y=[], yd=[], m=[], md=[], nwd=[], wd=[], delta=[]):

if delta == []:

delta = [datetime.timedelta()]

if evtype not in ['birthday', # дни рождений (всегда ук. год)

'holiday', # праздники (год указан - ук, когда, не указан - пох)

'event']: # свое событие (полная проверка)

raise Exception('Неизвестный тип события')

self.type = evtype

self.description = description

self.y = y

self.yd = yd

self.m = m

self.md = md

self.nwd = nwd

self.wd = wd

self.delta = delta

def match(self, date):

"""Проверяет, попадает ли описание события под дату"""

if self.y and date.year not in self.y: # если год не совпал

return False

if self.yd and date.timetuple().tm_yday not in self.yd: # день года

return False

if self.m and date.month not in self.m: # месяц

return False

if self.md and date.day not in self.md: # день месяца

return False

if self.wd:

if date.timetuple().tm_wday not in self.wd:

return False

elif self.nwd:

wdn = (((date.timetuple().tm_mday - 1) // 7) + 1)

if wdn not in self.nwd: # n-тый день недели

if 10 in self.nwd: # если условие последний пн..вс в месяце

_, max_mday = monthrange(date.year, date.month)

if (date.timetuple().tm_mday + 7) > max_mday: # проверяем, последний ли это такой день недели в месяце

return True

return False

return True

def get_dates(self, start_date=datetime.date.today() + datetime.timedelta(days=-63), end_date=datetime.date.today() + datetime.timedelta(days=366)):

dates = []

if self.type == 'birthday':

for y in range(start_date.year, end_date.year + 1, 1):

dates.append((datetime.date(y, self.m[0], self.md[0]), '%s (%d годовщина)' % (self.description, y - self.y[0])))

else:

dinc = datetime.timedelta(days=1)

while start_date <= end_date:

if self.match(start_date):

for d in self.delta:

if self.description == 'holiday' and self.y:

date = (start_date + d, '%s (%d)' % (self.description, start_date.year - self.y[0]))

else:

date = (start_date + d, self.description)

dates.append(date)

start_date += dinc

return dates

# методы эквивалентности

def __eq__(self, other):

return isinstance(other, self.__class__) and self.__dict__ == other.__dict__

def __ne__(self, other):

return not self.__eq__(other)

def __str__(self):

res = self.description

if self.type == 'birthday':

res += ' (%d.%d.%d)' % (self.md[0], self.m[0], self.y[0])

else:

if self.y:

res += ', г: ' + ' '.join([str(x) for x in self.y])

if self.yd:

res += ', дг: ' + ' '.join([str(x) for x in self.yd])

if self.m:

res += ', м: ' + ' '.join([str(x) for x in self.m])

if self.md:

res += ', дм: ' + ' '.join([str(x) for x in self.md])

if self.wd:

wdn = {0: 'Пн', 1: 'Вт', 2: 'Ср', 3: 'Чт', 4: 'Пт', 5: 'Сб', 6: 'Вс'}

res += ', дн: ' + ' '.join([wdn[x] for x in self.wd])

if self.nwd:

res += ', №дн: ' + ' '.join([str(x) for x in self.nwd])

if self.delta and self.delta[0].days != 0:

res += ', см: ' + ' '.join([str(x.days) for x in self.delta])

return res

class Calendar(QtGui.QWidget):

"""Календарь"""

def __init__(self, parent, data=None):

"""Инициализация"""

QtGui.QWidget.__init__(self, parent)

self.name = 'Calendar'

self.wm = parent

# если пришли данные - восстановим

if data:

data = data.split('\x03')

self.show_notific = eval(data[1])

self.show_tray_notific = eval(data[2])

self.rep_events = pickle.loads(eval(data[3]))

self.single_events = pickle.loads(eval(data[4]))

else:

self.show_notific = True

self.show_tray_notific = True

self.rep_events = []

self.single_events = []

self.grabpoint = None

self.moveing = None

self.minion = False

self.note_wgt = None

self.sin_dates = []

self.rep_dates = []

self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.FramelessWindowHint)

self.setWindowTitle("Calendar widget")

self.resize(230, 185)

self.bg = QtGui.QPixmap("widgets/calendar/bg.png")

pal = self.palette()

pal.setBrush(QtGui.QPalette.Normal, QtGui.QPalette.Window, QtGui.QBrush(self.bg))

pal.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, QtGui.QBrush(self.bg))

self.setPalette(pal)

self.setMask(self.bg.mask())

self.calendar = QtGui.QCalendarWidget(self)

self.calendar.setVerticalHeaderFormat(QtGui.QCalendarWidget.NoVerticalHeader)

self.calendar.setFirstDayOfWeek(QtCore.Qt.Monday)

self.calendar.move(0, 30)

self.dformat = QtGui.QTextCharFormat()

self.dformat.setFontWeight(QtGui.QFont.Black)

self.dformat.setFontUnderline(True)

ftoday = QtGui.QTextCharFormat()

tbrush = QtGui.QBrush(QtGui.QColor(20, 250, 20, 100))

ftoday.setBackground(tbrush)

self.calendar.setDateTextFormat(datetime.date.today(), ftoday)

self.update_events()

# форма добавление даты

self.add_ev_wgt = QtGui.QWidget()

self.add_ev_wgt.setWindowTitle('Добавление события')

self.add_ev_wgt.setLayout(QtGui.QVBoxLayout())

layout = QtGui.QHBoxLayout()

layout.addWidget(QtGui.QLabel('Тип события'))

self.ev_type = QtGui.QComboBox()

self.ev_type.addItem('День рождения')

self.ev_type.addItem('Знаменательная дата')

self.ev_type.addItem('Событие')

layout.addWidget(self.ev_type)

self.add_ev_wgt.layout().addLayout(layout)

layout = QtGui.QHBoxLayout()

self.rep_ev = QtGui.QCheckBox()

self.rep_ev.setText('Повторять событие')

layout.addWidget(self.rep_ev)

hlp_btn = QtGui.QPushButton('Справка')

QtCore.QObject.connect(hlp_btn, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('show_help()'))

layout.addWidget(hlp_btn)

self.add_ev_wgt.layout().addLayout(layout)

layout = QtGui.QHBoxLayout()

layout.addWidget(QtGui.QLabel('Описание'))

layout.addSpacing(37)

self.ev_descr = QtGui.QLineEdit()

layout.addWidget(self.ev_descr)

self.add_ev_wgt.layout().addLayout(layout)

layout = QtGui.QHBoxLayout()

layout.addWidget(QtGui.QLabel('Год'))

layout.addSpacing(68)

self.ev_y = QtGui.QLineEdit()

layout.addWidget(self.ev_y)

self.add_ev_wgt.layout().addLayout(layout)

layout = QtGui.QHBoxLayout()

layout.addWidget(QtGui.QLabel('Месяц'))

layout.addSpacing(56)

self.ev_m = QtGui.QLineEdit()

layout.addWidget(self.ev_m)

self.add_ev_wgt.layout().addLayout(layout)

layout = QtGui.QHBoxLayout()

layout.addWidget(QtGui.QLabel('День месяца'))

layout.addSpacing(23)

self.ev_md = QtGui.QLineEdit()

layout.addWidget(self.ev_md)

self.add_ev_wgt.layout().addLayout(layout)

layout = QtGui.QHBoxLayout()

layout.addWidget(QtGui.QLabel('День в году'))

layout.addSpacing(25)

self.ev_yd = QtGui.QLineEdit()

layout.addWidget(self.ev_yd)

self.add_ev_wgt.layout().addLayout(layout)

layout = QtGui.QHBoxLayout()

layout.addWidget(QtGui.QLabel('День недели'))

self.ev_wd0 = QtGui.QCheckBox()

self.ev_wd0.setText('Пн')

layout.addWidget(self.ev_wd0)

self.ev_wd1 = QtGui.QCheckBox()

self.ev_wd1.setText('Вт')

layout.addWidget(self.ev_wd1)

self.ev_wd2 = QtGui.QCheckBox()

self.ev_wd2.setText('Ср')

layout.addWidget(self.ev_wd2)

self.ev_wd3 = QtGui.QCheckBox()

self.ev_wd3.setText('Чт')

layout.addWidget(self.ev_wd3)

self.ev_wd4 = QtGui.QCheckBox()

self.ev_wd4.setText('Пт')

layout.addWidget(self.ev_wd4)

self.ev_wd5 = QtGui.QCheckBox()

self.ev_wd5.setText('Сб')

layout.addWidget(self.ev_wd5)

self.ev_wd6 = QtGui.QCheckBox()

self.ev_wd6.setText('Вс')

layout.addWidget(self.ev_wd6)

self.add_ev_wgt.layout().addLayout(layout)

layout = QtGui.QHBoxLayout()

layout.addWidget(QtGui.QLabel('n-ый день недели'))

self.ev_nwd1 = QtGui.QCheckBox()

self.ev_nwd1.setText('1')

layout.addWidget(self.ev_nwd1)

self.ev_nwd2 = QtGui.QCheckBox()

self.ev_nwd2.setText('2')

layout.addWidget(self.ev_nwd2)

self.ev_nwd3 = QtGui.QCheckBox()

self.ev_nwd3.setText('3')

layout.addWidget(self.ev_nwd3)

self.ev_nwd4 = QtGui.QCheckBox()

self.ev_nwd4.setText('4')

layout.addWidget(self.ev_nwd4)

self.ev_nwd5 = QtGui.QCheckBox()

self.ev_nwd5.setText('5')

layout.addWidget(self.ev_nwd5)

self.ev_nwd10 = QtGui.QCheckBox()

self.ev_nwd10.setText('Последний')

layout.addWidget(self.ev_nwd10)

self.add_ev_wgt.layout().addLayout(layout)

layout = QtGui.QHBoxLayout()

layout.addWidget(QtGui.QLabel('Смещение, дней'))

layout.addSpacing(4)

self.ev_delta = QtGui.QLineEdit()

layout.addWidget(self.ev_delta)

self.add_ev_wgt.layout().addLayout(layout)

add_ev_btn = QtGui.QPushButton('Добавить событие')

QtCore.QObject.connect(add_ev_btn, QtCore.SIGNAL('clicked()'), self, QtCore.SLOT('add_event()'))

self.add_ev_wgt.layout().addWidget(add_ev_btn)

# Список событий и их удаление

self.del_event_wgt = QtGui.QWidget()

self.del_event_wgt.setWindowTitle('Список событий')

self.del_event_wgt.setLayout(QtGui.QVBoxLayout())

layout = QtGui.QHBoxLayout()

layout.addWidget(QtGui.QLabel('Повторяемость события'))

self.del_ev_type = QtGui.QComboBox()

self.del_ev_type.addItem('Повторяемое')

self.del_ev_type.addItem('Разовое')

QtCore.QObject.connect(self.del_ev_type, QtCore.SIGNAL("currentIndexChanged(int)"), self.refresh_ev_list)

layout.addWidget(self.del_ev_type)

self.del_event_wgt.layout().addLayout(layout)

self.ev_list = QtGui.QListView()

self.ev_model = QtGui.QStandardItemModel(self.ev_list)

self.ev_list.setModel(self.ev_model)

self.del_event_wgt.layout().addWidget(self.ev_list)

del_ev_btn = QtGui.QPushButton()

del_ev_btn.setText('Удалить событие')

self.del_event_wgt.layout().addWidget(del_ev_btn)

QtCore.QObject.connect(del_ev_btn, QtCore.SIGNAL("clicked()"), self, QtCore.SLOT('del_event()'))

#-------------

self.cb = QtGui.QPushButton(self)

self.cb.resize(20, 20)

self.cb.setIcon(QtGui.QIcon(QtGui.QPixmap("widgets/calendar/cb.png")))

self.cb.move(205, 5)

self.cfg_b = QtGui.QPushButton(self)

self.cfg_b.resize(20, 20)

self.cfg_b.setIcon(QtGui.QIcon(QtGui.QPixmap("widgets/calendar/cfg_b.png")))

self.cfg_b.move(185, 5)

self.aeb = QtGui.QPushButton(self)

self.aeb.resize(20, 20)

self.aeb.setIcon(QtGui.QIcon(QtGui.QPixmap("widgets/calendar/aeb.png")))

self.aeb.move(165, 5)

QtCore.QObject.connect(self.aeb, QtCore.SIGNAL("clicked()"), self, QtCore.SLOT('create_event()'))

QtCore.QObject.connect(self.cfg_b, QtCore.SIGNAL("clicked()"), self, QtCore.SLOT('configure()'))

QtCore.QObject.connect(self.cb, QtCore.SIGNAL("clicked()"), self, QtCore.SLOT("exit()"))

QtCore.QObject.connect(self.calendar, QtCore.SIGNAL("clicked(QDate)"), self.date_clicked)

QtCore.QObject.connect(self.calendar, QtCore.SIGNAL("activated(QDate)"), self.on_activate)

self.show()

self.timer_id = self.startTimer(1000)

def timerEvent(self, *args, **kwargs):

self.killTimer(self.timer_id)

self.date_clicked(QtCore.QDate(datetime.date.today()), init_msg=True)

def on_activate(self, date):

date = date.toPyDate()

self.ev_y.setText(str(date.year))

self.ev_m.setText(str(date.month))

self.ev_md.setText(str(date.day))

self.ev_type.setCurrentIndex(2)

self.add_ev_wgt.show()

self.note_wgt.hide()

def update_events(self):

self.calendar.setDateTextFormat(QtCore.QDate(), self.dformat)

ftoday = QtGui.QTextCharFormat()

tbrush = QtGui.QBrush(QtGui.QColor(20, 250, 20, 100))

ftoday.setBackground(tbrush)

self.calendar.setDateTextFormat(datetime.date.today(), ftoday)

self.rep_dates.clear()

for ev in self.rep_events:

self.rep_dates.extend(ev.get_dates())

self.sin_dates.clear()

for ev in self.single_events:

self.sin_dates.extend(ev.get_dates())

for dt in self.rep_dates:

self.calendar.setDateTextFormat(dt[0], self.dformat)

for dt in self.sin_dates:

self.calendar.setDateTextFormat(dt[0], self.dformat)

@QtCore.pyqtSlot()

def create_event(self):

"""Слот при нажатии на 'Добавить событие'"""

self.add_ev_wgt.show()

@QtCore.pyqtSlot()

def add_event(self):

"""Слот при нажатии на 'Добавить событие'"""

all_ok = True

err_txt = ''

try:

y = [int(i) for i in filter(len, self.ev_y.text().replace(' ', ',').split(','))]

m = [int(i) for i in filter(len, self.ev_m.text().replace(' ', ',').split(','))]

md = [int(i) for i in filter(len, self.ev_md.text().replace(' ', ',').split(','))]

descr = self.ev_descr.text()

if len(descr) < 3:

all_ok = False

err_txt = 'Описание должно быть больше 4 символов\n'

if self.ev_type.currentText() == 'День рождения':

if len(y) == 1 and len(m) == 1 and len(md) == 1:

ev = Event(evtype='birthday', y=y, m=m, md=md, description=descr)

else:

err_txt = 'Для дня рождения только год, месяц и день\n'

all_ok = False

else:

yd = [int(i) for i in filter(len, self.ev_yd.text().replace(' ', ',').split(','))]

wd = []

if self.ev_wd0.isChecked():

wd.append(0)

if self.ev_wd1.isChecked():

wd.append(1)

if self.ev_wd2.isChecked():

wd.append(2)

if self.ev_wd3.isChecked():

wd.append(3)

if self.ev_wd4.isChecked():

wd.append(4)

if self.ev_wd5.isChecked():

wd.append(5)

if self.ev_wd6.isChecked():

wd.append(6)

nwd = []

if self.ev_nwd1.isChecked():

nwd.append(1)

if self.ev_nwd2.isChecked():

nwd.append(2)

if self.ev_nwd3.isChecked():

nwd.append(3)

if self.ev_nwd4.isChecked():

nwd.append(4)

if self.ev_nwd5.isChecked():

nwd.append(5)

if self.ev_nwd10.isChecked():

nwd.append(10)

delta = [datetime.timedelta(days=int(i)) for i in filter(len, self.ev_delta.text().replace(' ', ',').split(','))]

evtype = 'event' if self.ev_type.currentText() == 'Событие' else 'holiday'

if len(y) < 1 and len(yd) < 1 and len(m) < 1 and len(md) < 1 and len(nwd) < 1 and len(wd) < 1 and len(delta) < 1:

all_ok = False

err_txt = 'Поля пусты!\n'

if evtype == 'event' and not self.rep_ev.isChecked():

if len(y) != 1 or len(yd) > 1 or len(m) > 1 or len(md) > 1 or len(nwd) > 1 or len(wd) > 1 or len(delta) > 1:

all_ok = False

err_txt = 'Для неповторяемых событий нельзя\nзадавать больше одного значения в критерии\nГод должен быть задан\n(например, несколько чисел месяца)\n'

ev = Event(evtype=evtype, description=descr, y=y, yd=yd, m=m, md=md, nwd=nwd, wd=wd, delta=delta)

except:

all_ok = False

if all_ok:

if not self.rep_ev.isChecked() and ev.type == 'event':

self.single_events.append(ev)

else:

self.rep_events.append(ev)

self.add_ev_wgt.hide()

self.update_events()

self.wm.trayMessage('Событие добавлено', 'Событие "%s" добавлено!' % self.ev_descr.text())

else:

msg = QtGui.QMessageBox(self.add_ev_wgt)

msg.setWindowTitle('Ошибка')

msg.setText('Поля заполнены неверно\n%sОбратитесь к справке' % err_txt)

msg.show()

@QtCore.pyqtSlot()

def del_event(self):

"""Слот при нажатии на 'Удалить событие'"""

sel = self.ev_list.selectedIndexes()

if len(sel) == 1:

ev_str = sel[0].data()

for i in range(len(self.ev_l_storage)):

if str(self.ev_l_storage[i]) == ev_str:

self.ev_l_storage.pop(i)

break

self.refresh_ev_list()

self.update_events()

@QtCore.pyqtSlot()

def show_help(self):

"""Слот при нажатии 'Справка'"""

msg = QtGui.QMessageBox(self.add_ev_wgt)

msg.setWindowTitle('Справка по добавлению события')

help_text = """

Сперва выберите тип события:

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

Знаменательная дата - памятная дата. Какой-либо праздник, день траура или просто приметное событие. В заметке календаря, помимо описания и даты будет указано количество лет, прошедшее с года наступления, если указан год.

Событие - просто событие, или напоминание. В заметке календаря будет указана только дата и описание. События могут быть разовыми, а могут и повторяться (галочка "повторять событие"). Если событие одноразовое, для него нельзя указать несколько критериев одного типа, например, указать дни месяца "14, 17" или отметить галочкой "Пн" и "Вт"

Затем, необходимо заполнить критерии, по которым эти события наступят. В полях критериев пишутся целые цисла. Можно указывать несколько значений через зяпятую или через пробел (например, указав "1, 12" месяц и поставив галочку на "Пн", событие будет наступать каждый понедельник Января и Декабря каждого года).

Если критерий не указан, то этот критерий считается неважным (например, если год не указан, то событие будет проверяться каждый год; если указан только 1 день месяца, то событие будет наступать каждый этот день месяца каждого года).

Событие наступит только если все указанные критерии совпали.

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

Для Знаменательной даты можно указывать любые критерии.

Для События можно указывать любые критерии. Только это событие может быть разовым.

Особый критерий - Смещение. Может быть положительным или отрицательным. Он указывает сдвиг в днях относительно критериев. Он помогает при праздновании переходящих праздников.

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

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

"""

msg.setText(help_text)

msg.show()

@QtCore.pyqtSlot()

def exit(self):

"""Слот при нажатии кнопки закрытия"""

self.wm.widget_list.remove(self)

self.hide()

self.destroy()

def near_events_text(self):

dnow = datetime.date.today()

edate = dnow + datetime.timedelta(days=30)

today = []

near_events = []

for d in self.rep_dates:

if d[0] == dnow:

today.append(d)

if dnow < d[0] <= edate:

near_events.append(d)

for d in self.sin_dates:

if d[0] == dnow:

today.append(d)

if dnow < d[0] <= edate:

near_events.append(d)

res = 'В ближайшие 30 дней событий нет'

if today:

res = 'Сегодня\n' + '\n'.join([d[1] for d in today])

if near_events:

res = 'Через:\n'

for d in near_events:

res += '%d дней %s\n' % ((d[0] - dnow).days, d[1])

return res

def date_clicked(self, date, init_msg=False):

"""Кликнули на дате"""

dnow = date.toPyDate()

text = 'Заметка календаря\n%s\n' % str(dnow)

dates = []

if init_msg:

text = 'Заметка календаря\n%s\n' % self.near_events_text()

else:

for d in self.rep_dates:

if d[0] == dnow:

dates.append(d)

for d in self.sin_dates:

if d[0] == dnow:

dates.append(d)

if dates or init_msg:

text += '\n'.join([d[1] for d in dates])

if self.note_wgt is None or self.note_wgt.isHidden():

self.note_wgt = self.wm.addWidget('Note')

self.note_wgt.minion = True

self.note_wgt.tf.setReadOnly(True)

self.note_wgt.move(self.x() + (self.width() - self.note_wgt.width())//2, self.y() + self.height())

self.note_wgt.set_text(text)

else:

if self.note_wgt:

self.note_wgt.hide()

@QtCore.pyqtSlot()

def configure(self):

"""Кнопка конфигурации"""

self.refresh_ev_list()

self.del_event_wgt.show()

def refresh_ev_list(self):

self.ev_model.clear()

self.ev_l_storage = self.single_events if self.del_ev_type.currentText() == 'Разовое' else self.rep_events

for ev in self.ev_l_storage:

item = QtGui.QStandardItem(str(ev))

self.ev_model.appendRow(item)

# следующие 2 метода задают поведение: виджет будет перетаскиваться при перетаскивании мыши

def mousePressEvent(self, event):

"""Записывает координаты нажатия мыши"""

if event.button() == QtCore.Qt.LeftButton:

self.grabpoint = event.pos()

self.moveing = True

# up

def mouseMoveEvent(self, event):

"""Перетаскивает виджет за курсором мыши"""

if (event.buttons() & QtCore.Qt.LeftButton) and self.moveing:

newx = self.x() + event.pos().x() - self.grabpoint.x()

newy = self.y() + event.pos().y() - self.grabpoint.y()

if 0 <= newx <= (self.wm.max_size.width() - self.width()):

self.move(newx, self.y())

if 0 <= newy <= (self.wm.max_size.height() - self.height()):

self.move(self.x(), newy)

def serialize(self):

"""Сериализация. Ввиду того, что это PyQt,

стандартные методы не подходят

(c object'ты не сериализуются)"""

return '\x03'.join([self.name,

repr(self.show_notific),

repr(self.show_tray_notific),

repr(pickle.dumps(self.rep_events)),

repr(pickle.dumps(self.single_events))])

widgets/note/__init__.py

"""

Виджет заметок

"""

from PyQt4 import QtCore, QtGui

class Note(QtGui.QWidget):

"""Заметка"""

def __init__(self, parent, data=None):

"""Инициализация"""

QtGui.QWidget.__init__(self, parent)

self.name = 'Note'

self.wm = parent

# если пришли данные - восстановим

if data:

data = data.split('\x03')

self.tf = QtGui.QTextEdit(eval(data[1]), self)

else:

self.tf = QtGui.QTextEdit(self)

self.grabpoint = None

self.moveing = None

self.minion = False # если True, то cwm не будет его сериализовывать

self.setWindowFlags(QtCore.Qt.Window | QtCore.Qt.FramelessWindowHint)

self.setWindowTitle("Note widget")

self.resize(220, 220)

self.bg = QtGui.QPixmap("widgets/note/bg.png")

pal = self.palette()

pal.setBrush(QtGui.QPalette.Normal, QtGui.QPalette.Window, QtGui.QBrush(self.bg))

pal.setBrush(QtGui.QPalette.Inactive, QtGui.QPalette.Window, QtGui.QBrush(self.bg))

self.setPalette(pal)

self.setMask(self.bg.mask())

self.cb = QtGui.QPushButton(self)

self.cb.resize(20, 20)

self.cb.setIcon(QtGui.QIcon(QtGui.QPixmap("widgets/note/cb.png")))

self.cb.move(195, 5)

self.clrb = QtGui.QPushButton(self)

self.clrb.resize(20, 20)

self.clrb.setIcon(QtGui.QIcon(QtGui.QPixmap("widgets/note/clrb.png")))

self.clrb.move(175, 5)

self.tf.move(-1, 30)

self.tf.resize(222, 191)

p = self.tf.palette()

p.setColor(QtGui.QPalette.Base, QtGui.QColor(0xf7, 0xf5, 0xb5, 255))

self.tf.setPalette(p)

QtCore.QObject.connect(self.clrb, QtCore.SIGNAL("clicked()"), self, QtCore.SLOT('clear_note()'))

QtCore.QObject.connect(self.cb, QtCore.SIGNAL("clicked()"), self, QtCore.SLOT("exit()"))

self.show()

@QtCore.pyqtSlot()

def clear_note(self):

"""Слот при нажатии стерки. Удаляет текст заметки"""

self.tf.setText('')

@QtCore.pyqtSlot()

def exit(self):

"""Слот при нажатии кнопки закрытия"""

self.wm.widget_list.remove(self)

self.hide()

self.destroy()

def set_text(self, text):

"""Метод для задания текста путем внешнего вызова"""

self.tf.setText(text)

self.update()

# следующие 2 метода задают поведение: виджет будет перетаскиваться при перетаскивании мыши

def mousePressEvent(self, event):

"""Записывает координаты нажатия мыши"""

if event.button() == QtCore.Qt.LeftButton:

self.grabpoint = event.pos()

self.moveing = True

# up

def mouseMoveEvent(self, event):

"""Перетаскивает виджет за курсором мыши"""

if (event.buttons() & QtCore.Qt.LeftButton) and self.moveing:

newx = self.x() + event.pos().x() - self.grabpoint.x()

newy = self.y() + event.pos().y() - self.grabpoint.y()

if 0 <= newx <= (self.wm.max_size.width() - self.width()):

self.move(newx, self.y())

if 0 <= newy <= (self.wm.max_size.height() - self.height()):

self.move(self.x(), newy)

def serialize(self):

"""Сериализация. Ввиду того, что это PyQt,

стандартные методы не подходят

(c object'ты не сериализуются)"""

return '\x03'.join([self.name,

repr(self.tf.toPlainText())])

server.py

import socket

import os

def mainloop():

sock = socket.socket()

sock.bind(('', 33333))

sock.listen(1)

print('started')

while True:

ans = ''

conn, addr = sock.accept()

print('connected to ' + str(addr))

data = conn.recv(1024)

if data:

print('recieved from ' + str(addr) + ' : ' + str(data))

code, login, password, data = data.split(' ', 3)

if code == 'L':

try:

with open('userdata/%s/pass' % str(login)) as lg:

if password.strip() != lg.read().strip():

ans = 'W'

else:

ans = 'Y ' + open('userdata/%s/data' % str(login)).read()

except IOError:

ans = 'E'

elif code == 'S':

try:

with open('userdata/%s/pass' % str(login)) as lg:

if password.strip() != lg.read().strip():

ans = 'W'

else:

open('userdata/%s/data' % str(login), 'w').write(data)

except IOError:

# такого юзера еще нет, создаем

if not os.path.exists('userdata/%s' % str(login)):

os.mkdir('userdata/%s' % str(login))

open('userdata/%s/pass' % str(login), 'w').write(password)

open('userdata/%s/data' % str(login), 'w').write(data)

ans = 'Y'

else:

ans = 'N'

else:

ans = 'N'

conn.send(ans)

conn.close()

print('disconnected from ' + str(addr))

if __name__ == '__main__':

try:

if not os.path.exists('userdata'):

os.mkdir('userdata')

mainloop()

except Exception as e:

print(e)

PAGE 1

РАЗРАБОТКА КРОССПЛАТФОРМЕННОГО МЕНЕДЖЕРА ВИДЖЕТОВ РАБОЧЕГО СТОЛА