[Из песочницы] Пишем простую программу захвата скриншотов

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

Не долго думая и имея под рукой Visual Studio 2015 конечно же создал новый C# проект т.к. это очень удобно и я уже делал ранее небольшие C# программы.

Задача первая


Глобальный перехват нажатия кнопок PrintScreen и Alt+PrintScreen. Чтобы не изобретать велосипед, пару минут гугления и почти сразу нашлось решение. Суть заключается в использовании callback-функции LowLevelKeyboardProc и функции SetWindowsHookEx с WH_KEYBOARD_LL из user32.dll. С небольшой модификацией под перехват двух комбинаций код заработал и успешно ловит нажатия клавиш.
Код захвата нажатия клавиш
namespace ScreenShot_Grab
{
    static class Program
    {
        private static MainForm WinForm;
        /// 
        /// Главная точка входа для приложения.
        /// 

    [STAThread]
        static void Main()
        {
            Application.EnableVisualStyles();
            Application.SetCompatibleTextRenderingDefault(false);
            _hookID = SetHook(_proc);
            Application.Run(new MainForm());
            UnhookWindowsHookEx(_hookID);
        }

        private const int WH_KEYBOARD_LL = 13;
        //private const int WH_KEYBOARD_LL = 13;  
        private const int VK_F1 = 0x70;
        private static LowLevelKeyboardProc _proc = HookCallback;
        private static IntPtr _hookID = IntPtr.Zero;

        private static IntPtr SetHook(LowLevelKeyboardProc proc) {
            using (Process curProcess = Process.GetCurrentProcess())
            using (ProcessModule curModule = curProcess.MainModule) {
                return SetWindowsHookEx(WH_KEYBOARD_LL, proc, GetModuleHandle(curModule.ModuleName), 0);
            }
        }

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

        private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) {
            if (nCode >= 0) {
                Keys number = (Keys)Marshal.ReadInt32(lParam);
                //MessageBox.Show(number.ToString());
                if (number == Keys.PrintScreen) {
                    if (wParam == (IntPtr)261 && Keys.Alt == Control.ModifierKeys && number == Keys.PrintScreen) {
                        // Alt+PrintScreen
                    } else if (wParam == (IntPtr)257 && number == Keys.PrintScreen) {
                        // PrintScreen
                    }
                }
            }

            return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
        }

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr SetWindowsHookEx(int idHook, LowLevelKeyboardProc lpfn, IntPtr hMod, uint dwThreadId);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        [return: MarshalAs(UnmanagedType.Bool)]
        private static extern bool UnhookWindowsHookEx(IntPtr hhk);

        [DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, IntPtr lParam);

        [DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
        private static extern IntPtr GetModuleHandle(string lpModuleName);

    }

}


Задача вторая


Собственно захват скриншота при нажатии клавиш. Вновь гугление и решение найдено. В этом случае используются функции GetForegroundWindow и GetWindowRect всё из того же user32.dll, а также штатная функция .NET Graphics.CopyFromScreen. Пару проверок и код работает, но с одной проблемой — захватывает также границы окна. К решению этого вопроса вернусь чуть позже.
Код захвата скриншотов

class ScreenCapturer
{

    public enum CaptureMode
    {
        Screen,
        Window
    }

    [DllImport("user32.dll")]
    private static extern IntPtr GetForegroundWindow();

    [DllImport("user32.dll")]
    private static extern IntPtr GetWindowRect(IntPtr hWnd, ref Rect rect);
    
    [StructLayout(LayoutKind.Sequential)]
    public struct Rect
    {
        public int Left;
        public int Top;
        public int Right;
        public int Bottom;
    }

    public Bitmap Capture(CaptureMode screenCaptureMode = CaptureMode.Window)
    {
        Rectangle bounds;

        if (screenCaptureMode == CaptureMode.Screen)
        {
            bounds = Screen.GetBounds(Point.Empty);
            CursorPosition = Cursor.Position;
        }
        else
        {
            var handle = GetForegroundWindow();
            var rect = new Rect();
            GetWindowRect(handle, ref rect);

            bounds = new Rectangle(rect.Left, rect.Top, rect.Right, rect.Bottom);
            //CursorPosition = new Point(Cursor.Position.X - rect.Left, Cursor.Position.Y - rect.Top);
        }

        var result = new Bitmap(bounds.Width, bounds.Height);

        using (var g = Graphics.FromImage(result))
        {
            g.CopyFromScreen(new Point(bounds.Left, bounds.Top), Point.Empty, bounds.Size);
        }

        return result;
    }

