年轻人的第一个 GO 桌面应用:用 Wails 做个学习搭子计时器

告别控制台应用,用Go语言打造你的第一个桌面软件

🎯 痛点场景:为什么需要桌面应用开发技能?

你是否遇到这些问题?

  • 学了Go语言,却只会写命令行工具?
  • 想学桌面开发,却觉得C++/C#太复杂?
  • 面试官对你的"烂大街"项目不感兴趣?

你能学到什么?

  • ✅️ Wails 基础使用
  • ✅️ 实时事件通信
  • ✅️ 系统托盘集成
  • ✅️ 多线程编程
  • ✅️ 附完整源码

🛠️ 环境准备

步骤1:安装Go语言

bash 复制代码
# 官网下载安装包:https://golang.org/dl/
go version  # 验证安装

步骤2:安装Wails框架

bash 复制代码
go install github.com/wailsapp/wails/v2/cmd/wails@latest
wails version  # 验证安装

步骤3:创建项目骨架

bash 复制代码
wails init -n Punktime
cd Punktime

🚀 核心功能开发

设计 UI (前端)

  • 使用 HTML/CSS/JavaScript实现:

    • 时间/倒计时主界面
    • 倒计时设置遮罩层
  • 示例代码

html 复制代码
<!-- ============================================================================
     * 主界面结构
     * 应用程序的主要显示区域
     * ============================================================================ -->

    <!-- 主容器:显示倒计时/时间 -->
    <div class="container">
      <div class="countdown" id="countdown"></div>
    </div>

    <!-- ============================================================================
     * 倒计时输入框模态对话框
     * 用于设置倒计时时长的弹出窗口
     * ============================================================================ -->

    <!-- 倒计时输入框遮罩层 -->
    <div
      id="countdownInputOverlay"
      class="countdown-input-overlay"
      style="display: none"
    >
      <div class="countdown-input-container">
        <div class="countdown-input-title"></div>
        <div>
          <!-- 分钟输入框 -->
          <input
            type="number"
            id="minutesInput"
            class="countdown-input"
            min="0"
            max="59"
            value="25"
            placeholder="分"
          />
          <span style="color: lightgreen">:</span>
          <!-- 秒数输入框 -->
          <input
            type="number"
            id="secondsInput"
            class="countdown-input"
            min="0"
            max="59"
            value="0"
            placeholder="秒"
          />
        </div>
        <div class="countdown-input-buttons">
          <!-- 确认按钮 -->
          <button id="confirmCountdown" class="countdown-input-button">
            确定
          </button>
          <!-- 取消按钮 -->
          <button id="cancelCountdown" class="countdown-input-button">
            取消
          </button>
        </div>
      </div>
    </div>

实现计时逻辑(Go后端)

  • 核心代码
Go 复制代码
/**
 * @description: 计时器更新循环,每秒执行一次的时间/倒计时更新逻辑
 * 函数作为计时器管理器的核心循环,负责:
 * 1. 每秒更新计时器状态和显示内容
 * 2. 根据当前显示模式(倒计时/时间)执行不同的更新逻辑
 * 3. 处理倒计时结束事件和状态转换
 * 4. 通过Wails事件系统向前端发送更新数据
 *
 * 倒计时模式逻辑:
 * - 运行中:计算剩余时间,倒计时结束时触发timerEnd事件
 * - 暂停中:显示暂停时的剩余时间
 * - 未开始:显示设置的倒计时总时长
 *
 * 时间模式逻辑:
 * - 显示当前系统时间(MM:SS格式)
 *
 * @example
 * // 每秒自动执行,无需手动调用
 * // 倒计时模式:显示"25:00" → "24:59" → ... → "00:00"(触发结束事件)
 * // 时间模式:显示当前时间如"14:30"
 *
 * @see runtime.EventsEmit 发送事件到前端
 * @see time.Since 计算时间间隔
 *
 * @note 使用互斥锁确保线程安全,避免并发访问计时器状态
 * @note 倒计时结束时自动重置状态并发送结束事件
 * @note 时间格式统一为两位数(如"05:09"而非"5:9")
 */
