wails 和 gopsutil 构建系统工具应用(二)

在上一篇文章中,我们介绍了如何使用 wails 与第三方扩展包 getlantern/systray 配合开发带有托盘功能的系统工具。由于 wails v2 版本并不原生支持 systray 类似的托盘功能,因此我们通过将 getlantern/systray 作为子程序运行,并且通过进程间通信来解决这一问题。

这篇文章将详细讲解如何实现两个进程之间的通信,并完善相关功能。

进程间通信

在这个项目中,使用管道(pipe)来在主程序和子进程之间进行数据通信。管道是一种非常轻量的通信方式,适用于本地的进程间通信场景。

wails 的启动与关闭

wails 的应用中,需要在程序启动和关闭时处理与子进程的通信。为此,在 OnStartupOnShutdown 回调中设置相应的逻辑:

1
2
3
OnStartup:        app.startup,  
OnShutdown: app.closeup,

管道的创建与管理

app.gostartup 方法中,创建了两个管道,一个用于主程序向子进程发送数据,另一个用于接收子进程发送的数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

func (a *App) startup(ctx context.Context) {
a.ctx = ctx
// 创建两个管道:一个用于发送数据,一个用于接收数据
sendPipeR, sendPipeW, err := os.Pipe() // 用于主程序向子进程发送数据
if err != nil {
fmt.Println("创建发送管道失败:", err)
return
}
receivePipeR, receivePipeW, err := os.Pipe() // 用于子进程向主程序发送数据
if err != nil {
fmt.Println("创建接收管道失败:", err)
return
}
a.sendPipeW = sendPipeW // 主程序向子进程写
a.sendPipeR = sendPipeR // 主程序向子进程写
a.receivePipeR = receivePipeR // 主程序从子进程读
a.receivePipeW = receivePipeW // 主程序从子进程读

go func() {
// 启动 systray_run.go 程序
cmd := exec.Command("go", "run", "./pak/sys_run/systray_run.go")
cmd.Stdin = a.sendPipeR
cmd.Stdout = a.receivePipeW
cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil {
fmt.Println("启动 systray_run.go 失败:", err)
return
}
if err := cmd.Wait(); err != nil {
fmt.Println("systray_run.go 执行失败:", err)
} }()
a.monitorPipe()
}

monitorPipe 方法监听子进程向主程序发送的数据,并根据接收到的指令进行操作,例如退出程序、显示或隐藏主窗口:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
func (a *App) monitorPipe() {  
reader := bufio.NewReader(a.receivePipeR)
for {
line, err := reader.ReadString('\n') // 读取一行直到换行符
if err != nil {
if err == io.EOF {
fmt.Println("管道关闭")
break // 管道关闭,结束读取
}
fmt.Println("读取管道数据失败:", err)
continue
}
// 处理从 systray_run.go 中接收到的输出
fmt.Printf("从 systray_run.go 接收到: %s", line)
switch line {
case "systray_run: quit\n":
fmt.Println("收到退出请求")
closeType = 1
runtime.Quit(a.ctx)
break
case "systray_run: panel show\n":
fmt.Println("收到控制面板请求")
runtime.WindowShow(a.ctx) // 窗口显示
break
case "systray_run: panel hide\n":
fmt.Println("收到控制面板请求")
runtime.WindowHide(a.ctx) // 窗口隐藏
break
}
}}

子程序 systray 的实现

systray_run.go 中,托盘菜单设置了“控制面板”和“退出”选项。点击这些选项时,发送相应的指令给主程序。以下是主要的实现逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
package main  

import (
"bufio"
"fmt" "github.com/getlantern/systray" "github.com/getlantern/systray/example/icon" "io" "os")

var show = true

func onReady() {
systray.SetIcon(icon.Data)
systray.SetTitle("CPU usage: 0%")
systray.SetTooltip("PFinal南丞")
Panel := systray.AddMenuItem("控制面板", "Panel")
mQuit := systray.AddMenuItem("退出", "Quit the whole app")
go func() {
for {
select {
case <-Panel.ClickedCh:
toggerPanel()
case <-mQuit.ClickedCh:
sendQuitMessage()
} } }() go ListenToMain()
mQuit.SetIcon(icon.Data)
Panel.SetIcon(icon.Data)
// 启动 TCP 服务器
systray.Run(onReady, nil)
}

func ListenToMain() {
reader := bufio.NewReader(os.Stdin)
for {
line, err := reader.ReadString('\n')
if err != nil {
if err == io.EOF {
fmt.Println("管道关闭")
break // 管道关闭,结束读取
}
fmt.Println("读取标准输入失败:", err)
continue
}
fmt.Println("从标准输入读取到:" + line)
if line == "quit\n" {
fmt.Println("收到退出请求")
// 可以添加其他处理逻辑
systray.Quit() // 根据需要执行退出操作
}
}}

func sendQuitMessage() {
// 发送退出消息给主程序
_, _ = fmt.Fprintln(os.Stdout, "systray_run: quit") // 发送特定的退出消息
systray.Quit()
}

var toggerPanel = func() {
if show {
show = false
// 发送退出消息给主程序
_, _ = fmt.Fprintln(os.Stdout, "systray_run: panel hide") // 发送特定的退出消息
} else {
show = true
// 发送退出消息给主程序
_, _ = fmt.Fprintln(os.Stdout, "systray_run: panel show") // 发送特定的退出消息
}
}

func main() {
onReady()
}

通过 ListenToMain 方法,监听来自 wails 主程序的消息,做出相应的操作,比如当接收到 quit 消息时,托盘程序将退出。

通过上面, 实现了 wailssystray 的协同工作,并通过管道实现了两个进程之间的通信。此方法不仅解决了 wails 原生不支持托盘功能的问题,还为今后多进程应用的开发提供了很好的参考

动态调整窗口大小

在之前的项目中,遇到了一个挑战:即如何在不同游戏之间保持窗口大小的一致性。这个问题最终通过查阅 wails 的文档得到了解决。发现可以动态地调整窗口大小,因此在代码中添加了对 runtime.WindowSetSizeruntime.WindowReload 的调用,以实现在游戏间切换时自动调整窗口尺寸的功能。

具体来说,Greet 方法不仅返回问候信息,还在每次调用时动态地设置了窗口大小,并重新加载了窗口,确保了用户体验的一致性和流畅性。

1
2
3
4
5
6
func (a *App) Greet(name string) string {  
runtime.WindowSetSize(a.ctx, 1000, 500) // 动态设置窗口大小
runtime.WindowReload(a.ctx) // 重新加载窗口
return fmt.Sprintf("Hello %s, It's show time!", name)
}

最后

通过上述功能的开发,实现了 窗口托盘程序的联动, 接下来就可以动手开发, 监听系统的功能了.