    public Point CursorPosition
    {
        get;
        protected set;
    }
}


Задача третья


Сохранения скриншота на компьютер, тут всё очень просто достаточно было использовать функцию Bitmap.Save.

        private void save_Click(object sender, EventArgs e)
        {
            if (lastres == null) { return; }
            // генерируем имя с помощью base36
            Int32 unixTimestamp = (Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds;
            var FileName = base_convert(unixTimestamp.ToString(), 10, 36);
            lastres.Save(spath + FileName);
        }

Задача четвёртая


Загрузка скриншота на сервер, тут вроде кажется, что всё просто, но это не совсем так. После небольшого размышления пришла в голову довольно простая идея — загружать скриншот при помощи WebClient в бинарном формате используя заголовок «application/octet-stream» и функцию WebClient.UploadData, а на стороне сервера брать данные с помощью file_get_contents («php://input»). Собственно так и поступил, написал очень простой php скрипт в пару строк и привязал всё это дело к программе. Итог — скриншоты сохраняет и загружает. Вместе с этим надо было найти простой алгоритм генерации коротких ссылок, итого нагуглил очень простой и элегантный способ заключающийся в использовании Base36, взяв за int unix время в секундах (linux epoch).

        // переводим bitmap в byte[]
        private Byte[] BitmapToArray(Bitmap bitmap)
        {
            if (bitmap == null) return null;
            using (MemoryStream stream = new MemoryStream()) {
                bitmap.Save(stream, ImgFormat[Properties.Settings.Default.format]);
                return stream.ToArray();
            }
        }

        private void upload_Click(object sender, EventArgs e)
        {
            using (var client = new WebClient()) {
                client.Headers.Add("Content-Type", "application/octet-stream");
                try {
                    var response = client.UploadData(svurl, BitmapToArray(lastres);
                    var result = Encoding.UTF8.GetString(response);
                    if (result.StartsWith("http")) {
                        System.Diagnostics.Process.Start(result);
                    }
                } catch { }
            }
        }

Принимающий PHP-скрипт



Редактирование скриншотов


Далее захотелось также как-то быстро редактировать скриншоты и загружать их на сервер. Вместо изобретения очередного редактора изображений родилась очень простая идея — сделать кнопку «редактировать» которая открывала paint с захваченным скриншотом (последним что сохранил на диск), а после редактирования можно было спокойно загрузить этот файл на сервер.

        private void edit_Click(object sender, EventArgs e)
        {
            if (lastres == null) return;
            if (lastfile == "") save_Click(sender, e);
            Process.Start("mspaint.exe", "\"" + lastfile + "\"");
        }

Настройки


Также надо было где-то указывать url сайта и папку по умолчанию куда сохранять скриншоты, в итоге создал простенькую форму настроек где это можно было указать. Ну и вдобавок сделал кнопку «открыть папку» чтобы всё было ещё проще и быстрее с помощью функции System.Diagnostics.Process.Start. Кроме этого быстро научил программу сворачиваться в трей.

Итак после всего этого был готов первый рабочий прототип, и выглядел он так:

97ad83c4b6c24e97b2d578bebbc3ee5c.png

Предосмотр


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

        private void PreviewForm_Load(object sender, EventArgs e)
        {
            if (form1.lastfile!="") {
                img.Image = Image.FromFile(form1.lastfile);
            } else {
                img.Image = form1.lastres;
            }
            ClientSize = new Size(img.Image.Width + 10, img.Image.Height + 10);
            img.Width = img.Image.Width+10;
            img.Height = img.Image.Height+10;
            if (img.Image.Width >= Screen.PrimaryScreen.Bounds.Width || img.Image.Height >= Screen.PrimaryScreen.Bounds.Height) {
                WindowState = FormWindowState.Maximized;
            }
            CenterToScreen();
        }

Формат изображений


Кроме этого появилась также необходимость сохранения скриншотов в разных форматах (а не только PNG как по умолчанию), благо всё это легко решается с помощью всё той же функции Bitmap.Save, правда вот качество jpg изображений меня не устроило. Возможность указать качество у jpg было не так очевидно, быстрое гугление и есть решение. Реализуется с помощью доп параметра EncoderParameter к Bitmap.Save.

        // получаем энкодер по формату
        private ImageCodecInfo GetEncoder(ImageFormat format)
        {
            ImageCodecInfo[] codecs = ImageCodecInfo.GetImageDecoders();
            foreach (ImageCodecInfo codec in codecs) {
                if (codec.FormatID == format.Guid) {
                    return codec;
                }
            }
            return null;
        }

        internal void SaveFile(string FilePath, ImageFormat format)
        {
            var curimg = lastres;
            if (format == ImageFormat.Jpeg) {
                System.Drawing.Imaging.Encoder myEncoder = System.Drawing.Imaging.Encoder.Quality;
                ImageCodecInfo Encoder = GetEncoder(format);
                EncoderParameters myEncoderParameters = new EncoderParameters(1);
                myEncoderParameters.Param[0] = new EncoderParameter(myEncoder, Properties.Settings.Default.quality);
                curimg.Save(stream, Encoder, myEncoderParameters);
            } else {
                curimg.Save(FilePath, format);
            }
        }

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

После добавления кнопки предосмотр, программа как-то стала выглядеть «не так», расположение кнопок было разбросано, подумал немного, и попереставлял кнопки, так что вышло следующее:

6f1ebbe309874f2082bb0b231f098749.jpg

Мелкие доработки


Немного передохнув и подумав, понял чего ещё не хватает — информации о последней загрузке скриншота. Сделал соответствующее поле, при нажатии на которое можно было перейти по ссылке. Помимо этого сделал кнопки сохранения/редактирования недоступными пока не сделаешь скриншот. Ну и ещё один штрих — добавил кнопку «о программе» с кратким описанием, версией и датой билда (кстати для получения даты опять нагуглил решение, получая дату с заголовка самого приложения).

Итого после этих действий вышло следующее:

be2ec8b00ccf4d4e8db994c31977cfb8.png

Чуть позже понял что также не хватает отображения последнего сохранённого файла, что быстро добавил, а ещё сделал эти поля более функциональными — прикрутив контекстное меню (при щелчке правой кнопкой мышью) где можно было скопировать ссылку/путь в буфер обмена при помощи Clipboard.SetText.

Готовность программы, локализация


Ну и вроде бы основной функционал был готов, всё работало, и подумал я — может поделиться программой с народом? Если делать это, тогда нужно как минимум сделать возможность локализации и добавить английский язык. Благо студия легко позволяет всё это реализовать штатными средствами, начал я всё это дело переводить. Итого получилось:
e1c6f8c90a8e4c3c9927d577337b61cf.png

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

    internal ResourceManager LocM = new ResourceManager("ScreenShot_Grab.Resources.WinFormStrings", typeof(MainForm).Assembly);
    
    LocM.GetString("key_name");

Файл с русским языком у меня WinFormStrings.resx, для английского WinFormStrings.en.resx, которые положил в папку Resources.

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

Код смены языка в реальном времени
        private void ChangeLanguage(string lang)
        {
            foreach (Form frm in Application.OpenForms) {
                localizeForm(frm);
            }
        }

        private void localizeForm(Form frm)
        {
            var manager = new ComponentResourceManager(frm.GetType());
            manager.ApplyResources(frm, "$this");
            applyResources(manager, frm.Controls);
        }

        private void applyResources(ComponentResourceManager manager, Control.ControlCollection ctls)
        {
            foreach (Control ctl in ctls) {
                manager.ApplyResources(ctl, ctl.Name);
                Debug.WriteLine(ctl.Name);
                applyResources(manager, ctl.Controls);
            }
        }
        
        private void language_SelectedIndexChanged(object sender, EventArgs e)
        {
            var lang = ((ComboboxItem)language.SelectedItem).Value;
            if (Properties.Settings.Default.language == lang) return;
            UpdateLang(lang);
        }
        
        private void UpdateLang(string lang)
        {
            Thread.CurrentThread.CurrentUICulture = new CultureInfo(lang);
            ChangeLanguage(lang);
            Properties.Settings.Default.language = lang;
            Properties.Settings.Default.Save();
            form1.OnLangChange();
        }
        
        private void Form2_Load(object sender, EventArgs e)
        {
            language.Items.Clear();
            foreach (CultureInfo item in GetSupportedCulture()) {
                var lc = item.TwoLetterISOLanguageName;
                var citem = new ComboboxItem(item.NativeName, lc);
                //Debug.WriteLine(item.NativeName);
                // Задаём для дефолтного языка свой код и заголовок в списке
                if (item.Name == CultureInfo.InvariantCulture.Name) {
                    lc = "ru";
                    citem = new ComboboxItem("Русский", lc);
                }
                language.Items.Add(citem);
                if (Properties.Settings.Default.language == lc) {
                    language.SelectedItem = citem;
                }
            }
        }
        
        private IList GetSupportedCulture()
        {
            //Get all culture 
            CultureInfo[] culture = CultureInfo.GetCultures(CultureTypes.AllCultures);

            //Find the location where application installed.
            string exeLocation = Path.GetDirectoryName(Uri.UnescapeDataString(new UriBuilder(Assembly.GetExecutingAssembly().CodeBase).Path));

            //Return all culture for which satellite folder found with culture code.
            IList cultures = new List();
            foreach(var cultureInfo in culture) {
                if (Directory.Exists(Path.Combine(exeLocation, cultureInfo.Name))) {
                    cultures.Add(cultureInfo);
                }
            }
            return cultures;
        }


Проблема захвата границ у окна


А теперь я вернусь к проблеме захвата границ окна, этот вопрос сначала был решён с помощью функции автоматической обрезки окна (которую я добавил в настройки), указав значения для windows 10, но это был скорее костыль чем решение. Чтобы было понятнее о чём речь вот скриншот того что я имею ввиду:
f8b93338381b43028f66bfffe5008aa6.png

(скриншот с более новой версии)

Как видно на скриншоте — кроме окна захватывало его границы и то что под ними. Довольно долго гуглил как решить эту проблему, но потом наткнулся на эту статью, где собственно описывалось решение вопроса, суть заключается в том что на windows vista и новее нужно использовать dwmapi для получения корректных границ окна с учётом aero и тд. С небольшой модификацией своего кода успешно привязал к dwmapi и проблема наконец была полностью решена. Но т.к. функционал обрезки окна уже был написан, решил оставить его, возможно кому-то будет полезен.

    [DllImport(@"dwmapi.dll")]
    private static extern int DwmGetWindowAttribute(IntPtr hwnd, int dwAttribute, out Rect pvAttribute, int cbAttribute);
    
    public Bitmap Capture(CaptureMode screenCaptureMode = CaptureMode.Window, bool cutborder = true)
    {
            ...
            var handle = GetForegroundWindow();
            var rect = new Rect();
            // Если Win XP и ранее то используем старый способ
            if (Environment.OSVersion.Version.Major < 6) {
                GetWindowRect(handle, ref rect);
            } else {
                var res = -1;
                try {
                    res = DwmGetWindowAttribute(handle, 9, out rect, Marshal.SizeOf(typeof(Rect)));
                } catch { }
                if (res<0) GetWindowRect(handle, ref rect);
            }
            ...

Поддержка imgur


Потом ещё подумав, раз я собираюсь публиковать программу для всех, то наверное было бы неплохо кроме загрузки на свой сервер сделать загрузку на какой-то сервис, ведь тогда программа будет более полезной, и не нужно иметь обязательно свой сервер для её использования, т.к. я давно использую imgur.com и у него есть простой api, то решил сделать привязку к нему. Посидев поизучав его api сначала реализовал анонимную загрузку, а чуть позже и возможность привязки аккаунта. Кроме этого реализовал возможность удаления последнего загруженного изображения в программе (для их сервиса только).

Полностью описывать код реализации их api я не буду, скажу лишь что для загрузки изображений на imgur использовал HttpClient и MultipartFormDataContent из .NET Framework 4.5 и при этом я переделал код загрузки изображений на свой сервер, вместо бинарной отправки использовал полноценную загрузку с помощью формы чтобы унифицировать код. Попутно для своего скрипта как способ идентификации использовал user-agent и $_GET[key] ключ, что-то не захотелось возиться с полноценной авторизацией (хотя это по идее не сложно).


        private void uploadfile(bool bitmap = true)
        {
            byte[] data;
            if (bitmap && !imgedit) {
                data = BitmapToArray(lastres);
            } else {
                if (!File.Exists(lastfile)) {
                    MessageBox.Show(LocM.GetString("file_nf"), LocM.GetString("error"), MessageBoxButtons.OK, MessageBoxIcon.Error);
                    return;
                }
                data = File.ReadAllBytes(lastfile);
            }
            HttpContent bytesContent = new ByteArrayContent(data);
            using (var client = new HttpClient())
            using (var formData = new MultipartFormDataContent()) {
            
                ...

                formData.Add(bytesContent, "image", "image");
                try {
                    var response = client.PostAsync(url, formData).Result;

                    if (!response.IsSuccessStatusCode) {
                        MessageBox.Show(response.ReasonPhrase, LocM.GetString("error"), MessageBoxButtons.OK, MessageBoxIcon.Exclamation);
                        lastlabel.Text = LocM.GetString("error");
                        lastlabel.Enabled = false;
                    } else {
                ...
            }

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

Список настроек на тот момент выглядел так:

95a065bba713494685924dc5f766237b.png

Совместимость с Win XP


После я стал думать о совместимости с Windows XP, в итоге оказалось что она поддерживает лишь .NET Framework 4.0, а MultipartFormDataContent доступен лишь в v4.5, но её можно по прежнему подключить в v4.0 установив пакет System.Net.Http. По началу я так и сделал. И вроде всё хорошо, кроме того что на Windows Vista/7 нужно устанавливать .NET Framework 4.0 для того чтобы программа заработала. Переключил проект на 3.5, переписал загрузку изображений на WebClient, и вместо загрузки файла использовал обычное поле с закодированным изображением в формате base64, благо api у imgur позволяет так загружать изображения, да и переписать свой php скрипт не составило труда под этот вариант. А потом решил также переключить проект на версию 2.0, и в итоге банальной правкой пары строк получил полностью рабочий .NET Framework 2.0 проект.

            using (var client = new WebClient()) {
                var pdata = new NameValueCollection();
                
                ...
                
                pdata.Add("image", Convert.ToBase64String(data));

                try {
                    var response = client.UploadValues(url, "POST", pdata);
                    var result = Encoding.UTF8.GetString(response);
                
                ...


    $file = base64_decode($_POST["image"]);

Это всё позволило запускать программу на старых фреймворках, а на Windows Vista/7 запускать без установки чего либо, т.к. согласно этой статье Windows Vista содержит v2.0, а Windows 7 содержит v3.5 по умолчанию. Но на этом проблемы не закончились. На Windows 8 и новее начало просить установку .NET Framework v3.5, что конечно плохо, но вопрос был быстро решил благодаря этой информации, подправив опции supportedRuntime в конфиге, позволяя запускать приложение на новой или старой версии без каких либо проблем. Кроме этого сделал возможность использования протокола TLS 1.2 если он доступен (т.е на системах с .NET Framework 4.5).

app.config

    
        
        
    

Поддержка TLS 1.2

            System.Net.ServicePointManager.SecurityProtocol |= SecurityProtocolType.Tls;
            try {
                System.Net.ServicePointManager.SecurityProtocol |= (SecurityProtocolType)3072; //SecurityProtocolType.Tls12;
            } catch { }

История событий


По большому счёту думал что всё, на этом хватит, можно релизить, но всёже по прежнему чего-то не хватало — истории действий с логом. Начал разработку соответствующего окна с некоторыми функциями, как удаления файла с пк и imgur, открытие файла/ссылки, копирования пути/ссылки с помощью контекстного меню. Также сделал возможность сохранить события в лог файл как из списка, так и автоматически задав в настройках.

Вышло вполне информативное окно:

bffc6f7d274548c1ab4ca6630161c2f7.png

Проблема в работе HookCallback на Win XP


Но вылезла одна проблема — на Windows XP при захвате сркиншотов запись добавлялась дважды. В ходе тестов выяснил, что HookCallback вызывается дважды при отпускании клавиши, причина такого поведения мне была не ясна, но решил вопрос довольно легко — сделал дополнительную проверку нажатия клавиши сохраняя это в переменную, а при отпускании клавиши изменение переменной на false, в итоге нужный мне код стал обрабатываться лишь 1 раз при отпускании клавиши.

        private static bool pressed = false;

        private static IntPtr HookCallback(int nCode, IntPtr wParam, IntPtr lParam) {
            if (nCode >= 0) {
                Keys number = (Keys)Marshal.ReadInt32(lParam);
                //MessageBox.Show(number.ToString());
                if (number == Keys.PrintScreen) {
                    if (pressed && wParam == (IntPtr)261 && Keys.Alt == Control.ModifierKeys && number == Keys.PrintScreen) {
                        var res = Scr.Capture(ScreenCapturer.CaptureMode.Window, Properties.Settings.Default.cutborder);
                        WinForm.OnGrabScreen(res, false, true);
                        pressed = false;
                    } else if (pressed && wParam == (IntPtr)257 && number == Keys.PrintScreen) {
                        var res = Scr.Capture(ScreenCapturer.CaptureMode.Screen);
                        WinForm.OnGrabScreen(res);
                        pressed = false;
                    } else if (wParam == (IntPtr)256 || wParam == (IntPtr)260) {
                        pressed = true; // fix for win xp double press
                    }
                }
            }

            return CallNextHookEx(IntPtr.Zero, nCode, wParam, lParam);
        }

Проблема захвата скриншотов из игр


Чуть позже в ходе тестирования столкнулся с проблемой захвата скриншотов из полноэкранных приложений (например игры), заметил что в windows 10 штатный printscreen захватывает это дело без проблем, в итоге добавил функцию вставки изображения из буфера обмена, а также добавил галочку «использовать буфер обмена вместо захвата» в настройки, тем самым «решил вопрос» для себя, но как оказалось в win 7 и ниже это не работает, начал изучать вопрос, и понял что это довольно сложная задача, с необходимостью использования directx инъекций, в итоге попросту забил на эту проблему, всё-таки основная цель не захват скриншотов из игр, для этого существует множество других программ и инструментов.

Попутно добавив настройки переделал меню настроек, сделал его более компактным чтобы вмещалось на экран с разрешением 640×480 пикселей, и оно стало выглядеть так:

fb453813c8964edbad1ce91ecd7d7c7c.png

Также сделал более функциональным иконку в трее, добавив туда все важные функции при щелчке правой кнопкой:
48f17b623a4049fbbc5b6cabbc0d3074.png

Проверка на Win98 и Win2000


Ну и уже чисто ради эксперимента развернул на виртуалке windows 2000 SP4 и 98 SE, поставил там .NET Framework 2.0. Это было сделать не так просто, т.к. требовалась установка некоторых патчей и обновить Windows Installer. Но всё же всё получилось и я попробовал запустить приложение.

Как оказалось на Windows 2000 SP4 приложение оказалось полностью рабочим, а вот на Windows 98 SE захват клавиш не работал, вставка из буфера тоже не работает, однако загрузка скриншота из файла работает без проблем. Собственно эти проблемы решить не получилось, информации крайне мало, всё что смог выяснить — параметр «WH_KEYBOARD_LL» добавили лишь в Windows 2000. А о причине не работающей вставки изображения из буфера вообще не нашёл никакой информации. Итого мин требования — Windows 2000.

Итак после некоторых проверок, дебага и мелких фиксов программа была наконец готова, и финальный вариант выглядит так:

90b04987a01a4dc59b27f10b613b56fb.png

Всё что осталось — создать github репозиторий, загрузить исходники, скомпилить приложение, написать ридми и сделать релиз. На этом история разработки заканчивается. Готовую программу можно скачать и посмотреть исходный код на GitHub. Надеюсь статья была полезной.

Комментарии (4)

  • 24 ноября 2016 в 08:57

    0

    Код в формах.

    С одной стороны — хорошая программа, неплохая практика и прочая.
    С другой — обычная лапша в формах, не то, что радует глаз =)

  • 24 ноября 2016 в 09:09

    0

    Проверка на Win98 и Win2000
    Это имело какой-то практический смысл, или сделано просто из любопытства? Про поддержку ХР понимаю, таких ещё относительно много, W2k ещё более-менее понимаю, знаю несколько крупных организаций с серверами на этой ОС, работающими до сих пор, но W9x всё же слишком глубокая археология, на мой взгляд.
  • 24 ноября 2016 в 09:52

    0

    Бросается в глаза слово «Screenshoot».
    С одной стороны — это как бы ошибка в написании слова «Screenshot», с другой — интересное обыгрывание фразы «Screen Shoot». Возможно…

    Однако в Readme.MD у вас там это слово неправильно написано не только в названии (где уместно написать такой «брэнд»), но и в тексте… Думаю, стоит исправить.

  • 24 ноября 2016 в 10:21

    0

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

© Habrahabr.ru