- 跨平台
- UI组件
- 桌面应用
【免费下载链接】toga
A Python native, OS native GUI toolkit.
本指南围绕仓库中的 hardware 示例应用 及其配套 README,系统讲解 Toga 框架如何通过App.camera与App.location两大设备接口完成拍照、权限申请、定位获取、持续跟踪与地图可视化。读完本文,你将掌握 Toga 硬件 API 的异步调用模式、平台权限配置(Android / iOS / macOS / Linux)以及一套可直接复制运行的最小实现方案。
示例概览:hardware 测试应用
examples/hardware是 Toga 官方提供的设备能力测试应用,用于验证两个核心硬件 API:
- Camera(摄像头):检查权限、申请权限、拍摄照片并在
ImageView中展示; - Location(定位服务):检查/申请前后台权限、获取当前定位、启动/停止持续位置跟踪,并通过
MapView把定位结果渲染到地图上。
应用通过 OptionContainer 将两个功能区组织为 "Camera" 与 "Geo" 两个标签页,是理解 Toga 硬件 API 最直接、可运行的参考实现。
快速开始
按 examples/hardware/README.md 给出的方式运行:
$ python -m pip install toga $ python -m hardwarehardware包的入口在 examples/hardware/hardware/main.py,它调用hardware.app.main()并进入main_loop();应用本体则在 examples/hardware/hardware/app.py 中定义:
def main(): return HardwareApp("Hardware", "org.beeware.toga.examples.hardware") if __name__ == "__main__": main().main_loop()应用名称为 "Hardware",应用 ID 为org.beeware.toga.examples.hardware,与 pyproject.toml 中tool.briefcase的bundle配置一一对应。若要在真实设备(尤其是移动平台)上打包分发,则需使用 Briefcase([build-system] requires = ["briefcase"])而非仅运行源码。
应用 UI 结构:Camera 页与 Geo 页
HardwareApp.startup()在 examples/hardware/hardware/app.py#L6-L93 中完成全部界面搭建:
- Camera 页:一个宽度 200 的
toga.ImageView用于显示照片,下方放置 "Take Photo" 按钮,其on_press绑定self.take_photo。按钮按flex=1, margin=5布局,外层以direction=COLUMN的Box纵向排列,照片区域两侧用Box(flex=1)实现水平居中。 - Geo 页:一个
flex=1的MapView占满主要空间,下方一行四个按钮:Update(获取当前定位)、Start(开始跟踪)、Stop(停止跟踪)、Background(申请后台定位权限),分别对应update_location、start_location_updates、stop_location_updates、request_background_location四个处理器。 - 主窗口内容为一个
OptionContainer,包含两个OptionItem:"Camera" 与 "Geo"。
界面搭建遵循 Toga 的盒模型布局(travertino),flex、margin、direction等样式参数均可直接用于控制硬件相关页面在桌面与移动端的自适应排布。
Camera API:拍照与权限管理
摄像头通过App.camera属性访问。接口定义位于 core/src/toga/hardware/camera.py,核心成员如下:
| 成员 | 类型 | 说明 |
|---|---|---|
has_permission | 属性 | 是否已获得摄像头使用权限;平台要求显式确认而未确认时返回False |
request_permission() | 异步 | 申请拍照所需权限;已授权则直接返回,不会重复弹窗。await后返回bool |
devices | 属性 | 可用摄像头设备列表,元素为CameraDevice |
take_photo(device=None, flash=FlashMode.AUTO) | 异步 | 拍照。await后返回toga.Image;用户取消时返回None。未授权时抛出PermissionError |
CameraDevice提供id(设备唯一标识)、name(人类可读名称)、has_flash(是否有闪光灯)三个属性,并实现了__eq__(按id判等)与__str__。FlashMode枚举(AUTO/ON/OFF)定义于toga.constants。
示例中的拍照流程
examples/hardware/hardware/app.py#L95-L118 展示了标准的三段式流程:
async def take_photo(self, widget, **kwargs): try: if not self.camera.has_permission: await self.camera.request_permission() image = await self.camera.take_photo() if image is None: self.photo.image = "resources/default.png" else: self.photo.image = image except NotImplementedError: await self.main_window.dialog( toga.InfoDialog("Oh no!", "The Camera API is not implemented on this platform") ) except PermissionError: await self.main_window.dialog( toga.InfoDialog("Oh no!", "You have not granted permission to take photos") )要点:
- 先查权限,再申请权限:
has_permission为False时调用request_permission(); - 拍照结果是
toga.Image,直接赋值给ImageView.image即可刷新显示;若返回None(用户取消),回退到默认占位图resources/default.png; - 双重异常兜底:平台未实现 Camera 后端时抛
NotImplementedError(以InfoDialog提示用户);用户拒绝授权时抛PermissionError。
权限的平台配置要求
Toga 只负责在运行时申请权限,应用本身必须在打包配置中声明对硬件的使用。官方 Camera 参考文档 明确了各平台要求:
- iOS:
Info.plist中必须定义NSCameraUsageDescription; - macOS:需启用
com.apple.security.device.camera授权(entitlement),并在Info.plist中定义NSCameraUsageDescription; - Android:必须声明
android.permission.CAMERA权限。
在 Briefcase 的 pyproject.toml 中,这些声明由permission.camera键统一描述,平台在构建时会自动转换为对应的原生配置(详见下文"权限声明与 Briefcase 配置")。
后端实现对照
Camera 的接口层(interface)通过工厂模式调用各平台实现(self._impl = self.factory.Camera(self))。从源码可以看到:
- Android 后端android/src/toga_android/hardware/camera.py:基于
camera2与MediaStore.ACTION_IMAGE_CAPTURE。get_devices()通过CameraManager.getCameraIdList()枚举设备,并根据LENS_FACING区分 Front/Back/External;take_photo()在应用缓存目录的shared子目录创建临时.jpg,借助FileProvider生成 URI 交给系统相机 Activity,回调中把临时文件包装为toga.Image。 - iOS 后端iOS/src/toga_iOS/hardware/camera.py:基于
UIImagePickerController,通过AVCaptureDevice.requestAccessForMediaType申请权限;take_photo()设置拍摄模式、前后摄像头与闪光灯后以模态方式弹出系统相机界面,结果经TogaImagePickerDelegate回调返回。 - 若平台未提供对应后端,访问
app.camera会抛出NotImplementedError,这正是示例中捕获该异常的原因。
需要特别说明的是:iOS 模拟器虽实现了 Camera API,但无法实际拍照,要测试拍照功能必须使用真机(官方文档明确提示)。
Location API:定位获取与持续跟踪
定位服务通过App.location属性访问,接口定义位于 core/src/toga/hardware/location.py,核心成员如下:
| 成员 | 类型 | 说明 |
|---|---|---|
has_permission | 属性 | 是否已获得前台定位权限 |
request_permission() | 异步 | 申请前台(应用运行期间)定位权限,await返回bool |
has_background_permission | 属性 | 是否已获得后台定位权限 |
request_background_permission() | 异步 | 申请后台定位权限。必须先获得前台权限,否则抛PermissionError |
current_location() | 异步 | 获取当前定位,await返回toga.LatLng(纬度/经度 NamedTuple,见 core/src/toga/types.py#L26-L36) |
on_change | 属性 | 位置更新时回调的处理器;签名见OnLocationChangeHandler协议 |
start_tracking() | 方法 | 开始持续监听位置变化,每次变化触发on_change;未授权抛PermissionError |
stop_tracking() | 方法 | 停止位置监听 |
on_change处理器收到的是关键字参数:location(toga.LatLng)与altitude(海拔,单位米,取不到时为None):
async def location_update(self, location, altitude, **kwargs): print(f"You are now at {location}, with altitude {altitude} meters")示例中的定位流程
示例将定位逻辑拆成四个异步处理器:
- Update(获取当前定位):app.py#L133-L153 先
await self.location.request_permission(),再await self.location.current_location(),得到LatLng后调用_set_location更新地图。未授权时抛出PermissionError,未实现时抛出NotImplementedError,均以InfoDialog提示。 - Start / Stop(持续跟踪):app.py#L155-L193。
start_tracking()前先申请权限;on_change在startup()中通过self.location.on_change = self.location_changed预先绑定。stop_tracking()同样先申请权限再停止。 - Background(后台权限):app.py#L195-L233 演示了标准的前后台权限阶梯:先检查
has_background_permission,未获得时依次await request_permission()与await request_background_permission(),任一步被拒绝都给出明确提示。
location_changed与_set_location(app.py#L120-L131)负责地图联动:
def location_changed(self, geo, location, altitude, **kwargs): self._set_location(location) def _set_location(self, location): self.map_view.location = location if self.pin is None: self.pin = toga.MapPin(location, title="Here!") self.map_view.pins.add(self.pin) self.map_view.zoom = 16 else: self.pin.location = location第一次获得定位时创建MapPin(location, title="Here!")并加入map_view.pins,同时把缩放级别设为 16;此后每次定位更新只需移动已有 pin 的location。MapPin支持location、title、subtitle属性(见 core/src/toga/widgets/mapview.py#L12-L75),MapPinSet提供add/remove/clear管理地图标记。
缩放级别与地图语义
示例中zoom = 16对应"适合导航的街道级别"。MapView.zoom的有效范围为 0–20(越界自动裁剪),其数学基础与 OpenStreetMap 一致:longitude_per_256_pixels = 360 / (2**zoom)。大致对应关系:0–2 全球、9–11 城市范围、12–14 城区/小镇、15–17 导航级道路、18–19 单体建筑、20 单个建筑。MapView默认中心为澳大利亚珀斯(-31.9559, 115.8606)。这些细节可在 core/src/toga/widgets/mapview.py#L189-L236 的 docstring 中核实。
后台定位的特殊语义
request_background_permission()的实现(core/src/toga/hardware/location.py#L96-L133)直接体现了"先前台、后后台"的约束:若has_permission为假,会立即在返回的异步结果中设置PermissionError,提示"Cannot ask for background location permission before confirming foreground location permission."。
需要留意平台差异(见官方 Location 参考文档):
- macOS 与 GTK:没有"后台权限"与"运行中权限"的区分;
- iOS:若用户仅授予"allow once"前台权限,后台权限请求会被拒绝;
- Android 8(API 34 之前):海拔报告的是相对 WGS84 椭球体的高度,而非平均海平面(MSL)高度。
Linux 系统依赖(GeoClue)
在 Linux 上使用 GTK 后端的定位功能,需要安装 GeoClue2 系统包及其 GObject Introspection 绑定,且 GeoClue 服务必须处于启用状态。官方文档给出的常见发行版包名:
- Ubuntu / Debian:
gir1.2-geoclue-2.0 - Fedora:
geoclue2-libs - Arch / Manjaro:
geoclue - OpenSUSE Tumbleweed:
geoclue2 typelib(geoclue2) - FreeBSD:
geoclue
部分发行版(如 Ubuntu)需要用户显式打开系统定位开关后 GeoClue 才能工作;此外,Linux 上非沙箱应用没有可靠的权限控制机制,沙箱应用(如 Flatpak)则通过 XDG Portal Location API 获得粗粒度、可按应用关闭的定位权限。
后端实现对照
- Android 后端android/src/toga_android/hardware/location.py:
has_permission同时检查粗/细定位权限(ACCESS_COARSE_LOCATION/ACCESS_FINE_LOCATION);current_location()通过LocationManager.getCurrentLocation(FUSED_PROVIDER, ...)单次获取;start_tracking()调用requestLocationUpdates注册监听,频率限制为"每 5 秒或每 10 米"触发一次,海拔优先取 API 34 引入的 MSL 高度。 - iOS 后端iOS/src/toga_iOS/hardware/location.py:基于
CLLocationManager,request_permission()调用requestWhenInUseAuthorization(),后台权限则要求Info.plist含NSLocationAlwaysAndWhenInUseUsageDescription并调用requestAlwaysAuthorization();start_tracking()会设置allowsBackgroundLocationUpdates与pausesLocationUpdatesAutomatically。注意该实现与 cocoa(macOS)后端 共用同一套 delegate 回调逻辑。 - 平台相关权限声明映射:iOS 需
NSLocationWhenInUseUsageDescription(后台还需NSLocationAlwaysAndWhenInUseUsageDescription与UIBackgroundModes中的location、processing值);macOS 需启用com.apple.security.personal-information.location授权并定义NSLocationUsageDescription;Android 至少声明ACCESS_FINE_LOCATION或ACCESS_COARSE_LOCATION之一(只声明一个会影响定位精度),后台跟踪另需ACCESS_BACKGROUND_LOCATION。
权限声明与 Briefcase 配置
hardware 示例把全部权限声明集中放在 pyproject.toml 的[tool.briefcase.app.hardware]段:
permission.camera = "This app demonstrates camera capabilities" permission.fine_location = "This app demonstrates fine-grained geolocation services." permission.coarse_location = "This app demonstrates coarse-grained geolocation services." permission.background_location = "This app demonstrates capturing your location while in the background"这四个键分别驱动平台生成对应的原生权限声明(如 Android 的AndroidManifest.xml权限条目、iOS 的Info.plistusage description 字符串),值是面向用户的权限用途说明。同时,Android 构建还配置了额外的 Gradle 依赖与主题:
base_theme = "Theme.MaterialComponents.Light.DarkActionBar" build_gradle_dependencies = [ "com.google.android.material:material:1.12.0", "org.osmdroid:osmdroid-android:6.1.20", ]其中osmdroid是 Android 后端渲染MapView所需的依赖;[tool.briefcase.app.hardware.android]段下的requires指向本地android后端源码。各桌面/移动平台的requires也在此处分别指定(cocoa / gtk / winforms / iOS / android 后端均以本地相对路径引入)。
异步模型与源码级原理
硬件 API 之所以必须await,是因为权限申请、拍照与定位都是耗时且可能被用户中断的操作。其底层是 core/src/toga/handlers.py 中的异步结果机制:
AsyncResult包装一个asyncio.Future,通过set_result()/set_exception()完成或失败,并实现__await__使其可直接await;任何对结果的直接比较(==、bool()等)都会抛出RuntimeError,提示"use await or an on_result handler"——这从语言层面强制了异步用法;PermissionResult与PhotoResult、LocationResult都是AsyncResult的子类;request_permission()在已授权时会同步set_result(True)直接完成,避免不必要的系统弹窗;未授权时把future传给后端实现,由原生回调异步填充结果;wrapped_handler统一包装on_change这类处理器:普通函数同步调用,协程自动asyncio.ensure_future挂载到事件循环,因此location_changed等回调可以安全地直接操作 UI。
定位/摄像头的懒加载同样值得注意:App.camera与App.location是属性(core/src/toga/app.py#L726-L761),首次访问时才实例化Camera(self)/Location(self)并缓存到_camera/_location;如果当前平台工厂没有注册对应实现,访问会抛出NotImplementedError,示例中的except NotImplementedError分支正是为多平台兼容而设计。
测试佐证
仓库为核心硬件 API 提供了完备的单元测试,可用于验证上述行为:
- core/tests/hardware/test_camera.py:覆盖权限申请(已授权不重复请求、未授权才请求)、
devices属性与设备相等性(按id判等)、take_photo在授权/未授权/用户取消三种场景下的行为; - core/tests/hardware/test_location.py:覆盖前台权限申请、后台权限必须先于前台确认(否则抛
PermissionError)、current_location的授权/拒绝分支、start_tracking/stop_tracking生命周期,以及on_change处理器收到location=LatLng(16, 30), altitude=6这类完整参数。
测试基于toga_dummy后端(见 dummy/src/toga_dummy),通过assert_action_performed等断言验证接口层与实现层的调用关系,可作为学习硬件 API 调用时序的补充资料。
小结
通过 hardware 示例,可以完整掌握 Toga 设备能力开发的三条主线:
- 权限先行:
has_permission查询 →request_permission()申请 → 平台打包配置(Briefcasepermission.*键 / 原生Info.plist、entitlement、Android 权限)兜底声明; - 异步调用:所有硬件长耗时操作返回
AsyncResult,必须在异步处理器中await,并对NotImplementedError(平台无后端)与PermissionError(用户拒绝)做显式处理; - 前后台定位阶梯:前台权限是后台权限的前提,
on_change+start_tracking()/stop_tracking()构成持续跟踪闭环,MapView+MapPin负责可视化。
以 examples/hardware/README.md 的快速开始命令为起点,结合 app.py、核心接口 camera.py 与 location.py,即可快速搭建自己的拍照与定位应用,并平滑迁移到 Android、iOS、macOS、Linux、Windows 等多个平台。
- 跨平台
- UI组件
- 桌面应用
【免费下载链接】toga
A Python native, OS native GUI toolkit.
相关推荐
Capacitor原生API详解:设备硬件功能调用实战
Capacitor原生API详解:设备硬件功能调用实战 引言:跨平台开发的硬件访问痛点与解决方案 移动应用开发中,硬件功能调用(如摄像头、传感器、文件系统等)一
移动开发跨平台插件系统前端Quasar + Capacitor API 使用指南:在 Quasar 应用中调用原生设备能力
Quasar + Capacitor API 使用指南:在 Quasar 应用中调用原生设备能力 Quasar Framework 通过 @quasar/app
前端UI组件跨平台如何快速激活Windows和Office:KMS_VL_ALL_AIO智能激活完整指南
如何快速激活Windows和Office:KMS_VL_ALL_AIO智能激活完整指南 还在为Windows系统激活而烦恼吗?KMS_VL_ALL_AIO智能激
跨平台UI组件桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考