1. 项目拆解与进阶路线
最近我在整理一个鸿蒙PC端的计数统计小工具,整个项目从最简单的“加减一”计数器开始,一路扩展到步长选择、目标进度、历史记录列表、弹窗确认、数据持久化,最后集成了十来个ArkUI常用组件,跑在宽屏窗口下体验已经很接近原生桌面工具。这个从计数器到多功能组件集成的过程,恰好把鸿蒙开发里最核心的几块内容都串了一遍:ArkTS声明式写法、状态与UI更新、自定义组件封装、常用容器组件组合、Preferences本地存储、PC窗口适配。
这篇文章就把这条进阶路线完整拆开来讲。适合两类人:一类是已经能把“Hello World”和官方计数器跑起来、但不知道下一步该学什么的初级开发者;另一类是准备做鸿蒙PC版工具类应用、想快速确认技术方案是否可行的移动端或前端开发者。项目本身不复杂,但我会把每一步“为什么这么设计”说明白,也会把我在实际工程里踩过的坑直接标出来,这些内容通常不会写在官方Demo里。
1.1 为什么拿计数器当项目主线
官方模板里就有计数器Demo,很多人跑完之后就丢到一边,觉得太简单、没有工程参考价值。但计数器其实是一个非常适合做进阶演练的“最小业务闭环”。它内部有明确的数据状态,比如当前数值、步长、目标值;有用户交互,比如点击按钮、输入配置;有结果反馈,比如进度百分比、历史记录。把这几件事梳理好,跟做一个正经桌面工具需要考虑的事情几乎没有差别。
更重要的是,计数器项目的状态流转非常清晰,好调试、好验证。在鸿蒙这种声明式UI框架里,最核心的机制就是“状态变化驱动界面更新”。计数器加一减一,界面上数字必须跟着变,这就是状态管理最基本也最典型的场景。从这个小点上把@State、数据流、组件刷新机制搞透,后面再去看复杂页面,思路会顺很多。
我通常建议团队里的新人先用计数器把整套工具链走通:建工程、调UI、处理交互、做持久化、上调试器、跑PC宽屏模拟器。这一套流程如果能在半天内走完,后面进入真实业务开发时就不会在基础问题上反复卡住。
1.2 这个进阶项目的最终形态
纯计数器只能按一下加一,没什么意思,所以我在实际项目里把它升级成了一个“计数工作台”,核心功能包括:
- 主计数区:支持加减、清零,数值范围限制在0到9999,防止溢出和一些无意义的负数。
- 步长配置:不是每次固定加一,而是可以切换1、5、10、50几种常用步长,用下拉组件实现。
- 目标进度:设置一个目标值,主界面上用进度条显示完成比例。
- 操作历史:每产生一次有效计数操作,记录一条带时间戳和当前值的历史。这个历史列表用List组件渲染,支持一键清空。
- 本地保存:应用重启后,计数值、步长、目标值、历史记录都能恢复,不丢数据。
- PC宽屏适配:窗口宽度足够时,主界面显示为左侧导航、右侧内容的两栏结构,窄窗口下自动变成单栏堆叠。
功能看着不少,但每个功能点都是ArkUI原生组件和状态API的直接应用。整个过程做下来大概800行不到的ArkTS代码,拆解清楚之后,读者完全可以自己敲一遍。
1.3 关键词与阅读路径
“鸿蒙”“PC”“组件集成”“计数器”这几个关键词是整篇文章的核心。如果你是第一次接触鸿蒙开发,建议先看第2章,把开发环境、工程结构搞定;如果你已经跑通过简单页面,直接跳到第3章开始跟随代码走;如果你只关心PC窗口适配和组件排坑,重点看第6章和第7章即可。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 环境准备与PC形态工程初始化
2.1 版本选择与工具链说明
进行鸿蒙应用开发前,先确认要使用的API版本。截至我写这个项目时,DevEco Studio稳定版已经能支持API 12以上的HarmonyOS NEXT应用开发,这套工具链对应的是ArkTS和ArkUI的声明式开发范式。不再建议使用老的基于Java的UI框架或FA模型写新项目,除非在维护遗留代码。
我这次用的组合比较简单:
- DevEco Studio 5.0系列,从官方渠道下载对应平台的安装包。
- SDK方面安装API 12及以上版本。
- 语言以ArkTS为主,UI用ArkUI声明式写法。
- 工程模型选Stage模型,这是当前推荐主模型。
这里多说一句,很多刚接触的朋友会把“兼容安卓APK”和鸿蒙原生开发混在一起。如果目标是把应用跑在纯粹的鸿蒙PC设备上,应该直接使用ArkTS原生工程,不要引入安卓兼容依赖,否则后续组件适配、权限控制和性能调优都会很别扭。
2.2 创建空白工程并声明设备类型
打开DevEco Studio后,选择新建工程,模板选Empty Ability,语言选ArkTS。如果你要直接做纯鸿蒙PC形态应用,创建完成之后先不要急着写页面,先检查module.json5里的设备类型声明。
在默认模板中,module.json5的deviceTypes字段通常包含phone和tablet,想支持PC、平板二合一形态时,需要把2in1也加进去,形如:
json复制{
"module": {
"name": "entry",
"type": "entry",
"deviceTypes": [
"phone",
"tablet",
"2in1"
]
}
}
这个字段跟你后续在模拟器、真机上能否正常拉起应用直接相关。如果没把2in1加进去,即使你的界面已经做了大屏适配,在PC形态设备上也可能出现安装后无法启动、或者跑起来还是竖屏比例的问题。
创建完工程后,建议先把默认的pages/Index.ets跑一遍。用DevEco Studio自带的模拟器或者把应用签名安装到支持2in1形态的设备上都可以。确认环境没问题再进入后续开发。
2.3 窗口尺寸监听:给PC布局提供依据
桌面应用跟手机应用的第一个明显差异就是窗口大小不固定。用户可能把窗口拖到1920像素宽,也可能拖成1100像素宽,界面不能像手机那样只按固定竖屏尺寸设计。所以工程初始化时,我建议把窗口宽度变化同步到一个全局状态里,页面后续根据这个值做响应式布局。
在EntryAbility.ets中,原本会有一段加载首页页面的代码,可以在此基础上增加窗口尺寸监听:
typescript复制onWindowStageCreate(windowStage: window.WindowStage): void {
windowStage.loadContent('pages/Index', (err) => {
if (err.code) {
return;
}
});
windowStage.getMainWindow((err, win) => {
if (err) {
console.error('getMainWindow failed');
return;
}
const winProperty = win.getWindowProperties();
AppStorage.setOrCreate<number>('windowWidth', winProperty.windowRect.width);
win.on('windowSizeChange', (size) => {
AppStorage.setOrCreate<number>('windowWidth', size.width);
});
});
}
之后在页面组件里用@StorageLink('windowWidth')去读取这个值,窗口宽度一变,页面里用到这个变量的布局就会被自动驱动更新。这个思路跟响应式网页设计很接近,代码量也不大,属于PC应用的基建工作。
3. 计数器核心页面与状态驱动
3.1 先用一个页面完成最小闭环
进入pages/Index.ets,先别急着做复杂的组件拆分,把最原始的计数器模型跑通。一个基础计数器页面需要三块内容:一个显示数值的Text,一个减一按钮,一个加一按钮。
typescript复制@Entry
@Component
struct Index {
@State countValue: number = 0;
build() {
Column({ space: 20 }) {
Text(`${this.countValue}`)
.fontSize(72)
.fontWeight(FontWeight.Bold)
.fontColor('#182431')
Row({ space: 16 }) {
Button('减一')
.width(100)
.height(48)
.onClick(() => {
this.countValue--;
})
Button('加一')
.width(100)
.height(48)
.onClick(() => {
this.countValue++;
})
}
}
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
}
}
这里最值得关注的是@State装饰器。countValue被@State标记后,ArkUI就会建立一条状态到UI的绑定关系。按钮点击时执行this.countValue++,框架检测到状态值改变后,自动找到依赖这个状态的Text组件并更新显示内容。开发者不需要手动去查DOM、改文本,只维护数据本身即可,这是声明式UI框架的基本心智模型。
3.2 加上边界保护,计数器才不会失控
真实工具里的计数器不可能没有限制。如果你在做一个库存计数、样品统计或者产能记录工具,负数可能没有业务意义,数值无限增长也会导致界面溢出或后续计算异常。所以核心的计数逻辑建议统一收敛到一个方法里,不要在每个按钮的onClick里直接写加减。
typescript复制const MIN_COUNT = 0;
const MAX_COUNT = 9999;
private changeCount(delta: number): void {
const next = this.countValue + delta;
if (next < MIN_COUNT) {
this.countValue = MIN_COUNT;
return;
}
if (next > MAX_COUNT) {
this.countValue = MAX_COUNT;
return
