2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Caps Lock を絶対に有効化

Posted at

動機

Caps Lock キーを物理的に無効化している人がいました。私はソフトウェア的に強制有効化してみようと思いました。

↓ Shift + Caps Lock キーで解除してもすぐに有効になる様子
out.gif

環境

Windows 10 22H2
rustc 1.88.0
windows crate 0.61.3

実装

短いので全部載せます。

コピペ用サンプルコード
Cargo:toml
[package]
name = "caps_lock_lock"
version = "0.1.0"
edition = "2024"

[dependencies]
anyhow = "1.0"

[dependencies.windows]
version = "0.61"
features = [
  "Win32_Graphics_Gdi",
  "Win32_UI_Input_KeyboardAndMouse",
  "Win32_UI_WindowsAndMessaging",
]
src/main.rs
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

use anyhow::Result;
use windows::{
    Win32::{
        Foundation::{HWND, LPARAM, LRESULT, WPARAM},
        UI::{
            Input::KeyboardAndMouse::{
                GetKeyState, INPUT, INPUT_KEYBOARD, KEYEVENTF_KEYUP, SendInput, VK_CAPITAL,
                VK_LSHIFT,
            },
            WindowsAndMessaging::{
                CW_USEDEFAULT, CreateWindowExW, DefWindowProcW, DispatchMessageW, GetMessageW,
                KillTimer, MSG, PostQuitMessage, RegisterClassW, SW_SHOW, SetTimer, ShowWindow,
                TranslateMessage, WINDOW_EX_STYLE, WM_DESTROY, WM_TIMER, WNDCLASSW,
                WS_OVERLAPPEDWINDOW,
            },
        },
    },
    core::{PCWSTR, w},
};

const CLASS_NAME: PCWSTR = w!("caps_lock_lock_win_class_name");
const ID_TIMER: usize = 42;

struct Timer(HWND);

impl Timer {
    fn new(hwnd: HWND, elapse_millisec: u32) -> Self {
        unsafe { SetTimer(Some(hwnd), ID_TIMER, elapse_millisec, None) };
        Self(hwnd)
    }
}

impl Drop for Timer {
    fn drop(&mut self) {
        _ = unsafe { KillTimer(Some(self.0), ID_TIMER) };
    }
}

unsafe extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wp: WPARAM, lp: LPARAM) -> LRESULT {
    match msg {
        WM_TIMER if wp.0 == ID_TIMER => {
            if unsafe { GetKeyState(VK_CAPITAL.0 as _) } & 0x0001 == 0 {
                let mut inputs = [INPUT::default(); 4];
                inputs[0].r#type = INPUT_KEYBOARD;
                inputs[0].Anonymous.ki.wVk = VK_LSHIFT;
                inputs[1].r#type = INPUT_KEYBOARD;
                inputs[1].Anonymous.ki.wVk = VK_CAPITAL;
                inputs[2].r#type = INPUT_KEYBOARD;
                inputs[2].Anonymous.ki.wVk = VK_CAPITAL;
                inputs[2].Anonymous.ki.dwFlags = KEYEVENTF_KEYUP;
                inputs[3].r#type = INPUT_KEYBOARD;
                inputs[3].Anonymous.ki.wVk = VK_LSHIFT;
                inputs[3].Anonymous.ki.dwFlags = KEYEVENTF_KEYUP;
                unsafe { SendInput(&inputs, size_of::<INPUT>() as _) };
            }
        }
        WM_DESTROY => unsafe {
            PostQuitMessage(0);
        },
        _ => return unsafe { DefWindowProcW(hwnd, msg, wp, lp) },
    }
    LRESULT::default()
}

fn main() -> Result<()> {
    let wc = WNDCLASSW {
        lpfnWndProc: Some(wnd_proc),
        lpszClassName: CLASS_NAME,
        ..Default::default()
    };

    unsafe { RegisterClassW(&wc) };

    let hwnd = unsafe {
        CreateWindowExW(
            WINDOW_EX_STYLE::default(),
            CLASS_NAME,
            w!("caps_lock_lock"),
            WS_OVERLAPPEDWINDOW,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            200,
            200,
            None,
            None,
            None,
            None,
        )?
    };

    #[cfg(debug_assertions)]
    unsafe {
        _ = ShowWindow(hwnd, SW_SHOW);
    }

    let _timer = Timer::new(hwnd, 1000);

    let mut msg = MSG::default();
    loop {
        if !unsafe { GetMessageW(&mut msg, None, 0, 0).as_bool() } {
            break;
        }
        unsafe {
            _ = TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }
    }
    Ok(())
}

実装の解説

コンソールウィンドウの抑止

リリースビルド時にコンソールウィンドウが表示されないようにします。

#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]

隠しウィンドウの生成

main関数でウィンドウを生成します。ShowWindowを呼ばないのでウィンドウは表示されず、タスクバーにも表示されません。プログラムを終了するにはタスクマネージャ等から強制終了する必要があります。

