GUI 框架基础需求、设计和实现 - 1 基础元素

GUI 框架基础需求、设计和实现 - 1 基础元素

以下皆为建议性内容,根据需求制定方案。

这里列出 GUI 开发中的 通用性 的 需求、问题的解法和方案 等,为了能够搭建出复杂 GUI 显示和交互逻辑,满足交互设计需求。

文章所在 Github 仓库 Staok/GUI-Framework-Study: GUI 框架基础需求、设计和实现文章 会保持最新,其它地方的不会跟进。

基础元素

GUI 综合性工程所需 Features

UI feature 基础需求,如 widgets,layout (如 flex,自定义排列、滑动 / 滑动条 等)),style、flag、state 系统(这里为引用 LVGL 的特性),ttf 字体支持(对 freeType 库的支持),图片显示支持(图片数组(未解码或解码的图片数据)、图片文件),event 系统(灵活的 input / driver 设置,事件响应回调设置等),多页面(screen) 等,渐变色支持,anim(obj 的基本变化形式(如位置、长宽、旋转、透明度 等)在时间轴打关键点实现)等,下面给出比较全面的列举。

这里总结 支持较复杂 UI 设计和实现 需求 的 基本元素 和 Features:

  • UI 创建。

    参考 交互设计搞确认项 一节。

  • 足够的基础组件。可进一步封装组件,可自设计组件。

    给 obj 添加事件回调(函数),事件类型要够丰富。

    够丰富的 style 支持(参考 lvgl 的 style 系统);丰富的 flag、state 支持;layout 支持(如 lvgl 的 flex)。

    参考 GUI 设计 常见基础组件 / Widgets 一节。

  • 多页面。

    参考 页面管理 一节。

  • 多字体 / 多语言支持。

    参考 多字体 / 多语言 以及 高度自动化 一节。

  • 多机型。资产管理。

    参考 多机型 / 功能宏 / 素材资产管理 一节。

  • UI Debug。

    参考 UI 测试 / 自动化测试 一节。

GUI 设计 常见基础组件

基础 Widgets
  • Panel 块。矩形等基础形状,长宽,颜色,倒圆角,透明度,边框线,背景图(可用于透明标识(灰白方格马赛克,可以软件生成)),渐变色。

  • 画点、画线,画圆(中心点坐标,半径,是否填充,宽度)。

  • 文本 Label。使用指定字体、多种大小;换行、省略号、滚动显示;加粗、下划线;(可选)文字中间变颜色、可点击、变字体、变大小 等。

  • 按键 Btn(可做多种风格的,用于不同的地方)。事件:短按、长按、按住 等。

  • 开关 Switch、选择框 CheckBox。状态:选中、非选中、半选中;选中状态下不可点、非选中状态下不可点。

  • 下拉框 Choose。

  • 进度条。

  • 拖动条 / 滑动条。

  • 输入框。

  • 键盘 KeyBoard。可定制项:每个键、键排列、多页面切换;数字键盘和全键盘;最短输入、最长输入;占位字符 place holder str;密码模式;输入匹配正则表达式 Regex。

  • ListView 长内容滚动显示。排列方向、间距、对齐子内容,程序可控上下移动,事件有比如 开始滚动、正在滚动 和 结束滚动 等。

  • 图片编解码。(多种图片格式解码)

    • jpeg、png、gif(可控播放),webp(推荐);图片文件、图片编码二进制数据、图片解码后二进制数据直接显示;图片缓存功能(不必每次解码);图片缩放(用于自适应长宽)、旋转。
  • 图片文件大小:

    • 可以先尝试,图片所有自动压缩,自动选择最好的压缩率,若压缩后体积还是比较大(如大于 1MB,或者 尺寸过大(如大于屏幕分辨率的),则单独打印出来告警)。

    • 图片可以尝试全 webp 格式(有官方的编解码库 libwebp),比 通常 png / gif 小很多,节省空间。

    • 对于大图片,使用图片文件形式存储在机器里,解码图片来显示,并使用图片缓存机制,不必反复解码。对于很多小图标图片,可以使用解码后的数组存储,小图标数量多,不必解码占cpu(46x46的图标图片,100张,将近1MB,权衡选择)。

  • 图片显示长宽:

    • 图片显示,尽量给的图片大小放到 屏幕 UI 上不经过缩放等而直接显示就正好。

    • 给设图片的API增加功能:则设置图片的 API 可以 可选的 设置 是否缩放 与 其 parent panel 大小对齐(autoFit 模式 或 fill 模式。autoFit 模式:图片缩放顶住 parent panel 边缘,确保是图片整个显示出来(如 图片 比 parent 宽 则图片两边 与 parent 对齐);fill 模式:图片缩放后不给 parent 留空,图片 比 parent 宽 则 图片上下 与 parent 的 上下对齐)。

  • 二维码 显示。

  • 视频显示。指定视频文件,开始、暂停、停止 的 控制,摄像头显示 等。

  • (可选)丰富交互动画。

    可以看看 LVGL Demos Demos --- LVGL,由专业交互设计人员进行设计。

  • 以上 UI 组件,可整体缩放、旋转、调整透明度等。

  • (添彩项)考虑多分辨率情况下可用。

    参考 多分辨率 一节。

  • (添彩项)考虑多主题。可定制 UI 主题。

    参考 多套主题 一节。

