首页 > 编程语言 >用C/C++(Win32API)写软件修改键位

用C/C++(Win32API)写软件修改键位

时间:2024-01-14 21:57:15浏览次数:27  
标签:键位 auto VK C++ 键盘 OEM define Win32API

title: 用C/C++(Win32API)写软件修改键位
date: 2021-06-25
categories: 编程
tags:
- 键盘
- 注册表
- C/C++
- Windows

前言

紧接上篇《Windows用注册表修改键盘映射(扫描码)》
用起来会发现处处不协调,除了需要熟悉新键位以外,最重要的是原本的快捷键也被拆散了,如原本都在左下角的Ctrl+Z/X/C/V
所以我们应该在保证快捷键相对位置不变的情况下,修改其他字母的位置,在本专栏中使用键盘钩子Keyboard Hook)。

注:

  • 由于软件很小而且要不安全操作,所以选择C/C++来写,并且暂时不显示窗口页面。

  • 每段代码会分别展示C/C++的写法,C在前、C++在后,两者相同时只会标注为C代码。一般来说C的代码C++也可以用,但写C++时建议用C++的标准。

  • 使用本方法改键位可能会被某些游戏判为作弊!但上篇专栏修改注册表的方法不会。

引入

本次我们以德沃夏克键盘Dvorak Keyboard)为例,把Qwerty键盘修改为德沃夏克键盘。

Qwerty键盘(Qwerty Keyboard)

1 ! 2 @ 3 # 4 $ 5 % 6 ^ 7 & 8 * 9 ( 0 ) - _ + =
Q W E R T Y U I O P [ { ] }
A S D F G H J K L ; : ' "
Z X C V B N M , < . > / ?

德沃夏克键盘(Dvorak Keyboard)

1 ! 2 @ 3 # 4 $ 5 % 6 ^ 7 & 8 * 9 ( 0 ) [ { ] }
' " , < . > P Y F G C R L / ? + =
A O E U I D H T N S - _
; : Q J K X B M W V Z

此外,根据维基百科[1]

钩子编程(Hooking),也称作“挂钩”,是计算机程序设计术语,指通过拦截软件模块间的函数调用、消息传递、事件传递来修改或扩展操作系统、应用程序或其他软件组件的行为的各种技术。处理被拦截的函数调用、事件、消息的代码,被称为钩子(Hook)。

简单来说就是拦截你输入的信息,处理过后再给电脑。

编写方法

以下默认引用头文件

#include<Windows.h>

首先是WinMain()函数,这里只有两件事要做:安装键盘钩子和进行事件循环。

注:因为keyboardHook在其他函数里也会用到,所以是全局变量。

// 键盘钩子
static HHOOK keyboardHook = NULL;
// 可编辑的键总数
#define KeysCount 47
static HHOOK KeyboardHook = nullptr;
constexpr auto KeysCount = 47;
// 主程序
int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPreINstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow)
{
    // 安装键盘钩子
    keyboardHook = SetWindowsHookExW(WH_KEYBOARD_LL, &KeyboardProc, hInstance, NULL);
    if (keyboardHook == NULL) // nullptr in C++
        return 1;
    // 进行事件循环
    MSG msg;
    while (GetMessageA(&msg, NULL, 0, 0)) // nullptr in C++
    {
        TranslateMessage(&msg);
        DispatchMessageA(&msg);
    }
    return msg.wParam;
}

事件循环并不重要,所以可以直接抄网上的代码,安装钩子主体是创建一个新对象,这部分需要重点解释。

修改键位

KeyboardLayoutList数列规定了各种键盘的布局,其中第一个键盘是Qwerty键盘,第二个以德沃夏克键盘为例(如S对应O,D对应E)。

注:由于后面一个函数keybd_event()需要BYTE类型的字符,所以我们用BYTE类型定义。

// 某些键盘上符号的虚拟键代码
#define _11 VK_OEM_3
#define _12 VK_OEM_MINUS
#define _13 VK_OEM_PLUS
#define _21 VK_OEM_4
#define _22 VK_OEM_6
#define _23 VK_OEM_5
#define _31 VK_OEM_1
#define _32 VK_OEM_7
#define _41 VK_OEM_COMMA
#define _42 VK_OEM_PERIOD
#define _43 VK_OEM_2
#pragma region 某些键盘上符号的虚拟键代码
constexpr auto _11 = VK_OEM_3;
constexpr auto _12 = VK_OEM_MINUS;
constexpr auto _13 = VK_OEM_PLUS;
constexpr auto _21 = VK_OEM_4;
constexpr auto _22 = VK_OEM_6;
constexpr auto _23 = VK_OEM_5;
constexpr auto _31 = VK_OEM_1;
constexpr auto _32 = VK_OEM_7;
constexpr auto _41 = VK_OEM_COMMA;
constexpr auto _42 = VK_OEM_PERIOD;
constexpr auto _43 = VK_OEM_2;
#pragma endregion
// Qwerty键盘(序号0)
BYTE QwertyKb[KeysCount] = {
    _11, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', _12, _13,
    'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', _21, _22, _23,
    'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', _31, _32,
    'Z', 'X', 'C', 'V', 'B', 'N', 'M', _41, _42, _43 };

