なんともいえないプログラム

仕事じゃ役に立たない何とも言えないプログラムについて書いています。

discordとかLINEをCtrl+Enterで送信する2

前回の続きです

LINEだけに限定する

SetWindowsHookExは1スレッドか全スレッドのどちらかにしかフックできません。 このままではLINEと関係ないプロセスにもフックが入ってしまうのでLINEだけに限定します。

void SetMyKeyboardProcTarget()
{
    HWND targetWnd;
    unsigned long processID = 0;
    targetWnd = FindWindow("Qt5QWindowIcon", NULL);
    GetWindowThreadProcessId(targetWnd, &processID);
    dwTargetProcessId = processID;
}

LINEのウィンドウタイトル"Qt5QWindowIcon"を探してウィンドウハンドルからプロセスIDを保存しておきます。この関数はDLL_PROCESS_ATTACHのところで実行させます。 あとはsendInput関数を使用して改行なり送信なりを入力させればよいのですがこのままだとsendInputで送信したキーをフックしてまた送信と無限ループになってしまうためキーボードからなのかsendInputなのかを判定する機能を付けます。

キーボードかSendInputなのかを判定する

INPUT temp;
    temp.type = INPUT_KEYBOARD;
    temp.ki.wVk = key;
    temp.ki.wScan = MapVirtualKey(0, 0);
    temp.ki.dwFlags = iskeyUp;
DLLEXPORT LRESULT CALLBACK KeyboardProc(int code, WPARAM wParam, LPARAM lParam) {
    if (code < 0) {
        return CallNextHookEx(g_hHook, code, wParam, lParam);
    }

    DWORD dwPid = GetCurrentProcessId();
    if (dwTargetProcessId == dwPid) {
        if (wParam == VK_RETURN && ((lParam & maskScan))) {
            if (GetKeyState(VK_CONTROL) & 0x8000) {
                SendInput(sendInput.size(), sendInput.data(), sizeof(INPUT));
                return 0;
            }
            if (SendInput(returnInput.size(), returnInput.data(), sizeof(INPUT)))
                return 0;
        }
        return CallNextHookEx(g_hHook, code, wParam, lParam);
    }

}

wScanを0にすることでキーボードかsendInputなのかを判定しています。これだけでLINEは完成です。
DiscordのほうはIMEに若干の癖がありますがほぼ同じです。ここまま完成でもよいですがexeが消えるとフックが外れて元に戻ってしまいます。
exeを常駐させればよいのですがやはりスタンドアロンのほうが夢があるので次はその説明に入ります。(完全なスタンドアロンではないですが..) dllのコードを載せておきます。

#include "pch.h"
#include <string>
#include <iostream>
#include <tchar.h>
#include "dllmain.h"
#include <vector>
#define DLLEXPORT extern "C" __declspec(dllexport)

HINSTANCE g_hInst;
#pragma data_seg(".shared")
HHOOK g_hHook = NULL;
std::vector<INPUT> returnInput;
std::vector<INPUT> sendInput;
static DWORD dwTargetProcessId = 0;
#pragma data_seg()
#pragma comment(linker, "/SECTION:.shared,RWS")
DLLEXPORT void HookEnd();
const long maskScan = ((long)pow(2, 8) - 1) << 16;
void SetMyKeyboardProcTarget()
{
    HWND targetWnd;
    unsigned long processID = 0;
    targetWnd = FindWindow("Qt5QWindowIcon", NULL);
    GetWindowThreadProcessId(targetWnd, &processID);

    dwTargetProcessId = processID;
}

void pushKey(std::vector<INPUT>& in, WORD key, int iskeyUp) {
    INPUT temp;
    temp.type = INPUT_KEYBOARD;
    temp.ki.wVk = key;
    temp.ki.wScan = MapVirtualKey(0, 0);
    temp.ki.dwFlags = iskeyUp;
    in.push_back(temp);
}

