1. 集成autogui-cpp库,以支持后续的自动化操作需求
2. 增加了串口设备管理类,以支持无法联网的嵌入式设备接入Yosuga 3. 基于COBS编码以解决串口收发的粘包问题
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
# IDEs
|
||||
/.idea
|
||||
/example/.idea
|
||||
.vscode
|
||||
|
||||
# Build files
|
||||
/cmake-build-debug
|
||||
/cmake-build-release
|
||||
/example/cmake-build-debug
|
||||
|
||||
|
||||
/build
|
||||
Vendored
+64
@@ -0,0 +1,64 @@
|
||||
cmake_minimum_required(VERSION 3.10)
|
||||
project(autogui-cpp VERSION 1.0.0 LANGUAGES CXX)
|
||||
|
||||
# 基础配置
|
||||
set(CMAKE_CXX_STANDARD 17) # Cpp17
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||
|
||||
# 创建库目标
|
||||
add_library(autogui-cpp STATIC
|
||||
src/Keyboard.cpp
|
||||
src/Mouse.cpp
|
||||
src/Utils.cpp
|
||||
src/Autogui.cpp
|
||||
)
|
||||
|
||||
# 设置头文件搜索路径
|
||||
# 使用生成器表达式支持add_subdirectory和install两种使用方式
|
||||
target_include_directories(autogui-cpp PUBLIC
|
||||
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/src>
|
||||
$<INSTALL_INTERFACE:include/autogui-cpp>
|
||||
)
|
||||
|
||||
# 平台特定配置
|
||||
# macOS平台
|
||||
if(APPLE)
|
||||
# 查找并链接ApplicationServices框架
|
||||
# 该框架包含CoreGraphics和Carbon相关API
|
||||
find_library(APPLICATIONSERVICES_LIBRARY ApplicationServices REQUIRED)
|
||||
target_link_libraries(autogui-cpp PUBLIC ${APPLICATIONSERVICES_LIBRARY})
|
||||
|
||||
# Windows平台
|
||||
elseif(WIN32)
|
||||
# 链接User32.lib (提供SendInput, GetCursorPos等API)
|
||||
target_link_libraries(autogui-cpp PUBLIC User32)
|
||||
|
||||
# Linux/Unix平台
|
||||
elseif(UNIX AND NOT APPLE)
|
||||
# Linux需要X11后端(Wayland尚未支持)
|
||||
# Linux需要X11后端
|
||||
find_package(X11 REQUIRED)
|
||||
# 查找Xtst库(XTest扩展)
|
||||
find_library(X11_Xtst_LIB Xtst)
|
||||
if(NOT X11_Xtst_LIB)
|
||||
message(FATAL_ERROR "Xtst library not found. Install libxtst-dev.")
|
||||
endif()
|
||||
# 查找Xext库(X扩展)
|
||||
find_library(X11_Xext_LIB Xext)
|
||||
if(NOT X11_Xext_LIB)
|
||||
message(FATAL_ERROR "Xext library not found.")
|
||||
endif()
|
||||
# 链接所有必需的X11库
|
||||
target_link_libraries(autogui-cpp PUBLIC
|
||||
${X11_LIBRARIES}
|
||||
${X11_Xtst_LIB}
|
||||
${X11_Xext_LIB}
|
||||
)
|
||||
target_include_directories(autogui-cpp PUBLIC ${X11_INCLUDE_DIR})
|
||||
endif()
|
||||
|
||||
# 集成方式: add_subdirectory
|
||||
# 在你的项目CMakeLists.txt中:
|
||||
# add_subdirectory(autogui-cpp)
|
||||
# target_link_libraries(your_target PRIVATE autogui-cpp)
|
||||
Vendored
+118
@@ -0,0 +1,118 @@
|
||||
# autogui-cpp
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
本项目继承自[Robot CPP](https://github.com/developer239/robot-cpp),在此感谢该项目的贡献。
|
||||
|
||||
|
||||
原先的项目仅支持`Windows`与`Mac OS`,本项目增加了对`Linux X11`的支持,`Linux Wanland`的支持正在开发中
|
||||
|
||||
为什么要创建本项目,这是因为我在苦苦寻找`pyautogui`的cpp实现,但很遗憾,我并没有找到这个轮子。
|
||||
|
||||
本项目在原项目的基础上,增加了以下文件:
|
||||
1. `Autogui.cpp/.h`
|
||||
2. `SimpleAutoGUI.h`
|
||||
|
||||
并修复了原项目的bug:
|
||||
1. 模拟键盘输入的时候,无法处理大小写与符号的转换
|
||||
|
||||
最终实现了与`pyautogui`类似的功能。
|
||||
|
||||
但是,本项目不支持以下功能:
|
||||
1. 模拟键盘输入只支持`ASCII`字符,如果你需要输入中文,或者其他语言的字符,这可能是一个很大的工程量。
|
||||
更好的解决方案是将内容copy到剪贴板,然后粘贴到目标位置。
|
||||
|
||||
同时,本项目裁剪掉了原项目的部分功能:
|
||||
1. `Hooks`
|
||||
2. `Screen`
|
||||
3. `Record`
|
||||
|
||||
主要是因为有着更加完美的上位替代品,例如`Screen`可用`Qt`的`QScreen`替代,并且支持的更加完美。
|
||||
|
||||
注意:原项目并没有说明代码的开源协议,因此关于`Robot-cpp`部分的代码,解释权归属于原作者`
|
||||
developer239`。
|
||||
|
||||
本项目被我直接使用在[Yosuga](https://github.com/Misakityan/Yosuga)这个我自己的项目当中。
|
||||
|
||||
项目已在以下平台完成测试:
|
||||
1. Windows 10
|
||||
2. kUbuntu 24.04
|
||||
|
||||
由于我没有Mac PC, 因此并未在mac上进行测试,不过mac部分都是直接使用`Robot-cpp`部分的代码,原作者测试没问题,
|
||||
大概也是可以使用的。如果遇到bug,尽情提issue即可。
|
||||
|
||||
## How to use?
|
||||
想使用本项目十分容易,在你的项目CMakeLists.txt中:
|
||||
```cmake
|
||||
add_subdirectory(autogui-cpp)
|
||||
target_link_libraries(your_target PRIVATE autogui-cpp) # your_target为你项目的构建目标
|
||||
# 注意:有时候你还需要根据导入本项目的位置,在autogui-cpp的前面增加一些路径信息
|
||||
```
|
||||
之后,在你需要的地方`#include "SimpleAutoGUI.h"`,访问AutoGUI当中已经封装好的函数即可使用。
|
||||
|
||||
|
||||
## some examples
|
||||
```c++
|
||||
#include <iostream>
|
||||
#include "SimpleAutoGUI.h"
|
||||
int main() {
|
||||
Robot::Point screenSize = AutoGUI::size();
|
||||
std::cout << "屏幕尺寸: " << screenSize.x << "x" << screenSize.y << std::endl;
|
||||
|
||||
// 测试1: 移动鼠标
|
||||
std::cout << "移动鼠标到屏幕中心..." << std::endl;
|
||||
int centerX = screenSize.x / 2;
|
||||
int centerY = screenSize.y / 2;
|
||||
AutoGUI::moveTo(centerX, centerY, 2);
|
||||
|
||||
// 测试2: 单击
|
||||
std::cout << "左键单击..." << std::endl;
|
||||
AutoGUI::click();
|
||||
AutoGUI::sleep(0.5);
|
||||
|
||||
// 测试3: 右键单击
|
||||
std::cout << "右键单击..." << std::endl;
|
||||
AutoGUI::rightSingle();
|
||||
AutoGUI::sleep(0.5);
|
||||
|
||||
// 测试4: 双击
|
||||
std::cout << "左键双击..." << std::endl;
|
||||
AutoGUI::leftDouble();
|
||||
AutoGUI::sleep(0.5);
|
||||
|
||||
// 测试5: 拖拽
|
||||
std::cout << "拖拽测试..." << std::endl;
|
||||
AutoGUI::dragRel(100, 100, 0.5);
|
||||
AutoGUI::sleep(0.5);
|
||||
|
||||
// 测试6: 输入文本
|
||||
std::cout << "输入文本测试..." << std::endl;
|
||||
AutoGUI::type("Hello, AutoGUI!", 0.05); // 每个字符间隔50ms
|
||||
AutoGUI::sleep(0.5);
|
||||
|
||||
// 测试7: 快捷键
|
||||
std::cout << "快捷键测试 (Ctrl+A)..." << std::endl;
|
||||
AutoGUI::hotkey({AutoGUI::Keys::CTRL, "a"});
|
||||
AutoGUI::sleep(0.5);
|
||||
|
||||
std::cout << "快捷键测试 (Ctrl+C)..." << std::endl;
|
||||
AutoGUI::hotkey({AutoGUI::Keys::CTRL, "c"});
|
||||
AutoGUI::sleep(0.5);
|
||||
|
||||
// 测试8: 滚动
|
||||
std::cout << "向上滚动..." << std::endl;
|
||||
AutoGUI::scroll(5);
|
||||
AutoGUI::sleep(0.5);
|
||||
|
||||
std::cout << "向下滚动..." << std::endl;
|
||||
AutoGUI::scroll(-5);
|
||||
AutoGUI::sleep(0.5);
|
||||
|
||||
// 测试9: 获取鼠标位置
|
||||
Robot::Point pos = AutoGUI::position();
|
||||
std::cout << "当前鼠标位置: (" << pos.x << ", " << pos.y << ")" << std::endl;
|
||||
return 0;
|
||||
}
|
||||
```
|
||||
Vendored
+562
@@ -0,0 +1,562 @@
|
||||
//
|
||||
// Created by misaki on 2026/1/25.
|
||||
//
|
||||
|
||||
#include "Autogui.h"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <iomanip>
|
||||
#include <iostream>
|
||||
#include <random>
|
||||
#include <thread>
|
||||
|
||||
namespace AutoGUI {
|
||||
|
||||
// 内部辅助函数
|
||||
namespace {
|
||||
|
||||
// 等待指定毫秒数
|
||||
void delayMs(const int ms) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
|
||||
}
|
||||
|
||||
// 将秒转换为毫秒
|
||||
int secondsToMs(double seconds) { return static_cast<int>(seconds * 1000); }
|
||||
|
||||
// 获取当前鼠标位置(内部使用)
|
||||
Robot::Point getCurrentPosition() { return Robot::Mouse::GetPosition(); }
|
||||
|
||||
} // namespace
|
||||
|
||||
// 实现主要API函数
|
||||
void moveTo(int x, int y, double duration) {
|
||||
Robot::Point target{x, y};
|
||||
|
||||
if (duration > 0.0) {
|
||||
Robot::Mouse::MoveSmooth(target);
|
||||
} else {
|
||||
Robot::Mouse::Move(target);
|
||||
}
|
||||
}
|
||||
|
||||
void moveRel(int xOffset, int yOffset, double duration) {
|
||||
Robot::Point current = getCurrentPosition();
|
||||
Robot::Point target{current.x + xOffset, current.y + yOffset};
|
||||
|
||||
moveTo(target.x, target.y, duration);
|
||||
}
|
||||
|
||||
void click(int x, int y, Button button, int clicks, double interval) {
|
||||
// 如果提供了坐标,先移动到该位置
|
||||
if (x >= 0 && y >= 0) {
|
||||
moveTo(x, y);
|
||||
delayMs(10);
|
||||
}
|
||||
|
||||
Robot::MouseButton robotButton = toRobotButton(button);
|
||||
|
||||
for (int i = 0; i < clicks; i++) {
|
||||
if (clicks == 2 && i == 0) {
|
||||
// 双击
|
||||
Robot::Mouse::DoubleClick(robotButton);
|
||||
} else {
|
||||
Robot::Mouse::Click(robotButton);
|
||||
}
|
||||
|
||||
// 如果不是最后一次点击,并且设置了间隔,则等待
|
||||
if (i < clicks - 1 && interval > 0) {
|
||||
sleep(interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void leftDouble(int x, int y) { click(x, y, Button::LEFT, 2); }
|
||||
|
||||
void rightSingle(int x, int y) { click(x, y, Button::RIGHT, 1); }
|
||||
|
||||
void middleClick(int x, int y) { click(x, y, Button::MIDDLE, 1); }
|
||||
|
||||
void mouseDown(Button button, int x, int y) {
|
||||
// 如果提供了坐标,先移动到该位置
|
||||
if (x >= 0 && y >= 0) {
|
||||
moveTo(x, y);
|
||||
delayMs(10);
|
||||
}
|
||||
|
||||
Robot::Mouse::ToggleButton(true, toRobotButton(button));
|
||||
}
|
||||
|
||||
void mouseUp(Button button, int x, int y) {
|
||||
// 如果提供了坐标,先移动到该位置
|
||||
if (x >= 0 && y >= 0) {
|
||||
moveTo(x, y);
|
||||
delayMs(10);
|
||||
}
|
||||
|
||||
Robot::Mouse::ToggleButton(false, toRobotButton(button));
|
||||
}
|
||||
|
||||
void drag(const int x1, const int y1, const int x2, const int y2,
|
||||
const double duration, const Button button) {
|
||||
// 参数验证
|
||||
if (!isValidCoord(x1, y1)) {
|
||||
throw AutoGUIException("Invalid start coordinates: (" + std::to_string(x1) +
|
||||
", " + std::to_string(y1) + ")");
|
||||
}
|
||||
if (!isValidCoord(x2, y2)) {
|
||||
throw AutoGUIException("Invalid end coordinates: (" + std::to_string(x2) +
|
||||
", " + std::to_string(y2) + ")");
|
||||
}
|
||||
if (duration < 0) {
|
||||
throw AutoGUIException("Duration cannot be negative");
|
||||
}
|
||||
Robot::MouseButton robotButton = toRobotButton(button);
|
||||
// 移动到起始位置
|
||||
Robot::Point start{x1, y1};
|
||||
Robot::Mouse::Move(start);
|
||||
delayMs(10); // 短暂延迟确保移动完成
|
||||
// 按下鼠标按钮
|
||||
Robot::Mouse::ToggleButton(true, robotButton);
|
||||
delayMs(10); // 短暂延迟确保按钮按下
|
||||
// 拖动到目标位置
|
||||
Robot::Point end{x2, y2};
|
||||
if (duration > 0.0) {
|
||||
// 使用平滑拖动
|
||||
Robot::Mouse::DragSmooth(end);
|
||||
} else {
|
||||
// 立即拖动
|
||||
Robot::Mouse::Drag(end);
|
||||
}
|
||||
// 释放鼠标按钮
|
||||
Robot::Mouse::ToggleButton(false, robotButton);
|
||||
}
|
||||
|
||||
void dragTo(const int x, const int y, const double duration,
|
||||
const Button button) {
|
||||
// 获取当前位置
|
||||
Robot::Point current = getCurrentPosition();
|
||||
// 如果当前位置就是目标位置,则只做点击操作
|
||||
if (current.x == x && current.y == y) {
|
||||
click(x, y, button);
|
||||
return;
|
||||
}
|
||||
// 调用 drag 函数
|
||||
drag(current.x, current.y, x, y, duration, button);
|
||||
}
|
||||
|
||||
void dragRel(const int xOffset, const int yOffset, const double duration,
|
||||
const Button button) {
|
||||
// 获取当前位置
|
||||
const Robot::Point current = getCurrentPosition();
|
||||
// 计算目标位置
|
||||
const int targetX = current.x + xOffset;
|
||||
const int targetY = current.y + yOffset;
|
||||
// 如果偏移量为0,则只做点击操作
|
||||
if (xOffset == 0 && yOffset == 0) {
|
||||
click(current.x, current.y, button);
|
||||
return;
|
||||
}
|
||||
// 调用 drag 函数
|
||||
drag(current.x, current.y, targetX, targetY, duration, button);
|
||||
}
|
||||
|
||||
Robot::Point position() { return getCurrentPosition(); }
|
||||
|
||||
void scroll(int clicks, int x) {
|
||||
// 注意:正数向上滚动,负数向下滚动
|
||||
// 与AutoGUI一致
|
||||
Robot::Mouse::ScrollBy(clicks, x);
|
||||
}
|
||||
|
||||
void type(const std::string &text, double interval) {
|
||||
if (interval > 0.0) {
|
||||
// 有间隔的输入
|
||||
for (char c : text) {
|
||||
Robot::Keyboard::Click(c);
|
||||
if (interval > 0) {
|
||||
sleep(interval);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// 无间隔快速输入
|
||||
Robot::Keyboard::Type(text);
|
||||
}
|
||||
}
|
||||
|
||||
void press(const std::string &key) {
|
||||
std::string lowerKey = toLower(key);
|
||||
|
||||
if (lowerKey.length() == 1) {
|
||||
// 单个字符键
|
||||
Robot::Keyboard::Click(lowerKey[0]);
|
||||
} else if (isSpecialKey(lowerKey)) {
|
||||
// 特殊键
|
||||
Robot::Keyboard::SpecialKey specialKey = stringToSpecialKey(lowerKey);
|
||||
Robot::Keyboard::Click(specialKey);
|
||||
} else {
|
||||
throw AutoGUIException("Unknown key: " + key);
|
||||
}
|
||||
}
|
||||
|
||||
void keyDown(const std::string &key) {
|
||||
std::string lowerKey = toLower(key);
|
||||
|
||||
if (lowerKey.length() == 1) {
|
||||
// 单个字符键
|
||||
Robot::Keyboard::Press(lowerKey[0]);
|
||||
} else if (isSpecialKey(lowerKey)) {
|
||||
// 特殊键
|
||||
Robot::Keyboard::SpecialKey specialKey = stringToSpecialKey(lowerKey);
|
||||
Robot::Keyboard::Press(specialKey);
|
||||
} else {
|
||||
throw AutoGUIException("Unknown key: " + key);
|
||||
}
|
||||
}
|
||||
|
||||
void keyUp(const std::string &key) {
|
||||
std::string lowerKey = toLower(key);
|
||||
|
||||
if (lowerKey.length() == 1) {
|
||||
// 单个字符键
|
||||
Robot::Keyboard::Release(lowerKey[0]);
|
||||
} else if (isSpecialKey(lowerKey)) {
|
||||
// 特殊键
|
||||
Robot::Keyboard::SpecialKey specialKey = stringToSpecialKey(lowerKey);
|
||||
Robot::Keyboard::Release(specialKey);
|
||||
} else {
|
||||
throw AutoGUIException("Unknown key: " + key);
|
||||
}
|
||||
}
|
||||
|
||||
void hotkey(const std::initializer_list<std::string> &keys) {
|
||||
// 按下所有键
|
||||
for (const auto &key : keys) {
|
||||
keyDown(key);
|
||||
}
|
||||
|
||||
delayMs(50);
|
||||
|
||||
// 释放所有键(按相反顺序)
|
||||
const std::string *end = keys.end();
|
||||
const std::string *begin = keys.begin();
|
||||
for (const std::string *it = end - 1; it >= begin; --it) {
|
||||
keyUp(*it);
|
||||
}
|
||||
}
|
||||
|
||||
void hotkey(const std::vector<std::string> &keys) {
|
||||
// 按下所有键
|
||||
for (const auto &key : keys) {
|
||||
keyDown(key);
|
||||
}
|
||||
|
||||
delayMs(50);
|
||||
|
||||
// 释放所有键(按相反顺序)
|
||||
for (auto it = keys.rbegin(); it != keys.rend(); ++it) {
|
||||
keyUp(*it);
|
||||
}
|
||||
}
|
||||
|
||||
void sleep(double seconds) {
|
||||
if (seconds > 0) {
|
||||
delayMs(secondsToMs(seconds));
|
||||
}
|
||||
}
|
||||
|
||||
// 平台特定函数实现
|
||||
Robot::Point size() {
|
||||
// 平台特定实现
|
||||
#ifdef _WIN32
|
||||
// Windows实现
|
||||
#include <Windows.h>
|
||||
int width = GetSystemMetrics(SM_CXSCREEN);
|
||||
int height = GetSystemMetrics(SM_CYSCREEN);
|
||||
return {width, height};
|
||||
|
||||
#elif defined(__APPLE__)
|
||||
// macOS实现
|
||||
#include <CoreGraphics/CoreGraphics.h>
|
||||
CGRect mainDisplayBounds = CGDisplayBounds(CGMainDisplayID());
|
||||
int width = static_cast<int>(CGRectGetWidth(mainDisplayBounds));
|
||||
int height = static_cast<int>(CGRectGetHeight(mainDisplayBounds));
|
||||
return {width, height};
|
||||
|
||||
#elif defined(__linux__)
|
||||
// Linux实现(X11)
|
||||
#include <X11/Xlib.h>
|
||||
|
||||
Display *display = XOpenDisplay(nullptr);
|
||||
if (display) {
|
||||
const Screen *screen = DefaultScreenOfDisplay(display);
|
||||
const int width = WidthOfScreen(screen);
|
||||
const int height = HeightOfScreen(screen);
|
||||
XCloseDisplay(display);
|
||||
return {width, height};
|
||||
}
|
||||
return {1920, 1080}; // 默认值
|
||||
|
||||
#else
|
||||
// 其他平台
|
||||
return {1920, 1080}; // 默认值
|
||||
#endif
|
||||
}
|
||||
|
||||
// 辅助函数实现
|
||||
|
||||
bool isValidCoord(int x, int y) {
|
||||
const Robot::Point screenSize = size();
|
||||
return x >= 0 && x < screenSize.x && y >= 0 && y < screenSize.y;
|
||||
}
|
||||
|
||||
std::string toLower(const std::string &str) {
|
||||
std::string result = str;
|
||||
std::transform(result.begin(), result.end(), result.begin(), ::tolower);
|
||||
return result;
|
||||
}
|
||||
|
||||
namespace ExtendedFunc {
|
||||
void dragRect(const int x1, const int y1, const int width, const int height,
|
||||
double const duration, const Button button) {
|
||||
// 参数验证
|
||||
if (width <= 0 || height <= 0) {
|
||||
throw AutoGUIException("Width and height must be positive");
|
||||
}
|
||||
// 计算矩形右下角坐标
|
||||
const int x2 = x1 + width;
|
||||
const int y2 = y1 + height;
|
||||
|
||||
// 拖动矩形:左上角 -> 右上角 -> 右下角 -> 左下角 -> 左上角
|
||||
drag(x1, y1, x2, y1, duration/4, button); // 上边
|
||||
drag(x2, y1, x2, y2, duration/4, button); // 右边
|
||||
drag(x2, y2, x1, y2, duration/4, button); // 下边
|
||||
drag(x1, y2, x1, y1, duration/4, button); // 左边
|
||||
}
|
||||
|
||||
void dragCircle(const int centerX, const int centerY, const int radius,
|
||||
double const duration, const Button button) {
|
||||
// 参数验证
|
||||
if (radius <= 0) {
|
||||
throw AutoGUIException("Radius must be positive");
|
||||
}
|
||||
// 计算圆上的点数量(越多越平滑)
|
||||
const int segments = 36; // 每10度一个点
|
||||
const double angleStep = 2.0 * M_PI / segments;
|
||||
// 从3点钟方向开始
|
||||
double startX = centerX + radius;
|
||||
double startY = centerY;
|
||||
|
||||
// 拖动圆形
|
||||
for (int i = 1; i <= segments; i++) {
|
||||
double angle = angleStep * i;
|
||||
double endX = centerX + radius * cos(angle);
|
||||
double endY = centerY + radius * sin(angle);
|
||||
// 拖动到下一个点
|
||||
drag(static_cast<int>(startX), static_cast<int>(startY),
|
||||
static_cast<int>(endX), static_cast<int>(endY),
|
||||
duration / segments, button);
|
||||
// 更新起点为终点
|
||||
startX = endX;
|
||||
startY = endY;
|
||||
}
|
||||
}
|
||||
|
||||
void dragRandomInArea(const int x1, const int y1, const int x2, const int y2,
|
||||
const double minDuration, const double maxDuration,
|
||||
const Button button) {
|
||||
// 参数验证
|
||||
if (x1 >= x2 || y1 >= y2) {
|
||||
throw AutoGUIException("Invalid area coordinates");
|
||||
}
|
||||
|
||||
if (minDuration < 0 || maxDuration < 0 || minDuration > maxDuration) {
|
||||
throw AutoGUIException("Invalid duration range");
|
||||
}
|
||||
|
||||
// 设置随机数生成器
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_int_distribution<> distX(x1, x2);
|
||||
std::uniform_int_distribution<> distY(y1, y2);
|
||||
std::uniform_real_distribution<> distDuration(minDuration, maxDuration);
|
||||
|
||||
// 生成随机起始点和结束点
|
||||
int startX = distX(gen);
|
||||
int startY = distY(gen);
|
||||
int endX = distX(gen);
|
||||
int endY = distY(gen);
|
||||
double duration = distDuration(gen);
|
||||
|
||||
// 执行随机拖动
|
||||
drag(startX, startY, endX, endY, duration, button);
|
||||
}
|
||||
|
||||
// 模拟人类打字
|
||||
void typeHumanLike(const std::string& text, double minDelay, double maxDelay, double errorRate) {
|
||||
std::random_device rd;
|
||||
std::mt19937 gen(rd());
|
||||
std::uniform_real_distribution<> delayDist(minDelay, maxDelay);
|
||||
std::uniform_real_distribution<> errorDist(0.0, 1.0);
|
||||
|
||||
for (char c : text) {
|
||||
// 有一定概率打错字
|
||||
if (errorDist(gen) < errorRate) {
|
||||
// 输入一个随机错误字符
|
||||
char wrongChar = 'a' + static_cast<char>(errorDist(gen) * 26);
|
||||
AutoGUI::press(std::string(1, wrongChar));
|
||||
|
||||
// 退格删除错误
|
||||
AutoGUI::press("backspace");
|
||||
AutoGUI::sleep(0.1);
|
||||
}
|
||||
|
||||
// 输入正确字符
|
||||
AutoGUI::press(std::string(1, c));
|
||||
|
||||
// 随机延迟
|
||||
double delay = delayDist(gen);
|
||||
AutoGUI::sleep(delay);
|
||||
}
|
||||
}
|
||||
|
||||
void typewriteEnter(const std::string& text, double interval) {
|
||||
AutoGUI::type(text, interval);
|
||||
AutoGUI::press("enter");
|
||||
}
|
||||
|
||||
void typewriteLines(const std::vector<std::string>& lines, double interval, double lineDelay) {
|
||||
for (size_t i = 0; i < lines.size(); i++) {
|
||||
AutoGUI::type(lines[i], interval);
|
||||
if (i < lines.size() - 1) {
|
||||
AutoGUI::press("enter");
|
||||
AutoGUI::sleep(lineDelay);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void typeDateTime(const std::string& format) {
|
||||
const auto now = std::chrono::system_clock::now();
|
||||
const auto time = std::chrono::system_clock::to_time_t(now);
|
||||
const std::tm tm = *std::localtime(&time);
|
||||
|
||||
std::stringstream ss;
|
||||
ss << std::put_time(&tm, format.c_str());
|
||||
AutoGUI::type(ss.str());
|
||||
}
|
||||
|
||||
/// 键盘快捷键扩展实现
|
||||
void saveFile() {
|
||||
AutoGUI::hotkey({"ctrl", "s"});
|
||||
}
|
||||
|
||||
void openFile() {
|
||||
AutoGUI::hotkey({"ctrl", "o"});
|
||||
}
|
||||
|
||||
void newFile() {
|
||||
AutoGUI::hotkey({"ctrl", "n"});
|
||||
}
|
||||
|
||||
void copy() {
|
||||
AutoGUI::hotkey({"ctrl", "c"});
|
||||
}
|
||||
|
||||
void paste() {
|
||||
AutoGUI::hotkey({"ctrl", "v"});
|
||||
}
|
||||
|
||||
void cut() {
|
||||
AutoGUI::hotkey({"ctrl", "x"});
|
||||
}
|
||||
|
||||
void selectAll() {
|
||||
AutoGUI::hotkey({"ctrl", "a"});
|
||||
}
|
||||
|
||||
void undo() {
|
||||
AutoGUI::hotkey({"ctrl", "z"});
|
||||
}
|
||||
|
||||
void redo() {
|
||||
#ifdef _WIN32
|
||||
AutoGUI::hotkey({"ctrl", "y"});
|
||||
#else
|
||||
AutoGUI::hotkey({"ctrl", "shift", "z"});
|
||||
#endif
|
||||
}
|
||||
|
||||
void find() {
|
||||
AutoGUI::hotkey({"ctrl", "f"});
|
||||
}
|
||||
|
||||
void replace() {
|
||||
AutoGUI::hotkey({"ctrl", "h"});
|
||||
}
|
||||
|
||||
void print() {
|
||||
AutoGUI::hotkey({"ctrl", "p"});
|
||||
}
|
||||
|
||||
void closeWindow(bool useAltF4) {
|
||||
if (useAltF4) {
|
||||
AutoGUI::hotkey({"alt", "f4"});
|
||||
} else {
|
||||
AutoGUI::hotkey({"ctrl", "w"});
|
||||
}
|
||||
}
|
||||
|
||||
void switchWindow(int times) {
|
||||
for (int i = 0; i < times; i++) {
|
||||
AutoGUI::hotkey({"alt", "tab"});
|
||||
AutoGUI::sleep(0.1);
|
||||
}
|
||||
}
|
||||
|
||||
void refresh() {
|
||||
AutoGUI::press("f5");
|
||||
}
|
||||
|
||||
/// 游戏/自动化操作实现
|
||||
void autoClicker(int x, int y, int count, double interval, AutoGUI::Button button) {
|
||||
for (int i = 0; i < count; i++) {
|
||||
AutoGUI::click(x, y, button);
|
||||
if (i < count - 1) {
|
||||
AutoGUI::sleep(interval);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void rapidClicker(int x, int y, double duration, double clickRate, AutoGUI::Button button) {
|
||||
int totalClicks = static_cast<int>(duration * clickRate);
|
||||
double interval = 1.0 / clickRate;
|
||||
|
||||
for (int i = 0; i < totalClicks; i++) {
|
||||
AutoGUI::click(x, y, button);
|
||||
AutoGUI::sleep(interval);
|
||||
}
|
||||
}
|
||||
|
||||
void rapidKeyPress(const std::string& key, double duration, double pressRate) {
|
||||
int totalPresses = static_cast<int>(duration * pressRate);
|
||||
double interval = 1.0 / pressRate;
|
||||
|
||||
for (int i = 0; i < totalPresses; i++) {
|
||||
AutoGUI::press(key);
|
||||
AutoGUI::sleep(interval);
|
||||
}
|
||||
}
|
||||
|
||||
void rapidHotkey(const std::vector<std::string>& keys, double duration, double pressRate) {
|
||||
int totalPresses = static_cast<int>(duration * pressRate);
|
||||
double interval = 1.0 / pressRate;
|
||||
|
||||
for (int i = 0; i < totalPresses; i++) {
|
||||
AutoGUI::hotkey(keys);
|
||||
AutoGUI::sleep(interval);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace ExtendedFunc
|
||||
|
||||
} // namespace AutoGUI
|
||||
Vendored
+500
@@ -0,0 +1,500 @@
|
||||
//
|
||||
// Created by misaki on 2026/1/25.
|
||||
//
|
||||
#pragma once
|
||||
|
||||
#include <algorithm>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
#include <map>
|
||||
#include <initializer_list>
|
||||
|
||||
#include "Keyboard.h"
|
||||
#include "Mouse.h"
|
||||
#include "types.h"
|
||||
|
||||
namespace AutoGUI {
|
||||
|
||||
// 错误类型
|
||||
class AutoGUIException : public std::exception {
|
||||
private:
|
||||
std::string message;
|
||||
|
||||
public:
|
||||
explicit AutoGUIException(std::string msg) : message(std::move(msg)) {}
|
||||
|
||||
[[nodiscard]] const char* what() const noexcept override {
|
||||
return message.c_str();
|
||||
}
|
||||
};
|
||||
|
||||
// 鼠标按钮枚举
|
||||
enum class Button {
|
||||
LEFT = 0,
|
||||
RIGHT = 1,
|
||||
MIDDLE = 2
|
||||
};
|
||||
|
||||
// 将 Button 转换为 Robot::MouseButton
|
||||
inline Robot::MouseButton toRobotButton(Button button) {
|
||||
switch (button) {
|
||||
case Button::LEFT: return Robot::MouseButton::LEFT_BUTTON;
|
||||
case Button::RIGHT: return Robot::MouseButton::RIGHT_BUTTON;
|
||||
case Button::MIDDLE: return Robot::MouseButton::CENTER_BUTTON;
|
||||
default: return Robot::MouseButton::LEFT_BUTTON;
|
||||
}
|
||||
}
|
||||
|
||||
// 将 Button 枚举转换为字符串
|
||||
inline std::string buttonToString(Button button) {
|
||||
switch (button) {
|
||||
case Button::LEFT: return "left";
|
||||
case Button::RIGHT: return "right";
|
||||
case Button::MIDDLE: return "middle";
|
||||
default: return "left";
|
||||
}
|
||||
}
|
||||
|
||||
// 键名字符串到 Keyboard::SpecialKey 的映射
|
||||
inline Robot::Keyboard::SpecialKey stringToSpecialKey(const std::string& key) {
|
||||
static std::map<std::string, Robot::Keyboard::SpecialKey> keyMap = {
|
||||
{"backspace", Robot::Keyboard::BACKSPACE},
|
||||
{"enter", Robot::Keyboard::ENTER},
|
||||
{"return", Robot::Keyboard::ENTER},
|
||||
{"tab", Robot::Keyboard::TAB},
|
||||
{"escape", Robot::Keyboard::ESCAPE},
|
||||
{"esc", Robot::Keyboard::ESCAPE},
|
||||
{"up", Robot::Keyboard::UP},
|
||||
{"down", Robot::Keyboard::DOWN},
|
||||
{"right", Robot::Keyboard::RIGHT},
|
||||
{"left", Robot::Keyboard::LEFT},
|
||||
{"win", Robot::Keyboard::META},
|
||||
{"command", Robot::Keyboard::META},
|
||||
{"cmd", Robot::Keyboard::META},
|
||||
{"alt", Robot::Keyboard::ALT},
|
||||
{"ctrl", Robot::Keyboard::CONTROL},
|
||||
{"control", Robot::Keyboard::CONTROL},
|
||||
{"shift", Robot::Keyboard::SHIFT},
|
||||
{"capslock", Robot::Keyboard::CAPSLOCK},
|
||||
{"f1", Robot::Keyboard::F1},
|
||||
{"f2", Robot::Keyboard::F2},
|
||||
{"f3", Robot::Keyboard::F3},
|
||||
{"f4", Robot::Keyboard::F4},
|
||||
{"f5", Robot::Keyboard::F5},
|
||||
{"f6", Robot::Keyboard::F6},
|
||||
{"f7", Robot::Keyboard::F7},
|
||||
{"f8", Robot::Keyboard::F8},
|
||||
{"f9", Robot::Keyboard::F9},
|
||||
{"f10", Robot::Keyboard::F10},
|
||||
{"f11", Robot::Keyboard::F11},
|
||||
{"f12", Robot::Keyboard::F12},
|
||||
{"space", Robot::Keyboard::BACKSPACE} // 特殊处理空格
|
||||
};
|
||||
|
||||
std::string lower = key;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
|
||||
|
||||
auto it = keyMap.find(lower);
|
||||
if (it != keyMap.end()) {
|
||||
return it->second;
|
||||
}
|
||||
|
||||
// 如果是单个字符,直接返回(会在其他地方处理)
|
||||
if (lower.length() == 1) {
|
||||
// 这是单个字符键,不是特殊键
|
||||
throw AutoGUIException("Unknown key: " + key);
|
||||
}
|
||||
|
||||
throw AutoGUIException("Unknown key: " + key);
|
||||
}
|
||||
|
||||
// 检查字符串是否是特殊键
|
||||
inline bool isSpecialKey(const std::string& key) {
|
||||
static std::vector<std::string> specialKeys = {
|
||||
"backspace", "enter", "return", "tab", "escape", "esc",
|
||||
"up", "down", "right", "left", "win", "command", "cmd",
|
||||
"alt", "ctrl", "control", "shift", "capslock",
|
||||
"f1", "f2", "f3", "f4", "f5", "f6", "f7", "f8", "f9", "f10", "f11", "f12",
|
||||
"space"
|
||||
};
|
||||
|
||||
std::string lower = key;
|
||||
std::transform(lower.begin(), lower.end(), lower.begin(), ::tolower);
|
||||
|
||||
return std::find(specialKeys.begin(), specialKeys.end(), lower) != specialKeys.end();
|
||||
}
|
||||
|
||||
// 主要 API 函数
|
||||
/**
|
||||
* @brief 移动鼠标到指定位置
|
||||
* @param x 目标位置的X坐标
|
||||
* @param y 目标位置的Y坐标
|
||||
* @param duration 移动持续时间(秒),0表示立即移动
|
||||
*/
|
||||
void moveTo(int x, int y, double duration = 0.0);
|
||||
|
||||
/**
|
||||
* @brief 相对移动鼠标
|
||||
* @param xOffset X方向偏移量
|
||||
* @param yOffset Y方向偏移量
|
||||
* @param duration 移动持续时间(秒),0表示立即移动
|
||||
*/
|
||||
void moveRel(int xOffset, int yOffset, double duration = 0.0);
|
||||
|
||||
/**
|
||||
* @brief 单击鼠标
|
||||
* @param x 点击位置的X坐标,-1表示当前位置
|
||||
* @param y 点击位置的Y坐标,-1表示当前位置
|
||||
* @param button 鼠标按钮:left, right, middle
|
||||
* @param clicks 点击次数
|
||||
* @param interval 多次点击之间的间隔(秒)
|
||||
*/
|
||||
void click(int x = -1, int y = -1,
|
||||
Button button = Button::LEFT,
|
||||
int clicks = 1,
|
||||
double interval = 0.0);
|
||||
|
||||
/**
|
||||
* @brief 左键双击
|
||||
* @param x 双击位置的X坐标,-1表示当前位置
|
||||
* @param y 双击位置的Y坐标,-1表示当前位置
|
||||
*/
|
||||
void leftDouble(int x = -1, int y = -1);
|
||||
|
||||
/**
|
||||
* @brief 右键单击
|
||||
* @param x 点击位置的X坐标,-1表示当前位置
|
||||
* @param y 点击位置的Y坐标,-1表示当前位置
|
||||
*/
|
||||
void rightSingle(int x = -1, int y = -1);
|
||||
|
||||
/**
|
||||
* @brief 中键单击
|
||||
* @param x 点击位置的X坐标,-1表示当前位置
|
||||
* @param y 点击位置的Y坐标,-1表示当前位置
|
||||
*/
|
||||
void middleClick(int x = -1, int y = -1);
|
||||
|
||||
/**
|
||||
* @brief 按下鼠标按钮
|
||||
* @param button 要按下的鼠标按钮
|
||||
* @param x 按下位置的X坐标,-1表示当前位置
|
||||
* @param y 按下位置的Y坐标,-1表示当前位置
|
||||
*/
|
||||
void mouseDown(Button button = Button::LEFT, int x = -1, int y = -1);
|
||||
|
||||
/**
|
||||
* @brief 释放鼠标按钮
|
||||
* @param button 要释放的鼠标按钮
|
||||
* @param x 释放位置的X坐标,-1表示当前位置
|
||||
* @param y 释放位置的Y坐标,-1表示当前位置
|
||||
*/
|
||||
void mouseUp(Button button = Button::LEFT, int x = -1, int y = -1);
|
||||
|
||||
/**
|
||||
* @brief 从指定位置拖动到另一个位置
|
||||
* @param x1 起始位置的X坐标
|
||||
* @param y1 起始位置的Y坐标
|
||||
* @param x2 目标位置的X坐标
|
||||
* @param y2 目标位置的Y坐标
|
||||
* @param duration 拖动持续时间(秒),0表示立即拖动
|
||||
* @param button 拖动时按住的鼠标按钮
|
||||
*/
|
||||
void drag(int x1, int y1, int x2, int y2,
|
||||
double duration = 0.0,
|
||||
Button button = Button::LEFT);
|
||||
|
||||
/**
|
||||
* @brief 拖拽鼠标到指定位置
|
||||
* @param x 目标位置的X坐标
|
||||
* @param y 目标位置的Y坐标
|
||||
* @param duration 拖拽持续时间(秒),0表示立即拖拽
|
||||
* @param button 拖拽时按住的鼠标按钮
|
||||
*/
|
||||
void dragTo(int x, int y, double duration = 0.0, Button button = Button::LEFT);
|
||||
|
||||
/**
|
||||
* @brief 相对拖拽鼠标
|
||||
* @param xOffset X方向偏移量
|
||||
* @param yOffset Y方向偏移量
|
||||
* @param duration 拖拽持续时间(秒),0表示立即拖拽
|
||||
* @param button 拖拽时按住的鼠标按钮
|
||||
*/
|
||||
void dragRel(int xOffset, int yOffset, double duration = 0.0, Button button = Button::LEFT);
|
||||
|
||||
/**
|
||||
* @brief 获取当前鼠标位置
|
||||
* @return 包含x,y坐标的Point结构体
|
||||
*/
|
||||
Robot::Point position();
|
||||
|
||||
/**
|
||||
* @brief 滚动鼠标滚轮
|
||||
* @param clicks 滚动量,正数向上滚动,负数向下滚动
|
||||
* @param x 水平滚动量(Linux/macOS支持)
|
||||
*/
|
||||
void scroll(int clicks, int x = 0);
|
||||
|
||||
/**
|
||||
* @brief 输入文本
|
||||
* @param text 要输入的文本
|
||||
* @param interval 字符之间的间隔时间(秒),0表示无间隔
|
||||
* 注意:只支持ASCII字符
|
||||
*/
|
||||
void type(const std::string& text, double interval = 0.0);
|
||||
|
||||
/**
|
||||
* @brief 按下并释放一个键
|
||||
* @param key 键名(如:"a", "enter", "ctrl"等)
|
||||
*/
|
||||
void press(const std::string& key);
|
||||
|
||||
/**
|
||||
* @brief 按下键(不释放)
|
||||
* @param key 键名
|
||||
*/
|
||||
void keyDown(const std::string& key);
|
||||
|
||||
/**
|
||||
* @brief 释放键
|
||||
* @param key 键名
|
||||
*/
|
||||
void keyUp(const std::string& key);
|
||||
|
||||
/**
|
||||
* @brief 按下组合键
|
||||
* @param keys 键名列表,如 {"ctrl", "c"}
|
||||
*/
|
||||
void hotkey(const std::initializer_list<std::string>& keys);
|
||||
|
||||
/**
|
||||
* @brief 按下组合键(向量版本)
|
||||
* @param keys 键名向量
|
||||
*/
|
||||
void hotkey(const std::vector<std::string>& keys);
|
||||
|
||||
/**
|
||||
* @brief 睡眠/等待
|
||||
* @param seconds 等待的秒数
|
||||
*/
|
||||
void sleep(double seconds);
|
||||
|
||||
/**
|
||||
* @brief 获取屏幕尺寸
|
||||
* @return 包含屏幕宽度和高度的Point结构体
|
||||
* @note 需要平台特定实现
|
||||
* @note 注意:不提供屏幕管理功能,如果你有多个屏幕,那么返回的size可能是错误的,对于单个屏幕是没有影响的,对于获取屏幕尺寸,更加推荐使用Qt当中的接口
|
||||
*/
|
||||
Robot::Point size();
|
||||
|
||||
// 辅助函数
|
||||
/**
|
||||
* @brief 检查坐标是否有效
|
||||
* @param x X坐标
|
||||
* @param y Y坐标
|
||||
* @return 如果坐标有效返回true
|
||||
*/
|
||||
bool isValidCoord(int x, int y);
|
||||
|
||||
/**
|
||||
* @brief 将字符串转换为小写
|
||||
* @param str 输入字符串
|
||||
* @return 小写字符串
|
||||
*/
|
||||
std::string toLower(const std::string& str);
|
||||
|
||||
namespace ExtendedFunc {
|
||||
/// drag拓展功能
|
||||
/**
|
||||
* @brief 从指定位置开始拖动一个矩形区域
|
||||
* @param x1 矩形左上角X坐标
|
||||
* @param y1 矩形左上角Y坐标
|
||||
* @param width 矩形宽度
|
||||
* @param height 矩形高度
|
||||
* @param duration 拖动持续时间(秒)
|
||||
* @param button 拖动时按住的鼠标按钮
|
||||
*/
|
||||
void dragRect(int x1, int y1, int width, int height,
|
||||
double duration = 0.5, Button button = Button::LEFT);
|
||||
|
||||
/**
|
||||
* @brief 拖动一个圆形区域(从中心开始到边缘)
|
||||
* @param centerX 圆心X坐标
|
||||
* @param centerY 圆心Y坐标
|
||||
* @param radius 圆半径
|
||||
* @param duration 拖动持续时间(秒)
|
||||
* @param button 拖动时按住的鼠标按钮
|
||||
*/
|
||||
void dragCircle(int centerX, int centerY, int radius,
|
||||
double duration = 0.5, Button button = Button::LEFT);
|
||||
|
||||
/**
|
||||
* @brief 在区域内随机拖动(用于测试或自动化)
|
||||
* @param x1 区域左上角X坐标
|
||||
* @param y1 区域左上角Y坐标
|
||||
* @param x2 区域右下角X坐标
|
||||
* @param y2 区域右下角Y坐标
|
||||
* @param minDuration 最小拖动持续时间(秒)
|
||||
* @param maxDuration 最大拖动持续时间(秒)
|
||||
* @param button 拖动时按住的鼠标按钮
|
||||
*/
|
||||
void dragRandomInArea(int x1, int y1, int x2, int y2,
|
||||
double minDuration = 0.1, double maxDuration = 0.5,
|
||||
Button button = Button::LEFT);
|
||||
|
||||
/// 文字输入扩展功能
|
||||
/**
|
||||
* @brief 模拟人类打字(带随机延迟和可能的错误)
|
||||
* @param text 要输入的文本
|
||||
* @param minDelay 最小延迟(秒)
|
||||
* @param maxDelay 最大延迟(秒)
|
||||
* @param errorRate 错误率(0.0-1.0)
|
||||
*/
|
||||
void typeHumanLike(const std::string& text, double minDelay = 0.05,
|
||||
double maxDelay = 0.2, double errorRate = 0.0);
|
||||
|
||||
/**
|
||||
* @brief 输入文本并回车
|
||||
* @param text 要输入的文本
|
||||
* @param interval 字符间隔
|
||||
*/
|
||||
void typewriteEnter(const std::string& text, double interval = 0.0);
|
||||
|
||||
/**
|
||||
* @brief 输入多行文本
|
||||
* @param lines 文本行数组
|
||||
* @param interval 字符间隔
|
||||
* @param lineDelay 行间延迟
|
||||
*/
|
||||
void typewriteLines(const std::vector<std::string>& lines,
|
||||
double interval = 0.0, double lineDelay = 0.1);
|
||||
|
||||
/**
|
||||
* @brief 输入当前日期时间
|
||||
* @param format 时间格式(如:"%Y-%m-%d %H:%M:%S")
|
||||
*/
|
||||
void typeDateTime(const std::string& format = "%Y-%m-%d %H:%M:%S");
|
||||
|
||||
/// 键盘快捷键扩展功能
|
||||
/**
|
||||
* @brief 保存文件 (Ctrl+S)
|
||||
*/
|
||||
void saveFile();
|
||||
|
||||
/**
|
||||
* @brief 打开文件 (Ctrl+O)
|
||||
*/
|
||||
void openFile();
|
||||
|
||||
/**
|
||||
* @brief 新建文件 (Ctrl+N)
|
||||
*/
|
||||
void newFile();
|
||||
|
||||
/**
|
||||
* @brief 复制 (Ctrl+C)
|
||||
*/
|
||||
void copy();
|
||||
|
||||
/**
|
||||
* @brief 粘贴 (Ctrl+V)
|
||||
*/
|
||||
void paste();
|
||||
|
||||
/**
|
||||
* @brief 剪切 (Ctrl+X)
|
||||
*/
|
||||
void cut();
|
||||
|
||||
/**
|
||||
* @brief 全选 (Ctrl+A)
|
||||
*/
|
||||
void selectAll();
|
||||
|
||||
/**
|
||||
* @brief 撤销 (Ctrl+Z)
|
||||
*/
|
||||
void undo();
|
||||
|
||||
/**
|
||||
* @brief 重做 (Ctrl+Y 或 Ctrl+Shift+Z)
|
||||
*/
|
||||
void redo();
|
||||
|
||||
/**
|
||||
* @brief 查找 (Ctrl+F)
|
||||
*/
|
||||
void find();
|
||||
|
||||
/**
|
||||
* @brief 替换 (Ctrl+H)
|
||||
*/
|
||||
void replace();
|
||||
|
||||
/**
|
||||
* @brief 打印 (Ctrl+P)
|
||||
*/
|
||||
void print();
|
||||
|
||||
/**
|
||||
* @brief 关闭窗口 (Alt+F4 或 Ctrl+W)
|
||||
*/
|
||||
void closeWindow(bool useAltF4 = true);
|
||||
|
||||
/**
|
||||
* @brief 切换窗口 (Alt+Tab)
|
||||
* @param times 切换次数
|
||||
*/
|
||||
void switchWindow(int times = 1);
|
||||
|
||||
/**
|
||||
* @brief 刷新 (F5)
|
||||
*/
|
||||
void refresh();
|
||||
|
||||
/// 游戏/自动化操作功能拓展
|
||||
/**
|
||||
* @brief 自动点击器(连续点击)
|
||||
* @param x 点击位置X坐标
|
||||
* @param y 点击位置Y坐标
|
||||
* @param count 点击次数
|
||||
* @param interval 点击间隔(秒)
|
||||
* @param button 鼠标按钮
|
||||
*/
|
||||
void autoClicker(int x, int y, int count = 10, double interval = 0.1,
|
||||
Button button = Button::LEFT);
|
||||
|
||||
/**
|
||||
* @brief 连点器(按住连点)
|
||||
* @param x 点击位置X坐标
|
||||
* @param y 点击位置Y坐标
|
||||
* @param duration 持续时间(秒)
|
||||
* @param clickRate 点击频率(次/秒)
|
||||
* @param button 鼠标按钮
|
||||
*/
|
||||
void rapidClicker(int x, int y, double duration = 1.0,
|
||||
double clickRate = 10.0,
|
||||
Button button = Button::LEFT);
|
||||
|
||||
/**
|
||||
* @brief 按键连发(按住按键)
|
||||
* @param key 按键名称
|
||||
* @param duration 持续时间(秒)
|
||||
* @param pressRate 按键频率(次/秒)
|
||||
*/
|
||||
void rapidKeyPress(const std::string& key, double duration = 1.0,
|
||||
double pressRate = 10.0);
|
||||
|
||||
/**
|
||||
* @brief 组合键连发
|
||||
* @param keys 按键组合
|
||||
* @param duration 持续时间
|
||||
* @param pressRate 按键频率
|
||||
*/
|
||||
void rapidHotkey(const std::vector<std::string>& keys, double duration = 1.0,
|
||||
double pressRate = 5.0);
|
||||
|
||||
} // namespace ExtendedFunc
|
||||
|
||||
} // namespace AutoGUI
|
||||
+898
@@ -0,0 +1,898 @@
|
||||
#ifdef __APPLE__
|
||||
#include <CoreGraphics/CoreGraphics.h>
|
||||
#endif
|
||||
#ifdef __linux__
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/XKBlib.h>
|
||||
#include <X11/extensions/XTest.h>
|
||||
#endif
|
||||
#include <iostream>
|
||||
#include <random>
|
||||
#include <map>
|
||||
#include <cstring>
|
||||
|
||||
#include "./Keyboard.h"
|
||||
#include "./Utils.h"
|
||||
|
||||
namespace Robot {
|
||||
|
||||
int Keyboard::delay = 1;
|
||||
|
||||
const char Keyboard::INVALID_ASCII = static_cast<char>(0xFF);
|
||||
|
||||
std::thread Keyboard::keyPressThread;
|
||||
std::atomic<bool> Keyboard::continueHolding(false);
|
||||
std::set<char> Keyboard::heldAsciiChars;
|
||||
std::set<Keyboard::SpecialKey> Keyboard::heldSpecialKeys;
|
||||
|
||||
#ifdef __linux__
|
||||
Display* Keyboard::display = nullptr;
|
||||
Window Keyboard::rootWindow = 0;
|
||||
|
||||
void Keyboard::InitializeX11() {
|
||||
if (display == nullptr) {
|
||||
display = XOpenDisplay(nullptr);
|
||||
if (display == nullptr) {
|
||||
throw std::runtime_error("Cannot open X11 display");
|
||||
}
|
||||
rootWindow = DefaultRootWindow(display);
|
||||
}
|
||||
}
|
||||
|
||||
void Keyboard::CleanupX11() {
|
||||
if (display != nullptr) {
|
||||
XCloseDisplay(display);
|
||||
display = nullptr;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#ifdef __linux__
|
||||
static bool NeedShiftForKeySym(KeySym keysym) {
|
||||
// 检查这个键是否需要 Shift 键配合
|
||||
switch (keysym) {
|
||||
case XK_exclam: // !
|
||||
case XK_at: // @
|
||||
case XK_numbersign: // #
|
||||
case XK_dollar: // $
|
||||
case XK_percent: // %
|
||||
case XK_asciicircum: // ^
|
||||
case XK_ampersand: // &
|
||||
case XK_asterisk: // *
|
||||
case XK_parenleft: // (
|
||||
case XK_parenright: // )
|
||||
case XK_underscore: // _
|
||||
case XK_plus: // +
|
||||
case XK_braceleft: // {
|
||||
case XK_braceright: // }
|
||||
case XK_bar: // |
|
||||
case XK_colon: // :
|
||||
case XK_quotedbl: // "
|
||||
case XK_less: // <
|
||||
case XK_greater: // >
|
||||
case XK_question: // ?
|
||||
case XK_asciitilde: // ~
|
||||
return true;
|
||||
|
||||
// 大写字母需要 Shift 键
|
||||
case XK_A:
|
||||
case XK_B:
|
||||
case XK_C:
|
||||
case XK_D:
|
||||
case XK_E:
|
||||
case XK_F:
|
||||
case XK_G:
|
||||
case XK_H:
|
||||
case XK_I:
|
||||
case XK_J:
|
||||
case XK_K:
|
||||
case XK_L:
|
||||
case XK_M:
|
||||
case XK_N:
|
||||
case XK_O:
|
||||
case XK_P:
|
||||
case XK_Q:
|
||||
case XK_R:
|
||||
case XK_S:
|
||||
case XK_T:
|
||||
case XK_U:
|
||||
case XK_V:
|
||||
case XK_W:
|
||||
case XK_X:
|
||||
case XK_Y:
|
||||
case XK_Z:
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
static bool NeedShiftForChar(const char c) {
|
||||
// 简单判断:大写字母和特殊符号需要 Shift
|
||||
if (std::isupper(c)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 特殊符号需要 Shift
|
||||
const char* shiftChars = "!@#$%^&*()_+{}|:\"<>?~";
|
||||
return (strchr(shiftChars, c) != nullptr);
|
||||
}
|
||||
#endif
|
||||
|
||||
|
||||
void Keyboard::HoldStart(char asciiChar) {
|
||||
if (heldAsciiChars.empty() && heldSpecialKeys.empty()) {
|
||||
continueHolding = true;
|
||||
keyPressThread = std::thread(KeyHoldThread);
|
||||
}
|
||||
heldAsciiChars.insert(asciiChar);
|
||||
}
|
||||
|
||||
void Keyboard::HoldStart(SpecialKey specialKey) {
|
||||
if (heldAsciiChars.empty() && heldSpecialKeys.empty()) {
|
||||
continueHolding = true;
|
||||
keyPressThread = std::thread(KeyHoldThread);
|
||||
}
|
||||
heldSpecialKeys.insert(specialKey);
|
||||
}
|
||||
|
||||
void Keyboard::HoldStop(char asciiChar) {
|
||||
heldAsciiChars.erase(asciiChar);
|
||||
if (heldAsciiChars.empty() && heldSpecialKeys.empty()) {
|
||||
continueHolding = false;
|
||||
if (keyPressThread.joinable()) {
|
||||
keyPressThread.join();
|
||||
}
|
||||
}
|
||||
Release(asciiChar);
|
||||
}
|
||||
|
||||
void Keyboard::HoldStop(SpecialKey specialKey) {
|
||||
heldSpecialKeys.erase(specialKey);
|
||||
if (heldAsciiChars.empty() && heldSpecialKeys.empty()) {
|
||||
continueHolding = false;
|
||||
if (keyPressThread.joinable()) {
|
||||
keyPressThread.join();
|
||||
}
|
||||
}
|
||||
Release(specialKey);
|
||||
}
|
||||
|
||||
void Keyboard::KeyHoldThread() {
|
||||
while (continueHolding) {
|
||||
for (char asciiChar : heldAsciiChars) {
|
||||
Press(asciiChar);
|
||||
}
|
||||
for (SpecialKey specialKey : heldSpecialKeys) {
|
||||
Press(specialKey);
|
||||
}
|
||||
Robot::delay(50);
|
||||
}
|
||||
|
||||
for (char asciiChar : heldAsciiChars) {
|
||||
Release(asciiChar);
|
||||
}
|
||||
for (SpecialKey specialKey : heldSpecialKeys) {
|
||||
Release(specialKey);
|
||||
}
|
||||
}
|
||||
|
||||
void Keyboard::Type(const std::string &query) {
|
||||
for (const char c : query) {
|
||||
if (!KeyUtils::IsValidAscii(c)) {
|
||||
std::cerr << "Warning: Skipping invalid ASCII character: " << static_cast<int>(c) << std::endl;
|
||||
continue;
|
||||
}
|
||||
Click(c);
|
||||
}
|
||||
}
|
||||
|
||||
void Keyboard::TypeHumanLike(const std::string &query) {
|
||||
std::normal_distribution<double> distribution(75, 25);
|
||||
std::random_device rd;
|
||||
std::mt19937 engine(rd());
|
||||
|
||||
for (char c : query) {
|
||||
if (!KeyUtils::IsValidAscii(c)) {
|
||||
std::cerr << "Warning: Skipping invalid ASCII character: " << static_cast<int>(c) << std::endl;
|
||||
continue;
|
||||
}
|
||||
|
||||
Click(c);
|
||||
Robot::delay((int)distribution(engine));
|
||||
}
|
||||
}
|
||||
|
||||
void Keyboard::Click(char asciiChar) {
|
||||
#ifdef __linux__
|
||||
InitializeX11();
|
||||
// Linux 特殊处理:对于需要 Shift 的字符,先按下 Shift
|
||||
bool needShift = NeedShiftForChar(asciiChar);
|
||||
|
||||
if (needShift) {
|
||||
// 获取 Shift 键的键码
|
||||
KeyCode shiftKeyCode = XKeysymToKeycode(display, XK_Shift_L);
|
||||
if (shiftKeyCode != 0) {
|
||||
XTestFakeKeyEvent(display, shiftKeyCode, True, CurrentTime);
|
||||
XFlush(display);
|
||||
Robot::delay(delay);
|
||||
}
|
||||
}
|
||||
Press(asciiChar);
|
||||
Release(asciiChar);
|
||||
if (needShift) {
|
||||
// 释放 Shift 键
|
||||
KeyCode shiftKeyCode = XKeysymToKeycode(display, XK_Shift_L);
|
||||
if (shiftKeyCode != 0) {
|
||||
XTestFakeKeyEvent(display, shiftKeyCode, False, CurrentTime);
|
||||
XFlush(display);
|
||||
Robot::delay(delay);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
// macOS实现
|
||||
bool needShift = KeyUtils::NeedsShift(asciiChar);~
|
||||
char baseKey = KeyUtils::GetBaseKey(asciiChar);
|
||||
auto it = asciiToVirtualKeyMap.find(baseKey);
|
||||
if (it == asciiToVirtualKeyMap.end()) {
|
||||
std::cerr << "Warning: Character not found in key map: " << asciiChar << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
CGKeyCode keycode = static_cast<CGKeyCode>(it->second);
|
||||
CGEventSourceRef source = CGEventSourceCreate(kCGEventSourceStateHIDSystemState);
|
||||
|
||||
if (needShift) {
|
||||
// 按下Shift键
|
||||
CGEventRef shiftDown = CGEventCreateKeyboardEvent(source, kVK_Shift, true);
|
||||
CGEventPost(kCGHIDEventTap, shiftDown);
|
||||
CFRelease(shiftDown);
|
||||
Robot::delay(delay);
|
||||
}
|
||||
|
||||
// 按下并释放目标键
|
||||
CGEventRef keyDown = CGEventCreateKeyboardEvent(source, keycode, true);
|
||||
CGEventRef keyUp = CGEventCreateKeyboardEvent(source, keycode, false);
|
||||
|
||||
CGEventPost(kCGHIDEventTap, keyDown);
|
||||
Robot::delay(delay);
|
||||
CGEventPost(kCGHIDEventTap, keyUp);
|
||||
Robot::delay(delay);
|
||||
|
||||
if (needShift) {
|
||||
// 释放Shift键
|
||||
CGEventRef shiftUp = CGEventCreateKeyboardEvent(source, kVK_Shift, false);
|
||||
CGEventPost(kCGHIDEventTap, shiftUp);
|
||||
CFRelease(shiftUp);
|
||||
Robot::delay(delay);
|
||||
}
|
||||
|
||||
CFRelease(keyDown);
|
||||
CFRelease(keyUp);
|
||||
CFRelease(source);
|
||||
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
// Windows实现
|
||||
bool needShift = KeyUtils::NeedsShift(asciiChar);
|
||||
char baseKey = KeyUtils::GetBaseKey(asciiChar);
|
||||
|
||||
// 获取基础键的虚拟键码
|
||||
SHORT vkAndShift = VkKeyScan(baseKey);
|
||||
if (vkAndShift == -1) {
|
||||
std::cerr << "Warning: Cannot get virtual key for character: " << asciiChar << std::endl;
|
||||
return;
|
||||
}
|
||||
|
||||
WORD keycode = static_cast<WORD>(vkAndShift & 0xFF);
|
||||
|
||||
std::vector<INPUT> inputs;
|
||||
|
||||
if (needShift) {
|
||||
// Shift键按下
|
||||
INPUT shiftDown = {0};
|
||||
shiftDown.type = INPUT_KEYBOARD;
|
||||
shiftDown.ki.wVk = VK_SHIFT;
|
||||
inputs.push_back(shiftDown);
|
||||
}
|
||||
|
||||
// 目标键按下
|
||||
INPUT keyDown = {0};
|
||||
keyDown.type = INPUT_KEYBOARD;
|
||||
keyDown.ki.wVk = keycode;
|
||||
inputs.push_back(keyDown);
|
||||
|
||||
// 目标键释放
|
||||
INPUT keyUp = {0};
|
||||
keyUp.type = INPUT_KEYBOARD;
|
||||
keyUp.ki.wVk = keycode;
|
||||
keyUp.ki.dwFlags = KEYEVENTF_KEYUP;
|
||||
inputs.push_back(keyUp);
|
||||
|
||||
if (needShift) {
|
||||
// Shift键释放
|
||||
INPUT shiftUp = {0};
|
||||
shiftUp.type = INPUT_KEYBOARD;
|
||||
shiftUp.ki.wVk = VK_SHIFT;
|
||||
shiftUp.ki.dwFlags = KEYEVENTF_KEYUP;
|
||||
inputs.push_back(shiftUp);
|
||||
}
|
||||
|
||||
// 发送所有输入
|
||||
SendInput(static_cast<UINT>(inputs.size()), inputs.data(), sizeof(INPUT));
|
||||
#endif
|
||||
}
|
||||
|
||||
void Keyboard::Click(SpecialKey specialKey) {
|
||||
Press(specialKey);
|
||||
Release(specialKey);
|
||||
}
|
||||
|
||||
void Keyboard::Press(char asciiChar) {
|
||||
KeyCode keycode = AsciiToVirtualKey(asciiChar);
|
||||
#ifdef __APPLE__
|
||||
CGEventSourceRef source =
|
||||
CGEventSourceCreate(kCGEventSourceStateHIDSystemState);
|
||||
CGEventRef event = CGEventCreateKeyboardEvent(source, keycode, true);
|
||||
CGEventPost(kCGHIDEventTap, event);
|
||||
|
||||
CFRelease(event);
|
||||
CFRelease(source);
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
INPUT input = {0};
|
||||
input.type = INPUT_KEYBOARD;
|
||||
input.ki.wVk = keycode;
|
||||
SendInput(1, &input, sizeof(INPUT));
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
InitializeX11();
|
||||
KeyCode xkeycode = XKeysymToKeycode(display, keycode);
|
||||
if (xkeycode != 0) {
|
||||
XTestFakeKeyEvent(display, xkeycode, True, CurrentTime);
|
||||
XFlush(display);
|
||||
}
|
||||
#endif
|
||||
Robot::delay(delay);
|
||||
}
|
||||
|
||||
void Keyboard::Press(SpecialKey specialKey) {
|
||||
KeyCode keycode = SpecialKeyToVirtualKey(specialKey);
|
||||
#ifdef __APPLE__
|
||||
CGEventRef event = CGEventCreateKeyboardEvent(nullptr, keycode, true);
|
||||
CGEventPost(kCGHIDEventTap, event);
|
||||
CFRelease(event);
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
INPUT input = {0};
|
||||
input.type = INPUT_KEYBOARD;
|
||||
input.ki.wVk = keycode;
|
||||
SendInput(1, &input, sizeof(INPUT));
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
InitializeX11();
|
||||
KeyCode xkeycode = XKeysymToKeycode(display, keycode);
|
||||
if (xkeycode != 0) {
|
||||
XTestFakeKeyEvent(display, xkeycode, True, CurrentTime);
|
||||
XFlush(display);
|
||||
}
|
||||
#endif
|
||||
Robot::delay(delay);
|
||||
}
|
||||
|
||||
void Keyboard::Release(char asciiChar) {
|
||||
KeyCode keycode = AsciiToVirtualKey(asciiChar);
|
||||
#ifdef __APPLE__
|
||||
CGEventRef event =
|
||||
CGEventCreateKeyboardEvent(nullptr, (CGKeyCode)keycode, false);
|
||||
CGEventPost(kCGHIDEventTap, event);
|
||||
CFRelease(event);
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
INPUT input = {0};
|
||||
input.type = INPUT_KEYBOARD;
|
||||
input.ki.wVk = keycode;
|
||||
input.ki.dwFlags = KEYEVENTF_KEYUP;
|
||||
SendInput(1, &input, sizeof(INPUT));
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
InitializeX11();
|
||||
KeyCode xkeycode = XKeysymToKeycode(display, keycode);
|
||||
if (xkeycode != 0) {
|
||||
XTestFakeKeyEvent(display, xkeycode, False, CurrentTime);
|
||||
XFlush(display);
|
||||
}
|
||||
#endif
|
||||
Robot::delay(delay);
|
||||
}
|
||||
|
||||
void Keyboard::Release(SpecialKey specialKey) {
|
||||
KeyCode keycode = SpecialKeyToVirtualKey(specialKey);
|
||||
#ifdef __APPLE__
|
||||
CGEventRef event =
|
||||
CGEventCreateKeyboardEvent(nullptr, (CGKeyCode)keycode, false);
|
||||
CGEventPost(kCGHIDEventTap, event);
|
||||
CFRelease(event);
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
INPUT input = {0};
|
||||
input.type = INPUT_KEYBOARD;
|
||||
input.ki.wVk = keycode;
|
||||
input.ki.dwFlags = KEYEVENTF_KEYUP;
|
||||
SendInput(1, &input, sizeof(INPUT));
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
InitializeX11();
|
||||
KeyCode xkeycode = XKeysymToKeycode(display, keycode);
|
||||
if (xkeycode != 0) {
|
||||
XTestFakeKeyEvent(display, xkeycode, False, CurrentTime);
|
||||
XFlush(display);
|
||||
}
|
||||
#endif
|
||||
Robot::delay(delay);
|
||||
}
|
||||
|
||||
KeyCode Keyboard::SpecialKeyToVirtualKey(SpecialKey specialKey) {
|
||||
return specialKeyToVirtualKeyMap.at(specialKey);
|
||||
}
|
||||
|
||||
KeyCode Keyboard::AsciiToVirtualKey(char asciiChar) {
|
||||
#ifdef __APPLE__
|
||||
auto it = asciiToVirtualKeyMap.find(asciiChar);
|
||||
if (it == asciiToVirtualKeyMap.end()) {
|
||||
std::cerr
|
||||
<< "Warning: Character not found in the virtual key map. Ignoring..."
|
||||
<< std::endl;
|
||||
return 0xFFFF; // Return an invalid keycode
|
||||
}
|
||||
return static_cast<KeyCode>(it->second);
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
// Windows: VkKeyScan 函数返回一个short,高字节是shift状态,低字节是虚拟键码
|
||||
SHORT vkAndShift = VkKeyScan(asciiChar);
|
||||
if (vkAndShift == -1) {
|
||||
std::cerr
|
||||
<< "Warning: Character not found in the virtual key map. Ignoring..."
|
||||
<< std::endl;
|
||||
return 0xFFFF; // Return an invalid keycode
|
||||
}
|
||||
// 返回虚拟键码(低字节)
|
||||
return static_cast<KeyCode>(vkAndShift & 0xFF);
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
// Convert ASCII to X11 KeySym
|
||||
// For Linux, we need to handle case sensitivity properly
|
||||
if (std::isalpha(asciiChar)) {
|
||||
// For letters, use lowercase KeySym
|
||||
switch (char lowerChar = std::tolower(asciiChar)) {
|
||||
case 'a': return XK_a;
|
||||
case 'b': return XK_b;
|
||||
case 'c': return XK_c;
|
||||
case 'd': return XK_d;
|
||||
case 'e': return XK_e;
|
||||
case 'f': return XK_f;
|
||||
case 'g': return XK_g;
|
||||
case 'h': return XK_h;
|
||||
case 'i': return XK_i;
|
||||
case 'j': return XK_j;
|
||||
case 'k': return XK_k;
|
||||
case 'l': return XK_l;
|
||||
case 'm': return XK_m;
|
||||
case 'n': return XK_n;
|
||||
case 'o': return XK_o;
|
||||
case 'p': return XK_p;
|
||||
case 'q': return XK_q;
|
||||
case 'r': return XK_r;
|
||||
case 's': return XK_s;
|
||||
case 't': return XK_t;
|
||||
case 'u': return XK_u;
|
||||
case 'v': return XK_v;
|
||||
case 'w': return XK_w;
|
||||
case 'x': return XK_x;
|
||||
case 'y': return XK_y;
|
||||
case 'z': return XK_z;
|
||||
}
|
||||
}
|
||||
|
||||
// For numbers and special characters
|
||||
switch (asciiChar) {
|
||||
// Numbers
|
||||
case '0': return XK_0;
|
||||
case '1': return XK_1;
|
||||
case '2': return XK_2;
|
||||
case '3': return XK_3;
|
||||
case '4': return XK_4;
|
||||
case '5': return XK_5;
|
||||
case '6': return XK_6;
|
||||
case '7': return XK_7;
|
||||
case '8': return XK_8;
|
||||
case '9': return XK_9;
|
||||
|
||||
// Special characters that need shift
|
||||
case '!': return XK_exclam; // Shift+1
|
||||
case '@': return XK_at; // Shift+2
|
||||
case '#': return XK_numbersign; // Shift+3
|
||||
case '$': return XK_dollar; // Shift+4
|
||||
case '%': return XK_percent; // Shift+5
|
||||
case '^': return XK_asciicircum; // Shift+6
|
||||
case '&': return XK_ampersand; // Shift+7
|
||||
case '*': return XK_asterisk; // Shift+8
|
||||
case '(': return XK_parenleft; // Shift+9
|
||||
case ')': return XK_parenright; // Shift+0
|
||||
case '_': return XK_underscore; // Shift+-
|
||||
case '+': return XK_plus; // Shift+=
|
||||
case '{': return XK_braceleft; // Shift+[
|
||||
case '}': return XK_braceright; // Shift+]
|
||||
case '|': return XK_bar; // Shift+\
|
||||
case ':': return XK_colon; // Shift+;
|
||||
case '"': return XK_quotedbl; // Shift+'
|
||||
case '<': return XK_less; // Shift+,
|
||||
case '>': return XK_greater; // Shift+.
|
||||
case '?': return XK_question; // Shift+/
|
||||
case '~': return XK_asciitilde; // Shift+`
|
||||
|
||||
// Special characters without shift
|
||||
case ' ': return XK_space;
|
||||
case '\t': return XK_Tab;
|
||||
case '\n': return XK_Return;
|
||||
case '\b': return XK_BackSpace;
|
||||
case 27: return XK_Escape; // ESC
|
||||
|
||||
case '-': return XK_minus;
|
||||
case '=': return XK_equal;
|
||||
case '[': return XK_bracketleft;
|
||||
case ']': return XK_bracketright;
|
||||
case '\\': return XK_backslash;
|
||||
case ';': return XK_semicolon;
|
||||
case '\'': return XK_apostrophe;
|
||||
case ',': return XK_comma;
|
||||
case '.': return XK_period;
|
||||
case '/': return XK_slash;
|
||||
case '`': return XK_grave;
|
||||
|
||||
case ':': return XK_colon;
|
||||
|
||||
default:
|
||||
std::cerr << "Warning: Character " << static_cast<int>(asciiChar)
|
||||
<< " not mapped to X11 KeySym. Using XK_space instead."
|
||||
<< std::endl;
|
||||
return XK_space;
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
#ifdef __APPLE__
|
||||
std::map<Keyboard::SpecialKey, KeyCode> Keyboard::specialKeyToVirtualKeyMap = {
|
||||
{Keyboard::BACKSPACE, kVK_Delete},
|
||||
{Keyboard::ENTER, kVK_Return},
|
||||
{Keyboard::TAB, kVK_Tab},
|
||||
{Keyboard::ESCAPE, kVK_Escape},
|
||||
{Keyboard::UP, kVK_UpArrow},
|
||||
{Keyboard::DOWN, kVK_DownArrow},
|
||||
{Keyboard::RIGHT, kVK_RightArrow},
|
||||
{Keyboard::LEFT, kVK_LeftArrow},
|
||||
{Keyboard::META, kVK_Command},
|
||||
{Keyboard::ALT, kVK_Option},
|
||||
{Keyboard::CONTROL, kVK_Control},
|
||||
{Keyboard::SHIFT, kVK_Shift},
|
||||
{Keyboard::CAPSLOCK, kVK_CapsLock},
|
||||
{Keyboard::F1, kVK_F1},
|
||||
{Keyboard::F2, kVK_F2},
|
||||
{Keyboard::F3, kVK_F3},
|
||||
{Keyboard::F4, kVK_F4},
|
||||
{Keyboard::F5, kVK_F5},
|
||||
{Keyboard::F6, kVK_F6},
|
||||
{Keyboard::F7, kVK_F7},
|
||||
{Keyboard::F8, kVK_F8},
|
||||
{Keyboard::F9, kVK_F9},
|
||||
{Keyboard::F10, kVK_F10},
|
||||
{Keyboard::F11, kVK_F11},
|
||||
{Keyboard::F12, kVK_F12}};
|
||||
#endif
|
||||
|
||||
|
||||
|
||||
#ifdef __linux__
|
||||
std::map<Keyboard::SpecialKey, KeyCode> Keyboard::specialKeyToVirtualKeyMap = {
|
||||
{Keyboard::BACKSPACE, XK_BackSpace},
|
||||
{Keyboard::ENTER, XK_Return},
|
||||
{Keyboard::TAB, XK_Tab},
|
||||
{Keyboard::ESCAPE, XK_Escape},
|
||||
{Keyboard::UP, XK_Up},
|
||||
{Keyboard::DOWN, XK_Down},
|
||||
{Keyboard::RIGHT, XK_Right},
|
||||
{Keyboard::LEFT, XK_Left},
|
||||
{Keyboard::META, XK_Super_L}, // Super/Windows key
|
||||
{Keyboard::ALT, XK_Alt_L},
|
||||
{Keyboard::CONTROL, XK_Control_L},
|
||||
{Keyboard::SHIFT, XK_Shift_L},
|
||||
{Keyboard::CAPSLOCK, XK_Caps_Lock},
|
||||
{Keyboard::F1, XK_F1},
|
||||
{Keyboard::F2, XK_F2},
|
||||
{Keyboard::F3, XK_F3},
|
||||
{Keyboard::F4, XK_F4},
|
||||
{Keyboard::F5, XK_F5},
|
||||
{Keyboard::F6, XK_F6},
|
||||
{Keyboard::F7, XK_F7},
|
||||
{Keyboard::F8, XK_F8},
|
||||
{Keyboard::F9, XK_F9},
|
||||
{Keyboard::F10, XK_F10},
|
||||
{Keyboard::F11, XK_F11},
|
||||
{Keyboard::F12, XK_F12}};
|
||||
#endif
|
||||
|
||||
char Keyboard::VirtualKeyToAscii(KeyCode virtualKey) {
|
||||
#ifdef __APPLE__
|
||||
auto map = asciiToVirtualKeyMap;
|
||||
auto it = std::find_if(
|
||||
map.begin(),
|
||||
map.end(),
|
||||
[virtualKey](const std::pair<char, int> &p) {
|
||||
return p.second == virtualKey;
|
||||
}
|
||||
);
|
||||
|
||||
if (it == map.end()) {
|
||||
return INVALID_ASCII;
|
||||
}
|
||||
|
||||
return it->first;
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
// Convert the virtual key code to a scan code
|
||||
UINT scanCode = MapVirtualKey(virtualKey, MAPVK_VK_TO_VSC);
|
||||
|
||||
// Convert the scan code to the corresponding character
|
||||
char character = 0;
|
||||
BYTE keyboardState[256] = {0};
|
||||
GetKeyboardState(keyboardState);
|
||||
wchar_t buffer[2];
|
||||
if (ToUnicode(virtualKey, scanCode, keyboardState, buffer, 2, 0) == 1) {
|
||||
character = static_cast<char>(buffer[0]);
|
||||
}
|
||||
return character;
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
InitializeX11();
|
||||
|
||||
// Get the keycode from keysym
|
||||
KeyCode keycode = XKeysymToKeycode(display, virtualKey);
|
||||
if (keycode == 0) {
|
||||
return INVALID_ASCII;
|
||||
}
|
||||
|
||||
// Get the current keyboard state
|
||||
char keys[32];
|
||||
XQueryKeymap(display, keys);
|
||||
|
||||
// Get the current modifier state
|
||||
XKeyEvent event;
|
||||
event.display = display;
|
||||
event.window = rootWindow;
|
||||
event.root = rootWindow;
|
||||
event.subwindow = None;
|
||||
event.time = CurrentTime;
|
||||
event.x = event.y = event.x_root = event.y_root = 0;
|
||||
event.same_screen = True;
|
||||
event.keycode = keycode;
|
||||
event.state = 0;
|
||||
|
||||
// Try to get the character with different modifier states
|
||||
KeySym keysym;
|
||||
XLookupString(&event, nullptr, 0, &keysym, nullptr);
|
||||
|
||||
// Convert keysym to ASCII if possible
|
||||
if (keysym >= 0x20 && keysym <= 0x7E) { // Printable ASCII range
|
||||
return static_cast<char>(keysym);
|
||||
}
|
||||
|
||||
return INVALID_ASCII;
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
Keyboard::SpecialKey Keyboard::VirtualKeyToSpecialKey(KeyCode virtualKey) {
|
||||
#ifdef __APPLE__
|
||||
switch (virtualKey) {
|
||||
case 123:
|
||||
return Keyboard::LEFT;
|
||||
case 124:
|
||||
return Keyboard::RIGHT;
|
||||
case 125:
|
||||
return Keyboard::DOWN;
|
||||
case 126:
|
||||
return Keyboard::UP;
|
||||
case 36:
|
||||
return Keyboard::ENTER;
|
||||
case 48:
|
||||
return Keyboard::TAB;
|
||||
case 51:
|
||||
return Keyboard::BACKSPACE;
|
||||
case 53:
|
||||
return Keyboard::ESCAPE;
|
||||
case 55:
|
||||
return Keyboard::META;
|
||||
case 56:
|
||||
return Keyboard::SHIFT;
|
||||
case 57:
|
||||
return Keyboard::CAPSLOCK;
|
||||
case 58:
|
||||
return Keyboard::ALT;
|
||||
case 59:
|
||||
return Keyboard::CONTROL;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
switch (virtualKey) {
|
||||
case VK_LEFT:
|
||||
return Keyboard::LEFT;
|
||||
case VK_RIGHT:
|
||||
return Keyboard::RIGHT;
|
||||
case VK_DOWN:
|
||||
return Keyboard::DOWN;
|
||||
case VK_UP:
|
||||
return Keyboard::UP;
|
||||
case VK_RETURN:
|
||||
return Keyboard::ENTER;
|
||||
case VK_TAB:
|
||||
return Keyboard::TAB;
|
||||
case VK_BACK:
|
||||
return Keyboard::BACKSPACE;
|
||||
case VK_ESCAPE:
|
||||
return Keyboard::ESCAPE;
|
||||
case VK_LWIN:
|
||||
case VK_RWIN:
|
||||
return Keyboard::META;
|
||||
case VK_SHIFT:
|
||||
return Keyboard::SHIFT;
|
||||
case VK_CAPITAL:
|
||||
return Keyboard::CAPSLOCK;
|
||||
case VK_MENU:
|
||||
return Keyboard::ALT;
|
||||
case VK_CONTROL:
|
||||
return Keyboard::CONTROL;
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
switch (virtualKey) {
|
||||
case XK_Left:
|
||||
return Keyboard::LEFT;
|
||||
case XK_Right:
|
||||
return Keyboard::RIGHT;
|
||||
case XK_Down:
|
||||
return Keyboard::DOWN;
|
||||
case XK_Up:
|
||||
return Keyboard::UP;
|
||||
case XK_Return:
|
||||
return Keyboard::ENTER;
|
||||
case XK_Tab:
|
||||
return Keyboard::TAB;
|
||||
case XK_BackSpace:
|
||||
return Keyboard::BACKSPACE;
|
||||
case XK_Escape:
|
||||
return Keyboard::ESCAPE;
|
||||
case XK_Super_L:
|
||||
case XK_Super_R:
|
||||
return Keyboard::META;
|
||||
case XK_Shift_L:
|
||||
case XK_Shift_R:
|
||||
return Keyboard::SHIFT;
|
||||
case XK_Caps_Lock:
|
||||
return Keyboard::CAPSLOCK;
|
||||
case XK_Alt_L:
|
||||
case XK_Alt_R:
|
||||
return Keyboard::ALT;
|
||||
case XK_Control_L:
|
||||
case XK_Control_R:
|
||||
return Keyboard::CONTROL;
|
||||
}
|
||||
#endif
|
||||
// Default case for all platforms
|
||||
return static_cast<Keyboard::SpecialKey>(0);
|
||||
}
|
||||
|
||||
#ifdef _WIN32
|
||||
std::map<Keyboard::SpecialKey, KeyCode> Keyboard::specialKeyToVirtualKeyMap = {
|
||||
{Keyboard::BACKSPACE, VK_BACK},
|
||||
{Keyboard::ENTER, VK_RETURN},
|
||||
{Keyboard::TAB, VK_TAB},
|
||||
{Keyboard::ESCAPE, VK_ESCAPE},
|
||||
{Keyboard::UP, VK_UP},
|
||||
{Keyboard::DOWN, VK_DOWN},
|
||||
{Keyboard::RIGHT, VK_RIGHT},
|
||||
{Keyboard::LEFT, VK_LEFT},
|
||||
{Keyboard::META, VK_LWIN},
|
||||
{Keyboard::ALT, VK_MENU},
|
||||
{Keyboard::CONTROL, VK_CONTROL},
|
||||
{Keyboard::SHIFT, VK_SHIFT},
|
||||
{Keyboard::CAPSLOCK, VK_CAPITAL},
|
||||
{Keyboard::F1, VK_F1},
|
||||
{Keyboard::F2, VK_F2},
|
||||
{Keyboard::F3, VK_F3},
|
||||
{Keyboard::F4, VK_F4},
|
||||
{Keyboard::F5, VK_F5},
|
||||
{Keyboard::F6, VK_F6},
|
||||
{Keyboard::F7, VK_F7},
|
||||
{Keyboard::F8, VK_F8},
|
||||
{Keyboard::F9, VK_F9},
|
||||
{Keyboard::F10, VK_F10},
|
||||
{Keyboard::F11, VK_F11},
|
||||
{Keyboard::F12, VK_F12}};
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
std::map<char, int> Keyboard::asciiToVirtualKeyMap = {
|
||||
// 数字
|
||||
{'0', kVK_ANSI_0}, {'1', kVK_ANSI_1}, {'2', kVK_ANSI_2},
|
||||
{'3', kVK_ANSI_3}, {'4', kVK_ANSI_4}, {'5', kVK_ANSI_5},
|
||||
{'6', kVK_ANSI_6}, {'7', kVK_ANSI_7}, {'8', kVK_ANSI_8},
|
||||
{'9', kVK_ANSI_9},
|
||||
|
||||
// 字母(只使用小写)
|
||||
{'a', kVK_ANSI_A}, {'b', kVK_ANSI_B}, {'c', kVK_ANSI_C},
|
||||
{'d', kVK_ANSI_D}, {'e', kVK_ANSI_E}, {'f', kVK_ANSI_F},
|
||||
{'g', kVK_ANSI_G}, {'h', kVK_ANSI_H}, {'i', kVK_ANSI_I},
|
||||
{'j', kVK_ANSI_J}, {'k', kVK_ANSI_K}, {'l', kVK_ANSI_L},
|
||||
{'m', kVK_ANSI_M}, {'n', kVK_ANSI_N}, {'o', kVK_ANSI_O},
|
||||
{'p', kVK_ANSI_P}, {'q', kVK_ANSI_Q}, {'r', kVK_ANSI_R},
|
||||
{'s', kVK_ANSI_S}, {'t', kVK_ANSI_T}, {'u', kVK_ANSI_U},
|
||||
{'v', kVK_ANSI_V}, {'w', kVK_ANSI_W}, {'x', kVK_ANSI_X},
|
||||
{'y', kVK_ANSI_Y}, {'z', kVK_ANSI_Z},
|
||||
|
||||
// 特殊字符(不需要Shift的)
|
||||
{' ', kVK_Space},
|
||||
{'-', kVK_ANSI_Minus},
|
||||
{'=', kVK_ANSI_Equal},
|
||||
{'[', kVK_ANSI_LeftBracket},
|
||||
{']', kVK_ANSI_RightBracket},
|
||||
{'\\', kVK_ANSI_Backslash},
|
||||
{';', kVK_ANSI_Semicolon},
|
||||
{'\'', kVK_ANSI_Quote},
|
||||
{',', kVK_ANSI_Comma},
|
||||
{'.', kVK_ANSI_Period},
|
||||
{'/', kVK_ANSI_Slash},
|
||||
{'`', kVK_ANSI_Grave},
|
||||
|
||||
// 需要Shift的字符(映射到对应数字或符号键)
|
||||
{'!', kVK_ANSI_1}, // Shift+1
|
||||
{'@', kVK_ANSI_2}, // Shift+2
|
||||
{'#', kVK_ANSI_3}, // Shift+3
|
||||
{'$', kVK_ANSI_4}, // Shift+4
|
||||
{'%', kVK_ANSI_5}, // Shift+5
|
||||
{'^', kVK_ANSI_6}, // Shift+6
|
||||
{'&', kVK_ANSI_7}, // Shift+7
|
||||
{'*', kVK_ANSI_8}, // Shift+8
|
||||
{'(', kVK_ANSI_9}, // Shift+9
|
||||
{')', kVK_ANSI_0}, // Shift+0
|
||||
{'_', kVK_ANSI_Minus}, // Shift+-
|
||||
{'+', kVK_ANSI_Equal}, // Shift+=
|
||||
{'{', kVK_ANSI_LeftBracket}, // Shift+[
|
||||
{'}', kVK_ANSI_RightBracket}, // Shift+]
|
||||
{'|', kVK_ANSI_Backslash}, // Shift+\
|
||||
{':', kVK_ANSI_Semicolon}, // Shift+;
|
||||
{'"', kVK_ANSI_Quote}, // Shift+'
|
||||
{'<', kVK_ANSI_Comma}, // Shift+,
|
||||
{'>', kVK_ANSI_Period}, // Shift+.
|
||||
{'?', kVK_ANSI_Slash}, // Shift+/
|
||||
{'~', kVK_ANSI_Grave} // Shift+`
|
||||
};
|
||||
#endif
|
||||
} // namespace Robot
|
||||
Vendored
+124
@@ -0,0 +1,124 @@
|
||||
#pragma once
|
||||
|
||||
#include <cctype>
|
||||
#include <string>
|
||||
#include <map>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <Windows.h>
|
||||
#endif
|
||||
|
||||
#ifdef __APPLE__
|
||||
#import <Carbon/Carbon.h>
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/keysym.h>
|
||||
#endif
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <set>
|
||||
#include <thread>
|
||||
|
||||
namespace Robot {
|
||||
|
||||
#ifdef __APPLE__
|
||||
typedef CGKeyCode KeyCode;
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
typedef WORD KeyCode;
|
||||
#endif
|
||||
|
||||
#ifdef __linux__
|
||||
typedef KeySym KeyCode;
|
||||
#endif
|
||||
|
||||
class Keyboard {
|
||||
public:
|
||||
enum SpecialKey {
|
||||
BACKSPACE,
|
||||
ENTER,
|
||||
TAB,
|
||||
ESCAPE,
|
||||
UP,
|
||||
DOWN,
|
||||
RIGHT,
|
||||
LEFT,
|
||||
META,
|
||||
ALT,
|
||||
CONTROL,
|
||||
SHIFT,
|
||||
CAPSLOCK,
|
||||
F1,
|
||||
F2,
|
||||
F3,
|
||||
F4,
|
||||
F5,
|
||||
F6,
|
||||
F7,
|
||||
F8,
|
||||
F9,
|
||||
F10,
|
||||
F11,
|
||||
F12
|
||||
};
|
||||
|
||||
static const char INVALID_ASCII;
|
||||
|
||||
Keyboard() = delete;
|
||||
virtual ~Keyboard() = default;
|
||||
|
||||
static void Type(const std::string& query);
|
||||
|
||||
static void TypeHumanLike(const std::string& query);
|
||||
|
||||
static void Click(char asciiChar);
|
||||
static void Click(SpecialKey specialKey);
|
||||
|
||||
static void HoldStart(char asciiChar);
|
||||
static void HoldStart(SpecialKey specialKey);
|
||||
static void HoldStop(char asciiChar);
|
||||
static void HoldStop(SpecialKey specialKey);
|
||||
|
||||
static void Press(char asciiChar);
|
||||
static void Press(SpecialKey specialKey);
|
||||
|
||||
static void Release(char asciiChar);
|
||||
static void Release(SpecialKey specialKey);
|
||||
|
||||
static char VirtualKeyToAscii(KeyCode virtualKey);
|
||||
static SpecialKey VirtualKeyToSpecialKey(KeyCode virtualKey);
|
||||
|
||||
private:
|
||||
static std::thread keyPressThread;
|
||||
static std::atomic<bool> continueHolding;
|
||||
static std::set<char> heldAsciiChars;
|
||||
static std::set<SpecialKey> heldSpecialKeys;
|
||||
|
||||
static void KeyHoldThread();
|
||||
|
||||
static int delay;
|
||||
|
||||
static KeyCode AsciiToVirtualKey(char asciiChar);
|
||||
|
||||
static KeyCode SpecialKeyToVirtualKey(SpecialKey specialKey);
|
||||
|
||||
static std::map<SpecialKey, KeyCode> specialKeyToVirtualKeyMap;
|
||||
// Platform-specific implementations
|
||||
#ifdef __linux__
|
||||
static Display* display;
|
||||
static Window rootWindow;
|
||||
static void InitializeX11();
|
||||
static void CleanupX11();
|
||||
#endif
|
||||
// note: windows alternative doesn't use map
|
||||
#ifdef __APPLE__
|
||||
static std::map<char, int> asciiToVirtualKeyMap;
|
||||
#endif
|
||||
};
|
||||
|
||||
} // namespace Robot
|
||||
Vendored
+375
@@ -0,0 +1,375 @@
|
||||
#include "./Mouse.h"
|
||||
#include "./Utils.h"
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <Windows.h>
|
||||
#elif __APPLE__
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
#elif __linux__
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#include <X11/extensions/XTest.h>
|
||||
#include <iostream>
|
||||
#include <cstring>
|
||||
#endif
|
||||
|
||||
namespace Robot {
|
||||
|
||||
unsigned int Mouse::delay = 16;
|
||||
bool Mouse::isPressed = false;
|
||||
MouseButton Mouse::pressedButton = MouseButton::LEFT_BUTTON;
|
||||
|
||||
#ifdef __linux__
|
||||
Display* Mouse::display = nullptr;
|
||||
Window Mouse::rootWindow = 0;
|
||||
|
||||
void Mouse::InitializeX11() {
|
||||
if (display == nullptr) {
|
||||
display = XOpenDisplay(nullptr);
|
||||
if (display == nullptr) {
|
||||
throw std::runtime_error("Cannot open X11 display");
|
||||
}
|
||||
rootWindow = DefaultRootWindow(display);
|
||||
}
|
||||
}
|
||||
|
||||
void Mouse::CleanupX11() {
|
||||
if (display != nullptr) {
|
||||
XCloseDisplay(display);
|
||||
display = nullptr;
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifdef _WIN32
|
||||
POINT Mouse::getCurrentPosition() {
|
||||
POINT winPoint;
|
||||
GetCursorPos(&winPoint);
|
||||
return winPoint;
|
||||
}
|
||||
#elif __APPLE__
|
||||
CGPoint Mouse::getCurrentPosition() {
|
||||
CGEventRef event = CGEventCreate(nullptr);
|
||||
CGPoint cursor = CGEventGetLocation(event);
|
||||
CFRelease(event);
|
||||
return cursor;
|
||||
}
|
||||
#elif __linux__
|
||||
Robot::Point Mouse::getCurrentPosition() {
|
||||
InitializeX11();
|
||||
|
||||
Robot::Point point;
|
||||
Window root_return, child_return;
|
||||
int root_x, root_y;
|
||||
int win_x, win_y;
|
||||
unsigned int mask_return;
|
||||
|
||||
XQueryPointer(display, rootWindow, &root_return, &child_return,
|
||||
&root_x, &root_y, &win_x, &win_y, &mask_return);
|
||||
|
||||
point.x = root_x;
|
||||
point.y = root_y;
|
||||
|
||||
return point;
|
||||
}
|
||||
#endif
|
||||
|
||||
void Mouse::Move(Robot::Point point) {
|
||||
#ifdef _WIN32
|
||||
SetCursorPos(point.x, point.y);
|
||||
#elif __APPLE__
|
||||
CGPoint target = CGPointMake(point.x, point.y);
|
||||
CGEventRef event = CGEventCreateMouseEvent(
|
||||
nullptr,
|
||||
kCGEventMouseMoved,
|
||||
target,
|
||||
kCGMouseButtonLeft
|
||||
);
|
||||
CGEventPost(kCGHIDEventTap, event);
|
||||
CFRelease(event);
|
||||
|
||||
if (Mouse::isPressed) {
|
||||
Mouse::MoveWithButtonPressed(point, Mouse::pressedButton);
|
||||
}
|
||||
#elif __linux__
|
||||
InitializeX11();
|
||||
|
||||
// Move the mouse using XTest
|
||||
XTestFakeMotionEvent(display, -1, point.x, point.y, CurrentTime);
|
||||
XFlush(display);
|
||||
|
||||
if (Mouse::isPressed) {
|
||||
Mouse::MoveWithButtonPressed(point, Mouse::pressedButton);
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
Robot::Point Mouse::GetPosition() {
|
||||
// TODO: how long exactly should we wait?
|
||||
Robot::delay(16);
|
||||
|
||||
Robot::Point point;
|
||||
#ifdef _WIN32
|
||||
POINT cursor = getCurrentPosition();
|
||||
#elif __APPLE__
|
||||
CGPoint cursor = getCurrentPosition();
|
||||
#elif __linux__
|
||||
Robot::Point cursor = getCurrentPosition();
|
||||
point.x = cursor.x;
|
||||
point.y = cursor.y;
|
||||
#endif
|
||||
point.x = cursor.x;
|
||||
point.y = cursor.y;
|
||||
|
||||
return point;
|
||||
}
|
||||
|
||||
void Mouse::ToggleButton(bool down, MouseButton button, bool doubleClick) {
|
||||
#ifdef _WIN32
|
||||
INPUT input = {0};
|
||||
input.type = INPUT_MOUSE;
|
||||
input.mi.dwFlags =
|
||||
(button == MouseButton::LEFT_BUTTON
|
||||
? (down ? MOUSEEVENTF_LEFTDOWN : MOUSEEVENTF_LEFTUP)
|
||||
: button == MouseButton::RIGHT_BUTTON
|
||||
? (down ? MOUSEEVENTF_RIGHTDOWN : MOUSEEVENTF_RIGHTUP)
|
||||
: (down ? MOUSEEVENTF_MIDDLEDOWN : MOUSEEVENTF_MIDDLEUP));
|
||||
SendInput(1, &input, sizeof(INPUT));
|
||||
#elif __APPLE__
|
||||
CGPoint currentPosition = getCurrentPosition();
|
||||
|
||||
CGEventType buttonType;
|
||||
switch (button) {
|
||||
case MouseButton::LEFT_BUTTON:
|
||||
buttonType = down ? kCGEventLeftMouseDown : kCGEventLeftMouseUp;
|
||||
break;
|
||||
case MouseButton::RIGHT_BUTTON:
|
||||
buttonType = down ? kCGEventRightMouseDown : kCGEventRightMouseUp;
|
||||
break;
|
||||
case MouseButton::CENTER_BUTTON:
|
||||
buttonType = down ? kCGEventOtherMouseDown : kCGEventOtherMouseUp;
|
||||
break;
|
||||
}
|
||||
|
||||
CGEventRef buttonEvent = CGEventCreateMouseEvent(
|
||||
nullptr,
|
||||
buttonType,
|
||||
currentPosition,
|
||||
(button == MouseButton::CENTER_BUTTON) ? kCGMouseButtonCenter
|
||||
: kCGMouseButtonLeft
|
||||
);
|
||||
|
||||
if (doubleClick) {
|
||||
CGEventSetIntegerValueField(buttonEvent, kCGMouseEventClickState, 2);
|
||||
}
|
||||
|
||||
CGEventPost(kCGHIDEventTap, buttonEvent);
|
||||
CFRelease(buttonEvent);
|
||||
#elif __linux__
|
||||
InitializeX11();
|
||||
|
||||
unsigned int buttonCode;
|
||||
switch (button) {
|
||||
case MouseButton::LEFT_BUTTON:
|
||||
buttonCode = 1;
|
||||
break;
|
||||
case MouseButton::RIGHT_BUTTON:
|
||||
buttonCode = 3;
|
||||
break;
|
||||
case MouseButton::CENTER_BUTTON:
|
||||
buttonCode = 2;
|
||||
break;
|
||||
default:
|
||||
buttonCode = 1;
|
||||
}
|
||||
|
||||
if (down) {
|
||||
XTestFakeButtonEvent(display, buttonCode, True, CurrentTime);
|
||||
} else {
|
||||
XTestFakeButtonEvent(display, buttonCode, False, CurrentTime);
|
||||
}
|
||||
|
||||
XFlush(display);
|
||||
|
||||
// Handle double click
|
||||
if (doubleClick && !down) {
|
||||
Robot::delay(10);
|
||||
XTestFakeButtonEvent(display, buttonCode, True, CurrentTime);
|
||||
XTestFakeButtonEvent(display, buttonCode, False, CurrentTime);
|
||||
XFlush(display);
|
||||
}
|
||||
#endif
|
||||
|
||||
if (down) {
|
||||
Mouse::isPressed = true;
|
||||
Mouse::pressedButton = button;
|
||||
} else {
|
||||
Mouse::isPressed = false;
|
||||
}
|
||||
}
|
||||
|
||||
void Mouse::MoveWithButtonPressed(Robot::Point point, MouseButton button) {
|
||||
#ifdef _WIN32
|
||||
// On Windows, just calling Move is enough as it will keep the button state.
|
||||
Mouse::Move(point);
|
||||
#elif __APPLE__
|
||||
CGPoint target = CGPointMake(point.x, point.y);
|
||||
|
||||
CGEventType dragEventType;
|
||||
CGMouseButton cgButton;
|
||||
switch (button) {
|
||||
case MouseButton::LEFT_BUTTON:
|
||||
dragEventType = kCGEventLeftMouseDragged;
|
||||
cgButton = kCGMouseButtonLeft;
|
||||
break;
|
||||
case MouseButton::RIGHT_BUTTON:
|
||||
dragEventType = kCGEventRightMouseDragged;
|
||||
cgButton = kCGMouseButtonRight;
|
||||
break;
|
||||
case MouseButton::CENTER_BUTTON:
|
||||
dragEventType = kCGEventOtherMouseDragged;
|
||||
cgButton = kCGMouseButtonCenter;
|
||||
break;
|
||||
}
|
||||
|
||||
CGEventRef mouseDragEvent =
|
||||
CGEventCreateMouseEvent(nullptr, dragEventType, target, cgButton);
|
||||
CGEventPost(kCGHIDEventTap, mouseDragEvent);
|
||||
CFRelease(mouseDragEvent);
|
||||
#elif __linux__
|
||||
InitializeX11();
|
||||
|
||||
// For dragging, we simulate mouse motion with button pressed
|
||||
unsigned int buttonState = 0;
|
||||
switch (button) {
|
||||
case MouseButton::LEFT_BUTTON:
|
||||
buttonState = Button1Mask;
|
||||
break;
|
||||
case MouseButton::RIGHT_BUTTON:
|
||||
buttonState = Button3Mask;
|
||||
break;
|
||||
case MouseButton::CENTER_BUTTON:
|
||||
buttonState = Button2Mask;
|
||||
break;
|
||||
}
|
||||
|
||||
// Create a motion event with the appropriate button state
|
||||
XTestFakeMotionEvent(display, -1, point.x, point.y, CurrentTime);
|
||||
|
||||
// Ensure the button is held down
|
||||
unsigned int buttonCode;
|
||||
switch (button) {
|
||||
case MouseButton::LEFT_BUTTON:
|
||||
buttonCode = 1;
|
||||
break;
|
||||
case MouseButton::RIGHT_BUTTON:
|
||||
buttonCode = 3;
|
||||
break;
|
||||
case MouseButton::CENTER_BUTTON:
|
||||
buttonCode = 2;
|
||||
break;
|
||||
}
|
||||
|
||||
XFlush(display);
|
||||
#endif
|
||||
}
|
||||
|
||||
void Mouse::Click(MouseButton button) {
|
||||
ToggleButton(true, button);
|
||||
Robot::delay(10); // add a little delay
|
||||
ToggleButton(false, button);
|
||||
}
|
||||
|
||||
void Mouse::DoubleClick(MouseButton button) {
|
||||
Click(button);
|
||||
Robot::delay(80);
|
||||
Click(button);
|
||||
}
|
||||
|
||||
void Mouse::ScrollBy(int y, int x) {
|
||||
#ifdef _WIN32
|
||||
INPUT input = {0};
|
||||
input.type = INPUT_MOUSE;
|
||||
|
||||
input.mi.dwFlags = MOUSEEVENTF_WHEEL;
|
||||
input.mi.mouseData = static_cast<DWORD>(WHEEL_DELTA * y);
|
||||
SendInput(1, &input, sizeof(INPUT));
|
||||
|
||||
input.mi.dwFlags = MOUSEEVENTF_HWHEEL;
|
||||
input.mi.mouseData = static_cast<DWORD>(WHEEL_DELTA * x);
|
||||
SendInput(1, &input, sizeof(INPUT));
|
||||
#elif __APPLE__
|
||||
CGEventRef scrollEvent =
|
||||
CGEventCreateScrollWheelEvent(nullptr, kCGScrollEventUnitPixel, 2, y, x);
|
||||
CGEventPost(kCGHIDEventTap, scrollEvent);
|
||||
CFRelease(scrollEvent);
|
||||
#elif __linux__
|
||||
InitializeX11();
|
||||
|
||||
// Vertical scrolling
|
||||
if (y != 0) {
|
||||
unsigned int buttonCode = (y > 0) ? 4 : 5; // 4=up, 5=down
|
||||
for (int i = 0; i < std::abs(y); i++) {
|
||||
XTestFakeButtonEvent(display, buttonCode, True, CurrentTime);
|
||||
XTestFakeButtonEvent(display, buttonCode, False, CurrentTime);
|
||||
XFlush(display);
|
||||
Robot::delay(10);
|
||||
}
|
||||
}
|
||||
|
||||
// Horizontal scrolling
|
||||
if (x != 0) {
|
||||
unsigned int buttonCode = (x > 0) ? 7 : 6; // 6=left, 7=right
|
||||
for (int i = 0; i < std::abs(x); i++) {
|
||||
XTestFakeButtonEvent(display, buttonCode, True, CurrentTime);
|
||||
XTestFakeButtonEvent(display, buttonCode, False, CurrentTime);
|
||||
XFlush(display);
|
||||
Robot::delay(10);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
}
|
||||
|
||||
void Mouse::Drag(Robot::Point toPoint) {
|
||||
Robot::Mouse::ToggleButton(true, Robot::MouseButton::LEFT_BUTTON);
|
||||
Robot::delay(10);
|
||||
Mouse::Move(toPoint);
|
||||
Robot::delay(10);
|
||||
Mouse::ToggleButton(false, MouseButton::LEFT_BUTTON);
|
||||
}
|
||||
|
||||
void Mouse::MoveSmooth(Robot::Point point) {
|
||||
Robot::Point currentPosition = GetPosition();
|
||||
|
||||
int dx = point.x - currentPosition.x;
|
||||
int dy = point.y - currentPosition.y;
|
||||
|
||||
int steps = (std::abs(dx) > std::abs(dy)) ? std::abs(dx) : std::abs(dy);
|
||||
|
||||
float deltaX = static_cast<float>(dx) / steps;
|
||||
float deltaY = static_cast<float>(dy) / steps;
|
||||
|
||||
for (int i = 1; i <= steps; i++) {
|
||||
Robot::Point stepPosition;
|
||||
stepPosition.x = currentPosition.x + static_cast<int>(deltaX * i);
|
||||
stepPosition.y = currentPosition.y + static_cast<int>(deltaY * i);
|
||||
|
||||
if (Mouse::isPressed) {
|
||||
MoveWithButtonPressed(stepPosition, Mouse::pressedButton);
|
||||
} else {
|
||||
Move(stepPosition);
|
||||
}
|
||||
|
||||
Robot::delay(1);
|
||||
}
|
||||
}
|
||||
|
||||
void Mouse::DragSmooth(Robot::Point toPoint) {
|
||||
Robot::Mouse::ToggleButton(true, Robot::MouseButton::LEFT_BUTTON);
|
||||
Robot::delay(10);
|
||||
Mouse::MoveSmooth(toPoint);
|
||||
Robot::delay(10);
|
||||
Mouse::ToggleButton(false, MouseButton::LEFT_BUTTON);
|
||||
}
|
||||
|
||||
} // namespace Robot
|
||||
Vendored
+72
@@ -0,0 +1,72 @@
|
||||
#pragma once
|
||||
|
||||
#include "./types.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
#ifdef _WIN32
|
||||
#include <Windows.h>
|
||||
#elif __APPLE__
|
||||
#include <ApplicationServices/ApplicationServices.h>
|
||||
#elif __linux__
|
||||
#include <X11/Xlib.h>
|
||||
#include <X11/Xutil.h>
|
||||
#endif
|
||||
namespace Robot {
|
||||
|
||||
enum class MouseButton : uint8_t {
|
||||
LEFT_BUTTON = 0,
|
||||
RIGHT_BUTTON = 1,
|
||||
CENTER_BUTTON = 2
|
||||
};
|
||||
|
||||
class Mouse {
|
||||
public:
|
||||
static unsigned int delay;
|
||||
|
||||
static bool isPressed;
|
||||
static MouseButton pressedButton;
|
||||
|
||||
Mouse() = delete;
|
||||
|
||||
static void Move(Robot::Point point);
|
||||
|
||||
static void MoveSmooth(Robot::Point point);
|
||||
|
||||
static void Drag(Robot::Point toPoint);
|
||||
|
||||
static void DragSmooth(Robot::Point toPoint);
|
||||
|
||||
static Robot::Point GetPosition();
|
||||
|
||||
static void ToggleButton(
|
||||
bool down, MouseButton button, bool doubleClick = false
|
||||
);
|
||||
|
||||
static void Click(MouseButton button);
|
||||
|
||||
static void DoubleClick(MouseButton button);
|
||||
|
||||
static void ScrollBy(int y, int x = 0);
|
||||
|
||||
private:
|
||||
static void MoveWithButtonPressed(Robot::Point point, MouseButton button);
|
||||
// Platform-specific implementations
|
||||
#ifdef __linux__
|
||||
static Display* display;
|
||||
static Window rootWindow;
|
||||
static void InitializeX11();
|
||||
static void CleanupX11();
|
||||
#endif
|
||||
#ifdef _WIN32
|
||||
static POINT getCurrentPosition();
|
||||
#elif __APPLE__
|
||||
static CGPoint getCurrentPosition();
|
||||
#elif __linux__
|
||||
static Robot::Point getCurrentPosition();
|
||||
#endif
|
||||
|
||||
};
|
||||
|
||||
} // namespace Robot
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
//
|
||||
// Created by misaki on 2026/1/25.
|
||||
//
|
||||
#pragma once
|
||||
|
||||
// 包含所有必要头文件
|
||||
#include "Autogui.h"
|
||||
|
||||
// 常用常量
|
||||
namespace AutoGUI {
|
||||
// 鼠标按钮常量
|
||||
const Button LEFT = Button::LEFT;
|
||||
const Button RIGHT = Button::RIGHT;
|
||||
const Button MIDDLE = Button::MIDDLE;
|
||||
|
||||
// 常用键常量(字符串形式,用于press、keyDown等函数)
|
||||
namespace Keys {
|
||||
const std::string BACKSPACE = "backspace";
|
||||
const std::string ENTER = "enter";
|
||||
const std::string TAB = "tab";
|
||||
const std::string ESCAPE = "escape";
|
||||
const std::string ESC = "esc";
|
||||
const std::string UP = "up";
|
||||
const std::string DOWN = "down";
|
||||
const std::string RIGHT = "right";
|
||||
const std::string LEFT = "left";
|
||||
const std::string WIN = "win";
|
||||
const std::string COMMAND = "command";
|
||||
const std::string ALT = "alt";
|
||||
const std::string CTRL = "ctrl";
|
||||
const std::string SHIFT = "shift";
|
||||
const std::string CAPSLOCK = "capslock";
|
||||
const std::string F1 = "f1";
|
||||
const std::string F2 = "f2";
|
||||
const std::string F3 = "f3";
|
||||
const std::string F4 = "f4";
|
||||
const std::string F5 = "f5";
|
||||
const std::string F6 = "f6";
|
||||
const std::string F7 = "f7";
|
||||
const std::string F8 = "f8";
|
||||
const std::string F9 = "f9";
|
||||
const std::string F10 = "f10";
|
||||
const std::string F11 = "f11";
|
||||
const std::string F12 = "f12";
|
||||
const std::string SPACE = "space";
|
||||
}
|
||||
}
|
||||
Vendored
+9
@@ -0,0 +1,9 @@
|
||||
#include "./Utils.h"
|
||||
|
||||
namespace Robot {
|
||||
|
||||
void delay(unsigned int ms) {
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(ms));
|
||||
}
|
||||
|
||||
} // namespace Robot
|
||||
Vendored
+71
@@ -0,0 +1,71 @@
|
||||
#pragma once
|
||||
|
||||
#include <cctype>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <thread>
|
||||
|
||||
namespace Robot {
|
||||
|
||||
void delay(unsigned int ms);
|
||||
|
||||
namespace KeyUtils {
|
||||
// 检查字符是否需要Shift键
|
||||
inline bool NeedsShift(char c) {
|
||||
// 大写字母需要Shift
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
return true;
|
||||
}
|
||||
|
||||
// 特殊符号需要Shift
|
||||
const char* shiftChars = "!@#$%^&*()_+{}|:\"<>?~";
|
||||
return (strchr(shiftChars, c) != nullptr);
|
||||
}
|
||||
|
||||
// 获取字符的基础键(物理键位)
|
||||
inline char GetBaseKey(char c) {
|
||||
// 对于字母,返回小写形式
|
||||
if (c >= 'A' && c <= 'Z') {
|
||||
return std::tolower(c);
|
||||
}
|
||||
|
||||
// 对于数字,保持不变
|
||||
if (c >= '0' && c <= '9') {
|
||||
return c;
|
||||
}
|
||||
|
||||
// 对于需要Shift的符号,返回对应的基础键
|
||||
switch (c) {
|
||||
case '!': return '1';
|
||||
case '@': return '2';
|
||||
case '#': return '3';
|
||||
case '$': return '4';
|
||||
case '%': return '5';
|
||||
case '^': return '6';
|
||||
case '&': return '7';
|
||||
case '*': return '8';
|
||||
case '(': return '9';
|
||||
case ')': return '0';
|
||||
case '_': return '-';
|
||||
case '+': return '=';
|
||||
case '{': return '[';
|
||||
case '}': return ']';
|
||||
case '|': return '\\';
|
||||
case ':': return ';';
|
||||
case '"': return '\'';
|
||||
case '<': return ',';
|
||||
case '>': return '.';
|
||||
case '?': return '/';
|
||||
case '~': return '`';
|
||||
default: return c;
|
||||
}
|
||||
}
|
||||
|
||||
// 检查字符是否是有效的ASCII字符
|
||||
inline bool IsValidAscii(char c) {
|
||||
return c >= 32 && c <= 126; // 可打印ASCII字符
|
||||
}
|
||||
|
||||
} // namespace KeyUtils
|
||||
|
||||
} // namespace Robot
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
#pragma once
|
||||
|
||||
#include <cmath>
|
||||
|
||||
namespace Robot {
|
||||
|
||||
struct Point {
|
||||
int x;
|
||||
int y;
|
||||
|
||||
[[nodiscard]] double Distance(Point target) const {
|
||||
return sqrt(pow(target.x - x, 2) + pow(target.y - y, 2));
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace Robot
|
||||
+13
-9
@@ -8,9 +8,6 @@ set(CMAKE_AUTOUIC ON)
|
||||
|
||||
set(CMAKE_PREFIX_PATH "/home/misaki/Qt6.3/6.6.3/gcc_64") # 设置Qt6安装路径(此处请根据你的Qt6安装位置填写)
|
||||
|
||||
# 添加ElaWidgetTools UI库项目
|
||||
add_subdirectory(3rdparty/ElaWidgetTools)
|
||||
|
||||
# 设置是否为debug模式 默认为Debug
|
||||
if(NOT CMAKE_BUILD_TYPE)
|
||||
set(CMAKE_BUILD_TYPE Debug)
|
||||
@@ -40,6 +37,9 @@ file(GLOB_RECURSE YosugaSrc
|
||||
"src/Render/TextRender/Inc/*.h"
|
||||
"src/Core/Src/*.cpp"
|
||||
"src/Core/Inc/*.h"
|
||||
"src/Utils/Inc/*.hpp"
|
||||
"src/Utils/Inc/*.h"
|
||||
"src/Utils/Src/*.cpp"
|
||||
)
|
||||
|
||||
# 查找Qt6模块以及其他必须模块
|
||||
@@ -47,16 +47,18 @@ find_package(Qt6 COMPONENTS
|
||||
Core
|
||||
Gui
|
||||
Widgets
|
||||
Network
|
||||
Svg
|
||||
SerialPort
|
||||
WebSockets
|
||||
Multimedia
|
||||
Network # 网络
|
||||
Svg # svg
|
||||
SerialPort # 串口
|
||||
WebSockets # websocket
|
||||
Multimedia # 音频
|
||||
OpenGLWidgets
|
||||
Concurrent
|
||||
REQUIRED)
|
||||
find_package(OpenGL REQUIRED)
|
||||
|
||||
add_subdirectory(3rdparty/ElaWidgetTools) # 添加ElaWidgetTools UI库
|
||||
add_subdirectory(3rdparty/autogui-cpp) # 添加autogui-cpp GUI自动化库
|
||||
|
||||
add_executable(${PROJECT_NAME} main.cpp ${LAppLive2D} ${YosugaSrc})
|
||||
|
||||
@@ -166,6 +168,7 @@ target_link_libraries(${PROJECT_NAME}
|
||||
target_link_libraries(${PROJECT_NAME}
|
||||
PRIVATE
|
||||
ElaWidgetTools
|
||||
autogui-cpp
|
||||
Qt::Core
|
||||
Qt::Gui
|
||||
Qt::Widgets
|
||||
@@ -196,6 +199,7 @@ target_include_directories(${PROJECT_NAME}
|
||||
src/Render/TextRender/Inc
|
||||
src/Core/Inc
|
||||
src/DAO/Inc
|
||||
src/Utils/Inc
|
||||
)
|
||||
|
||||
|
||||
@@ -262,7 +266,7 @@ if(PLAT STREQUAL "windows")
|
||||
set(WINDEPLOYQT_CMD "${WINDEPLOYQT_CMD} \"$<TARGET_FILE_DIR:${PROJECT_NAME}>\"")
|
||||
|
||||
# 添加自定义命令运行windeployqt
|
||||
# 最简单的版本 - 只传递目录
|
||||
# 只传递目录
|
||||
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
|
||||
COMMAND "${WINDEPLOYQT_EXE}" "$<TARGET_FILE_DIR:${PROJECT_NAME}>"
|
||||
COMMENT "Running windeployqt to deploy Qt dependencies..."
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
_**本项目为Yosuga.**_
|
||||
|
||||
本项目使用CMake构建,基于C++Qt6.6.3以及Live2D官方SDK(CubismSdkForNative-5-r.4.1)实现Live2D桌面宠物
|
||||
(本项目由Yosuga[Qt5] 发展更新而来,项目架构与代码都有所不同,最显著的特点是本项目支持多平台)
|
||||
(本项目由[Yosuga-qt5](https://github.com/Misakityan/Yosuga-qt5) 发展更新而来,项目架构与代码都有所不同,最显著的特点是本项目支持多平台)
|
||||
|
||||
环境为:
|
||||
|
||||
|
||||
@@ -0,0 +1,11 @@
|
||||
//
|
||||
// Created by misaki on 2026/1/24.
|
||||
//
|
||||
|
||||
#pragma once
|
||||
|
||||
/**
|
||||
* 客户端业务核心
|
||||
* 1. 处理来自服务端的数据,分发并执行
|
||||
* 2. 完成非阻塞的事件循环处理,构建业务状态机
|
||||
*/
|
||||
@@ -0,0 +1,5 @@
|
||||
//
|
||||
// Created by misaki on 2026/1/24.
|
||||
//
|
||||
|
||||
#include "AppCore.h"
|
||||
@@ -81,7 +81,7 @@ GLCore::GLCore(const int width, const int height, QWidget *parent)
|
||||
this->setMouseTracking(true);
|
||||
|
||||
// 连接一些必要的信号与槽
|
||||
connect(contextMenu, &Menu::closeMainWindow, this, &GLCore::closeGL);
|
||||
connect(contextMenu, &Menu::closeMainWindow, this, &GLCore::closeGL); // 关闭窗口信号
|
||||
|
||||
// 注册当前实例到中介类
|
||||
AppContext::RegisterGLCore(this);
|
||||
|
||||
@@ -44,7 +44,7 @@ NetworkDO::~NetworkDO()
|
||||
// 业务逻辑实现
|
||||
void NetworkDO::registerSender(SenderFunc sender)
|
||||
{
|
||||
QMutexLocker locker(&m_mutex); // 简单保护一下赋值
|
||||
QMutexLocker locker(&m_mutex); // 加个小锁,简单保护一下赋值
|
||||
m_sender = std::move(sender);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,189 @@
|
||||
//
|
||||
// Created by misaki on 2026/1/26.
|
||||
//
|
||||
|
||||
/**
|
||||
* 串口通信模块 —— 支持多设备并行 + JSON 协议
|
||||
* 每个 SerialPortClient 实例对应一个物理串口
|
||||
*/
|
||||
#pragma once
|
||||
#include <QSerialPort>
|
||||
#include <QSerialPortInfo>
|
||||
#include <QObject>
|
||||
#include <QThread>
|
||||
#include <QTimer>
|
||||
#include <QMutex>
|
||||
#include <QScopedPointer>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <utility>
|
||||
|
||||
/**
|
||||
* @brief 串口管理器(工作线程侧)
|
||||
* @details 负责单一串口的实际 I/O、参数配置、心跳、JSON 自动编解码
|
||||
* 与 WebSocketManager 对称设计,支持多实例并行
|
||||
*/
|
||||
class SerialPortManager final : public QObject {
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit SerialPortManager(QString deviceName, QObject *parent = nullptr);
|
||||
~SerialPortManager() override;
|
||||
|
||||
// 禁止拷贝
|
||||
SerialPortManager(const SerialPortManager&) = delete;
|
||||
SerialPortManager& operator=(const SerialPortManager&) = delete;
|
||||
|
||||
// 设备标识(用于多实例区分)
|
||||
[[nodiscard]] QString deviceName() const { return m_deviceName; }
|
||||
|
||||
// 配置结构体
|
||||
struct SerialPortConfig {
|
||||
QString portName; // 端口名: "COM3", "/dev/ttyUSB0"
|
||||
qint32 baudRate; // 波特率: 9600, 115200
|
||||
QSerialPort::DataBits dataBits;
|
||||
QSerialPort::Parity parity;
|
||||
QSerialPort::StopBits stopBits;
|
||||
QSerialPort::FlowControl flowControl;
|
||||
QByteArray heartbeatData; // 心跳包内容(为空则禁用)
|
||||
int maxJsonSize; // JSON 最大长度(防内存溢出)
|
||||
|
||||
// 默认配置:115200-N-8-1 + 无流控 + 心跳禁用 + JSON 上限 64KB
|
||||
explicit SerialPortConfig(
|
||||
QString port = "",
|
||||
qint32 baud = 115200,
|
||||
QSerialPort::DataBits db = QSerialPort::Data8,
|
||||
QSerialPort::Parity p = QSerialPort::NoParity,
|
||||
QSerialPort::StopBits sb = QSerialPort::OneStop,
|
||||
QSerialPort::FlowControl fc = QSerialPort::NoFlowControl,
|
||||
QByteArray hb = QByteArray(),
|
||||
int maxJson = 65536
|
||||
) : portName(std::move(port)), baudRate(baud), dataBits(db), parity(p),
|
||||
stopBits(sb), flowControl(fc), heartbeatData(std::move(hb)), maxJsonSize(maxJson) {}
|
||||
};
|
||||
|
||||
signals:
|
||||
// 状态与数据信号
|
||||
void opened(); // 串口成功打开
|
||||
void closed(); // 串口关闭
|
||||
void dataReceived(const QByteArray &data); // 原始二进制数据
|
||||
void textReceived(const QString &text); // 文本数据(UTF-8 解码)
|
||||
void hexReceived(const QString &hex); // 十六进制字符串: "AA BB CC"
|
||||
void jsonReceived(const QString &type, const QJsonObject &data); // JSON 已解析
|
||||
void error(const QString &errorMsg); // 错误信息
|
||||
void log(const QString &msg); // 运行日志
|
||||
void reconnecting(int attempt); // 自动重连中
|
||||
|
||||
public slots:
|
||||
bool setConfig(const SerialPortManager::SerialPortConfig& config); // 设置配置
|
||||
[[nodiscard]] SerialPortManager::SerialPortConfig currentConfig() const;
|
||||
|
||||
bool open(); // 打开串口
|
||||
void close(); // 关闭串口
|
||||
|
||||
// 多层次发送接口(JSON、文本、HEX、原始二进制)
|
||||
void sendJson(const QString &type, const QJsonObject &data); // 发送 JSON(自动封装)
|
||||
void sendText(const QString &text); // 发送文本(UTF-8)
|
||||
void sendHex(const QString &hex); // 发送十六进制: "12 AB CD"
|
||||
void sendRaw(const QByteArray &data); // 发送原始二进制(重命名为 sendRaw 更清晰)
|
||||
|
||||
void setAutoReconnect(bool enabled); // 是否开启自动重连
|
||||
void setHeartbeatInterval(int msecs); // 心跳间隔(毫秒)
|
||||
|
||||
[[nodiscard]] bool isOpen() const; // 串口是否已打开
|
||||
|
||||
private slots:
|
||||
void onReadyRead(); // 串口有数据到达
|
||||
void onErrorOccurred(QSerialPort::SerialPortError error);
|
||||
void sendHeartbeat(); // 定时发送心跳
|
||||
void tryReconnect(); // 重连逻辑
|
||||
|
||||
void processCOBSBuffer(); // 尝试解析 COBS帧
|
||||
|
||||
private:
|
||||
QSerialPort *m_serial; /// 串口实例
|
||||
QString m_deviceName; /// 设备标识(如 "STM32_Master", "ESP32_Slave")
|
||||
SerialPortConfig m_config; /// 当前配置
|
||||
QTimer *m_heartbeatTimer; /// 心跳定时器
|
||||
QTimer *m_reconnectTimer; /// 重连定时器
|
||||
bool m_isAutoReconnect; /// 是否启用自动重连
|
||||
int m_reconnectAttempts; /// 重连尝试次数
|
||||
|
||||
QByteArray m_cobsBuffer; /// COBS 解码缓冲区
|
||||
bool m_cobsInFrame; /// 帧状态
|
||||
};
|
||||
|
||||
|
||||
/**
|
||||
* @brief 串口客户端(主线程接口层)
|
||||
* @details 每个实例对应一个物理串口,支持构造多个并行工作
|
||||
* 封装线程迁移、信号转发、生命周期管理
|
||||
*/
|
||||
class SerialPortClient final : public QObject {
|
||||
Q_OBJECT
|
||||
Q_DISABLE_COPY(SerialPortClient)
|
||||
|
||||
public:
|
||||
// 构造函数:deviceName 为设备标识,用于日志和多实例区分
|
||||
explicit SerialPortClient(const QString &deviceName, QObject *parent = nullptr);
|
||||
~SerialPortClient() override;
|
||||
|
||||
// 设备标识
|
||||
[[nodiscard]] QString deviceName() const { return m_deviceName; }
|
||||
|
||||
// 配置串口
|
||||
bool setConfiguration(const SerialPortManager::SerialPortConfig& config);
|
||||
|
||||
// 串口操作
|
||||
void open();
|
||||
void close();
|
||||
void reconnect();
|
||||
|
||||
// 多层次发送接口
|
||||
void sendJson(const QString &type, const QJsonObject &data);
|
||||
void sendText(const QString &text);
|
||||
void sendHex(const QString &hex);
|
||||
void sendRaw(const QByteArray &data);
|
||||
|
||||
// 状态查询
|
||||
[[nodiscard]] bool isOpen() const;
|
||||
[[nodiscard]] bool hasConfiguration() const { return !m_config.portName.isEmpty(); }
|
||||
[[nodiscard]] SerialPortManager::SerialPortConfig currentConfig() const { return m_config; }
|
||||
|
||||
// 高级功能
|
||||
void setAutoReconnect(bool enabled);
|
||||
void setHeartbeatInterval(int msecs);
|
||||
[[nodiscard]] static QStringList availablePorts(); // 枚举系统可用串口
|
||||
|
||||
signals:
|
||||
// 事件信号(与 SerialPortManager 一一对应,转发到主线程)
|
||||
void opened();
|
||||
void closed();
|
||||
void dataReceived(const QByteArray &data);
|
||||
void textReceived(const QString &text);
|
||||
void hexReceived(const QString &hex);
|
||||
void jsonReceived(const QString &type, const QJsonObject &data);
|
||||
void error(const QString &errorMsg);
|
||||
void log(const QString &msg);
|
||||
void reconnecting(int attempt);
|
||||
|
||||
// 配置变更
|
||||
void configurationChanged(const SerialPortManager::SerialPortConfig &oldConfig,
|
||||
const SerialPortManager::SerialPortConfig &newConfig);
|
||||
|
||||
private:
|
||||
// 内部信号(用于跨线程通信,对标 internal*)
|
||||
Q_SIGNAL void internalSetConfig(const SerialPortManager::SerialPortConfig& config);
|
||||
Q_SIGNAL void internalOpen();
|
||||
Q_SIGNAL void internalClose();
|
||||
Q_SIGNAL void internalSendJson(const QString &type, const QJsonObject &data);
|
||||
Q_SIGNAL void internalSendText(const QString &text);
|
||||
Q_SIGNAL void internalSendHex(const QString &hex);
|
||||
Q_SIGNAL void internalSendRaw(const QByteArray &data);
|
||||
Q_SIGNAL void internalSetAutoReconnect(bool enabled);
|
||||
Q_SIGNAL void internalSetHeartbeatInterval(int msecs);
|
||||
|
||||
QString m_deviceName; /// 设备标识
|
||||
QThread *m_workerThread; /// 工作线程
|
||||
SerialPortManager *m_serialManager; /// 管理器实例(工作线程侧)
|
||||
SerialPortManager::SerialPortConfig m_config; /// 配置缓存
|
||||
};
|
||||
@@ -0,0 +1,583 @@
|
||||
//
|
||||
// Created by misaki on 2026/1/26.
|
||||
//
|
||||
|
||||
#include "serialportmanager.h"
|
||||
#include <QDebug>
|
||||
#include <QMutexLocker>
|
||||
#include <QJsonDocument>
|
||||
#include <utility>
|
||||
#include "cobs.hpp"
|
||||
|
||||
/// SerialPortManager
|
||||
SerialPortManager::SerialPortManager(QString deviceName, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_serial(new QSerialPort(this))
|
||||
, m_deviceName(std::move(deviceName))
|
||||
, m_heartbeatTimer(new QTimer(this))
|
||||
, m_reconnectTimer(new QTimer(this))
|
||||
, m_isAutoReconnect(false)
|
||||
, m_reconnectAttempts(0)
|
||||
, m_cobsBuffer()
|
||||
, m_cobsInFrame(false)
|
||||
{
|
||||
// 心跳定时器配置(默认 5 秒)
|
||||
m_heartbeatTimer->setInterval(5000);
|
||||
connect(m_heartbeatTimer, &QTimer::timeout,
|
||||
this, &SerialPortManager::sendHeartbeat);
|
||||
|
||||
// 重连定时器(单次触发)
|
||||
m_reconnectTimer->setSingleShot(true);
|
||||
connect(m_reconnectTimer, &QTimer::timeout,
|
||||
this, &SerialPortManager::tryReconnect);
|
||||
|
||||
// 串口信号连接
|
||||
connect(m_serial, &QSerialPort::readyRead,
|
||||
this, &SerialPortManager::onReadyRead);
|
||||
connect(m_serial, QOverload<QSerialPort::SerialPortError>::of(&QSerialPort::errorOccurred),
|
||||
this, &SerialPortManager::onErrorOccurred);
|
||||
|
||||
emit log(QString("[%1] SerialPortManager initialized in worker thread").arg(m_deviceName));
|
||||
}
|
||||
|
||||
SerialPortManager::~SerialPortManager() {
|
||||
if (m_serial->isOpen()) {
|
||||
m_serial->close();
|
||||
}
|
||||
}
|
||||
|
||||
bool SerialPortManager::setConfig(const SerialPortConfig &config) {
|
||||
if (m_serial->isOpen()) {
|
||||
emit error(QString("[%1] Cannot change config while port is open. Close it first.").arg(m_deviceName));
|
||||
return false;
|
||||
}
|
||||
|
||||
if (config.portName.isEmpty()) {
|
||||
emit error(QString("[%1] Port name cannot be empty!").arg(m_deviceName));
|
||||
return false;
|
||||
}
|
||||
|
||||
m_config = config;
|
||||
emit log(QString("[%1] Config updated: %2 @ %3 bps, JSON max: %4 bytes")
|
||||
.arg(m_deviceName, config.portName).arg(config.baudRate).arg(config.maxJsonSize));
|
||||
return true;
|
||||
}
|
||||
|
||||
SerialPortManager::SerialPortConfig SerialPortManager::currentConfig() const {
|
||||
return m_config;
|
||||
}
|
||||
|
||||
bool SerialPortManager::open() {
|
||||
if (m_serial->isOpen()) {
|
||||
emit log(QString("[%1] Port already opened, closing first...").arg(m_deviceName));
|
||||
m_serial->close();
|
||||
}
|
||||
// 配置串口参数
|
||||
m_serial->setPortName(m_config.portName);
|
||||
m_serial->setBaudRate(m_config.baudRate);
|
||||
m_serial->setDataBits(m_config.dataBits);
|
||||
m_serial->setParity(m_config.parity);
|
||||
m_serial->setStopBits(m_config.stopBits);
|
||||
m_serial->setFlowControl(m_config.flowControl);
|
||||
|
||||
emit log(QString("[%1] Opening %2...").arg(m_deviceName, m_config.portName));
|
||||
|
||||
if (!m_serial->open(QIODevice::ReadWrite)) {
|
||||
QString errMsg = QString("[%1] Failed to open %2: %3")
|
||||
.arg(m_deviceName, m_config.portName, m_serial->errorString());
|
||||
emit error(errMsg);
|
||||
if (m_isAutoReconnect) {
|
||||
m_reconnectTimer->start(3000);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
m_reconnectAttempts = 0;
|
||||
m_cobsBuffer.clear(); // 清空 COBS 缓冲区
|
||||
m_cobsInFrame = false; // 重置帧状态
|
||||
emit opened();
|
||||
emit log(QString("[%1] Serial port opened successfully").arg(m_deviceName));
|
||||
|
||||
// 启动心跳(如果有配置心跳包)
|
||||
if (!m_config.heartbeatData.isEmpty()) {
|
||||
m_heartbeatTimer->start();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SerialPortManager::close() {
|
||||
m_isAutoReconnect = false;
|
||||
m_heartbeatTimer->stop();
|
||||
m_reconnectTimer->stop();
|
||||
if (m_serial->isOpen()) {
|
||||
m_serial->close();
|
||||
m_cobsBuffer.clear(); // 清空 COBS 缓冲区
|
||||
m_cobsInFrame = false; // 重置帧状态
|
||||
emit closed();
|
||||
emit log(QString("[%1] Serial port closed").arg(m_deviceName));
|
||||
}
|
||||
}
|
||||
|
||||
void SerialPortManager::sendJson(const QString &type, const QJsonObject &data) {
|
||||
if (!m_serial->isOpen()) {
|
||||
emit error(QString("[%1] Cannot send JSON: serial port not opened").arg(m_deviceName));
|
||||
return;
|
||||
}
|
||||
|
||||
// 封装成 { "type": "...", "data": {...}, "timestamp": 123456 }
|
||||
QJsonObject wrapper;
|
||||
wrapper["type"] = type;
|
||||
wrapper["data"] = data;
|
||||
wrapper["timestamp"] = QDateTime::currentMSecsSinceEpoch();
|
||||
|
||||
QJsonDocument doc(wrapper);
|
||||
QByteArray jsonBytes = doc.toJson(QJsonDocument::Compact);
|
||||
// COBS 编码
|
||||
std::vector<uint8_t> encoded;
|
||||
auto result = cobs::encode(encoded, std::span<const uint8_t>(
|
||||
reinterpret_cast<const uint8_t*>(jsonBytes.constData()), jsonBytes.size()
|
||||
));
|
||||
if (result.status != cobs::Status::OK) { // 编码失败
|
||||
emit error(QString("[%1] COBS encode failed: status=%2").arg(m_deviceName).arg(static_cast<int>(result.status)));
|
||||
return;
|
||||
}
|
||||
|
||||
// 发送编码数据 + 0x00
|
||||
const qint64 written = m_serial->write(reinterpret_cast<const char*>(encoded.data()), static_cast<qint64>(encoded.size()));
|
||||
if (written == -1) { // 写入失败
|
||||
emit error(QString("[%1] Write error: %2").arg(m_deviceName, m_serial->errorString()));
|
||||
} else { // 发送成功
|
||||
m_serial->flush();
|
||||
m_serial->write("\0", 1); // 帧结束符
|
||||
emit log(QString("[%1] Sent JSON: type=%2, %3 bytes encoded").arg(m_deviceName, type).arg(encoded.size() + 1));
|
||||
}
|
||||
}
|
||||
|
||||
void SerialPortManager::sendText(const QString &text) {
|
||||
if (!m_serial->isOpen()) {
|
||||
emit error(QString("[%1] Cannot send text: serial port not opened").arg(m_deviceName));
|
||||
return;
|
||||
}
|
||||
|
||||
QByteArray data = text.toUtf8();
|
||||
qint64 written = m_serial->write(data);
|
||||
if (written == -1) {
|
||||
emit error(QString("[%1] Write error: %2").arg(m_deviceName, m_serial->errorString()));
|
||||
} else if (written != data.size()) {
|
||||
emit error(QString("[%1] Incomplete write: %2/%3 bytes sent").arg(m_deviceName).arg(written).arg(data.size()));
|
||||
} else {
|
||||
m_serial->flush();
|
||||
emit log(QString("[%1] Sent text: %2 bytes").arg(m_deviceName).arg(written));
|
||||
}
|
||||
}
|
||||
|
||||
void SerialPortManager::sendHex(const QString &hex) {
|
||||
if (!m_serial->isOpen()) {
|
||||
emit error(QString("[%1] Cannot send hex: serial port not opened").arg(m_deviceName));
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析十六进制字符串: "AA BB 1A" → QByteArray
|
||||
QString cleaned = hex.simplified().remove(' ');
|
||||
QByteArray data = QByteArray::fromHex(cleaned.toUtf8());
|
||||
|
||||
if (data.isEmpty() && !cleaned.isEmpty()) {
|
||||
emit error(QString("[%1] Invalid hex format. Use: 'AA BB CC' or 'AABBCC'").arg(m_deviceName));
|
||||
return;
|
||||
}
|
||||
|
||||
qint64 written = m_serial->write(data);
|
||||
if (written == -1) {
|
||||
emit error(QString("[%1] Write error: %2").arg(m_deviceName, m_serial->errorString()));
|
||||
} else {
|
||||
m_serial->flush();
|
||||
emit log(QString("[%1] Sent hex: %2").arg(m_deviceName, cleaned.left(50)));
|
||||
}
|
||||
}
|
||||
|
||||
void SerialPortManager::sendRaw(const QByteArray &data) {
|
||||
if (!m_serial->isOpen()) {
|
||||
emit error(QString("[%1] Cannot send raw: serial port not opened").arg(m_deviceName));
|
||||
return;
|
||||
}
|
||||
|
||||
qint64 written = m_serial->write(data);
|
||||
if (written == -1) {
|
||||
emit error(QString("[%1] Write error: %2").arg(m_deviceName, m_serial->errorString()));
|
||||
} else {
|
||||
m_serial->flush();
|
||||
emit log(QString("[%1] Sent raw: %2 bytes").arg(m_deviceName).arg(written));
|
||||
}
|
||||
}
|
||||
|
||||
bool SerialPortManager::isOpen() const {
|
||||
return m_serial->isOpen();
|
||||
}
|
||||
|
||||
void SerialPortManager::setAutoReconnect(bool enabled) {
|
||||
m_isAutoReconnect = enabled;
|
||||
if (!enabled) {
|
||||
m_reconnectTimer->stop();
|
||||
}
|
||||
emit log(QString("[%1] Auto reconnect %2").arg(m_deviceName, enabled ? "enabled" : "disabled"));
|
||||
}
|
||||
|
||||
void SerialPortManager::setHeartbeatInterval(int msecs) {
|
||||
m_heartbeatTimer->setInterval(msecs);
|
||||
emit log(QString("[%1] Heartbeat interval: %2 ms").arg(m_deviceName).arg(msecs));
|
||||
}
|
||||
|
||||
void SerialPortManager::onReadyRead() {
|
||||
// 读取所有可用数据到 COBS 缓冲区
|
||||
QByteArray chunk = m_serial->readAll(); // 读取原始字节流
|
||||
m_cobsBuffer.append(chunk); // 追加到cobs缓冲区
|
||||
|
||||
// 触发原始数据信号
|
||||
emit dataReceived(chunk);
|
||||
emit textReceived(QString::fromUtf8(chunk)); // 尝试 UTF-8 解码
|
||||
emit hexReceived(chunk.toHex(' ').toUpper()); // 十六进制表示
|
||||
|
||||
// 处理 COBS 帧
|
||||
processCOBSBuffer();
|
||||
}
|
||||
|
||||
void SerialPortManager::processCOBSBuffer() {
|
||||
while (true) {
|
||||
// 查找帧结束符 0x00
|
||||
int zeroPos = m_cobsBuffer.indexOf('\0');
|
||||
// 未找到完整帧,继续等待
|
||||
if (zeroPos == -1) {
|
||||
m_cobsInFrame = true;
|
||||
// 防溢出
|
||||
if (m_cobsBuffer.size() > m_config.maxJsonSize * 2) {
|
||||
emit error(QString("[%1] COBS buffer overflow, clearing").arg(m_deviceName));
|
||||
m_cobsBuffer.clear();
|
||||
m_cobsInFrame = false;
|
||||
}
|
||||
return;
|
||||
}
|
||||
// 提取编码帧(不含 0x00)
|
||||
QByteArray encodedFrame = m_cobsBuffer.left(zeroPos);
|
||||
m_cobsBuffer.remove(0, zeroPos + 1); // 移除结束符
|
||||
m_cobsInFrame = false;
|
||||
// 跳过空帧
|
||||
if (encodedFrame.isEmpty()) {
|
||||
emit log(QString("[%1] Empty COBS frame, skipped").arg(m_deviceName));
|
||||
continue;
|
||||
}
|
||||
// COBS 解码
|
||||
std::vector<uint8_t> decoded;
|
||||
auto result = cobs::decode(decoded, std::span<const uint8_t>(
|
||||
reinterpret_cast<const uint8_t*>(encodedFrame.constData()), encodedFrame.size()
|
||||
));
|
||||
|
||||
if (result.status != cobs::Status::OK) { // 解码失败
|
||||
emit error(QString("[%1] COBS decode failed: status=%2").arg(m_deviceName).arg(static_cast<int>(result.status)));
|
||||
continue;
|
||||
}
|
||||
// 解析 JSON
|
||||
QByteArray jsonData(reinterpret_cast<const char*>(decoded.data()), static_cast<qint64>(decoded.size()));
|
||||
QJsonParseError parseError;
|
||||
QJsonDocument doc = QJsonDocument::fromJson(jsonData, &parseError);
|
||||
|
||||
if (parseError.error != QJsonParseError::NoError) {
|
||||
emit error(QString("[%1] JSON parse error: %2").arg(m_deviceName, parseError.errorString()));
|
||||
continue;
|
||||
}
|
||||
if (!doc.isObject()) {
|
||||
emit error(QString("[%1] JSON is not an object").arg(m_deviceName));
|
||||
continue;
|
||||
}
|
||||
|
||||
QJsonObject obj = doc.object();
|
||||
const QString type = obj.value("type").toString();
|
||||
const QJsonObject data = obj.value("data").toObject();
|
||||
|
||||
if (type.isEmpty()) {
|
||||
emit error(QString("[%1] JSON missing 'type' field").arg(m_deviceName));
|
||||
continue;
|
||||
}
|
||||
|
||||
// 成功,向上层交付
|
||||
emit jsonReceived(type, data);
|
||||
emit log(QString("[%1] JSON delivered: type=%2, size=%3").arg(m_deviceName, type).arg(jsonData.size()));
|
||||
}
|
||||
}
|
||||
|
||||
void SerialPortManager::onErrorOccurred(QSerialPort::SerialPortError error) {
|
||||
if (error == QSerialPort::NoError) return;
|
||||
|
||||
QString errorMsg;
|
||||
switch (error) {
|
||||
case QSerialPort::DeviceNotFoundError:
|
||||
errorMsg = "Device not found";
|
||||
break;
|
||||
case QSerialPort::PermissionError:
|
||||
errorMsg = "Permission denied. Check udev rules or run with sudo";
|
||||
break;
|
||||
case QSerialPort::OpenError:
|
||||
errorMsg = "Already opened or system error";
|
||||
break;
|
||||
case QSerialPort::WriteError:
|
||||
errorMsg = "Write error";
|
||||
break;
|
||||
case QSerialPort::ReadError:
|
||||
errorMsg = "Read error";
|
||||
break;
|
||||
case QSerialPort::ResourceError:
|
||||
errorMsg = "Resource error: device removed or I/O error";
|
||||
// 设备被拔插,触发重连
|
||||
if (m_isAutoReconnect) {
|
||||
m_reconnectTimer->start(2000);
|
||||
}
|
||||
break;
|
||||
case QSerialPort::UnsupportedOperationError:
|
||||
errorMsg = "Unsupported operation";
|
||||
break;
|
||||
case QSerialPort::TimeoutError:
|
||||
errorMsg = "Operation timed out";
|
||||
break;
|
||||
case QSerialPort::NotOpenError:
|
||||
errorMsg = "Device not open";
|
||||
break;
|
||||
default:
|
||||
errorMsg = m_serial->errorString();
|
||||
}
|
||||
|
||||
emit this->error(QString("[%1] Serial error: %2").arg(m_deviceName, errorMsg));
|
||||
}
|
||||
|
||||
void SerialPortManager::sendHeartbeat() {
|
||||
if (!m_serial->isOpen() || m_config.heartbeatData.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
|
||||
qint64 written = m_serial->write(m_config.heartbeatData);
|
||||
if (written == -1) {
|
||||
emit error(QString("[%1] Heartbeat write failed: %2").arg(m_deviceName, m_serial->errorString()));
|
||||
} else {
|
||||
emit log(QString("[%1] Heartbeat sent").arg(m_deviceName));
|
||||
}
|
||||
}
|
||||
|
||||
void SerialPortManager::tryReconnect() {
|
||||
if (!m_isAutoReconnect) return;
|
||||
|
||||
m_reconnectAttempts++;
|
||||
emit reconnecting(m_reconnectAttempts);
|
||||
emit log(QString("[%1] Reconnecting... (attempt %2)").arg(m_deviceName).arg(m_reconnectAttempts));
|
||||
|
||||
// 直接调用 open()
|
||||
open();
|
||||
}
|
||||
|
||||
/// SerialPortClient
|
||||
SerialPortClient::SerialPortClient(const QString &deviceName, QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_deviceName(deviceName)
|
||||
, m_workerThread(new QThread(this))
|
||||
, m_serialManager(new SerialPortManager(deviceName))
|
||||
, m_config()
|
||||
{
|
||||
// 命名线程,方便调试
|
||||
m_workerThread->setObjectName(QString("SerialPortThread_%1").arg(deviceName));
|
||||
|
||||
// 将 Manager 移到工作线程
|
||||
m_serialManager->moveToThread(m_workerThread);
|
||||
|
||||
// 线程结束时清理 Manager
|
||||
connect(m_workerThread, &QThread::finished,
|
||||
m_serialManager, &QObject::deleteLater);
|
||||
|
||||
// 信号转发:Manager → Client(主线程)
|
||||
connect(m_serialManager, &SerialPortManager::opened,
|
||||
this, &SerialPortClient::opened);
|
||||
connect(m_serialManager, &SerialPortManager::closed,
|
||||
this, &SerialPortClient::closed);
|
||||
connect(m_serialManager, &SerialPortManager::dataReceived,
|
||||
this, &SerialPortClient::dataReceived);
|
||||
connect(m_serialManager, &SerialPortManager::textReceived,
|
||||
this, &SerialPortClient::textReceived);
|
||||
connect(m_serialManager, &SerialPortManager::hexReceived,
|
||||
this, &SerialPortClient::hexReceived);
|
||||
connect(m_serialManager, &SerialPortManager::jsonReceived,
|
||||
this, &SerialPortClient::jsonReceived);
|
||||
connect(m_serialManager, &SerialPortManager::error,
|
||||
this, &SerialPortClient::error);
|
||||
connect(m_serialManager, &SerialPortManager::log,
|
||||
this, &SerialPortClient::log);
|
||||
connect(m_serialManager, &SerialPortManager::reconnecting,
|
||||
this, &SerialPortClient::reconnecting);
|
||||
|
||||
// 内部信号:Client → Manager(跨线程调用)
|
||||
connect(this, &SerialPortClient::internalSetConfig,
|
||||
m_serialManager, [this](const SerialPortManager::SerialPortConfig& cfg) {
|
||||
bool ok = m_serialManager->setConfig(cfg);
|
||||
if (!ok) emit error(QString("[%1] Failed to set config").arg(m_deviceName));
|
||||
}, Qt::QueuedConnection);
|
||||
|
||||
connect(this, &SerialPortClient::internalOpen,
|
||||
m_serialManager, &SerialPortManager::open,
|
||||
Qt::QueuedConnection);
|
||||
|
||||
connect(this, &SerialPortClient::internalClose,
|
||||
m_serialManager, &SerialPortManager::close,
|
||||
Qt::QueuedConnection);
|
||||
|
||||
connect(this, &SerialPortClient::internalSendJson,
|
||||
m_serialManager, &SerialPortManager::sendJson,
|
||||
Qt::QueuedConnection);
|
||||
|
||||
connect(this, &SerialPortClient::internalSendText,
|
||||
m_serialManager, &SerialPortManager::sendText,
|
||||
Qt::QueuedConnection);
|
||||
|
||||
connect(this, &SerialPortClient::internalSendHex,
|
||||
m_serialManager, &SerialPortManager::sendHex,
|
||||
Qt::QueuedConnection);
|
||||
|
||||
connect(this, &SerialPortClient::internalSendRaw,
|
||||
m_serialManager, &SerialPortManager::sendRaw,
|
||||
Qt::QueuedConnection);
|
||||
|
||||
connect(this, &SerialPortClient::internalSetAutoReconnect,
|
||||
m_serialManager, &SerialPortManager::setAutoReconnect,
|
||||
Qt::QueuedConnection);
|
||||
|
||||
connect(this, &SerialPortClient::internalSetHeartbeatInterval,
|
||||
m_serialManager, &SerialPortManager::setHeartbeatInterval,
|
||||
Qt::QueuedConnection);
|
||||
|
||||
// 启动工作线程
|
||||
m_workerThread->start();
|
||||
|
||||
emit log(QString("[%1] SerialPortClient initialized, worker thread started").arg(m_deviceName));
|
||||
}
|
||||
|
||||
SerialPortClient::~SerialPortClient() {
|
||||
emit log(QString("[%1] Shutting down SerialPortClient...").arg(m_deviceName));
|
||||
|
||||
// 关闭串口
|
||||
close();
|
||||
|
||||
// 优雅退出工作线程
|
||||
if (m_workerThread && m_workerThread->isRunning()) {
|
||||
m_workerThread->quit();
|
||||
if (!m_workerThread->wait(3000)) {
|
||||
m_workerThread->terminate();
|
||||
m_workerThread->wait();
|
||||
}
|
||||
delete m_workerThread;
|
||||
m_workerThread = nullptr;
|
||||
}
|
||||
}
|
||||
|
||||
bool SerialPortClient::setConfiguration(const SerialPortManager::SerialPortConfig &config) {
|
||||
if (config.portName.isEmpty()) {
|
||||
emit error(QString("[%1] Invalid config: port name empty").arg(m_deviceName));
|
||||
return false;
|
||||
}
|
||||
|
||||
auto oldConfig = m_config;
|
||||
m_config = config;
|
||||
|
||||
// 跨线程设置
|
||||
emit internalSetConfig(config);
|
||||
|
||||
// 通知配置变更
|
||||
if (oldConfig.portName != config.portName || oldConfig.baudRate != config.baudRate) {
|
||||
emit configurationChanged(oldConfig, config);
|
||||
}
|
||||
|
||||
emit log(QString("[%1] Configuration updated: %2 @ %3 bps")
|
||||
.arg(m_deviceName, config.portName).arg(config.baudRate));
|
||||
return true;
|
||||
}
|
||||
|
||||
void SerialPortClient::open() {
|
||||
if (!hasConfiguration()) {
|
||||
emit error(QString("[%1] Not configured. Call setConfiguration() first.").arg(m_deviceName));
|
||||
return;
|
||||
}
|
||||
emit log(QString("[%1] Opening serial port: %2").arg(m_deviceName, m_config.portName));
|
||||
emit internalOpen();
|
||||
}
|
||||
|
||||
void SerialPortClient::close() {
|
||||
emit log(QString("[%1] Closing serial port...").arg(m_deviceName));
|
||||
emit internalClose();
|
||||
}
|
||||
|
||||
void SerialPortClient::reconnect() {
|
||||
if (!hasConfiguration()) {
|
||||
emit error(QString("[%1] No configuration. Cannot reconnect.").arg(m_deviceName));
|
||||
return;
|
||||
}
|
||||
emit log(QString("[%1] Attempting to reconnect...").arg(m_deviceName));
|
||||
close();
|
||||
QTimer::singleShot(100, this, [this]() { open(); });
|
||||
}
|
||||
|
||||
void SerialPortClient::sendJson(const QString &type, const QJsonObject &data) {
|
||||
if (!isOpen()) {
|
||||
emit error(QString("[%1] Cannot send JSON: serial port not opened").arg(m_deviceName));
|
||||
return;
|
||||
}
|
||||
emit internalSendJson(type, data);
|
||||
}
|
||||
|
||||
void SerialPortClient::sendText(const QString &text) {
|
||||
if (!isOpen()) {
|
||||
emit error(QString("[%1] Cannot send text: serial port not opened").arg(m_deviceName));
|
||||
return;
|
||||
}
|
||||
emit internalSendText(text);
|
||||
}
|
||||
|
||||
void SerialPortClient::sendHex(const QString &hex) {
|
||||
if (!isOpen()) {
|
||||
emit error(QString("[%1] Cannot send hex: serial port not opened").arg(m_deviceName));
|
||||
return;
|
||||
}
|
||||
emit internalSendHex(hex);
|
||||
}
|
||||
|
||||
void SerialPortClient::sendRaw(const QByteArray &data) {
|
||||
if (!isOpen()) {
|
||||
emit error(QString("[%1] Cannot send raw: serial port not opened").arg(m_deviceName));
|
||||
return;
|
||||
}
|
||||
emit internalSendRaw(data);
|
||||
}
|
||||
|
||||
bool SerialPortClient::isOpen() const {
|
||||
if (m_serialManager) {
|
||||
bool opened = false;
|
||||
QMetaObject::invokeMethod(const_cast<SerialPortManager*>(m_serialManager),
|
||||
[&opened, mgr = m_serialManager]() {
|
||||
opened = mgr->isOpen();
|
||||
},
|
||||
Qt::BlockingQueuedConnection);
|
||||
return opened;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
void SerialPortClient::setAutoReconnect(bool enabled) {
|
||||
emit log(QString("[%1] Auto reconnect %2").arg(m_deviceName, enabled ? "enabled" : "disabled"));
|
||||
emit internalSetAutoReconnect(enabled);
|
||||
}
|
||||
|
||||
void SerialPortClient::setHeartbeatInterval(int msecs) {
|
||||
emit log(QString("[%1] Heartbeat interval: %2 ms").arg(m_deviceName).arg(msecs));
|
||||
emit internalSetHeartbeatInterval(msecs);
|
||||
}
|
||||
|
||||
QStringList SerialPortClient::availablePorts() {
|
||||
QStringList ports;
|
||||
const auto portList = QSerialPortInfo::availablePorts();
|
||||
for (const QSerialPortInfo &info : portList) {
|
||||
QString desc = QString("%1 (%2)").arg(info.portName(), info.description());
|
||||
ports << desc;
|
||||
}
|
||||
return ports;
|
||||
}
|
||||
@@ -2,8 +2,7 @@
|
||||
// Created by Administrator on 2025/1/21.
|
||||
//
|
||||
|
||||
#ifndef AIRI_DESKTOPGRIL_SETTING_H
|
||||
#define AIRI_DESKTOPGRIL_SETTING_H
|
||||
#pragma once
|
||||
|
||||
#include <ElaWidget.h>
|
||||
#include <ElaWindow.h>
|
||||
@@ -64,5 +63,3 @@ private:
|
||||
|
||||
};
|
||||
|
||||
|
||||
#endif //AIRI_DESKTOPGRIL_SETTING_H
|
||||
|
||||
@@ -2,8 +2,7 @@
|
||||
// Created by Administrator on 2025/3/2.
|
||||
//
|
||||
|
||||
#ifndef AIRI_DESKTOPGRIL_UISETTING_H
|
||||
#define AIRI_DESKTOPGRIL_UISETTING_H
|
||||
#pragma once
|
||||
|
||||
#include "BasePage.h"
|
||||
class ElaRadioButton;
|
||||
@@ -25,5 +24,3 @@ private:
|
||||
ElaRadioButton* _maximumButton = nullptr;
|
||||
ElaRadioButton* _autoButton = nullptr;
|
||||
};
|
||||
|
||||
#endif //AIRI_DESKTOPGRIL_UISETTING_H
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
//
|
||||
// Created by misaki on 2026/1/26.
|
||||
//
|
||||
|
||||
/**
|
||||
* cobs.hpp
|
||||
* 所谓COBS,即Consistent Overhead Byte Stuffing(持续开销字节填充)
|
||||
* 是一种将字节包编码成不包含值为零的字节(0x00)形式的方法。
|
||||
* 输入的字节包可以包含从 0x00 到 0xFF 的全部范围内的字节。
|
||||
* COBS 编码的数据包保证生成字节范围 0x01 到 0xFF 的数据包。
|
||||
* 因此,在通信协议中,数据包边界可以用 0x00 字节可靠地界定。
|
||||
*
|
||||
* 在Yosuga项目当中,COBS编码被用于解决Yosuga与嵌入式设备使用串口收发数据时出现的粘包问题。
|
||||
* 之所以使用COBS编码而不是常用的字符填充法,这是因为字符填充法会使得数据包的大小无法确定,并且往往会使得数据包变得更大。
|
||||
*
|
||||
* 本模块为COBS的C++实现,而在Yosuga_embedded当中,则使用了cobs的C实现。
|
||||
*/
|
||||
#pragma once
|
||||
#include <cstdint>
|
||||
#include <cstddef>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
#include <string>
|
||||
|
||||
namespace cobs {
|
||||
|
||||
// 状态码
|
||||
enum class [[nodiscard]] Status : uint8_t {
|
||||
OK = 0x00,
|
||||
NULL_POINTER = 0x01,
|
||||
OUT_BUFFER_OVERFLOW = 0x02,
|
||||
ZERO_BYTE_IN_INPUT = 0x04, // 仅 decode
|
||||
INPUT_TOO_SHORT = 0x08 // 仅 decode
|
||||
};
|
||||
|
||||
// 结果结构体
|
||||
struct [[nodiscard]] EncodeResult {
|
||||
size_t out_len = 0;
|
||||
Status status = Status::OK;
|
||||
};
|
||||
|
||||
struct [[nodiscard]] DecodeResult {
|
||||
size_t out_len = 0;
|
||||
Status status = Status::OK;
|
||||
};
|
||||
|
||||
// 缓冲区大小计算
|
||||
constexpr size_t encode_dst_len_max(const size_t src_len) noexcept {
|
||||
return (src_len == 0) ? 1 : (src_len + (src_len + 253) / 254);
|
||||
}
|
||||
|
||||
constexpr size_t decode_dst_len_max(const size_t src_len) noexcept {
|
||||
return (src_len == 0) ? 0 : (src_len - 1);
|
||||
}
|
||||
|
||||
constexpr size_t encode_src_offset(const size_t src_len) noexcept {
|
||||
return (src_len + 253) / 254;
|
||||
}
|
||||
|
||||
// 底层核心实现
|
||||
inline EncodeResult encode_core(std::span<uint8_t> dst, const std::span<const uint8_t> src) noexcept {
|
||||
EncodeResult result;
|
||||
if (dst.empty() || src.empty()) {
|
||||
result.status = Status::NULL_POINTER;
|
||||
return result;
|
||||
}
|
||||
|
||||
const uint8_t* src_read_ptr = src.data();
|
||||
const uint8_t* src_end_ptr = src_read_ptr + src.size();
|
||||
uint8_t* dst_start_ptr = dst.data();
|
||||
const uint8_t* dst_end_ptr = dst_start_ptr + dst.size();
|
||||
uint8_t* dst_code_write_ptr = dst_start_ptr;
|
||||
uint8_t* dst_write_ptr = dst_code_write_ptr + 1;
|
||||
uint8_t search_len = 1;
|
||||
|
||||
if (src.empty()) {
|
||||
*dst_code_write_ptr = search_len;
|
||||
result.out_len = 1;
|
||||
return result;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
if (dst_write_ptr >= dst_end_ptr) {
|
||||
result.status = Status::OUT_BUFFER_OVERFLOW;
|
||||
break;
|
||||
}
|
||||
|
||||
const uint8_t src_byte = *src_read_ptr++;
|
||||
if (src_byte == 0) {
|
||||
*dst_code_write_ptr = search_len;
|
||||
dst_code_write_ptr = dst_write_ptr++;
|
||||
search_len = 1;
|
||||
if (src_read_ptr >= src_end_ptr) break;
|
||||
} else {
|
||||
*dst_write_ptr++ = src_byte;
|
||||
search_len++;
|
||||
if (src_read_ptr >= src_end_ptr) break;
|
||||
if (search_len == 0xFF) {
|
||||
*dst_code_write_ptr = search_len;
|
||||
dst_code_write_ptr = dst_write_ptr++;
|
||||
search_len = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (dst_code_write_ptr >= dst_end_ptr) {
|
||||
result.status = Status::OUT_BUFFER_OVERFLOW;
|
||||
} else {
|
||||
*dst_code_write_ptr = search_len;
|
||||
}
|
||||
|
||||
result.out_len = static_cast<size_t>(dst_write_ptr - dst_start_ptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
inline DecodeResult decode_core(std::span<uint8_t> dst, const std::span<const uint8_t> src) noexcept {
|
||||
DecodeResult result;
|
||||
if (dst.empty() || src.empty()) {
|
||||
result.status = Status::NULL_POINTER;
|
||||
return result;
|
||||
}
|
||||
|
||||
const uint8_t* src_read_ptr = src.data();
|
||||
const uint8_t* src_end_ptr = src_read_ptr + src.size();
|
||||
uint8_t* dst_start_ptr = dst.data();
|
||||
uint8_t* dst_end_ptr = dst_start_ptr + dst.size();
|
||||
uint8_t* dst_write_ptr = dst_start_ptr;
|
||||
|
||||
if (src.empty()) {
|
||||
return result; // out_len = 0, status = OK
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
uint8_t len_code = *src_read_ptr++;
|
||||
if (len_code == 0) {
|
||||
result.status = Status::ZERO_BYTE_IN_INPUT;
|
||||
break;
|
||||
}
|
||||
len_code--;
|
||||
|
||||
auto remaining = static_cast<size_t>(src_end_ptr - src_read_ptr);
|
||||
if (len_code > remaining) {
|
||||
result.status = Status::INPUT_TOO_SHORT;
|
||||
len_code = static_cast<uint8_t>(remaining);
|
||||
}
|
||||
|
||||
remaining = static_cast<size_t>(dst_end_ptr - dst_write_ptr);
|
||||
if (len_code > remaining) {
|
||||
result.status = Status::OUT_BUFFER_OVERFLOW;
|
||||
len_code = static_cast<uint8_t>(remaining);
|
||||
}
|
||||
|
||||
for (uint8_t i = len_code; i != 0; i--) {
|
||||
const uint8_t src_byte = *src_read_ptr++;
|
||||
if (src_byte == 0) {
|
||||
result.status = Status::ZERO_BYTE_IN_INPUT;
|
||||
}
|
||||
*dst_write_ptr++ = src_byte;
|
||||
}
|
||||
|
||||
if (src_read_ptr >= src_end_ptr) break;
|
||||
if (len_code != 0xFE) {
|
||||
if (dst_write_ptr >= dst_end_ptr) {
|
||||
result.status = Status::OUT_BUFFER_OVERFLOW;
|
||||
break;
|
||||
}
|
||||
*dst_write_ptr++ = 0;
|
||||
}
|
||||
}
|
||||
|
||||
result.out_len = static_cast<size_t>(dst_write_ptr - dst_start_ptr);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 便捷接口(std::vector)
|
||||
inline EncodeResult encode(std::vector<uint8_t>& dst, const std::span<const uint8_t> src) noexcept {
|
||||
dst.resize(encode_dst_len_max(src.size()));
|
||||
const auto result = encode_core(dst, src);
|
||||
dst.resize(result.out_len);
|
||||
return result;
|
||||
}
|
||||
|
||||
inline DecodeResult decode(std::vector<uint8_t>& dst, const std::span<const uint8_t> src) noexcept {
|
||||
dst.resize(decode_dst_len_max(src.size()));
|
||||
const auto result = decode_core(dst, src);
|
||||
dst.resize(result.out_len);
|
||||
return result;
|
||||
}
|
||||
|
||||
// 便捷接口(std::string)
|
||||
inline EncodeResult encode(std::string& dst, const std::string_view src) noexcept {
|
||||
dst.resize(encode_dst_len_max(src.size()));
|
||||
const auto result = encode_core(
|
||||
std::span<uint8_t>(reinterpret_cast<uint8_t*>(dst.data()), dst.size()),
|
||||
std::span<const uint8_t>(reinterpret_cast<const uint8_t*>(src.data()), src.size())
|
||||
);
|
||||
dst.resize(result.out_len);
|
||||
return result;
|
||||
}
|
||||
|
||||
inline DecodeResult decode(std::string& dst, const std::span<const uint8_t> src) noexcept {
|
||||
std::vector<uint8_t> temp;
|
||||
const auto result = decode(temp, src);
|
||||
if (result.status == Status::OK) {
|
||||
dst.assign(reinterpret_cast<const char*>(temp.data()), temp.size());
|
||||
}
|
||||
return {result.out_len, result.status};
|
||||
}
|
||||
|
||||
// 类型安全辅助函数
|
||||
template <typename T>
|
||||
requires std::is_trivially_copyable_v<T>
|
||||
inline EncodeResult encode(std::vector<uint8_t>& dst, const T& obj) noexcept {
|
||||
return encode(dst, std::span<const uint8_t>(
|
||||
reinterpret_cast<const uint8_t*>(&obj), sizeof(T)
|
||||
));
|
||||
}
|
||||
|
||||
template <typename T>
|
||||
requires std::is_trivially_copyable_v<T>
|
||||
inline DecodeResult decode(T& obj, const std::span<const uint8_t> src) noexcept {
|
||||
return decode_core(std::span<uint8_t>(
|
||||
reinterpret_cast<uint8_t*>(&obj), sizeof(T)
|
||||
), src);
|
||||
}
|
||||
|
||||
} // namespace cobs
|
||||
Reference in New Issue
Block a user