按键精灵手机端开发安卓版实战:手写一个可最小化、可拖动的「运行日志悬浮窗」(附完整 源码 + 踩坑记录)

写过安卓脚本的人应该都有过这种体验:

脚本跑在手机上,你在电脑这头盯着 按键精灵助手的调试面板 看 TracePrint 的输出。一旦拔掉数据线、或者换成手机上直接跑,日志就彻底看不见了。脚本"跑飞了"你也不知道飞在哪一步、卡在哪个循环、找色失败了多少次。

常见的三种日志方案对比:

|-------------------|--------|-------|------------|----------------|
| 方案 | 实时性 | 需要连电脑 | 需要看文件 | 适合场景 |
| TracePrint 调试面板 | 实时 | ✅ 必须 | ❌ | 开发调试阶段 |
| Log.Open 写文件 | 事后 | ❌ | ✅ 要用文件管理器翻 | 长期运行记录 |
| 悬浮窗日志(本文) | 实时 | ❌ | ❌ | 真机跑脚本、无人值守 |

悬浮窗日志最大的好处是:脚本在手机上自己跑,日志就浮在屏幕上,随时能看;嫌挡视线就点一下最小化,缩成一个小图标。

先看效果与设计拆解

效果

  • 屏幕左上角一个半透明深色浮窗,绿色字体滚动输出运行日志;
  • 顶部有标题栏和「---」最小化按钮;
  • 按住浮窗任意位置可以拖动,防止挡住脚本操作;
  • 点「---」后缩成右下角一个 54×54 的小方块,点一下还原。

结构拆解

整个界面其实就是两个悬浮窗 + 若干控件:

复制代码
日志浮窗 (240×320)
├── 标题栏      文字控件 / 深灰底 / 左右内缩 5px
├── 标题文字    文字控件 / 白色 "运行日志"
├── btn_min     按钮控件 / 右上角 "---"  → 触发 LogMinimize
└── 日志内容    文字控件 / 亮绿 / 实时刷新

图标浮窗 (54×54)   ← 最小化后显示
└── 图标文字    文字控件 / "日志"
   整体点击 → 触发 LogRestore
复制代码

这里有个很关键的设计选择 :最小化不是"隐藏控件",而是两个独立浮窗互相显示/隐藏。原因后面踩坑部分会讲。


完整源码(可直接复制运行)

下面这份代码可以直接贴进按键精灵移动版,把 FindColor 那段换成你自己的业务逻辑即可。 文件扩展名保存为 .mq。

复制代码
// ================================================================
// 按键精灵移动版(MQ / Q语法)
// 功能:可拖动、可最小化的【运行日志悬浮窗】
// 环境:安卓 root / 无障碍(FW 系列命令最低客户端 3.3.8,无障碍 5.0.0)
// ----------------------------------------------------------------
// ⚠️ 核心约束:FW 的 SetOn* 事件函数内部【无法访问外部变量】,
//    因此 LogMinimize / LogRestore 里的浮窗名必须写成字符串字面量,
//    不能引用 FW_LOG、FW_ICON 这些全局变量。
// ================================================================

// ---------------- 全局配置(仅在主流程与普通函数中使用) ----------------
Dim FW_LOG = "日志浮窗"           ' 主日志浮窗名
Dim FW_ICON = "图标浮窗"          ' 最小化后的小图标浮窗名
Dim TV_LOG = "日志内容"           ' 日志正文控件名
Dim LBL_ICON = "图标文字"         ' 小图标上的文字控件名

Dim LOG_X = 10, LOG_Y = 60        ' 主浮窗初始坐标
Dim ICO_X = 0, ICO_Y = 300        ' 图标浮窗初始坐标
Dim LOG_W = 240, LOG_H = 320      ' 主浮窗宽高
Dim ICO_W = 54, ICO_H = 54        ' 图标浮窗宽高

Dim LOG_MAX = 12                  ' 日志窗最多保留的行数
Dim LOG_BUF = ""                  ' 日志文本缓冲
Dim NL = Chr(10)                  ' 换行符(若你的版本不换行,改成 Chr(13) & Chr(10))

