手写笔子系统(CategoryPen)
SDL 提供的压感手写笔(触控笔/橡皮擦)事件处理模块,适用于绘图板、配备手写笔的移动设备/平板等场景的精准输入处理。
核心事件类型
要处理手写笔输入,只需监听以下核心事件:
SDL_EVENT_PEN_PROXIMITY_IN/SDL_EVENT_PEN_PROXIMITY_OUT:手写笔进入/离开感应范围事件(SDL_PenProximityEvent);SDL_EVENT_PEN_DOWN/SDL_EVENT_PEN_UP:手写笔按下/抬起事件(SDL_PenTouchEvent);SDL_EVENT_PEN_MOTION:手写笔移动事件(SDL_PenMotionEvent);SDL_EVENT_PEN_BUTTON_DOWN/SDL_EVENT_PEN_BUTTON_UP:手写笔按键按下/抬起事件(SDL_PenButtonEvent);SDL_EVENT_PEN_AXIS:手写笔轴数据变化事件(SDL_PenAxisEvent)。
手写笔高级特性
手写笔不仅支持基础的触摸输入,还可提供压力、倾斜角度、旋转角度等额外轴数据,满足精细绘图、书写等场景需求。
手写笔ID规则
- 手写笔开始输入时,SDL 会分配唯一的
SDL_PenID,只要设备保持连接,该ID在进程生命周期内保持不变; - 手写笔离开感应范围(如远离数位板)后重新进入,会触发接近事件,但
SDL_PenID仍保持一致; - 数位板拔插后重新连接,SDL 可能无法识别为同一硬件,新输入会分配新的
SDL_PenID。
跨平台注意事项
不同平台对手写笔的支持程度差异极大:
- 部分平台支持多设备同时工作,部分平台会将所有连接的手写笔视为单个逻辑设备(类似多USB鼠标控制同一系统光标);
- 部分平台不支持手写笔按键、距离轴等功能;
- 极少平台能提前报告手写笔支持的功能,最佳实践是:要么提供UI让用户配置手写笔,要么在首次收到事件时动态适配新功能;
- 若手写笔硬件支持某功能但SDL未响应,大概率是操作系统层面的支持问题。
函数
- SDL_GetPenDeviceType:获取手写笔关联设备的类型(如数位板、触摸屏、平板等)
数据类型
- SDL_PenID:手写笔唯一标识类型(区分不同手写笔设备/实例)
- SDL_PenInputFlags:手写笔输入标志类型(位掩码,标识输入特性:如橡皮擦模式、桶按钮按下等)
结构体
- SDL_PenAxisEvent:手写笔轴事件结构体(包含轴类型、轴数值、手写笔ID等)
- SDL_PenButtonEvent:手写笔按键事件结构体(包含按键ID、按下/抬起状态、手写笔ID等)
- SDL_PenMotionEvent:手写笔移动事件结构体(包含位置、压力、倾斜角度、手写笔ID等)
- SDL_PenProximityEvent:手写笔接近事件结构体(包含进入/离开状态、手写笔ID、设备类型等)
- SDL_PenTouchEvent:手写笔触摸事件结构体(包含按下/抬起状态、位置、压力、手写笔ID等)
枚举
- SDL_PenAxis:手写笔轴类型枚举(如压力轴、X/Y倾斜轴、旋转轴、距离轴等)
- SDL_PenDeviceType:手写笔关联设备类型枚举(如数位板、触摸屏、平板、未知设备等)
宏
- SDL_PEN_MOUSEID:手写笔模拟鼠标事件的设备ID(用于区分原生鼠标和手写笔模拟鼠标)
- SDL_PEN_TOUCHID:手写笔关联的触摸设备ID标识(用于关联手写笔事件到触摸设备)
FreeBASIC 示例代码
' 引入 SDL 相关声明(需确保 FreeBASIC 已链接 SDL3 库)
#Include "SDL.bi"
' 绘图状态结构体
Type DrawingState
isDrawing As Boolean ' 是否正在绘图
lastX As Integer ' 上一帧X坐标
lastY As Integer ' 上一帧Y坐标
penID As SDL_PenID ' 当前手写笔ID
pressure As Single ' 压感值(0.0~1.0)
tiltX As Single ' X轴倾斜角度(-1.0~1.0)
tiltY As Single ' Y轴倾斜角度(-1.0~1.0)
End Type
Dim Shared As DrawingState drawState
' 初始化绘图状态
Sub InitDrawingState()
drawState.isDrawing = False
drawState.lastX = 0
drawState.lastY = 0
drawState.penID = 0
drawState.pressure = 0.0
drawState.tiltX = 0.0
drawState.tiltY = 0.0
End Sub
' 绘制手写笔轨迹
Sub DrawPenStroke(ByVal renderer As SDL_Renderer Ptr, ByVal x As Integer, ByVal y As Integer)
If (Not drawState.isDrawing) Then Return
' 根据压感值调整线条宽度(1~20像素)
Dim As Integer lineWidth = 1 + Int(drawState.pressure * 19)
' 根据倾斜角度调整颜色(倾斜越大,颜色越浅)
Dim As UByte alpha = 255 - Int((Abs(drawState.tiltX) + Abs(drawState.tiltY)) * 128)
SDL_SetRenderDrawColor(renderer, 0, 150, 255, alpha)
' 设置线条宽度(SDL3 支持渲染器线条宽度)
SDL_SetRenderDrawLineWidth(renderer, lineWidth)
' 绘制线条(从上一位置到当前位置)
SDL_RenderDrawLine(renderer, drawState.lastX, drawState.lastY, x, y)
' 更新上一位置
drawState.lastX = x
drawState.lastY = y
End Sub
' 打印手写笔事件信息
Sub PrintPenEventInfo(ByRef evt As SDL_Event)
Select Case evt.type
Case SDL_EVENT_PEN_PROXIMITY_IN
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "手写笔进入感应范围 | PenID:%llu | 设备类型:%d", _
evt.penproximity.penid, evt.penproximity.devicetype)
Case SDL_EVENT_PEN_PROXIMITY_OUT
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "手写笔离开感应范围 | PenID:%llu", evt.penproximity.penid)
Case SDL_EVENT_PEN_DOWN
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, _
"手写笔按下 | PenID:%llu | 位置:(%d, %d) | 压力:%.2f", _
evt.pentouch.penid, evt.pentouch.x, evt.pentouch.y, evt.pentouch.pressure)
Case SDL_EVENT_PEN_UP
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, _
"手写笔抬起 | PenID:%llu | 位置:(%d, %d)", _
evt.pentouch.penid, evt.pentouch.x, evt.pentouch.y)
Case SDL_EVENT_PEN_MOTION
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, _
"手写笔移动 | PenID:%llu | 位置:(%d, %d) | 压力:%.2f | 倾斜(X/Y):(%.2f, %.2f)", _
evt.penmotion.penid, evt.penmotion.x, evt.penmotion.y, _
evt.penmotion.pressure, evt.penmotion.tiltx, evt.penmotion.tilty)
Case SDL_EVENT_PEN_BUTTON_DOWN
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, _
"手写笔按键按下 | PenID:%llu | 按键ID:%d", _
evt.penbutton.penid, evt.penbutton.button)
Case SDL_EVENT_PEN_BUTTON_UP
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, _
"手写笔按键抬起 | PenID:%llu | 按键ID:%d", _
evt.penbutton.penid, evt.penbutton.button)
Case SDL_EVENT_PEN_AXIS
Dim As ZString Ptr axisName = SDL_GetStringForPenAxis(evt.penaxis.axis)
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, _
"手写笔轴变化 | PenID:%llu | 轴类型:%s | 数值:%.2f", _
evt.penaxis.penid, axisName, evt.penaxis.value)
If (axisName <> NULL) Then SDL_free(axisName)
End Select
End Sub
' 主程序
Dim As SDL_Window Ptr window = NULL
Dim As SDL_Renderer Ptr renderer = NULL
Dim As SDL_Event evt
Dim As Boolean quit = False
' 1. 初始化 SDL
If (SDL_Init(SDL_INIT_VIDEO) < 0) Then
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "SDL 初始化失败:%s", SDL_GetError())
End 1
End If
' 2. 创建绘图窗口(适配手写板/平板的高分辨率)
window = SDL_CreateWindow("SDL 手写笔绘图示例", _
SDL_WINDOWPOS_CENTERED, SDL_WINDOWPOS_CENTERED, _
1280, 720, SDL_WINDOW_SHOWN Or SDL_WINDOW_RESIZABLE)
renderer = SDL_CreateRenderer(window, -1, _
SDL_RENDERER_ACCELERATED Or SDL_RENDERER_PRESENTVSYNC Or SDL_RENDERER_TARGETTEXTURE)
If (window = NULL Or renderer = NULL) Then
SDL_LogError(SDL_LOG_CATEGORY_APPLICATION, "创建窗口/渲染器失败:%s", SDL_GetError())
SDL_Quit()
End 1
End If
' 初始化绘图状态
InitDrawingState()
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "=== SDL 手写笔绘图示例 ===")
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "手写笔操作:绘图(压感/倾斜生效) | 按键:测试笔侧键 | ESC:退出")
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "提示:若无手写笔硬件,可测试鼠标模拟输入(区分原生/手写笔模拟)")
' 3. 主循环
While (Not quit)
' 清屏(白色背景,模拟画纸)
SDL_SetRenderDrawColor(renderer, 255, 255, 255, 255)
SDL_RenderClear(renderer)
' 处理事件队列
While (SDL_PollEvent(@evt))
Select Case evt.type
Case SDL_QUITEVENT
quit = True
' 键盘事件
Case SDL_EVENT_KEY_DOWN
If (evt.key.scancode = SDL_SCANCODE_ESCAPE) Then
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "按下 ESC,退出程序")
quit = True
ElseIf (evt.key.scancode = SDL_SCANCODE_C) Then
' 按C键清空画布
InitDrawingState()
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "清空画布")
End If
' 手写笔核心事件
Case SDL_EVENT_PEN_PROXIMITY_IN, SDL_EVENT_PEN_PROXIMITY_OUT, _
SDL_EVENT_PEN_DOWN, SDL_EVENT_PEN_UP, SDL_EVENT_PEN_MOTION, _
SDL_EVENT_PEN_BUTTON_DOWN, SDL_EVENT_PEN_BUTTON_UP, SDL_EVENT_PEN_AXIS
' 打印事件信息
PrintPenEventInfo(evt)
' 处理绘图逻辑
Select Case evt.type
Case SDL_EVENT_PEN_DOWN
drawState.isDrawing = True
drawState.penID = evt.pentouch.penid
drawState.lastX = evt.pentouch.x
drawState.lastY = evt.pentouch.y
drawState.pressure = evt.pentouch.pressure
Case SDL_EVENT_PEN_UP
drawState.isDrawing = False
Case SDL_EVENT_PEN_MOTION
drawState.pressure = evt.penmotion.pressure
drawState.tiltX = evt.penmotion.tiltx
drawState.tiltY = evt.penmotion.tilty
DrawPenStroke(renderer, evt.penmotion.x, evt.penmotion.y)
Case SDL_EVENT_PEN_AXIS
' 处理轴事件(如压力轴变化)
If (evt.penaxis.axis = SDL_PEN_AXIS_PRESSURE) Then
drawState.pressure = evt.penaxis.value
ElseIf (evt.penaxis.axis = SDL_PEN_AXIS_TILT_X) Then
drawState.tiltX = evt.penaxis.value
ElseIf (evt.penaxis.axis = SDL_PEN_AXIS_TILT_Y) Then
drawState.tiltY = evt.penaxis.value
End If
End Select
' 区分原生鼠标和手写笔模拟鼠标
Case SDL_EVENT_MOUSE_BUTTON_DOWN
If (evt.button.which = SDL_PEN_MOUSEID) Then
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "鼠标点击(手写笔模拟):位置(%d, %d)", evt.button.x, evt.button.y)
' 模拟手写笔按下
drawState.isDrawing = True
drawState.lastX = evt.button.x
drawState.lastY = evt.button.y
drawState.pressure = 0.5 ' 默认压力值
Else
SDL_LogInfo(SDL_LOG_CATEGORY_INPUT, "鼠标点击(原生):位置(%d, %d)", evt.button.x, evt.button.y)
End If
Case SDL_EVENT_MOUSE_BUTTON_UP
If (evt.button.which = SDL_PEN_MOUSEID) Then
drawState.isDrawing = False
End If
Case SDL_EVENT_MOUSE_MOTION
If (evt.motion.state And SDL_BUTTON_LMASK) Then
If (evt.motion.which = SDL_PEN_MOUSEID) Then
' 模拟手写笔移动
drawState.pressure = 0.5 + (Sin(SDL_GetTicks() * 0.001) * 0.4) ' 模拟压力变化
DrawPenStroke(renderer, evt.motion.x, evt.motion.y)
End If
End If
End Select
Wend
' 刷新画面
SDL_RenderPresent(renderer)
SDL_Delay(16) ' 约60FPS
Wend
' 4. 清理资源
SDL_DestroyRenderer(renderer)
SDL_DestroyWindow(window)
SDL_Quit()
SDL_LogInfo(SDL_LOG_CATEGORY_APPLICATION, "程序正常退出")
End 0
' 辅助函数:将手写笔轴枚举转换为可读字符串(SDL3 内置接口封装)
Function SDL_GetStringForPenAxis(ByVal axis As SDL_PenAxis) As ZString Ptr
Select Case axis
Case SDL_PEN_AXIS_PRESSURE: Return SDL_strdup("压力轴")
Case SDL_PEN_AXIS_X: Return SDL_strdup("X轴")
Case SDL_PEN_AXIS_Y: Return SDL_strdup("Y轴")
Case SDL_PEN_AXIS_TILT_X: Return SDL_strdup("X倾斜轴")
Case SDL_PEN_AXIS_TILT_Y: Return SDL_strdup("Y倾斜轴")
Case SDL_PEN_AXIS_ROTATION: Return SDL_strdup("旋转轴")
Case SDL_PEN_AXIS_DISTANCE: Return SDL_strdup("距离轴")
Case Else: Return SDL_strdup("未知轴")
End Select
End Function
核心知识点补充
-
手写笔轴类型说明: 轴枚举 取值范围 说明 SDL_PEN_AXIS_PRESSURE 0.0~1.0 压感值(0=无压力,1=最大) SDL_PEN_AXIS_TILT_X -1.0~1.0 X轴倾斜角度(左右倾斜) SDL_PEN_AXIS_TILT_Y -1.0~1.0 Y轴倾斜角度(前后倾斜) SDL_PEN_AXIS_ROTATION 0.0~1.0 旋转角度(笔杆旋转) SDL_PEN_AXIS_DISTANCE 0.0~1.0 与数位板的距离(仅接近事件有效) -
压感绘图优化技巧:
- 压感值映射为线条宽度/透明度,模拟真实画笔效果;
- 倾斜角度可调整线条颜色/纹理方向,提升绘图真实感;
- 处理手写笔移动事件时,需插值补点避免线条断裂(高速移动时)。
-
模拟输入适配:
- 无手写笔硬件时,可通过
SDL_PEN_MOUSEID识别手写笔模拟的鼠标事件; - 示例中模拟了鼠标输入的压感变化,便于无硬件时测试绘图逻辑;
- 移动平台上,手写笔事件会自动转换为触摸/鼠标事件,无需额外适配。
- 无手写笔硬件时,可通过
总结
- 核心优势:
- SDL 统一封装了不同平台的手写笔事件,无需关注底层驱动差异;
- 支持压感、倾斜、旋转等高级特性,满足专业绘图场景需求;
- 通过
SDL_PEN_MOUSEID可区分原生鼠标和手写笔模拟输入;
- 使用建议:
- 优先监听
SDL_EVENT_PEN_*系列事件处理精准手写笔输入; - 针对不同平台的功能差异做兼容处理,避免依赖特定平台的特性;
- 无手写笔硬件时,可通过鼠标模拟输入验证核心逻辑;
- 优先监听
- 关键接口:
- 事件处理:
SDL_EVENT_PEN_DOWN/UP/MOTION等核心事件; - 设备信息:
SDL_GetPenDeviceType获取设备类型; - 输入区分:
SDL_PEN_MOUSEID宏区分模拟/原生输入。
- 事件处理:
评论一下?