Пишем часы со стрелками

Цель этой статьи - показать практическое использование некоторых тригонометрических функций в программировании.

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

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



На рисунке 1 показана окружность радиуса r = 1 с центром в точке O. Произвольная точка A заданная на окружности образует дугу AB и угол α, численно (в радианах) равных друг другу.
Проекция точки A на ось абсцисс (x) называется косинусом угла α или дуги AB.
Проекция точки A на ось ординат (y) называется синусом угла α или дуги AB.
Если рассматривать полученные треугольники AxOA или AyOA, то можно говорить о следующих отношениях.
Косинусом острого угла называется отношение прилежащего катета к гипотенузе.
Синусом острого угла называется отношение противолежащего катета к гипотенузе.
Углы в системе координат откладываются против часовой стрелки.
Так как длина всей окружности равна 2πr, а радиус нашей окружности равен единице, то длина окружности в нашем случае равна . То есть, угол в 180 градусов соответствует π радиан. Из этого мы можем вывести формулу: 1 радиан = 180 / π.
Более подробную информацию Вы можете получить, например, здесь.


Результатом нашего проекта должны стать часы, показанные на рисунке 2.




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

Файл Clock.h
#ifndef CLOCK_H
#define CLOCK_H

#include <QtGui/QWidget>
#include <QtCore/QPoint>

class CClock : public QWidget
{
Q_OBJECT
public:
explicit CClock(QWidget *parent = 0);

public slots:
void onTimer();

protected:
virtual void paintEvent(QPaintEvent * event);

private:
QPoint rotatePoint(const QPoint & point, int degree, double radius);
};

#endif // CLOCK_H

Файл Clock.cpp
#include "Clock.h"
#include <QtGui/QPainter>
#include <QtCore/QTime>
#include <QtCore/QTimer>
#include <cmath>
//===================================================================

CClock::CClock(QWidget *parent) :
QWidget(parent)
{
setMinimumSize(500, 500);
QTimer * timer = new QTimer(this);
connect(timer, SIGNAL(timeout()), this, SLOT(onTimer()));
timer->start(1000);
}
//===================================================================

void CClock::onTimer()
{
QWidget::update();
}
//===================================================================

