【前言】
说来话长,关于这个手机控制电脑执行特定任务的想法早在几年前就有,但因为对安卓平台开发经验实在不足,就一直拖到了现在。不过好在没有忘记初衷,接下来我们一起来看我的思路和方法。
【思路】
想要通过手机作为控制端,来发送指令给同一网络下的电脑端,执行特定任务,例如打开桌面应用。
【思考】
市面上有很多电脑端和移动端相互通讯并控制的软件,可以“暴力”得控制并实现。但是现在不想依赖这类软件。这样的话我们需要设立模型,一个电脑服务端,一个安卓客户端。即多个安卓移动端可请求控制。
设计思路:
- 通信协议:通过TCP/IP协议在局域网内建立手机和电脑的Socket连接。
- 指令传输:手机端发送预定义的指令字符串(如open_app:notepad),电脑端解析后执行对应操作。
- 软件启动:电脑端通过命令行或脚本启动目标程序(例如借用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窗口程序,换成正常的窗口程序,并隐藏窗口,只出现在任务栏。
【再次编程】
思路:
- 修改项目输出类型为Windows应用程序。
- 创建托盘图标窗体,处理系统托盘图标和菜单。
- 重构后台服务,使用线程持续监听TCP连接。
- 修改主程序入口,启动托盘窗体和后台服务。
- 添加必要的异常处理和资源释放。
// 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}";}}
这样的话,就避免了出现黑窗口,顺利执行。
【总结】
初步实现了设想。
不过这才刚刚开始,在此基础上可以添加很多功能,优化应用,提升体验。我们列一下优化方向。
- 美化电脑端应用,替换默认图标,美化安卓端应用
- 添加电脑端应用右键菜单内容,比如
查看当前IP地址
、说明
、设置
等 - 设置电脑端应用自启动
- 安卓端应用添加功能,可以直接编写一段字符串,当成指令发送给电脑端执行,比如cmd指令
- 安卓端应用添加功能,可以检查版本,可以更新。
- 安卓端应用可以自动查找网络中的特定电脑端并设定目标IP地址。
这些是目前的想法。希望笔者自己可以不忘初心,再接再厉,整理出来供读者参考学习。