在这里插入图片描述

一、PC 开发和平板有什么本质区别

在手机上做鸿蒙开发,窗口大小基本固定、交互全靠触摸、文件操作很少涉及。但到了 PC 上,窗口可以自由缩放和最大化、鼠标有悬停和右键、键盘有快捷键、应用需要读写本地文件系统。这些能力在手机上要么不需要、要么体验很弱,PC 开发本质上是要补全这些桌面场景的能力。

HarmonyOS NEXT 的 PC 应用开发,是在 ArkUI 基础上叠加一套完整的桌面开发能力:窗口管理 API、鼠标键盘事件、文件选择器、本地存储。

二、窗口管理:最大化、最小化、置顶

PC 应用最基础的能力是窗口控制。用户习惯了自己拖动标题栏、点最大化、把窗口置顶,ArkUI 提供了完整的窗口操作 API:

import Window from '@ohos.window'
import promptAction from '@ohos.promptAction'

const mainWindow = Window.getLastWindowSync(getContext(this))

async function minimizeWindow() {
  await mainWindow.minimize()
}

async function toggleMaximize() {
  const isMax = await mainWindow.isMaximized()
  if (isMax) await mainWindow.restore()
  else await mainWindow.maximize()
}

async function setFullScreen(enabled: boolean) {
  await mainWindow.setFullScreen(enabled)
}

async function setAlwaysOnTop(onTop: boolean) {
  await mainWindow.setTopOnOtherEnabled(onTop)
  promptAction.showToast({ message: onTop ? '已置顶' : '已取消置顶', duration: 1500 })
}

自定义标题栏组件,包含上述所有窗口控制能力:

@Builder
CustomTitleBar() {
  Row({ space: 12 }) {
    Row({ space: 8 }) {
      Text('📝').fontSize(20)
      Text('笔记应用').fontSize(15).fontWeight(FontWeight.Medium)
    }
    Blank()
    Row({ space: 4 }) {
      Text('📌').fontSize(16).padding(8).onClick(() => this.setAlwaysOnTop(!this.isOnTop))
      Text('⛶').fontSize(16).padding(8).onClick(() => this.setFullScreen(!this.isFullScreen))
      Text('−').fontSize(18).fontWeight(FontWeight.Bold).padding(8).onClick(() => this.minimizeWindow())
      Text('□').fontSize(16).padding(8).onClick(() => this.toggleMaximize())
      Text('✕').fontSize(14).padding(8).fontColor('#FF4757').onClick(() => mainWindow.close())
    }
  }
  .width('100%').height(44).padding({ left: 16, right: 16 }).backgroundColor('#1A1A2E')
}

在这里插入图片描述

三、窗口监听:实时响应大小变化

PC 应用需要实时响应窗口大小改变。用户拖动窗口边缘,布局要跟着自动调整,不能出现内容溢出或大量空白。通过 window.on('windowSizeChange') 监听窗口尺寸变化:

@State windowWidth: number = 1024
@State windowHeight: number = 768

private windowChangeCallback = (size: Window.Size) => {
  this.windowWidth = px2vp(size.width)
  this.windowHeight = px2vp(size.height)
  console.info(`[Window] ${this.windowWidth}vp × ${this.windowHeight}vp`)
}

aboutToAppear() {
  const win = Window.getLastWindowSync(getContext(this))
  win.on('windowSizeChange', this.windowChangeCallback)
  const props = win.getWindowProperties()
  this.windowWidth = px2vp(props.windowWidth)
  this.windowHeight = px2vp(props.windowHeight)
}

aboutToDisappear() {
  const win = Window.getLastWindowSync(getContext(this))
  win.off('windowSizeChange')  // 必须取消监听,否则内存泄漏
}

最常见的坑:每次 aboutToAppear 都会注册新监听,如果不在 aboutToDisappear 中清理,监听器会越叠越多直到内存溢出。窗口监听必须配对注册和取消,这是硬性规范。

四、鼠标交互:悬停和右键菜单

PC 上鼠标比触摸有更多交互状态——进入、离开、悬停、右键。ArkUI 中这些都有对应 API:

@State hoverText: string = '将鼠标移入蓝色区域'
@State contextMenuVisible: boolean = false
@State contextMenuX: number = 0
@State contextMenuY: number = 0

// 悬停效果
Column()
  .width(240).height(80).backgroundColor('#667eea').borderRadius(12)
  .onHover((isHover: boolean) => {
    animateTo({ duration: 200 }, () => {
      this.hoverText = isHover ? '✅ 鼠标已悬停' : '将鼠标移入蓝色区域'
    })
  })
  .child(Text(this.hoverText).fontSize(13).fontColor('#FFFFFF')
    .width('100%').textAlign(TextAlign.Center))
  .alignItems(HorizontalAlign.Center).justifyContent(FlexAlign.Center)

// 右键菜单(需要自己实现浮层)
if (this.contextMenuVisible) {
  Column({ space: 2 }) {
    ForEach(['📋 复制', '📝 粘贴', '✂️ 剪切', '🗑 删除'], (item: string) => {
      Text(item).fontSize(13).fontColor('#333333')
        .width('100%').padding({ left: 12, top: 8, bottom: 8 })
        .onClick(() => {
          this.contextMenuVisible = false
          promptAction.showToast({ message: `执行: ${item}`, duration: 1500 })
        })
    })
  }
  .width(160).backgroundColor('#FFFFFF').borderRadius(8)
  .shadow({ radius: 8, color: 'rgba(0,0,0,0.15)', offsetX: 2, offsetY: 2 })
  .position({ x: this.contextMenuX, y: this.contextMenuY })
}

