使用小爱同学语音控制电脑:从开关机到更多功能扩展的新探索


在之前的文章中,我介绍了如何使用小爱同学控制电脑的开关机操作,许多朋友对这一项目表示出了浓厚的兴趣,并希望获得完整的成品代码。然而,由于某些原因,之前的源码已经丢失。因此,在这里我将重新撰写一篇教程,并在此基础上增加一些新功能,以提升项目的实用性和趣味性。

原帖链接:使用小爱同学语音控制电脑关机 - Winform C#

准备工作

基本的准备工作在此不再赘述,之前的文章中已有详细介绍,包括从巴法云的注册到米家接入巴法云的步骤。如果您还未完成这些步骤,请参考以下链接中的内容:

本篇帖子将专注于代码功能的实现。通过本文介绍的方法,您可以实现更多功能,而不仅仅是关机操作。例如,您还可以使用小爱同学来启动电脑上的指定软件。不过,本文仅提供关机功能的详细教程。

如果您对其他功能感兴趣或有任何疑问,欢迎随时联系我。根据需求,我会进一步更新相关内容。

页面设计

尽管当前页面的设计较为简陋,但它已经具备了所有基础功能。重点在于实现核心功能的稳定性和可靠性,我没有过多关注界面的美观性。
在这里插入图片描述
当前界面包含了以下基本元素:

  • 文本框(textBox1): 显示从配置文件中读取的 UID,就是巴法云提供的秘钥。
  • 文本框(textBox2): 显示默认端口号(例如:8344)。
  • 文本框(textBox3): 显示从配置文件中读取的 TopicName,巴法云中设置的主题名称。
  • 复选框(开机自启): 选中后下次开机可以自动启动,这里是修改注册表来实现自启功能。
  • 复选框(自动连接):在下次启动软件时自动连接服务器,前提是需要先设置好配置文件Voice control.exe.config
  • 复选框(启动隐藏到菜单栏):下次启动软件时自动隐藏界面,仅显示任务栏小图标。

配置文件

编译后的配置文件名称是Voice control.exe.config,编译前是App.config,添加内容如下:

	<appSettings>
		<add key="UID" value="7b497ce9703f3dac9ca78d3eff6cdbed" />
		<add key="TopicName" value="computer001" />
		<!-- 是否在下次启动时运行方法 -->
		<add key="RunMethodOnStartup" value="false" />
		<add key="ShowForm" value="false" />
	</appSettings>

程序编写

在这个系统中,我将所有的参数都写入配置文件中。频繁地调用 ConfigurationManager.AppSettings 可能会影响性能,因此在程序启动时加载所有必要的配置项并保存到一个字典中。

配置加载方法

首先定义一个字典用于缓存配置项:

private Dictionary<string, string> configCache;

然后实现一个方法 LoadConfigurations来加载配置:

private void LoadConfigurations()
{
    // 将所有配置项加载到字典中,避免频繁访问 ConfigurationManager
    configCache = ConfigurationManager.AppSettings.AllKeys.ToDictionary(key => key, key => 	ConfigurationManager.AppSettings[key]);
}

窗口加载方法

private void VoiceControl_Load(object sender, EventArgs e)
{
    // 从配置缓存中读取并显示 UID
    textBox1.Text = configCache["UID"];
    
    // 设置默认端口号(此处为示例值)
    textBox2.Text = "8344";
    
    // 从配置缓存中读取并显示 TopicName
    textBox3.Text = configCache["TopicName"];
    
    // 加载启动时复选框的设置
    LoadStartupSetting();
    
    // 判断之前选择的结果判断是否启动就连接服务器
    HandleStartupActions();
    
    // 根绝之前的结果判断是否启动隐藏界面
    ShowFormActions();
}

方法说明
LoadConfigurations: 在程序启动时加载所有必要的配置项并存储在字典中,以减少后续对ConfigurationManager.AppSettings 的频繁访问,从而提升性能。
VoiceControl_Load: 窗体加载时执行的方法,初始化界面元素的值,并调用其他初始化方法完成进一步的设置。
通过这种方式,您可以确保程序在启动时高效地加载所需配置,并为用户提供一个友好的界面。如果有更多功能需求或需要进一步优化的地方,欢迎随时联系我进行讨论。

开机自启方法

开机自启有两个部分,一个是判断是否已经设置开机自启,然后修改界面的复选框,另一个就是如何实现开机自启.

  1. 界面加载
    读取注册表检查是否已经设置开机自启,然后对界面的复选框进行修改