自构建组件
  • 设置项组件。

  • 步骤组件(显示工作的当前步骤,每个步骤有未到、正在进行、执行完成、执行失败等状态图标,有步骤编号,有标题和副标题,多个步骤组件排列显示当前工作进度)。

  • 弹窗组件:任务完成、任务失败、信息提示、警告、错误 等。

  • Toast 组件(在屏幕中间上方,显示带背景的指定的文字,指定秒数后消失)。

  • 简易图文显示。比如 使用其他库 解析 MarkDown、html 等,进行建议的多段落、标题、图片等的图文页面展示。

  • 画线段(传入多个点坐标,在之间画线段,可选传入是否圆弧、圆弧弧度)。

GUI 工程 程序组件

基础软件组件、模块
  • CallRepeatChecker 按键重复点击检测:如果连续调用间隔时间小于指定时间则 不响应 或者 按照指定低频响应。

  • 线程池使用,接受提交函数到内部队列进行逐一执行。

    UI 刷新线程(提交 UI 操作 的函数 到 单线程 的 线程池 或者 函数队列)。

    参考 线程池库 ThreadPool libs

    注意易出现的问题:Cpp-Learning/编程经验-规范, 调试、性能和内存检查工具集合.md at main · Staok/Cpp-Learning

    内部 Debug:

    • 若 UI 刷新间隔 过长(比如大于 200ms)则打印(系统有其它进程监控 和 打印 CPU、RAM、最高占用(CPU 和 RAM)的进程 等等信息)。

    • 使用线程看门狗。刷新间隔超过如 30s 则认为是发生死锁,主动抛异常 退出,产生 Coredump 进行分析。

    线程看门狗。

    定时器。后端周期性获取 硬件 或 其它软件模块 状态,或其它需要周期性执行的业务。

  • 信号槽使用。

    UI 前端 留 事件回调(在回调里面发射信号) 给 UI 后端(也可用于埋点,参考后文 埋点 一节),并留所有 UI 元素 操作函数(可以均留为 property(槽函数、变量,直接控制 UI 显示 的 变化)) 列清晰;UI 后端 调用 UI 元素 操作函数(UI 前端留的 property)来 操作 UI。

    更详细的 前后端软件设计模型 描述 参考 基本 UI 开发模式 一节。

    参考 信号槽库 Sig-Slot libs

  • 数据抽象,类抽象和继承等,工厂模式创建和管理(可参考 C-Cpp-design-patterns);对软硬件的数据结构进行建模,设计的方便扩展和维护,方便添加新功能,方便进行操作。

    使用 Device Manager 概念的类进行统一管理(也可以分多层)。

  • Task Manager(FSM .etc)。

    参考 FSM 库

  • GateWay(管理所有对外的数据通讯交互,UART、Socket、MQTT .etc)。

    参考 网络库

  • Power Manager(屏保、待机、开关机等)。

  • Logger Or User Manager。

  • System Diagnosis(自检、校准、维护提醒 等)。

  • Upgrade(升级检查、固件升级 等)。

  • Debug Utils。参考 GUI 综合性工程所需 Features 一节里面列到的。

  • 配置参数读写,如 settings.json 文件 读写,保存用户配置、机器信息等。

  • 统一的打印信息,基本的,有 提醒(Info)、警告(Warning)、错误(Error)、调试信息(Debug)分类。

    • 一般的打印信息格式:[20XX-0X-0X-24:59:59:999] [INFO | WARN | ERROR] [<file name>] [<function name>():<line num>]: <debug str>

    • 打印调试信息的时机:

      • 页面切换的时候(这种类型的信息应该设置一个开关控制是否打印)。

      • 设置功用的按键按下的时候(这种类型的信息应该设置一个开关控制是否打印)。

      • 每一个函数在返回错误之前视情况要打印错误信息。

      • 与其它端进行消息通讯的时候,异步发送 和 异步回调 的这两个时刻都要打印信息。

  • 进程间通讯中间件。DBUS、DDS、unix socket(+ protobuf 定义数据包结构 + 自定义 protocol 打包解包) 等。

