关于一个手机控制电脑执行特定任务的解决方案探索【1】

news/2025/2/22 8:11:22/文章来源:https://www.cnblogs.com/sharpeye/p/18725562

【前言】
说来话长,关于这个手机控制电脑执行特定任务的想法早在几年前就有,但因为对安卓平台开发经验实在不足,就一直拖到了现在。不过好在没有忘记初衷,接下来我们一起来看我的思路和方法。

【思路】
想要通过手机作为控制端,来发送指令给同一网络下的电脑端,执行特定任务,例如打开桌面应用。

【思考】
市面上有很多电脑端和移动端相互通讯并控制的软件,可以“暴力”得控制并实现。但是现在不想依赖这类软件。这样的话我们需要设立模型,一个电脑服务端,一个安卓客户端。即多个安卓移动端可请求控制。
设计思路:

  1. 通信协议:通过TCP/IP协议在局域网内建立手机和电脑的Socket连接。
  2. 指令传输:手机端发送预定义的指令字符串(如open_app:notepad),电脑端解析后执行对应操作。
  3. 软件启动:电脑端通过命令行或脚本启动目标程序(例如借用Windows的cmd指令 /c start notepad.exe)。

【编程】
首先编写电脑端的程序,使用C#写一个cmd应用,等待被连接然后接受命令字符并解析执行

