Writing a simple screenshot capture program

There are many different programs for capturing images from the screen, editing them "right on the screen" and uploading to various services. This is all good, but most programs are tied to certain services and do not allow downloading anywhere else. It has long been a thought in my head to create my own simple service for downloading pictures to fit my needs. And I want to share the history of the development of this program.

Without hesitation and having at hand Visual Studio 2015 of course created a new C # project since it is very convenient and I have already done small C # programs before.

Task one


Global interception of pressing the PrintScreen and Alt + PrintScreen buttons. In order not to reinvent the wheel, a couple of minutes of googling and almost immediately a solution was found . The bottom line is to use the LowLevelKeyboardProc callback function and the SetWindowsHookEx function with WH_KEYBOARD_LL of user32.dll. With a slight modification to intercepting two combinations, the code worked and successfully catches keystrokes.

Keystroke capture code
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);
    }
}


Task two


Actually capture a screenshot when you press the keys. Googling again and solution found . In this case, the GetForegroundWindow and GetWindowRect functions are all from the same user32.dll, as well as the standard .NET Graphics.CopyFromScreen function. A couple of checks and the code works, but with one problem - it also captures the borders of the window. I will return to the solution of this question a bit later.

Screenshot capture code

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;
    }
}


Task three


Saving the screenshot to the computer, everything was very simple, it was enough to use the Bitmap.Save function.


        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);
        }

Task four


Uploading a screenshot to the server, it seems like everything is simple, but it’s not quite right. After a little thought, a rather simple idea came to my mind - to upload a screenshot using WebClient in binary format using the “application / octet-stream” header and the WebClient.UploadData function, and on the server side to take data using file_get_contents (“php: // input” ) Actually, I did so, I wrote a very simple php script in a couple of lines and attached this whole thing to the program. Bottom line - saves and loads screenshots. Along with this, it was necessary to find a simple algorithm for generating short links, in total, googling a very simple and elegant way consisting in using Base36, taking the time in seconds (linux epoch) for int unix.


        // переводим 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 { }
            }
        }

Host php script



Screenshot Editing


Then I also wanted to somehow quickly edit screenshots and upload them to the server. Instead of inventing another image editor, a very simple idea was born - to make an “edit” button that opened paint with a captured screenshot (the last one that was saved to disk), and after editing it was possible to safely upload this file to the server.


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

Settings


Also, it was necessary to specify the site url somewhere and the default folder where to save screenshots, as a result, created a simple form of settings where it could be indicated. Well, in addition, I made the “open folder” button to make everything even easier and faster using the System.Diagnostics.Process.Start function. In addition, he quickly taught the program to minimize to tray.

So after all this, the first working prototype was ready , and it looked like this:


Search


Everything seems to be good, but it became clear what was missing. And the preview button was missing! It was somewhat uncomfortable to open a folder or click edit to just see what was captured from the screen before sending. As a result, I quickly sketched the preview form, there was a small problem with displaying a full-screen screenshot in the form (it is with frames), I didn’t want to delete the frame (I don’t even know why), as a result I made a scroll in the form and it completely suited me.


        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();
        }

Image format


In addition, there was also the need to save screenshots in different formats (and not just PNG as the default), since all this can be easily solved using the same Bitmap.Save function, though the quality of jpg images did not suit me. The ability to specify the quality of jpg was not so obvious, quick google is the solution . It is implemented using the additional parameter EncoderParameter to 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);
            }
        }

Also, the idea was born of automatically opening a folder after saving a screenshot, as well as automatically opening a link after loading. Quickly implemented this and added checkmarks to the settings. I also added the function of copying the link to the clipboard.

After adding a preview button, the program somehow began to look “wrong”, the button layout was scattered, thought a little, and rearranged the buttons, so the following came out:


Minor improvements


Having a little rest and thinking, I realized what was still missing - information about the last screenshot download. I made a corresponding field, when clicked it was possible to follow the link. In addition, he made the save / edit buttons inaccessible until you take a screenshot. Well, one more touch - added a button “about the program” with a brief description, version and date of the build (by the way, to get the date, I googled the solution again , getting the date from the title of the application itself).

Total after these actions came out the following:


A little later I realized that the last saved file was also not enough display, which I quickly added, and also made these fields more functional - by screwing up the context menu (by right-clicking) where it was possible to copy the link / path to the clipboard using Clipboard.SetText.

Program readiness, localization


Well, it seems that the main functionality was ready, everything worked, and I thought - can I share the program with the people? If you do this, then at least you need to make it possible to localize and add English. Fortunately, the studio makes it easy to implement all this with regular means, I began to translate this whole thing. Total result:


To translate some messages, it was necessary to create new resource files and then take lines from it as follows:


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

I have a file with the Russian language WinFormStrings.resx, for English WinFormStrings.en.resx, which I put in the Resources folder.

But in order to change the language a restart of the application was required, of course I wanted to be able to do without it, fortunately there is a solution to this issue, which I quickly applied. In addition to this, it was also necessary to obtain a list of supported languages ​​by the application (for the future, if there are suddenly more localizations), in total, google such a solution , combining all this to get the following construction:

Real-time language change code
        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;
        }


The problem of capturing borders at the window


And now I will return to the problem of capturing the borders of a window, this issue was first resolved using the automatic window trimming function (which I added to the settings), specifying the values ​​for windows 10, but it was more a crutch than a solution. To make it clearer what I’m talking about here is a screenshot of what I mean:


(screenshot from a newer version)

As can be seen in the screenshot - in addition to the window, its borders and what was under them captured. Googled for a long time how to solve this problem, but then came across this article , which actually described the solution to the problem, the bottom line is that on windows vista and newer you need to use dwmapi to get the correct window borders based on aero, etc. With a slight modification of its code, it was successfully bound to dwmapi and the problem was finally completely resolved. But since window cropping functionality has already been written, I decided to leave it, maybe someone will be useful.

    [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 support


Then, after thinking twice, since I am going to publish the program for everyone, it would probably be nice to make a download to some service besides uploading to my server, because then the program will be more useful, and you don’t need to have your own server to use it, etc. to. I have been using imgur.com for a long time and it has a simple api , I decided to bind to it. After sitting after studying it, the api first implemented an anonymous download, and a little later, the ability to bind an account. In addition, he implemented the ability to delete the last downloaded image in the program (for their service only).

I won’t completely describe the implementation code for their api, I’ll just say that I used HttpClient and MultipartFormDataContent from the .NET Framework 4.5 to upload images to imgur and at the same time I redid the image loading code to my server, instead of using binary submission I used full upload using a form to unify the code. Along the way, for my script I used the user-agent and $ _GET [key] keys as a method of identification, something didn’t want to mess with full authorization (although this is not difficult in theory).


        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 {
                ...
            }

The result was a fully functional and functional program, which could already do much more things than I planned to do initially.

The list of settings at that time looked like this:


Win XP compatible


After I started thinking about compatibility with Windows XP, it turned out that it only supports the .NET Framework 4.0, and MultipartFormDataContent is only available in v4.5, but you can still connect it in v4.0 by installing the System.Net.Http package. I did just that. And everything seems to be fine, except that on Windows Vista / 7 you need to install the .NET Framework 4.0 in order for the program to work. I switched the project to 3.5, rewrote uploading images to WebClient, and instead of uploading a file, I used an ordinary field with an encoded image in base64 format, since the api of imgur allows uploading images like that, and it was not difficult to rewrite my php script for this option. And then I also decided to switch the project to version 2.0, and as a result of a banal editing of a couple of lines I got a fully working .NET Framework 2.0 project.


            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"]);

This all made it possible to run the program on old frameworks, and on Windows Vista / 7 to run without installing anything, because according to this article, Windows Vista contains v2.0, and Windows 7 contains v3.5 by default. But the problems did not end there. On Windows 8 and newer, I started asking for the installation of the .NET Framework v3.5, which is certainly bad, but the issue was quickly resolved thanks to this information , by tweaking the supportedRuntime options in the config, allowing you to run the application on a new or old version without any problems. In addition, he made it possible to use the TLS 1.2 protocol if it is available (i.e. on systems with the .NET Framework 4.5).

app.config


TLS 1.2 support


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

Event history


By and large, I thought that everything, that's enough for that, could be released, but still something was still missing - the history of actions with the log. I started developing a corresponding window with some functions, like deleting a file from PC and imgur, opening a file / link, copying the path / link using the context menu. He also made it possible to save events to the log file both from the list and automatically setting in the settings.

A quite informative window came out:


HookCallback issue on Win XP


But one problem got out - on Windows XP, when capturing srkinshots, the record was added twice. During the tests, I found out that the HookCallback is called twice when the key is released, the reason for this behavior was not clear to me, but I solved the problem quite easily - I did an additional test of the keystroke saving it in a variable, and when releasing the key, changing the variable to false, in the end I need the code began to be processed only 1 time when releasing the key.


        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);
        }

Problem capturing screenshots from games


Later, during testing, I encountered the problem of capturing screenshots from full-screen applications (for example, games), noticed that in windows 10 a regular printscreen captures this business without problems, as a result, I added the function to paste the image from the clipboard, and also added a tick “use clipboard instead capture ”into the settings, thereby“ resolving the issue ”for myself, but as it turned out in win 7 and below, it doesn’t work, I began to study the issue, and realized that it was a rather difficult task, with the need to use directx injections, in the end I simply scored it the problem is, after all, the main goal is not to capture screenshots from games, for this there are many other programs and tools.

Along the way, adding settings, redid the settings menu, made it more compact to fit on a screen with a resolution of 640 * 480 pixels, and it began to look like this:


Also made the tray icon more functional, adding all the important functions there when right-clicking:


Check on Win98 and Win2000


Well, just for the sake of experiment, I deployed windows 2000 SP4 and 98 SE on the virtual machine, installed the .NET Framework 2.0 there. It was not so easy to do, because required the installation of some patches and update the Windows Installer. But still it worked out and I tried to run the application.

As it turned out on Windows 2000 SP4, the application turned out to be fully operational, but on Windows 98 SE the key capture didn’t work, pasting from the buffer didn’t work either, however loading a screenshot from a file worked without problems. Actually, these problems could not be resolved, there was very little information, all I could figure out was that the “WH_KEYBOARD_LL” parameter was added only in Windows 2000. I didn’t find any information about the reason for the broken image from the buffer. Total min requirements - Windows 2000.

So, after some checks, debugging and minor fixes, the program was finally ready, and the final version looks like this:


All that remains is to create a github repository, download the sources, compile the application, write a readme and make a release. This is where the development story ends. The finished program can be downloaded and see the source code on GitHub . Hope the article was helpful.

Also popular now: