使用鸿蒙端云一体化模板快速开发一个显示桌面文案的元服务

作为一个文艺青年,偶尔会有想要表达一下自己心情的需求,这时候鸿蒙系统的桌面卡片就非常合适。

简单设想一下,这是一个常驻桌面、能显示个性文案的元服务。如果用户对文案内容不满意的话可以手动切换文案。由于每次只能单条刷新很不方便,因此还需要一个应用界面,里面能够分类显示很多不同的文案,便于快速选择,只要手动点击某一条文案,就能直接显示到桌面卡片上。

使用端云一体化开发可以很容易的实现这个效果

效果展示:
每日走心文案阶段性功能演示

简单介绍一下端云一体化开发:

作为一个独立开发者,由于时间和开发能力的限制,想要独立开发一个应用程序其实还是挺难的,微博、B站那种功能复杂又高大上的应用自然不必说,很多时候连简单的应用也会受限于前后端开发的差异,学前端的不会写后端、不会配置服务器,学后端的搞不懂前端三件套,让人搞得一头雾水,项目未半而中道崩卒。

DevEco的端云一体化开发工具能让应用开发变得非常简单,前后端语法统一,前端用基于JS衍生的ArkTS,后端能直接用Node.js,语法高度相似。
severless不需要手动安装各种服务器软件,也不需要进行大量复杂难懂的系统配置。直接新建项目就能让客户端自动连接上severless,并且服务器常用的登录、后端环境、数据库、文件存储系统、日志系统他都有。突出一个简单快捷,傻瓜式操作。

想法有了,开始分析项目:


简单的分析完这个项目,就可以动手操作啦

  1. 第一步:在AGC中新建项目,新建应用。

    如果不会创建端云一体化工程的话可以参考一下这篇文章:【纯新手向】手把手带你使用模板创建第一个端云一体化元服务

  2. 第二步:按照项目需求开通对应的服务。

    • 由于这个案例非常的简单,登陆服务、云函数、云存储都用不上,只要有一个云数据库就够了。在这里就只开通云数据库。

    • 完成云数据库的基本配置

    • 存储区命名为sloganZone

    • 云数据配置好以后导出对象类型的js文件。后续会用到。

    • 手动录入一些数据以便后续开发中进行测试

  3. 配置完成后新建本地工程。(强烈建议新手先完成severless的各种配置再新建本地项目,因为建立本地项目时会自动下载与severless配置情况对应的agconnect-services.json与schema.json文件。改动severless配置后需要手动更新这两个文件,否则可能会无法运行。

    • 删除不需要的程序文件,开始编写我们自己的代码。
      • ets目录下除了EntryAbility、EntryFormAbility和WidgetCard以外的文件可以全部删除。
      • 将之前下载的对象类型文件(dailySlogan.js)复制进来,后续查询云数据库必须要用到它。
      • 重建一个Index.ets文件。

    最终的项目结构如下:

  4. 开发桌面卡片的代码

  • 卡片界面WidgetCard.ets的代码
let storage=new LocalStorage()//页面级存储,靠它才能卡片内容的跨文件刷新
@Entry(storage)
@Component
struct WidgetCard {
  @LocalStorageProp('formId') @Watch('sentFormID') formId: string = '0';//保存FormID的变量
  @LocalStorageProp('slogan') slogan:string='我希望有个如你一般的人。如山间清爽的风,如古城温暖的光,由清晨到傍晚,由山野到书房,只要最后是你,就好。'//卡片的默认文案

 sentFormID(){
   console.log('发送formID')
   postCardAction(this,{//call事件,可以后台启动UIAbility并执行预设的事件。
     'action':'call',
     "abilityName": 'EntryAbility',
     'params': {
       'method': 'createFormId',
       'formId': this.formId,
       'detail': ''
     }
   })
 }
  build() {
    Stack() {
      Image($r("app.media.background1"))
        .width('100%')
        .height('100%')
      Column() {
        Row(){
          Image($r("app.media.logo_white"))
            .objectFit(ImageFit.Contain)
            .width('30vp')
          Text('每日走心文案')
            .fontColor(Color.White)
        }
        .height('20%')
        .width('100%')
        .justifyContent(FlexAlign.Start)
        Row(){
          Text(this.slogan)
            .fontColor(Color.White)
            .width('80%')
          Column(){
            Image($r('app.media.freshButton'))
              .height('35%')
              .objectFit(ImageFit.Contain)
              .padding(5)
          }
          .onClick(()=>{
            postCardAction(this,{//message事件,用于启动FormAbility并执行预设的事件。
              'action':'message',
              'params': {
                'msgTest': 'messageEvent'
              }
            })
          })
          .padding(5)
          .margin(5)
          .backgroundColor(Color.Gray)
          .borderRadius(50)
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)
        }
        .height('80%')
        .width('100%')
        .justifyContent(FlexAlign.Center)
      }
      .width('100%')
      .height('100%')
      .padding($r('app.float.column_padding'))
     }
    .width('100%')
    .height('100%')
    .onClick(() => {
      postCardAction(this, {//router事件,可以跳转到UIAbiliity,同时也可以执行UIAbiliity中预设的事件
        "action": "router",
        "abilityName": 'EntryAbility',
        "params": {
          "message": ''
        }
      });
    })
  }
}
  • FormAbility部分EntryFormAbility.ts的关键代码
