基于C#的全局转义程序Conversion使用说明和算法分析

功能概述

Conversion是一款基于 .net 和 C# 运行于Windows环境下的一款工具软件。它能够帮助你快速的插入特殊符号,启动设定好的程序(甚至可以带上参数)或者从设置好的源代码文件中复制代码段并自动粘贴。

其实这玩意儿是某人让我帮忙写的作业然后感觉很有意思魔改了一下……

使用说明

首先,我们需要自行编写一个表,它规定了需要转义的输入字符串和对应的要执行的操作。它应该被命名为 table.txt 并和主程序 Conversion.exe 置于同一目录下。

表里的内容应当遵守以下规则:

  • 每一行的格式应当是这样的:需要转义的字符,操作
  • 逗号(半角)用于分隔转义字符和操作,不要输入任何多余的空格
  • 键盘上的竖线/反斜杠按键是程序的启动按钮,在按下这个按钮后程序将开始监听输入准备进行转义
  • 空格键是转义过程的终止TAG,在启动过程之后按下空格就会对已输入的内容进行转义
  • 区分大小写
  • “\run”和”\code”是动作TAG,动作TAG和具体的操作内容之间应当用空格分隔
  • 当你需要给启动的程序设置启动参数的时候,你可以用“|”竖线来分隔参数和启动路径

实例

  1. 只插入符号

    aleph,ℵ
    当你输入 aleph 进行转义时,将直接在光标处插入ℵ

  2. 打开一个外部程序

    chrome,\run C:\Program Files (x86)\Google\Chrome\Application\chrome.exe
    如果你只是想打开一个外部程序,只需要按照上面的格式即可。
    partitionC,\run explorer.exe| c:
    记住,参数前的空格非常关键,比如这里正确的格式是“| c:”而不是“|c:”。

  3. 粘贴代码片段

    myCode,\code D:\1.ts
    如果源代码文件和主程序同目录,那么也可以用下面的写法
    myCode,\code 1.ts
    文件路径前的空格也是非常关键的。

其他说明

Conversion使用了Win32API中的键盘钩子,可能会被当做病毒,但是源码都在GitHub上面了……有没有害一目了然。也正因为此,下载源码自行编译并使用可能是最安全的方法。

GitHub:https://github.com/ChaoShengze/Conversion

算法分析

基本工作方式

首先需要明确一点,那就是因为软件希望可以全局进行转义操作,那么这就注定了只能够以键盘钩子的方式捕获一个按键和一个按键的输入来进行判断而不是直接接受一个字符串来进行判断和处理。
程序在启动时需要挂载钩子,在关闭时应当卸载相应的钩子。相关的方法只需要写入窗体的Load和Closing过程中。
程序并不是始终在捕获输入,而是在接收到启动命令(也就是按下了反斜杠键)才开始捕获输入,将有效的按键信息写入一个String变量。同时程序在接收到结束命令(按下了空格键)时根据记录的按键输入来进行判断。
下面分析具体的实现方法。

按键的响应处理

先说说按键的响应处理,对于当前的这个程序来说,键盘上面有这些键比较特殊:反斜杠(用于启动)、空格(用于终止)、退格(删除一个待转义字符)、左右Shift(涉及大小写)、大写锁定(涉及大小写)。
理论上来说,按键的判定只需要用一个switch判断即可。但是实际上,按键的捕获包含的信息可能不止数字和字母,因此部分按键在switch中需要特殊处理,确保String变量中存储的是有效的内容。个人觉得比较好的做法是在switch中把特殊按键case掉进行处理,用default来接受有效的输入。
当然了,键盘上特殊的按键太多,这里只对使用较多的按键进行处理,其他不予考虑。

大小写区分的实现

