当前位置: 首页 > news >正文

关于按键映射软件的探索(其一)

        那么先说结论——重构了一次,我还是失败了,失败于拓展调整个性化的设计,不过我还是实现了按键监测然后显示的功能。只不过是说我对于WPF软件等的封装和软窗口的功能还是不怎么熟悉。

        

引言

        在许多游戏玩家中,高难度操作(高APM)复现始终是技术提升的核心,而在各类剪辑、特效、建模软件教学视频中,“快捷键教学”也逐渐成为主流。为此,我希望实现一个全局按键监听器,能以视觉化方式实时展示当前操作,辅助观众更好地理解、复现操作步骤。

        按键检测读取,注意不是按键精灵,前者多半是为了教学中的复现,自证,亦或是实现某种真实在进行的直播动作直播效果。而后者我的理解是一种按键宏,也就是传统意义上的外挂。我们观看游戏直播时,很多主播会使用到,教学视频方面,我观察到的更多是belender软件的教学和使用。毕竟学会更多的快捷键,就可以大大提高生产效率。但是我的目光聚焦在这个按键映射软件本身,于是我进行了开发,与大G老师深入交流。

🛠️ 项目目标

        1.实现全局键盘与鼠标监听。

        2.监听操作后在屏幕左下角浮现按键组合。

        3.拓展配置:控制显示位置、字体缩放、最大数量等个性化设置。(失败啦!)

