65.9K
CodeProject 正在变化。 阅读更多。
Home

为新的 Ultrabook 设备在 Intel AppUp 商店开发和发布应用程序的简易指南

starIconstarIconstarIconstarIconstarIcon

5.00/5 (9投票s)

2012 年 12 月 27 日

CPOL

42分钟阅读

viewsIcon

32529

downloadIcon

557

如何在 Intel AppUP 商店构建和发布 Ultrabook 桌面应用程序

下载 PART-1-Rough-Layout-noexe.zip

下载 PART-1-Rough-Layout-noexe.zip

下载 PART-2-Paint-Without-Color-Change-Working-noexe.zip

下载 PART-3-Paint-With-Color-Change-noexe.zip

下载 Part-4-No-Style-Solution.zip

 全部更新

 下载 KIDSOO_LAPTOP_VERSION_GAL_SUPPORT_NO_SENSORS-noexe.zip

下载 kidsoo_laptop_version_gal_support_no_sensors.zip 

1. Intel AppUp 简介及机遇

Intel AppUp 是一个主要致力于为支持 Intel 架构的设备提供应用程序的应用商店。AppUp 主要提供适用于 PC、上网本、笔记本电脑和超极本的应用程序。随着 Windows 8 的推出,桌面体验被分为 Metro 风格环境和桌面环境。目前 AppUp 为超极本提供的是桌面环境和桌面风格的应用程序。

那么 AppUp 中有什么机遇呢?如果您从未接触过应用程序开发,或者您是 Apple 或 Android 市场的现有应用程序开发者,您可能会问这个问题。我告诉您,AppUp 仍处于早期阶段,可以被视为一家初创公司。尽管如此,AppUp 仍然有很多优点可以作为起点:

a) 对开发者免费。是的,这在目前是一个非常大的激励。

b) 它为您提供了许多好处和资源。其中一项好处是免费的代码签名证书,否则这会花费您大约 200 美元。

c) AppUp 承担了在具有不同规格的各种环境和系统上测试您的应用程序的负担,它们会测试您的 UI 同质性、应用程序崩溃以及功能。所以它们为您做了很多工作。

如果这还不足以激励您,那么请深呼吸,阅读一些关于新型超极本设备的评论,并观看一些介绍超极本的视频。我告诉您,超极本在各个方面都是一项伟大的创新。它为您提供了笔记本电脑的强大功能和平板电脑的灵活性。除了其他传感器,超极本设备最重要的方面是它们具有触摸屏。这改变了您进行计算的方式以及您如何使用它来处理复杂程序。

好消息是,超极本目前还不算便宜。所以这些设备是由富裕的用户购买的。因此,与主要依赖广告支持且免费应用程序下载量大于付费应用程序的 Android 市场不同,超极本为我们提供了更多思考严肃商业应用程序的理由。

超极本运行在 Windows 8 平台上,该平台有两个主要的应用程序商店:Windows Store 和 Intel AppUp 商店。Windows Store 提供 Metro 风格和桌面风格的应用程序。然而,在 Windows 应用商店中发布桌面应用程序是昂贵的。您每年需要向微软支付 100 美元的版税,此外还需要花费几百美元从 Symantec 购买代码签名证书。

根据我的个人经验,我还可以告诉您,Windows Store 的应用程序提交和验证过程一点也不愉快。

在对我们正在讨论的设备、应用程序环境和应用商店进行了一番简要概述后,我们可以轻松地倾向于选择 Intel AppUp。

我建议您从以下网址下载 AppUp 客户端到您的笔记本电脑:

http://appup.com

浏览一些应用程序,下载一些免费应用程序,先感受一下系统。您会惊讶于 AppUp 如何使管理您的桌面应用程序、下载、安装和卸载应用程序变得如此简单。

2. 您可以为超极本桌面风格制作哪种应用程序?

有几篇关于桌面应用程序和 Metro 风格应用程序之间区别的文章。但是,如果您像我一样懒惰,并且不想离开此页面来查看区别,这里有一个简单易懂的解释。

请看下面的图片。它描绘了 Windows 8 的 GUI。观察左下方的矩形。那是 Windows 8 桌面的标签。简单来说,在 Windows 8 中,我们习惯的传统桌面环境本身就是一个很棒的小应用程序。

 

图 1:Windows 8 GUI

在理解或“看到”了 Windows 8 的桌面和 Metro UI 之间的区别之后,现在是时候理解 Metro 风格应用程序和桌面应用程序之间的区别了。

如果您再次仔细查看 Win8 UI,您可以看到几个色彩鲜艳的图块。这些图块实际上是不同的应用程序或应用。如果您花一些时间查看和使用其中一些应用程序,您会意识到这些应用程序属于“阅读器”类别。

Metro 风格 UI 的开发是为了给 Windows 8 手机和平板电脑带来现代的外观和感觉。您用平板电脑做什么?您阅读新闻、观看电影、玩游戏、进行社交网络。这正是 Metro 风格的用途。这些应用程序主要用于娱乐您并提供快速信息。GPS、新闻相关的应用程序在 Metro 环境中都非常出色。但您清楚地可以看到,Metro 风格中没有严肃的“工具”。我们习惯使用的工具,如编辑器、Shell、计算器、IDE,在这些 Metro 应用列表中都缺失了。

现在,如果您单击(或触摸)“开始”菜单顶部的搜索按钮,您会看到许多小应用程序图标,其中肯定包括记事本、画图、写字板、命令窗口、计算器等。选择其中任何一个,您都会被自动带到桌面环境。当您关闭或最小化这些应用程序时,您将停留在桌面环境中。 

图 2:在应用程序列表中搜索

总而言之,我们可以说桌面应用程序是生产者,Metro 应用程序是消费者。博客阅读器、位置查找器、简单的触摸游戏、待办事项列表、个人信息管理、社交网络集成是一些最适合 Metro 环境的功能。

桌面是传统的强大应用程序。您对应用程序的工作方式和类型拥有很大的控制权。为 Windows XP 开发的许多模块和组件也可以在 Windows 8 上运行。所以,如果您已经是产品设计师和开发者(“产品开发者”这个名称现在已经过时,人们更喜欢更性感、更吸引人的“应用程序发布者”这个名称),那么在桌面环境中,您更有可能快速且强大地开发应用程序,在那里您可以重用许多您已经或曾经使用过的组件。

直到今天,我还没有见过一个像 SAP、Photoshop 或 Tally 那样在任何现代移动环境中构建的优秀企业应用程序。这主要是因为移动环境是为了方便移动或快速查看,而严肃的生产力或企业解决方案需要您坐在办公桌前使用键盘快捷键和鼠标工作。

如果您像我一样幸运地已经拥有一台超极本,您一定知道在桌面上结合鼠标、触摸、传感器和键盘使用超极本是多么美妙,以及在沙发上休息时使用它阅读 PDF 文件是多么方便。

如果您还没有超极本,请不要失望。这里有一个好消息。您实际上可以在 Windows 7 环境中构建应用程序,而且它们(技术上说,大部分)都可以在超极本上运行。除了环境光传感器和加速度计等特殊传感器外,所有触摸界面在 Windows 7 开发环境中都可用,您可以使用鼠标开发这些逻辑,然后将其移植到触摸界面。

即使没有超极本或 Windows 8,您仍然可以开发一款出色的超极本应用程序,这一知识是您必须珍惜的。

有了对应用程序细分市场的理解,以及桌面应用程序可以是(而不是“应该是”)强大应用程序的知识,并且由于它们可以轻松地在所有 Windows 平台(从 XP 甚至 98?)上提供,而且有超过十亿的 Windows 用户(尽管我没有确切的数字!),我们将开始我们的“应用程序发布者”之旅。

3. 开始使用 AppUp 开发者账户

一旦您对应用商店、应用程序、环境和可能性有了初步了解,现在就该展示您的能力了,直接前往 AppUp 创建开发者账户。

技术上讲,您将创建一个 Intel 合作伙伴账户,该账户会自动将您的凭证转换为 AppUp 发布者。

如果您还不是应用程序发布者,请注意,应用商店、发布以及熟悉两者在开始时可能会很痛苦。但是,一旦您决定承受这种痛苦,学习曲线和舒适度应该会逐渐缓解。

在我们继续之前,有一件重要的事情您应该做:在您的笔记本电脑(或 PC,无论您使用什么)上下载并安装 Firefox。Intel 网站上的许多交易不支持 Chrome,而一些交易在 Internet Explorer 中会完全失败。目前,Firefox 是最稳定的浏览器,可以避免您在刚开始使用 AppUp 发布时的一些噩梦。

直接前往 http://software.intel.com

注册您的账户。注册后,您会收到一封通知。但是,与其他一些网站不同,Intel 账户的通知可能需要一些时间。两分钟后刷新您的邮件,查看账户激活邮件。

 

登录您的账户后,您看到的第一个屏幕如下。 

图 3:AppUp 初始仪表板

