Six months ago, I open-sourced DreamScene2, a small, fast, and powerful Windows dynamic desktop software. Many people liked it, which gave me the confidence to continue with open source. This is my second open-source project, ScreenshotEx, a simple and easy-to-use Windows screenshot enhancement tool.
Welcome to Star and Fork https://github.com/he55/ScreenshotEx
Preface
When using the Windows screenshot shortcut PrintScreen, if you need to save the screenshot to a file, you first have to paste it into a drawing tool and then save it as a file. I didn't find it very troublesome before, but after using the macOS screenshot tool, I realized how a small screenshot tool can be so simple and easy to use. So I referred to the macOS screenshot tool to create a Windows version.
Features
- Automatically save screenshots to desktop

- Click the screenshot preview to edit the screenshot

Implementation Principle
If you want to do something after pressing the system's screenshot shortcut, the way to think about it would be how to listen for keyboard events. The SetWindowsHookExA hook function provided by the WIN32 API can fulfill this requirement. When the idHook parameter is set to WH_KEYBOARD_LL, it is a low-level keyboard hook that can capture keyboard messages.
SetWindowsHookExA function definition
HHOOK SetWindowsHookExA(
[in] int idHook, // hook type
[in] HOOKPROC lpfn, // hook handler function
[in] HINSTANCE hmod, // module handle
[in] DWORD dwThreadId // thread id
);
Keyboard handler function definition
LRESULT CALLBACK LowLevelKeyboardProc(
_In_ int nCode,
_In_ WPARAM wParam, // keyboard message
_In_ LPARAM lParam // pointer to KBDLLHOOKSTRUCT
);
Code
C# PInvoke Definitions
const int HC_ACTION = 0;
const int WH_KEYBOARD_LL = 13;
const int WM_KEYUP = 0x0101;
const int WM_SYSKEYUP = 0x0105;
const int VK_SNAPSHOT = 0x2C;
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct KBDLLHOOKSTRUCT
{
public uint vkCode;
public uint scanCode;
public uint flags;
public uint time;
public UIntPtr dwExtraInfo;
}
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
public delegate IntPtr HookProc(int nCode, IntPtr wParam, ref KBDLLHOOKSTRUCT lParam);
[DllImport("User32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr SetWindowsHookEx(int idHook, HookProc lpfn, IntPtr hmod, int dwThreadId);
[DllImport("User32.dll", SetLastError = true, ExactSpelling = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool UnhookWindowsHookEx(IntPtr hhk);
[DllImport("User32.dll", SetLastError = false, ExactSpelling = true)]
public static extern IntPtr CallNextHookEx(IntPtr hhk, int nCode, IntPtr wParam, ref KBDLLHOOKSTRUCT lParam);
[DllImport("Kernel32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern IntPtr GetModuleHandle([Optional] string lpModuleName);
Registering the Keyboard Hook
Note: Because SetWindowsHookEx is an unmanaged function whose second parameter is a delegate type, the GC does not record references from unmanaged functions to .NET objects. If you store the delegate in a temporary variable, it may be collected by the GC when it goes out of scope. Then when SetWindowsHookEx calls the released delegate, an error will occur.
For the SetWindowsHookEx function, the first parameter is WH_KEYBOARD_LL for a low-level keyboard hook, the second parameter is the delegate for the keyboard message handler, the third parameter uses GetModuleHandle to get the module handle, and the fourth parameter is 0.
HookProc _hookProc;
IntPtr _hhook;
void StartHook()
{
_hookProc = new HookProc(LowLevelKeyboardProc); // Use member variable to store the delegate
_hhook = SetWindowsHookEx(WH_KEYBOARD_LL, _hookProc, GetModuleHandle(null), 0); // Register the keyboard hook; save the return value for unhooking later. GetModuleHandle(null) gets the current module handle
}
Keyboard Message Handler Function
In the keyboard message handler function, capture the PrintScreen key message, then handle displaying the preview and saving the image.
IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, ref KBDLLHOOKSTRUCT lParam)
{
if (nCode == HC_ACTION)
{
if (lParam.vkCode == VK_SNAPSHOT) // Capture PrintScreen key message
{
if ((int)wParam == WM_KEYUP || (int)wParam == WM_SYSKEYUP) // Save image on key release
SaveImage();
else
_previewWindow.SetHide();
}
}
return CallNextHookEx(_hhook, nCode, wParam, ref lParam);
}
Saving the Image
Get the image from the system clipboard.
void SaveImage()
{
if (Clipboard.ContainsImage())
{
if (!Directory.Exists(_settings.SavePath))
Directory.CreateDirectory(_settings.SavePath);
string ext = "png";
ImageFormat imageFormat = ImageFormat.Png;
switch (_settings.SaveExtension)
{
case 0:
imageFormat = ImageFormat.Png;
ext = "png";
break;
case 1:
imageFormat = ImageFormat.Jpeg;
ext = "jpg";
break;
case 2:
imageFormat = ImageFormat.Bmp;
ext = "bmp";
break;
}
if (_settings.SaveName == 0)
{
string name = DateTime.Now.ToString("yyyy-MM-dd HH.mm.ss");
_saveFilePath = Path.Combine(_settings.SavePath, $"{PrefixName} {name}.{ext}");
}
else
{
do
{
_saveFilePath = Path.Combine(_settings.SavePath, $"{PrefixName} {_nameIndex}.{ext}");
_nameIndex++;
} while (File.Exists(_saveFilePath));
}
Image image = Clipboard.GetImage();
image.Save(_saveFilePath, imageFormat);
if (_settings.IsPlaySound)
_soundPlayer.Play();
if (_settings.IsShowPreview)
_previewWindow.SetImage(_saveFilePath);
}
}
Full code: https://github.com/he55/ScreenshotEx