вентилятор
Хорошего настроения!

Пишем Скриншотер на C#



Всем привет! Мне давно была нужна программа скриншотер. Раньше приходилось нажимать на кнопку PrtSc, вставлять изображение экрана в Paint и там выбирать нужный фрагмент.


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


Поэтому сегодня мы начнём создавать собственный сриншотер на языке программирования C#.


Что должна будет уметь наша программа? После запуска она должна прятаться в трее. После нажатия горячих клавиш она должна активизироваться, и пользователь может выделить мышкой часть экрана, которая будет сохранена в буфер обмена. После этого программа снова "засыпает" в трее.





Приступим. Открываем Visual Studio, создаём проект Windows Forms (.NET Framework).


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


Для главной формы настроим свойства: FormBorderStyle: None (убираем границы у окна), WindowState: Maximized (разворачиваем окно на весь экран), Opacity: 0 (делаем окно прозрачным), ShowInTaskbar: False (делаем, чтобы форма не отображалась на панели задач), Cursor: Cross.


Интересный момент: если свойство Opacity равно нулю, то окна как-будто вооще нет, но если мы поставим хотя бы 1% непрозрачности, то форма "заслонит" экран и не даст взаимодействовать мышке с элементами экрана.


При нажатии Ctrl + Q наша программа должна активизироваться. Этого можно достигнуть с помощью хуков.





Добавим нужные функции и переменные в начале класса Form1:


private static Form1 _instance;

// ... { GLOBAL HOOK }
[DllImport("user32.dll")]
static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc callback, IntPtr hInstance, uint threadId);

[DllImport("user32.dll")]
static extern bool UnhookWindowsHookEx(IntPtr hInstance);

[DllImport("user32.dll")]
static extern IntPtr CallNextHookEx(IntPtr idHook, int nCode, int wParam, IntPtr lParam);

[DllImport("kernel32.dll")]
static extern IntPtr LoadLibrary(string lpFileName);

private delegate IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, IntPtr lParam);

const int WH_KEYBOARD_LL = 13; // Номер глобального LowLevel-хука на клавиатуру
const int WM_KEYDOWN = 0x100; // Сообщения нажатия клавиши

private LowLevelKeyboardProc _proc = hookProc;

private static IntPtr hhook = IntPtr.Zero;

static bool ctrlPressed = false;

Переменная _instance поможет нам обратится из статичного класса к самой форме. Далее идут функции, которые позволяют отловить нажатие клавиш, когда программа будет не в фокусе (не в активном режиме). Чтобы они нормально подключились, нужно прописать "using System.Runtime.InteropServices;" в самом начале. Переменная ctrlPressed это просто флаг. Если нажата клавиша Ctrl, то значение должно быть истинным. Но после нажатия Ctrl, значение True этой переменной мы сделаем 0,5 секунд. Если за это время не будет нажата вторая клавиша Q, значит, активизация программы не сработает. Так мы хотим отловить "одновременное" нажатие Ctrl + Q. Функция hookProc будет написана ниже.





Сделаем, чтобы переменная _instance ссылалась на объект формы.


Ссылаемся на объект формы

Пропишем далее в классе формы:


Функции хука

Функции хука 2

В функции hookProc выполняется логика: если нажат левый Ctrl (код 162), то запускается поток (функция Wait), который через пол секунды снова установит флаг ctrlPressed в false. Если опять сработала эта функция, и код клавиши уже 81 (буква Q), и при этом флаг ctrlPressed ещё будет true, то мы считаем, что нажаты клавиши для активации скриншотера. Тогда форма становится в фокусе, непрозрачность устанавливается в 1%, тем самым застилает весь экран, ожидая прямоугольной области от пользователя.





Функция Wait().


Функция ожидания

Перейдём на окно События (иконка в виде молнии).


Иконка События

Найдём событие для формы Load (загрузка формы) и кликнем два раза по ячейке справа. Пропишем в обработчике события:


Устанавливаем хук

Аналогично подключим событие FormClosing (закрытие формы).


Убираем хук

При тестировании программы на данном этапе иногда сложно закрыть её, если мы решили активировать наш скриншотер. Т.к. программа блокирует взаимодействие с экраном и рамки у самой формы тоже нет. Можно пользоваться Ctrl +F4.


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


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