在这里插入图片描述

ArkUI 没有内置右键菜单组件,需要自己实现:监听右键事件获取坐标,然后渲染浮层。关闭逻辑是监听全局左键点击,菜单外点击时隐藏。

五、键盘快捷键

键盘快捷键是 PC 效率应用的核心。Ctrl+S 保存、Ctrl+N 新建、Ctrl+F 搜索、Escape 关闭——这些约定俗成的交互不容忽视:

function handleKeyDown(event: KeyEvent) {
  if (event.type === KeyType.Down && event.ctrlKey) {
    switch (event.keyCode) {
      case 49:  // Ctrl+S 保存
        this.saveData()
        break
      case 50:  // Ctrl+N 新建
        this.createNewNote()
        break
      case 52:  // Ctrl+F 搜索
        this.focusSearch()
        break
    }
  }
  if (event.type === KeyType.Down && event.keyCode === 2) {  // Escape
    this.closeDialog()
  }
}

// 快捷键提示 UI
@Builder
ShortcutBar() {
  Column({ space: 8 }) {
    Text('⌨️ 快捷键').fontSize(14).fontWeight(FontWeight.Medium).width('100%')
    ForEach([
      { keys: 'Ctrl + S', action: '保存', color: '#667eea' },
      { keys: 'Ctrl + N', action: '新建', color: '#43E97B' },
      { keys: 'Ctrl + F', action: '搜索', color: '#4facfe' },
      { keys: 'Escape', action: '关闭', color: '#FF4757' },
    ], (item) => {
      Row() {
        Text(item.keys).fontSize(12).fontWeight(FontWeight.Medium).fontColor(item.color)
          .padding({ left: 8, right: 8, top: 3, bottom: 3 })
          .backgroundColor(item.color + '20').borderRadius(6)
        Text(item.action).fontSize(13).fontColor('#666666').margin({ left: 12 })
      }.width('100%')
    })
  }
  .width('100%').padding(12).backgroundColor('#FFFFFF').borderRadius(12)
}

六、文件对话框:读写本地文件

PC 应用最核心的能力之一是访问本地文件系统。@ohos.file.picker 负责唤起系统文件选择器,@ohos.file.fs 负责读写文件内容:

import filePicker from '@ohos.file.picker'
import file from '@ohos.file.fs'

// 打开文件选择器
async function pickFile(): Promise<string | null> {
  try {
    const picker = new filePicker.DocumentPicker()
    const result = await picker.select({ multiSelect: false })
    return result?.[0] ?? null
  } catch (e) {
    console.error('File pick failed:', JSON.stringify(e))
    return null
  }
}

// 另存为对话框
async function saveFile(content: string, defaultName: string): Promise<boolean> {
  try {
    const savePath = await picker.save({ newFileName: defaultName })
    if (savePath) {
      await file.writeText(savePath, content)
      promptAction.showToast({ message: '文件已保存', duration: 2000 })
      return true
    }
    return false
  } catch (e) {
    console.error('Save failed:', JSON.stringify(e))
    return false
  }
}

// 读取文件
async function readFile(filePath: string): Promise<string> {
  try {
    return await file.readText(filePath)
  } catch (e) {
    console.error('Read failed:', JSON.stringify(e))
    return ''
  }
}

文件操作按钮组 UI:

@Builder
FileOperationBar() {
  Column({ space: 12 }) {
    Text('📁 文件操作').fontSize(14).fontWeight(FontWeight.Bold).width('100%')
    Row({ space: 10 }) {
      Button('📂 打开', { type: ButtonType.Normal })
        .fontSize(13).height(38).borderRadius(8).backgroundColor('#667eea')
        .onClick(async () => {
          const path = await this.pickFile()
          if (path) promptAction.showToast({ message: `已选择: ${path}`, duration: 2000 })
        })
      Button('💾 导出', { type: ButtonType.Normal })
        .fontSize(13).height(38).borderRadius(8).backgroundColor('#43E97B')
        .onClick(async () => {
          const content = this.notes.map(n => `# ${n.title}\n\n${n.content}\n\n---\n`).join('')
          await this.saveFile(content, 'notes.md')
        })
    }.width('100%')
  }.width('100%').padding(16).backgroundColor('#FFFFFF').borderRadius(12)
}

七、实战建议

PC 应用开发有两条硬性规范:窗口操作必须配对注册和取消、文件访问必须处理异常。这两条踩过一次的坑,基本不会再犯。

窗口控制按钮的视觉设计要和系统风格一致。Windows 平台的窗口控制按钮在右上角,如果应用有自定义标题栏,三个控制按钮的视觉风格要尽量和系统一致,避免用户感到割裂。

右键菜单实现逻辑不复杂,但菜单项不宜过多,通常 4-6 项最佳,层级控制在两层以内。快捷键功能的优先级:Ctrl+S 和 Escape 必须有,是最高频的操作;Ctrl+N 和 Ctrl+F 其次;其他根据业务需求决定。


Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