某天写代码时突发奇想想知道自己用那个键用的最多,于是在网上搜了一通键盘记录软件,没找到一个满意的,于是决定自己写一个出来
ps:第一次用 C# 写 Windows 程序,可能很多地方不够规范
先撸一个简单的界面出来,大概长这个样子
列表表头这样设置,id宽度为0且不可调整(即隐藏该列),每列分别是:用于排序的id,按键中文名,按键名,次数
添加加一个托盘图标和菜单
再写一个数据管理的类,根据 Windows 键盘表的顺序把需要记录的键写到一个数组里去
using System;
using System.IO;
using System.Windows.Forms;
namespace KeyboardRecord {
public class Data {
public string[] nameZh = {
"退格","Tab","回车","大写锁","Esc",
"空格",
"PageUp","PageDown","End","Home",
"左","上","右","下",
// 14
"插入","删除",
// 16
"0","1","2","3","4","5","6","7","8","9",
"右括号","感叹号","@","井号","$","百分号","^","&","星号","左括号",
// 36
"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z",
// 62
"Win","App",
// 64
"F1","F2","F3","F4","F5","F6","F7","F8","F9","F10","F11","F12",
// 76
"左Shift","右Shift","左Ctrl","右Ctrl","左Alt","右Alt",
// 82
"分号","等于号","逗号","横线(减号)","点","斜杠","`","左中括号","反斜杠","右中括号","单引号",
// 93
"冒号","加号","小于号","下划线","大于号","问号","~","左花括号","|","右花括号","双引号",
// 104
"复制","粘贴","撤销","保存","剪切"
};
public string[] name = {
// 0
"Back","Tab","Enter","Caps","Esc",
// 5
"Space",
"PageUp","PageDown","End","Home",
"Left","Up","Right","Down",
// 14
"Insert","Delete",
// 16
"0","1","2","3","4","5","6","7","8","9",
")","!","@","#","$","%","^","&","*","(",
// 36
"A","B","C","D","E","F","G","H","I","J","K","L","M","N","O","P","Q","R","S","T","U","V","W","X","Y","Z",
// 62
"Win","App",
// 64
"F1","F2","F3","F4","F5","F6","F7","F8","F9","F10","F11","F12",
// 76
"LShift","RShift","LCtrl","RCtrl","LAlt","RAlt",
// 82
";","=",",","-",".","/","`","[","\\","]","'",
// 93
":","+","<","_",">","?","~","{","|","}","\"",
// 104
"Ctrl+C","Ctrl+V","Ctrl+Z","Ctrl+S","Ctrl+X"
};
// 存放按键次数的数组
public int[] times;
public Data() {
loadData();
}
public void loadData() {
times = new int[name.Length];
// 从本地的文件读取数据
string path = Path.Combine((Application.StartupPath + @"\"),"data.csv");
try {
if(!File.Exists(path)) return;
using StreamReader reader = new StreamReader(new FileStream(path,FileMode.Open,FileAccess.Read,FileShare.None));
for(int i = 0;i<name.Length;i++) {
// 一次读一行数据
string line = reader.ReadLine();
string[] sp = line.Split('\t');
times[i] = int.Parse(sp[1]);
}
} catch(Exception e) {
// 读取遇到问题就从上一次保存的文件恢复数据(我可不想辛辛苦苦记录的数据说没就没了)
File.Delete(path);
if(File.Exists(path + ".bak")) {
File.Move(path + ".bak",path);
// 重新载入数据
loadData();
MessageBox.Show("载入数据时出错,尝试从备份文件中恢复\n"+e.Message,"错误");
} else {
MessageBox.Show("载入数据时出错!\n"+e.Message,"错误");
}
}
}
// 保存记录的数据
public void saveData() {
string path = Path.Combine((Application.StartupPath + @"\"),"data.csv");
try {
// 把之前保存的数据备份
if(File.Exists(path)) {
if(File.Exists(path + ".bak")) File.Delete(path + ".bak");
File.Copy(path,path + ".bak");
}
using StreamWriter writer = new StreamWriter(new FileStream(path,FileMode.Create,FileAccess.Write,FileShare.None));
for(int i = 0;i<name.Length;i++) {
// 一行的数据格式为:名称 + Tab + 按键次数
writer.Write(name[i]);
writer.Write('\t');
writer.Write(times[i].ToString());
writer.Write("\r\n");
}
} catch(Exception e) {
if(File.Exists(path)) File.Delete(path);
MessageBox.Show("保存数据时出错!\n"+e.Message,"错误");
}
}
}
}
把界面交互做出来
using System;
using System.Collections;
using System.Windows.Forms;
namespace KeyboardRecord {
public partial class Form1 : Form {
// 储存按键次数的类
private Data data;
// 监听按键按下的钩子类
private KeyboardHook keyboardHook;
// 用于给列表元素排序的类
private ListViewItemComparer comparer;
public Form1() {
InitializeComponent();
// 给托盘菜单的选项添加点击事件
show.Click += ShowApp;
exit.Click += ExitApp;
}
private void MainLoad(object sender,EventArgs e) {
// 初始化需要用到的类
data = new Data();
keyboardHook = new KeyboardHook(data);
comparer = new ListViewItemComparer();
keyList.ListViewItemSorter = comparer;
initList();
}
// 初始化列表
private void initList() {
// 把记录的数据加到ListView里去
for(int i = 0;i<data.name.Length;i++) {
keyList.Items.Add(new ListViewItem(new string[] { i.ToString(), data.name[i],data.nameZh[i],data.times[i].ToString() }));
}
}
// 双击托盘图标显示程序主界面
private void IconDoubleClick(object sender,MouseEventArgs e) {
ShowApp(null,null);
}
// 监听窗口最小化
private void MainSizeChanged(object sender,EventArgs e) {
if(WindowState == FormWindowState.Minimized) {
// 最小化的时候在任务栏里隐藏
this.ShowInTaskbar = false;
}
}
// 监听右上角关闭按钮
private void MainFormClosing(object sender,FormClosingEventArgs e) {
// 让窗口最小化,而不是退出程序
WindowState = FormWindowState.Minimized;
// 取消点击事件,让系统不会关闭程序
e.Cancel = true;
}
// 显示主界面
private void ShowApp(object sender,EventArgs e) {
if(WindowState == FormWindowState.Minimized) {
// 还原窗口
WindowState = FormWindowState.Normal;
Activate();
// 重新在任务栏里显示
this.ShowInTaskbar = true;
}
}
// 确认是否退出
private void ExitApp(object sender,EventArgs e) {
if(MessageBox.Show("退出程序?","退出",MessageBoxButtons.OKCancel,MessageBoxIcon.Question) == DialogResult.OK) {
// 保存数据
data.saveData();
// 停止监听按键
keyboardHook.Stop();
notifyIcon.Visible = false;
// 关闭所有的线程
this.Dispose();
this.Close();
}
}
// 刷新列表
private void UpdateList() {
// 移除现有的所有Item
keyList.Items.Clear();
initList();
}
// 启动按钮
private void status_Click(object sender,EventArgs e) {
if(keyboardHook.IsStarted) {
// 停止监听
keyboardHook.Stop();
status.Text = "开始";
Invalidate();
} else {
// 启用钩子,监听按键操作
keyboardHook.Start();
status.Text = "停止";
Invalidate();
}
}
// 导出按钮
private void explore_Click(object sender,EventArgs e) {
data.saveData();
MessageBox.Show("导出完成!","完成");
}
// 清除按钮
private void clear_Click(object sender,EventArgs e) {
if(MessageBox.Show("确定清除记录的次数?","清除",MessageBoxButtons.OKCancel,MessageBoxIcon.Question) == DialogResult.OK) {
data.times = new int[data.name.Length];
UpdateList();
}
}
// List表头点击监听
private void keyList_SelectedIndexChanged(object sender,ColumnClickEventArgs e) {
// 设置点击的列
comparer.setIndex(e.Column);
// 执行排序
keyList.Sort();
}
}
public class ListViewItemComparer : IComparer {
// 是否反向排序
private bool flip = true;
// 要排序的列
private int index;
public int Compare(object x,object y) {
// 获取需要排序的文本
string s1 = ((ListViewItem)x).SubItems[index].Text;
string s2 = ((ListViewItem)y).SubItems[index].Text;
// 正向排序/反向排序
if(flip) return int.Parse(s1) > int.Parse(s2) ? 1 : -1;
else return int.Parse(s1) > int.Parse(s2) ? -1 : 1;
}
public void setIndex(int index) {
// 当点击的是按键次数的列时按按键次数排序,否则就按id排序
if(index==3) this.index = 3;
else this.index = 0;
// 反向排序
flip = !flip;
}
}
}
然后就到了最重要的按键监听记录的类了,不清楚怎么监听按键,于是网上找了段代码,一通修改
using System;
using System.Runtime.InteropServices;
using System.Reflection;
using System.Windows.Forms;
namespace KeyboardRecord {
// 钩子类,负责监听键盘点击事件
public abstract class GlobalHook {
#region Windows API Code
[StructLayout(LayoutKind.Sequential)]
protected class KeyboardHookStruct {
public int vkCode;
public int scanCode;
public int flags;
public int time;
public int dwExtraInfo;
}
[DllImport("user32.dll",CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall,SetLastError = true)]
protected static extern int SetWindowsHookEx(int idHook,HookProc lpfn,IntPtr hMod,int dwThreadId);
[DllImport("user32.dll",CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall,SetLastError = true)]
protected static extern int UnhookWindowsHookEx(int idHook);
[DllImport("user32.dll",CharSet = CharSet.Auto, CallingConvention = CallingConvention.StdCall)]
protected static extern int CallNextHookEx(int idHook,int nCode,int wParam, IntPtr lParam);
[DllImport("user32")]
protected static extern int ToAscii( int uVirtKey, int uScanCode, byte[] lpbKeyState, byte[] lpwTransKey, int fuState);
[DllImport("user32")]
protected static extern int GetKeyboardState(byte[] pbKeyState);
[DllImport("user32.dll",CharSet = CharSet.Auto,CallingConvention = CallingConvention.StdCall)]
protected static extern short GetKeyState(int vKey);
protected delegate int HookProc(int nCode,int wParam,IntPtr lParam);
protected const int WH_KEYBOARD_LL = 13;
protected const int WM_KEYDOWN = 0x100;
protected const int WM_KEYUP = 0x101;
protected const int WM_SYSKEYDOWN = 0x104;
protected const int WM_SYSKEYUP = 0x105;
protected const byte VK_SHIFT = 0x10;
protected const byte VK_LSHIFT = 0xA0;
protected const byte VK_RSHIFT = 0xA1;
protected const byte VK_LCONTROL = 0xA2;
protected const byte VK_RCONTROL = 0x3;
protected const byte VK_LALT = 0xA4;
protected const byte VK_RALT = 0xA5;
#endregion
#region 私有变量
protected int _hookType;
protected int _handleToHook;
protected bool _isStarted;
protected HookProc _hookCallback;
#endregion
#region Properties
public bool IsStarted {
get {
return _isStarted;
}
}
#endregion
#region Constructor
public GlobalHook() {
Application.ApplicationExit += new EventHandler(Application_ApplicationExit);
}
#endregion
#region Methods
public void Start() {
if(!_isStarted && _hookType != 0) {
// Make sure we keep a reference to this delegate!
// If not, GC randomly collects it, and a NullReference exception is thrown
_hookCallback = new HookProc(HookCallbackProcedure);
_handleToHook = SetWindowsHookEx( _hookType, _hookCallback,Marshal.GetHINSTANCE(Assembly.GetExecutingAssembly().GetModules()[0]),0);
// Were we able to sucessfully start hook?
if(_handleToHook != 0) {
_isStarted = true;
}
}
}
public void Stop() {
if(_isStarted) {
UnhookWindowsHookEx(_handleToHook);
_isStarted = false;
}
}
protected virtual int HookCallbackProcedure(int nCode,Int32 wParam,IntPtr lParam) {
// This method must be overriden by each extending hook
return 0;
}
protected void Application_ApplicationExit(object sender,EventArgs e) {
if(_isStarted) {
Stop();
}
}
#endregion
}
public class KeyboardHook : GlobalHook {
// 储存按键次数的类
public Data data;
public KeyboardHook(Data data) {
this.data = data;
_hookType = WH_KEYBOARD_LL;
}
// 监听的回调方法
protected override int HookCallbackProcedure(int nCode,int wParam,IntPtr lParam) {
if(nCode > -1) {
KeyboardHookStruct keyboardHookStruct = (KeyboardHookStruct)Marshal.PtrToStructure(lParam,typeof(KeyboardHookStruct));
// 只需要监听按下事件
if(wParam == WM_KEYDOWN || wParam == WM_SYSKEYDOWN) {
// 获取按键的代码
int code = keyboardHookStruct.vkCode;
// 根据按键代码让对应数据的数据+1
// 只要合理设计一下数组就能大幅减少if-else的数量
// 根据code算出数组储存的位置
if(code<32) {
switch(code) {
case 8:data.times[0]++;break;// back
case 9: data.times[1]++; break;// tab
case 13: data.times[2]++; break;// enter
case 20: data.times[3]++; break;//caps
case 27: data.times[4]++; break;// esc
}
} else if(code<41){
// 方向键区域
data.times[code-27]++;
}else if(code>47&&code<65) {
// 如果shift也是按下状态的话数据的就是数字上面的符号
if(GetKeyState(VK_LSHIFT) + GetKeyState(VK_RSHIFT) != 0) data.times[code-22]++;
// 0-9
else data.times[code-32]++;
}else if(code>64&&code<91) {
// a-z
// 如果此时ctrl也是按下状态
if(GetKeyState(162)!=0) {
// ctrl 组合键
if(code==67) data.times[104]++;
else if(code==86) data.times[105]++;
else if(code==90) data.times[106]++;
else if(code==83) data.times[107]++;
else if(code==88) data.times[108]++;
}else data.times[code-29]++;
}else if(code>95&&code<106) {
// 小键盘0-9
data.times[code-80]++;
}else if(code<112) {
// 不常用的键和小键盘符号
switch(code) {
case 45: data.times[14]++;break;// ins
case 46: data.times[15]++; break;// del
case 91: data.times[62]++; break; // win
case 93: data.times[63]++; break;// apps
case 106: data.times[34]++; break;// 乘
case 107: data.times[94]++; break;// 加
case 109: data.times[85]++; break;// 减
case 110: data.times[86]++; break;// 小数点
case 111: data.times[87]++; break;// 除
}
}else if(code<124) {
// F1-F12
data.times[code-48]++;
}else if(code>159&&code<166) {
//left right shift left right ctrl
data.times[code-84]++;
}else if(code>185&&code<193) {
// ;=,-./`[\]'
if(GetKeyState(VK_LSHIFT) + GetKeyState(VK_RSHIFT) != 0) data.times[code-93]++;
else data.times[code-104]++;
}else if(code>218&&code<223) {
if(GetKeyState(VK_LSHIFT) + GetKeyState(VK_RSHIFT) != 0) data.times[code-119]++;
else data.times[code-130]++;
}
}
}
return CallNextHookEx(_handleToHook,nCode,wParam,lParam);
}
}
}
至此,一个键盘记录软件就差不多完成了
用了一段时间后发现我用的最多的键居然是退格
最后附上项目github链接:https://github.com/2891954521/KeyboardRecord