DLLEXPORT LRESULT CALLBACK KeyboardProc(int code, WPARAM wParam, LPARAM lParam) {
    if (code < 0) {
        return CallNextHookEx(g_hHook, code, wParam, lParam);
    }

    DWORD dwPid = GetCurrentProcessId();
    if (dwTargetProcessId == dwPid) {
        if (wParam == VK_RETURN && ((lParam & maskScan))) {
            if (GetKeyState(VK_CONTROL) & 0x8000) {
                SendInput(sendInput.size(), sendInput.data(), sizeof(INPUT));
                return 0;
            }
            if (SendInput(returnInput.size(), returnInput.data(), sizeof(INPUT)))
                return 0;
        }
        return CallNextHookEx(g_hHook, code, wParam, lParam);
    }

}
DLLEXPORT void HookStart()
{

    g_hHook = SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)KeyboardProc, g_hInst, 0);
    if (g_hHook == NULL) {
        MessageBox(NULL, "フック開始は失敗しました", "HookStart", MB_OK);
        return;
    }
}

DLLEXPORT void HookEnd()
{
    UnhookWindowsHookEx(g_hHook);
}

BOOL APIENTRY DllMain(HMODULE hModule,
    DWORD  ul_reason_for_call,
    LPVOID lpReserved
)
{
    if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
        SetMyKeyboardProcTarget();
        if (sendInput.empty()) {
            pushKey(returnInput, VK_SHIFT, 0);
            pushKey(returnInput, VK_RETURN, 0);
            pushKey(returnInput, VK_RETURN, KEYEVENTF_KEYUP);
            pushKey(returnInput, VK_SHIFT, KEYEVENTF_KEYUP);
            pushKey(sendInput, VK_CONTROL, KEYEVENTF_KEYUP);
            pushKey(sendInput, VK_RETURN, 0);
            pushKey(sendInput, VK_RETURN, KEYEVENTF_KEYUP);
        }
        g_hInst = (HINSTANCE)hModule;   // DLLモジュールのハンドル取得
    }
    return TRUE;
}

discordとかLINEをCtrl+Enterで送信する1

初めに

皆さん普段からLINEやdiscord、slackといった様々なコミュニケーションツールを使用していると思います。これらのツールで送信するときLINEやdiscordはEnter、slackやtwitterはctrl+enterとツールによってバラバラです。個人的には誤爆を防ぐためにすべてCtrl+Enterで統一したいところですが送信方法を設定できないツールも多く設定できてもAlt+Enterで送信とかいう微妙な設定しかありません。
そこでツールにキー入力(Ctrl+Enter)が入ったときに入力を内部でEnterに変えてツールに送ればCtrl+Enterで送信できるはずです。

キー入力乗っ取り

あるプログラムの中で任意のプログラムを動作させるにはdllインジェクションを行います。プログラムに流れるキー入力を監視しEnterが押されたら破棄し、改行キーを送り直します。Ctrl+Etnerが来ればEnterのみを送信します。

dllインジェクション

dllをプログラムに埋め込むにはSetWindowsHookEx関数を使用します。

HHOOK SetWindowsHookExW(
  int       idHook,   //フックタイプ
  HOOKPROC  lpfn,     //コールバック関数
  HINSTANCE hmod,     //dllハンドル
  DWORD     dwThreadId//スレッド識別子
);

今回のフックタイプにはWH_KEYBOARDを指定します。このフックタイプはキーボードの入力があれば指定したコールバック関数が実行されます。 dllのエントリポイントでハンドルを取得しておきます。

BOOL APIENTRY DllMain(HMODULE hModule,DWORD  ul_reason_for_call,LPVOID lpReserved)
{
    if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
        g_hInst = (HINSTANCE)hModule;   // DLLモジュールのハンドル取得
    }
    return TRUE;
}

そしてフックする関数を以下のように定義しておきます。

DLLEXPORT void HookStart()
{
    g_hHook = SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)KeyboardProc, g_hInst, 0);

    if (g_hHook == NULL) {
        MessageBox(NULL, "フック開始は失敗しました", "HookStart", MB_OK);
        return;
    }
}

コールバック関数にはとりあえず適当に作っておきます。

DLLEXPORT LRESULT CALLBACK KeyboardProc(int code, WPARAM wParam, LPARAM lParam) 
{
    MessageBox(NULL, std::to_string(wParam).c_str(), "Hook", MB_OK);
    return CallNextHookEx(g_hHook, code, wParam, lParam);
}

