netlib.narod.ru< Назад | Оглавление | Далее >

Аналоговые часы

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

Для большей универсальности я решил реализовать логику отображения часов в качестве дочернего элемента управления окна подобно классу CheckerChild в программе CheckerWithChildren в главе 8. Это облегчит введение часов в другое приложение или написание приложения, отображающего множество циферблатов. Вот листинг класса ClockControl.

ClockControl.cs

  //---------------------------------------------
  // ClockControl.cs (C) 2001 by Charles Petzold
  //---------------------------------------------
  using System;
  using System.Drawing;
  using System.Drawing.Drawing2D;
  using System.Windows.Forms;

  namespace Petzold.ProgrammingWindowsWithCSharp
  {
  class ClockControl: UserControl
  {
      DateTime dt;

      public ClockControl()
      {
          ResizeRedraw = true;
          Enabled = false;
      }
      public DateTime Time
      {
          get 
          { 
              return dt;
          }
          set 
          { 
              Graphics grfx = CreateGraphics();
              InitializeCoordinates(grfx);

              Pen pen = new Pen(BackColor);

              if (dt.Hour != value.Hour)
              {
                  DrawHourHand(grfx, pen);
              }
              if (dt.Minute != value.Minute)
              {
                  DrawHourHand(grfx, pen);
                  DrawMinuteHand(grfx, pen);
              }
              if (dt.Second != value.Second)
              {
                  DrawMinuteHand(grfx, pen);
                  DrawSecondHand(grfx, pen);
              }
              if (dt.Millisecond != value.Millisecond)
              {
                  DrawSecondHand(grfx, pen);
              }
              dt  = value;
              pen = new Pen(ForeColor);

              DrawHourHand(grfx, pen);
              DrawMinuteHand(grfx, pen);
              DrawSecondHand(grfx, pen);

              grfx.Dispose();
          }
      }
      protected override void OnPaint(PaintEventArgs pea)
      {
          Graphics grfx  = pea.Graphics;
          Pen      pen   = new Pen(ForeColor);
          Brush    brush = new SolidBrush(ForeColor);

          InitializeCoordinates(grfx);
          DrawDots(grfx, brush);
          DrawHourHand(grfx, pen);
          DrawMinuteHand(grfx, pen);
          DrawSecondHand(grfx, pen);
      }
      void InitializeCoordinates(Graphics grfx)
      {
          if (Width == 0 || Height == 0)
              return;

          grfx.TranslateTransform(Width / 2, Height / 2);

          float fInches = Math.Min(Width / grfx.DpiX, Height / grfx.DpiY);

          grfx.ScaleTransform(fInches * grfx.DpiX / 2000,
                              fInches * grfx.DpiY / 2000);
      }
      void DrawDots(Graphics grfx, Brush brush)
      {
          for (int i = 0; i < 60; i++)
          {
              int iSize = i % 5 == 0 ? 100 : 30;

              grfx.FillEllipse(brush, 0 - iSize / 2, -900 - iSize / 2, 
                                      iSize, iSize);
              grfx.RotateTransform(6);
          }
      }
      protected virtual void DrawHourHand(Graphics grfx, Pen pen)
      {
          GraphicsState gs = grfx.Save();
          grfx.RotateTransform(360f * Time.Hour / 12 +
                                30f * Time.Minute / 60);

          grfx.DrawPolygon(pen, new Point[]
                              {
                                  new Point(0,  150), new Point( 100, 0),
                                  new Point(0, -600), new Point(-100, 0)
                              });
          grfx.Restore(gs);
      }
      protected virtual void DrawMinuteHand(Graphics grfx, Pen pen)
      {
          GraphicsState gs = grfx.Save();
          grfx.RotateTransform(360f * Time.Minute / 60 +
                                 6f * Time.Second / 60);

          grfx.DrawPolygon(pen, new Point[]
                              {
                                  new Point(0,  200), new Point( 50, 0),
                                  new Point(0, -800), new Point(-50, 0)
                              });
          grfx.Restore(gs);
      }
      protected virtual void DrawSecondHand(Graphics grfx, Pen pen)
      {
          GraphicsState gs = grfx.Save();
          grfx.RotateTransform(360f * Time.Second / 60 +
                                 6f * Time.Millisecond / 1000);

          grfx.DrawLine(pen, 0, 0, 0, -800);
          grfx.Restore(gs);          
      }
  }
  }

Класс ClockControl унаследован от класса UserControl и переопределяет метод OnPaint. В конструкторе ClockControl стилю ResizeRedraw элемента управления присваивается значение true, а свойству Enabled — false. Для работы классу ClockControl не нужен ввод с клавиатуры или мыши, так что этот ввод будет контролироваться родительскими объектами.

Обратите внимание на закрытое поле типа DateTime, которому я присвоил имя dt, и на доступное для чтения и записи, открытое свойство с именем Time, предоставляющее другим объектам доступ к этому полю. Элемент управления не использует собственный таймер и не устанавливает это свойство самостоятельно; он лишь отображает время, соответствующее значению свойства Time. Обновление значения свойства Time входит в задачу класса, создающего экземпляр объекта ClockControl.

Код метода доступа set свойства Time выглядит чрезмерно длинным. Есть искушение сделать его короче, примерно так:

  dt = value;
  Invalidate();

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