// ================================================================
// 一、初始化:创建两个浮窗(全脚本只调用一次)
// ================================================================
Function LogInit()
    ' ---------- 1. 主日志浮窗 ----------
    FW.NewFWindow FW_LOG, LOG_X, LOG_Y, LOG_W, LOG_H
    FW.SetBackColor FW_LOG, "3A3A3A"    ' 16进制颜色,格式为 BBGGRR
    FW.Opacity FW_LOG, 85               ' 整体不透明度:0 全透明 ~ 100 不透明
    FW.IsTouchMove FW_LOG, 1            ' 允许整窗拖动
    FW.SetSmoothing FW_LOG, 5           ' 圆角

    ' 标题栏:左右各内缩 5px,绕开浮窗圆角,避免方形控件把圆角盖成直角
    FW.AddTextView FW_LOG, "标题栏", "", 5, 5, LOG_W - 10, 24
    FW.SetBackColor "标题栏", "2B2B2B"

    FW.AddTextView FW_LOG, "标题文字", "运行日志", 12, 8, LOG_W - 60, 20
    FW.SetTextColor "标题文字", "FFFFFF"
    FW.SetTextSize "标题文字", 10

    ' 最小化按钮(右上角)
    FW.AddButton FW_LOG, "btn_min", "---", LOG_W - 30, 8, 22, 20
    FW.SetBackColor "btn_min", "2B2B2B"
    FW.SetTextColor "btn_min", "FFFFFF"
    FW.SetOnClick "btn_min", LogMinimize

    ' 日志正文
    FW.AddTextView FW_LOG, TV_LOG, "", 10, 36, LOG_W - 20, LOG_H - 46
    FW.SetTextColor TV_LOG, "0FE7A2"    ' BBGGRR → RGB(162,231,15) 亮绿
    FW.SetTextSize TV_LOG, 10

    FW.Show FW_LOG

    ' ---------- 2. 小图标浮窗(点击还原日志) ----------
    FW.NewFWindow FW_ICON, ICO_X, ICO_Y, ICO_W, ICO_H
    FW.SetBackColor FW_ICON, "3A3A3A"
    FW.SetSmoothing FW_ICON, 5
    FW.IsTouchMove FW_ICON, 1

    ' 关键:点击事件绑在【悬浮窗本身】,不再额外套一个按钮
    ' 否则按钮会吃掉触摸事件,导致"能拖动浮窗、拖不动按钮"
    FW.SetOnClick FW_ICON, LogRestore

    FW.AddTextView FW_ICON, LBL_ICON, "日志", 0, 0, ICO_W, ICO_H
    FW.SetTextColor LBL_ICON, "0FE7A2"
    FW.SetTextSize LBL_ICON, 10
End Function

// ================================================================
// 二、最小化 / 还原(悬浮窗事件函数)
// ⚠️ 事件函数内读不到外部变量,所以这里的名字只能写字面量
// ================================================================
Function LogMinimize()
    FW.Hide "日志浮窗"      ' 隐藏大窗
    FW.Show "图标浮窗"      ' 显示小图标
End Function

Function LogRestore()
    FW.Hide "图标浮窗"      ' 隐藏小图标
    FW.Show "日志浮窗"      ' 恢复大窗
End Function

// ================================================================
// 三、写日志(核心,普通函数,可以正常使用全局变量)
// 参数:text ------ 一行日志内容
// 说明:自动加时间戳;超过 LOG_MAX 行自动丢弃最旧的,模拟滚动效果
// ================================================================
Function LogAdd(text)
    LOG_BUF = LOG_BUF & "[" & LogStamp() & "] " & text & NL
    LOG_BUF = LogKeepLast(LOG_BUF, LOG_MAX)
    FW.SetText TV_LOG, LOG_BUF          ' FW.SetText 需要客户端 3.5.1 及以上
End Function

' 只保留最后 n 行
Function LogKeepLast(s, n)
    Dim arr = Split(s, NL)
    Dim total = Len(arr)
    Dim i, out
    If total <= n Then
        LogKeepLast = s
        Exit Function
    End If
    out = ""
    For i = total - n + 1 To total
        out = out & arr[i] & NL
    Next
    LogKeepLast = out
End Function

' 时:分:秒(由设备系统时间戳换算,用作日志前缀)
Function LogStamp()
    Dim sec = Time() Mod 86400
    LogStamp = Pad0(sec \ 3600) & ":" & Pad0((sec Mod 3600) \ 60) & ":" & Pad0(sec Mod 60)
End Function

' 个位数补零
Function Pad0(v)
    If v < 10 Then
        Pad0 = "0" & v
    Else
        Pad0 = CStr(v)
    End If
End Function

// ================================================================
// 四、主流程演示:循环找色,把每次结果实时写进日志悬浮窗
// ================================================================
LogInit
LogAdd "脚本启动,开始监听目标颜色"

Dim intX, intY, count = 0
Do
    count = count + 1

    ' 0,0,0,0 表示全屏范围;实际使用时换成你自己的目标颜色与容差
    FindColor 0, 0, 0, 0, "FFFFFF", 0, 0.9, intX, intY

    If intX > -1 Then
        LogAdd "第" & count & "次:找到目标 (" & intX & "," & intY & "),执行点击"
        Tap intX, intY
    Else
        LogAdd "第" & count & "次:未找到目标,1 秒后重试"
    End If

    Delay 1000
