Сайт разработчика Александр Климова

/* Моя кошка замечательно разбирается в программировании. Стоит мне объяснить проблему ей - и все становится ясно. */
John Robbins, Debugging Applications, Microsoft Press, 2000

Программа-шутка. Хэллоуин: кровь на мониторе

Мы с вами попытаемся создать приложение, которое заставит монитор истекать кровью. Использовать эту программу можно на компьютере жертвы во время праздника Хэллоуин.

Готовую программу необходимо будет скопировать заранее на компьютер друга и установить дату и время срабатывания. В назначенный час на мониторе выбранной жерты появится стекающая кровь, после чего появится картинка какого-нибудь монстра. Все это безобразие будет сопровождаться криком. Это должно напугать жертву, который любит отмечать праздник Хэллоуина.

Подготовка

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

Установите у формы следующие свойства:

BackColor – Fuchsia
FormBorderStyle – None
DoubleBuffered – True
Text - <empty string>
WindowState – Maximized
ShowInTaskbar – False
TransparencyKey – Fuchsia
TopMost - True

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

Первая кровь

Создадим класс BloodDrop, который будет отвечать за кровь, стекающую по экрану. Этот эффект реализован при помощи заранне подготовленных картинок, сохраненных как ресурсы программы. Вы можете увидеть эти картинки в папке images. Чтобы включить картинки в ресурсы программы, необходимо установить у каждой картинки в свойстве Build Action значение Embedded Resource.

Картинки, которые хранятся как ресурсы, мы можем загружать в конструкторе BloodDrop следующим образом:

public BloodDrop()
{
    // load each animation frame from the app's resources and add it to our frame list
    foreach(string file in _files)
    {
        Image img = Image.FromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream("Halloween.images." + file));
        _frames.Add(img);
    }

    // set our first frame
    _image = _frames[0];
}

Метод FromStream объекта Image позволяет загружать файл с изображением из ресурсов. Для этого нам необходимо знать имя ресурса и имя файла. В нашем случае это Halloween.images.<filename>. Мы загружаем каждый файл и сохраняем его в списке List объекта Image.

Рисование

Для гладкой анимации мы сделаем следующее. Мы будем использовать событие Tick для перерисовки экрана, при этом будет срабатывать событие OnPaint. Для плавности анимации мы установим значение свойства DoubleBuffered в True.

Для анимации крови используется таймер с интервалом 100 миллисекунд, который просто вызывает метод Invalidate. При этом срабатывает переопределенное событие OnPaint. Внутри обработчика OnPaint мы создаем новый объект BloodDrop и устанавливаем в случайном порядке горизонтальную позицию и скорость стекания капельки крови. Это создаст интересный эффект на экране монитора. Новый созданный объект BloodDrop добавляется в список List типа BloodDrop. Наконец, мы перечисляем список капель крови для анимации и вывода на экран.

// create a new blood drop
BloodDrop drop = new BloodDrop();

// set it to a random location on the screen
drop.Location = new Point(_random.Next(SystemInformation.PrimaryMonitorSize.Width), 0);

// set it's velocity to a random number
drop.Velocity = _random.Next(40) + 20;

// add it to our drawing list
_dropList.Add(drop);

// enumerate the drawing list
foreach(BloodDrop bd in _dropList)
{    
    // animate and then draw the drop            
    bd.Animate();
    bd.Draw(g);
}

При запуске программы, создается экземпляр класс Random, который сохраняется в переменной _random . Метод Next использует параметр для максимального случайного значения числа. Для этого параметра мы будем использовать ширину экрана, которую можно определить с помощью объекта SystemInformation. Для скорости стекания капли будем использовать значение 40 пикселов и добавочные 20 пикселов, чтобы случайное число не было очень маленьким.

Само рисование капли очень просто. Метод Draw класса BloodDrop выглядит следующим образом:

// draw the drop
public void Draw(Graphics g)
{
    // if we're in "drop" mode, draw the stream behind it
    if(_index >= 4)
        g.DrawImage(_frames[4], this._location.X + 24, 0, _frames[4].Width, this._location.Y + 10);

    // if we're beyond the bottom of the screen, don't bother drawing the drop
    if(this._location.Y < SystemInformation.PrimaryMonitorSize.Height)
        g.DrawImage(_image, this._location.X, this._location.Y, _image.Width, _image.Height);
}

Мы используем объект Graphics и используем метод DrawImage для рисования нужного кадра в нужной позиции. Чтобы не перегружать процессор компьютера, мы сначала определяем, когда капля достигнет нижнего края экрана и больше ее не рисуем. Размер экрана определяется при помощи метода PrimaryMonitorSize класса SystemInformation.

Чтобы не возникало искажений и мерцаний картинки, нужно установить следующие настройки для объекта Graphics перед выводом в OnPaint.

protected override void OnPaint(PaintEventArgs e)
{
    ...
    // grab the Graphics object for this app and set some properties for correct drawing
    Graphics g = e.Graphics;
    g.PixelOffsetMode = PixelOffsetMode.Half;
    g.InterpolationMode = InterpolationMode.NearestNeighbor;
    g.CompositingQuality = CompositingQuality.HighSpeed;
    ...
}

Финальная часть

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

Нам понадобится создать PictureBox на лету и загрузить в нее картинку. Затем объект добавляется в коллекцию Controls формы и выводится на экран. Дополнительно, мы создаем два обработчика событий, которые закрывают приложение, когда пользователь нажимает на клавишу или кнопку мыши.

Следует заметить, что звук также загружается из ресурсов и проигрывается через SoundPlay:

// disable the animation timer
tmrAnim.Enabled = false;

// create a Picturebox
PictureBox pb = new PictureBox();

// pull in the scary image from our resources
pb.Image = Image.FromStream(Assembly.GetExecutingAssembly().GetManifestResourceStream("Halloween.images.scary.jpg"));

// set it to fill the screen
pb.SizeMode = PictureBoxSizeMode.Zoom;
pb.Width = SystemInformation.PrimaryMonitorSize.Width;
pb.Height = SystemInformation.PrimaryMonitorSize.Height;
pb.BackColor = Color.Black;

// if it gets clicked, or someone presses a key, we want to end the app
pb.Click += new EventHandler(pb_Click);
this.KeyDown += new KeyEventHandler(MainForm_KeyDown);

// add the Picturebox to the controls list
this.Controls.Add(pb);

// play a scary sound
SoundPlayer sp = new SoundPlayer(Assembly.GetExecutingAssembly().GetManifestResourceStream("Halloween.scream.wav"));
sp.Play();

Планировщик

Теперь нам необходимо установить программу на компьютере жертвы и выбрать дату и время запуска. Для этого создадим простое окно конфигурации, которое показывает первое время запуска приложения. С помощью этой настройки можно будет выбрать нужную дату и время для кровавой анимации. Эти настройки сохраняются при помощи объекта Settings. Таким образом, можно скопировать программу в папку Автозагрузка или прописать запуск в реестре, чтобы в дальнейшем программа сама могла запуститься.

Для настроек заходим в Project | Properties | выбираем вкладку Settings и устанавливаем новое значение:

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

 
// called once per minute to check whether it's time to run the show
private void tmrScheduler_Tick(object sender, EventArgs e)
{
    // if the current time is greater than the time set by the user
    if(DateTime.Now >= Properties.Settings.Default.Time)
    {
        // re-maximize the window
        this.WindowState = FormWindowState.Maximized;

        // disable this timer
        tmrScheduler.Enabled = false;

        // enable the animation timer
        tmrAnim.Enabled = true;
    }
}

Распространение

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

Для показа окна настроек нужно вызвать программу с параметром -config

Заключение

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

Перевод: Василий Котов

Скачать пример