OnPaint создает перо и кисть, используя основной цвет элемента управления, и вызывает пять других методов. Прежде всего метод InitializeCoordinates устанавливает систему координат с началом в центре элемента управления и изотропными координатами, содержащими 1000 единиц по каждому из четырех направлений.

Далее метод DrawDots рисует точки, отмечающие минуты и часы. Он использует методы класса Graphics: метод FillEllipse для вывода точки в позиции 12:00 и RotateTransform для поворота на 6° к следующей точке. Методы DrawHourHand, DrawMinuteHand и DrawSecondHand также используют метод RotateTransform. Я сделал три этих метода виртуальными, чтобы иметь возможность переопределить их в дальнейшем (точнее, в программе из главы 13).

При самом рисовании (часовой и минутной стрелок методом DrawPolygon и секундной — методом DrawLine) предполагается, что стрелки направлены вертикально вверх. Вызов метода RotateTransform перед рисованием стрелок поворачивает их на требуемый угол. Каждая процедура рисования стрелок содержит вызов метода Save класса Graphics для сохранения текущего состояния перед вызовом RotateTransform. По завершении рисования вызывается метод Restore.

На положение часовой стрелки влияют два свойства структуры DateTime: Hour и Minute. Положение минутной стрелки зависит от свойств Minute и Second, а положение секундной — от свойств Second и Millisecond. Это позволяет стрелкам не перескакивать с позиции на позицию, а плавно двигаться по кругу.

Теперь можно вернуться к метоу доступа set свойства DateTime. После вызова CreateGraphics для получения объекта Graphics следует вызов метода InitializeCoordinates, инициализирующего систему координат. Затем создается перо фонового цвета. Это нужно для оптимального стирания изменяющей свое положение стрелки. Тут правда, есть небольшая проблема: стирание какой-либо стрелки может повредить две другие. Поэтому придется перерисовывать все три стрелки. Несмотря на большой объем рисования, такой подход значительно снижает мерцание изображения.

Теперь, имея такой элемент управления, создать форму, использующую его, будет довольно просто.

В конструкторе программа создает объект класса ClockControl, устанавливает свойство Parent элемента управления на форму и инициализирует свойство Time текущим значением даты и времени.

AnalogClock.cs

  //--------------------------------------------
  // AnalogClock.cs (C) 2001 by Charles Petzold
  //--------------------------------------------
  using System;
  using System.Drawing;
  using System.Windows.Forms;
  using Petzold.ProgrammingWindowsWithCSharp;

  class AnalogClock: Form
  {
      ClockControl clkctl;

      public static void Main()
      {
          Application.Run(new AnalogClock());
      }
      public AnalogClock()
      {
          Text = "Analog Clock";
          BackColor = SystemColors.Window;
          ForeColor = SystemColors.WindowText;

          clkctl = new ClockControl();
          clkctl.Parent    = this;
          clkctl.Time      = DateTime.Now;
          clkctl.Dock      = DockStyle.Fill;
          clkctl.BackColor = Color.Black;
          clkctl.ForeColor = Color.White;

          Timer timer    = new Timer();
          timer.Interval = 100;
          timer.Tick    += new EventHandler(TimerOnTick);
          timer.Start();
      }
      void TimerOnTick(object obj, EventArgs ea)
      {
          clkctl.Time = DateTime.Now;
      }
  }

Далее устанавливается свойство элемента управления, о котором я еще не упоминал, — Dock. Оно содержится в классе Control, и в главе 12 я расскажу о нем подробнее. Пока скажу лишь о том, что присвоение ему значения DockStyle.Fill растягивает элемент управления на всю поверхность предка. Таким образом, часы будут автоматически изменять свой размер, чтобы соответствовать размерам формы.

В последнюю очередь свойству BackColor сопоставляется черный цвет, а свойству ForeColor — белый. Это делается лишь для того, чтобы показать, что элемент управления не управляет своим цветом. Это делает предок. Ну и, кроме того, белые часы на черном фоне выглядят довольно здорово:


Рис. 10.6.

Обработка конструктора завершается установкой таймера на интервал в 100 миллисекунд (1/10 секунды). Обычно часы требуется обновлять лишь раз в секунду, но если сделать так в данном случае, секундная стрелка будет перемещаться рывками. Обработчик события TimerOnTick просто устанавливает свойство Time элемента управления на текущую дату и время.

Если программе не требуется плавного перемещения секундной стрелки, интервал срабатывания таймера можно установить в 1 000 миллисекунд и присваивать свойству Time объект DateTime, содержащий в своем свойстве Millisecond значение 0. Поскольку свойство Millisecond доступно только для чтения, его обнуление потребует повторного создания объекта DateTime. Тогда обработчик TimerOnTick будет выглядеть так:

  DateTime dt = DateTime.Now;
  dt = new DateTime(dt.Year, dt.Month, dt.Day, dt.Hour, dt.Minute, dt.Second);
  clkctl.Time = dt;

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

  clkctl.Time += new TimeSpan(10000000);

Часы устанавливаются на правильное время, но идут в 10 раз быстрее. А если попробовать

  clkctl.Time -= new TimeSpan(1000000);

то часы будут идти с нормальной скоростью, но в обратном направлении.


netlib.narod.ru< Назад | Оглавление | Далее >

Сайт управляется системой uCoz