import formInfo from '@ohos.app.form.formInfo';
import formBindingData from '@ohos.app.form.formBindingData';
import FormExtensionAbility from '@ohos.app.form.FormExtensionAbility';
import formProvider from '@ohos.app.form.formProvider';
import agconnect from '@hw-agconnect/api-ohos';
import { AGConnectCloudDB, CloudDBZone, CloudDBZoneConfig, CloudDBZoneQuery } from '@hw-agconnect/database-ohos';

import {dailySlogan} from'../models/dailySlogan'


export default class EntryFormAbility extends FormExtensionAbility {
  objectTypeInfo=null
  cloudZone:CloudDBZone=null
  cloud:AGConnectCloudDB=null


  onAddForm(want) {//want中包含了FormId等信息,在卡片创建时读取并更新,以便卡片执行call时使用
    let formId = want.parameters["ohos.extra.param.key.form_identity"];
    let dataObj1 = {
      "formId": formId
    };
    let obj1 = formBindingData.createFormBindingData(dataObj1);
    console.log('formId向卡片发送'+formId)
    return obj1;
  }

  async onFormEvent(formId, message) {//当message事件触发时,查询数据库并刷新卡片
   // Called when a specified message event defined by the form provider is triggered.

    try {
      agconnect.instance().init(this.context)//初始化agc
    }
    catch (error){}
    
    try {
      await  AGConnectCloudDB.initialize(this.context)//初始化AGConnectCloudDB
    }
    catch (error){}
    
    if (this.cloud == null) {
      this.cloud = await AGConnectCloudDB.getInstance()//获取实例
    }
    if (this.objectTypeInfo == undefined) {//查询并构建云数据库的数据类型
      const value = await this.context.resourceManager.getRawFile('schema.json');
      let json = "";
      for (var i = 0; i < value.length; i++) {
        json += String.fromCharCode(value[i]);
      }
      this.objectTypeInfo = JSON.parse(json);
      this.cloud.createObjectType(this.objectTypeInfo);
    }
    
    
    if (this.cloudZone == null) {
      this.cloudZone = await this.cloud.openCloudDBZone(new CloudDBZoneConfig("sloganZone"))//打开存储区
    }
    
    
    const sql = CloudDBZoneQuery.where(dailySlogan)
      .limit(1, Math.floor(Math.random() * 9)) //用于查询的sql语句
     
    this.cloudZone.executeQuery(sql).then((sloganInfo) => {
      let info:dailySlogan[]=sloganInfo.getSnapshotObjects()//处理查询结果
      let slogan=info[0].Slogan
      let formData = {
        'slogan': slogan, // 此处需要和卡片页面的变量相对应
      };
      let formInfo = formBindingData.createFormBindingData(formData)
      formProvider.updateForm(formId, formInfo).then((data) => {//刷新卡片
        console.info('FormAbility updateForm success.' + JSON.stringify(data));
      }).catch((error) => {
        console.error('FormAbility updateForm failed: ' + JSON.stringify(error));
      })
    }).catch((error) => {
      console.error('数据查询失败: ' + JSON.stringify(error));
    })
  }

  onRemoveForm(formId) {
    // Called to notify the form provider that a specified form has been destroyed.

  }

  onAcquireFormState(want) {
    // Called to return a {@link FormState} object.
    return formInfo.FormState.READY;
  }
};
  • 另外为了显示效果,我们需要把卡片尺寸修改为2*4
  1. 开发应用主页的代码
  • page部分Index.ets文件的代码
import agconnect from '@hw-agconnect/api-ohos';
import { AGConnectCloudDB, CloudDBZone, CloudDBZoneConfig, CloudDBZoneQuery } from '@hw-agconnect/database-ohos';
import {dailySlogan} from'../models/dailySlogan'
import formBindingData from '@ohos.app.form.formBindingData';
import formProvider from '@ohos.app.form.formProvider';
import formInfo from '@ohos.app.form.formInfo';

@Entry
@Component
struct  homepage{