using (RegistryKey rk = Registry.CurrentUser.OpenSubKey(RunKeyPath))
{
    if (rk?.GetValue(AppName) != null)
    {
        chkStartup.Checked = true;
    }
    else
    {
        chkStartup.Checked = false;
    }
}
  1. 选中开机自启
    这里是选中开机自启时的操作
  private void chkStartup_CheckedChanged(object sender, EventArgs e)
  {
      SetStartup(chkStartup.Checked);
  }

修改注册表操作

private void SetStartup(bool enable)
{
    using (RegistryKey rk = Registry.CurrentUser.OpenSubKey(RunKeyPath, true))
    {
        if (enable)
            rk?.SetValue(AppName, Application.ExecutablePath.ToString());
        else
            rk?.DeleteValue(AppName, false);
    }
}

AppName通过方法GetAppName()获取,RunKeyPath是注册表中自启设置的位置,在最开始就定义好:

  private static string AppName = GetAppName();
  private const string RunKeyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";

下面是获取AppName的方法:

 private static string GetAppName()
 {
     Assembly entryAssembly = Assembly.GetEntryAssembly();
     if (entryAssembly != null)
     {
         return System.IO.Path.GetFileNameWithoutExtension(entryAssembly.Location);
     }
     else
     {
         // 如果无法获取到程序集,默认返回一个固定值
         return "MyApplication";
     }
 }

自动连接与隐藏界面方法

这里逻辑是一样的,所以两个方法卸载一起,
首先是界面修改,根据配置文件来加载

  // 加载是否运行特定方法的设置
  bool runMethodOnStartup = bool.Parse(configCache["RunMethodOnStartup"]);
  chkRunMethod.Checked = runMethodOnStartup;

  // 加载是否自动隐藏到菜单栏的设置
  bool showForm = bool.Parse(configCache["ShowForm"]);
  chkShow.Checked = showForm;

连接按钮的方法:

 private void button1_Click(object sender, EventArgs e)
 {
     // 尝试从配置缓存中获取UID和TopicName
     string uid = configCache.ContainsKey("UID") ? configCache["UID"] : null;
     string topicName = configCache.ContainsKey("TopicName") ? configCache["TopicName"] : null;

     // 检查UID是否为空,并从textBox1获取值(如果需要)
     if (string.IsNullOrEmpty(uid))
     {
         uid = textBox1.Text.Trim(); // 使用Trim()去除可能的前后空格
         if (string.IsNullOrEmpty(uid))
         {
             ShowMsg("UID不能为空!");
             return; // 如果UID仍然为空,则直接返回,不继续执行
         }
         configCache["UID"] = uid; // 更新configCache中的UID
     }

     // 检查TopicName是否为空,并从textBox2获取值(如果需要)
     if (string.IsNullOrEmpty(topicName))
     {
         topicName = textBox2.Text.Trim(); // 同样使用Trim()以确保没有额外的空白
         if (string.IsNullOrEmpty(topicName))
         {
             ShowMsg("TopicName不能为空!");
             return; // 如果TopicName仍然为空,则直接返回,不继续执行
         }
         configCache["TopicName"] = topicName; // 更新configCache中的TopicName
     }

     TcpL();

 }

心跳机制

为了防止与服务器断联导致无法使用的问题,在这里添加了心跳机制,定时发送消息给服务器判断是否还处于连接状态:

 public void Heartbeat()
 {
 
     while (true)
     {
         if (_isConnected && tcpClient.Connected)
         {
             try
             {
                 Send($"ping\r\n");
                 Thread.Sleep(60000);
             }
             catch (Exception)
             {
                 TcpL();
             }
         }
     }
 }

接收消息

开关机的操作主要就是这里,接收服务器返回的消息来判断是否运行指令.

