📜 ⬆️ ⬇️

We write 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. It's all good, but most programs are tied to certain services and do not give the ability to download somewhere else. In my head the thought has long been spinning to create my own simple service for downloading images to fit your 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 earlier small C # programs.

First task


Global interception of pressing the buttons PrintScreen and Alt + PrintScreen. 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 from user32.dll. With a small modification for intercepting two combinations, the code earned and successfully captures keystrokes.

Keystroke Capture Code
namespace ScreenShot_Grab { static class Program { private static MainForm WinForm; /// <summary> ///     . /// </summary> [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 the screenshot when you press the keys. Again google and the solution is found . In this case, the GetForegroundWindow and GetWindowRect functions are all from the same user32.dll, as well as the regular .NET Graphics.CopyFromScreen function. A couple of checks and the code works, but with one problem - it also captures the borders of the window. To address this issue will come back 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; } } 


Third task


Saving the screenshot on the computer, everything is very simple 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 that everything is simple, but it is not quite so. After a little reflection, a rather simple idea came up - loading a screenshot using WebClient in binary format using the “application / octet-stream” header and the WebClient.UploadData function, and taking data on the server side using file_get_contents (“php: // input” ). Actually, I did, wrote a very simple php script in a couple of lines and tied the whole thing to the program. The result - the screenshots saves and loads. At the same time, it was necessary to find a simple algorithm for generating short links; in total, it was a very simple and elegant way to use Base36, taking int unix time in seconds (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 { } } } 

Accept PHP script


 <?php $file = file_get_contents("php://input"); $id = base_convert(time(), 10, 36); file_put_contents("img/".$id.".png",$file); echo "http://".$_SERVER['SERVER_NAME']."/img/".$id.".png"; ?> 

Screenshot Editing


Then I also wanted to somehow quickly edit the 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 it saved to disk), and after editing, you could 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 somewhere the url of the site and the default folder where to save the screenshots, eventually created a simple form of settings where it could be specified. 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 fold to tray.

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


Preview


Everything seems to be good, but it became clear what is missing. But the preview button was missing! It was somewhat inconvenient to open a folder or click edit to just see what was captured from the screen before sending. As a result, I quickly sketched out the preview form, there was a slight problem with displaying a full-screen screenshot in the form (it’s with frames), I didn’t want to delete the frame (I don’t even know why), eventually scrolled into the form and I was completely satisfied.

 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 is easily solved with the help of the same Bitmap.Save function, though that’s not the quality of jpg images I was satisfied with. The ability to specify the quality of jpg was not so obvious, fast googling is a solution . It is implemented using the EncoderParameter additional parameter 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 the folder after saving the screenshot, as well as auto opening the link after downloading. Quickly implemented it and added checkboxes to settings. Also added the function of copying links to the clipboard.

After adding the preview button, the program somehow began to look “not so”, the layout of the buttons was scattered, I thought a little, and I rearranged the buttons, so the following happened:


Minor improvements


Having rested a little and thought, I understood what is still missing - information about the latest screenshot download. Made the appropriate field, when clicked, you could click on the link. In addition, made the save / edit buttons inaccessible until you take a screenshot. Well, one more stroke - I added the “about the program” button with a brief description, version and build date (by the way, to get the date, I again google the solution , getting the date from the title of the application itself).

Total after these actions came the following:


A little later, I realized that I also lacked the display of the last saved file, which I quickly added, and also made these fields more functional by screwing a context menu (by right-clicking) where you could copy the link / path to the clipboard using Clipboard.SetText.

Program availability, localization


Well, it seems that the basic functionality was ready, everything worked, and I thought - could I share the program with the people? If you do this, then you need to at least make the possibility of localization and add English. The benefit of the studio makes it easy to implement all this with standard means, I began to translate the whole thing. Total happened:


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

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

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

But in order to change the language, it was necessary to restart the application, 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, it was also necessary to get a list of supported languages ​​by the application (for the future, if suddenly there are more localizations), so this decision was nuggled, and all this combined resulted in 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<CultureInfo> 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<CultureInfo> cultures = new List<CultureInfo>(); foreach(var cultureInfo in culture) { if (Directory.Exists(Path.Combine(exeLocation, cultureInfo.Name))) { cultures.Add(cultureInfo); } } return cultures; } 


The problem of capturing the boundaries of the window


And now I will return to the problem of capturing the borders of the window, this issue was first solved 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 it is, here is a screenshot of what I mean:


(screenshot from a newer version)

As can be seen in the screenshot - except for the window, it captured its boundaries and what was below them. Googled for quite a long time how to solve this problem, but then came across this article , which actually described the solution to the issue, the point is that windows vista and newer need to use dwmapi to get the correct window borders taking into account aero and so on. With a small modification of my code, I successfully tied it to dwmapi and the problem was finally completely resolved. But since window trim 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 another thought, since I am going to publish a program for everyone, then probably it would be nice, in addition to downloading to your server, to download a service, because then the program will be more useful, and you don’t need to have your own server to use it, t. to. I have been using imgur.com for a long time and it has a simple api , then I decided to bind to it. After sitting, having studied his api, first implemented an anonymous download, and a little later, the ability to link an account. In addition, it implemented the ability to delete the last downloaded image in the program (for their service only).

I will not fully 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 I redid the image upload code to my server, instead of binary sending I used a full download using a form to unify code. Along the way, as a means of identification, I used the user-agent and the $ _GET [key] key for my script, and I didn’t want to bother 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 { ... } 

Total it turned out quite workable and functional program, which already could do a lot more things than I planned to do initially.

The list of settings at the time looked like this:


Compatible with Win XP


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

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

All this allowed to run the program on old frameworks, and on Windows Vista / 7 to run without installing anything, since 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 question was quickly resolved thanks to this information , by correcting the supportedRuntime options in the config, allowing you to run the application on a new or old version without any problems. In addition, I made it possible to use the TLS 1.2 protocol if it is available (ie, on systems with the .NET Framework 4.5).

app.config

