Meshbeyn / C#

Как программно делать снимки экрана (скриншоты)

Тема снятия скриншотов из своей программы довольно часто всплывает на программистских форумах. Цели бывают разные: кому-то действительно нужен просто снимок экрана, кто-то хочет следить за появлением определенного значка, кто-то хочет передавать картинку по сети на другую машину. Довольно часто нужен не весь экран, а только определенная часть. Например, перед обновлением сложного контрола делают его снимок и потом несколько секунд создают новый, показывая картинку старого. Вне зависимости от цели, начальный шаг всегда один: вот у меня перед носом содержимое экрана и я хочу скопировать его в свой блок памяти для обработки. Благодаря библиотеке Windows Forms эта операция выполняется на удивление просто.

В этом тьюториале мы реализуем очень простой проект, который при этом может быть еще и полезным.

Цель проекта

Для многих людей, которые долго и уныло работают с разными документами, требуется система учета времени. Например, менеджеру или фрилансеру всегда нужно знать сколько времени затрачено на отдельные проекты. Если ведется одновременная работа с несколькими проектами, заводится специальная табличка, куда заносится время работы над тем или иным проектом. По этой табличке потом можно выставить оплату за выполненую работу или спланировать временные затраты для следующих проектов. Эта простая технология отлично работает, пока не вмешивается проклятый склероз. Переключаясь с проекта на проект, мы часто забываем отметить это в табличке. А после рабочего дня восстановить часы работы уже нереально. Это создает лишний стресс: стараясь не забыть отметиться, мы отвлекаем мозг от решаемой задачи. Чтобы подстраховаться, мы можем переложить слежку за своей работой на компьютер: для него это совсем не трудно. Большинству пользователей достаточно иметь в конце дня периодические скриншоты - раз в 10-15 минут. Их будет не слишком много, и время работы можно установить достаточно точно. Мы ведь не будем подсчитывать время работы над каждым проектом с точностью до секунды. Кванта в 15 минут уже более чем достаточно.

Необходимый функционал

План реализации

Снятие скриншотов - операция недолгая и производить мы ее будем очень нечасто, так что для ее реализации нам достаточно иметь обычный таймер из WinForms. При срабатывании таймера мы рисуем содержимое экрана на картинку и сохраняем ее в памяти вместе с отметкой времени. Чтобы не возиться с файлами, картинки будут храниться в памяти. Но растры занимают очень много места и к концу дня наша программа отожрет себе уже очень хороший кусок памяти. Поэтому картинки мы будем хранить в сжатых файлах PNG (для скриншотов это самый подходящий формат), но в памяти. Для реализации этой функциональности нам вообще не нужен GUI - просто будет делаться скриншот каждые 10 минут.

Для просмотра скриншотов достаточно одного окна в разметке "браузер": слева будет список скриншотов по времени, а справа - показ выделенного скриншота.

Хранимые данные

Для представления одного скриншота мы создадим класс ScreenShot. Хранить мы в нем будем отметку времени создания скриншота и поток для хранения сжатого файла. Оба свойства извне доступны только для чтения. В конструктор передается картинка, которая сжимается в формате PNG и хранится в потоке в памяти. Также конструктор определяет текущее время. В нашем случае это допустимо. Когда у нас запрашивают поток с картинкой, мы перематываем его в начало перед возвращением.

class ScreenShot
{
    public ScreenShot(System.Drawing.Bitmap BMP)
    {
        Time = DateTime.Now;
        file = new MemoryStream();
        BMP.Save(file, ImageFormat.Png);
    }

    public DateTime Time { get; private set; }

    MemoryStream file;
    public MemoryStream File
    {
        get
        {
            file.Position = 0;
            return file;
        }
    }
}

Реализация показа скриншотов

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

  1. Кидаем на форму SplitContainer и устанавливаем его свойство Dock в значение Fill, чтобы он всегда занимал всю площадь окна.
  2. В левую часть контейнера кидаем ListBox, называем его lstShots и также устанавливаем ему Dock. Этот список будет отображать все скриншоты и показывать их время. Поэтому установим ему свойство DisplayMember в значение "Time", а FormatString в значение "G".
  3. В правую часть контейнера кидаем PictureBox, называем его picPicture и устанавливаем координаты (Location) в 0,0. Также устанавливаем свойство SizeMode в AutoSize, чтобы он подгонял свой размер под размер картинки.
  4. Выделяем правую часть контейнера (splitContainer1.Panel2) и устанавливаем свойство Autoscroll в true. Картинка всегда будет больше доступного места, поэтому полосы прокрутки понадобятся.
  5. Делаем двойной щелчок по ListBox и реализуем событие смены выделения:
    private void lstShots_SelectedIndexChanged(object sender, EventArgs e)
    {
        picPicture.Image = new Bitmap((lstShots.SelectedItem as ScreenShot).File);
    }

Реализация снятия скриншотов

В нашем примере мы снимаем скриншот всего содержимого главного экрана. У большинства пользователей подключен только один монитор, поэтому мы ограничимся главным экраном. При срабатывании таймера мы создаем картинку соответствующего размера и получаем ее графический контекст. Этот самый контекст умеет рисовать на картинке содержимое буфера экрана в памяти. Основные параметры там определяют положение копируемого прямоугольника на экране и в картинке и размер прямоугольника. Мы копируем все, поэтому указываем верхний левый угол и размер экрана. Затем создается новый объект ScreenShot, который и будет представлять данный снимок. Созданный объект мы просто добавляем в ListBox.

Добавляем на форму невидимый компонент Timer. Устанавливаем ему свойства Enabled = true и Interval = 600000. Таймер начнет работать сразу же при запуске программы и будет срабатывать каждые 10 минут (600000 мс). Делаем двойной щелчок по таймеру и реализуем событие срабатывания таймера:

private void timer1_Tick(object sender, EventArgs e)
{
    int W = Screen.PrimaryScreen.Bounds.Width, H = Screen.PrimaryScreen.Bounds.Height;
    using (Bitmap BMP = new Bitmap(W, H))
    {
        using (Graphics G = Graphics.FromImage(BMP))
            G.CopyFromScreen(0, 0, 0, 0, new System.Drawing.Size(W, H));
        lstShots.Items.Add(new ScreenShot(BMP));
    }
}

Результат

В результате пятиминутной работы у нас получилась уже вполне полезная программа. Разумеется, теперь Ваша задача - развивать ее дальше. Вариантов море:

Примечания

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