大小写区分的实现是比较有意思的。因为我们是捕获键盘的按键输入而非接受一个String,因此单单靠对字母键的捕获是无法进行判断的。那么怎么处理呢?
通过键盘输入大小写字符无非四种情况:

  1. 大写锁定打开,Shift没有按住,此时是大写
  2. 大写锁定打开,Shift按住,此时是小写
  3. 大写锁定关闭,Shift没有按住,此时是小写
  4. 大写锁定关闭,Shift按住,此时是大写

只需要对上述四种情况进行判断就能够知道按下的字母键代表的其实是大写还是小写了。当然我们需要使用Windows的API来获取大写锁定按钮和Shift按钮的状态。
大小写区分和按钮响应处理的代码如下:

        /// 
        /// 判断输入的字符的大小写,根据CapsLock和Shift按键的状况共计四种情况
        /// 重要:因为实际是捕获键盘的按键输入而不是捕获输入了的文本,所以需要自行判断按键输入的是大写还是小写字母
        /// 
        private static void isUpper()
        {
            if (CapsLock == true && ShiftStatus == true)
            {
                Upper = false;
            }
            else if (CapsLock == false && ShiftStatus == true)
            {
                Upper = true;
            }
            else if (CapsLock == true && ShiftStatus == false)
            {
                Upper = true;
            }
            else if (CapsLock == false && ShiftStatus == false)
            {
                Upper = false;
            }
        }

        /// 
        /// CapsLockStatus
        /// 
        public static bool CapsLockStatus
        {
            get
            {
                byte[] bs = new byte[256];
                Win32API.GetKeyboardState(bs);
                return (bs[0x14] == 1);
            }
        }

        /// 
        /// ShiftStatus
        /// 
        public static bool ShiftStatus
        {
            get
            {
                int keyNum = 16;
                return (Win32API.GetKeyState(keyNum) < 0);
            }
        }

        /// 
        /// 按键捕获响应函数
        /// 
        private void HookKeyDown(object sender, KeyEventArgs e)
        {
            bool needWrite = false; // 判断是否要将捕获的这次按键输出至log并写入inputStr

            // 判断大小写以让检测支持大小写区分
            isUpper();

            // 对按键进行判断和截断(不响应某些按键)
            switch (e.KeyData.ToString())
            {
                case "Oem5": // "Oem5"即是反斜杠,修改workMode开始处理捕获的信息
                    workMode = true;
                    needWrite = false;
                    break;
                case "Space": // 空格键修改workMode结束处理
                    workMode = false;
                    if (inputStr != null) // 判断在输入反斜杠和输入空格之间有没有输入内容,若有则调用判断函数处理并清空inputStr以准备响应下次流程
                    {
                        Judgment(inputStr);
                        inputStr = null;
                    }
                    needWrite = false;
                    break;
                case "Back": // 退格时按顺序删除上一个字符,此体系只对顺序输入有效
                    BackSpace();
                    needWrite = false;
                    break;
                case "LShiftKey":
                case "RShiftKey":
                case "Return":
                case "Tab":
                case "Up":
                case "Down":
                case "Left":
                case "Right":
                    needWrite = false;
                    break;
                case "Capital": // 当按下CapsLock按键时截获并修改状态
                    if (CapsLock == true)
                    {
                        CapsLock = false;
                    }
                    else
                    {
                        CapsLock = true;
                    }
                    needWrite = false;
                    break;
                default:
                    needWrite = true;
                    break;
            }

            if (needWrite)
            {
                if (Upper == true) // 根据大小写判断向inputStr中输入大小写格式的字符
                {
                    LogAndKeyStringWrite(e.KeyData.ToString().ToUpper());
                }
                else
                {
                    LogAndKeyStringWrite(e.KeyData.ToString().ToLower());
                }
            }
        }

        /// 
        /// 输出Log和写入inputStr
        /// 
        private void LogAndKeyStringWrite(string txt)
        {
            if (this.resultinfo.Lines.Length > 100)
            {
                this.resultinfo.Text = null;
            }

            if (workMode == true && txt != "Space") // 空格这个按键不写入inputStr
            {
                this.resultinfo.AppendText("Key: " + txt + Environment.NewLine);
                this.resultinfo.SelectionStart = this.resultinfo.Text.Length;
                inputStr += txt;
            }
        }

        /// 
        /// 按下backspace时响应
        /// 
        private void BackSpace()
        {
            if (inputStr != null && inputStr.Length > 1)
            {
                inputStr = inputStr.Remove(inputStr.Length - 1, 1);
            }
        }

读取配置

表其实是一个规定了格式的txt文件,考虑到文件可能较大,因此使用文件流的方式来处理。
我们只需要将txt中的每行读取,并以逗号进行分割为两个String,分别存入两个数组中就可以备用了。
两个数组通过相同的下标就可以把待转义字符串和操作对应起来。

        /// 
        /// 读表函数
        /// 
        private void ReadTable()
        {
            try
            {
                StreamReader fileReader = new StreamReader(filePath, Encoding.UTF8); // 虽然指定了是UTF8格式,但是实际上Unicode格式也能读取
                string nextLine; // 用以存储读取的每一行的变量

                // 循环读取至文件末
                while ((nextLine = fileReader.ReadLine()) != null)
                {
                    strTemp = nextLine.Split(','); // 以半角字符","来分割每一行的字符串,strTemp[0]是需要转义的字符串,strTemp[1]是目标字符串
                    convIndex.Add(strTemp[0]);
                    convChar.Add(strTemp[1]);
                }
            }
            catch (Exception)
            {
                MessageBox.Show("Can not read \"table.txt\",please check it.", "ERROR"); // 读取异常时提示并自动退出
                Close();
            }
        }

判断和处理

首先明确一点,待转义字符串是没有任何特征的,因此不需要考虑它。
程序在尝试转义时先遍历一次存储了所有待转义字符串的数组,如果输入的内容和待转义字符串的数组中任何一个相等,则可以进行下一步的判断,确定要执行什么操作。
在使用说明中可以看到我们对操作进行了约定,可以分为以下三种情况:

  1. 含有“\run”
  2. 含有“\code”
  3. 不包含以上两种