请不要因为看不到任何与 AppUp 相关的内容而感到困惑。实际上,AppUp 的工作方式如下。

您属于一个组织,并代表该组织。组织发布软件,您拥有发布权限。所以,在任何事情之前,您都需要创建一个组织。不用担心,如果您不属于任何组织,或者您想创建一个个人账户,就用您的名字创建一个组织。更新您第一次登录时看到的个人资料信息。

在顶部,您会看到社区、资源和开发者工具菜单。打开资源,在“业务”选项卡下选择“软件合作伙伴”,然后创建组织。创建组织后,右侧选项卡会显示“注册 Intel AppUP”。更新您的信息并返回仪表板。

图 4:带有 AppUp 选项卡的 AppUp 仪表板

 

现在您会看到另一个选项卡:AppUP。恭喜!您已经跨过了注册和找到进入 AppUP 的第一道门槛。创建一个应用程序(即使您还没有应用程序或尚未构建,只需创建一个应用程序)。一旦您的应用程序列出,您将在右侧选项卡中看到代码签名证书选项。

图 5:带有代码签名证书应用程序选项的 AppUp 仪表板

4. 代码签名证书

 

我假设你们大多数人已经知道它是什么了,但对于那些不知道的人,请看下面的图片。

图 6:纯安装程序与代码签名证书签名的安装程序的区别

当您为应用程序创建 exe 或 MSI 安装程序时,在安装过程中,您的系统会检查数字签名,如果找不到签名,就会警告您该程序来自未知发布者。数字签名不仅能让用户产生信任,还能保护发布者的利益。由于您的应用程序将在互联网上,它有可能被修改。有了数字签名,您的 exe 或安装程序就不能被更改。

在稍微了解了代码签名以及数字签名是什么以及为什么它很重要之后,我们将去获取一个证书,目前这是 Intel 的一项福利。

如果您懒得跳过文章的某些部分,或者您已经拥有 Intel 账户并设法跳过了文章的前一部分,我将再次告诉您,代码签名是与您的浏览器协同完成的,目前 Comodo 只支持 Internet Explorer 和 Firefox。如果您希望以一种无障碍的方式获得它,最好从 Firefox 浏览器申请代码签名证书。

代码签名的先决条件

请注意,他们需要某些文件才能为您分配代码。其中之一是您过去几个月的电费账单或固定电话账单,上面应该有您的姓名和您提供的地址。护照、驾驶执照用于身份证明,而不是地址证明。所以您应该准备好这些。

但是还有另一种方法。当您进入激动人心的应用程序世界时,除了编程之外,还有几个方面。应用程序的宣传和支持是其中两项。所以最好以您申请代码签名的名称注册一个域名。例如,如果您想为您的个人账户(例如“Harry”)获得一个代码,那么就用该名称购买一个域名,例如 harry.com 或 harry.net 或其他任何名称,并购买托管。托管一个简单的 WordPress 博客或网站,并将您的信息放在上面。您可能会觉得这笔钱花得不值,但应用程序通常需要发布者的 URL 和支持 URL。所以,如果您认真对待应用程序业务,拥有自己的网站总是明智的。您可以使用您的网站提供技术规范、回答用户的疑问等。

在您名下或您的组织名下拥有域名可以减轻验证的负担,您可以在几天内获得一个。

dejaré de luchar por obtener un certificado de firma de código, y pasaré a lo real: crear una aplicación para Ultrabook.

5. Kidsoo:一款供您的孩子在超极本上玩耍的应用程序

我已经在 AppUp 上发布了三个超极本应用程序,还有六个正在等待验证。但是,我决定从头开始构建一个简单的儿童应用程序。这将使我更全面地理解为超极本开发真正应用程序的细节。同时,它也能让您审视自己的开发周期。

 5.1 先决条件

在我们继续之前,请记住以下几点关于超极本应用程序的内容:

1) 超极本是混合设备,所以它们有不同的规格和屏幕尺寸(当然还有分辨率)。所以请尽量让您的应用程序在屏幕尺寸和分辨率上尽可能独立。

2) 应用程序的主要要求之一是 GUI 和菜单在整个应用程序中必须是同质的。所以您不能在一个表单中使用白色按钮背景,而在另一个表单中使用绿色。

3) 应用程序不得崩溃(尽可能)。

4) 它们必须做到它们所声称的功能。

5) 任何用于超极本环境的应用程序都应特别考虑作为触摸屏应用程序,因此您必须相应地选择控件。手指太粗糙,无法选择普通菜单。

6) 应用程序应使用特殊的用户文件夹,并且不应将数据写入程序文件夹(它们显然不能)。如果用户卸载了应用程序,由应用程序写入的所有数据都必须被删除。

7) 每当您更新应用程序时,无论是为了实现功能发布还是更新以满足验证标准,您都需要指定版本。在某些时候,管理版本会变得很困难。所以,如果您从一开始就采用某种标准的版本控制技术,总是很有帮助的。它可以让您省去很多麻烦。

好了,如果您认真对待应用程序业务,那么您的另一个优先事项应该是让您的应用程序尽可能多地出现在各种应用商店、渠道和环境中。还记得愤怒的小鸟吗?您可以在任何地方找到它,Google Play 商店、Apple App Store、Google Chrome 商店、Intel AppUp 等等。所以,逻辑上讲,您的应用程序应该是可以轻松迁移或移植到其他平台的。

因此,将视觉效果与您的主要逻辑分离至关重要。

触摸逻辑在所有环境中几乎都是相似的。所以,您最好处理触摸逻辑,然后为超极本添加其他功能作为奖励。

考虑到许多其他因素,WPF 对我来说是最适合超极本开发的。主要原因之一是它允许以无样式模式开发,然后应用样式。

请记住,此时应用程序在功能上需要视觉上令人愉悦,并且我已经用一种非常艰难的方式学会了这一点。

让我们开始理解布局。

5.2 应用程序布局

我为 Intel AppUp 开发的第一个应用程序,ImageGrassy,在布局方面存在严重问题,并且在很多方面都很混乱。这部分是因为那是我第一个 WPF 应用程序,其次,我从来没想到过应用程序必须在如此多不同的屏幕规格上运行。我最终不得不重新设计 GUI 和界面来解决问题。我在布局上的挣扎促使我提供一些关于应用程序布局问题的见解。

这是您应用程序中最关键的步骤之一。您应该设计一个即使在较低分辨率或较低屏幕规格下也能保持灵活的布局。许多开发者错误地认为,如果您构建一个 WPF 应用程序,它会自动独立于分辨率。嗯,这句话只说对了一半。您仍然需要找到方法来使应用程序独立于分辨率。

 

让我们来看一些快速修复和技巧,以获得真正独立的视觉效果。

首先看下面的图片。这非常符合大多数软件和应用程序的标准布局。您可以根据您的应用程序使用整个布局或其中一部分。我们将首先构建一个满足我们主要目标的简单模式。为此,我们将使用一些标准规则和技术。

图 7:应用程序使用的标准布局

1. 我们将从一个“L”形开始。它可以是右上、左上、左下或右下、左下,并将所有其他属性设置为自动。

<Window x:Class="Kidsoo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="600" Width="800" 
        WindowState="Maximized">
    <Grid Background="Azure">
    </Grid>
</Window>

2. 我们将开发最低分辨率(800x600)的项目,并尝试查看它是否可以扩展到更高分辨率。这是一种自 GUI 设计早期就存在的技术,并且至今仍然有效。

我们的 WPF 解决方案一开始很简单。如果您将分辨率更改为 600x800,您仍然可以看到您的窗口运行良好。

<Window x:Class="Kidsoo.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="600" 
        Width="800" WindowState="Maximized">
    <Grid Background="Azure">
        <DockPanel VerticalAlignment="Top" HorizontalAlignment="Stretch" 
          Height="100"  Background="Beige"></DockPanel>
        <DockPanel VerticalAlignment="Stretch" HorizontalAlignment="Left" 
           Width="100" Margin="0,100" Background="BlueViolet">
            
        </DockPanel>
        <DockPanel VerticalAlignment="Bottom" HorizontalAlignment="Stretch" 
           Height="100" Background="Brown">
            
        </DockPanel>
        <DockPanel HorizontalAlignment="Right" VerticalAlignment="Stretch" 
          Width="100" Margin="0,100" Background="Aquamarine">
            
        </DockPanel>
        <DockPanel VerticalAlignment="Stretch" HorizontalAlignment="Stretch" 
          Margin="100" Width="Auto" Height="Auto" 
          Background="Chartreuse"></DockPanel>
    </Grid>
</Window> 

我们将设计一个简单的布局,并针对所有可能的分辨率进行验证。

 

这是我们用于初始布局的 WPF MainWindow 代码。

外观如下

图 8:显示仅布局解决方案输出的屏幕

请不要因为颜色组合而浪费精力大笑。布局的每个部分都赋予了不同的颜色以便于区分,我们将在接下来的过程中修改它们的每一部分。