UI 相关软件组件、模块
  • 具体组件的管理,比如对于弹窗组件,需要搞个全局弹窗管理,就需要包括创建弹窗(传入必要信息(比如传入弹窗id(用于关闭时候引用),弹窗类型、显示文字(和图片路径)、弹窗按键数量和各个的显示文字以及绑定的函数),并且里面会判断是否先存着不显示啊、如何叠层啊之类的)、关闭弹窗等。

    具体组件类型具体做。

  • 多页面,页面管理。参考 页面管理 一节。

  • 多图层,图层管理。比如所有 UI 正常 显示在 第 1 层,而 弹窗 等 这种需要全局弹出覆盖当前内容,就在 比如 第 2 层 显示,以此类推。

    支持透明图层,其它进程 可以 直接从 DRM 等图层管理 往画面添加显示。

    各图层定义和管理,以及获取图层用来创建组件。

  • 字体管理,字体创建、使用。

    多语言支持,自动化处理。

    参考 下面 多语言 / 字体 以及 事务高度自动化 一节。

  • 屏保。设定屏保时间,到时,先设置背光到一半,延时2秒,关闭背光,屏幕显示黑白切换(防止烧屏),有输入,恢复正常显示,设置半背光(此时还不能响应点击),延时1s后,恢复背光,恢复正常响应点击。

  • 待机 / 休眠。首先进入屏保,一段时间没有输入后进入 待机 / 休眠:

    • 关闭屏幕背光电源。

    • 停掉 UI 渲染和刷新。

    • 关闭一些外设,包括关闭屏幕外设,只保留退出休眠相关的外设。

    • 处理器进入低功耗模式。

    退出 待机 / 休眠 序列与上面相反即可。

  • UI 测试。参考 下面 UI 测试 / 自动化测试 一节。

  • 颜色相关实用函数。以下默认为 32 位颜色,lvgl 支持 各种其它位的颜色,以及不同位的颜色之间的转换,还有混合、变亮、变暗 等。

    • ARGB 和 RGB 相互转换。

    • 从 uint32_t 的 ARGB 获得 uint8_t 的 A、R、G 和 B;以及反转换。

    • 从 ARGB 或 RGB 字符串值 转 uint32_t 的 ARGB 颜色 整数值。

    • 获得两个颜色的相近程度(RGB 转 XYZ 再转 Lab,使用 DeltaE76 算法得到差异值)。

    • 获得一些预设的主题颜色,从 颜色名称 返回 颜色 ARGB;以及反转换;反转换,增加模糊匹配(给定一个颜色 整数值,返回最相近的颜色的颜色名称)。

    • RGB 转 HSV,以及反转换(lvgl 的 lv_color.h 有)。

  • 单位量相关转换实用函数。

    • 时间(年月日时分秒毫秒)、容量大小(B、KB、GB、TB)、重量(g、Kg)、长度(mm、m、km) 等等 的 可读化显示。

    • 比如,一个文件容量大小为 123456Byte 存储在一个整数变量,写一个转换函数,转换为 xxx.x KB 字符串,用于 UI 显示。

基本 UI 开发模式

编程设计模式总结

适用于 UI 的设计模式 MVVM

MVVM 是一种将用户界面(GUI)代码与业务逻辑和数据模型分离的架构模式。它的核心目标是实现 UI 与 逻辑的解耦

