半年前我開源了 DreamScene2 一個小而快並且功能強大的 Windows 動態桌面軟體。有很多的人喜歡,這使我有了繼續做開源的信心。這是我的第二個開源作品 ScreenshotEx 一個簡單易用的 Windows 截屏增強工具。 歡迎 Star 和 Fork https://github.c ...
半年前我開源了 DreamScene2 一個小而快並且功能強大的 Windows 動態桌面軟體。有很多的人喜歡,這使我有了繼續做開源的信心。這是我的第二個開源作品 ScreenshotEx 一個簡單易用的 Windows 截屏增強工具。
歡迎 Star 和 Fork https://github.com/he55/ScreenshotEx
前言
在使用 Windows 系統的截屏快捷鍵 PrintScreen
截屏時,如果需要把截屏保存到文件,需要先粘貼到畫圖工具然後另存為文件。以前我還沒有覺得很麻煩,後來使用了 macOS 系統的截屏工具,我才知道原來一個小小的截屏工具也可以這麼簡單易用。於是參考 macOS 系統的截屏工具做了一個 Windows 版的。
功能
-
自動保存截屏到桌面
-
點擊截屏預覽可以編輯截屏
實現原理
如果想在按下系統的截屏快捷鍵後做一些事情,能想到的方法應該就是如何監聽鍵盤事件。WIN32 API 提供的 SetWindowsHookExA 鉤子函數剛好可以實現這個需求,idHook
參數設置成 WH_KEYBOARD_LL
時是低等級鍵盤鉤子可以捕獲鍵盤消息。
SetWindowsHookExA
函數定義
HHOOK SetWindowsHookExA(
[in] int idHook, // 鉤子類型
[in] HOOKPROC lpfn, // 鉤子處理函數
[in] HINSTANCE hmod, // 模塊句柄
[in] DWORD dwThreadId // 線程Id
);
鍵盤處理函數定義
LRESULT CALLBACK LowLevelKeyboardProc(
_In_ int nCode,
_In_ WPARAM wParam, // 鍵盤消息
_In_ LPARAM lParam // KBDLLHOOKSTRUCT 結構體指針
);
代碼
C# PInvoke 定義
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);
註冊鍵盤鉤子
需要註意:因為 SetWindowsHookEx
是非托管函數第二個參數是個委托類型,GC
不會記錄非托管函數對 .NET
對象的引用。如果用臨時變數保存委托出作用域就會被 GC
釋放,當 SetWindowsHookEx
去調用已經被釋放的委托就會報錯。
SetWindowsHookEx
函數第一個參數傳 WH_KEYBOARD_LL
低等級鍵盤鉤子、第二個參數傳鍵盤消息處理函數的委托、第三個參數使用 GetModuleHandle
函數獲取模塊句柄、第四個參數傳 0。
HookProc _hookProc;
IntPtr _hhook;
void StartHook()
{
_hookProc = new HookProc(LowLevelKeyboardProc); // 使用成員變數保存委托
_hhook = SetWindowsHookEx(WH_KEYBOARD_LL, _hookProc, GetModuleHandle(null), 0); // 註冊鍵盤鉤子,保存返回值卸載鉤子時用到。GetModuleHandle(null) 獲取當前模塊句柄
}
鍵盤消息處理函數
在鍵盤消息處理函數裡面捕獲 PrintScreen
按鍵消息,然後顯示預覽和保存圖片邏輯
IntPtr LowLevelKeyboardProc(int nCode, IntPtr wParam, ref KBDLLHOOKSTRUCT lParam)
{
if (nCode == HC_ACTION)
{
if (lParam.vkCode == VK_SNAPSHOT) // 捕獲 PrintScreen 按鍵消息
{
if ((int)wParam == WM_KEYUP || (int)wParam == WM_SYSKEYUP) // 按鍵釋放時保存圖片
SaveImage();
else
_previewWindow.SetHide();
}
}
return CallNextHookEx(_hhook, nCode, wParam, ref lParam);
}
保存圖片
從系統剪貼板獲取圖片
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);
}
}
完整代碼 https://github.com/he55/ScreenshotEx