另外请注意,还有许多其他设计您所选布局的方法。例如,定义网格行和列等。这是您的选择,您可以根据您的舒适度和应用程序选择一个合适的初步布局。

一旦我们完成了布局的固定,我们就可以继续进行应用程序本身了。

5.3 关于功能的初步调查

我现在将回到应用程序的概念。我们正在为孩子们构建一个应用程序。目的是让孩子们可以用手指绘画,或者学习字母、数字或水果、动物。Intel AppUp 的内容评级从 3 岁以上开始,但这里的应用程序非常针对蹒跚学步的孩子,考虑到了我 20 个月大的儿子。

我的儿子看到超极本时会做什么?他会像看到我那样用手指在屏幕上划过,或者敲击键盘或鼠标垫旁边的空白处,他有时会把手放在鼠标垫上,有时会试图把它抬起来(由于重量因素,他实际上能抬起一点)。所以主要重点是让他以他与超极本互动的方式获得一些乐趣,并牢记他活动的随机性。

所以 Kidsoo 应该是:

1) 用手指绘画,自动选择随机颜色。

2) 当孩子按下任何键盘键时,显示带有图片的字母。

3) 当孩子随机触摸屏幕的不同区域时(或为了好玩),播放一些音乐。

4) 当孩子尝试移动超极本时,播放一些图片或音乐。

请记住一件事,您正在为用户群体和细分市场构建应用程序。因此,首先将自己置于该用户类别中,并思考他们将如何最好地使用该应用程序。什么样的布局或 UI 对他们最有帮助。如果您对应用程序进行一些初步调查,您可以构建一个具有高度灵活性的应用程序,并且该应用程序有机会赢得用户的喜爱并获得商业成功。

正如我们在这里讨论的,Kidsoo 针对的是蹒跚学步的孩子,您不会用复选框、单选按钮和数据网格来烦扰或让他们感到厌倦。同样,如果您正在构建一个包含图片的应用程序,请不要在 64x64 的缩略图图像查看器中显示最终结果或主图像。 

理解应用程序的概念和目标受众,并在此基础上选择控件的布局,将在很大程度上帮助您构建出色的应用程序。

5.4 继续 Kidsoo 的开发

首先,我们想先让绘画部分工作起来。我将只使用中心和左侧窗格,而不使用其他布局部分。为了绘图,我们需要一个画布或 Image 控件。我将偏好使用 Image 控件,并通过鼠标来实现这部分功能。

<Grid >
    <DockPanel VerticalAlignment="Top" HorizontalAlignment="Stretch" 
            Height="0"  Background="Beige"></DockPanel>
    <DockPanel VerticalAlignment="Stretch" HorizontalAlignment="Left" 
            Width="100" Margin="0,0" Background="AliceBlue">
        
    </DockPanel>
    <DockPanel VerticalAlignment="Bottom" 
       HorizontalAlignment="Stretch" Height="00" Background="Brown">
        
    </DockPanel>
    <DockPanel HorizontalAlignment="Right" VerticalAlignment="Stretch" 
      Width="0" Margin="0,0" Background="Aquamarine">
        
    </DockPanel>
    <DockPanel VerticalAlignment="Stretch" HorizontalAlignment="Stretch" 
                Margin="100,0,0,0" Width="Auto" 
                Height="Auto" Background="WhiteSmoke">
        <Image x:Name="Image1" Height="Auto" Width="Auto"></Image>
    </DockPanel>
</Grid> 

当鼠标按下并移动时,它应该在屏幕上绘制像素。周期性地,颜色应该发生变化。偶尔,屏幕也必须被清除,以便孩子们继续绘画。

这是我们新的布局。

 using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Media;
using System.Windows.Media.Imaging;
namespace Kidsoo
{
    public class Logic
    {
        public static BitmapImage DrawPoint(BitmapImage originalImage, 
                      Point center, Brush b, Pen p, double rad)
        {
            double Scale = 1;
            double newWidth = originalImage.Width * Scale;
            double newHeight = originalImage.Height * Scale;
            RenderTargetBitmap targetImage = new RenderTargetBitmap((int)Math.Ceiling(newWidth),
                                                                    (int)Math.Ceiling(newHeight),
                                                                    96, 96,
                                                                    PixelFormats.Default);
            DrawingVisual dv = new DrawingVisual();
            using (DrawingContext dc = dv.RenderOpen())
            {
 
                ImageBrush db = new ImageBrush(originalImage);
                double Degrees = 0;
 
 
 
                //dc.DrawText(formattedText, p);
                dc.DrawEllipse(b, p, center, rad, rad);
            }
            targetImage.Render(dv);
            BitmapEncoder be = new BmpBitmapEncoder();
            be.Frames.Add(BitmapFrame.Create(targetImage));
            MemoryStream memoryStream = new MemoryStream();
            BitmapImage bImg = new BitmapImage();
            be.Save(memoryStream);
            bImg.BeginInit();
            bImg.StreamSource = new MemoryStream(memoryStream.ToArray());
            bImg.EndInit();
            memoryStream.Close();
 
            return bImg;
        }
    }
}

请注意,我们仍然拥有相同的布局,但 DockPanel 的宽度和高度已根据我们的需要进行了修改。

固定好布局后,我们现在要开始编码了。但是,正如我们在本文开头已经学到的,将逻辑部分与 GUI 分开,我们将向项目中添加另一个 C# 类,名为 Logic,并将功能添加到其中。

我们现在有一个 DrawPoint 方法,它以指定的半径在指定点用指定的笔刷样式绘制一个椭圆。它使用 WPF 的绘图上下文在图像中渲染所需的形状。您还可以使用 DrawingContext 来绘制文本、矩形、线条或其他内容。

有了主要功能到位,我们所要做的就是附加一个事件处理程序,看看它是否正常工作。

public partial class MainWindow : Window
{
    public MainWindow()
    {
        InitializeComponent();
       img= new BitmapImage();
       Image1.Source = img;
    }
    BitmapImage img;
    private void Image1_MouseMove_1(object sender, MouseEventArgs e)
    {
        if (e.LeftButton.Equals(MouseButtonState.Pressed))
        {
            
            Point current = e.GetPosition(Image1);
            img = Logic.DrawPoint(img, current, new SolidColorBrush(Colors.Red), new Pen(Brushes.Red, 1), 1);
            Image1.Source = img;
        }
    }
}

我们将使用 MouseMove 事件。在事件处理程序中,我们将检查左按钮是否被按下,如果被按下,我们将找到相对于图像的位置。已经存在的图像内容将与位置数据一起传递给此函数,我们将使用一个简单的红色笔刷来测试此部分。

public MainWindow()
{
    InitializeComponent();
    img = new BitmapImage(new Uri(System.IO.Path.GetFullPath("main.png")));
    Image1.Source = img;
 
}
BitmapImage img;
private void Image1_MouseMove_1(object sender, MouseEventArgs e)
{
    if (e.LeftButton.Equals(MouseButtonState.Pressed))
    {
        Point current = e.GetPosition(Image1);
        img = Logic.DrawPoint(img, current, 
          new SolidColorBrush(Colors.Red), new Pen(Brushes.Red, 2), 3);
        Image1.Source = img;
    }
}

让我们看看 MainWindow.cs 的样子。

<Image x:Name="Image1" Height="Auto" Width="Auto" 
         MouseMove="Image1_MouseMove_1" Stretch="Fill"></Image> 

您现在可以构建应用程序并查看结果。您看到什么?什么都没有,对吧?这是因为您实际上没有图像可以写入或绘画。所以我们将创建一个空白图像并将其存储在应用程序执行文件夹中。我们将在 InitializeComponent 之后在构造函数中读取它。

好的,由于您使用的是外部图像,不要忘记将图像的拉伸属性设置为 Fill。

现在构建应用程序,看看发生了什么。

if (e.LeftButton.Equals(MouseButtonState.Pressed))
{
 
    Point current = new Point(e.GetPosition(Image1).X * img.Width / 
      (System.Windows.SystemParameters.PrimaryScreenWidth - 100), 
      e.GetPosition(Image1).Y * img.Height / System.Windows.SystemParameters.PrimaryScreenHeight);
    img = Logic.DrawPoint(img, current, new SolidColorBrush(Colors.Red), new Pen(Brushes.Red, 2), 3);
    Image1.Source = img;
}

它将用红色颜料绘制,但距离您的鼠标位置很远,正如您所见。这是因为您没有为 Image1 指定任何宽度和大小,而 main.png 的大小与您的图像不同。由于 Image1 的高度和宽度未明确设置,Image1.Height 和 Image1.Width 将返回 NaN。所以您无法获取绘制上下文的实际规格。

我们将使用 System.Windows.SystemParameters.PrimaryScreenWidthSystem.Windows.SystemParameters.PrimaryScreenHeight 来获取绘制上下文的大小。我们将计算图像和 main.png(或 .bmp 等)之间的差异分数,并将我们的点缩放到该分数。