void CClock::paintEvent(QPaintEvent * /*event*/)
{
//
// константы для отрисовки.
//

// цвет окружности.
static const QColor circle_color(88, 88, 88, 255);
// цвет засечек часов.
static const QColor stroke_hour_color(19, 66, 91, 255);
// цвет засечек минут.
static const QColor stroke_min_color(100, 120, 120, 255);
// цвет часовой стрелки.
static const QColor hour_hand_color(60, 65, 65, 255);
// цвет минутной стрелки.
static const QColor min_hand_color(90, 105, 105, 255);
// цвет секундной стрелки.
static const QColor sec_hand_color(125, 150, 150, 255);

//
// константы метрик и координат.
//

// ширина линии для отрисовки окружности.
static const int circle_line_width = 5;
// ширина линии штриха (деление).
static const int stroke_line_width = 3;
// длина штриха часов.
static const int stroke_hour_length = 10;
// длина штриха минут.
static const int stroke_min_length = 5;

// зазор меду окружностью и засечками.
static const int spacing = 10;
// абсцисса центра окружности в оконных координатах.
const double circle_center_x = width() / 2;
// ордината центра окружности в оконных координатах.
const double circle_center_y = height() / 2;
// радиус окружности.
const double circle_radius = (circle_center_x < circle_center_y ?
circle_center_x : circle_center_y) - spacing - circle_line_width;

// радиус, описываемый часовой стрелкой.
const double hour_hand_radius = circle_radius / 2;
// радиус, описываемый минутной стрелкой.
const double min_hand_radius = hour_hand_radius + circle_radius / 6;
// радиус, описываемый секундной стрелкой.
const double sec_hand_radius = hour_hand_radius + circle_radius / 4;

static const double hour_hand_tail = 20.0; // длина хвоста часовой стрелки.
static const double min_hand_tail = 30.0; // длина хвоста минутной стрелки.
static const double sec_hand_tail = 40.0; // длина хвоста секундной стрелки.

// половина основания часовой стрелки.
static const double hour_hand_half_found = 15.0;
// половина основания минутной стрелки.
static const double min_hand_half_found = 10.0;
// половина основания секундной стрелки.
static const double sec_hand_half_found = 5.0;

// радиус, описываемый крайними точками хвоста часовой стрелки.
const double hour_hand_tail_radius = ::sqrt(::pow(hour_hand_tail, 2) +
::pow(hour_hand_half_found, 2));
// радиус, описываемый крайними точками хвоста минутной стрелки.
const double min_hand_tail_radius = ::sqrt(::pow(min_hand_tail, 2) +
::pow(min_hand_half_found, 2));
// радиус, описываемый крайними точками хвоста секундной стрелки.
const double sec_hand_tail_radius = ::sqrt(::pow(sec_hand_tail, 2) +
::pow(sec_hand_half_found, 2));


// координаты часовой стрелки в начальном состоянии (указывает на 3).
//
// конец стрелки.
const QPoint hour_hand_a0(hour_hand_radius, 0);
// координаты первой точки основания часовой стрелки.
const QPoint hour_hand_b0(-hour_hand_tail, hour_hand_half_found);
// координаты второй точки основания часовой стрелки.
const QPoint hour_hand_c0(-hour_hand_tail, -hour_hand_half_found);

// координаты минутной стрелки в начальном состоянии (указывает на 3).
//
// конец стрелки.
const QPoint min_hand_a0(min_hand_radius, 0);
// координаты первой точки основания минутной стрелки.
const QPoint min_hand_b0(-min_hand_tail, min_hand_half_found);
// координаты второй точки основания минутной стрелки.
const QPoint min_hand_c0(-min_hand_tail, -min_hand_half_found);

// координаты секундной стрелки в начальном состоянии (указывает на 3).
//
// конец стрелки.
const QPoint sec_hand_a0(sec_hand_radius, 0);
// координаты первой точки основания секундной стрелки.
const QPoint sec_hand_b0(-sec_hand_tail, sec_hand_half_found);
// координаты второй точки основания секундной стрелки.
const QPoint sec_hand_c0(-sec_hand_tail, -sec_hand_half_found);


//
// рисуем.
//

QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);

// устанавливаем новое начало координат.
painter.translate(circle_center_x, circle_center_y);

// копируем перо из устройства рисования.
QPen pen = painter.pen();

// рисуем окружность.
//
// устанавливаем ширину и цвет линии пера.
pen.setWidth(circle_line_width);
pen.setColor(circle_color);
// устанавливаем перо в устройство рисования.
painter.setPen(pen);
// рисуем.
painter.drawEllipse(QPoint(0, 0), static_cast<int>(circle_radius),
static_cast<int>(circle_radius));

// рисуем 60 засечек.
//
// крайняя к окружности точка.
const QPoint p1(circle_radius - circle_line_width - spacing, 0);
// вторая точка для штрихов часов.
const QPoint p2(p1.x() - stroke_min_length, 0);
// вторая точка для штрихов минут.
const QPoint p3(p1.x() - stroke_hour_length, 0);
pen.setWidth(stroke_line_width);
pen.setColor(stroke_min_color);
painter.setPen(pen);
for(int i = 0; i < 60; i++)
{
if(i % 5 == 0)
{
pen.setColor(stroke_hour_color);
painter.setPen(pen);
painter.drawLine(p1, p3);
pen.setColor(stroke_min_color);
painter.setPen(pen);
}
else
{
painter.drawLine(p1, p2);
}
painter.rotate(6.0);
}