private void Received()
{
    if (_isConnected && tcpClient.Connected)
    {
        try
        {
            NetworkStream networkStream = tcpClient.GetStream();
            byte[] datas = new byte[1024];
            while (true)
            {
                if (networkStream.DataAvailable)
                {
                    int len = networkStream.Read(datas, 0, 1024);
                    string v = Encoding.UTF8.GetString(datas);
                    string cmd = ParseQueryString(v, "cmd");
                    string msg = ParseQueryString(v, "msg");
                    // 执行自动关机!
                    if (cmd.Equals("2") && msg.IndexOf("off") == 0)
                    {
                        ShowMsg("电脑将于3s后关闭!");
                        Process proc = new Process();
                        proc.StartInfo.FileName = "cmd.exe"; // 启动命令行程序
                        proc.StartInfo.UseShellExecute = false; // 不使用Shell来执行,用程序来执行
                        proc.StartInfo.RedirectStandardError = true; // 重定向标准输入输出
                        proc.StartInfo.RedirectStandardInput = true;
                        proc.StartInfo.RedirectStandardOutput = true;
                        proc.StartInfo.CreateNoWindow = true; // 执行时不创建新窗口
                        proc.Start();
                        string commandLine;
                        //if (isCancel)
                        //    commandLine = @"shutdown /a"; 
                        commandLine = @"shutdown /s";

                        proc.StandardInput.WriteLine(commandLine);
                        return;
                    }

                    ShowMsg("\n" + Encoding.UTF8.GetString(datas));

                    //Console.WriteLine($"From:{remoteEndPoint}:Received ({len})");
                }
                Thread.Sleep(1);
            }
        }
        catch (Exception ex)
        {

            ShowMsg("Received Error: " + ex.ToString());
        }

    }


}

发送消息

 private void Send(string msg)
 {
     ShowMsg("Send Msg: " + msg.Replace("\n", ""));
     NetworkStream networkStream = tcpClient.GetStream();
     //EndPoint remoteEndPoint = tcpClient.Client.RemoteEndPoint;
     byte[] datas = new byte[1024];
     datas = Encoding.ASCII.GetBytes(msg);
     networkStream.Write(datas, 0, datas.Length);
 }

所有代码