如果您现在编译应用程序,您会看到点更接近鼠标,但当您继续绘画时,它们会开始变得模糊。这是因为您仍然没有物理高度和宽度,并且您的图像仍然具有无限的高度和宽度。

另一个修复是在 DrawPoint 方法中。请记住我们使用了 dpi=96。您创建的图像可能没有这个 dpi,所以我们用图像的实际 dpi 替换默认 dpi。

如果您创建了一个 256x256 的图像(main.png),您将获得所需的最佳结果。孩子们希望看到大而粗的线条和点。如果您制作一个 3000x3000 的图像,绘制的点将小得多,失去乐趣。

好的。我们的解决方案对于单色和鼠标移动完美运行。

但是:a) 我们希望定期更改颜色

和          b) 偶尔清除绘图窗口,以保持乐趣。

我们将通过两个后台线程来实现这一点。我们使用 workerClear 在 50 秒后清除窗口,并使用 workerNewColor 每 5 秒更改一次颜色。

注意 Dispatcher 的使用。由于您的父线程和后台线程是两个不同的堆,您需要通过 Dispatcher.Invoke 委托来更新任何控件或 DependencyObject(如 currentColor)。

您可以类似地添加其他动画效果。请注意,无限运行的后台线程有时很难被您的垃圾回收器清除。如果您在单击关闭按钮后仍然看不到应用程序关闭,请不要感到惊讶。

 using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using System.Windows.Threading;
namespace Kidsoo
{
    /// <summary>
    /// Interaction logic for MainWindow.xaml
    /// </summary>
    public partial class MainWindow : Window
    {
        #region variables
        BitmapImage img;
        BackgroundWorker workerClear;
        BackgroundWorker workerNewColor;
        Brush currentBrush;
        Random rnd = new Random();
        #endregion
        public MainWindow()
        {
           InitializeComponent();
           img = new BitmapImage(new Uri(System.IO.Path.GetFullPath("main.bmp")));
           Image1.Source = img;
           Image1.Width = System.Windows.SystemParameters.PrimaryScreenWidth - 100;
           Image1.Height = System.Windows.SystemParameters.PrimaryScreenHeight;
           currentBrush = new SolidColorBrush(Color.FromRgb(255,0,0));
           ////////////////////Worker Clear will be used to clear the screen////////////////////////////////////
           workerClear = new BackgroundWorker();
           workerClear.DoWork += workerClear_DoWork;
           workerClear.WorkerReportsProgress = true;
           workerClear.ProgressChanged += workerClear_ProgressChanged;
           workerClear.RunWorkerAsync();
////////////////////////////////// This will be used for changing the Color///////////////////////
          workerNewColor = new BackgroundWorker();
          workerNewColor.DoWork += workerNewColor_DoWork;
          workerNewColor.WorkerReportsProgress = true;
          workerNewColor.ProgressChanged += workerNewColor_ProgressChanged;
          workerNewColor.RunWorkerAsync();
            //// Why do we have ProgressChanged? There is no progress bar here.
            // ProgressChange pumps a message to parent thread when invoked. This is a good way to 
            // let your parent thread know about any background thread to avoid application hang
        }
        void workerNewColor_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            
        }
        void workerNewColor_DoWork(object sender, DoWorkEventArgs e)
        {
            while (true)
            {
                System.Threading.Thread.Sleep(5000);
                byte r = (byte)rnd.Next(0, 255);
                byte g = (byte)rnd.Next(0, 255);
                byte b = (byte)rnd.Next(0, 255);
           
                this.Dispatcher.Invoke(
                     DispatcherPriority.Normal,
                     (System.Windows.Forms.MethodInvoker)delegate()
                     {
                         currentBrush = new SolidColorBrush(Color.FromRgb(r, g, b));
                     }
         );
                workerNewColor.ReportProgress(10);
                System.Threading.Thread.Sleep(1000);
                //throw new NotImplementedException();
            }
            //throw new NotImplementedException();
        }
        void workerClear_ProgressChanged(object sender, ProgressChangedEventArgs e)
        {
            //throw new NotImplementedException();
        }
        void workerClear_DoWork(object sender, DoWorkEventArgs e)
        {
            while (true)
            {
                System.Threading.Thread.Sleep(40000);
                
                this.Dispatcher.Invoke(
                        DispatcherPriority.Normal,
                        (System.Windows.Forms.MethodInvoker)delegate()
                        {
                            img = new BitmapImage(new Uri(System.IO.Path.GetFullPath("main.bmp")));
                            Image1.Source = img;
              
                        }
            );
                workerClear.ReportProgress(2);
                System.Threading.Thread.Sleep(1000);
            }
            //throw new NotImplementedException();
        }
       
        private void Image1_MouseMove_1(object sender, MouseEventArgs e)
        {
            if (e.LeftButton.Equals(MouseButtonState.Pressed))
            {
                Point current = new Point(Math.Floor(e.GetPosition(Image1).X * img.Width / 
                  (System.Windows.SystemParameters.PrimaryScreenWidth - 100)), 
                  Math.Floor(e.GetPosition(Image1).Y * img.Height / 
                  System.Windows.SystemParameters.PrimaryScreenHeight));
                img = Logic.DrawPoint(img, current, currentBrush, new Pen(currentBrush, 2), 3);
                Image1.Source = img;
            }
        }
        private void Window_Closed_1(object sender, EventArgs e)
        {
            this.Dispatcher.InvokeShutdown();
        }
    }
}

为了强制关闭任何正在运行的后台线程并清除它们占用的内存,我们将为窗口添加 Closed 事件处理程序,并为 dispatcher 使用 InvokeShutdown()。

这是 Kidsoo 绘画部分的代码。

几秒钟后屏幕看起来是这样的

private void Image1_TouchMove_1(object sender, TouchEventArgs e)
{
    Point current = new Point(Math.Floor(e.GetPosition(Image1).X * img.Width / 
      (System.Windows.SystemParameters.PrimaryScreenWidth - 100)), 
      Math.Floor(e.GetPosition(Image1).Y * img.Height / 
      System.Windows.SystemParameters.PrimaryScreenHeight));
    img = Logic.DrawPoint(img, current, currentBrush, new Pen(currentBrush, 2), 3);
    Image1.Source = img;
}

图 9:几秒钟绘画后屏幕看起来是这样的

但是,这里所做的一切都是通用功能。那么,我们如何为超极本和触摸屏做呢?很简单。将鼠标移动方法复制到 TouchMove 事件处理程序。所以我们的代码应该类似于这样:

private void Image1_TouchMove_1(object sender, TouchEventArgs e)
{
    Point current = new Point(Math.Floor(e.TouchDevice.GetTouchPoint(Image1).Position.X * img.Width / 
      (System.Windows.SystemParameters.PrimaryScreenWidth - 100)), 
      Math.Floor(e.TouchDevice.GetTouchPoint(Image1).Position.Y * img.Height / 
      System.Windows.SystemParameters.PrimaryScreenHeight));
    img = Logic.DrawPoint(img, current, currentBrush, new Pen(currentBrush, 2), 3);
    Image1.Source = img;
}

但遗憾的是,TouchEvents 与 MouseEvents 略有不同,TouchEventArgs 没有 GetPosition() 方法。

您需要使用 TouchEventArgs 中的 TouchDevice 的 GetTouchPosition()。

这很简单,不是吗?

<Image x:Name="Image1" Height="Auto" Width="Auto" 
  MouseMove="Image1_MouseMove_1" Stretch="Fill" TouchMove="Image1_TouchMove_1" 
  IsManipulationEnabled="True" ManipulationStarting="Image1_ManipulationStarting" 
  ManipulationDelta="Image1_ManipulationDelta" ManipulationCompleted="Image1_ManipulationCompleted" 
  ManipulationInertiaStarting="Image1_ManipulationInertiaStarting"   ></Image> 

但触摸手势和滑动、向下或捏合、缩放手势怎么办?即使 Kidsoo 不需要它们,我也会稍微扩展一下触摸手势部分。触摸手势可以通过“Manipulation”事件组进行分析。首先,您需要启用 Manipulation,然后捕捉 ManipulationDelta 来跟踪 X 和 Y 坐标的操纵量。

以下是 Image1 的 XAML 部分

private void Image1_ManipulationStarting(object sender, ManipulationStartingEventArgs e)
{
    e.ManipulationContainer = Image1; 
    e.Handled = true;
    cumulativeDeltaX = 0;
    cumulativeDeltaY = 0;
    linearVelocity = 0;
}

请记住,Manipulation 事件会捕获总的移动量和速度,而不会让您控制独立的像素。所以您应该将这些事件用于手势,而不是像素特定的操作。

