嵌入式GUI多任务架构实战:emWin与RTOS集成优化指南

嵌入式GUI多任务架构实战:emWin与RTOS集成优化指南 1. 项目概述与核心价值在嵌入式系统开发中图形用户界面GUI的响应速度和实时性往往是决定产品用户体验的关键。当你的系统需要同时处理触摸屏交互、实时数据采集、网络通信和后台逻辑运算时一个设计不当的GUI很容易成为性能瓶颈导致界面卡顿、响应迟缓。这正是多任务GUI架构要解决的核心问题。它不是简单地在RTOS实时操作系统里创建几个任务然后调用GUI函数那么简单其精髓在于如何让GUI任务与其它实时任务和谐共处既保证界面的流畅更新又不妨碍系统对紧急事件的即时响应。emWin作为一款成熟的嵌入式GUI库其多任务支持机制设计得非常巧妙。它没有强制捆绑某一种RTOS而是通过一套清晰的内核接口Kernel Interface和事件驱动模型让开发者可以将其灵活地适配到uC/OS-II、FreeRTOS、ThreadX乃至Linux等任何多任务环境中。理解这套机制意味着你能构建出真正“活”起来的嵌入式界面——用户滑动列表时数据仍在后台刷新按下按钮的瞬间系统能立即响应而CPU负载却依然保持低位。本文将从一个资深嵌入式GUI开发者的视角拆解emWin多任务开发的完整链条。我们会从最基础的任务调度策略讲起探讨为什么官方推荐使用独立的低优先级GUI任务然后深入事件处理机制揭秘如何用GUI_SetWaitEventFunc将轮询的CPU占用率降为0%最后我们会手把手完成内核接口的配置与移植并提供针对不同RTOS的实战代码。无论你正在基于STM32和FreeRTOS开发智能家居面板还是在i.MX RT上用ThreadX打造工业HMI这篇文章中的思路和代码都能直接为你所用。2. 多任务GUI的整体架构与设计哲学2.1 核心矛盾GUI更新与实时任务的资源竞争在单任务系统中GUI_Exec()或GUI_Delay()的调用是顺序执行的不存在冲突。但在多任务环境下多个任务可能同时尝试绘制窗口、读取触摸坐标或修改同一块显示内存这就导致了资源竞争。最直接的后果是显示撕裂Tearing、数据错乱甚至系统死锁。emWin解决这一问题的核心设计是资源信号量Resource Semaphore或互斥锁Mutex。简单来说任何任务在调用emWin的API如GUI_DrawLineWM_CreateWindow前都必须先“锁住”GUI资源。这个“锁”的获取和释放正是通过我们后面要实现的GUI_X_Lock()和GUI_X_Unlock()函数来完成的。emWin内部在需要访问显示设备或关键数据结构时会自动调用这些接口从而保证了线程安全。2.2 官方推荐架构专用GUI任务模式emWin用户手册第16.4.5节给出了明确的建议这也是经过大量项目验证的最佳实践。其核心思想是职责分离单一更新入口所有emWin的更新函数主要是GUI_Exec()和GUI_Delay()应该只从一个任务中调用。这能保持程序结构清晰避免更新逻辑分散带来的混乱。低优先级专用任务如果系统RAM充足强烈建议创建一个专用于GUI更新的任务并赋予其最低的优先级。这个任务几乎只做一件事在一个无限循环中调用GUI_Exec()。这样做的好处是高优先级的实时任务如处理传感器中断、网络包可以随时抢占CPU而GUI任务只在系统“空闲”时才执行界面刷新从而确保了系统的实时性。界面与逻辑分离将决定系统行为的实时任务I/O、通信、控制算法与调用emWin的任务分开。你的用户界面任务只负责“显示”和“交互”而具体的业务逻辑由其他高优先级任务计算好后通过消息队列、信号量等IPC机制传递过来。这种架构类似于电脑上的前台桌面应用和后台系统服务。你的鼠标点击高优先级中断总能得到即时响应而窗口的动画渲染低优先级GUI任务则会在CPU有空闲时平滑进行。2.3 事件驱动 vs. 轮询性能的关键抉择这是多任务GUI配置中最影响性能的部分。默认情况下emWin需要周期性轮询来检查是否有事件如触摸、定时器需要处理。这通常意味着在你的GUI任务循环中你需要频繁调用GUI_Exec()。// 默认的轮询方式 - 简单但CPU占用高 void GUI_Task_Polling(void *p_arg) { while(1) { GUI_Exec(); // 执行后台工作如重绘无效窗口 OS_TimeDly(10); // 延时10个系统节拍但任务仍在就绪队列 } }这种方式下即使没有界面更新GUI_Exec()也会被调用任务也会被调度造成不必要的CPU开销。emWin提供了更高效的事件等待机制。通过配置GUI_SetWaitEventFunc()和GUI_SetSignalEventFunc()你可以让GUI任务在无事可做时主动挂起CPU占用率降至0%。只有当真正有事件发生如用户触摸屏幕、定时器超时时才通过信号函数唤醒GUI任务。// 高效的事件等待方式 - 空闲时CPU占用为0% void GUI_Task_EventDriven(void *p_arg) { while(1) { GUI_Exec(); // 执行后台工作 GUI_X_WaitEvent(); // 挂起任务等待事件信号 } } // 当触摸中断服务程序ISR或定时器触发时 void Touch_ISR_Handler(void) { // ... 读取触摸坐标 ... GUI_X_SignalEvent(); // 发出事件信号唤醒GUI任务 }如何选择如果你的系统CPU资源非常紧张或者对功耗有严格要求电池供电设备那么务必使用事件等待方式。如果系统简单且CPU负载本身不高轮询方式更为简单直接。3. 核心配置函数与宏详解要让emWin在多任务环境中跑起来你需要正确配置一组函数和宏。它们像是emWin和你的RTOS之间的“翻译官”。3.1 基础配置宏开启多任务支持首先你需要在GUIConf.h这个配置文件中进行如下设置/* GUIConf.h */ #define GUI_OS 1 // 启用多任务支持这是总开关 #define GUI_MAXTASK 4 // 定义最大可调用emWin的任务数根据实际任务数设置GUI_OS必须设置为1。这会激活emWin内部的GUITask模块该模块负责管理任务ID和资源锁。GUI_MAXTASK这个值定义了可能调用任何emWin API的任务的最大数量。注意这不包括那个只调用GUI_Exec()的专用GUI任务。例如如果你有两个任务一个通信任务用于更新状态文本一个控制任务用于修改进度条会直接调用GUI_DispString或WM_SetValue那么GUI_MAXTASK至少应设为2。设置过小会导致未定义行为设置过大会浪费少量内存。一个安全的做法是将其设为你预估的最大值通常4或8对于大多数应用足够了。3.2 事件处理函数从轮询到事件驱动如前所述这是优化性能的关键。emWin提供了两套设置方式函数接口和宏接口。推荐使用函数接口因为它更灵活可以在运行时动态配置。函数接口运行时配置void GUI_SetSignalEventFunc(GUI_SIGNAL_EVENT_FUNC pfSignalEvent); void GUI_SetWaitEventFunc(GUI_WAIT_EVENT_FUNC pfWaitEvent); void GUI_SetWaitEventTimedFunc(GUI_WAIT_EVENT_TIMED_FUNC pfWaitEventTimed);你需要在系统初始化阶段创建GUI任务之前调用这些函数将对应的RTOS事件操作函数赋值给emWin。通常你会使用emWin提供的移植层函数GUI_SetSignalEventFunc(GUI_X_SignalEvent); GUI_SetWaitEventFunc(GUI_X_WaitEvent); GUI_SetWaitEventTimedFunc(GUI_X_WaitEventTimed);你的工作就是去实现GUI_X_SignalEvent()和GUI_X_WaitEvent()这两个函数。宏接口编译时配置你也可以在GUIConf.h中通过宏来定义但这属于较旧的方法不够灵活#define GUI_X_SIGNAL_EVENT GUI_X_SignalEvent #define GUI_X_WAIT_EVENT GUI_X_WaitEvent #define GUI_X_WAIT_EVENT_TIMED GUI_X_WaitEventTimedGUI_X_WaitEventTimed的用途这个函数用于带超时的事件等待。当emWin内部有激活的定时器例如窗口动画、控件闪烁时它会调用此函数。这样GUI任务可以在等待外部事件如触摸的同时也能在定时器到期时被唤醒以更新界面。如果你的应用没有任何基于emWin定时器的动画可以不实现此函数或让其直接调用GUI_X_WaitEvent()。3.3 内核接口API实现线程安全的核心这是移植工作的重中之重。你需要为你的目标RTOS实现以下六个函数。它们通常被放在一个名为GUI_X_OS.c的文件中。函数原型核心职责在RTOS中的典型实现void GUI_X_InitOS(void)初始化OS相关资源如创建用于保护GUI的互斥信号量。创建二值信号量或互斥锁。U32 GUI_X_GetTaskID(void)返回当前任务的唯一ID。emWin用它来区分不同的调用者。返回RTOS中当前任务的句柄、优先级或ID号。void GUI_X_Lock(void)锁住GUI。在访问显示资源前调用阻止其他任务进入。获取Pend互斥信号量。void GUI_X_Unlock(void)解锁GUI。在访问显示资源后调用允许其他任务进入。释放Post互斥信号量。void GUI_X_SignalEvent(void)发出事件信号。通常由外部中断触摸、按键触发用于唤醒等待中的GUI任务。向GUI任务发送信号量、事件标志或直接任务通知。void GUI_X_WaitEvent(void)等待事件。GUI任务在无事可做时调用此函数挂起自己。等待Pend一个信号量或事件标志。关键理解GUI_X_Lock/Unlock保护的是对emWin API的调用防止多个任务同时操作GUI导致数据混乱。而GUI_X_WaitEvent/SignalEvent管理的是GUI任务的执行状态目的是让任务在空闲时休眠以节省CPU。这是两个不同维度的保护机制。4. 针对不同RTOS的内核接口实现实战理论说再多不如一行代码。下面我将展示针对三种主流RTOSFreeRTOS uC/OS-III ThreadX的GUI_X_OS.c实现。你可以根据自己使用的系统进行参考和修改。4.1 FreeRTOS 实现FreeRTOS使用任务句柄TaskHandle_t和信号量SemaphoreHandle_t。我们使用一个互斥信号量Mutex来保护GUI使用一个二值信号量Binary Semaphore作为事件通知机制。/* GUI_X_FreeRTOS.c */ #include FreeRTOS.h #include task.h #include semphr.h /* 静态全局变量用于事件通知 */ static TaskHandle_t _xGUITaskHandle NULL; static SemaphoreHandle_t _xGUIEventSemaphore NULL; /* 保护GUI资源的互斥锁 */ static SemaphoreHandle_t _xGUIMutex NULL; void GUI_X_InitOS(void) { /* 创建互斥锁用于保护GUI API调用 */ _xGUIMutex xSemaphoreCreateMutex(); configASSERT(_xGUIMutex ! NULL); /* 创建二值信号量用于事件通知 */ _xGUIEventSemaphore xSemaphoreCreateBinary(); configASSERT(_xGUIEventSemaphore ! NULL); } U32 GUI_X_GetTaskId(void) { /* 返回当前任务的句柄作为唯一ID。也可以返回uxTaskPriorityGet(NULL)即优先级作为ID */ return (U32)xTaskGetCurrentTaskHandle(); } void GUI_X_Lock(void) { /* 无限期等待互斥锁。如果锁被其他任务持有本任务将阻塞在此 */ xSemaphoreTake(_xGUIMutex, portMAX_DELAY); } void GUI_X_Unlock(void) { /* 释放互斥锁 */ xSemaphoreGive(_xGUIMutex); } void GUI_X_SignalEvent(void) { BaseType_t xHigherPriorityTaskWoken pdFALSE; /* 通常在中斷服務程序(ISR)中調用所以使用GiveFromISR版本 */ if (xPortIsInsideInterrupt()) { xSemaphoreGiveFromISR(_xGUIEventSemaphore, xHigherPriorityTaskWoken); portYIELD_FROM_ISR(xHigherPriorityTaskWoken); } else { /* 如果在任務中調用 */ xSemaphoreGive(_xGUIEventSemaphore); } } void GUI_X_WaitEvent(void) { /* GUI任务在此无限期等待事件信号量 */ xSemaphoreTake(_xGUIEventSemaphore, portMAX_DELAY); } void GUI_X_WaitEventTimed(int Period) { TickType_t xTicksToWait; /* 将毫秒转换为FreeRTOS的系统节拍数 */ if (Period 0) { xTicksToWait pdMS_TO_TICKS(Period); /* 带超时地等待事件信号量 */ xSemaphoreTake(_xGUIEventSemaphore, xTicksToWait); } }注意事项与实操心得中断安全GUI_X_SignalEvent()很可能在触摸屏或按键的中断服务程序ISR中被调用。因此必须使用xSemaphoreGiveFromISR()这个中断安全版本并处理可能的上下文切换portYIELD_FROM_ISR。优先级反转使用xSemaphoreCreateMutex()创建的互斥锁具有优先级继承机制可以缓解优先级反转问题。如果你的GUI任务优先级很低而另一个高优先级任务也需要调用emWin API这个机制很重要。初始化时机GUI_X_InitOS()必须在任何RTOS任务调度开始之前调用通常放在main()函数中在xTaskCreate()和vTaskStartScheduler()之前执行。4.2 uC/OS-III 实现uC/OS-III使用信号量OS_SEM和事件标志组OS_FLAG_GRP等内核对象。其实现逻辑与FreeRTOS类似。/* GUI_X_uCOSIII.c */ #include os.h static OS_SEM _GUISem; /* 用于保护GUI的信号量 */ static OS_SEM _GUIEventSem; /* 用于事件通知的信号量 */ void GUI_X_InitOS(void) { OS_ERR err; /* 创建保护GUI的信号量初始值为1可用 */ OSSemCreate(_GUISem, GUI Mutex, 1, err); /* 创建事件通知信号量初始值为0不可用 */ OSSemCreate(GUIEventSem, GUI Event, 0, err); } U32 GUI_X_GetTaskId(void) { OS_TCB *p_tcb; /* 获取当前任务控制块返回其地址或优先级作为ID */ p_tcb OSTaskGetCur(); return (U32)p_tcb; /* 或者 return (U32)(p_tcb-Prio); */ } void GUI_X_Lock(void) { OS_ERR err; /* 等待信号量0表示无限等待 */ OSSemPend(_GUISem, 0, OS_OPT_PEND_BLOCKING, NULL, err); } void GUI_X_Unlock(void) { OS_ERR err; OSSemPost(_GUISem, OS_OPT_POST_1, err); } void GUI_X_SignalEvent(void) { OS_ERR err; /* 发出事件信号 */ OSSemPost(_GUIEventSem, OS_OPT_POST_1, err); } void GUI_X_WaitEvent(void) { OS_ERR err; OSSemPend(_GUIEventSem, 0, OS_OPT_PEND_BLOCKING, NULL, err); } void GUI_X_WaitEventTimed(int Period) { OS_ERR err; if (Period 0) { /* 将毫秒转换为uC/OS-III的时钟节拍 */ OS_TICK dly (Period * OS_CFG_TICK_RATE_HZ) / 1000; OSSemPend(_GUIEventSem, dly, OS_OPT_PEND_BLOCKING, NULL, err); } }4.3 ThreadX 实现ThreadX的API风格与前两者略有不同但核心概念相通。/* GUI_X_ThreadX.c */ #include tx_api.h static TX_MUTEX _gui_mutex; static TX_SEMAPHORE _gui_event_semaphore; void GUI_X_InitOS(void) { UINT status; /* 创建互斥锁 */ status tx_mutex_create(_gui_mutex, GUI Mutex, TX_NO_INHERIT); /* 创建计数信号量初始为0最大为1 */ status tx_semaphore_create(_gui_event_semaphore, GUI Event, 0); } U32 GUI_X_GetTaskId(void) { /* 返回当前任务的指针作为ID */ return (U32)tx_thread_identify(); } void GUI_X_Lock(void) { UINT status; /* 获取互斥锁TX_WAIT_FOREVER表示无限等待 */ status tx_mutex_get(_gui_mutex, TX_WAIT_FOREVER); } void GUI_X_Unlock(void) { UINT status; status tx_mutex_put(_gui_mutex); } void GUI_X_SignalEvent(void) { UINT status; /* 释放信号量如果有任务在等待则唤醒它 */ status tx_semaphore_put(_gui_event_semaphore); } void GUI_X_WaitEvent(void) { UINT status; status tx_semaphore_get(_gui_event_semaphore, TX_WAIT_FOREVER); } void GUI_X_WaitEventTimed(int Period) { UINT status; if (Period 0) { ULONG wait_ticks (Period * TX_TIMER_TICKS_PER_SECOND) / 1000; status tx_semaphore_get(_gui_event_semaphore, wait_ticks); } }5. 专用GUI任务与系统集成实战内核接口实现好后我们需要创建那个核心的专用GUI任务并将它集成到你的应用程序中。5.1 GUI任务函数实现这是一个最简化的、采用事件等待模式的GUI任务模板/* gui_task.c */ #include GUI.h void GUI_Task_Entry(void *argument) { /* 1. GUI初始化 */ GUI_Init(); /* 2. 可选配置事件等待函数启用高效模式 */ /* 注意必须在GUI_Init()之后创建其他窗口之前调用 */ GUI_SetSignalEventFunc(GUI_X_SignalEvent); GUI_SetWaitEventFunc(GUI_X_WaitEvent); GUI_SetWaitEventTimedFunc(GUI_X_WaitEventTimed); /* 3. 创建你的主窗口、控件等 */ CreateMainWindow(); /* 4. 主循环 - 核心 */ while(1) { /* 执行emWin后台工作处理消息、重绘无效区域 */ GUI_Exec(); /* 挂起任务等待事件触摸、定时器唤醒 */ /* 如果没有配置事件函数这里可以用GUI_Delay(10)进行简单延时 */ GUI_X_WaitEvent(); /* 任务被唤醒后循环继续再次执行GUI_Exec()处理积压的事件 */ } }5.2 系统初始化与任务创建流程一个典型的main()函数和系统初始化流程如下/* main.c */ #include FreeRTOS.h #include task.h /* 外部声明 */ extern void GUI_X_InitOS(void); extern void GUI_Task_Entry(void *); extern void App_Communication_Task(void *); extern void App_Control_Task(void *); int main(void) { /* 1. 硬件初始化时钟、GPIO、显示屏、触摸屏等 */ SystemClock_Config(); LCD_Init(); Touch_Init(); /* 2. 初始化RTOS内核对象必须在调度器启动前 */ GUI_X_InitOS(); // 初始化emWin的OS接口创建信号量等 /* 3. 创建应用任务 */ /* 注意GUI任务应设置为最低优先级之一 */ xTaskCreate(GUI_Task_Entry, GUI Task, 1024, NULL, tskIDLE_PRIORITY 1, NULL); xTaskCreate(App_Communication_Task, Comm Task, 512, NULL, tskIDLE_PRIORITY 3, NULL); xTaskCreate(App_Control_Task, Ctrl Task, 512, NULL, tskIDLE_PRIORITY 4, NULL); /* 4. 启动RTOS调度器任务开始运行 */ vTaskStartScheduler(); /* 调度器启动后不会返回 */ while(1); } /* 触摸屏中断服务程序示例 */ void Touch_IRQHandler(void) { BaseType_t xHigherPriorityTaskWoken pdFALSE; static int touched 0; /* 读取触摸状态 */ if (Touch_GetState() TOUCH_PRESSED) { touched 1; } else if (Touch_GetState() TOUCH_RELEASED touched) { touched 0; /* 关键步骤发送事件信号唤醒GUI任务处理触摸 */ GUI_X_SignalEvent(); } /* ... 其他中断处理 ... */ }5.3 其他任务如何安全调用emWin API你的通信任务或控制任务可能需要更新界面上的某个控件如修改文本框内容、更新进度条。绝对不能在中断服务程序ISR中直接调用emWin API。正确的做法是在任务中调用并且emWin的内部锁机制GUI_X_Lock/Unlock会保证安全。void App_Communication_Task(void *p_arg) { char status_text[32]; while(1) { /* 模拟接收到网络数据 */ if (receive_data_from_network()) { sprintf(status_text, Rx: %d bytes, data_len); /* 安全地更新GUI。GUI_X_Lock()会被自动调用 */ GUI_SetTextMode(GUI_TM_NORMAL); GUI_DispStringAt(status_text, 10, 50); /* GUI_X_Unlock()会被自动调用 */ /* 或者通过窗口管理器更新控件更推荐 */ WM_SetWindowText(hStatusText, status_text); } vTaskDelay(pdMS_TO_TICKS(100)); } }重要提示虽然emWin API是线程安全的但频繁地从高优先级任务调用绘图函数可能会因为获取锁而阻塞GUI任务本身的重绘导致界面更新不及时。最佳实践是高优先级任务只通过消息队列向GUI任务发送更新请求由低优先级的GUI任务统一执行绘图操作。这完全符合emWin官方“单一更新入口”的建议。6. 常见问题、调试技巧与性能优化6.1 问题排查速查表现象可能原因排查步骤与解决方案界面卡死无响应1.GUI_X_Lock()/GUI_X_Unlock()未成对调用或实现有误。2. 高优先级任务长时间占用GUI锁。3. GUI任务优先级过高导致其他任务饿死。1. 检查GUI_X_Lock中信号量Pend和GUI_X_Unlock中Post是否匹配。2. 使用RTOS的调试工具查看信号量持有者。3.确保GUI任务为最低优先级之一。触摸或按键响应延迟1.GUI_X_SignalEvent()未在中断中被正确调用。2. GUI任务被其他同等或更高优先级任务阻塞。3. 未启用事件等待仍在使用GUI_Delay轮询。1. 确认触摸中断触发并在ISR中调用了GUI_X_SignalEvent()。2. 检查是否有同等优先级的任务在空跑未调用阻塞API。3. 确认已配置GUI_SetWaitEventFunc。多任务同时绘图时屏幕撕裂GUI_MAXTASK设置过小或GUI_X_Lock机制未生效。1. 增大GUIConf.h中的GUI_MAXTASK值。2. 确保GUI_OS已定义为1。3. 在GUI_X_Lock/Unlock中添加调试打印确认锁机制工作。GUI任务CPU占用率居高不下仍在使用轮询模式GUI_Delay未使用事件等待。切换到事件驱动模式配置GUI_SetWaitEventFunc并实现GUI_X_WaitEvent。创建窗口或控件时程序跑飞1. 堆栈溢出。GUI任务或emWin本身需要较多堆栈。2. 在中断中调用了WM_CreateWindow等非重入函数。1. 增大GUI任务的堆栈大小例如从1024增加到2048字。2.严禁在ISR中调用任何emWin API必须通过任务间通信。6.2 调试技巧与工具锁机制调试在GUI_X_Lock和GUI_X_Unlock函数的开头添加一个计数器或调试输出。static int lock_count 0; void GUI_X_Lock(void) { lock_count; //DEBUG_PRINT(Lock taken, count%d, Task:0x%x\n, lock_count, GUI_X_GetTaskId()); xSemaphoreTake(_xGUIMutex, portMAX_DELAY); }观察锁的获取和释放是否平衡。如果lock_count只增不减说明有任务拿了锁没释放。任务状态监控利用FreeRTOS的uxTaskGetSystemState或Segger SystemView等工具实时观察GUI任务的状态。在事件等待模式下大部分时间它应该处于“阻塞”Blocked状态CPU占用率为0%。性能 profiling如果怀疑GUI更新拖慢系统可以测量GUI_Exec()一次执行的时间。在低端MCU上复杂窗口的重绘可能耗时数毫秒。这时可以考虑使用存储设备Memory Device来避免闪烁或者优化窗口结构减少无效区域。6.3 高级优化策略使用存储设备WM_CF_MEMDEV在创建窗口时添加WM_CF_MEMDEV标志或者使用WM_EnableMemdev()。这会使窗口在离屏内存中绘制完成后再一次性刷到屏幕彻底消除复杂界面更新时的闪烁现象。代价是需要额外的RAM和一点绘制时间。合理使用定时器emWin内部的定时器如GUI_TIMER会触发WM_TIMER消息。如果你有需要定期更新的动画如进度条、闪烁光标使用emWin定时器并配合GUI_X_WaitEventTimed比在你自己任务里周期性调用GUI_Exec()更高效。精简GUI_Exec的调用在事件等待模式下GUI_Exec只在被事件唤醒后执行一次。确保你的GUI_X_SignalEvent不会过于频繁地被调用例如触摸按下时连续发送信号。一个常见的优化是在触摸中断中设置一个标志在一个低优先级的“触摸处理任务”中聚合处理再发送一次事件信号给GUI任务。关注窗口管理器WM的无效区域GUI_Exec()的核心工作是重绘所有标记为“无效”Invalid的窗口区域。确保你的应用程序只在界面确实需要更新时才调用WM_InvalidateWindow()避免不必要的重绘计算。