关键组成部分:

  • Model (模型):

    • 代表真实状态的数据内容(类似于领域模型)。

    • 包含业务逻辑和数据验证。

    • 完全不知道 ViewModel 或 View 的存在。

  • View (视图):

    • 用户看到的结构、布局和外观(GUI)。

    • 被动:它不处理业务逻辑。

    • 通过 数据绑定 (Data Binding) 连接到 ViewModel。

    • 将用户交互(点击、输入)转发给 ViewModel。

  • ViewModel (视图模型):

    • MVVM 的核心。它是 View 的抽象,包含 View 的状态(如"按钮是否启用")。

    • 充当"转换器":将 Model 的数据转换为 View 可以显示的格式。

    • 不知道 View 的具体实现(没有 UI 控件的引用),通过 通知机制 (Notification) 或 命令 (Command) 与 View 通信。

核心机制:

  1. 数据绑定 (Data Binding): 自动同步 View 和 ViewModel 之间的数据。

  2. 通知机制 (INotifyPropertyChanged): 当 ViewModel 数据改变时,自动通知 View 更新。

  3. 命令 (Commands): 处理 View 的事件(如点击),而不是在 View 的后台代码(Code-behind)中写逻辑。

  4. 可测试性: 因为 ViewModel 不依赖 UI 控件,可以对其进行纯粹的单元测试。

复制代码
+----------------+          +-------------------+          +----------------+
|      View      |          |     ViewModel     |          |      Model     |
|  (UI / Window) |          | (Logic / State)   |          | (Data / Rules) |
+----------------+          +-------------------+          +----------------+
|                |  Binding |                   |  Updates |                |
|  [ Text Box ] <-------------> [ String Prop ] <---------->   Data Field   |
|                |          |                   |          |                |
|  [  Label   ] <-------------> [ Format()    ] |          |                |
|                | Notification |               |          |                |
|  [  Button  ] --------------> [ Command()   ] ---------->  SaveToDB()   |
|                |          |                   |          |                |
+----------------+          +-------------------+          +----------------+
       ^                             ^                             ^
       |                             |                             |
  Knows ViewModel           Knows Model Only              Knows Nothing
(Observes changes)       (Does NOT know View)          (Pure Data/Logic)
​
MVVM 框架的实现例子

总结

(其中的 信号变量 Property 或 信号 Signal 可用 Github 开源库如 KDBindings 来做)