这里展示所有程序的代码,但是要注意绑定组件.

    private static string AppName = GetAppName();
    private const string RunKeyPath = @"SOFTWARE\Microsoft\Windows\CurrentVersion\Run";
    private Dictionary<string, string> configCache;
    private TcpClient tcpClient = new TcpClient();

    public VoiceControl()
    {
        InitializeComponent();
        LoadConfigurations();
    }
    private void LoadConfigurations()
    {
        configCache = ConfigurationManager.AppSettings.AllKeys.ToDictionary(key => key, key => ConfigurationManager.AppSettings[key]);
    }

    private void VoiceControl_Load(object sender, EventArgs e)
    {
        textBox1.Text = configCache["UID"];
        textBox2.Text = "8344";
        textBox3.Text = configCache["TopicName"];
        LoadStartupSetting();
        HandleStartupActions();
        ShowFormActions();
    }

    private static string GetAppName()
    {
        Assembly entryAssembly = Assembly.GetEntryAssembly();
        if (entryAssembly != null)
        {
            return System.IO.Path.GetFileNameWithoutExtension(entryAssembly.Location);
        }
        else
        {
            // 如果无法获取到程序集,默认返回一个固定值
            return "MyApplication";
        }
    }

    private void chkStartup_CheckedChanged(object sender, EventArgs e)
    {
        SetStartup(chkStartup.Checked);
    }

    private void chkRunMethod_CheckedChanged(object sender, EventArgs e)
    {
        SetRunMethodOnStartup(chkRunMethod.Checked);
    }
    private void chkShow_CheckedChanged(object sender, EventArgs e)
    {
        SetShowForm(chkShow.Checked);
    }

    private void SetRunMethodOnStartup(bool enable)
    {
        Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
        config.AppSettings.Settings["RunMethodOnStartup"].Value = enable.ToString();
        config.Save(ConfigurationSaveMode.Modified);
        ConfigurationManager.RefreshSection("appSettings");
    }
    private void SetShowForm(bool enable)
    {
        Configuration config = ConfigurationManager.OpenExeConfiguration(ConfigurationUserLevel.None);
        config.AppSettings.Settings["ShowForm"].Value = enable.ToString();
        config.Save(ConfigurationSaveMode.Modified);
        ConfigurationManager.RefreshSection("appSettings");
    }
    private void HandleStartupActions()
    {
        // 检查是否需要在启动时运行特定方法
        bool runMethodOnStartup = bool.Parse(configCache["RunMethodOnStartup"]);
        if (runMethodOnStartup)
        {
            button1_Click(null, null);
        }
    }
    private void ShowFormActions()
    {
        try
        {
            // 检查是否需要在启动时自动隐藏
            bool runMethodOnStartup;
            if (bool.TryParse(configCache["ShowForm"], out runMethodOnStartup) && runMethodOnStartup)
            {
                this.Visible = false; // 设置为不可见
                this.ShowInTaskbar = false; // 不在任务栏显示
                this.WindowState = FormWindowState.Minimized; // 最小化窗体

                // 显示提示气泡
                notifyIcon1.BalloonTipTitle = "提示";
                notifyIcon1.BalloonTipText = "程序已启动并最小化到系统托盘";
                notifyIcon1.BalloonTipIcon = ToolTipIcon.Info;
                notifyIcon1.ShowBalloonTip(5000); // 显示5秒
            }
        }
        catch (Exception ex)
        {
            // 记录异常日志(可以根据实际情况选择如何处理异常)
            MessageBox.Show($"读取配置失败: {ex.Message}", "错误", MessageBoxButtons.OK, MessageBoxIcon.Error);
        }
    }
    private void SetStartup(bool enable)
    {
        using (RegistryKey rk = Registry.CurrentUser.OpenSubKey(RunKeyPath, true))
        {
            if (enable)
                rk?.SetValue(AppName, Application.ExecutablePath.ToString());
            else
                rk?.DeleteValue(AppName, false);
        }
    }

    private void LoadStartupSetting()
    {
        using (RegistryKey rk = Registry.CurrentUser.OpenSubKey(RunKeyPath))
        {
            if (rk?.GetValue(AppName) != null)
            {
                chkStartup.Checked = true;
            }
            else
            {
                chkStartup.Checked = false;
            }
        }

        // 加载是否运行特定方法的设置
        bool runMethodOnStartup = bool.Parse(configCache["RunMethodOnStartup"]);
        chkRunMethod.Checked = runMethodOnStartup;

        // 加载是否自动隐藏到菜单栏的设置
        bool showForm = bool.Parse(configCache["ShowForm"]);
        chkShow.Checked = showForm;
    }

    private void button1_Click(object sender, EventArgs e)
    {
        // 尝试从配置缓存中获取UID和TopicName
        string uid = configCache.ContainsKey("UID") ? configCache["UID"] : null;
        string topicName = configCache.ContainsKey("TopicName") ? configCache["TopicName"] : null;

        // 检查UID是否为空,并从textBox1获取值(如果需要)
        if (string.IsNullOrEmpty(uid))
        {
            uid = textBox1.Text.Trim(); // 使用Trim()去除可能的前后空格
            if (string.IsNullOrEmpty(uid))
            {
                ShowMsg("UID不能为空!");
                return; // 如果UID仍然为空,则直接返回,不继续执行
            }
            configCache["UID"] = uid; // 更新configCache中的UID
        }

        // 检查TopicName是否为空,并从textBox2获取值(如果需要)
        if (string.IsNullOrEmpty(topicName))
        {
            topicName = textBox2.Text.Trim(); // 同样使用Trim()以确保没有额外的空白
            if (string.IsNullOrEmpty(topicName))
            {
                ShowMsg("TopicName不能为空!");
                return; // 如果TopicName仍然为空,则直接返回,不继续执行
            }
            configCache["TopicName"] = topicName; // 更新configCache中的TopicName
        }

        TcpL();

    }

    private bool _isConnected = false;

    // 连接巴法云TCP服务器,(修改为异步)
    private async void TcpL()
    {
        var hostEntry = await Dns.GetHostEntryAsync("bemfa.com");
        try
        {
            tcpClient = new TcpClient(hostEntry.AddressList[0].ToString(), 8344);
            _isConnected = true;
            Thread c_thread = new Thread(Received);
            c_thread.IsBackground = true;
            c_thread.Start();
            StartCommIdle();
            Send($"cmd=1&uid={textBox1.Text}&topic={textBox3.Text}\r\n");

        }
        catch (Exception ex)
        {
            _isConnected = false;
            ShowMsg(ex.ToString());
        }
    }

 

    public void StartCommIdle()
    {
        //定时器配置
        Thread c_thread = new Thread(Heartbeat);
        c_thread.IsBackground = true;
        c_thread.Start();
    }

    public void Heartbeat()
    {
 
        while (true)
        {
            if (_isConnected && tcpClient.Connected)
            {
                try
                {
                    Send($"ping\r\n");
                    Thread.Sleep(60000);
                }
                catch (Exception)
                {
                    TcpL();
                }
            }
        }
    }

    /// <summary>
    /// 接收信息
    /// </summary>
    private void Received()
    {
        if (_isConnected && tcpClient.Connected)
        {
            try
            {
                NetworkStream networkStream = tcpClient.GetStream();
                byte[] datas = new byte[1024];
                while (true)
                {
                    if (networkStream.DataAvailable)
                    {
                        int len = networkStream.Read(datas, 0, 1024);
                        string v = Encoding.UTF8.GetString(datas);
                        string cmd = ParseQueryString(v, "cmd");
                        string msg = ParseQueryString(v, "msg");
                        // 执行自动关机!
                        if (cmd.Equals("2") && msg.IndexOf("off") == 0)
                        {
                            ShowMsg("电脑将于3s后关闭!");
                            Process proc = new Process();
                            proc.StartInfo.FileName = "cmd.exe"; // 启动命令行程序
                            proc.StartInfo.UseShellExecute = false; // 不使用Shell来执行,用程序来执行
                            proc.StartInfo.RedirectStandardError = true; // 重定向标准输入输出
                            proc.StartInfo.RedirectStandardInput = true;
                            proc.StartInfo.RedirectStandardOutput = true;
                            proc.StartInfo.CreateNoWindow = true; // 执行时不创建新窗口
                            proc.Start();
                            string commandLine;
                            //if (isCancel)
                            //    commandLine = @"shutdown /a"; 
                            commandLine = @"shutdown /s";

                            proc.StandardInput.WriteLine(commandLine);
                            return;
                        }

                        ShowMsg("\n" + Encoding.UTF8.GetString(datas));

                        //Console.WriteLine($"From:{remoteEndPoint}:Received ({len})");
                    }
                    Thread.Sleep(1);
                }
            }
            catch (Exception ex)
            {

                ShowMsg("Received Error: " + ex.ToString());
            }

        }


    }

    /// <summary>
    /// 发送信息
    /// </summary>
    /// <param name="msg"></param>
    private void Send(string msg)
    {
        ShowMsg("Send Msg: " + msg.Replace("\n", ""));
        NetworkStream networkStream = tcpClient.GetStream();
        //EndPoint remoteEndPoint = tcpClient.Client.RemoteEndPoint;
        byte[] datas = new byte[1024];
        datas = Encoding.ASCII.GetBytes(msg);
        networkStream.Write(datas, 0, datas.Length);
    }

    

    void ShowMsg(string str)
    {
        this.Invoke(new Action(() =>
        {
            richTextBox1.AppendText(str + "\r\n");
        }));

    }
    void ShowNotice(string str)
    {
        this.Invoke(new Action(() =>
        {
            textBox3.AppendText(str + "\r\n");
        }));
    }


    private void smi_exit_Click(object sender, EventArgs e)
    {
        DialogResult result = MessageBox.Show("你确定要关闭吗!", "提示信息", MessageBoxButtons.OKCancel, MessageBoxIcon.Information);
        if (result == DialogResult.OK)
        {
            // 关闭所有的线程
            this.Dispose();
            this.Close();
        }
    }

    private void Form1_FormClosing(object sender, FormClosingEventArgs e)
    {
        try
        {
            if (e != null) e.Cancel = true;
            this.Visible = false;

        }
        catch (Exception ex)
        {
            MessageBox.Show(ex.Message);
        }
    }



    public static string ParseQueryString(string url, string key)
    {
        if (string.IsNullOrWhiteSpace(url))
        {
            throw new ArgumentNullException("url");
        }

        //1.去除第一个前导?字符
        //var dic = 
        string value = null;
        Dictionary<string, string> dictionary = url
                //2.通过&划分各个参数
                .Split(new char[] { '&' }, StringSplitOptions.RemoveEmptyEntries)
                //3.通过=划分参数key和value,且保证只分割第一个=字符
                .Select(param => param.Split(new char[] { '=' }, 2, StringSplitOptions.RemoveEmptyEntries))
                //4.通过相同的参数key进行分组
                .GroupBy(part => part[0], part => part.Length > 1 ? part[1] : string.Empty)
                //5.将相同key的value以,拼接
                .ToDictionary(group => group.Key, group => string.Join(",", group));
        dictionary.TryGetValue(key, out value);
        return value;
    }

    private void Form1_FormShow(object sender, EventArgs e)
    {
        this.Visible = true;
    }

    private void notifyIcon1_DoubleClick(object sender, EventArgs e)
    {
        Form1_FormShow(sender, e);
    }

源码与成品

源码与成品最后上传到网站中供下载
https://download.csdn.net/download/baozi141990/90360905

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值