开发(C#)(第一次)

        通过第三方库 Gma.System.MouseKeyHook 监听全局按键,然后把每次捕获到的按键以文字的形式展示在屏幕右下角,用 WPF 搭配 StackPanel + Border + TextBlock 动态生成显示框。

        

        直接用按键转换包,按下那个按键就可以自动从码值转换成按键对应的文本。

        Title="KeyCaster"Height="450"Width="800"WindowStyle="None"AllowsTransparency="True"Background="Transparent"

        最后用窗体大小和生成位置调整最后我们需要显示提示的位置,这个窗体透明就可以。

        然后奉上按键映射监测的核心代码:

using System;
using System.Collections.Generic;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Media.Animation;
using System.Windows.Threading;
using Gma.System.MouseKeyHook;namespace WpfApp1
{public partial class MainWindow : Window{#region 字段定义private IKeyboardMouseEvents _globalHook;private readonly List<string> _keyList = new();private DateTime _lastInputTime = DateTime.MinValue;private Border _activeBlock = null;private DispatcherTimer _groupTimer;private double _scaleFactor = 1.0;private Thickness _screenOffset = new Thickness(0, 0, 20, 20);#endregion#region 构造函数与初始化public MainWindow(){InitializeComponent();Console.WriteLine("MainWindow 构造函数执行");StartGlobalHook();var settings = SettingsManager.Load();_scaleFactor = settings.Scale;_screenOffset = new Thickness(settings.OffsetX, settings.OffsetY, 20, 20);}private void StartGlobalHook(){Console.WriteLine("尝试注册键鼠监听");_globalHook = Hook.GlobalEvents();_globalHook.KeyDown += OnInputEvent;_globalHook.MouseDown += OnInputEvent;Console.WriteLine("全局钩子已启动");}#endregion#region 输入处理private void OnInputEvent(object sender, EventArgs e){Console.WriteLine($"捕获事件:{e}");var now = DateTime.Now;string inputStr = e switch{System.Windows.Forms.KeyEventArgs keyEvent => keyEvent.KeyCode.ToString(),System.Windows.Forms.MouseEventArgs mouseEvent => mouseEvent.Button switch{System.Windows.Forms.MouseButtons.Left => "MouseLeft",System.Windows.Forms.MouseButtons.Right => "MouseRight",System.Windows.Forms.MouseButtons.Middle => "MouseMiddle",_ => mouseEvent.Button.ToString()},_ => string.Empty};if (string.IsNullOrEmpty(inputStr))return;double interval = (now - _lastInputTime).TotalSeconds;_lastInputTime = now;if (interval <= 0.5 && _activeBlock != null){_keyList.Add(inputStr);string formatted = FormatKeyList(_keyList);UpdateActiveDisplay(formatted);ResetGroupTimer();}else{if (_activeBlock != null){StartFadeOut(_activeBlock);_activeBlock = null;}_keyList.Clear();_keyList.Add(inputStr);string formatted = FormatKeyList(_keyList);ShowNewDisplay(formatted);ResetGroupTimer();}}private string FormatKeyList(List<string> keys){var sb = new StringBuilder();for (int i = 0; i < keys.Count; i++){string current = keys[i];bool currentIsLetter = current.Length == 1 && current[0] >= 'A' && current[0] <= 'Z';if (i > 0){string previous = keys[i - 1];bool previousIsLetter = previous.Length == 1 && previous[0] >= 'A' && previous[0] <= 'Z';if (!(previousIsLetter && currentIsLetter)){sb.Append(" + ");}}sb.Append(current);}return sb.ToString();}#endregion#region UI 显示与更新private void ShowNewDisplay(string text){_activeBlock = new Border{Background = System.Windows.Media.Brushes.Black,Opacity = 0.8,CornerRadius = new CornerRadius(10),Padding = new Thickness(10),Margin = _screenOffset,LayoutTransform = new ScaleTransform(_scaleFactor, _scaleFactor),Child = new TextBlock{Text = text,Foreground = System.Windows.Media.Brushes.White,FontSize = 20}};KeyDisplayPanel.Children.Add(_activeBlock);}private void UpdateActiveDisplay(string text){if (_activeBlock != null){((TextBlock)_activeBlock.Child).Text = text;}}private void ResetGroupTimer(){_groupTimer?.Stop();_groupTimer = new DispatcherTimer{Interval = TimeSpan.FromSeconds(0.5)};_groupTimer.Tick += (s, e) =>{_groupTimer.Stop();if (_activeBlock != null){StartFadeOut(_activeBlock);_activeBlock = null;}};_groupTimer.Start();}#endregion#region 动画淡出private void StartFadeOut(Border border){var animation = new DoubleAnimation{From = border.Opacity,To = 0.0,Duration = TimeSpan.FromSeconds(3),FillBehavior = FillBehavior.HoldEnd};animation.Completed += (s, e) =>{KeyDisplayPanel.Children.Remove(border);};border.BeginAnimation(UIElement.OpacityProperty, animation);}private void OpenSettings(){var settingsWindow = new SettingsWindow();settingsWindow.ShowDialog();var settings = SettingsManager.Load();_scaleFactor = settings.Scale;_screenOffset = new Thickness(settings.OffsetX, settings.OffsetY, 20, 20);}#endregion#region 清理资源protected override void OnClosed(EventArgs e){_globalHook.KeyDown -= OnInputEvent;_globalHook.MouseDown -= OnInputEvent;_globalHook.Dispose();base.OnClosed(e);}#endregion}
}

        使用到的字段:监听器对象、按键队列、动画定时器、缩放与位移。

        加入组合键监听,使用‘+’连接

private string FormatKeyList(List<string> keys)
{var sb = new StringBuilder();for (int i = 0; i < keys.Count; i++){string current = keys[i];bool currentIsLetter = current.Length == 1 && current[0] >= 'A' && current[0] <= 'Z';if (i > 0){string previous = keys[i - 1];bool previousIsLetter = previous.Length == 1 && previous[0] >= 'A' && previous[0] <= 'Z';if (!(previousIsLetter && currentIsLetter)){sb.Append(" + ");}}sb.Append(current);}return sb.ToString();
}

        加上鼠标监听,和键盘的要放在一起,毕竟光有按键的同时,有些操作仍然需要鼠标的参与

private void OnInputEvent(object sender, EventArgs e)
{string inputStr = e switch{System.Windows.Forms.KeyEventArgs keyEvent => keyEvent.KeyCode.ToString(),System.Windows.Forms.MouseEventArgs mouseEvent => mouseEvent.Button switch{System.Windows.Forms.MouseButtons.Left => "MouseLeft",System.Windows.Forms.MouseButtons.Right => "MouseRight",System.Windows.Forms.MouseButtons.Middle => "MouseMiddle",_ => mouseEvent.Button.ToString()},_ => string.Empty};if (string.IsNullOrEmpty(inputStr))return;// 以下省略……
}

        最后加入最大的框体数限制,获取连续输入的最大时间间隔(我这里用了0.5s),同时保证单纯输入A~Z的字母的时候不需要使用+连接,设置每个框体的淡出时间防止遮挡视野。。。

private void ResetGroupTimer()
{_groupTimer?.Stop();_groupTimer = new DispatcherTimer{Interval = TimeSpan.FromSeconds(0.5)};_groupTimer.Tick += (s, e) =>{_groupTimer.Stop();if (_activeBlock != null){StartFadeOut(_activeBlock);_activeBlock = null;}};_groupTimer.Start();
}
private void StartFadeOut(Border border)
{var animation = new DoubleAnimation{From = border.Opacity,To = 0.0,Duration = TimeSpan.FromSeconds(3),FillBehavior = FillBehavior.HoldEnd};animation.Completed += (s, e) =>{KeyDisplayPanel.Children.Remove(border);};border.BeginAnimation(UIElement.OpacityProperty, animation);
}

        我们得到了。。。电脑右下角的按键提示!

大概就是这个效果

重构(第二次)

        本来想着加功能,做一个设置调试的,但是“钩子”就是不触发!!!可以说是我菜,但是我想象中的逻辑没有跑通,设置对于另外一个窗口毫无影响,虽然只是一些改变量的事情。。。实际上这是一个多窗口多事件的软件,因此我就卡在这里了。相信我一定有机会进行下一次重构,这样我就可以打包并且发放出来啦!

总结

        我尝试通过 WPF 实现托盘图标与隐藏窗口的控制、通过设置窗口更改显示行为,但遇到了一些实际上的限制,比如窗口状态变更后监听可能失效、MainWindow 的引用生命周期等问题。虽然“个性化拓展”功能暂时搁浅,但这也让我意识到未来如何规划组件化开发更加合理

        期待我下一次重构他的时候,可能会是下周,也可能会是明年,不过,我都记着呢!

                                                                                                                        ——By;Oldmeat

http://www.xdnf.cn/news/111745.html

相关文章:

  • STM32F407使用ESP8266实现阿里云OTA(下)
  • postgis:添加索引时提示“对访问方法 gist 数据类型 geometry 没有默认的操作符表“
  • 将视频生成视频二维码步骤
  • 深入浅出学会函数(下)
  • 【霍夫变换】图像处理(OpenCV)-part11
  • 【阿里云大模型高级工程师ACP习题集】2.4 自动化评测答疑机器人的表现(⭐️⭐️⭐️ 重点章节!!!)
  • 数据结构-图
  • HOW - Code Review 流程自动化
  • 学习threejs,使用EffectComposer后期处理组合器(采用RenderPass、ShaderPass渲染通道),案例一
  • 17.第二阶段x64游戏实战-人工遍历二叉树结构
  • 给git配置SSH(github,gitee)
  • 【前端】【业务场景】【面试】在前端项目中,当涉及大量数据渲染时,如何提高渲染性能并避免页面卡顿?
  • uniapp 安卓离线本地打包,Android Studio生成apk包
  • 27、Session有什么重⼤BUG?微软提出了什么⽅法加以解决?
  • Linux 命令行与 vi/vim 编辑器完全指南
  • continue插件实现IDEA接入本地离线部署的deepseek等大模型
  • 想要从视频中提取背景音乐怎么搞?其实视频提取音频非常简单
  • 金融系统上云之路:云原生后端架构在金融行业的演化与实践
  • 以太网的mac帧格式
  • 基于Vulkan Specialization Constants的材质变体系统
  • 第三篇:Django创建表关系及生命周期流程图
  • Java后端开发面试题(含答案)
  • java kafka
  • 初始https附带c/c++源码使用curl库调用
  • Java 调用webservice接口输出xml自动转义
  • 关于 xpath 查找 XML 元素的一点总结
  • 2025新版懒人精灵零基础及各板块核心系统视频教程-全分辨率免ROOT自动化开发
  • Docker从0-1搭建个人云盘(支持Android iOS PC)
  • 关于Safari浏览器在ios<16.3版本不支持正则表达式零宽断言的解决办法
  • 汽车自动驾驶介绍