C#でレジストリ書き換えてキー入れ替えてみた

レジストリのバイナリをいじるのがつらいのでちょっとましにしてみた。
(ツール探せばあるようだが、処理内容を把握せずに使いたくないので自作してみた。)

# 注意事項【超重要】

- レジストリを書き換えますので、わりと危ないです。
- <b>ソフトがバグってなくても、やらかす（設定内容をミスる）とキー入力が正常に行えなくなります。</b>
- <b>Scancodeは環境に依存して異なります。</b>
- 管理者権限を使うので、わりと危ないです。
- <b>反映には再起動が必要です。</b>
- 環境によってはうまくいかないようです。（私用PC環境では意図通り動作しましたが、職場PC環境ではうまくいきませんでした。）

# 入力ファイル

自分のPCのscancode割り当てを https://qiita.com/kob58im/items/b800ccce8b2f230401be で調べました。

<b>Scancodeの割り当てはPCに依存する(別のキーに割り当たってるかもしれない)ので、この入力ファイルを使ってそのまま実行するのはおやめください。</b>

```text:ScanCodeMapping.txt

0xE01D to 0xE05D % Right_Ctrl  to  ContextMenu
0xE020 to 0x003B % F1  mute  swap
0x003B to 0xE020 % F1  mute  swap
0xE02E to 0x003C % F2  volDown  swap
0x003C to 0xE02E % F2  volDown  swap
0xE030 to 0x003D % F3  volUp  swap
0x003D to 0xE030 % F3  volUp  swap
```

# 実行結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/7ae9369e-5d7e-f24d-1aa6-75b71c3e4d1b.png)

Write Registryを押すと、レジストリキーに値がかかれる。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/438183/506e9733-9bdb-46be-76a3-ddf28ac5504d.png)

# ソースコード

