C# 編寫一個簡單易用的 Windows 截圖增強工具

he55發表於2022-05-11

半年前我開源了 DreamScene2 一個小而快並且功能強大的 Windows 動態桌面軟體。有很多的人喜歡,這使我有了繼續做開源的信心。這是我的第二個開源作品 ScreenshotEx 一個簡單易用的 Windows 截圖增強工具。

歡迎 Star 和 Fork https://github.com/he55/ScreenshotEx

前言

在使用 Windows 系統的截圖快捷鍵 PrintScreen 截圖時,如果需要把截圖儲存到檔案,需要先貼上到畫圖工具然後另存為檔案。以前我還沒有覺得很麻煩,後來使用了 macOS 系統的截圖工具,我才知道原來一個小小的截圖工具也可以這麼簡單易用。於是參考 macOS 系統的截圖工具做了一個 Windows 版的。

功能

  • 自動儲存截圖到桌面

    img

  • 點選截圖預覽可以編輯截圖

    img

實現原理

如果想在按下系統的截圖快捷鍵後做一些事情,能想到的方法應該就是如何監聽鍵盤事件。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

相關文章