// рисуем стрелки.
//
QPoint points[3]; // точки для рисования.
// узнаём текущее время.
QTime cur_time = QTime::currentTime();
// часовая стрелка.
//
// угол часовой стрелки от начального состояния (3 часа).
const int hour_beta = 90 - (cur_time.hour() * 30
+ cur_time.minute() / 2);
points[0] = rotatePoint(hour_hand_a0, hour_beta, hour_hand_radius);
points[1] = rotatePoint(hour_hand_b0, hour_beta, -hour_hand_tail_radius);
points[2] = rotatePoint(hour_hand_c0, hour_beta, -hour_hand_tail_radius);
painter.setPen(Qt::NoPen);
painter.setBrush(hour_hand_color);
painter.drawConvexPolygon(points, 3);
// минутная стрелка.
//
// угол минутной стрелки от начального состояния (3 часа).
const int min_beta = 90 - cur_time.minute() * 6;
points[0] = rotatePoint(min_hand_a0, min_beta, min_hand_radius);
points[1] = rotatePoint(min_hand_b0, min_beta, -min_hand_tail_radius);
points[2] = rotatePoint(min_hand_c0, min_beta, -min_hand_tail_radius);
painter.setBrush(min_hand_color);
painter.drawConvexPolygon(points, 3);
// секундная стрелка.
//
// угол секундной стрелки от начального состояния (3 часа).
const int sec_beta = 90 - cur_time.second() * 6;
points[0] = rotatePoint(sec_hand_a0, sec_beta, sec_hand_radius);
points[1] = rotatePoint(sec_hand_b0, sec_beta, -sec_hand_tail_radius);
points[2] = rotatePoint(sec_hand_c0, sec_beta, -sec_hand_tail_radius);
painter.setBrush(sec_hand_color);
painter.drawConvexPolygon(points, 3);

// переварачиваем всё вверх ногами, так как на экране направление
// оси ординат сверху вниз.
painter.rotate(180);
}
//===================================================================

QPoint CClock::rotatePoint(const QPoint & point, int degree, double radius)
{
static const double pi = 3.14159265359; // число Пи.

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

// старый угол.
double old_degree_rad = ::asin(point.y() / radius);
// переводим угол в радианы.
double degree_rad = degree * pi / 180.0;
// новый угол.
double new_degree_rad = old_degree_rad - degree_rad;

return QPoint(::cos(new_degree_rad) * radius, ::sin(new_degree_rad) * radius);
}
//===================================================================

Ну, и использование класса для отображения в простейшем случае выглядит так
#include <QtGui/QApplication>
#include "Clock.h"

int main(int argc, char *argv[])
{
QApplication a(argc, argv);
CClock w;
w.show();
return a.exec();
}





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

Мы определяем координаты для всех стрелок в начальном состоянии. За начальное состояние
я принял угол равный 0 градусов. На рисунке 3 показано начальное состояние стрелки, соответствующее треугольнику AoBoCo. Стрелку мы условно разделяем на две части: собственно стрелку и хвост. Хвост - это та часть, которая находится с обратной стороны точки вращения стрелки. Более подробно хвостовая часть показана на рисунке 4.

На рисунке 4 мы видим треугольники OBoDo и OCoDo. Из любого из них мы можем легко найти радиус окружности по теореме Пифагора.

Итак, координаты стрелки в начальном состоянии таковы:
Ao: x равен длине отрезка OAo, у равен нулю.
Bo: x равен длине отрезка ODo, взятой с отрицательным знаком, y равен длине отрезка DoBo, взятой с отрицательным знаком.
Co: x равен длине отрезка ODo, взятой с отрицательным знаком, y равен длине отрезка DoCo.

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

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

Обёртка для вызова функций из dll при динамическом связывании

Как-то раз мне пришлось загружать очень много функций из dll. После десятого вызова GetProcAddress и добавление переменной в класс (к тому же функции вызывались не из одной библиотеки, что приводило ещё к вызовам LoadLibrary и FreeLibrary) я решил автоматизировать этот процесс раз и навсегда.

#ifndef DYNAMIC_LIB_PROC
#define DYNAMIC_LIB_PROC

class CDynamicLibProcException
{
// Здесь какая-то начинка исключения.
};

struct CUnloadLibOnExeption
{
void operator () (HMODULE hlib)
{
if(hlib)
::FreeLibrary(hlib);
}
};

struct CNotUnloadLibOnExeption { void operator () (HMODULE) { } };