Model(不依赖 View 或 ViewModel)(这里当作 后端程序

  • 定义和实现功能函数接口:uint32_t execFun()。如果是耗时任务,则应该内部设计超时机制,外部传入超时时间,超时则直接 返回 超时 Code。根据其返回值判断 执行 的 成功(0 表示成功,正数表示带条件的成功)、失败(负值,不同负值表示失败的情况,包括超时的情况)。

  • 定义信号变量 Property:Property<Mode> mode。根据业务直接赋值 mode 即可,下面有定义其变化的回调函数,将可直接修改 UI 显示。

ViewModel(依赖 Mode)

  • 持有 Mode 实例的指针 model

  • 定义 UI 事件 的 信号 Signal,并连接到槽函数如:

    复制代码
    Signal signalExecFun;
    signalExecFun.connect(
        [this] () {
            // 注意这里的 上下文 Context 为 View 中触发 signalExecFun 所在的上下文
            (下面整体提交到后端执行线程池(单线程或多线程),进行异步处理)
            判断各种 UI 交互逻辑。
            调用 model.execFun()
            根据返回值判断处理是否成功,根据结果进一步执行需求业务逻辑(比如弹窗提示等视觉显示)
        }
    );

    即 View -> 事件回调 -> 触发 ViewModel 信号 -> 处理 UI 逻辑 -> 调用 Model 功能函数(异步执行)。

  • 定义 与 UI 显示对应的 Property(也可以为信号,槽函数就是 View 中 修改 UI 执行刷新变化的函数)

    比如 Property<bool> isPanelAShow; Property<bool> isPanelBShow;

  • 设置 Model 的 Property 变化 的回调函数,如:

    复制代码
    model.mode.onChanged(
        [this] (Mode mode) {
            // 注意这里的 上下文 Context 为 Mode 中修改 mode 所在的上下文
            switch (mode) {
                case Mode::MODEA:
                    isPanelAShow = true;
                    isPanelBShow = false;
                    break;
                case Mode::MODEB:
                    isPanelAShow = false;
                    isPanelBShow = true;
                    break;
                ...
                default: break;
            }
        }
    );

View(依赖 ViewModel)

  • 持有 ViewModel 实例的指针 viewModel

  • 设置事件回调函数,在里面 触发 ViewModel 的信号,如:

    复制代码
    setEventCb(
        [viewModel] () {
            viewModel.signalExecFun.emit();
        }
    );
  • 设置 ViewModel 中修改 UI 的 Property 的变化的回调函数,如:

    复制代码
    viewModel.isPanelAShow.onChanged(
        [viewModel] (bool is) {
            // 注意这里的 上下文 Context 为 ViewModel 中修改 isPanelAShow 所在的上下文
            ui_obj_set_Visibility(panelA, is); // 注意需要在 UI 线程中执行这类语句,以此类推
        }
    );
    ​
    viewModel.isPanelBShow.onChanged(
        [viewModel] (bool is) {
            ui_obj_set_Visibility(panelB, is);
        }
    );

调用图

复制代码
   [ View ]                             [ ViewModel ]                           [ Model ]
      |                                       |                                     |
(User Click)                                  |                                     |
      |                                       |                                     |
Call viewModel.signalExecFun.emit() ------->  |                                     |
      |                                   (Check Logic)                             |
      |                                   Call model.execFun() ------------------>  |
      |                                       |                               (Async Work)
      |                                       | <---------- (Callback/Signal) Update mode
      |                                   (On mode Changed)                         |
      |                                   Update isPanelAShow .etc                  |
(On isPanelAShow .etc Changed) <------------- |                                     |
Update UI                                     |                                     |
      |                                       |                                     |
复杂 UI 交互逻辑举例

应用 MVVM 设计模式举例。

复杂 UI 交互逻辑 场景 描述:在 屏幕 上 点击 执行格式化 SD 卡,首先检测 SD 卡是否存在,存在则屏幕弹窗提示是否执行,如果点击否则关闭弹窗,如果点击是则关闭弹窗并弹出执行中请等待的弹窗,执行完毕后关闭弹窗,相应的界面上 SD 卡 状态、容量等等均更新显示了;如果 SD 卡 不存在则弹窗表示错误,点击是则关闭弹窗。这一通逻辑可以在 前端(分为两个部分,View 和 ViewModel)里面做,后端(Model 部分) 只提供 具体的功能 / 方法 比如 检查sd 卡是否存在、执行 sd 卡格式化、弹出 sd 卡 等等。

下面是 AI 大模型给出的建议,实际方案可以调整。

在 MVVM 中,弹窗和界面的切换也是数据驱动(状态驱动) 的。我们把"是否显示弹窗"、"弹窗的内容是什么"抽象成 ViewModel 里的状态数据

1. Model 层 (底层逻辑)

负责真实的硬件/数据操作,完全不管弹窗。

  • 功能CheckExist() 返回布尔值;FormatAsync() 执行异步格式化;GetCapacity() 获取容量;GetStatus() 获取状态。

    这里,实际 Model 可以搞个定时器周期性获取 SD 卡状态等信息 并直接通知 ViewModel 进行更新 UI。

2. ViewModel 层 (状态与交互核心)

维护页面的所有状态,将 UI 的动作转化为命令,并更新状态。

  • 状态属性 (需支持通知更新)

    • SdCardCapacity (字符串,例如 "64GB")

    • SdCardStatus (字符串,例如 "正常" / "未挂载")

    • 通知全局弹窗管理进行弹窗(传入弹窗id(用于关闭时候引用),弹窗类型、显示文字(和图片路径)、弹窗按键数量和各个的显示文字以及绑定的函数)

  • 命令 (Commands)

    • RequestFormatCommand (绑定到主界面的"格式化"按钮)

    • 弹窗按键的各个回调函数。

3. View 层 (纯粹的显示者)

  • SD 卡 的 UI 显示部分 绑定 SdCardCapacitySdCardStatus

  • 按钮的点击事件绑定到 ViewModel 的 Commands。

  • 搞一个 弹窗组件,以及弹窗管理,包括创建弹窗(传入必要信息(比如传入弹窗id(用于关闭时候引用),弹窗类型、显示文字(和图片路径)、弹窗按键数量和各个的显示文字以及绑定的函数),并且里面会判断是否先存着不显示啊、如何叠层啊之类的)、关闭弹窗等。

通用控件事件处理

这里为实验性设计。

下面是通用控件事件处理程序结构,写个统一处理的函数,就是实现下面这一套。

每个用户点击需要响应的控件,如 按键、开关 / 选择框、下拉框,其 回调函数里面 应该怎么执行的信息都打包在一个 结构体 的 实例 里面,然后一个 表格、hash 等结构 管理这些 控件实例 和 其执行信息结构体的实例 的一一对应关系。

1. 前端逻辑

UI 组件配置 (分为三类):

  1. 按键: 属性包括 enable, checked

  2. 开关 / 选择框: 属性包括 enbale, checked

  3. 下拉框: 属性包括 enbale, textList, currentIndex

    设计为,每个组件的事件回调里面都统一调用下面的入口函数 handle_user_operate()

执行函数:

伪代码描述:

复制代码
struct ExecInfo{
    check_fun
    task_fun
    time_out
    callback_fun
    execSeqConfig
};
hash<obj, ExecInfo> objExecInfos;
handle_user_operate(obj);

handle_user_operate() 传入控件实例的 obj 或者 id,其内部 通过 查表 objExecInfos 得到 其对应的 描述怎么执行的 结构体。

其中 check_funtask_funtime_outcallback_fun 几个参数在下面的执行流程中被使用,而 execSeqConfig 是个结构体,里面信息 配置了下面 所有标识 Optional 的项是否执行或跳过。

执行流程步骤:

  1. CallRepeatChecker 按键重复点击检测

    如果连续调用间隔时间小于指定时间则 不响应 或者 按照指定低频响应。

  2. 检查是否可以执行 (Optional)

    使用 检查函数 check_fun 执行检查,根据检查函数返回值,如果不可以执行则弹窗提示 (弹出后 return, 弹窗一个 ok 按键,点击后只关闭弹窗)。

  3. 是否要开始执行提示弹窗 (二次确认) (Optional)

    弹窗有 yes, no 按键,yes 点击后向下执行, no 点击后 return。

  4. 已经开始执行提示弹窗 (Optional)

    弹窗有 ok 按键,点击后只关闭弹窗。

  5. 弹出示意执行中的转圈弹窗 (Optional)

    弹窗提示处理中请等待。

  6. 执行下面的部分。

2. 后端执行

下面的都提交到后端业务线程池中执行,相对于前端为异步执行、不阻塞前端执行流程。

主要逻辑流程:

  1. 调用后端功能函数

    调用后端功能函数 task_fun(如果是耗时任务,则应该内部设计超时机制,外部传入超时时间 time_out,超时则直接 返回 超时 Code),根据其返回值判断 执行 的 成功(0 表示成功,正数表示带条件的成功)、失败(负值,不同负值表示失败的情况,包括超时的情况)。

  2. 收尾处理

    1. 执行 callback_fun

    2. 如果前面弹出了 示意执行中的转圈弹窗,则在这里关闭

    3. 根据后端功能函数的返回值,显示结果提示弹窗 (Optional)

    4. 是否重试弹窗,重试次数,重试间隔,重试结果弹窗提示 (Optional)

相关推荐
oBxkQwKTLam2 小时前
探索人工势场法:简单高效的路径规划算法
ux
Rsingstarzengjx2 小时前
【Photoshop从入门到精通】 A16 画笔工具 笔记
ui·photoshop
工业HMI实战笔记2 小时前
新能源行业HMI:光伏电站与储能系统监控界面
ui·性能优化·自动化·汽车·交互
Real-Staok2 小时前
GUI 框架基础需求、设计和实现 - 4 具体组件、模块设计
ui·ux
Real-Staok2 小时前
QT & QML 总结备查
qt·ui·ux
未来龙皇小蓝14 小时前
RBAC前端架构-05:引入Element-UI及相关逻辑
前端·ui
工控小龙人19 小时前
船舶维修HMI:船舶发动机的检修诊断界面
ui·人机交互·用户界面
钛态20 小时前
Flutter for OpenHarmony:mason_cli 拒绝重复劳动,用砖块构建你的代码模板(强大的脚手架生成器) 深度解析与鸿蒙适配指南
flutter·ui·华为·自动化·harmonyos
我命由我123451 天前
Photoshop - Photoshop 工具栏(60)污点修复工具
ui·adobe·职场和发展·求职招聘·职场发展·课程设计·photoshop