  @State sloganList:Array<string>=[]//保存文案列表的变量
  objectTypeInfo=null
  cloudZone:CloudDBZone=null
  cloud:AGConnectCloudDB=null

  aboutToAppear(){
    this.fresh()
  }

  async fresh(){//用于查询新文案的代码,与FormAbility中的基本一致
    try {
      try {
        agconnect.instance().init(getContext(this))
      }
      catch (error){}
      try {
        await  AGConnectCloudDB.initialize(getContext(this))
      }
      catch (error){}
      if (this.cloud == null) {
        this.cloud = await AGConnectCloudDB.getInstance()
      }
      if (this.objectTypeInfo == undefined) {
        const value = await getContext(this).resourceManager.getRawFile('schema.json');
        let json = "";
        for (var i = 0; i < value.length; i++) {
          json += String.fromCharCode(value[i]);
        }
        this.objectTypeInfo = JSON.parse(json);
        this.cloud.createObjectType(this.objectTypeInfo);
      }
      if (this.cloudZone == null) {
        this.cloudZone = await this.cloud.openCloudDBZone(new CloudDBZoneConfig("sloganZone"))
      }
      const sql = CloudDBZoneQuery.where(dailySlogan)
        .limit(4, Math.floor(Math.random() * 9)) 
      this.cloudZone.executeQuery(sql).then((sloganInfo) => {
        let info: dailySlogan[] = sloganInfo.getSnapshotObjects()
        this.sloganList = []
        for (var i = 0;i < info.length; i++) {
          this.sloganList.push(info[i].Slogan)
        }
      }).catch((error) => {
        console.error('数据查询失败: ' + JSON.stringify(error));
      })
    }
    catch (error){
        console.error('刷新异常: ' + JSON.stringify(error));
    }
  }
  build(){

    List(){
      ListItem(){
       Button('刷新文案')
        .onClick(()=>{
          this.fresh()
        })
      }
      ForEach(this.sloganList,(listInfo)=>{
        ListItem(){
          Column(){
            Text(listInfo)
          }
          .width('90%')
          .height('20%')
          .backgroundImage($r('app.media.background1'))
          .margin({top:5,bottom:5})
          .justifyContent(FlexAlign.Center)
          .alignItems(HorizontalAlign.Center)
          .onClick(async()=>{//判断是否存在卡片并根据用户点击的文案来刷新这些卡片


            if(AppStorage.Has('formId')){
              let formIdList:Array<string>=AppStorage.Get('formId')
              let formData = {
                "slogan": listInfo
              };
              let formMsg = formBindingData.createFormBindingData(formData)
               console.log('开始刷新')

              for(let i=0;i<formIdList.length;i++){
                try {
                  await formProvider.updateForm(formIdList[i], formMsg)
                }
               catch (error){
                 if(error.code==16501001){//formId都不存在的错误码
                   console.log('formID是'+formIdList[i])
                   formIdList[i]=' '
                 }
               }
              }

              let newFormIdList=[]
              for(var i=0;i<formIdList.length;i++){
              if(formIdList[i]!=' '){
                console.log('检测成功的formID是'+formIdList[i])
                newFormIdList.push(formIdList[i])
              }else{
                console.log(i+'id值已清空'+formIdList[i])
              }
              }
              if(newFormIdList.length==0){
                console.log('卡片已被清空')
                AppStorage.Delete('formId')
                AlertDialog.show({
                  message:'卡片不存在,请重新添加卡片'
                })
              }
              else{
                AppStorage.SetOrCreate('formId',newFormIdList)
                AlertDialog.show({
                  message:'卡片刷新已完成'
                })
              }

            }else {
              AlertDialog.show({
                message:'请先添加卡片'
              })
            }

          })
        }
      },)
    }
    .width('100%')
    .height('100%')
    .alignListItem(ListItemAlign.Center)
  }
}



  • UIAbility部分EntryAbility.ts文件的关键代码
import hilog from '@ohos.hilog';
import UIAbility from '@ohos.app.ability.UIAbility';
import Window from '@ohos.window';
import abilityAccessCtrl from '@ohos.abilityAccessCtrl';
import formInfo from '@ohos.app.form.formInfo';
import formBindingData from '@ohos.app.form.formBindingData';
import formProvider from '@ohos.app.form.formProvider';

function saveFormId(data){//保存新增的FormId的函数
  console.log('监听事件已激活')
  let FormIdList=[]
  if(AppStorage.Has('formId')){
    FormIdList=AppStorage.Get('formId')
  }

  let params = JSON.parse(data.readString())
  if (params.formId !== undefined) {
    let FormId = params.formId;
    console.log('读取到formId')
    FormIdList.push(FormId)

    AppStorage.SetOrCreate('formId', FormIdList)

    for(var i=0;i<FormIdList.length;i++){
      console.log('formID保存成功')
      console.log(FormIdList[i])
    }
  }
  else {
    console.log('formId读取失败')
  }
  return null
}