private void Image1_ManipulationDelta(object sender, ManipulationDeltaEventArgs e)
{
    cumulativeDeltaX = cumulativeDeltaX+e.CumulativeManipulation.Translation.X; 
    cumulativeDeltaY = cumulativeDeltaY+e.CumulativeManipulation.Translation.Y; 
    //store value of linear velocity into horizontal direction   
    linearVelocity = linearVelocity+e.Velocities.LinearVelocity.X;
        ////////////////////////////
         Matrix borderMatrix = ((MatrixTransform)Image1.RenderTransform).Matrix;
    //determine if action is zoom or pinch
        int AppConstantsMinimumZoom=-40;
        int AppConstantsMaximumZoom=40;
    var maxScale = Math.Max(e.DeltaManipulation.Scale.X, e.DeltaManipulation.Scale.Y);
    //check if not crossing minimum and maximum zoom limit
    if((maxScale <  1 && borderMatrix.M11 *maxScale >  AppConstantsMinimumZoom) ||
    (maxScale >  1 && borderMatrix.M11 *maxScale <  AppConstantsMaximumZoom))
    {
        Image1.Height = Image1.Height * maxScale;
        Image1.Width = Image1.Width * maxScale;
        
        //scale to most recent change (delta) in X & Y 
        borderMatrix.ScaleAt(e.DeltaManipulation.Scale.X, 
           e.DeltaManipulation.Scale.Y, Image1.ActualWidth/2,Image1.ActualHeight/2);
        //render new matrix
        Image1.RenderTransform = new MatrixTransform(borderMatrix);
}

double cumulativeDeltaX, cumulativeDeltaY, linearVelocity;

下面的代码部分将为您提供标准的捏合和缩放图像功能。但是,我们的 Kidsoo 将没有这段代码,因为我们认为孩子们不会使用多点触控。但是,您可以包含此代码来分析和理解操纵行为,并明智地将其用于您的工作。

Point to be noted here is that even this code is developed in Windows 7 and works perfectly for Ultrabook. So not having an Ultrabook is no more a good excuse to not develop an App for Ultrabook.

private void Window_KeyDown_1(object sender, KeyEventArgs e)
{
    switch (e.Key)
    {
        case Key.A:
            img = new BitmapImage(new Uri(System.IO.Path.GetFullPath("Alphabets/A.png")));
                    Image1.Source = img;
            break;
        case Key.B:
            img = new BitmapImage(new Uri(System.IO.Path.GetFullPath("Alphabets/B.png")));
            Image1.Source = img;
            break;
    }
}

现在是时候给孩子一些更值得称赞的东西了:在他们敲击键盘时添加一些功能。我们在这里添加字母学习。所以,当孩子敲击 A 键时,会出现一个包含带有字母 A 的苹果的大图。要做到这一点,我们将创建一个名为 Alphabet 的资源文件夹,并将我们的媒体资源保存在那里。

现在为您的主窗口添加 KeyDown 事件。并添加显示不同按键不同图像的逻辑。

您可以完成剩余的字母。这里重要的是,虽然我们创建了 Alphabet 目录,但它在您的项目目录内。但是,当您制作安装程序时,您的应用程序将从安装目录运行。所以 Alphabets 目录必须相对于安装目录。简单地说,一旦您完成了 Alphabet 目录中的所有图像,您就可以将其复制到 bin/debug(如果您处于调试模式)或 bin/release(如果您处于发布模式)。

另一个我想在这个应用程序中添加的有趣功能。我想让应用程序真正教会孩子认识图片。怎么做?简单,使用 Microsoft TextToSpeech 引擎。而且,相信我,使用 TextToSpeech 和它一样容易。

speaker = new System.Speech.Synthesis.SpeechSynthesizer();
speaker.Rate = -8; 

所以,首先我们将添加对 System.Speech 的引用。

private void Window_KeyDown_1(object sender, KeyEventArgs e)
{
    switch (e.Key)
    {
        case Key.A:
            img = new BitmapImage(new Uri(System.IO.Path.GetFullPath("Alphabets/A.png")));
                    Image1.Source = img;
                    System.Threading.Thread.Sleep(400);                    
                    speaker.SpeakAsync("A for Apple");
            break;
        case Key.B:
            img = new BitmapImage(new Uri(System.IO.Path.GetFullPath("Alphabets/B.png")));
            Image1.Source = img;
            speaker.SpeakAsync("B for Banana");
            break;
    }
}

现在只需声明一个 SpeechSynthesizer 对象并调整 Synthesizer 的速率。让它慢一些,以便孩子容易理解。

我们的 KeyDown 事件处理程序修改如下。

请注意使用 Async 方法而不是简单的 Speak 方法。如果您使用 Speak,首先合成器将被激活并朗读句子,然后加载图像。那样太没用了,对吧?

我们讨论的最后一个媒体部分是播放带有图片音乐,供蹒跚学步的孩子学习。在这里,我们添加了几个按钮来播放两种不同的歌曲以及相应的图片。

请看下面的屏幕

图 10:添加媒体选项后的屏幕

<DockPanel VerticalAlignment="Stretch" HorizontalAlignment="Left" 
           Width="100" Margin="0,0" Background="AliceBlue">
    <StackPanel>
    <Button x:Name="btnTwinkle" Content="Twinkle" 
            Margin="2,2,2,2" Click="btnTwinkle_Click"></Button>
    <Button x:Name="btnAlphabet" Content="Alphabet Song" 
            Margin="2,2,2,2" Click="btnOther_Click"></Button>
        <MediaElement x:Name="mediaTwinkle" Source="Songs/Music2.mp3" 
            LoadedBehavior="Manual"  />
        <MediaElement x:Name="mediaAlphabet" 
            Source="Songs/AlphabetSong.wav" LoadedBehavior="Manual"  />
    </StackPanel>
</DockPanel>

代码呢?它们非常简单明了。我们添加了两个 MediaElements 来保存各自的音频文件,并在按钮点击时播放 media element。

void StopAllSongs()
{
    mediaTwinkle.Stop();
    mediaAlphabet.Stop();
}
private void btnOther_Click(object sender, RoutedEventArgs e)
{
    img = new BitmapImage(new Uri(System.IO.Path.GetFullPath("Songs/alphabet.jpg")));
    
    Image1.Source = img;
    StopAllSongs();
    mediaAlphabet.Play();
}
private void btnTwinkle_Click(object sender, RoutedEventArgs e)
{
    img = new BitmapImage(new Uri(System.IO.Path.GetFullPath(
                   "Songs/Twinkle_Twinkle_Little_Star.gif")));
    Image1.Source = img;
    StopAllSongs();
    mediaTwinkle.Play();
} 

我们的左侧 DockPanel 现在修改如下:

后面的代码如下。

5.5 应用样式

<Style x:Key="AnimButton" TargetType="Button">
    <Setter Property="OverridesDefaultStyle" Value="True"/>
    <Setter Property="Margin" Value="5"/>
    <Setter Property="Template">
        <Setter.Value>
            <ControlTemplate TargetType="Button">
                <Border Name="border" 
                BorderThickness="0" 
                Background="{TemplateBinding Background}">
                    <ContentPresenter HorizontalAlignment="Left" 
                               VerticalAlignment="Top" />
                </Border>
                <ControlTemplate.Triggers>
                    <Trigger Property="IsMouseOver" Value="True">
                        <Setter Property="Opacity" Value=".6">
                            
                        </Setter>
                        
                    </Trigger>
                </ControlTemplate.Triggers>
            </ControlTemplate>
        </Setter.Value>
    </Setter>
</Style>

如果您仔细观察工作流程,您会笑设计。嗯,WPF 就是这样工作的。您设置布局,添加功能,然后应用样式。由于这个解决方案是为孩子准备的,您期望他们不认识字母,他们只会理解照片。所以我们将应用一些按钮样式,并用图像替换文本内容。

这里有一个小 ButtonStyle。

<Button x:Name="btnTwinkle"   Margin="2,2,2,2" 
            Style="{StaticResource AnimButton}" Click="btnTwinkle_Click"  Width="88" >
    <StackPanel Orientation="Horizontal">
        <Image Source="/Songs/twinkle.jpg" Width="85" ></Image>
    </StackPanel>
</Button>

将其添加到 <Windows.Resources> 部分。您会注意到样式带有 ContentPresenter,它允许我们用 Image 和 Text Content 替换文本内容。

它使用一个简单的触发器来改变不透明度。您可以在触发器中添加一些声音(使用 MediaElement)以增加更多效果。

这是添加一些样式和效果到应用程序后的简单屏幕。

图 11:添加样式后的屏幕

5.6 处理目录

如果您的应用程序包含数据库,或者您正在将文件写入磁盘,或者游戏需要存储和更新商店,那么您不能在应用程序的安装目录中进行。您的应用程序将被安装在 Program Files 或 Program Files(x86) 中,这将不允许您的应用程序进行写入权限。在这种情况下,任何您想写入的内容或任何您想更新的文件都应该放在特殊用户文件夹中。

这是应用程序的另一个棘手部分,因为 Intel 有一项严格的要求,即在应用程序卸载后必须清除应用程序数据。

我们将遵循一个规则:

c:\Users\USER_NAME\AppData\Roaming\YOUR_COMPANY_NAME\APP_NAME

其中大写字母是变量。请记住,安装程序不会在用户特殊文件夹中创建任何目录。所以您需要通过安装程序类来创建文件夹,或者通过您的程序来创建。为了简单起见,我们将只通过您的应用程序来创建应用程序文件夹。

namespace Kidsoo
{
    class WorkingPath
    {
        public static string Path = Environment.GetFolderPath(
          Environment.SpecialFolder.ApplicationData) + "\\Integrated Ideas\\Kidsoo";
    }
}

当应用程序加载时,我们将检查应用程序文件夹是否存在。如果不存在,我们将创建它。然后我们将检查 ApplicationData\COMPANY_NAME\APP_NAME,并做同样的事情。

private void Window_Loaded_1(object sender, RoutedEventArgs e)
{
    try
    {
        if(!Directory.Exists(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData)+"\\Integrated Ideas"))
        {
            Directory.CreateDirectory(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\Integrated Ideas");
          
        }
        if (!Directory.Exists(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\Integrated Ideas\\Kidsoo"))
        {
            Directory.CreateDirectory(Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData) + "\\Integrated Ideas\\Kidsoo");
        }
    }
    catch (Exception ex) 
    {
        MessageBox.Show(ex.Message);
    }
}

为了展示用法,我们将添加一个简单的保存 Image1 中绘制内容的方法,并在清除 Image 之前调用该方法。

public void Save(string filePath,BitmapSource Image)
{
    try
    {
        BitmapEncoder encoder = null;
        var image = Image;
        using (var fileStream = new FileStream(filePath, FileMode.Create, FileAccess.Write))
        {
            if (filePath.EndsWith(".jpg"))
                encoder = new JpegBitmapEncoder();

            if (filePath.EndsWith(".jpeg"))
                encoder = new JpegBitmapEncoder();

            if (filePath.EndsWith(".jpg"))
                encoder = new JpegBitmapEncoder();

            if (filePath.EndsWith(".png"))
                encoder = new PngBitmapEncoder();

            if (filePath.EndsWith(".tif"))
                encoder = new TiffBitmapEncoder();

            if (filePath.EndsWith(".tiff"))
                encoder = new TiffBitmapEncoder();

            if (filePath.EndsWith(".bmp"))
                encoder = new BmpBitmapEncoder();
            encoder.Frames.Add(BitmapFrame.Create(image));
            encoder.Save(fileStream);
        }
    }
    catch (Exception ex)
    {
  //      MessageBox.Show("Can not save! Make sure you have permission " + 
             //   "to access the folder and that image is not opened elsewhere");
    }
}

此方法可以将 BitmapSource 类型的对象保存到任何特定的 Image 类型。我添加了所有编码器,以便您可以在您的应用程序中适当地重用代码。

workerClaer 的 DoWork 方法现在修改如下:

void workerClear_DoWork(object sender, DoWorkEventArgs e)
{
    
    while (true)
    {
        System.Threading.Thread.Sleep(40000);
        if (!SongsMode)
        {
            this.Dispatcher.Invoke(
                    DispatcherPriority.Normal,
                    (System.Windows.Forms.MethodInvoker)delegate()
                    {/////////////
                        try
                        {
                            string fileName = WorkingPath.Path+"\\"+DateTime.Now.Date.ToShortDateString() + 
                              "_" + DateTime.Now.TimeOfDay.TotalMilliseconds + ".jpg";
                            Save(fileName, Image1.Source as BitmapSource);
                        }
                        catch (Exception ) {//MessageBox.Show(ex.ToString()) ;
                        }
                        //////////////
                        img = new BitmapImage(new Uri(System.IO.Path.GetFullPath("main.bmp")));
                        Image1.Source = img;
                    }
        );
        }
        workerClear.ReportProgress(2);
        System.Threading.Thread.Sleep(1000);
    }
    //throw new NotImplementedException();
} 

您可以在下图看到工作文件夹以及捕获的图像。

图 12:保存在目录中的图像

5.7 微调超极本应用程序 

超极本中有许多传感器可以巧妙地用于为您的应用程序添加一点效果。我将在此处展示环境光传感器的绝佳用法。

当孩子们玩弄系统时,他们可能会在屏幕上随机移动手和手指,这可能导致应用程序最小化或关闭,从而影响他们的乐趣。所以我们将移除控制框,并通过完全阻挡光线来允许关闭应用程序。

光传感器位于相机旁边。所以当您用手挡住相机面板时,应用程序应该关闭。

这很有趣,不是吗?

您可以参考 与环境光传感器协同工作 来开始使用 WinRT(Windows Runtime)环境,该环境使您能够捕获传感器事件并与传感器进行交互。

您可以在 MainWindow() 构造函数中添加 LightSensor 事件处理程序,如下所示:

try
{ 
    ls = Windows.Devices.Sensors.LightSensor.GetDefault();
    ls.ReadingChanged += ls_ReadingChanged;
}
catch (Exception ex)
{
}  

在 ReadingChanged 方法中添加这段简单的代码片段。

this.Dispatcher.Invoke(
                           DispatcherPriority.Normal,
                           (System.Windows.Forms.MethodInvoker)delegate()
{
    if (ls.GetCurrentReading().IlluminanceInLux < 2)
   {
       this.Close();
   }
}
);

您需要做的最后一件事是移除窗口上的控制框,并禁止任何大小调整。

WindowStyle="None" ResizeMode="NoResize"  

只需在 XAML 部分的 Window 声明中添加以上两个属性,即可获得所需的窗口状态。

您可以尝试使用其他传感器,发挥创意,想出孩子们会喜欢的特效。

 

请记住,拥有传感器并了解它们的作用和功能是一回事,而将它们无缝地融入您的应用程序以使其更具吸引力是完全不同的游戏。请记住,归根结底,传感器应该为您的应用程序增加乐趣,而不是仅仅因为它们存在而存在!

好的。这里的好消息是,该应用程序确实触动了它的目标客户!我把它安装在我的超极本上,然后把超极本给了我 20 个月大的儿子(很有勇气!)。现在他非常喜欢玩它,花费大量时间敲击键盘和按钮,并在屏幕上戳来戳去。

然而,正如我们常说的,让您的应用程序接受真实用户的测试。观察我的儿子玩 Kidsoo 和超极本,也呈现了一些新的情况:

 

a) 孩子们倾向于敲击超极本的任何空隙。

b) 他们不断地前后拉动屏幕。

