На главную Наши проекты:
Журнал   ·   Discuz!ML   ·   Wiki   ·   DRKB   ·   Помощь проекту
ПРАВИЛА FAQ Помощь Участники Календарь Избранное RSS
msm.ru
Модераторы: maxim84_
  
> Аналоговые часы (контролл Windows Forms) , в стиле Vista
    По мотивам недавно прошедшего конкурса, решил написать заметку о том, как реализовать аналоговые часы в виде повторно используемого контрола WinForms.

    Ниже приводится скрин окна с работающим контролом:

    user posted image

    Исходники можно загрузить тут:
    http://ifolder.ru/7309260 (2008)
    http://ifolder.ru/7316730 (2005)


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

    Создадим проект типа class Library и назовем его Clock.

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

    Следующим этапом будет создание перечисления Faces
    ExpandedWrap disabled
      public enum Faces
          {
              BlueRoman,
              Ocean,
              Vinil,
              Vista,
              Wood
          }


    По сути, оно является перечислением всех возможных вариантов отображения циферблата наших часов.

    На следующем шаге создадим вспомогательный класс FacesManager, поредством которого наш контрол будет извлекать изображения из ресурсов сборки и предоставлять общий интерфейс получения изображений с циферблатами клиенту:

    ExpandedWrap disabled
      public static class FacesManager
          {
              public static Dictionary<Faces, Bitmap> _faces = new Dictionary<Faces, Bitmap>();
       
              static FacesManager()
              {
                  _faces.Add(Faces.BlueRoman, Resource.BlueRoman);
                  _faces.Add(Faces.Ocean, Resource.Ocean);
                  _faces.Add(Faces.Vinil, Resource.Vinil);
                  _faces.Add(Faces.Vista, Resource.Vista);
                  _faces.Add(Faces.Wood, Resource.Wood);
              }
       
              public static Bitmap Load(Faces face)
              {
                  return _faces[face];
              }
          }


    Вроде просто, класс содержит словарь хранящий изображения, по ключу Faces.
    Клиент класса используя метод Load, с параметром тип циферблата, извлекает изображение.
    Если же нам в будующем понадобиться обеспечить другие механизмы загрузки изображений или изменится механизм хранения
    изображений в ресурсах, то мы сможем реализовать все это в классе FaceManager.
    Более того, возможно, Вам захочется добавить собственные изображения в словарь, в Runtime,
    тогда Вам придется добавить еще один метод AddFace в класс FacesManager. При любых изменениях.
    В тоже самое время код который получает изображения для рисования останется без изменений.

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

    ExpandedWrap disabled
      public class GraphicsHolder
          {
              private GraphicsState _state;
       
              public void Save(Graphics grfx)
              {
                  _state = grfx.Save();
              }
       
              public void Resotore(Graphics grfx)
              {
                  grfx.Restore(_state);
              }
          }


    Начнем реализовывать контрол:

    ExpandedWrap disabled
      public class Clock : Control
      {
      }


    Так как мы решили рисовать контрол самостоятельно, то выбор базового класса Control для нас выглядит вроде логичным.
    На этом этапе мы должны опредилиться со свойствами доступными пользователю нашего контрола для редактирования поведения и внешнего вида. Я решил остановиться на следующих:

    ExpandedWrap disabled
      #region Управление внешним видом контрола
       
              [Category("Clock color settings")]
              public Faces Skin
              {
                  get { return _skin; }
                  set
                  {
                      _skin = value;
                      Invalidate();
                  }
              }
       
              [Category("Clock color settings")]
              public Color Second
              {
                  get { return _secondHandColor; }
                  set
                  {
                      _secondHandColor = value;
                      Invalidate();
                  }
              }
       
              [Category("Clock color settings")]
              public Color Minute
              {
                  get { return _minuteHandColor; }
                  set
                  {
                      _minuteHandColor = value;
                      Invalidate();
                  }
              }
       
              [Category("Clock color settings")]
              public Color Hour
              {
                  get { return _hourHandColor; }
                  set
                  {
                      _hourHandColor = value;
                      Invalidate();
                  }
              }
       
              [Category("Clock color settings")]
              public Color Dot
              {
                  get { return _dotColor; }
                  set
                  {
                      _dotColor = value;
                      Invalidate();
                  }
              }
       
              [Category("Clock color settings")]
              public Color Shadow
              {
                  get { return _shadowColor; }
                  set
                  {
                      _shadowColor = value;
                      Invalidate();
                  }
              }
       
              [Category("Clock behaviour")]
              public bool DotNeed
              {
                  get { return _dotNeed; }
                  set
                  {
                      _dotNeed = value;
                      Invalidate();
                  }
              }
       
              [Category("Clock behaviour")]
              public bool ShadowNeed
              {
                  get { return _shadowNeed; }
                  set
                  {
                      _shadowNeed = value;
                      Invalidate();
                  }
              }
       
              [Category("Clock general")]
              public DateTime Current
              {
                  get { return _currentTime; }
                  set
                  {
                      _currentTime = value;
                      Invalidate();
                  }
              }
       
              #endregion



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

    Тут пожалуй следует обсудить еще одну немаловажную деталь. Я решил не перегружать контрол внутреним таймером. С одной стороны Контрол который сам вычисляет время которое нужно отобразить выглядит заманчиво, с другой зачастую в приложении можно обойтись единственым таймером для отображения различных значений времени. Мне кажется мой выбор верен, в конечном итоге контрол всего лишь Вид для отображения времени и он не должен использоваться иначе. Паралельно контрол остается легковесным и использование нескольких таких контролов на форме не будет приводить к созданию нескольких таймеров. В любом случае никто в будующем не может Вам помешать реализовать контрол AutoClock c использованием обычного наследования контрола Clock и добавления таймера позволяющего сделать часы полностью автономными.

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

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

    ExpandedWrap disabled
      protected void CalculateRoundRegionView()
              {
                  var path = new GraphicsPath();
                  path.AddEllipse(0, 0, Width, Height);
                  var region = new Region(path);
                  Region = region;
              }


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

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

    ExpandedWrap disabled
      protected void CalculateBorderSize()
              {
                  int median = (Width + Height)/2;
                  Width = median;
                  Height = median;
              }


    Переопределим метод OnResize:

    ExpandedWrap disabled
      protected override void OnResize(EventArgs e)
              {
                  CalculateBorderSize();
                  CalculateRoundRegionView();
              }


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

    ExpandedWrap disabled
      public int Radius
              {
                  get { return Width / 2; }
              }


    Следующий метод InitializeDefault:

    ExpandedWrap disabled
      protected virtual void InitializeDefault()
              {
                  Width            = DefaultClockSize;
                  Height           = DefaultClockSize;
       
                  DoubleBuffered   = true;
                  Text             = string.Empty;
       
                  Second           = Color.Red;
                  Minute           = Color.Black;
                  Hour             = Color.Black;
                  Dot              = Color.Red;
                  Shadow           = Color.DarkGray;
       
                  DotNeed          = true;
                  ShadowNeed       = true;
       
                  Current          = DateTime.Now;
              }


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

    В конечном итоге последнее (и самое важное), что мы должны сделать, это реализовать рисование нашего контрола.
    Для этого мы переопределим метод OnPaint унаследованый от класса Control.

    Мне хотелось предоставить возможность в дальнейшем реализовать различные варианты для отрисовки внешенего вида контрола. Для этого я определяю максимально простой интерфейс IClockPainter:

    ExpandedWrap disabled
      public interface IClockPainter
          {
              void Draw(Clock clock, Graphics grfx);
          }


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

    Ниже приводится одна из возможных реализаций такого интерфейса (расмотрите класс внимательно он содержит общий алгоритм рисования):


    ExpandedWrap disabled
      using System.Drawing;
      using System.Drawing.Drawing2D;
       
      namespace Analog.Controls.Strategy
      {
          /// <summary>
          /// Класс ClockPainter представляет алгоритм
          /// отрисовывающий эллементы часов с использованием графического контекста.
          /// </summary>
          public abstract class ClockPainter : IClockPainter
          {
              protected GraphicsHolder _holder = new GraphicsHolder();
       
              #region Relative factor
       
              // Свойства объявлены как виртуальные с целью дать возможность классам наследникам определить
              // собственные коэфициенты для определения размеров эллементов часов
       
              protected virtual float RelativeDotSize
              {
                  get { return 0.025f; }
              }
       
              protected virtual float RelativeSecondSize
              {
                  get { return 0.8f; }
              }
       
              protected virtual float RelativeMinuteSize
              {
                  get { return 0.6f; }
              }
       
              protected virtual float RelativeHourSize
              {
                  get { return 0.4f; }
              }
       
              protected virtual float DisplacemenShadow
              {
                  get { return 0.03f; }
              }
              #endregion
       
              // Draw - алгоритм отрисовки часов
              // 1. Включаем сглаживание
              // 2. Отрисовываем цифирблат
              // 3. Выводим текст
              // 4. Переносим начало отсчета чуть ниже цента часов, для отрисовки тени
              //
              // *''''''''|
              // |        |
              // |        |
              // |        | * - начало отсчета (0, 0), до переноса
              // |________|
              //
              //  ________
              // |        |
              // |        |
              // |    *   | * - начало отсчета (0, 0), после переноса
              // |________|
              //
              // 5. Отрисовываем тень
              // 6. Переносим начало координат в самый центр часов, для отрисовки стрелок
              //
              //  _________
              // |         |
              // |         |
              // |    *    |  * - начало отсчета (0, 0), после переноса
              // |         |
              // |_________|
              //
              // 7. Рисуем стрелки
              // 8. Рисуем точку
              // 9. Отключаем сглаживание
       
              Public virtual void Draw(Clock clock, Graphics grfx)
              {
                  EnableAntialiasing(grfx);
       
                  DrawFace(clock, grfx);
                  DrawText(clock, grfx);
       
                  TranslateAxis(grfx, clock.Radius + clock.Radius * DisplacemenShadow);
       
                  if(clock.ShadowNeed)
                      DrawShadow(clock, grfx);
       
                  TranslateAxis(grfx, -clock.Radius * DisplacemenShadow);
                  DrawHands(clock, grfx);
       
                  if(clock.DotNeed)
                      DrawDot(clock, grfx);
       
                  DisableAntialiasing(grfx);
              }
       
              protected abstract void DrawText(Clock clock, Graphics grfx);
       
              // Отрисовываем все три стрелки
              protected abstract void DrawHands(Clock clock, Graphics grfx);
       
              // Рисуем циферблат
              protected abstract void DrawFace(Clock clock, Graphics grfx);
       
              protected abstract void DrawHour(Clock clock, Graphics grfx, bool shadow);
              protected abstract void DrawMinute(Clock clock, Graphics grfx, bool shadow);
              protected abstract void DrawSecond(Clock clock, Graphics grfx, bool shadow);
       
              // Отрисовка тени
              protected abstract void DrawShadow(Clock clock, Graphics grfx);
       
              // Отрисовка точки
              protected abstract void DrawDot(Clock clock, Graphics grfx);
       
              #region Helper Methods
       
              // Вспомогательные методы для управления графическим контекстом
       
              #region Translate Coordinate
              // Перенос координат по оси X и Y на величину diff
       
              private static void TranslateAxis(Graphics grfx, float diff)
              {
                  grfx.TranslateTransform(diff, diff);
              }
       
              #endregion
       
              #region Antialiasing
       
              // Управление сглаживанием
       
              private static void DisableAntialiasing(Graphics grfx)
              {
                  grfx.SmoothingMode = SmoothingMode.Default;
              }
       
              private static void EnableAntialiasing(Graphics grfx)
              {
                  grfx.SmoothingMode = SmoothingMode.AntiAlias;
              }
       
              #endregion
       
              #region Work with graphics state
       
              // Управление сохранением состояния графического контекста
       
              protected void Save(Graphics grfx)
              {
                  _holder.Save(grfx);
              }
       
              protected void Restore(Graphics grfx)
              {
                  _holder.Resotore(grfx);
              }
       
              #endregion
       
              #endregion
          }
      }


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

    Ниже приводтся реализация такого класса (по сути обычная математика 3-5 класс ;) ).

    ExpandedWrap disabled
      using System;
      using System.Drawing;
       
      namespace Analog.Controls.Strategy
      {
          public class SimpleClockPainter : ClockPainter
          {
              protected override void DrawText(Clock clock, Graphics grfx)
              {
                  SizeF textSize = grfx.MeasureString(clock.Text, clock.Font);
       
                  if(textSize.Width > clock.Radius || textSize.Height > clock.Radius)
                      return;
       
                  using (Brush brush = new SolidBrush(clock.ForeColor))
                  {
                      grfx.DrawString(clock.Text, clock.Font, brush, clock.Radius - textSize.Width / 2f, clock.Radius / 2f);                
                  }
              }
       
              protected override void DrawHands(Clock clock, Graphics grfx)
              {
                  DrawHour(clock, grfx, false);
                  DrawMinute(clock, grfx, false);
                  DrawSecond(clock, grfx, false);
              }
       
              protected override void DrawFace(Clock clock, Graphics grfx)
              {
                  Bitmap face = FacesManager.Load(clock.Skin);
                  grfx.DrawImage(face, 0, 0, clock.Width, clock.Height);
              }
       
              protected override void DrawDot(Clock clock, Graphics grfx)
              {
                  float size = clock.Width * RelativeDotSize;
       
                  if (size == 0)
                      size = 1;
       
                  float halfSize = size / 2;
       
                  grfx.DrawEllipse(new Pen(clock.Dot), -halfSize, -halfSize, size, size);
                  grfx.FillEllipse(new SolidBrush(clock.Dot), -halfSize, -halfSize, size, size);
              }
       
              protected override void DrawSecond(Clock clock, Graphics grfx, bool shadow)
              {
                  Save(grfx);
       
                  float rotateAngel = 360f*clock.Current.Second/60 + 6f*clock.Current.Millisecond/1000;
                  grfx.RotateTransform(rotateAngel);
       
                  int arrowLength = Convert.ToInt32((clock.Radius) * RelativeSecondSize);
                  int tail = arrowLength / 3;
       
                  using (var pen = shadow ? new Pen(clock.Shadow) : new Pen(clock.Second))
                  {
                      grfx.DrawLine(pen, 0, -arrowLength, 0, tail);
                  }
       
                  Restore(grfx);
              }
       
              protected override void DrawHour(Clock clock, Graphics grfx, bool shadow)
              {
                  Save(grfx);
       
                  float rotateAngel = 30f * clock.Current.Hour + clock.Current.Minute/2f;
                  grfx.RotateTransform(rotateAngel);
       
                  int arrowLength = Convert.ToInt32((clock.Radius) * RelativeHourSize);
                  int tail = arrowLength / 4;
       
                  using (var pen = shadow ? new Pen(clock.Shadow) : new Pen(clock.Hour))
                  {
                      grfx.DrawLine(pen, 0, -arrowLength, 0, tail);                
                  }
       
                  Restore(grfx);
              }
       
              protected override void DrawMinute(Clock clock, Graphics grfx, bool shadow)
              {
                  Save(grfx);
       
                  float rotateAngel = 6f * clock.Current.Minute + clock.Current.Second/10f;
                  grfx.RotateTransform(rotateAngel);
       
                  int arrowLength = Convert.ToInt32((clock.Radius) * RelativeMinuteSize);
                  int tail = arrowLength / 4;
       
                  using (var pen = shadow ? new Pen(clock.Shadow) : new Pen(clock.Minute))
                  {
                      grfx.DrawLine(pen, 0, -arrowLength, 0, tail);
                  }
       
                  Restore(grfx);
              }
       
              protected override void DrawShadow(Clock clock, Graphics grfx)
              {
                  DrawHour(clock, grfx, true);
                  DrawMinute(clock, grfx, true);
                  DrawSecond(clock, grfx, true);
              }
          }
      }


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

    Наследуя классу ClockPainter мы легко можем изменять алгоритмы рисования, а если наследовать SimpleClockPainter, то можно модифицировать существующую отрисовку часов, переопределяя или дополняя существующую. Более того, реализовав несколько стратегий отрисовки (в примере не показано), мы можем дать возможность клиенту динамически менять ее по ходу выполнения программы.
    Теперь мы можем объявить в классе Clock соответствующую переменную и выбрать нужный класс для рисования.

    ExpandedWrap disabled
      protected readonly IClockPainter _painter = new SimpleClockPainter();


    Ниже приводится реализация OnPaint()

    ExpandedWrap disabled
              protected override void OnPaint(PaintEventArgs e)
              {
                  _painter.Draw(this, e.Graphics);
              }


    Все просто не правда ли? :)

    Осталось только испытать наши часы. Для этого создадим обычное WinForms приложение в нашем Solution добавим таймер и с частотой 100 мс (я люблю когда стрелочки двигаются плавно) будем устанавливать свойство Current нашего контрола в DateTime.Now

    Можете загрузить проект для самостоятельных эксперементов с ним
    http://ifolder.ru/7309260 (2008)
    http://ifolder.ru/7316730 (2005)
    Удачи.
      Архив для 2005-й студии удалили.
      0 пользователей читают эту тему (0 гостей и 0 скрытых пользователей)
      0 пользователей:


      Рейтинг@Mail.ru
      [ Script execution time: 0,0356 ]   [ 16 queries used ]   [ Generated: 26.04.24, 20:41 GMT ]