HarmonyOS智慧农业管理应用开发教程--高高种地--第31篇:桌面小组件开发
独立运行:卡片在桌面独立显示,不依赖主应用快速访问:用户无需打开应用即可查看信息定时更新:支持定时刷新数据交互能力:支持点击跳转和简单交互。
第31篇:桌面小组件开发
📚 本篇导读
桌面小组件(Form卡片)是HarmonyOS的特色功能,可以在桌面直接显示应用信息,提供快捷操作入口。本篇教程将实现天气卡片、任务提醒卡片等实用小组件。
本篇将实现:
- 🌤️ 天气卡片(显示当前天气和温度)
- 📋 任务提醒卡片(显示待办任务)
- 🌾 节气卡片(显示当前节气和农事建议)
- 📊 数据统计卡片(显示地块和作物统计)
- 🔄 卡片更新机制(定时更新、手动刷新)
🎯 学习目标
完成本篇教程后,你将掌握:
- HarmonyOS Form卡片的开发流程
- 卡片生命周期管理
- 卡片数据更新机制
- 卡片与主应用的交互
- 卡片布局设计和适配
- 卡片性能优化
一、Form卡片基础
1.1 Form卡片概述
Form卡片是一种轻量级的UI展示形式,具有以下特点:
- 独立运行:卡片在桌面独立显示,不依赖主应用
- 快速访问:用户无需打开应用即可查看信息
- 定时更新:支持定时刷新数据
- 交互能力:支持点击跳转和简单交互
1.2 卡片尺寸规格
HarmonyOS支持多种卡片尺寸:
| 尺寸类型 | 宽度 | 高度 | 适用场景 |
|---|---|---|---|
| 1x2 | 2列 | 1行 | 简单信息展示 |
| 2x2 | 2列 | 2行 | 常用卡片尺寸 |
| 2x4 | 4列 | 2行 | 详细信息展示 |
| 4x4 | 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篇将讲解应用测试、优化与调试,包括单元测试、性能优化、内存管理等内容。
📖 参考资料
更多推荐


所有评论(0)