c) 他们推超极本并试图移动它。

d) 他们喜欢任何声音。

 

作为一个纯粹为孩子准备的趣味应用程序,我想为什么不更具创新性,为他们做一些愚蠢但有趣的事情呢?这里的目标是点亮孩子的心,让他们做自己。所以我想到了一些更有趣的事情。

a) 建立一个框架,在他们触摸屏幕时播放随机的钢琴曲。

b) 拥有一个包含 360 种颜色的数组,并根据指南针位置更改应用程序的颜色。

c) 加载信息库,例如野生动物或蔬菜,当他们将屏幕拉近或推开时,加载媒体。

d) 当他们敲击超极本时播放有趣的音乐。(别担心!他们的小手无法损坏您的超极本。我的仍然完好。)

 

所以,让我们开始另一轮激动人心的乐趣。我们将从上面段落的 c) 点开始,并尝试实现这个概念。看,超极本优于平板电脑的优势在于,超极本拥有笔记本电脑的强大功能。所以您不用太担心将资源保存在内存中(当然,前提是它们不消耗太多内存)。所以我们将以更可重用的方式组织资源,而不是像我们对按钮所做的那样。我们想要有几张图片和关于这些图片的某些信息。

WPF 提供的一个强大功能是强大的数据访问和绑定机制。您可以将任何类型的媒体与任何类型的控件进行绑定,并进行适当的样式设置。既然我们已经设计好了我们的资源是什么样的,让我们继续进行编码。

 

我们的资源将是一组图片以及与之关联的某些信息。硬编码图片不是解决方案,因为每次我们想更新资源时都需要重新编译。所以我们将通过 XML 文件保留一个资源字典,并使用 List(不是 ListBox)来访问它。

 public class ImageEntity
    {
        #region Property

        public String ImagePath
        {
            get;
            set;
        }
        public String Info
        {
            get;
            set;
        }

        #endregion
    } 

正如您所见,主资源包含图片路径以及与之关联的某些信息。您可以选择与语音合成一起渲染文本。

ImageEntity 是数据定义类,我们将再次保持我们的设计策略不变,并将数据访问写在另一个类中。

让我们有一个名为 ImageView 的类,并将读取 XML 文件、将信息转换为 ImageEntity 对象并返回信息列表的方法放在里面。

 public static List<ImageEntity> GetAllImagesData(string path)
        {
            
            try
            {
                // Load Xml Document
                XDocument XDoc = XDocument.Load(path);
                
                // Query for retriving all Images data from XML
                var Query = from Q in XDoc.Descendants("Image")
                            select new ImageEntity
                            {
                                ImagePath = System.IO.Directory.GetCurrentDirectory() + "\\" + Q.Element("ImagePath").Value,
                                Info= Q.Element("Info").Value
                            };
                // return images data
                
                return Query.ToList<ImageEntity>();
            }
            catch (Exception ex)
            {
                System.Windows.MessageBox.Show("Problem Loading this gallery"+ex);
                return null;
            }
        }   