// 德沃夏克键盘(序号1)
BYTE DvorakKb[KeysCount] = {
    _11, '1', '2', '3', '4', '5', '6', '7', '8', '9', '0', _21, _22,
    _32, _41, _42, 'P', 'Y', 'F', 'G', 'C', 'R', 'L', _43, _13, _23,
    'A', 'O', 'E', 'U', 'I', 'D', 'H', 'T', 'N', 'S', _22,
    _21, 'Q', 'J', 'K', 'X', 'B', 'M', 'W', 'V', 'Z' };
// 键盘列表
BYTE* KeyboardLayoutList[2] = { QwertyKb, DvorakKb };
#include<vector>
std::vector<BYTE*> KeyboardLayoutList{ QwertyKb, DvorakKb };

首先记录原键盘的键位,代码中一行代表现实中的一行(其实不记录也没关系,但如果以后要搞自定义键盘功能时,就一定要先留一个默认布局)

其中这些宏或常量表达式是为了提高可读性,对应了键盘上的标点符号,可以见WinUser.h的文件里定义:

// WinUser.h
#define VK_OEM_1          0xBA   // ';:' for US
#define VK_OEM_PLUS       0xBB   // '+' any country
#define VK_OEM_COMMA      0xBC   // ',' any country
#define VK_OEM_MINUS      0xBD   // '-' any country
#define VK_OEM_PERIOD     0xBE   // '.' any country
#define VK_OEM_2          0xBF   // '/?' for US
#define VK_OEM_3          0xC0   // '`~' for US

当需要用某种布局时,只要改变选择的序号就可以了:

// 目前选择的键盘序号
int KeyboardLayoutIndex = 1;

安装钩子

这段是本项目的核心区:

// 某键是否被按下
// nVirtualKey: int 需判断的键
// return: bool 发送的键对应在数组里的序号
#define IsKeyPressed(nVirtualKey) ((GetKeyState(nVirtualKey) & (1 << (sizeof(SHORT) * 8 - 1))) != 0)

// 发送键盘事件
// index: int
#define Kbe(index) keybd_event(KeyboardLayoutList[KeyboardLayoutIndex][index], 0, 0x0000, 1 << 24)
inline auto IsKeyPressed(const int nVirtualKey) { return (GetKeyState(nVirtualKey) & (1 << (sizeof(SHORT) * 8 - 1))) != 0; }

inline auto Kbe(const int index) { keybd_event(KeyboardLayoutList[KeyboardLayoutIndex][index], 0, 0x0000, 1 << 24); }
// 键盘钩子处理程序
LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    const PKBDLLHOOKSTRUCT p = (PKBDLLHOOKSTRUCT)lParam;
    bool handled = false;
    if (wParam == WM_KEYDOWN)
        if (p->dwExtraInfo != 1 << 24 &&
            !IsKeyPressed(VK_CONTROL) &&
            !IsKeyPressed(VK_LWIN) &&
            !IsKeyPressed(VK_RWIN) &&
            !IsKeyPressed(VK_MENU) &&
            !IsKeyPressed(VK_TAB))
            for (int i = 0; i < KeysCount; ++i)
                if (p->vkCode == QwertyKb[i])
                {
                    handled = true;
                    Kbe(i);
                }
    if (handled)
        return 1;
    return CallNextHookEx(keyboardHook, nCode, wParam, lParam);
}
inline LRESULT CALLBACK KeyboardProc(int nCode, WPARAM wParam, LPARAM lParam)
{
    const auto p = reinterpret_cast<PKBDLLHOOKSTRUCT>(lParam);
    auto handled = false;
    if (wParam == WM_KEYDOWN)
        if (p->dwExtraInfo != 1 << 24 &&
            !IsKeyPressed(VK_CONTROL) &&
            !IsKeyPressed(VK_LWIN) &&
            !IsKeyPressed(VK_RWIN) &&
            !IsKeyPressed(VK_MENU) &&
            !IsKeyPressed(VK_TAB))
            for (auto i = 0; i < KeysCount; ++i)
                if (p->vkCode == QwertyKb[i])
                {
                    handled = true;
                    Kbe(i);
                }
    if (handled)
        return 1;
    return CallNextHookEx(KeyboardHook, nCode, wParam, lParam);
}

KeyboardProc()开始看,首先p储存了键盘事件,即当前按下或抬起了什么键。handled是一个临时标志,表示有没有对键盘事件进行处理,表示是否对键盘事件进行处理,处理了为true

下面就要写除了快捷键以外的键位修改了,一般来说快捷键开头都是CtrlWinAltTab以及它们的组合(Shift一般不会出现在第一个,因为Shift按下会转换符号或者转换大小写),所以当以上四个键(左右Win键算同一个)按下时就不处理,其他才会处理,当确定要处理时,令handled变为true

IsKeyPressed():当按键按下时,GetKeyState()返回值(SHORT类型)的最高位为1,否则为0,所以与图中1<<(sizeof(SHORT)*8-1)按位与结果不为0就是按下,为0就是没按下。