Найдём функцию в интернете, которая делает скриншот экрана. Первая найденная функция https://myrusakov.ru/csharp-create-screenshot.html не принесла результатов. Мой экран имеет увеличение 125% в Windows 10. И данная функция не захватывает низ экрана :(


Масштабирование Windows

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

class NativeUtilities
{
     [Flags()]
     public enum DisplayDeviceStateFlags : int
     {
         // The device is part of the desktop.
         AttachedToDesktop = 0x1,
         MultiDriver = 0x2,
         // This is the primary display.
         PrimaryDevice = 0x4,
         // Represents a pseudo device used to mirror application drawing for remoting or other purposes.
         MirroringDriver = 0x8,
         // The device is VGA compatible.
         VGACompatible = 0x16,
         // The device is removable; it cannot be the primary display
         Removable = 0x20,
         // The device has more display modes than its output devices support.
         ModesPruned = 0x8000000,
         Remote = 0x4000000,
         Disconnect = 0x2000000
     }

     [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
     public struct DisplayDevice
     {
         [MarshalAs(UnmanagedType.U4)]
         public int cb;
         [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
         public string DeviceName;
         [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
         public string DeviceString;
         [MarshalAs(UnmanagedType.U4)]
         public DisplayDeviceStateFlags StateFlags;
         [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
         public string DeviceID;
         [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
         public string DeviceKey;
     }

     [StructLayout(LayoutKind.Sequential)]
     public struct DEVMODE
     {
         private const int CCHDEVICENAME = 0x20;
         private const int CCHFORMNAME = 0x20;
         [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
         public string dmDeviceName;
         public short dmSpecVersion;
         public short dmDriverVersion;
         public short dmSize;
         public short dmDriverExtra;
         public int dmFields;
         public int dmPositionX;
         public int dmPositionY;
         public ScreenOrientation dmDisplayOrientation;
         public int dmDisplayFixedOutput;
         public short dmColor;
         public short dmDuplex;
         public short dmYResolution;
         public short dmTTOption;
         public short dmCollate;
         [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 0x20)]
         public string dmFormName;
         public short dmLogPixels;
         public int dmBitsPerPel;
         public int dmPelsWidth;
         public int dmPelsHeight;
         public int dmDisplayFlags;
         public int dmDisplayFrequency;
         public int dmICMMethod;
         public int dmICMIntent;
         public int dmMediaType;
         public int dmDitherType;
         public int dmReserved1;
         public int dmReserved2;
         public int dmPanningWidth;
         public int dmPanningHeight;
     }

     [DllImport("user32.dll")]
     public static extern bool EnumDisplaySettings(string deviceName, int modeNum, ref DEVMODE devMode);

     public const int ENUM_CURRENT_SETTINGS = -1;
     const int ENUM_REGISTRY_SETTINGS = -2;

     [DllImport("User32.dll")]
     public static extern int EnumDisplayDevices(string lpDevice, int iDevNum, ref DisplayDevice lpDisplayDevice, int dwFlags);
 }


Теперь добавим саму функцию для создания скриншотов. Её разместим в классе формы.


public static Bitmap ScreenCapture()
{
     // Initialize the virtual screen to dummy values
     int screenLeft = int.MaxValue;
     int screenTop = int.MaxValue;
     int screenRight = int.MinValue;
     int screenBottom = int.MinValue;

     Bitmap bmp=null;

     // Enumerate system display devices
     int deviceIndex = 0;
     while (true)
     {
          NativeUtilities.DisplayDevice deviceData = new NativeUtilities.DisplayDevice { cb = Marshal.SizeOf(typeof(NativeUtilities.DisplayDevice)) };
          if (NativeUtilities.EnumDisplayDevices(null, deviceIndex, ref deviceData, 0) != 0)
          {
               // Get the position and size of this particular display device
               NativeUtilities.DEVMODE devMode = new NativeUtilities.DEVMODE();
               if (NativeUtilities.EnumDisplaySettings(deviceData.DeviceName, NativeUtilities.ENUM_CURRENT_SETTINGS, ref devMode))
               {
                    // Update the virtual screen dimensions
                    screenLeft = Math.Min(screenLeft, devMode.dmPositionX);
                    screenTop = Math.Min(screenTop, devMode.dmPositionY);
                    screenRight = Math.Max(screenRight, devMode.dmPositionX + devMode.dmPelsWidth);
                    screenBottom = Math.Max(screenBottom, devMode.dmPositionY + devMode.dmPelsHeight);
               }
               deviceIndex++;
          }
          else
               break;
     }

     // Create a bitmap of the appropriate size to receive the screen-shot.
     bmp = new Bitmap(screenRight - screenLeft, screenBottom - screenTop);

     Graphics g = Graphics.FromImage(bmp);
               
     g.InterpolationMode = InterpolationMode.NearestNeighbor;
     g.DrawImage(bmp, new Rectangle(Point.Empty, bmp.Size));
     g.CopyFromScreen(screenLeft, screenTop, 0, 0, bmp.Size);
                
     return bmp;       
}

Создадим переменные в начале класса формы:


Заводим переменные

Переменная ScreenShot будет хранить изображение всего экрана. Переменная LocPress - координаты нажатия левой клавиши мыши, LocUp - координаты отпускания левой клавиши мыши, LocNow - координаты текущего положения мыши. Переменная RectangleFlag показывает, создаёт ли пользователь в данный момент прямоугольную область, которую хочет заскриншотить. Если да, то эта переменная будет равна true, если нет, то flase.





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


В обозревателе решений кликним правой кнопкой мыши по названию проекта. Выберем Добавить-> Форма -> Добавить.


Обозреватель решений

Для второй формы установим свойства: BackColor: НоtTrak (цвет прямоугольной области), FormBorderStyle: None (убираем рамку), ShowInTaskbar: false (скрываем вторую форму в панеле задач).


Создадим объект newForm второй формы в самом начале класса Form1.

Form2 newForm = new Form2();

Добавим обработчик события MouseDown (нажатие клавиши мыши) для главной формы:


Обработчик события mousedown

Это начало выделение прямоугольной области пользователем. Получаем координаты нажатия клавиши мыши в переменную LocPress. Показываем воторую форму. Размещаем её левый верхний угол в точке, где было нажатие. Устанавливаем пока нулевой размер второй формы. Делаем её полупрозрачной. Флаг того, что началось выделение, устанавливаем в true.





Для главной формы создадим событие MouseMove (Перемещение мыши).


private void Form1_MouseMove(object sender, MouseEventArgs e)
{
     LocNow = e.Location;

     if (RectangleFlag)
     {
          newForm.Size = new Size((int)(LocNow.X - LocPress.X), (int)(LocNow.Y - LocPress.Y));

          Graphics gra = newForm.CreateGraphics();

          gra.Clear(newForm.BackColor);

          // Create a new pen.
          Pen skyPen = new Pen(ChangeColorBrightness(newForm.BackColor, 0.5f));

          // Set the pen's width.
          skyPen.Width = 4.0F;

          // Set the LineJoin property.
          skyPen.LineJoin = System.Drawing.Drawing2D.LineJoin.Bevel;

          // Draw a rectangle.
          gra.DrawRectangle(skyPen, new Rectangle(0, 0, (int)(LocNow.X - LocPress.X), (int)(LocNow.Y - LocPress.Y)));

          //Dispose of the pen.
          skyPen.Dispose();
     }
}

В этой функции получаем координаты мыши при перемещении. Нас интересует только тот момент, когда пользователь зажал левую кнопку мыши, т.е. когда происходит выделение прямоугольной области. Изменяем размеры формы. Её правый нижний угол должен тянуться за мышкой, тем самым получается прямоугольная область. Эту роль как раз и играет вторая форма newForm. С помощью инструмента рисования Pen обводим форму newForm по периметру цветом, который близкий основному цвету newForm.BackColor. Чтобы найти близкий цвет к newForm.BackColor воспользуемся функцией ChangeColorBrightness. Её нагуглили в интернете.





Функция изменения яркости

Добавим самое важное событие MouseUp к главной форме.


private void Form1_MouseUp(object sender, MouseEventArgs e)
{
     LocUp = e.Location;

     double r = 1;

     newForm.Opacity = 0;
     this.Opacity = 0;

     ScreenShot = ScreenCapture();

     newForm.Opacity = 0.3;
     this.Opacity = 0.01;

     Rectangle bounds = Screen.GetBounds(Point.Empty);

     r = (double)ScreenShot.Width / (double)bounds.Width;

     // Clone a portion of the Bitmap object.
     RectangleF cloneRect = new RectangleF((int)(LocPress.X * r), (int)(LocPress.Y * r), (int)(LocUp.X * r - LocPress.X * r), (int)(LocUp.Y * r - LocPress.Y * r));
     PixelFormat format = ScreenShot.PixelFormat;

     try
     {
          Bitmap cloneBitmap = ScreenShot.Clone(cloneRect, format);
          Clipboard.SetImage(cloneBitmap);
     }
     catch
     {

     }

     RectangleFlag = false;
  
     Thread.Sleep(500);
 
     newForm.Hide();
     this.Opacity = 0;
     this.ActiveControl = null;
}

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


Считываются координаты мыши в LocUp. Переменная r - это коэффициент, который помогает учесть масштабирование экрана. Если бы не было масштабирования, то ScreenShot.Width и bounds.Width были бы одинаковые.


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


Точка нажатия кнопки мыши — это начало прямоугольника. Высота и ширина прямоугольника легко вычисляется (Ширина = LocUp.X - LocPress.X ; Высота = LocUp.Y - LocPress.X).





Копирование прямоугольника с картинки ScreenShot осуществляем с помощью функции .Clone(). Функция .SetImage помогает занести этот фрагмент экрана в буфер обмена.


Задержка в пол секунды сделана для того, чтобы выделенная область ещё немного повисела на экране. После этого программа опять переходит в спящий режим.


Чтобы нашу программу можно было закрыть и настроить, расположим её в трее. Кинем на главную форму элемент notifyIcon1. Для этого элемента свойство Visible: true. Добавим в ресурсы проекта иконку для этого элемента. Переходим Обозреватель решений->Свойства (Проекта)->Ресурсы. Выбираем в верхнем выпадающем меню Значки.


Возле кнопки Добавить ресурс нажимаем чёрный треугольник и выбираем "добавить существующий файл". Можно выбрать изображение формата .ico с разрешением 150 на 150 пикселей.


Теперь можно обратится в коде к этому ресурсу. Напишем в классе главной формы внутри функции public Form1() после инициализации:

notifyIcon1.Icon = Properties.Resources.s;

s - это название иконки.


Добавим для элемента notifyIcon1 контекстное меню. Кинем элемент contextMenuStrip1 на главную форму. Добавим в свойство ContextMenuStrip для notifyIcon1 имя контекстного меню. Установим свойство Text: "Скриншотер".


Добавим две позиции в контекстное меню.

Контекстное меню

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





Для выхода пропишем:


Выход из программы

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


Окно настроек

Пропишем свойства формы настроек. FormBorderStyle: FixedSingle (делаем, чтобы окно нельзя было растягивать), Text: "Настройки скриншотера", StartPosition: CenterScreen, MaximizeBox: False (запрещаем разворачивать окно).


Элемент label1. Свойство Font: 14 пт, Text: "Цвет выделения:". Синий прямоугольник (индикатор цвета) - это кнопка (button1). Для неё Text убираем. Свойство BackColor: HotTrack, UseVisualStyleBackColor: False.


Элемент checkBox1. Мы также сделаем звуковой эффект при создании скриншота. С помощью этого элемента, звуковой эффект можно будет отключить в настройках. Для него Font: 14 пт, Text: "Звук", Checked: True.


Последний элемент на третьей форме это кнопка (button1).Свойство Text: "ОК".


Создадим объект третьей формы в самом начале главной формы.

Form3 FormSitting = new Form3();

Вернёмся к пункту Настройки в контекстном меню для элемента notifyIcon1. При нажатии на этот пункт меню, сделаем видимой форму FormSitting.


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

Кинем так же элемент colorDialog1 на форму настроек. Перейдём в код для формы настроек, нажав F7. Заведём переменную в начале класса Form3, которая будет отвечать за цвет выделения:

public Color BC;



Для элемента button1 добавим обработчик события Click.


Настройка colorDialog

Здесь в переменной BC будет цвет, который выберет пользователь с помощью colorDialog. Этим цветом мы должны окрасить вторую форму newForm, которая олицетворяет собой прямоугольную область.


Для кнопки button2 (форма настроек) тоже добавим обработчик события Click.


Скрываем форму настроек

При нажатии кнопки "ОК", форма настроек должна исчезнуть.





Добавим в класс Form3 функцию:


Убираем кнопку закрыть для Form3

Эта функция блокируем кнопку "Закрыть" в правом верхнем углу у формы настроек. Мы будем форму скрывать и показывать. Если её закрыть, может случится авария в программе.


Вернёмся к коду главной формы. Окрасим вторую форму в цвет, который указал пользователь. В функции Form1_MouseDown в блоке try пропишем:


Устанавливаем цвет выделения

В функции Form1_MouseUp после блока catch добавим условие, чтобы добавить звуковой эффект.


Делаем звуковой эффект

Мы должны в ресурсы проекта (аналогично, как добавляли иконку), добавить звуковой файл формата .wav. У меня называется этот файл zvuk2. Чтобы обратится в классе главной формы к элементы другой формы checkBox1, в файле Form3.Designer.cs (его можно найти в Обозревателе решений) исправим:


Исправляем файл Form3.Designer.cs

На этом всё! При запуске программы она будет прятаться в трее. Чтобы сделать скриншот, нужно нажать Ctrl + Q. Выделять прямоугольную область пользователь должен с левого верхнего угла. Если кликнуть по программе в трее, можно зайти в настройки или выйти. Стоит отметить, что в некоторых программах данное сочетание клавиш тоже является горячими клавишами. Тогда они буду срабатывать и в той, и в нашей программе. Поэтому можно доделать выбор для пользователя горячих клавиш в настройках.


Внизу Вы можете скачать саму программу и пользоваться данным скриншотером. Я там всё-таки добавил функцию выбора горячих клавиш. Можете купить проект, который описывался в данной статье за 600 499 рублей. Удачи!


Купить проект данной программы

Стоимость: 600 499 рублей.

Скачать программу скриншотер, полученную в этом проекте


Скачать усовершенствованную программу ScreenShoter





09-10-2022 в 16:42:23





Поддержать сайт:


Похожая статья:

Пишем программу будильник на C#

Привет! Сегодня напишем программу будильник на C#....

Категория: C#  Подкатегория: -
Дата: 19-08-2022 в 16:35:54 0


Комментарии:

VS выдает ошибку "System. OutOf MemoryException: "Недостаточно памяти." на строку: Bitmap cloneBitmap = ScreenShot.Clone(cloneRect, format);
Илья 19-12-2022 в 17:16:34

Я бы обработчик клавиш изменил, примерно так: Добавляем функцию: [DllImport("user32.dll")] static extern short GetKeyState(int nVirtKey); Допфункция: static bool IsKeyDown(Keys keys) { return (GetKeyState((int)keys) & 0x8000) == 0x8000; } И главный хук: public static IntPtr hookProc(int code, IntPtr wParam, IntPtr lParam) { if (code == 0) { bool ctrl = IsKeyDown(Keys.LControlKey) || IsKeyDown(Keys.RControlKey); bool shift = IsKeyDown(Keys.LShiftKey) || IsKeyDown(Keys.RShiftKey); bool alt = IsKeyDown(Keys.Alt); int vkCode = Marshal.ReadInt32(lParam); if (vkCode == (int)Keys.Q && ctrl && shift) { _instance.Focus(); _instance.Opacity = 0.01; Point point = Cursor.Position; Cursor.Position = point; } } return CallNextHookEx(hhook, code, (int)wParam, lParam);
Александр 31-01-2023 в 07:18:41

Спасибо, изучим!
Калужский Александр 31-01-2023 в 07:31:58



Оставить коментарий:



Напишите email, чтобы получать сообщения о новых комментариях (необязательно):


Задача против робота. Расположите картинки горизонтально:




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