Loop

' 脚本停止后,悬浮窗会自动消失,无需手动清理

核心代码逐段拆解

1. 创建浮窗:FW.NewFWindow

|----------------|--------|-------------|
| 参数 | 类型 | 说明 |
| fwName | string | 悬浮窗名称(全局唯一) |
| leftX / topY | number | 屏幕坐标,默认 0 |
| width / height | number | 宽高,省略则自适应内容 |

两个必须知道的点:

  • 一个脚本最多创建 32 个悬浮窗;
  • 脚本停止后浮窗会自动消失,不用手动清理。

2. 添加子控件

注意 leftX / topY 是相对于父控件(这里是浮窗)左上角的坐标,不是屏幕绝对坐标。整个浮窗内部就是一个简易的相对布局系统。

3. 外观设置

|-----------|--------------------------------------|-------------------|
| 目的 | 命令 | 说明 |
| 浮窗背景色 | FW.SetBackColor(名称, "3A3A3A") | 16 进制,格式 BBGGRR |
| 整体不透明度 | FW.Opacity(名称, 85) | 0 全透明 ~ 100 不透明 |
| 圆角 | FW.SetSmoothing(名称, 5) | 数值越大越圆 |
| 允许拖动 | FW.IsTouchMove(名称, 1) | 1 允许 / 0 禁止 |
| 文字颜色 / 字号 | FW.SetTextColor / FW.SetTextSize | 按控件设置 |
| 运行时改文字 | FW.SetText(eleName, text) | 就是靠它刷新日志 |

4. 写日志的核心:LogAdd

原文的浮窗里塞的是一段固定文字,看着像日志,其实不会变。真正的日志系统必须能动态追加 ,靠的就是 FW.SetText:

三个细节:

  • 加时间戳,方便定位"卡在哪一秒";
  • 只保留最后 N 行:文字控件高度是固定的,行数超了不会自动滚动,手动裁掉旧行比什么都简单可靠;
  • 用字符串拼接缓冲 ,不要用 table 去做队列------按键的 table 是可以 t[Len(t)+1] = v 追加,但每次都要重排、转字符串,不如直接拼字符串来得直观。

业务代码里只要这样调用:

复制代码
If intX > -1 Then
    LogAdd "第" & count & "次:找到目标 (" & intX & "," & intY & ")"
Else
    LogAdd "第" & count & "次:未找到目标,1 秒后重试"
End If

【日志窗里滚动的效果,能看到时间戳前缀】


踩坑记录(这部分最重要)

坑 1(最隐蔽):悬浮窗事件函数里读不到全局变量

这是这次最大的收获。MQ 的官方语法说明里明确写着:

在悬浮窗控件的事件函数(如 SetOnClick 这类以 SetOn 开头的事件)中,无法访问外部变量,只能使用事件函数内部的局部变量。

也就是说,你下面这么写是无效的:

复制代码
Dim FW_LOG = "日志浮窗"

Function LogMinimize()
    FW.Hide FW_LOG        ' ❌ FW_LOG 在这里是 null,浮窗名传进去是空的
End Function

必须写成字面量:

复制代码
Function LogMinimize()
    FW.Hide "日志浮窗"     ' ✅ 直接写名字
    FW.Show "图标浮窗"
End Function

所以浮窗名、控件名建议一次定好,之后当成"常量"用字符串直接写。 这也解释了为什么很多示例代码里事件函数中的名字都是硬编码的------不是作者懒得抽变量,是这套机制不允许。

顺带一提:普通函数(比如上面的 LogAdd)不受这个限制 ,全局变量照常用。受限的只有 FW.SetOn* 绑定的事件函数。

坑 2:顶部标题栏会把浮窗的圆角切成直角

现象:浮窗设了 FW.SetSmoothing,但顶部那条深色标题栏是方形的,直接把上面两个圆角盖住了。

原因很简单:子控件永远是矩形,绘制在浮窗之上,不会继承浮窗的圆角裁剪。 只要控件贴着左右边缘且铺满颜色,圆角就一定被遮。

两个解决思路:

  • 方案 A(本文用的) :标题栏不贴边,左右各内缩 5px(leftX = 5, width = 浮窗宽 - 10),同时把标题栏和浮窗的底色设成很接近的深灰(2B2B2B / 3A3A3A),视觉上几乎看不出那 5px 的接缝;
  • 方案 B:干脆不给标题栏设背景色,标题只用一个文字控件,靠浮窗自身的背景色撑住。

如果非要"完美圆角 + 实心标题栏",那得靠两张带圆角的图片去贴,成本不划算。