func (tm *TimerManager) updateLoop() {
    for range tm.ticker.C {
        tm.mu.Lock()
        if tm.isTimerRunning {
                tm.timerElapsed = time.Since(tm.timerStartTime)
        }
        switch tm.displayMode {
        case "countdown":
                if tm.isTimerRunning {
                        remaining := tm.countdownDuration - tm.timerElapsed
                        if remaining <= 0 {
                                remaining = 0
                                tm.isTimerRunning = true
                                tm.timerElapsed = tm.countdownDuration
                                tm.isPaused = false
                                runtime.EventsEmit(tm.ctx, "timerEnd")

                                timerText := "00:00"
                                runtime.EventsEmit(tm.ctx, "timerUpdate", timerText)
                        } else {
                                minutes := int(remaining.Minutes())
                                seconds := int(remaining.Seconds()) % 60
                                timerText := fmt.Sprintf("%02d:%02d", minutes, seconds)
                                runtime.EventsEmit(tm.ctx, "timerUpdate", timerText)
                        }

                } else {
                        if tm.isPaused {
                                remaining := tm.countdownDuration - tm.timerElapsed
                                if remaining < 0 {
                                        remaining = 0
                                }
                                minutes := int(remaining.Minutes())
                                seconds := int(remaining.Seconds()) % 60
                                timerText := fmt.Sprintf("%02d:%02d", minutes, seconds)
                                runtime.EventsEmit(tm.ctx, "timerUpdate", timerText)
                        } else {
                                minutes := int(tm.countdownDuration.Minutes())
                                seconds := int(tm.countdownDuration.Seconds()) % 60
                                timerText := fmt.Sprintf("%02d:%02d", minutes, seconds)
                                runtime.EventsEmit(tm.ctx, "timerUpdate", timerText)
                        }
                }

        case "time":
                currentTime := time.Now().Format("15:04")
                runtime.EventsEmit(tm.ctx, "timeUpdate", currentTime)

        }
        tm.mu.Unlock()
}
}
  • 前后端通信
    • 前端负责监听更新事件并渲染
    • 后端负责触发更新事件并发送数据
js 复制代码
 // 监听倒计时更新事件
  window.runtime.EventsOn("timerUpdate", (data) => {
    document.getElementById("countdown").textContent = data;
  });
  // 监听时间显示更新事件
  window.runtime.EventsOn("timeUpdate", (data) => {
    document.getElementById("countdown").textContent = data;
  });
go 复制代码
if tm.isTimerRunning {
    remaining := tm.countdownDuration - tm.timerElapsed
    if remaining <= 0 {
            remaining = 0
            tm.isTimerRunning = true
            tm.timerElapsed = tm.countdownDuration
            tm.isPaused = false
            runtime.EventsEmit(tm.ctx, "timerEnd")

            timerText := "00:00"
            // 发送事件显示更新事件
            runtime.EventsEmit(tm.ctx, "timerUpdate", timerText)
    } else {
            minutes := int(remaining.Minutes())
            seconds := int(remaining.Seconds()) % 60
            timerText := fmt.Sprintf("%02d:%02d", minutes, seconds)
            // 发送事件显示更新事件
            runtime.EventsEmit(tm.ctx, "timerUpdate", timerText)
    }

📦️ 打包发布

bash 复制代码
wails build

📖 学习资源

👋你还希望有哪些学习搭子?欢迎留言!

❤️如果对你有帮助,别忘了点赞 + 关注!

相关推荐
Tsblns3 小时前
从Go http.HandleFunc()函数 引出"函数到接口"的适配思想
go
狼爷1 天前
Go 重试机制终极指南:基于 go-retry 打造可靠容错系统
架构·go
不爱笑的良田1 天前
从零开始的云原生之旅(十六):金丝雀发布实战:灰度上线新版本
云原生·容器·kubernetes·go
嘿嘿2 天前
使用 Gin 框架加载 HTML 模板:`LoadHTMLGlob` 和 `LoadHTMLFiles` 的比较与优化
后端·go·gin
Java水解2 天前
为何最终我放弃了 Go 的 sync.Pool
后端·go
得物技术2 天前
Golang HTTP请求超时与重试:构建高可靠网络请求|得物技术
java·后端·go
喵个咪3 天前
Kratos 下使用 Protobuf FieldMask 完全指南
后端·go
Mgx3 天前
内存网盘 - Go语言实现的WebDAV内存文件系统
go
百锦再4 天前
第15章 并发编程
android·java·开发语言·python·rust·django·go