下面是一个for循环,找到原键位的键后映射到新的键,用Kbe()keybd_event())进行处理。有四个参数,第一个填虚拟键值,之前已经定义好;第二个填扫描码(可见上个专栏),但可以不填;第三填选项标志,键抬起时为KEYEVENTF_KEYUP,落下时为0,此处填0;最后一个是附加信息,要填1<<24因为根据MSDN[2],最后一个ULONG_PTR类型参数对应了p中的dwExtraInfo,可以传递额外的信息,而dwExtraInfo只有25-28位是保留的,其他都会被其他的信息占据,所以填1<<24(刚好到25位)。如果不是我们目标的键,进入default,也不处理。

最后,如果处理了数据就返回1,表示屏蔽原来的事件并发送已编辑的新事件,第二次再被抓获时会因为dwExtraInfo的标志而直接不处理跳过;如果不处理数据则直接放行事件,并让下一个钩子再处理。

综上,一个KeyboardCorrector项目就写完了,可以完成预设的任务,并有自定义键位的改进空间。

注:关闭软件时可以用任务管理器,也可以在程序里设置快捷键关闭。

完整代码(Github)

C:https://github.com/Poker-sang/KeyboardCorrector/blob/main/KeyboardCorrector.c

C++(C++/CLI):https://github.com/Poker-sang/KeyboardCorrector/blob/main/KeyboardCorrector/KeyboardCorrector.h


  1. Wikipedia ↩︎

  2. 微软文档 ↩︎

标签:键位,auto,VK,C++,键盘,OEM,define,Win32API
From: https://www.cnblogs.com/pokersang/p/17964162

相关文章

  • C#与C++代码的互操作方式
    title:C#与C++代码的互操作方式date:2024-01-10categories:编程tags:-C#-.NET-C++-COM-平台调用大致介绍在写C#程序时经常有与本地代码(C/C++)代码交互的需求。微软提供了许多种方式供我们选择,最常用的有以下三种(A->B指A可以引用B):flowchartLRA--P/Invoke......
  • C++U6-02-最短路算法1-dijkstra迪杰斯特拉最短路径
    学习目标 最短路径的基本概念  练习1 最短路的定义 本节课迪杰斯特拉dijkstra最短路算法 算法流程:以下是Dijkstra最短路径算法的逐步计算松弛的过程:初始化起始节点的距离为0,其他节点的距离为无穷大。选择当前距离最小且未被访问的节点作为当前节点。......
  • 为什么C++ 单例局部static初始化是线程安全的?
    为什么C++单例局部static初始化是线程安全的?constbg::AppSettings&bg::AppSettings::GetInstance(){staticAppSettingsinstance;returninstance;}对于以上单例模式代码,在C++11(及更高版本)中,函数局部静态AppSettings的构造保证是线程安全的。编译器将在AppS......
  • Python与C++联合编程
    C++代码#编译指令#gcc-otest.so-shared-fPICtest.cg++-otest.so-shared-fPICtest.cc#forc++#include<iostream>usingnamespacestd;extern"C"{//forC++intfoo(inta,intb){cout<<"a:"<<a<<&qu......
  • string 字符串用法C++
    substr() c_str() size()/length()  empty() clear() #include<iostream>#include<cstring>#include<cstdio>#include<algorithm>#include<vector>usingnamespacestd;intmain(){stringa="abc";......
  • C++ vector和set排序效率比较
    转自:https://blog.csdn.net/adaptiver/article/details/529257921.介绍vector+sort实际是快排,快速排序是目前已知的所有排序算法中最快的排序算法。例子:#include<vector>#include<set>#include<algorithm>#include<stdio.h>#include<string.h>#include<unistd......
  • C++中for_each用法学习
    转自:chatgpt1.介绍std::for_each是C++标准库中的一个算法,用于对指定范围内的元素执行指定的操作。它的一般形式如下:template<classInputIt,classUnaryFunction>UnaryFunctionfor_each(InputItfirst,InputItlast,UnaryFunctionf);first和last是表示范围的......
  • C++U3-第09课-递归函数的应用
    学习目标 斐波那契数列例题  我们需要求出斐波那契第n项的值是多少【思路分析】我们用递归的方式去求解,当第一项和第二项返回1,否则返回前两项的和当前为第一项和第二项返回1当前不为第一项和第二项返回前两项的和定义n并把n输入,带入到递归求解【参考代......
  • C++ 学习宝藏网站分享
    C++学习宝藏网站分享1.C++在线参考手册Cppreferencehttps://zh.cppreference.comC++开发者必备的在线参考手册,是我最常访问的C++网站之一。作为参考手册,不仅包含了语言本身的词法、语法特性,还包含了对C++标准库的介绍:需要include哪个头文件、接口参数/返回值说明......
  • C++源码中司空见惯的PIMPL是什么?
    前言:C++源码中司空见惯的PIMPL是什么?用原始指针、std::unique_ptr和std::shared_ptr指向Implementation,会有什么不同?优缺点是什么?读完这篇文章,相信你能搞懂这种设计方式并将其运用于实践,也将更容易阅读源码。1.PIMPL是什么?PIMPL是PointertoIMPLementation的缩写,意思是指......