Координатные преобразования

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

Создайте в проекте форму с именем DemoTimer. Установите её размеры равными 300 (ширина) на 250 (высота) пикселей. Разместите на форме управляющий элемент Timer, установите его свойство Enabled в «ложь», а свойству Interval присвойте значение 1000, чтобы таймер срабатывал один раз в секунду.
Зафиксируйте размеры формы и удалите кнопки MinButton и MaxButton.
Добавьте в форму следующие свойства:

  • nativeGraphics — для хранения дескриптора объекта Graphics
  • nativePen — для хранения дескриптора пера (им мы будем рисовать шкалу часов и стрелки)
  • nativeBrush — для хранения дескриптора кисти (ею мы нарисуем кружочек в центре циферблата)
  • cPointsHour — массив координат точек часовой стрелки
  • cPointsMinute — массив координат точек минутной стрелки

Установите начальные значения свойств nativeGraphics, nativePen и nativeBrush равными нулю.
В этом примере мы не будем пользоваться какими-либо классами, поэтому объявите в методе Init формы все необходимые API функции:

Листинг 23.24. Код метода Init формы DemoTimer

 
 DECLARE Long GdipCreateFromHWND IN Gdiplus.dll Long, Long @
 DECLARE Long GdipGraphicsClear IN Gdiplus.dll Long, Long
 DECLARE Long GdipTranslateWorldTransform IN Gdiplus.dll Long, Single, Single, Long
 DECLARE Long GdipRotateWorldTransform IN Gdiplus.dll Long, Single, Long
 DECLARE Long GdipDrawLineI IN Gdiplus.dll Long, Long, Long, Long, Long, Long
 DECLARE Long GdipFillEllipseI IN Gdiplus.dll Long, Long, Long, Long, Long, Long
 DECLARE Long GdipCreatePen1 IN Gdiplus.dll Long, Single, Long, Long @
 DECLARE Long GdipSetPenColor IN Gdiplus.dll Long, Long
 DECLARE Long GdipSetPenWidth IN Gdiplus.dll Long, Single
 DECLARE Long GdipCreateSolidFill IN Gdiplus.dll Long, Long @
 DECLARE Long GdipDeleteGraphics IN Gdiplus.dll Long
 DECLARE Long GdipDeletePen IN Gdiplus.dll Long
 DECLARE Long GdipDeleteBrush IN Gdiplus.dll Long
 DECLARE Long GdipDrawClosedCurveI IN Gdiplus.dll Long, Long, String, Long
 DECLARE Long GdipSetSolidFillColor IN Gdiplus.dll Long, Long
 DECLARE Long GdipSetSmoothingMode IN Gdiplus.dll Long, Long
 DECLARE Long GetLocalTime IN WIN32API String @
 * Заполнение структур, описывающих массивы размеров часовой и минутной стрелок
 thisform.cPointsHour = BINTOC(0, "4RS") + BINTOC(-90, "4RS") + BINTOC(-10, "4RS") + ;
                        BINTOC(10, "4RS") + BINTOC(10, "4RS") + BINTOC(10, "4RS")
 thisform.cPointsMinute = BINTOC(0, "4RS") + BINTOC(-115, "4RS") + BINTOC(-10, "4RS") + ;
                          BINTOC(5, "4RS") + BINTOC(10, "4RS") + BINTOC(5, "4RS")
 

Последняя объявленная функция, GetLocalTime, подробно рассмотрена в главе 19 «Windows API». Она необходима для получения значений часов, минут и секунд.
В свойствах cPoinsHour и cPoinsMinute запоминаются координаты точек для рисования часовой и минутной стрелок. Каждая стрелка описывается тремя точками, по которым рисуется замкнутый сплайн; в системе координат стрелки направлены вертикально вверх. Перемещение стрелок по циферблату часов будет выполняться не за счёт пересчёта значений их координат, а за счёт поворота самих осей координат.

Событие Destroy возникает перед уничтожением формы. В методе, обрабатывающем это событие, необходимо удалить все созданные объекты GDIPlus.

Листинг 23.25. Код метода Destroy формы DemoTimer

 
 = GdipDeleteGraphics(this.nativeGraphics)
 = GdipDeletePen(this.nativePen)
 = GdipDeleteBrush(this.nativeBrush)
 

Где лучше всего создавать связанный с формой объект Graphics? Вероятно, в методе, обрабатывающем событие Paint. Событие Paint происходит, когда форма уже выведена на экран. Одновременно с созданием объекта Graphics так же создадим перо и кисть. В этом же методе при помощи функции GdipTranslateWorldTransform перенесём оси координат в центр формы.

Листинг 23.26. Код метода Paint формы DemoTimer

 
 LOCAL lnGraphics, lnPen, lnBrush
 lnGraphics = 0
 IF GdipCreateFromHWND(this.HWnd, @lnGraphics) = 0
    this.nativeGraphics = lnGraphics
    = GdipSetSmoothingMode(lnGraphics, 4)
    lnPen = 0
    IF GdipCreatePen1(0, 1, 0, @lnPen) = 0
       this.nativePen = lnPen
       lnBrush = 0
       IF GdipCreateSolidFill(0xFF00FF00, @lnBrush) = 0
          this.nativeBrush = lnBrush
          = GdipTranslateWorldTransform(lnGraphics, this.width/2, this.height/2, 0)
          this.Timer1.Timer()
          this.Timer1.Enabled = .t.
       ENDIF 
    ENDIF 
 ENDIF
 