确认了这三种情况就很好处理了,前两者对表示操作内容的String进行IndexOf(),值为0或者不为-1即可,前两种情况都不是则直接else到第三种情况。
首先是含有“\run”,这里其实需要分为两种情况讨论,一是不带有参数执行程序,二是需要带参数执行程序。我们约定了以“|”来分隔参数和路径,因此只需要检测有无“|”即可轻松判断。C#中调用外部程序的方法是System.Diagnostics.Process.Start(),该方法有多种重载,我们只需要使用接受路径、同时接受路径和参数两种即可实现功能。
其次是含有“\code”,这里按照约定只要获取“\code ”右侧的字符串作为路径,使用Clipboard.SetDataObject()方法将用文件流读取的txt文件的内容放入剪贴板,再使用Win32API或者.net提供的方法模拟按键Ctrl+V即可实现功能。
第三种情况类似第二种情况,少了一个读取文件的过程而已。
代码如下:

        /// 
        /// 判断函数
        /// 
        private void Judgment(string txt)
        {
            bool canConv = false;

            resultinfo.AppendText("Judgment:" + txt + Environment.NewLine);
            resultinfo.SelectionStart = resultinfo.Text.Length;

            for (int i = 0; i < txt.Length + 1; i++) // 根据输入的inputStr的长度来删除需要转义的字符串以在原位置输入要转义的目标字符,+1是要算上不在inputStr中的反斜杠;如果未能匹配成功此处也当做自动删除
            {
                SendKeys.Send("{BACKSPACE}");
            }

            // 遍历convIndex检查是否需要进行转义
            foreach (var item in convIndex)
            {
                if (item.ToString() == txt)
                {
                    if (convChar[convIndex.IndexOf(txt)].ToString().IndexOf(@"\run ") == 0) // 用以启动程序,智能识别参数
                    {
                        string runPath = "";
                        runPath = convChar[convIndex.IndexOf(txt)].ToString().Replace(@"\run ", "");
                        System.Diagnostics.Debug.WriteLine(runPath);
                        try
                        {
                              if (runPath.IndexOf(@"| ") != -1)
                            {
                                string startPath = runPath.Substring(0, runPath.IndexOf(@"|"));
                                string startParameter = runPath.Substring(runPath.IndexOf(@"|"), runPath.Length - runPath.IndexOf(@"|")).Replace("|","");
                                System.Diagnostics.Debug.WriteLine(startPath);
                                System.Diagnostics.Process.Start(startPath, startParameter);
                            }
                            else
                            {
                                System.Diagnostics.Process.Start(runPath);
                            }
                        }
                        catch (Exception)
                        {
                            MessageBox.Show("Executable file path is illegal!","PLEASE CHECK THE TABLE");
                            //throw;
                        }
                    }
                    else if (convChar[convIndex.IndexOf(txt)].ToString().IndexOf(@"\code ") == 0) // 用以复制和粘贴代码段,从单独的文件中读取(原格式是文本即可,后缀是js、cpp、cs等无所谓)
                    {
                        string codePath = convChar[convIndex.IndexOf(txt)].ToString().Replace(@"\code ", "");
                        try
                        {
                            StreamReader fileReader = new StreamReader(codePath, Encoding.UTF8); // 虽然指定了是UTF8格式,但是实际上Unicode格式也能读取
                            string nextLine; // 用以存储读取的每一行的变量
                            string allLine = ""; // 用以存储所有行

                            // 循环读取至文件末
                            while ((nextLine = fileReader.ReadLine()) != null)
                            {
                                allLine += "\n" + nextLine;
                            }

                            Clipboard.SetDataObject(allLine);
                            resultinfo.AppendText("Output code fragment!" + Environment.NewLine);
                            resultinfo.SelectionStart = resultinfo.Text.Length;

                            // 模拟键盘Ctrl+V操作,C#的SendKey和SendKeyWait方法对于Edge、Adobe系列等软件不兼容
                            keybd_event(Keys.ControlKey, 0, 0, 0);
                            keybd_event(Keys.V, 0, 0, 0);
                            keybd_event(Keys.ControlKey, 0, KEYEVENTF_KEYUP, 0);
                        }
                        catch (Exception)
                        {
                            MessageBox.Show("Can not read \"table.txt\",please check it.", "ERROR"); // 读取异常时提示并自动退出
                            Close();
                        }
                    }
                    else // 是正常的符号转义
                    {
                        Clipboard.SetDataObject(convChar[convIndex.IndexOf(txt)]); // 将要输出的目标字符以text形式存储至系统的剪贴板中
                        resultinfo.AppendText("Output:" + convChar[convIndex.IndexOf(txt)] + Environment.NewLine);
                        resultinfo.SelectionStart = resultinfo.Text.Length;

                        // 模拟键盘Ctrl+V操作,C#的SendKey和SendKeyWait方法对于Edge、Adobe系列等软件不兼容
                        keybd_event(Keys.ControlKey, 0, 0, 0);
                        keybd_event(Keys.V, 0, 0, 0);
                        keybd_event(Keys.ControlKey, 0, KEYEVENTF_KEYUP, 0);
                    }

                    canConv = true;
                    inputStr = null; // 复位inputStr
                }
            }

            if (canConv == false)
            {
                notifyIcon1.ShowBalloonTip(1, "Error", "Can not convert \"" + inputStr + "\" .", ToolTipIcon.Error);
            }
        }

总结

整个程序的代码并不复杂,算法也比较简单,但是就功能而言还是有其一定的价值。对设计思维和综合积累有一定的要求。

个人水平有限,如有纰漏和错误望不吝指正。如有其它好的实现方法也欢迎交流!

你可能感兴趣的:(C#,.Net,windows,.net,c#)