fn main() -> Result<()> {
    let wc = WNDCLASSW {
        lpfnWndProc: Some(wnd_proc),
        lpszClassName: CLASS_NAME,
        ..Default::default()
    };

    unsafe { RegisterClassW(&wc) };

    // ウィンドウの生成
    let hwnd = unsafe {
        CreateWindowExW(
            WINDOW_EX_STYLE::default(),
            CLASS_NAME,
            w!("caps_lock_lock"),
            WS_OVERLAPPEDWINDOW,
            CW_USEDEFAULT,
            CW_USEDEFAULT,
            200,
            200,
            None,
            None,
            None,
            None,
        )?
    };

    // リリースビルド時には ShowWindow は呼ばれない
    #[cfg(debug_assertions)]
    unsafe {
        _ = ShowWindow(hwnd, SW_SHOW);
    }

    // Timer を設定する(後述)
    let _timer = Timer::new(hwnd, 1000);

    // メッセージループ
    let mut msg = MSG::default();
    loop {
        if !unsafe { GetMessageW(&mut msg, None, 0, 0).as_bool() } {
            break;
        }
        unsafe {
            _ = TranslateMessage(&msg);
            DispatchMessageW(&msg);
        }
    }
    Ok(())
}

タイマーの設定

Caps Lock が有効になっているかどうかのチェックですが、今回は Windows のタイマーを使用して 1 秒ごとにキーの状態を確認します。SetTimer関数でタイマーを設定してKillTimer関数でタイマーを解除します。解除忘れがないようにTimer構造体を作ってスコープを抜けるときに自動的にタイマーが解除されるようにしておきます。(今回のような小さなプログラムではあまり恩恵は感じられませんが、リソースの解放忘れを防ぐ目的で積極的に使使っていきたいテクニックだと思います)

struct Timer(HWND);

impl Timer {
    fn new(hwnd: HWND, elapse_millisec: u32) -> Self {
        unsafe { SetTimer(Some(hwnd), ID_TIMER, elapse_millisec, None) };
        Self(hwnd)
    }
}

impl Drop for Timer {
    fn drop(&mut self) {
        _ = unsafe { KillTimer(Some(self.0), ID_TIMER) };
    }
}

fn main() -> Result<()> {
    // 省略
    let _timer = Timer::new(hwnd, 1000); // _timer 変数に代入する必要あり
    // 省略
}

main関数内で_timer変数に代入していますがこのように記述しておく必要があります。下記のようにTimer::newだけだとすぐにKillTimerが呼ばれてタイマーが解除されてしまいます。

fn main() -> Result<()> {
    // 省略
    Timer::new(hwnd, 1000); // 変数に代入しないと、ここですぐに drop して KillTimer が呼ばれてしまう
    // 省略
}

ウィンドウプロシージャ

最後にウィンドウプロシージャの説明です。1 秒ごとにGetKeyState関数で Caps Lock の状態を確認します。 OFF だったらSetInput関数で Shift + Caps Lock を送信して Caps Lock を ON にしています。SetInput関数の第二引数が配列全体のサイズではなくて、INPUT構造体のサイズであることに留意してください。(これに気付かなくて時間を溶かしました:sob:

unsafe extern "system" fn wnd_proc(hwnd: HWND, msg: u32, wp: WPARAM, lp: LPARAM) -> LRESULT {
    match msg {
        WM_TIMER if wp.0 == ID_TIMER => {
            // Caps Lock が OFF の場合
            if unsafe { GetKeyState(VK_CAPITAL.0 as _) } & 0x0001 == 0 {
                let mut inputs = [INPUT::default(); 4];
                // Shift キー押下
                inputs[0].r#type = INPUT_KEYBOARD;
                inputs[0].Anonymous.ki.wVk = VK_LSHIFT;
                // Caps Lock キー押下
                inputs[1].r#type = INPUT_KEYBOARD;
                inputs[1].Anonymous.ki.wVk = VK_CAPITAL;
                // Caps Lock キー離上
                inputs[2].r#type = INPUT_KEYBOARD;
                inputs[2].Anonymous.ki.wVk = VK_CAPITAL;
                inputs[2].Anonymous.ki.dwFlags = KEYEVENTF_KEYUP;
                // Shift キー離上
                inputs[3].r#type = INPUT_KEYBOARD;
                inputs[3].Anonymous.ki.wVk = VK_LSHIFT;
                inputs[3].Anonymous.ki.dwFlags = KEYEVENTF_KEYUP;
                // 第二引数は INPUT 構造体のサイズ
                unsafe { SendInput(&inputs, size_of::<INPUT>() as _) };
            }
        }
        WM_DESTROY => unsafe {
            PostQuitMessage(0);
        },
        _ => return unsafe { DefWindowProcW(hwnd, msg, wp, lp) },
    }
    LRESULT::default()
}

まとめ

これは私の思考実験です。同僚の PC にいたずらしないでね :stuck_out_tongue:

2
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?