前言
躺在床上投屏到电脑的时候, 调节音量和一些简单的操作还需要起身操作, 觉得麻烦, 就开发了这么一个小工具。
思路
- go语言负责后端,负责模拟键盘输入和鼠标移动
- vue负责页面编写,调用后端接口,使用petite-vue单个页面开发, 够轻量
- go直接调用user32.dll完成键盘和鼠标的操作, 不依赖三方框架
- 前端完全基于浏览器, 有微信有扫一扫就能直接打开控制页
- 按键传输采用
http请求, 鼠标移动采用websocket
使用技术
- 前端:petite-vue、qrcode
- 后端:go1.20、systray、websocket
开始操作
前端
封装fetch请求后端api
function request(options, temp) {
let opts = temp
if (typeof options != 'string') {
opts = options
}
let { url, method = 'get', params = {}, data = null } = opts || {};
if (typeof options == 'string') url = options
// 将查询参数转换为url编码字符串
const querystring = object.keys(params).map(key => `${encodeuricomponent(key)}=${encodeuricomponent(params[key])}`).join('&');
// 构建完整的请求url
let finalurl = url + (url.includes('?') ? '&' : '?') + querystring;
finalurl = finalurl.includes('http') ? finalurl : `${baseapi}${finalurl}`
// 设置请求头部
const headers = {};
if (data) headers['content-type'] = 'application/json'
// 发起fetch请求
return new promise((resolve, reject) => {
fetch(finalurl, { method, headers, body: data ? json.stringify(data) : null}).then(res => {
if (!res.ok) throw new error(`http error! status: ${response.status}`);
return res.json();
}).then(r => resolve(r)).catch(e => reject(e));
});
}
websocket初始化
websocket = new websocket(`ws://${location.host}/ws`);
websocket.onmessage = function(evt) {
if(evt.data=="reload"){
window.location.pathname = "/";
window.location.reload(true);
}
};
websocket.onopen = function() {
console.log('socket open....');
document.getelementbyid('touch').style.display = 'block';
};
websocket.onclose = function() {
console.log('socket close....');
document.getelementbyid('touch').style.display = 'none';
};
let starttime = 0;
function senddata(data, force) {
const curr = new date().gettime();
if (curr - starttime > 60 || force) {
console.log('socket send....', data);
websocket.send(data);
starttime = curr;
}
};
按键布局
<div id="keyboard">
<div class="f2">
<i k="ctrl,a">全选</i><i k="ctrl,c">复制</i><i k="ctrl,v">粘贴</i><i k="ctrl,x">剪切</i><i k="ctrl,z">撤销</i><i k="ctrl,s">保存</i>
</div>
<div class="f2">
<i k="ctrl,shift">输入法</i><i k="alt,f4">关闭</i><i k="win,d">桌面</i><i k="media_prev_track">上曲</i><i k="media_next_track">下曲</i>
<i k="media_play_pause">播放</i><i k="volume_down">音量-</i><i k="volume_up">音量+</i><i k="volume_mute">静音</i>
</div>
<div class="f1"><i>esc</i><i>f1</i><i>f2</i><i>f3</i><i>f4</i><i>f5</i><i>f6</i><i>f7</i><i>f8</i><i>f9</i><i>f10</i><i>f11</i><i>f12</i></div>
<div><i k="oem_3">`</i><i>1</i><i>2</i><i>3</i><i>4</i><i>5</i><i>6</i><i>7</i><i>8</i><i>9</i><i>0</i><i>back</i></div>
<div><i>tab</i><i>q</i><i>w</i><i>e</i><i>r</i><i>t</i><i>y</i><i>u</i><i>i</i><i>o</i><i>p</i></div>
<div><i k="capital">caps</i><i>a</i><i>s</i><i>d</i><i>f</i><i>g</i><i>h</i><i>j</i><i>k</i><i>l</i><i k="enter">回车</i></div>
<div><i>shft</i><i>z</i><i>x</i><i>c</i><i>v</i><i>b</i><i>n</i><i>m</i><i k="home">hm</i><i k="up">↑</i><i k="end">ed</i></div>
<div>
<i k="oem_comma">,</i><i k="oem_period">.</i><i k="oem_2">/ </i><i k="oem_4"> { </i><i k="oem_6"> } </i><i k="semicolon"> ; </i>
<i k="oem_7"> ' </i><i k="oem_minu"> - </i><i k="oem_plus"> + </i><i k="left">←</i><i k="down">↓</i><i k="right">→</i>
</div>
<div><span onclick="toggle('#keyboard')">隐藏</span><i style="flex: 1; line-height: 46px; margin-left: 10px;">space</i></div>
</div>
其他说明
鼠标移动使用websocket实时通信后端, 做了防抖处理, 避免请求太多, 灵敏度高的时候鼠标会有卡顿, 还待优化
后端
初始化项目
go mod init dcontrol
下载依赖
go get github.com/getlantern/systray go get github.com/spf13/viper go get github.com/gorilla/websocket ...
编写代码
1.主函数
- 通过
go:embed指定静态资源目录, 可以直接将前端打包的资源封装入exe中 http.handlefunc将api前缀交给函数处理, 在函数里面具体处理子路由- 配置文件通过
config.yml加载, 可以指定快捷应用和启动端口
//go:embed webapp
var f embed.fs
func main() {
port := flag.int("p", 0, "server port")
base.runport = *port
filepath := flag.string("f", "./config.yml", "server config file")
// dir := flag.string("d", "./webapp", "server static dir")
flag.parse()
//1.加载配置
setting.init(*filepath)
base.runport = setting.conf.port
if *port != 0 {
base.runport = *port
}
addr := fmt.sprintf(":%d", base.runport)
http.handlefunc("/control-api/monitor/", monitor.handleapi)
http.handlefunc("/ws", ws.servews)
// 注册静态资源
st, _ := fs.sub(f, "webapp")
http.handle("/", http.stripprefix("/", http.fileserver(http.fs(st))))
err := http.listenandserve(addr, nil)
if err != nil {
fmt.println("start http error: ", err)
}
fmt.println("start http success ", base.runport)
}
2.handleapi 处理子路由
func handleapi(w http.responsewriter, r *http.request) {
w.header().set("access-control-allow-origin", "*") //允许访问所有域
w.header().add("access-control-allow-headers", "content-type") //header的类型
w.header().set("content-type", "application/json")
// 获取请求路径 strings.hassuffix
path := r.url.path
fmt.println("redis handleapi path:", path)
switch {
case strings.contains(path, "/getkeymap"):
getkeymap(w, r)
case strings.contains(path, "/getip"):
getip(w, r)
case strings.contains(path, "/getapps"):
getapps(w, r)
case strings.contains(path, "/sendkey"):
sendkey(w, r)
case strings.contains(path, "/open"):
open(w, r)
default:
http.notfound(w, r)
}
}
3.windows任务栏添加应用小图标和菜单
func gentaskbaricon() {
if runtime.goos == "windows" {
systray.run(onready, onexit)
}
}
func onready() {
systray.seticon(icondata)
systray.settitle("d-control")
systray.settooltip("d-control 右键点击打开菜单!")
menuopen := systray.addmenuitem("打开网页", "打开系统网页")
systray.addseparator()
menuquit := systray.addmenuitem("退出", "退出程序")
go func() {
for {
select {
case <-menuopen.clickedch:
openbrowser(fmt.sprintf("http://localhost:%d/", base.runport))
case <-menuquit.clickedch:
systray.quit()
os.exit(0)
}
}
}()
}
func onexit() {}
4.调用user32.dll, 实现模拟键盘输入和鼠标移动
var dll = syscall.newlazydll("user32.dll")
var prockeybd = dll.newproc("keybd_event")
var procsetcursorpos = dll.newproc("setcursorpos")
var procgetcursorpos = dll.newproc("getcursorpos")
var procmouseevent = dll.newproc("mouse_event")
func setmouse(x int, y int, isdiff bool) {
if isdiff {
procgetcursorpos.call(uintptr(unsafe.pointer(&cursorpos)))
fmt.println("cursorpos: ", cursorpos.x, cursorpos.y)
procsetcursorpos.call(uintptr(cursorpos.x+int32(x)), uintptr(cursorpos.y+int32(y)))
} else {
procsetcursorpos.call(uintptr(int32(x)), uintptr(int32(y)))
}
}
func clickmouse(str string) {
if str == "l" {
procmouseevent.call(mouseeventf_leftdown, 0, 0, 0, 0)
time.sleep(50 * time.millisecond) // 短暂延迟
procmouseevent.call(mouseeventf_leftup, 0, 0, 0, 0)
} else if str == "r" {
procmouseevent.call(mouseeventf_rightdown, 0, 0, 0, 0)
time.sleep(50 * time.millisecond) // 短暂延迟
procmouseevent.call(mouseeventf_rightup, 0, 0, 0, 0)
} else if str == "m" {
procmouseevent.call(mouseeventf_middledown, 0, 0, 0, 0)
time.sleep(50 * time.millisecond) // 短暂延迟
procmouseevent.call(mouseeventf_middleup, 0, 0, 0, 0)
}
}
func downkey(key int) {
flag := 0
if key < 0xfff { // detect if the key code is virtual or no
flag |= _keyeventf_scancode
} else {
key -= 0xfff
}
vkey := key + 0x80
prockeybd.call(uintptr(key), uintptr(vkey), uintptr(flag), 0)
}
func upkey(key int) {
flag := _keyeventf_keyup
if key < 0xfff {
flag |= _keyeventf_scancode
} else {
key -= 0xfff
}
vkey := key + 0x80
prockeybd.call(uintptr(key), uintptr(vkey), uintptr(flag), 0)
}
// 按键映射map
var keymap = map[string]int{
"shift": 0x10 + 0xfff,
"ctrl": 0x11 + 0xfff,
"alt": 0x12 + 0xfff,
"lshift": 0xa0 + 0xfff,
"rshift": 0xa1 + 0xfff,
"lcontrol": 0xa2 + 0xfff,
"rcontrol": 0xa3 + 0xfff,
"win": 0x5b + 0xfff,
...
}
5.websocket服务监听
func servews(w http.responsewriter, r *http.request) {
ws, err := upgrader.upgrade(w, r, nil)
if err != nil {
fmt.println("upgrade:", err)
return
}
fmt.println("servews connected......")
defer ws.close()
for {
// 读取消息
messagetype, msg, err := ws.readmessage()
if err != nil {
fmt.println("error while reading message:", err)
break
}
// 打印接收到的消息
fmt.printf("ws received: %s\n", msg)
wsdata := string(msg)
if wsdata == "pos,click" {
// go keys.runkeys(keys.keymap["lbutton"])
keys.clickmouse("l")
} else if wsdata == "pos,longclick" {
keys.clickmouse("r")
} else if strings.hasprefix(wsdata, "pos,start") {
parts := strings.split(wsdata, ",")
if len(parts) == 4 {
fx, _ := strconv.parsefloat(parts[2], 64)
fy, _ := strconv.parsefloat(parts[3], 64)
keys.setmouse(int(fx), int(fy), true)
}
}
// 可以选择回送消息给客户端
err = ws.writemessage(messagetype, msg)
if err != nil {
fmt.println("error while writing message:", err)
break
}
}
}
后言
搭配macast开源投屏神器, 躺在床上手机随时投屏视频到电脑上, 手机再遥控电脑音量和简易操作, 美滋滋了
源码地址
源码和程序截图详见github.com/dhjz/dcontrol
页面效果图见appimg目录


以上就是基于golang+vue编写一个手机远程控制电脑的懒人工具的详细内容,更多关于go vue手机远程控制电脑的资料请关注代码网其它相关文章!
发表评论