WH_KEYBOARDではwParamに入力されたキー情報が格納されています。 CallNextHookEx(g_hHook, code, wParam, lParam);では受け取った情報をそのまま親プロセスに渡します。NULL等を返せば横取りしたキー入力をそのまま破棄するのでなにも入力できなくなります。
ここまではdllに書くプログラムでしたが最初にこのdllを読み込んでHookStart()を実行しなければdllを流し込めません。 そこで、このdllを流し込むexeを作成していきます。

   MSG msg;
    HINSTANCE hinst = LoadLibrary(TEXT("CTRLSENDLL.dll"));
    if (hinst) {
        typedef void (*Install)();
        typedef void (*Uninstall)();

        Install install = (Install)GetProcAddress(hinst, "HookStart");

        install();

        while (GetMessage(&msg, NULL, 0, 0) > 0)
        {
            TranslateMessage(&msg);
            DispatchMessage(&msg);
        }
   }

これを実行すれば起動しているあらゆるプログラムにdllが注入されてキー入力するたびにメッセージボックスが出現します。
ここで注意するのはLINEやdiscordは32bitソフトなので流し込むdllも32bitで作成しないとdllインジェクションは失敗します。
続きはまた書きます。dllの全文は以下に置いておきます。

#include "pch.h"
#include <string>
#pragma comment(lib, "imm32.lib")
#include "dllmain.h"
#define DLLEXPORT extern "C" __declspec(dllexport)

HINSTANCE g_hInst;
#pragma data_seg(".shared")
HHOOK g_hHook = NULL;
#pragma data_seg()
#pragma comment(linker, "/SECTION:.shared,RWS")

DLLEXPORT LRESULT CALLBACK KeyboardProc(int code, WPARAM wParam, LPARAM lParam) {
    MessageBox(NULL, std::to_string(wParam).c_str(), "Hook", MB_OK);
    return CallNextHookEx(g_hHook, code, wParam, lParam);
}
DLLEXPORT void HookStart()
{

    g_hHook = SetWindowsHookEx(WH_KEYBOARD, (HOOKPROC)KeyboardProc, g_hInst, 0);

    if (g_hHook == NULL) {
        MessageBox(NULL, "フック開始は失敗しました", "HookStart", MB_OK);
        return;
    }
}

DLLEXPORT void HookEnd()
{
    UnhookWindowsHookEx(g_hHook);
}

BOOL APIENTRY DllMain(HMODULE hModule,DWORD  ul_reason_for_call,LPVOID lpReserved)
{
    if (ul_reason_for_call == DLL_PROCESS_ATTACH) {
        g_hInst = (HINSTANCE)hModule;   // DLLモジュールのハンドル取得
    }
    return TRUE;
}

文鎮化したpro microをArduino Unoを使って直す

はじめに

少し前にCorne cherryという分離型の自作キーボードのPro microを文鎮化させてしまい、せっかくなので手元にあったArduino Unoを使って直してみたのでメモ代わりに置いておきます。
正常なPro microがあればわざわざArduino Unoなんて使わなくても直せるみたいです。

用意するもの
Arduino Unoをavrライター化

Arduino Unoを文鎮化したPro microに書き込めるようにavrライター化させます。
Arduino IDE公式ページからダウンロードしてきます。Arduino UnoをつないでからIDEの メニューのファイルからスケッチ例→11. ArduinolSPを開きArduino Unoにそのまま書き込みます。この時、書き込んだUnoのシリアルポートを覚えておきます。(自分の環境ではCOM10)

Arduino UnoとPro microをつなぐ

配線は以下の通りです

Uno Pro micro
5V VCC
D12 14(PB1[SCK])
D13 15(PB3[MISO])
D11 16(PB2[MOSI])
GND GND

こんな感じ
f:id:ro1533:20201204211432j:plain

avrdudeコマンドを使ってブートローダを書き込む

avrdudeコマンドはQmk Toolboxに入っているのでそれを使います。
コマンドプロンプトからQmk Toolboxのデータファイルに飛びます

cd "AppData\Local\QMK\QMK Toolbox\QMK Toolboxのバージョン\"

私の時のバージョンは0.0.20でした。 次のコマンドで書き込みます。

avrdude.exe -p atmega32u4 -c stk500v1 -b 19200 -U flash:w:"Caterina-Micro.hexファイル":i -P さっき調べたシリアルポート -U efuse:w:0xC3:m -U hfuse:w:0xD9:m -U lock:w:0x3F:m

私の場合はCOM10です。また、Caterina-Micro.hexファイルはパスなのでダブルコーテーションつけとくといいと思います。
すぐに書き込みが始まるはずですがエラーもでず、ずっと止まってたら一度Unoの電源(USB抜き差し)しておくと治ります。