// Program.cs
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;class Server
{static void Main(){TcpListener server = null;try{// 绑定IP和端口IPAddress localAddr = IPAddress.Any;int port = 12345;server = new TcpListener(localAddr, port);server.Start();Console.WriteLine("等待手机连接...");while (true){// 接受客户端连接TcpClient client = server.AcceptTcpClient();Console.WriteLine("已连接客户端.");// 处理指令NetworkStream stream = client.GetStream();byte[] buffer = new byte[1024];int bytesRead = stream.Read(buffer, 0, buffer.Length);string command = Encoding.ASCII.GetString(buffer, 0, bytesRead);Console.WriteLine($"收到指令: {command}");// 执行命令并返回结果string response = ExecuteCommand(command);byte[] responseData = Encoding.ASCII.GetBytes(response);stream.Write(responseData, 0, responseData.Length);// 关闭连接client.Close();}}catch (Exception ex){Console.WriteLine($"错误: {ex.Message}");}finally{server?.Stop();}}static string ExecuteCommand(string command){try{if (command.StartsWith("open_app:")){string appPath = command.Substring(9).Trim('"'); // 移除引号Process.Start("cmd.exe", $"/c start \"\" \"{appPath}\""); // 兼容路径空格return "Success: 程序已启动";}return "Error: 未知指令";}catch (Exception ex){return $"Error: {ex.Message}";}}
}

接着编写安卓端程序。使用Kotlin借Android studio软件。
新建一个Empty Views Activity Project,编写MainActivity:

package com.example.myapplication6import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.util.Log
import android.widget.Toast
import com.example.myapplication6.databinding.ActivityMainBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import java.net.Socket
import java.nio.charset.StandardCharsets// MainActivity.kt
class MainActivity : AppCompatActivity() {private lateinit var binding: ActivityMainBindingoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = ActivityMainBinding.inflate(layoutInflater)setContentView(binding.root)// 按钮点击事件:打开桌面快捷方式binding.btnOpenApp.setOnClickListener {sendCommand("open_app:\"C:\\Users\\Administrator\\Desktop\\cctv13.url\"")}}private fun sendCommand(command: String) {CoroutineScope(Dispatchers.IO).launch {try {val socket = Socket("192.168.1.8", 12345) // 替换为电脑IPval outputStream = socket.getOutputStream()outputStream.write(command.toByteArray(Charsets.UTF_8))//  outputStream.write(command.getBytes(StandardCharsets.UTF_8))outputStream.flush()// 读取响应val inputStream = socket.getInputStream()val buffer = ByteArray(1024)val bytesRead = inputStream.read(buffer)val response = String(buffer, 0, bytesRead)// val response = buffer.decodeToString(0, bytesRead)Log.d("ServerResponse", response)socket.close()} catch (e: Exception) {e.printStackTrace()withContext(Dispatchers.Main) {Toast.makeText(this@MainActivity, "连接失败: ${e.message}", Toast.LENGTH_SHORT).show()}}}}
}

接着设置布局Activity_main.xml。创建一个按钮,方便发送指令:

<?xml version="1.0" encoding="utf-8"?>  <!-- activity_main.xml -->
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"android:layout_width="match_parent"android:layout_height="match_parent"android:orientation="vertical"android:padding="16dp"><Buttonandroid:id="@+id/btnOpenApp"android:layout_width="match_parent"android:layout_height="wrap_content"android:text="打开桌面应用" /></LinearLayout>

另外build.gradle设置也一并给出参考:
这个是build.gradle(module-app)

plugins {id 'com.android.application'id 'kotlin-android'
}android {compileSdk 34defaultConfig {applicationId "com.example.myapplication6"minSdk 23targetSdk 34versionCode 1versionName "1.0"namespace 'com.example.myapplication6'testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"}buildFeatures {viewBinding true  // 启用视图绑定}buildTypes {release {minifyEnabled falseproguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'}}compileOptions {sourceCompatibility JavaVersion.VERSION_1_8targetCompatibility JavaVersion.VERSION_1_8}kotlinOptions {jvmTarget = '1.8'}
}dependencies {implementation "org.jetbrains.kotlin:kotlin-stdlib:1.6.4"implementation 'androidx.core:core-ktx:1.3.2'implementation 'androidx.appcompat:appcompat:1.2.0'implementation 'com.google.android.material:material:1.3.0'implementation 'androidx.constraintlayout:constraintlayout:2.0.4'testImplementation 'junit:junit:4.+'androidTestImplementation 'androidx.test.ext:junit:1.1.2'androidTestImplementation 'androidx.test.espresso:espresso-core:3.3.0'implementation 'androidx.core:core-ktx:1.9.0'implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.4'implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.4'androidTestImplementation 'androidx.test:runner:1.5.2'androidTestImplementation 'androidx.test:core:1.5.0'androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'}

这个是build.gradle(project)

// Top-level build file where you can add configuration options common to all sub-projects/modules.
buildscript {repositories {google()mavenCentral()}ext.kotlin_version = '1.8.22' // 或者更新版本dependencies {classpath "com.android.tools.build:gradle:8.0.0"classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"classpath 'com.android.tools.build:gradle:8.0.0' // 更新到最新版本// NOTE: Do not place your application dependencies here; they belong// in the individual module build.gradle files}}task clean(type: Delete) {delete rootProject.buildDir
}

【阶段性结果】

各自编译生成应用并运行,可以得到最基本的功能。注意的是,debug阶段,很多参数都设了常量,如IP地址、执行命令的字符串等。
值得优化的地方还有很多,比如替换cmd窗口程序,换成正常的窗口程序,并隐藏窗口,只出现在任务栏。

【再次编程】
思路:

  1. 修改项目输出类型为Windows应用程序。
  2. 创建托盘图标窗体,处理系统托盘图标和菜单。
  3. 重构后台服务,使用线程持续监听TCP连接。
  4. 修改主程序入口,启动托盘窗体和后台服务。
  5. 添加必要的异常处理和资源释放。
// Program.cs
using System;
using System.Diagnostics;
using System.Net;
using System.Net.Sockets;
using System.Text;
using System.Windows.Forms;
using System.Threading;
using System.Drawing;namespace RemoteControlServer
{static class Program{/// <summary>/// 应用程序的主入口点/// </summary>[STAThread]static void Main(){Application.EnableVisualStyles();Application.SetCompatibleTextRenderingDefault(false);// 初始化托盘图标窗体var trayForm = new TrayIconForm();// 启动后台服务var server = new BackgroundServer();server.Start();// 启动消息循环(保持程序运行)Application.Run(trayForm);}}/// <summary>/// 后台服务类 - 处理TCP通信和指令执行/// </summary>public class BackgroundServer{private TcpListener listener;private bool isRunning = true;/// <summary>/// 启动服务监听/// </summary>public void Start(){new Thread(() =>{try{listener = new TcpListener(IPAddress.Any, 12345);listener.Start();//Console.WriteLine("服务已启动,监听端口 12345");while (isRunning){// 异步接受客户端连接var client = listener.AcceptTcpClient();new Thread(HandleClient).Start(client);}}catch (Exception ex){Console.WriteLine($"服务异常: {ex.Message}");}}){ IsBackground = true }.Start(); // 设置为后台线程}/// <summary>/// 处理客户端请求/// </summary>private void HandleClient(object obj){using (var client = (TcpClient)obj)using (var stream = client.GetStream()){try{byte[] buffer = new byte[1024];int bytesRead = stream.Read(buffer, 0, buffer.Length);string command = Encoding.UTF8.GetString(buffer, 0, bytesRead);//Console.WriteLine($"收到指令: {command}");// 执行命令并返回响应string response = ExecuteCommand(command);byte[] responseData = Encoding.UTF8.GetBytes(response);stream.Write(responseData, 0, responseData.Length);}catch (Exception ex){Console.WriteLine($"处理客户端错误: {ex.Message}");}}}/// <summary>/// 执行指令逻辑/// </summary>private string ExecuteCommand(string command){try{if (command.StartsWith("open_app:")){string appPath = command.Substring(9).Trim('"');Process.Start("cmd.exe", $"/c start \"\" \"{appPath}\"");return "SUCCESS: 程序已启动";}return "ERROR: 未知指令";}catch (Exception ex){return $"ERROR: {ex.Message}";}}/// <summary>/// 停止服务/// </summary>public void Stop(){isRunning = false;listener?.Stop();Console.WriteLine("服务已停止");}}/// <summary>/// 托盘图标窗体 - 处理系统托盘交互/// </summary>public class TrayIconForm : Form{private NotifyIcon trayIcon;private ContextMenuStrip trayMenu;public TrayIconForm(){// 初始化托盘图标trayIcon = new NotifyIcon{Text = "远程控制服务",Icon = SystemIcons.Application, // 可替换为自定义.ico文件Visible = true};// 初始化右键菜单trayMenu = new ContextMenuStrip();trayMenu.Items.Add("退出", null, OnExitClick);trayIcon.ContextMenuStrip = trayMenu;// 双击托盘图标事件(示例:显示日志窗口)trayIcon.DoubleClick += (s, e) =>{MessageBox.Show("服务运行中", "状态", MessageBoxButtons.OK, MessageBoxIcon.Information);};}/// <summary>/// 退出菜单点击事件/// </summary>private void OnExitClick(object sender, EventArgs e){var result = MessageBox.Show("确定要退出服务吗?", "确认退出", MessageBoxButtons.YesNo, MessageBoxIcon.Question);if (result == DialogResult.Yes){trayIcon.Visible = false;Application.Exit();}}/// <summary>/// 窗体加载时隐藏窗口/// </summary>protected override void OnLoad(EventArgs e){Visible = false;       // 隐藏窗体ShowInTaskbar = false; // 不在任务栏显示base.OnLoad(e);}/// <summary>/// 清理资源/// </summary>protected override void Dispose(bool disposing){if (disposing){trayIcon?.Dispose();trayMenu?.Dispose();}base.Dispose(disposing);}}
}

这样,我们就可以实现一个无窗口的windows监听程序。不过通过测试发现,一旦接收到请求并执行,屏幕会闪过一个cmd黑窗口。这不是想要的结果,进行优化。放弃使用cmd指令,转而直接使用Process.start函数来开始新任务。重写ExecuteCommand方法:

        // 修改后的 ExecuteCommand 方法private string ExecuteCommand(string command){try{if (command.StartsWith("open_app:")){string appPath = command.Substring(9).Trim('"');// 创建进程配置var processInfo = new ProcessStartInfo{FileName = appPath,          // 直接指向目标程序UseShellExecute = true,      // 使用系统Shell解析路径(支持快捷方式)CreateNoWindow = true,       // 不创建任何窗口WindowStyle = ProcessWindowStyle.Hidden // 隐藏窗口(双重保险)};Process.Start(processInfo);return "SUCCESS: 程序已静默启动";}return "ERROR: 未知指令";}catch (Exception ex){return $"ERROR: {ex.Message}";}}

这样的话,就避免了出现黑窗口,顺利执行。

【总结】
初步实现了设想。
不过这才刚刚开始,在此基础上可以添加很多功能,优化应用,提升体验。我们列一下优化方向。

  1. 美化电脑端应用,替换默认图标,美化安卓端应用
  2. 添加电脑端应用右键菜单内容,比如查看当前IP地址说明设置
  3. 设置电脑端应用自启动
  4. 安卓端应用添加功能,可以直接编写一段字符串,当成指令发送给电脑端执行,比如cmd指令
  5. 安卓端应用添加功能,可以检查版本,可以更新。
  6. 安卓端应用可以自动查找网络中的特定电脑端并设定目标IP地址。

这些是目前的想法。希望笔者自己可以不忘初心,再接再厉,整理出来供读者参考学习。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.hqwc.cn/news/886995.html

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

如何升级 PowerShell 到最新版本

前言最近,需要大量使用PowerShell,然后有需要PowerShell 7正文升级的步骤也比较简单,按照下面的步骤就好了文字版本的,方便大家复制粘贴。PS C:\WINDOWS\system32> $PSVersionTable.PSVersionMajor Minor Build Revision ----- ----- ----- -------- 5 1 …

百万架构师第四十课:RabbitMq:RabbitMq-工作模型与JAVA编程|JavaGuide

来源:https://javaguide.net RabbitMQ 1-工作模型与Java编程 课前准备 预习资料 Windows安装步骤 Linux安装步骤 官网文章中文翻译系列 环境说明 操作系统:CentOS 7 JDK:1.8 Erlang:19.0.4或最新版 RabbitMQ:3.6.12或最新版 版本对应关系 典型应用场景跨系统的异步通信。人…

1月16日java假期学习读书笔记

一、学习目标 掌握HTML的基本结构和常用标签。 了解CSS的基本选择器和样式规则。 通过实际代码练习,构建一个简单的网页。 二、学习内容 (一)HTML基础 HTML简介 HTML(HyperText Markup Language,超文本标记语言)是用于构建网页的标准标记语言。 它通过一系列的标签(如、…

MapStruct使用指南并结合Lombok

MapStruct使用指南并结合Lombokhttps://juejin.cn/post/6956190395319451679#heading-1 2024-01-11 18:34:06如何结合 lombok 也就说说如果代码中使用了 lombok 注解来生成代码,mapstruct 的 getter/setter 方法也使用了 lombok 的 api,那就需要额外的配置,因为这两个工具都是使…

史上最全桌面级CPU天梯图-2024年10月更新(包含13/14代Intel/7000系列锐龙)

史上最全桌面级CPU天梯图-2024年10月更新(包含13/14代Intel/7000系列锐龙) 原文:https://www.zhihu.com/tardis/bd/art/499783467?source_id=1001

large_bin_attack

large_bin的结构如下 /*This struct declaration is misleading (but accurate and necessary).It declares a "view" into memory allowing access to necessaryfields at known offsets from a given base. See explanation below. */ struct malloc_chunk {INTERN…

体验用ai做了个python小游戏

写在前面:最近ai确实比较火。各种生成式AI,包括文字、图片、视频。之前听说ai生产代码能力比较强,一直想试试。所以及就有了本问。使用的工具deepinseek :用来生成python代码即梦:用来生成图片素材Remove.bg:用来对生成的图片素材去除背景pixabay.com:用来下载音乐素材游…

2.1.5 节省内存

首先来介绍一下可变对象和不可变对象可变对象:整数,浮点数,字符串,元组等 不可变对象:列表,字典,集合等然后看一下Python中内存分配的方式 执行x=1会发生什么?此时,内存会分配一个地址给1,1是一个整型对象,而x是一个引用(不是对象!),指向1所在的位置,并不占用实…

ABC392E翻译

AT_abc392_e [ABC392E] Cables and Servers 题目描述 有编号从 \(1\) 到 \(N\) 的 \(N\) 台服务器和编号从 \(1\) 到 \(M\) 的 \(M\) 根电缆。 电缆 \(i\) 双向连接服务器 \(A_i\) 和服务器 \(B_i\)。 通过进行以下操作(可以是 \(0\) 次),使得所有服务器之间都能通过电缆相互…

【外贸】集装箱的规格

集装箱类型(以米为单位)集装箱类型 外部尺寸(长宽高) 内部尺寸(长宽高) 容积(立方米) 载重(公斤)20英尺标准集装箱 6.1m 2.44m 2.59m 5.9m 2.35m 2.39m 33 28,00040英尺标准集装箱 12.2m 2.44m 2.59m 12m 2.35m 2.39m 67 26,50040英尺高柜集装箱 12.2m 2.44…

PriorityBlockingQueue 的put方法底层源码

一、PriorityBlockingQueue 的put方法底层源码 PriorityBlockingQueue 的 put 方法用于将元素插入队列。由于 PriorityBlockingQueue 是一个无界队列,put 方法不会阻塞,总是会成功插入元素 1、put 方法的作用将元素插入队列。由于队列无界,put 方法不会阻塞,总是会成功插入…