嵌入式GUI皮肤系统:emWin控件外观定制与状态驱动绘制实战
1. 项目概述:为什么嵌入式GUI需要皮肤系统?
在嵌入式设备上做界面开发,尤其是用emWin这类库,很多开发者都经历过一个阶段:界面能用,但不好看。早期的嵌入式GUI,控件外观往往是库内置的、固定的,想改个颜色、调个圆角,要么改源码,要么用位图硬贴,费时费力还不灵活。皮肤系统(Skinning)的出现,就是为了解决这个痛点。它本质上是一种“换肤”机制,把控件的外观绘制逻辑从核心功能中剥离出来,让你能像换衣服一样,动态地改变按钮、复选框、下拉框这些控件的“长相”。
我接触过不少项目,从工业HMI到消费电子,UI风格迭代是家常便饭。今天老板说要科技蓝,明天产品经理说要活力橙,如果每个控件都要重画一遍位图或者改一遍底层绘制代码,那工作量简直不敢想。皮肤系统的价值就在这里:它通过一套标准化的API和回调机制,让你用配置的方式定义外观。比如,按钮按下时是什么颜色,获得焦点时边框怎么变,禁用时如何呈现灰色效果,这些都可以通过BUTTON_SetSkinFlexProps、CHECKBOX_SetSkinFlexProps这类函数,传入一个定义好的颜色和属性结构体就搞定了。这不仅仅是“美化”,更是提升开发效率和维护性的关键。
输入资料里提到的WIDGET_ITEM_DRAW_INFO结构体和一系列WIDGET_ITEM_DRAW_*命令,是皮肤系统的“骨架”和“指令集”。皮肤回调函数就像一个画师,emWin引擎(窗口管理器)会告诉画师:“现在要画这个按钮的背景了”(WIDGET_ITEM_DRAW_BACKGROUND),并且把画布的位置(x0, y0, x1, y1)、控件当前的状态(ItemIndex,如按下、聚焦)都通过这个结构体传给你。你的任务就是根据这些指令和状态信息,调用emWin的基础绘图API(如GUI_DrawGradientV画渐变、GUI_DrawRoundedFrame画圆角边框)把控件画出来。这种设计实现了逻辑与表现的彻底解耦,让UI风格的定制变得模块化和可复用。
2. 皮肤系统的核心架构与工作原理拆解
要玩转emWin的皮肤系统,不能只停留在调用API的层面,必须理解它背后的设计思想和工作流程。这样当你遇到绘制错位、状态不更新这些坑时,才能快速定位。
2.1 皮肤与控件的绑定机制
皮肤不是独立存在的,它必须“附着”在控件上。emWin提供了两套设置方法:全局默认皮肤和单个控件皮肤。
- 设置全局默认皮肤:使用
WIDGET_SetDefaultSkin()或控件专属的WIDGET_SetDefaultSkinClassic()。这之后创建的所有该类型控件,都会自动使用这个皮肤。这在项目初期统一UI风格时非常有用。比如,在程序初始化时调用BUTTON_SetDefaultSkin(BUTTON_SKIN_FLEX),那么之后所有创建的按钮都会是Flex皮肤风格。 - 设置单个控件皮肤:使用
WIDGET_SetSkin()或控件专属的WIDGET_SetSkinClassic()。这用于对特定控件进行个性化设置。比如,一个对话框里的“确定”按钮需要突出显示,你可以单独为它设置一个高亮颜色的皮肤。
这里有个关键细节:_SetSkinClassic()系列函数,是把控件的外观重置回emWin内置的“经典”风格。这在你需要临时禁用自定义皮肤,或者进行A/B风格对比测试时很有用。
2.2 状态(State)驱动的绘制逻辑
皮肤系统的精髓在于对控件“状态”的响应。一个按钮至少有四种状态:启用(Enabled)、聚焦(Focused)、按下(Pressed)、禁用(Disabled)。复选框(CHECKBOX)有启用/禁用和选中/未选中的状态组合。下拉框(DROPDOWN)还有展开(Open)状态。
这些状态信息,是通过WIDGET_ITEM_DRAW_INFO结构体中的ItemIndex字段,或者SetSkinFlexProps函数的Index参数来传递的。皮肤回调函数必须根据不同的ItemIndex值,决定使用哪一套颜色和绘制属性。例如,当ItemIndex等于BUTTON_SKINFLEX_PI_PRESSED时,你就应该用代表“按下”状态的颜色(比如更深的渐变)来绘制按钮背景。
2.3 配置结构体:皮肤的“配方”
每个Flex皮肤都对应一个专属的配置结构体,比如BUTTON_SKINFLEX_PROPS、CHECKBOX_SKINFLEX_PROPS。你可以把它理解为这个皮肤的“配方”或“样式表”。
这个结构体里定义了绘制所需的所有视觉属性:
- 颜色数组:用于定义边框、渐变。例如,
BUTTON_SKINFLEX_PROPS中的aColorFrame[3]定义了边框的三种颜色(外、中、内),aColorInner[2]定义了内部渐变区域的上下两种颜色。 - 尺寸与形状参数:如
Radius(圆角半径)、BorderSizeL/R/T/B(边框大小,见于FRAMEWIN)。 - 其他属性:如
ColorText(文本颜色)、ColorArrow(箭头颜色)。
初始化与动态修改:你可以在编译时通过GUIConf.h中的宏(如BUTTON_SKINPROPS_ENABLED)来静态定义默认样式。更强大的是在运行时,通过SetSkinFlexProps函数动态修改。这意味着你可以实现运行时主题切换——白天模式和夜间模式的切换,本质上就是为所有控件重新设置一套不同颜色的皮肤属性。
2.4 绘制命令流:皮肤回调函数的执行过程
皮肤回调函数(如BUTTON_DrawSkinFlex)是真正的执行者。它被emWin在特定时机调用,并接收一系列绘制命令。理解这个命令流至关重要:
- 创建阶段 (
WIDGET_ITEM_CREATE):控件创建后立即调用。这里通常进行一些一次性初始化,比如设置文本对齐方式(GUI_SetTextAlign)、使能透明效果(WM_SetHasTrans)等。注意:很多新手会忽略这一步,导致后续文本绘制不对齐或透明背景无效。 - 绘制背景 (
WIDGET_ITEM_DRAW_BACKGROUND):这是最核心的命令。你需要根据ItemIndex指示的状态,使用配置结构体中的颜色,在(x0,y0)到(x1,y1)的矩形区域内绘制控件的背景(包括边框、渐变填充)。x0, y0通常是0(相对于控件窗口原点),x1, y1是控件的宽度-1和高度-1。 - 绘制其他元素:这取决于控件类型。
- 按钮/复选框:可能接着收到
WIDGET_ITEM_DRAW_TEXT(绘制文本)和WIDGET_ITEM_DRAW_BITMAP(绘制位图,如复选框的勾选标记)。关键点:文本和位图的位置需要你根据背景区域和设计稿自行计算。emWin只告诉你“该画文本了”,并把文本指针给你,但画在哪儿、什么颜色,得由皮肤回调函数决定(通常使用GUI_DispStringInRect或GUI_DrawBitmap)。 - 框架窗口(FRAMEWIN):会收到更细分的命令,如
DRAW_FRAME(画边框)、DRAW_SEP(画标题栏与客户区的分隔线)、GET_BORDERSIZE(查询边框大小,用于计算客户区位置)。 - 进度条(PROGBAR):
DRAW_BACKGROUND命令会发送两次(PROGBAR_SKINFLEX_L和PROGBAR_SKINFLEX_R),分别绘制已完成部分和未完成部分的背景,并通过p指针传递一个PROGBAR_SKINFLEX_INFO结构体,告诉你当前是画左边还是右边,以及进度文本。
- 按钮/复选框:可能接着收到
这个基于命令的绘制流程,赋予了皮肤系统极大的灵活性。你可以完全控制每个像素的绘制方式,实现从扁平化到拟物化的任何风格。
3. 核心API详解与实战配置
了解了原理,我们来看具体怎么用。这里以最常用的BUTTON和CHECKBOX为例,深入每个API和结构体。
3.1 BUTTON控件皮肤定制实战
按钮是交互的核心,其皮肤也最复杂,支持四种状态。
第一步:定义皮肤属性结构体首先,你需要为每种状态定义一个BUTTON_SKINFLEX_PROPS。下面是一个定义“启用”状态蓝色渐变按钮的示例:
/* 定义启用状态的皮肤属性 */ static const GUI_COLOR _aButtonEnabledFrame[] = {GUI_BLUE, GUI_LIGHTBLUE, GUI_WHITE}; // 边框:外蓝、中亮蓝、内白 static const GUI_COLOR _aButtonEnabledInner[] = {0x00C0FF, 0x0066CC}; // 内部渐变:从浅蓝到深蓝 const BUTTON_SKINFLEX_PROPS _ButtonSkinFlex_Enabled = { .aColorFrame = {_aButtonEnabledFrame[0], _aButtonEnabledFrame[1], _aButtonEnabledFrame[2]}, .aColorInner = {_aButtonEnabledInner[0], _aButtonEnabledInner[1]}, .Radius = 5, // 圆角半径为5像素 };注意:颜色数组的长度是固定的,必须按文档要求提供足够数量的颜色值。Radius为0时是直角。
第二步:设置皮肤创建按钮后,为其设置皮肤属性。你可以一次性设置所有状态,也可以只修改某一个。
BUTTON_Handle hButton; hButton = BUTTON_Create(10, 10, 80, 30, GUI_ID_OK, WM_CF_SHOW); /* 方法1:分别设置四种状态 */ BUTTON_SetSkinFlexProps(&_ButtonSkinFlex_Enabled, BUTTON_SKINFLEX_PI_ENABLED); BUTTON_SetSkinFlexProps(&_ButtonSkinFlex_Focused, BUTTON_SKINFLEX_PI_FOCUSSED); // 假设已定义_focused属性 BUTTON_SetSkinFlexProps(&_ButtonSkinFlex_Pressed, BUTTON_SKINFLEX_PI_PRESSED); BUTTON_SetSkinFlexProps(&_ButtonSkinFlex_Disabled, BUTTON_SKINFLEX_PI_DISABLED); /* 方法2:使用循环和状态数组(更优雅) */ const BUTTON_SKINFLEX_PROPS* apProps[4] = {&_ButtonSkinFlex_Enabled, &_ButtonSkinFlex_Focused, &_ButtonSkinFlex_Pressed, &_ButtonSkinFlex_Disabled}; int aStateIndex[4] = {BUTTON_SKINFLEX_PI_ENABLED, BUTTON_SKINFLEX_PI_FOCUSSED, BUTTON_SKINFLEX_PI_PRESSED, BUTTON_SKINFLEX_PI_DISABLED}; for(int i = 0; i < 4; i++) { BUTTON_SetSkinFlexProps(apProps[i], aStateIndex[i]); }第三步:理解绘制回调(进阶)如果你需要超越Flex皮肤预定义的效果(比如绘制复杂的纹理、异形按钮),就需要实现自己的BUTTON_SKIN_FLEX回调函数。这个函数原型是固定的:
void MyButtonSkinDrawer(const WIDGET_ITEM_DRAW_INFO * pDrawItemInfo) { switch (pDrawItemInfo->Cmd) { case WIDGET_ITEM_CREATE: /* 初始化,例如设置文本对齐为居中 */ GUI_SetTextAlign(GUI_TA_HCENTER | GUI_TA_VCENTER); break; case WIDGET_ITEM_DRAW_BACKGROUND: { int Index = pDrawItemInfo->ItemIndex; /* 根据Index获取对应的颜色配置 */ const MY_BUTTON_SKIN* pSkin = _apMySkinProps[Index]; // 你自己的皮肤数据 /* 在 pDrawItemInfo->x0...y1 区域内绘制 */ GUI_SetColor(pSkin->frameColor); GUI_DrawRoundedFrame(pDrawItemInfo->x0, pDrawItemInfo->y0, pDrawItemInfo->x1, pDrawItemInfo->y1, 3, 3); // 画圆角边框 /* ... 绘制渐变背景等 ... */ } break; case WIDGET_ITEM_DRAW_TEXT: { const char* pText = (const char*)pDrawItemInfo->p; /* 在背景区域中央绘制文本 */ GUI_DispStringInRect(pText, &(pDrawItemInfo->rItem), GUI_TA_HCENTER | GUI_TA_VCENTER); } break; default: break; } }然后,你需要用BUTTON_SetSkin(hButton, MyButtonSkinDrawer)将这个回调函数设置给按钮。这样,该按钮的绘制就完全由你的MyButtonSkinDrawer函数接管了。
3.2 CHECKBOX控件皮肤定制详解
复选框的皮肤逻辑与按钮类似,但更简单,因为它主要关注启用/禁用和选中/未选中状态。
核心属性结构体:CHECKBOX_SKINFLEX_PROPS
typedef struct { U32 aColorFrame[3]; // 复选框外框的三种颜色(外、中、内) U32 aColorInner[2]; // 复选框内部填充的渐变颜色(上、下) U32 ColorCheck; // 勾选标记(对号)的颜色 int ButtonSize; // 复选框按钮部分的大小(已过时,建议用API设置) } CHECKBOX_SKINFLEX_PROPS;特别注意:文档提到ButtonSize已过时(Obsolete)。正确的做法是使用CHECKBOX_SetSkinFlexButtonSize()函数来动态设置复选框方框的尺寸。这是一个常见的坑,如果你直接修改结构体里的ButtonSize,可能不会生效,因为皮肤回调函数内部可能不再读取这个字段。
设置复选框尺寸与皮肤:
CHECKBOX_Handle hCheck; hCheck = CHECKBOX_Create(10, 50, 150, 25, "同意协议", WM_CF_SHOW); /* 1. 先设置按钮(方框)的大小,比如20x20像素 */ CHECKBOX_SetSkinFlexButtonSize(hCheck, 20); /* 2. 定义并设置皮肤属性(这里只展示启用状态) */ const GUI_COLOR _aCheckboxFrame[] = {GUI_DARKGRAY, GUI_GRAY, GUI_LIGHTGRAY}; const GUI_COLOR _aCheckboxInner[] = {GUI_WHITE, 0xEEEEEE}; const U32 _ColorCheck = GUI_BLUE; const CHECKBOX_SKINFLEX_PROPS _CheckboxSkin_Enabled = { .aColorFrame = {_aCheckboxFrame[0], _aCheckboxFrame[1], _aCheckboxFrame[2]}, .aColorInner = {_aCheckboxInner[0], _aCheckboxInner[1]}, .ColorCheck = _ColorCheck, .ButtonSize = 0, // 这个字段可以忽略或设为0 }; CHECKBOX_SetSkinFlexProps(&_CheckboxSkin_Enabled, CHECKBOX_SKINFLEX_PI_ENABLED); /* 同样需要设置 DISABLED 状态的属性 */绘制命令的差异:复选框的绘制命令与按钮略有不同。除了DRAW_BACKGROUND(画方框背景),它还有:
WIDGET_ITEM_DRAW_BITMAP:这个命令不是用来画外部位图的,而是用来画内部的勾选标记。ItemIndex为1表示选中状态,你需要在这个命令里绘制对号。通常用GUI_DrawLine或GUI_FillPolygon画一个简单的对号图形。WIDGET_ITEM_DRAW_TEXT:绘制复选框旁边的文本标签。文本指针通过pDrawItemInfo->p传递。WIDGET_ITEM_DRAW_FOCUS:绘制文本周围的焦点虚线框。这个通常可以调用emWin的GUI_DrawFocusRect函数来完成。
3.3 其他控件皮肤要点速览
DROPDOWN(下拉框):
- 属性结构体
DROPDOWN_SKINFLEX_PROPS包含上下两个渐变区域(aColorUpper,aColorLower)、箭头颜色(ColorArrow)、文本颜色(ColorText)、分隔线颜色(ColorSep)和圆角半径(Radius)。 - 它有OPEN(展开)、FOCUSSED、ENABLED、DISABLED四种状态。展开状态通常用于高亮下拉框的触发区域。
- 绘制命令包括
DRAW_ARROW(画右侧小三角)和DRAW_TEXT(画当前选中的文本)。
- 属性结构体
FRAMEWIN(框架窗口):
- 这是最复杂的皮肤之一,因为它要管理标题栏、边框、客户区。
- 属性结构体
FRAMEWIN_SKINFLEX_PROPS包含边框各方向的大小(BorderSizeL/R/T/B),这直接影响客户区(Client Window)的位置和大小。窗口管理器(WM)会调用GET_BORDERSIZE命令来查询这些值,以正确安置客户区。 - 有ACTIVE(活动)和INACTIVE(非活动)两种状态,用于区分当前前台窗口和后台窗口的视觉效果。
- 重要提示:文档明确指出,创建窗口时,非活动状态的边框尺寸被用于计算客户区初始大小。这意味着即使你的窗口默认是活动的,也要确保非活动状态的边框尺寸设置正确。
PROGBAR(进度条):
- 其独特之处在于
DRAW_BACKGROUND命令会被调用两次,分别绘制已完成部分(左/上)和未完成部分(右/下)。通过pDrawItemInfo->p指向的PROGBAR_SKINFLEX_INFO结构体中的Index字段(PROGBAR_SKINFLEX_L或PROGBAR_SKINFLEX_R)来区分。 - 属性结构体
PROGBAR_SKINFLEX_PROPS包含了左右(或上下)两部分的独立渐变颜色设置(aColorUpperL/LowerL,aColorUpperR/LowerR),方便实现“已填充”和“未填充”区域的视觉区分。
- 其独特之处在于
4. 实战开发流程与避坑指南
掌握了API,我们来看一个完整的皮肤系统集成流程,以及我踩过的一些坑。
4.1 皮肤系统集成四步法
第一步:规划与设计在写代码前,先用设计工具(如Photoshop, Figma)或手绘草图,明确每个控件在不同状态下的视觉规范:颜色值(RGB或emWin颜色索引)、圆角大小、边框粗细、渐变方向、字体大小颜色等。制作一个样式表(Style Sheet),这将是你后续编码的直接依据。
第二步:资源与配置定义
- 颜色定义:在
GUIConf.h或单独的头文件中,用宏或const数组定义你的调色板。避免在代码中硬编码0xFF0000这样的魔数。// 主题颜色定义 #define THEME_PRIMARY 0x007ACC // 主色调蓝 #define THEME_PRIMARY_DARK 0x005A9E #define THEME_SECONDARY 0x2D2D30 // 深灰 #define THEME_TEXT 0xFFFFFF // 白色文字 #define THEME_DISABLED 0x767676 // 禁用态灰色 - 皮肤属性结构体初始化:根据样式表,为每个控件的每种状态初始化对应的
*_SKINFLEX_PROPS结构体。建议按控件和状态组织在独立的.c文件中。
第三步:初始化与设置在GUI初始化函数中(通常是GUI_Init()之后):
- 调用
WIDGET_SetDefaultSkin(WIDGET_SKIN_FLEX)启用全局Flex皮肤(如果你的项目全部用Flex皮肤)。 - 或者,为每个控件类型调用
*_SetDefaultSkin(*_SKIN_FLEX)。 - 使用
*_SetSkinFlexProps()为默认皮肤设置你定义好的属性结构体。 - 创建控件。如果创建后需要修改单个控件的皮肤,再使用
*_SetSkin()或*_SetSkinFlexProps()。
第四步:自定义绘制回调(如果需要)如果Flex皮肤无法满足需求(例如需要绘制图片背景、特殊形状),则需要实现自定义皮肤回调函数。
- 编写符合
WIDGET_SKIN_DRAW_FUNC原型的函数。 - 在函数内处理
WIDGET_ITEM_CREATE,DRAW_BACKGROUND,DRAW_TEXT等命令。 - 将函数指针通过
*_SetSkin()赋给特定控件或默认皮肤。
4.2 常见问题与排查技巧实录
以下是我在实际项目中遇到的典型问题及解决方法,这些在官方手册里不一定找得到。
问题1:设置了皮肤,但控件外观毫无变化。
- 排查步骤:
- 确认皮肤已启用:你调用了
BUTTON_SetSkinFlexProps,但控件创建前是否调用了BUTTON_SetDefaultSkin(BUTTON_SKIN_FLEX)或WIDGET_SetDefaultSkin(WIDGET_SKIN_FLEX)?如果没调用,控件使用的可能是经典皮肤,你的Flex属性设置不会生效。 - 检查颜色格式:emWin的颜色是
U32类型,但格式取决于颜色模式(GUI_USE_ARGB等)。确保你设置的颜色值在当前显示驱动配置下是有效的。一个快速验证方法是直接用GUI_SetColor(YOUR_COLOR); GUI_FillRect(0,0,10,10);看是否能画出预期颜色。 - 验证API调用顺序:确保是在控件创建之后才调用
SetSkinFlexProps。虽然通常也可以在创建前设置默认皮肤属性,但为已存在的控件设置属性,必须在创建之后。
- 确认皮肤已启用:你调用了
问题2:控件文本不显示或位置不对。
- 原因与解决:
- 文本颜色与背景色相同:检查
ColorText属性是否设置正确,或者在你的皮肤回调DRAW_TEXT命令中是否设置了文本颜色(GUI_SetColor)。 - 文本对齐问题:在
WIDGET_ITEM_CREATE命令或绘制文本前,没有设置对齐方式。Flex皮肤默认的文本对齐方式可能不符合你的布局。在CREATE命令中调用GUI_SetTextAlign(GUI_TA_LEFT | GUI_TA_TOP)(或你需要的对齐方式)来全局设置。 - 绘制区域计算错误:在自定义皮肤回调中,
DRAW_TEXT命令收到的pDrawItemInfo->rItem是整个控件的矩形区域。如果你希望文本在特定位置(比如按钮居中、复选框右侧),需要自己计算文本矩形。例如,按钮文本居中:GUI_RECT TextRect = pDrawItemInfo->rItem; GUI_DispStringInRect(pText, &TextRect, GUI_TA_HCENTER | GUI_TA_VCENTER);
- 文本颜色与背景色相同:检查
问题3:动态修改皮肤属性(如颜色)后,控件没有立即重绘。
- 解决方案:调用
*_SetSkinFlexProps()只是修改了皮肤的数据结构,并不会自动触发窗口重绘。你需要手动通知控件无效化(invalidate)自身,强制重画。
对于大量控件更新,可以考虑使用BUTTON_SetSkinFlexProps(&newButtonSkin, BUTTON_SKINFLEX_PI_ENABLED); WM_InvalidateWindow(hButton); // 关键!使按钮窗口区域无效,触发重绘WM_InvalidateArea或直接WM_InvalidateWindow(WM_HBKWIN)(使整个桌面背景窗口无效,但需谨慎使用,性能有影响)。
问题4:自定义皮肤回调函数导致性能下降或闪烁。
- 优化技巧:
- 减少重绘区域:在回调函数中,如果可能,根据
pDrawItemInfo->rItem进行精细绘制,避免全控件区域重绘。但通常皮肤回调的绘制区域已经是需要更新的最小区域了。 - 避免复杂计算:皮肤回调函数会被频繁调用。不要在回调内部进行浮点运算、内存动态分配或复杂的逻辑判断。所有颜色、坐标等数据最好预先计算好,以
const数组或结构体的形式供回调函数快速读取。 - 启用内存设备(Memory Device):对于复杂的皮肤或动画,在控件上使用内存设备(
WM_SetCreateFlags(WM_CF_MEMDEV))可以极大减少闪烁。但会消耗更多RAM。 - 谨慎使用透明效果:
WM_SetHasTrans()可以设置透明,但混合计算会消耗CPU。非必要不使用。
- 减少重绘区域:在回调函数中,如果可能,根据
问题5:框架窗口(FRAMEWIN)的客户区内容被边框或标题栏遮挡。
- 根本原因:
FRAMEWIN_SKINFLEX_PROPS中的BorderSizeL/R/T/B设置得太小,或者皮肤回调函数在处理WIDGET_ITEM_GET_BORDERSIZE_*命令时没有返回正确的值。 - 检查点:
- 确保你为
FRAMEWIN_SKINFLEX_PI_ACTIVE和_INACTIVE状态都正确设置了边框大小属性。 - 如果你实现了自定义的FRAMEWIN皮肤回调,在收到
WIDGET_ITEM_GET_BORDERSIZE_L等命令时,必须返回你设定的边框大小值。Flex皮肤内部已经处理了,但自定义皮肤需要你手动处理。 - 创建FRAMEWIN后,可以用
WM_GetClientWindow()获取客户区句柄,再用WM_GetWindowRectEx()打印其坐标,确认其位置是否在边框和标题栏之内。
- 确保你为
问题6:进度条(PROGBAR)皮肤,已完成和未完成部分衔接处有缝隙或重叠。
- 解决方案:这是因为在
DRAW_BACKGROUND命令中,为左右(或上下)两部分绘制的矩形区域(x0,y0,x1,y1)计算有误。PROGBAR_SKINFLEX_INFO结构体中的IsVertical告诉你方向,Index告诉你画哪一部分。你需要根据进度百分比,精确计算每一部分的矩形范围。确保左部分的x1和右部分的x0恰好衔接(对于水平进度条),不要留出1像素的间隙,也不要重叠1像素。计算时注意整数除法的舍入问题,建议使用(Width * Percentage) / 100来计算分界点坐标。
