Разработка на Win32 платформе Основную разработку будем вести на следующем ПО:
ОС: Windows XP
IDE: Eclipse Ganymede + Qt Eclipse Integration 1.5.0 for Win32
Qt: Open source 4.5.0 for Win32
Compiler: MinGW32 3.81
Будем вести разработку программы с применением водопадной модели жизненного цикла ПО.
Сценарии использования. Функциональные требования. Предполагается, что разрабатываемая программа будет иметь массовый круг пользователей в широкой возрастной категории. Необходимо учесть специфику работы программы на будущих устройствах (разрешение экрана, не сенсорный ввод).
Основная цель:
отображение на экране конечного устройства звёзд и созвездий в зависимости от положения наблюдателя
Сопутствующие цели:
поиск звёзд и созвездий,
по двойному клику вывод информации об объекте
перемещение между звёздами по нажатию визуальной кнопки (актуально в устройствах, обладающих лишь клавиатурным вводом)
перемещение по карте мышью или иными средствами сенсорного ввода
скрытие боковых панелей для увеличения отображаемого пространства
полноэкранный/оконный вид программы
возможность увеличивать и уменьшать выбранный вид
возможность поворота выбранного вида
центрирование вида
изменение местоположения путём ввода текущих координат в диалоговое окно программы или правкой содержимого ini-файла
отображение компаса и линейки текущего местоположения (Ascension:Declination)
поддержка сглаживания графики (Antialiasing)
поддержка обработки графики библиотекой OpenGL
1.2 Проектирование. Архитектура. Пользовательский интерфейс. Основную модель программы отобразим на схеме 1.1
Схема 1.1
В виде голубых прямоугольников изображены основные классы. Многоуровневая структура программы позволяет изменять тип окна, в котором будет расположен вид звёзд. Это даёт возможность интегрировать программу в любой вид интерфейсов (см. 2. Портирование).
В основном окне происходит считывание звёзд из файла, их добавление в сцену, размещение сцены в виджете сцены и его установка в окне.
Весь пользовательский интерфейс размещён в виджете сцены. В нём создаются слои (layouts) элементов управления (кнопки, слайдеры и т.д.). Так же в нем устанавливается перехватчик событий (event filter) для возможности управления мышью и клавиатурой настройками данного вида. К нему подключается диалог выбора текущего положения и класс линейки.
Класс звезды на вход получает строку из файла с её характеристиками, а так же сдвиг, на который необходимо перенести звезду для получения текущего вида. Класс звезды хранит свои плоские координаты, экваториальные координаты, идентификационный номер, объём, своё название и созвездие, к которому она относится.
Класс созвездия хранит плоские координаты, цвет и название.
Для обоих классов предусмотрено событие двойного клика мышью, в котором создаётся информационное сообщение о свойствах данного созвездия/звезды.
Так как считывание и расположение объектов на карте занимает достаточно продолжительное время, при входе в программу в методе main создаётся класс Splash Screen, обозначающий загрузку программы. При изменении вида для той же цели в Main Window организован класс Progress Bar.
Для хранения звёзд, созвездий и текущего положения предусмотрены соответствующие файлы mycat.txt (чтение), sozvezd.txt (чтение) и coord.ini (чтение/запись). Их обработка идёт в Main Window.
Присоединение интерактивных объектов, а так же классов осуществляется с помощью сигналов/слотов.
Общую схему пользовательского интерфейса см. на схеме 1.2
Схема 1.2 1.3. Реализация
Реализация программы осуществлялась на языке C++. При установке win32 версии под Eclipse Integration переконфигурирование среды не обязательно. Использовались библиотеки Qt: QtCore4, QtGui4, QtOpenGL4; а так же native windows библиотека mingwm10.
Соответственно файл проекта выглядит следующим образом:
TEMPLATE = app
TARGET = Constellations_win32
QT += core \
gui \
opengl
HEADERS += ruler.h \
InputDialog.h \
Stars.h \
constellation.h \
mainwindow.h \
view.h \
star.h \
SOURCES += ruler.cpp \
InputDialog.cpp \
constellation.cpp \
star.cpp \
main.cpp \
mainwindow.cpp \
view.cpp
RESOURCES += images.qrc Файл ресурсов images.qrc содержит иконки кнопок и Splash Screen.
Опишем основные моменты в реализации классов. 1.3.1. main
В методе main находится точка входа в программу, а так же организовано создание Splash Sreen.
QSplashScreen splash(QPixmap(":/splsh.png"));
splash.show();
app.processEvents();
MainWindow window;
window.resize(240,270);
window.show();
splash.finish(&window); Splash отображается до тех пор, пока объект window не передаст значение об успешной загрузке в память. 1.3.2. MainWindow
Рассмотрим заголовочный файл
class MainWindow : public QWidget
{
Q_OBJECT
public:
MainWindow(QWidget *parent = 0); public slots:
void setValue(int nValue1, int nValue2); private:
void populateScene(int dH, int dG);
QGraphicsScene *scene;
QProgressBar *progressBar;
};
Класс содержит метаинформацию (Q_OBJECT) для moc компилятора Qt, который позволяет ввести в синтаксис Си программы специфические команды Qt. Именно moc реализует метод signals/slots для широкого взаимодействия как внутри программы, так и создания взаимодействий с пользователем.
Данный класс занимается наполнением сцены. Рассмотрим реализацию конструктора:
MainWindow::MainWindow(QWidget *parent)
: QWidget(parent)
{
scene = new QGraphicsScene;
progressBar = new QProgressBar;
QString dH, dG;
QFile file("coord.ini");
if (file.open(QIODevice::ReadOnly)) {
QTextStream stream(&file);
if (stream.status() != QTextStream::Ok) {
qDebug() << "Ошибка чтения файла";
file.close ();
}
while(!stream.atEnd()){
stream.readLine();
dH = stream.readLine();
dG = stream.readLine();
} file.close();
}
int intDh = dH.mid(dH.indexOf("=")+1,dH.indexOf(";")-dH.indexOf("=")-1).toInt();
int intDg = dG.mid(dG.indexOf("=")+1,dG.indexOf(";")-dG.indexOf("=")-1).toInt();
populateScene(intDh,intDg);
progressBar->setRange(0,5807);
View *view = new View("*");
view->view()->setScene(scene);
scene->installEventFilter(view);
scene->setItemIndexMethod(QGraphicsScene::NoIndex);
QVBoxLayout *layout = new QVBoxLayout;
layout->setMargin(0);
layout->addWidget(view);
layout->addWidget(progressBar);
setLayout(layout);
setWindowTitle(tr("Constellations"));
connect(view, SIGNAL(valueChanged(int, int)), this, SLOT(setValue(int, int)));
} Конструктор создаёт класс сцены, прогресс бара и виджета сцены (View). А так же считывает из ini-файла данные о текущей позиции пользователя. Затем заполняет сцену (populateScene) с параметрами текущей позиции. Для перехвата событий, при нахождении фокуса на сцене, устанавливаем фильтр виджета сцены (View), что позволит однотипно обрабатывать события мыши и клавиатуры при навигации по панелям управления, так и по окну звёзд/созвездий.
Для основного окна устанавливаем один слой (layout), на котором размещаем виджет и ниже Progress Bar.
Так как в программе, в классе View имеется возможность изменять текущее положение через диалоговое окно, то для перерасчёта координат создаётся слот setValue, который получает от View переменные позиции пользователя.
Рассмотрим реализацию заполнения сцены:
Для перевода вводимого пользователем часового пояса в представление «сдвига» звёзд используем следующую формулу:
if (dH>12) dH = 36 - dH;
else dH = 12 - dH; Это необходимо, так как по умолчанию текущая позиция устанавливается на 12 поясе. Таким образом, получаем необходимый «сдвиг», который реализовывается в классе star.
Чтобы показывать progress bar только после загрузки окна(флаг isVisible), создадим условие
if (this->isVisible())
progressBar->show(); Создадим «камеру», за которой будет перемещаться наш вид, чтобы осуществить плавную анимацию перемещения.
QGraphicsItem *camera = scene->addEllipse(0,0,1,1); Её реализация находится в классе View.
Строка
GraphicsLineItem *GMTline = scene->addLine(0,0,0,0,QPen(QColor(Qt::NoBrush),0));
создаёт горизонтальную линейку. Далее наносим цифры на неё. Цифры должны иметь свойство не менять размер при увеличении/уменьшении, поэтому создаём отдельный класс Ruler. Для правильного отображения деления линейки в соответствии с положением заводим переменную h, которая вычисляется из переменной «сдвига». Объединяем цифры и засечки с линейкой методом setParent, что позволит перемещать линейку как единое целое:
int h = 24 - dH;
if (h == 24) h = 0;
for (int i = 0; i<24; i++){
QFont font("Times", 40);
font.setStyleStrategy(QFont::ForceOutline);
QVariant hStr = h;
QGraphicsTextItem *hour = new Ruler(hStr.toString());
hour->setData(0,"ruler");
hour->setParentItem(GMTline);
hour->setPos(i*942.47,-8);
QGraphicsLineItem *hLine = scene->addLine(0,0,0,30,QPen(QColor(Qt::magenta),0));
hLine->setParentItem(hour);
hLine->setData(0,"ruler");
h++;
if (h == 24) h = 0;
} Аналогично, создаётся вертикальная линейка, для отображения наклона.
Далее считываем файл, содержащий координаты звёзд:
QFile file("mycat.txt");
if (file.open(QIODevice::ReadOnly)) {
QTextStream stream(&file);
if (stream.status() != QTextStream::Ok) {
qDebug() << "Ошибка чтения файла";
file.close ();
}
while(!stream.atEnd()){
starData = stream.readLine();
QGraphicsItem *item = new Star(starData, dH, dG);
scene->addItem(item);
progressBar->setValue(progressBar->value()+1);
} file.close();
} Здесь создаётся объект класса звезда (star) с данными о позиции, объёме, названии, а также «сдвигом» по вертикальной и горизонтальной осям, и помещается на сцену.
Когда все звёзды размещены, можно получить конечный размер сцены:
QRectF sceneRect = scene->sceneRect(); Далее объединяем звёзды в созвездия. Так как наш вид отображает звёзды в плоской проекции, поэтому встаёт проблема замыкания нашей области на границах. Для этого мы создаём ещё 8 виртуальных областей-копий вокруг созданной. Наш мозг рисует воображаемые линии, соединяющие звёзды в созвездия, по кратчайшей траектории, следовательно, будем находить линии минимального размера, и, если 1 линия находится в разных областях, отрезать её на стыке. Это проделаем для всех областей. Данный алгоритм в итоге изобразит начало созвездия с одной стороны нашей области, а его конец с другой. Схема алгоритма представлена на рис. 1.1
рис. 1.1 Создаём 9 линий-претендентов и 1 временную для нахождения минимума.
QLineF Line[10];
Line[0] = QLineF(posStar1,posStar2);
Line[1] = QLineF(posStar1,posStar2+QPointF(sceneRect.width(),0));
Line[2] = QLineF(posStar1,posStar2+QPointF(sceneRect.width(),-scene->height()));
Line[3] = QLineF(posStar1,posStar2+QPointF(0,-sceneRect.height()));
Line[4] = QLineF(posStar1,posStar2+QPointF(-sceneRect.width(),-sceneRect.height()));
Line[5] = QLineF(posStar1,posStar2+QPointF(-sceneRect.width(),0));
Line[6] = QLineF(posStar1,posStar2+QPointF(-sceneRect.width(),sceneRect.height()));
Line[7] = QLineF(posStar1,posStar2+QPointF(0,sceneRect.height()));
Line[8] = QLineF(posStar1,posStar2+QPointF(sceneRect.width(),sceneRect.height()));
Line[9] = QLineF(posStar1,posStar2);
for(int i = 1; i < 9; ++i)
{
if (Line[i].length() < Line[9].length())
Line[9] = Line[i];
} Если линия вышла за пределы области, то создаём вторую линию, являющуюся продолжением данной:
if (Line[9].length() != Line[0].length()){
QLineF Line[10];
Line[0] = QLineF(posStar1,posStar2);
Line[1] = QLineF(posStar1+QPointF(sceneRect.width(),0),posStar2);
Line[2] = QLineF(posStar1+QPointF(sceneRect.width(),-sceneRect.height()),posStar2);
Line[3] = QLineF(posStar1+QPointF(0,-sceneRect.height()),posStar2);
Line[4] = QLineF(posStar1+QPointF(-sceneRect.width(),-sceneRect.height()),posStar2);
Line[5] = QLineF(posStar1+QPointF(-sceneRect.width(),0),posStar2);
Line[6] = QLineF(posStar1+QPointF(-sceneRect.width(),sceneRect.height()),posStar2);
Line[7] = QLineF(posStar1+QPointF(0,sceneRect.height()),posStar2);
Line[8] = QLineF(posStar1+QPointF(sceneRect.width(),sceneRect.height()),posStar2);
Line[9] = QLineF(posStar1,posStar2);
for(int i = 1; i < 9; ++i)
{
if (Line[i].length() < Line[9].length())
Line[9] = Line[i];
} В это же время создаём названия созвездий (constellation). Вносим соответствующие прямые в данный класс методом setParent. Специфика setParent переводит глобальные координаты в локальные координаты относительно координат предка. Поэтому, чтобы не испортить расположения прямых, создадим объекты-прямоугольники, обрамляющие наши прямые. Это необходимо для поиска созвездий, чтобы была возможность центрировать на выбранном созвездии, по возможности умещая его, ровно в окно вида. Реализация поиска представлена в классе View.
QGraphicsLineItem *line = scene->addLine(Line[9],QPen(QColor(Qt::white),7));
QGraphicsRectItem *boundRect = scene->addRect(Line[9].x1(),Line[9].y1(),-Line[9].x1()+Line[9].x2(),-Line[9].y1()+Line[9].y2(),Qt::NoPen,Qt::NoBrush);
|