template<typename ProcT, typename ExceptionActionT = CUnloadLibOnExeption>
class CDynamicLibProc
{
public:
CDynamicLibProc(LPCTSTR libname, LPCSTR procname,
bool unload_lib_on_destroy = true) :
m_hlib(::LoadLibrary(libname)),
m_unload_lib_on_destroy(unload_lib_on_destroy)
{
LoadProc(procname);
}

CDynamicLibProc(const HMODULE hlib, LPCSTR procname,
bool unload_lib_on_destroy = false) :
m_hlib(hlib),
m_unload_lib_on_destroy(unload_lib_on_destroy)
{
LoadProc(procname);
}

~CDynamicLibProc()
{
if(m_unload_lib_on_destroy && m_hlib)
::FreeLibrary(m_hlib);
}

HMODULE GetLibHandle() const
{
return m_hlib;
}

public:
ProcT Execute;

private:

void LoadProc(LPCSTR procname)
{
if(!m_hlib)
throw CDynamicLibProcException(); // Тут Вы дополняете своё исключение.
Execute = (ProcT)::GetProcAddress(m_hlib, procname);
if(!Execute)
{
ExceptionActionT ea;
ea(m_hlib);
throw CDynamicLibProcException(); // Тут Вы дополняете своё исключение.
}
}

private:
HMODULE m_hlib;
bool m_unload_lib_on_destroy;
};

#endif // #ifndef DYNAMIC_LIB_PROC

Ну, а использовать этот класс можно так
#include <windows.h>
#include <tchar.h>
#include "DynamicLibProc.h"

int main()
{
CDynamicLibProc<DWORD (WINAPI *)(HWND, LPCWSTR, DWORD, DWORD)>
message_box(_T("user32.dll"), "MessageBoxW");
message_box.Execute(NULL, L"Hello from MessageBox", NULL, 0);
return 0;
}

"Умные" указатели с подсчётом ссылок

Язык C++ был разработан с учётом специфики системного программирования, поэтому не имеет встроенного сборщика мусора. Но порой требуется передать куда-то указатель и не заботиться о его дальнейшей судьбе. Для этих целей существует такое понятие, как "Умные указатели".
Концепция заключается в том, что Вы пишите класс-обёртку для указателей и определяете в нём необходимые операторы. В деструкторе этого класса следует высвободить память, которую занимает указатель. Для того, что бы не удалить указатель раньше времени, принято считать количество ссылок на объект. Для этого объявляем счётчик-переменную в куче и в операторе присвоения и копирующем конструкторе инкрементируем его, а в деструкторе - декрементируем. Как только счётчик в деструкторе достигает нуля - высвобождаем всю занятую память. Следующий пример демонстрирует концепцию создания класса "умного" указателя.

#ifndef SMARTPTR_H
#define SMARTPTR_H

namespace SmartPtr {


template<typename TypeT>
class CSmartPtr
{
public:
explicit CSmartPtr(TypeT * ptr = NULL)
{
mp_pointer = ptr;
mp_link_counter = new int;
*mp_link_counter = 1;
}

CSmartPtr(const CSmartPtr<TypeT> & rhs)
{
mp_pointer = rhs.mp_pointer;
mp_link_counter = rhs.mp_link_counter;
(*mp_link_counter)++;
}

~CSmartPtr()
{
Release();
}

CSmartPtr<TypeT> & operator = (const CSmartPtr & rhs)
{
if(&rhs != this)
{
Release();
mp_pointer = rhs.mp_pointer;
mp_link_counter = rhs.mp_link_counter;
(*mp_link_counter)++;
}
return *this;
}

TypeT * operator -> ()
{
return mp_pointer;
}

TypeT operator * ()
{
return *mp_pointer;
}

TypeT * GetPointer()
{
return mp_pointer;
}


private:
void Release()
{
if(! --(*mp_link_counter))
{
delete mp_link_counter;
mp_link_counter = NULL;
if(mp_pointer)
{
delete mp_pointer;
mp_pointer = NULL;
}
}
}

private:
TypeT * mp_pointer;
int * mp_link_counter;
};

} // namespace SmartPtr

#endif // SMARTPTR_H

Приведу небольшой пример, использующий этот класс.
#include <iostream>
#include "SmartPtr.h"

class CTest
{
public:
~CTest()
{
std::cout << "destructorn";
}

void print() const
{
std::cout << "i'm testn";
}
};


void funct(SmartPtr::CSmartPtr<CTest> test)
{
test->print();
}

int main()
{
SmartPtr::CSmartPtr<CTest> test(new CTest);
funct(test);
return 0;
}

Вывод программы, как и ожидалось, будет таким
i'm test
destructor
В некоторых реализациях я встречал определение оператора приведения типа. Так как приведение типов в C++ не приветствуется, то и я не стал определять этот оператор. В случае крайней нужды можно воспользоваться методом GetPointer.