Qmk Toolboxで通常通りファームウェアを書き込む

通常通りGNDとRSTをショートさせればファームウェアが書き込めるはずです。

おわりに

試行錯誤しながら変なブートローダー書き込んだりしていたらPro microの電源回りが壊れたのかUSB電源が供給されないという微妙に壊れたままです。
USBの通信そのものはできるらしいので書き込むときは5Vのピンヘッダから電源を供給しつつUSBをつないで書き込むというかなりめんどくさいことになってます。

chrome拡張機能で背景画像を設定する

 

はじめに

ソフトとかカスタマイズするときに背景画像を設定とか大好きなので普段使うchromeにも背景画像を設定します。

 完成品はこんな感じになります。

見た目

f:id:ro1533:20201114015838p:plain

背景画像なし

f:id:ro1533:20201114020016p:plain

背景画像あり

 chromeのそのままの文字だと背景画像が暗いときに見づらくなってしまうのでchrome://flags/からダークモードをオンにしておきます。(リンク化してありますがセキュリティ上直接URL欄に打たないと飛べません)明るい画像を背景画像にする場合はダークモードをオンにする必要はないと思います。

chrome拡張機能

色々な記事で解説されているので詳しい説明は省きますが最小構成は「manifest.json」があれば拡張機能として認識されるようです。拡張機能chrome://extensions/からデベロッパーモードをオンにして「パッケージ化されていない拡張機能を読み込む」からmanifest.jsonとかが入っているファイルを選択してあげることで読み込めます。

 

 manifest.jsonの内容は以下の通りです。

 manifest.json
{
    "manifest_version": 2,
    "name": "background",
    "description": "",
    "version": "1.0",
    "icons": {
        "32": "icon_32.png",
        "48": "icon_48.png",
        "128": "icon_128.png"
    },
    "content_scripts": [{
        "matches": ["http://*/*", "https://*/*" ],
        "js": ["jquery-3.5.1.min.js","content_scripts.js"]
    }],
    "browser_action": {
        "default_icon": "icon_32.png",
        "default_title": "",
         "default_popup": "popup.html"
    },
    "permissions": [
        "tabs",
        "background",
        "http://*/*",
        "https://*/*"
    ],
    "web_accessible_resources": [
        "*.png"
    ]
}

アイコン関連は省略可能です。書いた場合は実際にpngで画像を作成しておく必要があります。ペイントか何かで編集しておきましょう。
重要なのは以下の項目です。

    "web_accessible_resources": [
        "*.png"
    ]

これによりローカルで保存してあるpngファイルを読み込むことができます。manifest.jsonで宣言したファイル達をそれぞれ作成していきます。
jQueryここから取ってきます。

 popup.html
<!DOCTYPE HTML>
<BODY>
<script src="jquery-3.5.1.min.js"></script>
<script src="content_scripts.js"></script>
</BODY>
</HTML>
content_scripts.js
$(document).ready(function() {
var image = '<style type="text/css"> body{background-image:url('
image+=chrome.extension.getURL('background.png')
image +=');background-attachment: fixed;}</style>'
$('head').append($(image))
});

以上を保存して再度読み込ませれば完成です。

解説

やってることは単純です。chromeの検証からソースを見てみると以下のようなCSSが挿入されていることがわかります。

<style type="text/css"> 
body{
background-image:url(chrome-extension://aghlpijkpccdlefanpfdahlicenikdpd/background.png);
background-attachment: fixed;
}
</style>

CSSでbackground-imageを付けると背景画像を設定することができるのでJavascriptを利用してこれを挿入しているわけです。 ローカル画像を読み込むには「chrome.extension.getURL('パス')」と記述する必要があります。また、ダークモード限定ですが画像の置き方を指定する「background-attachment:」はfixedで固定しておかないとダークモードの反転対象になって白くなるという悲惨なことが起きます。

popup.htmlには何も書いてないので複数画像を管理したり画像処理して明るさを暗くしたりといった機能を付けるのもありかもしれませんね。

このブログについて

主にプログラム関係でやったこととかメモ程度に残していきたいと思います。Qiitaとかでもよかったんですがもしかしたらプログラム関係ないことも書くかもしれないので念のためということで...