export default class EntryAbility extends UIAbility {
  onCreate() {
    this.callee.on('createFormId',saveFormId);//应用启动时监听call事件

    let AtManager = abilityAccessCtrl.createAtManager();
    AtManager.requestPermissionsFromUser(this.context, ['ohos.permission.READ_MEDIA', 'ohos.permission.MEDIA_LOCATION']).then((data) => {
      hilog.info(0x0000, 'testTag', '%{public}s', 'request permissions from user success' + data);
    }).catch((err) => {
      hilog.error(0x0000, 'testTag', 'Failed to request permissions from user. Cause: %{public}s', JSON.stringify(err) ?? '');
    });
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onCreate');
  }

  onDestroy() {
    hilog.info(0x0000, 'testTag', '%{public}s', 'Ability onDestroy');
    this.callee.off('createFormId');//应用销毁时解除监听call事件
  }
}

   
  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 6
    评论
Python是一种高级编程语言,而OpenCV是一个流行的计算机视觉库。这两个工具的结合可以让我们实现一些有趣的应用程序,例如电脑音量控制。 在这篇文案中,我们将介绍如何使用Python和OpenCV开发一个电脑音量控制器的代码。这个程序将会使用摄像头捕获用户的手势,然后根据手势的位置和动作来控制电脑的音量。 首先,我们需要安装Python和OpenCV的相关库。你可以使用pip来安装它们: ``` pip install opencv-python pip install numpy pip install pycaw ``` 接下来,我们需要创建一个Python脚本来实现音量控制的功能。这个脚本将会使用OpenCV来捕获摄像头的视频流,并使用numpy库来处理图像。我们还需要使用pycaw库来控制电脑的音量。 下面是代码示例: ```python import cv2 import numpy as np from pycaw.pycaw import AudioUtilities, ISimpleAudioVolume cap = cv2.VideoCapture(0) devices = AudioUtilities.GetSpeakers() interface = devices.Activate(ISimpleAudioVolume._iid_, CLSCTX_ALL, None) volume = interface.QueryInterface(ISimpleAudioVolume) while True: ret, frame = cap.read() frame = cv2.flip(frame, 1) frame = cv2.resize(frame, (640, 480)) hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV) lower_skin = np.array([0, 20, 70], dtype=np.uint8) upper_skin = np.array([20, 255, 255], dtype=np.uint8) mask = cv2.inRange(hsv, lower_skin, upper_skin) mask = cv2.medianBlur(mask, 5) contours, _ = cv2.findContours(mask, cv2.RETR_TREE, cv2.CHAIN_APPROX_SIMPLE) if len(contours) > 0: contour = max(contours, key=cv2.contourArea) if cv2.contourArea(contour) > 10000: x, y, w, h = cv2.boundingRect(contour) cv2.rectangle(frame, (x, y), (x+w, y+h), (0, 255, 0), 2) cx, cy = x+w//2, y+h//2 if cy < 150: volume.SetMasterVolumeLevelScalar(1.0, None) elif cy > 350: volume.SetMasterVolumeLevelScalar(0.0, None) else: volume.SetMasterVolumeLevelScalar((cy-150)/200, None) cv2.imshow('frame', frame) if cv2.waitKey(1) == ord('q'): break cap.release() cv2.destroyAllWindows() ``` 在这个代码中,我们首先使用OpenCV从摄像头捕获视频流。然后,我们将图像转换为HSV格式,并根据手的肤色范围创建一个二进制掩码。接下来,我们对掩码进行中值模糊和轮廓检测,以获取手部区域的轮廓。如果轮廓面积大于10000像素,则我们将手的位置和动作映射到音量控制器上。 当手放在屏幕的顶部时,音量将被设置为最大值;当手放在屏幕的底部时,音量将被设置为最小值。当手在屏幕中间时,音量将根据手的位置进行线性插值。 最后,我们使用pycaw库来控制电脑的音量。这个库可以让我们获取电脑的音量控制接口,并使用SetMasterVolumeLevelScalar()方法来设置音量的值。 在完成这个程序的编写后,我们可以执行它来测试它的功能。当我们用手指向屏幕的不同位置时,我们应该能够听到音量的变化。 总之,Python和OpenCV的结合可以让我们实现一些有趣的应用程序。通过使用这个简单的音量控制器示例,我们可以了解到如何使用OpenCV来处理图像,如何使用pycaw库来控制电脑的音量。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 6
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值