欢迎光临
我们一直在努力

HarmonyOS智慧农业管理应用开发教程--高高种地--第31篇:桌面小组件开发

第31篇:桌面小组件开发

📚 本篇导读

桌面小组件(Form卡片)是HarmonyOS的特色功能,可以在桌面直接显示应用信息,提供快捷操作入口。本篇教程将实现天气卡片、任务提醒卡片等实用小组件。

本篇将实现:

  • 🌤️ 天气卡片(显示当前天气和温度)
  • 📋 任务提醒卡片(显示待办任务)
  • 🌾 节气卡片(显示当前节气和农事建议)
  • 📊 数据统计卡片(显示地块和作物统计)
  • 🔄 卡片更新机制(定时更新、手动刷新)

🎯 学习目标

完成本篇教程后,你将掌握:

  • HarmonyOS Form卡片的开发流程
  • 卡片生命周期管理
  • 卡片数据更新机制
  • 卡片与主应用的交互
  • 卡片布局设计和适配
  • 卡片性能优化

  • 一、Form卡片基础

    1.1 Form卡片概述

    Form卡片是一种轻量级的UI展示形式,具有以下特点:

    • 独立运行:卡片在桌面独立显示,不依赖主应用
    • 快速访问:用户无需打开应用即可查看信息
    • 定时更新:支持定时刷新数据
    • 交互能力:支持点击跳转和简单交互

    1.2 卡片尺寸规格

    HarmonyOS支持多种卡片尺寸:

    尺寸类型宽度高度适用场景
    1×2 2列 1行 简单信息展示
    2×2 2列 2行 常用卡片尺寸
    2×4 4列 2行 详细信息展示
    4×4 4列 4行 复杂内容展示

    1.3 技术架构

    Form卡片系统
    ├── FormExtensionAbility
    │ ├── onAddForm(创建卡片)
    │ ├── onUpdateForm(更新卡片)
    │ ├── onFormEvent(处理事件)
    │ └── onRemoveForm(删除卡片)

    ├── 卡片页面(ETS)
    │ ├── 天气卡片
    │ ├── 任务卡片
    │ └── 节气卡片

    └── 数据服务
    ├── 数据获取
    ├── 数据缓存
    └── 定时更新


    二、配置Form卡片能力

    2.1 module.json5配置

    在 entry/src/main/module.json5 中配置Form扩展能力:

    {
    "module": {
    "extensionAbilities": [
    {
    "name": "EntryFormAbility",
    "srcEntry": "./ets/entryformability/EntryFormAbility.ets",
    "type": "form",
    "exported": true,
    "metadata": [
    {
    "name": "ohos.extension.form",
    "resource": "$profile:form_config"
    }
    ]
    }
    ]
    }
    }

    2.2 创建form_config.json

    在 entry/src/main/resources/base/profile/form_config.json 中定义卡片配置:

    {
    "forms": [
    {
    "name": "WeatherWidget",
    "description": "天气卡片",
    "src": "./ets/widget/pages/WeatherCard.ets",
    "uiSyntax": "arkts",
    "window": {
    "designWidth": 720,
    "autoDesignWidth": true
    },
    "colorMode": "auto",
    "isDefault": true,
    "updateEnabled": true,
    "scheduledUpdateTime": "10:30",
    "updateDuration": 1,
    "defaultDimension": "2*2",
    "supportDimensions": ["2*2", "2*4"]
    },
    {
    "name": "TaskWidget",
    "description": "任务提醒卡片",
    "src": "./ets/widget/pages/TaskCard.ets",
    "uiSyntax": "arkts",
    "window": {
    "designWidth": 720,
    "autoDesignWidth": true
    },
    "colorMode": "auto",
    "isDefault": false,
    "updateEnabled": true,
    "updateDuration": 1,
    "defaultDimension": "2*2",
    "supportDimensions": ["2*2", "2*4"]
    },
    {
    "name": "SolarTermWidget",
    "description": "节气卡片",
    "src": "./ets/widget/pages/SolarTermCard.ets",
    "uiSyntax": "arkts",
    "window": {
    "designWidth": 720,
    "autoDesignWidth": true
    },
    "colorMode": "auto",
    "isDefault": false,
    "updateEnabled": true,
    "scheduledUpdateTime": "08:00",
    "updateDuration": 24,
    "defaultDimension": "2*2",
    "supportDimensions": ["2*2"]
    }
    ]
    }


    三、实现FormExtensionAbility

    3.1 基础框架

    文件位置:entry/src/main/ets/entryformability/EntryFormAbility.ets

    import { formBindingData, FormExtensionAbility, formInfo, formProvider } from '@kit.FormKit';
    import { Want } from '@kit.AbilityKit';
    import { hilog } from '@kit.PerformanceAnalysisKit';

    const TAG = 'EntryFormAbility';
    const DOMAIN = 0x0000;

    export default class EntryFormAbility extends FormExtensionAbility {
    /**
    * 创建卡片时调用
    * @param want 卡片信息
    * @returns FormBindingData 卡片数据
    */

    onAddForm(want: Want) {
    hilog.info(DOMAIN, TAG, '[onAddForm] Form added');

    try {
    // 获取卡片名称
    const formName = want.parameters?.['ohos.extra.param.key.form_name'] as string;
    hilog.info(DOMAIN, TAG, `[onAddForm] Form name: ${formName}`);

    // 根据卡片类型返回不同的数据
    let formData = {};

    switch (formName) {
    case 'WeatherWidget':
    formData = this.getWeatherData();
    break;
    case 'TaskWidget':
    formData = this.getTaskData();
    break;
    case 'SolarTermWidget':
    formData = this.getSolarTermData();
    break;
    default:
    formData = { message: '未知卡片类型' };
    }

    return formBindingData.createFormBindingData(formData);
    } catch (error) {
    hilog.error(DOMAIN, TAG, `[onAddForm] Error: ${JSON.stringify(error)}`);
    return formBindingData.createFormBindingData({});
    }
    }

    /**
    * 卡片更新时调用
    * @param formId 卡片ID
    */

    onUpdateForm(formId: string) {
    hilog.info(DOMAIN, TAG, `[onUpdateForm] Form ${formId} updated`);

    try {
    // 获取更新的数据
    const formData = this.getWeatherData(); // 示例:更新天气数据

    // 更新卡片
    formProvider.updateForm(formId, formBindingData.createFormBindingData(formData));
    } catch (error) {
    hilog.error(DOMAIN, TAG, `[onUpdateForm] Error: ${JSON.stringify(error)}`);
    }
    }

    /**
    * 卡片事件处理
    * @param formId 卡片ID
    * @param message 事件消息
    */

    onFormEvent(formId: string, message: string) {
    hilog.info(DOMAIN, TAG, `[onFormEvent] Form ${formId}, message: ${message}`);

    try {
    const eventData = JSON.parse(message);

    // 处理不同的事件
    switch (eventData.action) {
    case 'refresh':
    this.onUpdateForm(formId);
    break;
    case 'openApp':
    // 打开主应用
    break;
    default:
    hilog.warn(DOMAIN, TAG, `Unknown event: ${eventData.action}`);
    }
    } catch (error) {
    hilog.error(DOMAIN, TAG, `[onFormEvent] Error: ${JSON.stringify(error)}`);
    }
    }

    /**
    * 卡片删除时调用
    * @param formId 卡片ID
    */

    onRemoveForm(formId: string) {
    hilog.info(DOMAIN, TAG, `[onRemoveForm] Form ${formId} removed`);
    // 清理资源
    }

    /**
    * 临时卡片转为正式卡片
    * @param formId 卡片ID
    */

    onCastToNormalForm(formId: string) {
    hilog.info(DOMAIN, TAG, `[onCastToNormalForm] Form ${formId} cast to normal`);
    }

    /**
    * 获取卡片状态
    * @param want 卡片信息
    * @returns FormState 卡片状态
    */

    onAcquireFormState(want: Want) {
    return formInfo.FormState.READY;
    }

    /**
    * 获取天气数据
    */

    private getWeatherData(): Record<string, Object> {
    return {
    temperature: '22°C',
    weather: '晴',
    location: '北京',
    humidity: '65%',
    wind: '东南风 2级',
    updateTime: new Date().toLocaleTimeString('zh-CN', {
    hour: '2-digit',
    minute: '2-digit'
    })
    };
    }

    /**
    * 获取任务数据
    */

    private getTaskData(): Record<string, Object> {
    return {
    totalTasks: 5,
    completedTasks: 2,
    todayTasks: [
    { title: '浇水施肥', time: '09:00' },
    { title: '检查病虫害', time: '14:00' },
    { title: '记录生长情况', time: '17:00' }
    ]
    };
    }

    /**
    * 获取节气数据
    */

    private getSolarTermData(): Record<string, Object> {
    return {
    currentTerm: '立春',
    nextTerm: '雨水',
    daysToNext: 12,
    suggestion: '适宜播种春季作物'
    };
    }
    }


    四、实现天气卡片

    4.1 卡片页面

    文件位置:entry/src/main/ets/widget/pages/WeatherCard.ets

    @Entry
    @Component
    struct WeatherCard {
    @LocalStorageProp('temperature') temperature: string = '–°C';
    @LocalStorageProp('weather') weather: string = '–';
    @LocalStorageProp('location') location: string = '–';
    @LocalStorageProp('humidity') humidity: string = '–';
    @LocalStorageProp('wind') wind: string = '–';
    @LocalStorageProp('updateTime') updateTime: string = '–';

    build() {
    Column() {
    // 头部:位置和更新时间
    Row() {
    Text(`📍 ${this.location}`)
    .fontSize(14)
    .fontColor('#333333')
    .layoutWeight(1)

    Text(this.updateTime)
    .fontSize(12)
    .fontColor('#999999')
    }
    .width('100%')
    .margin({ bottom: 12 })

    // 主要信息:温度和天气
    Row() {
    Column() {
    Text(this.temperature)
    .fontSize(48)
    .fontWeight(FontWeight.Bold)
    .fontColor('#FF6B35')

    Text(this.weather)
    .fontSize(16)
    .fontColor('#666666')
    .margin({ top: 4 })
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)

    // 天气图标
    Text(this.getWeatherIcon(this.weather))
    .fontSize(64)
    }
    .width('100%')
    .margin({ bottom: 16 })

    // 详细信息
    Row() {
    Column({ space: 4 }) {
    Text('💧 湿度')
    .fontSize(12)
    .fontColor('#999999')
    Text(this.humidity)
    .fontSize(14)
    .fontColor('#333333')
    }
    .layoutWeight(1)

    Column({ space: 4 }) {
    Text('🌬️ 风力')
    .fontSize(12)
    .fontColor('#999999')
    Text(this.wind)
    .fontSize(14)
    .fontColor('#333333')
    }
    .layoutWeight(1)
    }
    .width('100%')
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .onClick(() => {
    // 点击卡片打开主应用
    postCardAction(this, {
    action: 'router',
    abilityName: 'EntryAbility',
    params: { page: 'weather' }
    });
    })
    }

    /**
    * 根据天气获取对应的图标
    */

    getWeatherIcon(weather: string): string {
    const iconMap: Record<string, string> = {
    '晴': '☀️',
    '多云': '⛅',
    '阴': '☁️',
    '雨': '🌧️',
    '雪': '❄️',
    '雾': '🌫️'
    };
    return iconMap[weather] || '🌤️';
    }
    }


    五、实现任务提醒卡片

    5.1 卡片页面

    文件位置:entry/src/main/ets/widget/pages/TaskCard.ets

    interface Task {
    title: string;
    time: string;
    }

    @Entry
    @Component
    struct TaskCard {
    @LocalStorageProp('totalTasks') totalTasks: number = 0;
    @LocalStorageProp('completedTasks') completedTasks: number = 0;
    @LocalStorageProp('todayTasks') todayTasks: Task[] = [];

    build() {
    Column() {
    // 头部:标题和统计
    Row() {
    Text('📋 今日任务')
    .fontSize(16)
    .fontWeight(FontWeight.Bold)
    .fontColor('#333333')
    .layoutWeight(1)

    Text(`${this.completedTasks}/${this.totalTasks}`)
    .fontSize(14)
    .fontColor('#4CAF50')
    }
    .width('100%')
    .margin({ bottom: 12 })

    // 进度条
    Progress({ value: this.completedTasks, total: this.totalTasks, type: ProgressType.Linear })
    .width('100%')
    .height(6)
    .color('#4CAF50')
    .backgroundColor('#E0E0E0')
    .margin({ bottom: 16 })

    // 任务列表
    if (this.todayTasks.length > 0) {
    Column({ space: 8 }) {
    ForEach(this.todayTasks.slice(0, 3), (task: Task, index: number) => {
    this.buildTaskItem(task, index);
    })
    }
    .width('100%')
    } else {
    Column() {
    Text('✅')
    .fontSize(32)
    .margin({ bottom: 8 })
    Text('暂无待办任务')
    .fontSize(14)
    .fontColor('#999999')
    }
    .width('100%')
    .layoutWeight(1)
    .justifyContent(FlexAlign.Center)
    }
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .onClick(() => {
    postCardAction(this, {
    action: 'router',
    abilityName: 'EntryAbility',
    params: { page: 'tasks' }
    });
    })
    }

    @Builder
    buildTaskItem(task: Task, index: number) {
    Row() {
    Text(`${index + 1}`)
    .fontSize(12)
    .fontColor('#FFFFFF')
    .width(20)
    .height(20)
    .textAlign(TextAlign.Center)
    .backgroundColor('#2196F3')
    .borderRadius(10)
    .margin({ right: 8 })

    Column({ space: 2 }) {
    Text(task.title)
    .fontSize(14)
    .fontColor('#333333')
    .maxLines(1)
    .textOverflow({ overflow: TextOverflow.Ellipsis })

    Text(task.time)
    .fontSize(12)
    .fontColor('#999999')
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)
    }
    .width('100%')
    .padding(8)
    .backgroundColor('#F5F5F5')
    .borderRadius(8)
    }
    }


    六、实现节气卡片

    6.1 卡片页面

    文件位置:entry/src/main/ets/widget/pages/SolarTermCard.ets

    @Entry
    @Component
    struct SolarTermCard {
    @LocalStorageProp('currentTerm') currentTerm: string = '–';
    @LocalStorageProp('nextTerm') nextTerm: string = '–';
    @LocalStorageProp('daysToNext') daysToNext: number = 0;
    @LocalStorageProp('suggestion') suggestion: string = '–';

    build() {
    Column() {
    // 当前节气
    Column({ space: 8 }) {
    Text('🌾 当前节气')
    .fontSize(14)
    .fontColor('#999999')

    Text(this.currentTerm)
    .fontSize(32)
    .fontWeight(FontWeight.Bold)
    .fontColor('#4CAF50')
    }
    .width('100%')
    .margin({ bottom: 16 })

    // 下一个节气
    Row() {
    Column({ space: 4 }) {
    Text('下一节气')
    .fontSize(12)
    .fontColor('#999999')
    Text(this.nextTerm)
    .fontSize(16)
    .fontColor('#333333')
    }
    .alignItems(HorizontalAlign.Start)
    .layoutWeight(1)

    Column({ space: 4 }) {
    Text('距离')
    .fontSize(12)
    .fontColor('#999999')
    Text(`${this.daysToNext}`)
    .fontSize(16)
    .fontColor('#FF9800')
    }
    .alignItems(HorizontalAlign.End)
    }
    .width('100%')
    .padding(12)
    .backgroundColor('#F5F5F5')
    .borderRadius(8)
    .margin({ bottom: 16 })

    // 农事建议
    Column({ space: 8 }) {
    Text('💡 农事建议')
    .fontSize(12)
    .fontColor('#999999')
    .width('100%')

    Text(this.suggestion)
    .fontSize(14)
    .fontColor('#333333')
    .maxLines(2)
    .textOverflow({ overflow: TextOverflow.Ellipsis })
    .width('100%')
    }
    .width('100%')
    }
    .width('100%')
    .height('100%')
    .padding(16)
    .backgroundColor('#FFFFFF')
    .borderRadius(16)
    .onClick(() => {
    postCardAction(this, {
    action: 'router',
    abilityName: 'EntryAbility',
    params: { page: 'solarTerm' }
    });
    })
    }
    }


    七、卡片数据更新

    7.1 定时更新

    在 FormExtensionAbility 中实现定时更新:

    import { formProvider } from '@kit.FormKit';

    /**
    * 更新所有卡片
    */

    async updateAllForms(): Promise<void> {
    try {
    // 获取所有卡片ID
    const formIds = await formProvider.getAllFormsInfo();

    for (const formInfo of formIds) {
    await this.updateFormById(formInfo.formId);
    }
    } catch (error) {
    hilog.error(DOMAIN, TAG, `[updateAllForms] Error: ${JSON.stringify(error)}`);
    }
    }

    /**
    * 更新指定卡片
    */

    async updateFormById(formId: string): Promise<void> {
    try {
    // 获取最新数据
    const formData = this.getWeatherData();

    // 更新卡片
    await formProvider.updateForm(formId, formBindingData.createFormBindingData(formData));

    hilog.info(DOMAIN, TAG, `[updateFormById] Form ${formId} updated successfully`);
    } catch (error) {
    hilog.error(DOMAIN, TAG, `[updateFormById] Error: ${JSON.stringify(error)}`);
    }
    }

    7.2 手动刷新

    在卡片中添加刷新按钮:

    @Entry
    @Component
    struct WeatherCard {
    // … 其他代码

    build() {
    Column() {
    // 头部添加刷新按钮
    Row() {
    Text(`📍 ${this.location}`)
    .fontSize(14)
    .fontColor('#333333')
    .layoutWeight(1)

    // 刷新按钮
    Text('🔄')
    .fontSize(16)
    .onClick(() => {
    postCardAction(this, {
    action: 'message',
    params: { action: 'refresh' }
    });
    })
    }
    .width('100%')

    // … 其他内容
    }
    }
    }

    7.3 后台更新服务

    创建后台任务定期更新卡片数据:

    import { backgroundTaskManager } from '@kit.BackgroundTasksKit';

    /**
    * 启动后台更新任务
    */

    async startBackgroundUpdate(): Promise<void> {
    try {
    // 申请长时任务
    const bgMode = backgroundTaskManager.BackgroundMode.DATA_TRANSFER;
    await backgroundTaskManager.requestSuspendDelay('formUpdate', () => {
    // 更新卡片数据
    this.updateAllForms();
    });
    } catch (error) {
    hilog.error(DOMAIN, TAG, `[startBackgroundUpdate] Error: ${JSON.stringify(error)}`);
    }
    }


    八、卡片与主应用交互

    8.1 从卡片跳转到主应用

    在卡片中使用 postCardAction 跳转:

    // 跳转到指定页面
    postCardAction(this, {
    action: 'router',
    abilityName: 'EntryAbility',
    params: {
    page: 'weather',
    data: { location: this.location }
    }
    });

    8.2 在主应用中处理跳转

    在 EntryAbility.ets 中处理卡片跳转:

    onNewWant(want: Want, launchParam: AbilityConstant.LaunchParam): void {
    hilog.info(0x0000, 'EntryAbility', 'onNewWant called');

    // 获取卡片传递的参数
    const params = want.parameters;
    if (params) {
    const page = params['page'] as string;
    const data = params['data'];

    // 根据参数跳转到对应页面
    if (page === 'weather') {
    // 跳转到天气页面
    router.pushUrl({
    url: 'pages/Weather/WeatherPage',
    params: data
    });
    } else if (page === 'tasks') {
    // 跳转到任务页面
    router.pushUrl({
    url: 'pages/Task/TaskListPage'
    });
    }
    }
    }


    九、卡片性能优化

    9.1 数据缓存

    避免频繁请求数据,使用缓存机制:

    class FormDataCache {
    private static cache: Map<string, { data: any, timestamp: number }> = new Map();
    private static CACHE_DURATION = 5 * 60 * 1000; // 5分钟

    /**
    * 获取缓存数据
    */

    static get(key: string): any | null {
    const cached = this.cache.get(key);
    if (!cached) return null;

    const now = Date.now();
    if (now cached.timestamp > this.CACHE_DURATION) {
    this.cache.delete(key);
    return null;
    }

    return cached.data;
    }

    /**
    * 设置缓存数据
    */

    static set(key: string, data: any): void {
    this.cache.set(key, {
    data: data,
    timestamp: Date.now()
    });
    }
    }

    9.2 减少更新频率

    合理设置更新间隔,避免过于频繁:

    {
    "updateEnabled": true,
    "updateDuration": 1, // 最小1小时
    "scheduledUpdateTime": "10:30" // 定时更新
    }

    9.3 优化布局性能

    • 减少嵌套层级
    • 使用固定尺寸
    • 避免复杂动画
    • 优化图片资源

    十、实操练习

    10.1 添加卡片到桌面

  • 长按桌面空白处,进入编辑模式
  • 点击添加小组件,找到"高高种地"
  • 选择卡片类型(天气、任务、节气)
  • 调整卡片位置和大小
  • 10.2 测试卡片功能

  • 查看数据显示,验证信息正确性
  • 点击刷新按钮,测试手动更新
  • 点击卡片,测试跳转功能
  • 等待自动更新,验证定时刷新
  • 10.3 调试卡片

    使用DevEco Studio的卡片调试功能:

  • 运行配置中选择"Form"
  • 选择要调试的卡片
  • 查看日志输出
  • 断点调试代码

  • 十一、常见问题

    11.1 卡片不显示

    问题:添加卡片后显示空白

    解决方案:

  • 检查 form_config.json 配置
  • 验证卡片页面路径
  • 确认数据绑定正确
  • 查看日志错误信息
  • 11.2 卡片不更新

    问题:卡片数据长时间不刷新

    解决方案:

  • 检查 updateEnabled 配置
  • 验证更新逻辑实现
  • 确认后台任务权限
  • 测试手动刷新功能
  • 11.3 卡片点击无响应

    问题:点击卡片没有跳转

    解决方案:

  • 检查 postCardAction 调用
  • 验证 abilityName 正确
  • 确认主应用处理逻辑
  • 查看跳转参数传递
  • 11.4 卡片布局错乱

    问题:不同尺寸下布局异常

    解决方案:

  • 使用响应式布局
  • 适配不同尺寸规格
  • 测试各种屏幕密度
  • 优化布局代码

  • 十二、本篇小结

    本篇教程实现了完整的桌面小组件功能,包括:

    ✅ 天气卡片:显示实时天气信息 ✅ 任务卡片:展示待办任务列表 ✅ 节气卡片:提供节气和农事建议 ✅ 数据更新:定时和手动刷新机制 ✅ 交互功能:卡片点击跳转到主应用

    核心技术点:

    • FormExtensionAbility 生命周期
    • Form卡片配置和开发
    • 卡片数据绑定和更新
    • 卡片与主应用交互
    • 后台任务和定时更新

    最佳实践:

    • 合理设置更新频率
    • 使用数据缓存机制
    • 优化卡片性能
    • 提供友好的交互体验
    • 适配多种卡片尺寸

    下一篇预告: 第32篇将讲解应用测试、优化与调试,包括单元测试、性能优化、内存管理等内容。


    📖 参考资料

    • HarmonyOS Form卡片开发
    • FormExtensionAbility
    • 卡片数据交互
    赞(0)
    未经允许不得转载:171主机测评 » HarmonyOS智慧农业管理应用开发教程--高高种地--第31篇:桌面小组件开发
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址