Всплывающее меню для Windows Phone Classic

Все мы видели контекстные меню в Windows Mobile. Несмотря на то, что они более-менее функциональные, работать с ними зачастую не очень удобно. В то же время общие тренды говорят о том, что всплывающее меню должно занимать весь экран и позволять работать пальцами. Такие меню уже есть в других мобильных платформах (например, Android). В Windows Phone 7 также реализуются крупные меню для приложений. Давайте попробуем сделать удобное с точки зрения использование контекстное меню для Windows Phone Classic. Для того, чтобы было понятно что именно мы будем делать в этой статье, я приведу несколько снимков экранов того, что должно получиться в результате.

Т.о. при отображении меню общий фон затемняется и поверх него появляется всплывающее меню с ориентированными под палец кнопками. Давайте попробуем сделать такую реализацию.

Для достижения поставленной цели мы будем придерживаться следующего порядка действий:

  1. При попытке отобразить всплывающее меню предварительно делаем снимок экрана;
  2. Слегка затемняем получившийся снимок;
  3. Создаем новую форму, фон которой будет содержать затемненный список;
  4. Рисуем на этой форме прямоугольник нужных размеров, а также разделители элементов (горизонтальные линии);
  5. Добавляем на форму нужно количество кнопок;
  6. Отображаем форму в полноэкранном режиме;
  7. При нажатии на какую-либо из кнопок закрываем форму.

Как видим, последовательность действий простая. Понятно, что затемнение фона в данном случае – это только иллюзия (например, если будет изменяться время, то в «затемненном» варианте оно не изменится), но это работает достаточно успешно.

Итак, приступим. Для реализации описанной логики создадим новый класс, унаследованный от базового класса Form. Этот класс будет реализовывать всю логику нового всплывающего меню. Реализуем в этом объекте также интерфейс IDisposable (поскольку дочерние элементы Bitmap, которые нам пригодятся требует освобождения памяти).

public class PopupForm : Form, IDisposable
{
  // ..
}

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

public class ButtonDescription
{
  public ButtonDescription()
  {
    Checked = false;
    CanBeChecked = false;
    Icon = null;
  }

  public string Name { get; set; }
  public string Title { get; set; }
  public bool CanBeChecked { get; set; }
  public bool Checked { get; set; }
  public Bitmap Icon { get; set; }
}

Теперь при создании нашего всплывающего меню в конструкторе передадим коллекцию таких кнопок и сохраним эту коллекцию в приватном поле. Также установим в конструкторе стиль окна.

public PopupForm(ButtonDescription[] buttons)
{
  WindowState = FormWindowState.Maximized;
  FormBorderStyle = FormBorderStyle.None;

  _buttons = buttons;
}

Следующим шагом необходимо получить снимок экрана и затемнить его. Для получения снимка воспользуемся функцией BitBlt из системной библиотеки coredll.dll. Для затемнения изображения воспользуемся функцией AlphaBlend из той же библиотеки. Я не буду подробно рассматривать последовательность действий для этого шага. Всю последовательность действий иллюстрирует метод, код которого приведен ниже.

[DllImport("coredll.dll")]
extern public static Int32 AlphaBlend(IntPtr hdcDest, Int32 xDest, 
                Int32 yDest, Int32 cxDest, Int32 cyDest, 
                IntPtr hdcSrc, Int32 xSrc, Int32 ySrc, 
                Int32 cxSrc, Int32 cySrc, 
                BlendFunction blendFunction);

[DllImport("coredll.dll")]
public static extern bool BitBlt(IntPtr hObject, int nXDest, 
                int nYDest, int nWidth,
                int nHeight, IntPtr hObjSource, int nXSrc, 
                int nYSrc, TernaryRasterOperations dwRop);


[DllImport("coredll.dll")]
public static extern IntPtr GetDC(IntPtr hwnd);

[DllImport("coredll.dll")]
public static extern int ReleaseDC(IntPtr hwnd, IntPtr hdc);

public static Bitmap GenerateSnapshot(byte alpha)
{
  Rectangle rect = Screen.PrimaryScreen.Bounds;
  var result = new Bitmap(rect.Width, rect.Height);

  var deviceContext = PlatformAPIs.GetDC(IntPtr.Zero);

  try
  {
    using (Graphics resultGx = Graphics.FromImage(result))
    {
      var screenHdc = deviceContext;
      var resultHdc = resultGx.GetHdc();

      PlatformAPIs.BitBlt(screenHdc, 0, 0, 
        rect.Width, rect.Height, deviceContext, 0, 0, 
        TernaryRasterOperations.SRCCOPY);

      if (alpha <= 255)
      {
        PlatformAPIs.AlphaBlend(resultHdc, 0, 0, 
            result.Width, result.Height, screenHdc, 
            rect.Left, rect.Top, rect.Width, rect.Height,
                    new BlendFunction
                    {
                        BlendOp = 0,
                        BlendFlags = 0,
                        SourceConstantAlpha = alpha,
                        AlphaFormat = 0
                    });
      }

      resultGx.ReleaseHdc(resultHdc);
    }
  }
  finally
  {
    PlatformAPIs.ReleaseDC(IntPtr.Zero, deviceContext);
  }

  return result;
}

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

private Image _formSnapshot;

public new void Show()
{
  ShowMenu();
}

public new void ShowDialog()
{
  ShowMenu();
}

public string ShowMenu()
{
  UpdateBackground(PlatformAPIs.GenerateSnapshot(true));

  // ...
}

