1. 项目概述与核心价值
在嵌入式GUI开发领域,emWin以其高效、稳定和功能丰富而著称,是许多嵌入式工程师构建人机界面的首选。然而,默认的控件外观往往千篇一律,难以满足现代产品对界面美观和品牌差异化的需求。这时,皮肤定制技术就成了我们手中的“魔法棒”。它允许我们深入控件的绘制层,从颜色、渐变到边框细节,全方位重塑控件的外观,而无需改动任何一行核心的业务逻辑代码。这不仅仅是“美化”,更是一种将UI设计与底层逻辑彻底解耦的工程实践,极大地提升了代码的可维护性和产品的可定制性。
今天,我们就来深入探讨emWin中几个核心控件的Flex皮肤定制,特别是
MULTIPAGE_SKIN_FLEX
、
PROGBAR_SKIN_FLEX
和
RADIO_SKIN_FLEX
。这些Flex皮肤代表了emWin皮肤系统中最灵活、最强大的一类,它们通过一套结构化的配置和回调机制,将控件的视觉表现完全交给了开发者。理解并掌握它们,意味着你不仅能做出漂亮的界面,更能深刻理解emWin的渲染架构。本文将从原理拆解到实战配置,手把手带你走过从理解
WIDGET_ITEM_DRAW_INFO
结构体到实现一个完整自定义皮肤的全过程,分享我在实际项目中积累的参数调优心得和那些官方手册里不会写的“避坑指南”。
2. 皮肤定制核心原理与架构解析
2.1 皮肤系统的工作机制:回调与命令驱动
emWin的皮肤系统本质上是一个基于回调函数的命令驱动绘制引擎。当你为一个控件(如
MULTIPAGE
)设置了Flex皮肤后,该控件在需要绘制自身或其子项(如标签页)时,就不再使用内部默认的绘制函数,而是转而调用你指定的皮肤回调函数。
这个回调函数会收到一个至关重要的参数:指向
WIDGET_ITEM_DRAW_INFO
结构体的指针。这个结构体是皮肤系统的“指令集”和“上下文信息包”。它的
Cmd
成员告诉回调函数当前需要执行什么绘制任务(例如画背景、画边框、画文本),而其他成员(如
hWin
,
ItemIndex
,
x0
,
y1
等)则提供了执行这个任务所需的所有上下文信息,比如控件句柄、当前绘制项的索引、绘制区域的坐标等。
这种设计的精妙之处在于 职责分离 。控件本身只关心逻辑状态(哪个标签页被选中、进度条的值是多少),而将“如何画”这个视觉问题完全委托给皮肤回调函数。这使得UI视觉风格的变更独立于业务逻辑,我们可以为同一套功能逻辑轻松切换多套视觉主题。
2.2 配置结构体:皮肤的外观“配方”
如果说回调函数是“厨师”,那么
SKINFLEX_PROPS
这类配置结构体就是“菜谱”。它以一种结构化的方式,定义了皮肤所有可调的视觉属性。以
MULTIPAGE_SKINFLEX_PROPS
为例,它包含了:
-
BkColor: 选中项的背景色。 -
aBkUpper[2]: 上方渐变的起始和结束颜色。 -
aBkLower[2]: 下方渐变的起始和结束颜色。 -
FrameColor: 边框颜色。 -
TextColor: 文本颜色。
这些属性通常在两个层面进行配置:
-
静态默认配置
:在
GUIConf.h中通过宏(如MULTIPAGE_SKINPROPS_ENABLED)定义一套全局默认的皮肤属性。这是皮肤的“出厂设置”。 -
运行时动态配置
:通过
MULTIPAGE_SetSkinFlexProps()等API,在程序运行时动态修改某个特定控件实例的皮肤属性。这允许我们实现动态主题切换、状态高亮等高级效果。
实操心得:颜色格式的选择 结构体中的颜色值类型为
U32,通常使用emWin的GUI_颜色宏或GUI_RGB/GUI_COLOR等函数生成的32位ARGB值。在实际项目中,我强烈建议定义一个统一的调色板头文件,将所有皮肤用到的颜色定义为宏或常量。这样不仅便于统一修改主题色,也能避免在代码中散落大量的魔数(Magic Number),极大提升代码可读性和可维护性。例如:// theme_colors.h #define THEME_PRIMARY GUI_RGB(0x33, 0x99, 0xFF) #define THEME_PRIMARY_DARK GUI_RGB(0x2B, 0x78, 0xD2) #define THEME_BG_LIGHT GUI_RGB(0xF5, 0xF5, 0xF5) #define THEME_TEXT GUI_RGB(0x33, 0x33, 0x33) // ... 其他颜色定义
2.3 状态管理:同一控件,不同面貌
一个专业的皮肤必须能响应控件的不同状态。Flex皮肤通过
Index
参数完美支持了这一点。在设置属性时,你需要指定这个
Index
,它代表控件的特定状态。
-
MULTIPAGE
: 使用
MULTIPAGE_SKINFLEX_PI_ENABLED、MULTIPAGE_SKINFLEX_PI_SELECTED、MULTIPAGE_SKINFLEX_PI_DISABLED来区分标签页的启用、选中和禁用状态。一个选中的标签页通常会有更醒目的背景色。 -
RADIO/SLIDER/SCROLLBAR
: 使用
_PI_PRESSED和_PI_UNPRESSED(或_CHECKED和_UNCHECKED)来区分按下/未按下(或选中/未选中)状态。这为按钮、滑块提供了视觉反馈。
在皮肤回调函数内部,你需要根据
WIDGET_ITEM_DRAW_INFO
结构体中传递的
State
、
PageStatus
等信息,来决定当前绘制项处于何种状态,从而应用对应的那套“颜色配方”。这种状态机思维是编写健壮皮肤回调函数的关键。
3. 核心控件Flex皮肤配置详解
3.1 MULTIPAGE_SKIN_FLEX:多页控件的标签栏美化
MULTIPAGE
控件常用于实现标签页(Tab)切换。其Flex皮肤将每个标签页视为一个由
边框
、
上下两个水平渐变矩形
和
文本
组成的复合图形。
3.1.1 配置结构体与视觉映射
MULTIPAGE_SKINFLEX_PROPS
结构体直接对应了标签页的视觉构成:
-
aBkUpper[0]和aBkUpper[1]:定义了标签页上部矩形的渐变。[0]是顶部颜色,[1]是底部颜色。通过设置这两个颜色相同可以消除渐变,实现纯色背景。 -
aBkLower[0]和aBkLower[1]:定义了下部矩形的渐变。上下两个矩形共同组成了标签页的主体背景。 -
BkColor: 特别注意 ,此颜色用于绘制 选中项 的背景。当某个标签页被选中时,emWin会使用这个纯色覆盖整个标签页区域,而忽略aBkUpper和aBkLower的渐变设置。这是实现选中态高亮的关键。 -
FrameColor和TextColor:分别控制边框和文字颜色。
3.1.2 关键API与使用流程
-
定义并初始化配置结构体 :为启用、选中、禁用三种状态分别定义属性。
static const MULTIPAGE_SKINFLEX_PROPS _aPropsEnabled = { .BkColor = GUI_BLUE, // 选中态背景色 .aBkUpper = {GUI_LIGHTBLUE, GUI_WHITE}, .aBkLower = {GUI_WHITE, GUI_LIGHTGRAY}, .FrameColor = GUI_DARKGRAY, .TextColor = GUI_BLACK, }; static const MULTIPAGE_SKINFLEX_PROPS _aPropsSelected = { .BkColor = GUI_DARKBLUE, // 选中时背景更深 .aBkUpper = {GUI_BLUE, GUI_LIGHTBLUE}, .aBkLower = {GUI_LIGHTBLUE, GUI_BLUE}, .FrameColor = GUI_BLACK, .TextColor = GUI_WHITE, // 选中时文字反白 }; // ... 类似定义 _aPropsDisabled -
应用皮肤属性 :在创建控件后或需要切换主题时调用。
MULTIPAGE_SetSkinFlexProps(&_aPropsEnabled, MULTIPAGE_SKINFLEX_PI_ENABLED); MULTIPAGE_SetSkinFlexProps(&_aPropsSelected, MULTIPAGE_SKINFLEX_PI_SELECTED); // 为特定控件hMultiPage设置皮肤 MULTIPAGE_SetSkin(hMultiPage, MULTIPAGE_DrawSkinFlex); -
理解绘制命令 :在自定义的皮肤回调函数中(虽然Flex皮肤已内置
MULTIPAGE_DrawSkinFlex,但理解其原理有助于深度定制),你需要处理如WIDGET_ITEM_DRAW_BACKGROUND(绘制标签背景)、WIDGET_ITEM_DRAW_FRAME(绘制边框)、WIDGET_ITEM_DRAW_TEXT(绘制文本)等命令。对于Flex皮肤,我们通常不需要重写整个回调,只需配置好属性即可。
注意事项:边框绘制的细节 在处理
WIDGET_ITEM_DRAW_FRAME命令时,ItemIndex参数至关重要。当ItemIndex为-1时,表示需要绘制的是整个MULTIPAGE客户区(Client Area)的边框。当ItemIndex >= 0时,表示需要绘制的是特定索引标签页的边框。WIDGET_ITEM_DRAW_INFO中的FrameFlags成员会以位掩码的形式告诉你需要绘制哪几条边(上、下、左、右),这对于绘制标签页之间衔接处的边框非常有用,可以避免重叠或缝隙。
3.2 PROGBAR_SKIN_FLEX:进度条的视觉增强
进度条(Progress Bar)的Flex皮肤设计得相当细致,它将进度条分为 左/右 (或 上/下 )两个部分进行独立渲染,每部分又由 上下两个渐变 组成,从而能够模拟出具有立体感的光照效果。
3.2.1 配置结构体解析
PROGBAR_SKINFLEX_PROPS
结构体包含了四组渐变颜色和一个文本颜色:
-
aColorUpperL[2],aColorLowerL[2]:控制进度条“已完成”部分(左侧或上部)的顶部和底部渐变。 -
aColorUpperR[2],aColorLowerR[2]:控制进度条“未完成”部分(右侧或下部)的顶部和底部渐变。 -
ColorFrame:整个进度条外围的边框颜色。 -
ColorText:进度百分比文本的颜色(如果启用了文本显示)。
3.2.2 绘制流程与方向处理
进度条皮肤的绘制逻辑是理解其如何工作的关键。皮肤回调函数会收到两次
WIDGET_ITEM_DRAW_BACKGROUND
命令:
-
第一次调用时,
PROGBAR_SKINFLEX_INFO结构体(通过p指针访问)中的Index成员为PROGBAR_SKINFLEX_L。此时,WIDGET_ITEM_DRAW_INFO中的x0, y0, x1, y1定义了 已完成部分 的矩形区域。你应该使用aColorUpperL和aColorLowerL定义的颜色来绘制这个区域的渐变。 -
第二次调用时,
Index为PROGBAR_SKINFLEX_R。坐标区域对应 未完成部分 ,应使用aColorUpperR和aColorLowerR的颜色。
PROGBAR_SKINFLEX_INFO
中的
IsVertical
成员指示进度条是水平还是垂直。对于垂直进度条,“L”对应顶部(已完成),“R”对应底部(未完成)。绘制渐变时,你需要根据
IsVertical
来调整渐变方向(水平渐变还是垂直渐变)。通常,使用
GUI_DrawGradientV()
或
GUI_DrawGradientH()
函数来实现。
3.2.3 实战配置示例 假设我们要创建一个蓝色渐变(已完成部分)到灰色渐变(未完成部分)的水平进度条:
static const PROGBAR_SKINFLEX_PROPS _ProgBarProps = {
// 已完成部分:从深蓝到浅蓝的垂直渐变(即使水平进度条,渐变方向也可垂直以模拟光照)
.aColorUpperL = {GUI_RGB(0, 0x66, 0xCC), GUI_RGB(0x33, 0x99, 0xFF)},
.aColorLowerL = {GUI_RGB(0x33, 0x99, 0xFF), GUI_RGB(0x99, 0xCC, 0xFF)},
// 未完成部分:浅灰色到更浅灰色的渐变
.aColorUpperR = {GUI_RGB(0xE0, 0xE0, 0xE0), GUI_RGB(0xF0, 0xF0, 0xF0)},
.aColorLowerR = {GUI_RGB(0xF0, 0xF0, 0xF0), GUI_RGB(0xF8, 0xF8, 0xF8)},
.ColorFrame = GUI_GRAY,
.ColorText = GUI_BLACK,
};
// 应用属性并设置皮肤
PROGBAR_SetSkinFlexProps(&_ProgBarProps, 0); // Index 固定为0
PROGBAR_SetSkin(hProgBar, PROGBAR_DrawSkinFlex);
3.3 RADIO_SKIN_FLEX:单选按钮的精致化
单选按钮(Radio Button)的Flex皮肤专注于美化那个圆形(或方形)的选择按钮。它通过多层同心圆环来模拟按钮的立体感和选中状态。
3.3.1 按钮的层次化渲染
RADIO_SKINFLEX_PROPS
结构体中的
aColorButton[4]
数组定义了按钮从外到内的四层颜色:
-
aColorButton[0]: 最外圈边框的颜色。 -
aColorButton[1]: 中间圈的颜色。 -
aColorButton[2]: 内圈边框的颜色。 -
aColorButton[3]: 按钮最中心实心区域的颜色。
通过为选中和未选中状态设置不同的颜色数组,可以清晰地反馈用户选择。例如,未选中时中心色为白色,选中时中心色变为高亮的蓝色。
ButtonSize
成员定义了按钮的直径(以像素为单位)。
文档明确要求必须为偶数
,如果传入奇数,emWin会自动减1。这是为了避免在绘制同心圆时出现像素不对齐导致的模糊。
3.3.2 绘制命令与焦点处理 RADIO皮肤的绘制被分解为几个独立的命令:
-
WIDGET_ITEM_DRAW_BUTTON: 绘制指定索引项的单选按钮图形。你需要根据ItemIndex和当前选中状态,使用对应的aColorButton颜色数组,通过多次调用GUI_DrawCircle()或GUI_FillCircle()(结合裁剪区域)来绘制多层圆环。 -
WIDGET_ITEM_DRAW_TEXT: 在按钮旁边绘制该项的文本标签。 -
WIDGET_ITEM_DRAW_FOCUS: 仅在控件获得焦点时触发 ,用于在 当前选中项 的文本周围绘制一个焦点矩形(通常是虚线框)。这是一个非常容易被忽略但能显著提升键盘操作体验的细节。 -
WIDGET_ITEM_GET_BUTTONSIZE: 当emWin需要布局控件(计算文本起始位置)时,会调用此命令询问按钮的大小。你的回调函数必须返回ButtonSize的值。
3.3.3 状态同步技巧
在皮肤回调函数中,如何知道当前绘制的按钮是否被选中?你需要通过控件句柄
hWin
和
ItemIndex
,调用
RADIO_GetItemState
等emWin API来查询该单选按钮项的逻辑状态。
切忌
在皮肤回调中维护自己的状态变量,因为皮肤实例可能被多个控件共享,状态必须与控件实际状态同步。
4. 高级皮肤定制:从配置到自定义绘制
4.1 深入WIDGET_ITEM_DRAW_INFO与命令处理
虽然Flex皮肤提供了默认的绘制回调(如
MULTIPAGE_DrawSkinFlex
),但有时默认效果仍无法满足需求,这时就需要我们实现自己的皮肤回调函数。理解
WIDGET_ITEM_DRAW_INFO
是这一切的基础。
这个结构体是皮肤回调与emWin核心通信的桥梁。除了通用的
hWin
(控件句柄)、
Cmd
(命令)、坐标信息外,其
p
成员是一个
void*
指针,它指向一个与特定控件皮肤相关的信息结构(如
MULTIPAGE_SKIN_INFO
)。通过类型转换,你可以获取到更具体的绘制上下文。
一个自定义皮肤回调的基本骨架如下:
int MyCustomMultiPageSkin(const WIDGET_ITEM_DRAW_INFO * pDrawItemInfo) {
MULTIPAGE_SKIN_INFO * pSkinInfo;
pSkinInfo = (MULTIPAGE_SKIN_INFO *)pDrawItemInfo->p;
switch (pDrawItemInfo->Cmd) {
case WIDGET_ITEM_DRAW_BACKGROUND: {
// 1. 判断当前标签页状态:启用、选中、禁用?
// 可通过 pSkinInfo->PageStatus, pSkinInfo->Sel, ItemIndex 判断
// 2. 根据状态,选择对应的颜色配置(可从全局变量或通过API获取)
// 3. 使用 GUI_DrawGradientH/V 等函数在 (x0,y0,x1,y1) 区域内绘制背景
// 4. 如果需要更复杂的图形(如圆角),可以在此处实现
break;
}
case WIDGET_ITEM_DRAW_FRAME: {
// 根据 pDrawItemInfo->FrameFlags 判断需要绘制哪几条边
// 使用 GUI_DrawLine 或 GUI_SetPenSize 配合 GUI_DrawRect 绘制边框
// 注意处理 ItemIndex == -1(客户区边框)的情况
break;
}
case WIDGET_ITEM_DRAW_TEXT: {
// 设置字体、颜色、对齐方式
// 使用 GUI_DispStringInRect 或类似函数在指定区域绘制文本
// 文本内容通常需要通过 WM_GetDialogItemText 等API根据 ItemIndex 获取
break;
}
case WIDGET_ITEM_CREATE: {
// 初始化皮肤私有数据(如果需要),例如设置文本对齐方式
// 可以在这里调用 MULTIPAGE_SetTextAlign 等
break;
}
default:
// 对于不处理的命令,返回0表示使用默认处理(如果可能)
// 或者直接 break
break;
}
return 0; // 返回值含义取决于具体命令,通常0表示成功
}
4.2 性能优化与内存考量
在资源受限的嵌入式系统中,皮肤绘制性能至关重要。
- 避免冗余计算 :在皮肤回调中,尽量减少复杂的数学运算和API调用。例如,颜色值、字体句柄等可以在初始化阶段计算好并存储起来,而不是每次绘制都重新计算。
-
善用裁剪区域
:
WIDGET_ITEM_DRAW_INFO提供的(x0, y0, x1, y1)坐标就是当前需要绘制的裁剪区域。确保你的所有绘制操作都限制在这个区域内。emWin的GUI函数本身会尊重裁剪,但自己实现的算法也应注意。 - 谨慎使用透明度和Alpha混合 :虽然emWin支持Alpha混合,但这通常是性能杀手。在低端MCU上,应尽量避免使用,或仅在小范围内使用。
-
皮肤属性的存储
:为每个控件状态(启用、选中等)定义静态的
const配置结构体,并将其存储在Flash中而非RAM,可以节省宝贵的RAM空间。运行时通过指针引用这些常量即可。
4.3 实现动态皮肤与主题切换
Flex皮肤的魅力在于运行时可变。你可以通过一个函数来统一切换整个应用程序的皮肤主题。
typedef enum {
THEME_LIGHT,
THEME_DARK,
THEME_CUSTOM
} APP_THEME;
void APP_ChangeTheme(APP_THEME theme) {
const MULTIPAGE_SKINFLEX_PROPS *pMultiPageProps;
const PROGBAR_SKINFLEX_PROPS *pProgBarProps;
// ... 其他控件皮肤属性指针
switch(theme) {
case THEME_LIGHT:
pMultiPageProps = &_MultiPageLightProps;
pProgBarProps = &_ProgBarLightProps;
break;
case THEME_DARK:
pMultiPageProps = &_MultiPageDarkProps;
pProgBarProps = &_ProgBarDarkProps;
break;
// ...
}
// 应用新主题到所有相关控件
MULTIPAGE_SetSkinFlexProps(pMultiPageProps, MULTIPAGE_SKINFLEX_PI_ENABLED);
MULTIPAGE_SetSkinFlexProps(pMultiPageProps, MULTIPAGE_SKINFLEX_PI_SELECTED);
PROGBAR_SetSkinFlexProps(pProgBarProps, 0);
// ...
// 重要:更新所有窗口,触发重绘
WM_InvalidateWindow(WM_HBKWIN); // 使整个桌面窗口无效,触发重绘
}
调用
WM_InvalidateWindow
会标记窗口为“脏”区域,emWin的消息循环会在下一帧自动触发重绘,所有控件将用新的皮肤属性重新绘制自己。
5. 常见问题、调试技巧与实战心得
5.1 皮肤不生效或显示异常
这是新手最常见的问题,通常由以下原因导致:
-
未正确设置皮肤回调
:调用
MULTIPAGE_SetSkinFlexProps只是设置了属性,还必须调用MULTIPAGE_SetSkin(hWin, MULTIPAGE_DrawSkinFlex)将控件的绘制函数切换到Flex皮肤。 顺序上,先设属性,再设皮肤回调 。 -
颜色格式错误
:确保使用的颜色值是emWin有效的32位颜色值。使用
GUI_RGB()或GUI_COLOR等宏来生成。直接使用0xFF0000这样的十六进制数可能不符合当前显示驱动的像素格式(RGB565, ARGB8888等)。 -
坐标理解错误
:皮肤回调中的坐标
(x0, y0, x1, y1)是包含边界的矩形区域。例如,绘制一个填充矩形时,应使用GUI_FillRect(x0, y0, x1, y1)。x1和y1是右下角坐标,不是宽度和高度。 -
状态判断逻辑错误
:在自定义绘制回调中,错误地判断了控件的状态(如选中、按下)。务必使用
pSkinInfo指向的结构体中的信息(如Sel,State)或通过控件API(如RADIO_GetItemState)来查询,而不是依赖假设。
5.2 绘制效率低下导致界面卡顿
如果发现界面刷新缓慢,特别是在有动画或频繁更新的控件上:
- 使用示波器或调试引脚 :在皮肤回调函数的开始和结束处翻转一个GPIO引脚,用示波器测量其高电平时间,即可精确测量该次绘制的耗时。这是定位性能瓶颈最直接的方法。
-
检查绘制操作数量
:避免在单个绘制命令中执行过多的
GUI_DrawLine、GUI_FillRect等基础绘图调用。Flex皮肤默认的渐变绘制已经过优化。 -
禁用非必要重绘
:确保没有在不需要的时候调用
WM_InvalidateWindow。例如,只有在皮肤属性确实改变时才触发全局重绘。
5.3 实现圆角、阴影等高级效果
Flex皮肤的基础配置只支持纯色和线性渐变。如果你需要圆角矩形、阴影、纹理等更复杂的效果,就必须实现完全自定义的皮肤回调函数。
以圆角MULTIPAGE标签页为例,思路如下:
-
在
WIDGET_ITEM_DRAW_BACKGROUND命令中,不使用GUI_FillRect,而是使用GUI_FillPolygon或GUI_DrawBitmap(如果使用预渲染的位图)来绘制一个圆角矩形区域。 - 你需要自己计算圆角的路径。一个简单的方法是先填充一个中间矩形,再在四个角填充四分之一圆。
- 这无疑会增加计算量。一种折中方案是:为常用的圆角大小预生成一小套位图(如左上角、右上角等),在绘制时进行位图拼接。这用CPU换取了绘制速度和效果的平衡。
5.4 皮肤与窗口管理器的协作
皮肤是控件的一部分,而控件位于窗口中。需要理解两者的层级关系:
- 控件的皮肤只负责绘制控件 客户区 内的内容。控件的非客户区(如标题栏、边框)由窗口管理器(WM)负责。
- 如果设置了窗口的背景色或背景回调,它会在控件绘制 之前 被绘制。确保你的皮肤颜色与窗口背景协调。
-
皮肤回调中不应调用可能触发递归重绘的API(如在绘制函数中调用
WM_InvalidateRect),这可能导致栈溢出或死循环。
5.5 调试与验证清单
在完成一个皮肤的配置或自定义后,建议按此清单进行检查:
- [ ] 功能正确性 :控件在所有交互状态(正常、按下、选中、禁用、获得焦点)下显示是否正确?
- [ ] 布局适应性 :改变控件大小时,皮肤是否能正确缩放或适应?特别是边框和渐变区域。
- [ ] 内存占用 :自定义皮肤回调是否引入了大的静态数组或频繁的动态内存分配?
- [ ] 执行时间 :在目标硬件上,皮肤绘制的耗时是否在可接受范围内(通常一帧内所有控件绘制总时间应小于屏幕刷新间隔)?
- [ ] 多实例 :多个同类型控件使用同一套皮肤属性时,是否都能正常工作?
- [ ] 主题切换 :运行时切换皮肤主题功能是否正常,有无残留绘制或显示错误?
掌握emWin的Flex皮肤定制,是从“能用”到“好用”、“好看”的关键一步。它要求开发者不仅了解API的调用,更要理解其背后的渲染架构和状态机思想。开始时可能会觉得配置繁琐,但一旦建立起自己的皮肤库和工具函数,后续的UI开发效率将得到质的飞跃。希望本文的详解和实战心得,能帮助你在下一个嵌入式GUI项目中,打造出令人眼前一亮的专业界面。

7万+


被折叠的 条评论
为什么被折叠?