坑 3:小图标上的按钮点得动、却拖不动

现象:主浮窗能随便拖,但最小化后那个小图标怎么拖都不动。

原因:FW.IsTouchMove 是作用在悬浮窗 这一层的;而你在浮窗里放了一个铺满的按钮,触摸落在按钮上时事件被子控件消费掉了,浮窗根本收不到拖动。

修复:小图标不要套按钮,直接把点击事件绑在悬浮窗本身上 。FW.SetOnClick 的第一个参数支持"悬浮窗或控件名称",所以这样写是合法的:

复制代码
FW.NewFWindow "图标浮窗", 0, 300, 54, 54
FW.IsTouchMove "图标浮窗", 1          ' 整窗可拖
FW.SetOnClick  "图标浮窗", LogRestore ' 整窗可点

这样拖动和点击都在同一层级,不会互相吞事件。

如果实测发现你的版本上"整窗点击"没能触发,退一步的稳妥做法是:保留按钮但别让它铺满,浮窗做成 70×70、按钮只占中间 46×46,留出一圈可拖动的边缘。

坑 4:颜色是 BBGGRR,不是 RRGGBB

这条坑过无数人。FW 系列命令里的颜色字符串,格式是 BBGGRR (和按键精灵取色命令一致),不是网页/CSS 里的 RRGGBB。

对照一下就明白了:

|--------|----------|----------------------------|
| 颜色 | FW 里应写 | 说明 |
| 红色 | 0000FF | B=00, G=00, R=FF |
| 纯绿 | 00FF00 | 这次 G=FF |
| 白色 | FFFFFF | 顺序无所谓 |
| 深灰 | 3A3A3A | 顺序无所谓 |
| 本文的日志绿 | 0FE7A2 | B=0F, G=E7, R=A2 → 实际显示为亮绿 |

所以灰色系随便写,彩色一定要换算 。你从取色器拿到的 RRGGBB,要反过来写成 BBGGRR 才会显示成你想要的颜色。

坑 5:FW.Hide 和 FW.Close 不是一回事

  • FW.Hide ------ 隐藏 ,之后 FW.Show 还能显示回来(最小化要用这个);
  • FW.Close ------ 关闭并销毁 ,销毁后的浮窗不能再 FW.Show ,必须重新 FW.NewFWindow。

很多人做最小化用错了 Close,结果点还原直接没反应。


这套悬浮窗日志我现在的用法是把它当成**脚本的""仪表盘""**∶循环次数、找色成功/失败、超时重试、当前状态全部实时打上去。脚本在手机上一跑几小时,出了什么问题扫一眼浮窗就知道,比事后翻日志文件快得多。

代码里两个关键点再强调一遍,别的都可以忘:│

1.**Fw的'Seton*'事件函数读不到全局变量**---浮窗名---律写字面量;

2.**颜色是`BBGGRR'不是'RRGGBB~**------彩色必须换算。

如果这篇对你有帮助,点个赞收藏一下。有任何关于按键精灵移动端开发安卓版的问题,或者想看"悬浮窗日志+多线程"的做法,欢迎评论区交流。

复制代码
复制代码
相关推荐
终端安全笔记1 小时前
安卓做 MDM 管控,先分清三种注册入口
android·安全·智能手机
天远Date Lab1 小时前
零信任架构实战:基于天远全能消金报告(标准版)构建自动化骑手资信评估网关
运维·人工智能·架构·自动化
赛博守夜人2 小时前
反舞弊系列之二十一:数据驱动型反舞弊体系搭建,如何利用ERP与财务数据构建“异常交易自动化风控看板”?
运维·自动化
VIP_CQCRE2 小时前
Visual Studio 也能接入统一 AI 能力:用 Ace Data Cloud + LMLocal 打通 OpenAI 兼容模型
openai·ai编程·开发工具·visual studio·acedatacloud
乌萨达2 小时前
手机模拟器安卓怎么用?电脑安装应用、玩手游与账号同步指南
android·智能手机·电脑·雷电模拟器
IT小白杨3 小时前
市场调研的自动化访问,怎样控制节奏才不被误判?
运维·经验分享·安全·自动化·指纹浏览器
VIP_CQCRE3 小时前
在 Visual Studio 里接入 AI 编程助手:用 Ace Data Cloud + LMLocal 打通 OpenAI 兼容模型
ai编程·visual studio·openai兼容·ace data cloud·lmlocal
张晓宇 .3 小时前
Codex 使用教程(2026年9月 0.158 最新版):安装、国内接入 DeepSeek / GPT-6、config.toml 配置与报错排查
gpt·大模型·openai·ai编程·codex·deepseek