private void UpdateBackground(Image formSnapshot)
{
  if (_formSnapshot != null)
  {
    _formSnapshot.Dispose();
  }

  _formSnapshot = formSnapshot;

  // ...
}

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

private void UpdateBackground(Image formSnapshot)
{
  // ..

  RefreshFormBackground();
}

private void RefreshFormBackground()
{
  // удаляем старое изображение, если есть
  if (_backgroundImage != null)
  {
    _backgroundImage.Dispose();
    _backgroundImage = null;
  }


  // подсчитываем количество кнопок и размер экрана
  var itemsCount = _buttons.Length;
  var currentSize = Screen.PrimaryScreen.Bounds;

  // создаем чистое изображения для фона
  var backgroundImage = new Bitmap(currentSize.Width, currentSize.Height);

  // генерируем фон
  using (var backgroundImageGraphics = Graphics.FromImage(backgroundImage))
  {
    // наносим скриншот экрана, если есть
    if (_formSnapshot != null)
    {
      backgroundImageGraphics.DrawImage(_formSnapshot, 0, 0);
    }

    // наносим фон меню
    using (var menuBackgroundImage = GenerateMenuBackground(itemsCount, 
                                      ItemWidth, ItemHeight, itemsCount))
    {
      backgroundImageGraphics.DrawImage(menuBackgroundImage, 
                              currentSize.Width / 2 - menuBackgroundImage.Width / 2, 
                              currentSize.Height / 2 - menuBackgroundImage.Height / 2);
  }
  }

  // сохраняем результат
  _backgroundImage = backgroundImage;
}

private static Bitmap GenerateMenuBackground(int itemsCount, int itemWidth, 
                                             int itemHeight, int borderRadius)
{
  // чистое изображение
  var result = new Bitmap(itemWidth, itemHeight * itemsCount);

  using (var gx = Graphics.FromImage(result))
  {
    // наносим прямоугольник со скругленными краям
    gx.DrawRoundedRectangle(Color.White, Color.White, 
      new Rectangle(0, 0, itemWidth, itemHeight * itemsCount), 
      new Size(borderRadius, borderRadius));

    // наносим разделители
    var dividerPen = new Pen(Color.Silver);

    for (int i = 1; i < itemsCount; i++)
    {
      gx.DrawLine(dividerPen, borderRadius, itemHeight * i, 
                  itemWidth - borderRadius, itemHeight * i);
    }
  }

  return result;
}

Как видно, мы используем метод DrawRoundedRectangle для генерации прямоугольника со скругленными краями. На самом деле это – метод расширения для класса Graphics, разработанный Алексом Яхниным в его библиотеке UI Framework. Теперь при отрисовке фона формы будем использовать заранее подготовленное изображение.

protected override void OnPaintBackground(PaintEventArgs e)
{
  if (_backgroundImage != null)
  {
    e.Graphics.DrawImage(_backgroundImage, 0, 0);
  }
  else
  {
    base.OnPaintBackground(e);
  }
}

Теперь у нас есть форма, которая делает снимок экрана, затемняет его и выводит на экран форму с прямоугольником. Осталось только добавить кнопки на эту форму. Все кнопки у нас будут графическими. Поэтому стандартные кнопки нам не подходят (по вашему вкусу здесь вы можете добавить свои элементы управления). Для простоты создадим свои кнопки, которые имеют несколько изображений для разных состояний. Для этого создадим класс, унаследованный от элемента управления Control, который принимает в конструкторе текст и другие параметры и генерирует три изображения (обычное состояние, нажатое состояние, отмеченное состояние). Эти изображения будут использоваться в момент отрисовки элемента управления. Дальше обработам события OnMouseUp и OnMouseDown и будем переключать состояние, управляющее выбором одного из трех изображений. Я не буду приводить полный исходный код этого элемента управления (чтобы не загромождать пространство). При желании вы можете посмотреть его в приложенных исходных кодах.

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

private void RefreshButtonControls()
{
  // очистка старых элементов управления
  var oldControls = Controls.OfType<IDisposable>().ToArray();
  Controls.Clear();
  foreach (IDisposable control in oldControls)
  {
    control.Dispose();
  }

  // создание элементов управления и размещение на форме
  var itemsCount = _buttons.Length;
  int dx = Screen.PrimaryScreen.Bounds.Width / 2 
                    - ItemWidth / 2;
  int dy = Screen.PrimaryScreen.Bounds.Height / 2 
                    - (ItemHeight * itemsCount) / 2;

  for (int i = 0; i < itemsCount; i++)
  {
    var buttonInfo = _buttons[i];
    var btn = new PopupButton(_buttons[i].Name, buttonInfo.Title, 
                            MenuFont, ItemWidth - 10, ItemHeight - 10, 
                            Color.Black, Color.White, PressedColor, CheckedColor, 
                            buttonInfo.CanBeChecked, buttonInfo.Checked, 
                            buttonInfo.Icon) 
                            { Left = dx + 5, Top = i * ItemHeight + 5 + dy };
    btn.Click += delegate(object sender, EventArgs e)
    {
      foreach (PopupButton control in 
                Controls.OfType<PopupButton>())
      {
        if (control != sender)
        {
          control.IsChecked = false;

        }
      }

      // сохраняем результат
      Result = ((PopupButton)sender).Name;

      // закрываем форму
      Close();
    };
    Controls.Add(btn);
  }
}

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

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