所以上面的代码应该采用您管理资源的 xml 文件的路径,搜索 ImagePath 和 Info 部分,使用简单的 LinQ 将它们选为 ImageEntity 对象,并将对象添加到 List 中。

此时,我想提醒您注意 AppUp 和应用程序的一个主要问题。

请注意使用 System.IO.Directory.GetCurrentDirectory() 来获取当前目录。此代码假定您直接通过 exe 或用户开始选项或程序目录中的 exe 快捷方式执行应用程序。在这两种情况下,GetCurrentDirectory() 都将返回 exe 的实际安装目录。

然而,AppUp 的一个首选先决条件(虽然不是强制性的)是应用程序也应该从 AppUp 客户端执行。AppUp 驻留在完全不同的目录中,因此我更喜欢您使用 Environment.CurrentDirectory 而不是 GetCurrentDirectory() 方法。

现在让我们看看 XML 文件的结构。正如您所见,查询会查找 Image 元素,并从顶部的后代块中提取 ImagePath Info 元素。因此,应该有一个根元素来包含 Image 元素。让我们称根元素为 Images。 这里的代码假定您的图像与 xml 文件位于同一目录中。您可以根据需要进行修改,将 Animal 资源保存在 Animal 目录中,依此类推。

这是 xml 文件的示例结构。

 <?xml version="1.0" encoding="utf-8" standalone="yes"?>
<Images>
<Image>
<ImagePath>rose.png</ImagePath>
<Info>
Rose Flower
</Info>
</Image>
<Image>
<ImagePath>monkey.png</ImagePath>
<Info>
Monkey Animal 
</Info>
</Image>
</Images>   

所以您可以创建许多不同的 xml 文件,包含不同的资源,例如包含家畜的一个,包含食用物的另一个,等等。

这种方法的一个很棒的地方是,我们不是将所有资源加载到内存中。相反,我们只保留资源的路径。我们可以根据逻辑从路径元素加载图像。

现在,让我们在构造函数中,在 InitializeComponent() 调用之后,将一个类型为 List<ImageEntity> 的 MainWindow 类成员,称为 childImages,并加载 xml 中的资源。

childImages=ImageView.<ImageEntity>GetAllImagesData("myRes.xml");  

有了图像管理,让我们回到传感器部分,看看如何让它工作。

根据我们的讨论,我们希望在孩子点击或敲击超极本时加载随机资源。在这种情况下,会触发 Accelerometer Shaken 事件。为了构建一个几乎所有超极本相关应用程序都可以使用的传感器框架,我们在类上下文中声明传感器对象,并从名为 InitializeSensors() 的方法中初始化所有传感器。它会自动检查传感器的存在并注册适当的事件处理程序。您可以下载仅包含环境光传感器并已初始化其他传感器的超极本 Kidsoo。

 

现在只需要为应用程序编写每个传感器事件处理程序中的适当代码,以根据传感器事件执行有趣的操作。供读者参考,这是带有环境光传感器功能的传感器部分。

  #region declare all sensors
        System.Collections.Generic.IList<string> sensors = new List<string>();
        Windows.Devices.Sensors.Compass cmp;
        Windows.Devices.Sensors.Gyrometer gyro;
        Windows.Devices.Sensors.Inclinometer inc;
        Windows.Devices.Sensors.LightSensor ls;
        Windows.Devices.Sensors.SimpleOrientationSensor sos; 
        #region Initialize sensors
                 
        void InitializeSensors()
        {
            
            
             
        Windows.Devices.Sensors.Accelerometer acc;
        #endregion
            try
            {
                acc = Windows.Devices.Sensors.Accelerometer.GetDefault();
                //acc.GetCurrentReading();
                acc.Shaken += acc_Shaken;
<pre>                sensors.Add("ACCELEROMETER");
            }
            catch (Exception ex)
            {
            }
            try
            {
                cmp = Windows.Devices.Sensors.Compass.GetDefault();
                cmp.GetCurrentReading();
                sensors.Add("COMPASS");
                cmp.ReadingChanged += cmp_ReadingChanged;
            }
            catch (Exception ex)
            {
            }
            try
            {
                gyro = Windows.Devices.Sensors.Gyrometer.GetDefault();
                gyro.GetCurrentReading();
                gyro.ReportInterval = 100;
                gyro.ReadingChanged += gyro_ReadingChanged;
                sensors.Add("GYROMETER");
            }
            catch (Exception ex)
            {
            }
            try
            {
                inc = Windows.Devices.Sensors.Inclinometer.GetDefault();
                //       inc.GetCurrentReading();
                inc.ReadingChanged += inc_ReadingChanged;
                sensors.Add("INCLINOMETER");
            }
            catch (Exception ex)
            {
            }
            try
            {
                ls = Windows.Devices.Sensors.LightSensor.GetDefault();
                ls.ReadingChanged += ls_ReadingChanged;
                sensors.Add("LIGHT SENSOR");
            }
            catch (Exception ex)
            {
            }
            try
            {
                Windows.Devices.Sensors.OrientationSensor os = Windows.Devices.Sensors.OrientationSensor.GetDefault();
                sensors.Add("ORIENTATION SENSOR");
            }
            catch (Exception ex)
            {
            }
            try
            {
                sos = Windows.Devices.Sensors.SimpleOrientationSensor.GetDefault();
                sensors.Add("SORIENTATION SENSOR");
                sos.OrientationChanged += sos_OrientationChanged;
            }
            catch (Exception ex)
            {
            }
            
        }
        #endregion
 #region Event HandlingMethods
        void sos_OrientationChanged(Windows.Devices.Sensors.SimpleOrientationSensor sender, Windows.Devices.Sensors.SimpleOrientationSensorOrientationChangedEventArgs args)
        {
            
        }
        void cmp_ReadingChanged(Windows.Devices.Sensors.Compass sender, Windows.Devices.Sensors.CompassReadingChangedEventArgs args)
        {
            
        }
        
        void acc_Shaken(Windows.Devices.Sensors.Accelerometer sender, Windows.Devices.Sensors.AccelerometerShakenEventArgs args)
        {
            
            
        }
        void inc_ReadingChanged(Windows.Devices.Sensors.Inclinometer sender, Windows.Devices.Sensors.InclinometerReadingChangedEventArgs args)
        {
            this.Dispatcher.Invoke(
                           DispatcherPriority.Normal,
                           (System.Windows.Forms.MethodInvoker)delegate()
                           {
                               double pitch = args.Reading.PitchDegrees;
                               double roll = args.Reading.RollDegrees;
                               if (pitch > 60)
                               {
                                   if (Math.Abs(roll) > 30)
                                   {
                                       if (roll < 0)
                                       {
                                           try
                                           { 
                                           }
                                           catch (Exception ex) { }
                                       }
                                   }
                               }
                           });
            //throw new NotImplementedException();
        }
        void ls_ReadingChanged(Windows.Devices.Sensors.LightSensor sender, Windows.Devices.Sensors.LightSensorReadingChangedEventArgs args)
        {
            this.Dispatcher.Invoke(
                           DispatcherPriority.Normal,
                           (System.Windows.Forms.MethodInvoker)delegate()
                           {
                               if (ls.GetCurrentReading().IlluminanceInLux < 2)
                               {
                                   this.Close(); ;
                               }
                           }
            );
            //throw new NotImplementedException();
        }
        void gyro_ReadingChanged(Windows.Devices.Sensors.Gyrometer sender, Windows.Devices.Sensors.GyrometerReadingChangedEventArgs args)
        {
            this.Dispatcher.Invoke(
                           DispatcherPriority.Normal,
                           (System.Windows.Forms.MethodInvoker)delegate()
                           {
                               try
                               {
                                   //labStatus.Content = string.Format("X=>{0},Y=>{1},Z=>{2}",args.Reading.AngularVelocityX,args.Reading.AngularVelocityY,args.Reading.AngularVelocityZ);
                                   if ((Math.Abs(args.Reading.AngularVelocityX) > 15) && (Math.Abs(args.Reading.AngularVelocityY) > 5) && (Math.Abs(args.Reading.AngularVelocityZ) > 5))
                                   {
                                   }
                               } 
                               catch (Exception ex) { } 
                           }
                                                   );
            //throw new NotImplementedException();
        } 
        #endregion  
    

这里您可以看到的一个重要之处是使用    

  this.Dispatcher.Invoke(
                           DispatcherPriority.Normal,
                           (System.Windows.Forms.MethodInvoker)delegate()
                           {
});  