```csharp:ScancodeMapRegEditor.cs

using System;
using System.IO;
using System.Drawing;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using System.Windows.Forms;
using System.Globalization;
using System.Text.RegularExpressions;
using Microsoft.Win32; // Microsoft.Win32.Registry を使うので。


// https://docs.microsoft.com/ja-jp/windows-hardware/drivers/hid/keyboard-and-mouse-class-drivers#scan-code-mapper-for-keyboards

// レジストリアクセスのために管理者権限が必要。
// 管理者権限の要求設定(manifest)
//  https://www.atmarkit.co.jp/fdotnet/dotnettips/958uacmanifest/uacmanifest.html


public class ScanCodePair
{
    public ushort keyOrginal;  // 変更前scancode
    public ushort keyReplaced; // 変更後scancode
}

public class ScancodeMap
{
    // LE : Little Endian
    private void WriteUIntLE(uint x, byte[] dest, ref int offset)
    {
        dest[offset] = (byte)x;
        offset++;
        dest[offset] = (byte)(x>>8);
        offset++;
        dest[offset] = (byte)(x>>16);
        offset++;
        dest[offset] = (byte)(x>>24);
        offset++;
    }

    private void WriteUShortLE(ushort x, byte[] dest, ref int offset)
    {
        dest[offset] = (byte)x;
        offset++;
        dest[offset] = (byte)(x>>8);
        offset++;
    }

    List<ScanCodePair> pairList;


    public byte[] ConvertToRegBinary()
    {
        if ( pairList.Count == 0 ) {
            return null;
        }

        byte[] a = new byte[4+4+4 + 4*pairList.Count +4];
        int offset = 0;

        WriteUIntLE(0x00000000, a, ref offset); // 0..3 byte
        WriteUIntLE(0x00000000, a, ref offset); // 4..7 byte
        WriteUIntLE((uint)pairList.Count, a, ref offset); // 8..11 byte

        foreach ( ScanCodePair pair in pairList ) {
            WriteUShortLE(pair.keyReplaced, a, ref offset); // 変更後
            WriteUShortLE(pair.keyOrginal,  a, ref offset); // 変更前
        }
        
        WriteUIntLE(0x00000000, a, ref offset);

        return a;
    }

    public void Add(ScanCodePair t)
    {
        pairList.Add(t);
    }

    public ScancodeMap()
    {
        pairList = new List<ScanCodePair>();
    }
}



class RegKeyEditor : Form
{
    const string ScanCodeTextFileName = @"ScanCodeMapping.txt";

    const string RegValueName = @"Scancode Map";

    // 効果がなかった
    //  static readonly RegistryKey RootRegKey = Registry.CurrentUser; // const不可
    //  const string SubRegKeyName = @"Keyboard Layout";
    
    // 全userに適用される。
    //  管理者権限必要
    static readonly RegistryKey RootRegKey = Registry.LocalMachine; // const不可
    const string SubRegKeyName = @"SYSTEM\CurrentControlSet\Control\Keyboard Layout";

    ListView lsv;


    RegKeyEditor()
    {
        Button btn = new Button();
        btn.Text = "Write Registry";
        btn.Width = 150;
        btn.Click += (sender,e)=>{UpdateRegValue();};
        Controls.Add(btn);

        lsv = new ListView();
        lsv.View = View.Details;
        lsv.FullRowSelect = true;
        lsv.GridLines = true;
        lsv.Columns.Add("Code(old)", 120, HorizontalAlignment.Left);
        lsv.Columns.Add("Code(new)", 120, HorizontalAlignment.Left);
        lsv.Top = 30;
        lsv.Size = new Size(300 , 300);
        Controls.Add(lsv);

        ClientSize = new Size(300, 330);

        LoadKeyMap();
    }


    ScancodeMap ScancodeMapFromListView()
    {
        ScancodeMap map = new ScancodeMap();

        foreach ( ListViewItem item in lsv.Items ) {
            var pair = (ScanCodePair)item.Tag;
            map.Add(pair);
        }

        return map;
    }

    void UpdateRegValue()
    {
        // https://docs.microsoft.com/ja-jp/dotnet/api/microsoft.win32.registry.setvalue?view=netframework-4.8#Microsoft_Win32_Registry_SetValue_System_String_System_String_System_Object_Microsoft_Win32_RegistryValueKind_

        ScancodeMap map = ScancodeMapFromListView();
        byte[] data = map.ConvertToRegBinary();

        if ( data == null ) {
            // レジストリキーを削除する。
            DeleteRegValue(RootRegKey, SubRegKeyName, RegValueName);
        }
        else {
            /* dump
            int i=0;
            foreach(byte t in data){
                Console.Write(t.ToString("X2"));
                Console.Write(",");
                i++;
                if ( i%4 == 0 ) {
                    Console.WriteLine();
                }
            }
            Console.WriteLine();
            */

            // レジストリキーを書き込む。存在しない場合は作成される。
            SetRegValue(RootRegKey, SubRegKeyName, RegValueName, data, RegistryValueKind.Binary);

            // 上記は Registry.SetValueメソッドでもよいが、DeleteValueメソッドが用意されていないので、
            // 引数を統一させるために自作メソッドを使うこととした。
            // Registry.SetValue(@"HKEY_CURRENT_USER\Keyboard Layout", RegValueName, data, RegistryValueKind.Binary);
        }
    }
    
    static void SetRegValue(RegistryKey rootKey, string keyname, string valuename, object value, RegistryValueKind kind)
    {
        RegistryKey regKey = rootKey.OpenSubKey(keyname, true); // 書き込み権限で開く
        try { regKey.SetValue(valuename, value, kind);} finally { regKey.Close(); }
    }

    static void DeleteRegValue(RegistryKey rootKey, string keyname, string valuename)
    {
        RegistryKey regKey = rootKey.OpenSubKey(keyname, true); // 書き込み権限で開く
        try { regKey.DeleteValue(valuename, false);} finally { regKey.Close(); }
    }

    void LoadKeyMap()
    {
        string[] lines = File.ReadAllLines(ScanCodeTextFileName);
        Regex r = new Regex(@"^ *0x([0-9A-Fa-f]+) +to +0x([0-9A-Fa-f]+)\b");

        foreach (string s in lines) {
            Match m = r.Match(s);
            if ( m.Success ) {
                string scanCodeOrginal  = m.Groups[1].Value;
                string scanCodeReplaced = m.Groups[2].Value;
                lsv.Items.Add(MakeItem(scanCodeOrginal, scanCodeReplaced));
            }
        }
    }

    ListViewItem MakeItem(string sOrg, string sReplaced)
    {
        var item = new ListViewItem(new string[]{sOrg, sReplaced});
        ScanCodePair pair = new ScanCodePair();
        pair.keyOrginal = (ushort)Convert.ToInt32(sOrg, 16);
        pair.keyReplaced = (ushort)Convert.ToInt32(sReplaced, 16);
        item.Tag = pair;
        return item;
    }

    
    [STAThread]
    static void Main()
    {
        Application.Run(new RegKeyEditor());
    }
}

```

# コンパイル時の設定ファイル

今回のレジストリに対しては管理者権限が必要。(これやらないと実行時エラーとなる。)

参考サイト  https://www.atmarkit.co.jp/fdotnet/dotnettips/958uacmanifest/uacmanifest.html

```xml:app.manifest

<?xml version="1.0" encoding="utf-8"?>
<asmv1:assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1" xmlns:asmv1="urn:schemas-microsoft-com:asm.v1" xmlns:asmv2="urn:schemas-microsoft-com:asm.v2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
  <assemblyIdentity version="0.0.0.0" name="ScancodeMapRegEditor"/>
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
        <!-- UAC マニフェスト オプション
             Windows のユーザー アカウント制御のレベルを変更するには、
             requestedExecutionLevel ノードを以下のいずれかで置換します。

        <requestedExecutionLevel level="asInvoker" uiAccess="false" />
        <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
        <requestedExecutionLevel level="highestAvailable" uiAccess="false" />

            下位互換性のためにファイルおよびレジストリの仮想化を
            利用する場合は、requestedExecutionLevel ノードを削除してください。
        -->
        <requestedExecutionLevel level="requireAdministrator" uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>
</asmv1:assembly>
```

# コンパイル
`csc /win32manifest:app.manifest ScancodeMapRegEditor.cs`