В методе функцией GdipCreateFromHWND создаётся объект Graphics, связанный с окном формы. Так как мы создали обычную немодальную форму без полос прокрутки, то нам не требуется определять «истинный» hWnd верхнего окна.
При помощи функции GdipSetSmoothingMode включаем режим антиалисинга — все рисуемые линии не будут иметь характерных зубцов.
Функция GdipCreatePen1 создаёт перо, которым мы будем рисовать контуры стрелок часов, а функция GdipCreateSolidFill создаёт кисть ярко-зелёного цвета; этой кистью мы будем рисовать ось, на которой «насажены» стрелки часов.

Теперь самое главное: при помощи функции GdipTranslateWorldTransform центр осей координат переносится в центр окна формы. Надеюсь, вы обратили внимание на то, что в методе Init координаты точек стрелок заданы отрицательными числами; сделано это как раз с учётом того, что центр оси координат будет смещён, и поэтому стрелки будут находиться внутри окна формы.
Далее вызывается метод Timer объекта-таймера Timer1. В этом методе собственно и выполняется рисование циферблата и стрелок, а вызываем мы его здесь только для того, чтобы, после появления формы на экране, целую секунду не ждать, когда же на ней появится изображение часов.

Откройте для редактирования метод Timer управляющего элемента Timer1 и введите в него код из листинга 23.27.

Листинг 23.26. Код метода Timer объекта-таймера Timer1 формы DemoTimer

 
 LOCAL i, lnHour, lnMinute, lnSecond, lcSystemTime
 * Всё стираем и заливаем форму однотонным цветом
 = GdipGraphicsClear(thisform.nativeGraphics, 0xFFEEFFEE)
 STORE 0 TO lnHour, lnMinute, lnSecond
 * Формируем структуру SystemTime и вызываем функцию GetLocalTime
 * для получения текущего значения часов, минут и секунд
 lcSystemTime = REPLICATE(CHR(0),16)
 = GetLocalTime(@lcSystemTime)
 lnHour = CTOBIN(SUBSTR(lcSystemTime,9,2),'2RS')
 lnMinute = CTOBIN(SUBSTR(lcSystemTime,11,2),'2RS')
 lnSecond = CTOBIN(SUBSTR(lcSystemTime,13,2),'2RS')
 * Корректируем значение часов
 IF lnHour > 12
    lnHour = lnHour – 12
 ENDIF 
 lnHour = INT(5 * lnHour + lnMinute / 12)
 * Цикл на 60 итераций
 FOR i = 0 TO 59
 * Рисуем минутные отметки шкалы циферблата
    = GdipSetPenWidth(thisform.nativePen, 4)
    = GdipSetPenColor(thisform.nativePen, 0xFF00AA00)
    = GdipDrawLineI(thisform.nativeGraphics, thisform.nativePen, 117, 0, 120, 0)
    IF MOD(i, 5) = 0
 * Рисуем часовые отметки циферблата
       = GdipSetPenWidth(thisform.nativePen, 3)
       = GdipSetPenColor(thisform.nativePen, 0xFF0000AA)
       = GdipDrawLineI(thisform.nativeGraphics, thisform.nativePen, 110, 0, 125, 0)
    ENDIF
    IF lnHour = i && Рисуем часовую стрелку
       = GdipSetPenWidth(thisform.nativePen, 2)
       = GdipSetPenColor(thisform.nativePen, 0xFF0000AA)
       = GdipDrawClosedCurveI(thisform.nativeGraphics, thisform.nativePen, ;
                              thisform.cPointsHour, 3)
    ENDIF 
    IF lnMinute = i && Рисуем минутную стрелку
       = GdipSetPenWidth(thisform.nativePen, 2)
       = GdipSetPenColor(thisform.nativePen, 0xFF0000AA)
       = GdipDrawClosedCurveI(thisform.nativeGraphics, ;
                              thisform.nativePen, thisform.cPointsMinute, 3)
    ENDIF 
    IF lnSecond = i && Рисуем секундную стрелку
       = GdipSetPenWidth(thisform.nativePen, 2)
       = GdipSetPenColor(thisform.nativePen, 0xFFAA6600)
       = GdipDrawLineI(thisform.nativeGraphics, thisform.nativePen, 0, -125, 0, 30)
    ENDIF 
 * Поворачиваем оси координат на 6 градусов (360 / 60 = 6)
    = GdipRotateWorldTransform(thisform.nativeGraphics, 6, 0)
 ENDFOR 
 * Рисуем часовую ось
 = GdipFillEllipseI(thisform.nativeGraphics, thisform.nativeBrush, -6, -6, 12, 12)
 

Сохраните форму и запустите её на выполнение. Вы должны увидеть нечто подобное показанному на рис.23.6. Часы должны «ходить».

Рис. 23.6. Форма DemoTimer в работе

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