在所有传感器事件处理程序中。我想在这里快速说明一下这项技术。请注意,传感器在 UI 线程之外的单独线程中运行。所以,如果您想从传感器事件更新任何 UI 元素,您应该获取 UI 线程的 dispatcher 并更新元素。当然,还有一种替代方法是使用 DependencyObjects 并通过 INotifyPropertyChange 接口方法将对象绑定到当前的 MainWindow 类。但是,这取决于您的编码偏好和风格,您应该采用哪种技术。专家会建议使用 DependencyProperty,但是,如果您是初学者(如文章所假定的),那么使用事件处理技术是一种简单且首选的方式。一旦您对传感器行为有了足够的了解,您就可以使用 DependencyProperty。

但对我来说,为每个传感器编写一个单独的类,声明其属性并注册属性更改是一项乏味的任务。

     

现在是时候编写一个名为 DisplayResource() 的小方法了。该方法应该从 childImages 列表中随机或按顺序显示一个资源。

List<ImageEntity> childImages = new List<ImageEntity>();
        Random rndImg = new Random();
        void DisplayResource()
        {
            int n = rndImg.Next(childImages.Count);
            ImageEntity selectedIE = childImages[n];
            img = new BitmapImage(new Uri(selectedIE.ImagePath));
            Image1.Source = img;
            speaker.SpeakAsync(selectedIE.Info);
        } 

您想从哪个事件处理程序调用此方法以及在哪些情况下调用,完全取决于您。让您的想象力飞翔,为这个方法或传感器事件处理程序增添趣味。

当我的孩子玩 Kidsoo 时,我观察到的一个有趣之处是,他喜欢声音,并且喜欢“小星星”的音乐。这对于所有孩子来说都是普遍情况。为什么不修改 ImageEntity 类以也包含一个 wav 文件,并在渲染图像时播放它呢?例如,当您加载狗的图像时,它可能还会播放包含狗叫声的 wave 文件。

请告诉我您采取了哪些措施来娱乐孩子们!

 

6. 创建应用程序安装程序 

好了,您已经完成了艰苦的工作,构建了应用程序,添加了效果,测试并通过了,现在应用程序已准备好安装。现在我们需要关注安装程序的问题。在我们继续之前,Intel 希望您的安装程序执行以下几项快速操作:

a) 程序菜单或桌面,或者最好两者都有快捷方式。

b) 安装程序必须使用您的代码签名证书进行签名。

c) 卸载时,您的应用程序数据必须被清除。

我想在这里添加一些有趣的事情。还记得我们讨论过 Windows 8 如何管理您的应用程序吗?您实际上需要搜索应用程序才能找到您的应用程序。搜索是从程序菜单而不是桌面填充的。所以,如果您将应用程序的快捷方式放在程序菜单中,它将是可搜索的。

如果您创建桌面快捷方式,应用程序的图标会出现在主 Windows 8 磁贴中。所以,我在这里会选择两者。

您已使用 Visual Studio 2012 开发您的应用程序。但是,此版本的 VS 没有内置任何标准安装程序。所以我们将借助 visual studio 2010。

6.1 创建用于删除应用程序数据的自定义操作 

自定义操作基本上是安装程序类或外部可执行文件,可以在安装或卸载过程中由安装程序执行。您指定。

我们假设我们的应用程序文件夹,即 \Integrated Ideas\Kidsoo,可能包含几个文件,在您的一些应用程序中可能包含几个子文件夹,每个子文件夹可能包含几个文件。所以我们需要编写一个程序,该程序可以递归地擦除这些子目录和文件以及主应用程序目录,并在卸载选项中调用该程序的 exe。

您可以在 VS 2010 或 VS 2012 中创建命令行程序来实现此目的。

//Here is the program for our Kidsoo App.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
namespace DeleteAppPath
{
    class Program
    {
        static void Main(string[] args)
        {
            string settingsDirectory = Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData);
            string s = settingsDirectory + "\\Integrated Ideas\\Kidsoo";
            if (Directory.Exists(s))
            {
                clearFolder(s);   
            }
            Directory.Delete(s);
        }
        static private void clearFolder(string FolderName)
        {
            DirectoryInfo dir = new DirectoryInfo(FolderName);
            foreach (FileInfo fi in dir.GetFiles())
            {
                fi.Delete();
            }
            foreach (DirectoryInfo di in dir.GetDirectories())
            {
                clearFolder(di.FullName);
                di.Delete();
            }
        }
    }
}

运行此程序后,您的应用程序文件夹及其所有子文件夹和文件将被擦除。

6.2 准备安装 

在 Visual Studio 2010 中启动一个设置项目,例如 KidsooSetup。

将文件添加到您的应用程序文件夹。您 Debug 或 Release 目录中的所有文件都应该在这里。

您还有一些额外的文件夹在您的应用程序中,如 Songs 和 Alphabet。右键单击应用程序文件夹并选择添加文件夹。一旦创建了具有适当名称的文件夹,就将文件从 Apps 的 debug/Alphabet 文件夹复制到 Installer 的 Application Folder/Alphabet 文件夹,依此类推。

现在您的应用程序文件夹看起来如下:

图 13:添加其他文件夹后的设置应用程序文件夹

现在是创建桌面和程序文件快捷方式的时候了。

右键单击应用程序的可执行文件并选择“创建快捷方式”选项。创建快捷方式后,将其复制到用户的桌面。创建另一个快捷方式并将其复制到用户的程序菜单。

您还可以创建自定义应用程序图标,并将这些图标用于快捷方式。您的图标必须在应用程序目录中。创建快捷方式后,转到用户桌面,单击快捷方式文件,在属性编辑器中您将看到一个 Icon 属性。只需浏览并选择特定的图标。

6.3 为卸载过程添加自定义操作

您已经拥有名为 DelAppPath 的应用程序自定义安装程序 Exe。首先将其加载到应用程序文件夹中,方法是添加文件。单击图像下方的 Custom Action。

 

图 14:VS 2010 设置项目中的自定义操作

您会看到一个 Uninstall 选项。右键单击 Uninstall 并选择 Add Custom Action。浏览并为自定义操作选择 DeleteAppPath.exe。

现在选择 exe,并将“Installer Class”更改为 false,如下所示。

 

图 15:为卸载设置自定义操作

这就是创建安装程序所需的一切。现在构建此设置项目。您将在设置项目的 debug 文件夹中看到 KidsooSetup.msi。

但是,我强烈建议更新设置设置,并添加产品名称、制造商 URL、支持 URL 和产品版本信息。

7. 使用代码签名证书对安装程序进行签名

收到 Comodo 发送的证书后,在 Firefox 中打开您的电子邮件,然后单击链接。但是您找不到任何物理证书。请记住,我们讨论过证书与浏览器绑定。所以您现在需要打开 Tools->Options,在高级选项卡中选择 View Certificates。

您会看到证书如下。

 

图 16:Comodo 中的代码签名证书

您需要备份此证书。请记住,在备份时,它会要求您输入一个密码,我们称之为 YOUR_PASSWORD。无论如何,请不要忘记此密码。否则您的证书将毫无用处。备份后,文件看起来如下。

图 17:备份后的证书。

太好了。现在是时候对安装程序进行签名了。Visual Studio 配备了数字签名工具,需要通过 Visual Studio 命令提示符进行操作。我编写了一个小批处理脚本来简化此过程。

创建一个名为 'Sign.bat' 的批处理文件,并添加以下行

signtool sign /f Comodo-IICS.p12 /p YOUR_PASSWORD KidsooSetup.msi.

将证书和 bat 文件复制到您的设置项目的 debug 目录中。

打开 Visual Studio 命令提示符,将目录更改为您的设置项目的 debug 目录,然后输入“sign”命令(不带引号)。就是这样!

现在直接转到 AppUp 并提交您的应用程序以供验证。好的,如果您在应用程序的名称或描述中使用“Ultrabook”,请不要忘记添加 (TM)。

8. 结论

本文旨在指导初学者入门 AppUp(尤其是超极本应用程序发布)。本文使用了一个简单的儿童应用程序来演示在超极本环境中进行应用程序开发和发布时涉及的重要因素和概念。

我本来可以写我的提交 ImageGrassy 的经历,但我认为从头开始做一个应用程序可以让我更灵活地写教程。我还上传了每个阶段的 zip 文件,供您区分不同阶段并在每个阶段以自己的方式工作。此外,本文中呈现了许多未嵌入主代码的片段,供您自行找出并使用。

我已尽力涵盖了我在初期阶段遇到的许多方面,并试图详细解释。但是,如果我遗漏了什么,您可以随时建议需要补充的章节和修改。

Kidsoo 对我来说是一个大项目,计划于 3 月在 AppUp 发布,包含大量功能以及令人兴奋和有趣的内容。但是,即使在目前的形式下,孩子们也会非常喜欢这个应用程序。希望我能正确地为您展示超极本应用程序开发和发布的道路。

 

9. 免责声明 

示例应用程序中使用的所有媒体文件可能受版权保护。我没有版权来将这些媒体文件用于商业用途。我请求您在使用这些文件之前弄清楚文件的版权。

© . All rights reserved.