  <startup> <supportedRuntime version="v4.0"/> <supportedRuntime version="v2.0.50727"/> </startup> 

TLS 1.2 support

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

History of events


By and large, I thought that everything was enough for this, you can release it, but still something was missing - the history of actions with the log. Began to develop the appropriate window with some functions, like deleting a file from the PC and imgur, opening a file / link, copying the path / link using the context menu. I also made it possible to save events to the log file both from the list and automatically set in the settings.

Quite an informative window appeared:


Problem with HookCallback on Win XP


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

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

The problem of capturing screenshots from games


A little later, during testing, I encountered the problem of capturing screenshots from full-screen applications (for example, games), noticed that in windows 10 regular printscreen captures this case without problems, eventually added a function to insert images from the clipboard, and also added a tick "use the clipboard instead of capture "in the settings, thus" solved the issue "for myself, but as it turned out in win 7 and below it does not work, I began to study the question, and I realized that this is quite a difficult task, with the need to use directx injections, in the end I simply scored on this 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 remade 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:


I also made the tray icon more functional by adding all the important functions there when I right-click it:


Check for Win98 and Win2000


Well, purely for the sake of experiment, I deployed windows 2000 SP4 and 98 SE on a virtual machine, installed the .NET Framework 2.0 there. It was not so easy to do, because Some patches were required to install and upgrade Windows Installer. But still everything turned out and I tried to run the application.

As it turned out on Windows 2000 SP4, the application turned out to be fully working, but on Windows 98 SE, the key capture did not work, pasting from the buffer also does not work, but loading the screenshot from the file works without problems. Actually, these problems could not be solved, there is very little information, everything that I could find out - the “WH_KEYBOARD_LL” parameter was added only in Windows 2000. And I didn’t find any information about the reason for not working the image from the buffer. Total min requirements - Windows 2000.

So after some checks, debugs 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 source code, compile the application, write readme and make a release. This is where the story ends. You can download the finished program and view the source code on GitHub . I hope the article was helpful.

Source: https://habr.com/ru/post/316012/


All Articles