БлогNot. C#: делаем полноэкранное приложение с анимацией без мерцания

C#: делаем полноэкранное приложение с анимацией без мерцания

Мы хотим, чтобы на полном экране выводилась анимация, например, перемещались частицы-окружности, а приложение работало по принципу заставки в Windows, то есть, завершалось при нажатии клавиши и клике мышью или же при достаточно заметном перемещении мыши.

Заметим, что немного повозившись с подключением к проекту DLL-библиотек Windows, на основе показанного подхода можно и создать и "настоящую" заставку-"хранитель экрана".

Создав визуальное приложение C# (Файл - Создать проект - Visual C# - Приложение Windows Forms), настроим свойства формы:

  • FormBorderStyle = None (отключаем стандартное обрамление окна),
  • WindowState = Maximized (запускаем окно в максимальном размере).

Часто рекомендуют ставить для формы опцию DoubleBufferd = True, чтобы бороться с мерцанием, но в нашем случае этого ничего особо не даст, можете ставить, можете нет. Ниже поясним, почему так.

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

вид окна в конструкторе форм
вид окна в конструкторе форм

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

//Файл Program.cs
using System;
using System.Windows.Forms;

namespace WindowsFormsApp1 {
 static class Program  {
  /// <summary>
  /// Главная точка входа для приложения.
  /// </summary>
  [STAThread]
  static void Main() {
   Application.EnableVisualStyles();
   Application.SetCompatibleTextRenderingDefault(false);
   foreach (Screen screen in Screen.AllScreens) { //Запустим на всех экранах
    Form1 myForm = new Form1 (screen.Bounds);
    myForm.Show ();
   }
   Application.Run ();
  }
 }
}

Выбрав в Обозревателе решений файл Form1.cs, обратимся к меню Вид - Конструктор и добавим в класс формы нужные нам данные:

using System;
using System.Drawing;
using System.Windows.Forms;

namespace WindowsFormsApp1 {
 public partial class Form1 : Form {

  //Добавлять будем сюда!

 }
}
  Bitmap myBitmap; //Рисунок для буферизации графики
  Graphics graph; //Графический контекст
  Pen pen; //Перо
  SolidBrush figure; //Кисть
  Random rnd; //Генератор случайных чисел
  Point OriginalLocation = new Point (int.MaxValue, int.MaxValue); //Позиция мыши
  const int number_of_particles = 50; //Количество частиц
  //Для простоты не будем создавать для частиц отдельный класс:
  int [] x = new int [number_of_particles]; //координаты X
  int [] y = new int [number_of_particles]; //координаты Y
  int [] radius = new int [number_of_particles]; //радиусы
  int [] dir = new int [number_of_particles]; //направления движения
  int xC, yC; //служебные, для вычисления краёв эллипса

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

public Form1 (Rectangle Bounds) {
   this.SetStyle(ControlStyles.AllPaintingInWmPaint | ControlStyles.UserPaint, true);
   this.SetStyle(ControlStyles.OptimizedDoubleBuffer, true);
   InitializeComponent();
   this.BackColor = Color.Black;
   this.Bounds = Bounds;
   Cursor.Hide();
}

Пара строк до инициализации компонент тоже служит "подстраховкой" в борьбе с мерцанием при перерисовке, хотя и их будет недостаточно :)

Также мы ставим форме чёрный фоновый цвет и края, соответствующие краям экрана (а обрамление окна мы уже отключили).

Последняя строка прячет курсор мыши. Результат - мы получим чёрный фон на весь экран.

Начнём обычным способом из окна "Свойства" кликами против выбранных событий формы создавать обработчики этих событий. По событию загрузки формы сделаем только то, что понадобится для обеспечения работы формы в целом:

private void Form1_Load(object sender, EventArgs e) {
   myBitmap = new Bitmap(this.Width, this.Height);
   graph = Graphics.FromImage(myBitmap);
   pen = new Pen(Color.Green);
   figure = new SolidBrush(Color.Transparent);
   rnd = new Random();
   for (int i = 0; i < number_of_particles; i++) {
    genCicleData(ref x[i], ref y[i], ref radius[i], ref dir[i]);
   }
   timer1.Interval = 150;
   timer1.Enabled = true;
}

Мы создали рисунок-Bitmap по размерам формы и получили из него графический контекст, с помощью которого будем рисовать.

Здесь же один раз мы создали перо, кисть, генератор случайных чисел и набор частиц функцией genCicleData.

Наконец, мы запустили таймер, который каждые 150 миллисекунд будет обновлять картинку.

Функцию genCicleData можно написать следом, она просто будет создавать случайные радиус, координаты и направление движения (одно из восьми) для частицы-окружности:

private void genCicleData(ref int x, ref int y, ref int radius, ref int dir) {
   radius = rnd.Next(3, 15);
   x = radius + rnd.Next(this.Width - 2 * radius);
   y = radius + rnd.Next(this.Height - 2 * radius);
   dir = rnd.Next(0, 8);
}

Главное будет происходить в обработчике события Paint, где рекомендуется помещать всю отрисовку и не загружать этот метод ничем лишним. Мы так и сделаем.

А реальный секрет избавления от мерцания состоит в том, что мы полностью отрисуем очередной кадр на нашем предварительно очищенном Bitmap myBitmap (из которого мы получили контекст рисования Graphics graph), а потом присвоим его фону формы.

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

private void Form1_Paint (object sender, PaintEventArgs e) {
   //Рисуем не битмапе:
   graph.Clear (Color.Black);
   for (int i = 0; i < number_of_particles; i++) {
    xC = x [i] - radius [i];
    yC = y [i] - radius [i];
    graph.FillEllipse (figure, xC, yC, radius [i], radius [i]);
    graph.DrawEllipse (pen, xC, yC, radius [i], radius [i]);
   }
   //Присваиваем изменённый битмап как фон формы:
   this.BackgroundImage = myBitmap;
}

Если анимация в приложении существенно сложнее нашей, могут понадобиться, конечно, и более сложные решения, ну либо вообще использование другого графического "движка" (вроде DirectX), а не .NET Framework, изначально мало предназначенного для разработки сложной графики.

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

Из других событий формы нам понадобится обработать движение мышки MouseMove, её клик Click и нажатие клавиши KeyDown.

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

private void Form1_MouseMove(object sender, MouseEventArgs e) {
   if (OriginalLocation.X == int.MaxValue & OriginalLocation.Y == int.MaxValue) {
    OriginalLocation = e.Location; //Установить положение
   }
   const int dist = 50;
   if (Math.Abs(e.X - OriginalLocation.X) > dist | Math.Abs(e.Y - OriginalLocation.Y) > dist) {
    Application.Exit(); //выйти, если сдвигались больше, чем на dist пикселей
   }
}
private void Form1_Click(object sender, EventArgs e) {
   Application.Exit();
}
private void Form1_KeyDown(object sender, KeyEventArgs e) {
   Application.Exit();
}

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

От таймера нам понадобится всего 2 действия, они закомментированы в листинге:

private void timer1_Tick(object sender, EventArgs e) {
   this.Invalidate(); //Запросить перерисовку графики методом Paint
   for (int i = 0; i < number_of_particles; i++) {
    //После перерисовки пересчитать координаты частиц
    changeCoords(ref x[i], ref y[i], ref radius[i], ref dir[i]);
   }
}

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

private void changeCoords (ref int x, ref int y, ref int radius, ref int dir) {
   const int step = 5; //шаг изменения координат
   switch (dir) {
   case 0:
    if (x < radius | y < radius) genCicleData(ref x, ref  y, ref radius, ref dir);
    else { x -= step; y -= step; }
    break;
   case 1:
    if (x < radius) genCicleData(ref x, ref y, ref radius, ref dir);
     else { x -= step; }
    break;
   case 2:
    if (x < radius | y > this.Height - radius) genCicleData(ref x, ref y, ref radius, ref dir);
    else { x -= step; y += step; }
    break;
   case 3:
    if (y > this.Height - radius) genCicleData(ref x, ref y, ref radius, ref dir);
    else { y += step; }
    break;
   case 4:
    if (x > this.Width - radius | y > this.Height - radius) genCicleData(ref x, ref y, ref radius, ref dir);
    else { x += step; y += step; }
    break;
   case 5:
    if (x > this.Width - radius) genCicleData(ref x, ref y, ref radius, ref dir);
    else { x += step; }
    break;
   case 6:
    if (x > this.Width - radius | y < radius) genCicleData(ref x, ref y, ref radius, ref dir);
    else { x += step; y -= step; }
    break;
   case 7:
    if (y < radius) genCicleData(ref x, ref y, ref radius, ref dir);
    else { y -= step; }
    break;
   }
   radius++;
}

Это всё, полноэкранное приложение с анимацией на C# можно собирать. Ниже показан уменьшенный скриншот программы в работе.

Приложение собиралось в Visual Studio 2015 и 2019, при открытии проекта должно настроиться на нужный профиль.

скриншот
скриншот

 Скачать этот проект Visual C# в архиве .zip, папка уже создана внутри архива (13 Кб)

09.04.2021, 12:26 [2322 просмотра]


теги: c# программирование графика

показать комментарии (1)